我为什么要在 Git 中提交之前使用 stage?

我对版本控制还是个新手,我知道“提交”实际上就是在更新你正在处理的新的“当前”版本时创建一个备份。

我不明白的是,从实际的角度来看,什么是分期。上演一些名义上存在的东西,还是只是为了达到某种目的?当你承诺的时候,它无论如何都会承诺一切,对吗?

编辑: 我想我可能弄混了术语。“分级”文件和“追踪”文件是一回事吗?

49404 次浏览

当您提交时,它只会提交索引中的更改(“暂存”文件)。这有很多用途,但最明显的是将您的工作变更分解成更小的、独立的部分。也许您在实现某个特性时修复了一个 bug。你可以 git add只是该文件(或 git add -p添加只是文件的一部分!)然后在提交其他所有内容之前提交该错误修复程序。如果您使用的是 git commit -a,那么您只是在提交之前强制使用 add。如果您想利用暂存文件,请不要使用 -a

您还可以将暂存文件视为许多命令的 --cached中间工作副本。例如,git diff --cached将向您展示这个阶段与 HEAD的不同之处,这样您就可以看到您将要提交的内容,而无需混合其他工作更改。

  • 分段区域提供了使提交更小的控件。只要在代码中做一个逻辑修改,将修改过的文件添加到临时区域,最后如果修改不好,那么检出之前的提交或以其他方式提交修改。它提供了灵活性,可以将任务分割成更小的任务,并提交更小的修改。有了临时区域,在小型任务中更容易集中注意力。
  • 它也给你提供了休息的机会,在休息之前忘记你已经完成了多少工作。假设您需要更改三个文件以进行一次逻辑更改,并且已经更改了第一个文件,在开始进行其他更改之前需要进行长时间的休息。现在你不能提交,你想跟踪哪些文件你已经做了,所以回来后,你不需要尝试记住有多少工作已经完成。因此,将该文件添加到暂存区域,它将保存您的工作。当你回来只是做 git diff --staged和检查哪些文件你改变了,在哪里,并开始作出其他更改。

阶段区域帮助我们以更大的灵活性精心制作提交。所谓精心设计,我的意思是将提交分解成逻辑单元。如果您想要一个可维护的软件,这是非常关键的。实现这一目标的最明显方法是:

你可以在一个工作目录中处理多个特性/bug,但仍然可以进行有意义的提交。拥有一个包含我们所有活动工作的单一工作目录也是非常方便的。(这可以在没有临时区域的情况下完成,只要更改不会与文件重叠。而且您还有额外的责任手动跟踪它们是否重叠)

你可在此找到更多例子: 索引的用途

而且最好的部分是,优势并不止于这个工作流列表。如果确实出现了一个独特的工作流,那么几乎可以肯定,临时区域将帮助您解决这个问题。

如果设想在 Github 上的存储库中维护一个日志文件,那么更容易理解 git 命令 addcommit的用法。 对我来说,典型的项目日志文件可能是这样的:

---------------- Day 1 --------------------
Message: Complete Task A
Index of files changed: File1, File2


Message: Complete Task B
Index of files changed: File2, File3
-------------------------------------------


---------------- Day 2 --------------------
Message: Correct typos
Index of files changed: File3, File1
-------------------------------------------
...
...
...and so on

我通常以 git pull请求开始一天,以 git push请求结束一天。因此,一天记录中的一切都对应于它们之间发生的事情。在每一天,有一个或多个 逻辑任务,我完成,需要更改几个文件。在该任务期间编辑的文件列在索引中。

这些子任务(这里是 TaskA 和 TaskB)中的每一个都是单独的提交。git add命令将文件添加到“已更改文件索引”列表中。这个过程也称为分段。git commit命令记录/完成更改和相应的索引列表以及自定义消息。

请记住,您仍然只是更改存储库的本地副本,而不是 Github 上的副本。在此之后,只有当您执行“ git 推送”操作时,所有这些记录的更改以及每次提交的索引文件才会登录到主存储库(在 Github 上)。

例如,要获得假想日志文件中的第二个条目,我可以这样做:

git pull
# Make changes to these files
git add File3 File4
# Verify changes, run tests etc..
git commit -m 'Correct typos'
git push

