In the previous lesson, every time Git created a merge commit it told us the same thing: Merge made by the 'ort' strategy. We walked straight past that word, but it hides an important part of how Git works.

A merge strategy is the algorithm Git uses to combine two (or more) lines of work. There is not just one: Git ships several, picks one by default depending on the situation, and lets you impose a different one. On top of that, each strategy accepts options that tweak its behaviour in specific details, such as what to do about a conflict or whether to ignore whitespace changes.

In practice, 95 % of the merges you do will use the default strategy without you ever having to think about it. But the remaining 5 % — a merge with hundreds of trivial conflicts, an abandoned branch that has to be formally "closed", a feature built from twenty chaotic commits you do not want anywhere near main — is exactly where knowing the alternatives saves you hours.

This lesson also covers a decision that goes beyond the algorithm: which form of integration to choose, based on the history you want to end up with. A normal merge, --no-ff, a squash… or a rebase, which we will see in module 5 but which is worth placing on the map now.

Contents

  1. What a merge strategy actually is
  2. ort: the default strategy
  3. resolve: the veteran
  4. octopus: several branches at once
  5. The ours strategy: discard the content, keep the history
  6. subtree: merging nested projects
  7. Strategy options with -X
  8. The trap: the ours strategy versus the -X ours option
  9. The squash merge
  10. --no-commit: reviewing before you seal it
  11. Decision table: which form of integration to choose

  1. What a merge strategy actually is

When git merge has to genuinely combine work — that is, when it is not a fast-forward — it has to answer one question for every file and every line: given what was in the common ancestor and what is on each side, what do I put here?

The set of rules that answers that question is the strategy. You choose it with -s or --strategy:

git merge -s <strategy> <branch>

And its internal options with -X or --strategy-option:

git merge -X <option> <branch>

The two can be combined, and -X can be repeated:

git merge -s ort -X ignore-space-change -X patience <branch>

Let us compare all the available strategies first, and then look at each one:

Strategy Branches it accepts When Git uses it What it is for
ort Two The default since Git 2.34 General-purpose merging; the one you want almost always
recursive Two The default from Git 2.0 to 2.33 Predecessor of ort; today an alias for ort
resolve Two Never the default Old, simple algorithm; a single common ancestor
octopus Three or more The default when merging 3+ branches Integrating several trivial branches in one go
ours Two or more Never the default Recording the merge while discarding the other branch's content
subtree Two Never the default Merging a project that lives in a subdirectory

  1. ort: the default strategy

ort stands for Ostensibly Recursive's Twin, a name with a sense of humour that gives away its origin: it is a from-scratch rewrite of the recursive strategy, designed to give the same results but much faster and with fewer odd corner cases.

It has been the default strategy since Git 2.34 (November 2021). Before that it was recursive. Today, if you type -s recursive, Git uses ort anyway.

What it does, in short:

  1. It works out the common ancestor of the two branches.
  2. It performs the three-way merge file by file and line by line, with the table of rules we saw in lesson 03-03.
  3. It detects renames: if you moved styles.css to css/styles.css on one branch and modified it on the other, it applies the changes to the file in its new location instead of handing you an absurd conflict.
  4. If there are several common ancestors, it merges them together recursively until it has a virtual one, and uses that as the base.

Point 4 is the one that gives the family its name, and it deserves a moment. There can be more than one common ancestor when the history contains criss-cross merges:

graph RL
    A["A"]
    B["B"] --> A
    C["C"] --> A
    D["D"] --> B
    D --> C
    E["E"] --> C
    E --> B
    F["F<br/>branch-1"] --> D
    G["G<br/>branch-2"] --> E

Here both D and E are common ancestors of branch-1 and branch-2, and neither is an ancestor of the other. A simple strategy would have to pick one arbitrarily, at the risk of producing false conflicts. ort merges them together to build a virtual base that combines the information from both.

You do not need to engineer this situation to get on with your work: you only need to know it exists and that Git handles it on its own. It is the reason merges in complex histories work surprisingly well.

  1. resolve: the veteran

git merge -s resolve feature/pending-filter

