Code

Merge branch 'jc/advise-i18n' into maint-1.7.8
[git.git] / builtin / revert.c
index 87df70edc33fcc71177b863e6ced870330e505d0..00df5266d574ea1f8c9ab745ed606bac566392b5 100644 (file)
@@ -40,7 +40,12 @@ static const char * const cherry_pick_usage[] = {
 };
 
 enum replay_action { REVERT, CHERRY_PICK };
-enum replay_subcommand { REPLAY_NONE, REPLAY_RESET, REPLAY_CONTINUE };
+enum replay_subcommand {
+       REPLAY_NONE,
+       REPLAY_REMOVE_STATE,
+       REPLAY_CONTINUE,
+       REPLAY_ROLLBACK
+};
 
 struct replay_opts {
        enum replay_action action;
@@ -55,13 +60,14 @@ struct replay_opts {
        int allow_rerere_auto;
 
        int mainline;
-       int commit_argc;
-       const char **commit_argv;
 
        /* Merge strategy */
        const char *strategy;
        const char **xopts;
        size_t xopts_nr, xopts_alloc;
+
+       /* Only used by REPLAY_NONE */
+       struct rev_info *revs;
 };
 
 #define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
@@ -133,11 +139,13 @@ static void parse_args(int argc, const char **argv, struct replay_opts *opts)
 {
        const char * const * usage_str = revert_or_cherry_pick_usage(opts);
        const char *me = action_name(opts);
-       int reset = 0;
+       int remove_state = 0;
        int contin = 0;
+       int rollback = 0;
        struct option options[] = {
-               OPT_BOOLEAN(0, "reset", &reset, "forget the current operation"),
-               OPT_BOOLEAN(0, "continue", &contin, "continue the current operation"),
+               OPT_BOOLEAN(0, "quit", &remove_state, "end revert or cherry-pick sequence"),
+               OPT_BOOLEAN(0, "continue", &contin, "resume revert or cherry-pick sequence"),
+               OPT_BOOLEAN(0, "abort", &rollback, "cancel revert or cherry-pick sequence"),
                OPT_BOOLEAN('n', "no-commit", &opts->no_commit, "don't automatically commit"),
                OPT_BOOLEAN('e', "edit", &opts->edit, "edit the commit message"),
                OPT_NOOP_NOARG('r', NULL),
@@ -162,31 +170,38 @@ static void parse_args(int argc, const char **argv, struct replay_opts *opts)
                        die(_("program error"));
        }
 
-       opts->commit_argc = parse_options(argc, argv, NULL, options, usage_str,
-                                       PARSE_OPT_KEEP_ARGV0 |
-                                       PARSE_OPT_KEEP_UNKNOWN);
+       argc = parse_options(argc, argv, NULL, options, usage_str,
+                       PARSE_OPT_KEEP_ARGV0 |
+                       PARSE_OPT_KEEP_UNKNOWN);
 
        /* Check for incompatible subcommands */
        verify_opt_mutually_compatible(me,
-                               "--reset", reset,
+                               "--quit", remove_state,
                                "--continue", contin,
+                               "--abort", rollback,
                                NULL);
 
        /* Set the subcommand */
-       if (reset)
-               opts->subcommand = REPLAY_RESET;
+       if (remove_state)
+               opts->subcommand = REPLAY_REMOVE_STATE;
        else if (contin)
                opts->subcommand = REPLAY_CONTINUE;
+       else if (rollback)
+               opts->subcommand = REPLAY_ROLLBACK;
        else
                opts->subcommand = REPLAY_NONE;
 
        /* Check for incompatible command line arguments */
        if (opts->subcommand != REPLAY_NONE) {
                char *this_operation;
-               if (opts->subcommand == REPLAY_RESET)
-                       this_operation = "--reset";
-               else
+               if (opts->subcommand == REPLAY_REMOVE_STATE)
+                       this_operation = "--quit";
+               else if (opts->subcommand == REPLAY_CONTINUE)
                        this_operation = "--continue";
+               else {
+                       assert(opts->subcommand == REPLAY_ROLLBACK);
+                       this_operation = "--abort";
+               }
 
                verify_opt_compatible(me, this_operation,
                                "--no-commit", opts->no_commit,
@@ -199,9 +214,6 @@ static void parse_args(int argc, const char **argv, struct replay_opts *opts)
                                NULL);
        }
 
-       else if (opts->commit_argc < 2)
-               usage_with_options(usage_str, options);
-
        if (opts->allow_ff)
                verify_opt_compatible(me, "--ff",
                                "--signoff", opts->signoff,
@@ -209,7 +221,20 @@ static void parse_args(int argc, const char **argv, struct replay_opts *opts)
                                "-x", opts->record_origin,
                                "--edit", opts->edit,
                                NULL);
-       opts->commit_argv = argv;
+
+       if (opts->subcommand != REPLAY_NONE) {
+               opts->revs = NULL;
+       } else {
+               opts->revs = xmalloc(sizeof(*opts->revs));
+               init_revisions(opts->revs, NULL);
+               opts->revs->no_walk = 1;
+               if (argc < 2)
+                       usage_with_options(usage_str, options);
+               argc = setup_revisions(argc, argv, opts->revs, NULL);
+       }
+
+       if (argc > 1)
+               usage_with_options(usage_str, options);
 }
 
 struct commit_message {
@@ -286,19 +311,20 @@ static char *get_encoding(const char *message)
        return NULL;
 }
 
-static void write_cherry_pick_head(struct commit *commit)
+static void write_cherry_pick_head(struct commit *commit, const char *pseudoref)
 {
+       const char *filename;
        int fd;
        struct strbuf buf = STRBUF_INIT;
 
        strbuf_addf(&buf, "%s\n", sha1_to_hex(commit->object.sha1));
 
-       fd = open(git_path("CHERRY_PICK_HEAD"), O_WRONLY | O_CREAT, 0666);
+       filename = git_path("%s", pseudoref);
+       fd = open(filename, O_WRONLY | O_CREAT, 0666);
        if (fd < 0)
-               die_errno(_("Could not open '%s' for writing"),
-                         git_path("CHERRY_PICK_HEAD"));
+               die_errno(_("Could not open '%s' for writing"), filename);
        if (write_in_full(fd, buf.buf, buf.len) != buf.len || close(fd))
-               die_errno(_("Could not write to '%s'"), git_path("CHERRY_PICK_HEAD"));
+               die_errno(_("Could not write to '%s'"), filename);
        strbuf_release(&buf);
 }
 
@@ -317,11 +343,10 @@ static void print_advice(int show_hint)
                return;
        }
 
