qinghuan
← 工程实践 / 全部笔记

单元 14 / 工程实践

P0 · 必会主线L0 · 零基础

Git 从零工作流:让每次修改都能检查、解释和撤回

从工作区、暂存区和提交三个状态开始,每次先检查再提交,学会解释修改并在共享历史中撤回效果。

本篇目录 · 按需要跳转

读完这篇,你能做什么

能在独立练习仓库提交一个可审查的改动,并用反向提交修正已共享的错误。

学习理由、准备要求与知识点

为什么学它

版本控制让每一步学习和修改可比较、可回退,是协作与自动交付的基础。

先读这些内容

知识点清单

  • 工作区
  • 暂存区
  • 提交
  • git status
  • diff 与 diff cached
  • 小步提交
  • 分支
  • revert

运行前先确认Git 2.x 基础工作流;Java 17 教学算法不操作真实仓库。摘要模型限定传统 SHA-1 blob 格式。JDK 源码在 Microsoft OpenJDK 17.0.20.1 src.zip 核验,上游链接 jdk-17.0.20-ga。

01先弄懂它是什么

Git 帮你记录每次改了什么,并保留可回看的项目快照。工作区是正在编辑的文件,暂存区是准备交进下一次提交的版本,HEAD 通常指向当前分支最新提交。三者可以不同:暂存后又改文件,commit 仍只记录暂存的内容。所以提交前除了看编辑器,还要看 diff --cached。

第一次练习时,先运行 git --version,再在自己新建的独立空目录 git init,并创建 Main.java。用 git status --short 和 git diff 查看修改,用 git add -- Main.java 放进暂存区,再用 git diff --cached 核对。

确认作者身份配置后,以 Add greeting program 为提交说明执行提交,最后再看状态。每一步都对照工作区、暂存区和提交发生了什么变化。

分支是指向提交的可移动引用,不是复制整个项目的独立文件夹。提交记录父提交,形成有向无环的历史;新分支让尚未完成的工作有独立推进位置。远程仓库与本地仓库分别保存对象和引用,fetch 获取远程更新并刷新远程跟踪引用,push 尝试更新远端引用。网络传输成功与代码行为正确是两件事。

文中这几个词,先认一认

暂存区
保存下一次准备提交的内容;文件后续又改动,不会自动把新改动一起加入。
共同祖先
两个分支都来自的那份历史提交,合并时用它区分双方各自做了什么修改。
反向提交
用 revert 新增一笔抵消旧修改的提交,保留已经共享的历史记录。

还有陌生词?到术语小抄里看一个具体例子 ↗

02看看它怎么工作

普通 git diff 比较工作区和暂存区,git diff --cached 比较暂存区和 HEAD,git diff HEAD 则显示当前工作区相对 HEAD 的变化。先确定比较端点,才能解释为什么某次 diff 为空:它只意味着指定两个状态在该比较范围内相同,不能推导整个仓库没有其他变更。未跟踪文件也需要通过 status 观察。

合并不能只比较两边最终内容,因为不同内容不一定冲突。设共同基准为 B,我方为 O,对方为 T:若 O 等于 T,直接采用;若 O 等于 B,说明我方没改,采用 T;若 T 等于 B,采用 O;其余才需要更细的合并或人工决定。真实 Git 还会处理行级差异、重命名和多个祖先,本文算法刻意使用整文件内容作为原子值。

共享历史中发现错误,revert 通常通过新增反向提交保留审计关系;它也可能与后续修改冲突,必须检查最终代码并重新验证。reset、rebase 会涉及指针或历史整理,适合先明确本地状态和协作约定再使用。冲突标记消失只代表文本已处理,不代表语义正确,两个独立功能可能合并后才产生业务矛盾。

一次提交围绕一个能解释的行为变化,评审会更容易定位问题。提交说明应描述触发条件和结果,测试应针对该变化,而不是只写 update。若误提交了凭据,后续删除文件不能清除历史泄露,需按凭据处理流程撤销或轮换;日常应在提交前排除不该进入仓库的本机配置与生成产物。

