rebase: update refs from 'update-ref' commands

The previous change introduced the 'git rebase --update-refs' option which added 'update-ref <ref>' commands to the todo list of an interactive rebase. Teach Git to record the HEAD position when reaching these 'update-ref' commands. The ref/before/after triple is stored in the $GIT_DIR/rebase-merge/update-refs file. A previous change parsed this file to avoid having other processes updating the refs in that file while the rebase is in progress. Not only do we update the file when the sequencer reaches these 'update-ref' commands, we then update the refs themselves at the end of the rebase sequence. If the rebase is aborted before this final step, then the refs are not updated. The 'before' value is used to ensure that we do not accidentally obliterate a ref that was updated concurrently (say, by an older version of Git or a third-party tool). Now that the 'git rebase --update-refs' command is implemented to write to the update-refs file, we can remove the fake construction of the update-refs file from a test in t2407-worktree-heads.sh. Signed-off-by: Derrick Stolee <derrickstolee@github.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Derrick Stolee committed Jul 19, 2022 at 18:33 UTC 89fc0b53fdb4ea2f9581ea728577f4d2a4947672
3 files changed +140 -18
sequencer.c
+117 -3
@@ -36,6 +36,7 @@
36 #include "rebase-interactive.h"
37 #include "reset.h"
38 #include "branch.h"
39 +#include "log-tree.h"
40
41 #define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
42
@@ -193,6 +194,21 @@ struct update_ref_record {
194 struct object_id after;
195 };
196
197 +static struct update_ref_record *init_update_ref_record(const char *ref)
198 +{
199 + struct update_ref_record *rec;
200 +
201 + CALLOC_ARRAY(rec, 1);
202 +
203 + oidcpy(&rec->before, null_oid());
204 + oidcpy(&rec->after, null_oid());
205 +
206 + /* This may fail, but that's fine, we will keep the null OID. */
207 + read_ref(ref, &rec->before);
208 +
209 + return rec;
210 +}
211 +
212 static int git_sequencer_config(const char *k, const char *v, void *cb)
213 {
214 struct replay_opts *opts = cb;
@@ -4106,11 +4122,97 @@ leave_merge:
4122 return ret;
4123 }
4124
4109 -static int do_update_ref(struct repository *r, const char *ref_name)
4125 +static int write_update_refs_state(struct string_list *refs_to_oids)
4126 +{
4127 + int result = 0;
4128 + struct lock_file lock = LOCK_INIT;
4129 + FILE *fp = NULL;
4130 + struct string_list_item *item;
4131 + char *path;
4132 +
4133 + if (!refs_to_oids->nr)
4134 + return 0;
4135 +
4136 + path = rebase_path_update_refs(the_repository->gitdir);
4137 +
4138 + if (safe_create_leading_directories(path)) {
4139 + result = error(_("unable to create leading directories of %s"),
4140 + path);
4141 + goto cleanup;
4142 + }
4143 +
4144 + if (hold_lock_file_for_update(&lock, path, 0) < 0) {
4145 + result = error(_("another 'rebase' process appears to be running; "
4146 + "'%s.lock' already exists"),
4147 + path);
4148 + goto cleanup;
4149 + }
4150 +
4151 + fp = fdopen_lock_file(&lock, "w");
4152 + if (!fp) {
4153 + result = error_errno(_("could not open '%s' for writing"), path);
4154 + rollback_lock_file(&lock);
4155 + goto cleanup;
4156 + }
4157 +
4158 + for_each_string_list_item(item, refs_to_oids) {
4159 + struct update_ref_record *rec = item->util;
4160 + fprintf(fp, "%s\n%s\n%s\n", item->string,
4161 + oid_to_hex(&rec->before), oid_to_hex(&rec->after));
4162 + }
4163 +
4164 + result = commit_lock_file(&lock);
4165 +
4166 +cleanup:
4167 + free(path);
4168 + return result;
4169 +}
4170 +
4171 +static int do_update_ref(struct repository *r, const char *refname)
4172 {
4173 + struct string_list_item *item;
4174 + struct string_list list = STRING_LIST_INIT_DUP;
4175 +
4176 + if (sequencer_get_update_refs_state(r->gitdir, &list))
4177 + return -1;
4178 +
4179 + for_each_string_list_item(item, &list) {
4180 + if (!strcmp(item->string, refname)) {
4181 + struct update_ref_record *rec = item->util;
4182 + if (read_ref("HEAD", &rec->after))
4183 + return -1;
4184 + break;
4185 + }
4186 + }
4187 +
4188 + write_update_refs_state(&list);
4189 + string_list_clear(&list, 1);
4190 return 0;
4191 }
4192
4193 +static int do_update_refs(struct repository *r)
4194 +{
4195 + int res = 0;
4196 + struct string_list_item *item;
4197 + struct string_list refs_to_oids = STRING_LIST_INIT_DUP;
4198 + struct ref_store *refs = get_main_ref_store(r);
4199 +
4200 + if ((res = sequencer_get_update_refs_state(r->gitdir, &refs_to_oids)))
4201 + return res;
4202 +
4203 + for_each_string_list_item(item, &refs_to_oids) {
4204 + struct update_ref_record *rec = item->util;
4205 +
4206 + res |= refs_update_ref(refs, "rewritten during rebase",
4207 + item->string,
4208 + &rec->after, &rec->before,
4209 + 0, UPDATE_REFS_MSG_ON_ERR);
4210 + }
4211 +
4212 + string_list_clear(&refs_to_oids, 1);
4213 + return res;
4214 +}
4215 +
4216 static int is_final_fixup(struct todo_list *todo_list)
4217 {
4218 int i = todo_list->current;
@@ -4627,6 +4729,9 @@ cleanup_head_ref:
4729
4730 strbuf_release(&buf);
4731 strbuf_release(&head_ref);
4732 +
4733 + if (do_update_refs(r))
4734 + return -1;
4735 }
4736
4737 /*
@@ -5711,7 +5816,7 @@ static int add_decorations_to_list(const struct commit *commit,
5816
5817 sti = string_list_insert(&ctx->refs_to_oids,
5818 decoration->name);
5714 - sti->util = oiddup(the_hash_algo->null_oid);
5819 + sti->util = init_update_ref_record(decoration->name);
5820 }
5821
5822 item->offset_in_buf = base_offset;
@@ -5731,7 +5836,7 @@ static int add_decorations_to_list(const struct commit *commit,
5836 */
5837 static int todo_list_add_update_ref_commands(struct todo_list *todo_list)
5838 {
5734 - int i;
5839 + int i, res;
5840 static struct string_list decorate_refs_exclude = STRING_LIST_INIT_NODUP;
5841 static struct string_list decorate_refs_exclude_config = STRING_LIST_INIT_NODUP;
5842 static struct string_list decorate_refs_include = STRING_LIST_INIT_NODUP;
@@ -5767,7 +5872,16 @@ static int todo_list_add_update_ref_commands(struct todo_list *todo_list)
5872 }
5873 }
5874
5875 + res = write_update_refs_state(&ctx.refs_to_oids);
5876 +
5877 string_list_clear(&ctx.refs_to_oids, 1);
5878 +
5879 + if (res) {
5880 + /* we failed, so clean up the new list. */
5881 + free(ctx.items);
5882 + return res;
5883 + }
5884 +
5885 free(todo_list->items);
5886 todo_list->items = ctx.items;
5887 todo_list->nr = ctx.items_nr;
t/t2407-worktree-heads.sh
+6 -15
@@ -81,25 +81,16 @@ test_expect_success !SANITIZE_LEAK 'refuse to overwrite: worktree in rebase (mer
81 grep "cannot force update the branch '\''wt-2'\'' checked out at.*wt-2" err
82 '
83
84 -test_expect_success 'refuse to overwrite: worktree in rebase with --update-refs' '
85 - test_when_finished rm -rf .git/worktrees/wt-3/rebase-merge &&
86 -
87 - mkdir -p .git/worktrees/wt-3/rebase-merge &&
88 - touch .git/worktrees/wt-3/rebase-merge/interactive &&
84 +test_expect_success !SANITIZE_LEAK 'refuse to overwrite: worktree in rebase with --update-refs' '
85 + test_when_finished git -C wt-3 rebase --abort &&
86
90 - cat >.git/worktrees/wt-3/rebase-merge/update-refs <<-EOF &&
91 - refs/heads/fake-3
92 - $(git rev-parse HEAD~1)
93 - $(git rev-parse HEAD)
94 - refs/heads/fake-4
95 - $(git rev-parse HEAD)
96 - $(git rev-parse HEAD)
97 - EOF
87 + git branch -f can-be-updated wt-3 &&
88 + test_must_fail git -C wt-3 rebase --update-refs conflict-3 &&
89
90 for i in 3 4
91 do
101 - test_must_fail git branch -f fake-$i HEAD 2>err &&
102 - grep "cannot force update the branch '\''fake-$i'\'' checked out at.*wt-3" err ||
92 + test_must_fail git branch -f can-be-updated HEAD 2>err &&
93 + grep "cannot force update the branch '\''can-be-updated'\'' checked out at.*wt-3" err ||
94 return 1
95 done
96 '
t/t3404-rebase-interactive.sh
+17
@@ -1817,6 +1817,23 @@ test_expect_success '--update-refs adds commands with --rebase-merges' '
1817 )
1818 '
1819
1820 +test_expect_success '--update-refs updates refs correctly' '
1821 + git checkout -B update-refs no-conflict-branch &&
1822 + git branch -f base HEAD~4 &&
1823 + git branch -f first HEAD~3 &&
1824 + git branch -f second HEAD~3 &&
1825 + git branch -f third HEAD~1 &&
1826 + test_commit extra2 fileX &&
1827 + git commit --amend --fixup=L &&
1828 +
1829 + git rebase -i --autosquash --update-refs primary &&
1830 +
1831 + test_cmp_rev HEAD~3 refs/heads/first &&
1832 + test_cmp_rev HEAD~3 refs/heads/second &&
1833 + test_cmp_rev HEAD~1 refs/heads/third &&
1834 + test_cmp_rev HEAD refs/heads/no-conflict-branch
1835 +'
1836 +
1837 # This must be the last test in this file
1838 test_expect_success '$EDITOR and friends are unchanged' '
1839 test_editor_unchanged