Technology Sep 12, 2026 · 7 min read

Lazygit's M Key Hides Four Merge Options — Here's the Git History Each One Leaves Behind

I use lazygit daily, and for a long time M was just "the merge key". Press it, pick the option, the merge happens. It took me a while to actually slow down and pay attention to the options in the menu. Each option runs the same merge, but the commit graph left behind is different, and that influence...

DE
DEV Community
by Kamon Ayeva
Lazygit's M Key Hides Four Merge Options — Here's the Git History Each One Leaves Behind

I use lazygit daily, and for a long time M was just "the merge key". Press it, pick the option, the merge happens. It took me a while to actually slow down and pay attention to the options in the menu. Each option runs the same merge, but the commit graph left behind is different, and that influences the git history.

So I did what I do with any keystroke I don't fully understand: I built a throwaway repo, pressed M one option at a time, and read the history graph after each one. This article is the result — each option, the history it leaves, and at the end, the two git log commands that show you the merge shape of any repo you care about.

The one key with four options

In lazygit's branches panel, put your cursor on the branch you want to merge in (the source branch) and press M. A panel titled Merge opens with four options (in lazygit version 0.62.2):

  1. Regular merge (fast forward)m (fast-forwards when possible, otherwise creates a merge commit)
  2. Regular merge (with merge commit)n (always creates a merge commit)
  3. Squash merge and leave uncommitteds
  4. Squash merge and commitS

The lazygit M panel with the four options

Same panel, four different histories. Let's experiment with each one.

Set up a throwaway repo

Everything below reproduces on any machine with git and lazygit installed (brew install lazygit or apt install lazygit):

mkdir /tmp/merge-modes && cd /tmp/merge-modes
git init && git commit --allow-empty -m "init"

# a feature branch with three commits
git checkout -b feature
echo one > a.txt && git add . && git commit -m "feature: step one"
echo two > b.txt && git add . && git commit -m "feature: step two"
echo three > c.txt && git add . && git commit -m "feature: step three"

git checkout main

Open lazygit in this repo, go to the branches panel, put the cursor on feature, and press M. After each experiment, run git log --oneline --graph in the shell and look at the shape. Then git reset --hard HEAD@{1} to try the next option.

Regular merge (fast forward) — m option

This is the default in the M menu, and it behaves differently depending on the state of the target branch:

  • If main has not moved since you cut feature, git can fast-forward: main's pointer slides up to feature's tip. No new commit is created.
  • If main has moved on, git cannot fast-forward, and a merge commit is created instead (same shape as the no-fast-forward option we discuss next).

Our throwaway repo is in the fast-forwardable state, so pressing m produces the linear graph:

* feature: step three
* feature: step two
* feature: step one
* init

The branch itself leaves no trace. That is the nice thing and the cost at the same time: the history looks as one straight line, and nothing in it says "these three commits arrived together as one reviewed unit." Use this option when you have a really short-lived branch and the target branch did not move; the moment the target has its own commits, you get the merge-commit graph instead.

Regular merge (with merge commit) — n option

This one always produces a merge commit with two parents, regardless of whether the target has moved: the previous tip of main and the tip of feature. Press n when you specifically want the "a branch landed here" marker on the graph. The result looks like this:

*   merge branch 'feature' into main
|\
| * (feature) feature: step three
| * feature: step two
| * feature: step one
|/
* init

The three feature commits survive as themselves, and the merge commit is a permanent marker that says "a branch landed here." That marker is the point of this option. If you ever want to read the history as a series of landed branches, one entry per merge, git log --first-parent gives you exactly that view, and the merge commit is what makes it work.

The cost: the graph has merge knots, and anything that assumes a linear history (such as some revert flows) gets slightly more work to do.

Squash — s and S options

Both squash options collapse the source branch's commits (the three feature: step commits) into one new commit on the target. The intermediate commits on the source branch do not appear on the target. Only the one squashed commit does. So git log on the target cannot show how the source branch evolved, step by step. The trade is: you gain a clean, one-commit-per-change history, but you lose the record of the journey.

After S, the target's history looks like this:

* squash merge feature into main
* init

After s, git log on the target is unchanged from before the merge — the squashed changes land in your working tree, staged but uncommitted. You write the commit message yourself, then commit.

In practice s is the one I reach for when the commit message matters, which is most of the time: after the squash, that message is the only record of the branch that survives. S is the quick path.

Reading the shape of a repo's past merges

Once you know the four shapes, you start seeing them in real repos. Run these on any repo with some history (a long-lived side project is a good start):

git log --oneline --merges -20
git log --oneline --first-parent -20

--merges shows only merge commits, while --first-parent walks only the target side of each merge, which is the "one entry per landed change" view. Check the two lists together:

--merges shows --first-parent shows The repo's merge style
many merge commits each merge is one entry merge commits (no-fast-forward, or m when the target has moved)
few or no merge commits one commit per landed branch, "squash"-style messages squash (s or S)
no merge commits, granular commits linear history fast-forward (m when the target is quiet) or direct pushes

I ran these on my own old repos and found exactly what you'd expect from someone who never thought about it: a mix. Squash here, a merge commit there, a fast-forward when the target happened to be quiet. No single shape, because no single decision was ever made. The commands are useful precisely because the shape is usually undocumented — the history is the only record of the choice, whether it was deliberate or not.

Picking one and living with it

Pick one shape (and merge option) and keep to it. Six months from now, you may have to revert a bad change or read the history to understand why something was done — in those situations, the job is easier when the history has a consistent shape, and harder when it mixes all four options at random.

If you pick wrong, the merge commit is just a commit: in the shell, do git reset --hard HEAD@{1}, or in lazygit, from the commits panel, put the cursor on the bad commit, and press z to soft-reset. And if the merge produced a conflict, there is no abort path — only forward: lazygit opens the conflict view, you resolve each hunk, do esc back to the files panel, and the merge flow continues from there.

That's what is behind lazygit's M panel and the four types of history its options leave. Pick whichever one matches the shape you want your future self to read.

This article expands issue #7 of my newsletter, Shellcraft — one piece of terminal craft per week.

DE
Source

This article was originally published by DEV Community and written by Kamon Ayeva.

Read original article on DEV Community
Back to Discover

Reading List