resolve is the classic strategy: a three-way merge with a single common ancestor, picked if there are several. It does no recursion and its rename detection is far more limited.

When should you use it? Practically never. Its niche is the rare case where ort produces a result you are not happy with in a history full of criss-cross merges and you want to try a different base. It is more of a diagnostic resource than a working tool.

It is on the syllabus because you will see it mentioned in old documentation, and because knowing it helps you understand why ort does what it does.

  1. octopus: several branches at once

git merge is not limited to one argument. You can name several branches:

git merge branch-a branch-b branch-c

When there are three or more, Git automatically switches to the octopus strategy, which creates a single merge commit with as many parents as there are branches, plus one.

Suppose the team has piled up three tiny, independent fixes, each on its own branch:

git switch main
git merge fix/readme-typo fix/favicon fix/html-lang
Trying simple merge with fix/readme-typo
Trying simple merge with fix/favicon
Trying simple merge with fix/html-lang
Merge made by the 'octopus' strategy.
 README.md  | 2 +-
 index.html | 3 ++-
 3 files changed, 4 insertions(+), 2 deletions(-)
git cat-file -p HEAD | head -5
tree 5f8b2e1c9a3d7f4b6e2a8c1d5f9b3e7a4c6d2f8b
parent f7a3e92b5d1c8f4a6e3b7d9f2a5c1e8b4d6f3a9c
parent 2f6a3c8e9b1d5f7a3c2e8b4d6f1a9c5e7b3d2f4a
parent 7d2f9a1c5e8b3d6f2a4c9e1b7d5f3a8c2e6b4d9f
parent 3c9d4a2f8e1b5d7c3a9f2e6b4d8c1a5f7e3b9d2c

Four parents. The first is main and the other three are the tips of the merged branches.

gitGraph
   commit id: "f7a3e92"
   branch readme-typo
   commit id: "2f6a3c8"
   checkout main
   branch favicon
   commit id: "7d2f9a1"
   checkout main
   branch html-lang
   commit id: "3c9d4a2"
   checkout main
   merge readme-typo
   merge favicon
   merge html-lang id: "octopus"

The crucial limitation of octopus: it refuses to work if there is any conflict at all.

Merge with strategy octopus failed.

There is no resolution to attempt, no markers in the files, nothing to fix: the whole operation is aborted. The reason is a design decision: resolving conflicts between four simultaneous versions would be unmanageable for a human being.

That is why its realistic use is very specific: integrating in one go several branches that do not step on each other. If any of them conflicts, you will have to merge them one at a time. In the day-to-day life of a small team you will rarely see it; in projects with a great many independent contributions (the Linux kernel is the canonical example, and no coincidence, since it was the project Git was written for) it makes complete sense.

  1. The ours strategy: discard the content, keep the history

This is the oddest strategy of the lot and the most widely misunderstood.

git merge -s ours <branch>

What it does: it creates a merge commit with the two normal parents, but the resulting tree is exactly the one from the current branch. The other branch's content is discarded entirely.

It sounds absurd. Why merge something you are going to throw away? To record in the history that the branch has already been considered, without bringing in its code.

The real use case in Ana's project: a month ago the team opened the branch experiment/indexeddb-storage to try out a different storage system. The experiment was dropped — in the end they stuck with localStorage — but the branch is still there and every so often somebody asks whether it needs integrating.

git switch main
git merge -s ours experiment/indexeddb-storage -m "Drop the IndexedDB experiment

We tried IndexedDB for task storage. Dropping it: the added
complexity is not worth it for the volume of data we handle.
We keep localStorage. The branch is recorded as integrated."
Merge made by the 'ours' strategy.

Notice that it lists no changed files: nothing has changed.

git diff HEAD~1 HEAD
(no output)

Zero differences from the previous commit on main. The code is identical. But:

git branch --merged
  experiment/indexeddb-storage
* main

The branch counts as merged, so it will show up in the lists of branches you can delete without losing anything (lesson 03-06), it will stop appearing in "branches still to integrate" warnings, and the history holds a permanent explanation of why it was dropped.

Another classic use: when two branches have diverged so far that reconciling them by hand is unrealistic and you want one of them to win outright, leaving a record of the decision.

