git进阶

本文最后更新于:2 个月前

git存储原理

git的存储比我想象的暴力的多(

自己尝试了一下发现,先提交一个10M的压缩包上去,.git 文件的大小是10M多一点

然后随便改这个压缩包的一点字符,再提交一次,.git 变成20M了…

我曾一直以为它有什么神奇的办法可以记录任意类型文件的修改量,没想到它只是整个拷贝…

并且所有版本信息都存储在.git中,分支,提交节点什么的只是存储索引,因此git能够很快地创建和切换分支,我认为这也是一种空间换时间的做法。至少它在创新新分支时没有整个拷贝。


不过仔细一想,又发现这样存储文件,其实占用并不会特别大。git对每个文件单独暴力地存所有版本,在节点信息中,记录每个文件所在的版本号。上述例子中我用了一个压缩包作为单独的文件,但在实际工作中,其实很少会出现10M大小的单个文件,更多的是大量小文件的组合。修改一处,也只是会创建一个小文件的副本罢了。

git存储实现

粗略写一下大体概念,细节不重要(

首先,git对每个单独的文件暴力存储所有修改版本,每个版本有一ID(哈希生成的40位数),这些版本的实际数据存放在 .git/object 中,每个文件夹是一个两位数,里面有文件名38位的文件,存放实际数据。这些东西就是数据对象(blob)

每个文件夹作为一个树对象,树对象仅存储其目录下数据对象和子树对象的ID(当然是对应版本的),这样下来,最终的每个commit节点只需要存储一个树对象ID,和其父节点ID即可。

而分支仅仅只是指向commit节点的指针,因此git能很快地建立分支

git如何发现你的修改

这一点也比我想的暴力:直接对比当前工作区中的文件和commit节点中的文件是否一致。

当然也没那么暴力,还记得git存储的文件ID是通过哈希生成的吗,既然如此,只需要对比哈希值就可以了。

追踪变更历史

追踪变更历史这个东西,直观上来讲就是及其不严谨的…

试验了一下确实如此,git log 这条命令,在文件重命名文件移动路径,以及修改其路径上任意文件夹名时,都会导致其变更历史丢失。

由此可以简单猜测:git其实只是利用了树对象,记录了一个特定路径的文件的提交历史,在日常不频繁更改文件名和路径的情况下,这一命令才能正常追溯。

git Diff原理

这里细化到一个问题:给你两段代码 $X$ 和 $Y$,如何仅根据这两串代码最后的样子推断出 $X$ 是怎么修改到 $Y$ 的,当然方案有很多,git Diff希望找出最优的,最优的原则基本有以下几条,按重要程度排序:

  • 修改量尽可能少,
  • 同类修改(增加删除)尽可能多相邻
  • 修改尽可能符合语法

先不考虑最后一条,然后我们可以把问题想的简单点:假设程序员只会一行一行地修改代码,拿怕他只改了一个字符,我们也认为他修改了一行

这样把一片代码抽象成一个字符串,一行代表一个字符,我们要从S1通过删除字符,添加字符,变换到S2。

我直接贴:https://zhuanlan.zhihu.com/p/67024353 建模很妙,可以n^2dp求解

至于第二条,我觉得不需要太过严谨,其实只需要简单规定一下优先级即可:优先删除,再是插入

第三条感觉严谨来说也很难搞,不如不管了

暂存区,工作区

git add将工作区中的修改内容提交到暂存区,git commit将暂存区内容提交,保存为一个新的节点。

在切换分支,合并分支等过程中,必须要保证工作区clean,不然麻烦事很多

创建分支的话不影响(因为本地工作区不用动)

git分支

我的理解中,git是按照commit作为节点存放的,每个节点有其内容,父节点

所谓branch只是一个指向commit的指针。两个branch之间有祖孙关系,它们就很容易合并,否则需要进行一个三节点的合并:两个待合并的branch和它们的LCA

三节点合并成功之后,会产生一个新的节点,它有两个父亲

合并如果遇到冲突,暂不会产生节点,而是直接在你的工作区中修改,不冲突的部分合并完成,冲突的部分会在文件中打上特殊标记,长这样:

<<<<<<< HEAD
b
=======
a
>>>>>>> br1

你可以手动地去文件中删除这些标记,改到你希望的样子,然后手动提交产生一个新节点。

SSH

ssh -T git@github.com 中有哪些信息:

这条命令中的github.com即github网站的地址,它是一个git服务器

其中git是一个公用的用户,他记录了你的ssh公钥(我们写到github上的公钥)

我们连接时就是通过这个公用的用户git来传输的?

这条命令就是测试能否和github建立ssh连接

git服务器

我们自己本地的git仓库和服务端的git仓库是不一样的,服务端的git仓库是一个裸仓库,仅保存了版本信息,没有工作区。

创建一个裸仓库:

git init --bare

裸仓库中无法查看提交的文件

向一个非裸仓库push会报错

如果想要搭建一个可以查看文件的git服务端,一个可行的办法是使用钩子(hook),在接受到提交时,自动的转向另一个非裸仓库,再从这个裸库中pull一遍。现在这个页面就是通过这种方式部署的

git实操

这部分是实习的时候,发现自己实际操作非常手生,在公司记了一些

fast-forward,即向前合并,跟进当前分支的最新提交,不会产生冲突,不会产生合并节点。

git pull = git fetch + git merge,注意是merge而不是fast-forward,可以pull有分歧的分支(例如自己的开发分支完成后,主分支已经有新的东西了),然后在本地解决冲突。

cherry pick 单独提取一个commit的修改内容并应用。cherry-pick实际上是也是一个三路Merge,此时LCA视为目标commit的上一个commit。也就是说,以此LCA为基,我们的当前commit也视为是基于这个LCA修改的,这样来合并,当然也要解决冲突。
再理一下:Base是目标commit的上一个commit,可能导致冲突的是当前commit和目标commit,各自相对Base的更改内容的不同之处。

track,git的每个本地分支都追踪一个远程分支,在本地创建的分支,提交时需要推送上去形成一个新分支并追踪;远程的分支拉取到本地时,需要在本地创建一个分支并追踪。git pull之类的操作默认拉取追踪的分支。

HEAD,即当前分支,工作区所在的分支。

git通常都是在各个分支间进行切换操作,如果要切换到某个commit处,最好相应地创建一个分支(smartgit里的提示,基于commit的commit容易丢失。)

submodule:子模块自己管自己的,主模块中,.gitmodules记录了子模块的本地路径和远程url,并在index中记录了子模块的状态(当前commitid),可以认为主模块git仅追踪了子模块的状态,而不追踪子模块的文件。子模块commitid发生变化时,主模块也会worktree not clean(主模块中的记录与实际子模块id不一致)。
git pull默认仅更新主模块,包括了主模块中记录的子模块id,而不会直接更新子模块,因此在主模块pull之后,可能仍然是worktree not clean的状态,因为拉新后主模块记录id与子模块实际id不同了。

git pull支持递归拉新。

子模块中的远程分支,主模块是不知道的,子模块的远程有所更新并不会被主模块捕捉到,子模块需要自己去拉远程。

实际操作中会遇到更新了子模块,同时又要拉父模块的情况,此时可以先将子模块内容提交,然后回退(到父模块中记录的那个节点),使得父模块work tree clean,然后拉父模块,再切换子模块到最新节点。父模块只是fast-forward时,似乎也可以在work tree not clean的情况下直接pull。


git进阶
http://www.lxtyin.ac.cn/2022/04/02/2022-04-02-git进阶/
作者
lx_tyin
发布于
2022年4月2日
许可协议