Git:协作历史、发布选源与离线交付
发布系统接收 main、v1.0.0 或一个提交号时,选中的对象并不相同。分支和标签是名称,提交保存一份文件树及其父提交关系;构建工具再把文件树、依赖和构建参数转换成 JAR、前端目录或镜像。部署记录需要连接这些对象,才能回答线上运行的是哪一份程序。
Git 的安装、暂存区、日常分支操作和凭据配置见 Git 工具主文。发布与故障接管还要进一步确认引用来源、工作副本状态和历史修改的影响。
分清名称、提交和构建输入
远端 main 与本地 origin/main
服务端仓库
└─ refs/heads/main ──────→ 提交 C
│ fetch 获取对象并更新本地记录
本地仓库 ↓
├─ refs/remotes/origin/main → 提交 C(最近获取的记录)
├─ refs/heads/main ─────────→ 提交 B(可以尚未整合 C)
├─ HEAD → refs/heads/main
└─ refs/tags/v1.0.0 → 标签对象 → 提交 Borigin/main 是本地保存的远端跟踪引用。其他人刚推送了新提交,本地没有 fetch 时,它仍可能指向旧位置。fetch 获取对象并更新配置指定的引用,不会自动把当前工作区切换到新提交;整合要由 merge、rebase 等操作完成。refspec 与抓取行为见 git-fetch。
分支名可以继续向前移动。轻量标签直接引用对象,附注标签另有标签对象,记录说明、标记者及可选签名。查发布所对应的提交时使用 ^{commit} 解引用,并检查类型;不要假定 rev-parse v1.0.0 的结果必然就是提交号。语法见 git-rev-parse。
同一个提交为什么仍可能构建出不同程序
未跟踪文件、Git 忽略文件和本地生成物可能参与构建。构建脚本如果读取工作目录中的旧配置,即使 HEAD 相同,输入也已经不同。发布宜在独立干净工作副本中完成,并按项目规则准备依赖锁文件、工具链和构建参数。
子模块记录的是另一仓库的提交,父仓库不会把子模块源码全部嵌进去;Git LFS 文件在 Git 对象里通常只是指针,真实大文件还要从 LFS 存储获取。浅克隆可能缺少版本计算或变更分析所需历史,partial clone 也可能在构建时才请求缺失对象。这些情况应在构建前检查,不能等到离线环境再发现输入不全。配置和操作分别见 git-submodule、Git LFS和 git-clone。
源码提交、构建工具版本、依赖解析结果、制品 SHA-256 和运行配置版本应保存在发布清单中。Git 提交说明代码来自哪里,制品摘要用于确认实际交付的字节;重新构建旧提交与重新部署旧制品是两种恢复操作。
在本机复现协作整合与公共历史修复
建立一个没有外部服务的实验仓库
使用 Linux、Bash、Git 2.43 或更新版本,普通用户执行,不使用 sudo。下面创建新的临时目录,不接触已有项目,也不需要 GitHub、Gitee 账号。包管理器安装 Git 的平台差异见 官方安装入口。
LAB=$(mktemp -d)
cd "$LAB"
git --version
git init --bare --initial-branch=main remote.git
git clone "$LAB/remote.git" alice
git -C alice config user.name 'Alice Lab'
git -C alice config user.email 'alice@example.invalid'
git -C alice config commit.gpgsign false
git -C alice config tag.gpgsign false
printf 'service=orders\n' > alice/app.properties
git -C alice add app.properties
git -C alice commit -m '初始化服务配置'
git -C alice push -u origin main
git clone "$LAB/remote.git" bob
git -C bob config user.name 'Bob Lab'
git -C bob config user.email 'bob@example.invalid'
git -C bob config commit.gpgsign false
git -C bob config tag.gpgsign false首次 clone 的空仓库提示正常。remote.git 是无工作区的 bare 仓库;alice、bob 模拟两名协作者。姓名和邮箱只进入实验提交元数据,不代表服务端认证身份。没有签名密钥的实验显式关闭提交签名,不改变全局 Git 配置。
远端前进后,push 为什么被拒绝
printf 'timeout=3\n' > alice/timeout.properties
git -C alice add timeout.properties
git -C alice commit -m '添加超时配置'
git -C alice push origin main
printf 'retries=1\n' > bob/retry.properties
git -C bob add retry.properties
git -C bob commit -m '添加重试配置'
git -C bob push origin main最后一次 push 预期非零,通常显示 fetch first 或 non-fast-forward。Alice 与 Bob 从同一个父提交各自修改,服务端无法只向前移动 main 就同时保留两边历史。此时先获取差异,不使用 force 覆盖远端。
git -C bob fetch origin
git -C bob log --oneline --graph --all
git -C bob rev-list --left-right --count HEAD...origin/main
git -C bob merge --no-edit origin/main
git -C bob push origin main整合前左右计数应为 1 1:本地与远端各有一个对方没有的提交。本例修改不同文件,merge 自动得到包含两份配置的合并提交。push 后,两条开发历史都在远端可达。快速前进检查与强制更新的影响见 git-push,合并语义见 git-merge。
若修改同一位置产生冲突,先用 git status 和 git diff --name-only --diff-filter=U 找到未合并路径。编辑出满足业务需求的结果,运行测试,git add 后继续提交;暂时无法决定时使用 git merge --abort 返回合并前状态。冲突标记消失只表示文本处理完成,仍需检查两边功能是否保留。
rebase 会把本地提交重新应用到新的起点,提交身份因此改变,适合按团队约定整理尚未共享的分支。merge 保留原有分叉及合并关系。共享发布分支上优先使用团队允许的整合方式;--force-with-lease 只是限制远端引用必须仍符合预期,不是评审或数据保护流程的替代品。
修正已经共享的错误提交
在 Bob 工作副本加入一条故意错误配置并推送,再用新提交撤销它:
printf 'unsafe=true\n' > bob/unsafe.properties
git -C bob add unsafe.properties
git -C bob commit -m '加入实验错误配置'
BAD=$(git -C bob rev-parse HEAD)
git -C bob push origin main
git -C bob revert --no-edit "$BAD"
git -C bob push origin main
git -C bob log -2 --format='%h %s'
test ! -e bob/unsafe.properties预期生成一个新的 Revert 提交,错误提交仍在历史中,工作树已经移除对应文件。这样其他协作者可以继续正常拉取,不需要把公共分支强制移动到旧位置。复杂修改的 revert 也可能冲突;撤销合并提交还必须指定 mainline 父提交,并理解它对后续合并的影响,见 git-revert。
restore 主要改变工作区或暂存区;reset 可以改变当前引用、暂存区或工作区;revert 通过新提交表达反向修改。已经部署出去的程序不会因为其中任一命令自动变化,仍需执行制品发布或运行回退。
将批准的源码固定为发布输入
标签与精确提交一起核对
继续使用实验仓库,为当前已修复状态建立附注标签:
git -C bob tag -a v1.0.0 -m '实验发布 v1.0.0'
git -C bob push origin refs/tags/v1.0.0
git -C bob rev-parse refs/tags/v1.0.0
git -C bob rev-parse 'refs/tags/v1.0.0^{commit}'
git -C bob ls-remote origin 'refs/tags/v1.0.0*'附注标签对象和解引用后的提交会得到两个不同 ID;ls-remote 输出中的 ^{} 行对应解引用对象。标签也可以被删除或重建,发布平台应保护发布标签,记录批准的精确 commit,并拒绝同名标签偷偷指向其他提交。标签创建与签名见 git-tag。
在新的构建副本中只取指定引用,再切到批准的提交:
EXPECTED_COMMIT=$(git -C bob rev-parse 'refs/tags/v1.0.0^{commit}')
git clone --no-checkout "$LAB/remote.git" build
git -C build fetch origin refs/tags/v1.0.0:refs/tags/v1.0.0
ACTUAL_COMMIT=$(git -C build rev-parse 'refs/tags/v1.0.0^{commit}')
test "$ACTUAL_COMMIT" = "$EXPECTED_COMMIT"
git -C build switch --detach "$EXPECTED_COMMIT"
test -z "$(git -C build status --porcelain=v1 --untracked-files=all)"
git -C build show HEAD:app.properties实验中 EXPECTED_COMMIT 来自自己刚创建的标签;正式流水线应从已批准的发布记录取得期望值,不能把同一次远端查询同时当作期望与实测。detached HEAD 在这种只读构建副本中是正常状态,构建不需要推动 main。
干净状态检查没有覆盖被忽略文件、外部缓存和依赖仓库,因此更可靠的做法是每轮创建新工作副本,并让构建输入显式化。不要在运行应用的目录中执行 pull 后顺手编译。构建与环境配置如何分离见 构建与制品,制品安装与进程恢复见 发布脚本。
签名、凭据和仓库策略
需要验证发布签名时,使用 git verify-tag 或 git verify-commit,并事先配置可信密钥与签名者授权。数学签名有效只说明内容由相应私钥签发,团队仍需判断该密钥是否允许发布这个项目。未经校验的任意签名者不能直接进入发布。签名检查入口见 git-verify-tag。
个人开发身份、CI 读取源码身份和发布打标签身份按职责授权。SSH 同时校验服务器主机密钥与客户端凭据,HTTPS 则需要服务端证书与凭据助手;不要为了让自动化通过而关闭 TLS 或主机密钥检查。配置方法见 SSH。令牌不放在 remote URL、命令日志或构建镜像中。
分支保护、评审和服务端检查由 Git 托管平台实施,客户端配置与本地 hook 可被修改。部署进程一般只需拉取制品,没有理由长期持有可写源码仓库的个人凭据。
离线传递仓库,再验证接收结果
Bundle 保留哪些内容
bundle 打包选定引用可达的 Git 对象和必要头部信息,可用于没有网络的 clone 或 fetch。它不包含工作区未提交修改、所有本地配置、托管平台权限、Issues、CI Secret 或 LFS 二进制;子模块仍需自己的仓库材料。完整规则见 git-bundle。
在实验目录创建包含分支和标签的完整 bundle:
git -C bob bundle create "$LAB/source.bundle" --all
git -C bob bundle verify "$LAB/source.bundle"
git bundle list-heads "$LAB/source.bundle"
sha256sum "$LAB/source.bundle"
git clone "$LAB/source.bundle" "$LAB/offline"
RECEIVED_COMMIT=$(git -C offline rev-parse 'refs/tags/v1.0.0^{commit}')
test "$RECEIVED_COMMIT" = "$EXPECTED_COMMIT"
git -C offline fsck --full预期 bundle 验证通过,接收端标签对应相同提交,fsck 没有对象损坏错误。摘要用于传输后的字节比较,应从可信交付清单取得期望摘要;在接收端临时计算一个值再和自己比较没有意义。对象完整性检查见 git-fsck。
增量 bundle 可以只带某段历史,但接收仓库必须已经持有它声明的先决提交。交付方要明确基线,接收方在自己的仓库执行 bundle verify;缺基线时重新传全量材料或正确前序增量,而不是忽略验证错误。部分克隆或浅仓库导出前同样要检查对象是否足够。
只需要源码快照时使用 archive
git -C bob archive --format=tar --prefix=source/ \
--output="$LAB/source.tar" "$EXPECTED_COMMIT"
tar -tf "$LAB/source.tar" | headarchive 输出指定树的文件快照,不带可继续 fetch 的 .git 历史。导出还受 export-ignore、export-subst 等属性影响,子模块源码和 LFS 对象需另外安排。它适合明确只消费源码快照的流程,不能替代仓库备份;细节见 git-archive。
实验材料保留在 LAB 指向的临时目录。检查 printf '%s\n' "$LAB" 后可将整份实验目录归档;无需删除任何真实仓库或执行全盘 clean。
按失败对象选择恢复动作
| 现象 | 首先检查 | 修复与验证 |
|---|---|---|
| push 被拒绝 | git fetch 后的左右提交数、服务端保护提示 | 正常整合或走评审;重新 push 后检查远端精确引用 |
| 标签与批准提交不同 | 完整 refs/tags 路径、标签对象和 ^{commit} | 停止构建,查标签变更;发布负责人确定新引用后重新审批 |
| 切换分支被本地修改阻止 | git status、工作区与暂存区 diff | 保存到有名分支或明确 stash;切换后检查需要的文件,不直接丢弃 |
| 意外 detached HEAD 中提交了修复 | git log -1、git reflog | 用 git branch rescue-fix HEAD 保住当前提交,再按流程合入 |
| dubious ownership | 目录实际属主、挂载身份、当前 UID | 修正预期所有权;确需共享时只信任已核验的精确目录,不设置 safe.directory=* |
| 离线构建缺文件 | 子模块提交、LFS 指针、浅/部分克隆、本地依赖 | 补齐对应对象或依赖,再在断网环境重新构建 |
| 凭据进入历史 | 已暴露凭据的权限与使用记录 | 先撤销轮换,再按团队流程清理历史与缓存;gitignore 不能撤销已经泄露的秘密 |
误移动本地分支后,reflog 常能找到旧位置。先读取 git reflog --all,核对目标提交内容,再创建新的恢复分支;不要直接把公共 main 重置到某个看起来熟悉的编号。reflog 保留与对象回收受配置和时间影响,也不随普通 clone 一起复制,完整说明见 git-reflog。
运行故障需要优先恢复服务时,部署上一份已验证制品,并保留对应源码及配置记录。源码后续用修复提交整理,数据库和外部副作用按自身恢复方案处理;把代码反向提交到 Git,不会撤销已发出的短信、支付或数据迁移。
权威资料与规范地址
安装、对象与仓库获取
协作与版本标识
| 查阅内容 | 官方地址 |
|---|---|
| 推送 | https://git-scm.com/docs/git-push |
| 合并 | https://git-scm.com/docs/git-merge |
| 公共历史修复 | https://git-scm.com/docs/git-revert |
| 标签 | https://git-scm.com/docs/git-tag |
| 标签签名检查 | https://git-scm.com/docs/git-verify-tag |
离线交付与恢复
| 查阅内容 | 官方地址 |
|---|---|
| 离线对象包 | https://git-scm.com/docs/git-bundle |
| 对象完整性 | https://git-scm.com/docs/git-fsck |
| 文件树归档 | https://git-scm.com/docs/git-archive |
| 本地引用恢复 | https://git-scm.com/docs/git-reflog |
