Merge branch 'nd/merge-quit'
authorJunio C Hamano <gitster@pobox.com>
Thu, 13 Jun 2019 20:19:41 +0000 (13:19 -0700)
committerJunio C Hamano <gitster@pobox.com>
Thu, 13 Jun 2019 20:19:41 +0000 (13:19 -0700)
"git merge" learned "--quit" option that cleans up the in-progress
merge while leaving the working tree and the index still in a mess.

* nd/merge-quit:
merge: add --quit
merge: remove drop_save() in favor of remove_merge_branch_state()

1  2 
Documentation/git-merge.txt
branch.c
branch.h
builtin/merge.c
t/t7600-merge.sh
index 6294dbc09d213815fb3a31328de87af52fcf606a,b7d581fc76f8d768838ed5790af19827efe088be..c01cfa659529b1fc69026fea790725a0f91c9731
@@@ -83,8 -83,7 +83,8 @@@ invocations. The automated message can 
  If `--log` is specified, a shortlog of the commits being merged
  will be appended to the specified message.
  
 ---[no-]rerere-autoupdate::
 +--rerere-autoupdate::
 +--no-rerere-autoupdate::
        Allow the rerere mechanism to update the index with the
        result of auto-conflict resolution if possible.
  
@@@ -100,6 -99,10 +100,10 @@@ commit or stash your changes before run
  'git merge --abort' is equivalent to 'git reset --merge' when
  `MERGE_HEAD` is present.
  
