创建一个只包含一些未提交更改的新分支

Create a new branch containing only some of the uncommitted changes

我正在尝试创建一个新分支,同时使用一些未提交的更改,是否可以只将一些更改导入到新分支?

其中一些命令有点不透明(其中两个是破坏性的)。如果您不确定它们的作用,那么在尝试它们之前先阅读这一部分可能会有所帮助。

git add some_file some_directory

git add 可以只按文件或目录添加(其实常见的git add .只是将当前目录添加为目录)。

git commit -m "temp"

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

git add .

添加其他所有内容,以便我们可以将其存放干净。

git stash

将当前工作状态存储在缓冲区中(稍后我们将把它取回来)。

git checkout -b some-branch

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

git checkout -

“-”是返回到您所在的上一个分支的快捷方式。

git rev-parse HEAD^^ | pbcopy

这是一堆。 . .最好单独尝试。

git rev-parse HEAD^ 将 return 当前分支的最新提交 sha。

添加第二个 ^ 如 HEAD^^ 将引用第二个。 . .等等

管道到 pbcopy(MacOS 上的系统剪贴板)只是为了方便。您可以省略这部分,只突出显示终端的输出。

git reset --hard <some_sha>

:警告:破坏性行为:警告:

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

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

git stash pop

带回我们存放在藏匿处的东西。


这是 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”,您的代码更改在“临时”提交下提交。

并且您的主分支没有新分支中的更改。


如果我决定我在分支上所做的工作不是我正在开发的功能的方向,我将使用类似这样的流程。 . .但也许我不想完全删除代码(以防万一我以后想要它)。

是根本没有任何“未提交的更改”。虽然 git status 报告 “为提交准备的更改”和“未为提交准备的更改”,但这份报告本身就是一种谎言,旨在 Git更有用。 (是否真的做到这一点完全是另一个问题。)

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

龙:为什么这是答案

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

  • Git 都是关于提交的。
  • 提交包含 快照,不包含差异。
  • Git 从 提议的快照 进行新提交,它位于 Git 不同的调用中, index暂存区,以及(现在很少)缓存。这三个名字都是为了一个东西。
  • 您在工作树中可以查看和使用的文件与 Git 索引中的副本1 是分开的。

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

  • 一个副本严格 read-only,存储在 当前提交 中。你可以让 Git 读出来,但没有任何东西——甚至 Git 本身——都不能改变它。

  • 第二个副本(见脚注 1)位于 Git 的索引中。

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

这就是为什么您在提交之前总是必须 git add 文件。2 git add 步骤告诉 Git:将此文件的 work-tree 副本复制到您的索引副本中。 作为此复制过程的一部分,Git 实际上准备了文件的 to-be-committed 版本(位于一种特殊的 Git-ized、read-only 格式和 pre-de-duplicated 以便所有使用相同内容的提交,只需 re-use 准备好的文件)。

换句话说,索引中的内容在任何时间点都是将在您将进行的下一个 提交中的文件集。这直接导致 git status 如何告诉你它告诉你什么:

  • 首先,git status 打印 on branch master 之类的内容,以及其他有用的内容,我们将在此处忽略。

  • 接下来,对于当前提交中的每个文件,Git将该文件与索引中的文件进行比较。如果两者相同,Git 就什么都不说了。如果它们不同,Git 表示该文件 暂存用于提交 。如果文件在索引中丢失,Git 表示它已被删除,如果索引中的文件不在提交中,Git 表示它是一个新文件。但无论哪种方式,这都是“暂存提交”因为索引包含建议的下一次提交。索引中的某些内容与当前提交中的内容相匹配,因此 Git 只是没有提及。

  • 第三,对于 index 中的每个文件,Git 将该文件与工作树中的文件进行比较。如果两者相同,Git 就什么都不说了。如果它们不同,Git 表示该文件未暂存提交。如果文件丢失,Git 表示它已被删除。

  • 最后,对于工作树中但不在索引中的文件有一个特殊情况。这些是您的未跟踪文件。 Git 分别列出它们。 (如果它们列在 .gitignore 中,Git 会禁止有关文件未被跟踪的投诉。)

