Skip to content

Instantly share code, notes, and snippets.

@canton7
Created January 6, 2012 13:45
Show Gist options
  • Save canton7/1570681 to your computer and use it in GitHub Desktop.
Save canton7/1570681 to your computer and use it in GitHub Desktop.
Workflows for long-running feature branches

Workflows for long-running feature branches

These examples were created during a discussion with j416 on #github about how best to manage long-running feature branches.

The scenario is this: You have a long-running feature branch, based off master. The feature branch and master both touch the same parts of the same files, so you know that there will be conflicts when you finally merge the feature branch in. What can you do to minimise the conflicts?

The simplest answer is to rebase the feature branch frequently onto master. This will work fine, so long as the feature branch isn't public. If it is, you get the standard rewriting-public-history problem. Another consideration is that, if the feature branch is large, it might take a long time to rebase.

The other option is to merge master into the feature branch frequently. This gist shows how this approach works, and

This gist has four examples:

  1. The feature branch is never merged into.
  2. Master is merged into the feature branch.
  3. Master is merged into the feature branch, then the feature branch is rebased onto master before it's merged into master.
  4. The above, but with the help of git-rerere.

In all of the examples, master makes a single change, A1, which modifies the two files, file1 and file2. The feature branch makes two changes, B1 (which modifies file1) and B2 (which modifies file2).

A1 and B1 conflict, as do A1 and B2.

Example 1: The feature branch is never merged into

Here, master makes the change A1, and the feature branch makes the changes B1 and B2. When the feature branch is merged into master, the conflicts A1/B1 (in file1) and A1/B2 (in file2) both occur.

Example 2: Master is merged into the feature branch

Here, master makes the change A1, and the feature branch makes the change B1. Master is then merged into feature, causing A1/B1 to conflict in file1. The feature branch then makes the change B2, and the feature branch is merged into master without conflict.

Example 3: Master is merged into the feature branch, then the feature branch is rebased onto master before it's merged into master

This example is largely the same as above. However, before the final merge, the feature branch is rebased on top of master. This results in the merges into the feature branch being stripped, and the history is nice and linear (git-flow style). The downside is that the conflict A1/B1 must be resolved twice -- once on the merge into the feature branch, and once on the rebase.

Example 4: The above, but with the help of git-rerere

This example is exactly the same as before, except we use git-rerere to avoid the second merge conflict. Git-rerere automatically records the result of the first merge conflict, and then uses this to automatically resolve the second one (the results of the rebase).

