切换当前分支并保留本地更改

3
我现在在分支a上。我想在分支b上提交代码,这样克隆分支b的人就能和我的当前工作目录一样了。
因为有时候暂存当前更改会导致冲突,所以无法使用stash命令。
我正在寻找与创建工作目录临时副本相当的方法。具体做法是:先将工作目录复制一份到其他位置,然后执行git checkout -f b切换到分支b,再删除所有文件,最后将之前复制出的工作目录拷贝到项目目录并提交代码。

提交你在分支a上的更改;切换到分支b;git pull origin/a --force;git reset HEAD1;进行你的提交;git push --force;再次切换到分支a;git reset HEAD1;就这样。 - ovimunt
3个回答

2

如果你想让B版本之后的修订版本与你当前的工作树(即现在未提交)保持一致,可以使用git reset --soft命令:

最初的回答:

git reset --soft如果您的朋友。 如果您希望B版本之后的修订版本与您在工作树上拥有的版本相同(即目前尚未提交),则可以执行此操作:

git checkout --detach # disconnect from A
git reset --soft b # set the branch pointer to whatever revision B is pointing to.... Your working tree will be unaffected
git add . # add everything to index
git commit -m "revision after B that made it look the way I had it where I was working"
# if you like everything, move b branch to this revision and push
git branch -f b
git checkout b
git push some-remote b

那就这样吧。

最初的回答。


1
任何提交实质上都是“工作目录的临时副本”。
当你使用git show <commit>命令时,你要查看的实际上是快照中的“变更差异”。
因此回答你的问题有点困难。让我们尝试这两个可能的解决方案:

关于分支 b 的历史重写

如果你想让分支 b 上的提交反映出此刻分支 a 的确切状态,为什么不直接将分支 b 指向应该指向的位置呢?就像这样:

# get the uncommited changes on the branch
git commit -am "Useful message"

# point b where a is now
git branch -f b a

# instead of forcing the branch we could merge with a strategy ours
# but it's hard to tell what you need from your description alone

# reset a to its previous state
git reset --hard HEAD^

起始状态:

C1---C2 <<< a <<< HEAD # with a dirty tree, uncommited changes

      ? <<< b # (you didn't mention where b was pointing)

之后的结果

C1---C2 <<< a <<< HEAD
     \
      C3 <<< b # where your last (previously uncommited) changes are

由于它会重写分支历史记录,因此应仔细考虑,并且如果该分支是共享的,则可能被排除。尽管如此,它确实做到了您的要求:“克隆分支 b 的人与我现在拥有相同的工作目录”。

不重写分支 b 的历史记录

为了避免重写分支 b 的历史记录,一种替代方法是使用一种策略将 a 合并到 b 中,这种策略将从 a 方面获取所有内容,而不仅仅是冲突部分。操作步骤如下:

# we work on a copy of branch a
git checkout -b a-copy a

# get the uncommited changes on the branch
git commit -am "Useful message"

# proceed with the merge, taking nothing from b
git merge -s ours b

# we now reflect the merge on b, and this is a fast-forward
git checkout b
git merge a-copy

# no further need for the working copy of a
git branch -D a-copy

而且 a 不需要重置,因为它没有在任何时候移动。

第一次提交后:

C1---C2 <<< a
     \
      C3 <<< a-copy <<< HEAD

      o <<< b (anywhere in the tree)

第一次合并后:

C1---C2 <<< a
     \
      C3 (same state as a, plus the previously uncommited changes)
       \
        C4 <<< a-copy <<< HEAD (the merge taking only C3 side)
       /
      o <<< b (anywhere in the tree)

终态:

C1---C2 <<< a
     \
      C3 (same state as a, plus the previously uncommited changes)
       \
        C4 <<< b <<< HEAD
       /
      o

你能提供一种方法来保留分支b的历史记录吗?这样只需添加一个提交。 - Toast

1
实际上,git stash确实保存了当前的工作目录。问题在于它保存的方式不直接适合您的需求。还有一个次要问题,可能是一个非常严重的问题,但也可能很小。请参见下面的警告。但最终,您可能能够使用git stash来完成您想要的操作。

需要知道的事情

首先,请记住Git根本不会从您的工作目录(您的工作目录)中进行提交——提交是所有文件的完整快照。它们是从索引而不是工作目录中制作的快照。新提交中的文件是现在在索引中的文件。
(还要记住,索引是Git的其他部分称为“暂存区”的东西,有时也称为“缓存”。它保存将进入下一个提交的每个文件的一个副本。该副本最初是从当前提交中取出的副本,除了一些边缘情况,如在当前分支上有未提交的更改时检出另一个分支中所述。)
如果您的工作树与索引不同,并且您想要快照您的工作树,则需要对每个文件使用git add,覆盖索引中的副本,然后才能提交此更改。当然,这会破坏您在索引中进行的任何仔细分阶段。
但这就是为什么git stash实际上会生成两个提交的原因:
  • 一次提交会将当前的索引状态保存为一个新的提交,该提交不属于任何分支。现在可以安全地销毁索引状态。
  • 第二次提交将您当前的工作树保存为一个带有两个父级的提交:索引提交和当前提交。为了使该提交生效,Git使用工作树变体替换索引中的所有文件(因为Git从索引而不是工作树中进行提交)。1

