Git 是目前最先进的分布式版本管理控制系统(VCS),但命令概念比较反直觉和抽象。
如果只学习上层操作使用,不理解底层实现原理,非常容易陷入死记硬背命令的状态,把几个命令当做魔法,因而非常容易遗忘。
虽然现在很多 GUI 工具辅助操作,但出现各种冲突和报错时,理解底层实现处理起来会更得心应手,否则有时候非常棘手,非常消磨心智,非常浪费时间。
重点理解:快照存储、哈希索引、引用指针、分支合并策略等底层原理
可以练习:模块化设计、面向对象编程、状态管理、序列化、文件系统操作
Git 的 Java 简易实现。主要实现了以下操作命令
- init,初始化本地仓库,init commit,HEAD 指向默认 master 分支
- add,Stage.addtions.add,Blob
- rm,Stage.removals.add
- commit,继承父 Commit,遍历 Stage 的 add 和 remove 表,更新 Commit 的 Map,创建新 Commit,更新分支头
- log,按父指针,遍历 commit,打印信息
- global-log,遍历 Commits 文件夹,打印信息
- show,根据 BlobID 打印文件内容
- find,加载 Commit 对象文件夹所有 Commit 对象,遍历,根据 Commit message 找 Commit
- status,
- 分支状态,遍历分支文件夹 refs/heads,HEAD 指向当前分支,第一个打印,并前面加*
- 暂存状态,打印 Stage 对象的 add 作为 Staged Files ,remove 作为 Removed Files
- 工作区修改状态,遍历文件集合,每个文件在三种状态区(工作目录、Stage、Commit)有不同的存在与否情况。
- 意外删除:工作目录没有,Stage remove 没有,但 Stage add 或 Commit 有,说明文件被意外删除
- 修改未 add:工作目录有,Stage add 没有,Commit 有,但 BlobID 不同,说明被修改但没 add
- add 后被修改:工作目录有,Stage add 有,但 BlobID 不同,说明 add 后被修改
- 未追踪状态,不在 Commit ,不在 Stage add,说明未追踪
- branch,创建分支文件 refs/heads/branchname,内容是指向的 CommitID 文本
- rm-branch,删除分支文件
- checkout,
- 1,恢复指定 Commit 中的文件(默认头 Commit),由文件名 filename 从 Commit.blobs 拿到 blobID,根据 blobID 加载 blob,再持久化到文件名
- 2,切换分支,清空工作目录,恢复目标分支 headCommit 的所有 blobs 到工作目录,设置当前分支,清空暂存区。
- reset,复用 checkout 分支,只不过是 checkout 任意 Commit,而非分支头 Commit。
- merge,
-
1,DAG 上用 BFS 找最近公共祖先
-
2,比较:split point, current commit, 给定分支的 head commit
- split point == 给定分支头,给定分支已经包含在当前分支中,无需合并
- split point == 当前分支头,可以直接快进,相当于 checkout
-
3,处理文件变动,遍历文件集合
-
4,创建新合并 Commit,两个父指针,发展为 DAG
-
| 情况 | 条件判断 | 操作 |
|---|---|---|
| 未修改 | S == C && S == G |
不处理 |
| 只给定分支修改 | S == C && S != G |
使用 G 的内容,checkout 并 stage |
| 只当前分支修改 | S != C && S == G |
保留当前内容 |
| 两分支同改且相同 | S != C && S != G && C == G |
不处理 |
| 两分支不同改 | S != C && S != G && C != G |
冲突,写冲突标记 |
| 新文件:仅给定分支有 | S == null && C == null && G != null |
使用 G 内容,checkout 并 stage |
| 新文件:两边都有不同内容 | S == null && C != null && G != null && C != G |
冲突 |
| 删除:当前删除,目标未删 | S != null && C == null && G != null |
冲突(或保留?) |
- add-remote,创建远程分支文件 refs/remotes/branchname,内容指向远程地址(未实现网络,指向本地其他文件目录)
- rm-remote,删除远程分支文件
- push,读取远程分支 HeadCommit 和本地分支 HeadCommit,检查 fast-forward 合法性(远程 commit 是本地 commit 的祖先),所有远程没有的 Commit 写到远程 commit 目录,更新远程分支指针
- fetch,读取远程分支 HeadCommit,DAG DFS 向回遍历,把所有 commit、blob 拉取到本地,创建[remoteName]/[branch] 分支
- pull,fetch + merge
-
git stash:暂存
-
git commit --amend:撤销 commit
-
git rebase:在当前分支重放一遍目标分支的操作,能精简分支,但会丢失一些现有提交,有一定风险
数据的状态在四种区域流转
本地工作目录下的新建文件或修改文件
保存下次提交的文件列表信息,可看做是文件操作索引
本地保存持久化的数据对象:Blob、Commit
远端本地保存持久化的数据对象:Blob、Commit
Git 本质实现文件状态集合的备份系统
Git 实现的难点是 0-1 的设计。如何设计程序来组织数据,实现目的?
Git 初始化后会在本地目录下生成 .git 隐藏文件,里面存储持久化的数据
-
1,使用文本文件存储状态:存储字符串标识状态
-
2,使用对象文件存储状态:序列化后存储为文件
Git 表面上的复杂性源于各种抽象概念的反直觉,但核心模型极致简洁和强大。
此项目没有引入目录处理
存储文件索引,指向对应的 Blob。使用 Map 存储:<文件名, Blob ID>
历史记录建模为关联快照:快照由链表连接,当多分支时 Commit 可有多个父节点,变成有向无环图(DAG)
Commit 是不可变的
分支本质是指向 Commit 的指针,因此创建销毁极快,因此 Git 推荐使用多分支管理。
HEAD 指向当前分支,分支指向 Commit
add 文件,Map 表存储: 文件名 -> blobID
rm 文件,Set 存储:文件名
用 SHA-1 哈希生成对象地址作为指针
异常处理
条件分支
List 遍历
有向无环图 DFS
暂时没有用设计模式
单个本地仓库时,Stage 类只有一个实例,可以用单例模式提供一个全局访问点。
问题:生命周期难以管理,在每次 commit 后 stage 被清空/重置,所以 Stage 状态必须与磁盘保持一致性,可以 Stage.getInstance().reload(); // 从磁盘强制刷新
将对象创建的复杂细节隐藏起来,使业务代码更简洁,也更符合单一职责原则
集中管理这些核心对象的创建逻辑,提高代码的灵活性和可维护性
抽象类中定义算法的骨架(模板),将步骤的实现延迟到子类中完成。
特征:每种命令的操作基本遵循 3 个步骤,首先加载磁盘中持久化的状态到内存中,其次对其进行操作,最后操作完将状态持久化到磁盘中。
优点:
- 复用代码:抽象类统一封装流程,避免每个子类都写一遍流程逻辑。
- 开放封闭原则:子类可以灵活扩展步骤,而不用动流程。
- 提升可读性和维护性:流程结构统一,行为局部差异。
特征:很多命令有多种不同情况判断,比如 merge 合并冲突处理有10 来种情况判断,导致 if-else 炼狱,可读性差,扩展性差。if-else 大于 5,且后续可能扩展判断,推荐使用策略模式。
优点:
-
解耦复杂逻辑,每种合并情况分成独立类,清晰单一职责
-
易扩展,新增策略不需修改已有逻辑,只需添加新类