git $ git init j416_test
Initialized empty Git repository in <blah>/git/j416_test/.git
git $ cd j416_test
j416_test (master) $ git commit --allow-empty -m "Initial commit"
[master (root-commit) 611c451] Initial commit
j416_test (master) $ touch file1 file2
j416_test (master) $ git add file1 file2
j416_test (master) $ git commit -m "Initial content"
[master 39f5b84] Initial content
0 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 file1
create mode 100644 file2
j416_test (master) $ git branch feature
j416_test (master) $ echo A1 > file1
j416_test (master) $ echo A1 > file2
j416_test (master) $ git commit -am "A1"
[master 1683701] A1
2 files changed, 2 insertions(+), 0 deletions(-)
j416_test (master) $ git checkout feature
Switched to branch 'feature'
j416_test (feature) $ echo B1 > file1
j416_test (feature) $ git commit -am "B1"
[feature 67129fc] B1
1 files changed, 1 insertions(+), 0 deletions(-)
j416_test (feature) $ echo B2 > file2
j416_test (feature) $ git commit -am "B2"
[feature dcd3ded] B2
1 files changed, 1 insertions(+), 0 deletions(-)
j416_test (feature) $ git checkout master
Switched to branch 'master'
j416_test (master) $ git merge --no-ff feature
Auto-merging file1
CONFLICT (content): Merge conflict in file1
Auto-merging file2
CONFLICT (content): Merge conflict in file2
Automatic merge failed; fix conflicts and then commit the result.
j416_test (master|MERGING) $ git st
# On branch master
# Unmerged paths:
# (use "git add/rm <file>..." as appropriate to mark resolution)
#
# both modified: file1
# both modified: file2
#
no changes added to commit (use "git add" and/or "git commit -a")
j416_test (master|MERGING) $
# As you can see, two conflicts A1/B1 and A1/B2
git $ git init j416_test
Initialized empty Git repository in <blah>/j416_test/.git
git $ cd j416_test/
j416_test (master) $ git commit --allow-empty -m "Initial commit"
[master (root-commit) 6439bf7] Initial commit
j416_test (master) $ touch file1 file2
j416_test (master) $ git add file1 file2
j416_test (master) $ git commit -m "Initial content"
[master 91de979] Initial content
0 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 file1
create mode 100644 file2
j416_test (master) $ git branch feature
j416_test (master) $ echo A1 > file1
j416_test (master) $ echo A1 > file2
j416_test (master) $ git commit -am "A1"
[master 21e8834] A1
2 files changed, 2 insertions(+), 0 deletions(-)
j416_test (master) $ git checkout feature
Switched to branch 'feature'
j416_test (feature) $ echo B1 > file1
j416_test (feature) $ git commit -am "B1"
[feature edbee1c] B1
1 files changed, 1 insertions(+), 0 deletions(-)
j416_test (feature) $ git merge master
Auto-merging file1
CONFLICT (content): Merge conflict in file1
Automatic merge failed; fix conflicts and then commit the result.
j416_test (feature|MERGING) $ git st
# On branch feature
# Changes to be committed:
#
# modified: file2
#
# Unmerged paths:
# (use "git add/rm <file>..." as appropriate to mark resolution)
#
# both modified: file1
#
j416_test (feature|MERGING) $ echo B1 > file1 # by way of conflict resolution
j416_test (feature|MERGING) $ git add file1
j416_test (feature|MERGING) $ git commit
[feature 88505f2] Merge branch 'master' into feature
j416_test (feature) $ echo B2 > file2
j416_test (feature) $ git commit -am "B2"
[feature 4589026] B2
1 files changed, 1 insertions(+), 1 deletions(-)
j416_test (feature) $ git checkout master
Switched to branch 'master'
j416_test (master) $ git merge --no-ff feature
Updating 21e8834..4589026
Fast-forward
file1 | 2 +-
file2 | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
j416_test (master) $ cat file1 file2
B1 B2
j416_test (master) $ git log --graph --decorate --oneline --all
* 85a0b70 (HEAD, master) Merge branch 'feature'
|\
| * 4589026 (feature) B2
| * 88505f2 Merge branch 'master' into feature
| |\
| |/
|/|
* | 21e8834 A1
| * edbee1c B1
|/
* 91de979 Initial content
* 6439bf7 Initial commit
git $ git init j416_test
Initialized empty Git repository in <blah>/j416_test/.git
git $ cd j416_test
j416_test (master) $ git commit --allow-empty -m "Initial commit"
[master (root-commit) 1eff68a] Initial commit
j416_test (master) $ touch file1 file2
j416_test (master) $ git add file1 file2
j416_test (master) $ git commit -m "Initial content"
[master 4773ffe] Initial content
0 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 file1
create mode 100644 file2
j416_test (master) $ git branch feature
j416_test (master) $ echo A1 > file1
j416_test (master) $ echo A1 > file2
j416_test (master) $ git commit -am "A1"
[master f19dec2] A1
2 files changed, 2 insertions(+), 0 deletions(-)
j416_test (master) $ git checkout feature
Switched to branch 'feature'
j416_test (feature) $ echo B1 > file1
j416_test (feature) $ git commit -am "B1"
[feature ca56202] B1
1 files changed, 1 insertions(+), 0 deletions(-)
j416_test (feature) $ git merge master
Auto-merging file1
CONFLICT (content): Merge conflict in file1
Automatic merge failed; fix conflicts and then commit the result.
j416_test (feature|MERGING) $ echo B1 > file1
j416_test (feature|MERGING) $ git add file1
j416_test (feature|MERGING) $ git commit
[feature d2d7bfc] Merge branch 'master' into feature
j416_test (feature) $ echo B2 > file2
j416_test (feature) $ git commit -am "B2"
[feature d8e987e] B2
1 files changed, 1 insertions(+), 1 deletions(-)
j416_test (feature) $ git rebase master
First, rewinding head to replay your work on top of it...
Applying: B1
Using index info to reconstruct a base tree...
Falling back to patching base and 3-way merge...
Auto-merging file1
CONFLICT (content): Merge conflict in file1
Failed to merge in the changes.
Patch failed at 0001 B1
When you have resolved this problem run "git rebase --continue".
If you would prefer to skip this patch, instead run "git rebase --skip".
To restore the original branch and stop rebasing run "git rebase --abort".
j416_test ((f19dec2...)|REBASE) $ git st
# Not currently on any branch.
# Unmerged paths:
# (use "git reset HEAD <file>..." to unstage)
# (use "git add/rm <file>..." as appropriate to mark resolution)
#
# both modified: file1
#
no changes added to commit (use "git add" and/or "git commit -a")
j416_test ((f19dec2...)|REBASE) $ echo B1 > file1 # Resolve the conflict
j416_test ((f19dec2...)|REBASE) $ git add file1
j416_test ((f19dec2...)|REBASE) $ git rebase --continue
Applying: B1
Applying: B2
j416_test (feature) $ git checkout master
Switched to branch 'master'
j416_test (master) $ git merge --no-ff feature
Merge made by recursive.
file1 | 2 +-
file2 | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
j416_test (master) $ git log --graph --oneline --decorate --all
* 9e0d666 (HEAD, master) Merge branch 'feature'
|\
| * f747817 (feature) B2
| * 499ba9a B1
|/
* f19dec2 A1
* 4773ffe Initial content
* 1eff68a Initial commit
git $ git init j416_test
Initialized empty Git repository in <blah>/j416_test/.git
git $ cd j416_test
j416_test (master) $ git config rerere.enabled true
j416_test (master) $ git config rerere.autoupdate true
j416_test (master) $ git commit --allow-empty -m "Initial commit"
[master (root-commit) d63ab0c] Initial commit
j416_test (master) $ touch file1 file2
j416_test (master) $ git add file1 file2
j416_test (master) $ git commit -m "Initial content"
[master 3c7afb5] Initial content
0 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 file1
create mode 100644 file2
j416_test (master) $ git branch feature
j416_test (master) $ echo A1 > file1
j416_test (master) $ echo A1 > file2
j416_test (master) $ git commit -am "A1"
[master 090ff8d] A1
2 files changed, 2 insertions(+), 0 deletions(-)
j416_test (master) $ git checkout feature
Switched to branch 'feature'
j416_test (feature) $ echo B1 > file1
j416_test (feature) $ git commit -am "B1"
[feature 810f60d] B1
1 files changed, 1 insertions(+), 0 deletions(-)
j416_test (feature) $ git merge master
Auto-merging file1
CONFLICT (content): Merge conflict in file1
Automatic merge failed; fix conflicts and then commit the result.
j416_test (feature|MERGING) $ git st
# On branch feature
# Changes to be committed:
#
# modified: file2
#
# Unmerged paths:
# (use "git add/rm <file>..." as appropriate to mark resolution)
#
# both modified: file1
#
j416_test (feature|MERGING) $ echo B1 > file1 # by way of conflict resolution
j416_test (feature|MERGING) $ git add file1
j416_test (feature|MERGING) $ git commit
[feature 87d1c9a] Merge branch 'master' into feature
j416_test (feature) $ ls .git/
hooks/ info/ logs/ objects/ refs/ rr-cache/
j416_test (feature) $ echo B2 > file2
j416_test (feature) $ git commit -am "B2"
[feature 41bf61a] B2
1 files changed, 1 insertions(+), 1 deletions(-)
j416_test (feature) $ git rebase master
First, rewinding head to replay your work on top of it...
Applying: B1
Using index info to reconstruct a base tree...
Falling back to patching base and 3-way merge...
Auto-merging file1
CONFLICT (content): Merge conflict in file1
Staged 'file1' using previous resolution.
Failed to merge in the changes.
Patch failed at 0001 B1
When you have resolved this problem run "git rebase --continue".
If you would prefer to skip this patch, instead run "git rebase --skip".
To restore the original branch and stop rebasing run "git rebase --abort".
j416_test ((090ff8d...)|REBASE) $ git st
# Not currently on any branch.
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# modified: file1
#
j416_test ((090ff8d...)|REBASE) $ git rebase --continue
Applying: B1
Applying: B2
j416_test (feature) $ git checkout master
Switched to branch 'master'
j416_test (master) $ git merge --no-ff feature
Merge made by recursive.
file1 | 2 +-
file2 | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
j416_test (master) $ git log --graph --decorate --oneline --all
* 8aabdfc (HEAD, master) Merge branch 'feature'
|\
| * 727a3df (feature) B2
| * d8f3d52 B1
|/
* 090ff8d A1
* 3c7afb5 Initial content
* d63ab0c Initial commit
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment