Undo Git Rebase
Asked Answered
B

3

27

I performed a git rebase master on my branch and didn't realize it wasn't what I wanted until after I pushed it to the remote. It's only myself and 1 other person working on the project, so I know they've not pulled the latest changes.

Reading other questions on StackOverflow, it said to use git reflog and then git reset --hard HEAD@{n} to before the rebase. I did this to go to a commit I created before the rebase, but it didn't restore things to how it was before.

Am I missing a step? Is there a way to have the other person force-push his repo back up to restore things to how they were?

Thanks

Bactericide answered 9/9, 2015 at 22:41 Comment(5)
Elaborate "it didn't restore things to how it was before."Refugiorefulgence
Before the rebase, I had several additional commits and commit messages that are not showing up and the graph showed 2 tiers as opposed to only 1.Bactericide
Let's back up here: git rebase master has basically taken what was in your local master and moved your branch to the tip of what was in your local master branch. Is it the case that your local master branch was out of date with the remote branch? It's not going to be a simple process to unwind this.Roustabout
I was on master, did a pull, made a quick change and pushed that to master. I wanted that change on branch2 so I checked out branch2 and ran git rebase master. All I wanted was that last commit on master to be on branch2 too, but instead it did something else (I'm new to Git, so not sure how to explain what it did). But several commits disappeared or got smashed together, not sure which. I'm wanting to revert it to before all of this.Bactericide
Possible duplicate of Undoing a git rebaseMell
S
61

As Makoto already noted, you probably shouldn't bother undoing this rebase: it's probably what you wanted. Nonetheless, feel free to read on for how to undo it.


Use the reflog for the branch, as it will be easier to read. (The HEAD reflog has the same information, but has a whole lot more stuff in it, hence it's harder to find what you're looking for.)

For instance if I had just rebased mybranch, I would see:

$ git reflog mybranch
nnnnnnn mybranch@{0}: rebase finished: refs/heads/mybranch onto biguglysha1
ooooooo mybranch@{1}: commit: some sort of commit message
...

The name mybranch@{1} is therefore synonymous (at the moment) with ooooooo, the old abbreviated SHA-1. Every time you do something to the branch (such as git reset) the number inside the @{...} part will change, while the SHA-1s are forever permanent, so it's a bit safer to use the SHA-1 (full or abbreviated) for cut-and-paste.

If you then:

$ git checkout mybranch # if needed

and:

$ git reset --hard ooooooo  # or mybranch@{1}

you should have the original back. This is because rebase simply copies commits and then moves the label. After the rebase, but before the reset, the commit graph looks something like this, where A through C are "your" commits:

          A - B - C              <-- (only in reflog now)
        /
... - o - o - o - A' - B' - C'   <-- mybranch (after rebase)

and git reset simply1 erases the current branch label and pastes it on to the supplied SHA-1 (turning a reflog name into an SHA-1 first if needed). Hence, after the reset:

          A - B - C              <-- mybranch, plus older reflog
        /
... - o - o - o - A' - B' - C'   <-- (only in reflog now)

Note that now, post-reset, the rebase-made commit copies are the "abandoned" ones that are only found in reflog entries. The originals, which had been abandoned, are now claimed under mybranch again.

The way to think about this stuff is to draw the commit graph (with new commits pointing back at their parent commits), then draw in branch labels with long arrows pointing to the commit graph. The graph never2 changes except to add new commits, which have new and different big ugly SHA-1s (which is why I use letters like A B and C instead, and tack on additives like A' for copies). The SHA-1s are guaranteed to be unique3 and are permanent, but the labels with their long arrows, get erased and re-pointed all the time. (If you do this on a whiteboard, you should generally use black for the commit graph and a color, or several colors, for the labels.)


1Well, git reset does more than just move the label, unless you add some command-line flags. By default, it moves the label and resets the index; with --hard, it moves the label and resets the index and cleans out your work-tree. With --soft it just moves the label, leaving the index and work-tree alone. With git being what it is, there are a bunch more flags that twist up the meaning even further, but those are the big three: --soft, nothing aka --mixed, and --hard.

2If git only ever added things, your repo would grow huge over time. So, eventually, "unreachable" commits—those with no labels, and not even any leftover reflog entries, pointing to them, and that are not pointed-to by some commit that does have a label, or some other pointed-to commit—eventually these unreachable commits (and any other unreachable objects) are removed when git runs git gc for you automatically. You can force them to be removed earlier, but there's rarely a good reason to bother.

3Git itself depends on the guarantee. It's mathematically possible, but extremely improbable, for any two different objects to wind up with the same SHA-1. If this happens, git breaks.4 If the distribution is good enough the probability is 1 out of 2160, which is really tiny. This is a good thing as the "Birthday paradox" raises the possibility pretty rapidly, but because it started so tiny, it stays tiny and in practice it's never been a problem.

4By design, the "breakage" is that git simply stops adding objects, so that everything-so-far is still good. You then have to move to whatever new system has been devised to handle billions-of-objects repositories, the "next generation" git or whatever.

Surely answered 10/9, 2015 at 0:21 Comment(2)
What would be the correct way of pulling changes from another branch, as I needed to here, but without losing my branch's existing commit history? Would I use an interactive rebase, a merge, or?... ThanksBactericide
It's not clear to me what you mean by "my branch's existing commit history": there's the history your git keeps in the reflog for your branch, and there's the history implied by the commit graph. In the example in my answer, the commit-graph history is "C points to B, B points to A, A points to an otherwise undistinguished commit o". There's a different history, "C' points to B', B' points to A', A' points to a different otherwise undistinguished commit". These are of course different—but, if nobody cares about which otherwise undistinguished commit o they use, they're both OK.Surely
R
4

I have good news and bad news for you.

The good news is, you're in the state that you express that you want! The last commit on master is indeed now in your branch, and all is well with the universe.

The bad news is that now, with your rebase operation, you have effectively moved your branch to the tip of master; that is, your development branch is now in the same state as if you had simply branched from the tip of master.

But this is [considered to be] a good thing: provided that you haven't had any merge conflicts, you haven't lost any data in this process, and you also don't have a worthless merge commit on your branch.

The book does a great job explaining what a rebase is, so I won't repeat it. From the operations you describe, you shouldn't have lost any work.


If you do want to recreate your original history before you force-pushed, then someone with a non-pulled history can force-push their branch up to the remote repository. This will cause the state of your repo to be whatever was on their box at the time they pushed.

Roustabout answered 9/9, 2015 at 23:41 Comment(2)
But I want my commit history, is there no way to revert? As I stated in my original post, the person I'm working with has not pulled - so if there's not a way I can fix it, is there a way he can use his local repo to restore to the last state that he it at? In the future, how should I go about doing this so that I don't lose my commit history?Bactericide
Absolutely. But I want you to understand two things: you've definitely rewritten history, and that should only ever be done with great caution, and the situation you describe isn't a bad situation at all. You've got the commit you want on your branch and you've got a cleaner, linear history. But, if you're not comfortable with that, your partner can force-push their branch.Roustabout
P
-1

If you do take the commit hash from the reflog (also the tail of the entry in a normal git log) you can do get reset --hard COMMIT and then git push -f origin master which is usually a bad thing . I would recommend checking out the full history to a separate, clean directory before doing a force push.

Phillane answered 9/9, 2015 at 22:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.