-       if (show_hint) {
-               advise("after resolving the conflicts, mark the corrected paths");
-               advise("with 'git add <paths>' or 'git rm <paths>'");
-               advise("and commit the result with 'git commit'");
-       }
+       if (show_hint)
+               advise(_("after resolving the conflicts, mark the corrected paths\n"
+                        "with 'git add <paths>' or 'git rm <paths>'\n"
+                        "and commit the result with 'git commit'"));
 }
 
 static void write_message(struct strbuf *msgbuf, const char *filename)
@@ -331,7 +356,7 @@ static void write_message(struct strbuf *msgbuf, const char *filename)
        int msg_fd = hold_lock_file_for_update(&msg_file, filename,
                                               LOCK_DIE_ON_ERROR);
        if (write_in_full(msg_fd, msgbuf->buf, msgbuf->len) < 0)
-               die_errno(_("Could not write to %s."), filename);
+               die_errno(_("Could not write to %s"), filename);
        strbuf_release(msgbuf);
        if (commit_lock_file(&msg_file) < 0)
                die(_("Error wrapping up %s"), filename);
@@ -593,7 +618,9 @@ static int do_pick_commit(struct commit *commit, struct replay_opts *opts)
         * write it at all.
         */
        if (opts->action == CHERRY_PICK && !opts->no_commit && (res == 0 || res == 1))
