为什么 git 在提取未跟踪两个文件的提交时删除一个被忽略的文件(但不是一个未被忽略的文件)?
Why does git remove an ignored file (but not an un-ignored file) when pulling a commit that untracked both files?
考虑以下情况:
- 远程存储库创建并跟踪两个文件
- 稍后远程存储库将两个文件之一添加到 .gitignore(我知道这是不正确的,但它发生在我们的组织中)
- 本地存储库克隆远程存储库
- 在本地存储库中,我们使用
git rm --cached
取消跟踪这两个文件
- 远程提取这些更改
我希望遥控器仍然有这两个文件,并从现在开始停止跟踪它们。
为什么会这样?
这是一个 bash 脚本 MWE,它复制了我的意思
#! /bin/bash
# 1. Set up remote repository
mkdir remote
cd remote
git init .
touch file_to_remain.txt
touch file_to_remove.txt
touch file_to_ignore_and_remove.txt
git add .
git commit -m 'first commit'
echo "file_to_ignore_and_remove.txt" > .gitignore
git add .
git commit -m 'gitignore ignores a file that is already in the index'
# 2. clone local repo
cd ../
git clone ./remote local
# 3. untrack both files
cd local
git rm --cached file_to_ignore_and_remove.txt
git rm --cached file_to_remove.txt
git add .
git commit -m 'removed two files from index'
# 4. pull changes into remote
cd ../remote
git remote add origin `pwd`/../local
git pull origin master
相反,发生的是:
- 这两个文件仍然存在于本地存储库中
- 在远程,被忽略的文件被删除,而未被忽略的文件仍然存在。
另一个发现:如果我在第 3 阶段(在 MWE 中)提交之前执行 git status
,file_to_remove.txt
显示为已删除和未跟踪,而 file_to_ignore_and_remove.txt
仅显示为已删除。当我执行 git add .
时,只记录 file_to_ignore_and_remove.txt
的删除。
您的问题从第 1 步开始,假设为:
- a remote repository creates and tracks two files
A 存储库 不跟踪文件(也不跟踪 not-track 文件)。 Git 存储库主要由一组 提交 组成。每个提交都包含 制作的 提交告诉 Git 包含 in 的所有文件的完整快照。
这意味着什么——在我们讨论跟踪与未跟踪的问题之前——我们可以有一个包含文件 f1
和 f2
的提交 a123456
,另一个提交b56789a
包含文件 f2
、f3
和 secret
,第三次提交 cbcdef0
包含文件 f3
和 f1
.
成功签出提交 a123456
后,您会发现您有名为 f1
和 f2
的文件,其中包含提交 [=11= 中的快照中的任何内容].成功检出提交 cbcdef0
后,您会发现您有文件 f1
和 f3
,其中包含提交 cbcdef0
中快照中的任何内容。提交 b56789a
中的内容并不重要,因为我们从未检查过它,即使 存储库 有该提交。我们从未注意到名为 secret
的文件,因为我们从未查看包含该文件的 commit。
Git 在 commit-by-commit 基础上工作。我们使用 git checkout
或 git switch
.1 选择一些要处理或处理的提交,因为 any 的所有部分都存在提交完全是 read-only,每个提交中的文件都以特殊的 Git-only、压缩和 de-duplicated 格式存储,other 程序在您的计算机无法使用,此“选择一些要处理的提交”步骤通过 将文件从提交 复制到工作区。提交中的文件不可见!它们对 Git 本身是私有的。您使用的文件 是 可见的,并且是普通文件,但它们只是从 Git 实际使用的文件中 复制而来(通过 Git 对象 存储,保存在 Git 内的数据库中。
这意味着当您使用 Git 时,您看到和使用的文件实际上并不 在 Git. 记住这一点对于接下来的内容很重要。同时,请记住 Git 的基本“存储单元”,可以说是提交。根据定义,每个提交包含每个 文件,Git 知道该提交的文件。但是这些文件在Git里,你问了就直接复制出来了。
1new-in-Git-2.23 git switch
实现了 git checkout
的“安全”子集,其中 new-in-Git- 2.23 git restore
实施“不安全”子集。如果您一直使用 Git 2.23 或更高版本,并想避免某些悲惨事故,最好训练自己使用这两个新命令。不过,旧的 git checkout
命令继续有效,所以如果您已经 self-trained 只使用 git checkout
,您可以继续这样做。
关于提交的更多信息
除了我们已经在上面描述或假设的事实之外——提交是由那些丑陋的大 random-looking 哈希 ID 编号,并且每个提交有每个提交 和 的文件的完整(但 read-only)快照,关于提交还有一件事需要了解:每个文件都包含一些元数据,其中,与提交的所有部分一样,完全 read-only:创建提交后无法更改它。
提交中的元数据包括提交人的姓名和电子邮件地址等内容。它包括 date-and-time-stamps(两个,出于各种原因)。它包括一条日志消息,其中提交提交的人应该解释 为什么 他们进行提交,尽管此解释的质量取决于提交作者。不过,对于 Git 本身而言,最重要的是,任何一次提交的元数据都将包含一个 较早提交 哈希 ID 的列表。这些是相关提交的 parents。
大多数提交只有一个父项。这些是普通的 (non-merge) 提交。它们 是 Git 存储库中的历史。添加一个新的提交,或许多新的提交,到一些 Git 存储库,同时保留现有的,是我们添加历史记录的方式。我们可以通过自己提交一次来一次提交一个提交,或者通过从其他 Git 存储库中获取许多提交来集体提交。使所有这些工作成功的秘诀与那些又大又丑的哈希 ID 有关。我们不会在这里适当地介绍它;我们只是说 every Git 使用 same 加密哈希函数来计算哈希 ID,因此所有 Gits 同意任何特定的提交都会获得其特定的哈希 ID。
无论如何,Git 安排每个 new 提交记住它的 前一个提交 的哈希 ID。这意味着普通 (non-merge) 提交形成一个简单的 backwards-looking 提交链:
... <-F <-G <-H
在这里,我们用一个大写字母替换了每个实际的哈希 ID,并在右侧绘制了最新的提交。我们将其哈希 ID 命名为 H
。 Git 可以通过哈希 ID 找到此提交,因为它存储在所有 Git 对象的数据库中,由哈希 ID 索引。这让 Git 可以获取存储的快照,也可以获取元数据。
不过请注意,提交 H
的元数据包括早期提交 G
的哈希 ID。所以 Git 可以找到提交 G
,这让 Git 获得 G
的存储快照。通过比较存储的快照——G
vs H
——Git可以告诉我们改变了两次提交。这就是事情变得有趣的地方。
当然,由于 G
是一个普通提交,只有一个父提交,G
的元数据中存储了 F
的哈希 ID。这意味着 Git 也可以从 G
返回到 F
,并比较两个快照。同时 F
存储在 its 元数据中,its 父哈希 ID,因此 Git 可以根据需要退回另一步。这一直重复到第一次提交——历史的开始——第一次提交根本没有没有父级,告诉Git:这是开始。例如,这就是 git log
将停止的地方,因为 运行 没有提交。
最终,这也是 分支名称 的工作方式。这里就不细说了,每个branch name只是持有one个hash ID。该哈希 ID 是我们希望在某些提交链中调用 last 提交的提交的 ID。即使在此之后有更多提交,那个特定标记的提交也是 this 链的结尾。正因为如此,Git 最终总是向后推:它从结尾开始,然后通过历史倒推到开头。
请记住,none 提交中的这些东西是可以更改的。 分支名称可以更改:例如,您可以根据需要重命名分支,但更重要的是,您可以将不同的哈希ID填充到一个分支名称。当您这样做时,作为链末端的提交已经改变。这就是分支增长或(如果需要)收缩的方式:通过将分支名称移动到我们添加的新提交,或者通过将分支名称向后移动到某个历史提交。
Git 的 index 和你的工作树
现在我们来讨论实际处理提交的问题。正如我们之前提到的,提交中的所有内容——所有文件的快照和元数据——都是 read-only。没有什么可以改变它,甚至 Git(因为使 Git 作为分布式系统工作的散列技巧)。但是要在提交中使用 in 文件,我们肯定需要能够读取它们——当它们在 object-ized 和 [=582] 中时我们不能=] 内部 Git 格式——我们几乎肯定也需要能够写入它们。
这就是为什么 Git 将提交的文件 复制到 之外。副本进入工作区域,Git 称为您的 工作树 或 work-tree。正如我们之前看到的,这些是您可以查看和使用的文件。它们实际上只是普通的日常文件。 Git不控制这个工作区! Git 确实——通常——最初是在 git clone
上实现的,但是 你 最初是在 git init
上实现的。这个工作区现在是你的,你可以随意处理。请记住,git checkout
是对 Git 的请求,用从某个提交中提取的文件填充您的工作区。
请注意,这意味着您的各种文件必须至少有 两个 个活动副本:
- current commit中有一个被冻结的Git取出并放入你的work-tree;和
- 您的 work-tree 中有一个您正在处理/正在处理的内容。
Git 可以 到此为止,工作区充满了文件,并且有提交。其他一些版本控制系统就是这样做的。但这不是 Git 所做的。相反,Git 在每个文件的冻结提交副本和您的 work-tree 副本之间挤压一个 third 副本。这意味着如果您在提交 a123456
中有这些文件 f1
和 f2
并且这是您当前的提交,那么 Git 将具有:
- 每个文件
f1
和 f2
在其提交中的冻结副本;
f1
和 f2
的另一个“副本”准备进入 下一个 提交;和
f1
和 f2
在您的 work-tree 中可用的副本。
这中间&qot;copy"—在这里用引号引起来,因为它是 Git 的内部格式,即 de-duplicates 文件,所以它最初只是从字面上共享原件commit——每个文件都位于 Git 给出三个名称的区域。Git 称其为 index,或 staging area,或者——现在很少见——缓存。姓氏主要出现在标志中,比如git rm --cached
。
每个文件的索引副本的特别之处在于Git会让你替换它。无法替换提交中的副本,因为无法更改任何现有提交。但是索引只是一个 提议的 提交。它实际上还不是一个提交。那么 Git 的索引 中的内容可以 更改。
这就是 git add
所做的。这也是 git rm --cached
所做的:它更改了 提议的下一次提交 。更改提议的提交不会影响任何现有的提交,所以没关系。 Git 通过执行以下三项操作之一实现此更改:
- 替换一些现有文件:用新版本覆盖索引
f1
;2
- 添加一个 new-to-the-proposed 提交文件:为我们在提议的提交中没有的文件创建一个新的索引条目;或
- 从建议的下一次提交中删除一个文件。
然后,所有这些更改都发生在 Git 的 index 中。这意味着建议的下一次提交始终是最新的,当您 运行 git add
到 make 该提交时,Git 只需快照Git 索引中的任何内容。
这导致了跟踪文件的定义。从技术上讲,Git 只是定义了术语 未跟踪的文件 ,但很明显如何反转它。
2由于旧文件版本内容是共享的,这实际上并没有覆盖它,而只是创建一个新的,或找到 de-duplicate 的其他一些现有副本。这背后的实际机制使用了 Git 用于提交的相同对象哈希 ID 技巧。提交总是得到一个 unique 哈希 ID,因为 about 每个提交总是保证是唯一的。 (这背后有很多魔法,这里实际上不需要日期,但是 Git 保证的一种方式是每个提交都“现在”作为 date-and-time 戳记。你有为了使这部分相同,每秒进行多次提交。)文件内容,如果它们复制了一些较早保存的版本,最终将使用旧版本的哈希 ID,因此自动 de-duplicated.
跟踪与未跟踪文件
一旦您知道 Git 如何使用其索引,作为提议的下一次提交,跟踪或未跟踪文件的定义变得几乎微不足道:跟踪文件是 Git的索引.
仅此而已,真的。 untracked 文件是您现在在工作树中的文件,它现在不在 Git 的索引中。将该文件 放入 Git 的索引中(例如 git add
),它就变成了 tracked 文件。将它从 Git 的索引中删除——例如,使用 git rm --cached
——它变成了一个 untracked 文件。您可以随时 运行 git add
或 git rm --cached
,因此您可以随时将文件从跟踪转换为未跟踪,反之亦然。
但是这里有一个大的、毛茸茸的皱纹。当您 运行 git checkout
选择对 使用 的提交时,Git 将:
- 从提交中填写Git的索引;然后
- 从 Git 的索引中填写您的 work-tree。
假设您正在提交 a123456
,其中包含文件 f1
和 f2
。您以正常的日常方式到达那里,现在您的工作树中有文件 f1
和 f2
。 Git 的索引中有 f1
和 f2
。 这两个文件的所有三个副本都匹配 ,因此从 a123456
移动到 cbcdef0
是非常安全的。因此,您 运行 git checkout
在标识提交 cbcdef0
的分支名称上,例如,将 切换到 该提交。
提交 cbcdef0
说我们应该有名为 f1
和 f3
的文件。 Git 的 index 当前有 f1
和 f2
在里面。要使 Git 的索引保留 f1
和 f3
,Git 必须从索引中 删除 f2
。因为 f2
是一个 tracked 文件——它在索引中——Git 也会 从中删除 f2
你的工作树。 Git 可以将 f1
和 f3
的正确副本放入其索引和您的工作树中,结帐完成并且文件 f2
消失了.
但它并没有真的消失,是吗?在提交 a123456
中,它是绝对安全的。只是 git checkout
那个提交和文件 f3
会消失——它现在存在并且被跟踪但不应该存在t 因为 a123456
缺少 文件 f3
—文件 f2
会回来,从 a123456
中提取,现在在两个 [=613] =]的索引和你的工作树。
请注意,如果您愿意,现在可以 运行 git rm --cached f3
,然后再切换到 a123456
。 从 Git 的索引中删除了 f3
。现在 f3
是一个 未跟踪的文件 。现在你可以 git checkout
提交 a123456
,并且 Git 不会 删除 f3
,因为 f3
不在Git的索引。事实上,您的工作树中有一个文件 f3
:嗯,那是 您的 业务。它不在 Git 的索引中,所以它是一个未跟踪的文件:您的一个文件,无论出于何种目的您都留在那里:它不是 Git 需要打扰的。
您的本地和“远程”存储库
但是现在,在您的示例中,您已经将另一个存储库和 work-tree 组合加入其中。您现在拥有 local 存储库,您在其中 运行ning git rm --cached
和 git add
并进行新提交。没关系!但是您在某个地方(在您自己的机器上或在其他机器上)也有另一个 Git 存储库。 Git 存储库有 自己的 索引和 自己的 工作树。
如果您正在进行一些提交,并使用 git rm --cached
删除了一些文件,它现在已从 Git 的索引中消失,但仍在 您的 中工作树。您进行了新的提交, 缺少 文件,一切仍然正常。
但是现在,在另一个 Git 存储库中,您做一些事情来获得这个新的提交。你仍然检查了旧的提交,并且有一些文件在 Git 的索引和你的工作树中,在这台机器上的另一个存储库中。现在你告诉Git:切换到其他提交,而另一个提交缺少文件。该文件在Git的索引这里——这个索引是这个存储库的一部分——所以文件是tracked 在这里,所以 Git 删除文件 ,就像它应该的那样。
如果您使用这种 git rm --cached
技巧从 Git 的索引中删除文件,您将在其他已签出提交的 Git 存储库中始终遇到此问题并在将文件保留在 your 工作树中的同时进行新的提交。那是因为 他们的 索引和 他们的 Git 从未被告知保留文件。他们看到的只是“旧提交有文件,新提交缺少文件”:这是一条 删除文件 的指令。它没有从 存储库 中消失,但它从 工作树 .
中消失了
关于.gitignore
.gitignore
文件命名错误。 Git 将从 Git 的索引中的任何内容构建一个新的提交。那是提议的下一次提交。如果文件在 in Git 的索引中,则它在 Git 的索引中,无论名称或模式是否在 .gitignore
文件。因此:
- the remote repository later on adds one of the two files to .gitignore (that's not correct, I know, but it happened in our organization)
不会*立即有害。以后可以;这不完全是错误,但我认为我自己是一个不好的做法。
.gitignore
文件有几个功能。最重要的是影响人们从事新工作的那些。
当你运行git status
时,你的Git:
- 打印一些通常有用的东西,比如
on branch xyzzy
;
- 可能会或可能不会打印有关 为提交准备的文件的内容;
- 可能会或可能不会打印有关 未暂存提交的文件; 和
- 可能会也可能不会打印有关未跟踪文件的内容
步骤 2 中打印的文件名列表是比较当前提交 与提议的下一次提交 的结果。对于每个相同的文件,Git 什么也没说。对于每个以任何方式不同的文件,Git 表示该文件是 staged for commit
,并带有一个 status-code 字母:M
表示已修改(文件同时存在于 HEAD
commit and index / staging-area), D
for deleted (file exists in HEAD
but not in index); A
表示已添加,依此类推。3
第 3 步中打印的名称列表是将提议的下一次提交 与您的工作树进行比较的结果。也就是说,Git 区分索引中的文件与 work-tree 中的文件。对于那些相同的,Git 什么也没说。对于那些不同的,Git 打印与以前大致相同,但现在称这些为 not staged for commit
。您可以 运行 git add
将文件的 work-tree 版本复制到索引中。
这里有点特别的是,Git 不是说某些文件是 A
dded,而是收集所有这些文件名,然后将它们洗牌到第 4 步。这些是你的 未跟踪的文件。 Git 现在抱怨他们,暗示你应该使用 git add
来做广告他们。
在许多设置中,会有许多未跟踪的文件,绝对不应该 git add
ed,因为它们不应该在下一次提交中。添加它们会将它们放入提议的 next 提交中,这意味着现在您必须将它们取出 (git rm --cached
),这样当您做它。我们想让 git status
就此 闭嘴 。
因此,我们在 .gitignore
中列出这些文件。这意味着也许这个文件应该命名为 .git-do-not-whine-about-these-untracked-files-in-git-status-output
.
而不是 .gitignore
但我们也有一个简单的方法 git add
-ing 所有 文件:我们 运行 git add .
或 git add *
或 git add -A
或其他。这告诉 Git 在 one swell foop 中添加 所有 个文件。但是有些文件我们不想添加:那些我们曾 git status
闭嘴的文件。所以该文件应该被称为 .git-do-not-whine-about-these-untracked-files-in-git-status-output-and-do-not-auto-add-them-when-I-use-an-en-masse-style-git-add-operation
,或类似的名称。
这些文件并不是字面上的 忽略 ,因为 Git 的索引 中的任何文件都将 下一次提交。所以 .gitignore
显然是错误的名字。但是正确的名称太长而且输入起来很荒谬:我们不妨称这个文件为 .gitignore
.
不过,.gitignore
还有另外一件事,那就是列出 跟踪的 文件——具体来说,任何将 成为 在签出一些提交时跟踪文件——可能很危险。 .gitignore
列表所做的是给予 Git 权限,在某些情况下,4 可以 覆盖或删除 文件,破坏您可能没有保存在任何地方的数据。这就是为什么我不喜欢你有一个文件被跟踪,但也匹配 .gitignore
模式的情况:它会让你稍后意外丢失数据。
(这意味着完整的正确名称可能真的是.git-do-not-whine-about-these-untracked-files-in-git-status-output-and-do-not-auto-add-them-when-I-use-an-en-masse-style-git-add-operation-but-do-feel-free-to-clobber-them
。那是......更荒谬。)
3当您处于冲突合并的中间时,所有这些都会改变。 git status
文档正在为 Git 的下一个版本进行改进,以帮助更好地理解状态代码字母。不过我不会在这里详细介绍。
4关于这个问题的很长 wide-ranging 的讨论,例如,参见 this thread from the Git mailing list archives。当您 git merge
一个包含配置文件的提交不在您的 current 提交中,但 是 并且 是 在 .gitignore
文件中列出。
考虑以下情况:
- 远程存储库创建并跟踪两个文件
- 稍后远程存储库将两个文件之一添加到 .gitignore(我知道这是不正确的,但它发生在我们的组织中)
- 本地存储库克隆远程存储库
- 在本地存储库中,我们使用
git rm --cached
取消跟踪这两个文件
- 远程提取这些更改
我希望遥控器仍然有这两个文件,并从现在开始停止跟踪它们。
为什么会这样?
这是一个 bash 脚本 MWE,它复制了我的意思
#! /bin/bash
# 1. Set up remote repository
mkdir remote
cd remote
git init .
touch file_to_remain.txt
touch file_to_remove.txt
touch file_to_ignore_and_remove.txt
git add .
git commit -m 'first commit'
echo "file_to_ignore_and_remove.txt" > .gitignore
git add .
git commit -m 'gitignore ignores a file that is already in the index'
# 2. clone local repo
cd ../
git clone ./remote local
# 3. untrack both files
cd local
git rm --cached file_to_ignore_and_remove.txt
git rm --cached file_to_remove.txt
git add .
git commit -m 'removed two files from index'
# 4. pull changes into remote
cd ../remote
git remote add origin `pwd`/../local
git pull origin master
相反,发生的是:
- 这两个文件仍然存在于本地存储库中
- 在远程,被忽略的文件被删除,而未被忽略的文件仍然存在。
另一个发现:如果我在第 3 阶段(在 MWE 中)提交之前执行 git status
,file_to_remove.txt
显示为已删除和未跟踪,而 file_to_ignore_and_remove.txt
仅显示为已删除。当我执行 git add .
时,只记录 file_to_ignore_and_remove.txt
的删除。
您的问题从第 1 步开始,假设为:
- a remote repository creates and tracks two files
A 存储库 不跟踪文件(也不跟踪 not-track 文件)。 Git 存储库主要由一组 提交 组成。每个提交都包含 制作的 提交告诉 Git 包含 in 的所有文件的完整快照。
这意味着什么——在我们讨论跟踪与未跟踪的问题之前——我们可以有一个包含文件 f1
和 f2
的提交 a123456
,另一个提交b56789a
包含文件 f2
、f3
和 secret
,第三次提交 cbcdef0
包含文件 f3
和 f1
.
成功签出提交 a123456
后,您会发现您有名为 f1
和 f2
的文件,其中包含提交 [=11= 中的快照中的任何内容].成功检出提交 cbcdef0
后,您会发现您有文件 f1
和 f3
,其中包含提交 cbcdef0
中快照中的任何内容。提交 b56789a
中的内容并不重要,因为我们从未检查过它,即使 存储库 有该提交。我们从未注意到名为 secret
的文件,因为我们从未查看包含该文件的 commit。
Git 在 commit-by-commit 基础上工作。我们使用 git checkout
或 git switch
.1 选择一些要处理或处理的提交,因为 any 的所有部分都存在提交完全是 read-only,每个提交中的文件都以特殊的 Git-only、压缩和 de-duplicated 格式存储,other 程序在您的计算机无法使用,此“选择一些要处理的提交”步骤通过 将文件从提交 复制到工作区。提交中的文件不可见!它们对 Git 本身是私有的。您使用的文件 是 可见的,并且是普通文件,但它们只是从 Git 实际使用的文件中 复制而来(通过 Git 对象 存储,保存在 Git 内的数据库中。
这意味着当您使用 Git 时,您看到和使用的文件实际上并不 在 Git. 记住这一点对于接下来的内容很重要。同时,请记住 Git 的基本“存储单元”,可以说是提交。根据定义,每个提交包含每个 文件,Git 知道该提交的文件。但是这些文件在Git里,你问了就直接复制出来了。
1new-in-Git-2.23 git switch
实现了 git checkout
的“安全”子集,其中 new-in-Git- 2.23 git restore
实施“不安全”子集。如果您一直使用 Git 2.23 或更高版本,并想避免某些悲惨事故,最好训练自己使用这两个新命令。不过,旧的 git checkout
命令继续有效,所以如果您已经 self-trained 只使用 git checkout
,您可以继续这样做。
关于提交的更多信息
除了我们已经在上面描述或假设的事实之外——提交是由那些丑陋的大 random-looking 哈希 ID 编号,并且每个提交有每个提交 和 的文件的完整(但 read-only)快照,关于提交还有一件事需要了解:每个文件都包含一些元数据,其中,与提交的所有部分一样,完全 read-only:创建提交后无法更改它。
提交中的元数据包括提交人的姓名和电子邮件地址等内容。它包括 date-and-time-stamps(两个,出于各种原因)。它包括一条日志消息,其中提交提交的人应该解释 为什么 他们进行提交,尽管此解释的质量取决于提交作者。不过,对于 Git 本身而言,最重要的是,任何一次提交的元数据都将包含一个 较早提交 哈希 ID 的列表。这些是相关提交的 parents。
大多数提交只有一个父项。这些是普通的 (non-merge) 提交。它们 是 Git 存储库中的历史。添加一个新的提交,或许多新的提交,到一些 Git 存储库,同时保留现有的,是我们添加历史记录的方式。我们可以通过自己提交一次来一次提交一个提交,或者通过从其他 Git 存储库中获取许多提交来集体提交。使所有这些工作成功的秘诀与那些又大又丑的哈希 ID 有关。我们不会在这里适当地介绍它;我们只是说 every Git 使用 same 加密哈希函数来计算哈希 ID,因此所有 Gits 同意任何特定的提交都会获得其特定的哈希 ID。
无论如何,Git 安排每个 new 提交记住它的 前一个提交 的哈希 ID。这意味着普通 (non-merge) 提交形成一个简单的 backwards-looking 提交链:
... <-F <-G <-H
在这里,我们用一个大写字母替换了每个实际的哈希 ID,并在右侧绘制了最新的提交。我们将其哈希 ID 命名为 H
。 Git 可以通过哈希 ID 找到此提交,因为它存储在所有 Git 对象的数据库中,由哈希 ID 索引。这让 Git 可以获取存储的快照,也可以获取元数据。
不过请注意,提交 H
的元数据包括早期提交 G
的哈希 ID。所以 Git 可以找到提交 G
,这让 Git 获得 G
的存储快照。通过比较存储的快照——G
vs H
——Git可以告诉我们改变了两次提交。这就是事情变得有趣的地方。
当然,由于 G
是一个普通提交,只有一个父提交,G
的元数据中存储了 F
的哈希 ID。这意味着 Git 也可以从 G
返回到 F
,并比较两个快照。同时 F
存储在 its 元数据中,its 父哈希 ID,因此 Git 可以根据需要退回另一步。这一直重复到第一次提交——历史的开始——第一次提交根本没有没有父级,告诉Git:这是开始。例如,这就是 git log
将停止的地方,因为 运行 没有提交。
最终,这也是 分支名称 的工作方式。这里就不细说了,每个branch name只是持有one个hash ID。该哈希 ID 是我们希望在某些提交链中调用 last 提交的提交的 ID。即使在此之后有更多提交,那个特定标记的提交也是 this 链的结尾。正因为如此,Git 最终总是向后推:它从结尾开始,然后通过历史倒推到开头。
请记住,none 提交中的这些东西是可以更改的。 分支名称可以更改:例如,您可以根据需要重命名分支,但更重要的是,您可以将不同的哈希ID填充到一个分支名称。当您这样做时,作为链末端的提交已经改变。这就是分支增长或(如果需要)收缩的方式:通过将分支名称移动到我们添加的新提交,或者通过将分支名称向后移动到某个历史提交。
Git 的 index 和你的工作树
现在我们来讨论实际处理提交的问题。正如我们之前提到的,提交中的所有内容——所有文件的快照和元数据——都是 read-only。没有什么可以改变它,甚至 Git(因为使 Git 作为分布式系统工作的散列技巧)。但是要在提交中使用 in 文件,我们肯定需要能够读取它们——当它们在 object-ized 和 [=582] 中时我们不能=] 内部 Git 格式——我们几乎肯定也需要能够写入它们。
这就是为什么 Git 将提交的文件 复制到 之外。副本进入工作区域,Git 称为您的 工作树 或 work-tree。正如我们之前看到的,这些是您可以查看和使用的文件。它们实际上只是普通的日常文件。 Git不控制这个工作区! Git 确实——通常——最初是在 git clone
上实现的,但是 你 最初是在 git init
上实现的。这个工作区现在是你的,你可以随意处理。请记住,git checkout
是对 Git 的请求,用从某个提交中提取的文件填充您的工作区。
请注意,这意味着您的各种文件必须至少有 两个 个活动副本:
- current commit中有一个被冻结的Git取出并放入你的work-tree;和
- 您的 work-tree 中有一个您正在处理/正在处理的内容。
Git 可以 到此为止,工作区充满了文件,并且有提交。其他一些版本控制系统就是这样做的。但这不是 Git 所做的。相反,Git 在每个文件的冻结提交副本和您的 work-tree 副本之间挤压一个 third 副本。这意味着如果您在提交 a123456
中有这些文件 f1
和 f2
并且这是您当前的提交,那么 Git 将具有:
- 每个文件
f1
和f2
在其提交中的冻结副本; f1
和f2
的另一个“副本”准备进入 下一个 提交;和f1
和f2
在您的 work-tree 中可用的副本。
这中间&qot;copy"—在这里用引号引起来,因为它是 Git 的内部格式,即 de-duplicates 文件,所以它最初只是从字面上共享原件commit——每个文件都位于 Git 给出三个名称的区域。Git 称其为 index,或 staging area,或者——现在很少见——缓存。姓氏主要出现在标志中,比如git rm --cached
。
每个文件的索引副本的特别之处在于Git会让你替换它。无法替换提交中的副本,因为无法更改任何现有提交。但是索引只是一个 提议的 提交。它实际上还不是一个提交。那么 Git 的索引 中的内容可以 更改。
这就是 git add
所做的。这也是 git rm --cached
所做的:它更改了 提议的下一次提交 。更改提议的提交不会影响任何现有的提交,所以没关系。 Git 通过执行以下三项操作之一实现此更改:
- 替换一些现有文件:用新版本覆盖索引
f1
;2 - 添加一个 new-to-the-proposed 提交文件:为我们在提议的提交中没有的文件创建一个新的索引条目;或
- 从建议的下一次提交中删除一个文件。
然后,所有这些更改都发生在 Git 的 index 中。这意味着建议的下一次提交始终是最新的,当您 运行 git add
到 make 该提交时,Git 只需快照Git 索引中的任何内容。
这导致了跟踪文件的定义。从技术上讲,Git 只是定义了术语 未跟踪的文件 ,但很明显如何反转它。
2由于旧文件版本内容是共享的,这实际上并没有覆盖它,而只是创建一个新的,或找到 de-duplicate 的其他一些现有副本。这背后的实际机制使用了 Git 用于提交的相同对象哈希 ID 技巧。提交总是得到一个 unique 哈希 ID,因为 about 每个提交总是保证是唯一的。 (这背后有很多魔法,这里实际上不需要日期,但是 Git 保证的一种方式是每个提交都“现在”作为 date-and-time 戳记。你有为了使这部分相同,每秒进行多次提交。)文件内容,如果它们复制了一些较早保存的版本,最终将使用旧版本的哈希 ID,因此自动 de-duplicated.
跟踪与未跟踪文件
一旦您知道 Git 如何使用其索引,作为提议的下一次提交,跟踪或未跟踪文件的定义变得几乎微不足道:跟踪文件是 Git的索引.
仅此而已,真的。 untracked 文件是您现在在工作树中的文件,它现在不在 Git 的索引中。将该文件 放入 Git 的索引中(例如 git add
),它就变成了 tracked 文件。将它从 Git 的索引中删除——例如,使用 git rm --cached
——它变成了一个 untracked 文件。您可以随时 运行 git add
或 git rm --cached
,因此您可以随时将文件从跟踪转换为未跟踪,反之亦然。
但是这里有一个大的、毛茸茸的皱纹。当您 运行 git checkout
选择对 使用 的提交时,Git 将:
- 从提交中填写Git的索引;然后
- 从 Git 的索引中填写您的 work-tree。
假设您正在提交 a123456
,其中包含文件 f1
和 f2
。您以正常的日常方式到达那里,现在您的工作树中有文件 f1
和 f2
。 Git 的索引中有 f1
和 f2
。 这两个文件的所有三个副本都匹配 ,因此从 a123456
移动到 cbcdef0
是非常安全的。因此,您 运行 git checkout
在标识提交 cbcdef0
的分支名称上,例如,将 切换到 该提交。
提交 cbcdef0
说我们应该有名为 f1
和 f3
的文件。 Git 的 index 当前有 f1
和 f2
在里面。要使 Git 的索引保留 f1
和 f3
,Git 必须从索引中 删除 f2
。因为 f2
是一个 tracked 文件——它在索引中——Git 也会 从中删除 f2
你的工作树。 Git 可以将 f1
和 f3
的正确副本放入其索引和您的工作树中,结帐完成并且文件 f2
消失了.
但它并没有真的消失,是吗?在提交 a123456
中,它是绝对安全的。只是 git checkout
那个提交和文件 f3
会消失——它现在存在并且被跟踪但不应该存在t 因为 a123456
缺少 文件 f3
—文件 f2
会回来,从 a123456
中提取,现在在两个 [=613] =]的索引和你的工作树。
请注意,如果您愿意,现在可以 运行 git rm --cached f3
,然后再切换到 a123456
。 从 Git 的索引中删除了 f3
。现在 f3
是一个 未跟踪的文件 。现在你可以 git checkout
提交 a123456
,并且 Git 不会 删除 f3
,因为 f3
不在Git的索引。事实上,您的工作树中有一个文件 f3
:嗯,那是 您的 业务。它不在 Git 的索引中,所以它是一个未跟踪的文件:您的一个文件,无论出于何种目的您都留在那里:它不是 Git 需要打扰的。
您的本地和“远程”存储库
但是现在,在您的示例中,您已经将另一个存储库和 work-tree 组合加入其中。您现在拥有 local 存储库,您在其中 运行ning git rm --cached
和 git add
并进行新提交。没关系!但是您在某个地方(在您自己的机器上或在其他机器上)也有另一个 Git 存储库。 Git 存储库有 自己的 索引和 自己的 工作树。
如果您正在进行一些提交,并使用 git rm --cached
删除了一些文件,它现在已从 Git 的索引中消失,但仍在 您的 中工作树。您进行了新的提交, 缺少 文件,一切仍然正常。
但是现在,在另一个 Git 存储库中,您做一些事情来获得这个新的提交。你仍然检查了旧的提交,并且有一些文件在 Git 的索引和你的工作树中,在这台机器上的另一个存储库中。现在你告诉Git:切换到其他提交,而另一个提交缺少文件。该文件在Git的索引这里——这个索引是这个存储库的一部分——所以文件是tracked 在这里,所以 Git 删除文件 ,就像它应该的那样。
如果您使用这种 git rm --cached
技巧从 Git 的索引中删除文件,您将在其他已签出提交的 Git 存储库中始终遇到此问题并在将文件保留在 your 工作树中的同时进行新的提交。那是因为 他们的 索引和 他们的 Git 从未被告知保留文件。他们看到的只是“旧提交有文件,新提交缺少文件”:这是一条 删除文件 的指令。它没有从 存储库 中消失,但它从 工作树 .
关于.gitignore
.gitignore
文件命名错误。 Git 将从 Git 的索引中的任何内容构建一个新的提交。那是提议的下一次提交。如果文件在 in Git 的索引中,则它在 Git 的索引中,无论名称或模式是否在 .gitignore
文件。因此:
- the remote repository later on adds one of the two files to .gitignore (that's not correct, I know, but it happened in our organization)
不会*立即有害。以后可以;这不完全是错误,但我认为我自己是一个不好的做法。
.gitignore
文件有几个功能。最重要的是影响人们从事新工作的那些。
当你运行git status
时,你的Git:
- 打印一些通常有用的东西,比如
on branch xyzzy
; - 可能会或可能不会打印有关 为提交准备的文件的内容;
- 可能会或可能不会打印有关 未暂存提交的文件; 和
- 可能会也可能不会打印有关未跟踪文件的内容
步骤 2 中打印的文件名列表是比较当前提交 与提议的下一次提交 的结果。对于每个相同的文件,Git 什么也没说。对于每个以任何方式不同的文件,Git 表示该文件是 staged for commit
,并带有一个 status-code 字母:M
表示已修改(文件同时存在于 HEAD
commit and index / staging-area), D
for deleted (file exists in HEAD
but not in index); A
表示已添加,依此类推。3
第 3 步中打印的名称列表是将提议的下一次提交 与您的工作树进行比较的结果。也就是说,Git 区分索引中的文件与 work-tree 中的文件。对于那些相同的,Git 什么也没说。对于那些不同的,Git 打印与以前大致相同,但现在称这些为 not staged for commit
。您可以 运行 git add
将文件的 work-tree 版本复制到索引中。
这里有点特别的是,Git 不是说某些文件是 A
dded,而是收集所有这些文件名,然后将它们洗牌到第 4 步。这些是你的 未跟踪的文件。 Git 现在抱怨他们,暗示你应该使用 git add
来做广告他们。
在许多设置中,会有许多未跟踪的文件,绝对不应该 git add
ed,因为它们不应该在下一次提交中。添加它们会将它们放入提议的 next 提交中,这意味着现在您必须将它们取出 (git rm --cached
),这样当您做它。我们想让 git status
就此 闭嘴 。
因此,我们在 .gitignore
中列出这些文件。这意味着也许这个文件应该命名为 .git-do-not-whine-about-these-untracked-files-in-git-status-output
.
.gitignore
但我们也有一个简单的方法 git add
-ing 所有 文件:我们 运行 git add .
或 git add *
或 git add -A
或其他。这告诉 Git 在 one swell foop 中添加 所有 个文件。但是有些文件我们不想添加:那些我们曾 git status
闭嘴的文件。所以该文件应该被称为 .git-do-not-whine-about-these-untracked-files-in-git-status-output-and-do-not-auto-add-them-when-I-use-an-en-masse-style-git-add-operation
,或类似的名称。
这些文件并不是字面上的 忽略 ,因为 Git 的索引 中的任何文件都将 下一次提交。所以 .gitignore
显然是错误的名字。但是正确的名称太长而且输入起来很荒谬:我们不妨称这个文件为 .gitignore
.
不过,.gitignore
还有另外一件事,那就是列出 跟踪的 文件——具体来说,任何将 成为 在签出一些提交时跟踪文件——可能很危险。 .gitignore
列表所做的是给予 Git 权限,在某些情况下,4 可以 覆盖或删除 文件,破坏您可能没有保存在任何地方的数据。这就是为什么我不喜欢你有一个文件被跟踪,但也匹配 .gitignore
模式的情况:它会让你稍后意外丢失数据。
(这意味着完整的正确名称可能真的是.git-do-not-whine-about-these-untracked-files-in-git-status-output-and-do-not-auto-add-them-when-I-use-an-en-masse-style-git-add-operation-but-do-feel-free-to-clobber-them
。那是......更荒谬。)
3当您处于冲突合并的中间时,所有这些都会改变。 git status
文档正在为 Git 的下一个版本进行改进,以帮助更好地理解状态代码字母。不过我不会在这里详细介绍。
4关于这个问题的很长 wide-ranging 的讨论,例如,参见 this thread from the Git mailing list archives。当您 git merge
一个包含配置文件的提交不在您的 current 提交中,但 是 并且 是 在 .gitignore
文件中列出。