Johannes
Johannes

Reputation: 3099

How does git rebase work between merges?

I have a feature and a master branch, e.g. like this (where master has been merged twice into feature):

*   27e89b5 (HEAD -> feature) f4
|\  
| * 6849a63 (master) 3
* | 3b78807 f3
* |   e84e33f f2
|\ \  
| |/  
| * 90e6f74 2
* | 4e4025b f1
|/  
* e4e0759 (tag: initial) 1

You can rebuild this MWE with the following commands:

git init
f() {   echo "$1" > README.md && git add README.md && git commit -m "$1"; }
f 1
git tag initial
git checkout -b feature
f f1
git checkout master
f 2
git checkout feature
git merge master
f f2
f f3
git checkout master
f 3
git checkout feature
git merge master
f f4

When I'm on feature, I'd like to git rebase -i initial to squash some of the feature branch commits. Git shows this:

pick 4e4025b f1
pick 90e6f74 2
pick 3b78807 f3
pick 6849a63 3

squashing only "f3" yields the following graph.

* be4387c (HEAD -> feature) 3
* a90d504 2
* 2afe0a4 f1
| * c447c7f (master) 3
| * efec11d 2
|/  
* 661d8fc (tag: initial) 1

Questions:

Upvotes: 1

Views: 49

Answers (1)

Schwern
Schwern

Reputation: 164809

Why doesn't git offer to squash "f2"?

Because f2 is a merge commit and you didn't ask to preserve merges. By default, git rebase linearizes the history and eliminates merges. It's just stacking commits on top of the base as if they were fresh patches.

You can preserve merges with -p and then you'll get all commits.

$ git rebase -i initial
pick bde29dc f1
pick c9a9c74 2
pick e5565c5 f2
pick 7f18c18 f3
pick ffdd62c 3
pick 35d8ae9 f4

# Rebase aa61de7..35d8ae9 onto aa61de7 (6 commands)

Can I only squash "f3" without further changes on the whole branch structure?

Yes, but whereas before you were squashing f3 into 2, if you want to preserve the structure you have to squash f3 into f2 because that's the immediately preceding commit, topologically.

pick bde29dc f1
pick c9a9c74 2
pick e5565c5 f2
squash 7f18c18 f3
pick ffdd62c 3
pick 35d8ae9 f4

You'll wind up with this.

*  (HEAD -> feature) f4
|\  
| *  (master) 3
* |  f2
|\ \  
| |/  
| *  2
* |  f1
|/  
* (tag: initial) 1

Though it doesn't make much sense to squash a commit into a merge commit.

If you instead reorder the commits so you can squash f3 into 2...

pick bde29dc f1
pick c9a9c74 2
squash 7f18c18 f3
pick e5565c5 f2
pick ffdd62c 3
pick 35d8ae9 f4

...you'll wind up with... something odd.

*   (HEAD -> feature) f4
|\  
| * 3
|/  
* f3
* 2
* (tag: initial) 1

Attempting to move commits between branches with git rebase -i -p is going to get confusing because git rebase -i presents you with a linear view of a non-linear history. Pretending non-linear history is linear is where Git gets confusing.


But you probably shouldn't be worrying about preserving all that. Keeping Git history mostly linear makes everything much simpler. Here's how to do it.

Your history is probably the result of updating feature with git merge master. Here's a better way to see that.

1 -- 2 ------- 3 [master]
 \    \         \
  f1 - f2 - f3 - f4 [feature]

f2 and f4 are of no real value; they're updates from master and contain no interesting content. They just make the history confusing. Only f1 and f3 contain actual content. You'd be better off eliminating those update merges with git rebase master and getting a linear history.

$ git rebase master

1 - 2 - 3 [master]
         \
         f1 - f3 [feature]

Then everything is clear and simple. Continue to keep your history simple by updating branches with git rebase master rather than git merge master.

# After more updates with `git rebase master`
1 - 2 - 3 - 4 - 5 [master]
                 \
                  f1 - f3 - f5 - f6 [feature]

When you're ready to merge feature into master this is when you want a merge commit. Use git merge --no-ff feature to force a merge commit and then delete feature.

$ git merge --no-ff feature
$ git branch -d feature

1 - 2 - 3 - 4 - 5 ------------------ 6 [master]
                 \                 /
                  f1 - f3 - f5 - f6

This will keep the branch in the history without having to keep the branch head around. This makes it clear for future code archeologists that f1 - f3 - f5 - f6 were all developed for a single feature.

Upvotes: 2

Related Questions