Git-Mastery: Lessons

Tour 7: Keeping Branches in Sync

Target Usage: To keep branches in a local repository synchronized 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., to take advantage of a bug fix introduced in another branch).

Lesson plan:

Merging is one way to keep one branch synchronized with another.

   T7L1. Merging to Sync Branches covers that part.

Rebasing is another way to sync one branch with another.

   T7L2. Rebasing to Sync Branches covers that part.

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

   T7L3. Copying Specific Commits covers that part.

T7L1. Merging to Sync Branches


Merging is one way to keep one branch synchronized with another.

This lesson covers that part.

When working in parallel branches, you’ll often need to sync (short for synchronize) one branch with 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': 'main'}} }%%
    commit id: "m1"
    branch bug-fix
    branch feature
    commit id: "f1"
    checkout main
    checkout bug-fix
    commit id: "b1"
    checkout main
    merge bug-fix
    checkout feature
    merge main id: "mc1"
    commit id: "f2"
    checkout main
    commit id: "m2"
    checkout feature
    merge main id: "mc2"
    checkout main
    commit id: "m3"
    checkout feature
    commit id: "[feature] f3"
    checkout main
    commit id: "[HEAD → main] m4"

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

HANDS-ON: Merge periodically to sync a branch

Preparation Run the following commands to create a sample repo that we'll use for this hands-on practical:

mkdir samplerepo-sync
cd samplerepo-sync
git init -b main

echo "v1" > app.txt
git add .
git commit -m "m1: initial commit"

git branch bug-fix
git switch -c feature
echo "new feature" >> feature.txt
git add .
git commit -m "f1: start feature"

git switch bug-fix
echo "fix" >> app.txt
git commit -am "b1: fix a bug"

Target Bring the fix from bug-fix into main, then sync feature with main, so that feature also gets the fix.

1 Merge bug-fix into main.

CLI
git switch main
git merge bug-fix
Sourcetree

Switch to the main branch, then right-click on the bug-fix branch and choose merge bug-fix into the current branch.

Because main had not diverged from bug-fix, Git fast-forwards main -- no merge commit is created.

2 Sync feature with main by merging main into feature.

CLI
git switch feature
git merge main
Sourcetree

Switch to the feature branch, then right-click on the main branch and choose merge main into the current branch.

This time, feature and main have diverged (each has a commit the other lacks), so Git creates a merge commit. Run git log --oneline --graph --all to see that feature now contains the fix from b1, alongside its own commit f1.

You could repeat this merge periodically -- for example, each time main gets a new commit -- to keep feature up to date, as illustrated in the revision graph earlier in this lesson.

done!


T7L2. Rebasing to Sync Branches


Rebasing is another way to sync one branch with another.

This lesson covers that part.

Rebasing is another way to synchronize 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 moves the base of your branch to the tip of the other branch (i.e., it 're-bases' it — 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 main, so that changes in commit m2 become visible to the feature branch.

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

If we merge the main branch to the feature branch as shown below, m2 becomes visible to the feature branch. However, it creates a merge commit.

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

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

gitGraph
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'main'}} }%%
    commit id: "m1"
    checkout main
    commit id: "[branch: main] 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 from 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, you should avoid rebasing branches that you’ve already published and that others might be using -- rewriting published history can cause confusion and conflicts for those using the previous version of the commits.

HANDS-ON: Rebase a branch to sync it

Preparation Run the following commands to create a sample repo that we'll use for this hands-on practical:

mkdir samplerepo-sync-rebase
cd samplerepo-sync-rebase
git init -b main

echo "v1" > app.txt
git add .
git commit -m "m1: initial commit"

git switch -c feature
echo "feature work" >> feature.txt
git add .
git commit -m "f1: start feature"
echo "more feature work" >> feature.txt
git commit -am "f2: continue feature"

git switch main
echo "fix" >> app.txt
git commit -am "m2: fix a bug"

Run git log --oneline --graph --all to see that feature (with f1 and f2) branched off before m2 was added to main.

Target Rebase feature onto main, so that m2 becomes part of feature's history.

1 Switch to feature, then rebase it onto main.

CLI
git switch feature
git rebase main
Sourcetree

Switch to the feature branch, then right-click on the main branch and choose Rebase current changes onto main.

2 Run git log --oneline --graph --all again. Observe that main and feature now form a single straight line, with f1 and f2 replayed on top of m2. Their commit SHAs have changed, even though the file changes are the same.

If Git cannot automatically combine a replayed commit -- for example, if feature and main had modified the same lines -- Git pauses the rebase partway and marks the conflict in the affected files, similar to a merge conflict. Resolve the conflict, stage the fixed files with git add, then run git rebase --continue to resume (or git rebase --abort to cancel and return feature to its pre-rebase state).

done!


T7L3. Copying Specific Commits


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

This lesson covers that part.

Cherry-picking is another way to synchronize 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 main branch) onto the feature branch.

gitGraph
    %%{init: { 'theme': 'default', 'gitGraph': {'mainBranchName': 'main'}} }%%
    commit id: "m1"
    branch feature
    checkout feature
    commit id: "f1"
    checkout main
    commit id: "m2"
    commit id: "m3" type: HIGHLIGHT
    commit id: "[main] 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': 'main'}} }%%
    commit id: "m1"
    branch feature
    checkout feature
    commit id: "f1"
    checkout main
    commit id: "m2"
    commit id: "m3" type: HIGHLIGHT
    commit id: "[main] m4"
    checkout feature
    commit id: "f2"
    commit id: "[HEAD → feature] m3a" type: HIGHLIGHT

Note how it makes the changes from m3 available from that point on in the feature branch, with minimal changes to the revision graph. Also note that the new commit m3a contains the same changes as m3, but it will be 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 conflict with the changes in the receiving branch.

HANDS-ON: Cherry-pick a commit

Preparation Run the following commands to create a sample repo that we'll use for this hands-on practical:

mkdir samplerepo-sync-cherry
cd samplerepo-sync-cherry
git init -b main

echo "v1" > app.txt
git add .
git commit -m "m1: initial commit"

git switch -c feature
echo "feature work" >> feature.txt
git add .
git commit -m "f1: start feature"

git switch main
echo "update" > update.txt
git add .
git commit -m "m2: update app"

echo "urgent fix" > bugfix.txt
git add .
git commit -m "m3: urgent bug fix"

echo "more" > more.txt
git add .
git commit -m "m4: more work"

git switch feature
echo "more feature work" >> feature.txt
git commit -am "f2: continue feature"

Target Bring only the m3: urgent bug fix commit from main into feature, without bringing m2 or m4.

1 Find the SHA of the m3: urgent bug fix commit.

git log --oneline main
a1b2c3d (main) m4: more work
9f8e7d6 m3: urgent bug fix
5c4b3a2 m2: update app
1a2b3c4 m1: initial commit

Note down the SHA next to m3: urgent bug fix (here, 9f8e7d6 -- yours will differ).

2 While on feature, cherry-pick that commit.

CLI
git switch feature
git cherry-pick 9f8e7d6
Sourcetree

Switch to the feature branch. In the main branch's history, right-click on the m3: urgent bug fix commit and choose Cherry Pick.

Run git log --oneline on feature. It now has a new commit with the same change and message as m3, but a different SHA -- and it does not have m2 or m4.

done!


At this point: You should now be able to bring changes from one branch to another in your local repository.

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