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):
-
Regular merge (fast forward) —
m(fast-forwards when possible, otherwise creates a merge commit) -
Regular merge (with merge commit) —
n(always creates a merge commit) -
Squash merge and leave uncommitted —
s -
Squash merge and commit —
S
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
mainhas not moved since you cutfeature, git can fast-forward:main's pointer slides up tofeature's tip. No new commit is created. - If
mainhas 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.
This article was originally published by DEV Community and written by Kamon Ayeva.
Read original article on DEV Community