Rebasing normally updates the current branch to the rewritten version. If any other branches point to commits rewritten along the way, those remain untouched. This commit adds an `--update-branches` option, which instructs the command to update any such branches that it encounters to point to the rewritten versions of those commits. Signed-off-by: Warren He <wh109@xxxxxxxxx> --- Documentation/git-rebase.txt | 9 ++++ builtin/rebase.c | 11 ++++- sequencer.c | 58 +++++++++++++++++++++++- sequencer.h | 6 ++- t/t3431-rebase-update-branches.sh | 94 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 173 insertions(+), 5 deletions(-) create mode 100755 t/t3431-rebase-update-branches.sh diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt index 6156609..b650a8f 100644 --- a/Documentation/git-rebase.txt +++ b/Documentation/git-rebase.txt @@ -246,6 +246,13 @@ leave out at most one of A and B, in which case it defaults to HEAD. + See also INCOMPATIBLE OPTIONS below. +--update-branches:: + If there are branch refs that point to commits that will be + reapplied, add shell commands to the todo list to update those + refs to point to the commits in the final history. ++ +See also INCOMPATIBLE OPTIONS below. + --allow-empty-message:: By default, rebasing commits with an empty message will fail. This option overrides that behavior, allowing commits with empty @@ -535,6 +542,7 @@ are incompatible with the following options: * --interactive * --exec * --keep-empty + * --update-branches * --edit-todo * --root when used in combination with --onto @@ -543,6 +551,7 @@ In addition, the following pairs of options are incompatible: * --preserve-merges and --interactive * --preserve-merges and --signoff * --preserve-merges and --rebase-merges + * --preserve-merges and --update-branches * --rebase-merges and --strategy * --rebase-merges and --strategy-option diff --git a/builtin/rebase.c b/builtin/rebase.c index 670096c..ab2308c 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -78,6 +78,7 @@ struct rebase_options { int signoff; int allow_rerere_autoupdate; int keep_empty; + int update_branches; int autosquash; char *gpg_sign_opt; int autostash; @@ -349,8 +350,8 @@ static int do_interactive_rebase(struct rebase_options *opts, unsigned flags) split_exec_commands(opts->cmd, &commands); ret = complete_action(the_repository, &replay, flags, - shortrevisions, opts->onto_name, opts->onto, head_hash, - &commands, opts->autosquash, &todo_list); + shortrevisions, opts->onto_name, opts->onto, opts->head_name, + head_hash, &commands, opts->autosquash, &todo_list); } string_list_clear(&commands, 0); @@ -375,6 +376,7 @@ static int run_rebase_interactive(struct rebase_options *opts, flags |= opts->rebase_merges ? TODO_LIST_REBASE_MERGES : 0; flags |= opts->rebase_cousins > 0 ? TODO_LIST_REBASE_COUSINS : 0; flags |= command == ACTION_SHORTEN_OIDS ? TODO_LIST_SHORTEN_IDS : 0; + flags |= opts->update_branches ? TODO_LIST_UPDATE_BRANCHES : 0; switch (command) { case ACTION_NONE: { @@ -1453,6 +1455,8 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) OPT_RERERE_AUTOUPDATE(&options.allow_rerere_autoupdate), OPT_BOOL('k', "keep-empty", &options.keep_empty, N_("preserve empty commits during rebase")), + OPT_BOOL(0, "update-branches", &options.update_branches, + N_("update branches that point to rebased commits")), OPT_BOOL(0, "autosquash", &options.autosquash, N_("move commits that begin with " "squash!/fixup! under -i")), @@ -1710,6 +1714,9 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) if (options.keep_empty) imply_interactive(&options, "--keep-empty"); + if (options.update_branches) + imply_interactive(&options, "--update-branches"); + if (gpg_sign) { free(options.gpg_sign_opt); options.gpg_sign_opt = xstrfmt("-S%s", gpg_sign); diff --git a/sequencer.c b/sequencer.c index 34ebf8e..0fe8452 100644 --- a/sequencer.c +++ b/sequencer.c @@ -4901,6 +4901,59 @@ void todo_list_add_exec_commands(struct todo_list *todo_list, todo_list->alloc = alloc; } +/* + * Add commands to update branch refs after the todo list would pick a commit + * that a branch ref points to. + */ +static void todo_list_add_branch_updates(struct repository *r, + struct todo_list *todo_list, + const char *head_name) +{ + struct strbuf *buf = &todo_list->buf; + /* watch out: items in here point into todo_list's buf, not its own */ + struct todo_list new_list = TODO_LIST_INIT; + int i; + + load_ref_decorations(NULL, 0); + + for (i = 0; i < todo_list->nr; i++) { + const struct todo_item *item = todo_list->items + i; + enum todo_command command = item->command; + const struct name_decoration *decoration; + + *append_new_todo(&new_list) = todo_list->items[i]; + + if (!(is_pick_or_similar(command) || command == TODO_MERGE)) + continue; + + decoration = get_name_decoration(&item->commit->object); + for (; decoration; decoration = decoration->next) { + size_t base_offset; + + /* + * (i) skip other refs like tags and remote refs + * (ii) rebase itself will update the current branch + * for us + */ + if (decoration->type != DECORATION_REF_LOCAL || + !strcmp(decoration->name, head_name)) + continue; + + base_offset = buf->len; + strbuf_addf(buf, "exec git branch -f %s\n", + prettify_refname(decoration->name)); + parse_insn_line(r, append_new_todo(&new_list), buf->buf, + buf->buf + base_offset, + buf->buf + buf->len - 1); + } + } + + SWAP(new_list.items, todo_list->items); + SWAP(new_list.nr, todo_list->nr); + SWAP(new_list.alloc, todo_list->alloc); + todo_list_release(&new_list); +} + static void todo_list_to_strbuf(struct repository *r, struct todo_list *todo_list, struct strbuf *buf, int num, unsigned flags) { @@ -5051,7 +5104,7 @@ static int skip_unnecessary_picks(struct repository *r, int complete_action(struct repository *r, struct replay_opts *opts, unsigned flags, const char *shortrevisions, const char *onto_name, - struct commit *onto, const char *orig_head, + struct commit *onto, const char *head_name, const char *orig_head, struct string_list *commands, unsigned autosquash, struct todo_list *todo_list) { @@ -5070,6 +5123,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 (flags & TODO_LIST_UPDATE_BRANCHES) + todo_list_add_branch_updates(r, todo_list, head_name); + if (autosquash && todo_list_rearrange_squash(todo_list)) return -1; diff --git a/sequencer.h b/sequencer.h index 6704acb..69c6f71 100644 --- a/sequencer.h +++ b/sequencer.h @@ -143,6 +143,7 @@ int sequencer_remove_state(struct replay_opts *opts); */ #define TODO_LIST_REBASE_COUSINS (1U << 4) #define TODO_LIST_APPEND_TODO_HELP (1U << 5) +#define TODO_LIST_UPDATE_BRANCHES (1U << 6) int sequencer_make_script(struct repository *r, struct strbuf *out, int argc, const char **argv, unsigned flags); @@ -152,8 +153,9 @@ void todo_list_add_exec_commands(struct todo_list *todo_list, int check_todo_list_from_file(struct repository *r); int complete_action(struct repository *r, struct replay_opts *opts, unsigned flags, const char *shortrevisions, const char *onto_name, - struct commit *onto, const char *orig_head, struct string_list *commands, - unsigned autosquash, struct todo_list *todo_list); + struct commit *onto, const char *head_name, const char *orig_head, + struct string_list *commands, unsigned autosquash, + struct todo_list *todo_list); int todo_list_rearrange_squash(struct todo_list *todo_list); /* diff --git a/t/t3431-rebase-update-branches.sh b/t/t3431-rebase-update-branches.sh new file mode 100755 index 0000000..b57b64a --- /dev/null +++ b/t/t3431-rebase-update-branches.sh @@ -0,0 +1,94 @@ +#!/bin/sh + +test_description='git rebase --update-branches + +This test runs git rebase, updating branch refs that point to commits +that are rebased. + +Initial setup: + + A - B (master) + |\ + | C (linear-early) + | \ + | D (linear-late) + |\ + | E (feat-e) + |\ \ + | F | (feat-f) + | \| + | G (interim) + | \ + | H (my-dev, my-hotfixes) + \ + I - J - fixup! I (fixup-early) + \ + K - fixup! J (fixup-late) +' +. ./test-lib.sh + +test_expect_success 'set up common' ' + test_commit A && + test_commit B +' + +test_expect_success 'set up linear' ' + git checkout -b linear-early A && + test_commit C && + git checkout -b linear-late && + test_commit D +' + +test_expect_success 'smoketest linear' ' + git rebase --update-branches master +' + +test_expect_success 'check linear' ' + test_cmp_rev linear-early HEAD^ +' + +test_expect_success 'set up merge' ' + git checkout -b feat-e A && + test_commit E && + git checkout -b feat-f A && + test_commit F && + git checkout -b interim && + test_merge G feat-e && + git checkout -b my-dev && + test_commit H && + git branch my-hotfixes +' + +test_expect_success 'smoketest merge' ' + git rebase -r --update-branches master +' + +test_expect_success 'check merge' ' + test_cmp_rev feat-e HEAD^^2 && + test_cmp_rev feat-f HEAD^^ && + test_cmp_rev interim HEAD^ && + test_cmp_rev my-hotfixes HEAD +' + +test_expect_success 'set up fixup' ' + git checkout -b fixup-early A && + test_commit I && + test_commit J && + test_commit "fixup! I" I.t II fixup-I && + git checkout -b fixup-late && + test_commit K && + test_commit "fixup! J" J.t JJ fixup-J +' + +test_expect_success 'smoketest fixup' ' + git rebase -i --autosquash --update-branches master +' + +test_expect_success 'check fixup' ' + test_cmp_rev fixup-early HEAD^ && + test_cmp_rev fixup-early^:I.t fixup-I:I.t && + test_cmp_rev fixup-early:J.t fixup-J:J.t && + test_cmp_rev HEAD:K.t K:K.t +' + +test_done -- 2.7.4