object-name: explain why <rev>~N fails in a shallow clone
Asking for a commit's ancestor with <rev>~N in a shallow clone that does not have N commits of history locally fails with a bare "is not a commit" error, with no indication that the repository being shallow is the reason, or what to do about it. Add a hint, shown when a walk runs out of parents exactly at a recorded shallow boundary, not merely because the repository happens to be shallow elsewhere. When the revision <rev> is given as a remote-tracking branch, the remote and branch are exactly named in the suggested command, found by reverse mapping through the remote's real refspec instead of assuming a refs/remotes/<remote>/ layout, since a refspec can put branches anywhere. For <rev>~N it suggests the exact --deepen needed, accounting for history already present. For <rev>^N it always suggests --deepen=1: a shallow boundary commit has no parents recorded at all, so one more generation reveals its complete real parent list regardless of which index was asked for. The hint only suggests that older history might exist. A recorded shallow boundary does not distinguish a depth cutoff from a commit that is the true root: cloning with a depth that happens to exactly match the available history still records that root commit as a boundary, and only the remote knows for certain whether deepening would fetch anything new. The advice is threaded through GET_OID_QUIETLY so it is not shown during the internal re-resolution some commands do while building a better error message, which would otherwise print it twice for the same failing argument. A chained <rev>^^^ resolves one parent at a time, and each step re-resolves everything before it through a fresh, always non-quiet lookup, so a chained caret landing past a shallow boundary printed the hint twice instead of once. Thread the quiet bit through that inner lookup too, so it stays quiet exactly when the outer resolution asked it to. Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>seen
parent
e9019fcafe
commit
f8bc48ea42
|
|
@ -119,6 +119,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
|
|
@ -79,6 +79,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
|
|
@ -46,6 +46,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"
|
||||
|
|
@ -825,13 +826,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;
|
||||
|
||||
|
|
@ -852,19 +960,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);
|
||||
|
|
@ -872,8 +983,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);
|
||||
|
|
@ -1120,9 +1237,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);
|
||||
|
|
|
|||
|
|
@ -94,6 +94,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 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