1

I am working in a team with a branching strategy that sometimes rewrites the upstream commit history for some of the branches.

It would be incredibly useful to be able to move the last N commits from my history into the stash, then reset HEAD, then move those stashes back into my local history, whilst preserving the commit messages.

How can I move commits between my local branch and the stash? And is there a way to automate this for N commits? e.g. to create aliases that would be invoked something like git stashHistory 5; git popHistory 5. Assume my current working directory has no changes when doing this.

fommil
  • 5,757
  • 8
  • 41
  • 81
  • I think you can use git cherry pick instead http://nathanhoad.net/how-to-cherry-pick-changes-with-git – dhamibirendra Aug 07 '14 at 15:20
  • that's effectively how I do it right now. That involves getting the list of commit id, then resetting head, then copying and pasting each id manually. I'd like to automate this because I can easily have 10 commits that I need to apply. – fommil Aug 07 '14 at 15:28
  • aah.. then you should check this out http://stackoverflow.com/questions/1670970/how-to-cherry-pick-multiple-commits – dhamibirendra Aug 07 '14 at 15:36
  • yeah, but that doesn't work when the branch is deleted beneath you :-) – fommil Aug 07 '14 at 15:55

1 Answers1

6

Technically you can't move commits at all, not in the sense that you mean. Moreover, "the stash" is actually just a single reference-name (refs/stash). The git stash script uses the reflog to hide multiple items under that single name. When you run git stash save to make a new one, the script creates a new commit (different from every other commit so far) and makes the stash point to it.1 It would be possible to achieve your goal this way, I think, but this is the wrong way to try it.

Fortunately, that's not a problem anyway. What you really want is just a plain old git rebase! The git rebase command simply automates a series of git cherry-pick operations with a git reset somewhere along the way.

Here's how this works. You have your work, on your branch B, which you start doing when your upstream team has upstream/B pointing to the commit-chain I mark o (for old) here:

..- * - o - o - o           <-- upstream/B
                  \
                    x - y   <-- B

Now your upstream team goes and "rewrites history" in upstream/B, replacing the three old os with new ns (maybe 4 ns, even). The old ones are still there though, especially in your repo since your B includes them:

        n - n - n - n       <-- upstream/B
      /
..- * - o - o - o           <-- upstream/B@{1}, in reflog
                  \
                    x - y   <-- B

What I believe you want to do is to copy x and y (or a longer chain of 5 in your example) to new, slightly-different commits x' and y', resulting in this graph:

                        x' - y'   <-- B
                      /
        n - n - n - n       <-- upstream/B
      /
..- * - o - o - o           <-- upstream/B@{1}, in reflog
                  \
                    x - y   <-- [reflog only]

To do this, you simply need to tell git rebase to start with the commit right after upstream/B@{1} (that's x) when rebasing branch B onto the new upstream/B:

$ git rebase --onto upstream/B 'upstream/B@{1}' B

This is why I left the reflog label upstream/B@{1} in the graph drawings: after a git fetch updates upstream/B with one of these rewrites, it's the label that lets you find x easily. Note that if upstream/B has been updated several times, the reflog number might have increased beyond 1. (I also put the ...@{1} into single quotes above, to protect it from shells—there are a few—that like to eat braces. Chances are yours doesn't and they're not needed.)

The latest versions of git (2.x, though 1.9 had it I think) have a new flag option to git merge-base called --fork-point that is meant to help automatically figure out where the o-to-x transition is, even if the reflog number is no longer just 1. So if you have a new enough git you can totally automate the whole thing. If not, you can count manually (as you have been doing), or manually poke through the upstream/B reflog to make sure @{1} is the right suffix. In all cases, what you need is to locate that o-to-x transition, since that's the argument you must pass to git rebase.

(You might not need the --onto argument, but it's safe to include. Without --onto git chooses the "on-to" from the branch's so-called "upstream". This is a terribly confusing term, especially since one of the rebase arguments is actually called upstream and it means something different! The upstream argument is where we are using upstream/B@{1}; it identifies commits to exclude from the rebase cherry-pick copying process.)


1In fact, it's at least two new commits—two for plain git stash save, three if you add -a or -u—and the one to which the refs/stash reference points is a merge commit. It's not what one would normally think of as a merge; the script simply uses—one might say "abuses"—the multiple-parent-commits aspect of a merge commit to be able to store these commits and use a single reference-name to find all of them later.

torek
  • 448,244
  • 59
  • 642
  • 775
  • Great explanation! Maybe I would add, that `reflog param` is not a nuber, but commit ID. – kraag22 Dec 21 '16 at 10:32
  • @kraag22: the reflog number I'm talking about is the one inside the curly braces. You can, indeed, use the raw commit ID directly instead—but then you're not using the reflog: `git rebase --onto face0ff cafedad` uses no reflog entries at all. (You can look up the IDs in the reflog and cut-and-paste them, if you prefer this method.) – torek Dec 21 '16 at 13:00