Git - 推送一个目录然后忽略它

Git - Push a directory and then ignore it

我有一个文件夹,其中包含我在 PhpStorm 上执行的所有 http 请求文件。 我首先将此文件夹添加到我的 gitignore 文件中:

/app/http/

然后强制推送到我的远程仓库:

git add -f app/http
git commit -m 'http folder added'
git push

但在那之后,我的文件夹没有被忽略。 推送后如何忽略它?

TL;DR

你得不到你想要的。不要那样做。如果可以,请考虑存储一个 dot-file,例如 .gitignore 列出 *。如果没有,请考虑让 不是 Git 的程序在需要时自动创建空文件夹。

您开始时有许多不正确的假设:

  • 您认为您添加了一个文件夹,但 Git 提交仅存储 文件 ,而不存储文件夹。稍后我们将了解这意味着什么,以及为什么 Git 是这样的。
  • 您认为您将文件推送到文件夹 and/or。 Git 不推送文件。 Git 推送 提交.
  • 您认为在 .gitignore 中列出某些内容会使 Git 忽略它。事实并非如此。

这些都导致了您遇到的问题。

您需要从这个开始:Git 的基本存储单元——无论如何您将与之交互的单元——是 提交。 Git 完全是关于 提交 ,因此您需要详细了解提交的具体内容和作用。

提交

Git 提交由两部分组成:数据和元数据:

  • 每次提交中的数据由文件的完整快照组成。这里没有文件夹,只有名称可能包含嵌入斜杠的文件,例如 path/to/file.ext.

  • 每次提交中的元数据包含诸如您的姓名和电子邮件地址、提交时的 date-and-time 戳记以及解释 原因 你做出了承诺。包含在这个元数据中的是 Git 使用的一些元数据=609=]:父提交哈希 ID 的列表。通常这个列表中只有一个commit hash ID。

每个提交都有一个唯一的哈希 ID。这个哈希 ID 实际上是提交的真实名称。这就是 Git 检索数据(您的文件)和元数据的方式。因此,您需要使用提交哈希,以便 Git 可以 找到 提交。但是这里有一个问题:提交哈希 ID 又大又丑,人类无法使用。

分支名称

这个问题的解决方案很明显:我们有一台计算机;我们可以让计算机为我们存储哈希ID,使用一个简单的名称,例如masterdevelop,人类可以处理,以记住正确的哈希 ID。这就是分支名称:一个分支名称包含一个 - 并且 一个 - 提交的哈希 ID。根据定义,我们在分支名称中 Git 记住的提交是分支中的 newestlatest 提交。 Git 称之为 提示提交

如果我们说 git checkout master,Git 将使用存储在名称 master 中的哈希 ID 来检查 master 的 tip 提交。如果我们添加一个 new 提交 分支,Git 将写出新提交,其(单个)parent hash ID 设置为旧的分支提示。新提交获得一个新的、唯一的哈希 ID,Git 写入分支名称,现在 new 提交是分支提示。

之前的commit还存在,Git可以自己找到:Git使用分支名查找last[=414=的hash ID ] 提交,然后读取该提交并使用其元数据来查找父项的哈希 ID。 Git 然后使用该哈希 ID 读取父提交。如果合适,Git 使用 that commit 的存储父级再返回一步。

画出你的提交和分支

在视觉上,如果我们画这个,我们得到:

... <-F <-G <-H   <--branch

其中 branch 是分支名称,它存储一些哈希 ID,我们将在这里调用 H。这允许 Git 到 find HH 本身存储,作为其父散列 ID,我们将调用 G 的提交的 ID。所以 H 指向 G,允许 Git 从它的数据库中检索 GG 依次指向较早的提交 F,依此类推。整个过程有效,因为 分支名称 指向 last 提交,从那里,Git 可以向后工作。

你的work-tree