正在加载流程图…

提交记录暂存快照;推送发布引用关系,不能替代对内容和行为的检查。
查看流程图源码(Mermaid)
flowchart LR
 W["工作区"] -->|"git add"| I["暂存区"]
 I -->|"git commit"| C["提交与分支"]
 C -->|"git push"| R["远端引用"]
 C --> D["评审差异与验证"]
 D --> E["修正或反向提交"]

按顺序看,每一步做了什么

  1. 检查版本、当前目录与仓库状态。
  2. 对工作区差异和暂存差异分别确认。
  3. 形成目的明确的小提交。
  4. 合并时理解基准、我方与对方。
  5. 纠错后验证最终结果并按约定共享。

03对照实现代码

MessageDigest.digest:内容寻址实验中的摘要入口 (新标签页)↗

Microsoft OpenJDK 17.0.20.1;java.base/java/security/MessageDigest.java · java.security.MessageDigest#digest(byte[])

以下为原创 Java 17 教学实现,不是原样 JDK 源码;只保留文中明确讨论的机制,省略优化、兼容及内部接口。

核心机制代码 · java
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.util.HexFormat;
public final class SourceModel {
  static String blobId(String text) throws NoSuchAlgorithmException {
    byte[] body=text.getBytes(StandardCharsets.UTF_8); // 长度按字节计,不按 Java 字符计。
    byte[] header=("blob "+body.length+"\0").getBytes(StandardCharsets.US_ASCII);
    MessageDigest digest=MessageDigest.getInstance("SHA-1"); // 仅演示传统 SHA-1 对象格式。
    digest.update(header); // Git blob 标识包含对象类型、长度和零字节分隔。
    return HexFormat.of().formatHex(digest.digest(body));
  }
}

Java MessageDigest.digest(byte[]) 的本机源码先 update(input),再调用无参 digest 完成摘要。此前已 update 的头部也参与计算,因此教学代码能把对象头和内容连续送入同一个摘要过程。不能将只对文件正文取摘要的结果直接冒称 Git blob 标识。

传统 SHA-1 Git 对象格式把类型、字节长度、零字节分隔和正文共同计算标识。教学实现限定 UTF-8 文本 blob,没有实现 tree、commit、压缩存储或仓库引用。支持其他对象格式的仓库需要按实际配置处理,不能依据摘要长度之外的猜测随意混用算法。

这里定位的是 JDK 的 Java 摘要 API,Git 的主要实现并不是 Java。代码是学习内容寻址的一小段客户端模型,不是 Git 内核源码,也没有模拟真实 Git 的碰撞防护等实现细节。源代码阅读的目的在于解释增量输入和最终摘要的联系,而非声称实现了完整版本控制。

相同字节内容可以共用一个对象,但文件路径保存在 tree 中,不属于 blob 正文,所以相同内容能出现在不同文件名下。commit 又记录 tree、父提交、作者等信息;即使文件没变,改提交说明也可能得到新 ID。提交 ID 不能简单理解成全部项目文件压缩后的哈希。

代码先调用谁,再调用谁

  1. UTF-8 正文转字节
  2. 构造 blob 头
  3. MessageDigest.update 头部
  4. digest 正文并完成摘要
  5. 对象标识与树路径分别管理

04动手写一个例子

按文件原子值进行三方快照合并

输入基准、我方和对方三个路径到文本的映射;缺少路径表示删除,值禁止 null。输出可自动合并的文件和冲突路径,不改任何仓库。完整 Main 用 Java17 运行自测。

输入 1
基准 x=a,我方 x=b,对方 x=a
输出 1
x=b
输入 2
基准 x=a,我方 x=b,对方 x=c
输出 2
冲突 x

将三张快照的路径做并集,确保新增和删除都参与比较。每个路径上,null 仅代表不存在,所以先禁止用户传入 null 值,避免无法区分空值文件和删除。空字符串仍是合法文件内容,与不存在不同。

