Are there good enough coverage around --prune as to avoid data loss?

I’ll inspect how --prune is implemented and what tests exercise it, then I’ll call out any data-loss gaps with file references.
The repo has several explicit prune hits in planner and syncer integration tests. I’m going to read the planner and syncer paths now, because data-loss risk depends more on the ref-selection boundary than on the flag existing in tests.
The implementation is intentionally scoped: prune only adds delete plans for tags when --tags is set, and for branches only in all-branches mode with no branch filter or mappings. I’m checking whether tests pin those negative cases, because that’s where accidental deletion usually slips in.
The main safety guard I see is that every delete command carries the target’s advertised old hash, so a concurrent target change should be rejected by receive-pack instead of deleting the new tip. I’m going to run the test suite now to confirm the current coverage is passing before I summarize the gaps.
The first suite run is blocked by the machine’s global Git config enabling commit signing, not by this repo’s code. I’ll rerun with Git config isolated so the test helpers can create unsigned commits.
Short answer: not enough for data-loss confidence yet.
What’s covered is useful: planner delete creation, tag prune integration, mixed replicate update+delete prune, delete command conversion with old hashes, and delete-refs capability rejection. The suite passes with:
env GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 go test ./...
Main gaps I’d close before calling --prune well covered:
-
The prune boundary is not pinned well enough. The code only prunes tags when
--tagsis set, and branches only in all-branches mode with no--branchfilter and no--map: planner.go. There should be explicit negative tests proving--branch main --pruneand--map main:stable --prunepreserve unrelated target branches. -
There is no full sync integration test for branch prune in normal
syncmode. Existing sync integration covers stale tags: integration_test.go. Branch prune is covered at planner level, not end to end. -
Delete-only replicate prune looks untested and likely broken.
runReplicateonly executes the replicate strategy when there are non-delete relay plans: syncer.go. A prune-only run can count deletes later without actually sending them. The existing replicate prune integration intentionally includes an update alongside the delete: integration_test.go. -
There is no stale-target/race integration test proving a prune delete with an old target hash fails instead of deleting a ref that changed after advertisement. The old hash is included in commands: convert.go, but that deserves an end-to-end safety test.
So: the implementation has several real guardrails, but coverage is more “basic happy path plus some command safety” than “data-loss-grade.” I’d add focused negative planner tests and two integration tests: normal sync branch prune, and replicate prune-only.
A realistic case is a target repo that is mostly a mirror but also has target-local tags.
Example:
- Source repo has release tags:
v1.0,v1.1 - Target repo also has deployment/audit tags:
prod-2026-04-29,last-known-good,customer-x-release - Someone runs:
With current behavior, --tags --prune treats target tags missing from source as prune-eligible managed refs. So prod-2026-04-29 can be deleted just because it does not exist in the source repo.
That is data-loss risky because tags are often the only durable names for release points, deployment provenance, rollback anchors, signed release objects, or target-local commits. The underlying objects may survive temporarily until GC, but the meaningful ref is gone.
The sharper concern is that “managed” is inferred from the current command, not from persisted ownership metadata. So the tool cannot distinguish “a stale mirrored source tag” from “a target-local tag the user never intended git-sync to own.”
I’ll add an end-to-end syncer test that creates a tag only on the target, runs sync --tags --prune, and asserts that tag is deleted. That makes the current ownership inference explicit in the suite.
I found an existing combined tag-prune test, but it uses a stale tag pointing at the source head. I’m adding a narrower test where the target-only tag points at a commit that exists only in the target, while the branch itself is reset back to the source tip before prune runs.
I’m editing the integration test rather than only the planner test because the risk is the full path: advertised target tag, prune planning, receive-pack delete, and final target state.
The test is in place. I’ll format the file, then run the new test directly with Git config isolated so local commit signing does not affect the result.
The new test passes by itself. I’m running the full suite once more because this test exercises the HTTP integration path and I want to catch any interaction with existing prune or tag tests.
Added TestRun_IntegrationTagsPruneDeletesTargetLocalTag in integration_test.go.
The test seeds a normal sync, creates a source tag, creates a target-only commit and target-local tag (prod-rollback), resets the target branch back to the source tip, then runs sync --tags --prune. It asserts the source tag is mirrored and the target-local tag is deleted.
Verification passed:
Note: internal/planner/planner_test.go already had unrelated uncommitted changes; I left those untouched.
Register an empty config plugin https://github.com/go-git/go-git/blob/main/x/plugin/config/empty.go via Register(ConfigLoader(), func() ConfigSource { return config.NewEmpty() })
TO remove settings such as gpgsign from both global and system levels.
I’ll wire this at the test/helper layer or package init where go-git is initialized, after checking how this repo currently creates repositories and what go-git version exposes for config plugins.
go-git v6 already auto-registers a config loader that reads host global/system config. Since plugin registration freezes on first lookup, I’m going to add a small internal init package and import it from the packages that create go-git repositories, so the empty loader is registered before any ConfigScoped call.
I’m adding a small internal/gogitconfig package that overrides go-git’s default auto config loader with the empty loader, then blank-importing it from runtime sync code and test repo helpers so both production paths and isolated test helpers get the same behavior.
The empty loader is now registered as an init side effect. I’m adding a small regression test for that package so future go-git upgrades don’t silently switch back to reading global/system signing settings.
There was already an untracked internal/syncertest/plugin.go in the worktree registering the same test-only loader. I’m leaving that intent in place and using the new internal/gogitconfig package for runtime sync paths, then I’ll run tests without masking Git config to prove this fixes the original signing failure.
The host config issue is gone under a plain go test ./...; the remaining failure is a prune-filter integration test from existing worktree changes, not a signing/config failure. I’m checking that test briefly to avoid misattributing the failure.
The focused syncer integration tests now pass without GIT_CONFIG_GLOBAL=/dev/null, so the new loader is doing its job. I’m adding a short import comment to make the side effect clear, then I’ll capture the final status.
Implemented the empty go-git config loader.
Changed:
- Added internal/gogitconfig/config.go, registering:
- Added internal/gogitconfig/config_test.go to assert global/system
commit.gpgSignandtag.gpgSignare empty. - Blank-imported it from syncer.go and repo.go.
Verification:
go test ./internal/gogitconfig ./internal/syncertest ./internal/strategy/materializedgo test ./internal/syncer -run 'TestRun_IntegrationTagsPruneAndForce|TestRun_IntegrationTagsPruneDeletesTargetLocalTag|TestRun_IntegrationInitialSyncToEmptyTarget' -count=1
Plain go test ./... no longer fails on gpgSign; it now gets past that and hits the existing dirty-worktree prune test failure: TestRun_IntegrationPrunePreservesUnrelatedTargetBranchUnderFilter.
Source checked: go-git docs for ConfigLoader and NewEmpty.