Every merge conflict ends the same way. You fix the files, and then you have to stage them. And this is exactly the moment when a small accident happens: most of us type git add -u or git add . out of habit. Both commands stage everything that is modified, not only the files you have just resolved.
I’ve been bitten by this more than once. A set -x or a debug print in some unrelated file quietly goes into the merge commit together with the resolution. Nobody notices it, because nobody reads merge commits line by line. A few weeks later someone asks why the start script suddenly prints every command.
Git 2.56 finally has a proper answer for this:
git add --resolved
It stages only the files that were in conflict, and it refuses to stage anything while conflict markers are still left in them. Everything else in your working tree stays untouched.
In this article, I show how it works step by step, from an empty directory to the finished merge. But first, a few words about the release itself.
What’s new in Git 2.56
Git 2.56 is a big release, with many changes for daily work. I went through the release notes and tried the features that looked the most useful. Here is my short list:
git add --resolvedstages only resolved conflicts. This article is about it.git branch --delete-mergeddeletes local branches whose work has already landed in the upstream they track. It has safety rules: it keeps the checked-out branch, branches that are not merged yet, and branches you mark as “keep”.git bisect --reset-when-foundends the bisect session by itself as soon as the bad commit is found.git history dropremoves one commit from the middle of the history, and the branches that depend on it follow.git refs create/update/delete/renameis a readable and safe way to work with refs: no silent overwrites, and a compare-and-swap update.git replay --linearizeflattens a branch with merges in memory, even in a bare repository.git log --graphno longer draws unrelated histories as if they were connected.[includeIf "worktree:..."]gives you per-directory configuration that also works for linked worktrees.fetch.followRemoteHEADkeepsorigin/HEADin sync with the remote in every repository, with one setting.git repack --drop-filteredreclaims disk space in partial clones.
There are also small things that I like. For example, a typo in git push now gets a hint:
$ git push origin/main
fatal: 'origin/main' is not a valid push target
hint: Did you mean to use: git push origin main?
And -h now exits with code 0 in most commands (it was 129), so git status -h in a script no longer looks like an error. You can find the full list in the release notes.
How to get Git 2.56
Linux distributions need some time to ship a new Git version. On my Fedora, the system Git is still 2.55. I didn’t want to compile Git from source, so I used mise with the conda backend, which installs a prebuilt Git:
# mise.toml
[tools]
"conda:git" = "2.56.0"
[settings]
experimental = true # the conda backend is still marked experimental
mise trust
mise install
mise exec -- git --version # git version 2.56.0
It doesn’t replace the system Git: the new version is used only inside the project with this mise.toml.
The problem
After fixing the conflicts, the usual next step is git add -u or git add .. Both commands stage every modified tracked file. This includes the debugging line you’ve just added to an unrelated file, or the change you had in your working tree before the merge. Merge commits get much less review than normal commits, so these changes easily go unnoticed.
The safe alternative was to type every conflicted path by hand. But even then, nothing checked whether you had really removed all the conflict markers.
What git add –resolved does
- It looks only at unmerged paths, the
UU,AA,UDentries ingit status. - It checks each of these files for leftover conflict markers.
- If any file still has markers, it refuses to stage anything and shows the files that still need work.
- Otherwise, it stages all of them.
- Tracked files that are not in conflict are never staged.
- You can narrow it with a path:
git add --resolved config.ini. - It can’t be combined with
-uor-A.
Step by step
Let’s try it on a tiny project. Every step below is recorded in a real terminal with Git 2.56.0. I write the files and resolve the conflicts in vim, exactly as I would do it myself. My prompt shows the directory and the current branch, for example shop (main) $, and shop (main|MERGING) $ while a merge is in progress.
Your commit IDs will be different from mine, which is normal.
Step 1: Create a repository
We start from nothing: a directory, git init, and git status to check that there are no commits yet. As soon as the repository exists, the prompt starts showing the branch.
mkdir shop
cd shop
git init
git status
Step 2: Write the first files
Three small files: a server config, a changelog, and a start script. In vim, i starts insert mode, Esc leaves it, and :wq saves the file and quits. git status --short shows all three files as untracked (??).
# config.ini
[server]
port = 8080
workers = 2
Step 3: The first commit
We stage the three files and commit them. This commit (a25ceb7 for me) is the starting point for both branches.
git add config.ini CHANGELOG.md app.sh
git commit -m "Initial version"
git log --oneline
Step 4: A feature branch
On a new branch feature, we move the server to port 9090 and add a line to the changelog. git branch marks the current branch with *, and git diff shows both changes before the commit.
In vim, /port jumps to the port line, $ goes to the end of the line, and ciw9090 replaces the word 8080. In the changelog, Go opens a new line at the end of the file.
git switch -c feature
git branch
git diff
git commit -am "Move the server to port 9090"
Step 5: Meanwhile, on main
Back on main, someone raises the number of workers to 8. This is the line right next to the port, so Git won’t be able to combine the two changes by itself. The graph shows how the two branches go in different directions.
git switch main
git commit -am "Use 8 workers by default"
git log --oneline --graph --all
Step 6: The trap
While debugging something else, I add set -x to app.sh (gg jumps to the first line, o opens a new line below it). It has nothing to do with the merge and must not be committed. Right now, it’s just a local modification ( M in git status --short), and this is completely normal. We all have such changes in our working trees.
Step 7: Merge, and two conflicts
git merge feature stops with conflicts in CHANGELOG.md and config.ini, and the prompt changes to main|MERGING. Look at git status: it shows two different groups:
- Unmerged paths: the two conflicts.
- Changes not staged for commit: our
set -xinapp.sh.
The whole point of this article is to keep these two groups apart until the very end.
Step 8: Resolve config.ini
Between <<<<<<< HEAD and ======= is our side (main), and between ======= and >>>>>>> feature is theirs. We want both changes: 8 workers from main and port 9090 from feature. In vim:
| Keys | What it does |
|---|---|
/<<<<<<< Enter | jump to the first marker |
dd | delete the <<<<<<< HEAD line |
$ciw9090 Esc | on our port line, change 8080 to 9090 |
jj | move down to the ======= line |
3dd | delete ======= and their two lines |
dd | delete the >>>>>>> feature line |
:wq | save and quit |
The result:
[server]
port = 9090
workers = 8
Step 9: One file is still not resolved
Let’s say we forgot about the changelog and run git add --resolved right away. Git refuses and tells us which file still has markers:
$ git add --resolved
fatal: the following paths still have conflict markers:
CHANGELOG.md
More importantly, it stages nothing at all, not even the config.ini we have already fixed. git status --short still shows both files as UU. It’s all or nothing, so you never end up with a half-staged resolution.
Step 10: What the old habit would do
For comparison, here is a dry run of git add -u. Besides the two conflicted files, it would happily stage app.sh with our set -x. This is exactly the accident from the beginning of the article. --dry-run only shows what would happen, nothing is staged.
$ git add -u --dry-run
add 'CHANGELOG.md'
add 'app.sh'
add 'config.ini'
Step 11: Stage one file you are sure about
--resolved accepts a path, so you can stage files one by one as you finish them. Now config.ini is staged (M ), CHANGELOG.md is still in conflict (UU), and app.sh is still an unstaged local change ( M).
git add --resolved config.ini
git status --short
Step 12: Resolve the changelog
In the changelog, we want to keep both new lines, so we only delete the three marker lines:
| Keys | What it does |
|---|---|
/<<<<<<< Enter, dd | find and delete the first marker |
/======= Enter, dd | find and delete the separator |
/>>>>>>> Enter, dd | find and delete the last marker |
:wq | save and quit |
This time git add --resolved goes through. Both conflicted files are staged, and app.sh is still left alone:
$ git add --resolved
$ git status --short
M CHANGELOG.md
M app.sh
M config.ini
Step 13: Finish the merge
git commit opens the editor with the prepared message Merge branch 'feature'. I like to add one line that explains how the conflict was resolved. Your future self will thank you. Then a few checks:
git log --oneline --graph: the merge commita64a650joins the two branches.git diff --stat HEAD^1 HEAD: the merge changed onlyCHANGELOG.mdandconfig.ini.git status --short:app.shis still modified and not committed, exactly as it should be.
$ git diff --stat HEAD^1 HEAD
CHANGELOG.md | 1 +
config.ini | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
$ git status --short
M app.sh
Step 14: A small guard rail
--resolved is narrow on purpose, so Git doesn’t let you combine it with -u or -A, which would stage everything anyway:
$ git add --resolved -u
fatal: options '-u/--update' and '--resolved' cannot be used together
Things to know
- Modify/delete conflicts have no markers. If one side deleted a file and the other side changed it, the file in your working tree is plain content.
--resolvedtreats it as resolved and stages it as it is: it keeps the file if it’s there, and records the deletion if you have removed it. So decide whether to keep or delete the file before you run it. - The marker check is a safety net, not a proof. A resolution without markers can still be wrong, so build and test as usual.
- It works not only for merge. It looks at the unmerged entries in the index, so it works the same way after
rebase,cherry-pick,revertorstash pop. - Untracked files are never touched, so it’s also safe when you have scratch files lying around.
If you want to make it a habit, an alias helps:
[alias]
resolved = add --resolved
After that, the flow is simple: git merge topic, fix the files, git resolved, git commit.
I post practical DevOps and SRE notes, new tool features and small tricks in my Telegram channel DevOps & SRE notes.
Conclusion
git add --resolved is a small option, but it solves a real problem. It stages only what you have resolved, it refuses to stage anything while conflict markers are left, and it leaves the rest of your working tree alone. It’s a good replacement for git add -u after every conflict.
In summary: merge, resolve, git add --resolved, commit. And your debugging changes stay where they belong, in your working tree.