]> git.ipfire.org Git - thirdparty/git.git/blobdiff - sequencer.c
Merge branch 'pw/rebase-reflog-fixes'
[thirdparty/git.git] / sequencer.c
index 5790b35d763d0f97ffc37625232211215a535c78..e658df7e8ff66df9edc1e7d909f4688f5eea48c1 100644 (file)
@@ -35,6 +35,8 @@
 #include "commit-reach.h"
 #include "rebase-interactive.h"
 #include "reset.h"
+#include "branch.h"
+#include "log-tree.h"
 
 #define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
 
@@ -147,6 +149,20 @@ static GIT_PATH_FUNC(rebase_path_squash_onto, "rebase-merge/squash-onto")
  */
 static GIT_PATH_FUNC(rebase_path_refs_to_delete, "rebase-merge/refs-to-delete")
 
+/*
+ * The update-refs file stores a list of refs that will be updated at the end
+ * of the rebase sequence. The 'update-ref <ref>' commands in the todo file
+ * update the OIDs for the refs in this file, but the refs are not updated
+ * until the end of the rebase sequence.
+ *
+ * rebase_path_update_refs() returns the path to this file for a given
+ * worktree directory. For the current worktree, pass the_repository->gitdir.
+ */
+static char *rebase_path_update_refs(const char *wt_git_dir)
+{
+       return xstrfmt("%s/rebase-merge/update-refs", wt_git_dir);
+}
+
 /*
  * The following files are written by git-rebase just after parsing the
  * command-line.
@@ -169,6 +185,30 @@ static GIT_PATH_FUNC(rebase_path_no_reschedule_failed_exec, "rebase-merge/no-res
 static GIT_PATH_FUNC(rebase_path_drop_redundant_commits, "rebase-merge/drop_redundant_commits")
 static GIT_PATH_FUNC(rebase_path_keep_redundant_commits, "rebase-merge/keep_redundant_commits")
 
+/**
+ * A 'struct update_refs_record' represents a value in the update-refs
+ * list. We use a string_list to map refs to these (before, after) pairs.
+ */
+struct update_ref_record {
+       struct object_id before;
+       struct object_id after;
+};
+
+static struct update_ref_record *init_update_ref_record(const char *ref)
+{
+       struct update_ref_record *rec;
+
+       CALLOC_ARRAY(rec, 1);
+
+       oidcpy(&rec->before, null_oid());
+       oidcpy(&rec->after, null_oid());
+
+       /* This may fail, but that's fine, we will keep the null OID. */
+       read_ref(ref, &rec->before);
+
+       return rec;
+}
+
 static int git_sequencer_config(const char *k, const char *v, void *cb)
 {
        struct replay_opts *opts = cb;
@@ -497,7 +537,7 @@ static struct tree *empty_tree(struct repository *r)
 static int error_dirty_index(struct repository *repo, struct replay_opts *opts)
 {
        if (repo_read_index_unmerged(repo))
-               return error_resolve_conflict(_(action_name(opts)));
+               return error_resolve_conflict(action_name(opts));
 
        error(_("your local changes would be overwritten by %s."),
                _(action_name(opts)));
@@ -535,7 +575,7 @@ static int fast_forward_to(struct repository *r,
        if (checkout_fast_forward(r, from, to, 1))
                return -1; /* the callee should have complained already */
 
-       strbuf_addf(&sb, _("%s: fast-forward"), _(action_name(opts)));
+       strbuf_addf(&sb, "%s: fast-forward", action_name(opts));
 
        transaction = ref_transaction_begin(&err);
        if (!transaction ||
@@ -875,7 +915,7 @@ int read_author_script(const char *path, char **name, char **email, char **date,
                error(_("missing 'GIT_AUTHOR_EMAIL'"));
        if (date_i == -2)
                error(_("missing 'GIT_AUTHOR_DATE'"));
-       if (date_i < 0 || email_i < 0 || date_i < 0 || err)
+       if (name_i < 0 || email_i < 0 || date_i < 0 || err)
                goto finish;
        *name = kv.items[name_i].util;
        *email = kv.items[email_i].util;
@@ -1689,20 +1729,21 @@ static struct {
        char c;
        const char *str;
 } todo_command_info[] = {
-       { 'p', "pick" },
-       { 0,   "revert" },
-       { 'e', "edit" },
-       { 'r', "reword" },
-       { 'f', "fixup" },
-       { 's', "squash" },
-       { 'x', "exec" },
-       { 'b', "break" },
-       { 'l', "label" },
-       { 't', "reset" },
-       { 'm', "merge" },
-       { 0,   "noop" },
-       { 'd', "drop" },
-       { 0,   NULL }
+       [TODO_PICK] = { 'p', "pick" },
+       [TODO_REVERT] = { 0,   "revert" },
+       [TODO_EDIT] = { 'e', "edit" },
+       [TODO_REWORD] = { 'r', "reword" },
+       [TODO_FIXUP] = { 'f', "fixup" },
+       [TODO_SQUASH] = { 's', "squash" },
+       [TODO_EXEC] = { 'x', "exec" },
+       [TODO_BREAK] = { 'b', "break" },
+       [TODO_LABEL] = { 'l', "label" },
+       [TODO_RESET] = { 't', "reset" },
+       [TODO_MERGE] = { 'm', "merge" },
+       [TODO_UPDATE_REF] = { 'u', "update-ref" },
+       [TODO_NOOP] = { 0,   "noop" },
+       [TODO_DROP] = { 'd', "drop" },
+       [TODO_COMMENT] = { 0,   NULL },
 };
 
 static const char *command_to_string(const enum todo_command command)
@@ -2381,7 +2422,7 @@ static int read_and_refresh_cache(struct repository *r,
        if (repo_read_index(r) < 0) {
                rollback_lock_file(&index_lock);
                return error(_("git %s: failed to read the index"),
-                       _(action_name(opts)));
+                       action_name(opts));
        }
        refresh_index(r->index, REFRESH_QUIET|REFRESH_UNMERGED, NULL, NULL, NULL);
 
@@ -2389,7 +2430,7 @@ static int read_and_refresh_cache(struct repository *r,
                if (write_locked_index(r->index, &index_lock,
                                       COMMIT_LOCK | SKIP_IF_UNCHANGED)) {
                        return error(_("git %s: failed to refresh the index"),
-                               _(action_name(opts)));
+                               action_name(opts));
                }
        }
 
@@ -2481,7 +2522,7 @@ static int parse_insn_line(struct repository *r, struct todo_item *item,
                             command_to_string(item->command));
 
        if (item->command == TODO_EXEC || item->command == TODO_LABEL ||
-           item->command == TODO_RESET) {
+           item->command == TODO_RESET || item->command == TODO_UPDATE_REF) {
                item->commit = NULL;
                item->arg_offset = bol - buf;
                item->arg_len = (int)(eol - bol);
@@ -3712,7 +3753,7 @@ static int do_reset(struct repository *r,
        init_checkout_metadata(&unpack_tree_opts.meta, name, &oid, NULL);
 
        if (repo_read_index_unmerged(r)) {
-               ret = error_resolve_conflict(_(action_name(opts)));
+               ret = error_resolve_conflict(action_name(opts));
                goto cleanup;
        }
 
@@ -4081,6 +4122,221 @@ leave_merge:
        return ret;
 }
 
+static int write_update_refs_state(struct string_list *refs_to_oids)
+{
+       int result = 0;
+       struct lock_file lock = LOCK_INIT;
+       FILE *fp = NULL;
+       struct string_list_item *item;
+       char *path;
+
+       if (!refs_to_oids->nr)
+               return 0;
+
+       path = rebase_path_update_refs(the_repository->gitdir);
+
+       if (safe_create_leading_directories(path)) {
+               result = error(_("unable to create leading directories of %s"),
+                              path);
+               goto cleanup;
+       }
+
+       if (hold_lock_file_for_update(&lock, path, 0) < 0) {
+               result = error(_("another 'rebase' process appears to be running; "
+                                "'%s.lock' already exists"),
+                              path);
+               goto cleanup;
+       }
+
+       fp = fdopen_lock_file(&lock, "w");
+       if (!fp) {
+               result = error_errno(_("could not open '%s' for writing"), path);
+               rollback_lock_file(&lock);
+               goto cleanup;
+       }
+
+       for_each_string_list_item(item, refs_to_oids) {
+               struct update_ref_record *rec = item->util;
+               fprintf(fp, "%s\n%s\n%s\n", item->string,
+                       oid_to_hex(&rec->before), oid_to_hex(&rec->after));
+       }
+
+       result = commit_lock_file(&lock);
+
+cleanup:
+       free(path);
+       return result;
+}
+
+/*
+ * Parse the update-refs file for the current rebase, then remove the
+ * refs that do not appear in the todo_list (and have not had updated
+ * values stored) and add refs that are in the todo_list but not
+ * represented in the update-refs file.
+ *
+ * If there are changes to the update-refs list, then write the new state
+ * to disk.
+ */
+void todo_list_filter_update_refs(struct repository *r,
+                                 struct todo_list *todo_list)
+{
+       int i;
+       int updated = 0;
+       struct string_list update_refs = STRING_LIST_INIT_DUP;
+
+       sequencer_get_update_refs_state(r->gitdir, &update_refs);
+
+       /*
+        * For each item in the update_refs list, if it has no updated
+        * value and does not appear in the todo_list, then remove it
+        * from the update_refs list.
+        */
+       for (i = 0; i < update_refs.nr; i++) {
+               int j;
+               int found = 0;
+               const char *ref = update_refs.items[i].string;
+               size_t reflen = strlen(ref);
+               struct update_ref_record *rec = update_refs.items[i].util;
+
+               /* OID already stored as updated. */
+               if (!is_null_oid(&rec->after))
+                       continue;
+
+               for (j = 0; !found && j < todo_list->total_nr; j++) {
+                       struct todo_item *item = &todo_list->items[j];
+                       const char *arg = todo_list->buf.buf + item->arg_offset;
+
+                       if (item->command != TODO_UPDATE_REF)
+                               continue;
+
+                       if (item->arg_len != reflen ||
+                           strncmp(arg, ref, reflen))
+                               continue;
+
+                       found = 1;
+               }
+
+               if (!found) {
+                       free(update_refs.items[i].string);
+                       free(update_refs.items[i].util);
+
+                       update_refs.nr--;
+                       MOVE_ARRAY(update_refs.items + i, update_refs.items + i + 1, update_refs.nr - i);
+
+                       updated = 1;
+                       i--;
+               }
+       }
+
+       /*
+        * For each todo_item, check if its ref is in the update_refs list.
+        * If not, then add it as an un-updated ref.
+        */
+       for (i = 0; i < todo_list->total_nr; i++) {
+               struct todo_item *item = &todo_list->items[i];
+               const char *arg = todo_list->buf.buf + item->arg_offset;
+               int j, found = 0;
+
+               if (item->command != TODO_UPDATE_REF)
+                       continue;
+
+               for (j = 0; !found && j < update_refs.nr; j++) {
+                       const char *ref = update_refs.items[j].string;
+
+                       found = strlen(ref) == item->arg_len &&
+                               !strncmp(ref, arg, item->arg_len);
+               }
+
+               if (!found) {
+                       struct string_list_item *inserted;
+                       struct strbuf argref = STRBUF_INIT;
+
+                       strbuf_add(&argref, arg, item->arg_len);
+                       inserted = string_list_insert(&update_refs, argref.buf);
+                       inserted->util = init_update_ref_record(argref.buf);
+                       strbuf_release(&argref);
+                       updated = 1;
+               }
+       }
+
+       if (updated)
+               write_update_refs_state(&update_refs);
+       string_list_clear(&update_refs, 1);
+}
+
+static int do_update_ref(struct repository *r, const char *refname)
+{
+       struct string_list_item *item;
+       struct string_list list = STRING_LIST_INIT_DUP;
+
+       if (sequencer_get_update_refs_state(r->gitdir, &list))
+               return -1;
+
+       for_each_string_list_item(item, &list) {
+               if (!strcmp(item->string, refname)) {
+                       struct update_ref_record *rec = item->util;
+                       if (read_ref("HEAD", &rec->after))
+                               return -1;
+                       break;
+               }
+       }
+
+       write_update_refs_state(&list);
+       string_list_clear(&list, 1);
+       return 0;
+}
+
+static int do_update_refs(struct repository *r, int quiet)
+{
+       int res = 0;
+       struct string_list_item *item;
+       struct string_list refs_to_oids = STRING_LIST_INIT_DUP;
+       struct ref_store *refs = get_main_ref_store(r);
+       struct strbuf update_msg = STRBUF_INIT;
+       struct strbuf error_msg = STRBUF_INIT;
+
+       if ((res = sequencer_get_update_refs_state(r->gitdir, &refs_to_oids)))
+               return res;
+
+       for_each_string_list_item(item, &refs_to_oids) {
+               struct update_ref_record *rec = item->util;
+               int loop_res;
+
+               loop_res = refs_update_ref(refs, "rewritten during rebase",
+                                          item->string,
+                                          &rec->after, &rec->before,
+                                          0, UPDATE_REFS_MSG_ON_ERR);
+               res |= loop_res;
+
+               if (quiet)
+                       continue;
+
+               if (loop_res)
+                       strbuf_addf(&error_msg, "\t%s\n", item->string);
+               else
+                       strbuf_addf(&update_msg, "\t%s\n", item->string);
+       }
+
+       if (!quiet &&
+           (update_msg.len || error_msg.len)) {
+               fprintf(stderr,
+                       _("Updated the following refs with %s:\n%s"),
+                       "--update-refs",
+                       update_msg.buf);
+
+               if (res)
+                       fprintf(stderr,
+                               _("Failed to update the following refs with %s:\n%s"),
+                               "--update-refs",
+                               error_msg.buf);
+       }
+
+       string_list_clear(&refs_to_oids, 1);
+       strbuf_release(&update_msg);
+       strbuf_release(&error_msg);
+       return res;
+}
+
 static int is_final_fixup(struct todo_list *todo_list)
 {
        int i = todo_list->current;
@@ -4456,6 +4712,12 @@ static int pick_commits(struct repository *r,
                                return error_with_patch(r, item->commit,
                                                        arg, item->arg_len,
                                                        opts, res, 0);
+               } else if (item->command == TODO_UPDATE_REF) {
+                       struct strbuf ref = STRBUF_INIT;
+                       strbuf_add(&ref, arg, item->arg_len);
+                       if ((res = do_update_ref(r, ref.buf)))
+                               reschedule = 1;
+                       strbuf_release(&ref);
                } else if (!is_noop(item->command))
                        return error(_("unknown command %d"), item->command);
 
@@ -4591,6 +4853,9 @@ cleanup_head_ref:
 
                strbuf_release(&buf);
                strbuf_release(&head_ref);
+
+               if (do_update_refs(r, opts->quiet))
+                       return -1;
        }
 
        /*
@@ -4994,7 +5259,8 @@ struct labels_entry {
        char label[FLEX_ARRAY];
 };
 
-static int labels_cmp(const void *fndata, const struct hashmap_entry *eptr,
+static int labels_cmp(const void *fndata UNUSED,
+                     const struct hashmap_entry *eptr,
                      const struct hashmap_entry *entry_or_key, const void *key)
 {
        const struct labels_entry *a, *b;
@@ -5643,10 +5909,135 @@ static int skip_unnecessary_picks(struct repository *r,
        return 0;
 }
 
+struct todo_add_branch_context {
+       struct todo_item *items;
+       size_t items_nr;
+       size_t items_alloc;
+       struct strbuf *buf;
+       struct commit *commit;
+       struct string_list refs_to_oids;
+};
+
+static int add_decorations_to_list(const struct commit *commit,
+                                  struct todo_add_branch_context *ctx)
+{
+       const struct name_decoration *decoration = get_name_decoration(&commit->object);
+       const char *head_ref = resolve_ref_unsafe("HEAD",
+                                                 RESOLVE_REF_READING,
+                                                 NULL,
+                                                 NULL);
+
+       while (decoration) {
+               struct todo_item *item;
+               const char *path;
+               size_t base_offset = ctx->buf->len;
+
+               /*
+                * If the branch is the current HEAD, then it will be
+                * updated by the default rebase behavior.
+                */
+               if (head_ref && !strcmp(head_ref, decoration->name)) {
+                       decoration = decoration->next;
+                       continue;
+               }
+
+               ALLOC_GROW(ctx->items,
+                       ctx->items_nr + 1,
+                       ctx->items_alloc);
+               item = &ctx->items[ctx->items_nr];
+               memset(item, 0, sizeof(*item));
+
+               /* If the branch is checked out, then leave a comment instead. */
+               if ((path = branch_checked_out(decoration->name))) {
+                       item->command = TODO_COMMENT;
+                       strbuf_addf(ctx->buf, "# Ref %s checked out at '%s'\n",
+                                   decoration->name, path);
+               } else {
+                       struct string_list_item *sti;
+                       item->command = TODO_UPDATE_REF;
+                       strbuf_addf(ctx->buf, "%s\n", decoration->name);
+
+                       sti = string_list_insert(&ctx->refs_to_oids,
+                                                decoration->name);
+                       sti->util = init_update_ref_record(decoration->name);
+               }
+
+               item->offset_in_buf = base_offset;
+               item->arg_offset = base_offset;
+               item->arg_len = ctx->buf->len - base_offset;
+               ctx->items_nr++;
+
+               decoration = decoration->next;
+       }
+
+       return 0;
+}
+
+/*
+ * For each 'pick' command, find out if the commit has a decoration in
+ * refs/heads/. If so, then add a 'label for-update-refs/' command.
+ */
+static int todo_list_add_update_ref_commands(struct todo_list *todo_list)
+{
+       int i, res;
+       static struct string_list decorate_refs_exclude = STRING_LIST_INIT_NODUP;
+       static struct string_list decorate_refs_exclude_config = STRING_LIST_INIT_NODUP;
+       static struct string_list decorate_refs_include = STRING_LIST_INIT_NODUP;
+       struct decoration_filter decoration_filter = {
+               .include_ref_pattern = &decorate_refs_include,
+               .exclude_ref_pattern = &decorate_refs_exclude,
+               .exclude_ref_config_pattern = &decorate_refs_exclude_config,
+       };
+       struct todo_add_branch_context ctx = {
+               .buf = &todo_list->buf,
+               .refs_to_oids = STRING_LIST_INIT_DUP,
+       };
+
+       ctx.items_alloc = 2 * todo_list->nr + 1;
+       ALLOC_ARRAY(ctx.items, ctx.items_alloc);
+
+       string_list_append(&decorate_refs_include, "refs/heads/");
+       load_ref_decorations(&decoration_filter, 0);
+
+       for (i = 0; i < todo_list->nr; ) {
+               struct todo_item *item = &todo_list->items[i];
+
+               /* insert ith item into new list */
+               ALLOC_GROW(ctx.items,
+                          ctx.items_nr + 1,
+                          ctx.items_alloc);
+
+               ctx.items[ctx.items_nr++] = todo_list->items[i++];
+
+               if (item->commit) {
+                       ctx.commit = item->commit;
+                       add_decorations_to_list(item->commit, &ctx);
+               }
+       }
+
+       res = write_update_refs_state(&ctx.refs_to_oids);
+
+       string_list_clear(&ctx.refs_to_oids, 1);
+
+       if (res) {
+               /* we failed, so clean up the new list. */
+               free(ctx.items);
+               return res;
+       }
+
+       free(todo_list->items);
+       todo_list->items = ctx.items;
+       todo_list->nr = ctx.items_nr;
+       todo_list->alloc = ctx.items_alloc;
+
+       return 0;
+}
+
 int complete_action(struct repository *r, struct replay_opts *opts, unsigned flags,
                    const char *shortrevisions, const char *onto_name,
                    struct commit *onto, const struct object_id *orig_head,
                    struct string_list *commands, unsigned autosquash,
+                   unsigned update_refs,
                    struct todo_list *todo_list)
 {
        char shortonto[GIT_MAX_HEXSZ + 1];
@@ -5665,6 +6056,9 @@ int complete_action(struct repository *r, struct replay_opts *opts, unsigned fla
                item->arg_len = item->arg_offset = item->flags = item->offset_in_buf = 0;
        }
 
+       if (update_refs && todo_list_add_update_ref_commands(todo_list))
+               return -1;
+
        if (autosquash && todo_list_rearrange_squash(todo_list))
                return -1;
 
@@ -5743,7 +6137,7 @@ struct subject2item_entry {
        char subject[FLEX_ARRAY];
 };
 
-static int subject2item_cmp(const void *fndata,
+static int subject2item_cmp(const void *fndata UNUSED,
                            const struct hashmap_entry *eptr,
                            const struct hashmap_entry *entry_or_key,
                            const void *key)
@@ -5814,8 +6208,6 @@ int todo_list_rearrange_squash(struct todo_list *todo_list)
                        return error(_("the script was already rearranged."));
                }
 
-               *commit_todo_item_at(&commit_todo, item->commit) = item;
-
                parse_commit(item->commit);
                commit_buffer = logmsg_reencode(item->commit, NULL, "UTF-8");
                find_commit_subject(commit_buffer, &subject);
@@ -5882,6 +6274,8 @@ int todo_list_rearrange_squash(struct todo_list *todo_list)
                                        strhash(entry->subject));
                        hashmap_put(&subject2item, &entry->entry);
                }
+
+               *commit_todo_item_at(&commit_todo, item->commit) = item;
        }
 
        if (rearranged) {
@@ -5941,3 +6335,54 @@ int sequencer_determine_whence(struct repository *r, enum commit_whence *whence)
 
        return 0;
 }
+
+int sequencer_get_update_refs_state(const char *wt_dir,
+                                   struct string_list *refs)
+{
+       int result = 0;
+       FILE *fp = NULL;
+       struct strbuf ref = STRBUF_INIT;
+       struct strbuf hash = STRBUF_INIT;
+       struct update_ref_record *rec = NULL;
+
+       char *path = rebase_path_update_refs(wt_dir);
+
+       fp = fopen(path, "r");
+       if (!fp)
+               goto cleanup;
+
+       while (strbuf_getline(&ref, fp) != EOF) {
+               struct string_list_item *item;
+
+               CALLOC_ARRAY(rec, 1);
+
+               if (strbuf_getline(&hash, fp) == EOF ||
+                   get_oid_hex(hash.buf, &rec->before)) {
+                       warning(_("update-refs file at '%s' is invalid"),
+                                 path);
+                       result = -1;
+                       goto cleanup;
+               }
+
+               if (strbuf_getline(&hash, fp) == EOF ||
+                   get_oid_hex(hash.buf, &rec->after)) {
+                       warning(_("update-refs file at '%s' is invalid"),
+                                 path);
+                       result = -1;
+                       goto cleanup;
+               }
+
+               item = string_list_insert(refs, ref.buf);
+               item->util = rec;
+               rec = NULL;
+       }
+
+cleanup:
+       if (fp)
+               fclose(fp);
+       free(path);
+       free(rec);
+       strbuf_release(&ref);
+       strbuf_release(&hash);
+       return result;
+}