假设我有两个分支:master和dev。第一个包含名为" 1.txt"的文件。内容
"Hello, world"
第二个包含文件" 1.txt"内容
"Goodbye, world!!"
git将在何处以及如何存储文件的不同副本?我的意思是,在.git文件夹中的确切位置?
答案 0 :(得分:10)
Git并不完全存储文件。 Git存储的是对象。
分支机构也不包含文件。分支名称,如master
或dev
,存储提交哈希ID。
理解这一点的关键有点循环:当你理解它时,你才真正理解它。 :-)但是要开始,请考虑将Git存储为提交对象,并以提交的概念为中心。
提交是这些Git对象之一。有四种对象:提交,树, blobs 和标记。树和blob用于构建提交。标记对象用于带注释的标记,但不要担心这些标记。
所以Git就是存储提交,并提交给你的文件(通过那些树和blob对象)。但提交不是文件本身:它更像是一个包装器。提交的内容是:您的姓名(作为作者),您的电子邮件地址以及您提交的时间;提交的父提交的哈希ID;你的提交日志消息;以及记住哪些文件进入提交的树对象的哈希ID。
所以你可能会认为树对象包含你的文件 - 但它也没有!相反,树对象包含文件的名称,以及blob对象的哈希ID。它是保存文件的blob对象。
提交的名称或任何其他Git对象被写为40个字符的哈希ID,如d35688db19c9ea97e9e2ce751dc7b47aee21636b
。您可能已经在git log
输出中看到了它们,或者在运行其他Git命令时显示的缩短版本。
这些哈希ID对于人类来说无法以任何实际的方式使用,因此Git提供了一种将简短有意义的名称转换为大丑陋哈希ID的方法。这些名称有多种形式,但您使用的第一个名称是分支名称。
这意味着如果您有两个分支名称,master
和dev
,这些实际上会存储哈希ID。
Git使用哈希ID来查找提交对象。然后,每个提交对象都存储树ID。 Git使用它来查找树对象。树对象包含(以及其他内容)名称,例如1.txt
与blob哈希ID配对。 Git使用blob哈希ID来查找blob对象,blob对象存储文件的完整内容。
git将在何处以及如何存储文件的不同副本?我的意思是,在.git文件夹中的确切位置?
当你运行git add 1.txt
然后提交它时,Git会生成一个blob来保存1.txt
中的任何内容。新blob有一些哈希ID。我们说它以1234567...
开头。 Git以压缩形式将实际内容存储在.git/objects/12/34567...
中,以及一些将对象类型标识为blob的前置位。
如果您再次更改1.txt
和git add
以及git commit
,则会获得一个新的带有新ID的blob。我们说它以fedcba9...
开头。该对象进入.git/objects/fe/dcba9...
。
为了存储这些blob,当然,Git也必须编写 tree 对象和 commit 对象。如果你在分支dev
上,当Git写出新的提交时,Git将更改名称dev
以存储新的提交哈希ID。
为了在所有这些之前在dev
上找到 的提交,Git将新提交与先前的dev
提示提交ID一起写为父的
假设代替大丑陋的哈希ID,我们给每个提交一个字母,从A
开始计数。这绘制起来要容易得多,但当然我们在26次提交后会用完字母。 : - )
让我们从只有一次提交的存储库开始:
A <-- master
分支名称master
存储A
,以便我们知道提交名为A
。
这不是很有趣,所以让我们进行新的提交B
:
A <-B <-- master
现在名称master
存储了字母B
。提交本身B
对象在其中包含提交A
的ID。
要在master
上进行另一次新提交,我们为其分配一个新的哈希C
,使用相应的日志消息和树写一个提交对象,依此类推,并使C
& #39; s 父级为B
:
A <-B <-C
然后我们将C
写入master
:
A <-B <-C <-- master
这意味着分支名称,如master
,只需指向分支的提示。从某种意义上说,分支本身就是提交链从最新开始并向后工作。
请注意,Git的内部箭头都指向后方。 Git始终向后运行所有内容,从最新版开始。
我们可以通过创建一个新的分支dev
来使这更有趣。最初,dev
指向与master
相同的提交:
A--B--C <-- dev (HEAD), master
我们添加了这个有趣的符号(HEAD)
,以便记住我们正在使用的分支名称。
现在让我们像往常一样进行新的提交。新提交一如既往地获取其作者和日志消息,并将当前提交的哈希ID(C
)存储为其父级,但现在我们必须更新分支名称指向D
。我们应该更新哪个分支名称? HEAD
进来的地方:它告诉我们要更新哪一个!
A--B--C <-- master
\
D <-- dev (HEAD)
现在dev
标识提交D
,而master
仍标识C
。
这是理解Git的第一个主要秘密。 Git不存储文件,它存储提交。提交形成链。这些链是 Git存储库中的历史记录。
Git使用分支名称来记住最新或提示提交。这些提交提交让我们找到旧的提交。如果我们向E
添加新的提交master
,我们会得到:
A--B--C--E <-- master
\
D <-- dev
我们现在可以直观地看到master
和dev
在提交C
时加入。
运行git checkout <branch-name>
告诉Git在分支的尖端提取提交,使用提交查找树来查找blob以获取所有文件。然后,作为分支名称的git checkout
的最后一步,Git将HEAD
附加到该分支名称,以便在我们添加 new 提交时它知道要更新的分支名称
答案 1 :(得分:4)
Torek有一个excellent answer,我不会尝试复制......但如果它仍然让你感到困惑,那么让我试着用Javascript来演示它是如何工作的。我将简化一些事情,所以这不是JS中Git的精确实现,但它足以理解一些基本原理。
文件由两个不同的部分组成:文件的实际内容;和该文件的元数据(它的名称和模式)。让我们定义内容并存储它们,以便我们以后可以引用它们:
allTheThings['06f19763'] = "blob " + "Hello, world";
这里的变量名称是值的SHA1哈希值。这是一个非常重要的概念... git中的所有东西都是SHA1哈希值。你可以使用你想要的任何SHA1工具自己生成这些哈希值(我使用了online tool)。
为简洁起见,我将哈希值截断为前8个字符。在git中工作时,只要git仍然能够唯一地标识一个对象,就可以截断你想要的数量。通常8个字符就足够了(两个具有相同前8个提交的对象的几率非常非常小),这就是大多数示例中甚至大部分文档中都会看到的内容。
很酷......所以现在我们已经有了内容。但我们现在想要文件的另一半...它的名字。为此,我们需要创建一个基本上复制文件夹/目录的树对象。
allTheThings['5e91b67a'] = "tree " + "100644 blob 06f19763 file1.txt";
此树对象表示06f19763
(或“Hello,world”)引用的文件内容名为file1.txt
且可读/写(100644
基于Unix模式 - 这意味着file1.txt是普通文件。)
除了文件,树还可以包含其他树,这就是我们如何创建任意深度的目录。
每个提交都包含对树的引用,表示repo的根目录。在我们的示例中,file1.txt位于根目录中,是repo中唯一的文件。所以让我们创建一个提交:
allTheThings['a9d13be8'] =
"commit\n" +
"tree 5e91b67a\n" +
"author JD <email> 1508777071\n" +
"committer JD <email> 1508777071\n" +
"\n" +
"Commit message";
提交指向我们的树,并包含一些其他信息,如提交作者和提交消息。
分支几乎只是提交的方便名称。更新分支时,您只需创建一个新提交,然后重置分支以指向它。
到目前为止我们创建的所有内容都存储在allTheThings
对象中,因此它们都存储在一起。我们可以根据前缀(“blob”,“tree”和“commit”)判断一切是什么。每个条目都是从内容的哈希中键入的,几乎保证是唯一的。每当我们更改文件的内容,文件名,提交消息等时,我们都会更改哈希值,但原始对象仍然存在,并且仍然可以被其他对象(树,提交等)引用。
例如,如果我们更新文件,我们最终会在整个链中使用新的哈希值:
allTheThings['3e103e35'] = "blob " + "Goodbye, world!!";
allTheThings['05abc8ab'] = "tree " + "100644 blob 3e103e35 file1.txt";
allTheThings['a5944bfa'] =
"commit\n" +
"tree 05abc8ab\n" +
"author JD <email> 1508777071\n" +
"committer JD <email> 1508777071\n" +
"\n" +
"Commit message";
请注意,即使文件名和提交消息/作者/等没有改变,对file1内容的更改也会导致链接反应直到提交:
06f19763 => 3e103e35 (the contents changed...)
5e91b67a => 05abc8ab (so the content reference in the tree changed)
a9d13be8 => a5944bfa (so the tree reference in the commit changed )
我们的allTheThings
对象中存在所有六个对象,幸福地生活在彼此旁边:
allTheThings = {
06f19763: "blob Hello, world",
3e103e35: "blob Goodbye, world!!",
5e91b67a: "tree 100644 blob 06f19763 file1.txt",
05abc8ab: "tree 100644 blob 3e103e35 file1.txt",
a9d13be8: "commit\ntree 5e91b67a\nauthor JD <email> 1508777071\ncommitter JD <email> 1508777071\n\nCommit message",
a5944bfa: "commit\ntree 05abc8ab\nauthor JD <email> 1508777071\ncommitter JD <email> 1508777071\n\nCommit message",
}
最后,您的master
分支指向a9d13be8
,而您的dev
分支指向a5944bfa
。
在真正的git中,这些对象作为单独的(压缩的)文件存储在.git
目录中(.git/objects/12/34567...
作为Torek said),但它是相同的概念。
因为git repo可以包含这么多对象,所以哈希的前两个字符用于将文件细分为目录,以确保目录中的maximum file count不是超过(特别是在旧系统上)。很容易认为这些前缀比这更具意义,比如对象类型,但它们没有。
这就是它。文件,树,提交和其他一些东西都被认为是Git Objects,并且在对象目录中被集中在一起。您可以使用plumbing commands直接使用这些对象并提取它们以供使用,但使用许多porcelain commands间接使用它们几乎总是容易得多。
答案 2 :(得分:0)
分支是一个文本文件,其中包含提交的哈希。
它是 Git 引用的一部分——一组引用提交的对象。
Git 将所有引用存储在 .git/refs 文件夹下,分支存储在 .git/refs/heads 目录中。
由于 branch 是一个简单的文本文件,我们可以使用提交哈希的内容创建一个文件。