简而言之,git addgit commit允许您将对主存储库的更改分解为系统的逻辑子更改。正如其他的回答和评论所指出的那样,它们当然有更多的用途。然而,这是 Git 背后最常见的用法和驱动原则之一,Git 是一个多阶段修订控制系统,不像其他流行的系统,比如 Svn。

暂存的一个实际用途是文件提交的逻辑分离。

由于分段编辑允许您继续编辑文件/工作目录,并在您认为准备就绪时分段提交,因此您可以使用单独的分段进行逻辑上不相关的编辑。

假设您有4个文件 fileA.htmlfileB.htmlfileC.htmlfileD.html。对所有4个文件进行更改并准备提交,但是 fileA.htmlfileB.html中的更改在逻辑上是相关的(例如,两个文件中相同的新特性实现) ,而 fileC.htmlfileD.html中的更改是独立的,并且在逻辑上与以前的文件无关。您可以首先处理文件 fileA.htmlfileB.html并提交它们。

git add fileA.html
git add fileB.html
git commit -m "Implemented new feature XYZ"

然后,在下一步中,对剩下的两个文件进行分阶段更改和提交更改。

git add fileC.html
git add fileD.html
git commit -m "Implemented another feature EFG"

我看到了@Ben Jackson 和@Tapashee Tabassum Urmi 提到的使用 stage 使提交变小的要点,有时我用它来达到这个目的,但我主要是用它来使我的提交变大!我想说的是:

假设我想添加一个需要几个小步骤的小特性。我不认为单独提交小步骤和充斥我的时间线有什么意义。然而,我想保存每一步,如果必要的话回去,

我只是简单地把小步骤叠加在一起,当我觉得值得承诺的时候,我就承诺。通过这种方式,我从时间线中删除了不必要的提交,但仍然能够撤消(签出)最后一步。

我看到了其他方法(简化 git 历史) ,您可以根据自己的喜好使用这些方法:

  1. Git 修改(修改你上次提交的内容) ,这不是你想要的特定目的(我认为它主要是做一个错误的提交,然后修复它)
  2. Git rebase,这是后来才想到的,可能会给您和使用您的存储库的其他人带来严重的问题。
  3. 创建一个临时分支,合并,然后删除它(这也是一个很好的选择,需要更多的步骤,但给你更多的控制)

它就像一个复选框,提供选择提交哪些文件的能力。

例如,如果我已经编辑了 fileA.txtfileB.txt,但是我只想提交对 fileA.txt的更改。因为我还没有完成 fileB.txt

我可以简单地使用 git add fileA.txt和提交使用 git commit -m "changed fileA.txt"和继续与 fileB.txt的工作,完成后,我可以提交 fileB.txt容易

为了扩展 本 · 杰克逊的回答,这很好,让我们仔细看看最初的问题。(参见他对 何必呢类型问题的回答; 这是关于 怎么回事的更多信息。)

我对版本控制还是个新手,我知道“提交”实际上就是在更新你正在处理的新的“当前”版本时创建一个备份。

这不是 没错。备份和版本控制当然是相关的ーー具体有多大程度上取决于某些在某种程度上取决于意见的东西ーー但是如果只是在意图上,也肯定有一些不同: 备份通常是为灾难恢复而设计的(机器故障、火灾会摧毁整个建筑,包括所有的存储介质,等等)。版本控制通常为更细粒度的交互设计,并提供备份所不具备的特性。备份通常存储一段时间,然后作为“太旧”抛弃: 更新的备份才是最重要的。版本控制通常永远保存每个提交的版本。

我不明白的是,从实际的角度来看,什么是分期。上演一些名义上存在的东西,还是只是为了达到某种目的?当你承诺的时候,它无论如何都会承诺一切,对吗?

是也不是。Git 的设计有些特别。有版本控制系统,不要需要一个单独的登台步骤。例如,Mercurial 在使用方面与 Git 非常相似,没有需要一个单独的 hg add步骤,而不仅仅是第一个引入全新文件的步骤。对于 Mercurial,您可以使用选择一些提交的 hg命令,然后执行工作,然后运行 hg commit,就完成了。对于 Git,您使用 git checkout1,然后执行工作,然后运行 git add,然后运行 git commit。为什么额外的 git add步骤?

