版本控制(Git)
进行版本控制的方法很多。Git 拥有一个经过精心设计的模型,这使其能够支持版本控制所需的所有特性,例如维护历史记录、支持分支和促进协作。
快照
Git 将顶级目录中的文件和文件夹作为集合,并通过一系列快照来管理其历史记录。在 Git 的术语里,文件被称作 Blob 对象(数据对象),也就是一组数据。目录则被称之为“树”,它将名字与 Blob 对象或树对象进行映射(使得目录中可以包含其他目录)。快照则是被追踪的最顶层的树。例如,一个树看起来可能是这样的:
1
2
3
4
5
6
7
<root> (tree)
|
+- foo (tree)
| |
| + bar.txt (blob, contents = "hello world")
|
+- baz.txt (blob, contents = "git is wonderful")
这个顶层的树包含了两个元素,一个名为 “foo” 的树(它本身包含了一个 blob 对象 “bar.txt”),以及一个 blob 对象 “baz.txt”。
历史记录建模:关联快照
版本控制系统和快照有什么关系呢?线性历史记录是一种最简单的模型,它包含了一组按照时间顺序线性排列的快照。不过出于种种原因,Git 并没有采用这样的模型。
在 Git 中,历史记录是一个由快照组成的有向无环图,快照具有多个“父辈”而非一个,因为某个快照可能由多个父辈而来。例如,经过合并后的两条分支。在 Git 中,这些快照被称为“提交”。通过可视化的方式来表示这些历史提交记录时,看起来差不多是这样的:
1
2
3
4
o <-- o <-- o <-- o
^
\
--- o <-- o
其中的 o 表示一次提交(快照)箭头指向了当前提交的父辈(这是一种“在…之前”,而不是“在…之后”的关系)。在第三次提交之后,历史记录分岔成了两条独立的分支。它们之间是相互独立的。开发完成后,这些分支可能会被合并并创建一个新的提交,这个新的提交会同时包含这些特性。新的提交会创建一个新的历史记录,看上去像这样:
1
2
3
4
o <-- o <-- o <-- o <---- o
^ /
\ v
--- o <-- o
Git 中的提交是不可改变的。但这并不代表错误不能被修改,只不过这种“修改”实际上是创建了一个全新的提交记录。而引用(参见下文)则被更新为指向这些新的提交。
对象和内存寻址
Git 中的对象可以是 blob、树或提交:
1
type object = blob | tree | commit
Git 在储存数据时,所有的对象都会基于它们的 SHA-1 哈希 进行寻址。
1
2
3
4
5
6
7
8
objects = map<string, object>
def store(object):
id = sha1(object)
objects[id] = object
def load(id):
return objects[id]
Blobs、树和提交都一样,它们都是对象。当它们引用其他对象时,它们并没有真正的在硬盘上保存这些对象,而是仅仅保存了它们的哈希值作为引用。
1
2
100644 blob 4448adbf7ecd394f42ae135bbeed9676e894af85 baz.txt
040000 tree c68d233a33c5c06e0340e4c224f0afca87c8ce87 foo
引用
给这些哈希值赋予人类可读的名字,也就是引用(references)。引用是指向提交的指针。与对象不同的是,它是可变的(引用可以被更新,指向新的提交)。例如,master 引用通常会指向主分支的最新一次提交。
PS:通常情况下,我们会想要知道“我们当前所在位置”,并将其标记下来在 Git 中,我们当前的位置有一个特殊的索引,它就是 “HEAD”。
1
commit c39eaaf3f5a5628c721419a15aec3fad5a4e007c (HEAD -> main, origin/main, origin/HEAD)
Git 的命令行接口
基础
git help <command>: 获取 git 命令的帮助信息git init: 创建一个新的 git 仓库,其数据会存放在一个名为.git的目录下git status: 显示当前的仓库状态
1
2
3
4
5
oplisty@oplistydeMacBook-Air oplisty.github.io % git status
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
git add <filename>: 添加文件到暂存区1
git commit
: 创建一个新的提交
git log: 显示历史日志git log --all --graph --decorate: 可视化历史记录(有向无环图)
1
2
3
4
5
6
7
8
9
10
11
12
13
oplisty@oplistydeMacBook-Air oplisty.github.io % git log --all --graph --decorate
* commit c39eaaf3f5a5628c721419a15aec3fad5a4e007c (HEAD -> main, origin/main, origin/HEAD)
| Author: oplisty <oplisty@oplistydeMacBook-Air.local>
| Date: Tue Jan 20 11:33:04 2026 +0800
|
| 1
|
* commit 1b8d6a81c1def19ec2cf53fd663f7315af66b6c6
| Author: oplisty <oplisty@oplistydeMacBook-Air.local>
| Date: Tue Jan 20 11:24:46 2026 +0800
|
| 1
:
git diff <filename>: 显示与暂存区文件的差异
1
2
3
4
5
6
7
8
9
diff --git a/_posts/2026-01-20-CS-git.md b/_posts/2026-01-20-CS-git.md
index 788f137..c008a55 100644
--- a/_posts/2026-01-20-CS-git.md
+++ b/_posts/2026-01-20-CS-git.md
@@ -8,3 +8,131 @@ tags: [Computer science]
进行版本控制的方法很多。Git 拥有一个经过精心设计的模型,这使其能够支持版本控制所需的所有特性,例如维护历史记录、支持分支和促进协作。
+## 快照
git diff <revision> <filename>: 显示某个文件两个版本之间的差异
1
git diff a1b2c3d README.md
看 README.md 相对上一个提交(a1b2c3d)改了什么
git checkout <revision>: 更新 HEAD(如果是检出分支则同时更新当前分支)- 检出某个提交(进入 detached HEAD)
1
git checkout a1b2c3d
- HEAD 指向提交
a1b2c3d - 工作区文件会变成那次提交的内容
- 你不再“在某个分支上”(detached HEAD)
- 这时候可以查看、编译、运行、做临时修改;但如果要保留修改,最好新建分支:
1
git checkout -b debug-old-state
2. 检出某个分支(HEAD 跟着分支走,同时切换当前分支)
1
git checkout feature/login
- HEAD 指向
feature/login分支的最新提交 - 当前分支变成
feature/login 工作区更新为该分支内容
- 把某个文件恢复到某个版本(只影响文件,不切换分支)
1
git checkout HEAD~1 -- README.md
效果:
- 只把
README.md恢复成上一个提交里的版本 - 其它文件和分支不变
- 恢复后这个变化会出现在工作区/暂存区里(你可以再
git add/git commit)
分支和合并
git branch: 显示分支git branch <name>: 创建分支1
git checkout -b <name>
: 创建分支并切换到该分支
- 相当于
git branch <name>; git checkout <name>
- 相当于
git merge <revision>: 合并到当前分支
1
2
3
4
git checkout main
git pull
git merge feature/login
#将login分支合并到main中
git mergetool: 使用工具来处理合并冲突当
merge(或rebase)出现冲突时,Git 会把冲突写进文件(带<<<<<<======>>>>>>标记)。git mergetool会启动你配置的合并工具(如 VS Code、Meld、KDiff3、Beyond Compare 等)帮你交互式选择/编辑冲突。
1
2
3
4
5
6
git checkout main
git merge feature/login # 提示 README.md 冲突
git status # 显示 both modified: README.md
git mergetool # 打开工具解决 README.md
git add README.md
git commit # 完成 merge commit
git rebase: 将一系列补丁变基(rebase)为新的基线把当前分支的一系列提交,当作“补丁”,重新应用到另一个基线(base)上,让历史变成更线性:
merge:保留分叉 + 合并点(历史更真实)
rebase:把你的提交“搬家”,让历史看起来像是从最新主干上直接开发的(更线性)
关键:rebase 会生成新的提交 SHA(因为历史被重写了)。
假设:
main以前在B- 你从
B切了feature/login,做了两个提交F1 F2 - 后来别人把
main又推进到了C D
历史图(从左到右是时间):
1
2
3
A --- B --- C --- D (origin/main 最新)
\
F1 --- F2 (feature/login 你的提交)
现在问题是:你的 feature 是基于旧的 B,而 main 已经到 D 了。
1
2
3
4
5
6
7
8
git checkout feature/login
#切到你的功能分支上(很关键):
#rebase 是“改当前分支的历史”,所以要先在 feature/login 上。
git fetch origin
#从远端把最新提交取回来,更新 origin/main 的指针。
#执行完后,你本地就知道 origin/main 最新在哪(比如 D)
git rebase origin/main
#这是关键:把 feature/login 上的提交“搬家”。
执行完后情况如下
1
A --- B --- C --- D --- F1' --- F2' (feature/login)
git fetch:从远端把最新的分支/提交拉到本地的远端跟踪分支(比如origin/main),但不改动你当前分支的代码。
远端操作
git remote: 列出远端git remote add <name> <url>: 添加一个远端git push <remote> <local branch>:<remote branch>: 将对象传送至远端并更新远端引用git branch --set-upstream-to=<remote>/<remote branch>: 创建本地和远端分支的关联关系git fetch: 从远端获取对象/索引git pull: 相当于git fetch; git mergegit clone: 从远端下载仓库
撤销
git commit --amend: 编辑提交的内容或信息你刚 commit 完,发现:
漏加了文件 / 少加了改动
commit message 写错了
用 amend 可以把“上一次提交”重做一遍(产生新的 commit SHA)
1
git commit --amend -m "fix: correct typo in README"
git reset HEAD <file>: 恢复暂存的文件你
git add加到暂存区了,但突然不想把某个文件这次提交:1 2 3
git add a.txt b.txt # 现在只想提交 a.txt,不想提交 b.txt git reset HEAD b.txtgit checkout -- <file>: 丢弃修改git restore: git2.32 版本后取代 git reset 进行许多撤销操作
Git 高级操作
git config: Git 是一个 高度可定制的 工具git clone --depth=1: 浅克隆(shallow clone),不包括完整的版本历史信息git add -p: 交互式暂存git rebase -i: 交互式变基git blame: 查看最后修改某行的人git stash: 暂时移除工作目录下的修改内容git bisect: 通过二分查找搜索历史记录.gitignore: 指定 故意不追踪的文件
作业
从 GitHub 上克隆某个仓库,修改一些文件。当您使用 git stash 会发生什么?当您执行 git log –all –oneline 时会显示什么?通过 git stash pop 命令来撤销 git stash 操作,什么时候会用到这一技巧?
git stash会发生什么?
- Git 会把你工作区的改动(默认也包括已暂存的改动)打包成一份“stash”,存到仓库里一个专门的栈中。
- 然后你的工作区会回到一个干净状态(就像你没改过一样),
git status往往会显示working tree clean。 - 这些改动没有丢,只是被临时“收起来”了。
注意:默认
git stash不包含未跟踪文件(新建但没 git add 的文件)。要包含需要git stash -u。
1
2
oplisty@oplistydeMacBook-Air oplisty.github.io % git stash
Saved working directory and index state WIP on main: 61a389b 更新周报
git log --all --oneline会显示什么
会看到除了正常分支提交外,还会多出和 stash 相关的提交/引用,常见表现是出现类似:
refs/stash指向的一条提交(stash 的“主提交”)- 以及它可能关联的额外提交(用于保存 index 状态/未暂存状态)
所以 --all 会把这些 stash 的提交也一起显示出来,你会看到类似“stash@{0}…”对应的那条记录(具体显示形式因 Git 版本略有差异,但核心是:stash 本质上也是提交对象,被 refs/stash 引用)。
1
2
3
4
5
360ea85 (refs/stash) WIP on main: 61a389b 更新周报
9df1026 index on main: 61a389b 更新周报
61a389b (HEAD -> main, origin/main, origin/HEAD) 更新周报
7f36dd2 1
bab908a 更新了一下日期
git stash pop撤销 stash:什么时候会用到这个技巧?
git stash pop = 把最近一次 stash 的改动恢复到工作区,并从 stash 栈里删除它。