-               write_cherry_pick_head(commit);
+               write_cherry_pick_head(commit, "CHERRY_PICK_HEAD");
+       if (opts->action == REVERT && ((opts->no_commit && res == 0) || res == 1))
+               write_cherry_pick_head(commit, "REVERT_HEAD");
 
        if (res) {
                error(opts->action == REVERT
@@ -614,23 +641,15 @@ static int do_pick_commit(struct commit *commit, struct replay_opts *opts)
        return res;
 }
 
-static void prepare_revs(struct rev_info *revs, struct replay_opts *opts)
+static void prepare_revs(struct replay_opts *opts)
 {
-       int argc;
-
-       init_revisions(revs, NULL);
-       revs->no_walk = 1;
        if (opts->action != REVERT)
-               revs->reverse = 1;
+               opts->revs->reverse ^= 1;
 
-       argc = setup_revisions(opts->commit_argc, opts->commit_argv, revs, NULL);
-       if (argc > 1)
-               usage(*revert_or_cherry_pick_usage(opts));
-
-       if (prepare_revision_walk(revs))
+       if (prepare_revision_walk(opts->revs))
                die(_("revision walk setup failed"));
 
-       if (!revs->commits)
+       if (!opts->revs->commits)
                die(_("empty commit set passed"));
 }
 
@@ -767,7 +786,7 @@ static void read_populate_todo(struct commit_list **todo_list,
 
        fd = open(todo_file, O_RDONLY);
        if (fd < 0)
-               die_errno(_("Could not open %s."), todo_file);
+               die_errno(_("Could not open %s"), todo_file);
        if (strbuf_read(&buf, fd, 0) < 0) {
                close(fd);
                strbuf_release(&buf);
@@ -827,14 +846,13 @@ static void read_populate_opts(struct replay_opts **opts_ptr)
 static void walk_revs_populate_todo(struct commit_list **todo_list,
                                struct replay_opts *opts)
 {
-       struct rev_info revs;
        struct commit *commit;
        struct commit_list **next;
 
-       prepare_revs(&revs, opts);
+       prepare_revs(opts);
 
        next = todo_list;
-       while ((commit = get_revision(&revs)))
+       while ((commit = get_revision(opts->revs)))
                next = commit_list_append(commit, next);
 }
 
@@ -842,10 +860,13 @@ static int create_seq_dir(void)
 {
        const char *seq_dir = git_path(SEQ_DIR);
 
-       if (file_exists(seq_dir))
-               return error(_("%s already exists."), seq_dir);
+       if (file_exists(seq_dir)) {
+               error(_("a cherry-pick or revert is already in progress"));
+               advise(_("try \"git cherry-pick (--continue | --quit | --abort)\""));
+               return -1;
+       }
        else if (mkdir(seq_dir, 0777) < 0)
-               die_errno(_("Could not create sequencer directory '%s'."), seq_dir);
+               die_errno(_("Could not create sequencer directory %s"), seq_dir);
        return 0;
 }
 
@@ -859,11 +880,77 @@ static void save_head(const char *head)
        fd = hold_lock_file_for_update(&head_lock, head_file, LOCK_DIE_ON_ERROR);
        strbuf_addf(&buf, "%s\n", head);
        if (write_in_full(fd, buf.buf, buf.len) < 0)
-               die_errno(_("Could not write to %s."), head_file);
+               die_errno(_("Could not write to %s"), head_file);
        if (commit_lock_file(&head_lock) < 0)
                die(_("Error wrapping up %s."), head_file);
 }
 
+static int reset_for_rollback(const unsigned char *sha1)
+{
+       const char *argv[4];    /* reset --merge <arg> + NULL */
+       argv[0] = "reset";
+       argv[1] = "--merge";
+       argv[2] = sha1_to_hex(sha1);
+       argv[3] = NULL;
+       return run_command_v_opt(argv, RUN_GIT_CMD);
+}
+
+static int rollback_single_pick(void)
+{
+       unsigned char head_sha1[20];
+
+       if (!file_exists(git_path("CHERRY_PICK_HEAD")) &&
+           !file_exists(git_path("REVERT_HEAD")))
+               return error(_("no cherry-pick or revert in progress"));
+       if (!resolve_ref("HEAD", head_sha1, 0, NULL))
+               return error(_("cannot resolve HEAD"));
+       if (is_null_sha1(head_sha1))
+               return error(_("cannot abort from a branch yet to be born"));
+       return reset_for_rollback(head_sha1);
+}
+
+static int sequencer_rollback(struct replay_opts *opts)
+{
+       const char *filename;
+       FILE *f;
+       unsigned char sha1[20];
+       struct strbuf buf = STRBUF_INIT;
+
+       filename = git_path(SEQ_HEAD_FILE);
+       f = fopen(filename, "r");
+       if (!f && errno == ENOENT) {
+               /*
+                * There is no multiple-cherry-pick in progress.
+                * If CHERRY_PICK_HEAD or REVERT_HEAD indicates
+                * a single-cherry-pick in progress, abort that.
+                */
+               return rollback_single_pick();
+       }
+       if (!f)
+               return error(_("cannot open %s: %s"), filename,
+                                               strerror(errno));
+       if (strbuf_getline(&buf, f, '\n')) {
+               error(_("cannot read %s: %s"), filename, ferror(f) ?
+                       strerror(errno) : _("unexpected end of file"));
+               fclose(f);
+               goto fail;
+       }
+       fclose(f);
+       if (get_sha1_hex(buf.buf, sha1) || buf.buf[40] != '\0') {
+               error(_("stored pre-cherry-pick HEAD file '%s' is corrupt"),
+                       filename);
+               goto fail;
+       }
+       if (reset_for_rollback(sha1))
+               goto fail;
+       remove_sequencer_state();
+       strbuf_release(&buf);
+       return 0;
+fail:
+       strbuf_release(&buf);
+       return -1;
+}
+
 static void save_todo(struct commit_list *todo_list, struct replay_opts *opts)
 {
        const char *todo_file = git_path(SEQ_TODO_FILE);
@@ -876,7 +963,7 @@ static void save_todo(struct commit_list *todo_list, struct replay_opts *opts)
                die(_("Could not format %s."), todo_file);
        if (write_in_full(fd, buf.buf, buf.len) < 0) {
                strbuf_release(&buf);
-               die_errno(_("Could not write to %s."), todo_file);
+               die_errno(_("Could not write to %s"), todo_file);
        }
        if (commit_lock_file(&todo_lock) < 0) {
                strbuf_release(&buf);
@@ -930,33 +1017,64 @@ static int pick_commits(struct commit_list *todo_list, struct replay_opts *opts)
        for (cur = todo_list; cur; cur = cur->next) {
                save_todo(cur, opts);
                res = do_pick_commit(cur->item, opts);
-               if (res) {
-                       if (!cur->next)
-                               /*
-                                * An error was encountered while
-                                * picking the last commit; the
-                                * sequencer state is useless now --
-                                * the user simply needs to resolve
-                                * the conflict and commit
-                                */
-                               remove_sequencer_state(0);
+               if (res)
                        return res;
-               }
        }
 
        /*
         * Sequence of picks finished successfully; cleanup by
         * removing the .git/sequencer directory
         */
-       remove_sequencer_state(1);
+       remove_sequencer_state();
        return 0;
 }
 
+static int continue_single_pick(void)
+{
+       const char *argv[] = { "commit", NULL };
+
+       if (!file_exists(git_path("CHERRY_PICK_HEAD")) &&
+           !file_exists(git_path("REVERT_HEAD")))
+               return error(_("no cherry-pick or revert in progress"));
+       return run_command_v_opt(argv, RUN_GIT_CMD);
+}
+
+static int sequencer_continue(struct replay_opts *opts)
+{
+       struct commit_list *todo_list = NULL;
+
+       if (!file_exists(git_path(SEQ_TODO_FILE)))
+               return continue_single_pick();
+       read_populate_opts(&opts);
+       read_populate_todo(&todo_list, opts);
+
+       /* Verify that the conflict has been resolved */
+       if (file_exists(git_path("CHERRY_PICK_HEAD")) ||
+           file_exists(git_path("REVERT_HEAD"))) {
+               int ret = continue_single_pick();
+               if (ret)
+                       return ret;
+       }
+       if (index_differs_from("HEAD", 0))
+               return error_dirty_index(opts);
+       todo_list = todo_list->next;
+       return pick_commits(todo_list, opts);
+}
+
+static int single_pick(struct commit *cmit, struct replay_opts *opts)
+{
+       setenv(GIT_REFLOG_ACTION, action_name(opts), 0);
+       return do_pick_commit(cmit, opts);
+}
+
 static int pick_revisions(struct replay_opts *opts)
 {
        struct commit_list *todo_list = NULL;
        unsigned char sha1[20];
 
+       if (opts->subcommand == REPLAY_NONE)
+               assert(opts->revs);
+
        read_and_refresh_cache(opts);
 
        /*
@@ -964,43 +1082,52 @@ static int pick_revisions(struct replay_opts *opts)
         * cherry-pick should be handled differently from an existing
         * one that is being continued
         */
-       if (opts->subcommand == REPLAY_RESET) {
-               remove_sequencer_state(1);
+       if (opts->subcommand == REPLAY_REMOVE_STATE) {
+               remove_sequencer_state();
                return 0;
-       } else if (opts->subcommand == REPLAY_CONTINUE) {
-               if (!file_exists(git_path(SEQ_TODO_FILE)))
-                       goto error;
-               read_populate_opts(&opts);
-               read_populate_todo(&todo_list, opts);
-
-               /* Verify that the conflict has been resolved */
-               if (!index_differs_from("HEAD", 0))
-                       todo_list = todo_list->next;
-       } else {
-               /*
-                * Start a new cherry-pick/ revert sequence; but
-                * first, make sure that an existing one isn't in
-                * progress
-                */
+       }
+       if (opts->subcommand == REPLAY_ROLLBACK)
+               return sequencer_rollback(opts);
+       if (opts->subcommand == REPLAY_CONTINUE)
+               return sequencer_continue(opts);
 
-               walk_revs_populate_todo(&todo_list, opts);
-               if (create_seq_dir() < 0) {
-                       error(_("A cherry-pick or revert is in progress."));
-                       advise(_("Use --continue to continue the operation"));
-                       advise(_("or --reset to forget about it"));
-                       return -1;
-               }
-               if (get_sha1("HEAD", sha1)) {
-                       if (opts->action == REVERT)
-                               return error(_("Can't revert as initial commit"));
-                       return error(_("Can't cherry-pick into empty head"));
-               }
-               save_head(sha1_to_hex(sha1));
-               save_opts(opts);
+       /*
+        * If we were called as "git cherry-pick <commit>", just
+        * cherry-pick/revert it, set CHERRY_PICK_HEAD /
+        * REVERT_HEAD, and don't touch the sequencer state.
+        * This means it is possible to cherry-pick in the middle
+        * of a cherry-pick sequence.
+        */
+       if (opts->revs->cmdline.nr == 1 &&
+           opts->revs->cmdline.rev->whence == REV_CMD_REV &&
+           opts->revs->no_walk &&
+           !opts->revs->cmdline.rev->flags) {
+               struct commit *cmit;
+               if (prepare_revision_walk(opts->revs))
+                       die(_("revision walk setup failed"));
+               cmit = get_revision(opts->revs);
+               if (!cmit || get_revision(opts->revs))
+                       die("BUG: expected exactly one commit from walk");
+               return single_pick(cmit, opts);
+       }
+
+       /*
+        * Start a new cherry-pick/ revert sequence; but
+        * first, make sure that an existing one isn't in
+        * progress
+        */
+
+       walk_revs_populate_todo(&todo_list, opts);
+       if (create_seq_dir() < 0)
+               return -1;
+       if (get_sha1("HEAD", sha1)) {
+               if (opts->action == REVERT)
+                       return error(_("Can't revert as initial commit"));
+               return error(_("Can't cherry-pick into empty head"));
        }
+       save_head(sha1_to_hex(sha1));
+       save_opts(opts);
        return pick_commits(todo_list, opts);
-error:
-       return error(_("No %s in progress"), action_name(opts));
 }
 
 int cmd_revert(int argc, const char **argv, const char *prefix)