这里的秘密就是 Git 所说的 索引集结地,或者有时候ーー现在很少说了ーー 缓存。这些都是同一个东西的名字。

编辑: 我想我可能弄混了术语。“分级”文件和“追踪”文件是一回事吗?

没有,但这些是有关联的。追踪到了文件是存在于 Git 索引中的文件。要正确理解索引,最好从理解提交开始。


自从 Git 版本2.23以来,您可以使用 git switch而不是 git checkout。对于这种特殊情况,这两个命令完全做同样的事情。新命令之所以存在,是因为 git checkout被太多的东西填满了; 它们被拆分成两个独立的命令 git switchgit restore,以使 Git 的使用更加简单和安全。


承诺

在 Git 中,提交保存 Git 知道的 每个文件的完整快照。(Git 知道哪些文件?我们将在下一节中看到这一点。)这些快照以一种特殊的、只读的、只 Git 的、压缩的和去重复的形式存储,通常只有 Git 自己可以读取。(在这个快照中,每个提交中的内容都比 只是多,但这就是我们将在这里讨论的全部内容。)

重复数据删除有助于节省空间: 我们通常只更改少量文件,然后进行新的提交。因此,提交中的文件的 大部分大多与前一次提交中的文件相同。通过直接重用这些文件,Git 节省了大量空间: 如果我们只接触一个文件,新的提交只占用 新副本的空间。即使这样,它也是被压缩的ーー有时候压缩得非常厉害,尽管这种情况会在以后发生ーー这样,一旦 .git目录被展开为普通的日常文件,它实际上可以比它包含的文件小。重复数据删除是 安全,因为提交的文件一直被冻结。没有人可以去改变一个,所以提交依赖于彼此的副本是安全的。

因为存储的文件是这种特殊的、永久冻结的、只有 Git 的格式,所以 Git 必须将每个文件 向外扩张成为一个普通的日常副本。这个普通的拷贝不是 饭桶的拷贝: 它是 你的的拷贝,随你怎么处理。Git 只会在你让它这么做的时候写这些,这样你就可以使用你的拷贝了。这些可用的副本在您的 工作树工作树中。

这意味着当您签出某个特定的提交时,每个文件都会自动有 副本:

  • Git 在 当前提交中有一个永久冻结的、 Git 化的副本。您不能更改此副本(当然,您可以选择不同的提交,或者进行新的提交)。

  • 在您的工作树中,有一个标准格式的副本。您可以使用计算机上的任何命令对此进行任何操作。

其他版本控制系统(包括上面提到的 Mercurial)在这里停止,只有这两个副本。您只需修改您的工作树副本,然后提交。Git... 不会。

索引

在这两个副本之间,Git 存储每个文件的 第三 copy 2。这第三个拷贝位于冻结的 格式中,但是与提交中的冻结拷贝不同,您需要对它进行 可以更改。要更改它,可以使用 git add

git add命令的意思是 使文件的索引副本与工作树副本相匹配。也就是说,告诉 Git: 通过压缩更新后的工作树副本,对其进行解复制,并准备将其冻结到一个新的提交中,从而替换现在索引中的冻结格式的解复制副本。如果 不要使用 git add,索引仍然保存当前提交的冻结格式副本。

当您运行 git commit时,Git 将索引 就在那时中的任何内容打包为新的快照。由于 Git 已经采用了冻结格式,并且是预复制的,所以 Git 不需要做很多额外的工作。

这也解释了 无法追踪的文件是关于什么的。未跟踪的文件是位于您的工作树中但是位于 Git 索引 就现在中的 不是中的文件。不管文件是怎么到这个地步的。也许你把它从你电脑上的其他地方复制到你的工作树上。也许是你在这里创造的。也许在 Git 的索引中有一个 曾经是副本,但是您用 git rm --cached删除了该副本。不管怎样,在您的工作树中有一个副本,但是在 Git 的索引中没有副本。如果现在进行新提交,则该文件 不会位于新提交中。

