diff options
-rw-r--r-- | Documentation/config/rebase.txt | 3 | ||||
-rw-r--r-- | Documentation/git-rebase.txt | 10 | ||||
-rw-r--r-- | branch.c | 13 | ||||
-rw-r--r-- | builtin/rebase.c | 10 | ||||
-rw-r--r-- | rebase-interactive.c | 15 | ||||
-rw-r--r-- | sequencer.c | 474 | ||||
-rw-r--r-- | sequencer.h | 23 | ||||
-rw-r--r-- | t/lib-rebase.sh | 15 | ||||
-rwxr-xr-x | t/t2407-worktree-heads.sh | 103 | ||||
-rwxr-xr-x | t/t3404-rebase-interactive.sh | 273 |
10 files changed, 895 insertions, 44 deletions
diff --git a/Documentation/config/rebase.txt b/Documentation/config/rebase.txt index 8c979cb20f..f19bd0e040 100644 --- a/Documentation/config/rebase.txt +++ b/Documentation/config/rebase.txt @@ -21,6 +21,9 @@ rebase.autoStash:: `--autostash` options of linkgit:git-rebase[1]. Defaults to false. +rebase.updateRefs:: + If set to true enable `--update-refs` option by default. + rebase.missingCommitsCheck:: If set to "warn", git rebase -i will print a warning if some commits are removed (e.g. a line was deleted), however the diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt index a872ab0fbd..080658c871 100644 --- a/Documentation/git-rebase.txt +++ b/Documentation/git-rebase.txt @@ -612,6 +612,15 @@ provided. Otherwise an explicit `--no-reschedule-failed-exec` at the start would be overridden by the presence of `rebase.rescheduleFailedExec=true` configuration. +--update-refs:: +--no-update-refs:: + Automatically force-update any branches that point to commits that + are being rebased. Any branches that are checked out in a worktree + are not updated in this way. ++ +If the configuration variable `rebase.updateRefs` is set, then this option +can be used to override and disable this setting. + INCOMPATIBLE OPTIONS -------------------- @@ -635,6 +644,7 @@ are incompatible with the following options: * --empty= * --reapply-cherry-picks * --edit-todo + * --update-refs * --root when used in combination with --onto In addition, the following pairs of options are incompatible: @@ -388,6 +388,7 @@ static void prepare_checked_out_branches(void) char *old; struct wt_status_state state = { 0 }; struct worktree *wt = worktrees[i++]; + struct string_list update_refs = STRING_LIST_INIT_DUP; if (wt->is_bare) continue; @@ -423,6 +424,18 @@ static void prepare_checked_out_branches(void) strbuf_release(&ref); } wt_status_state_free_buffers(&state); + + if (!sequencer_get_update_refs_state(get_worktree_git_dir(wt), + &update_refs)) { + struct string_list_item *item; + for_each_string_list_item(item, &update_refs) { + old = strmap_put(¤t_checked_out_branches, + item->string, + xstrdup(wt->path)); + free(old); + } + string_list_clear(&update_refs, 1); + } } free_worktrees(worktrees); diff --git a/builtin/rebase.c b/builtin/rebase.c index 70aa7c842f..56e4214b44 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -102,6 +102,7 @@ struct rebase_options { int reschedule_failed_exec; int reapply_cherry_picks; int fork_point; + int update_refs; }; #define REBASE_OPTIONS_INIT { \ @@ -298,6 +299,7 @@ static int do_interactive_rebase(struct rebase_options *opts, unsigned flags) ret = complete_action(the_repository, &replay, flags, shortrevisions, opts->onto_name, opts->onto, &opts->orig_head, &commands, opts->autosquash, + opts->update_refs, &todo_list); } @@ -800,6 +802,11 @@ static int rebase_config(const char *var, const char *value, void *data) return 0; } + if (!strcmp(var, "rebase.updaterefs")) { + opts->update_refs = git_config_bool(var, value); + return 0; + } + if (!strcmp(var, "rebase.reschedulefailedexec")) { opts->reschedule_failed_exec = git_config_bool(var, value); return 0; @@ -1124,6 +1131,9 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) OPT_BOOL(0, "autosquash", &options.autosquash, N_("move commits that begin with " "squash!/fixup! under -i")), + OPT_BOOL(0, "update-refs", &options.update_refs, + N_("update branches that point to commits " + "that are being rebased")), { OPTION_STRING, 'S', "gpg-sign", &gpg_sign, N_("key-id"), N_("GPG-sign commits"), PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, diff --git a/rebase-interactive.c b/rebase-interactive.c index 87649d0c01..7407c59319 100644 --- a/rebase-interactive.c +++ b/rebase-interactive.c @@ -54,9 +54,12 @@ void append_todo_help(int command_count, "l, label <label> = label current HEAD with a name\n" "t, reset <label> = reset HEAD to a label\n" "m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]\n" -". create a merge commit using the original merge commit's\n" -". message (or the oneline, if no original merge commit was\n" -". specified); use -c <commit> to reword the commit message\n" +" create a merge commit using the original merge commit's\n" +" message (or the oneline, if no original merge commit was\n" +" specified); use -c <commit> to reword the commit message\n" +"u, update-ref <ref> = track a placeholder for the <ref> to be updated\n" +" to this position in the new commits. The <ref> is\n" +" updated at the end of the rebase\n" "\n" "These lines can be re-ordered; they are executed from top to bottom.\n"); unsigned edit_todo = !(shortrevisions && shortonto); @@ -143,6 +146,12 @@ int edit_todo_list(struct repository *r, struct todo_list *todo_list, return -4; } + /* + * See if branches need to be added or removed from the update-refs + * file based on the new todo list. + */ + todo_list_filter_update_refs(r, new_todo); + return 0; } diff --git a/sequencer.c b/sequencer.c index 61a8e0020d..5f22b7cd37 100644 --- a/sequencer.c +++ b/sequencer.c @@ -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" @@ -148,6 +150,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; @@ -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) @@ -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); @@ -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; } /* @@ -5638,10 +5903,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]; @@ -5660,6 +6050,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; @@ -5936,3 +6329,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; +} diff --git a/sequencer.h b/sequencer.h index 698599fe4e..563fe59933 100644 --- a/sequencer.h +++ b/sequencer.h @@ -96,6 +96,7 @@ enum todo_command { TODO_LABEL, TODO_RESET, TODO_MERGE, + TODO_UPDATE_REF, /* commands that do nothing but are counted for reporting progress */ TODO_NOOP, TODO_DROP, @@ -132,6 +133,18 @@ void todo_list_release(struct todo_list *todo_list); const char *todo_item_get_arg(struct todo_list *todo_list, struct todo_item *item); +/* + * 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); + /* Call this to setup defaults before parsing command line options */ void sequencer_init_config(struct replay_opts *opts); int sequencer_pick_revisions(struct repository *repo, @@ -167,6 +180,7 @@ int complete_action(struct repository *r, struct replay_opts *opts, unsigned fla 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); int todo_list_rearrange_squash(struct todo_list *todo_list); @@ -233,4 +247,13 @@ void sequencer_post_commit_cleanup(struct repository *r, int verbose); int sequencer_get_last_command(struct repository* r, enum replay_action *action); int sequencer_determine_whence(struct repository *r, enum commit_whence *whence); + +/** + * Append the set of ref-OID pairs that are currently stored for the 'git + * rebase --update-refs' feature if such a rebase is currently happening. + * + * Localized to a worktree's git dir. + */ +int sequencer_get_update_refs_state(const char *wt_dir, struct string_list *refs); + #endif /* SEQUENCER_H */ diff --git a/t/lib-rebase.sh b/t/lib-rebase.sh index ec6b9b107d..b57541356b 100644 --- a/t/lib-rebase.sh +++ b/t/lib-rebase.sh @@ -207,3 +207,18 @@ check_reworded_commits () { >reword-log && test_cmp reword-expected reword-log } + +# usage: set_replace_editor <file> +# +# Replace the todo file with the exact contents of the given file. +set_replace_editor () { + cat >script <<-\EOF && + cat FILENAME >"$1" + + echo 'rebase -i script after editing:' + cat "$1" + EOF + + sed -e "s/FILENAME/$1/g" <script | write_script fake-editor.sh && + test_set_editor "$(pwd)/fake-editor.sh" +} diff --git a/t/t2407-worktree-heads.sh b/t/t2407-worktree-heads.sh index b6be42f74a..50815acd3e 100755 --- a/t/t2407-worktree-heads.sh +++ b/t/t2407-worktree-heads.sh @@ -7,13 +7,18 @@ TEST_PASSES_SANITIZE_LEAK=true test_expect_success 'setup' ' test_commit init && - git branch -f fake-1 && - git branch -f fake-2 && for i in 1 2 3 4 do + git checkout -b conflict-$i && + echo "not I" >$i.t && + git add $i.t && + git commit -m "will conflict" && + + git checkout - && test_commit $i && git branch wt-$i && + git branch fake-$i && git worktree add wt-$i wt-$i || return 1 done && @@ -44,26 +49,50 @@ test_expect_success 'refuse to overwrite: checked out in worktree' ' done ' -test_expect_success 'refuse to overwrite: worktree in bisect' ' - test_when_finished rm -rf .git/worktrees/wt-*/BISECT_* && +test_expect_success !SANITIZE_LEAK 'refuse to overwrite: worktree in bisect' ' + test_when_finished git -C wt-4 bisect reset && - touch .git/worktrees/wt-4/BISECT_LOG && - echo refs/heads/fake-2 >.git/worktrees/wt-4/BISECT_START && + # Set up a bisect so HEAD no longer points to wt-4. + git -C wt-4 bisect start && + git -C wt-4 bisect bad wt-4 && + git -C wt-4 bisect good wt-1 && - test_must_fail git branch -f fake-2 HEAD 2>err && - grep "cannot force update the branch '\''fake-2'\'' checked out at.*wt-4" err + test_must_fail git branch -f wt-4 HEAD 2>err && + grep "cannot force update the branch '\''wt-4'\'' checked out at.*wt-4" err ' -test_expect_success 'refuse to overwrite: worktree in rebase' ' - test_when_finished rm -rf .git/worktrees/wt-*/rebase-merge && +test_expect_success !SANITIZE_LEAK 'refuse to overwrite: worktree in rebase (apply)' ' + test_when_finished git -C wt-2 rebase --abort && - mkdir -p .git/worktrees/wt-3/rebase-merge && - touch .git/worktrees/wt-3/rebase-merge/interactive && - echo refs/heads/fake-1 >.git/worktrees/wt-3/rebase-merge/head-name && - echo refs/heads/fake-2 >.git/worktrees/wt-3/rebase-merge/onto && + # This will fail part-way through due to a conflict. + test_must_fail git -C wt-2 rebase --apply conflict-2 && + + test_must_fail git branch -f wt-2 HEAD 2>err && + grep "cannot force update the branch '\''wt-2'\'' checked out at.*wt-2" err +' + +test_expect_success !SANITIZE_LEAK 'refuse to overwrite: worktree in rebase (merge)' ' + test_when_finished git -C wt-2 rebase --abort && - test_must_fail git branch -f fake-1 HEAD 2>err && - grep "cannot force update the branch '\''fake-1'\'' checked out at.*wt-3" err + # This will fail part-way through due to a conflict. + test_must_fail git -C wt-2 rebase conflict-2 && + + test_must_fail git branch -f wt-2 HEAD 2>err && + grep "cannot force update the branch '\''wt-2'\'' checked out at.*wt-2" err +' + +test_expect_success !SANITIZE_LEAK 'refuse to overwrite: worktree in rebase with --update-refs' ' + test_when_finished git -C wt-3 rebase --abort && + + git branch -f can-be-updated wt-3 && + test_must_fail git -C wt-3 rebase --update-refs conflict-3 && + + for i in 3 4 + do + test_must_fail git branch -f can-be-updated HEAD 2>err && + grep "cannot force update the branch '\''can-be-updated'\'' checked out at.*wt-3" err || + return 1 + done ' test_expect_success !SANITIZE_LEAK 'refuse to fetch over ref: checked out' ' @@ -77,24 +106,24 @@ test_expect_success !SANITIZE_LEAK 'refuse to fetch over ref: checked out' ' ' test_expect_success !SANITIZE_LEAK 'refuse to fetch over ref: worktree in bisect' ' - test_when_finished rm -rf .git/worktrees/wt-*/BISECT_* && + test_when_finished git -C wt-4 bisect reset && - touch .git/worktrees/wt-4/BISECT_LOG && - echo refs/heads/fake-2 >.git/worktrees/wt-4/BISECT_START && + # Set up a bisect so HEAD no longer points to wt-4. + git -C wt-4 bisect start && + git -C wt-4 bisect bad wt-4 && + git -C wt-4 bisect good wt-1 && - test_must_fail git fetch server +refs/heads/fake-2:refs/heads/fake-2 2>err && + test_must_fail git fetch server +refs/heads/wt-4:refs/heads/wt-4 2>err && grep "refusing to fetch into branch" err ' test_expect_success !SANITIZE_LEAK 'refuse to fetch over ref: worktree in rebase' ' - test_when_finished rm -rf .git/worktrees/wt-*/rebase-merge && + test_when_finished git -C wt-3 rebase --abort && - mkdir -p .git/worktrees/wt-4/rebase-merge && - touch .git/worktrees/wt-4/rebase-merge/interactive && - echo refs/heads/fake-1 >.git/worktrees/wt-4/rebase-merge/head-name && - echo refs/heads/fake-2 >.git/worktrees/wt-4/rebase-merge/onto && + # This will fail part-way through due to a conflict. + test_must_fail git -C wt-3 rebase conflict-3 && - test_must_fail git fetch server +refs/heads/fake-1:refs/heads/fake-1 2>err && + test_must_fail git fetch server +refs/heads/wt-3:refs/heads/wt-3 2>err && grep "refusing to fetch into branch" err ' @@ -126,4 +155,26 @@ test_expect_success 'refuse to overwrite when in error states' ' done ' +. "$TEST_DIRECTORY"/lib-rebase.sh + +test_expect_success !SANITIZE_LEAK 'refuse to overwrite during rebase with --update-refs' ' + git commit --fixup HEAD~2 --allow-empty && + ( + set_cat_todo_editor && + test_must_fail git rebase -i --update-refs HEAD~3 >todo && + ! grep "update-refs" todo + ) && + git branch -f allow-update HEAD~2 && + ( + set_cat_todo_editor && + test_must_fail git rebase -i --update-refs HEAD~3 >todo && + grep "update-ref refs/heads/allow-update" todo + ) +' + +# This must be the last test in this file +test_expect_success '$EDITOR and friends are unchanged' ' + test_editor_unchanged +' + test_done diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index f31afd4a54..688b01e3eb 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -1743,6 +1743,279 @@ test_expect_success 'ORIG_HEAD is updated correctly' ' test_cmp_rev ORIG_HEAD test-orig-head@{1} ' +test_expect_success '--update-refs adds label and update-ref commands' ' + git checkout -b update-refs no-conflict-branch && + git branch -f base HEAD~4 && + git branch -f first HEAD~3 && + git branch -f second HEAD~3 && + git branch -f third HEAD~1 && + git commit --allow-empty --fixup=third && + git branch -f is-not-reordered && + git commit --allow-empty --fixup=HEAD~4 && + git branch -f shared-tip && + ( + set_cat_todo_editor && + + cat >expect <<-EOF && + pick $(git log -1 --format=%h J) J + fixup $(git log -1 --format=%h update-refs) fixup! J # empty + update-ref refs/heads/second + update-ref refs/heads/first + pick $(git log -1 --format=%h K) K + pick $(git log -1 --format=%h L) L + fixup $(git log -1 --format=%h is-not-reordered) fixup! L # empty + update-ref refs/heads/third + pick $(git log -1 --format=%h M) M + update-ref refs/heads/no-conflict-branch + update-ref refs/heads/is-not-reordered + update-ref refs/heads/shared-tip + EOF + + test_must_fail git rebase -i --autosquash --update-refs primary >todo && + test_cmp expect todo && + + test_must_fail git -c rebase.autosquash=true \ + -c rebase.updaterefs=true \ + rebase -i primary >todo && + + test_cmp expect todo + ) +' + +test_expect_success '--update-refs adds commands with --rebase-merges' ' + git checkout -b update-refs-with-merge no-conflict-branch && + git branch -f base HEAD~4 && + git branch -f first HEAD~3 && + git branch -f second HEAD~3 && + git branch -f third HEAD~1 && + git merge -m merge branch2 && + git branch -f merge-branch && + git commit --fixup=third --allow-empty && + ( + set_cat_todo_editor && + + cat >expect <<-EOF && + label onto + reset onto + pick $(git log -1 --format=%h branch2~1) F + pick $(git log -1 --format=%h branch2) I + update-ref refs/heads/branch2 + label merge + reset onto + pick $(git log -1 --format=%h refs/heads/second) J + update-ref refs/heads/second + update-ref refs/heads/first + pick $(git log -1 --format=%h refs/heads/third~1) K + pick $(git log -1 --format=%h refs/heads/third) L + fixup $(git log -1 --format=%h update-refs-with-merge) fixup! L # empty + update-ref refs/heads/third + pick $(git log -1 --format=%h HEAD~2) M + update-ref refs/heads/no-conflict-branch + merge -C $(git log -1 --format=%h HEAD~1) merge # merge + update-ref refs/heads/merge-branch + EOF + + test_must_fail git rebase -i --autosquash \ + --rebase-merges=rebase-cousins \ + --update-refs primary >todo && + + test_cmp expect todo && + + test_must_fail git -c rebase.autosquash=true \ + -c rebase.updaterefs=true \ + rebase -i \ + --rebase-merges=rebase-cousins \ + primary >todo && + + test_cmp expect todo + ) +' + +test_expect_success '--update-refs updates refs correctly' ' + git checkout -B update-refs no-conflict-branch && + git branch -f base HEAD~4 && + git branch -f first HEAD~3 && + git branch -f second HEAD~3 && + git branch -f third HEAD~1 && + test_commit extra2 fileX && + git commit --amend --fixup=L && + + git rebase -i --autosquash --update-refs primary 2>err && + + test_cmp_rev HEAD~3 refs/heads/first && + test_cmp_rev HEAD~3 refs/heads/second && + test_cmp_rev HEAD~1 refs/heads/third && + test_cmp_rev HEAD refs/heads/no-conflict-branch && + + cat >expect <<-\EOF && + Successfully rebased and updated refs/heads/update-refs. + Updated the following refs with --update-refs: + refs/heads/first + refs/heads/no-conflict-branch + refs/heads/second + refs/heads/third + EOF + + # Clear "Rebasing (X/Y)" progress lines and drop leading tabs. + sed -e "s/Rebasing.*Successfully/Successfully/g" -e "s/^\t//g" \ + <err >err.trimmed && + test_cmp expect err.trimmed +' + +test_expect_success 'respect user edits to update-ref steps' ' + git checkout -B update-refs-break no-conflict-branch && + git branch -f base HEAD~4 && + git branch -f first HEAD~3 && + git branch -f second HEAD~3 && + git branch -f third HEAD~1 && + git branch -f unseen base && + + # First, we will add breaks to the expected todo file + cat >fake-todo-1 <<-EOF && + pick $(git rev-parse HEAD~3) + break + update-ref refs/heads/second + update-ref refs/heads/first + + pick $(git rev-parse HEAD~2) + pick $(git rev-parse HEAD~1) + update-ref refs/heads/third + + pick $(git rev-parse HEAD) + update-ref refs/heads/no-conflict-branch + EOF + + # Second, we will drop some update-refs commands (and move one) + cat >fake-todo-2 <<-EOF && + update-ref refs/heads/second + + pick $(git rev-parse HEAD~2) + update-ref refs/heads/third + pick $(git rev-parse HEAD~1) + break + + pick $(git rev-parse HEAD) + EOF + + # Third, we will: + # * insert a new one (new-branch), + # * re-add an old one (first), and + # * add a second instance of a previously-stored one (second) + cat >fake-todo-3 <<-EOF && + update-ref refs/heads/unseen + update-ref refs/heads/new-branch + pick $(git rev-parse HEAD) + update-ref refs/heads/first + update-ref refs/heads/second + EOF + + ( + set_replace_editor fake-todo-1 && + git rebase -i --update-refs primary && + + # These branches are currently locked. + for b in first second third no-conflict-branch + do + test_must_fail git branch -f $b base || return 1 + done && + + set_replace_editor fake-todo-2 && + git rebase --edit-todo && + + # These branches are currently locked. + for b in second third + do + test_must_fail git branch -f $b base || return 1 + done && + + # These branches are currently unlocked for checkout. + for b in first no-conflict-branch + do + git worktree add wt-$b $b && + git worktree remove wt-$b || return 1 + done && + + git rebase --continue && + + set_replace_editor fake-todo-3 && + git rebase --edit-todo && + + # These branches are currently locked. + for b in second third first unseen + do + test_must_fail git branch -f $b base || return 1 + done && + + # These branches are currently unlocked for checkout. + for b in no-conflict-branch + do + git worktree add wt-$b $b && + git worktree remove wt-$b || return 1 + done && + + git rebase --continue + ) && + + test_cmp_rev HEAD~2 refs/heads/third && + test_cmp_rev HEAD~1 refs/heads/unseen && + test_cmp_rev HEAD~1 refs/heads/new-branch && + test_cmp_rev HEAD refs/heads/first && + test_cmp_rev HEAD refs/heads/second && + test_cmp_rev HEAD refs/heads/no-conflict-branch +' + +test_expect_success '--update-refs: check failed ref update' ' + git checkout -B update-refs-error no-conflict-branch && + git branch -f base HEAD~4 && + git branch -f first HEAD~3 && + git branch -f second HEAD~2 && + git branch -f third HEAD~1 && + + cat >fake-todo <<-EOF && + pick $(git rev-parse HEAD~3) + break + update-ref refs/heads/first + + pick $(git rev-parse HEAD~2) + update-ref refs/heads/second + + pick $(git rev-parse HEAD~1) + update-ref refs/heads/third + + pick $(git rev-parse HEAD) + update-ref refs/heads/no-conflict-branch + EOF + + ( + set_replace_editor fake-todo && + git rebase -i --update-refs base + ) && + + # At this point, the values of first, second, and third are + # recorded in the update-refs file. We will force-update the + # "second" ref, but "git branch -f" will not work because of + # the lock in the update-refs file. + git rev-parse third >.git/refs/heads/second && + + test_must_fail git rebase --continue 2>err && + grep "update_ref failed for ref '\''refs/heads/second'\''" err && + + cat >expect <<-\EOF && + Updated the following refs with --update-refs: + refs/heads/first + refs/heads/no-conflict-branch + refs/heads/third + Failed to update the following refs with --update-refs: + refs/heads/second + EOF + + # Clear "Rebasing (X/Y)" progress lines and drop leading tabs. + tail -n 6 err >err.last && + sed -e "s/Rebasing.*Successfully/Successfully/g" -e "s/^\t//g" \ + <err.last >err.trimmed && + test_cmp expect err.trimmed +' + # This must be the last test in this file test_expect_success '$EDITOR and friends are unchanged' ' test_editor_unchanged |