(实际上,还有第三个可选提交来保存未跟踪或未跟踪加忽略的文件。如果存在该提交,则它是工作树提交的第三个父级。通常它不存在。)

完成这两个提交后,git stash会更新refs/stash以记住工作树提交w的哈希ID。该提交记住了索引提交i的哈希ID,以及当前提交的哈希ID:

...--o--o--T   <-- your-branch (HEAD)
 \         |\
  \        i-w   <-- refs/stash
   \
    o--A   <-- b

然后git stash运行git reset --hard,以便您的索引和工作树返回匹配提交T。我已经突出显示了另一个提交A,由其他分支b指向。

1从技术上讲,git stash 会使用第二个辅助/临时索引来创建提交 w,以防万一出现问题。在那种情况下,它可以放弃临时索引。然而,制作索引提交 i非常容易,因为管道命令 git write-tree 完成所有工作。


使用Git的储藏功能

需要注意的是:git stash 实际上只会对已经在索引中的所有文件执行 git add 操作。任何未被跟踪的文件,包括未被跟踪且被忽略的文件,都不在提交 w 中。它们就在你的工作目录中。即使在这种情况下,如果你执行了 git checkout A 来切换到提交 A,其中一些文件也将被复制到你的索引中。(当然,在这种情况下,你通常会看到 Git 需要覆盖某些未被跟踪的文件的警告。)

除了这个重要的注意事项外,储藏提交 w快照恰好是你想要添加到提交 A 后面的快照。

现在,既然这个快照存在了,你可以告诉 Git 创建一个新的提交 B,它以提交 A 为父提交,以 w树对象为其快照。这需要一个 Git 的内部命令:

git commit-tree -p refs/heads/b refs/stash^{tree}

也就是说,我们使用名称 refs/heads/b(分支 b,指向提交 A)来告诉 Git 我们新提交的父级哈希 ID 应该是什么。我们使用 refs/stash^{tree} 来告诉 Git 我们新提交的(快照)应该是什么。Git 读取标准输入以收集日志消息 - 如果您愿意,可以添加 -m <message>-F <file> 来提供消息,或将其发送到标准输入:

echo some message | git commit-tree -p refs/heads/b refs/stash^{tree}

结果是:

这是结果。

...--o--o--T   <-- your-branch (HEAD)
 \         |\
  \        i-w   <-- refs/stash
   \
    o--A   <-- b
        \
         B

新提交B的快照与贮藏提交w相同。

git commit-tree命令打印出新提交的哈希ID。你需要将其存储在一个shell变量中,然后很可能设置一些名称,例如refs/heads/b,以记住此提交。例如:

hash=$(git commit-tree -p refs/heads/b refs/stash^{tree})
git update-ref -m "add stashed work-tree commit" refs/heads/b $hash

提供:

...--o--o--T   <-- your-branch (HEAD)
 \         |\
  \        i-w   <-- refs/stash
   \
    o--A--B   <-- b

即,新提交b现在是现有分支b的顶端。 B中的快照是w中的快照;它们自动共享。 B中的日志消息是您提供给git commit-tree的任何内容。 B的哈希ID现在存储在b中,并且B的父级是A,因此这个新提交位于分支b上,就像您想要的那样。
现在,所有的操作都已完成,你需要恢复索引和工作树,这是 git stash 所扔掉但首先保存在这两个提交中的。要做到这一点,请使用 git stash pop --index 命令。--index 参数很重要:它会将当前索引与 i 进行比较,并使用差异来恢复你的索引。2 然后,它会将当前工作树与 w 进行比较,并使用差异从 w 中恢复你的工作树。然后,pop 部分会丢弃 i-w 提交,如果有其他存储的提交,则使 refs/stash 记住正确的提交。
因此,忽略所有可能出错的地方和所有适当的错误检查,以下命令序列可能会实现你想要的结果,具体取决于你想要的内容。
git stash push   # and make sure it does something
hash=$(echo automatic commit of work-tree |
    git commit-tree -p refs/heads/b refs/stash^{tree})
git update-ref -m "add stashed work-tree commit" refs/heads/b $hash
git stash pop --index

这是完全未经测试的(并且有一些糟糕的故障模式,特别是当git stash push说没有要保存的内容时,拒绝执行任何操作)。
这种直接将i读入索引的方式效率低下,但达到了相同的目的。对于w步骤也是如此。

我还要注意一点,除了它完全破坏你当前的索引方式外,只需添加所有内容并提交(使用git write-treegit commit-treegit update-ref将提交放到分支b上)更加高效。您可以通过使用临时索引来解决前面的问题,就像git stash所做的那样。但这不会说明为什么您可以使用git stash来获得您想要的结果。 :-) - torek

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