DevOps.Academy
CoursesCurriculumPricing

    Loading…

    ↑ ↓ move · ↵ open · Ctrl K anywhere

    ◔My learningSign in
    DevOps.Academy

    Free, hands-on DevOps lessons. Made with care in India.

    Learn

    All coursesLinux roadmapCurriculumMy learning

    Academy

    PricingCertificates

    Legal

    PrivacyTermsContact

    devops.rajeev.pro

    Git & GitHubLesson 6 of 13
    Your progress · 0%
    On this page
    1. Four ways to combine a branch
    2. Fast-forward or merge commit
    3. Rebase: replay your commits
    4. Squash: one commit per change
    5. Handling conflicts
    6. Cherry-pick: one commit
    ↖ Course roadmap
    Lesson 6 · Merge strategies

    Merge, rebase, squash: combining work, and fixing conflicts

    The four ways to bring a branch into main, the history each leaves behind, and when teams use each. How to read and resolve a conflict calmly, and copy one commit with cherry-pick.

    40 min read · intermediate · hands-on

    By the end of this lesson you'll be able to

    0 of 6 complete

    Four ways to combine a branch

    Every method below ends with the same files on main. What differs is the history left behind, and history is what you read when something breaks:

    Same code on main every time. Different history: a graph, a straight line, or one commit per change.© Diagram: DevOps Academy

    Same code on main every time. Different history: a graph, a straight line, or one commit per change.© Diagram: DevOps Academy
    MethodHistory on mainUse when
    Merge commitevery branch commit, plus a merge commityou want the true record of how work happened
    Rebase, then fast-forwardbranch commits, replayed in a straight lineeach commit is meaningful and you want a linear log
    Squashone commit per branchbranches are messy; main should read one change per pull request
    Fast-forwardbranch commits, no extra commitmain hadn't moved

    Fast-forward or merge commit

    You saw both in the Branches lesson. Two options control them:

    $ git merge --no-ff feature/x     # always make a merge commit, even when a fast-forward is possible
    $ git merge --ff-only feature/x   # only fast-forward; refuse if a merge commit would be needed
    
    git merge --no-ff feature/x
    • gitversion control

    --no-ff keeps a visible "this feature was merged here" commit that you can revert as one unit. --ff-only is a safe default for updating a local branch: it never creates surprise merge commits.

    Rebase: replay your commits

    A and B are replayed after 2 as new commits A′ and B′; then main fast-forwards. Same changes, new IDs.© Animation: DevOps Academy

    A and B are replayed after 2 as new commits A′ and B′; then main fast-forwards. Same changes, new IDs.© Animation: DevOps Academy

    git rebase main takes the commits on your branch that aren't on main, sets them aside, moves your branch to the tip of main, and replays them one by one:

    $ git log --oneline --graph --all
    * fbf0837 wip
    * 2022fd0 Return an empty list from search
    * 0d86374 Add search stub
    | * d45f583 Add VERSION
    |/
    *   18269df Merge branch 'feature/discount'
    $ git rebase main
    Successfully rebased and updated refs/heads/feature/search.
    $ git log --oneline --graph -4
    * 735332d wip
    * fa49403 Return an empty list from search
    * d9fd28b Add search stub
    * d45f583 Add VERSION
    
    git log --oneline --graph --all
    • gitversion control
    • loghistory

    The branch now starts after Add VERSION, and merging it into main would be a fast-forward. Look at the IDs: 0d86374 became d9fd28b. Rebase creates new commits. The old ones still exist for a while, but your branch no longer points at them.

    The golden rule

    Only rebase commits that nobody else has. Rebasing your own branch before opening a pull request is fine. Rebasing main, or a branch a teammate has pulled, rewrites commits under their feet, and their next pull turns into a mess. If you must update a pushed branch after a rebase, push with --force-with-lease (the Rewriting history lesson) and tell the people on it.

    Squash: one commit per change

    A branch often ends with commits like "wip", "fix typo", "address review". --squash brings in all the changes but none of those commits, so you write one good commit:

    $ git switch main
    $ git merge --squash feature/search
    Squash commit -- not updating HEAD
     search.py | 2 ++
     1 file changed, 2 insertions(+)
    $ git status --short             # the changes are staged, nothing is committed yet
    A  search.py
    $ git commit -m "Add search"
    
    git switch main
    • gitversion control
    git status --short
    • statuswhat changed
    git commit -m "Add search"
    • commitsave staged changes

    On GitHub you rarely type this: the pull request's Squash and merge button does it. The cost is that main loses the branch's individual commits, so git bisect (the History lesson) can only narrow a bug down to the whole pull request. Small pull requests make that cost tiny.

    Handling conflicts

    A conflict happens when both sides changed the same lines and Git can't know which is right. It stops and asks you:

    $ git merge feature/discount
    Auto-merging config.py
    CONFLICT (content): Merge conflict in config.py
    Automatic merge failed; fix conflicts and then commit the result.
    $ git status --short
    UU config.py
    $ cat config.py
    <<<<<<< HEAD
    PRICE = 120
    =======
    PRICE = 90
    >>>>>>> feature/discount
    TAX = 0.18
    
    git merge feature/discount
    • gitversion control
    git status --short
    • statuswhat changed
    cat config.py
    • catprint a file

    Yours above the line, theirs below. You write the final version and delete the markers.© Diagram: DevOps Academy

    Yours above the line, theirs below. You write the final version and delete the markers.© Diagram: DevOps Academy

    Resolving is always the same four steps:

    1. Understand both changes. Here main raised the price and the branch lowered it. Ask whoever made them if the right answer isn't obvious; never just pick a side to make the conflict go away.
    2. Edit the file to what it should finally say, and delete all three marker lines.
    3. Mark it resolved: git add config.py.
    4. Finish: git commit for a merge, git rebase --continue for a rebase.
    $ git diff                       # during a conflict: both sides, side by side
    $ git merge --abort              # changed your mind: back to before the merge
    $ git rebase --abort             # same, for a rebase
    $ git checkout --ours config.py  # take your side for this whole file
    $ git checkout --theirs config.py
    
    git diff
    • gitversion control

    During a rebase "ours" and "theirs" swap places: Git is replaying your commits onto their branch, so HEAD is main. It confuses everyone once.

    Two settings make conflicts easier:

    $ git config --global merge.conflictStyle zdiff3   # also show the original lines both sides changed
    $ git config --global rerere.enabled true          # remember how you resolved a conflict and reuse it
    
    git config --global merge.conflictStyle zdiff3
    • gitversion control
    • configread or set settings
    • --globalfor your user, in every repo

    With zdiff3 a third block, starting ||||||| and the ID of the common ancestor commit, shows what the line was before either change: PRICE = 100. Seeing that both sides moved away from 100 makes the right answer much easier to work out. In VS Code, the merge editor shows the same three panes.

    Cherry-pick: one commit

    cherry-pick copies the change from one commit onto your current branch, as a new commit. The classic use: a fix landed on main, and a release branch needs only that fix:

    $ git switch release/1.0
    $ git cherry-pick 2cb5a0e
    [release/1.0 ec15333] Add search
     1 file changed, 2 insertions(+)
    $ git cherry-pick -x 2cb5a0e     # -x adds "(cherry picked from commit …)" to the message
    
    git switch release/1.0
    • gitversion control

    Use it for single commits. If you find yourself cherry-picking ten commits, the branches are set up wrong; a merge is probably what you need.

    Pick one strategy as a team

    Mixing styles makes history hard to read. A common setup: squash-merge pull requests into main, keep feature branches short, and rebase your own branch on main before asking for review. Enforce it in the repository settings by enabling only the merge buttons you want (Settings → General → Pull Requests).

    ⌘
    Mini mission

    Resolve a real conflict, then land a clean branch

    Two branches change the same price on main. Merge them, resolve the conflict to the price the business agreed (110), and verify. Then rebase a second feature branch onto the updated main and squash it into a single commit.

    Input: Any empty folderOutput: main with the agreed price and one 'Add search' commit
    Reveal one safe solution
    $ git init shop && cd shop
    $ printf 'PRICE = 100\nTAX = 0.18\n' > config.py && git add . && git commit -m "Add pricing config"
    $ git switch -c feature/discount && sed -i 's/PRICE = 100/PRICE = 90/' config.py && git commit -am "Lower the price to 90"
    $ git switch main && sed -i 's/PRICE = 100/PRICE = 120/' config.py && git commit -am "Raise the price to 120"
    $ git merge feature/discount                       # CONFLICT
    $ printf 'PRICE = 110\nTAX = 0.18\n' > config.py   # the agreed price, no markers
    $ git add config.py && git commit --no-edit
    $ git switch -c feature/search
    $ echo "def search(q): return []" > search.py && git add . && git commit -m "Add search stub"
    $ echo "# TODO: real search" >> search.py && git commit -am "wip"
    $ git rebase main                                  # already on main's tip: nothing to replay over
    $ git switch main && git merge --squash feature/search && git commit -m "Add search"
    $ git branch -D feature/search feature/discount
    $ git log --oneline --graph
    * 728e61d Add search
    *   2278f46 Merge branch 'feature/discount'
    |\
    | * 6286003 Lower the price to 90
    * | f3a8a78 Raise the price to 120
    |/
    * 48f3407 Add pricing config
    $ grep -c '<<<<<<<' config.py                     # 0: no markers left behind
    0
    
    git init shop && cd shop
    • gitversion control
    • initcreate a repository here
    • &&run the next command only if this one worked
    • cdchange directory
    printf 'PRICE = 100\nTAX = 0.18\n' > config.py && git add . && git commit -m "Add pricing config"
    • >write output to a file, replacing it
    • addstage changes
    • .this folder
    • commitsave staged changes
    echo "def search(q): return []" > search.py && git add . && git commit -m "Add search stub"
    • echoprint text
    echo "# TODO: real search" >> search.py && git commit -am "wip"
    • >>append output to a file
    git log --oneline --graph
    • loghistory
    grep -c '<<<<<<<' config.py
    • grepfind lines that match a pattern
    • -ccount matching lines

    git branch -D is needed for feature/search because a squash merge copies the changes without recording the branch's commits as merged. Your commit IDs will differ from these.

    Try it yourself

    0 of 6 steps done

    Run each task in your own account or terminal, then tick it off.

    Knowledge check

    Quick check

    Question 1 of 5

    When does git merge create a fast-forward instead of a merge commit?

    Go deeper

    Official reading for this lesson

    • GuidePro Git: rebasingPro Git ↗
    • GuidePro Git: advanced mergingPro Git ↗
    • DocsGitHub Docs: about merge methods on GitHubGitHub Docs ↗
    • DocsGitHub Docs: resolving a merge conflict using the command lineGitHub Docs ↗
    • Docsgit-cherry-pick documentationGit ↗

    Or get every quiz answer right and it completes itself.

    ← PreviousRemotes: push, fetch and pull without surprisesNext lesson →Collaboration: issues, pull requests and code review