依次检查双方相等、我方未改和对方未改,能处理相同修改、单边修改以及双方同时删除。无法匹配这些规则时记录冲突,不擅自选择一边。删除与另一边修改同一文件也进入冲突,防止用删除无声覆盖对方工作。

结果使用按路径排序的映射和冲突列表,便于稳定检查。算法把整文件作为不可分割值,因此即便双方改的是不同代码行,也会保守报告冲突。这种局限适合作为三方判断的起点;进一步实现行级合并时,需要新的差异算法和重叠修改规则。

为什么这样写是对的每个已处理路径只在两边一致或至少一边相对基准未改时自动选值,其余明确报告冲突。

时间 O(k log k + L),k 为路径数,L 为文本和路径比较累计长度。额外空间 O(k),结果引用原文本,不复制全部正文。

路径并集排序需要 O(k log k) 次比较,每个路径执行有限次内容相等判断;字符串比较成本不能无条件视为常数。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

Main.java · 完整可运行程序 · Java
import java.util.*;
public class Main {
  record Result(Map<String,String> files,List<String> conflicts) {}
  static Result merge(Map<String,String> base,Map<String,String> ours,Map<String,String> theirs) {
    for(Map<String,String> m:List.of(base,ours,theirs))
      for(var e:m.entrySet()) {Objects.requireNonNull(e.getKey()); Objects.requireNonNull(e.getValue());}
    Set<String> paths=new TreeSet<>(); paths.addAll(base.keySet()); paths.addAll(ours.keySet()); paths.addAll(theirs.keySet());
    Map<String,String> files=new TreeMap<>(); List<String> conflicts=new ArrayList<>();
    for(String p:paths) {
      String b=base.get(p),o=ours.get(p),t=theirs.get(p),chosen;
      if(Objects.equals(o,t)) chosen=o; // 双方一致,包括共同删除。
      else if(Objects.equals(o,b)) chosen=t; // 我方未改,接受对方结果。
      else if(Objects.equals(t,b)) chosen=o;
      else {conflicts.add(p); continue;} // 双方不同修改,保留人工决策入口。
      if(chosen!=null) files.put(p,chosen); // 缺少值表示删除,空字符串仍是文件。
    }
    return new Result(Collections.unmodifiableMap(files),List.copyOf(conflicts));
  }
  static void eq(Object a,Object b) {if(!Objects.equals(a,b)) throw new AssertionError(a+" != "+b);}
  public static void main(String[] args) {
    Map<String,String> b=Map.of("x","a");
    eq(merge(b,Map.of("x","b"),b).files(),Map.of("x","b"));
    eq(merge(b,b,Map.of("x","b")).files(),Map.of("x","b"));
    eq(merge(b,Map.of("x","b"),Map.of("x","b")).conflicts(),List.of());
    eq(merge(b,Map.of("x","b"),Map.of("x","c")).conflicts(),List.of("x"));
    eq(merge(b,Map.of(),b).files(),Map.of()); // 单边删除而另一边未改。
    eq(merge(b,Map.of(),Map.of("x","b")).conflicts(),List.of("x"));
    eq(merge(Map.of(),Map.of("new",""),Map.of()).files(),Map.of("new",""));
    eq(merge(Map.of(),Map.of(),Map.of()).files(),Map.of());
    System.out.println("PASS 8");
  }
}

这些特殊情况也要试一试

  • 双方删除不产生冲突。
  • 删除与修改产生冲突。
  • 空字符串与路径缺失不同。
  • 输出不包含未经决策的冲突值。
进阶选读 · 行级三方合并与重命名

场景双方常修改同一个大文件的不同区域。

做法先生成相对基准的差异片段,再处理不重叠修改;重叠片段和重命名需要明确策略及语义验证。

复杂度差异算法成本依赖文本长度与差异规模,不能由本模型 O(k log k) 直接推出。

好处与代价自动化更强,但文本可合并不代表行为兼容,仍需构建和测试。

05项目里怎么用

