chohocvo
chohocvo

Reputation: 43

Git Changing rebase

I have 3 branches: master, feature, bugfix... And the commits looks like this:

    4-5-6(feature)
    |
1-2-3(master)
    |
    7(bugfix)

I did "git rebase bugfix feature" to test my feature with bugfix

    1-2-3(master)
        |
        7(bugfix)-4-5-6(feature)

Now I need to rebase create a pull request for my feature branch without the bugfix, so I did "git rebase master feature" and expect:

1-2-3(master)-4-5-6(feature)
    |
    7(bugfix)

Instead, it say feature is up-to-date with master. That's true but I don't want to merge commit 7 in there. I could do rebase interactive and remove that commit but I'd like to know if there is a better way to do this. I thought rebase would only carry the commits in 1 branch to another but looks like it's not.

Upvotes: 4

Views: 65

Answers (2)

torek
torek

Reputation: 487755

I thought rebase would only carry the commits in 1 branch to another but looks like it's not.

This is the key: your commit 7 in your diagram is in branch feature. It's also in branch bugfix. Commits 1-2-3 are in all three branches.

Git's branches are very different from most other version control systems. A branch "contains" a commit only by virtue of being able to "reach" that commit from the commit to which the branch-name points. Branch names like master, bugfix, and feature simply point to one particular commit, which Git calls the tip of the branch. It's the commits themselves that form a chain, by having each commit "point back" to its predecessor.

Because of this, git rebase actually copies commits: you went from:

        4--5--6   <-- feature
       /
1--2--3      <-- master
       \
        7      <-- bugfix

to:

        4--5--6   [abandoned - used to be feature]
       /
1--2--3      <-- master
       \
        7      <-- bugfix
         \
          D--E--F   <-- feature

where D is a copy of the original 4, E is a copy of 5, and F is a copy of 6 (I used the 4th, 5th, and 6th letters here so we could copy 7 to G for instance, if we wanted, but this technique is about to run out of steam).

You can still get what you want, though. You just need to copy D-E-F again, or—this is probably nicer for this particular case—just go back to the abandoned, original 4-5-6.

When you use git rebase to copy commits, the originals stick around. There are two names by which you can find them: ORIG_HEAD, and reflog names. The name ORIG_HEAD is overwritten by various other commands, but you can check to see if it's still pointing to commit 6:

git log ORIG_HEAD

and you will probably recognize your originals.

The reflog names have the form name@{number}, e.g., feature@{1}. The number part increments every time you change the commit to which the name part points, as Git simply saves the current value of name in the reflog, pushing the rest all up a notch.

Hence:

git log feature@{1}

should show you the same commits as git log ORIG_HEAD, except that feature@{1} sticks around longer (maybe becoming feature@{2}, feature@{3}, and so on, over time). By default, the previous values for each name are saved for at least 30 days, so that should be enough time to get it back.

To get it back, use git reflog feature to see which number goes in the @{...} part and then, while on feature (git checkout feature), run:

git reset --hard feature@{1}

or whatever the number is (though verifying yet again with git log first is a good idea).

(This assumes you don't have anything to check in, i.e., that git status says everything is clean, because git reset --hard wipes out not-yet-checked-in index and work-tree changes.)

Upvotes: 2

Schwern
Schwern

Reputation: 164659

Something to realize is that rebase doesn't rewrite history or move commits, commits in Git cannot be changed. Instead, it creates new history and says it was that way all along. For example, when you start with:

    4-5-6(feature)
    |
1-2-3(master)
    |
    7(bugfix)

And then git rebase bugfix feature what really happens is this:

    4-5-6
    |
1-2-3(master)
    |
    7(bugfix)-4A-5A-6A(feature)

Three new commits are made, 4A, 5A, and 6A. The original commits are still there, but nothing points to them. They'll eventually be cleaned up, but they'll stay there for a number of days.

That means you can undo a rebase, which is what you're trying to do. You'll need to find where feature was just before the rebase. That can be done with git reflog which tracks every time HEAD moves. That happens with checkout, commit, reset, and rebase. git reflog might be something like:

65e93ca (HEAD -> feature) HEAD@{0}: rebase finished: returning to refs/heads/feature
65e93ca (HEAD -> feature) HEAD@{1}: rebase: 3 feature
6d539a3 HEAD@{2}: rebase: 2 feature
3cd634f HEAD@{3}: rebase: 1 feature
b84924b (bugfix) HEAD@{4}: rebase: checkout bugfix
a9fd2f1 HEAD@{5}: commit: 3 feature
29136bc HEAD@{6}: commit: 2 feature
60543b0 HEAD@{7}: commit: 1 feature
c487530 (master) HEAD@{8}: checkout: moving from master to feature

That tells me a9fd2f1 was the last commit on feature before it was rebased. Instead of redoing the rebase, I can just move feature back.

git checkout feature
git reset --hard a9fd2f1

In the future, this sort of thing is made a lot easier if you git tag the original position of feature before doing the rebase. Then you can git reset back to that tag without having to search the reflog.


As to your specific problem, the issue is that after the rebase your repository now looks like this:

6A [feature]
|
5A
|
4A
|
7 [bugfix]
|
3 [master]
|
2
|
1

When you ask git rebase master feature Git notes that master is already an ancestor of feature and does nothing. It doesn't matter that bugfix is in-between.

Instead, you need to tell Git that you want to rebase just 4A, 5A, and 6A and ignore 7. That's done using the --onto syntax.

git rebase --onto master bugfix feature

That says to rebase from, but not including, bugfix to feature onto master.

I would recommend using git reset instead of trying to redo the rebase. There's no guarantee the second rebase will come out the same, especially if there were conflicts. Whereas with git reset you are explicitly moving back to the old state of the repository.

Upvotes: 2

Related Questions