所以每当Git说到变化,不管是staged还是unstaged,真正的意思是文件的各个副本是不同的。提交中的副本实际上无法更改:它们一直被冻结,或者至少,只要提交本身存在。索引中的副本处于冻结状态,de-duplicated 格式 但是 可以 通过覆盖它们来更改;这就是 git add 所做的。你的工作树中的副本是你的,你可以随心所欲地处理。除了 git add 将它们复制到 Git 的索引中,以及 git checkout 用来自某个提交的文件替换你的工作树文件,工作树是你的,你可以随意使用。


1从技术上讲,该索引包含对 de-duplicated 内部 Git [=139= 的 引用 ]blob 对象,而不是 t一个文件的副本。提交也在内部使用这些 blob 对象——这就是为什么文件的索引“副本”已准备好提交。

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

请注意 git commit -a 中的 git add 大致等同于 git add -u:它仅对 Git 已包含在其索引中的文件进行操作。如果您有一个全新的文件,但 Git 的索引中尚不存在,您仍然必须手动 git add 那个文件。


关于分支名称的知识

我在上面提到,提交包含快照,正如我们刚刚看到的,快照是根据您 运行 [=17] 时 Git 索引中的文件副本构建的=].但是关于每个提交还有更多要了解的。每个都有编号,带有 random-looking(但实际上根本不是随机的)哈希 ID。 Git 在内部使用这些数字从其“所有内部散列对象”的大数据库中读取实际提交数据。

因此,要找到一个提交,Git需要它的哈希ID。但是每个提交本身都包含其前一个提交的哈希 ID,Git 调用提交的 parent。这意味着如果我们有很长的提交链,我们可以这样画:

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

其中每个大写字母代表一个丑陋的大哈希 ID。这里链中的 last 提交是提交 H,在提交 H 内,Git 有每个文件的完整快照,plus 关于提交本身的一些信息:例如,谁在什么时候提交的,以及 immediately-previous 提交的哈希 ID G.

通过读取提交H,然后,Git可以得到更早的提交G的ID。将 G 中的快照与 H 中的快照进行比较,Git 向您展示了这两个提交之间 发生了什么变化。同样重要的是,这让 Git 回到过去,因为提交 G 包含较早提交 F 的哈希 ID。 F 依次包含 even-earlier 提交的哈希 ID,依此类推。

这里棘手的部分是 Git 必须以某种方式找到提交 H。这就是 分支名称 的用武之地。分支名称只包含链中 last 提交的哈希 ID。所以我们最终得到:

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

名称 master 让 Git 轻松找到提交 H。该提交让 Git 找到每个较早的提交。在 Git 中,历史就是这样运作的:一切都是 向后 。我们只是从头开始,然后根据需要向后工作。

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

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

现在我们还需要一件事,这是一种方法来判断我们正在使用这两个 names 中的哪一个 - git status 会在它说 在分支 <em>B</em>,对于某些 B。所以我们将特殊名称 HEAD 附加到一个分支名称。如果我们在分支 master 上,我们有:

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

git checkout还有一招

请注意,两个名称 select 相同的提交!当我们 git checkout newbranch 时,我们得到:

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

但我们仍在使用提交 H。这仍然是当前提交,并且由于我们没有更改提交git checkout 不会触及Git 的索引或我们的工作树。这意味着我们可以创建一个新分支并切换到它,就像 git checkout -b 一样,而不会弄乱任何其他状态。

现在我们有了这个新分支并正在使用它,现在我们可以使用git add(甚至git add -p)到select 主动更新 Git 索引中的特定文件。当我们 运行 git commit, Git 将打包其索引并进行新的提交 I:

...--F--G--H   <-- master
            \
             I   <-- newbranch (HEAD)

一旦 Git 做出这个新提交(从当时 Git 的索引中的任何内容),Git 将新提交的哈希 ID 写入当前分支,即 newbranch,因为 HEAD 附加到 newbranch。所以现在这个名字标识了分支上的最新提交。

(如果我们愿意,现在通常可以切换回 master。这里的关键是通过将未暂存的文件写入 Git 的索引来使它们暂存,然后写入索引进入提交,所有三个 those 文件匹配。只有文件 won' 需要在索引副本和 work-tree 副本不同的地方进行替换。有关这方面的更多详细信息,请参阅 Checkout another branch when there are uncommitted changes on the current branch。)