From: Junio C Hamano <gitster@pobox.com>
To: "Harald Nordgren via GitGitGadget" <gitgitgadget@gmail.com>,
Phillip Wood <phillip.wood@dunelm.org.uk>
Cc: git@vger.kernel.org, Harald Nordgren <haraldnordgren@gmail.com>
Subject: Re: [PATCH v4] checkout: -m (--merge) uses autostash when switching branches
Date: Sat, 14 Mar 2026 19:25:50 -0700 [thread overview]
Message-ID: <xmqqfr61vnf5.fsf@gitster.g> (raw)
In-Reply-To: <pull.2234.v4.git.git.1773482375668.gitgitgadget@gmail.com> (Harald Nordgren via GitGitGadget's message of "Sat, 14 Mar 2026 09:59:35 +0000")
"Harald Nordgren via GitGitGadget" <gitgitgadget@gmail.com> writes:
> From: Harald Nordgren <haraldnordgren@gmail.com>
>
> When switching branches with "git checkout -m", local modifications
> can block the switch. Teach the -m flow to create a temporary stash
> before switching and reapply it after. On success, only "Applied
> autostash." is shown. If reapplying causes conflicts, the stash is
> kept and the user is told they can resolve and run "git stash drop",
> or run "git reset --hard" and later "git stash pop" to recover their
> changes.
>
> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
> ---
> checkout: 'autostash' for branch switching
>
> cc: Phillip Wood phillip.wood123@gmail.com
I very much appreciate this, but somehow this "cc:" seems to have
been ignored.
> Documentation/git-checkout.adoc | 56 +++++-----
> Documentation/git-switch.adoc | 26 ++---
> builtin/checkout.c | 179 +++++++++++++++++---------------
> builtin/stash.c | 30 ++++--
> sequencer.c | 67 +++++++++---
> sequencer.h | 4 +
> t/t3420-rebase-autostash.sh | 24 +++--
> t/t7201-co.sh | 160 ++++++++++++++++++++++++++++
> t/t7600-merge.sh | 4 +-
> xdiff-interface.c | 12 +++
> xdiff-interface.h | 1 +
> 11 files changed, 412 insertions(+), 151 deletions(-)
>
> diff --git a/Documentation/git-checkout.adoc b/Documentation/git-checkout.adoc
> index 43ccf47cf6..9d5f5c51ae 100644
> --- a/Documentation/git-checkout.adoc
> +++ b/Documentation/git-checkout.adoc
> @@ -251,20 +251,17 @@ working tree, by copying them from elsewhere, extracting a tarball, etc.
> are different between the current branch and the branch to
> which you are switching, the command refuses to switch
> branches in order to preserve your modifications in context.
> - However, with this option, a three-way merge between the current
> - branch, your working tree contents, and the new branch
> - is done, and you will be on the new branch.
> -+
> -When a merge conflict happens, the index entries for conflicting
> -paths are left unmerged, and you need to resolve the conflicts
> -and mark the resolved paths with `git add` (or `git rm` if the merge
> -should result in deletion of the path).
> + With this option, the conflicting local changes are
> + automatically stashed before the switch and reapplied
> + afterwards. If the local changes do not overlap with the
> + differences between branches, the switch proceeds without
> + stashing. If reapplying the stash results in conflicts, the
> + entry is saved to the stash list so you can use `git stash
> + pop` to recover and `git stash drop` when done.
The new conflict message in sequencer.c correctly points out that,
if the user wants to use `git stash pop`, they would first need to
clear the working tree (e.g. `git reset --hard`) because the
conflicted changes are already there.
The documentation here is a bit too simplified and may give the
impression that `git stash pop` is the immediate next step when
conflicts are present in the worktree, which is probably something
we want to avoid.
> ------------
> $ git checkout -m mytopic
> -Auto-merging frotz
> +Applied autostash.
> ------------
I must be utterly confused, but the documentation _before_ this
patch does not seem to match the reality. In the following
illustration, I am switching to another branch that has contents in
transport.c that is different from the version in HEAD:
$ git checkout master
$ echo >>transport.c
$ git checkout jk/transport-color-leakfix
error: Your local changes to the following files would be overwritten...
transport.c
Please commit your changes or stash ...
Aborting
$ git checkout -m jk/transport-color-leakfix
M transport.c
Switched to branch 'jk/transport-color-leakfix'
Hmmmm.
The new behavior is silent about individual files, and instead
reports "Applied autostash."; even though it is consistent with how
other commands may behave when given the "--autostash" option, I am
not sure if it is even necessary.
Even in the documented version, the "Auto-merging" message is
presumably given only to the paths that needed real content-level
merges, and the paths that went through the trivial tree-level
merges (i.e., paths that are different from HEAD and mytopic but
without local changes) are not reported. The behaviour I am seeing
does not say anything to either trivially merged or really merged
paths while branch switching is happening, and shows the local
changes at the end of merge_working_tree() by calling
show_local_changes(), which runs "git diff --name-status"
in-process.
In either case, the stashing is a mere implementation detail of
carrying the local changes with you when you move to a different
branch, and moving with your local changes is what "Switching"
branch is about in this system, so I am not sure if we need to say
anything more than the current "Switched to branch 'mytopic'" in the
successful case.
> diff --git a/builtin/checkout.c b/builtin/checkout.c
> index 1d1667fa4c..1968e46ae9 100644
> --- a/builtin/checkout.c
> +++ b/builtin/checkout.c
> @@ -17,7 +17,6 @@
> #include "merge-ll.h"
> #include "lockfile.h"
> #include "mem-pool.h"
> -#include "merge-ort-wrappers.h"
> #include "object-file.h"
> #include "object-name.h"
> #include "odb.h"
> @@ -30,6 +29,7 @@
> #include "repo-settings.h"
> #include "resolve-undo.h"
> #include "revision.h"
> +#include "sequencer.h"
> #include "setup.h"
> #include "submodule.h"
> #include "symlinks.h"
> @@ -845,83 +845,8 @@ static int merge_working_tree(const struct checkout_opts *opts,
>
> ret = unpack_trees(2, trees, &topts);
> clear_unpack_trees_porcelain(&topts);
> - if (ret == -1) {
> -...
> - }
> + if (ret == -1)
> + return 1;
> }
>
> if (!cache_tree_fully_valid(the_repository->index->cache_tree))
> @@ -930,9 +855,6 @@ static int merge_working_tree(const struct checkout_opts *opts,
> if (write_locked_index(the_repository->index, &lock_file, COMMIT_LOCK))
> die(_("unable to write new index file"));
>
> - if (!opts->discard_changes && !opts->quiet && new_branch_info->commit)
> - show_local_changes(&new_branch_info->commit->object, &opts->diff_options);
Ah, this is what you lost. I do not see a reason why we would want
to lose it.
> +static int checkout_would_clobber_changes(struct branch_info *old_branch_info,
> + struct branch_info *new_branch_info)
> +{
> +...
> + init_tree_desc(&trees[0], &old_tree->object.oid,
> + old_tree->buffer, old_tree->size);
> + init_tree_desc(&trees[1], &new_tree->object.oid,
> + new_tree->buffer, new_tree->size);
> +
> + ret = unpack_trees(2, trees, &topts);
> + discard_index(&tmp_index);
In my original outline, I did suggest
- check if the local changes overlap with paths that differ between
HEAD and the new branch, and if not, we need no stashing;
otherwise stash.
- then switch branches with two-way merge
- then unstash.
But my untold assumption was that the initial check was done by
running a moral equivalent of "git diff --name-only HEAD <branch>"
to obtain the list of paths that differ between branches, and
a "git diff HEAD" to obtain the list of paths with local changes.
If you are using the real branch switching machinery in the
"dry-run" mode, then it is a waste to discard when we find out that
there is no overlap. So if I knew you would use the unpack-trees in
the dry-run mode, instead of two diffs, the outline would have been
more like
- Try unpack-trees in dry-run mode to see if there would get any
failure from overlap.
- If unpack-trees would succeed, we already have the right contents
in the index after the above dry-run. We did not touch the
working tree so far, but if we kept track of which paths we would
have written out to the working tree during our dry-run, we can
write them out of the index at this point and we are done (we of
course need to update HEAD to point at the new branch, give the
"Switched to branch 'foo'" message, etc., but the real work of
switching branches is done).
- If not, discard the dry-run index, stash, switch branches the
usual way, and unstash.
which might be more performant in a large tree.
Having said all that, what we see above, while running the same
unpack-trees once in dry-run mode and another for real, may be
performant enough and complicating the dry-run code might not be
worth it.
> @@ -1165,6 +1136,8 @@ static int switch_branches(const struct checkout_opts *opts,
> struct object_id rev;
> int flag, writeout_error = 0;
> int do_merge = 1;
> + struct strbuf old_commit_shortname = STRBUF_INIT;
> + const char *stash_label_ancestor = NULL;
>
> trace2_cmd_mode("branch");
>
> @@ -1202,10 +1175,34 @@ static int switch_branches(const struct checkout_opts *opts,
> do_merge = 0;
> }
>
> + if (old_branch_info.name)
> + stash_label_ancestor = old_branch_info.name;
> + else if (old_branch_info.commit) {
> + strbuf_add_unique_abbrev(&old_commit_shortname,
> + &old_branch_info.commit->object.oid,
> + DEFAULT_ABBREV);
> + stash_label_ancestor = old_commit_shortname.buf;
> + }
> +
> + if (opts->merge) {
> + if (repo_read_index(the_repository) < 0)
> + die(_("index file corrupt"));
> + if (checkout_would_clobber_changes(&old_branch_info,
> + new_branch_info))
> + create_autostash_ref_silent(the_repository,
> + "CHECKOUT_AUTOSTASH");
> + }
> +
> if (do_merge) {
> ret = merge_working_tree(opts, &old_branch_info, new_branch_info, &writeout_error);
> if (ret) {
> + apply_autostash_ref_with_labels(the_repository,
> + "CHECKOUT_AUTOSTASH",
> + new_branch_info->name,
> + "local",
> + stash_label_ancestor);
> branch_info_release(&old_branch_info);
> + strbuf_release(&old_commit_shortname);
> return ret;
> }
> }
Nice changes to preserve the conflict marker labels.
> diff --git a/builtin/stash.c b/builtin/stash.c
> index e79d612e57..1016d88e52 100644
> --- a/builtin/stash.c
> +++ b/builtin/stash.c
This can and should become a separate patch that is a preliminary
preparation step, [1/2], with its own documentation and test, only
about teaching "git stash" command a set of new options to use
custom conflict labels, I think. And on top of it, patch [2/2] that
updates "checkout -m" to use the improved "git stash" with custom
labels support is written. It would give us a clean logical
progression of the series.
> diff --git a/sequencer.c b/sequencer.c
> index aafd0bc959..53e04d8a94 100644
> --- a/sequencer.c
> +++ b/sequencer.c
> @@ -4632,7 +4632,8 @@ static enum todo_command peek_command(struct todo_list *todo_list, int offset)
>
> static void create_autostash_internal(struct repository *r,
> const char *path,
> - const char *refname)
> + const char *refname,
> + int silent)
> {
> struct strbuf buf = STRBUF_INIT;
> struct lock_file lock_file = LOCK_INIT;
> @@ -4677,7 +4678,8 @@ static void create_autostash_internal(struct repository *r,
> &oid, null_oid(the_hash_algo), 0, UPDATE_REFS_DIE_ON_ERR);
> }
>
> - printf(_("Created autostash: %s\n"), buf.buf);
> + if (!silent)
> + fprintf(stderr, _("Created autostash: %s\n"), buf.buf);
> if (reset_head(r, &ropts) < 0)
> die(_("could not reset --hard"));
> discard_index(r->index);
> @@ -4689,15 +4691,22 @@ static void create_autostash_internal(struct repository *r,
>
> void create_autostash(struct repository *r, const char *path)
> {
> - create_autostash_internal(r, path, NULL);
> + create_autostash_internal(r, path, NULL, 0);
> }
>
> void create_autostash_ref(struct repository *r, const char *refname)
> {
> - create_autostash_internal(r, NULL, refname);
> + create_autostash_internal(r, NULL, refname, 0);
> }
>
> -static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply)
> +void create_autostash_ref_silent(struct repository *r, const char *refname)
> +{
> + create_autostash_internal(r, NULL, refname, 1);
> +}
Changes around here to optionally squelch message(s) is better
presented as yet another preliminary preparation step, I think.
> @@ -4752,7 +4775,8 @@ static int apply_save_autostash(const char *path, int attempt_apply)
> }
> strbuf_trim(&stash_oid);
>
> - ret = apply_save_autostash_oid(stash_oid.buf, attempt_apply);
> + ret = apply_save_autostash_oid(stash_oid.buf, attempt_apply,
> + NULL, NULL, NULL);
>
> unlink(path);
> strbuf_release(&stash_oid);
> @@ -4771,11 +4795,13 @@ int apply_autostash(const char *path)
>
> int apply_autostash_oid(const char *stash_oid)
> {
> - return apply_save_autostash_oid(stash_oid, 1);
> + return apply_save_autostash_oid(stash_oid, 1, NULL, NULL, NULL);
> }
>
> static int apply_save_autostash_ref(struct repository *r, const char *refname,
> - int attempt_apply)
> + int attempt_apply,
> + const char *label1, const char *label2,
> + const char *label_ancestor)
> {
> struct object_id stash_oid;
> char stash_oid_hex[GIT_MAX_HEXSZ + 1];
> @@ -4791,7 +4817,8 @@ static int apply_save_autostash_ref(struct repository *r, const char *refname,
> return error(_("autostash reference is a symref"));
>
> oid_to_hex_r(stash_oid_hex, &stash_oid);
> - ret = apply_save_autostash_oid(stash_oid_hex, attempt_apply);
> + ret = apply_save_autostash_oid(stash_oid_hex, attempt_apply,
> + label1, label2, label_ancestor);
>
> refs_delete_ref(get_main_ref_store(r), "", refname,
> &stash_oid, REF_NO_DEREF);
>
> @@ -4801,12 +4828,20 @@ static int apply_save_autostash_ref(struct repository *r, const char *refname,
>
> int save_autostash_ref(struct repository *r, const char *refname)
> {
> - return apply_save_autostash_ref(r, refname, 0);
> + return apply_save_autostash_ref(r, refname, 0, NULL, NULL, NULL);
> }
>
> int apply_autostash_ref(struct repository *r, const char *refname)
> {
> - return apply_save_autostash_ref(r, refname, 1);
> + return apply_save_autostash_ref(r, refname, 1, NULL, NULL, NULL);
> +}
> +
> +int apply_autostash_ref_with_labels(struct repository *r, const char *refname,
> + const char *label1, const char *label2,
> + const char *label_ancestor)
> +{
> + return apply_save_autostash_ref(r, refname, 1,
> + label1, label2, label_ancestor);
> }
>
> static int checkout_onto(struct repository *r, struct replay_opts *opts,
So are these, to teach the machinery to take optional conflict
marker labels.
Thanks.
next prev parent reply other threads:[~2026-03-15 2:25 UTC|newest]
Thread overview: 29+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-03-12 13:26 [PATCH] checkout: add --autostash option for branch switching Harald Nordgren via GitGitGadget
2026-03-12 14:40 ` Junio C Hamano
2026-03-12 19:33 ` [PATCH v31 0/2] status: add status.compareBranches config for multiple branch comparisons Harald Nordgren
2026-03-13 14:29 ` [PATCH] checkout: add --autostash option for branch switching Phillip Wood
2026-03-14 17:17 ` Junio C Hamano
2026-03-16 16:36 ` Phillip Wood
2026-03-16 20:04 ` Junio C Hamano
2026-03-17 9:47 ` Harald Nordgren
2026-03-19 8:25 ` Harald Nordgren
2026-03-19 16:48 ` Junio C Hamano
2026-03-12 19:33 ` [PATCH v2] " Harald Nordgren via GitGitGadget
2026-03-12 19:50 ` Junio C Hamano
2026-03-13 9:22 ` [PATCH] " Harald Nordgren
2026-03-13 9:23 ` [PATCH v3] " Harald Nordgren via GitGitGadget
2026-03-13 17:16 ` Junio C Hamano
2026-03-13 19:33 ` [PATCH] " Harald Nordgren
2026-03-13 20:30 ` Junio C Hamano
2026-03-14 9:59 ` [PATCH v4] checkout: -m (--merge) uses autostash when switching branches Harald Nordgren via GitGitGadget
2026-03-15 2:25 ` Junio C Hamano [this message]
2026-03-15 11:19 ` [PATCH v5 0/4] checkout: 'autostash' for branch switching Harald Nordgren via GitGitGadget
2026-03-15 11:19 ` [PATCH v5 1/4] stash: add --ours-label, --theirs-label, --base-label for apply Harald Nordgren via GitGitGadget
2026-03-15 11:19 ` [PATCH v5 2/4] sequencer: allow create_autostash to run silently Harald Nordgren via GitGitGadget
2026-03-15 11:19 ` [PATCH v5 3/4] sequencer: teach autostash apply to take optional conflict marker labels Harald Nordgren via GitGitGadget
2026-03-15 11:19 ` [PATCH v5 4/4] checkout: -m (--merge) uses autostash when switching branches Harald Nordgren via GitGitGadget
2026-03-17 9:35 ` [PATCH v6 0/4] checkout: 'autostash' for branch switching Harald Nordgren via GitGitGadget
2026-03-17 9:35 ` [PATCH v6 1/4] stash: add --ours-label, --theirs-label, --base-label for apply Harald Nordgren via GitGitGadget
2026-03-17 9:35 ` [PATCH v6 2/4] sequencer: allow create_autostash to run silently Harald Nordgren via GitGitGadget
2026-03-17 9:35 ` [PATCH v6 3/4] sequencer: teach autostash apply to take optional conflict marker labels Harald Nordgren via GitGitGadget
2026-03-17 9:35 ` [PATCH v6 4/4] checkout: -m (--merge) uses autostash when switching branches Harald Nordgren via GitGitGadget
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=xmqqfr61vnf5.fsf@gitster.g \
--to=gitster@pobox.com \
--cc=git@vger.kernel.org \
--cc=gitgitgadget@gmail.com \
--cc=haraldnordgren@gmail.com \
--cc=phillip.wood@dunelm.org.uk \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox