在git rebase过程中自动暂存/恢复更改?

50

我的git工作流程经常使用rebase。我总是先获取上游更改(即我从中fork的主要存储库),然后合并到我的分支,然后进行rebase以删除对我无用的合并提交和树拆分。

这个工作流程中让我感到烦恼的一件事是:

$ git rebase upstream/master
Cannot rebase: You have unstaged changes.
Please commit or stash them.

$ git stash
Saved working directory and index state WIP on cc: abc1234 Merge remote-tracking branch 'upstream/master' into local_branch
HEAD is now at abc1234 Merge remote-tracking branch 'upstream/master' into local_branch

$ git rebase upstream/master
First, rewinding head to replay your work on top of it...
Applying: awesome code change

$ git stash pop

所以这里我们有4个命令,1=失败的变基(failed rebase),2=存储(stash),3=变基(rebase),4=弹出存储(stash pop)。除了3之外的任何东西都是毫无意义的工作。

那么问题是:最推荐的自动化方式是什么?运行git stash/rebase/pop的别名?一些git配置强制将变基视为存储或将其视为要重新应用的另一个提交?还是其他什么方法?


为什么你要先合并再变基,而不是一开始就直接变基呢? - Andrew C
@AndrewC,我在工作流中提到它只是因为大多数时候合并会“变基”,因为我强制仅使用快进(ff-only)... 我可能可以将其删除,因为这并不重要。在示例中我省略了它。 - gcb
在这种情况下,我 echo Torek 的回答。根据需要提交、变基和清理。 - Andrew C
1
一个非常相似的问题 https://dev59.com/J10a5IYBdhLWcg3wipNA - thorn0
5个回答

68
编辑:从Git版本1.8.4开始,但在Git版本2.0.1中修复了一个重要的副作用错误,git rebase现在具有--autostash。 您也可以通过git config --global rebase.autoStash truegit rebase配置为默认使用--autostash。 请注意文档中的以下句子:

然而,需要小心使用:成功合并后最终stash应用可能会导致非微不足道的冲突。

(我仍然更喜欢直接提交。)

TL;DR回答:只需进行提交(稍后再撤销)

您可能会发现git stash实际上只是git commit(以更复杂的形式,首先提交索引,然后提交工作树 - 当您应用存储时,可以保持索引和工作树的分离,或将它们组合成仅是工作树更改)。
一个存储区的特殊之处在于它所产生的提交——通过一种不寻常的方式(作为不真正合并的合并提交)进行两个或者甚至三个提交,而这些提交并没有放置在任何分支上(相反,使用了特殊的refs/stash引用来保留和查找它们)。
由于它们不在分支上,所以rebase不会对它们进行操作,在您的工作流中,是git stash pop将工作树更改带入新的工作树。然而,如果您自己做了一个(普通的)提交,在一个分支上,并且重新基础并包含该提交,则这个普通的提交将与其他提交一起被重新基础。我们将在一会儿讨论最后一个问题;现在,让我们把它画成一系列提交,这些提交会(或者不会)被重新基础:
... do some work ...
... make some commits ...
... more work ...
... do something that causes upstream/master to update, such as git fetch upstream
$ git stash

此时,您拥有以下内容:
... - o - * - A - B - C     <-- HEAD=master
           \          |\
            \         i-w   <-- stash
             \
              @-@-@         <-- upstream/master

在这里,ABC是您的提交(我假设您已经进行了3次提交),都在分支master上。挂在提交C上的i-w是您的stash,它不在分支上,但仍然是一个由两个提交组成的“git stash bag”,实际上连接到您的最新提交(C)。@提交(可能只有一个)是新的上游提交。

(如果您没有进行任何提交,则您的stash-bag挂在提交*上,您当前的分支指向提交*,因此git rebase除了将当前分支指针向前移动之外没有其他工作要做。在这种情况下,一切都一样,但我假设有一些提交。)

现在您运行git rebase upstream/master。这将复制您的提交以创建新的提交,具有新的ID和新的父ID,使它们位于最后一个@之上。Stash-bag不会移动,因此结果如下:

... - o - * - A - B - C     [abandoned, except for the stash]
           \          |\
            \         i-w   <-- stash
             \
              @-@-@         <-- upstream/master
                   \
                    A'-B'-C'   <-- HEAD=master

现在您使用的是git stash pop命令,它将把暂存区和工作区的更改都恢复,同时删除stash标签(准确地说是弹出,这样如果存在stash@{1},那么它现在就变成了stash,以此类推)。这会释放对原始的A - B - C链的最后引用,并意味着我们也不再需要i-w位,这样我们可以将其简化为以下形式:
... - @            <-- upstream/master
       \
        A'-B'-C'   <-- HEAD=master plus work tree changes

现在让我们画出如果不使用 git stash save,而是直接使用 git commit -a(或者使用 git addgit commit,但不加 -a 参数)创建一个实际的提交 D 会发生什么。你从以下状态开始:
... - o-*-A-B-C-D   <-- HEAD=master
         \
          @-@-@     <-- upstream/master

现在你执行git rebase upstream/master,这会将AD复制到最后一个@的末尾,得到如下结果:

... - o-*-@-@-@     <-- upstream/master
               \
                A'-B'-C'-D'   <-- HEAD=master

