automatically stash save/pop changes on git rebase?

谁都会走 提交于 2019-12-03 04:13:52

问题


my git workflow uses rebase a lot. I always fetch upstream changes (the main repo i forked from) and then merge to my branches, and then rebase to remove useless (to me :D) merge commits and tree splits.

one thing on this workflow that annoys me is:

$ 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

so here we have 4 commands, 1=failed rebase, 2=stash, 3=rebase, 4=stash pop. anything but 3 is just mindless work.

So, the question is: What is the most recommended way of automating it? an alias to run git stash/rebase/pop everytime? some git config that forces rebase to stash or treat it as another commit to reapply afterwards? something else?


回答1:


Edit: As of Git version 1.8.4, but with an important side bug fixed in Git version 2.0.1, git rebase now has --autostash. You can configure git rebase to use --autostash by default as well, with git config --global rebase.autoStash true. Please note the following sentence from the documentation:

However, use with care: the final stash application after a successful rebase might result in non-trivial conflicts.

(I still prefer to just make commits.)

TL;DR answer: just make a commit (then unmake it later)

It may help you to realize that git stash is really just git commit (in a more complicated form, which commits the index first, then the work-tree—when you apply a stash, you can maintain the separation of index and work-tree, or combine them into just a work-tree change).

What makes a stash special is that the commits it makes—the two or, with -u or -a, even three commits—are made in an unusual form (as a merge commit that's not really a merge) and not placed on any branch (instead, the special refs/stash reference is used to retain and find them).

Since they're not on a branch, rebase does not touch them, and in your workflow, it's the git stash pop that brings the work-tree changes into your new work-tree. However, if you make your own (normal) commit, on a branch, and rebase and include that commit, this normal commit will be rebased along with any others. We'll get to one last problem in a moment; for now, let's draw this up, as a series of commits that do (or don't) get rebased:

... do some work ...
... make some commits ...
... more work ...
... do something that causes upstream/master to update, such as git fetch upstream
$ git stash

At this point, here's what you have:

... - o - * - A - B - C     <-- HEAD=master
           \          |\
            \         i-w   <-- stash
             \
              @-@-@         <-- upstream/master

Here, A, B, and C are your commits (I'll assume you've made 3), all on branch master. The i-w hanging off commit C is your stash, which is not on the branch, but is still a two-commit "git stash bag" and is actually attached to your latest commit (C). The @ commits (there might be just one) are the new upstream commits.

(If you have made no commits, your stash-bag hangs off commit *, and your current branch points to commit *, so that git rebase has no work to do other than move your current branch pointer forward. Everything works out the same, in this case, but I'll assume there are some commits.)

Now you run git rebase upstream/master. This copies your commits to new commits, with new IDs and new parent IDs, so that they sit atop the last @. The stash-bag does not move, so the result looks like this:

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

You now use git stash pop, which restores the i/w stuff as work-tree changes, erasing the stash label (more precisely, popping it so that stash@{1}, if it exists, is now stash, and so on). That releases the last references to the original A - B - C chain, and means we don't need the i-w bit either, which lets us redraw this as the much simpler:

... - @            <-- upstream/master
       \
        A'-B'-C'   <-- HEAD=master plus work tree changes

Now let's draw what happens if, instead of git stash save, you just do a git commit -a (or git add and git commit without -a) to create an actual commit D. You start with:

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

Now you git rebase upstream/master, which copies A through D to place them at the end of the last @, and you have this:

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

The only problem is that you have this one unwanted extra commit D (well, D' now), instead of uncommitted work-tree changes. But this is trivially undone with git reset to step back one commit. We can use a --mixed reset—the default—to get the index (staging area) re-set too, so as to "un-add" all the files, or if you want them to stay git add-ed, a --soft reset. (Neither affects the resulting commit graph, only the index state is different.)

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

Here's what that looks like:

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

You might think this is inefficient, but when you use git stash you're actually making at least two commits, which you then abandon later when you git stash pop them. The real difference is that by making temporary, not-for-publication commits, you get those automatically rebased.

Don't be afraid of temporary commits

There's a general rule with git: make lots of temporary commits, to save your work as you go. You can always rebase them away later. That is, instead of this:

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

where A, B, and C are perfect and final commits atop commit * (from someone else or earlier published stuff), make this:

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

where a1 is an initial stab at A, a2 fixes a bug in a1, b1 is an initial attempt to make b work, a3 is from realizing that b1 requires A to be different after all, b2 fixes a bug in b1, a4 fixes a bug in a3's change to a2, and b3 is what b1 should have done; then c1 is an initial attempt at C, b4 is another fix to b1, c2 is a refinement, and so on.

Let's say that after c3 you think it's mostly ready. Now you run git rebase -i origin/master or whatever, shuffle the pick lines to get a1 through a4 into order, b1 through b4 into order, and c1 through c3 into order, and let the rebase run. Then you fix any conflicts and make sure stuff is still right, then you run another git rebase -i to collapse all four a versions into A, and so on.

When you're all done, it looks like you created a perfect A the first time (or maybe with a4 or some other one depending on which commits you keep and which ones you drop and whether you re-set any time stamps in things). Other people may not want or need to see your intermediate work—though you can retain it, not combining commits, if that's useful. Meanwhile you never need to have uncommitted stuff that you have to rebase, because you just have commits of partial stuff.

It does help to give these commits names, in the one-line commit text, that will guide your later rebase work:

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

and so on.




回答2:


One Simple Answer: git rebase -i --autosquash --autostash <tree-ish>

-i = interactively rebase

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


This will...

  • Auto stash your changes
  • Interactively rebase from <tree-ish>
    • Auto position your squashes and fixups
  • Auto pop stash in working directory after rebase

tree-ish can be a commit hash or a branch name or a tag or any identifier.




回答3:


The entire workflow in one command, including the fetching:

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



回答4:


You may use an external tools called git-up, which does exactly what you say for all branches. This will also help you keeping a clean history graph.

I've used for some years and it works pretty well, especially if you are not a git expert. If you add auto-rebasing feature you should know how to properly recover from failing rebase (continue, abort, ...)

Install

Open a shell and run:

sudo gem install git-up

Configuration

Open your global config file (~/.gitconfig), and add the following:

[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"

See the official documentation for more options.

Invocation

If everything is configured well, just run:

git up

This is (roughly) equivalent of executing the following:

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



回答5:


Answer from tjsingleton blogg

make a command alias of :

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

update

If you are using idea, pushing with a dirty working dir, it will prompt a dialog, choose rebase / merge, it will do stash, rebase / merge and pop automatically.



来源:https://stackoverflow.com/questions/27116671/automatically-stash-save-pop-changes-on-git-rebase

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!