Merge branch 'ds/rebase-update-ref'

"git rebase -i" learns to update branches whose tip appear in the
rebased range with "--update-refs" option.
source: <pull.1247.v5.git.1658255624.gitgitgadget@gmail.com>

* ds/rebase-update-ref:
  sequencer: notify user of --update-refs activity
  sequencer: ignore HEAD ref under --update-refs
  rebase: add rebase.updateRefs config option
  sequencer: rewrite update-refs as user edits todo list
  rebase: update refs from 'update-ref' commands
  rebase: add --update-refs option
  sequencer: add update-ref command
  sequencer: define array with enum values
  rebase-interactive: update 'merge' description
  branch: consider refs under 'update-refs'
  t2407: test branches currently using apply backend
  t2407: test bisect and rebase as black-boxes
This commit is contained in:
Junio C Hamano 2022-08-01 09:58:38 -07:00
commit 3d8e3dc4fc
10 changed files with 895 additions and 44 deletions

View File

@ -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

View File

@ -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:

View File

@ -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(&current_checked_out_branches,
item->string,
xstrdup(wt->path));
free(old);
}
string_list_clear(&update_refs, 1);
}
}
free_worktrees(worktrees);

View File

@ -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) "" },

View File

@ -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;
}

View 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;
@ -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;
}

View File

@ -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 */

View File

@ -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"
}

View File

@ -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 fake-1 HEAD 2>err &&
grep "cannot force update the branch '\''fake-1'\'' checked out at.*wt-3" err
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 &&
# 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

View File

@ -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