Git的机制
首先
Git是一种分布式版本控制系统,在软件开发中被广泛使用。通过使用Git,多人在管理同一项目时,版本控制和代码共享变得更加容易。
在本文中,我们将解释Git是如何通过提交(commit)、分支(branch)、标签(tag)、HEAD等功能来工作的。
基本原理
BLOB、树、提交是Git数据结构的主要组成部分,它们构成了Git的基础。
为了理解这些要素,我会举例加以说明。
首先,让我们考虑创建一个空的存储库的情况。执行命令 git init 后,Git 会自动创建一个名为 .git 的隐藏文件夹。该文件夹用于存储Git内部使用的数据。
二进制大对象
在Git中,可以使用git add命令将文件添加到仓库中。
本次例子中,我们将创建一个名为myfile.txt的文件,并使用命令git add myfile.txt来将其添加进去。
### myfile.txt を作成
$ echo "hello" > myfile.txt
### Git リポジトリに myfile.txt を追加
$ git add myfile.txt
进行此操作时,Git 将在 .git/objects 子文件夹中创建名为 BLOB 的文件。
这个 BLOB 存储了 myfile.txt 的内容。它不包含相关的元数据,如创建时间戳和创建者。
### `git catfile` コマンドで BLOB ファイルの内容を表示
### (コンテンツ種別に応じた表示の最適化を行うため、`-p` オプションを付与)
$ git cat-file -p ce013625030ba8dba906f756967f9e9ca394464a
hello
BLOB 名称是根据内容的哈希值来命名的。内容经过哈希化后,前两个字符将作为.git/objects的子文件夹名称,剩余的字符将被用作BLOB名称。
换句话说,将文件添加到 Git 中时,会执行以下步骤。
-
- Git会获取文件的内容并进行哈希化。
-
- Git会在.git/objects文件夹中创建BLOB。
使用哈希值的前两个字符创建子文件夹。
在创建的子文件夹下,使用哈希值的剩余字符命名BLOB。
Git会将(压缩的)原始文件内容保存在BLOB中。
顺带一提,如果存在两个不同的文件名分别为myfile.txt和ourfile.txt,并且这两个文件的内容完全相同,那么它们将具有相同的哈希值,并存储在同一个BLOB中。
### myfile.txt をコピーし、ourfile.txt を作成
$ cp -p myfile.txt ourfile.txt
### ourfile.txt を `git add` し、BLOB が新しく生成されていないことを確認
$ git add ourfile.txt
$ find .git/objects -type f
.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a
$
另外,当您稍微修改了myfile.txt的内容并再次添加到存储库时,Git会重新执行前面提到的流程。在此过程中,由于哈希值的改变,将会创建一个新的BLOB。
### myfile.txt の内容を少し変更し、再度 `git add` を実施
$ cat myfile.txt
hello!
$ git add myfile.txt
### BLOB ファイルが新しく生成されたことを確認
$ find .git/objects -type f
.git/objects/4e/ffa19f4f75f846c3229b9dbdbad14eff362f32
.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a
### それぞれの BLOB について中身を確認
$ git cat-file -p ce013625030ba8dba906f756967f9e9ca394464a
hello
$ git cat-file -p 4effa19f4f75f846c3229b9dbdbad14eff362f32
hello!
树
我打算在这里创建一个名为”mysubfolder”的子文件夹,并在其中创建一个名为”yourfile.txt”的文件,然后将其添加到版本库中。
### mysubfolder フォルダを作成
$ mkdir mysubfolder
### mysubfolder 配下に yourfile.txt を作成
$ echo "world" > mysubfolder/yourfile.txt
### Git リポジトリに mysubfolder/yourfile.txt を追加
$ git add mysubfolder/yourfile.txt
通过该操作,Git 将会创建一个名为 yourfile.txt 的 BLOB(二进制大对象)。
接下来,使用命令 git commit 来提交 myfile.txt 和 yourfile.txt 两个文件。
### myfile.txt と mysubfolder/yourfile.txt をコミット
$ git commit -m"first commit"
[master (root-commit) c262f57] first commit
2 files changed, 2 insertions(+)
create mode 100644 myfile.txt
create mode 100644 mysubfolder/yourfile.txt
在这个操作中,Git 将执行以下两个步骤。
-
- 创建存储库的根目录树。
- 创建提交。
根目录树是记录存储库中所有文件和文件夹结构的位置。根目录树包含存储库中所有文件和子文件夹的引用,并具有递归结构。
根目录树的每一行都引用了文件或其他子树。类似地,这些文件和子树也引用了其他文件和子树。这使得树具有类似目录的结构。换句话说,您可以像从目录访问文件和子文件夹一样,可以从树中访问BLOB或子树。
$ git cat-file -p 0fa5b90dcd339c2f52b492a5126ccb760478e
100644 blob cc628ccd10742baea8241c5924df992b5c019f71 yourfile.txt
$ git cat-file -p 9b35f55d56eb0c921c533ae31a6a0ed9c7859
100644 blob ce013625030ba8dba906f756967f9e9ca394464a myfile.txt
040000 tree 0fa5b90dcd339c2f52b492a5126ccb760478ecbd mysubfolder
一旦Git创建了根树及其相关的所有子树,就会如前所述对它们进行哈希处理并保存。具体来说,每个树都会被哈希处理,然后使用前两个字符在.git/objects目录下创建子文件夹,并将剩余的哈希字符串作为保存的文件名。这样一来,就会创建出与树中数据结构数量相等的新文件。
提交
执行提交后,将以包含以下元数据信息的形式将文件保存下来。
-
- ルートツリー (tree)
-
- 親コミット (parent commit)
-
- …存在する場合
-
- 作成者の名前および電子メール (author)
-
- コミッターの名前および電子メール (committer)
- コミットメッセージ (feat)
$ git cat-file -p c262f5725e96f52c6bf1dbebcaf2680570fba
tree 9b35f55d56eb0c921c533ae31a6a0ed9c7859057
author John Doe <johndoe@example.com> 1683579147 +0900
committer John Doe <johndoe@example.com> 1683579147 +0900
first commit
当创建提交文件时,Git会对其内容进行哈希处理,并使用该哈希来保存内容到新文件中。文件夹名使用.git/objects的子文件夹的前两个字符,剩下的哈希字符串将作为BLOB的名称构成。
处理执行时的行为
分支
分支是 Git 的功能,可以将代码的变更履历分开。使用分支可以使多人在同一项目上工作时,每个人都可以独立地进行工作。
执行 git branch mybranch 命令时,会创建一个名为 mybranch 的新分支,并生成一个名为 .git/refs/heads/mybranch 的新文件。这个文件包含了新分支的源提交的哈希值。
### mybranch というブランチを作成
$ git branch mybranch
### ブランチの状態を確認
$ git branch
* master
mybranch
$ cat .git/refs/heads/mybranch
c262f5725e96f52c6bf1dbebcaf2680570fba434
$ cat .git/logs/refs/heads/mybranch
0000000000000000000000000000000000000000 c262f5725e96f52c6bf1dbebcaf2680570fba434 John Doe <johndoe@example.com> 1683662253 +0900 branch: Created from master
接下来,在mybranch上进行新的提交,如前所述,Git会创建根树和提交文件,并将分支文件更新为新提交的哈希值。
因此,分支是跟踪提交的文件,这些文件的内容会在每次进行提交时更新。
### ブランチを mybranch に切り替え
$ git checkout mybranch
### myfile2.txt ファイルを作成し、Git リポジトリに追加
$ echo "branch test" > myfile2.txt
$ git add myfile2.txt
### myfile2.txt をコミット
$ git commit -m "second commit"
[mybranch 92efc63] second commit
1 file changed, 1 insertion(+)
create mode 100644 myfile2.txt
$ cat .git/refs/heads/mybranch
92efc63e7150cc2924db956c2f540facfb46dfa2
$ cat .git/logs/refs/heads/mybranch
0000000000000000000000000000000000000000 c262f5725e96f52c6bf1dbebcaf2680570fba434 John Doe <johndoe@example.com> 1683662253 +0900 branch: Created from master
c262f5725e96f52c6bf1dbebcaf2680570fba434 92efc63e7150cc2924db956c2f540facfb46dfa2 John Doe <johndoe@example.com> 1683663583 +0900 commit: second commit
标签 (biaoqian)
标签是对特定提交的永久引用。
如果执行了 git tag mytag 命令并创建了名为 mytag 的新标签,Git 会在路径 .git/refs/tags 下生成一个名为 mytag 的新文件。
$ git tag mytag
与分支文件类似,这个文件中包含了创建标签所用的提交哈希。然而,与分支文件不同的是,标签文件不会随着工作的进行而更新,而是继续指向创建它的特定提交。
$ cat .git/refs/tags/mytag
92efc63e7150cc2924db956c2f540facfb46dfa2
首先
“HEAD” 是指向当前工作分支最新提交的指针。换句话说,它指示了正在工作的分支中的最新提交。
Git使用HEAD来识别当前正在操作的分支。比如,当运行git branch命令时,Git会查看HEAD来判断当前所在的分支。
此外,在参考即将进行的提交的父提交时也会使用HEAD。创建提交时,父提交将被记录在新的提交中。
通常情况下,当使用主分支(master branch)时,HEAD指向主分支。此时,HEAD文件中会写着ref: refs/heads/master。
如果我们切换到一个名为mybranch的分支,并打开.git文件夹中的HEAD文件,可以确认里面写着ref: refs/heads/mybranch。
因此,HEAD不直接指向提交(commit),而是充当指向分支最新提交的指针。这使得Git能够追踪当前检出的提交。
### master ブランチに切り替え、HEAD ファイルを確認
$ git checkout master
Switched to branch 'master'
$ cat .git/HEAD
ref: refs/heads/master
### mybranch ブランチに切り替え、HEAD ファイルを確認
$ git checkout mybranch
Switched to branch 'mybranch'
$ cat .git/HEAD
ref: refs/heads/mybranch
当在分支上执行提交时,Git会读取HEAD文件的内容,并将其作为参考来写入作为父提交的提交,因此可以说HEAD间接地提供了下一个提交的父提交。
$ cat .git/refs/heads/mybranch
92efc63e7150cc2924db956c2f540facfb46dfa2
$ git cat-file -p 92efc63e7150cc2924db956c2f540facfb46dfa2
tree 02f94dfeb44bdbc4e5253e100b701b388a1a59fe
parent c262f5725e96f52c6bf1dbebcaf2680570fba434
author John Doe <johndoe@example.com> 1683663583 +0900
committer John Doe <johndoe@example.com> 1683663583 +0900
second commit
通过这样做,您可以在Git中检出先前的提交并从那里开始更改。这种模式被称为“分离模式”。HEAD 不再引用分支,而是直接指向特定的提交。
在进行提交的检出、修正、评估和比较时,分离模式通常被使用。通常情况下,当HEAD指向某个分支时,创建新的提交会自动移动该分支,并将新的提交与该分支关联起来。然而,在分离模式下,需要注意的是,即使创建了新的提交,因为HEAD指向的提交不会改变,所以新的提交将不会与任何分支相关联。
合并
Git的合并是将在不同分支上进行的更改合并为一个过程。这样可以将代码更改整合起来并保持分支的历史记录。
在Git中,主要有两种类型的合并:No-ff合并和Fast-forward合并。
不使用快进式合并
当两个分支发生分岐时,会出现No-ff合并。在这种情况下,Git会创建一个具有两个父节点的新提交。第一个父节点是当前分支的最新提交,而第二个父节点是要合并的分支的最新提交。通过这个新提交,两个分支的更改都会被体现出来。
No-ff合并可能会发生冲突。当两个变更试图修改同一个文件或行时,就会发生冲突。在有冲突的情况下,Git需要手动解决冲突。
快进 合并
快速前进合并是指当有两个分支同时存在时,分支A没有超过分支B所导致的情况。也就是说,分支A在继续进行时。在这种情况下,Git会简单地将当前分支的最新提交移动到要合并的分支的最新提交。这样就将两个分支的历史记录合并在一起,并反映了更改。
引用资料
- Webサイト, “How Git truly works”, Alberto Prospero, https://towardsdatascience.com/how-git-truly-works-cd9c375966f6