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. Resolve the conflicts
+ and run `git stash drop` when done, or clear the working
+ tree (e.g. with `git reset --hard`) before running `git stash
+ pop` later to re-apply your changes.
+
When checking out paths from the index, this option lets you recreate
the conflicted merge in the specified paths. This option cannot be
used when checking out paths from a tree-ish.
-+
-When switching branches with `--merge`, staged changes may be lost.
`--conflict=<style>`::
The same as `--merge` option above, but changes the way the
error: You have local changes to 'frotz'; not switching branches.
------------
-You can give the `-m` flag to the command, which would try a
-three-way merge:
+You can give the `-m` flag to the command, which would carry your local
+changes to the new branch:
------------
$ git checkout -m mytopic
-Auto-merging frotz
+Switched to branch 'mytopic'
------------
-After this three-way merge, the local modifications are _not_
+After the switch, the local modifications are reapplied and are _not_
registered in your index file, so `git diff` would show you what
changes you made since the tip of the new branch.
=== 3. Merge conflict
-When a merge conflict happens during switching branches with
-the `-m` option, you would see something like this:
+When the `--merge` (`-m`) option is in effect and the locally
+modified files overlap with files that need to be updated by the
+branch switch, the changes are stashed and reapplied after the
+switch. If this process results in conflicts, a stash entry is saved
+and made available in `git stash list`:
------------
$ git checkout -m mytopic
-Auto-merging frotz
-ERROR: Merge conflict in frotz
-fatal: merge program failed
-------------
+Your local changes are stashed, however, applying it to carry
+forward your local changes resulted in conflicts:
-At this point, `git diff` shows the changes cleanly merged as in
-the previous example, as well as the changes in the conflicted
-files. Edit and resolve the conflict and mark it resolved with
-`git add` as usual:
+ - You can try resolving them now. If you resolved them
+ successfully, discard the stash entry with "git stash drop".
+ - Alternatively you can "git reset --hard" if you do not want
+ to deal with them right now, and later "git stash pop" to
+ recover your local changes.
------------
-$ edit frotz
-$ git add frotz
-------------
+
+You can try resolving the conflicts now. Edit the conflicting files
+and mark them resolved with `git add` as usual, then run `git stash
+drop` to discard the stash entry. Alternatively, you can clear the
+working tree with `git reset --hard` and recover your local changes
+later with `git stash pop`.
CONFIGURATION
-------------
`-m`::
`--merge`::
- If you have local modifications to one or more files that 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).
+ If you have local modifications to one or more files that
+ are different between the current branch and the branch to
+ which you are switching, the command normally refuses to
+ switch branches in order to preserve your modifications in
+ context. However, 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. Resolve
+ the conflicts and run `git stash drop` when done, or clear
+ the working tree (e.g. with `git reset --hard`) before
+ running `git stash pop` later to re-apply your changes.
`--conflict=<style>`::
The same as `--merge` option above, but changes the way the
error: You have local changes to 'frotz'; not switching branches.
------------
-You can give the `-m` flag to the command, which would try a three-way
-merge:
+You can give the `-m` flag to the command, which would carry your local
+changes to the new branch:
------------
$ git switch -m mytopic
-Auto-merging frotz
+Switched to branch 'mytopic'
------------
-After this three-way merge, the local modifications are _not_
+After the switch, the local modifications are reapplied and are _not_
registered in your index file, so `git diff` would show you what
changes you made since the tip of the new branch.
#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"
#include "repo-settings.h"
#include "resolve-undo.h"
#include "revision.h"
+#include "sequencer.h"
#include "setup.h"
#include "submodule.h"
#include "symlinks.h"
.auto_advance = 1, \
}
+#define MERGE_WORKING_TREE_UNPACK_FAILED (-2)
+
struct branch_info {
char *name; /* The short name used */
char *path; /* The full name of a real branch */
static void init_topts(struct unpack_trees_options *topts, int merge,
int show_progress, int overwrite_ignore,
- struct commit *old_commit)
+ struct commit *old_commit, bool show_unpack_errors)
{
memset(topts, 0, sizeof(*topts));
topts->head_idx = -1;
topts->initial_checkout = is_index_unborn(the_repository->index);
topts->update = 1;
topts->merge = 1;
- topts->quiet = merge && old_commit;
+ topts->quiet = merge && old_commit && !show_unpack_errors;
topts->verbose_update = show_progress;
topts->fn = twoway_merge;
topts->preserve_ignored = !overwrite_ignore;
static int merge_working_tree(const struct checkout_opts *opts,
struct branch_info *old_branch_info,
struct branch_info *new_branch_info,
- int *writeout_error)
+ int *writeout_error,
+ bool show_unpack_errors)
{
int ret;
struct lock_file lock_file = LOCK_INIT;
/* 2-way merge to the new branch */
init_topts(&topts, opts->merge, opts->show_progress,
- opts->overwrite_ignore, old_branch_info->commit);
+ opts->overwrite_ignore, old_branch_info->commit,
+ show_unpack_errors);
init_checkout_metadata(&topts.meta, new_branch_info->refname,
new_branch_info->commit ?
&new_branch_info->commit->object.oid :
ret = unpack_trees(2, trees, &topts);
clear_unpack_trees_porcelain(&topts);
if (ret == -1) {
- /*
- * Unpack couldn't do a trivial merge; either
- * give up or do a real merge, depending on
- * whether the merge flag was used.
- */
- struct tree *work;
- struct tree *old_tree;
- struct merge_options o;
- struct strbuf sb = STRBUF_INIT;
- struct strbuf old_commit_shortname = STRBUF_INIT;
-
- if (!opts->merge) {
- rollback_lock_file(&lock_file);
- return 1;
- }
-
- /*
- * Without old_branch_info->commit, the below is the same as
- * the two-tree unpack we already tried and failed.
- */
- if (!old_branch_info->commit) {
- rollback_lock_file(&lock_file);
- return 1;
- }
- old_tree = repo_get_commit_tree(the_repository,
- old_branch_info->commit);
-
- if (repo_index_has_changes(the_repository, old_tree, &sb))
- die(_("cannot continue with staged changes in "
- "the following files:\n%s"), sb.buf);
- strbuf_release(&sb);
-
- /* Do more real merge */
-
- /*
- * We update the index fully, then write the
- * tree from the index, then merge the new
- * branch with the current tree, with the old
- * branch as the base. Then we reset the index
- * (but not the working tree) to the new
- * branch, leaving the working tree as the
- * merged version, but skipping unmerged
- * entries in the index.
- */
-
- add_files_to_cache(the_repository, NULL, NULL, NULL, 0,
- 0, 0);
- init_ui_merge_options(&o, the_repository);
- o.verbosity = 0;
- work = write_in_core_index_as_tree(the_repository,
- the_repository->index);
-
- ret = reset_tree(new_tree,
- opts, 1,
- writeout_error, new_branch_info);
- if (ret) {
- rollback_lock_file(&lock_file);
- return ret;
- }
- o.ancestor = old_branch_info->name;
- if (!old_branch_info->name) {
- strbuf_add_unique_abbrev(&old_commit_shortname,
- &old_branch_info->commit->object.oid,
- DEFAULT_ABBREV);
- o.ancestor = old_commit_shortname.buf;
- }
- o.branch1 = new_branch_info->name;
- o.branch2 = "local";
- o.conflict_style = opts->conflict_style;
- ret = merge_ort_nonrecursive(&o,
- new_tree,
- work,
- old_tree);
- if (ret < 0)
- die(NULL);
- ret = reset_tree(new_tree,
- opts, 0,
- writeout_error, new_branch_info);
- strbuf_release(&o.obuf);
- strbuf_release(&old_commit_shortname);
- if (ret) {
- rollback_lock_file(&lock_file);
- return ret;
- }
+ rollback_lock_file(&lock_file);
+ return MERGE_WORKING_TREE_UNPACK_FAILED;
}
}
struct object_id rev;
int flag, writeout_error = 0;
int do_merge = 1;
+ int created_autostash = 0;
+ struct strbuf old_commit_shortname = STRBUF_INIT;
+ struct strbuf autostash_msg = STRBUF_INIT;
+ const char *stash_label_base = NULL;
trace2_cmd_mode("branch");
do_merge = 0;
}
+ if (old_branch_info.name) {
+ stash_label_base = 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_base = old_commit_shortname.buf;
+ }
+
if (do_merge) {
- ret = merge_working_tree(opts, &old_branch_info, new_branch_info, &writeout_error);
+ ret = merge_working_tree(opts, &old_branch_info, new_branch_info,
+ &writeout_error, false);
+ if (ret == MERGE_WORKING_TREE_UNPACK_FAILED && opts->merge) {
+ strbuf_addf(&autostash_msg,
+ "autostash while switching to '%s'",
+ new_branch_info->name);
+ create_autostash_ref(the_repository,
+ "CHECKOUT_AUTOSTASH_HEAD",
+ autostash_msg.buf, true);
+ created_autostash = 1;
+ ret = merge_working_tree(opts, &old_branch_info, new_branch_info,
+ &writeout_error, true);
+ }
+ if (created_autostash) {
+ if (opts->conflict_style >= 0) {
+ struct strbuf cfg = STRBUF_INIT;
+ strbuf_addf(&cfg, "merge.conflictStyle=%s",
+ conflict_style_name(opts->conflict_style));
+ git_config_push_parameter(cfg.buf);
+ strbuf_release(&cfg);
+ }
+ apply_autostash_ref(the_repository,
+ "CHECKOUT_AUTOSTASH_HEAD",
+ new_branch_info->name,
+ "local",
+ stash_label_base,
+ autostash_msg.buf);
+ }
if (ret) {
branch_info_release(&old_branch_info);
- return ret;
+ strbuf_release(&old_commit_shortname);
+ strbuf_release(&autostash_msg);
+ return ret < 0 ? 1 : ret;
}
}
update_refs_for_switch(opts, &old_branch_info, new_branch_info);
+ if (created_autostash) {
+ discard_index(the_repository->index);
+ if (repo_read_index(the_repository) < 0)
+ die(_("index file corrupt"));
+
+ if (!opts->quiet && new_branch_info->commit) {
+ printf(_("The following paths have local changes:\n"));
+ show_local_changes(&new_branch_info->commit->object,
+ &opts->diff_options);
+ }
+ }
+
ret = post_checkout_hook(old_branch_info.commit, new_branch_info->commit, 1);
branch_info_release(&old_branch_info);
+ strbuf_release(&old_commit_shortname);
+ strbuf_release(&autostash_msg);
return ret || writeout_error;
}
strvec_push(&store.args, stash_oid);
if (run_command(&store))
ret = error(_("cannot store %s"), stash_oid);
+ else if (attempt_apply)
+ fprintf(stderr,
+ _("Your local changes are stashed, however applying them\n"
+ "resulted in conflicts. You can either resolve the conflicts\n"
+ "and then discard the stash with \"git stash drop\", or, if you\n"
+ "do not want to resolve them now, run \"git reset --hard\" and\n"
+ "apply the local changes later by running \"git stash pop\".\n"));
else
fprintf(stderr,
- _("%s\n"
+ _("Autostash exists; creating a new stash entry.\n"
"Your changes are safe in the stash.\n"
"You can run \"git stash pop\" or"
- " \"git stash drop\" at any time.\n"),
- attempt_apply ?
- _("Applying autostash resulted in conflicts.") :
- _("Autostash exists; creating a new stash entry."));
+ " \"git stash drop\" at any time.\n"));
}
return ret;
First, rewinding head to replay your work on top of it...
Applying: second commit
Applying: third commit
- Applying autostash resulted in conflicts.
- Your changes are safe in the stash.
- You can run "git stash pop" or "git stash drop" at any time.
+ Your local changes are stashed, however applying them
+ resulted in conflicts. You can either resolve the conflicts
+ and then discard the stash with "git stash drop", or, if you
+ do not want to resolve them now, run "git reset --hard" and
+ apply the local changes later by running "git stash pop".
EOF
}
create_expected_failure_merge () {
cat >expected <<-EOF
$(grep "^Created autostash: [0-9a-f][0-9a-f]*\$" actual)
- Applying autostash resulted in conflicts.
- Your changes are safe in the stash.
- You can run "git stash pop" or "git stash drop" at any time.
+ Your local changes are stashed, however applying them
+ resulted in conflicts. You can either resolve the conflicts
+ and then discard the stash with "git stash drop", or, if you
+ do not want to resolve them now, run "git reset --hard" and
+ apply the local changes later by running "git stash pop".
Successfully rebased and updated refs/heads/rebased-feature-branch.
EOF
}
test "$(git symbolic-ref HEAD)" = "refs/heads/side" &&
- printf "M\t%s\n" one >expect.messages &&
+ printf "The following paths have local changes:\nM\t%s\n" one >expect.messages &&
test_cmp expect.messages messages &&
fill "M one" "A three" "D two" >expect.main &&
test_cmp expect two
'
+test_expect_success 'checkout -m with mixed staged and unstaged changes' '
+ git checkout -f main &&
+ git clean -f &&
+
+ fill 0 x y z >same &&
+ git add same &&
+ fill 1 2 3 4 5 6 7 >one &&
+ git checkout -m side >actual 2>&1 &&
+ test_grep "Applied autostash" actual &&
+ fill 0 x y z >expect &&
+ test_cmp expect same &&
+ fill 1 2 3 4 5 6 7 >expect &&
+ test_cmp expect one
+'
+
+test_expect_success 'checkout -m creates a recoverable stash on conflict' '
+ git checkout -f main &&
+ git clean -f &&
+
+ fill 1 2 3 4 5 >one &&
+ test_must_fail git checkout side 2>stderr &&
+ test_grep "Your local changes" stderr &&
+ git checkout -m side >actual 2>&1 &&
+ test_grep "resulted in conflicts" actual &&
+ test_grep "git stash drop" actual &&
+ test_grep "git stash pop" actual &&
+ test_grep "The following paths have local changes" actual &&
+ git show --format=%B --diff-merges=1 refs/stash >actual &&
+ sed /^index/d actual >actual.trimmed &&
+ cat >expect <<-EOF &&
+ On main: autostash while switching to ${SQ}side${SQ}
+
+ diff --git a/one b/one
+ --- a/one
+ +++ b/one
+ @@ -3,6 +3,3 @@
+ 3
+ 4
+ 5
+ -6
+ -7
+ -8
+ EOF
+ test_cmp expect actual.trimmed &&
+ git stash drop &&
+ git reset --hard
+'
+
+test_expect_success 'checkout -m which would overwrite untracked file' '
+ git checkout -f --detach main &&
+ test_commit another-file &&
+ git checkout HEAD^ &&
+ >another-file.t &&
+ fill 1 2 3 4 5 >one &&
+ test_must_fail git checkout -m @{-1} 2>err &&
+ test_grep "would be overwritten by checkout" err &&
+ test_grep "another-file.t" err
+'
+
test_expect_success 'switch to another branch while carrying a deletion' '
git checkout -f main &&
git reset --hard &&
git diff >expect &&
test_when_finished "test_might_fail git stash drop" &&
git merge --autostash c3 2>err &&
- test_grep "Applying autostash resulted in conflicts." err &&
+ test_grep "applying them" err &&
+ test_grep "resulted in conflicts" err &&
git show HEAD:file >merge-result &&
test_cmp result.1-9 merge-result &&
git stash show -p >actual &&
return -1;
}
+const char *conflict_style_name(int style)
+{
+ switch (style) {
+ case XDL_MERGE_DIFF3:
+ return "diff3";
+ case XDL_MERGE_ZEALOUS_DIFF3:
+ return "zdiff3";
+ default:
+ return "merge";
+ }
+}
+
int git_xmerge_style = -1;
int git_xmerge_config(const char *var, const char *value,
void xdiff_clear_find_func(xdemitconf_t *xecfg);
struct config_context;
int parse_conflict_style_name(const char *value);
+const char *conflict_style_name(int style);
int git_xmerge_config(const char *var, const char *value,
const struct config_context *ctx, void *cb);
extern int git_xmerge_style;