From: Jeff King <hidden> Date: 2021-09-24 18:30:19
I recently ran into a situation where dealing with a corrupted
repository was more confusing than necessary, because Git by default
ignores corrupted refs in many commands.
A while ago we introduced GIT_REF_PARANOIA, which works by including
broken refs in iteration, which then typically causes later operations
to fail (e.g., during repacking, you'd prefer to barf loudly when trying
to access the missing object rather than incorrectly assume the objects
from the broken ref aren't reachable).
I think this is a better default for Git to have in general, not just
for a few select operations (we turn it on by default for pruning and
some repacks). We shouldn't see corruptions in general, and complaining
loudly when we do is the safest option. The reason we held back when the
knob was introduced was mostly out of deference to the historical
behavior.
So this series started as a patch to just flip that default, but I found
some interesting things:
- there are a couple of tests that get confused. IMHO this is
vindicating the idea of flipping the default, beacuse in each case
these tests were poorly written (either corruptions they didn't
realize they had, or doing questionable operations on an incomplete
set of refs)
- the existing GIT_REF_PARANOIA is over-eager to complain about
dangling symrefs, even though they're perfectly fine
- as usual, there was some obvious cleanup along the way. ;)
Even if you don't buy the argument that we should flip the default, I
think everything up through patch 11 is a worthwhile cleanup on its own.
Note that this conflicts with jt/no-abuse-alternate-odb-for-submodules,
since it is touching the innards of DO_FOR_EACH_REF_INCLUDE_BROKEN, too.
I left a note on that series about how I think that could be reconciled
(i.e., the conflict is just around how the code is written, and not
inherent to the goals).
In the end I left GIT_REF_PARANOIA as a knob, just defaulting to "1". I
think it's possibly useful as an escape hatch when dealing with a
corrupt repo. But we _could_ go all the way and basically drop
DO_FOR_EACH_REF_INCLUDE_BROKEN's do-we-have-the-object check entirely.
That would totally sever the relationship between the ref store and the
object store, which would make things conceptually a lot simpler (and I
saw was discussed in some of those earlier threads).
Just a breakdown of the series:
[01/16]: t7900: clean up some more broken refs
[02/16]: t5516: don't use HEAD ref for invalid ref-deletion tests
[03/16]: t5600: provide detached HEAD for corruption failures
[04/16]: t5312: drop "verbose" helper
[05/16]: t5312: create bogus ref as necessary
[06/16]: t5312: test non-destructive repack
[07/16]: t5312: be more assertive about command failure
Test cleanups. Necessary for the default flip, but I think each
stands on its own.
[08/16]: refs-internal.h: move DO_FOR_EACH_* flags next to each other
[09/16]: refs-internal.h: reorganize DO_FOR_EACH_* flag documentation
Cleanup of existing features.
[10/16]: refs: add DO_FOR_EACH_OMIT_DANGLING_SYMREFS flag
[11/16]: refs: omit dangling symrefs when using GIT_REF_PARANOIA
Fixing the current over-eager behavior of GIT_REF_PARANOIA.
[12/16]: refs: turn on GIT_REF_PARANOIA by default
The actual flip.
[13/16]: repack, prune: drop GIT_REF_PARANOIA settings
[14/16]: ref-filter: stop setting FILTER_REFS_INCLUDE_BROKEN
[15/16]: ref-filter: drop broken-ref code entirely
[16/16]: refs: drop "broken" flag from for_each_fullref_in()
Some small cleanups we can do as a result.
Documentation/git.txt | 19 ++++++------
builtin/branch.c | 2 +-
builtin/for-each-ref.c | 2 +-
builtin/prune.c | 1 -
builtin/repack.c | 3 --
builtin/rev-parse.c | 4 +--
cache.h | 8 -----
environment.c | 1 -
ls-refs.c | 2 +-
ref-filter.c | 22 ++++++--------
ref-filter.h | 1 -
refs.c | 42 +++++++++++++-------------
refs.h | 9 ++----
refs/files-backend.c | 5 ++++
refs/refs-internal.h | 56 ++++++++++++++++++++++-------------
revision.c | 2 +-
t/t1430-bad-ref-name.sh | 2 +-
t/t5312-prune-corruption.sh | 48 ++++++++++++++++++++++--------
t/t5516-fetch-push.sh | 19 ++++++------
t/t5600-clone-fail-cleanup.sh | 4 ++-
t/t7900-maintenance.sh | 6 +++-
21 files changed, 142 insertions(+), 116 deletions(-)
-Peff
From: Jeff King <hidden> Date: 2021-09-24 18:32:24
The "incremental-repack task" test replaces the object directory with a
known state. As a result, some of our refs point to objects that are not
included in that state.
Commit 3cf5f221be (t7900: clean up some broken refs, 2021-01-19) cleaned
up some of those (that were causing warnings to stderr from the
maintenance process). But there are a few more that were missed. These
aren't hurting anything for now, but it's certainly an unexpected state
to leave the test repository in, and it will become a problem if repack
ever gets more picky about broken refs.
Let's clean up those additional refs (which are all in refs/remotes,
with nothing there that isn't broken), and add an extra "for-each-ref"
call to assert that we've got everything.
Signed-off-by: Jeff King <redacted>
---
t/t7900-maintenance.sh | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
@@ -277,7 +277,7 @@ test_expect_success 'incremental-repack task' '# Delete refs that have not been repacked in these packs.gitfor-each-ref--format="delete %(refname)"\-refs/prefetchrefs/tags>refs&&+refs/prefetchrefs/tagsrefs/remotes>refs&&gitupdate-ref--stdin<refs&&# Replace the object directory with this pack layout.
@@ -286,6 +286,10 @@ test_expect_success 'incremental-repack task' 'ls$packDir/*.pack>packs-before&&test_line_count=3packs-before&&+# make sure we do not have any broken refs that were+# missed in the deletion above+gitfor-each-ref&&+# the job repacks the two into a new pack, but does not# delete the old ones.gitmaintenancerun--task=incremental-repack&&
From: Jeff King <hidden> Date: 2021-09-24 18:33:05
A few tests in t5516 want to assert that we can delete a corrupted ref
whose pointed-to object is missing. They do so by using the "main"
branch, which is also pointed to by HEAD.
This does work, but only because of a subtle assumption about the
implementation. We do not block the deletion because of the invalid ref,
but we _also_ do not notice that the deleted branch is pointed to by
HEAD. And so the safety rule of "do not allow HEAD to be deleted in a
non-bare repository" does not kick in, and the test passes.
Let's instead use a non-HEAD branch. That still tests what we care about
here (deleting a corrupt ref), but without implicitly depending on our
failure to notice that we're deleting HEAD. That will future proof the
test against that behavior changing.
Signed-off-by: Jeff King <redacted>
---
t/t5516-fetch-push.sh | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
@@ -662,10 +662,10 @@ test_expect_success 'push does not update local refs on failure' ' test_expect_success'allow deleting an invalid remote ref''-mk_testtestrepoheads/main&&+mk_testtestrepoheads/branch&&rm-ftestrepo/.git/objects/??/*&&-gitpushtestrepo:refs/heads/main&&-(cdtestrepo&&test_must_failgitrev-parse--verifyrefs/heads/main)+gitpushtestrepo:refs/heads/branch&&+(cdtestrepo&&test_must_failgitrev-parse--verifyrefs/heads/branch)'
From: Jeff King <hidden> Date: 2021-09-24 18:34:09
When checking how git-clone behaves when it fails, we stimulate some
failures by trying to do a clone from a local repository whose objects
have been removed. Because these clones use local optimizations, there's
a subtle dependency in how the corruption is handled on the sending
side.
If upload-pack does not show us the broken refs (which it does not
currently), then we see only HEAD (which is itself broken), and clone
that as a detached HEAD. When we try to write the ref, we notice that we
never got the object and bail.
But if upload-pack _does_ show us the broken refs (which it may in a
future patch), then we'll realize that HEAD is a symref and just write
that. You'd think we'd fail when writing out the refs themselves, but we
don't; we do a bulk write and skip the connectivity check because of our
--local optimizations. For the non-bare case, we do notice the problem
when we try to checkout. But for a bare repository, we unexpectedly
complete the clone successfully!
At first glance this may seem like a bug. But the whole point of those
local optimizations is to give up some safety for speed. If you want to
be careful, you should be using "--no-local", which would notice that
the pack did not transfer sufficient objects. We could do that in these
tests, but part of the point is for them to fail at specific moments
(and indeed, we have a later test that checks for transport failure).
However, we can make this less subtle and future-proof it against
changes on the upload-pack side by just having an explicit detached
HEAD in the corrupted repo. Now we'll fail as expected during the ref
write if any ref _or_ HEAD is corrupt, whether we're --bare or not.
Signed-off-by: Jeff King <redacted>
---
t/t5600-clone-fail-cleanup.sh | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
@@ -35,7 +35,9 @@ test_expect_success 'create a repo to clone' '' test_expect_success'create objects in repo for later corruption''-test_commit-Cfoofile+test_commit-Cfoofile&&+git-Cfoocheckout--detach&&+test_commit-Cfoodetached'# source repository given to git clone should be relative to the
From: Jeff King <hidden> Date: 2021-09-24 18:35:33
t5312 has several uses of the "verbose" helper, as described in
8ad1652418 (t5304: use helper to report failure of "test foo = bar",
2014-10-10). Back then the "-x" trace option for tests was new, and was
not as pleasant to use (e.g., some tests failed under "-x", we did not
support BASH_XTRACEFD, etc).
These days it is clear that "-x" is the preferred way to get extra
output, and we don't need to mark up individual tests. Let's get rid of
the uses of "verbose" here, as one step toward eradicating it totally.
Signed-off-by: Jeff King <redacted>
---
I've been tempted to do this tree-wide for a while, and I don't mind
doing that separately. But as I was touching these tests, it seemed like
a good time to do it here.
t/t5312-prune-corruption.sh | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
@@ -31,21 +31,21 @@ test_expect_success 'create history reachable only from a bogus-named ref' ' test_expect_success'pruning does not drop bogus object''test_when_finished"git hash-object -w -t commit saved"&&test_might_failgitprune--expire=now&&-verbosegitcat-file-e$bogus+gitcat-file-e$bogus' test_expect_success'put bogus object into pack''gittagreachable$bogus&&gitrepack-ad&&gittag-dreachable&&-verbosegitcat-file-e$bogus+gitcat-file-e$bogus' test_expect_success'destructive repack keeps packed object''test_might_failgitrepack-Ad--unpack-unreachable=now&&-verbosegitcat-file-e$bogus&&+gitcat-file-e$bogus&&test_might_failgitrepack-ad&&-verbosegitcat-file-e$bogus+gitcat-file-e$bogus'# subsequent tests will have different corruptions
@@ -78,7 +78,7 @@ test_expect_success 'create history with missing tip commit' ' test_expect_success'pruning with a corrupted tip does not drop history''test_when_finished"git hash-object -w -t commit saved"&&test_might_failgitprune--expire=now&&-verbosegitcat-file-e$recoverable+gitcat-file-e$recoverable' test_expect_success'pack-refs does not silently delete broken loose ref''
From: Jeff King <hidden> Date: 2021-09-24 18:36:20
Some tests in t5312 create an illegally-named ref, and then see how
various operations handle it. But between those operations, we also do
some more setup (e.g., repacking), and we are subtly depending on how
those setup steps react to the illegal ref.
To future-proof us against those behaviors changing, let's instead
create and clean up our bogus ref on demand in the tests that need it.
This has two small extra advantages:
- the tests are more stand-alone; we do not need an extra test to clean
up the ref before moving on to other parts of the script
- the creation and cleanup is together in one helper function. Because
these depend on touching the refs in the filesystem directly, they
may need to be tweaked for a world with alternate backends (they have
not been noticed so far in the reftable work because with a non-file
backend the tests don't fail; they simply become uninteresting noops
because the broken ref isn't read at all).
Signed-off-by: Jeff King <redacted>
---
t/t5312-prune-corruption.sh | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
@@ -18,18 +18,23 @@ test_expect_success 'disable reflogs' 'gitreflogexpire--expire=all--all'+create_bogus_ref(){+test_when_finished'rm -f .git/refs/heads/bogus..name'&&+echo$bogus>.git/refs/heads/bogus..name+}+ test_expect_success'create history reachable only from a bogus-named ref''test_tick&&gitcommit--allow-empty-mmain&&base=$(gitrev-parseHEAD)&&test_tick&&gitcommit--allow-empty-mbogus&&bogus=$(gitrev-parseHEAD)&&gitcat-filecommit$bogus>saved&&-echo$bogus>.git/refs/heads/bogus..name&&gitreset--hardHEAD^' test_expect_success'pruning does not drop bogus object''test_when_finished"git hash-object -w -t commit saved"&&+create_bogus_ref&&test_might_failgitprune--expire=now&&gitcat-file-e$bogus'
@@ -42,17 +47,13 @@ test_expect_success 'put bogus object into pack' '' test_expect_success'destructive repack keeps packed object''+create_bogus_ref&&test_might_failgitrepack-Ad--unpack-unreachable=now&&gitcat-file-e$bogus&&test_might_failgitrepack-ad&&gitcat-file-e$bogus'-# subsequent tests will have different corruptions-test_expect_success'clean up bogus ref''-rm.git/refs/heads/bogus..name-'-# We create two new objects here, "one" and "two". Our# main branch points to "two", which is deleted,# corrupting the repository. But we'd like to make sure
From: Jeff King <hidden> Date: 2021-09-24 18:36:46
In t5312, we create a state with a broken ref, and then make sure that
destructive repacks don't silently ignore the breakage (where a
destructive repack is one that might drop objects). But we don't check
the behavior of non-destructive repacks at all (i.e., ones where we'd
keep unreachable objects).
So let's add a test to confirm the current behavior, which is that
they are allowed (i.e., ignoring the breakage and considering any
objects it points to as unreachable). This may change in the future, but
we'd like for the test suite to alert us to that fact.
Signed-off-by: Jeff King <redacted>
---
t/t5312-prune-corruption.sh | 5 +++++
1 file changed, 5 insertions(+)
From: Jeff King <hidden> Date: 2021-09-24 18:37:14
When repacking or pruning in a corrupted repository, our tests in t5312
argue that it is OK to complete the operation or bail, as long as we
don't actually delete the objects pointed to by the corruption.
This isn't a wrong line of reasoning, but the tests are a bit permissive
by using test_might_fail. The fact is that we _do_ bail currently, and
if we ever stopped doing so, that would be worthy of a human
investigating. So let's switch these to test_must_fail.
Signed-off-by: Jeff King <redacted>
---
t/t5312-prune-corruption.sh | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
@@ -7,6 +7,9 @@ if we see, for example, a ref with a bogus name, it is OK either to bailoutortoproceedusingitasareachabletip,butitis_not_ OKtoproceedasifitdidnotexist.Otherwisewemightsilently deleteobjectsthatcannotberecovered.++Notethatwedoassertcommandfailureinthesecases,becausethatis+whatcurrentlyhappens.Ifthatchanges,thesetestsshouldberevisited.'GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=mainexportGIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
@@ -35,7 +38,7 @@ test_expect_success 'create history reachable only from a bogus-named ref' ' test_expect_success'pruning does not drop bogus object''test_when_finished"git hash-object -w -t commit saved"&&create_bogus_ref&&-test_might_failgitprune--expire=now&&+test_must_failgitprune--expire=now&&gitcat-file-e$bogus'
@@ -83,7 +86,7 @@ test_expect_success 'create history with missing tip commit' ' test_expect_success'pruning with a corrupted tip does not drop history''test_when_finished"git hash-object -w -t commit saved"&&-test_might_failgitprune--expire=now&&+test_must_failgitprune--expire=now&&gitcat-file-e$recoverable'
From: Jeff King <hidden> Date: 2021-09-24 18:38:00
There are currently two DO_FOR_EACH_* flags, which must not have their
bits overlap. Yet they're defined hundreds of lines apart. Let's move
them next to each other to make it clear that they are related and are a
complete set (which matters if you are adding a new flag and would like
to know what the next available bit is).
Signed-off-by: Jeff King <redacted>
---
You can probably guess how I found this problem. :)
refs/refs-internal.h | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
@@ -248,6 +248,14 @@ int refs_rename_ref_available(struct ref_store *refs,/* Include broken references in a do_for_each_ref*() iteration: */#define DO_FOR_EACH_INCLUDE_BROKEN 0x01+/*+*Onlyincludeper-worktreerefsinado_for_each_ref*()iteration.+*Normallythiswillbeusedwithafilesref_store,sincethat's+*whereallreferencebackendswillpresumablystoretheir+*per-worktreerefs.+*/+#define DO_FOR_EACH_PER_WORKTREE_ONLY 0x02+/**Referenceiterators*
From: Jeff King <hidden> Date: 2021-09-24 18:39:47
The documentation for the DO_FOR_EACH_* flags is sprinkled over the
refs-internal.h file. We define the two flags in one spot, and then
describe them in more detail far away from there, in the definitions of
refs_ref_iterator_begin() and ref_iterator_advance_fn().
Let's try to organize this a bit better:
- convert the #defines to an enum. This makes it clear that they are
related, and that the enum shows the complete set of flags.
- combine all descriptions for each flag in a single spot, next to the
flag's definition
- use the enum rather than a bare int for functions which take the
flags. This helps readers realize which flags can be used.
- clarify the mention of flags for ref_iterator_advance_fn(). It does
not take flags itself, but is meant to depend on ones set up
earlier.
Signed-off-by: Jeff King <redacted>
---
refs.c | 10 ++++++----
refs/refs-internal.h | 46 ++++++++++++++++++++++++++------------------
2 files changed, 33 insertions(+), 23 deletions(-)
@@ -245,16 +245,30 @@ int refs_rename_ref_available(struct ref_store *refs,/* We allow "recursive" symbolic refs. Only within reason, though */#define SYMREF_MAXDEPTH 5-/* Include broken references in a do_for_each_ref*() iteration: */-#define DO_FOR_EACH_INCLUDE_BROKEN 0x01-/*-*Onlyincludeper-worktreerefsinado_for_each_ref*()iteration.-*Normallythiswillbeusedwithafilesref_store,sincethat's-*whereallreferencebackendswillpresumablystoretheir-*per-worktreerefs.+*Theseflagsarepassedtorefs_ref_iterator_begin()(anddo_for_each_ref(),+*whichfeedsit).*/-#define DO_FOR_EACH_PER_WORKTREE_ONLY 0x02+enumdo_for_each_ref_flags{+/*+*Includebrokenreferencesinado_for_each_ref*()iteration,which+*wouldnormallybeomitted.Thisincludesbothrefsthatpointto+*missingobjects(atruerepositorycorruption),oneswithillegal+*names(whichweprefernottoexposetocallers),aswellas+*danglingsymbolicrefs(i.e.,thosethatpointtoanon-existent+*ref;thisisnotacorruption,butastheyhavenovalidoid,we+*omitthemfromnormaliterationresults).+*/+DO_FOR_EACH_INCLUDE_BROKEN=(1<<0),++/*+*Onlyincludeper-worktreerefsinado_for_each_ref*()iteration.+*Normallythiswillbeusedwithafilesref_store,sincethat's+*whereallreferencebackendswillpresumablystoretheir+*per-worktreerefs.+*/+DO_FOR_EACH_PER_WORKTREE_ONLY=(1<<1),+};/**Referenceiterators
@@ -357,16 +371,12 @@ int is_empty_ref_iterator(struct ref_iterator *ref_iterator);*Returnaniteratorthatgoesovereachreferencein`refs`for*whichtherefnamebeginswithprefix.Iftrimisnon-zero,then*trimthatmanycharactersoffthebeginningofeachrefname.-*Theoutputisorderedbyrefname.Thefollowingflagsaresupported:-*-*DO_FOR_EACH_INCLUDE_BROKEN:includebrokenreferencesin-*theiteration.-*-*DO_FOR_EACH_PER_WORKTREE_ONLY:onlyproduceREF_TYPE_PER_WORKTREErefs.+*Theoutputisorderedbyrefname.*/structref_iterator*refs_ref_iterator_begin(structref_store*refs,-constchar*prefix,inttrim,intflags);+constchar*prefix,inttrim,+enumdo_for_each_ref_flagsflags);/**Acallbackfunctionusedtoinstructmerge_ref_iteratorhowto
From: Jeff King <hidden> Date: 2021-09-24 18:41:35
When the DO_FOR_EACH_INCLUDE_BROKEN flag is used, we include both actual
corrupt refs (illegal names, missing objects), but also symrefs that
point to nothing. This latter is not really a corruption, but just
something that may happen normally. For example, the symref at
refs/remotes/origin/HEAD may point to a tracking branch which is later
deleted. (The local HEAD may also be unborn, of course, but we do not
access it through ref iteration).
Most callers of for_each_ref() etc, do not care. They don't pass
INCLUDE_BROKEN, so don't see it at all. But for those which do pass it,
this somewhat-normal state causes extra warnings (e.g., from
for-each-ref) or even aborts operations (destructive repacks with
GIT_REF_PARANOIA set).
This patch just introduces the flag and the mechanism; there are no
callers yet (and hence no tests). Two things to note on the
implementation:
- we actually skip any symref that does not resolve to a ref. This
includes ones which point to an invalidly-named ref. You could argue
this is a more serious breakage than simple dangling. But the
overall effect is the same (we could not follow the symref), as well
as the impact on things like REF_PARANOIA (either way, a symref we
can't follow won't impact reachability, because we'll see the ref
itself during iteration). The underlying resolution function doesn't
distinguish these two cases (they both get REF_ISBROKEN).
- we change the iterator in refs/files-backend.c where we check
INCLUDE_BROKEN. There's a matching spot in refs/packed-backend.c,
but we don't know need to do anything there. The packed backend does
not support symrefs at all.
The resulting set of flags might be a bit easier to follow if we broke
this down into "INCLUDE_CORRUPT_REFS" and "INCLUDE_DANGLING_SYMREFS".
But there are a few reasons not do so:
- adding a new OMIT_DANGLING_SYMREFS flag lets us leave existing
callers intact, without changing their behavior (and some of them
really do want to see the dangling symrefs; e.g., t5505 has a test
which expects us to report when a symref becomes dangling)
- they're not actually independent. You cannot say "include dangling
symrefs" without also including refs whose objects are not
reachable, because dangling symrefs by definition do not have an
object. We could tweak the implementation to distinguish this, but
in practice nobody wants to ask for that. Adding the OMIT flag keeps
the implementation simple and makes sure we don't regress the
current behavior.
Signed-off-by: Jeff King <redacted>
---
refs/files-backend.c | 5 +++++
refs/refs-internal.h | 6 ++++++
2 files changed, 11 insertions(+)
From: Jeff King <hidden> Date: 2021-09-24 18:42:41
Dangling symrefs aren't actually a corruption problem. It's perfectly
fine for refs/remotes/origin/HEAD to point to an unborn branch. And in
particular, if you are trying to establish reachability, a symref that
points nowhere doesn't matter either way. Any ref it could point to will
be examined during the rest of the traversal.
It's possible that a symref pointing nowhere _could_ be a sign that the
ref it was meant to point to was deleted accidentally (e.g., via
corruption). But there is no particular reason to think that is true for
any given case, and in the meantime, GIT_REF_PARANOIA kicking in
automatically for some operations means they'll fail unnecessarily.
So let's loosen it just a bit. The new test in t5312 shows off an
example that is safe, but currently fails (and no longer does after this
patch).
Note that we don't do anything if the caller explicitly asked for
DO_FOR_EACH_INCLUDE_BROKEN. In that case they may be looking for
dangling symrefs themselves, and setting GIT_REF_PARANOIA should not
_loosen_ things from what the caller asked for.
Signed-off-by: Jeff King <redacted>
---
refs.c | 12 ++++++++----
t/t5312-prune-corruption.sh | 7 +++++++
2 files changed, 15 insertions(+), 4 deletions(-)
@@ -62,6 +62,13 @@ test_expect_success 'destructive repack keeps packed object' 'gitcat-file-e$bogus'+test_expect_success'destructive repack not confused by dangling symref''+test_when_finished"git symbolic-ref -d refs/heads/dangling"&&+gitsymbolic-refrefs/heads/danglingrefs/heads/does-not-exist&&+gitrepack-ad&&+test_must_failgitcat-file-e$bogus+'+# We create two new objects here, "one" and "two". Our# main branch points to "two", which is deleted,# corrupting the repository. But we'd like to make sure
From: Jeff King <hidden> Date: 2021-09-24 18:46:16
The original point of the GIT_REF_PARANOIA flag was to include broken
refs in iterations, so that possibly-destructive operations would not
silently ignore them (and would generally instead try to operate on the
oids and fail when the objects could not be accessed).
We already turned this on by default for some dangerous operations, like
"repack -ad" (where missing a reachability tip would mean dropping the
associated history). But it was not on for general use, even though it
could easily result in the spreading of corruption (e.g., imagine
cloning a repository which simply omits some of its refs because
their objects are missing; the result quietly succeeds even though you
did not clone everything!).
This patch turns on GIT_REF_PARANOIA by default. So a clone as mentioned
above would actually fail (upload-pack tells us about the broken ref,
and when we ask for the objects, pack-objects fails to deliver them).
This may be inconvenient when working with a corrupted repository, but:
- we are better off to err on the side of complaining about
corruption, and then provide mechanisms for explicitly loosening
safety.
- this is only one type of corruption anyway. If we are missing any
other objects in the history that _aren't_ ref tips, then we'd
behave similarly (happily show the ref, but then barf when we
started traversing).
We retain the GIT_REF_PARANOIA variable, but simply default it to "1"
instead of "0". That gives the user an escape hatch for loosening this
when working with a corrupt repository. It won't work across a remote
connection to upload-pack (because we can't necessarily set environment
variables on the remote), but there the client has other options (e.g.,
choosing which refs to fetch).
As a bonus, this also makes ref iteration faster in general (because we
don't have to call has_object_file() for each ref), though probably not
noticeably so in the general case. In a repo with a million refs, it
shaved a few hundred milliseconds off of upload-pack's advertisement;
that's noticeable, but most repos are not nearly that large.
The possible downside here is that any operation which iterates refs but
doesn't ever open their objects may now quietly claim to have X when the
object is corrupted (e.g., "git rev-list new-branch --not --all" will
treat a broken ref as uninteresting). But again, that's not really any
different than corruption below the ref level. We might have
refs/heads/old-branch as non-corrupt, but we are not actively checking
that we have the entire reachable history. Or the pointed-to object
could even be corrupted on-disk (but our "do we have it" check would
still succeed). In that sense, this is merely bringing ref-corruption in
line with general object corruption.
One alternative implementation would be to actually check for broken
refs, and then _immediately die_ if we see any. That would cause the
"rev-list --not --all" case above to abort immediately. But in many ways
that's the worst of all worlds:
- it still spends time looking up the objects an extra time
- it still doesn't catch corruption below the ref level
- it's even more inconvenient; with the current implementation of
GIT_REF_PARANOIA for something like upload-pack, we can make
the advertisement and let the client choose a non-broken piece of
history. If we bail as soon as we see a broken ref, they cannot even
see the advertisement.
The test changes here show some of the fallout. A non-destructive "git
repack -adk" now fails by default (but we can override it). Deleting a
broken ref now actually tells the hooks the correct "before" state,
rather than a confusing null oid.
Signed-off-by: Jeff King <redacted>
---
Documentation/git.txt | 19 ++++++++++---------
refs.c | 2 +-
t/t5312-prune-corruption.sh | 10 ++++++++--
t/t5516-fetch-push.sh | 7 ++++---
4 files changed, 23 insertions(+), 15 deletions(-)
@@ -867,15 +867,16 @@ for full details. end user, to be recorded in the body of the reflog. `GIT_REF_PARANOIA`::- If set to `1`, include broken or badly named refs when iterating- over lists of refs. In a normal, non-corrupted repository, this- does nothing. However, enabling it may help git to detect and- abort some operations in the presence of broken refs. Git sets- this variable automatically when performing destructive- operations like linkgit:git-prune[1]. You should not need to set- it yourself unless you want to be paranoid about making sure- an operation has touched every ref (e.g., because you are- cloning a repository to make a backup).+ If set to `0`, ignore broken or badly named refs when iterating+ over lists of refs. Normally Git will try to include any such+ refs, which may cause some operations to fail. This is usually+ preferable, as potentially destructive operations (e.g.,+ linkgit:git-prune[1]) are better off aborting rather than+ ignoring broken refs (and thus considering the history they+ point to as not worth saving). The default value is `1` (i.e.,+ be paranoid about detecting and aborting all operations). You+ should not normally need to set this to `0`, but it may be+ useful when trying to salvage data from a corrupted repository. `GIT_ALLOW_PROTOCOL`:: If set to a colon-separated list of protocols, behave as if
From: Jeff King <hidden> Date: 2021-09-24 18:46:39
Now that GIT_REF_PARANOIA is the default, we don't need to selectively
enable it for destructive operations. In fact, it's harmful to do so,
because it overrides any GIT_REF_PARANOIA=0 setting that the user may
have provided (because they're trying to work around some corruption).
With these uses gone, we can further clean up the ref_paranoia global,
and make it a static variable inside the refs code.
Signed-off-by: Jeff King <redacted>
---
builtin/prune.c | 1 -
builtin/repack.c | 3 ---
cache.h | 8 --------
environment.c | 1 -
refs.c | 2 ++
5 files changed, 2 insertions(+), 13 deletions(-)
From: Jeff King <hidden> Date: 2021-09-24 18:48:09
Of the ref-filter callers, for-each-ref and git-branch both set the
INCLUDE_BROKEN flag (but git-tag does not, which is a weird
inconsistency). But now that GIT_REF_PARANOIA is on by default, that
produces almost the same outcome for all three.
The one exception is that GIT_REF_PARANOIA will omit dangling symrefs.
That's a better behavior for these tools, as they would never include
such a symref in the main output anyway (they can't, as it doesn't point
to an object). Instead they issue a warning to stderr. But that warning
is somewhat useless; a dangling symref is a perfectly reasonable thing
to have in your repository, and is not a sign of corruption. It's much
friendlier to just quietly ignore it.
And in terms of robustness, the warning gains us little. It does not
impact the exit code of either tool. So while the warning _might_ clue
in a user that they have an unexpected broken symref, it would not help
any kind of scripted use.
This patch converts for-each-ref and git-branch to stop using the
INCLUDE_BROKEN flag. That gives them more reasonable behavior, and
harmonizes them with git-tag.
We have to change one test to adapt to the situation. t1430 tries to
trigger all of the REF_ISBROKEN behaviors from the underlying ref code.
It uses for-each-ref to do so (because there isn't any other mechanism).
That will no longer issue a warning about the symref which points to an
invalid name, as it's considered dangling (and we can instead be sure
that it's _not_ mentioned on stderr). Note that we do still complain
about the illegally named "broken..symref"; its problem is not that it's
dangling, but the name of the symref itself is illegal.
Signed-off-by: Jeff King <redacted>
---
builtin/branch.c | 2 +-
builtin/for-each-ref.c | 2 +-
t/t1430-bad-ref-name.sh | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
From: Jeff King <hidden> Date: 2021-09-24 18:48:27
Now that none of our callers passes the INCLUDE_BROKEN flag, we can drop
it entirely, along with the code to plumb it through to the
for_each_fullref_in() functions.
Signed-off-by: Jeff King <redacted>
---
ref-filter.c | 11 ++++-------
ref-filter.h | 1 -
2 files changed, 4 insertions(+), 8 deletions(-)
From: Jeff King <hidden> Date: 2021-09-24 18:49:11
No callers pass in anything but "0" here. Likewise to our sibling
functions. Note that some of them ferry along the flag, but none of
their callers pass anything but "0" either.
Nor is anybody likely to change that. Callers which really want to see
all of the raw refs use for_each_rawref(). And anybody interested in
iterating a subset of the refs will likely be happy to use the
now-default behavior of showing broken refs, but omitting dangling
symlinks.
So we can get rid of this whole feature.
Signed-off-by: Jeff King <redacted>
---
builtin/rev-parse.c | 4 ++--
ls-refs.c | 2 +-
ref-filter.c | 19 +++++++++----------
refs.c | 22 ++++++----------------
refs.h | 9 +++------
revision.c | 2 +-
6 files changed, 22 insertions(+), 36 deletions(-)
@@ -2118,16 +2117,16 @@ static int for_each_fullref_in_pattern(struct ref_filter *filter,*sojustreturneverythingandletthecaller*sortitout.*/-returnfor_each_fullref_in("",cb,cb_data,broken);+returnfor_each_fullref_in("",cb,cb_data);}if(!filter->name_patterns[0]){/* no patterns; we have to look at everything */-returnfor_each_fullref_in("",cb,cb_data,broken);+returnfor_each_fullref_in("",cb,cb_data);}returnfor_each_fullref_in_prefixes(NULL,filter->name_patterns,-cb,cb_data,broken);+cb,cb_data);}/*
From: Jonathan Tan <hidden> Date: 2021-09-27 17:47:38
quoted hunk
@@ -277,7 +277,7 @@ test_expect_success 'incremental-repack task' ' # Delete refs that have not been repacked in these packs. git for-each-ref --format="delete %(refname)" \- refs/prefetch refs/tags >refs &&+ refs/prefetch refs/tags refs/remotes >refs && git update-ref --stdin <refs && # Replace the object directory with this pack layout.
@@ -286,6 +286,10 @@ test_expect_success 'incremental-repack task' ' ls $packDir/*.pack >packs-before && test_line_count = 3 packs-before &&+ # make sure we do not have any broken refs that were+ # missed in the deletion above+ git for-each-ref &&
For what it's worth, I verified that a fatal error is indeed caused in
"git for-each-ref" if refs/remotes was not deleted. This patch looks
good.
From: Jonathan Tan <hidden> Date: 2021-09-27 17:49:52
Deleting a
broken ref now actually tells the hooks the correct "before" state,
rather than a confusing null oid.
I believe that this is due to write_head_info() in
builtin/receive-pack.c now advertising the broken ref, so that the
client knows what the old OID is when pushing in order to delete the ref
(verified by running the relevant "git push" in the test with
GIT_TRACE_PACKET=1, with and without GIT_REF_PARANOIA=0). If rerolling,
maybe this is worth adding in parentheses (e.g. "(because such a ref
would be advertised to the client and the client would thus send the
"before" OID when deleting it)").
All the patches up to this look good.
From: Jonathan Tan <hidden> Date: 2021-09-27 17:51:00
No callers pass in anything but "0" here. Likewise to our sibling
functions. Note that some of them ferry along the flag, but none of
their callers pass anything but "0" either.
Nor is anybody likely to change that. Callers which really want to see
all of the raw refs use for_each_rawref(). And anybody interested in
iterating a subset of the refs will likely be happy to use the
now-default behavior of showing broken refs, but omitting dangling
symlinks.
So we can get rid of this whole feature.
Signed-off-by: Jeff King <redacted>
All the patches look good.
Reviewed-by: Jonathan Tan <redacted>
As you have mentioned before, this clashes with my series [1]. But since
I'll need to update my series anyway to reinclude the
DO_FOR_EACH_INCLUDE_BROKEN flag, the obvious thing is for me to rebase
mine on top of yours so that yours can go in first, so I don't mind
doing that.
[1] https://lore.kernel.org/git/cover.1632242495.git.jonathantanmy@google.com/
From: Jeff King <hidden> Date: 2021-09-27 19:49:49
On Mon, Sep 27, 2021 at 10:38:18AM -0700, Jonathan Tan wrote:
quoted
@@ -277,7 +277,7 @@ test_expect_success 'incremental-repack task' ' # Delete refs that have not been repacked in these packs. git for-each-ref --format="delete %(refname)" \- refs/prefetch refs/tags >refs &&+ refs/prefetch refs/tags refs/remotes >refs && git update-ref --stdin <refs && # Replace the object directory with this pack layout.
@@ -286,6 +286,10 @@ test_expect_success 'incremental-repack task' ' ls $packDir/*.pack >packs-before && test_line_count = 3 packs-before &&+ # make sure we do not have any broken refs that were+ # missed in the deletion above+ git for-each-ref &&
For what it's worth, I verified that a fatal error is indeed caused in
"git for-each-ref" if refs/remotes was not deleted. This patch looks
good.
Me too. :) It works because for-each-ref's default format will try to
show the type of the object, so it complains when the object is missing.
We could make this "git fsck" if that's less subtle, but this is a bit
cheaper.
-Peff