请注意,git checkout最初是 填补空缺Git 的索引,来自您签出的提交。因此,索引从匹配提交开始。Git 还从同一个源代码填充您的工作树。所以,最初,三个都是匹配。当您更改工作树和 git add中的文件时,现在索引和工作树匹配。然后运行 git commit,Git 从索引中提交一个新的提交,现在三个都匹配了。

因为 Git 从索引中进行了新的提交,所以我们可以这样说: Git 的索引包含您计划进行的 < em > next commit。这忽略了 Git 的索引在冲突合并中所扮演的扩展角色,但是我们现在还是想忽略它。:-)

这就是它的全部内容ーー但它仍然相当复杂!这是非常棘手的,因为没有简单的方法可以确切地看到 Git 的 index.3但是有一个 Git 命令可以告诉你发生了什么,在某种程度上非常有用,这个命令就是 git status


严格来说,这根本不是 收到。相反,它是一个 参考文献到 Git 化的文件,预先除去副本等等。这里还有更多的内容,比如模式、文件名、临时编号和一些缓存数据,这些数据可以让 Git 运行得更快。但是,除非您开始使用 Git 的一些低级命令(特别是 git ls-files --stagegit update-index) ,否则可以将其视为一个副本。

3 git ls-files --stage命令将显示 Git 索引中每个文件的名称和登台编号,但通常这并不是很有用。


git status

git status命令实际上是通过为您运行两个单独的 git diff命令来工作的(并且还执行一些其他有用的工作,例如告诉您所在的分支)。

第一个 git diff将当前提交(请记住,它将永远被冻结)与 Git 索引中的任何内容进行比较。对于 一样文件,Git 什么也不说。对于 与众不同文件,Git 会告诉您这个文件是 为了承诺而上演。这包括所有新文件ーー如果提交文件中没有 sub.py,但是索引 是的中有 sub.py,那么就会添加这个文件ーー以及任何已删除的文件,这些文件在提交文件中(现在也是) ,但是不在索引文件中(也许是 git rm)。

第二个 git diff将 Git 索引中的所有文件与工作树中的文件进行比较。对于 一样文件,Git 什么也不说。对于 与众不同文件,Git 会告诉您这个文件是 不是为了提交而设计的。与第一个 diff 不同,这个特定的列表 没有包含全新的文件: 如果文件 untracked存在于您的工作树中,但不在 Git 的索引中,Git 只是将其添加到 无法追踪的文件.4的列表中

最后,在一个列表中积累了这些未跟踪的文件之后,git status也会公布 那些文件的名称,但是有一个特殊的例外: 如果一个文件的名称在 .gitignore文件中列出,那么就会取消最后一个列表。请注意,在 .gitignore中列出一个 < em > 追踪的 文件ーー位于 Git 索引中的文件ーー在这里没有任何作用: 文件在索引中,所以它会被比较,并被提交,即使它在 .gitignore中列出。忽略文件只禁止“未跟踪的文件”投诉.5


使用 git statusgit status -sー的短版本时,未跟踪的文件不会被分离出来,但原理是一样的。像这样积累文件也可以让 git status通过打印一个目录名(有时是这样)来汇总一大堆未跟踪的文件名。要获得完整的列表,请使用 git status -uallgit status -u

5 列出一个文件还可以使大量的 添加许多文件操作(如 git add .git add *)跳过未跟踪的文件。这一部分稍微有点复杂,因为您可以使用 git add --force来添加一个通常会被跳过的文件。还有一些其他通常较小的特殊情况,所有这些加起来就是: 文件 .gitignore可能更适合称为 .git-do-not-complain-about-these-untracked-files-and-do-not-auto-add-them或者同样笨拙的东西。但这太荒谬了,所以就 .gitignore了。


git add -ugit commit -a

这里有几个方便的快捷方式:

  • git add .将在工作目录和任何子目录中添加 所有更新后的文件。这尊重 .gitignore,所以如果当前未被跟踪的文件没有被 git status抱怨,它就不会被自动添加。

  • git add -u将自动添加 所有更新的文件 在你的工作树上的任何地方.6这只影响 追踪到了文件。注意,如果您有 被移除了的工作树副本,这也会删除索引副本(git add将其作为 使索引与工作树匹配的一部分)。

  • git add -A就像从工作树的顶层运行 git add .(但请参见脚注6)。

