Post

版本控制(Git)

版本控制(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(如果是检出分支则同时更新当前分支)
    1. 检出某个提交(进入 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. 把某个文件恢复到某个版本(只影响文件,不切换分支)
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 merge
  • git 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.txt
    
  • git 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 操作,什么时候会用到这一技巧?

  1. 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 更新周报
  1. 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 更新了一下日期
  1. git stash pop 撤销 stash:什么时候会用到这个技巧?

git stash pop = 把最近一次 stash 的改动恢复到工作区,并从 stash 栈里删除它

This post is licensed under CC BY 4.0 by the author.