git push 命令发送提交,而不是文件。然后每个提交包含 每个 文件(它包含;见下文)。因此,要回答您提出的问题——git push 是否上传了.gitignore 中列出的任何文件——您必须检查 commits 以查看它们是否包含这些文件.
.gitignore 这个名字本质上是错误的:它不会生成 Git ignore 文件。问题是,一个正确的名字会非常笨拙以至于无法使用:它可能类似于.git-do-not-auto-add-these-files-if-they-are-untracked-and-if-they-are-untracked-do-not-complain-about-them-being-untracked-when-I-run-git-status。它还有一个效果,虽然很少见,但也很重要:它允许 Git 在特定情况下clobber 这些文件。所以一个完全正确的名字会更糟。但这两个都是关于 untracked 文件的,是两个常见的。
那么:这些关于未跟踪文件的东西到底是什么?是什么让文件“未被跟踪”?要回答这个问题,我们必须从 Git 存在的原因说起,那就是提交。
Git 就是提交
Git 新手通常认为它与文件有关。它不是:它是关于提交的。提交确实包含文件,但 Git 通常一次处理整个提交。或者,如果他们不认为 Git 是关于文件的,那么初学者通常会认为 Git 是关于 分支,但事实并非如此:它仍然是关于提交的。分支——或者更准确地说,分支names——很重要,因为它们帮助我们(和 Git)find 提交。但真正重要的是提交。 如果您要使用 Git,则必须了解提交是什么以及为您做什么。
每次提交:
-
有编号。提交的“数量”很大、丑陋且看起来很随机。它实际上是用hexadecimal 表示的通用唯一ID,例如dcc0cd074f0c639a0df20461a301af6d45bd582e。该数字一旦分配给 that 提交,就意味着 that 提交,而不是任何其他提交。这就是两个不同的 Git 存储库如何决定它们是否具有相同的提交:它们要么有编号,在这种情况下,它们有 that 提交,或者一个有,一个没有,然后一个没有它需要从有它的人那里获得提交。
-
完全只读。所有 Git 内部对象都是这种方式——必须使编号方案起作用。
-
存储 Git 在您或任何人进行提交时“知道”的每个文件的完整存档。提交中的文件 以一种特殊的、只读的、仅 Git 的、压缩的和 去重复 的形式间接存储,因此两个不同的文件具有相同的内容,或具有相同内容的相同文件的两个提交,只存储文件一次。因此,虽然每个提交都有每个文件的完整存档,但所有提交也共享所有文件,因此存储库不会变得非常臃肿。
-
还存储一些元数据,或者关于提交本身的信息。这包括提交人的姓名和电子邮件地址。它包括一些日期和时间戳。它包括您想要放入的任何提交日志消息。而且,对于 Git 自己的内部操作而言至关重要的是,每个提交都存储了一个先前提交哈希 ID 的列表。这个列表通常只有一个条目长:这意味着 this 提交紧随其列出的 parent 提交之后。
正是这个父列表,将字符串一起提交向后,在存储库中形成了真正的分支信息。例如,假设我们有一串提交,结束,其中一个带有哈希值,我们称之为H(代表Hash)。提交H 存储一些较早提交的哈希ID。我们说H 指向这个较早的提交:
<-H
如果我们调用之前的提交 G 并将其绘制进去,我们有:
<-G <-H
当然G 也指向一个更早的提交,它再次指向后,依此类推:
... <-F <-G <-H
那是一个“分支”。为了找到这个分支,Git 需要知道提交 H 的实际哈希 ID,我们将其粘贴在 分支名称中,并说名称 指向到 H:
...--F--G--H <-- main
如果我们有多个分支名称,每个名称只指向一个提交:
I--J <-- feature-1
/
...--G--H <-- main
\
K--L <-- feature-2
现在有三个“最后”提交:H 和以前一样是main 上的最后 提交,但J 是feature-1 上的最后一个提交,L是feature-2 上的最后一次提交。请注意,通过H 的提交都在所有三个 分支上(这是 Git 特有的;大多数版本控制系统都不是这样工作的)。
如果提交中的文件是只读的和仅 Git 的,我们如何处理它们?
事实上,每个提交都是只读的,并且提交内的文件只能由Git自己读取。因此,在我们可以使用提交之前,我们必须让 Git 提取提交。我们使用git checkout 或git switch 执行此操作:我们选择一些提交,通过像H 这样的哈希ID 或像main 这样的名称,它为Git 提供哈希ID H,然后说提取该提交:
git switch main
Git 会从提交H 中读取所有文件,并将它们展开为普通的日常文件。 Git 将这些文件放在一个工作区中,Git 将其称为我们的 working tree 或 work-tree。
那么,这就回答了我们如何完成工作的问题:我们使用文件的工作树副本。 这些文件不在 Git 中! 它们是从 Git 中出来出来的——至少最初的文件是这样的——但它们目前不在 Git 中 点。
Git 的 index 或 暂存区
在我们处理工作树中的某个文件之后,我们可能希望 Git 使用该文件以及我们没有更改的所有其他文件来进行新的提交。在其他版本控制系统中,我们通常会运行它们的“make commit”动词:
hg commit
例如。他们会找出我们更改了哪些文件并进行新的提交。唉,Git 并没有让它变得这么容易。相反,Git 要求我们运行:
git add updated-file
git commit
第一个命令——git add 步骤——告诉 Git:读取工作树文件,将其压缩为您的内部 Gitty 格式并使用重复数据删除,并将其插入您的索引/暂存区域,准备好承诺。
这里的秘密——它不是真正的秘密,但并不总是正确地宣传或教导——Git 已经在其索引/暂存区域中拥有每个现有的签出文件。这个东西——Git 调用索引或暂存区域的东西,取决于 Git 文档的哪一部分正在执行这个调用——将你的提议的下一次提交保存在其中。
当您第一次切换到某个特定的提交时,Git 会从您的工作树中删除所有签出文件,这些文件是您正在使用的任何提交所记录的在它的索引中。它还会从其索引中删除所有签出的文件。然后它将所有与您要使用的新提交一起出现的文件安装到它的索引和您的工作树中。也就是说,如果我们这样做了:
git switch main
我们拥有来自H 的所有文件,但如果我们随后决定查看feature-1 并运行:
git switch feature-1
Git 删除来自H 的所有文件并替换来自J 的文件,这是名称feature-1 指向的提交。
关于这个删除和替换步骤有几件重要的事情需要了解:
-
首先,Git only 删除并替换它 拥有 的任何文件。 Git 已经在对文件进行重复数据删除,因此它知道H 中的哪些文件与J 中的文件相同。对于两个提交中相同的任何文件,它可以跳过 R(emove)-and-R(eplace) 作业。对于在H 但不在 在J 中的文件,它必须执行第一个R(即删除)而不进行替换,对于在J 但不是@987654371 的文件@,它只需要替换。
-
其次,Git 仅对它知道的文件进行 R-and-R。但是那些文件是什么?这就是索引的用武之地。
因为您的工作树是一个包含常规日常文件的常规目录(或文件夹,如果您更喜欢该术语),您可以创建 Git 不知道的文件。这些是您的未跟踪文件。这给了我们“未跟踪”文件的定义:未跟踪文件是指现在不在 Git 索引中的文件。这一点非常重要,所以让我们重复一遍。
未跟踪文件是指当前不在 Git 索引中的文件
这是进行新提交的关键,也是.gitignore 的关键。 现在在 Git 索引中的文件 被跟踪。 现在在 Git 的索引中不的文件是未跟踪的。 “现在”部分很重要,因为:
-
git switch和git checkout填写索引;但是
-
git add 读取一个工作树文件并将其复制到索引中;和
-
git rm 从工作树和索引中删除一个文件。
这为您提供了几种更改索引/暂存区域中的内容的方法。 (还有更多方法,例如git restore,但我们现在只介绍这三种。)使用git rm --cached,您可以从 Git 的索引中删除一个文件,而无需将其从工作树中删除,所以很容易使用git rm --cached 将文件从跟踪到未跟踪,或使用git add 从未跟踪到跟踪。
Git 从索引中的任何内容构建新的提交现在
当你运行时:
git commit
Git 对索引中当时中的所有文件进行快照,以它们当时在索引中的形式时间>。所以如果你:
git switch main
<modify one file - say, README.txt>
git add file
<modify the file again>
你目前有三个版本的README.txt:当前提交中有一个不可更改,Git 的索引中有第二个,你的工作中有第三个树。如果你运行git commit,那么第二个——Git 索引中的那个——会进入新的提交。
(请注意,git commit -a 大致相当于git add -u && git commit。也就是说,-a 选项仅对 已在 Git 中的所有文件进行 更新-添加index。所以真正的新文件需要单独的git add 步骤。)
这就是.gitignore 的用武之地
你可以显式运行:
git add some-file
或者,如果您愿意,您可以运行一个集体“在此处添加所有内容”:
git add .
这两个操作都告诉 Git 添加一个文件;第二个告诉 Git 从当前目录添加 all 文件。但是有些文件不应该放在新的提交中,强迫你 git add 单个文件而不是让你使用 git add . 来让 Git 解决问题会很痛苦。 p>
除此之外,git status 命令将运行两个差异:
-
第一个差异将HEAD(当前)提交与 Git 的索引进行比较。对于每个相同的文件,Git 什么都不说。对于每个不同的文件——包括新添加的或删除的——Git 会告诉你它,说staged for commit。这是因为索引中的任何内容将在下一次提交中出现,如果它不同,那就很有趣了。如果你有一个大项目,有数千个文件,你可能不在乎dull/1 到dull/999 都没有改变;你关心的是important.code 是 改变了。这个git status diff 会告诉你。
-
第二个差异将索引与您的工作树进行比较。对于每个相同的文件,Git 什么都不说。对于不同的文件,Git 会告诉你它们的相关信息,例如not staged for commit。但是 new 文件——不在 Git 的索引中,但 在 在你的工作树中的文件——被分离出来了。 Git 告诉你的不是“新”而是untracked。
为了防止 git status 对 10,000 个应该未跟踪的未跟踪文件产生无用的干扰,并通过不添加这 10,000 个文件使 git add . 有用,Git 将读取名称不佳的文件.gitignore。这些是 Git 将 (a) 关闭 和 (b) 不 添加的文件,即使您说 git add .。
这对 Git 索引中已经存在的文件没有影响。 如果文件 在 Git 的索引中,它会被跟踪并且它将在下一次提交中。在.gitignore 中列出一个文件并不会使其不被跟踪,也不会让 Git 忽略它:这只是意味着 如果 它 不被跟踪,Git 不会 抱怨它并且不会自动添加它。1
1事实上,如果你明确地尝试用git add listed-ignored-file添加它,Git 会告诉你它当前未被跟踪和在.gitignore 中列出,但是——如果你没有禁用提示——你可以用git add -f 覆盖它。有些人喜欢使用这个技巧来忽略“所有内容”(例如,.gitignore 中的 *),然后强制添加他们不想想要忽略的少数文件.我个人不喜欢这样做,但它确实有效。请注意,如果您使用这个技巧,git rm --cached -r . && git add . 会做坏事。
诊断文件是否被“忽略”
假设您有某个存储库,有人在某个时候说要忽略*.zorg 文件。也许这在当时是正确的做法,甚至。但是现在您确实需要提交部分或全部存储坏人的文件,所以您不希望*.zorg被忽略。
你运行ls,你会看到:
jean-baptiste.emanuel.zorg
你运行git status,它没有列出jean-baptiste.emanuel.zorg。
文件是否未更改,或者是否已更改但被忽略,并且现在不存在于 Git 的索引中?
你可以运行:
git add jean-baptiste.emanuel.zorg
如果它什么也没说,则文件被添加; git status 现在会告诉您它是否已更改。如果它说该文件被明确忽略但您可以使用-f 覆盖,您可以git add -f jean-baptiste.emanuel.zorg 将其添加。
你也可以运行:
git ls-files jean-baptiste.emanuel.zorg
如果显示 nothing,则忽略该文件。或者你可以运行:
git check-ignore jean-baptiste.emanuel.zorg
(考虑添加-v)。如果这显示 nothing,则该文件没有被忽略,可能是因为它已被强制添加。