]> git.ipfire.org Git - thirdparty/git.git/blobdiff - builtin/revert.c
revert: Save data for continuing after conflict resolution
[thirdparty/git.git] / builtin / revert.c
index 90fe2efe94874638e7a7998efa9b4e75d6d28943..54df8a23b8f0262974ef824fc72083f0c9da79ac 100644 (file)
@@ -13,6 +13,7 @@
 #include "rerere.h"
 #include "merge-recursive.h"
 #include "refs.h"
+#include "dir.h"
 
 /*
  * This implements the builtins revert and cherry-pick.
@@ -60,6 +61,10 @@ struct replay_opts {
 
 #define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
 
+#define SEQ_DIR         "sequencer"
+#define SEQ_HEAD_FILE   "sequencer/head"
+#define SEQ_TODO_FILE   "sequencer/todo"
+
 static const char *action_name(const struct replay_opts *opts)
 {
        return opts->action == REVERT ? "revert" : "cherry-pick";
@@ -86,9 +91,26 @@ static int option_parse_x(const struct option *opt,
        return 0;
 }
 
+static void verify_opt_compatible(const char *me, const char *base_opt, ...)
+{
+       const char *this_opt;
+       va_list ap;
+
+       va_start(ap, base_opt);
+       while ((this_opt = va_arg(ap, const char *))) {
+               if (va_arg(ap, int))
+                       break;
+       }
+       va_end(ap);
+
+       if (this_opt)
+               die(_("%s: %s cannot be used with %s"), me, this_opt, base_opt);
+}
+
 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 noop;
        struct option options[] = {
                OPT_BOOLEAN('n', "no-commit", &opts->no_commit, "don't automatically commit"),
@@ -122,6 +144,13 @@ static void parse_args(int argc, const char **argv, struct replay_opts *opts)
        if (opts->commit_argc < 2)
                usage_with_options(usage_str, options);
 
+       if (opts->allow_ff)
+               verify_opt_compatible(me, "--ff",
+                               "--signoff", opts->signoff,
+                               "--no-commit", opts->no_commit,
+                               "-x", opts->record_origin,
+                               "--edit", opts->edit,
+                               NULL);
        opts->commit_argv = argv;
 }
 
@@ -563,33 +592,146 @@ static void read_and_refresh_cache(struct replay_opts *opts)
        rollback_lock_file(&index_lock);
 }
 
-static int pick_commits(struct replay_opts *opts)
+/*
+ * Append a commit to the end of the commit_list.
+ *
+ * next starts by pointing to the variable that holds the head of an
+ * empty commit_list, and is updated to point to the "next" field of
+ * the last item on the list as new commits are appended.
+ *
+ * Usage example:
+ *
+ *     struct commit_list *list;
+ *     struct commit_list **next = &list;
+ *
+ *     next = commit_list_append(c1, next);
+ *     next = commit_list_append(c2, next);
+ *     assert(commit_list_count(list) == 2);
+ *     return list;
+ */
+struct commit_list **commit_list_append(struct commit *commit,
+                                       struct commit_list **next)
+{
+       struct commit_list *new = xmalloc(sizeof(struct commit_list));
+       new->item = commit;
+       *next = new;
+       new->next = NULL;
+       return &new->next;
+}
+
+static int format_todo(struct strbuf *buf, struct commit_list *todo_list,
+               struct replay_opts *opts)
+{
+       struct commit_list *cur = NULL;
+       struct commit_message msg = { NULL, NULL, NULL, NULL, NULL };
+       const char *sha1_abbrev = NULL;
+       const char *action_str = opts->action == REVERT ? "revert" : "pick";
+
+       for (cur = todo_list; cur; cur = cur->next) {
+               sha1_abbrev = find_unique_abbrev(cur->item->object.sha1, DEFAULT_ABBREV);
+               if (get_message(cur->item, &msg))
+                       return error(_("Cannot get commit message for %s"), sha1_abbrev);
+               strbuf_addf(buf, "%s %s %s\n", action_str, sha1_abbrev, msg.subject);
+       }
+       return 0;
+}
+
+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;
 
-       setenv(GIT_REFLOG_ACTION, action_name(opts), 0);
-       if (opts->allow_ff) {
-               if (opts->signoff)
-                       die(_("cherry-pick --ff cannot be used with --signoff"));
-               if (opts->no_commit)
-                       die(_("cherry-pick --ff cannot be used with --no-commit"));
-               if (opts->record_origin)
-                       die(_("cherry-pick --ff cannot be used with -x"));
-               if (opts->edit)
-                       die(_("cherry-pick --ff cannot be used with --edit"));
+       prepare_revs(&revs, opts);
+
+       next = todo_list;
+       while ((commit = get_revision(&revs)))
+               next = commit_list_append(commit, next);
+}
+
+static void create_seq_dir(void)
+{
+       const char *seq_dir = git_path(SEQ_DIR);
+
+       if (!(file_exists(seq_dir) && is_directory(seq_dir))
+               && mkdir(seq_dir, 0777) < 0)
+               die_errno(_("Could not create sequencer directory '%s'."), seq_dir);
+}
+
+static void save_head(const char *head)
+{
+       const char *head_file = git_path(SEQ_HEAD_FILE);
+       static struct lock_file head_lock;
+       struct strbuf buf = STRBUF_INIT;
+       int fd;
+
+       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);
+       if (commit_lock_file(&head_lock) < 0)
+               die(_("Error wrapping up %s."), head_file);
+}
+
+static void save_todo(struct commit_list *todo_list, struct replay_opts *opts)
+{
+       const char *todo_file = git_path(SEQ_TODO_FILE);
+       static struct lock_file todo_lock;
+       struct strbuf buf = STRBUF_INIT;
+       int fd;
+
+       fd = hold_lock_file_for_update(&todo_lock, todo_file, LOCK_DIE_ON_ERROR);
+       if (format_todo(&buf, todo_list, opts) < 0)
+               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);
        }
+       if (commit_lock_file(&todo_lock) < 0) {
+               strbuf_release(&buf);
+               die(_("Error wrapping up %s."), todo_file);
+       }
+       strbuf_release(&buf);
+}
+
+static int pick_commits(struct replay_opts *opts)
+{
+       struct commit_list *todo_list = NULL;
+       struct strbuf buf = STRBUF_INIT;
+       unsigned char sha1[20];
+       struct commit_list *cur;
+       int res;
 
+       setenv(GIT_REFLOG_ACTION, action_name(opts), 0);
+       if (opts->allow_ff)
+               assert(!(opts->signoff || opts->no_commit ||
+                               opts->record_origin || opts->edit));
        read_and_refresh_cache(opts);
 
-       prepare_revs(&revs, opts);
+       walk_revs_populate_todo(&todo_list, opts);
+       create_seq_dir();
+       if (get_sha1("HEAD", sha1)) {
+               if (opts->action == REVERT)
+                       die(_("Can't revert as initial commit"));
+               die(_("Can't cherry-pick into empty head"));
+       }
+       save_head(sha1_to_hex(sha1));
 
-       while ((commit = get_revision(&revs))) {
-               int res = do_pick_commit(commit, opts);
+       for (cur = todo_list; cur; cur = cur->next) {
+               save_todo(cur, opts);
+               res = do_pick_commit(cur->item, opts);
                if (res)
                        return res;
        }
 
+       /*
+        * Sequence of picks finished successfully; cleanup by
+        * removing the .git/sequencer directory
+        */
+       strbuf_addf(&buf, "%s", git_path(SEQ_DIR));
+       remove_dir_recursively(&buf, 0);
+       strbuf_release(&buf);
        return 0;
 }