{"thread":{"id":"66082","subject":"[PATCH 0/4] git add --resolved","startedAt":"2026-07-28T21:52:21Z","lastAt":"2026-08-03T01:15:33Z","messageCount":30,"participants":["Junio C Hamano","Michael Montalbo","Jeff King"],"isPatch":true,"patchVersion":1,"patchTotal":4},"messages":[{"id":"549168","messageId":"20260728215219.753678-1-gitster@pobox.com","threadId":"66082","inReplyTo":null,"subject":"[PATCH 0/4] git add --resolved","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-28T21:52:15Z","receivedAt":"2026-07-28T21:52:21Z","isPatch":true,"body":"When you are the maintainer of a project and make many merges day\nin, day out, a lot of your time is spent resolving conflicts and\nadding the results to the index.  It is not unusual to have local\nchanges in your working tree that are unrelated to any particular\nmerge [*].  In such cases, 'git add -u', which adds all changes in\nthe working tree to the index, does not help much.\n\nHere is a new option for 'git add' that lets you add paths with\nresolved conflicts to the index, while keeping unrelated local\nchanges out.\n\nThe first two patches perform preliminary refactorings.\n\n - [1/4] consolidates a helper function to determine whether a line\n   is a conflict marker (replacing two slightly different\n   definitions).\n\n - [2/4] introduces a helper that makes registering path removals\n   from the index as easy as adding them, complete with automatic\n   '--dry-run' and '--verbose' support.\n\nThe third patch implements the new feature.\n\nThe fourth patch is a totally unrelated code cleanup that almost\ndisappears when viewed with 'git show -w'.\n\n 1/4: merge-ll: consolidate conflict marker scanning logic\n 2/4: read-cache: add remove_file_from_index_with_flags()\n 3/4: add: introduce '--resolved' option\n 4/4: read-cache: reindent\n\n[Footnote]\n\n * This is not limited to my own workflow. An earlier message on\n   this topic worth mentioning is:\n\n   https://lore.kernel.org/git/CA+55aFxP8j7YbYaRXt-8Y0n8cHafB=FPKMy8gKFYH5QsKX4S=Q@mail.gmail.com/\n\n\n Documentation/git-add.adoc |  10 +++-\n builtin/add.c              |  92 ++++++++++++++++++++++++++++---\n diff.c                     |  25 +--------\n merge-ll.c                 |  53 ++++++++++++++++++\n merge-ll.h                 |   2 +\n read-cache-ll.h            |   3 ++\n read-cache.c               |  89 +++++++++++++++++-------------\n rerere.c                   |  38 +++----------\n t/t2207-add-resolved.sh    | 108 +++++++++++++++++++++++++++++++++++++\n 9 files changed, 319 insertions(+), 101 deletions(-)\n create mode 100755 t/t2207-add-resolved.sh\n\n-- \n2.55.0-594-g42d2bf033e\n\n"},{"id":"549169","messageId":"20260728215219.753678-2-gitster@pobox.com","threadId":"66082","inReplyTo":"20260728215219.753678-1-gitster@pobox.com","subject":"[PATCH 1/4] merge-ll: consolidate conflict marker scanning logic","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-28T21:52:16Z","receivedAt":"2026-07-28T21:52:23Z","isPatch":true,"body":"The diff.c:is_conflict_marker() and rerere.c:is_cmarker() functions\nimplement duplicate logic for identifying conflict marker lines\n(lines that begin with a run of '<', '=', '>', and '|' characters).\n\ndiff.c's original version from 049540435f (diff --check: detect\nleftover conflict markers, 2008-06-26) accepts any whitespace (such\nas a newline) immediately following '<<<<<<<' and '>>>>>>>', whereas\nrerere.c's version from 191f241717 (rerere: prepare for customizable\nconflict marker length, 2010-01-16) strictly requires a space\ncharacter (' ') after them.\n\nImplement is_conflict_marker_line() in merge-ll.c to serve as a\nreplacement for both, and update diff.c and rerere.c to use the new\nhelper.  The unified helper intentionally adopts rerere's stricter\nrule, as the conflicts generated by Git always show the \"ours\" and\n\"theirs\" labels after these markers separated by a space.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c     | 25 +------------------------\n merge-ll.c | 31 +++++++++++++++++++++++++++++++\n merge-ll.h |  1 +\n rerere.c   | 38 ++++++--------------------------------\n 4 files changed, 39 insertions(+), 56 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex 589c1969e4..cfe515af4e 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -3519,29 +3519,6 @@ struct checkdiff_t {\n \tint last_line_kind;\n };\n \n-static int is_conflict_marker(const char *line, int marker_size, unsigned long len)\n-{\n-\tchar firstchar;\n-\tint cnt;\n-\n-\tif (len < marker_size + 1)\n-\t\treturn 0;\n-\tfirstchar = line[0];\n-\tswitch (firstchar) {\n-\tcase '=': case '>': case '<': case '|':\n-\t\tbreak;\n-\tdefault:\n-\t\treturn 0;\n-\t}\n-\tfor (cnt = 1; cnt < marker_size; cnt++)\n-\t\tif (line[cnt] != firstchar)\n-\t\t\treturn 0;\n-\t/* line[1] through line[marker_size-1] are same as firstchar */\n-\tif (len < marker_size + 1 || !isspace(line[marker_size]))\n-\t\treturn 0;\n-\treturn 1;\n-}\n-\n static void checkdiff_consume_hunk(void *priv,\n \t\t\t\t   long ob UNUSED, long on UNUSED,\n \t\t\t\t   long nb, long nn UNUSED,\n@@ -3571,7 +3548,7 @@ static int checkdiff_consume(void *priv, char *line, unsigned long len)\n \tif (line[0] == '+') {\n \t\tunsigned bad;\n \t\tdata->lineno++;\n-\t\tif (is_conflict_marker(line + 1, marker_size, len - 1)) {\n+\t\tif (is_conflict_marker_line(line + 1, len - 1, marker_size)) {\n \t\t\tdata->status |= 1;\n \t\t\tfprintf(data->o->file,\n \t\t\t\t\"%s%s:%d: leftover conflict marker\\n\",\ndiff --git a/merge-ll.c b/merge-ll.c\nindex fafe2c9197..41c97fb90a 100644\n--- a/merge-ll.c\n+++ b/merge-ll.c\n@@ -468,3 +468,34 @@ int ll_merge_marker_size(struct index_state *istate, const char *path)\n \t}\n \treturn marker_size;\n }\n+\n+int is_conflict_marker_line(const char *line, unsigned long len, int marker_size)\n+{\n+\tchar firstchar;\n+\tint cnt;\n+\n+\tif (len < marker_size + 1)\n+\t\treturn 0;\n+\n+\tfirstchar = line[0];\n+\tswitch (firstchar) {\n+\tcase '=': case '>': case '<': case '|':\n+\t\tbreak;\n+\tdefault:\n+\t\treturn 0;\n+\t}\n+\n+\tfor (cnt = 1; cnt < marker_size; cnt++) {\n+\t\tif (line[cnt] != firstchar)\n+\t\t\treturn 0;\n+\t}\n+\n+\tif (((firstchar == '<') || (firstchar == '>')) &&\n+\t    line[marker_size] != ' ')\n+\t\treturn 0;\n+\n+\tif (!isspace((unsigned char)line[marker_size]))\n+\t\treturn 0;\n+\n+\treturn firstchar;\n+}\ndiff --git a/merge-ll.h b/merge-ll.h\nindex d038ee0c1e..b348aee15d 100644\n--- a/merge-ll.h\n+++ b/merge-ll.h\n@@ -109,6 +109,7 @@ enum ll_merge_result ll_merge(mmbuffer_t *result_buf,\n \t     const struct ll_merge_options *opts);\n \n int ll_merge_marker_size(struct index_state *istate, const char *path);\n+int is_conflict_marker_line(const char *line, unsigned long len, int marker_size);\n void reset_merge_attributes(void);\n \n #endif\ndiff --git a/rerere.c b/rerere.c\nindex 1dda246098..4b05850479 100644\n--- a/rerere.c\n+++ b/rerere.c\n@@ -331,33 +331,6 @@ static int rerere_file_getline(struct strbuf *sb, struct rerere_io *io_)\n \treturn strbuf_getwholeline(sb, io->input, '\\n');\n }\n \n-/*\n- * Require the exact number of conflict marker letters, no more, no\n- * less, followed by SP or any whitespace\n- * (including LF).\n- */\n-static int is_cmarker(char *buf, int marker_char, int marker_size)\n-{\n-\tint want_sp;\n-\n-\t/*\n-\t * The beginning of our version and the end of their version\n-\t * always are labeled like \"<<<<< ours\" or \">>>>> theirs\",\n-\t * hence we set want_sp for them.  Note that the version from\n-\t * the common ancestor in diff3-style output is not always\n-\t * labelled (e.g. \"||||| common\" is often seen but \"|||||\"\n-\t * alone is also valid), so we do not set want_sp.\n-\t */\n-\twant_sp = (marker_char == '<') || (marker_char == '>');\n-\n-\twhile (marker_size--)\n-\t\tif (*buf++ != marker_char)\n-\t\t\treturn 0;\n-\tif (want_sp && *buf != ' ')\n-\t\treturn 0;\n-\treturn isspace(*buf);\n-}\n-\n static void rerere_strbuf_putconflict(struct strbuf *buf, int ch, size_t size)\n {\n \tstrbuf_addchars(buf, ch, size);\n@@ -375,7 +348,8 @@ static int handle_conflict(struct strbuf *out, struct rerere_io *io,\n \tint has_conflicts = -1;\n \n \twhile (!io->getline(&buf, io)) {\n-\t\tif (is_cmarker(buf.buf, '<', marker_size)) {\n+\t\tint marker = is_conflict_marker_line(buf.buf, buf.len, marker_size);\n+\t\tif (marker == '<') {\n \t\t\tif (handle_conflict(&conflict, io, marker_size, NULL) < 0)\n \t\t\t\tbreak;\n \t\t\tif (hunk == RR_SIDE_1)\n@@ -383,15 +357,15 @@ static int handle_conflict(struct strbuf *out, struct rerere_io *io,\n \t\t\telse\n \t\t\t\tstrbuf_addbuf(&two, &conflict);\n \t\t\tstrbuf_release(&conflict);\n-\t\t} else if (is_cmarker(buf.buf, '|', marker_size)) {\n+\t\t} else if (marker == '|') {\n \t\t\tif (hunk != RR_SIDE_1)\n \t\t\t\tbreak;\n \t\t\thunk = RR_ORIGINAL;\n-\t\t} else if (is_cmarker(buf.buf, '=', marker_size)) {\n+\t\t} else if (marker == '=') {\n \t\t\tif (hunk != RR_SIDE_1 && hunk != RR_ORIGINAL)\n \t\t\t\tbreak;\n \t\t\thunk = RR_SIDE_2;\n-\t\t} else if (is_cmarker(buf.buf, '>', marker_size)) {\n+\t\t} else if (marker == '>') {\n \t\t\tif (hunk != RR_SIDE_2)\n \t\t\t\tbreak;\n \t\t\tif (strbuf_cmp(&one, &two) > 0)\n@@ -442,7 +416,7 @@ static int handle_path(unsigned char *hash, struct rerere_io *io, int marker_siz\n \t\tgit_hash_init(&ctx, the_hash_algo);\n \n \twhile (!io->getline(&buf, io)) {\n-\t\tif (is_cmarker(buf.buf, '<', marker_size)) {\n+\t\tif (is_conflict_marker_line(buf.buf, buf.len, marker_size) == '<') {\n \t\t\thas_conflicts = handle_conflict(&out, io, marker_size,\n \t\t\t\t\t\t\thash ? &ctx : NULL);\n \t\t\tif (has_conflicts < 0)\n-- \n2.55.0-594-g42d2bf033e\n\n"},{"id":"549170","messageId":"20260728215219.753678-3-gitster@pobox.com","threadId":"66082","inReplyTo":"20260728215219.753678-1-gitster@pobox.com","subject":"[PATCH 2/4] read-cache: add remove_file_from_index_with_flags()","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-28T21:52:17Z","receivedAt":"2026-07-28T21:52:25Z","isPatch":true,"body":"add_file_to_index() takes flags such as ADD_CACHE_PRETEND and\nADD_CACHE_VERBOSE and internally handles both reporting (e.g.,\n\"add 'path'\") and suppressing index updates during dry runs.\n\nIn contrast, remove_file_from_index() takes only istate and path\nwithout flags.  Callers that perform file removals (such as\nupdate_callback() in read-cache.c) are forced to manually inspect\nADD_CACHE_PRETEND and ADD_CACHE_VERBOSE flags for removed\nfiles.\n\nIntroduce remove_file_from_index_with_flags() to encapsulate\npretend mode and verbose reporting for index removals.  Update\nupdate_callback() to use the new helper.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n read-cache-ll.h |  3 +++\n read-cache.c    | 19 +++++++++++++++----\n 2 files changed, 18 insertions(+), 4 deletions(-)\n\ndiff --git a/read-cache-ll.h b/read-cache-ll.h\nindex 71b87615eb..8eb266cfd1 100644\n--- a/read-cache-ll.h\n+++ b/read-cache-ll.h\n@@ -391,11 +391,14 @@ int remove_index_entry_at(struct index_state *, int pos);\n \n void remove_marked_cache_entries(struct index_state *istate, int invalidate);\n int remove_file_from_index(struct index_state *, const char *path);\n+int remove_file_from_index_with_flags(struct index_state *, const char *, int);\n+\n #define ADD_CACHE_VERBOSE 1\n #define ADD_CACHE_PRETEND 2\n #define ADD_CACHE_IGNORE_ERRORS\t4\n #define ADD_CACHE_IGNORE_REMOVAL 8\n #define ADD_CACHE_INTENT 16\n+\n /*\n  * These two are used to add the contents of the file at path\n  * to the index, marking the working tree up-to-date by storing\ndiff --git a/read-cache.c b/read-cache.c\nindex 38b55323dd..6fbab77225 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -638,6 +638,20 @@ int remove_file_from_index(struct index_state *istate, const char *path)\n \treturn 0;\n }\n \n+int remove_file_from_index_with_flags(struct index_state *istate,\n+\t\t\t\t      const char *path,\n+\t\t\t\t      int flags)\n+{\n+\tint verbose = flags & (ADD_CACHE_VERBOSE | ADD_CACHE_PRETEND);\n+\tint pretend = flags & ADD_CACHE_PRETEND;\n+\n+\tif (verbose)\n+\t\tprintf(_(\"remove '%s'\\n\"), path);\n+\tif (pretend)\n+\t\treturn 0;\n+\treturn remove_file_from_index(istate, path);\n+}\n+\n static int compare_name(struct cache_entry *ce, const char *path, int namelen)\n {\n \treturn namelen != ce_namelen(ce) || memcmp(path, ce->name, namelen);\n@@ -4002,10 +4016,7 @@ static void update_callback(struct diff_queue_struct *q,\n \t\tcase DIFF_STATUS_DELETED:\n \t\t\tif (data->flags & ADD_CACHE_IGNORE_REMOVAL)\n \t\t\t\tbreak;\n-\t\t\tif (!(data->flags & ADD_CACHE_PRETEND))\n-\t\t\t\tremove_file_from_index(data->index, path);\n-\t\t\tif (data->flags & (ADD_CACHE_PRETEND|ADD_CACHE_VERBOSE))\n-\t\t\t\tprintf(_(\"remove '%s'\\n\"), path);\n+\t\t\tremove_file_from_index_with_flags(data->index, path, data->flags);\n \t\t\tbreak;\n \t\t}\n \t}\n-- \n2.55.0-594-g42d2bf033e\n\n"},{"id":"549171","messageId":"20260728215219.753678-4-gitster@pobox.com","threadId":"66082","inReplyTo":"20260728215219.753678-1-gitster@pobox.com","subject":"[PATCH 3/4] add: introduce '--resolved' option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-28T21:52:18Z","receivedAt":"2026-07-28T21:52:26Z","isPatch":true,"body":"During a conflicted merge, rebase, or cherry-pick, 'git add -u' is a\nhandy way to add modified paths to the index.  However, '-u'\nindiscriminately adds all modified tracked paths, including unmerged\npaths that may still contain unresolved conflict markers.  It also\nadds tracked files modified in the worktree that are not involved in\nthe ongoing merge.\n\nThe latter is not a huge problem for \"git rebase\", which refuses to\nstart with any local changes, but is a problem for \"git merge\",\nwhich is often run with local changes in maintainer workflows.\n\nIntroduce 'git add --resolved' to add only unmerged paths, limited\nby an optional pathspec, where no conflict markers remain in the\nworking tree.\n\nBefore modifying the index, scan unmerged regular files for leftover\nconflict markers using a new helper, has_conflict_markers(), defined\nin merge-ll.c in terms of the is_conflict_marker_line() helper we\nintroduced earlier.  If any unmerged path still contains conflict\nmarkers, show an error listing the conflicted paths and abort\nwithout updating the index.  Otherwise, add these unmerged paths\nthat do not have conflict markers to the index.\n\nNote that unmerged paths without conflict markers (such as binary\nfiles and deletions) are added as resolved using add_file_to_index()\nand remove_file_from_index_with_flags().  Tracked files that were\nnot in a conflicted state are ignored by '--resolved'.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n Documentation/git-add.adoc |  10 +++-\n builtin/add.c              |  92 ++++++++++++++++++++++++++++---\n merge-ll.c                 |  22 ++++++++\n merge-ll.h                 |   1 +\n t/t2207-add-resolved.sh    | 108 +++++++++++++++++++++++++++++++++++++\n 5 files changed, 226 insertions(+), 7 deletions(-)\n create mode 100755 t/t2207-add-resolved.sh\n\ndiff --git a/Documentation/git-add.adoc b/Documentation/git-add.adoc\nindex 941135dc63..16b06e38e1 100644\n--- a/Documentation/git-add.adoc\n+++ b/Documentation/git-add.adoc\n@@ -11,7 +11,7 @@ SYNOPSIS\n git add [--verbose | -v] [--dry-run | -n] [--force | -f] [--interactive | -i] [--patch | -p]\n \t[--edit | -e] [--[no-]all | -A | --[no-]ignore-removal | [--update | -u]] [--sparse]\n \t[--intent-to-add | -N] [--refresh] [--ignore-errors] [--ignore-missing] [--renormalize]\n-\t[--chmod=(+|-)x] [--pathspec-from-file=<file> [--pathspec-file-nul]]\n+\t[--resolved] [--chmod=(+|-)x] [--pathspec-from-file=<file> [--pathspec-file-nul]]\n \t[--] [<pathspec>...]\n \n DESCRIPTION\n@@ -195,6 +195,14 @@ for `git add --no-all <pathspec>...`, i.e. ignored removed files.\n \twhile a _CRLF_ cleans to _LF_, a _CRCRLF_ sequence is only partially\n \tcleaned to _CRLF_.\n \n+`--resolved`::\n+\tUpdate the index for unmerged paths matching _<pathspec>_ where\n+\tno conflict markers remain in the working tree. Unmerged paths\n+\twithout conflict markers (including binary files and file\n+\tdeletions) are staged as resolved, while any path with leftover\n+\tconflict markers causes the command to refuse to stage any files.\n+\tCannot be combined with `-u` or `-A`.\n+\n `--chmod=(+|-)x`::\n \tOverride the executable bit of the added files.  The executable\n \tbit is only changed in the index, the files on disk are left\ndiff --git a/builtin/add.c b/builtin/add.c\nindex 60ffbede2b..6db57f5773 100644\n--- a/builtin/add.c\n+++ b/builtin/add.c\n@@ -26,6 +26,7 @@\n #include \"strvec.h\"\n #include \"submodule.h\"\n #include \"add-interactive.h\"\n+#include \"merge-ll.h\"\n \n static const char * const builtin_add_usage[] = {\n \tN_(\"git add [<options>] [--] <pathspec>...\"),\n@@ -35,6 +36,7 @@ static int patch_interactive, add_interactive, edit_interactive;\n static struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;\n static int take_worktree_changes;\n static int add_renormalize;\n+static int add_resolved;\n static int pathspec_file_nul;\n static int include_sparse;\n static const char *pathspec_from_file;\n@@ -265,6 +267,7 @@ static struct option builtin_add_options[] = {\n \tOPT__FORCE(&ignored_too, N_(\"allow adding otherwise ignored files\"), 0),\n \tOPT_BOOL('u', \"update\", &take_worktree_changes, N_(\"update tracked files\")),\n \tOPT_BOOL(0, \"renormalize\", &add_renormalize, N_(\"renormalize EOL of tracked files (implies -u)\")),\n+\tOPT_BOOL(0, \"resolved\", &add_resolved, N_(\"add conflict-resolved tracked files\")),\n \tOPT_BOOL('N', \"intent-to-add\", &intent_to_add, N_(\"record only the fact that the path will be added later\")),\n \tOPT_BOOL('A', \"all\", &addremove_explicit, N_(\"add changes from all tracked and untracked files\")),\n \tOPT_CALLBACK_F(0, \"ignore-removal\", &addremove_explicit,\n@@ -379,6 +382,76 @@ static int add_files(struct repository *repo, struct dir_struct *dir, int flags)\n \treturn exit_status;\n }\n \n+static int failed_to_add(int flags, const char *path)\n+{\n+\tif (!(flags & ADD_CACHE_IGNORE_ERRORS))\n+\t\tdie(_(\"updating file '%s' failed\"), path);\n+\treturn 1;\n+}\n+\n+static int add_resolved_files(struct repository *repo,\n+\t\t\t      const struct pathspec *pathspec,\n+\t\t\t      int flags)\n+{\n+\tstruct index_state *istate = repo->index;\n+\tstruct string_list unmerged_paths = STRING_LIST_INIT_DUP;\n+\tstruct string_list unresolved_paths = STRING_LIST_INIT_DUP;\n+\tint exit_status = 0;\n+\tsize_t i;\n+\n+\tfor (i = 0; i < istate->cache_nr; i++) {\n+\t\tstruct cache_entry *ce = istate->cache[i];\n+\t\tif (!ce_stage(ce))\n+\t\t\tcontinue;\n+\t\tif (pathspec->nr && !ce_path_match(istate, ce, pathspec, NULL))\n+\t\t\tcontinue;\n+\t\tif (!unmerged_paths.nr ||\n+\t\t    strcmp(unmerged_paths.items[unmerged_paths.nr - 1].string, ce->name))\n+\t\t\tstring_list_append(&unmerged_paths, ce->name);\n+\t}\n+\n+\tif (!unmerged_paths.nr) {\n+\t\tstring_list_clear(&unmerged_paths, 0);\n+\t\treturn 0;\n+\t}\n+\n+\tfor (i = 0; i < unmerged_paths.nr; i++) {\n+\t\tconst char *path = unmerged_paths.items[i].string;\n+\t\tstruct stat st;\n+\n+\t\tif (!lstat(path, &st) && S_ISREG(st.st_mode)) {\n+\t\t\tif (has_conflict_markers(istate, path))\n+\t\t\t\tstring_list_append(&unresolved_paths, path);\n+\t\t}\n+\t}\n+\n+\tif (unresolved_paths.nr) {\n+\t\tstruct strbuf sb = STRBUF_INIT;\n+\t\tfor (i = 0; i < unresolved_paths.nr; i++)\n+\t\t\tstrbuf_addf(&sb, \"\\t%s\\n\", unresolved_paths.items[i].string);\n+\t\tdie(_(\"the following paths still have conflict markers:\\n%s\"), sb.buf);\n+\t}\n+\n+\tfor (i = 0; i < unmerged_paths.nr; i++) {\n+\t\tconst char *path = unmerged_paths.items[i].string;\n+\t\tstruct stat st;\n+\n+\t\tif (lstat(path, &st)) {\n+\t\t\tif (errno != ENOENT)\n+\t\t\t\tdie_errno(_(\"cannot lstat: '%s'\"), path);\n+\t\t\tif (remove_file_from_index_with_flags(istate, path, flags))\n+\t\t\t\texit_status = failed_to_add(flags, path);\n+\t\t} else {\n+\t\t\tif (add_file_to_index(istate, path, flags))\n+\t\t\t\texit_status = failed_to_add(flags, path);\n+\t\t}\n+\t}\n+\n+\tstring_list_clear(&unmerged_paths, 0);\n+\tstring_list_clear(&unresolved_paths, 0);\n+\treturn exit_status;\n+}\n+\n int cmd_add(int argc,\n \t    const char **argv,\n \t    const char *prefix,\n@@ -438,8 +511,9 @@ int cmd_add(int argc,\n \telse if (take_worktree_changes && ADDREMOVE_DEFAULT)\n \t\taddremove = 0; /* \"-u\" was given but not \"-A\" */\n \n-\tif (addremove && take_worktree_changes)\n-\t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"-A\", \"-u\");\n+\tdie_for_incompatible_opt3(take_worktree_changes, \"-u/--update\",\n+\t\t\t\t  0 <= addremove_explicit, \"-A/--all\",\n+\t\t\t\t  add_resolved, \"--resolved\");\n \n \tif (!show_only && ignore_missing)\n \t\tdie(_(\"the option '%s' requires '%s'\"), \"--ignore-missing\", \"--dry-run\");\n@@ -448,8 +522,11 @@ int cmd_add(int argc,\n \t\t\t  chmod_arg[1] != 'x' || chmod_arg[2]))\n \t\tdie(_(\"--chmod param '%s' must be either -x or +x\"), chmod_arg);\n \n-\tadd_new_files = !take_worktree_changes && !refresh_only && !add_renormalize;\n-\trequire_pathspec = !(take_worktree_changes || (0 < addremove_explicit));\n+\tadd_new_files = !take_worktree_changes && !refresh_only &&\n+\t\t\t!add_renormalize && !add_resolved;\n+\trequire_pathspec = !(take_worktree_changes ||\n+\t\t\t     (0 < addremove_explicit) ||\n+\t\t\t     add_resolved);\n \n \trepo_hold_locked_index(repo, &lock_file, LOCK_DIE_ON_ERROR);\n \n@@ -481,7 +558,8 @@ int cmd_add(int argc,\n \t\treturn 0;\n \t}\n \n-\tif (!take_worktree_changes && addremove_explicit < 0 && pathspec.nr)\n+\tif (!take_worktree_changes && !add_resolved &&\n+\t    addremove_explicit < 0 && pathspec.nr)\n \t\t/* Turn \"git add pathspec...\" to \"git add -A pathspec...\" */\n \t\taddremove = 1;\n \n@@ -584,7 +662,9 @@ int cmd_add(int argc,\n \todb_transaction_begin_or_die(repo->objects, &transaction, 0);\n \n \tps_matched = xcalloc(pathspec.nr, 1);\n-\tif (add_renormalize)\n+\tif (add_resolved)\n+\t\texit_status |= add_resolved_files(repo, &pathspec, flags);\n+\telse if (add_renormalize)\n \t\texit_status |= renormalize_tracked_files(repo, &pathspec, flags);\n \telse\n \t\texit_status |= add_files_to_cache(repo, prefix,\ndiff --git a/merge-ll.c b/merge-ll.c\nindex 41c97fb90a..5e5044b9e3 100644\n--- a/merge-ll.c\n+++ b/merge-ll.c\n@@ -499,3 +499,25 @@ int is_conflict_marker_line(const char *line, unsigned long len, int marker_size\n \n \treturn firstchar;\n }\n+\n+int has_conflict_markers(struct index_state *istate, const char *path)\n+{\n+\tFILE *f;\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tint marker_size = ll_merge_marker_size(istate, path);\n+\tint has_markers = 0;\n+\n+\tf = fopen(path, \"r\");\n+\tif (!f)\n+\t\treturn 0;\n+\n+\twhile (strbuf_getwholeline(&sb, f, '\\n') != EOF) {\n+\t\tif (is_conflict_marker_line(sb.buf, sb.len, marker_size)) {\n+\t\t\thas_markers = 1;\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\tfclose(f);\n+\tstrbuf_release(&sb);\n+\treturn has_markers;\n+}\ndiff --git a/merge-ll.h b/merge-ll.h\nindex b348aee15d..f26aef238d 100644\n--- a/merge-ll.h\n+++ b/merge-ll.h\n@@ -110,6 +110,7 @@ enum ll_merge_result ll_merge(mmbuffer_t *result_buf,\n \n int ll_merge_marker_size(struct index_state *istate, const char *path);\n int is_conflict_marker_line(const char *line, unsigned long len, int marker_size);\n+int has_conflict_markers(struct index_state *istate, const char *path);\n void reset_merge_attributes(void);\n \n #endif\ndiff --git a/t/t2207-add-resolved.sh b/t/t2207-add-resolved.sh\nnew file mode 100755\nindex 0000000000..f88e3f413e\n--- /dev/null\n+++ b/t/t2207-add-resolved.sh\n@@ -0,0 +1,108 @@\n+#!/bin/sh\n+\n+test_description='git add --resolved\n+\n+Test that \"git add --resolved\" stages conflict-resolved paths and\n+refuses to stage when conflict markers remain.'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'setup repo' '\n+\techo base >file1.txt &&\n+\techo base >file2.txt &&\n+\techo base >file3.txt &&\n+\techo base >file4.txt &&\n+\tgit add file1.txt file2.txt file3.txt file4.txt &&\n+\tgit commit -m initial &&\n+\n+\tgit branch topic &&\n+\techo \"ours 1\" >file1.txt &&\n+\techo \"ours 2\" >file2.txt &&\n+\techo \"ours 3\" >file3.txt &&\n+\tgit commit -a -m ours &&\n+\n+\tgit checkout topic &&\n+\techo \"theirs 1\" >file1.txt &&\n+\techo \"theirs 2\" >file2.txt &&\n+\techo \"theirs 3\" >file3.txt &&\n+\tgit commit -a -m theirs &&\n+\n+\tgit checkout master\n+'\n+\n+test_expect_success 'git add --resolved refuses files with conflict markers' '\n+\ttest_when_finished \"git reset --hard HEAD\" &&\n+\ttest_must_fail git merge topic &&\n+\techo \"resolved 1\" >file1.txt &&\n+\ttest_must_fail git add --resolved 2>err &&\n+\ttest_grep \"the following paths still have conflict markers:\" err &&\n+\ttest_grep \"file2.txt\" err &&\n+\ttest_grep \"file3.txt\" err &&\n+\t# Index should remain unmerged for all files\n+\tgit ls-files -u file1.txt >unmerged &&\n+\ttest_line_count = 3 unmerged\n+'\n+\n+test_expect_success 'git add --resolved succeeds when all conflict markers are removed' '\n+\ttest_when_finished \"git reset --hard HEAD\" &&\n+\ttest_must_fail git merge topic &&\n+\techo \"resolved 1\" >file1.txt &&\n+\techo \"resolved 2\" >file2.txt &&\n+\techo \"resolved 3\" >file3.txt &&\n+\tgit add --resolved &&\n+\tgit ls-files -u >unmerged &&\n+\ttest_must_be_empty unmerged &&\n+\tgit ls-files -s file1.txt file2.txt file3.txt >staged &&\n+\ttest_line_count = 3 staged\n+'\n+\n+test_expect_success 'git add --resolved ignores unconflicted modified files' '\n+\ttest_when_finished \"git reset --hard HEAD\" &&\n+\techo \"unconflicted local change\" >>file4.txt &&\n+\ttest_must_fail git merge topic &&\n+\techo \"resolved 1\" >file1.txt &&\n+\techo \"resolved 2\" >file2.txt &&\n+\techo \"resolved 3\" >file3.txt &&\n+\tgit add --resolved &&\n+\t# file1, file2, file3 should be staged as resolved\n+\tgit ls-files -u >unmerged &&\n+\ttest_must_be_empty unmerged &&\n+\t# file4 should remain unstaged in working tree\n+\tgit diff file4.txt >diff_out &&\n+\ttest_grep \"unconflicted local change\" diff_out &&\n+\tgit diff --cached file4.txt >cached_out &&\n+\ttest_must_be_empty cached_out\n+'\n+\n+test_expect_success 'git add --resolved handles file removals' '\n+\ttest_when_finished \"git reset --hard HEAD\" &&\n+\ttest_must_fail git merge topic &&\n+\techo \"resolved 1\" >file1.txt &&\n+\trm file2.txt &&\n+\techo \"resolved 3\" >file3.txt &&\n+\tgit add --resolved &&\n+\tgit ls-files -s file2.txt >out &&\n+\ttest_must_be_empty out\n+'\n+\n+test_expect_success 'git add --resolved honors pathspec' '\n+\ttest_when_finished \"git reset --hard HEAD\" &&\n+\ttest_must_fail git merge topic &&\n+\techo \"resolved 1\" >file1.txt &&\n+\t# file2.txt and file3.txt still have conflict markers,\n+\t# but pathspec targets only file1.txt\n+\tgit add --resolved file1.txt &&\n+\tgit ls-files -u file1.txt >unmerged1 &&\n+\ttest_must_be_empty unmerged1 &&\n+\tgit ls-files -u file2.txt >unmerged2 &&\n+\ttest_line_count = 3 unmerged2\n+'\n+\n+test_expect_success 'git add --resolved incompatibility with -u and -A' '\n+\ttest_must_fail git add --resolved -u 2>err1 &&\n+\ttest_grep \"cannot be used together\" err1 &&\n+\ttest_must_fail git add --resolved -A 2>err2 &&\n+\ttest_grep \"cannot be used together\" err2\n+'\n+\n+test_done\n-- \n2.55.0-594-g42d2bf033e\n\n"},{"id":"549172","messageId":"20260728215219.753678-5-gitster@pobox.com","threadId":"66082","inReplyTo":"20260728215219.753678-1-gitster@pobox.com","subject":"[PATCH 4/4] read-cache: reindent","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-28T21:52:19Z","receivedAt":"2026-07-28T21:52:28Z","isPatch":true,"body":"I do not know how this happened without anybody noticing, but a few\nmonths ago we added a16c4a245a (read-cache: submodule add need\n--force given ignore=all configuration, 2026-02-06), and almost all\nlines the patch added were incorrectly indented.\n\nReindent these lines so that they play better with surrounding lines\nin the same file.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n read-cache.c | 70 +++++++++++++++++++++++++++-------------------------\n 1 file changed, 36 insertions(+), 34 deletions(-)\n\ndiff --git a/read-cache.c b/read-cache.c\nindex 6fbab77225..ad77c0d5e2 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -3924,32 +3924,33 @@ static int fix_unmerged_status(struct diff_filepair *p,\n }\n \n static int skip_submodule(const char *path,\n-\t\t\t\t\t\tstruct repository *repo,\n-\t\t\t\t\t\tstruct pathspec *pathspec,\n-\t\t\t\t\t\tint ignored_too)\n-{\n-    struct stat st;\n-    const struct submodule *sub;\n-    int pathspec_matches = 0;\n-    int ps_i;\n-    char *norm_pathspec = NULL;\n-\n-    /* Only consider if path is a directory */\n-    if (lstat(path, &st) || !S_ISDIR(st.st_mode))\n+\t\t\t  struct repository *repo,\n+\t\t\t  struct pathspec *pathspec,\n+\t\t\t  int ignored_too)\n+{\n+\tstruct stat st;\n+\tconst struct submodule *sub;\n+\tint pathspec_matches = 0;\n+\tint ps_i;\n+\tchar *norm_pathspec = NULL;\n+\n+\t/* Only consider if path is a directory */\n+\tif (lstat(path, &st) || !S_ISDIR(st.st_mode))\n \t\treturn 0;\n \n-    /* Check if it's a submodule with ignore=all */\n-    sub = submodule_from_path(repo, null_oid(the_hash_algo), path);\n-    if (!sub || !sub->name || !sub->ignore || strcmp(sub->ignore, \"all\"))\n+\t/* Check if it's a submodule with ignore=all */\n+\tsub = submodule_from_path(repo, null_oid(the_hash_algo), path);\n+\tif (!sub || !sub->name || !sub->ignore || strcmp(sub->ignore, \"all\"))\n \t\treturn 0;\n \n-    trace_printf(\"ignore=all: %s\\n\", path);\n-    trace_printf(\"pathspec %s\\n\", (pathspec && pathspec->nr)\n-\t\t\t\t\t\t\t\t\t? \"has pathspec\"\n-\t\t\t\t\t\t\t\t\t: \"no pathspec\");\n+\ttrace_printf(\"ignore=all: %s\\n\", path);\n+\ttrace_printf(\"pathspec %s\\n\",\n+\t\t     ((pathspec && pathspec->nr)\n+\t\t      ? \"has pathspec\"\n+\t\t      : \"no pathspec\"));\n \n-    /* Check if submodule path is explicitly mentioned in pathspec */\n-    if (pathspec) {\n+\t/* Check if submodule path is explicitly mentioned in pathspec */\n+\tif (pathspec) {\n \t\tfor (ps_i = 0; ps_i < pathspec->nr; ps_i++) {\n \t\t\tconst char *m = pathspec->items[ps_i].match;\n \t\t\tif (!m)\n@@ -3963,28 +3964,29 @@ static int skip_submodule(const char *path,\n \t\t\t}\n \t\t\tFREE_AND_NULL(norm_pathspec);\n \t\t}\n-    }\n+\t}\n \n-    /* If explicitly matched and forced, allow adding */\n-    if (pathspec_matches) {\n+\t/* If explicitly matched and forced, allow adding */\n+\tif (pathspec_matches) {\n \t\tif (ignored_too && ignored_too > 0) {\n \t\t\ttrace_printf(\"Add submodule due to --force: %s\\n\", path);\n \t\t\treturn 0;\n \t\t} else {\n \t\t\tadvise_if_enabled(ADVICE_ADD_IGNORED_FILE,\n-\t\t\t\t_(\"Skipping submodule due to ignore=all: %s\\n\"\n-\t\t\t\t\t\"Use --force if you really want to add the submodule.\"), path);\n+\t\t\t\t  _(\"Skipping submodule due to ignore=all: %s\\n\"\n+\t\t\t\t    \"Use --force if you really want to \"\n+\t\t\t\t    \"add the submodule.\"), path);\n \t\t\treturn 1;\n \t\t}\n-    }\n+\t}\n \n-    /* No explicit pathspec match -> skip silently */\n-    trace_printf(\"Pathspec to submodule does not match explicitly: %s\\n\", path);\n-    return 1;\n+\t/* No explicit pathspec match -> skip silently */\n+\ttrace_printf(\"Pathspec to submodule does not match explicitly: %s\\n\", path);\n+\treturn 1;\n }\n \n static void update_callback(struct diff_queue_struct *q,\n-\t\t\t\t\t\t\tstruct diff_options *opt UNUSED, void *cbdata)\n+\t\t\t    struct diff_options *opt UNUSED, void *cbdata)\n {\n \tint i;\n \tstruct update_callback_data *data = cbdata;\n@@ -3994,7 +3996,7 @@ static void update_callback(struct diff_queue_struct *q,\n \t\tconst char *path = p->one->path;\n \n \t\tif (!data->include_sparse &&\n-\t\t\t!path_in_sparse_checkout(path, data->index))\n+\t\t    !path_in_sparse_checkout(path, data->index))\n \t\t\tcontinue;\n \n \t\tswitch (fix_unmerged_status(p, data)) {\n@@ -4003,8 +4005,8 @@ static void update_callback(struct diff_queue_struct *q,\n \t\tcase DIFF_STATUS_MODIFIED:\n \t\tcase DIFF_STATUS_TYPE_CHANGED:\n \t\t\tif (skip_submodule(path, data->repo,\n-\t\t\t\t\t\t\t\tdata->pathspec,\n-\t\t\t\t\t\t\t\tdata->ignored_too))\n+\t\t\t\t\t   data->pathspec,\n+\t\t\t\t\t   data->ignored_too))\n \t\t\t\tcontinue;\n \n \t\t\tif (add_file_to_index(data->index, path, data->flags)) {\n-- \n2.55.0-594-g42d2bf033e\n\n"},{"id":"549186","messageId":"CAC2Qwm+AmeDubDrkLmu8Rz9rtO3697gokwF2=2c5PgP--hZT2w@mail.gmail.com","threadId":"66082","inReplyTo":"20260728215219.753678-4-gitster@pobox.com","subject":"Re: [PATCH 3/4] add: introduce '--resolved' option","fromName":"Michael Montalbo","fromEmail":"mmontalbo@gmail.com","sentAt":"2026-07-29T03:28:11Z","receivedAt":"2026-07-29T03:28:25Z","isPatch":true,"body":"On Tue, Jul 28, 2026 at 2:58 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> -       if (addremove && take_worktree_changes)\n> -               die(_(\"options '%s' and '%s' cannot be used together\"), \"-A\", \"-u\");\n> +       die_for_incompatible_opt3(take_worktree_changes, \"-u/--update\",\n> +                                 0 <= addremove_explicit, \"-A/--all\",\n> +                                 add_resolved, \"--resolved\");\n>\n\nShould this be \"0 < addremove_explicit\"? I thought addremove_explicit being\nset to 0 indicates either --no-all or --ignore-removal (via ignore_removal_cb)\nwas specified by the user. I think this causes \"git add --resolved --no-all\" to\ndie naming \"-A/--all\"  as the culprit even though the opposite flag was set.\nAlso, it may cause \"git add -u --ignore-removal\" and \"git add -u --no-all\" to\nnow die, whereas they were accepted before.\n\n>\n> diff --git a/t/t2207-add-resolved.sh b/t/t2207-add-resolved.sh\n> new file mode 100755\n> index 0000000000..f88e3f413e\n> --- /dev/null\n> +++ b/t/t2207-add-resolved.sh\n\nDoes this new test file need a t/meson.build entry?\n"},{"id":"549205","messageId":"xmqqcxw5c3na.fsf@gitster.g","threadId":"66082","inReplyTo":"CAC2Qwm+AmeDubDrkLmu8Rz9rtO3697gokwF2=2c5PgP--hZT2w@mail.gmail.com","subject":"Re: [PATCH 3/4] add: introduce '--resolved' option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-29T13:43:37Z","receivedAt":"2026-07-29T13:43:40Z","isPatch":true,"body":"Michael Montalbo <mmontalbo@gmail.com> writes:\n\n> On Tue, Jul 28, 2026 at 2:58 PM Junio C Hamano <gitster@pobox.com> wrote:\n>>\n>> -       if (addremove && take_worktree_changes)\n>> -               die(_(\"options '%s' and '%s' cannot be used together\"), \"-A\", \"-u\");\n>> +       die_for_incompatible_opt3(take_worktree_changes, \"-u/--update\",\n>> +                                 0 <= addremove_explicit, \"-A/--all\",\n>> +                                 add_resolved, \"--resolved\");\n>>\n>\n> Should this be \"0 < addremove_explicit\"?\n\nThanks for noticing.  '0 <=' is absolutely bogus.\n\nI wonder if we should simply write 'addremove', as this part of the\nchange is about rewriting the open-coded die_for_incompatible_opt2()\nin the original and using die_for_incompatible_opt3() to add a third\noption.\n\n\n>> diff --git a/t/t2207-add-resolved.sh b/t/t2207-add-resolved.sh\n>> new file mode 100755\n>> index 0000000000..f88e3f413e\n>> --- /dev/null\n>> +++ b/t/t2207-add-resolved.sh\n>\n> Does this new test file need a t/meson.build entry?\n\nYeah, it shows that I rarely exercise meson build.\n\nThanks.\n"},{"id":"549206","messageId":"xmqq8q6tc3cy.fsf@gitster.g","threadId":"66082","inReplyTo":"xmqqcxw5c3na.fsf@gitster.g","subject":"Re: [PATCH 3/4] add: introduce '--resolved' option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-29T13:49:49Z","receivedAt":"2026-07-29T13:49:56Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Michael Montalbo <mmontalbo@gmail.com> writes:\n>\n>> On Tue, Jul 28, 2026 at 2:58 PM Junio C Hamano <gitster@pobox.com> wrote:\n>>>\n>>> -       if (addremove && take_worktree_changes)\n>>> -               die(_(\"options '%s' and '%s' cannot be used together\"), \"-A\", \"-u\");\n>>> +       die_for_incompatible_opt3(take_worktree_changes, \"-u/--update\",\n>>> +                                 0 <= addremove_explicit, \"-A/--all\",\n>>> +                                 add_resolved, \"--resolved\");\n>>>\n>>\n>> Should this be \"0 < addremove_explicit\"?\n>\n> Thanks for noticing.  '0 <=' is absolutely bogus.\n>\n> I wonder if we should simply write 'addremove', as this part of the\n> change is about rewriting the open-coded die_for_incompatible_opt2()\n> in the original and using die_for_incompatible_opt3() to add a third\n> option.\n\nNot really.  I forgot that addremove is futzed before this part when\nadd_resolved is present, so using addremove would mean both\naddremove and add_resolved are on, mistakenly complaining about -A\nthat the user never gave us.\n\nSo \"0 < addremove_explicit\" that is.\n\n>\n>>> diff --git a/t/t2207-add-resolved.sh b/t/t2207-add-resolved.sh\n>>> new file mode 100755\n>>> index 0000000000..f88e3f413e\n>>> --- /dev/null\n>>> +++ b/t/t2207-add-resolved.sh\n>>\n>> Does this new test file need a t/meson.build entry?\n>\n> Yeah, it shows that I rarely exercise meson build.\n>\n> Thanks.\n"},{"id":"549209","messageId":"xmqqse51algy.fsf@gitster.g","threadId":"66082","inReplyTo":"20260728215219.753678-4-gitster@pobox.com","subject":"Re: [PATCH 3/4] add: introduce '--resolved' option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-29T15:01:33Z","receivedAt":"2026-07-29T15:01:35Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> +\n> +int has_conflict_markers(struct index_state *istate, const char *path)\n> +{\n> +\tFILE *f;\n> +\tstruct strbuf sb = STRBUF_INIT;\n> +\tint marker_size = ll_merge_marker_size(istate, path);\n> +\tint has_markers = 0;\n> +\n> +\tf = fopen(path, \"r\");\n> +\tif (!f)\n> +\t\treturn 0;\n> +\n> +\twhile (strbuf_getwholeline(&sb, f, '\\n') != EOF) {\n> +\t\tif (is_conflict_marker_line(sb.buf, sb.len, marker_size)) {\n> +\t\t\thas_markers = 1;\n> +\t\t\tbreak;\n> +\t\t}\n> +\t}\n> +\tfclose(f);\n> +\tstrbuf_release(&sb);\n> +\treturn has_markers;\n> +}\n\nLeft unchecked, this loop may end up scanning a large binary file to\nthe end in vain.  We may squeeze in something like this to punt\nearly.\n\n merge-ll.c | 3 +++\n 1 file changed, 3 insertions(+)\n\ndiff --git a/merge-ll.c b/merge-ll.c\nindex 5e5044b9e3..ef5287dee8 100644\n--- a/merge-ll.c\n+++ b/merge-ll.c\n@@ -516,6 +516,9 @@ int has_conflict_markers(struct index_state *istate, const char *path)\n \t\t\thas_markers = 1;\n \t\t\tbreak;\n \t\t}\n+\t\tif (buffer_is_binary(sb.buf,\n+\t\t\t\t     ULONG_MAX <= sb.len ? ULONG_MAX : sb.len))\n+\t\t\tbreak;\n \t}\n \tfclose(f);\n \tstrbuf_release(&sb);\n-- \n2.55.0-609-g9a17695db7\n\n"},{"id":"549220","messageId":"20260729172524.4022621-1-gitster@pobox.com","threadId":"66082","inReplyTo":"20260728215219.753678-1-gitster@pobox.com","subject":"[PATCH v2 0/4] git add --resolved","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-29T17:25:20Z","receivedAt":"2026-07-29T17:25:26Z","isPatch":true,"body":"When you are the maintainer of a project and make many merges day\nin, day out, a lot of your time is spent resolving conflicts and\nadding the results to the index.  It is not unusual to have local\nchanges in your working tree that are unrelated to any particular\nmerge [*].  In such cases, 'git add -u', which adds all changes in\nthe working tree to the index, does not help much.\n\nHere is a new option for 'git add' that lets you add paths with\nresolved conflicts to the index, while keeping unrelated local\nchanges out.\n\nThe first three patches perform preliminary refactorings.\n\n - [1/4] is a totally unrelated code cleanup that almost disappears\n   when viewed with 'git show -w', but it was an eyesore to have so\n   many lines with broken indentation while working in the vicinity.\n\n - [2/4] consolidates a helper function to determine whether a line\n   is a conflict marker (replacing two slightly different\n   definitions).\n\n - [3/4] introduces a helper that makes registering path removals\n   from the index as easy as adding them, complete with automatic\n   '--dry-run' and '--verbose' support.\n\nThe fourth patch implements the new feature.  Relative to v1, the\ndetection of the use of the '-A' option was fixed and the Meson build\nfile was updated to include the new test script, both thanks to\nMichael Montalbo.  In addition, the has_conflict_markers() helper\nhas been tightened to bail early on a binary file.\n\n 1/4: read-cache: reindent\n 2/4: merge-ll: consolidate conflict marker scanning logic\n 3/4: read-cache: add remove_file_from_index_with_flags()\n 4/4: add: introduce '--resolved' option\n\n[Footnote]\n\n * This is not limited to my own workflow. An earlier message on\n   this topic worth mentioning is:\n\n   https://lore.kernel.org/git/CA+55aFxP8j7YbYaRXt-8Y0n8cHafB=FPKMy8gKFYH5QsKX4S=Q@mail.gmail.com/\n\n Documentation/git-add.adoc |  10 +++-\n builtin/add.c              |  92 ++++++++++++++++++++++++++++---\n diff.c                     |  25 +--------\n merge-ll.c                 |  56 +++++++++++++++++++\n merge-ll.h                 |   2 +\n read-cache-ll.h            |   3 ++\n read-cache.c               |  89 +++++++++++++++++-------------\n rerere.c                   |  38 +++----------\n t/meson.build              |   1 +\n t/t2207-add-resolved.sh    | 108 +++++++++++++++++++++++++++++++++++++\n 10 files changed, 323 insertions(+), 101 deletions(-)\n create mode 100755 t/t2207-add-resolved.sh\n\nRange-diff against v1:\n4:  c503fbb785 = 1:  e46fe3e887 read-cache: reindent\n1:  03ea86d803 = 2:  b5490819bd merge-ll: consolidate conflict marker scanning logic\n2:  e94e3c1390 = 3:  e1f4aba480 read-cache: add remove_file_from_index_with_flags()\n3:  73679d6b69 ! 4:  b1308a0ca1 add: introduce '--resolved' option\n    @@ builtin/add.c: int cmd_add(int argc,\n     -\tif (addremove && take_worktree_changes)\n     -\t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"-A\", \"-u\");\n     +\tdie_for_incompatible_opt3(take_worktree_changes, \"-u/--update\",\n    -+\t\t\t\t  0 <= addremove_explicit, \"-A/--all\",\n    ++\t\t\t\t  0 < addremove_explicit, \"-A/--all\",\n     +\t\t\t\t  add_resolved, \"--resolved\");\n      \n      \tif (!show_only && ignore_missing)\n    @@ merge-ll.c: int is_conflict_marker_line(const char *line, unsigned long len, int\n     +\t\t\thas_markers = 1;\n     +\t\t\tbreak;\n     +\t\t}\n    ++\t\tif (buffer_is_binary(sb.buf,\n    ++\t\t\t\t     ULONG_MAX <= sb.len ? ULONG_MAX : sb.len))\n    ++\t\t\tbreak;\n     +\t}\n     +\tfclose(f);\n     +\tstrbuf_release(&sb);\n    @@ merge-ll.h: enum ll_merge_result ll_merge(mmbuffer_t *result_buf,\n      \n      #endif\n     \n    + ## t/meson.build ##\n    +@@ t/meson.build: integration_tests = [\n    +   't2204-add-ignored.sh',\n    +   't2205-add-worktree-config.sh',\n    +   't2206-add-submodule-ignored.sh',\n    ++  't2207-add-resolved.sh',\n    +   't2300-cd-to-toplevel.sh',\n    +   't2400-worktree-add.sh',\n    +   't2401-worktree-prune.sh',\n    +\n      ## t/t2207-add-resolved.sh (new) ##\n     @@\n     +#!/bin/sh\n-- \n2.55.0-609-g9a17695db7\n\n"},{"id":"549221","messageId":"20260729172524.4022621-2-gitster@pobox.com","threadId":"66082","inReplyTo":"20260729172524.4022621-1-gitster@pobox.com","subject":"[PATCH v2 1/4] read-cache: reindent","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-29T17:25:21Z","receivedAt":"2026-07-29T17:25:28Z","isPatch":true,"body":"I do not know how this happened without anybody noticing, but a few\nmonths ago we added a16c4a245a (read-cache: submodule add need\n--force given ignore=all configuration, 2026-02-06), and almost all\nlines the patch added were incorrectly indented.\n\nReindent these lines so that they play better with surrounding lines\nin the same file.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n read-cache.c | 70 +++++++++++++++++++++++++++-------------------------\n 1 file changed, 36 insertions(+), 34 deletions(-)\n\ndiff --git a/read-cache.c b/read-cache.c\nindex 38b55323dd..58c378414a 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -3910,32 +3910,33 @@ static int fix_unmerged_status(struct diff_filepair *p,\n }\n \n static int skip_submodule(const char *path,\n-\t\t\t\t\t\tstruct repository *repo,\n-\t\t\t\t\t\tstruct pathspec *pathspec,\n-\t\t\t\t\t\tint ignored_too)\n-{\n-    struct stat st;\n-    const struct submodule *sub;\n-    int pathspec_matches = 0;\n-    int ps_i;\n-    char *norm_pathspec = NULL;\n-\n-    /* Only consider if path is a directory */\n-    if (lstat(path, &st) || !S_ISDIR(st.st_mode))\n+\t\t\t  struct repository *repo,\n+\t\t\t  struct pathspec *pathspec,\n+\t\t\t  int ignored_too)\n+{\n+\tstruct stat st;\n+\tconst struct submodule *sub;\n+\tint pathspec_matches = 0;\n+\tint ps_i;\n+\tchar *norm_pathspec = NULL;\n+\n+\t/* Only consider if path is a directory */\n+\tif (lstat(path, &st) || !S_ISDIR(st.st_mode))\n \t\treturn 0;\n \n-    /* Check if it's a submodule with ignore=all */\n-    sub = submodule_from_path(repo, null_oid(the_hash_algo), path);\n-    if (!sub || !sub->name || !sub->ignore || strcmp(sub->ignore, \"all\"))\n+\t/* Check if it's a submodule with ignore=all */\n+\tsub = submodule_from_path(repo, null_oid(the_hash_algo), path);\n+\tif (!sub || !sub->name || !sub->ignore || strcmp(sub->ignore, \"all\"))\n \t\treturn 0;\n \n-    trace_printf(\"ignore=all: %s\\n\", path);\n-    trace_printf(\"pathspec %s\\n\", (pathspec && pathspec->nr)\n-\t\t\t\t\t\t\t\t\t? \"has pathspec\"\n-\t\t\t\t\t\t\t\t\t: \"no pathspec\");\n+\ttrace_printf(\"ignore=all: %s\\n\", path);\n+\ttrace_printf(\"pathspec %s\\n\",\n+\t\t     ((pathspec && pathspec->nr)\n+\t\t      ? \"has pathspec\"\n+\t\t      : \"no pathspec\"));\n \n-    /* Check if submodule path is explicitly mentioned in pathspec */\n-    if (pathspec) {\n+\t/* Check if submodule path is explicitly mentioned in pathspec */\n+\tif (pathspec) {\n \t\tfor (ps_i = 0; ps_i < pathspec->nr; ps_i++) {\n \t\t\tconst char *m = pathspec->items[ps_i].match;\n \t\t\tif (!m)\n@@ -3949,28 +3950,29 @@ static int skip_submodule(const char *path,\n \t\t\t}\n \t\t\tFREE_AND_NULL(norm_pathspec);\n \t\t}\n-    }\n+\t}\n \n-    /* If explicitly matched and forced, allow adding */\n-    if (pathspec_matches) {\n+\t/* If explicitly matched and forced, allow adding */\n+\tif (pathspec_matches) {\n \t\tif (ignored_too && ignored_too > 0) {\n \t\t\ttrace_printf(\"Add submodule due to --force: %s\\n\", path);\n \t\t\treturn 0;\n \t\t} else {\n \t\t\tadvise_if_enabled(ADVICE_ADD_IGNORED_FILE,\n-\t\t\t\t_(\"Skipping submodule due to ignore=all: %s\\n\"\n-\t\t\t\t\t\"Use --force if you really want to add the submodule.\"), path);\n+\t\t\t\t  _(\"Skipping submodule due to ignore=all: %s\\n\"\n+\t\t\t\t    \"Use --force if you really want to \"\n+\t\t\t\t    \"add the submodule.\"), path);\n \t\t\treturn 1;\n \t\t}\n-    }\n+\t}\n \n-    /* No explicit pathspec match -> skip silently */\n-    trace_printf(\"Pathspec to submodule does not match explicitly: %s\\n\", path);\n-    return 1;\n+\t/* No explicit pathspec match -> skip silently */\n+\ttrace_printf(\"Pathspec to submodule does not match explicitly: %s\\n\", path);\n+\treturn 1;\n }\n \n static void update_callback(struct diff_queue_struct *q,\n-\t\t\t\t\t\t\tstruct diff_options *opt UNUSED, void *cbdata)\n+\t\t\t    struct diff_options *opt UNUSED, void *cbdata)\n {\n \tint i;\n \tstruct update_callback_data *data = cbdata;\n@@ -3980,7 +3982,7 @@ static void update_callback(struct diff_queue_struct *q,\n \t\tconst char *path = p->one->path;\n \n \t\tif (!data->include_sparse &&\n-\t\t\t!path_in_sparse_checkout(path, data->index))\n+\t\t    !path_in_sparse_checkout(path, data->index))\n \t\t\tcontinue;\n \n \t\tswitch (fix_unmerged_status(p, data)) {\n@@ -3989,8 +3991,8 @@ static void update_callback(struct diff_queue_struct *q,\n \t\tcase DIFF_STATUS_MODIFIED:\n \t\tcase DIFF_STATUS_TYPE_CHANGED:\n \t\t\tif (skip_submodule(path, data->repo,\n-\t\t\t\t\t\t\t\tdata->pathspec,\n-\t\t\t\t\t\t\t\tdata->ignored_too))\n+\t\t\t\t\t   data->pathspec,\n+\t\t\t\t\t   data->ignored_too))\n \t\t\t\tcontinue;\n \n \t\t\tif (add_file_to_index(data->index, path, data->flags)) {\n-- \n2.55.0-609-g9a17695db7\n\n"},{"id":"549222","messageId":"20260729172524.4022621-3-gitster@pobox.com","threadId":"66082","inReplyTo":"20260729172524.4022621-1-gitster@pobox.com","subject":"[PATCH v2 2/4] merge-ll: consolidate conflict marker scanning logic","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-29T17:25:22Z","receivedAt":"2026-07-29T17:25:30Z","isPatch":true,"body":"The diff.c:is_conflict_marker() and rerere.c:is_cmarker() functions\nimplement duplicate logic for identifying conflict marker lines\n(lines that begin with a run of '<', '=', '>', and '|' characters).\n\ndiff.c's original version from 049540435f (diff --check: detect\nleftover conflict markers, 2008-06-26) accepts any whitespace (such\nas a newline) immediately following '<<<<<<<' and '>>>>>>>', whereas\nrerere.c's version from 191f241717 (rerere: prepare for customizable\nconflict marker length, 2010-01-16) strictly requires a space\ncharacter (' ') after them.\n\nImplement is_conflict_marker_line() in merge-ll.c to serve as a\nreplacement for both, and update diff.c and rerere.c to use the new\nhelper.  The unified helper intentionally adopts rerere's stricter\nrule, as the conflicts generated by Git always show the \"ours\" and\n\"theirs\" labels after these markers separated by a space.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c     | 25 +------------------------\n merge-ll.c | 31 +++++++++++++++++++++++++++++++\n merge-ll.h |  1 +\n rerere.c   | 38 ++++++--------------------------------\n 4 files changed, 39 insertions(+), 56 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex 589c1969e4..cfe515af4e 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -3519,29 +3519,6 @@ struct checkdiff_t {\n \tint last_line_kind;\n };\n \n-static int is_conflict_marker(const char *line, int marker_size, unsigned long len)\n-{\n-\tchar firstchar;\n-\tint cnt;\n-\n-\tif (len < marker_size + 1)\n-\t\treturn 0;\n-\tfirstchar = line[0];\n-\tswitch (firstchar) {\n-\tcase '=': case '>': case '<': case '|':\n-\t\tbreak;\n-\tdefault:\n-\t\treturn 0;\n-\t}\n-\tfor (cnt = 1; cnt < marker_size; cnt++)\n-\t\tif (line[cnt] != firstchar)\n-\t\t\treturn 0;\n-\t/* line[1] through line[marker_size-1] are same as firstchar */\n-\tif (len < marker_size + 1 || !isspace(line[marker_size]))\n-\t\treturn 0;\n-\treturn 1;\n-}\n-\n static void checkdiff_consume_hunk(void *priv,\n \t\t\t\t   long ob UNUSED, long on UNUSED,\n \t\t\t\t   long nb, long nn UNUSED,\n@@ -3571,7 +3548,7 @@ static int checkdiff_consume(void *priv, char *line, unsigned long len)\n \tif (line[0] == '+') {\n \t\tunsigned bad;\n \t\tdata->lineno++;\n-\t\tif (is_conflict_marker(line + 1, marker_size, len - 1)) {\n+\t\tif (is_conflict_marker_line(line + 1, len - 1, marker_size)) {\n \t\t\tdata->status |= 1;\n \t\t\tfprintf(data->o->file,\n \t\t\t\t\"%s%s:%d: leftover conflict marker\\n\",\ndiff --git a/merge-ll.c b/merge-ll.c\nindex fafe2c9197..41c97fb90a 100644\n--- a/merge-ll.c\n+++ b/merge-ll.c\n@@ -468,3 +468,34 @@ int ll_merge_marker_size(struct index_state *istate, const char *path)\n \t}\n \treturn marker_size;\n }\n+\n+int is_conflict_marker_line(const char *line, unsigned long len, int marker_size)\n+{\n+\tchar firstchar;\n+\tint cnt;\n+\n+\tif (len < marker_size + 1)\n+\t\treturn 0;\n+\n+\tfirstchar = line[0];\n+\tswitch (firstchar) {\n+\tcase '=': case '>': case '<': case '|':\n+\t\tbreak;\n+\tdefault:\n+\t\treturn 0;\n+\t}\n+\n+\tfor (cnt = 1; cnt < marker_size; cnt++) {\n+\t\tif (line[cnt] != firstchar)\n+\t\t\treturn 0;\n+\t}\n+\n+\tif (((firstchar == '<') || (firstchar == '>')) &&\n+\t    line[marker_size] != ' ')\n+\t\treturn 0;\n+\n+\tif (!isspace((unsigned char)line[marker_size]))\n+\t\treturn 0;\n+\n+\treturn firstchar;\n+}\ndiff --git a/merge-ll.h b/merge-ll.h\nindex d038ee0c1e..b348aee15d 100644\n--- a/merge-ll.h\n+++ b/merge-ll.h\n@@ -109,6 +109,7 @@ enum ll_merge_result ll_merge(mmbuffer_t *result_buf,\n \t     const struct ll_merge_options *opts);\n \n int ll_merge_marker_size(struct index_state *istate, const char *path);\n+int is_conflict_marker_line(const char *line, unsigned long len, int marker_size);\n void reset_merge_attributes(void);\n \n #endif\ndiff --git a/rerere.c b/rerere.c\nindex 216100925a..924a1f2e30 100644\n--- a/rerere.c\n+++ b/rerere.c\n@@ -331,33 +331,6 @@ static int rerere_file_getline(struct strbuf *sb, struct rerere_io *io_)\n \treturn strbuf_getwholeline(sb, io->input, '\\n');\n }\n \n-/*\n- * Require the exact number of conflict marker letters, no more, no\n- * less, followed by SP or any whitespace\n- * (including LF).\n- */\n-static int is_cmarker(char *buf, int marker_char, int marker_size)\n-{\n-\tint want_sp;\n-\n-\t/*\n-\t * The beginning of our version and the end of their version\n-\t * always are labeled like \"<<<<< ours\" or \">>>>> theirs\",\n-\t * hence we set want_sp for them.  Note that the version from\n-\t * the common ancestor in diff3-style output is not always\n-\t * labelled (e.g. \"||||| common\" is often seen but \"|||||\"\n-\t * alone is also valid), so we do not set want_sp.\n-\t */\n-\twant_sp = (marker_char == '<') || (marker_char == '>');\n-\n-\twhile (marker_size--)\n-\t\tif (*buf++ != marker_char)\n-\t\t\treturn 0;\n-\tif (want_sp && *buf != ' ')\n-\t\treturn 0;\n-\treturn isspace(*buf);\n-}\n-\n static void rerere_strbuf_putconflict(struct strbuf *buf, int ch, size_t size)\n {\n \tstrbuf_addchars(buf, ch, size);\n@@ -375,7 +348,8 @@ static int handle_conflict(struct strbuf *out, struct rerere_io *io,\n \tint has_conflicts = -1;\n \n \twhile (!io->getline(&buf, io)) {\n-\t\tif (is_cmarker(buf.buf, '<', marker_size)) {\n+\t\tint marker = is_conflict_marker_line(buf.buf, buf.len, marker_size);\n+\t\tif (marker == '<') {\n \t\t\tif (handle_conflict(&conflict, io, marker_size, NULL) < 0)\n \t\t\t\tbreak;\n \t\t\tif (hunk == RR_SIDE_1)\n@@ -383,15 +357,15 @@ static int handle_conflict(struct strbuf *out, struct rerere_io *io,\n \t\t\telse\n \t\t\t\tstrbuf_addbuf(&two, &conflict);\n \t\t\tstrbuf_release(&conflict);\n-\t\t} else if (is_cmarker(buf.buf, '|', marker_size)) {\n+\t\t} else if (marker == '|') {\n \t\t\tif (hunk != RR_SIDE_1)\n \t\t\t\tbreak;\n \t\t\thunk = RR_ORIGINAL;\n-\t\t} else if (is_cmarker(buf.buf, '=', marker_size)) {\n+\t\t} else if (marker == '=') {\n \t\t\tif (hunk != RR_SIDE_1 && hunk != RR_ORIGINAL)\n \t\t\t\tbreak;\n \t\t\thunk = RR_SIDE_2;\n-\t\t} else if (is_cmarker(buf.buf, '>', marker_size)) {\n+\t\t} else if (marker == '>') {\n \t\t\tif (hunk != RR_SIDE_2)\n \t\t\t\tbreak;\n \t\t\tif (strbuf_cmp(&one, &two) > 0)\n@@ -442,7 +416,7 @@ static int handle_path(unsigned char *hash, struct rerere_io *io, int marker_siz\n \t\tgit_hash_init(&ctx, the_hash_algo);\n \n \twhile (!io->getline(&buf, io)) {\n-\t\tif (is_cmarker(buf.buf, '<', marker_size)) {\n+\t\tif (is_conflict_marker_line(buf.buf, buf.len, marker_size) == '<') {\n \t\t\thas_conflicts = handle_conflict(&out, io, marker_size,\n \t\t\t\t\t\t\thash ? &ctx : NULL);\n \t\t\tif (has_conflicts < 0)\n-- \n2.55.0-609-g9a17695db7\n\n"},{"id":"549223","messageId":"20260729172524.4022621-4-gitster@pobox.com","threadId":"66082","inReplyTo":"20260729172524.4022621-1-gitster@pobox.com","subject":"[PATCH v2 3/4] read-cache: add remove_file_from_index_with_flags()","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-29T17:25:23Z","receivedAt":"2026-07-29T17:25:31Z","isPatch":true,"body":"add_file_to_index() takes flags such as ADD_CACHE_PRETEND and\nADD_CACHE_VERBOSE and internally handles both reporting (e.g.,\n\"add 'path'\") and suppressing index updates during dry runs.\n\nIn contrast, remove_file_from_index() takes only istate and path\nwithout flags.  Callers that perform file removals (such as\nupdate_callback() in read-cache.c) are forced to manually inspect\nADD_CACHE_PRETEND and ADD_CACHE_VERBOSE flags for removed\nfiles.\n\nIntroduce remove_file_from_index_with_flags() to encapsulate\npretend mode and verbose reporting for index removals.  Update\nupdate_callback() to use the new helper.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n read-cache-ll.h |  3 +++\n read-cache.c    | 19 +++++++++++++++----\n 2 files changed, 18 insertions(+), 4 deletions(-)\n\ndiff --git a/read-cache-ll.h b/read-cache-ll.h\nindex 71b87615eb..8eb266cfd1 100644\n--- a/read-cache-ll.h\n+++ b/read-cache-ll.h\n@@ -391,11 +391,14 @@ int remove_index_entry_at(struct index_state *, int pos);\n \n void remove_marked_cache_entries(struct index_state *istate, int invalidate);\n int remove_file_from_index(struct index_state *, const char *path);\n+int remove_file_from_index_with_flags(struct index_state *, const char *, int);\n+\n #define ADD_CACHE_VERBOSE 1\n #define ADD_CACHE_PRETEND 2\n #define ADD_CACHE_IGNORE_ERRORS\t4\n #define ADD_CACHE_IGNORE_REMOVAL 8\n #define ADD_CACHE_INTENT 16\n+\n /*\n  * These two are used to add the contents of the file at path\n  * to the index, marking the working tree up-to-date by storing\ndiff --git a/read-cache.c b/read-cache.c\nindex 58c378414a..ad77c0d5e2 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -638,6 +638,20 @@ int remove_file_from_index(struct index_state *istate, const char *path)\n \treturn 0;\n }\n \n+int remove_file_from_index_with_flags(struct index_state *istate,\n+\t\t\t\t      const char *path,\n+\t\t\t\t      int flags)\n+{\n+\tint verbose = flags & (ADD_CACHE_VERBOSE | ADD_CACHE_PRETEND);\n+\tint pretend = flags & ADD_CACHE_PRETEND;\n+\n+\tif (verbose)\n+\t\tprintf(_(\"remove '%s'\\n\"), path);\n+\tif (pretend)\n+\t\treturn 0;\n+\treturn remove_file_from_index(istate, path);\n+}\n+\n static int compare_name(struct cache_entry *ce, const char *path, int namelen)\n {\n \treturn namelen != ce_namelen(ce) || memcmp(path, ce->name, namelen);\n@@ -4004,10 +4018,7 @@ static void update_callback(struct diff_queue_struct *q,\n \t\tcase DIFF_STATUS_DELETED:\n \t\t\tif (data->flags & ADD_CACHE_IGNORE_REMOVAL)\n \t\t\t\tbreak;\n-\t\t\tif (!(data->flags & ADD_CACHE_PRETEND))\n-\t\t\t\tremove_file_from_index(data->index, path);\n-\t\t\tif (data->flags & (ADD_CACHE_PRETEND|ADD_CACHE_VERBOSE))\n-\t\t\t\tprintf(_(\"remove '%s'\\n\"), path);\n+\t\t\tremove_file_from_index_with_flags(data->index, path, data->flags);\n \t\t\tbreak;\n \t\t}\n \t}\n-- \n2.55.0-609-g9a17695db7\n\n"},{"id":"549224","messageId":"20260729172524.4022621-5-gitster@pobox.com","threadId":"66082","inReplyTo":"20260729172524.4022621-1-gitster@pobox.com","subject":"[PATCH v2 4/4] add: introduce '--resolved' option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-29T17:25:24Z","receivedAt":"2026-07-29T17:25:33Z","isPatch":true,"body":"During a conflicted merge, rebase, or cherry-pick, 'git add -u' is a\nhandy way to add modified paths to the index.  However, '-u'\nindiscriminately adds all modified tracked paths, including unmerged\npaths that may still contain unresolved conflict markers.  It also\nadds tracked files modified in the worktree that are not involved in\nthe ongoing merge.\n\nThe latter is not a huge problem for \"git rebase\", which refuses to\nstart with any local changes, but is a problem for \"git merge\",\nwhich is often run with local changes in maintainer workflows.\n\nIntroduce 'git add --resolved' to add only unmerged paths, limited\nby an optional pathspec, where no conflict markers remain in the\nworking tree.\n\nBefore modifying the index, scan unmerged regular files for leftover\nconflict markers using a new helper, has_conflict_markers(), defined\nin merge-ll.c in terms of the is_conflict_marker_line() helper we\nintroduced earlier.  If any unmerged path still contains conflict\nmarkers, show an error listing the conflicted paths and abort\nwithout updating the index.  Otherwise, add these unmerged paths\nthat do not have conflict markers to the index.\n\nNote that unmerged paths without conflict markers (such as binary\nfiles and deletions) are added as resolved using add_file_to_index()\nand remove_file_from_index_with_flags().  Tracked files that were\nnot in a conflicted state are ignored by '--resolved'.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n Documentation/git-add.adoc |  10 +++-\n builtin/add.c              |  92 ++++++++++++++++++++++++++++---\n merge-ll.c                 |  25 +++++++++\n merge-ll.h                 |   1 +\n t/meson.build              |   1 +\n t/t2207-add-resolved.sh    | 108 +++++++++++++++++++++++++++++++++++++\n 6 files changed, 230 insertions(+), 7 deletions(-)\n create mode 100755 t/t2207-add-resolved.sh\n\ndiff --git a/Documentation/git-add.adoc b/Documentation/git-add.adoc\nindex 941135dc63..16b06e38e1 100644\n--- a/Documentation/git-add.adoc\n+++ b/Documentation/git-add.adoc\n@@ -11,7 +11,7 @@ SYNOPSIS\n git add [--verbose | -v] [--dry-run | -n] [--force | -f] [--interactive | -i] [--patch | -p]\n \t[--edit | -e] [--[no-]all | -A | --[no-]ignore-removal | [--update | -u]] [--sparse]\n \t[--intent-to-add | -N] [--refresh] [--ignore-errors] [--ignore-missing] [--renormalize]\n-\t[--chmod=(+|-)x] [--pathspec-from-file=<file> [--pathspec-file-nul]]\n+\t[--resolved] [--chmod=(+|-)x] [--pathspec-from-file=<file> [--pathspec-file-nul]]\n \t[--] [<pathspec>...]\n \n DESCRIPTION\n@@ -195,6 +195,14 @@ for `git add --no-all <pathspec>...`, i.e. ignored removed files.\n \twhile a _CRLF_ cleans to _LF_, a _CRCRLF_ sequence is only partially\n \tcleaned to _CRLF_.\n \n+`--resolved`::\n+\tUpdate the index for unmerged paths matching _<pathspec>_ where\n+\tno conflict markers remain in the working tree. Unmerged paths\n+\twithout conflict markers (including binary files and file\n+\tdeletions) are staged as resolved, while any path with leftover\n+\tconflict markers causes the command to refuse to stage any files.\n+\tCannot be combined with `-u` or `-A`.\n+\n `--chmod=(+|-)x`::\n \tOverride the executable bit of the added files.  The executable\n \tbit is only changed in the index, the files on disk are left\ndiff --git a/builtin/add.c b/builtin/add.c\nindex 60ffbede2b..eab8f03cad 100644\n--- a/builtin/add.c\n+++ b/builtin/add.c\n@@ -26,6 +26,7 @@\n #include \"strvec.h\"\n #include \"submodule.h\"\n #include \"add-interactive.h\"\n+#include \"merge-ll.h\"\n \n static const char * const builtin_add_usage[] = {\n \tN_(\"git add [<options>] [--] <pathspec>...\"),\n@@ -35,6 +36,7 @@ static int patch_interactive, add_interactive, edit_interactive;\n static struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;\n static int take_worktree_changes;\n static int add_renormalize;\n+static int add_resolved;\n static int pathspec_file_nul;\n static int include_sparse;\n static const char *pathspec_from_file;\n@@ -265,6 +267,7 @@ static struct option builtin_add_options[] = {\n \tOPT__FORCE(&ignored_too, N_(\"allow adding otherwise ignored files\"), 0),\n \tOPT_BOOL('u', \"update\", &take_worktree_changes, N_(\"update tracked files\")),\n \tOPT_BOOL(0, \"renormalize\", &add_renormalize, N_(\"renormalize EOL of tracked files (implies -u)\")),\n+\tOPT_BOOL(0, \"resolved\", &add_resolved, N_(\"add conflict-resolved tracked files\")),\n \tOPT_BOOL('N', \"intent-to-add\", &intent_to_add, N_(\"record only the fact that the path will be added later\")),\n \tOPT_BOOL('A', \"all\", &addremove_explicit, N_(\"add changes from all tracked and untracked files\")),\n \tOPT_CALLBACK_F(0, \"ignore-removal\", &addremove_explicit,\n@@ -379,6 +382,76 @@ static int add_files(struct repository *repo, struct dir_struct *dir, int flags)\n \treturn exit_status;\n }\n \n+static int failed_to_add(int flags, const char *path)\n+{\n+\tif (!(flags & ADD_CACHE_IGNORE_ERRORS))\n+\t\tdie(_(\"updating file '%s' failed\"), path);\n+\treturn 1;\n+}\n+\n+static int add_resolved_files(struct repository *repo,\n+\t\t\t      const struct pathspec *pathspec,\n+\t\t\t      int flags)\n+{\n+\tstruct index_state *istate = repo->index;\n+\tstruct string_list unmerged_paths = STRING_LIST_INIT_DUP;\n+\tstruct string_list unresolved_paths = STRING_LIST_INIT_DUP;\n+\tint exit_status = 0;\n+\tsize_t i;\n+\n+\tfor (i = 0; i < istate->cache_nr; i++) {\n+\t\tstruct cache_entry *ce = istate->cache[i];\n+\t\tif (!ce_stage(ce))\n+\t\t\tcontinue;\n+\t\tif (pathspec->nr && !ce_path_match(istate, ce, pathspec, NULL))\n+\t\t\tcontinue;\n+\t\tif (!unmerged_paths.nr ||\n+\t\t    strcmp(unmerged_paths.items[unmerged_paths.nr - 1].string, ce->name))\n+\t\t\tstring_list_append(&unmerged_paths, ce->name);\n+\t}\n+\n+\tif (!unmerged_paths.nr) {\n+\t\tstring_list_clear(&unmerged_paths, 0);\n+\t\treturn 0;\n+\t}\n+\n+\tfor (i = 0; i < unmerged_paths.nr; i++) {\n+\t\tconst char *path = unmerged_paths.items[i].string;\n+\t\tstruct stat st;\n+\n+\t\tif (!lstat(path, &st) && S_ISREG(st.st_mode)) {\n+\t\t\tif (has_conflict_markers(istate, path))\n+\t\t\t\tstring_list_append(&unresolved_paths, path);\n+\t\t}\n+\t}\n+\n+\tif (unresolved_paths.nr) {\n+\t\tstruct strbuf sb = STRBUF_INIT;\n+\t\tfor (i = 0; i < unresolved_paths.nr; i++)\n+\t\t\tstrbuf_addf(&sb, \"\\t%s\\n\", unresolved_paths.items[i].string);\n+\t\tdie(_(\"the following paths still have conflict markers:\\n%s\"), sb.buf);\n+\t}\n+\n+\tfor (i = 0; i < unmerged_paths.nr; i++) {\n+\t\tconst char *path = unmerged_paths.items[i].string;\n+\t\tstruct stat st;\n+\n+\t\tif (lstat(path, &st)) {\n+\t\t\tif (errno != ENOENT)\n+\t\t\t\tdie_errno(_(\"cannot lstat: '%s'\"), path);\n+\t\t\tif (remove_file_from_index_with_flags(istate, path, flags))\n+\t\t\t\texit_status = failed_to_add(flags, path);\n+\t\t} else {\n+\t\t\tif (add_file_to_index(istate, path, flags))\n+\t\t\t\texit_status = failed_to_add(flags, path);\n+\t\t}\n+\t}\n+\n+\tstring_list_clear(&unmerged_paths, 0);\n+\tstring_list_clear(&unresolved_paths, 0);\n+\treturn exit_status;\n+}\n+\n int cmd_add(int argc,\n \t    const char **argv,\n \t    const char *prefix,\n@@ -438,8 +511,9 @@ int cmd_add(int argc,\n \telse if (take_worktree_changes && ADDREMOVE_DEFAULT)\n \t\taddremove = 0; /* \"-u\" was given but not \"-A\" */\n \n-\tif (addremove && take_worktree_changes)\n-\t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"-A\", \"-u\");\n+\tdie_for_incompatible_opt3(take_worktree_changes, \"-u/--update\",\n+\t\t\t\t  0 < addremove_explicit, \"-A/--all\",\n+\t\t\t\t  add_resolved, \"--resolved\");\n \n \tif (!show_only && ignore_missing)\n \t\tdie(_(\"the option '%s' requires '%s'\"), \"--ignore-missing\", \"--dry-run\");\n@@ -448,8 +522,11 @@ int cmd_add(int argc,\n \t\t\t  chmod_arg[1] != 'x' || chmod_arg[2]))\n \t\tdie(_(\"--chmod param '%s' must be either -x or +x\"), chmod_arg);\n \n-\tadd_new_files = !take_worktree_changes && !refresh_only && !add_renormalize;\n-\trequire_pathspec = !(take_worktree_changes || (0 < addremove_explicit));\n+\tadd_new_files = !take_worktree_changes && !refresh_only &&\n+\t\t\t!add_renormalize && !add_resolved;\n+\trequire_pathspec = !(take_worktree_changes ||\n+\t\t\t     (0 < addremove_explicit) ||\n+\t\t\t     add_resolved);\n \n \trepo_hold_locked_index(repo, &lock_file, LOCK_DIE_ON_ERROR);\n \n@@ -481,7 +558,8 @@ int cmd_add(int argc,\n \t\treturn 0;\n \t}\n \n-\tif (!take_worktree_changes && addremove_explicit < 0 && pathspec.nr)\n+\tif (!take_worktree_changes && !add_resolved &&\n+\t    addremove_explicit < 0 && pathspec.nr)\n \t\t/* Turn \"git add pathspec...\" to \"git add -A pathspec...\" */\n \t\taddremove = 1;\n \n@@ -584,7 +662,9 @@ int cmd_add(int argc,\n \todb_transaction_begin_or_die(repo->objects, &transaction, 0);\n \n \tps_matched = xcalloc(pathspec.nr, 1);\n-\tif (add_renormalize)\n+\tif (add_resolved)\n+\t\texit_status |= add_resolved_files(repo, &pathspec, flags);\n+\telse if (add_renormalize)\n \t\texit_status |= renormalize_tracked_files(repo, &pathspec, flags);\n \telse\n \t\texit_status |= add_files_to_cache(repo, prefix,\ndiff --git a/merge-ll.c b/merge-ll.c\nindex 41c97fb90a..ef5287dee8 100644\n--- a/merge-ll.c\n+++ b/merge-ll.c\n@@ -499,3 +499,28 @@ int is_conflict_marker_line(const char *line, unsigned long len, int marker_size\n \n \treturn firstchar;\n }\n+\n+int has_conflict_markers(struct index_state *istate, const char *path)\n+{\n+\tFILE *f;\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tint marker_size = ll_merge_marker_size(istate, path);\n+\tint has_markers = 0;\n+\n+\tf = fopen(path, \"r\");\n+\tif (!f)\n+\t\treturn 0;\n+\n+\twhile (strbuf_getwholeline(&sb, f, '\\n') != EOF) {\n+\t\tif (is_conflict_marker_line(sb.buf, sb.len, marker_size)) {\n+\t\t\thas_markers = 1;\n+\t\t\tbreak;\n+\t\t}\n+\t\tif (buffer_is_binary(sb.buf,\n+\t\t\t\t     ULONG_MAX <= sb.len ? ULONG_MAX : sb.len))\n+\t\t\tbreak;\n+\t}\n+\tfclose(f);\n+\tstrbuf_release(&sb);\n+\treturn has_markers;\n+}\ndiff --git a/merge-ll.h b/merge-ll.h\nindex b348aee15d..f26aef238d 100644\n--- a/merge-ll.h\n+++ b/merge-ll.h\n@@ -110,6 +110,7 @@ enum ll_merge_result ll_merge(mmbuffer_t *result_buf,\n \n int ll_merge_marker_size(struct index_state *istate, const char *path);\n int is_conflict_marker_line(const char *line, unsigned long len, int marker_size);\n+int has_conflict_markers(struct index_state *istate, const char *path);\n void reset_merge_attributes(void);\n \n #endif\ndiff --git a/t/meson.build b/t/meson.build\nindex 8ae6ab6c5f..e0f4b85ad5 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -304,6 +304,7 @@ integration_tests = [\n   't2204-add-ignored.sh',\n   't2205-add-worktree-config.sh',\n   't2206-add-submodule-ignored.sh',\n+  't2207-add-resolved.sh',\n   't2300-cd-to-toplevel.sh',\n   't2400-worktree-add.sh',\n   't2401-worktree-prune.sh',\ndiff --git a/t/t2207-add-resolved.sh b/t/t2207-add-resolved.sh\nnew file mode 100755\nindex 0000000000..f88e3f413e\n--- /dev/null\n+++ b/t/t2207-add-resolved.sh\n@@ -0,0 +1,108 @@\n+#!/bin/sh\n+\n+test_description='git add --resolved\n+\n+Test that \"git add --resolved\" stages conflict-resolved paths and\n+refuses to stage when conflict markers remain.'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'setup repo' '\n+\techo base >file1.txt &&\n+\techo base >file2.txt &&\n+\techo base >file3.txt &&\n+\techo base >file4.txt &&\n+\tgit add file1.txt file2.txt file3.txt file4.txt &&\n+\tgit commit -m initial &&\n+\n+\tgit branch topic &&\n+\techo \"ours 1\" >file1.txt &&\n+\techo \"ours 2\" >file2.txt &&\n+\techo \"ours 3\" >file3.txt &&\n+\tgit commit -a -m ours &&\n+\n+\tgit checkout topic &&\n+\techo \"theirs 1\" >file1.txt &&\n+\techo \"theirs 2\" >file2.txt &&\n+\techo \"theirs 3\" >file3.txt &&\n+\tgit commit -a -m theirs &&\n+\n+\tgit checkout master\n+'\n+\n+test_expect_success 'git add --resolved refuses files with conflict markers' '\n+\ttest_when_finished \"git reset --hard HEAD\" &&\n+\ttest_must_fail git merge topic &&\n+\techo \"resolved 1\" >file1.txt &&\n+\ttest_must_fail git add --resolved 2>err &&\n+\ttest_grep \"the following paths still have conflict markers:\" err &&\n+\ttest_grep \"file2.txt\" err &&\n+\ttest_grep \"file3.txt\" err &&\n+\t# Index should remain unmerged for all files\n+\tgit ls-files -u file1.txt >unmerged &&\n+\ttest_line_count = 3 unmerged\n+'\n+\n+test_expect_success 'git add --resolved succeeds when all conflict markers are removed' '\n+\ttest_when_finished \"git reset --hard HEAD\" &&\n+\ttest_must_fail git merge topic &&\n+\techo \"resolved 1\" >file1.txt &&\n+\techo \"resolved 2\" >file2.txt &&\n+\techo \"resolved 3\" >file3.txt &&\n+\tgit add --resolved &&\n+\tgit ls-files -u >unmerged &&\n+\ttest_must_be_empty unmerged &&\n+\tgit ls-files -s file1.txt file2.txt file3.txt >staged &&\n+\ttest_line_count = 3 staged\n+'\n+\n+test_expect_success 'git add --resolved ignores unconflicted modified files' '\n+\ttest_when_finished \"git reset --hard HEAD\" &&\n+\techo \"unconflicted local change\" >>file4.txt &&\n+\ttest_must_fail git merge topic &&\n+\techo \"resolved 1\" >file1.txt &&\n+\techo \"resolved 2\" >file2.txt &&\n+\techo \"resolved 3\" >file3.txt &&\n+\tgit add --resolved &&\n+\t# file1, file2, file3 should be staged as resolved\n+\tgit ls-files -u >unmerged &&\n+\ttest_must_be_empty unmerged &&\n+\t# file4 should remain unstaged in working tree\n+\tgit diff file4.txt >diff_out &&\n+\ttest_grep \"unconflicted local change\" diff_out &&\n+\tgit diff --cached file4.txt >cached_out &&\n+\ttest_must_be_empty cached_out\n+'\n+\n+test_expect_success 'git add --resolved handles file removals' '\n+\ttest_when_finished \"git reset --hard HEAD\" &&\n+\ttest_must_fail git merge topic &&\n+\techo \"resolved 1\" >file1.txt &&\n+\trm file2.txt &&\n+\techo \"resolved 3\" >file3.txt &&\n+\tgit add --resolved &&\n+\tgit ls-files -s file2.txt >out &&\n+\ttest_must_be_empty out\n+'\n+\n+test_expect_success 'git add --resolved honors pathspec' '\n+\ttest_when_finished \"git reset --hard HEAD\" &&\n+\ttest_must_fail git merge topic &&\n+\techo \"resolved 1\" >file1.txt &&\n+\t# file2.txt and file3.txt still have conflict markers,\n+\t# but pathspec targets only file1.txt\n+\tgit add --resolved file1.txt &&\n+\tgit ls-files -u file1.txt >unmerged1 &&\n+\ttest_must_be_empty unmerged1 &&\n+\tgit ls-files -u file2.txt >unmerged2 &&\n+\ttest_line_count = 3 unmerged2\n+'\n+\n+test_expect_success 'git add --resolved incompatibility with -u and -A' '\n+\ttest_must_fail git add --resolved -u 2>err1 &&\n+\ttest_grep \"cannot be used together\" err1 &&\n+\ttest_must_fail git add --resolved -A 2>err2 &&\n+\ttest_grep \"cannot be used together\" err2\n+'\n+\n+test_done\n-- \n2.55.0-609-g9a17695db7\n\n"},{"id":"549240","messageId":"xmqqy0etmoae.fsf@gitster.g","threadId":"66082","inReplyTo":"20260729172524.4022621-5-gitster@pobox.com","subject":"Re: [PATCH v2 4/4] add: introduce '--resolved' option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-29T22:19:53Z","receivedAt":"2026-07-29T22:19:55Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> diff --git a/t/t2207-add-resolved.sh b/t/t2207-add-resolved.sh\n> new file mode 100755\n> index 0000000000..f88e3f413e\n> --- /dev/null\n> +++ b/t/t2207-add-resolved.sh\n> @@ -0,0 +1,108 @@\n> +#!/bin/sh\n> +\n> +test_description='git add --resolved\n> +\n> +Test that \"git add --resolved\" stages conflict-resolved paths and\n> +refuses to stage when conflict markers remain.'\n> +\n> +. ./test-lib.sh\n> +\n> +test_expect_success 'setup repo' '\n> ...\n> +\tgit branch topic &&\n> +\techo \"ours 1\" >file1.txt &&\n> +\techo \"ours 2\" >file2.txt &&\n> +\techo \"ours 3\" >file3.txt &&\n> +\tgit commit -a -m ours &&\n> +\n> +\tgit checkout topic &&\n> +\techo \"theirs 1\" >file1.txt &&\n> +\techo \"theirs 2\" >file2.txt &&\n> +\techo \"theirs 3\" >file3.txt &&\n> +\tgit commit -a -m theirs &&\n> +\n> +\tgit checkout master\n> +'\n\nThis will fail with breaking changes.  The last step needs to be\n\n\tgit checkout @{-1}\n\nto go back to the original branch we were on before checking out the\n'topic' branch.\n"},{"id":"549253","messageId":"CAC2QwmKD1d=-mz4WrkA7CHwi-ko75MP2LDFPWx+1FGLQtf4QtA@mail.gmail.com","threadId":"66082","inReplyTo":"20260729172524.4022621-1-gitster@pobox.com","subject":"Re: [PATCH v2 0/4] git add --resolved","fromName":"Michael Montalbo","fromEmail":"mmontalbo@gmail.com","sentAt":"2026-07-29T23:41:15Z","receivedAt":"2026-07-29T23:41:29Z","isPatch":true,"body":"On Wed, Jul 29, 2026 at 11:37 AM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> When you are the maintainer of a project and make many merges day\n> in, day out, a lot of your time is spent resolving conflicts and\n> adding the results to the index.  It is not unusual to have local\n> changes in your working tree that are unrelated to any particular\n> merge [*].  In such cases, 'git add -u', which adds all changes in\n> the working tree to the index, does not help much.\n>\n> Here is a new option for 'git add' that lets you add paths with\n> resolved conflicts to the index, while keeping unrelated local\n> changes out.\n\nPlease take this feedback with a grain of salt, as I am probably not\nthe target audience for this feature and am still familiarizing myself\nwith the concepts involved.\n\nI wonder if it would make sense to separate the structural selection\nof unmerged paths from the conflict-marker safety policy.\n\nFor example, could the user-facing option be something like:\n\n    git add --unmerged\n\nwith the same default behavior being proposed: select only unmerged\nindex entries, and refuse the operation if any selected regular file\ncontains standard Git conflict markers.\n\nThat would also potentially leave room for explicit policy escape\nhatches later, such as:\n\n    git add --unmerged --allow-conflict-markers\n\nfor cases where marker-looking content is intentional; or\nsomething like:\n\n    git add --unmerged --skip-conflict-markers\n\nfor a mode that stages marker-free unmerged paths while leaving\nmarker-containing paths unmerged and returning non-zero.\n\nI do see the argument for \"--resolved\" in terms of naming the option\nafter the workflow the user is performing when resolving conflicts. My\nmain feedback is that the implementation selects unmerged paths and\napplies a conflict-marker safety check, rather than determining semantic\nresolution, which a name like \"--resolved\" might imply more strongly.\n"},{"id":"549265","messageId":"xmqqfr10lvhx.fsf@gitster.g","threadId":"66082","inReplyTo":"CAC2QwmKD1d=-mz4WrkA7CHwi-ko75MP2LDFPWx+1FGLQtf4QtA@mail.gmail.com","subject":"Re: [PATCH v2 0/4] git add --resolved","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-30T08:41:46Z","receivedAt":"2026-07-30T08:41:48Z","isPatch":true,"body":"Michael Montalbo <mmontalbo@gmail.com> writes:\n\n> For example, could the user-facing option be something like:\n>\n>     git add --unmerged\n>\n> with the same default behavior being proposed: select only unmerged\n> index entries, and refuse the operation if any selected regular file\n> contains standard Git conflict markers.\n>\n> That would also potentially leave room for explicit policy escape\n> hatches later, such as:\n>\n>     git add --unmerged --allow-conflict-markers\n>\n> for cases where marker-looking content is intentional; or\n\nSure, but I do not think of a reason why the posted patches cannot\nlater be extended in that direction if somebody wanted to (but\nbecause the option is called \"--resolved\", the command line would\nneed to become \"add --resolved --allow-conflict-markers\").  I just\ndid not feel the need for it myself, and because --allow would be a\nrare case, I would likely do \"git add --resolved\", see it fail due\nto conflict marker looking strings that are deliberately left in,\nand then do \"git add $path\" to cover these small exceptions, i.e., I\ndo not think anybody would need \"--allow-conflict-markers\" option.\n\n> something like:\n>\n>     git add --unmerged --skip-conflict-markers\n>\n> for a mode that stages marker-free unmerged paths while leaving\n> marker-containing paths unmerged and returning non-zero.\n\nHave no idea how the user recovers after performing an operation\nlike that.\n\nThe naming is very much deliberate.  \"git am --resolved\" tells the\ncommand \"I am done with them, so please continue\".  It is not like I\nwant to add anything that are unmerged.  I dealt with unmerged stuff\nand turned them into the resolved state, and the command is a request\nto record that fact.  It is a regression to call it \"--unmerged\".\n\n\n"},{"id":"549346","messageId":"20260731125605.3638938-1-gitster@pobox.com","threadId":"66082","inReplyTo":"20260728215219.753678-1-gitster@pobox.com","subject":"[PATCH v3 0/4] git add --resolved","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-31T12:56:01Z","receivedAt":"2026-07-31T12:56:08Z","isPatch":true,"body":"When you are the maintainer of a project and make many merges day\nin, day out, a lot of your time is spent resolving conflicts and\nadding the results to the index.  It is not unusual to have local\nchanges in your working tree that are unrelated to any particular\nmerge [*].  In such cases, 'git add -u', which adds all changes in\nthe working tree to the index, does not help much.\n\nHere is a new option for 'git add' that lets you add paths with\nresolved conflicts to the index, while keeping unrelated local\nchanges out.\n\nThe first three patches perform preliminary refactorings.\n\n - [1/4] is a totally unrelated code cleanup that almost disappears\n   when viewed with 'git show -w', but it was an eyesore to have so\n   many lines with broken indentation while working in the vicinity.\n\n - [2/4] consolidates a helper function to determine whether a line\n   is a conflict marker (replacing two slightly different\n   definitions).\n\n - [3/4] introduces a helper that makes registering path removals\n   from the index as easy as adding them, complete with automatic\n   '--dry-run' and '--verbose' support.\n\nThe fourth patch implements the new feature.\n\n 1/4: read-cache: reindent\n 2/4: merge-ll: consolidate conflict marker scanning logic\n 3/4: read-cache: add remove_file_from_index_with_flags()\n 4/4: add: introduce '--resolved' option\n\n\nThe only change since v2 is to fix the test under BREAKING_CHANGES,\nwhere the tests are run with a future default initial branch name of\n'main'.  Instead of assuming that the initial branch is named\n'master' when returning, we instruct 'checkout' to return to the\nprevious branch, i.e., '@{-1}'.\n\n\n[Footnote]\n\n * This is not limited to my own workflow. An earlier message on\n   this topic worth mentioning is:\n\n   https://lore.kernel.org/git/CA+55aFxP8j7YbYaRXt-8Y0n8cHafB=FPKMy8gKFYH5QsKX4S=Q@mail.gmail.com/\n\n Documentation/git-add.adoc |  10 +++-\n builtin/add.c              |  92 ++++++++++++++++++++++++++++---\n diff.c                     |  25 +--------\n merge-ll.c                 |  56 +++++++++++++++++++\n merge-ll.h                 |   2 +\n read-cache-ll.h            |   3 ++\n read-cache.c               |  89 +++++++++++++++++-------------\n rerere.c                   |  38 +++----------\n t/meson.build              |   1 +\n t/t2207-add-resolved.sh    | 108 +++++++++++++++++++++++++++++++++++++\n 10 files changed, 323 insertions(+), 101 deletions(-)\n create mode 100755 t/t2207-add-resolved.sh\n\nRange-diff against v2:\n1:  b0643ed516 = 1:  414219a89a read-cache: reindent\n2:  4533fa4da1 = 2:  f64ad26d1f merge-ll: consolidate conflict marker scanning logic\n3:  dbf70c2d3a = 3:  1431d0bfdb read-cache: add remove_file_from_index_with_flags()\n4:  033a3d2526 ! 4:  42fb33200a add: introduce '--resolved' option\n    @@ t/t2207-add-resolved.sh (new)\n     +\techo \"theirs 3\" >file3.txt &&\n     +\tgit commit -a -m theirs &&\n     +\n    -+\tgit checkout master\n    ++\tgit checkout @{-1}\n     +'\n     +\n     +test_expect_success 'git add --resolved refuses files with conflict markers' '\n\n-- \n2.55.0-599-g5cacd2ad51\n\n"},{"id":"549347","messageId":"20260731125605.3638938-2-gitster@pobox.com","threadId":"66082","inReplyTo":"20260731125605.3638938-1-gitster@pobox.com","subject":"[PATCH v3 1/4] read-cache: reindent","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-31T12:56:02Z","receivedAt":"2026-07-31T12:56:09Z","isPatch":true,"body":"I do not know how this happened without anybody noticing, but a few\nmonths ago we added a16c4a245a (read-cache: submodule add need\n--force given ignore=all configuration, 2026-02-06), and almost all\nlines the patch added were incorrectly indented.\n\nReindent these lines so that they play better with surrounding lines\nin the same file.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n read-cache.c | 70 +++++++++++++++++++++++++++-------------------------\n 1 file changed, 36 insertions(+), 34 deletions(-)\n\ndiff --git a/read-cache.c b/read-cache.c\nindex 38b55323dd..58c378414a 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -3910,32 +3910,33 @@ static int fix_unmerged_status(struct diff_filepair *p,\n }\n \n static int skip_submodule(const char *path,\n-\t\t\t\t\t\tstruct repository *repo,\n-\t\t\t\t\t\tstruct pathspec *pathspec,\n-\t\t\t\t\t\tint ignored_too)\n-{\n-    struct stat st;\n-    const struct submodule *sub;\n-    int pathspec_matches = 0;\n-    int ps_i;\n-    char *norm_pathspec = NULL;\n-\n-    /* Only consider if path is a directory */\n-    if (lstat(path, &st) || !S_ISDIR(st.st_mode))\n+\t\t\t  struct repository *repo,\n+\t\t\t  struct pathspec *pathspec,\n+\t\t\t  int ignored_too)\n+{\n+\tstruct stat st;\n+\tconst struct submodule *sub;\n+\tint pathspec_matches = 0;\n+\tint ps_i;\n+\tchar *norm_pathspec = NULL;\n+\n+\t/* Only consider if path is a directory */\n+\tif (lstat(path, &st) || !S_ISDIR(st.st_mode))\n \t\treturn 0;\n \n-    /* Check if it's a submodule with ignore=all */\n-    sub = submodule_from_path(repo, null_oid(the_hash_algo), path);\n-    if (!sub || !sub->name || !sub->ignore || strcmp(sub->ignore, \"all\"))\n+\t/* Check if it's a submodule with ignore=all */\n+\tsub = submodule_from_path(repo, null_oid(the_hash_algo), path);\n+\tif (!sub || !sub->name || !sub->ignore || strcmp(sub->ignore, \"all\"))\n \t\treturn 0;\n \n-    trace_printf(\"ignore=all: %s\\n\", path);\n-    trace_printf(\"pathspec %s\\n\", (pathspec && pathspec->nr)\n-\t\t\t\t\t\t\t\t\t? \"has pathspec\"\n-\t\t\t\t\t\t\t\t\t: \"no pathspec\");\n+\ttrace_printf(\"ignore=all: %s\\n\", path);\n+\ttrace_printf(\"pathspec %s\\n\",\n+\t\t     ((pathspec && pathspec->nr)\n+\t\t      ? \"has pathspec\"\n+\t\t      : \"no pathspec\"));\n \n-    /* Check if submodule path is explicitly mentioned in pathspec */\n-    if (pathspec) {\n+\t/* Check if submodule path is explicitly mentioned in pathspec */\n+\tif (pathspec) {\n \t\tfor (ps_i = 0; ps_i < pathspec->nr; ps_i++) {\n \t\t\tconst char *m = pathspec->items[ps_i].match;\n \t\t\tif (!m)\n@@ -3949,28 +3950,29 @@ static int skip_submodule(const char *path,\n \t\t\t}\n \t\t\tFREE_AND_NULL(norm_pathspec);\n \t\t}\n-    }\n+\t}\n \n-    /* If explicitly matched and forced, allow adding */\n-    if (pathspec_matches) {\n+\t/* If explicitly matched and forced, allow adding */\n+\tif (pathspec_matches) {\n \t\tif (ignored_too && ignored_too > 0) {\n \t\t\ttrace_printf(\"Add submodule due to --force: %s\\n\", path);\n \t\t\treturn 0;\n \t\t} else {\n \t\t\tadvise_if_enabled(ADVICE_ADD_IGNORED_FILE,\n-\t\t\t\t_(\"Skipping submodule due to ignore=all: %s\\n\"\n-\t\t\t\t\t\"Use --force if you really want to add the submodule.\"), path);\n+\t\t\t\t  _(\"Skipping submodule due to ignore=all: %s\\n\"\n+\t\t\t\t    \"Use --force if you really want to \"\n+\t\t\t\t    \"add the submodule.\"), path);\n \t\t\treturn 1;\n \t\t}\n-    }\n+\t}\n \n-    /* No explicit pathspec match -> skip silently */\n-    trace_printf(\"Pathspec to submodule does not match explicitly: %s\\n\", path);\n-    return 1;\n+\t/* No explicit pathspec match -> skip silently */\n+\ttrace_printf(\"Pathspec to submodule does not match explicitly: %s\\n\", path);\n+\treturn 1;\n }\n \n static void update_callback(struct diff_queue_struct *q,\n-\t\t\t\t\t\t\tstruct diff_options *opt UNUSED, void *cbdata)\n+\t\t\t    struct diff_options *opt UNUSED, void *cbdata)\n {\n \tint i;\n \tstruct update_callback_data *data = cbdata;\n@@ -3980,7 +3982,7 @@ static void update_callback(struct diff_queue_struct *q,\n \t\tconst char *path = p->one->path;\n \n \t\tif (!data->include_sparse &&\n-\t\t\t!path_in_sparse_checkout(path, data->index))\n+\t\t    !path_in_sparse_checkout(path, data->index))\n \t\t\tcontinue;\n \n \t\tswitch (fix_unmerged_status(p, data)) {\n@@ -3989,8 +3991,8 @@ static void update_callback(struct diff_queue_struct *q,\n \t\tcase DIFF_STATUS_MODIFIED:\n \t\tcase DIFF_STATUS_TYPE_CHANGED:\n \t\t\tif (skip_submodule(path, data->repo,\n-\t\t\t\t\t\t\t\tdata->pathspec,\n-\t\t\t\t\t\t\t\tdata->ignored_too))\n+\t\t\t\t\t   data->pathspec,\n+\t\t\t\t\t   data->ignored_too))\n \t\t\t\tcontinue;\n \n \t\t\tif (add_file_to_index(data->index, path, data->flags)) {\n-- \n2.55.0-599-g5cacd2ad51\n\n"},{"id":"549348","messageId":"20260731125605.3638938-3-gitster@pobox.com","threadId":"66082","inReplyTo":"20260731125605.3638938-1-gitster@pobox.com","subject":"[PATCH v3 2/4] merge-ll: consolidate conflict marker scanning logic","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-31T12:56:03Z","receivedAt":"2026-07-31T12:56:11Z","isPatch":true,"body":"The diff.c:is_conflict_marker() and rerere.c:is_cmarker() functions\nimplement duplicate logic for identifying conflict marker lines\n(lines that begin with a run of '<', '=', '>', and '|' characters).\n\ndiff.c's original version from 049540435f (diff --check: detect\nleftover conflict markers, 2008-06-26) accepts any whitespace (such\nas a newline) immediately following '<<<<<<<' and '>>>>>>>', whereas\nrerere.c's version from 191f241717 (rerere: prepare for customizable\nconflict marker length, 2010-01-16) strictly requires a space\ncharacter (' ') after them.\n\nImplement is_conflict_marker_line() in merge-ll.c to serve as a\nreplacement for both, and update diff.c and rerere.c to use the new\nhelper.  The unified helper intentionally adopts rerere's stricter\nrule, as the conflicts generated by Git always show the \"ours\" and\n\"theirs\" labels after these markers separated by a space.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n diff.c     | 25 +------------------------\n merge-ll.c | 31 +++++++++++++++++++++++++++++++\n merge-ll.h |  1 +\n rerere.c   | 38 ++++++--------------------------------\n 4 files changed, 39 insertions(+), 56 deletions(-)\n\ndiff --git a/diff.c b/diff.c\nindex 589c1969e4..cfe515af4e 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -3519,29 +3519,6 @@ struct checkdiff_t {\n \tint last_line_kind;\n };\n \n-static int is_conflict_marker(const char *line, int marker_size, unsigned long len)\n-{\n-\tchar firstchar;\n-\tint cnt;\n-\n-\tif (len < marker_size + 1)\n-\t\treturn 0;\n-\tfirstchar = line[0];\n-\tswitch (firstchar) {\n-\tcase '=': case '>': case '<': case '|':\n-\t\tbreak;\n-\tdefault:\n-\t\treturn 0;\n-\t}\n-\tfor (cnt = 1; cnt < marker_size; cnt++)\n-\t\tif (line[cnt] != firstchar)\n-\t\t\treturn 0;\n-\t/* line[1] through line[marker_size-1] are same as firstchar */\n-\tif (len < marker_size + 1 || !isspace(line[marker_size]))\n-\t\treturn 0;\n-\treturn 1;\n-}\n-\n static void checkdiff_consume_hunk(void *priv,\n \t\t\t\t   long ob UNUSED, long on UNUSED,\n \t\t\t\t   long nb, long nn UNUSED,\n@@ -3571,7 +3548,7 @@ static int checkdiff_consume(void *priv, char *line, unsigned long len)\n \tif (line[0] == '+') {\n \t\tunsigned bad;\n \t\tdata->lineno++;\n-\t\tif (is_conflict_marker(line + 1, marker_size, len - 1)) {\n+\t\tif (is_conflict_marker_line(line + 1, len - 1, marker_size)) {\n \t\t\tdata->status |= 1;\n \t\t\tfprintf(data->o->file,\n \t\t\t\t\"%s%s:%d: leftover conflict marker\\n\",\ndiff --git a/merge-ll.c b/merge-ll.c\nindex fafe2c9197..41c97fb90a 100644\n--- a/merge-ll.c\n+++ b/merge-ll.c\n@@ -468,3 +468,34 @@ int ll_merge_marker_size(struct index_state *istate, const char *path)\n \t}\n \treturn marker_size;\n }\n+\n+int is_conflict_marker_line(const char *line, unsigned long len, int marker_size)\n+{\n+\tchar firstchar;\n+\tint cnt;\n+\n+\tif (len < marker_size + 1)\n+\t\treturn 0;\n+\n+\tfirstchar = line[0];\n+\tswitch (firstchar) {\n+\tcase '=': case '>': case '<': case '|':\n+\t\tbreak;\n+\tdefault:\n+\t\treturn 0;\n+\t}\n+\n+\tfor (cnt = 1; cnt < marker_size; cnt++) {\n+\t\tif (line[cnt] != firstchar)\n+\t\t\treturn 0;\n+\t}\n+\n+\tif (((firstchar == '<') || (firstchar == '>')) &&\n+\t    line[marker_size] != ' ')\n+\t\treturn 0;\n+\n+\tif (!isspace((unsigned char)line[marker_size]))\n+\t\treturn 0;\n+\n+\treturn firstchar;\n+}\ndiff --git a/merge-ll.h b/merge-ll.h\nindex d038ee0c1e..b348aee15d 100644\n--- a/merge-ll.h\n+++ b/merge-ll.h\n@@ -109,6 +109,7 @@ enum ll_merge_result ll_merge(mmbuffer_t *result_buf,\n \t     const struct ll_merge_options *opts);\n \n int ll_merge_marker_size(struct index_state *istate, const char *path);\n+int is_conflict_marker_line(const char *line, unsigned long len, int marker_size);\n void reset_merge_attributes(void);\n \n #endif\ndiff --git a/rerere.c b/rerere.c\nindex 216100925a..924a1f2e30 100644\n--- a/rerere.c\n+++ b/rerere.c\n@@ -331,33 +331,6 @@ static int rerere_file_getline(struct strbuf *sb, struct rerere_io *io_)\n \treturn strbuf_getwholeline(sb, io->input, '\\n');\n }\n \n-/*\n- * Require the exact number of conflict marker letters, no more, no\n- * less, followed by SP or any whitespace\n- * (including LF).\n- */\n-static int is_cmarker(char *buf, int marker_char, int marker_size)\n-{\n-\tint want_sp;\n-\n-\t/*\n-\t * The beginning of our version and the end of their version\n-\t * always are labeled like \"<<<<< ours\" or \">>>>> theirs\",\n-\t * hence we set want_sp for them.  Note that the version from\n-\t * the common ancestor in diff3-style output is not always\n-\t * labelled (e.g. \"||||| common\" is often seen but \"|||||\"\n-\t * alone is also valid), so we do not set want_sp.\n-\t */\n-\twant_sp = (marker_char == '<') || (marker_char == '>');\n-\n-\twhile (marker_size--)\n-\t\tif (*buf++ != marker_char)\n-\t\t\treturn 0;\n-\tif (want_sp && *buf != ' ')\n-\t\treturn 0;\n-\treturn isspace(*buf);\n-}\n-\n static void rerere_strbuf_putconflict(struct strbuf *buf, int ch, size_t size)\n {\n \tstrbuf_addchars(buf, ch, size);\n@@ -375,7 +348,8 @@ static int handle_conflict(struct strbuf *out, struct rerere_io *io,\n \tint has_conflicts = -1;\n \n \twhile (!io->getline(&buf, io)) {\n-\t\tif (is_cmarker(buf.buf, '<', marker_size)) {\n+\t\tint marker = is_conflict_marker_line(buf.buf, buf.len, marker_size);\n+\t\tif (marker == '<') {\n \t\t\tif (handle_conflict(&conflict, io, marker_size, NULL) < 0)\n \t\t\t\tbreak;\n \t\t\tif (hunk == RR_SIDE_1)\n@@ -383,15 +357,15 @@ static int handle_conflict(struct strbuf *out, struct rerere_io *io,\n \t\t\telse\n \t\t\t\tstrbuf_addbuf(&two, &conflict);\n \t\t\tstrbuf_release(&conflict);\n-\t\t} else if (is_cmarker(buf.buf, '|', marker_size)) {\n+\t\t} else if (marker == '|') {\n \t\t\tif (hunk != RR_SIDE_1)\n \t\t\t\tbreak;\n \t\t\thunk = RR_ORIGINAL;\n-\t\t} else if (is_cmarker(buf.buf, '=', marker_size)) {\n+\t\t} else if (marker == '=') {\n \t\t\tif (hunk != RR_SIDE_1 && hunk != RR_ORIGINAL)\n \t\t\t\tbreak;\n \t\t\thunk = RR_SIDE_2;\n-\t\t} else if (is_cmarker(buf.buf, '>', marker_size)) {\n+\t\t} else if (marker == '>') {\n \t\t\tif (hunk != RR_SIDE_2)\n \t\t\t\tbreak;\n \t\t\tif (strbuf_cmp(&one, &two) > 0)\n@@ -442,7 +416,7 @@ static int handle_path(unsigned char *hash, struct rerere_io *io, int marker_siz\n \t\tgit_hash_init(&ctx, the_hash_algo);\n \n \twhile (!io->getline(&buf, io)) {\n-\t\tif (is_cmarker(buf.buf, '<', marker_size)) {\n+\t\tif (is_conflict_marker_line(buf.buf, buf.len, marker_size) == '<') {\n \t\t\thas_conflicts = handle_conflict(&out, io, marker_size,\n \t\t\t\t\t\t\thash ? &ctx : NULL);\n \t\t\tif (has_conflicts < 0)\n-- \n2.55.0-599-g5cacd2ad51\n\n"},{"id":"549349","messageId":"20260731125605.3638938-4-gitster@pobox.com","threadId":"66082","inReplyTo":"20260731125605.3638938-1-gitster@pobox.com","subject":"[PATCH v3 3/4] read-cache: add remove_file_from_index_with_flags()","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-31T12:56:04Z","receivedAt":"2026-07-31T12:56:12Z","isPatch":true,"body":"add_file_to_index() takes flags such as ADD_CACHE_PRETEND and\nADD_CACHE_VERBOSE and internally handles both reporting (e.g.,\n\"add 'path'\") and suppressing index updates during dry runs.\n\nIn contrast, remove_file_from_index() takes only istate and path\nwithout flags.  Callers that perform file removals (such as\nupdate_callback() in read-cache.c) are forced to manually inspect\nADD_CACHE_PRETEND and ADD_CACHE_VERBOSE flags for removed\nfiles.\n\nIntroduce remove_file_from_index_with_flags() to encapsulate\npretend mode and verbose reporting for index removals.  Update\nupdate_callback() to use the new helper.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n read-cache-ll.h |  3 +++\n read-cache.c    | 19 +++++++++++++++----\n 2 files changed, 18 insertions(+), 4 deletions(-)\n\ndiff --git a/read-cache-ll.h b/read-cache-ll.h\nindex 71b87615eb..8eb266cfd1 100644\n--- a/read-cache-ll.h\n+++ b/read-cache-ll.h\n@@ -391,11 +391,14 @@ int remove_index_entry_at(struct index_state *, int pos);\n \n void remove_marked_cache_entries(struct index_state *istate, int invalidate);\n int remove_file_from_index(struct index_state *, const char *path);\n+int remove_file_from_index_with_flags(struct index_state *, const char *, int);\n+\n #define ADD_CACHE_VERBOSE 1\n #define ADD_CACHE_PRETEND 2\n #define ADD_CACHE_IGNORE_ERRORS\t4\n #define ADD_CACHE_IGNORE_REMOVAL 8\n #define ADD_CACHE_INTENT 16\n+\n /*\n  * These two are used to add the contents of the file at path\n  * to the index, marking the working tree up-to-date by storing\ndiff --git a/read-cache.c b/read-cache.c\nindex 58c378414a..ad77c0d5e2 100644\n--- a/read-cache.c\n+++ b/read-cache.c\n@@ -638,6 +638,20 @@ int remove_file_from_index(struct index_state *istate, const char *path)\n \treturn 0;\n }\n \n+int remove_file_from_index_with_flags(struct index_state *istate,\n+\t\t\t\t      const char *path,\n+\t\t\t\t      int flags)\n+{\n+\tint verbose = flags & (ADD_CACHE_VERBOSE | ADD_CACHE_PRETEND);\n+\tint pretend = flags & ADD_CACHE_PRETEND;\n+\n+\tif (verbose)\n+\t\tprintf(_(\"remove '%s'\\n\"), path);\n+\tif (pretend)\n+\t\treturn 0;\n+\treturn remove_file_from_index(istate, path);\n+}\n+\n static int compare_name(struct cache_entry *ce, const char *path, int namelen)\n {\n \treturn namelen != ce_namelen(ce) || memcmp(path, ce->name, namelen);\n@@ -4004,10 +4018,7 @@ static void update_callback(struct diff_queue_struct *q,\n \t\tcase DIFF_STATUS_DELETED:\n \t\t\tif (data->flags & ADD_CACHE_IGNORE_REMOVAL)\n \t\t\t\tbreak;\n-\t\t\tif (!(data->flags & ADD_CACHE_PRETEND))\n-\t\t\t\tremove_file_from_index(data->index, path);\n-\t\t\tif (data->flags & (ADD_CACHE_PRETEND|ADD_CACHE_VERBOSE))\n-\t\t\t\tprintf(_(\"remove '%s'\\n\"), path);\n+\t\t\tremove_file_from_index_with_flags(data->index, path, data->flags);\n \t\t\tbreak;\n \t\t}\n \t}\n-- \n2.55.0-599-g5cacd2ad51\n\n"},{"id":"549350","messageId":"20260731125605.3638938-5-gitster@pobox.com","threadId":"66082","inReplyTo":"20260731125605.3638938-1-gitster@pobox.com","subject":"[PATCH v3 4/4] add: introduce '--resolved' option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-31T12:56:05Z","receivedAt":"2026-07-31T12:56:13Z","isPatch":true,"body":"During a conflicted merge, rebase, or cherry-pick, 'git add -u' is a\nhandy way to add modified paths to the index.  However, '-u'\nindiscriminately adds all modified tracked paths, including unmerged\npaths that may still contain unresolved conflict markers.  It also\nadds tracked files modified in the worktree that are not involved in\nthe ongoing merge.\n\nThe latter is not a huge problem for \"git rebase\", which refuses to\nstart with any local changes, but is a problem for \"git merge\",\nwhich is often run with local changes in maintainer workflows.\n\nIntroduce 'git add --resolved' to add only unmerged paths, limited\nby an optional pathspec, where no conflict markers remain in the\nworking tree.\n\nBefore modifying the index, scan unmerged regular files for leftover\nconflict markers using a new helper, has_conflict_markers(), defined\nin merge-ll.c in terms of the is_conflict_marker_line() helper we\nintroduced earlier.  If any unmerged path still contains conflict\nmarkers, show an error listing the conflicted paths and abort\nwithout updating the index.  Otherwise, add these unmerged paths\nthat do not have conflict markers to the index.\n\nNote that unmerged paths without conflict markers (such as binary\nfiles and deletions) are added as resolved using add_file_to_index()\nand remove_file_from_index_with_flags().  Tracked files that were\nnot in a conflicted state are ignored by '--resolved'.\n\nSigned-off-by: Junio C Hamano <gitster@pobox.com>\n---\n Documentation/git-add.adoc |  10 +++-\n builtin/add.c              |  92 ++++++++++++++++++++++++++++---\n merge-ll.c                 |  25 +++++++++\n merge-ll.h                 |   1 +\n t/meson.build              |   1 +\n t/t2207-add-resolved.sh    | 108 +++++++++++++++++++++++++++++++++++++\n 6 files changed, 230 insertions(+), 7 deletions(-)\n create mode 100755 t/t2207-add-resolved.sh\n\ndiff --git a/Documentation/git-add.adoc b/Documentation/git-add.adoc\nindex 941135dc63..16b06e38e1 100644\n--- a/Documentation/git-add.adoc\n+++ b/Documentation/git-add.adoc\n@@ -11,7 +11,7 @@ SYNOPSIS\n git add [--verbose | -v] [--dry-run | -n] [--force | -f] [--interactive | -i] [--patch | -p]\n \t[--edit | -e] [--[no-]all | -A | --[no-]ignore-removal | [--update | -u]] [--sparse]\n \t[--intent-to-add | -N] [--refresh] [--ignore-errors] [--ignore-missing] [--renormalize]\n-\t[--chmod=(+|-)x] [--pathspec-from-file=<file> [--pathspec-file-nul]]\n+\t[--resolved] [--chmod=(+|-)x] [--pathspec-from-file=<file> [--pathspec-file-nul]]\n \t[--] [<pathspec>...]\n \n DESCRIPTION\n@@ -195,6 +195,14 @@ for `git add --no-all <pathspec>...`, i.e. ignored removed files.\n \twhile a _CRLF_ cleans to _LF_, a _CRCRLF_ sequence is only partially\n \tcleaned to _CRLF_.\n \n+`--resolved`::\n+\tUpdate the index for unmerged paths matching _<pathspec>_ where\n+\tno conflict markers remain in the working tree. Unmerged paths\n+\twithout conflict markers (including binary files and file\n+\tdeletions) are staged as resolved, while any path with leftover\n+\tconflict markers causes the command to refuse to stage any files.\n+\tCannot be combined with `-u` or `-A`.\n+\n `--chmod=(+|-)x`::\n \tOverride the executable bit of the added files.  The executable\n \tbit is only changed in the index, the files on disk are left\ndiff --git a/builtin/add.c b/builtin/add.c\nindex 60ffbede2b..eab8f03cad 100644\n--- a/builtin/add.c\n+++ b/builtin/add.c\n@@ -26,6 +26,7 @@\n #include \"strvec.h\"\n #include \"submodule.h\"\n #include \"add-interactive.h\"\n+#include \"merge-ll.h\"\n \n static const char * const builtin_add_usage[] = {\n \tN_(\"git add [<options>] [--] <pathspec>...\"),\n@@ -35,6 +36,7 @@ static int patch_interactive, add_interactive, edit_interactive;\n static struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;\n static int take_worktree_changes;\n static int add_renormalize;\n+static int add_resolved;\n static int pathspec_file_nul;\n static int include_sparse;\n static const char *pathspec_from_file;\n@@ -265,6 +267,7 @@ static struct option builtin_add_options[] = {\n \tOPT__FORCE(&ignored_too, N_(\"allow adding otherwise ignored files\"), 0),\n \tOPT_BOOL('u', \"update\", &take_worktree_changes, N_(\"update tracked files\")),\n \tOPT_BOOL(0, \"renormalize\", &add_renormalize, N_(\"renormalize EOL of tracked files (implies -u)\")),\n+\tOPT_BOOL(0, \"resolved\", &add_resolved, N_(\"add conflict-resolved tracked files\")),\n \tOPT_BOOL('N', \"intent-to-add\", &intent_to_add, N_(\"record only the fact that the path will be added later\")),\n \tOPT_BOOL('A', \"all\", &addremove_explicit, N_(\"add changes from all tracked and untracked files\")),\n \tOPT_CALLBACK_F(0, \"ignore-removal\", &addremove_explicit,\n@@ -379,6 +382,76 @@ static int add_files(struct repository *repo, struct dir_struct *dir, int flags)\n \treturn exit_status;\n }\n \n+static int failed_to_add(int flags, const char *path)\n+{\n+\tif (!(flags & ADD_CACHE_IGNORE_ERRORS))\n+\t\tdie(_(\"updating file '%s' failed\"), path);\n+\treturn 1;\n+}\n+\n+static int add_resolved_files(struct repository *repo,\n+\t\t\t      const struct pathspec *pathspec,\n+\t\t\t      int flags)\n+{\n+\tstruct index_state *istate = repo->index;\n+\tstruct string_list unmerged_paths = STRING_LIST_INIT_DUP;\n+\tstruct string_list unresolved_paths = STRING_LIST_INIT_DUP;\n+\tint exit_status = 0;\n+\tsize_t i;\n+\n+\tfor (i = 0; i < istate->cache_nr; i++) {\n+\t\tstruct cache_entry *ce = istate->cache[i];\n+\t\tif (!ce_stage(ce))\n+\t\t\tcontinue;\n+\t\tif (pathspec->nr && !ce_path_match(istate, ce, pathspec, NULL))\n+\t\t\tcontinue;\n+\t\tif (!unmerged_paths.nr ||\n+\t\t    strcmp(unmerged_paths.items[unmerged_paths.nr - 1].string, ce->name))\n+\t\t\tstring_list_append(&unmerged_paths, ce->name);\n+\t}\n+\n+\tif (!unmerged_paths.nr) {\n+\t\tstring_list_clear(&unmerged_paths, 0);\n+\t\treturn 0;\n+\t}\n+\n+\tfor (i = 0; i < unmerged_paths.nr; i++) {\n+\t\tconst char *path = unmerged_paths.items[i].string;\n+\t\tstruct stat st;\n+\n+\t\tif (!lstat(path, &st) && S_ISREG(st.st_mode)) {\n+\t\t\tif (has_conflict_markers(istate, path))\n+\t\t\t\tstring_list_append(&unresolved_paths, path);\n+\t\t}\n+\t}\n+\n+\tif (unresolved_paths.nr) {\n+\t\tstruct strbuf sb = STRBUF_INIT;\n+\t\tfor (i = 0; i < unresolved_paths.nr; i++)\n+\t\t\tstrbuf_addf(&sb, \"\\t%s\\n\", unresolved_paths.items[i].string);\n+\t\tdie(_(\"the following paths still have conflict markers:\\n%s\"), sb.buf);\n+\t}\n+\n+\tfor (i = 0; i < unmerged_paths.nr; i++) {\n+\t\tconst char *path = unmerged_paths.items[i].string;\n+\t\tstruct stat st;\n+\n+\t\tif (lstat(path, &st)) {\n+\t\t\tif (errno != ENOENT)\n+\t\t\t\tdie_errno(_(\"cannot lstat: '%s'\"), path);\n+\t\t\tif (remove_file_from_index_with_flags(istate, path, flags))\n+\t\t\t\texit_status = failed_to_add(flags, path);\n+\t\t} else {\n+\t\t\tif (add_file_to_index(istate, path, flags))\n+\t\t\t\texit_status = failed_to_add(flags, path);\n+\t\t}\n+\t}\n+\n+\tstring_list_clear(&unmerged_paths, 0);\n+\tstring_list_clear(&unresolved_paths, 0);\n+\treturn exit_status;\n+}\n+\n int cmd_add(int argc,\n \t    const char **argv,\n \t    const char *prefix,\n@@ -438,8 +511,9 @@ int cmd_add(int argc,\n \telse if (take_worktree_changes && ADDREMOVE_DEFAULT)\n \t\taddremove = 0; /* \"-u\" was given but not \"-A\" */\n \n-\tif (addremove && take_worktree_changes)\n-\t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"-A\", \"-u\");\n+\tdie_for_incompatible_opt3(take_worktree_changes, \"-u/--update\",\n+\t\t\t\t  0 < addremove_explicit, \"-A/--all\",\n+\t\t\t\t  add_resolved, \"--resolved\");\n \n \tif (!show_only && ignore_missing)\n \t\tdie(_(\"the option '%s' requires '%s'\"), \"--ignore-missing\", \"--dry-run\");\n@@ -448,8 +522,11 @@ int cmd_add(int argc,\n \t\t\t  chmod_arg[1] != 'x' || chmod_arg[2]))\n \t\tdie(_(\"--chmod param '%s' must be either -x or +x\"), chmod_arg);\n \n-\tadd_new_files = !take_worktree_changes && !refresh_only && !add_renormalize;\n-\trequire_pathspec = !(take_worktree_changes || (0 < addremove_explicit));\n+\tadd_new_files = !take_worktree_changes && !refresh_only &&\n+\t\t\t!add_renormalize && !add_resolved;\n+\trequire_pathspec = !(take_worktree_changes ||\n+\t\t\t     (0 < addremove_explicit) ||\n+\t\t\t     add_resolved);\n \n \trepo_hold_locked_index(repo, &lock_file, LOCK_DIE_ON_ERROR);\n \n@@ -481,7 +558,8 @@ int cmd_add(int argc,\n \t\treturn 0;\n \t}\n \n-\tif (!take_worktree_changes && addremove_explicit < 0 && pathspec.nr)\n+\tif (!take_worktree_changes && !add_resolved &&\n+\t    addremove_explicit < 0 && pathspec.nr)\n \t\t/* Turn \"git add pathspec...\" to \"git add -A pathspec...\" */\n \t\taddremove = 1;\n \n@@ -584,7 +662,9 @@ int cmd_add(int argc,\n \todb_transaction_begin_or_die(repo->objects, &transaction, 0);\n \n \tps_matched = xcalloc(pathspec.nr, 1);\n-\tif (add_renormalize)\n+\tif (add_resolved)\n+\t\texit_status |= add_resolved_files(repo, &pathspec, flags);\n+\telse if (add_renormalize)\n \t\texit_status |= renormalize_tracked_files(repo, &pathspec, flags);\n \telse\n \t\texit_status |= add_files_to_cache(repo, prefix,\ndiff --git a/merge-ll.c b/merge-ll.c\nindex 41c97fb90a..ef5287dee8 100644\n--- a/merge-ll.c\n+++ b/merge-ll.c\n@@ -499,3 +499,28 @@ int is_conflict_marker_line(const char *line, unsigned long len, int marker_size\n \n \treturn firstchar;\n }\n+\n+int has_conflict_markers(struct index_state *istate, const char *path)\n+{\n+\tFILE *f;\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tint marker_size = ll_merge_marker_size(istate, path);\n+\tint has_markers = 0;\n+\n+\tf = fopen(path, \"r\");\n+\tif (!f)\n+\t\treturn 0;\n+\n+\twhile (strbuf_getwholeline(&sb, f, '\\n') != EOF) {\n+\t\tif (is_conflict_marker_line(sb.buf, sb.len, marker_size)) {\n+\t\t\thas_markers = 1;\n+\t\t\tbreak;\n+\t\t}\n+\t\tif (buffer_is_binary(sb.buf,\n+\t\t\t\t     ULONG_MAX <= sb.len ? ULONG_MAX : sb.len))\n+\t\t\tbreak;\n+\t}\n+\tfclose(f);\n+\tstrbuf_release(&sb);\n+\treturn has_markers;\n+}\ndiff --git a/merge-ll.h b/merge-ll.h\nindex b348aee15d..f26aef238d 100644\n--- a/merge-ll.h\n+++ b/merge-ll.h\n@@ -110,6 +110,7 @@ enum ll_merge_result ll_merge(mmbuffer_t *result_buf,\n \n int ll_merge_marker_size(struct index_state *istate, const char *path);\n int is_conflict_marker_line(const char *line, unsigned long len, int marker_size);\n+int has_conflict_markers(struct index_state *istate, const char *path);\n void reset_merge_attributes(void);\n \n #endif\ndiff --git a/t/meson.build b/t/meson.build\nindex 8ae6ab6c5f..e0f4b85ad5 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -304,6 +304,7 @@ integration_tests = [\n   't2204-add-ignored.sh',\n   't2205-add-worktree-config.sh',\n   't2206-add-submodule-ignored.sh',\n+  't2207-add-resolved.sh',\n   't2300-cd-to-toplevel.sh',\n   't2400-worktree-add.sh',\n   't2401-worktree-prune.sh',\ndiff --git a/t/t2207-add-resolved.sh b/t/t2207-add-resolved.sh\nnew file mode 100755\nindex 0000000000..1b88efcb99\n--- /dev/null\n+++ b/t/t2207-add-resolved.sh\n@@ -0,0 +1,108 @@\n+#!/bin/sh\n+\n+test_description='git add --resolved\n+\n+Test that \"git add --resolved\" stages conflict-resolved paths and\n+refuses to stage when conflict markers remain.'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'setup repo' '\n+\techo base >file1.txt &&\n+\techo base >file2.txt &&\n+\techo base >file3.txt &&\n+\techo base >file4.txt &&\n+\tgit add file1.txt file2.txt file3.txt file4.txt &&\n+\tgit commit -m initial &&\n+\n+\tgit branch topic &&\n+\techo \"ours 1\" >file1.txt &&\n+\techo \"ours 2\" >file2.txt &&\n+\techo \"ours 3\" >file3.txt &&\n+\tgit commit -a -m ours &&\n+\n+\tgit checkout topic &&\n+\techo \"theirs 1\" >file1.txt &&\n+\techo \"theirs 2\" >file2.txt &&\n+\techo \"theirs 3\" >file3.txt &&\n+\tgit commit -a -m theirs &&\n+\n+\tgit checkout @{-1}\n+'\n+\n+test_expect_success 'git add --resolved refuses files with conflict markers' '\n+\ttest_when_finished \"git reset --hard HEAD\" &&\n+\ttest_must_fail git merge topic &&\n+\techo \"resolved 1\" >file1.txt &&\n+\ttest_must_fail git add --resolved 2>err &&\n+\ttest_grep \"the following paths still have conflict markers:\" err &&\n+\ttest_grep \"file2.txt\" err &&\n+\ttest_grep \"file3.txt\" err &&\n+\t# Index should remain unmerged for all files\n+\tgit ls-files -u file1.txt >unmerged &&\n+\ttest_line_count = 3 unmerged\n+'\n+\n+test_expect_success 'git add --resolved succeeds when all conflict markers are removed' '\n+\ttest_when_finished \"git reset --hard HEAD\" &&\n+\ttest_must_fail git merge topic &&\n+\techo \"resolved 1\" >file1.txt &&\n+\techo \"resolved 2\" >file2.txt &&\n+\techo \"resolved 3\" >file3.txt &&\n+\tgit add --resolved &&\n+\tgit ls-files -u >unmerged &&\n+\ttest_must_be_empty unmerged &&\n+\tgit ls-files -s file1.txt file2.txt file3.txt >staged &&\n+\ttest_line_count = 3 staged\n+'\n+\n+test_expect_success 'git add --resolved ignores unconflicted modified files' '\n+\ttest_when_finished \"git reset --hard HEAD\" &&\n+\techo \"unconflicted local change\" >>file4.txt &&\n+\ttest_must_fail git merge topic &&\n+\techo \"resolved 1\" >file1.txt &&\n+\techo \"resolved 2\" >file2.txt &&\n+\techo \"resolved 3\" >file3.txt &&\n+\tgit add --resolved &&\n+\t# file1, file2, file3 should be staged as resolved\n+\tgit ls-files -u >unmerged &&\n+\ttest_must_be_empty unmerged &&\n+\t# file4 should remain unstaged in working tree\n+\tgit diff file4.txt >diff_out &&\n+\ttest_grep \"unconflicted local change\" diff_out &&\n+\tgit diff --cached file4.txt >cached_out &&\n+\ttest_must_be_empty cached_out\n+'\n+\n+test_expect_success 'git add --resolved handles file removals' '\n+\ttest_when_finished \"git reset --hard HEAD\" &&\n+\ttest_must_fail git merge topic &&\n+\techo \"resolved 1\" >file1.txt &&\n+\trm file2.txt &&\n+\techo \"resolved 3\" >file3.txt &&\n+\tgit add --resolved &&\n+\tgit ls-files -s file2.txt >out &&\n+\ttest_must_be_empty out\n+'\n+\n+test_expect_success 'git add --resolved honors pathspec' '\n+\ttest_when_finished \"git reset --hard HEAD\" &&\n+\ttest_must_fail git merge topic &&\n+\techo \"resolved 1\" >file1.txt &&\n+\t# file2.txt and file3.txt still have conflict markers,\n+\t# but pathspec targets only file1.txt\n+\tgit add --resolved file1.txt &&\n+\tgit ls-files -u file1.txt >unmerged1 &&\n+\ttest_must_be_empty unmerged1 &&\n+\tgit ls-files -u file2.txt >unmerged2 &&\n+\ttest_line_count = 3 unmerged2\n+'\n+\n+test_expect_success 'git add --resolved incompatibility with -u and -A' '\n+\ttest_must_fail git add --resolved -u 2>err1 &&\n+\ttest_grep \"cannot be used together\" err1 &&\n+\ttest_must_fail git add --resolved -A 2>err2 &&\n+\ttest_grep \"cannot be used together\" err2\n+'\n+\n+test_done\n-- \n2.55.0-599-g5cacd2ad51\n\n"},{"id":"549393","messageId":"20260801141414.GD2041176@coredump.intra.peff.net","threadId":"66082","inReplyTo":"20260731125605.3638938-1-gitster@pobox.com","subject":"Re: [PATCH v3 0/4] git add --resolved","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-08-01T14:14:14Z","receivedAt":"2026-08-01T14:14:15Z","isPatch":true,"body":"On Fri, Jul 31, 2026 at 05:56:01AM -0700, Junio C Hamano wrote:\n\n> When you are the maintainer of a project and make many merges day\n> in, day out, a lot of your time is spent resolving conflicts and\n> adding the results to the index.  It is not unusual to have local\n> changes in your working tree that are unrelated to any particular\n> merge [*].  In such cases, 'git add -u', which adds all changes in\n> the working tree to the index, does not help much.\n> \n> Here is a new option for 'git add' that lets you add paths with\n> resolved conflicts to the index, while keeping unrelated local\n> changes out.\n\nI think this is a reasonable thing to want, but just playing devil's\nadvocate for a moment: how big of a footgun might this be?\n\nWe have some changes in the working tree, we try a merge and get\nconflicts, do some resolving work and now we want to stage the\nresolution but nothing else. How close is --resolved to that? I can\nthink of two corner cases:\n\n  1. There are non-merge changes in foo.c, which also had some\n     conflicts. Because we stage the whole file, we accidentally stage\n     those along with the merge resolution.\n\n     But maybe this is impossible, because merge would have refused to\n     operate when one of the changed files is dirty? I think this is\n     dependent on the strategy, but it should be true for most\n     strategies. The exception is \"ours\", but that does not create\n     conflicts in the first place. ;)\n\n  2. Your resolution required touching other files (e.g., updating the\n     caller of a function), but we'll ignore them. Git has no idea that\n     those changes are related to the merge and not just stray changes.\n     You have to remember to stage them manually.\n\n     This is a problem even without --resolved, but I think it's easy\n     for --resolved to lure you into a false sense of security that it\n     is staging all of the important bits.\n\nThat doesn't seem too bad to me. I was mostly worried about (1), but\nafter digging into it, I think I've convinced myself that it's a\nnon-issue.\n\n-Peff\n"},{"id":"549408","messageId":"xmqqv79t9101.fsf@gitster.g","threadId":"66082","inReplyTo":"20260801141414.GD2041176@coredump.intra.peff.net","subject":"Re: [PATCH v3 0/4] git add --resolved","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-01T17:58:06Z","receivedAt":"2026-08-01T17:58:09Z","isPatch":true,"body":"Jeff King <peff@peff.net> writes:\n\n> That doesn't seem too bad to me. I was mostly worried about (1), but\n> after digging into it, I think I've convinced myself that it's a\n> non-issue.\n\nYeah, our merges that began with \"read-tree -m -u\" have never merged\ninto a path with local modification in it, so I have blindly\nassuming that (1) is a given.  I think \"ort\" did not change it, but\nquite honestly I do not know where the more recent invention that\nrenames paths that existed only on one side to follow adjacent paths\nthat were renamed from the same directory guarantees property (1),\nso in these new code paths my assumption truly is a blind faith X-<.\n\n\n"},{"id":"549418","messageId":"xmqqjyq97bx8.fsf@gitster.g","threadId":"66082","inReplyTo":"xmqqv79t9101.fsf@gitster.g","subject":"Re: [PATCH v3 0/4] git add --resolved","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-01T21:45:07Z","receivedAt":"2026-08-01T21:45:10Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Jeff King <peff@peff.net> writes:\n>\n>> That doesn't seem too bad to me. I was mostly worried about (1), but\n>> after digging into it, I think I've convinced myself that it's a\n>> non-issue.\n>\n> Yeah, our merges that began with \"read-tree -m -u\" have never merged\n> into a path with local modification in it, so I have blindly\n> assuming that (1) is a given.  I think \"ort\" did not change it, but\n> quite honestly I do not know where the more recent invention that\n> renames paths that existed only on one side to follow adjacent paths\n> that were renamed from the same directory guarantees property (1),\n> so in these new code paths my assumption truly is a blind faith X-<.\n\nSo I got curious, and found out that t6423 has a test labelled as\n\"Testcase 11b\" that covers this exact case.  So it is a bit better\nthan blind faith now ;-).\n"},{"id":"549422","messageId":"CAC2Qwm+6e8ZW36--iPmrCLT2tS7R4O_TDbx4S1MaSMb9bg=epg@mail.gmail.com","threadId":"66082","inReplyTo":"xmqqfr10lvhx.fsf@gitster.g","subject":"Re: [PATCH v2 0/4] git add --resolved","fromName":"Michael Montalbo","fromEmail":"mmontalbo@gmail.com","sentAt":"2026-08-02T00:22:45Z","receivedAt":"2026-08-02T00:22:57Z","isPatch":true,"body":"On Thu, Jul 30, 2026 at 1:41 AM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Sure, but I do not think of a reason why the posted patches cannot\n> later be extended in that direction if somebody wanted to...\n>\n\nYes, that is fair.\n\n> Have no idea how the user recovers after performing an operation\n> like that.\n>\n\nMy thought was that a partial set of resolved files without markers\nwould still be added and files with markers would be available for\na subsequent add once they were resolved too. My understanding\nmay be incorrect, though.\n\n> The naming is very much deliberate.  \"git am --resolved\" tells the\n> command \"I am done with them, so please continue\".  It is not like I\n> want to add anything that are unmerged.  I dealt with unmerged stuff\n> and turned them into the resolved state, and the command is a request\n> to record that fact.  It is a regression to call it \"--unmerged\".\n>\n\nThank you for the pointer to \"git am --resolved\", the context helps me\nsee what you mean that the naming is a regression in the workflow. The\nname makes sense to me, especially in relation to the existing --resolved\nflag.\n"},{"id":"549423","messageId":"CAC2QwmJeohdnWhUbcP6Pc5w1X8yZf3jXvpR8JC=Hb9gqkFF6ig@mail.gmail.com","threadId":"66082","inReplyTo":"xmqqse51algy.fsf@gitster.g","subject":"Re: [PATCH 3/4] add: introduce '--resolved' option","fromName":"Michael Montalbo","fromEmail":"mmontalbo@gmail.com","sentAt":"2026-08-02T00:49:40Z","receivedAt":"2026-08-02T00:49:52Z","isPatch":true,"body":"On Wed, Jul 29, 2026 at 8:17 AM Junio C Hamano <gitster@pobox.com> wrote:\n>\n>\n> Left unchecked, this loop may end up scanning a large binary file to\n> the end in vain.  We may squeeze in something like this to punt\n> early.\n>\n>  merge-ll.c | 3 +++\n>  1 file changed, 3 insertions(+)\n>\n> diff --git a/merge-ll.c b/merge-ll.c\n> index 5e5044b9e3..ef5287dee8 100644\n> --- a/merge-ll.c\n> +++ b/merge-ll.c\n> @@ -516,6 +516,9 @@ int has_conflict_markers(struct index_state *istate, const char *path)\n>                         has_markers = 1;\n>                         break;\n>                 }\n> +               if (buffer_is_binary(sb.buf,\n> +                                    ULONG_MAX <= sb.len ? ULONG_MAX : sb.len))\n> +                       break;\n>         }\n\nShould this check be before the conflict marker line check in case the first\niteration accidentally matches for a binary file and breaks with\nhas_markers = 1?\n"},{"id":"549426","messageId":"xmqq4ihd5khl.fsf@gitster.g","threadId":"66082","inReplyTo":"xmqq8q6tc3cy.fsf@gitster.g","subject":"Re: [PATCH 3/4] add: introduce '--resolved' option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-02T02:23:02Z","receivedAt":"2026-08-02T02:23:04Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n>> I wonder if we should simply write 'addremove', as this part of the\n>> change is about rewriting the open-coded die_for_incompatible_opt2()\n>> in the original and using die_for_incompatible_opt3() to add a third\n>> option.\n>\n> Not really.  I forgot that addremove is futzed before this part when\n> add_resolved is present, so using addremove would mean both\n> addremove and add_resolved are on, mistakenly complaining about -A\n> that the user never gave us.\n>\n> So \"0 < addremove_explicit\" that is.\n\nWell, this episode illustrates that the option parsing in \"git add\"\nis unnecessarily confusing, and I think the reason is because it\nconflates the presence of each command line option and the behaviour\nthe program wants to exhibit into the same variable.  addremove for\nexample is about \"removals are also recorded in the index\", and that\nbecomes effective primarily when \"-A\" option is given, but \"-A\" is\nnot the only thing to turn it on.\n\nPerhaps revamping the way the options are parsed so that the\nincompatible options are noticed upfront, and then deciding the\nbehaviour based on the primary operating mode that was parsed by the\nooption parser in a separate step might make things less confusing?\n\n"},{"id":"549457","messageId":"xmqqfr0w2lnt.fsf@gitster.g","threadId":"66082","inReplyTo":"CAC2QwmJeohdnWhUbcP6Pc5w1X8yZf3jXvpR8JC=Hb9gqkFF6ig@mail.gmail.com","subject":"Re: [PATCH 3/4] add: introduce '--resolved' option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-02T22:38:14Z","receivedAt":"2026-08-02T22:38:17Z","isPatch":true,"body":"Michael Montalbo <mmontalbo@gmail.com> writes:\n\n> On Wed, Jul 29, 2026 at 8:17 AM Junio C Hamano <gitster@pobox.com> wrote:\n>>\n>>\n>> Left unchecked, this loop may end up scanning a large binary file to\n>> the end in vain.  We may squeeze in something like this to punt\n>> early.\n>>\n>>  merge-ll.c | 3 +++\n>>  1 file changed, 3 insertions(+)\n>>\n>> diff --git a/merge-ll.c b/merge-ll.c\n>> index 5e5044b9e3..ef5287dee8 100644\n>> --- a/merge-ll.c\n>> +++ b/merge-ll.c\n>> @@ -516,6 +516,9 @@ int has_conflict_markers(struct index_state *istate, const char *path)\n>>                         has_markers = 1;\n>>                         break;\n>>                 }\n>> +               if (buffer_is_binary(sb.buf,\n>> +                                    ULONG_MAX <= sb.len ? ULONG_MAX : sb.len))\n>> +                       break;\n>>         }\n>\n> Should this check be before the conflict marker line check in case the first\n> iteration accidentally matches for a binary file and breaks with\n> has_markers = 1?\n\nIf we misidentify early, that is a desirable outcome, isn't it?\n\nWe did not have to scan much and we gave control back to the user as\nsoon as we saw \"<<<<<<\", telling them that we refused to add the path\nto the index, so that the user can inspect the situation more\ndeeply.  In this application, false positives are much better than\nfalse negatives, and failing early is better than failing late.\n\nSo, no, I do not think so, even though in practice I do not think it\nwould matter either way.\n\nA question that may have much more impact is whether the attribute\nsystem should have any say in this code path.  I am somewhat torn on\nthis.\n"},{"id":"549469","messageId":"CAC2Qwm+4qnAdUpinbda+etnFYUaYkycW-0HasHo95MM3rPY3yA@mail.gmail.com","threadId":"66082","inReplyTo":"xmqqfr0w2lnt.fsf@gitster.g","subject":"Re: [PATCH 3/4] add: introduce '--resolved' option","fromName":"Michael Montalbo","fromEmail":"mmontalbo@gmail.com","sentAt":"2026-08-03T01:15:20Z","receivedAt":"2026-08-03T01:15:33Z","isPatch":true,"body":"On Sun, Aug 2, 2026 at 3:38 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n>\n> If we misidentify early, that is a desirable outcome, isn't it?\n>\n> We did not have to scan much and we gave control back to the user as\n> soon as we saw \"<<<<<<\", telling them that we refused to add the path\n> to the index, so that the user can inspect the situation more\n> deeply.  In this application, false positives are much better than\n> false negatives, and failing early is better than failing late.\n>\n\nI didn't fully consider the case of the false negative that would come\nwith switching the order of the checks. I agree false positives are\nmuch better in this scenario.\n\n> So, no, I do not think so, even though in practice I do not think it\n> would matter either way.\n>\n> A question that may have much more impact is whether the attribute\n> system should have any say in this code path.  I am somewhat torn on\n> this.\n\nI do think something in that direction might make things more clear. When\nlooking at \"has_conflict_markers()\" in isolation my instinct was to guard\nagainst the case that the file is binary (among other \"easy\" checks to\neliminate non-conflict marker paths upfront) and return right away. That rested\non the assumption the check for a binary file had an unambiguous result as\nopposed to being a heuristic.\n\nWould it be possible to check the merge driver used and return early if a\nnon-text driver was used? I tried tracing the path where conflict markers end\nup being generated and it seems like the binary and union drivers cannot\nemit conflict markers. That might be conceptually more straightforward and\neliminate the need for a \"buffer_is_binary()\" check. Not sure if that would be\nbrittle in the face of new merge drivers being added, but directly relating\nthis conflict marker check with the drivers that emit those markers seems to\nmake sense to me.\n"}]}