mirror of
https://github.com/git/git.git
synced 2026-08-08 17:11:48 +00:00
history: protect branches when squashing a range
A local branch that descends from the selected graph without containing its tip cannot be replayed as a descendant of the squashed commit. Find those branches with ref-filter before creating any replacement objects and refuse the operation unless --update-refs=head was requested. Limit this protection to local branches, matching the refs that the default history rewrite mode updates; tags and remote-tracking refs remain untouched. Sort the blocking refs and print their short branch names so the user can decide whether to move them or leave them behind. Add advice.historyUpdateRefs for the hint that points to --update-refs=head. Helped-by: Phillip Wood <phillip.wood@dunelm.org.uk> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
committed by
Junio C Hamano
parent
044a87ed3f
commit
8a709ef6d2
@@ -59,6 +59,10 @@ all advice messages.
|
||||
forceDeleteBranch::
|
||||
Shown when the user tries to delete a not fully merged
|
||||
branch without the force option set.
|
||||
historyUpdateRefs::
|
||||
Shown when `git history squash` refuses because a local branch
|
||||
cannot be rewritten as a descendant of the squashed commit, to
|
||||
tell the user about `--update-refs=head`.
|
||||
ignoredHook::
|
||||
Shown when a hook is ignored because the hook is not
|
||||
set as executable.
|
||||
|
||||
1
advice.c
1
advice.c
@@ -58,6 +58,7 @@ static struct {
|
||||
[ADVICE_FETCH_SHOW_FORCED_UPDATES] = { "fetchShowForcedUpdates" },
|
||||
[ADVICE_FORCE_DELETE_BRANCH] = { "forceDeleteBranch" },
|
||||
[ADVICE_GRAFT_FILE_DEPRECATED] = { "graftFileDeprecated" },
|
||||
[ADVICE_HISTORY_UPDATE_REFS] = { "historyUpdateRefs" },
|
||||
[ADVICE_IGNORED_HOOK] = { "ignoredHook" },
|
||||
[ADVICE_IMPLICIT_IDENTITY] = { "implicitIdentity" },
|
||||
[ADVICE_MERGE_CONFLICT] = { "mergeConflict" },
|
||||
|
||||
1
advice.h
1
advice.h
@@ -25,6 +25,7 @@ enum advice_type {
|
||||
ADVICE_FETCH_SHOW_FORCED_UPDATES,
|
||||
ADVICE_FORCE_DELETE_BRANCH,
|
||||
ADVICE_GRAFT_FILE_DEPRECATED,
|
||||
ADVICE_HISTORY_UPDATE_REFS,
|
||||
ADVICE_IGNORED_HOOK,
|
||||
ADVICE_IMPLICIT_IDENTITY,
|
||||
ADVICE_MERGE_CONFLICT,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#define USE_THE_REPOSITORY_VARIABLE
|
||||
|
||||
#include "builtin.h"
|
||||
#include "advice.h"
|
||||
#include "cache-tree.h"
|
||||
#include "commit.h"
|
||||
#include "commit-reach.h"
|
||||
@@ -16,10 +17,12 @@
|
||||
#include "path.h"
|
||||
#include "read-cache.h"
|
||||
#include "refs.h"
|
||||
#include "ref-filter.h"
|
||||
#include "replay.h"
|
||||
#include "reset.h"
|
||||
#include "revision.h"
|
||||
#include "sequencer.h"
|
||||
#include "string-list.h"
|
||||
#include "strvec.h"
|
||||
#include "tree.h"
|
||||
#include "tree-walk.h"
|
||||
@@ -1061,6 +1064,7 @@ static int setup_squash_revisions(struct repository *repo,
|
||||
* of the oldest commit.
|
||||
*/
|
||||
static int resolve_squash_range(struct repository *repo,
|
||||
bool update_branches,
|
||||
int argc, const char **argv,
|
||||
struct commit **oldest_out,
|
||||
struct commit **tip_out)
|
||||
@@ -1069,6 +1073,8 @@ static int resolve_squash_range(struct repository *repo,
|
||||
struct commit *commit, *oldest = NULL, *tip = NULL;
|
||||
int ret, tip_count = 0;
|
||||
bool walk_started = false;
|
||||
struct ref_filter filter = REF_FILTER_INIT;
|
||||
struct ref_array refs = { 0 };
|
||||
|
||||
ret = setup_squash_revisions(repo, argc, argv, &revs);
|
||||
if (ret < 0)
|
||||
@@ -1101,9 +1107,12 @@ static int resolve_squash_range(struct repository *repo,
|
||||
* Allow parents that match the parents of the
|
||||
* squashed commit.
|
||||
*/
|
||||
for (q = oldest->parents; !seen && q; q = q->next)
|
||||
if (p->item == q->item)
|
||||
for (q = oldest->parents; !seen && q; q = q->next) {
|
||||
if (p->item == q->item) {
|
||||
seen = true;
|
||||
commit_list_insert(commit, &filter.with_commit);
|
||||
}
|
||||
}
|
||||
if (!seen) {
|
||||
ret = error(_("parent %s of commit %s is "
|
||||
"outside the revision range"),
|
||||
@@ -1119,12 +1128,17 @@ static int resolve_squash_range(struct repository *repo,
|
||||
o->flags &= ~SQUASH_TIP;
|
||||
}
|
||||
}
|
||||
if (!oldest)
|
||||
if (!oldest) {
|
||||
commit_list_insert(commit, &filter.with_commit);
|
||||
oldest = commit;
|
||||
}
|
||||
tip = commit;
|
||||
tip->object.flags |= SQUASH_SEEN | SQUASH_TIP;
|
||||
tip_count++;
|
||||
}
|
||||
clear_object_flags(repo, SQUASH_SEEN | SQUASH_TIP);
|
||||
reset_revision_walk();
|
||||
walk_started = false;
|
||||
|
||||
if (!tip_count) {
|
||||
ret = error(_("the revision range is empty"));
|
||||
@@ -1141,6 +1155,49 @@ static int resolve_squash_range(struct repository *repo,
|
||||
BUG("an in-range commit must have a parent");
|
||||
}
|
||||
|
||||
commit_list_insert(tip, &filter.no_commit);
|
||||
filter.kind = FILTER_REFS_BRANCHES;
|
||||
if (update_branches &&
|
||||
filter_refs(&refs, &filter, filter.kind)) {
|
||||
ret = error(_("could not filter refs"));
|
||||
goto out;
|
||||
}
|
||||
if (refs.nr) {
|
||||
struct ref_format format = REF_FORMAT_INIT;
|
||||
struct ref_sorting *sorting;
|
||||
struct string_list sorting_options = STRING_LIST_INIT_DUP;
|
||||
struct strbuf branches = STRBUF_INIT;
|
||||
struct strbuf err = STRBUF_INIT;
|
||||
|
||||
format.format = "%(refname:short)";
|
||||
if (verify_ref_format(&format))
|
||||
BUG("invalid branch format");
|
||||
string_list_append(&sorting_options, "refname");
|
||||
sorting = ref_sorting_options(&sorting_options);
|
||||
ref_array_sort(sorting, &refs);
|
||||
for (int i = 0; i < refs.nr; i++) {
|
||||
strbuf_reset(&err);
|
||||
strbuf_addstr(&branches, "\n ");
|
||||
if (format_ref_array_item(refs.items[i], &format,
|
||||
&branches, &err))
|
||||
BUG("could not format branch name: %s", err.buf);
|
||||
}
|
||||
/*
|
||||
* TODO: also check HEADS from other worktrees.
|
||||
*/
|
||||
ret = error(_("the following branches cannot be rewritten as "
|
||||
"descendants of the squashed commit:%s"), branches.buf);
|
||||
advise_if_enabled(ADVICE_HISTORY_UPDATE_REFS,
|
||||
_("Use --update-refs=head to rewrite only "
|
||||
"the current branch and leave such branches "
|
||||
"untouched."));
|
||||
strbuf_release(&err);
|
||||
strbuf_release(&branches);
|
||||
ref_sorting_release(sorting);
|
||||
string_list_clear(&sorting_options, 0);
|
||||
goto out;
|
||||
}
|
||||
|
||||
*oldest_out = oldest;
|
||||
*tip_out = tip;
|
||||
ret = 0;
|
||||
@@ -1150,6 +1207,8 @@ out:
|
||||
if (walk_started)
|
||||
reset_revision_walk();
|
||||
release_revisions(&revs);
|
||||
ref_filter_clear(&filter);
|
||||
ref_array_clear(&refs);
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -1183,8 +1242,11 @@ static int cmd_history_squash(int argc,
|
||||
if (argc < 2)
|
||||
return error(_("command expects a revision range"));
|
||||
repo_config(repo, git_default_config, NULL);
|
||||
if (action == REF_ACTION_DEFAULT)
|
||||
action = REF_ACTION_BRANCHES;
|
||||
|
||||
ret = resolve_squash_range(repo, argc, argv, &oldest, &tip);
|
||||
ret = resolve_squash_range(repo, action == REF_ACTION_BRANCHES,
|
||||
argc, argv, &oldest, &tip);
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
|
||||
|
||||
@@ -62,4 +62,43 @@ test_expect_success 'rejects a merge parent outside the range' '
|
||||
test_grep "parent .* of commit .* is outside the revision range" err
|
||||
'
|
||||
|
||||
test_expect_success 'prints branches that cannot follow the squash' '
|
||||
test_when_finished \
|
||||
"git switch -f $GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME; \
|
||||
git branch -D feature" &&
|
||||
git checkout -f -b feature start &&
|
||||
test_commit C1 &&
|
||||
test_commit C2 &&
|
||||
git checkout -b topic-1 start &&
|
||||
test_commit C3 &&
|
||||
test_commit C4 &&
|
||||
git checkout C3 &&
|
||||
test_commit C5 &&
|
||||
git checkout feature &&
|
||||
git merge C5 &&
|
||||
test_commit C6 &&
|
||||
git checkout -b topic-2 C2 &&
|
||||
test_commit C7 &&
|
||||
git checkout feature &&
|
||||
|
||||
test_must_fail git history squash start.. 2>err &&
|
||||
test_grep "^error: the following branches cannot be rewritten" err &&
|
||||
test_grep "^ topic-1$" err &&
|
||||
test_grep "^ topic-2$" err &&
|
||||
test_grep "^hint: .* --update-refs=head" err
|
||||
'
|
||||
|
||||
test_expect_success 'advice.historyUpdateRefs silences the hint' '
|
||||
git reset --hard three &&
|
||||
git branch -f mid HEAD~1 &&
|
||||
|
||||
test_must_fail git -c advice.historyUpdateRefs=false \
|
||||
history squash start.. 2>err &&
|
||||
test_grep "^error: the following branches cannot be rewritten" err &&
|
||||
test_grep "^ mid$" err &&
|
||||
test_grep ! "hint:" err &&
|
||||
|
||||
git branch -D mid
|
||||
'
|
||||
|
||||
test_done
|
||||
|
||||
Reference in New Issue
Block a user