Working with Git branches: best practices

This is a walk-though article. Covered in this article:

  • Git merging
  • Git rebasing
  • Git merging with squashing commits into one
  • Git merging with fast forwarding commits
  • Deleting old commits
  • Visualizing commits

When working with Git branches you need to check if your current feature branch deviated from the main branch (e.g. master) and bring those changes in from master branch.

Ideally, this is done pretty often, as it would allow you to easily merge the feature branch into master in the future. If you keep postponing master-to-feature merges then at some point there will be too many commits to deal with and the merge will be pretty painful.

Let’s review git log command that will be very helpful in visualizing our commits:
git log --oneline --graph --all

This comand will show you where your local feature branch is vs origin, and vs master, origin/master, and other branches and refs.

Note that this will not show you where you are with regards to the remote repository though – in order to see if there are any commits done by other people you need to execute git fetch and then re-run the above git log command.

Merging or rebasing? How to get a clean clean commit history.

When you look at the output of the log command, you may realize that your feature branch is missing some new commits on the master branch.
At this point you could merge master into the feature branch or you can rebase you feature branch to point to the latest commit on the master – depending on your actual needs.

The benefits of the rebasing are that you can incorporate the changes from the parent branch – for example bug fixes, and that you are avoiding unnecessary merge commits – giving you a clean commit history.
When rebasing, git will calculate the diffs between each commit on the feature branch, and apply the diffs to the new parent commit.
Since rebase is a form of merge, you may get conflicts that will need to be resolved.

You can rebase either by issuing
git rebase master featureBranch
or you can break this into two steps:

git checkout featureBranch
git rebase master

If you have a conflict, you can either fix the conflict, add fixed files to the staging area and issue “rebase –continue” command; or you can abort the rebase altogether via “rebase –abort“.

Critical to understand: once you have pushed your feature branch commits to the remote, you should not rebase them later on.

Let’s take a look at a rebase scenario.

Before rebase:

We are on uat – which is our ‘master’ branch

* 14beda5 (origin/uat, uat) password reset functionality verified
* ca01422 added start-task
* 75da58b added access logging and IP/user logging
* 9de1d28 add source docs list display on home page
* d220e80 fixing error notification in edit screen when validation fails
* c77b421 fixing timezone issue for created, modified, closed fields
* 24654a6 make short summary longer (600), redirect newly created items to edit screen
* bc5ba99 delete orphan migration
* bd6897e adjust gitignore
* 4bac52d Remove two dates and make them dynamic, reorganize sections and some fields bug fixes
* a2937af reorder some fields in init form

And wip/featureBranch is our ‘feature’ branch

* e6cbb43 (wip/featureBranch) bkp admin emails line
* c017b40 working on user admin part
* 3c6a4c5 (origin/master, origin/HEAD, master) cleanup migrations
* a5d0704 fix lookup population code
* d466a7a add email logic for admins to approve new users
* 4bac52d Remove two dates and make them dynamic, reorganize sections and some fields bug fixes
* a2937af reorder some fields in init form

Now doing REBASE: git rebase

/repo1 (wip/featureBranch)
$ git rebase uat -v
Changes from 4bac52d8dcd509591dd47e404bb846bdd4cc10e1 to 14beda55766d19e23ac580a48ff2b012498546dd:
 .gitignore                                         |   4 +-
 app/__init__.py                                    |  19 +++--
 app/main/forms.py                                  |   2 +-
 ...
 20 files changed, 191 insertions(+), 126 deletions(-)
First, rewinding head to replay your work on top of it...
Applying: add email logic for admins to approve new users
.git/rebase-apply/patch:277: trailing whitespace.
        FROM Data_Table_T1
.git/rebase-apply/patch:291: trailing whitespace.
        FROM Data_Table_T1
.git/rebase-apply/patch:350: trailing whitespace.
        FROM Data_Table_T1
warning: 3 lines add whitespace errors.
Using index info to reconstruct a base tree...
M       app/models.py
Falling back to patching base and 3-way merge...
Auto-merging config.py
CONFLICT (content): Merge conflict in config.py
Auto-merging app/templates/auth/reset_password.html
CONFLICT (content): Merge conflict in app/templates/auth/reset_password.html
Auto-merging app/models.py
error: Failed to merge in the changes.
hint: Use 'git am --show-current-patch' to see the failed patch
Patch failed at 0001 add email logic for admins to approve new users
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".

As we can see the REBASE failed to complete auto-merging.

When we check the status, Git is clear about what’s happening: rebase in progress, we are on featureBranch, rebasing commit 1/5.

/repo1 (wip/featureBranch|REBASE 1/5)
$ git status
rebase in progress; onto 14beda5
You are currently rebasing branch 'wip/featureBranch' on '14beda5'.
  (fix conflicts and then run "git rebase --continue")
  (use "git rebase --skip" to skip this patch)
  (use "git rebase --abort" to check out the original branch)

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        modified:   app/auth/email.py
        ...
        new file:   app/templates/auth/approve_user.html
        ...

Unmerged paths:
  (use "git reset HEAD <file>..." to unstage)
  (use "git add <file>..." to mark resolution)

        both modified:   app/templates/auth/reset_password.html
        both modified:   config.py

We now fix the conflict in app/templates/auth/reset_password.html, config.py; and then we do “git add .”

git add .

When you fix your conflicts, you can continue the rebase.

Note that git is very informative – when you ask for status, it tells you that you are in the middle of a rebase and the source’s top is commit with id 14beda5:
rebase in progress; onto 14beda5