An important warning: -s ours discards all of the other branch's content without asking and without warning. There are no conflicts, no review, no automatic way back. It is a deliberate decision, not a shortcut out of a difficult merge. And do not confuse it with -X ours, which is something completely different and which we will see in section 8.

  1. subtree: merging nested projects

git merge -s subtree <branch>

subtree is a variant of ort for the case where one branch contains a project that on the other branch lives inside a subdirectory.

Suppose the team develops a small component library separately, in its own repository, and that in task-manager that library lives in vendor/components/. When merging the library's branch, a normal strategy would try to match its files against the project root and produce a disaster. subtree detects the path shift and applies the changes where they belong.

You can also give the prefix by hand, which is more reliable than letting Git guess:

git merge -X subtree=vendor/components library-branch

This mechanism is the basis of the git subtree command, one of the two ways of nesting repositories. The other is submodules, which get a lesson of their own in module 6. For now it is enough to know that the strategy exists and which scenario it was designed for.

  1. Strategy options with -X

Options do not change the algorithm: they adjust it. These are the ones worth knowing:

Option Effect
-X ours Faced with a conflict, resolves it automatically in favour of the current branch
-X theirs Faced with a conflict, resolves it automatically in favour of the merged branch
-X ignore-space-change Ignores changes in the amount of whitespace when comparing
-X ignore-all-space Ignores all whitespace
-X ignore-space-at-eol Ignores whitespace at the end of a line
-X ignore-cr-at-eol Ignores the trailing carriage return (handy between Windows and Unix)
-X renormalize Normalises line endings before comparing, according to .gitattributes
-X find-renames=<n> Adjusts the similarity threshold for detecting renames
-X no-renames Turns rename detection off
-X patience Uses the "patience" diff algorithm, which sometimes lines things up better
-X diff-algorithm=histogram Picks a different comparison algorithm

The whitespace case

This is the one that will save you most often. A real situation: Bruno has set his editor to reindent automatically on save, and it has reindented the whole of app.js on his branch. Ana has modified three lines of the same file on hers.

git merge feature/pending-filter
Auto-merging app.js
CONFLICT (content): Merge conflict in app.js
Automatic merge failed; fix conflicts and then commit the result.

Eighty conflicts, and not one of them real: they are all indentation differences. The fix:

git merge --abort
git merge -X ignore-space-change feature/pending-filter
Auto-merging app.js
Merge made by the 'ort' strategy.
 app.js | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

The eighty false conflicts vanish and only the real changes are left.

Two important caveats:

  • These options affect how lines are compared, not the text that gets written. The final file keeps the indentation of one of the versions (the ours side in ambiguous stretches), so review the result.
  • If the indentation problem keeps recurring in the team, the real fix is not -X but .gitattributes plus a shared automatic formatter. That is the subject of lesson 08-04.

-X theirs in practice

A typical example: an automatically generated file (a bundle.js, a dependency lock file) that conflicts every single time and where the incoming branch's version is the correct one:

git merge -X theirs feature/pending-filter

It is convenient, but use -X ours/-X theirs with your brain switched on: they resolve every conflict on the same side, without distinguishing which ones mattered. If the result matters to you, it is better to resolve by hand (lesson 03-05) or to limit the shortcut to the specific files that deserve it, which is also possible and which we will see there.

  1. The trap: the ours strategy versus the -X ours option

They are written almost identically and they do radically different things. This is one of the most expensive confusions in Git, so let us nail it down.

-s ours (strategy) -X ours (option)
What it is A complete merge algorithm A tweak to the ort strategy
What it does Discards all of the other branch's content Merges normally, and picks your side only when there is a conflict
Conflict-free changes from the other branch All lost Brought in as usual
Result The tree is identical to your branch's The tree combines both branches
When to use it Formally closing a branch you have dropped Resolving a block of unimportant conflicts

