stash: add --label-ours, --label-theirs, --label-base for apply

Allow callers of "git stash apply" to pass custom labels for conflict markers instead of the default "Updated upstream" and "Stashed changes". Document the new options and add a test. Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Harald Nordgren committed Apr 28, 2026 at 18:39 UTC 13817db2746d48b6d3cfe68e3bf10a1be67865a9
4 files changed +57 -12
Documentation/git-stash.adoc
+10 -1
@@ -12,7 +12,7 @@ git stash list [<log-options>]
12 git stash show [-u | --include-untracked | --only-untracked] [<diff-options>] [<stash>]
13 git stash drop [-q | --quiet] [<stash>]
14 git stash pop [--index] [-q | --quiet] [<stash>]
15 -git stash apply [--index] [-q | --quiet] [<stash>]
15 +git stash apply [--index] [-q | --quiet] [--label-ours=<label>] [--label-theirs=<label>] [--label-base=<label>] [<stash>]
16 git stash branch <branchname> [<stash>]
17 git stash [push] [-p | --patch] [-S | --staged] [-k | --[no-]keep-index] [-q | --quiet]
18 [-u | --include-untracked] [-a | --all] [(-m | --message) <message>]
@@ -195,6 +195,15 @@ the index's ones. However, this can fail, when you have conflicts
195 (which are stored in the index, where you therefore can no longer
196 apply the changes as they were originally).
197
198 +`--label-ours=<label>`::
199 +`--label-theirs=<label>`::
200 +`--label-base=<label>`::
201 + These options are only valid for the `apply` command.
202 ++
203 +Use the given labels in conflict markers instead of the default
204 +"Updated upstream", "Stashed changes", and "Stash base".
205 +`--label-base` only has an effect with merge.conflictStyle=diff3.
206 +
207 `-k`::
208 `--keep-index`::
209 `--no-keep-index`::
builtin/stash.c
+20 -8
@@ -44,7 +44,7 @@
44 #define BUILTIN_STASH_POP_USAGE \
45 N_("git stash pop [--index] [-q | --quiet] [<stash>]")
46 #define BUILTIN_STASH_APPLY_USAGE \
47 - N_("git stash apply [--index] [-q | --quiet] [<stash>]")
47 + N_("git stash apply [--index] [-q | --quiet] [--label-ours=<label>] [--label-theirs=<label>] [--label-base=<label>] [<stash>]")
48 #define BUILTIN_STASH_BRANCH_USAGE \
49 N_("git stash branch <branchname> [<stash>]")
50 #define BUILTIN_STASH_STORE_USAGE \
@@ -591,7 +591,9 @@ static void unstage_changes_unless_new(struct object_id *orig_tree)
591 }
592
593 static int do_apply_stash(const char *prefix, struct stash_info *info,
594 - int index, int quiet)
594 + int index, int quiet,
595 + const char *label_ours, const char *label_theirs,
596 + const char *label_base)
597 {
598 int clean, ret;
599 int has_index = index;
@@ -643,9 +645,9 @@ static int do_apply_stash(const char *prefix, struct stash_info *info,
645
646 init_ui_merge_options(&o, the_repository);
647
646 - o.branch1 = "Updated upstream";
647 - o.branch2 = "Stashed changes";
648 - o.ancestor = "Stash base";
648 + o.branch1 = label_ours ? label_ours : "Updated upstream";
649 + o.branch2 = label_theirs ? label_theirs : "Stashed changes";
650 + o.ancestor = label_base ? label_base : "Stash base";
651
652 if (oideq(&info->b_tree, &c_tree))
653 o.branch1 = "Version stash was based on";
@@ -723,11 +725,18 @@ static int apply_stash(int argc, const char **argv, const char *prefix,
725 int ret = -1;
726 int quiet = 0;
727 int index = use_index;
728 + const char *label_ours = NULL, *label_theirs = NULL, *label_base = NULL;
729 struct stash_info info = STASH_INFO_INIT;
730 struct option options[] = {
731 OPT__QUIET(&quiet, N_("be quiet, only report errors")),
732 OPT_BOOL(0, "index", &index,
733 N_("attempt to recreate the index")),
734 + OPT_STRING(0, "label-ours", &label_ours, N_("label"),
735 + N_("label for the upstream side in conflict markers")),
736 + OPT_STRING(0, "label-theirs", &label_theirs, N_("label"),
737 + N_("label for the stashed side in conflict markers")),
738 + OPT_STRING(0, "label-base", &label_base, N_("label"),
739 + N_("label for the base in diff3 conflict markers")),
740 OPT_END()
741 };
742
@@ -737,7 +746,8 @@ static int apply_stash(int argc, const char **argv, const char *prefix,
746 if (get_stash_info(&info, argc, argv))
747 goto cleanup;
748
740 - ret = do_apply_stash(prefix, &info, index, quiet);
749 + ret = do_apply_stash(prefix, &info, index, quiet,
750 + label_ours, label_theirs, label_base);
751 cleanup:
752 free_stash_info(&info);
753 return ret;
@@ -836,7 +846,8 @@ static int pop_stash(int argc, const char **argv, const char *prefix,
846 if (get_stash_info_assert(&info, argc, argv))
847 goto cleanup;
848
839 - if ((ret = do_apply_stash(prefix, &info, index, quiet)))
849 + if ((ret = do_apply_stash(prefix, &info, index, quiet,
850 + NULL, NULL, NULL)))
851 printf_ln(_("The stash entry is kept in case "
852 "you need it again."));
853 else
@@ -877,7 +888,8 @@ static int branch_stash(int argc, const char **argv, const char *prefix,
888 strvec_push(&cp.args, oid_to_hex(&info.b_commit));
889 ret = run_command(&cp);
890 if (!ret)
880 - ret = do_apply_stash(prefix, &info, 1, 0);
891 + ret = do_apply_stash(prefix, &info, 1, 0,
892 + NULL, NULL, NULL);
893 if (!ret && info.is_stash_ref)
894 ret = do_drop_stash(&info, 0);
895
t/t3903-stash.sh
+24
@@ -56,6 +56,7 @@ setup_stash() {
56 git add other-file &&
57 test_tick &&
58 git commit -m initial &&
59 + git tag initial &&
60 echo 2 >file &&
61 git add file &&
62 echo 3 >file &&
@@ -1790,4 +1791,27 @@ test_expect_success 'stash.index=false overridden by --index' '
1791 test_cmp expect file
1792 '
1793
1794 +test_expect_success 'apply with custom conflict labels' '
1795 + git reset --hard initial &&
1796 + test_commit label-base conflict-file base-content &&
1797 + echo stashed >conflict-file &&
1798 + git stash push -m "stashed" &&
1799 + test_commit label-upstream conflict-file upstream-content &&
1800 + test_must_fail git -c merge.conflictStyle=diff3 stash apply --label-ours=UP --label-theirs=STASH &&
1801 + test_grep "^<<<<<<< UP" conflict-file &&
1802 + test_grep "^||||||| Stash base" conflict-file &&
1803 + test_grep "^>>>>>>> STASH" conflict-file
1804 +'
1805 +
1806 +test_expect_success 'apply with empty conflict labels' '
1807 + git reset --hard initial &&
1808 + test_commit empty-label-base conflict-file base-content &&
1809 + echo stashed >conflict-file &&
1810 + git stash push -m "stashed" &&
1811 + test_commit empty-label-upstream conflict-file upstream-content &&
1812 + test_must_fail git stash apply --label-ours= --label-theirs= &&
1813 + test_grep "^<<<<<<<$" conflict-file &&
1814 + test_grep "^>>>>>>>$" conflict-file
1815 +'
1816 +
1817 test_done
xdiff/xmerge.c
+3 -3
@@ -199,9 +199,9 @@ static int fill_conflict_hunk(xdfenv_t *xe1, const char *name1,
199 int size, int i, int style,
200 xdmerge_t *m, char *dest, int marker_size)
201 {
202 - int marker1_size = (name1 ? strlen(name1) + 1 : 0);
203 - int marker2_size = (name2 ? strlen(name2) + 1 : 0);
204 - int marker3_size = (name3 ? strlen(name3) + 1 : 0);
202 + int marker1_size = (name1 && *name1 ? strlen(name1) + 1 : 0);
203 + int marker2_size = (name2 && *name2 ? strlen(name2) + 1 : 0);
204 + int marker3_size = (name3 && *name3 ? strlen(name3) + 1 : 0);
205 int needs_cr = is_cr_needed(xe1, xe2, m);
206
207 if (marker_size <= 0)