Merge branch 'hn/shallow-history-advice' into seen
Providing advice when ~<n> or ^<n> resolves to a commit whose parents are missing due to the repository being a shallow clone, instead of an unhelpful 'not a commit' error. * hn/shallow-history-advice: object-name: explain why <rev>~N fails in a shallow cloneseen
commit
bf970699c6
|
|
@ -136,6 +136,10 @@ all advice messages.
|
|||
give directions on how to proceed from the current state.
|
||||
sequencerInUse::
|
||||
Shown when a sequencer command is already in progress.
|
||||
shallowHistory::
|
||||
Shown when `<rev>~<n>` cannot resolve enough ancestors of
|
||||
`<rev>` because history stops at a shallow boundary, to
|
||||
suggest fetching more history.
|
||||
skippedCherryPicks::
|
||||
Shown when linkgit:git-rebase[1] skips a commit that has already
|
||||
been cherry-picked onto the upstream branch.
|
||||
|
|
|
|||
1
advice.c
1
advice.c
|
|
@ -82,6 +82,7 @@ static struct {
|
|||
[ADVICE_RM_HINTS] = { "rmHints" },
|
||||
[ADVICE_SEQUENCER_IN_USE] = { "sequencerInUse" },
|
||||
[ADVICE_SET_UPSTREAM_FAILURE] = { "setUpstreamFailure" },
|
||||
[ADVICE_SHALLOW_HISTORY] = { "shallowHistory" },
|
||||
[ADVICE_SKIPPED_CHERRY_PICKS] = { "skippedCherryPicks" },
|
||||
[ADVICE_SPARSE_INDEX_EXPANDED] = { "sparseIndexExpanded" },
|
||||
[ADVICE_STATUS_AHEAD_BEHIND_WARNING] = { "statusAheadBehindWarning" },
|
||||
|
|
|
|||
1
advice.h
1
advice.h
|
|
@ -49,6 +49,7 @@ enum advice_type {
|
|||
ADVICE_RM_HINTS,
|
||||
ADVICE_SEQUENCER_IN_USE,
|
||||
ADVICE_SET_UPSTREAM_FAILURE,
|
||||
ADVICE_SHALLOW_HISTORY,
|
||||
ADVICE_SKIPPED_CHERRY_PICKS,
|
||||
ADVICE_SPARSE_INDEX_EXPANDED,
|
||||
ADVICE_STATUS_AHEAD_BEHIND_WARNING,
|
||||
|
|
|
|||
131
object-name.c
131
object-name.c
|
|
@ -22,6 +22,7 @@
|
|||
#include "repo-settings.h"
|
||||
#include "repository.h"
|
||||
#include "setup.h"
|
||||
#include "shallow.h"
|
||||
#include "midx.h"
|
||||
#include "commit-reach.h"
|
||||
#include "date.h"
|
||||
|
|
@ -817,13 +818,120 @@ static int get_oid_basic(struct repository *r, const char *str, int len,
|
|||
return 0;
|
||||
}
|
||||
|
||||
struct remote_tracking_search {
|
||||
char *dst;
|
||||
char *remote_name;
|
||||
char *branch_name;
|
||||
};
|
||||
|
||||
static int search_remote_tracking(struct remote *remote, void *cb_data)
|
||||
{
|
||||
struct remote_tracking_search *search = cb_data;
|
||||
struct refspec_item query;
|
||||
|
||||
memset(&query, 0, sizeof(query));
|
||||
query.dst = search->dst;
|
||||
if (remote_find_tracking(remote, &query))
|
||||
return 0;
|
||||
search->remote_name = xstrdup(remote->name);
|
||||
search->branch_name = query.src;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* If "name" resolves to a remote-tracking ref, find which configured
|
||||
* remote it actually belongs to and what branch of that remote's it is,
|
||||
* by reverse mapping through each remote's real fetch refspec instead
|
||||
* of assuming the common "refs/remotes/<remote>/<branch>" layout,
|
||||
* since a remote's refspec need not put its branches there at all. On a
|
||||
* match, fill "remote_out" and "branch_out" with newly allocated copies
|
||||
* and return true. Otherwise leave them untouched and return false.
|
||||
*/
|
||||
static bool parse_remote_tracking_name(struct repository *r,
|
||||
const char *name, int namelen,
|
||||
char **remote_out, char **branch_out)
|
||||
{
|
||||
struct object_id oid;
|
||||
char *real_ref = NULL;
|
||||
struct remote_tracking_search search = { 0 };
|
||||
const char *branch_name;
|
||||
|
||||
if (!repo_dwim_ref(r, name, namelen, &oid, &real_ref, 0) || !real_ref)
|
||||
return false;
|
||||
|
||||
search.dst = real_ref;
|
||||
if (!for_each_remote(search_remote_tracking, &search)) {
|
||||
free(real_ref);
|
||||
return false;
|
||||
}
|
||||
free(real_ref);
|
||||
|
||||
branch_name = search.branch_name;
|
||||
skip_prefix(branch_name, "refs/heads/", &branch_name);
|
||||
*remote_out = search.remote_name;
|
||||
*branch_out = xstrdup(branch_name);
|
||||
free(search.branch_name);
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* When a "name~<n>" or "name^<n>" walk runs out of parents at "commit",
|
||||
* and that is because "commit" is where this shallow repository's history
|
||||
* was cut off (rather than commit being a real root commit), let the
|
||||
* user know that fetching more history might be what they are after.
|
||||
*
|
||||
* "suggested_depth" is the --deepen value to recommend. For "name^<n>"
|
||||
* this is always 1: deepening by one generation fetches "commit"'s real
|
||||
* parent list in full, whatever it turns out to contain, regardless of
|
||||
* which parent index <n> asked for.
|
||||
*/
|
||||
static void advise_if_shallow_cutoff(struct repository *r,
|
||||
const char *name, int namelen,
|
||||
struct commit *commit,
|
||||
unsigned lookup_flags,
|
||||
int suggested_depth)
|
||||
{
|
||||
char *remote = NULL, *branch = NULL;
|
||||
struct strbuf cmd = STRBUF_INIT;
|
||||
|
||||
if (lookup_flags & GET_OID_QUIETLY)
|
||||
return;
|
||||
if (!is_repository_shallow(r))
|
||||
return;
|
||||
if (!commit_is_shallow_boundary(r, &commit->object.oid))
|
||||
return;
|
||||
|
||||
if (parse_remote_tracking_name(r, name, namelen, &remote, &branch))
|
||||
strbuf_addf(&cmd, "git fetch --deepen=%d %s %s",
|
||||
suggested_depth, remote, branch);
|
||||
else
|
||||
strbuf_addf(&cmd, "git fetch --deepen=%d <remote> <branch>",
|
||||
suggested_depth);
|
||||
free(remote);
|
||||
free(branch);
|
||||
|
||||
advise_if_enabled(ADVICE_SHALLOW_HISTORY,
|
||||
_("'%.*s' does not have that many ancestors locally.\n"
|
||||
"History stops at %s because this repository is a\n"
|
||||
"shallow clone, and might have more history upstream.\n"
|
||||
"To check, try:\n"
|
||||
"\n"
|
||||
" %s"),
|
||||
namelen, name,
|
||||
repo_find_unique_abbrev(r, &commit->object.oid, DEFAULT_ABBREV),
|
||||
cmd.buf);
|
||||
strbuf_release(&cmd);
|
||||
}
|
||||
|
||||
static enum get_oid_result get_parent(struct repository *r,
|
||||
const char *name, int len,
|
||||
struct object_id *result, int idx)
|
||||
struct object_id *result, int idx,
|
||||
unsigned lookup_flags)
|
||||
{
|
||||
struct object_id oid;
|
||||
enum get_oid_result ret = get_oid_1(r, name, len, &oid,
|
||||
GET_OID_COMMITTISH);
|
||||
GET_OID_COMMITTISH |
|
||||
(lookup_flags & GET_OID_QUIETLY));
|
||||
struct commit *commit;
|
||||
struct commit_list *p;
|
||||
|
||||
|
|
@ -844,19 +952,22 @@ static enum get_oid_result get_parent(struct repository *r,
|
|||
}
|
||||
p = p->next;
|
||||
}
|
||||
advise_if_shallow_cutoff(r, name, len, commit, lookup_flags, 1);
|
||||
return MISSING_OBJECT;
|
||||
}
|
||||
|
||||
static enum get_oid_result get_nth_ancestor(struct repository *r,
|
||||
const char *name, int len,
|
||||
struct object_id *result,
|
||||
int generation)
|
||||
int generation,
|
||||
unsigned lookup_flags)
|
||||
{
|
||||
struct object_id oid;
|
||||
struct commit *commit;
|
||||
int ret;
|
||||
|
||||
ret = get_oid_1(r, name, len, &oid, GET_OID_COMMITTISH);
|
||||
ret = get_oid_1(r, name, len, &oid,
|
||||
GET_OID_COMMITTISH | (lookup_flags & GET_OID_QUIETLY));
|
||||
if (ret)
|
||||
return ret;
|
||||
commit = lookup_commit_reference(r, &oid);
|
||||
|
|
@ -864,8 +975,14 @@ static enum get_oid_result get_nth_ancestor(struct repository *r,
|
|||
return MISSING_OBJECT;
|
||||
|
||||
while (generation--) {
|
||||
if (repo_parse_commit(r, commit) || !commit->parents)
|
||||
if (repo_parse_commit(r, commit))
|
||||
return MISSING_OBJECT;
|
||||
if (!commit->parents) {
|
||||
/* Remaining "generation" plus this failed step is the actual gap. */
|
||||
advise_if_shallow_cutoff(r, name, len, commit,
|
||||
lookup_flags, generation + 1);
|
||||
return MISSING_OBJECT;
|
||||
}
|
||||
commit = commit->parents->item;
|
||||
}
|
||||
oidcpy(result, &commit->object.oid);
|
||||
|
|
@ -1112,9 +1229,9 @@ static enum get_oid_result get_oid_1(struct repository *r,
|
|||
else if (num > INT_MAX)
|
||||
return MISSING_OBJECT;
|
||||
if (has_suffix == '^')
|
||||
return get_parent(r, name, len1, oid, num);
|
||||
return get_parent(r, name, len1, oid, num, lookup_flags);
|
||||
/* else if (has_suffix == '~') -- goes without saying */
|
||||
return get_nth_ancestor(r, name, len1, oid, num);
|
||||
return get_nth_ancestor(r, name, len1, oid, num, lookup_flags);
|
||||
}
|
||||
|
||||
ret = peel_onion(r, name, len, oid, lookup_flags);
|
||||
|
|
|
|||
|
|
@ -92,6 +92,14 @@ int is_repository_shallow(struct repository *r)
|
|||
return r->parsed_objects->is_shallow;
|
||||
}
|
||||
|
||||
int commit_is_shallow_boundary(struct repository *r,
|
||||
const struct object_id *oid)
|
||||
{
|
||||
struct commit_graft *graft = lookup_commit_graft(r, oid);
|
||||
|
||||
return graft && graft->nr_parent == -1;
|
||||
}
|
||||
|
||||
static void reset_repository_shallow(struct repository *r)
|
||||
{
|
||||
r->parsed_objects->is_shallow = -1;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,14 @@ int register_shallow(struct repository *r, const struct object_id *oid);
|
|||
int unregister_shallow(const struct object_id *oid);
|
||||
int is_repository_shallow(struct repository *r);
|
||||
|
||||
/*
|
||||
* True if "oid" is where this shallow repository's history was cut off,
|
||||
* i.e. a shallow-graft commit with no parents recorded locally, as
|
||||
* opposed to a commit that is actually a root.
|
||||
*/
|
||||
int commit_is_shallow_boundary(struct repository *r,
|
||||
const struct object_id *oid);
|
||||
|
||||
/*
|
||||
* Lock for updating the $GIT_DIR/shallow file.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -193,6 +193,126 @@ test_expect_success 'rev-parse --is-shallow-repository in non-shallow repo' '
|
|||
test_cmp expect actual
|
||||
'
|
||||
|
||||
check_shallow_history_advice () {
|
||||
name=$1 oid=$2 cmd=$3 &&
|
||||
grep '^hint:' err >actual &&
|
||||
cat >expect <<-EOF &&
|
||||
hint: '$name' does not have that many ancestors locally.
|
||||
hint: History stops at $oid because this repository is a
|
||||
hint: shallow clone, and might have more history upstream.
|
||||
hint: To check, try:
|
||||
hint:
|
||||
hint: $cmd
|
||||
hint: Disable this message with "git config set --global advice.shallowHistory false"
|
||||
EOF
|
||||
test_cmp expect actual
|
||||
}
|
||||
|
||||
test_expect_success 'shallowHistory advice on ~N beyond shallow boundary' '
|
||||
test_commit shallow_advice_1 &&
|
||||
test_commit shallow_advice_2 &&
|
||||
git clone --no-local --depth=1 --branch main --single-branch \
|
||||
.git shallow-advice &&
|
||||
test_when_finished "rm -rf shallow-advice" &&
|
||||
oid=$(git -C shallow-advice rev-parse --short origin/main) &&
|
||||
test_must_fail git -C shallow-advice rev-parse origin/main~1 2>err &&
|
||||
check_shallow_history_advice origin/main "$oid" \
|
||||
"git fetch --deepen=1 origin main"
|
||||
'
|
||||
|
||||
test_expect_success 'shallowHistory advice accounts for depth already present' '
|
||||
test_commit shallow_partial_1 &&
|
||||
test_commit shallow_partial_2 &&
|
||||
test_commit shallow_partial_3 &&
|
||||
test_commit shallow_partial_4 &&
|
||||
test_commit shallow_partial_5 &&
|
||||
test_commit shallow_partial_6 &&
|
||||
git clone --no-local --depth=3 --branch main --single-branch \
|
||||
.git shallow-advice-partial &&
|
||||
test_when_finished "rm -rf shallow-advice-partial" &&
|
||||
(
|
||||
cd shallow-advice-partial &&
|
||||
oid=$(git rev-parse --short origin/main~2) &&
|
||||
test_must_fail git rev-parse origin/main~5 2>err &&
|
||||
check_shallow_history_advice origin/main "$oid" \
|
||||
"git fetch --deepen=3 origin main" &&
|
||||
git fetch --deepen=3 origin &&
|
||||
git rev-parse origin/main~5 &&
|
||||
test_must_fail git rev-parse origin/main~6
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'shallowHistory advice on ^N (first parent) beyond shallow boundary' '
|
||||
test_commit shallow_caret_1 &&
|
||||
test_commit shallow_caret_2 &&
|
||||
git clone --no-local --depth=1 --branch main --single-branch \
|
||||
.git shallow-advice-caret &&
|
||||
test_when_finished "rm -rf shallow-advice-caret" &&
|
||||
oid=$(git -C shallow-advice-caret rev-parse --short origin/main) &&
|
||||
test_must_fail git -C shallow-advice-caret rev-parse origin/main^1 2>err &&
|
||||
check_shallow_history_advice origin/main "$oid" \
|
||||
"git fetch --deepen=1 origin main"
|
||||
'
|
||||
|
||||
test_expect_success 'shallowHistory advice on chained ^ shows the hint only once' '
|
||||
test_commit shallow_chain_1 &&
|
||||
git clone --no-local --depth=1 --branch main --single-branch \
|
||||
.git shallow-advice-chain &&
|
||||
test_when_finished "rm -rf shallow-advice-chain" &&
|
||||
oid=$(git -C shallow-advice-chain rev-parse --short origin/main) &&
|
||||
test_must_fail git -C shallow-advice-chain rev-parse origin/main^^ 2>err &&
|
||||
check_shallow_history_advice origin/main "$oid" \
|
||||
"git fetch --deepen=1 origin main"
|
||||
'
|
||||
|
||||
test_expect_success 'shallowHistory advice on ^N suggests deepen=1 even for a merge parent' '
|
||||
test_commit shallow_merge_base &&
|
||||
git checkout -q -b shallow-merge-side &&
|
||||
test_commit shallow_merge_side1 &&
|
||||
git checkout -q main &&
|
||||
test_commit shallow_merge_main1 &&
|
||||
git merge -q --no-ff shallow-merge-side -m "shallow merge commit" &&
|
||||
git clone --no-local --depth=1 --branch main --single-branch \
|
||||
.git shallow-advice-merge &&
|
||||
test_when_finished "rm -rf shallow-advice-merge" &&
|
||||
(
|
||||
cd shallow-advice-merge &&
|
||||
oid=$(git rev-parse --short origin/main) &&
|
||||
test_must_fail git rev-parse origin/main^2 2>err &&
|
||||
check_shallow_history_advice origin/main "$oid" \
|
||||
"git fetch --deepen=1 origin main" &&
|
||||
git fetch -q --deepen=1 origin &&
|
||||
git rev-parse origin/main^1 &&
|
||||
git rev-parse origin/main^2
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'shallowHistory advice can be disabled' '
|
||||
test_commit shallow_off_1 &&
|
||||
git clone --no-local --depth=1 --branch main --single-branch \
|
||||
.git shallow-advice-off &&
|
||||
test_when_finished "rm -rf shallow-advice-off" &&
|
||||
test_must_fail git -C shallow-advice-off \
|
||||
-c advice.shallowHistory=false rev-parse origin/main~1 2>err &&
|
||||
test_grep ! "^hint:" err
|
||||
'
|
||||
|
||||
test_expect_success 'shallowHistory advice not shown for a non-shallow repository' '
|
||||
test_must_fail git rev-parse HEAD~100000 2>err &&
|
||||
test_grep ! "^hint:" err
|
||||
'
|
||||
|
||||
test_expect_success 'shallowHistory advice not shown when resolution succeeds' '
|
||||
test_commit shallow_ok_1 &&
|
||||
test_commit shallow_ok_2 &&
|
||||
test_commit shallow_ok_3 &&
|
||||
git clone --no-local --depth=3 --branch main --single-branch \
|
||||
.git shallow-advice-ok &&
|
||||
test_when_finished "rm -rf shallow-advice-ok" &&
|
||||
git -C shallow-advice-ok rev-parse origin/main~1 >actual 2>err &&
|
||||
test_grep ! "^hint:" err
|
||||
'
|
||||
|
||||
test_expect_success 'rev-parse --show-object-format in repo' '
|
||||
test_oid algo >expect &&
|
||||
git rev-parse --show-object-format >actual &&
|
||||
|
|
|
|||
Loading…
Reference in New Issue