Reputation: 43
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
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
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