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"
struct tree *new_tree;
repo_hold_locked_index(the_repository, &lock_file, LOCK_DIE_ON_ERROR);
- if (repo_read_index_preload(the_repository, NULL, 0) < 0)
+ if (repo_read_index_preload(the_repository, NULL, 0) < 0) {
+ rollback_lock_file(&lock_file);
return error(_("index file corrupt"));
+ }
resolve_undo_clear_index(the_repository->index);
if (opts->new_orphan_branch && opts->orphan_from_empty_tree) {
} else {
new_tree = repo_get_commit_tree(the_repository,
new_branch_info->commit);
- if (!new_tree)
+ if (!new_tree) {
+ rollback_lock_file(&lock_file);
return error(_("unable to read tree (%s)"),
oid_to_hex(&new_branch_info->commit->object.oid));
+ }
}
if (opts->discard_changes) {
ret = reset_tree(new_tree, opts, 1, writeout_error, new_branch_info);
- if (ret)
+ if (ret) {
+ rollback_lock_file(&lock_file);
return ret;
+ }
} else {
struct tree_desc trees[2];
struct tree *tree;
refresh_index(the_repository->index, REFRESH_QUIET, NULL, NULL, NULL);
if (unmerged_index(the_repository->index)) {
+ rollback_lock_file(&lock_file);
error(_("you need to resolve your current index first"));
return 1;
}
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)
- 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)
- 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)
- 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)
- return ret;
+ rollback_lock_file(&lock_file);
+ return 1;
}
}
struct object_id rev;
int flag, writeout_error = 0;
int do_merge = 1;
+ int created_autostash = 0;
+ struct strbuf old_commit_shortname = 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);
+ if (ret && opts->merge) {
+ create_autostash_ref_silent(the_repository,
+ "CHECKOUT_AUTOSTASH_HEAD");
+ created_autostash = 1;
+ ret = merge_working_tree(opts, &old_branch_info, new_branch_info, &writeout_error);
+ }
if (ret) {
+ apply_autostash_ref_with_labels(the_repository,
+ "CHECKOUT_AUTOSTASH_HEAD",
+ new_branch_info->name,
+ "local",
+ stash_label_base);
branch_info_release(&old_branch_info);
+ strbuf_release(&old_commit_shortname);
return ret;
}
}
update_refs_for_switch(opts, &old_branch_info, new_branch_info);
+ 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_with_labels(the_repository, "CHECKOUT_AUTOSTASH_HEAD",
+ new_branch_info->name, "local",
+ stash_label_base);
+
+ discard_index(the_repository->index);
+ if (repo_read_index(the_repository) < 0)
+ die(_("index file corrupt"));
+
+ if (created_autostash && !opts->discard_changes && !opts->quiet &&
+ new_branch_info->commit)
+ 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);
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 it to carry\n"
+ "forward your local changes resulted in conflicts:\n"
+ "\n"
+ " - You can try resolving them now. If you resolved them\n"
+ " successfully, discard the stash entry with \"git stash drop\".\n"
+ "\n"
+ " - Alternatively you can \"git reset --hard\" if you do not want\n"
+ " to deal with them right now, and later \"git stash pop\" to\n"
+ " recover your local changes.\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 it to carry
+ forward your local changes resulted in conflicts:
+
+ - 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.
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 it to carry
+ forward your local changes resulted in conflicts:
+
+ - 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.
Successfully rebased and updated refs/heads/rebased-feature-branch.
EOF
}
test_cmp expect two
'
+test_expect_success 'checkout --merge --conflict=zdiff3 <branch>' '
+ git checkout -f main &&
+ git reset --hard &&
+ git clean -f &&
+
+ fill a b X d e >two &&
+ git checkout --merge --conflict=zdiff3 simple &&
+
+ cat <<-EOF >expect &&
+ a
+ <<<<<<< simple
+ c
+ ||||||| main
+ b
+ c
+ d
+ =======
+ b
+ X
+ d
+ >>>>>>> local
+ e
+ EOF
+ test_cmp expect two
+'
+
+test_expect_success 'checkout -m respects merge.conflictStyle config' '
+ git checkout -f main &&
+ git reset --hard &&
+ git clean -f &&
+
+ test_config merge.conflictStyle diff3 &&
+ fill b d >two &&
+ git checkout -m simple &&
+
+ cat <<-EOF >expect &&
+ <<<<<<< simple
+ a
+ c
+ e
+ ||||||| main
+ a
+ b
+ c
+ d
+ e
+ =======
+ b
+ d
+ >>>>>>> local
+ EOF
+ test_cmp expect two
+'
+
+test_expect_success 'checkout -m skips stash when no conflict' '
+ git checkout -f main &&
+ git clean -f &&
+
+ fill 0 x y z >same &&
+ git stash list >stash-before &&
+ git checkout -m side >actual 2>&1 &&
+ test_grep ! "Created autostash" actual &&
+ git stash list >stash-after &&
+ test_cmp stash-before stash-after &&
+ fill 0 x y z >expect &&
+ test_cmp expect same
+'
+
+test_expect_success 'checkout -m skips stash with non-conflicting dirty index' '
+ git checkout -f main &&
+ git clean -f &&
+
+ fill 0 x y z >same &&
+ git add same &&
+ git checkout -m side >actual 2>&1 &&
+ test_grep ! "Created autostash" actual &&
+ fill 0 x y z >expect &&
+ test_cmp expect same
+'
+
+test_expect_success 'checkout -m stashes and applies on conflicting changes' '
+ git checkout -f main &&
+ git clean -f &&
+
+ fill 1 2 3 4 5 6 7 >one &&
+ git checkout -m side >actual 2>&1 &&
+ test_grep ! "Created autostash" actual &&
+ test_grep "Applied autostash" actual &&
+ fill 1 2 3 4 5 6 7 >expect &&
+ test_cmp expect one
+'
+
+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 ! "Created autostash" actual &&
+ 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 stashes on truly conflicting changes' '
+ 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 ! "Created autostash" actual &&
+ test_grep "resulted in conflicts" actual &&
+ test_grep "git stash drop" actual &&
+ git stash drop &&
+ git reset --hard
+'
+
+test_expect_success 'checkout -m produces usable stash on conflict' '
+ git checkout -f main &&
+ git clean -f &&
+
+ fill 1 2 3 4 5 >one &&
+ git checkout -m side >actual 2>&1 &&
+ test_grep "recover your local changes" actual &&
+ git checkout -f main &&
+ git stash pop &&
+ fill 1 2 3 4 5 >expect &&
+ test_cmp expect one
+'
+
+test_expect_success 'checkout -m stashes on staged conflicting changes' '
+ git checkout -f main &&
+ git clean -f &&
+
+ fill 1 2 3 4 5 >one &&
+ git add one &&
+ git checkout -m side >actual 2>&1 &&
+ test_grep ! "Created autostash" actual &&
+ test_grep "resulted in conflicts" actual &&
+ test_grep "git stash drop" actual &&
+ git stash drop &&
+ git reset --hard
+'
+
+test_expect_success 'checkout -m applies stash cleanly with non-overlapping changes in same file' '
+ git checkout -f main &&
+ git reset --hard &&
+ git clean -f &&
+
+ git checkout -b nonoverlap_base &&
+ fill a b c d >file &&
+ git add file &&
+ git commit -m "add file" &&
+
+ git checkout -b nonoverlap_child &&
+ fill a b c INSERTED d >file &&
+ git commit -a -m "insert line near end of file" &&
+
+ fill DIRTY a b c INSERTED d >file &&
+
+ git stash list >stash-before &&
+ git checkout -m nonoverlap_base 2>stderr &&
+ test_grep "Applied autostash" stderr &&
+ test_grep ! "resulted in conflicts" stderr &&
+
+ git stash list >stash-after &&
+ test_cmp stash-before stash-after &&
+
+ fill DIRTY a b c d >expect &&
+ test_cmp expect file &&
+
+ git checkout -f main &&
+ git branch -D nonoverlap_base &&
+ git branch -D nonoverlap_child
+'
+
+test_expect_success 'checkout -m -b skips stash with dirty tree' '
+ git checkout -f main &&
+ git clean -f &&
+
+ fill 0 x y z >same &&
+ git checkout -m -b newbranch >actual 2>&1 &&
+ test_grep ! "Created autostash" actual &&
+ fill 0 x y z >expect &&
+ test_cmp expect same &&
+ git checkout main &&
+ git branch -D newbranch
+'
+
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 "your local changes 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;