创建一个新的分支,仅包含部分未提交的更改。

3

我想在使用一些未提交的更改的同时创建一个新分支,能否只将其中一部分更改导入到新分支中?


1
“现有更改”这个短语是没有意义的。在Git中,实际上并不存在所谓的“更改”。存在的是提交。每个提交本质上都是那一刻所有文件的快照。而“现有”的理解很困难,因为所有提交以及其中所有文件的所有版本都是“存在”的。鉴于此,您能否用一个有意义的方式表达您的问题? - matt
这个回答解决了你的问题吗?如何git-cherry-pick只挑选特定文件的更改? - jmargolisvt
1
@matt 我在询问未提交的更改,我已经修改了我的问题。 - Rafi Henig
好的,但你仍然在这样做。分支不会“导入”任何东西。如果您只想提交一些内容到您的分支上,则只需使用git add添加那些内容即可。您git add的内容就是您将要提交的内容。您可以git add一个文件,甚至只是文件的一部分。 - matt
据我所知,未提交的更改驻留在工作树中,当在分支之间切换时,不是创建了一个新的工作树吗?@matt - Rafi Henig
2个回答

5

Matt's point above是没有任何“未提交的更改”。虽然git status报告“已暂存提交的更改”和“未暂存提交的更改”,但这个报告本身就是一种谎言,旨在使Git更易用。(它是否真正实现了这一点是另一个问题。)

最终意味着解决方案很简单:只需创建新分支,添加任何你喜欢的内容,然后运行git commit。(不要使用-a选项来执行git commit。)

长篇大论:为什么这是答案

理解所有这些的关键部分是:

  • Git的核心是提交。
  • 提交包含“快照”,而不是差异。
  • Git从“建议的快照”中创建一个新的提交,该快照位于Git称之为“索引”、“暂存区”和(现在很少用)“缓存”的地方。这三个名称都是指同一件事情。
  • 您可以在工作树中看到和处理的文件与Git索引中的副本1是分开的。

最后一点的意思是,当您使用提交时,实际上每个文件有“三个”副本处于“活动”状态:

  • 一个副本是严格的只读的,存储在当前提交中。您可以让Git读取它,但没有什么(甚至不是Git本身)可以更改它。

  • 第二个副本(参见脚注1)驻留在Git的索引中。

  • 您在工作树中看到和使用的副本实际上并不被Git本身使用(但请参见下文)。在git checkout时,Git会将已提交的文件复制到Git的索引和您的工作树中。如果您更改了工作树副本,则新的git commit不会使用更新后的文件,而是提交Git索引中的副本。

这就是为什么您总是需要在提交之前git add文件。2 git add步骤告诉Git:将我的工作树副本复制到您的索引副本中。 作为此复制过程的一部分,Git实际上准备了要提交的文件版本(它以一种特殊的、Git化的只读格式存在,并且进行了预先去重处理,因此所有使用相同内容的提交都会简单地重用准备好的文件)。

换句话说,索引中的内容在任何时间点上都是下一次提交中将包含的文件集合。这直接导致了git status如何告诉你它告诉你的内容。
  • 首先,git status 会输出一些东西,比如 on branch master,和其他有用的信息,在这里我们将忽略它们。

  • 其次,对于当前提交中的每个文件,Git 会将该文件与索引中的文件进行比较。如果两者相同,Git 不会做任何提示。如果它们不同,Git 会说该文件已经被 暂存以备提交。如果索引中缺少该文件,Git 会说它已被删除,如果索引中的文件不在提交中,Git 会说它是一个新文件。但无论哪种情况,这都是“暂存以备提交”,因为索引包含了建议的下一次提交的内容。索引中包含的内容与当前提交中的一些内容匹配,因此 Git 就不会提到它们。

  • 第三,对于索引中的每个文件,Git 将该文件与您的工作目录中的文件进行比较。如果两者相同,Git 不会做任何提示。如果它们不同,Git 会说该文件未被暂存以备提交。如果该文件已丢失,Git 会说它已被删除。

  • 最后,对于工作目录中存在但不在索引中的文件,有一个特殊情况。这些是您的 未跟踪文件。Git 会单独列出它们。(如果它们在 .gitignore 中列出,Git 就不会提到这些未跟踪文件。)

