rebase: add --update-refs option
When working on a large feature, it can be helpful to break that feature into multiple smaller parts that become reviewed in sequence. During development or during review, a change to one part of the feature could affect multiple of these parts. An interactive rebase can help adjust the multi-part "story" of the branch. However, if there are branches tracking the different parts of the feature, then rebasing the entire list of commits can create commits not reachable from those "sub branches". It can take a manual step to update those branches. Add a new --update-refs option to 'git rebase -i' that adds 'update-ref <ref>' steps to the todo file whenever a commit that is being rebased is decorated with that <ref>. At the very end, the rebase process updates all of the listed refs to the values stored during the rebase operation. Be sure to iterate after any squashing or fixups are placed. Update the branch only after those squashes and fixups are complete. This allows a --fixup commit at the tip of the feature to apply correctly to the sub branch, even if it is fixing up the most-recent commit in that part. This change update the documentation and builtin to accept the --update-refs option as well as updating the todo file with the 'update-ref' commands. Tests are added to ensure that these todo commands are added in the correct locations. This change does _not_ include the actual behavior of tracking the updated refs and writing the new ref values at the end of the rebase process. That is deferred to a later change. Signed-off-by: Derrick Stolee <derrickstolee@github.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
parent
a97d79163e
commit
900b50c242
@ -609,6 +609,12 @@ provided. Otherwise an explicit `--no-reschedule-failed-exec` at the
|
|||||||
start would be overridden by the presence of
|
start would be overridden by the presence of
|
||||||
`rebase.rescheduleFailedExec=true` configuration.
|
`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.
|
||||||
|
|
||||||
INCOMPATIBLE OPTIONS
|
INCOMPATIBLE OPTIONS
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
@ -632,6 +638,7 @@ are incompatible with the following options:
|
|||||||
* --empty=
|
* --empty=
|
||||||
* --reapply-cherry-picks
|
* --reapply-cherry-picks
|
||||||
* --edit-todo
|
* --edit-todo
|
||||||
|
* --update-refs
|
||||||
* --root when used in combination with --onto
|
* --root when used in combination with --onto
|
||||||
|
|
||||||
In addition, the following pairs of options are incompatible:
|
In addition, the following pairs of options are incompatible:
|
||||||
|
@ -102,6 +102,7 @@ struct rebase_options {
|
|||||||
int reschedule_failed_exec;
|
int reschedule_failed_exec;
|
||||||
int reapply_cherry_picks;
|
int reapply_cherry_picks;
|
||||||
int fork_point;
|
int fork_point;
|
||||||
|
int update_refs;
|
||||||
};
|
};
|
||||||
|
|
||||||
#define REBASE_OPTIONS_INIT { \
|
#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,
|
ret = complete_action(the_repository, &replay, flags,
|
||||||
shortrevisions, opts->onto_name, opts->onto,
|
shortrevisions, opts->onto_name, opts->onto,
|
||||||
&opts->orig_head, &commands, opts->autosquash,
|
&opts->orig_head, &commands, opts->autosquash,
|
||||||
|
opts->update_refs,
|
||||||
&todo_list);
|
&todo_list);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1124,6 +1126,9 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
|
|||||||
OPT_BOOL(0, "autosquash", &options.autosquash,
|
OPT_BOOL(0, "autosquash", &options.autosquash,
|
||||||
N_("move commits that begin with "
|
N_("move commits that begin with "
|
||||||
"squash!/fixup! under -i")),
|
"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"),
|
{ OPTION_STRING, 'S', "gpg-sign", &gpg_sign, N_("key-id"),
|
||||||
N_("GPG-sign commits"),
|
N_("GPG-sign commits"),
|
||||||
PARSE_OPT_OPTARG, NULL, (intptr_t) "" },
|
PARSE_OPT_OPTARG, NULL, (intptr_t) "" },
|
||||||
|
107
sequencer.c
107
sequencer.c
@ -35,6 +35,7 @@
|
|||||||
#include "commit-reach.h"
|
#include "commit-reach.h"
|
||||||
#include "rebase-interactive.h"
|
#include "rebase-interactive.h"
|
||||||
#include "reset.h"
|
#include "reset.h"
|
||||||
|
#include "branch.h"
|
||||||
|
|
||||||
#define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
|
#define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
|
||||||
|
|
||||||
@ -5673,10 +5674,113 @@ static int skip_unnecessary_picks(struct repository *r,
|
|||||||
return 0;
|
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);
|
||||||
|
|
||||||
|
while (decoration) {
|
||||||
|
struct todo_item *item;
|
||||||
|
const char *path;
|
||||||
|
size_t base_offset = ctx->buf->len;
|
||||||
|
|
||||||
|
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 = oiddup(the_hash_algo->null_oid);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string_list_clear(&ctx.refs_to_oids, 1);
|
||||||
|
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,
|
int complete_action(struct repository *r, struct replay_opts *opts, unsigned flags,
|
||||||
const char *shortrevisions, const char *onto_name,
|
const char *shortrevisions, const char *onto_name,
|
||||||
struct commit *onto, const struct object_id *orig_head,
|
struct commit *onto, const struct object_id *orig_head,
|
||||||
struct string_list *commands, unsigned autosquash,
|
struct string_list *commands, unsigned autosquash,
|
||||||
|
unsigned update_refs,
|
||||||
struct todo_list *todo_list)
|
struct todo_list *todo_list)
|
||||||
{
|
{
|
||||||
char shortonto[GIT_MAX_HEXSZ + 1];
|
char shortonto[GIT_MAX_HEXSZ + 1];
|
||||||
@ -5695,6 +5799,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;
|
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))
|
if (autosquash && todo_list_rearrange_squash(todo_list))
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
|
@ -168,6 +168,7 @@ int complete_action(struct repository *r, struct replay_opts *opts, unsigned fla
|
|||||||
const char *shortrevisions, const char *onto_name,
|
const char *shortrevisions, const char *onto_name,
|
||||||
struct commit *onto, const struct object_id *orig_head,
|
struct commit *onto, const struct object_id *orig_head,
|
||||||
struct string_list *commands, unsigned autosquash,
|
struct string_list *commands, unsigned autosquash,
|
||||||
|
unsigned update_refs,
|
||||||
struct todo_list *todo_list);
|
struct todo_list *todo_list);
|
||||||
int todo_list_rearrange_squash(struct todo_list *todo_list);
|
int todo_list_rearrange_squash(struct todo_list *todo_list);
|
||||||
|
|
||||||
|
@ -164,4 +164,26 @@ test_expect_success 'refuse to overwrite when in error states' '
|
|||||||
done
|
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
|
test_done
|
||||||
|
@ -1743,6 +1743,80 @@ test_expect_success 'ORIG_HEAD is updated correctly' '
|
|||||||
test_cmp_rev ORIG_HEAD test-orig-head@{1}
|
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_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
|
||||||
|
)
|
||||||
|
'
|
||||||
|
|
||||||
# This must be the last test in this file
|
# This must be the last test in this file
|
||||||
test_expect_success '$EDITOR and friends are unchanged' '
|
test_expect_success '$EDITOR and friends are unchanged' '
|
||||||
test_editor_unchanged
|
test_editor_unchanged
|
||||||
|
Loading…
Reference in New Issue
Block a user