协作时每次修改要容易看懂和检查。分支策略可以简单,提交、评审和合并约定要一致;大量格式化与业务改动分开,能减少干扰。提交前核对暂存内容和测试,合并后再检查组合结果,尤其关注配置、接口与数据库迁移是否配套。

共享历史修正应考虑其他协作者已经基于哪些提交继续开发。反向提交通常便于保留因果记录,历史重写则需要协调引用变化。任何恢复动作都先确认工作区和暂存区状态,避免为修复一个旧提交丢掉新的未提交成果。reflog 能提供本地引用移动线索,但不应被当作替代备份的保证。

仓库中保存源码、声明配置与必要资产,构建缓存和机器专属文件通常通过忽略规则管理。忽略规则不自动停止跟踪已提交文件。若需要证据追溯,应把提交标识、构建产物与发布记录关联起来;仅凭某个分支名无法永久确定部署内容,因为分支引用会继续移动。

容易踩的坑

  • 暂存后继续编辑却以为 commit 会包含全部最新内容。
  • 把无冲突误当作业务兼容。
  • 在共享历史上随意重写导致协作引用分叉。
  • 删除秘密文件后误以为历史中已不存在。

几种做法,怎么选

表格有四列,窄屏可左右滑动查看代价和适用条件。

方案优势代价与限制适用条件
小步提交评审、定位和反向修正容易需要主动整理修改范围日常团队开发
合并提交保留分支汇合关系历史图更复杂强调并行开发上下文
压缩为单提交主线变化集中细分过程不保留在主线单功能交付且团队已约定

06检验是否读懂了

先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。

先用自己的话解释

01commit 读取哪个状态?

展开参考答案与推导

读取暂存区构造快照,而非自动复制工作区最新文件。暂存后继续修改会形成已暂存和未暂存两组差异,应分别检查。

02三方合并为何需要共同祖先?

展开参考答案与推导

只有两边结果不同无法判断谁修改了什么。共同祖先提供基准,从而识别单边未改并接受另一边变化;双方相对基准都不同才需要更细判断。

03同内容文件为何可出现在不同路径?

展开参考答案与推导

blob 表示内容,路径由 tree 关联。同一个内容对象可以被多个树项引用,路径与正文并非同一层信息。

换个情况,还会处理吗

01diff 为空但 status 仍有变化如何解释?

展开参考答案与推导

先确认 diff 比较的是工作区和暂存区还是暂存区和 HEAD,并检查未跟踪文件。一个比较为空只说明那两个端点相同。

02共享提交有错误如何选择修复?

展开参考答案与推导

先确认后续依赖和团队约定,通常可用新增修复或 revert 保留历史;若反向修改冲突,需要手动解决并验证最终行为,不直接以命令成功为结束。

03合并成功却测试失败怎么办?

展开参考答案与推导

检查组合后的接口、配置和数据假设,定位双方各自正确但共同使用时失效的关系。文本无冲突不能证明业务语义可组合。

写代码试一试

01判断单边新增。

输入 / 条件
基准无 x,我方 x=hi,对方无 x
输出 / 预期
x=hi

提示 未修改分支等于基准。

展开参考答案与推导

对方与基准都缺少路径,采用我方新增内容;不能把不存在简单当作空字符串,否则空文件会混淆判断。

02判断删除与编辑。

输入 / 条件
基准 x=a,我方删除,对方 x=b
输出 / 预期
冲突 x

提示 两边都改变基准。

展开参考答案与推导

删除与新内容既不相同,也没有一边等于基准,必须报告冲突。自动选择删除会丢弃对方编辑。

03提交前要比较什么?

输入 / 条件
已暂存 A,随后工作区改成 B
输出 / 预期
提交仍为 A

提示 查看暂存差异。

展开参考答案与推导

git diff --cached 展示下一次提交相对 HEAD 的内容,git diff 展示 A 到 B 的未暂存变化。根据提交目的决定是否再次暂存。

读完之后,独立完成

课后作业:自己做一遍

本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

能在独立练习仓库提交一个可审查的改动,并用反向提交修正已共享的错误。

本篇重点加练