所以无论 Git 谈论的是已暂存或未暂存的 更改,它实际上指的是不同的文件副本。提交中的副本不能被更改:它们被永久冻结,或者至少在提交存在的时间内都是如此。索引中的副本以冻结和去重复的格式存在,但是可以通过覆盖它们来进行更改;这就是 git add 的作用。工作树中的副本则可以按照您的喜好处理。除了 git add 将其复制到 Git 的索引中,以及 git checkout 用某个提交中的文件替换您的工作树文件之外,工作树由您自己随意操作。

1技术上讲,索引包含对去重后的内部Git blob对象引用,而不是文件的副本。提交也使用这些blob对象内部,因此索引中的文件“副本”可以准备提交。

2当您使用git commit -a时,Git基本上会为您添加所有文件。 这里的基本上掩盖了Git执行此操作的方式,即使用额外的临时索引,以防提交本身失败。 这个额外的索引允许Git“撤消”该特定情况下的添加。 如果您使用git commit --only,事情会变得更加复杂:此时Git会制作两个临时索引文件。 我们将在此处忽略此情况。

请注意,git commit -a 中的 git add 大致相当于 git add -u:它仅对 Git 索引中已有的文件进行操作。如果您有一个全新的文件,它还不存在于 Git 的索引中,则仍需手动添加该文件:git add

分支名称的相关知识

如上所述,提交包含快照,正如我们刚刚看到的那样,它们是从 Git 索引中的文件副本构建的,当您运行 git commit 时。但是,关于每个提交还有更多需要了解的内容。每个提交都有一个编号,具有看似随机(但实际上并不随机)的哈希 ID。Git 在内部使用这些数字来从其“所有内部散列对象”的大型数据库中读取实际的提交数据。

因此,要查找提交,Git 需要其哈希 ID。但是,每个提交本身都包含其直接前身提交的哈希 ID,Git 将其称为提交的父级。这意味着,如果我们有一长串提交,我们可以像这样绘制它:

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

在这里,每个大写字母都代表了一个丑陋的哈希ID。该链条中的最后提交是提交H,在提交H中,Git拥有每个文件的完整快照,以及关于提交本身的一些信息:例如,谁在什么时候做了它,以及前一个提交G的哈希ID。
因此,通过阅读提交H,Git可以获取先前提交G的ID。将G中的快照与H中的快照进行比较,这是Git显示在这两个提交之间发生了什么变化。同样重要的是,这使Git可以回溯到过去,因为提交G包含更早提交F的哈希ID。而F又包含更早提交的哈希ID,以此类推。
这里的棘手问题是Git必须以某种方式找到提交H。这就是分支名称的作用。分支名称仅保存链中最后提交的哈希ID。因此我们最终会得到:
...--F--G--H   <-- master
< p > < em > 名称 master 让 Git 轻松找到提交 H 。该提交使 Git 找到每个早期提交。这就是 Git 中历史记录的工作方式:它全部是 向后 的。我们只是从结尾开始,然后根据需要向后工作。

要创建新的分支名称,您必须选择某个现有的提交。通常,您将从当前提交开始,这是当前分支上的最后一个提交。 git branchgit checkout -b 命令将创建新的分支名称,以便选择相同的提交:

...--F--G--H   <-- master, newbranch

现在我们需要另外一件事情,那就是告诉我们正在使用的这两个“名称”中的哪一个——当它说“在分支B上”时,git status将打印出一个名字,其中B是某个分支。因此,我们将特殊的名称HEAD与一个分支名称精确地连接起来。如果我们在分支master上,我们有:
...--F--G--H   <-- master (HEAD), newbranch

git checkout 还有另一个技巧

请注意,这两个名称选择的是相同的提交!当我们执行 git checkout newbranch 时,会得到:

...--F--G--H   <-- master, newbranch (HEAD)

但我们仍在使用提交H。这仍然是当前提交,由于我们不会更改提交,git checkout不会触及Git的索引或我们的工作树。这意味着我们可以创建一个新分支并切换到它,就像使用git checkout -b一样,而不会影响任何其他状态。
现在我们有了这个新分支并正在使用它,现在我们可以使用git add(甚至是git add -p)选择性地更新Git索引中的特定文件。当我们运行git commit时,Git将打包其索引并创建一个新提交I
...--F--G--H   <-- master
            \
             I   <-- newbranch (HEAD)

一旦Git从当前索引中获取内容创建了新的提交,Git会将新提交的哈希ID写入当前分支(即newbranch),因为HEAD指向newbranch。现在,该名称标识了该分支上最新的提交。
(现在通常可以切换回master。关键在于通过将未暂存的文件写入Git的索引中以使它们处于暂存状态,然后将索引写入提交,所有这三个文件都匹配。只有在不需要替换的文件中,索引副本和工作树副本才会有所不同。有关更多详细信息,请参见 在当前分支存在未提交更改时检出另一个分支。)

@torek 感谢您的精彩详细文章!不过我想问一下:未提交的更改不是驻留在工作树中吗?这不意味着如果我在分支之间切换,我会失去那些更改,因为新的工作树将为新分支创建? - Rafi Henig
1
@RafiHenig:关键在于,当从一个分支“切换”到另一个分支时,Git 实际上查看的是提交哈希 ID,而不是分支名称。如果您在哈希 H 的 B1 分支上,并“切换”到具有相同哈希 H 的 B2 分支,那么什么也不需要更改,因此什么也不会更改。使用当前哈希创建新分支,然后切换到该分支,可以确保不需要更改任何内容,因此什么也不会更改。 - torek
这就是为什么我喜欢说在Git中分支的“名称”并不重要的原因。当然,从查找提交哈希ID的角度来看,它们确实很重要,但最终还是哈希ID才是最重要的。 - torek
除此之外,在 Git 中还有一件非常有用的事情(尽管 git checkout 通过拥有太多模式和选项来搞乱它:如果您开始使用新的 git switch,则所有内容在 Git 2.23 中都更加清晰)。特别是,在从提交 a12345(或其他)切换到提交 9876f(或其他)之前,checkout/switch 将确保,如果它要替换工作树或索引中的文件 F,则文件 F 在提交中被保存。如果没有,您将收到一个投诉,说切换到另一个提交不安全。有关详细信息,请参见“带有未提交更改的检出另一个分支”发布。 - torek
@torek 非常感谢!! - Rafi Henig

2

这些命令有些比较晦涩难懂(其中两个是破坏性的)。如果您不确定它们的作用,最好在尝试之前先阅读本部分。

git add some_file some_directory

git add 可以通过文件或目录添加(事实上,常见的 git add . 就是将当前目录添加为目录)。

git commit -m "temp"

创建一个我们将要丢弃的临时提交。

git add .

添加其他所有内容,以便我们可以干净地存储它。

git stash

将当前工作状态存储在缓冲区中(我们稍后会将其恢复)。

git checkout -b some-branch

将当前工作树(包括“temp”提交)带到您想要的新分支。

git checkout -

'-' 是返回到先前所在的分支的快捷方式。

git rev-parse HEAD^^ | pbcopy

这是一次性的操作...最好逐个尝试。

git rev-parse HEAD^ 将返回当前分支最近的提交 sha。

添加第二个 ^,如 HEAD^^,将引用第二个提交。等等。

将其导入到 pbcopy(MacOS 上的系统剪贴板)只是为了方便。您也可以省略此部分,只需在终端中突出显示输出即可。

git reset --hard <some_sha>

:warning: 破坏性操作 :warning:

这将使当前工作状态返回到所需的提交(删除其他所有内容)。

在这种情况下,我们将其他所有内容存储在 stash 中,所以没问题。

git stash pop

取回我们存储在 stash 中的东西。


以下是在 Mac 上的一种方法(虽然非常手动):

git add <file(s)>
git commit -m "temp"
git add .
git stash
git checkout -b new-branch
git checkout -
git rev-parse HEAD^^ | pbcopy
git reset --hard <paste from clipboard>
git stash pop

最终的状态:

您现在有一个名为“new-branch”的分支,其中包含提交的代码更改,这些更改位于“temp”提交下。

而且您的主分支没有新分支中包含的更改。


如果我觉得在分支上工作的方向不是我正在开发的功能所需要的,那么我通常会使用类似于这样的流程……但也许我不想完全删除代码(以防万一以后还需要它)。


1
Pop是危险的,它会删除存储区。他只想应用部分... - Christoph
@Christoph,您介意再深入地讨论一下您的意思吗?在我看来,最后的git stash pop只是在清理我们在第三行创建的临时存储。 - j7hoenix
好的,它并没有丢失,只是回到了我们开始操作之前的状态。我之前不知道apply这个函数,真是太棒了,谢谢。 - j7hoenix
好的呼叫!我尝试了一下,确实漏掉了一个步骤。已经编辑过了。谢谢@Christoph。 - j7hoenix
Git stash apply 是你的好朋友 ;-) - Christoph
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接