Software Engineering for Self-Directed Learners »

Tour 7: Keeping Branches in Sync

Destination: To be able to keep branches in a local repository synchronised with each other, as needed.

Motivation: While working on one branch, you might want to have access to changes introduced in another branch (e.g., take advantage of a bug fix introduced in another branch).

Lesson plan:

Merging is one way to keep one branch synchronise itself with changes introduced into another.

   Lesson: Merging to Sync Branches covers that part.

Rebasing is another way to synchronise one branch with another.

   Lesson: Rebasing to Sync Branches covers that part.

Cherry-picking is a Git operation that copies over a specific commit from one branch to another.

   Lesson: Copying Specific Commits covers that part.

Lesson: Merging to Sync Branches


Merging is one way to keep one branch synchronise itself with changes introduced into another.

This lesson covers that part.

When working in parallel branches, you’ll often need to sync (short for synchronise) one branch with changes made in another. For example, while developing a feature in one branch, you might want to bring in a recent bug fix from another branch that your branch doesn’t yet have.

The simplest way to sync branches is to merge — that is, to sync a branch b1 with changes from another branch b2, you merge b2 into b1. In fact, you can merge them periodically to keep one branch up to date with the other.

gitGraph
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "m1"
    branch bug-fix
    branch feature
    commit id: "f1"
    checkout master
    checkout bug-fix
    commit id: "b1"
    checkout master
    merge bug-fix
    checkout feature
    merge master id: "mc1"
    commit id: "f2"
    checkout master
    commit id: "m2"
    checkout feature
    merge master id: "mc2"
    checkout master
    commit id: "m3"
    checkout feature
    commit id: "[feature] f3"
    checkout master
    commit id: "[HEAD → master] m4"

In the example above, you can see how the feature branch is merging the master branch periodically to keep itself in sync with the changes being introduced to the master branch.


Lesson: Rebasing to Sync Branches


Rebasing is another way to synchronise one branch with another.

This lesson covers that part.

Rebasing is another way to synchronise one branch with another, while keeping the history cleaner and more linear. Instead of creating a merge commit to combine the branches, rebasing moves the entire sequence of commits from your branch and "replays" them on top of another branch. This effectively relocates the base of your branch to the tip of the other branch (i.e., 're-base', hence the name), as if you had started your work from there in the first place.

Rebasing is especially useful when you want to update your branch with the latest changes from a main branch, but you prefer an uncluttered history with fewer merge commits.

Suppose we have the following revision graph, and we want to sync the feature branch with master, so that changes in commit m2 become visible to the feature branch.

gitGraph
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "m1"
    branch feature
    checkout feature
    commit id: "f1"
    checkout master
    commit id: "[master] m2"
    checkout feature
    commit id: "[HEAD → feature] f2"

If we merge the master branch to the feature branch as given below, m2 become visible to feature branch. However, it creates a merge commit.

gitGraph
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "m1"
    branch feature
    checkout feature
    commit id: "f1"
    checkout master
    commit id: "[master] m2"
    checkout feature
    commit id: "f2"
    merge master id: "[HEAD → feature] mc1"

Instead of merging, if we rebased the feature branch on the master branch, we would get the following.

gitGraph
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "m1"
    checkout master
    commit id: "[branch: master] m2"
    branch feature
    checkout feature
    commit id: "f1a"
    commit id: "[HEAD → feature] f2a"

Note how the rebasing changed the base of the feature branch from m1 to m2. As a result, changes done in m2 are now visible to the feature branch. But there is no merge commit, and the revision graph is simpler.

Also note how the first commit in the feature branch, previously shown as f1, is now shown as f1a after the rebase. Although both commits contain the same changes, other details — such as the parent commit — are different, making them two distinct Git objects with different SHA values. Similarly, f2 and f2a are also different. Thus, the history of the entire feature branch has changed after the rebase.

Because rebasing rewrites the commit history of your branch, it's important to use it carefully. You should avoid rebasing branches that you’ve already shared with others, because rewriting published history can cause confusion and conflicts for anyone else working on the same branch.

force-pushing: If you modify past commits that have been pushed to a remote repository, you'll have to force-push the modified commit to the remote repo in order to update the commits in it.


Lesson: Copying Specific Commits


Cherry-picking is a Git operation that copies over a specific commit from one branch to another.

This lesson covers that part.

Cherry-picking is another way to synchronise branches, by applying specific commits from one branch onto another.

Unlike merging or rebasing — which bring over all changes since the branches diverged — cherry-picking lets you choose individual commits and apply just those, one at a time, to your current branch. This is useful when you want to bring over a bug fix or a small feature from another branch without merging the entire branch history.

Because cherry-picking copies only the chosen commits, it creates new commits on your branch with the same changes but different SHA values.

Suppose we have the following revision graph, and we want to bring over the changes introduced in m3 (in the master branch) onto the feature branch.

gitGraph
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "m1"
    branch feature
    checkout feature
    commit id: "f1"
    checkout master
    commit id: "m2"
    commit id: "m3" type: HIGHLIGHT
    commit id: "[master] m4"
    checkout feature
    commit id: "[HEAD → feature] f2"

After cherry-picking m3 onto the feature branch, the revision graph should look like the following:

gitGraph
%%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'master'}} }%%
    commit id: "m1"
    branch feature
    checkout feature
    commit id: "f1"
    checkout master
    commit id: "m2"
    commit id: "m3" type: HIGHLIGHT
    commit id: "[master] m4"
    checkout feature
    commit id: "f3"
    commit id: "[HEAD → feature] m3a" type: HIGHLIGHT

Note how it makes the changes done in m3 available (from now on) in the feature branch, with minimal changes to the revison graph.
Also note that the new commit m3a contains the same changes as m3, but it will a different Git object with a different SHA value.

Cherry-picking is another Git operation that can result in conflicts i.e., if the changes in the cherry-picked commit conflicts with the changes in the receiving branch.


At this point: Now you can bring over changes in one branch to another, in a local repository.

What's next: Tour 8: Working with Remote Branches