MS第三讲-版本管理器Git
Git Intro
Git是一种版本控制系统(VCS),用于追踪源代码或者其它文件更改。也就是说,这些工具可以帮助我们管理代码修改的历史记录,并且允许我们在不同版本之间切换。
如果你喜欢玩游戏,比如赛博朋克2077,那么Git对你来说会相当容易理解。想象一下,你在玩一个开放世界的游戏,可以随时保存你的进度。万一boss战的时候一不小心挂了,你可以回到之前的存档点,重新开始战斗,而不是从头再来一遍。Git就像是你游戏中的存档系统,帮助你保存代码的不同版本,让你可以随时回到之前的状态。
但是Git的强大之处不仅在于它能够不时地给代码和文件保存进度,实际上它甚至允许你为当前文件创建分支,这样你就可以在不影响主线代码的情况下进行实验和修改,而在2077中就没办法这么做了,除非你重头再玩一遍。
Git的另一个重要特点是它是分布式的,这意味着每个开发者的机器上都有完整的代码库和历史记录的副本。这样,即使中央服务器出现问题,开发者们仍然可以继续工作,并在服务器恢复后同步他们的更改。这就像是steam云存档功能,即使你换了一台电脑,登陆同一个steam账号,你的游戏进度依然会在云端保存着,反之亦然。
总的来说,Git是一个强大且灵活的工具,适用于个人项目和大型协作开发。通过使用Git,开发者可以更好地管理代码的变化,提高工作效率,并确保代码的安全性和完整性。
然而,Git也有一些缺点,比如抽象泄露问题,即通过自顶向下的学习方式可能让人感到困惑。但是是它的底层思想却是简单优雅的。因此,我们会自底向上开始学习Git,这样再学习它的接口时就会更加容易理解。
Git的数据模型
快照
Git将根目录及其下所有文件和文件夹作为一个整体来管理其历史记录。在Git中,文件被视为一组数据块,作为对象,它被称为Blob(Binary Large Object)对象;文件夹则被称为树(Tree)对象。快照(snapshot)则是被追踪的最顶层的目录的树对象。
那么Git是如何通过这些对象来管理版本历史记录的呢?最简单的方式是线性的历史记录,即包含一系列快照,每个快照包含一个指向前一个快照的引用。但是Git没有采用这样的方式,而是使用了一个由快照组成的有向无环图来存储版本历史记录。在Git中,这些快照被称为提交(commit),可以用下面的图来表示:
1 | o<--o<--o<--o |
在上面的图中,每个圆圈代表一个快照,而箭头表示快照之间的引用关系。可以看到,Git允许多个快照指向同一个前驱快照,这是因为我们可能需要同时独立开发两个不同的特性,而这两个特性都基于同一个代码版本进行开发。开发完成后,我们可以创建一个提交来合并这两个分支,它将同时包含两个分支的更改。比如:
1 | o<--o<--o<--o<--+ |
在上面的图中,最后一个提交包含了两个前驱提交的更改,这样我们就可以将两个分支的代码合并到一起。Git的提交是不可改变的,但是我们可以通过创建新的提交来记录代码的更改,从而形成一个完整的版本历史记录。
用伪代码表示数据模型
为了更好地理解Git的数据模型,我们可以使用伪代码来表示Git中的对象和它们之间的关系。下面是一个简单的伪代码示例,展示了Git中的Blob对象、Tree对象和Commit对象:
1 | type blob = array<byte> |
在上面的伪代码中,我们定义了三种类型的对象:
- Blob对象:表示文件内容,是一个字节数组。
- Tree对象:表示目录结构,是一个映射,键是文件或子目录的名称,值可以是另一个Tree对象或Blob对象。
- Commit对象:表示一个提交,包含一个父提交数组、作者信息、提交信息和一个快照(Tree对象)。
通过这种方式,我们可以清晰地看到Git是如何组织和管理代码的版本历史记录的。每个提交都包含一个快照,快照中包含了文件和目录的结构,而这些文件和目录又由Blob和Tree对象组成。这样,我们就可以通过提交来追踪代码的变化,并且可以随时回到之前的版本。
内存对象和寻址
在Git中,所有的对象(Blob、Tree和Commit)都是通过SHA-1哈希值来唯一标识的。SHA-1是一种加密哈希函数,它将任意长度的数据映射为一个固定长度的160位(20字节)哈希值。Git使用SHA-1哈希值作为对象的地址,这样我们就可以通过哈希值来访问和管理这些对象。
1 | type object = blob | tree | commit |
在上面的伪代码中,我们定义了一个对象存储系统,其中store函数用于将对象存储到一个映射中,并返回该对象的SHA-1哈希值作为其唯一标识符。load函数则用于通过哈希值来加载对象。因此,实际上电脑硬盘并没有真的保存这些对象,而只是保存了它们的哈希值作为引用。
引用
现在,所有的快照都可以用它们的哈希值来表示了,但是这也太难记了。所以Git的设计者Linus引入了引用(reference)的概念。引用是一个可变的指针,它指向一个特定的对象(通常是一个提交)。通过使用引用,我们让快照的名字更容易读取和使用了。
1 | references = map<string, string> // name -> object id |
在上面的伪代码中,我们定义了一个引用存储系统,其中update_reference函数用于更新引用的指向,read_reference函数用于读取引用所指向的对象的哈希值。load_reference函数则用于加载引用所指向的对象,无论是通过引用名称还是直接通过哈希值。
这样,我们就可以使用易于理解的名称来引用特定的提交,而不需要记住复杂的哈希值。例如,我们可以使用main作为主分支的引用名称,这样我们就可以通过load_reference("main")来加载主分支的最新提交。同时,通过更新引用,我们可以轻松地将分支指向新的提交,从而实现代码的版本管理。
Git的命令行接口
请阅读Pro Git获取更多信息。
下面是一些常用的Git命令
git help:显示Git的帮助信息。git init:初始化一个新的Git仓库,在当前目录下创建一个.git子目录。git status:显示当前工作目录和暂存区的状态,包括已修改但未提交的文件。git add <file>:将指定的文件添加到暂存区,准备提交。git commit -m "message":将暂存区的更改提交到仓库,并附加提交信息。git log:显示提交历史记录,包括每个提交的哈希值、作者和提交信息。git log --all --gragh --decorate:以图形化方式显示所有分支的提交历史记录。git diff:显示工作目录和暂存区之间的差异。git diff <revision> <filename>:显示指定修订版本和当前文件之间的差异。git checkout <branch>:切换到指定的分支。git branch:显示分支。git branch <branch>:创建一个新分支。git checkout -b <branch>:创建并切换到一个新分支。git merge <branch>:将指定分支的更改合并到当前分支。git mergtool:启动合并工具以解决合并冲突。git rebase: 将一系列补丁变基作为新的基线。git remote: 显示远程仓库。git remote add <name> <url>:添加一个新的远程仓库。git fetch <remote>:从远程仓库获取最新的更改,但不合并。git pull <remote> <branch>:从远程仓库拉取最新的更改并合并到当前分支。git push <remote> <branch local>:<branch remote>:将当前分支的更改推送到远程仓库。git clone <url>:克隆一个远程仓库到本地。git commit --amend:修改最后一次提交的信息或内容。git reset HEAD <file>:将指定文件从暂存区移除,但保留工作目录中的更改。git checkout -- <file>:丢弃工作目录中的更改,恢复到上次提交的状态。git restore: 恢复工作目录中的文件到指定状态。
2026年1月13日