A concrete example. The branch feature/pending-filter brings two things: a new file filters.js (which conflicts with nothing) and a modification to line 12 of app.js (which does conflict with a change of Ana's).

git merge -s ours feature/pending-filter

Result: filters.js does not appear and line 12 is Ana's. Everything has been discarded, even what was bothering nobody.

git merge -X ours feature/pending-filter

Result: filters.js does appear, because there was no conflict over it, and line 12 is Ana's, because there was one there. A real merge with an automatic tie-break.

Mnemonic: -s is the strategy, and a strategy decides everything. -X is an option, and an option only decides the ties.

And one point about the vocabulary, because it confuses people too: in a merge, ours is always the branch you are standing on and theirs is the one you named in the command. It sounds obvious, but during a rebase the roles are swapped compared with what you would expect, and that is one of the reasons rebasing deserves a lesson of its own (05-01).

  1. The squash merge

Time for something different. A squash merge is not a strategy: it is a different form of integration, and in many teams it is the most used of them all.

The situation in the project: Bruno has built CSV export for the task list on the branch feature/csv-export. It works, but the branch's history is a mess:

git log --oneline main..feature/csv-export
e9a2c5f Now it works
7e2a9c4 Remove the console.log
1d6b4f8 wip 2
5a8e2b9 Fix the separator
9f3a2c1 wip
4a1c7e3 First attempt at exporting

Six commits, four of which give nothing at all to anyone reading the history six months from now. Bruno does not want that in main.

git switch main
git merge --squash feature/csv-export
Updating f7a3e92..e9a2c5f
Fast-forward
Squash commit -- not updating HEAD
 app.js     | 34 ++++++++++++++++++++++++++++++++++
 index.html |  2 ++
 2 files changed, 36 insertions(+)

Read it carefully: Squash commit -- not updating HEAD. Git has worked out the result of the merge and left it in the staging area, but it has committed nothing.

git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	modified:   app.js
	modified:   index.html

Now Bruno writes a decent message and commits:

git commit -m "Add CSV export of the task list

Generates a CSV file with the title, the status and the creation
date of every task. It downloads from the new button in the
header."
[main 3b9e7d1] Add CSV export of the task list
 2 files changed, 36 insertions(+)

How it differs from a normal merge

graph TB
    subgraph squash["SQUASH MERGE"]
        S1["f7a3e92<br/>main"] --> S2["3b9e7d1<br/>main (a single commit,<br/>ONE parent)"]
        S3["4a1c7e3 → e9a2c5f<br/>csv-export<br/>(disconnected)"]
    end
    subgraph normal["NORMAL MERGE"]
        N1["f7a3e92"] --> N3["merge<br/>main (TWO parents)"]
        N2["4a1c7e3 → e9a2c5f<br/>csv-export"] --> N3
    end

The essential difference:

Normal merge Squash merge
New commits on main 1 (a merge commit) 1 (a normal one)
Parents of that commit Two One
The branch's commits in main's history Yes, all six No, none
Is the relationship with the branch recorded? Yes No
What does git branch --merged say? That it is merged That it is not
Authorship of the original commits Preserved Lost (the committer is credited)

That "the relationship is not recorded" has one very concrete practical consequence. After a squash, main is not a descendant of feature/csv-export:

git branch --merged
* main

Bruno's branch does not appear, even though its content is entirely in main. If you try to delete it:

git branch -d feature/csv-export
error: The branch 'feature/csv-export' is not fully merged.
If you are sure you want to delete it, run 'git branch -D feature/csv-export'.

Git is not lying: from the graph's point of view, those six commits are not in main. You will have to delete it with -D, knowing what you are doing. We will come back to this in lesson 03-06.

When to use squash

In favour:

  • main gets one commit per feature, clean and with a message written calmly.
  • The working branch's mess (the wips, the "now it works") does not contaminate the project.
  • git bisect (lesson 06-02) works better: every commit on main is a complete, working state.
  • It is the default model behind the "Squash and merge" button on hosting platforms, and therefore the most widespread today in teams that work with change proposals.

Against:

  • The detail of the development is lost. If the feature is 40 well-written commits, squashing them destroys useful information.
  • Individual authorship is lost: a commit built from three people's contributions ends up in the committer's name.
  • Branches have to be deleted with -D, without the safety net of -d.
  • If you carry on working on the branch after the squash and merge again, Git does not know that work was already integrated and can replay conflicts you had already resolved.

Rule of thumb: squash for short, single-person branches with untidy commits; a merge with --no-ff for large features whose history deserves keeping.

  1. --no-commit: reviewing before you seal it

git merge --no-commit does the merge for real but stops just before creating the commit:

git merge --no-commit feature/pending-filter
Automatic merge went well; stopped before committing as requested
git status
On branch main
All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Changes to be committed:
	modified:   app.js
	modified:   styles.css
	modified:   index.html

At this point you can:

# See exactly what is about to go in
git diff --cached

# Run the tests against the combined result
npm test

# Adjust something before committing

And afterwards, commit or abort:

git commit                  # seals the merge (with its two parents)
# or else
git merge --abort           # undoes the merge and goes back to the previous state

The difference from --squash is fundamental and often confused, because both of them "stop before committing":

--no-commit --squash
Is the merge recorded? Yes: committing produces a commit with two parents No: it produces a normal commit
Can it be aborted? Yes, with git merge --abort No: there is no merge in progress to abort
State of .git/MERGE_HEAD It exists It does not exist
What it is for Reviewing or testing before sealing Squashing a branch into a single commit

That MERGE_HEAD file is the technical detail that explains everything: with --no-commit there is a merge in progress that Git remembers, which is why the resulting commit will have two parents and --abort works. With --squash there is no merge in progress at all: there are just some changes staged in the index.

Common uses of --no-commit:

  • Running the test suite against the combined result before leaving it on main.
  • Reviewing a large merge file by file.
  • Adjusting minor details (a duplicated import, a stray space) and folding them into the merge commit itself.

  1. Decision table: which form of integration to choose

Let us gather everything from this module, plus the rebase coming in module 5, into a single decision table:

I want to… I use Resulting history
Integrate with no sign that there was ever a branch git merge (fast-forward) Linear, no merge node
Leave a record that a feature was integrated git merge --no-ff With a merge node and a bubble of commits
One single clean commit per feature git merge --squash + git commit Linear, one commit per branch
Forbid merge nodes on main git merge --ff-only Linear, or it fails
Test the result before sealing it git merge --no-commit Whatever you decide afterwards
Integrate several trivial branches at once git merge a b c (octopus) One node with many parents
Close a dropped branch without bringing its code git merge -s ours With a merge node, no changes
Bring my branch up to date with main without a merge node git rebase main05-01 Linear, with rewritten commits

About that last row, just enough to place it: a rebase does not merge; it rewrites your branch's commits as if you had made them on top of main's current state. The result is a linear history with no merge nodes, at the price of the commits changing hash and therefore being new commits. That has serious implications once the work has been shared, which is why it gets a lesson of its own. Here you only need to know that it is the third alternative alongside merging and squashing.

And a starting recommendation

If you are just getting going and have no team policy to follow:

  1. A normal merge for everything, letting Git decide between fast-forward and three-way.
  2. --no-ff when you integrate something you want to be able to identify as a unit in the future.
  3. --squash when the branch is yours, short and untidy.
  4. Leave -s and -X alone until a concrete problem asks you for one of those options.

Above all, have the whole team do the same thing. A consistent history is worth more than an optimal but erratic one. The named workflow conventions — Git Flow, GitHub Flow, Trunk Based Development — are precisely that: bundles of decisions somebody has already made for you. They have their place in module 7.

Common Mistakes and Tips

Mistake 1: confusing -s ours with -X ours. This is the most expensive mistake in this lesson. -s ours silently discards all of the other branch's work, including files that were causing no trouble at all. If what you wanted was to break the tie on conflicts, the option is -X ours. If you are ever in doubt, git diff HEAD~1 HEAD after the merge will tell you straight away whether you brought in anything or nothing.

Mistake 2: using -X theirs as a universal shortcut. It resolves every conflict on the same side, including the ones where your version was the right one. Work disappears without a single warning. Keep these options for conflicts you know are irrelevant.

Mistake 3: believing that --squash merges. It does not: it stages the changes and walks away. If you forget the git commit afterwards, you are left with a pile of staged changes on main and an odd feeling that "the merge did not work". And even once you commit, the branch will still show up as unmerged.

Mistake 4: squashing a branch and carrying on working on it. Since the resulting commit has no relationship with the originals, the next merge of that branch will bring everything back and replay conflicts you had already resolved. After a squash, the branch is closed: you delete it and open another.

Mistake 5: hunting for the perfect strategy for a difficult conflict. There is no such thing. If ort gives you conflicts, it is because there is a human decision to be made. Changing strategy only moves the problem or hides it. The right answer is the next lesson.

Tip 1: -X ignore-space-change is the one you will use most. When a merge blows up with dozens of conflicts that turn out to be nothing but indentation, abort and try again with that option. The improvement is spectacular.

Tip 2: check what an unusual merge has done. After any merge with -s or -X, look at the result before moving on:

git diff HEAD^1 HEAD --stat    # what changed relative to main
git log --oneline -1

Tip 3: --no-commit is your safety net. On large or delicate merges, stop before committing, run the tests and review. It costs thirty seconds and stops you putting a combination nobody has checked into main.

Tip 4: write down the team policy. Put in the project's README.md or CONTRIBUTING.md how work gets integrated into main. It is the kind of decision that, left unwritten, every person interprets differently.

Exercises

Exercise 1: demonstrating the difference between -s ours and -X ours

Set up a test repository where a branch brings two things: a new file that does not conflict and a modification that does conflict with the main branch. Merge it both ways (in two independent attempts) and show with commands what is left in each case.

Exercise 2: squash versus a normal merge

Create a branch with three commits and integrate it into main both ways, either in two different repositories or by undoing in between. Then answer:

  1. How many commits does main have in each case?
  2. How many parents does main's last commit have in each case?
  3. What does git branch --merged say in each case?
  4. Can the branch be deleted with -d in each case?

Exercise 3: choosing the form of integration

For each situation, choose between a normal merge, --no-ff, --squash, -s ours, octopus or --no-commit, and justify it:

  1. A colleague's branch with 25 well-written commits and careful messages, implementing the reporting module.
  2. Your own branch with 7 commits, four of which are called wip.
  3. Five translation branches, each touching a different language file.
  4. A branch for an experiment that has been dropped, and which you want on the record.
  5. A merge that affects the billing logic and that you want to validate with the tests before it goes into main.

Solutions

Solution 1:

mkdir /tmp/practice-ours && cd /tmp/practice-ours
git init -b main
echo "original line" > common.txt
git add . && git commit -m "Base"

# The branch brings a new file AND a conflicting change
git switch -c contribution
echo "new content" > exclusive.txt
echo "branch version" > common.txt
git add . && git commit -m "Add exclusive.txt and change common.txt"

# main changes the SAME line of common.txt
git switch main
echo "main version" > common.txt
git commit -am "Change common.txt on main"

Attempt A, with the strategy:

git merge -s ours contribution -m "Merge with the ours strategy"
ls
cat common.txt
common.txt
main version

exclusive.txt is not there. All of the branch's content has been discarded, including the file that was causing nobody any trouble.

git diff HEAD^1 HEAD
(no output)

Zero changes relative to main. That is the unmistakable signature of -s ours.

Attempt B, with the option:

git reset --hard HEAD~1     # we undo attempt A
git merge -X ours contribution -m "Merge with the -X ours option"
ls
cat common.txt
common.txt  exclusive.txt
main version

exclusive.txt is there, because there was no conflict over it. And common.txt holds main's version because there was one there and -X ours broke the tie in our favour.

Conclusion: -s ours discards everything; -X ours genuinely merges and only breaks ties.

Solution 2:

mkdir /tmp/practice-squash && cd /tmp/practice-squash
git init -b main
echo "base" > f.txt && git add . && git commit -m "Base"
git switch -c feature
echo "a" >> f.txt && git commit -am "Step 1"
echo "b" >> f.txt && git commit -am "Step 2"
echo "c" >> f.txt && git commit -am "Step 3"
git switch main
echo "change on main" > other.txt && git add . && git commit -m "Advance main"

Normal merge:

git merge --no-edit feature
git log --oneline | wc -l
6
git rev-parse HEAD^1 HEAD^2 > /dev/null && echo "it has two parents"
it has two parents
git branch --merged
  feature
* main
git branch -d feature
Deleted branch feature (was 2c9d4e6).

Squash (rebuilding the setup from scratch or undoing with git reset --hard HEAD~1):

git merge --squash feature
git commit -m "Add the complete feature"
git log --oneline | wc -l
3
git rev-parse HEAD^2
fatal: ambiguous argument 'HEAD^2': unknown revision or path not in the working tree.

A single parent.

git branch --merged
* main
git branch -d feature
error: The branch 'feature' is not fully merged.

Answers:

  1. 6 commits with a normal merge (base + 3 from the branch + main's own commit + the merge); 3 with squash (base + main's own commit + the squashed commit).
  2. Two parents with a normal merge; one with squash.
  3. With a normal merge the branch appears as merged; with squash it does not.
  4. With a normal merge, -d works; with squash, you have to use -D.

Solution 3:

  1. A merge with --no-ff. Twenty-five well-written commits are valuable information that should not be destroyed, and the merge node documents the module's integration as an identifiable unit.

  2. --squash. A short, personal, untidy branch: the textbook case. One clean commit on main with a message written calmly is worth more than seven commits of which four say nothing.

  3. octopus (git merge trans-es trans-ca trans-en trans-fr trans-de). Five branches touching different files and therefore incapable of conflicting: exactly the scenario it exists for. If any of them did conflict, octopus would fail and they would have to be integrated one by one.

  4. -s ours. It records in the history that the branch was considered and dropped, without bringing in its code, and it stops appearing as pending integration. Use the commit message to explain why it was dropped.

  5. --no-commit. It lets you run the test suite against the already-combined result and, if something fails, walk away cleanly with git merge --abort without main ever containing anything.

Conclusion

You now know that "merging" is not one single thing:

  • A strategy is the algorithm that combines the branches, and you choose it with -s. ort has been the default since Git 2.34: a three-way merge with rename detection and recursive resolution when there are several common ancestors. resolve is its simple predecessor; octopus merges three or more branches at once but fails on any conflict; subtree matches up projects that live in subdirectories.
  • The ours strategy (-s ours) creates a merge commit and discards all the content of the other branch. It exists to formally close a branch you have decided not to integrate.
  • The -X options adjust the strategy, they do not replace it. -X ours/-X theirs break ties only on conflicts; -X ignore-space-change wipes out false indentation conflicts at a stroke.
  • -s ours and -X ours have nothing to do with each other. The strategy decides everything; the option decides only the ties.
  • The squash merge (--squash) leaves the result staged without committing and without recording the merge: the final commit has a single parent and the branch does not count as merged. Ideal for short, untidy branches; bad for features whose history deserves keeping.
  • --no-commit does the merge for real but stops before committing, so you can review and test. Unlike --squash, the merge is still in progress and --abort works.
  • Choosing how to integrate is a team decision about the history you want to have, not a technical question.

What comes next

Throughout this lesson we have taken for granted something that does not always hold in the real world: that the merge goes well. But Ana and Bruno will not be so lucky next time. They are both about to touch the same lines of the function that renders the task list in app.js, each on their own branch and for different reasons.

When that happens, Git cannot decide. It stops, writes some strange markers inside the file and passes the ball to you. In the next lesson, Resolving Merge Conflicts, we will provoke a real conflict and resolve it step by step: you will learn to read the <<<<<<<, ======= and >>>>>>> markers (and the diff3 style, which also shows you what was in the common ancestor), to find your bearings with git status and git diff while the merge is half done, to use the --ours/--theirs shortcuts, to abort with git merge --abort when things go wrong, and to set up a graphical tool for the hard conflicts.

Mastering Git: From Beginner to Advanced

Module 1: Introduction to Git

Module 2: Basic Git Operations

Module 3: Branching and Merging

Module 4: Working with Remote Repositories

Module 5: Advanced Git Operations

Module 6: Git Tools and Techniques

Module 7: Collaboration and Workflow Strategies

Module 8: Git Best Practices and Tips

Module 9: Troubleshooting and Debugging

Module 10: Git in the Real World

© Copyright 2026. All rights reserved