除此之外,还可以运行 git commit -a,它大致相当于运行 git add -ugit commit。也就是说,这将使您获得在 Mercurial 中方便的相同行为。

我通常建议不要使用 git commit -a模式: 我发现最好经常使用 git status仔细观察产出,如果状态与您预期的不一样,那么就要弄清楚为什么会出现这种情况。使用 git commit -a,很容易意外地修改文件并提交您不打算提交的更改。但这主要是品味/观点的问题。


6 如果你的 Git 版本早于 Git 2.0,在这里要小心: abc0只能在工作目录和子目录下工作,所以你必须先爬到工作树的顶层。git add -A选项也有类似的问题。

7 我说 大致相当是因为 git commit -a实际上是通过创建一个额外的索引,并使用另一个索引执行提交来工作的。如果提交 工程,则获得与执行 git add -u && git commit相同的效果。如果提交 没有可行ーー如果你让 Git 跳过提交,你可以通过任何一种方式做到ーー那么之后就不会对任何文件进行 git add编辑,因为 Git 抛出了临时的额外索引,回到使用主索引。

如果在这里使用 git commit --only,还会出现其他并发症。在这种情况下,Git 创建一个 第三索引,事情变得非常棘手,特别是如果您使用 pre-commit hook。这是使用单独的 git add操作的另一个原因。

谁写了这么糟糕的代码却没有维护注释?哦,我的天,甚至有一个错误,不让代码编译。即使它编译了,运行速度也很慢。

成交。

$ git diff

一些可怕的密码

-for(int i = 0; i < 20; i++)

- this.assets[i] = clear();

+ foreach(var asset in assets) {

+ asset = clear()

+ }

非法移民阿比

+ //This Api has only one public method and only handles degrees Celsius

+ //It is the onus of the user of the Api to conduct conversions

Bug 代码

+ size_t NumberOfElements = sizeof(arr)/sizeof(arr[0]);

+ balance[NumberOfElements - 1];

慢码

+ i = * ( long * ) &y; // evil floating point bit level hacking

+ i = 0x5f3759df - ( i >> 1 ); // what the ****?

我太激动了。我想做一个小小的改变,但结果却是一气呵成。让我承诺。

$ git commit -m "fixed some terrible code and added api documentation and fixed some compile time errors. Also introduced fast inverse square root"

好吧,让我点击回车和交... 等等,我实际上做了四个完全不同的东西。

  • 跟踪 git 日志将是一场噩梦。如果我引入一些只有在将来才会发现的回归错误会怎么样呢?找到错误引入的“精确提交”将更加困难。

那很简单,让我一个一个来,让我从:

$ git commit someTerribleCode.foo -m "cleaned up the iteration loop"
  • 不,等等,我需要坐下来看看我要做什么,需要什么 知道 someTerribleCodeundocumentedApiHEAD

  • 而且,尽管卡马克和身份证的人声称,混乱播种 在 slowCode更新超过性能增益。我不应该 真正的承诺。

如果那个芬兰/瑞典的家伙在给我这个工具的时候把这个设计标准考虑进去就好了。啊,等等,他考虑了。

$ git add undocumentedApi buggyCode


$ git diff --cached

非法移民阿比

+ //This Api has only one public method and only handles degrees Celsius

+ //It is the onus of the user of the Api to conduct conversions

Bug 代码

+ size_t NumberOfElements = sizeof(arr)/sizeof(arr[0]);

+ balance[NumberOfElements - 1];

看起来不错。我要一口气干掉他们两个。他们没有关系,但一套评论,所以我不会强调。

$ git commit -m "fixed bug and added documentation"

下一个独立承诺:

$ git add someTerribleCode
$ git commit -m "refactored for loop"

让我看看集结区还剩下什么:

$ git status
Changes not staged for commit:
modified: slowCode

我可以摆脱这些改变。

$ git reset --hard

Git 本可以完全避免登台区域,而是设计成“选择性直接提交”,但是在与命令行和 理解各种指令的心智模型。当一个人建立一个集结区的心理模型时,指令就变得更加可口,更容易掌握。接口时,这样做就不那么直观了