+ --quit::
+       Forget about the current merge in progress. Leave the index
+       and the working tree as-is.
  --continue::
        After a 'git merge' stops due to conflicts you can conclude the
        merge by running 'git merge --continue' (see "HOW TO RESOLVE
diff --combined branch.c
index a594cc23e25458250885244f477a5d4879df2537,1db0601a1124be0dcb83c4805e37baa55bfff5b5..e70838fb872f98820a641ccaccc06fd4c0c381f7
+++ b/branch.c
@@@ -5,7 -5,6 +5,7 @@@
  #include "refs.h"
  #include "refspec.h"
  #include "remote.h"
 +#include "sequencer.h"
  #include "commit.h"
  #include "worktree.h"
  
@@@ -269,7 -268,7 +269,7 @@@ void create_branch(struct repository *r
        }
  
        real_ref = NULL;
 -      if (get_oid(start_name, &oid)) {
 +      if (get_oid_mb(start_name, &oid)) {
                if (explicit_tracking) {
                        if (advice_set_upstream_failure) {
                                error(_(upstream_missing), start_name);
        free(real_ref);
  }
  
- void remove_branch_state(struct repository *r)
+ void remove_merge_branch_state(struct repository *r)
  {
-       sequencer_post_commit_cleanup(r);
        unlink(git_path_merge_head(r));
        unlink(git_path_merge_rr(r));
        unlink(git_path_merge_msg(r));
        unlink(git_path_merge_mode(r));
 -      unlink(git_path_cherry_pick_head(r));
 -      unlink(git_path_revert_head(r));
+ }
+ void remove_branch_state(struct repository *r)
+ {
++      sequencer_post_commit_cleanup(r);
        unlink(git_path_squash_msg(r));
+       remove_merge_branch_state(r);
  }
  
  void die_if_checked_out(const char *branch, int ignore_current_worktree)
diff --combined branch.h
index 6f38db14e9c496c55e204791e5ab0a5243186118,c90ba9d7bfa2db0588092200f98c1e0c2cc1b80f..064ee576f29764637dfdc62141973e935f8fd488
+++ b/branch.h
@@@ -50,7 -50,7 +50,7 @@@ void create_branch(struct repository *r
   * Return 1 if the named branch already exists; return 0 otherwise.
   * Fill ref with the full refname for the branch.
   */
 -extern int validate_branchname(const char *name, struct strbuf *ref);
 +int validate_branchname(const char *name, struct strbuf *ref);
  
  /*
   * Check if a branch 'name' can be created as a new branch; die otherwise.
   * Return 1 if the named branch already exists; return 0 otherwise.
   * Fill ref with the full refname for the branch.
   */
 -extern int validate_new_branchname(const char *name, struct strbuf *ref, int force);
 +int validate_new_branchname(const char *name, struct strbuf *ref, int force);
  
+ /*
+  * Remove information about the merge state on the current
+  * branch. (E.g., MERGE_HEAD)
+  */
+ void remove_merge_branch_state(struct repository *r);
  /*
   * Remove information about the state of working on the current
   * branch. (E.g., MERGE_HEAD)
@@@ -72,26 -78,26 +78,26 @@@ void remove_branch_state(struct reposit
   * Returns 0 on success.
   */
  #define BRANCH_CONFIG_VERBOSE 01
 -extern int install_branch_config(int flag, const char *local, const char *origin, const char *remote);
 +int install_branch_config(int flag, const char *local, const char *origin, const char *remote);
  
  /*
   * Read branch description
   */
 -extern int read_branch_desc(struct strbuf *, const char *branch_name);
 +int read_branch_desc(struct strbuf *, const char *branch_name);
  
  /*
   * Check if a branch is checked out in the main worktree or any linked
   * worktree and die (with a message describing its checkout location) if
   * it is.
   */
 -extern void die_if_checked_out(const char *branch, int ignore_current_worktree);
 +void die_if_checked_out(const char *branch, int ignore_current_worktree);
  
  /*
   * Update all per-worktree HEADs pointing at the old ref to point the new ref.
   * This will be used when renaming a branch. Returns 0 if successful, non-zero
   * otherwise.
   */
 -extern int replace_each_worktree_head_symref(const char *oldref, const char *newref,
 -                                           const char *logmsg);
 +int replace_each_worktree_head_symref(const char *oldref, const char *newref,
 +                                    const char *logmsg);
  
  #endif
diff --combined builtin/merge.c
index e96f72af8044769df6b4ef42f0525b572b0c39e2,598d56edfeb30a1a58ea0cf8b9dbdd0e4e2ba166..5c83f89cc639c43754a1091be80c32a486c9a291
@@@ -37,8 -37,8 +37,9 @@@
  #include "packfile.h"
  #include "tag.h"
  #include "alias.h"
+ #include "branch.h"
  #include "commit-reach.h"
 +#include "wt-status.h"
  
  #define DEFAULT_TWOHEAD (1<<0)
  #define DEFAULT_OCTOPUS (1<<1)
@@@ -73,6 -73,7 +74,7 @@@ static int option_renormalize
  static int verbosity;
  static int allow_rerere_auto;
  static int abort_current_merge;
+ static int quit_current_merge;
  static int continue_current_merge;
  static int allow_unrelated_histories;
  static int show_progress = -1;
@@@ -99,9 -100,6 +101,9 @@@ enum ff_type 
  
  static enum ff_type fast_forward = FF_ALLOW;
  
 +static const char *cleanup_arg;
 +static enum commit_msg_cleanup_mode cleanup_mode;
 +
  static int option_parse_message(const struct option *opt,
                                const char *arg, int unset)
  {
        return 0;
  }
  
 -static int option_read_message(struct parse_opt_ctx_t *ctx,
 -                             const struct option *opt, int unset)
 +static enum parse_opt_result option_read_message(struct parse_opt_ctx_t *ctx,
 +                                               const struct option *opt,
 +                                               const char *arg_not_used,
 +                                               int unset)
  {
        struct strbuf *buf = opt->value;
        const char *arg;
  
 +      BUG_ON_OPT_ARG(arg_not_used);
        if (unset)
                BUG("-F cannot be negated");
  
@@@ -253,7 -248,6 +255,7 @@@ static struct option builtin_merge_opti
                N_("perform a commit if the merge succeeds (default)")),
        OPT_BOOL('e', "edit", &option_edit,
                N_("edit message before committing")),
 +      OPT_CLEANUP(&cleanup_arg),
        OPT_SET_INT(0, "ff", &fast_forward, N_("allow fast-forward (default)"), FF_ALLOW),
        OPT_SET_INT_F(0, "ff-only", &fast_forward,
                      N_("abort if fast-forward is not possible"),
                option_parse_message),
        { OPTION_LOWLEVEL_CALLBACK, 'F', "file", &merge_msg, N_("path"),
                N_("read message from file"), PARSE_OPT_NONEG,
 -              (parse_opt_cb *) option_read_message },
 +              NULL, 0, option_read_message },
        OPT__VERBOSITY(&verbosity),
        OPT_BOOL(0, "abort", &abort_current_merge,
                N_("abort the current in-progress merge")),
+       OPT_BOOL(0, "quit", &quit_current_merge,
+               N_("--abort but leave index and working tree alone")),
        OPT_BOOL(0, "continue", &continue_current_merge,
                N_("continue the current in-progress merge")),
        OPT_BOOL(0, "allow-unrelated-histories", &allow_unrelated_histories,
        OPT_END()
  };
  
- /* Cleans up metadata that is uninteresting after a succeeded merge. */
- static void drop_save(void)
- {
-       unlink(git_path_merge_head(the_repository));
-       unlink(git_path_merge_msg(the_repository));
-       unlink(git_path_merge_mode(the_repository));
- }
  static int save_state(struct object_id *stash)
  {
        int len;
@@@ -388,7 -376,7 +384,7 @@@ static void finish_up_to_date(const cha
  {
        if (verbosity >= 0)
                printf("%s%s\n", squash ? _(" (nothing to squash)") : "", msg);
-       drop_save();
+       remove_merge_branch_state(the_repository);
  }
  
  static void squash_message(struct commit *commit, struct commit_list *remoteheads)
@@@ -617,8 -605,6 +613,8 @@@ static int git_merge_config(const char 
                return git_config_string(&pull_twohead, k, v);
        else if (!strcmp(k, "pull.octopus"))
                return git_config_string(&pull_octopus, k, v);
 +      else if (!strcmp(k, "commit.cleanup"))
 +              return git_config_string(&cleanup_arg, k, v);
        else if (!strcmp(k, "merge.renormalize"))
                option_renormalize = git_config_bool(k, v);
        else if (!strcmp(k, "merge.ff")) {
@@@ -807,13 -793,8 +803,13 @@@ static void abort_commit(struct commit_
  static const char merge_editor_comment[] =
  N_("Please enter a commit message to explain why this merge is necessary,\n"
     "especially if it merges an updated upstream into a topic branch.\n"
 -   "\n"
 -   "Lines starting with '%c' will be ignored, and an empty message aborts\n"
 +   "\n");
 +
 +static const char scissors_editor_comment[] =
 +N_("An empty message aborts the commit.\n");
 +
 +static const char no_scissors_editor_comment[] =
 +N_("Lines starting with '%c' will be ignored, and an empty message aborts\n"
     "the commit.\n");
  
  static void write_merge_heads(struct commit_list *);
@@@ -821,19 -802,11 +817,19 @@@ static void prepare_to_commit(struct co
  {
        struct strbuf msg = STRBUF_INIT;
        strbuf_addbuf(&msg, &merge_msg);
 -      strbuf_addch(&msg, '\n');
        if (squash)
                BUG("the control must not reach here under --squash");
 -      if (0 < option_edit)
 -              strbuf_commented_addf(&msg, _(merge_editor_comment), comment_line_char);
 +      if (0 < option_edit) {
 +              strbuf_addch(&msg, '\n');
 +              if (cleanup_mode == COMMIT_MSG_CLEANUP_SCISSORS) {
 +                      wt_status_append_cut_line(&msg);
 +                      strbuf_commented_addf(&msg, "\n");
 +              }
 +              strbuf_commented_addf(&msg, _(merge_editor_comment));
 +              strbuf_commented_addf(&msg, _(cleanup_mode == COMMIT_MSG_CLEANUP_SCISSORS ?
 +                      scissors_editor_comment :
 +                      no_scissors_editor_comment), comment_line_char);
 +      }
        if (signoff)
                append_signoff(&msg, ignore_non_trailer(msg.buf, msg.len), 0);
        write_merge_heads(remoteheads);
                abort_commit(remoteheads, NULL);
  
        read_merge_msg(&msg);
 -      strbuf_stripspace(&msg, 0 < option_edit);
 +      cleanup_message(&msg, cleanup_mode, 0);
        if (!msg.len)
                abort_commit(remoteheads, _("Empty commit message."));
        strbuf_release(&merge_msg);
@@@ -881,7 -854,7 +877,7 @@@ static int merge_trivial(struct commit 
                        &result_commit, NULL, sign_commit))
                die(_("failed to write commit object"));
        finish(head, remoteheads, &result_commit, "In-index merge");
-       drop_save();
+       remove_merge_branch_state(the_repository);
        return 0;
  }
  
@@@ -900,6 -873,7 +896,6 @@@ static int finish_automerge(struct comm
        parents = remoteheads;
        if (!head_subsumed || fast_forward == FF_NO)
                commit_list_insert(head, &parents);
 -      strbuf_addch(&merge_msg, '\n');
        prepare_to_commit(remoteheads);
        if (commit_tree(merge_msg.buf, merge_msg.len, result_tree, parents,
                        &result_commit, NULL, sign_commit))
        strbuf_addf(&buf, "Merge made by the '%s' strategy.", wt_strategy);
        finish(head, remoteheads, &result_commit, buf.buf);
        strbuf_release(&buf);
-       drop_save();
+       remove_merge_branch_state(the_repository);
        return 0;
  }
  
@@@ -920,15 -894,7 +916,15 @@@ static int suggest_conflicts(void
        filename = git_path_merge_msg(the_repository);
        fp = xfopen(filename, "a");
  
 -      append_conflicts_hint(&the_index, &msgbuf);
 +      /*
 +       * We can't use cleanup_mode because if we're not using the editor,
 +       * get_cleanup_mode will return COMMIT_MSG_CLEANUP_SPACE instead, even
 +       * though the message is meant to be processed later by git-commit.
 +       * Thus, we will get the cleanup mode which is returned when we _are_
 +       * using an editor.
 +       */
 +      append_conflicts_hint(&the_index, &msgbuf,
 +                            get_cleanup_mode(cleanup_arg, 1));
        fputs(msgbuf.buf, fp);
        strbuf_release(&msgbuf);
        fclose(fp);
@@@ -1289,6 -1255,16 +1285,16 @@@ int cmd_merge(int argc, const char **ar
                goto done;
        }
  
+       if (quit_current_merge) {
+               if (orig_argc != 2)
+                       usage_msg_opt(_("--quit expects no arguments"),
+                                     builtin_merge_usage,
+                                     builtin_merge_options);
+               remove_merge_branch_state(the_repository);
+               goto done;
+       }
        if (continue_current_merge) {
                int nargc = 1;
                const char *nargv[] = {"commit", NULL};
        }
        resolve_undo_clear();
  
 +      if (option_edit < 0)
 +              option_edit = default_edit_option();
 +
 +      cleanup_mode = get_cleanup_mode(cleanup_arg, 0 < option_edit);
 +
        if (verbosity < 0)
                show_diffstat = 0;
  
                        fast_forward = FF_NO;
        }
  
 -      if (option_edit < 0)
 -              option_edit = default_edit_option();
 -
        if (!use_strategies) {
                if (!remoteheads)
                        ; /* already up-to-date */
                }
  
                finish(head_commit, remoteheads, &commit->object.oid, msg.buf);
-               drop_save();
+               remove_merge_branch_state(the_repository);
                goto done;
        } else if (!remoteheads->next && common->next)
                ;
diff --combined t/t7600-merge.sh
index 7f9c68cbe75a688357e6c71486c3897861421681,625a24a980c89a5fdda22c20860aa4675d7562c0..3e16aaed3b775b6e02f7cd6708f07746841271eb
@@@ -233,65 -233,20 +233,65 @@@ test_expect_success 'merge --squash c3 
        cat result.9z >file &&
        git commit --no-edit -a &&
  
 -      {
 -              cat <<-EOF
 -              Squashed commit of the following:
 +      cat >expect <<-EOF &&
 +      Squashed commit of the following:
  
 -              $(git show -s c7)
 +      $(git show -s c7)
  
 -              # Conflicts:
 -              #       file
 -              EOF
 -      } >expect &&
 -      git cat-file commit HEAD | sed -e '1,/^$/d' >actual &&
 +      # Conflicts:
 +      #       file
 +      EOF
 +      git cat-file commit HEAD >raw &&
 +      sed -e '1,/^$/d' raw >actual &&
        test_cmp expect actual
  '
  
 +test_expect_success 'merge c3 with c7 with commit.cleanup = scissors' '
 +      git config commit.cleanup scissors &&
 +      git reset --hard c3 &&
 +      test_must_fail git merge c7 &&
 +      cat result.9z >file &&
 +      git commit --no-edit -a &&
 +
 +      cat >expect <<-\EOF &&
 +      Merge tag '"'"'c7'"'"'
 +
 +      # ------------------------ >8 ------------------------
 +      # Do not modify or remove the line above.
 +      # Everything below it will be ignored.
 +      #
 +      # Conflicts:
 +      #       file
 +      EOF
 +      git cat-file commit HEAD >raw &&
 +      sed -e '1,/^$/d' raw >actual &&
 +      test_i18ncmp expect actual
 +'
 +
 +test_expect_success 'merge c3 with c7 with --squash commit.cleanup = scissors' '
 +      git config commit.cleanup scissors &&
 +      git reset --hard c3 &&
 +      test_must_fail git merge --squash c7 &&
 +      cat result.9z >file &&
 +      git commit --no-edit -a &&
 +
 +      cat >expect <<-EOF &&
 +      Squashed commit of the following:
 +
 +      $(git show -s c7)
 +
 +      # ------------------------ >8 ------------------------
 +      # Do not modify or remove the line above.
 +      # Everything below it will be ignored.
 +      #
 +      # Conflicts:
 +      #       file
 +      EOF
 +      git cat-file commit HEAD >raw &&
 +      sed -e '1,/^$/d' raw >actual &&
 +      test_i18ncmp expect actual
 +'
 +
  test_debug 'git log --graph --decorate --oneline --all'
  
  test_expect_success 'merge c1 with c2 and c3' '
@@@ -725,10 -680,10 +725,10 @@@ cat >editor <<\EO
  (
        echo "Merge work done on the side branch c1"
        echo
 -      cat <"$1"
 +      cat "$1"
  ) >"$1.tmp" && mv "$1.tmp" "$1"
  # strip comments and blank lines from end of message
 -sed -e '/^#/d' < "$1" | sed -e :a -e '/^\n*$/{$d;N;ba' -e '}' > expected
 +sed -e '/^#/d' "$1" | sed -e :a -e '/^\n*$/{$d;N;ba' -e '}' >expected
  EOF
  chmod 755 editor
  
@@@ -813,14 -768,14 +813,14 @@@ test_expect_success 'set up mod-256 con
        git commit -m base &&
  
        # one side changes the first line of each to "master"
 -      sed s/-1/-master/ <file >tmp &&
 +      sed s/-1/-master/ file >tmp &&
        mv tmp file &&
        git commit -am master &&
  
        # and the other to "side"; merging the two will
        # yield 256 separate conflicts
        git checkout -b side HEAD^ &&
 -      sed s/-1/-side/ <file >tmp &&
 +      sed s/-1/-side/ file >tmp &&
        mv tmp file &&
        git commit -am side
  '
@@@ -859,7 -814,7 +859,7 @@@ EO
  test_expect_success EXECKEEPSPID 'killed merge can be completed with --continue' '
        git reset --hard c0 &&
        ! "$SHELL_PATH" -c '\''
 -        echo kill -TERM $$ >> .git/FAKE_EDITOR
 +        echo kill -TERM $$ >>.git/FAKE_EDITOR
          GIT_EDITOR=.git/FAKE_EDITOR
          export GIT_EDITOR
          exec git merge --no-ff --edit c1'\'' &&
        verify_parents $c0 $c1
  '
  
+ test_expect_success 'merge --quit' '
+       git init merge-quit &&
+       (
+               cd merge-quit &&
+               test_commit base &&
+               echo one >>base.t &&
+               git commit -am one &&
+               git branch one &&
+               git checkout base &&
+               echo two >>base.t &&
+               git commit -am two &&
+               test_must_fail git -c rerere.enabled=true merge one &&
+               test_path_is_file .git/MERGE_HEAD &&
+               test_path_is_file .git/MERGE_MODE &&
+               test_path_is_file .git/MERGE_MSG &&
+               git rerere status >rerere.before &&
+               git merge --quit &&
+               test_path_is_missing .git/MERGE_HEAD &&
+               test_path_is_missing .git/MERGE_MODE &&
+               test_path_is_missing .git/MERGE_MSG &&
+               git rerere status >rerere.after &&
+               test_must_be_empty rerere.after &&
+               ! test_cmp rerere.after rerere.before
+       )
+ '
  test_done