任何现有提交中的任何内容都不能更改!一旦完成,每次提交都会被永久冻结。这包括存储在该提交中的所有文件。因为提交是冻结的——read-only——它们非常适合归档。但是提交中的文件以特殊的、冻结的、压缩的、read-only 和 Git-only 格式存储,只有 Git 可以读取。这些文件实际上是无法更改的,就像 Git 提交永远无法更改一样。这使得这些文件对于获取任何 工作完成。

这个问题的解决方案是 Git 提取 冻结的提交文件,从提交到工作区,在那里文件被转回您的计算机可以使用的普通文件。这就是 git checkout——或者在 Git 2.23 及更高版本中,git switch——所做的:你告诉它你想使用哪个分支,Git 使用分支 name 以找到正确的 哈希 ID,Git 将所有冻结的文件从该提交中提取到您的 work-treeworking tree,这是您可以看到它们并使用它们的地方。1


1这掩盖了很多关于git checkout的细节,所以它只是一个概述,而不是一个精确的定义。


索引

因此,正如您到目前为止从这张图片中看到的那样,当您处理提交时,实际上有 两组 文件处于活动状态:提交的副本,它们一直被冻结(并且根本不可见),以及您可以看到和处理的工作副本。一些版本控制系统到此为止,每个文件只有两个副本。 Git,然而,没有。

Git 有不同的名称,index,或 staging area,或有时(很少有这些天)缓存。它有两个(或三个)名称的原因可能是多种因素的结合:索引本身很复杂,但大多数时候使用它的方式相对简单。术语暂存区指的是您使用它的方式。我喜欢称它为索引,因为它在合并过程中发挥了更大的作用,您最终需要了解这一点。

无论如何,考虑索引的一个好方法是假设它拥有每个文件的 third 副本。2 这个额外的索引副本采用 冻结格式 ,但实际上并未冻结:您可以用新副本覆盖它。索引中每个文件的副本是 git commit 进行新提交时将使用的副本。这意味着您可以将索引视为 将进入下一次提交的文件 ,或者更短,索引是 您建议的下一次提交 .

因此,git checkoutgit switch 的作用实际上是将文件从您选择的提交复制到 both 索引 你的work-tree。索引副本现在已准备好进行新的提交,work-tree 副本是一个普通文件,存储在一个文件夹中,因为你的 OS 需要它。 Git 的 个文件副本,在提交和索引中,根本没有存储在文件夹中。它们采用特殊的 read-only、Git-only 格式。3

git add 的作用——这就是您在 work-tree 中修改文件后必须继续使用它的原因——是复制 work-tree 将文件复制到索引中。也就是说,它获取更新后的 work-tree 文件,re-compresses 并将其转换为 Git 的内部冻结格式,并 替换 旧索引与新副本一起复制。如果您 git add 一个根本不在索引中的文件,则会向索引添加一个 new 文件,但是如果您 git add 一个 在索引中是,它只是替换了frozen-format副本。

索引不能保存文件夹名称。索引中的文件只有长名称,如 path/to/file.ext。因此,当 Git 从 索引构建提交 时,新提交中唯一的东西就是文件。4 这就是阻止你的原因存储一个文件夹。

当 Git 去 提取 一个提交时,如果那个提交中的一个文件被命名为 path/to/file.ext 而你的 work-tree 没有path 文件夹,或者 path 文件夹缺少 to 文件夹,Git 将 创建 pathpath/to 根据需要。所以 Git 一开始没有存储 pathpath/to 的事实并不重要,除了以一种方式:不可能将空文件夹(空目录)存储在Git 提交。5


2从技术上讲,索引保存的不是文件的副本,而是文件模式的副本,其名称,以及一个 blob 散列 ID。但是,除非您使用 git ls-files --stagegit update-index 深入了解索引的内部工作原理,否则将索引视为持有副本就足够了。