唯一的问题是,您有一个不需要的额外提交D(现在是D'),而不是未提交的工作树更改。但是这可以通过使用git reset来轻松撤消,以回退一个提交。我们可以使用默认的--mixed重置来重新设置索引(暂存区),以便“取消添加”所有文件,或者如果您希望它们保持git add状态,则使用--soft重置。(两者都不会影响最终的提交图,只有索引状态不同。)

git reset --mixed HEAD^   # or leave out `--mixed` since it's the default

这是它的样子:

... - o-*-@-@-@     <-- upstream/master
               \
                A'-B'-C'      <-- HEAD=master
                        \
                         D'   [abandoned]

您可能认为这样效率低下,但是当您使用git stash时,实际上至少做了两次提交,然后在您git stash pop它们时放弃。真正的区别在于通过进行临时提交而不是发布提交,您可以自动重新应用它们。

不要害怕临时提交

Git有一个通用规则:进行大量的临时提交以便在工作过程中保存您的工作。您始终可以稍后对它们进行变基。也就是说,不要这样:

... - * - A - B - C   <-- mybranch

在提交 * (来自别人或之前发布的内容) 的基础上,使 ABC 成为完美的最终提交,操作如下:

... - * - a1 - a2 - b1 - a3 - b2 - a4 - b3 - c1 - b4 - c2 - c3

其中a1是对A的初始尝试,a2修复了a1中的错误,b1是使b起作用的初始尝试,a3是因为意识到b1需要不同于A的变化,而做出的变化,b2修复了b1中的错误,a4修复了a3a2的更改中的错误,b3b1本应该完成的任务;然后c1是对C的初始尝试,b4是对b1的另一个修复,c2是细化过程中的一步。

假设在后,您认为其基本准备好了。现在运行git rebase -i origin/master或其他命令,重排pick行,按顺序排列a1a4b1b4c1c3,然后运行rebase。然后您修复任何冲突,并确保一切正常,然后再次运行git rebase -i将所有四个版本合并成,以此类推。
完成后,看起来您第一次就创建了完美的
(或者可能是与或其他提交有关,具体取决于您保留和删除哪些提交以及是否重新设置了任何时间戳)。其他人可能不想要或不需要看到您的中间工作 - 虽然如果有用的话,可以保留它而合并提交。同时,您永远不需要拥有必须进行rebase的未提交的内容,因为您只有部分内容的提交。

在一行提交文本中为这些提交命名确实有助于您后续的变基工作:

git commit -m 'temp commit: work to enable frabulator, incomplete'

等等。


1
这是一个很好的回答。但是,如果我正在进行“脏”提交时,我想要git push其中一个并留下其他的,那么这是否可行? - Zombo
3
推送一个提交的方法是将其放在自己的分支上(使用 git cherry-pick 复制它)。假设您已经将所有更改暂存,使用 git checkout -b pushme upstream/master 创建一个命名为 pushme 的分支,然后使用 git cherry-pick <commit-identifier> 复制它,最后使用 git push upstream pushme:master 将本地分支 pushme 中的一个提交推送到上游 master 分支。完成推送后,您可以将临时链重新基于新更新的 upstream/master(如果可以,rebase 会自动省略您复制的提交)。 - torek
@torek,如果你更喜欢“选择要推送的提交”这种工作流程,那实际上是非常完美的...我肯定会在某个时候使用它。而且我一定会把你的评论指给那些哭着要用svn的朋友们!这是一个非常好的工作流程,我总是觉得太复杂了,但你是对的,它是实用的。 - gcb

21

一个简单的答案: git rebase -i --autosquash --autostash <tree-ish>

-i = 交互式的重新绑定

https://devdocs.io/git/git-rebase


这将会...

  • 自动存储您的更改
  • <tree-ish>交互式地重新绑定
    • 自动定位您的压缩和修复
  • 在重新绑定后,自动弹出在工作目录中的存储

tree-ish可以是一个提交哈希值,也可以是一个分支名称或者一个标签,或者是任何标识符


5

一条命令即可完成整个工作流程,包括获取操作:

git pull --rebase --autostash [...]

2
您可以使用一个名为git-up的外部工具,它可以对所有分支执行您所说的操作。这也有助于保持干净的历史记录图。
我已经使用了几年,它运行得非常好,特别是如果您不是git专家。如果您添加自动重新基础功能,则应知道如何正确地从失败的重新基础中恢复(继续、中止等)。

安装

打开shell并运行:
sudo gem install git-up

配置

打开全局配置文件 (~/.gitconfig),并添加以下内容:

[git-up "fetch"]
    all = true    # up all branches at once, default false
    prune = true  # prune deleted remote branches, default false
[git-up "rebase"]
    # recreate merges while rebasing, default: unset
    arguments = --preserve-merges
    # uncomment the following to show changed commit on rebase
    # log-hook = "git --no-pager log --oneline --color --decorate $1..$2"

详见官方文档了解更多选项。

调用

如果一切配置正确,只需运行:

git up

这大致相当于执行以下操作:

git stash
git fetch --all
[foreach branch]
    git rebase --preserve-merges <branch> <remote>/<branch>
    git merge --ff-only <branch>
[end foreach]
git checkout <prev_branch>
git stash pop

1
Git本身使用了大量的魔法,这就是为什么命令被分成了瓷器和管道。git-up只执行一系列“安全”的瓷器命令。如果发生任何“不好”的事情,你只会陷入一个rebase冲突合并的情境中:一个简单的git rebase --abort就可以解决所有问题。随着你对Git的信心和控制力的增强,你会逐渐发现这个脚本变得无用了。 - giosh94mhz

1

来自tjsingleton博客的答案:

创建一个命令别名:

git stash && git pull --rebase && git stash pop

更新

如果您正在使用Idea,在工作目录中推送脏文件,它将提示对话框,请选择rebase / merge,它将自动进行stash、rebase / merge和pop。


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