And your current branch is displayed as “wip/featureBranch|REBASE 1/5

/repo1 (wip/featureBranch|REBASE 1/5)
$ git rebase --continue
Applying: add email logic for admins to approve new users
Applying: fix lookup population code
Applying: cleanup migrations
Using index info to reconstruct a base tree...
A       migrations/versions/e40552ebfb59_nvarchar_change.py
Falling back to patching base and 3-way merge...
Removing migrations-mysql/versions/dae1274643e5_add_m_to_m_rel.py
...
Applying: working on user admin part
.git/rebase-apply/patch:135: new blank line at EOF.
+
warning: 1 line adds whitespace errors.
Using index info to reconstruct a base tree...
M       .gitignore
M       app/models.py
Falling back to patching base and 3-way merge...
Auto-merging app/models.py
Auto-merging .gitignore
CONFLICT (content): Merge conflict in .gitignore
error: Failed to merge in the changes.
hint: Use 'git am --show-current-patch' to see the failed patch
Patch failed at 0004 working on user admin part
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".

As we can see the REBASE auto-merge was successful for commits #2, #3, but failed to complete auto-merging for commit #4.

When we check the status, Git is clear about what’s happening: rebase in progress, we are on featureBranch, rebasing commit 4/5.

/repo1 (wip/featureBranch|REBASE 4/5)
$ git status
rebase in progress; onto 14beda5
You are currently rebasing branch 'wip/featureBranch' on '14beda5'.
  (fix conflicts and then run "git rebase --continue")
  (use "git rebase --skip" to skip this patch)
  (use "git rebase --abort" to check out the original branch)

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        modified:   app/auth/forms.py
        ...
        new file:   migrations/versions/f00d5c2854ab_add_user_role_column.py
        ...

Unmerged paths:
  (use "git reset HEAD <file>..." to unstage)
  (use "git add <file>..." to mark resolution)

        both modified:   .gitignore

So we fix conflict in .gitignore, then do “git add .gitignore” and continure rebasing via “git rebase –continue”

    git add .gitignore
/repo1 (wip/featureBranch|REBASE 4/5)
$ git rebase --continue
Applying: working on user admin part
Applying: bkp admin emails line
Using index info to reconstruct a base tree...
M       config.py
Falling back to patching base and 3-way merge...
Auto-merging config.py
CONFLICT (content): Merge conflict in config.py
error: Failed to merge in the changes.
hint: Use 'git am --show-current-patch' to see the failed patch
Patch failed at 0005 bkp admin emails line
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".

As we can see the REBASE failed to complete auto-merging one more time. When we check the status, Git is clear about what’s happening: rebase in progress, we are on featureBranch, rebasing commit 5/5.

So we fix conflict in config.py, then do “git add .”

/repo1 (wip/featureBranch|REBASE 5/5)
$ vi config.py

/repo1 (wip/featureBranch|REBASE 5/5)
$ git add .

Let’s show explicit status and then continue the rebase via “git rebase –continue”

/repo1 (wip/featureBranch|REBASE 5/5)
$ git status
rebase in progress; onto 14beda5
You are currently rebasing branch 'wip/featureBranch' on '14beda5'.
  (all conflicts fixed: run "git rebase --continue")

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        modified:   config.py
/repo1 (wip/featureBranch|REBASE 5/5)
$ git rebase --continue
Applying: bkp admin emails line

Now rebase is done:

/repo1 (wip/featureBranch)
$ git status
On branch wip/featureBranch
Your branch and 'origin/wip/featureBranch' have diverged,
and have 14 and 5 different commits each, respectively.
  (use "git pull" to merge the remote branch into yours)

nothing to commit, working tree clean

Now let’s run “git log” command to visualize our commits

Look at wip/featureBranch – feature branch now:

$ git log --oneline --graph --all
* 4572c9c (HEAD -> wip/featureBranch) bkp admin emails line
* 7589f4d working on user admin part
* a38fb50 cleanup migrations
* e3045f1 fix lookup population code
* bce74c6 add email logic for admins to approve new users
    * 14beda5 (origin/uat, uat) password reset functionality verified
    * ca01422 added start-task
    * 75da58b added access logging and IP/user logging
    * 9de1d28 add source docs list display on home page
    * d220e80 fixing error notification in edit screen when validation fails
    * c77b421 fixing timezone issue for created, modified, closed fields
    * 24654a6 make short summary longer (600), redirect newly created items to edit screen
    * bc5ba99 delete orphan migration
    * bd6897e adjust gitignore
* 4bac52d Remove two dates and make them dynamic, reorganize sections and some fields bug fixes
* a2937af reorder some fields in init form

And compare to wip/featureBranch – feature branch before the REBASE:

* e6cbb43 (wip/featureBranch) bkp admin emails line
* c017b40 working on user admin part
* 3c6a4c5 (origin/master, origin/HEAD, master) cleanup migrations
* a5d0704 fix lookup population code
* d466a7a add email logic for admins to approve new users
* 4bac52d Remove two dates and make them dynamic, reorganize sections and some fields bug fixes
* a2937af reorder some fields in init form

Now you can see that:

  • 14beda5 (origin/uat, uat) commits have been replayed on your feature branch and they retained their SHA1 IDs,
  • but the commits from the feature branch that happened after the initial checkout from master gained new SHA1 ids and have been placed after the 14beda5 base.

We have rebased the feature branch from 4bac52d to 14beda5 commit.