3从技术上讲,这些实际上是 blob 对象,它们具有哈希 ID,就像提交一样。 blob 对象的哈希 ID 取决于文件内容,如果内容匹配,文件可以 共享 多个不同的提交,因为每个提交实际上只存储 blob 哈希 ID。这一切都隐藏在另一层间接层之下:提交存储 tree 哈希 ID,树对象存储name-and-mode-and-hash ID。但是您不需要知道这些就可以使用 Git。您确实需要了解提交哈希 ID 和索引。

4从技术上讲,索引可以存储:

  • 普通文件(模式100644100755),或
  • 一个符号link(模式120000),或
  • a gitlink (模式160000).

所有这三个都与哈希 ID 相关联:文件或 sym 的 blob 哈希link,或 git 的子模块提交哈希 IDlink。

5有一些技巧,none完全令人满意:见How can I add an empty directory to a Git repository? The only one of these that really works is the empty submodule trick.


跟踪和未跟踪的文件

当您提取一个提交时,您会得到它的每个文件的三个副本。一个是冻结副本,在当前或 HEAD 提交中。第二个是索引中的 frozen-format 但可替换的副本。第三个是您实际上可以 使用 的唯一副本,在您的 work-tree 中。您可以查看和触摸 work-tree 副本,因为它们是您计算机上实际文件夹中的实际文件,而不是以某种特殊 Git 方式存储的内部 Git 实体。

因为您的 work-tree 是 您的 ,这意味着您可以创建和删除文件和文件夹,而无需 Git 参与。在您将这些文件复制到 Git 的索引中之前,Git 不会 使用 任何这些文件。使用 git checkout(或 git switch and/or git restore),您可以命令 Git 覆盖 您的 work-tree 文件具有 Git 的副本,但索引副本将进入 next 提交。

那么,如果您创建了一个 work-tree 文件并且 没有 将它添加到索引中,会发生什么情况?答案是:Git 称其为 未跟踪文件 .

未跟踪的文件非常简单,就是现在在您的 work-tree 中,但现在不在 Git 的索引中的文件。现在 部分很关键,因为您不仅可以将新文件复制到 Git 的索引中——使用 git add,当然——你也可以使用 git rm.

告诉 Git 从其索引中删除 文件

运行:

git rm path/to/file

告诉Git:从你的索引和我的work-tree中删除那个文件的副本。现在它已经从两者中消失了,它 不会 next 提交中。 运行:

git rm --cached path/to/file

告诉Git:从你的索引中删除该文件的副本,但不要触摸work-tree副本。现在它已经从索引,它不会在下一次提交中。

因此,如果您希望某个文件在提交中,您必须将其从索引中删除。就目前而言,这很好。如果您将它从索引中删除,或者一开始就没有在索引中, 该文件现在存在于您的 work-tree 中,该文件是一个 未跟踪文件.

如我们所见,未跟踪的文件可能很烦人,但也可能非常重要。

git status

当你运行git status、Git运行s两[=4​​14=]组比较。第一个比较将 HEAD 提交与索引进行比较。对于每个 相同 的文件,Git 什么也没说。对于不同的文件——或者是新文件或已被删除的文件——Git 表示此文件 暂存用于提交 。请注意,您不能更改 HEAD 提交的内容,6 因此,根据定义,此处的任何更改都是您在索引中更改的内容。

有了 运行 这第一次比较,HEAD-vs-index,Git 现在 运行 是 第二 比较。它将索引中的所有文件与 work-tree 中的文件进行比较。对于每个 相同 的文件,Git 什么也没说。对于不同的文件或已被删除的文件,Git 表示此文件 未暂存提交

请注意,我们还没有提到新文件。对于 work-tree 中但不在索引中的每个文件...嗯,这些正是您的 未跟踪文件 git status 对这些所做的是 抱怨 ,将它们列为 "untracked".


6虽然您无法更改 any 提交的内容,但您可以使用 git checkout—select 一个 不同的 提交成为 HEAD 提交。或者,当然,您可以 运行 git commit 并进行 new 提交,现在新提交 HEAD 提交。


git status闭嘴

.gitignore 文件的大约一半目的是让 git status 停止抱怨 未跟踪的文件。在 gitignore 文件中列出文件名或模式使得 git status 而不是 抱怨该文件未被跟踪。

这不会将输入的任何文件输出前任。 如果文件已经在 Git 的索引中,则在 .gitignore 中列出该文件无效。 文件 在索引中,以便该文件的副本 将在下一次提交中 ...除非,当然,你自己删除它,使用 git rm.

避免 automatically-adding 个您不想提交的文件

.gitignore 文件的其余目的是使您能够使用 en-masse git add 操作,而无需 also 复制一些当前未跟踪到 Git 的索引中的文件。例如,列出 *.o*.pyc 意味着您现在可以 git add .git add *。 Git 将 跳过 untracked-and-ignored 文件,同时将其他更新的或新的 work-tree 文件复制到 Git 的索引中。

Untracked-and-ignored 因此有两个用处

由于这些文件不会被复制到索引中,它们不会在下一次提交中。 运行 git status 这些文件都未被跟踪 被忽略,你根本不会看到它们被提及:它们不在索引中,所以它们是未被跟踪的, 但 git status 不会抱怨。 运行 git add . 如果这些文件既未被跟踪又被忽略,git add 不会将它们复制到索引中。

但是你已经在一些现有的提交中有了这些文件

在你的例子中,有一些提交在 app/http/ 中的某处有一些文件。当你 运行:

git checkout <commit-specifier-or-branch-name>

并且您通过此操作选择的提交是包含 app/http/foo.html、Git 等文件的提交将:

  • 如果需要,创建 app/http/
  • foo.html写入该文件夹
  • app/http/foo.html 的冻结副本复制到它的索引中

然后您就可以处理/使用该文件了。但是现在文件 在 Git 的索引中,所以它是 tracked 并且 git status 会告诉你这件事并且任何 en-masse git add 都会将 work-tree 文件复制到索引中,以便更新的 foo.html 将在下一次提交中。

即使您foo.html复制到Git的索引中,这样索引副本仍然是旧的 副本,那个 old 副本将在您所做的新提交中。 Git 从索引提交,索引有旧副本。

明显的解决方法是 完全删除 索引副本,然后进行新的提交。现在索引副本不存在,新提交中就没有 app/http/foo.html 了。如果app/http/.gitignore,Git不会,从现在开始在新作品中,添加 foo.html

但是如果您对 所有 app/http/ 文件执行此操作,那么将来克隆此存储库并将此提交提取到新的 otherwise-empty work-tree,你不会得到一个名为app/http的文件夹,因为不会有名字以app/http/开头的文件使Git 注意它需要创建 app 文件夹,然后在 app 文件夹中创建 http 文件夹。

此外,假设你现在有一个状态,在这个状态下你已经提交 a123456...(无论如何是一些哈希 ID)app/http/foo.html,并提交 b6789ab... 中没有 app/http/foo.html。如果你:

git checkout a123456

Git 将从该提交中提取所有文件,包括 app/http/foo.html,到您的 work-tree 和 Git 的索引中......然后,如果你:

git checkout b789abc

Git 将从 that commit 中提取文件,注意 app/http/foo.html 需要 removed 因为它在之前的提交中——因此现在在索引中——并且不是在你现在selecting的提交中,因此必须从指数。所以 Git 将从索引 和你的 work-tree.

中删除 app/http/foo.html

在top-level.gitignore中列出app/http/,或在app/http/.gitignore中列出*,不仅告诉Git它不应该自动添加这些文件索引,它也给Git权限销毁这些文件在某些情况下 work-tree 中,包括这个特定的情况。因此,如果您 select 到 .gitignore 它们在某些现有提交中拥有这些文件,那么事情就会很危险。在你确定所有 new 提交后 有并且忽略这些文件,在检查历史提交时要小心!