暂存内容才决定提交对象,代码评审必须识别工作区与暂存区不一致及不易显示的变更。

  • 暂存区
  • diff 与 diff cached

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01排错实验

    在新建临时仓库创建两个文本文件,只暂存其中一个,再修改已暂存文件。逐步记录工作区与暂存区内容,不操作现有项目。

    提交内容
    命令记录与两份差异
    怎样算完成
    • 每步注明当前目录
    • 可指出每段修改所在区域

    练到的知识点工作区 · 暂存区

  2. 作业 02代码推演

    继续在临时仓库提交一次,再新增未跟踪文件并删除已跟踪文件。运行前预测 status 分类,运行后解释提交包含与未包含的内容。

    提交内容
    预测表与实际状态
    怎样算完成
    • 区分文件状态和提交历史
    • 提交信息能描述实际变更

    练到的知识点提交 · git status

  3. 作业 03原理问答

    一个修改同时修正登录错误、更新排版和加入调试输出。设计可独立检查的小步提交计划,说明各次 diff 与 diff --cached 应检查什么。

    提交内容
    提交拆分计划
    怎样算完成
    • 每次提交有单一可解释目的
    • 临时调试内容有去留决定

    练到的知识点diff 与 diff cached · 小步提交

  4. 作业 04代码推演

    同一文件有修复和临时调试两段修改,只暂存修复后又继续编辑。定义此时本地测试实际验证的内容与待提交内容之间的关系。

    提交内容
    内容版本与证据对应表
    怎样算完成
    • 区分索引、工作区和提交
    • 不能把一次测试默认归属于所有版本

    练到的知识点暂存区

综合应用

  1. 作业 05设计评审

    两个临时分支分别修改同一配置项,合并后发现行为不符合需求。设计保留历史的恢复操作,说明 revert 对已有后续修改的影响。

    提交内容
    分支图与恢复计划
    怎样算完成
    • 不重写共享历史
    • 包含冲突检查与恢复后验证

    练到的知识点分支 · revert

  2. 作业 06编程实践

    在独立临时仓库完成需求说明、实现、测试三次提交;制造一次可控合并冲突,解决后再撤回其中一项需求。

    提交内容
    仓库路径、提交图和验证记录
    怎样算完成
    • 所有操作限制在临时仓库
    • 最终内容与提交说明一致

    练到的知识点工作区 · 暂存区 · 提交 · git status · diff 与 diff cached · 小步提交 · 分支 · revert

  3. 作业 07设计评审

    变更中包含重命名、换行转换和一份生成文件。制定差异审查顺序,说明文本 diff 可能隐藏或放大的信息。

    提交内容
    差异分类与审查清单
    怎样算完成
    • 核对真实内容及文件属性
    • 不只根据行数判断风险

    练到的知识点diff 与 diff cached

  4. 作业 08编程实践

    在独立临时仓库做部分暂存,并把暂存版本导出到另一临时目录运行一个小检查;比较工作区检查与待提交检查的证据。

    提交内容
    命令、目录与检查记录
    怎样算完成
    • 不改现有仓库
    • 精确记录被检查版本

    练到的知识点暂存区 · diff 与 diff cached

进阶挑战 · 掌握前置后选做

  1. 作业 09设计评审

    代码评审发现一个提交混入密钥占位样例和无关格式变更。制定提交前审查清单与共享后修复决策,不使用真实秘密进行演练。

    提交内容
    评审清单与处置说明
    怎样算完成
    • 区分占位数据和真实泄露的处置边界
    • 明确哪些操作影响协作者

    练到的知识点暂存区 · diff 与 diff cached · 小步提交 · 分支 · revert

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 10思维拓展 · 跨域迁移

    将暂存版本和差异审查思想迁移到训练数据集发布:标注者持续修改原始样本,评审者批准其中一部分。怎样保证训练实际使用的内容就是获批内容?

    提交内容
    跨域版本与审查协议
    怎样算完成
    • 区分工作数据和已批准快照
    • 包含评审期间样本再次修改

    练到的知识点暂存区 · diff 与 diff cached

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