Add --check · Entire
Add --check
95f0f4c→main·
Sessions
Transcript data is unavailable for this checkpoint.
Changes
4
cmd/git-sync
Mconvert_sha256.go+2
internal/sha256convert
- Msha256convert.go+114
- Msha256convert_test.go+22
docs
- Mconvert-sha256.md+32/-3
79 unmodified lines
cmd.Flags().BoolVarP(&req.Verbose, "verbose", "v", false, "verbose logging")
cmd.Flags().BoolVar(&req.Progress, "progress", false,
"show live per-phase object counts on stderr (TTY only)")
cmd.Flags().BoolVar(&req.Check, "check", false,
"verify the output after conversion (config, HEAD, refs, git fsck --full)")
cmd.Flags().BoolVar(&req.KeepSourceObjects, "keep-source-objects", false,
"keep the temporary SHA1 store on disk after conversion (for debugging)")
cmd.Flags().StringVar(&req.MappingFile, "write-mapping", "",
Mcmd/git-sync/convert_sha256.go+2
24 unmodified lines
// Check is one named verification step from --check, with the result
// and a short detail string suitable for logging/JSON output.
type Check struct {
Name string `json:"name"`
OK bool `json:"ok"`
Detail string `json:"detail,omitempty"`
}
// Lines satisfies the human-readable output contract used by other git-sync subcommands.
func (r Result) Lines() []string {
lines := []string{
// runChecks performs lightweight verification of the converted repo.
// Returns one Check per step. Callers print and/or fail-on-error based
// on these. No early return so users see the full picture even when an
// earlier check fails.
func runChecks(targetDir string, repo *git.Repository, refsExpected int) []Check {
checks := []Check{}
// 1. Config: extensions.objectformat = sha256.
cfgBytes, err := os.ReadFile(filepath.Join(targetDir, "config"))
switch {
case err != nil:
checks = append(checks, Check{Name: "config", OK: false, Detail: err.Error()})
case !bytes.Contains(cfgBytes, []byte("objectformat = sha256")):
checks = append(checks, Check{Name: "config", OK: false, Detail: "extensions.objectformat = sha256 not set"})
default:
checks = append(checks, Check{Name: "config", OK: true, Detail: "extensions.objectformat = sha256"})
}
// 2. HEAD resolves to an existing object.
head, err := repo.Reference(plumbing.HEAD, true)
switch {
case err != nil:
checks = append(checks, Check{Name: "HEAD", OK: false, Detail: err.Error()})
case head.Hash().IsZero():
checks = append(checks, Check{Name: "HEAD", OK: false, Detail: "resolves to zero hash"})
default:
if _, err := repo.Storer.EncodedObject(plumbing.AnyObject, head.Hash()); err != nil {
checks = append(checks, Check{Name: "HEAD", OK: false, Detail: fmt.Sprintf("%s: %v", head.Hash(), err)})
} else {
checks = append(checks, Check{Name: "HEAD", OK: true, Detail: head.Hash().String()})
}
}
// 3. Every written ref resolves to an existing object.
resolved := 0
missing := ""
refs, err := repo.References()
if err != nil {
checks = append(checks, Check{Name: "refs", OK: false, Detail: err.Error()})
} else {
_ = refs.ForEach(func(r *plumbing.Reference) error {
if r.Type() != plumbing.HashReference {
return nil
}
if r.Name() == plumbing.ReferenceName(originNotesRef) {
// Counted separately below; not in the refsExpected total.
return nil
}
if _, err := repo.Storer.EncodedObject(plumbing.AnyObject, r.Hash()); err != nil {
if missing == "" {
missing = fmt.Sprintf("%s → %s: %v", r.Name(), r.Hash(), err)
}
return nil
}
resolved++
return nil
})
if missing != "" {
checks = append(checks, Check{Name: "refs", OK: false, Detail: missing})
} else if resolved < refsExpected {
checks = append(checks, Check{Name: "refs", OK: false, Detail: fmt.Sprintf("only %d / %d refs resolved", resolved, refsExpected)})
} else {
checks = append(checks, Check{Name: "refs", OK: true, Detail: fmt.Sprintf("%d / %d resolve to objects", resolved, refsExpected)})
}
}
// 4. git fsck --full (if git is on PATH).
gitBin, err := exec.LookPath("git")
if err != nil {
checks = append(checks, Check{Name: "git fsck --full", OK: true, Detail: "skipped (git not in PATH)"})
return checks
}
cmd := exec.Command(gitBin, "-C", targetDir, "fsck", "--full")
fsckOut, err := cmd.CombinedOutput()
switch {
case err != nil:
checks = append(checks, Check{Name: "git fsck --full", OK: false, Detail: fmt.Sprintf("%v\n%s", err, fsckOut)})
case bytes.Contains(fsckOut, []byte("error")) || bytes.Contains(fsckOut, []byte("bad sha")):
checks = append(checks, Check{Name: "git fsck --full", OK: false, Detail: strings.TrimSpace(string(fsckOut))})
default:
checks = append(checks, Check{Name: "git fsck --full", OK: true, Detail: "clean"})
}
return checks
}
## Verifying the Output
Standard git tooling works against the converted repo without additional flags — the `extensions.objectformat` setting in the local config is enough for git to switch hashing:
Pass `--check` and the command runs four sanity checks against the converted repo at the end of the run, printing one line each:
verifying output ... ✓ config: extensions.objectformat = sha256 ✓ HEAD: ffe9fff421b77f2dcc049a95b3b8ba7b9da8976dd61bcf35e9fe2d993babc470 ✓ refs: 37 / 37 resolve to objects ✓ git fsck --full: clean
The checks are:
1. **config** — `extensions.objectformat = sha256` is present in `<target>/config`.
2. **HEAD** — resolves to a non-zero hash and that object exists in the store.
3. **refs** — every written ref (except `refs/notes/sha1-origin`, counted separately) resolves to an object in the store. The count matches `RefsConverted`.
4. **git fsck --full** — the external `git` binary runs a full integrity check. Skipped (and reported as such) when `git` isn't on `PATH`; the conversion still succeeds.
If any check fails the command exits non-zero. The full per-check results are also in `--json`'s `checks` array. Without `--check` no verification runs and the run completes as soon as the conversion itself finishes.
You can also run the checks by hand on a converted repo, with or without `--check`:
```bash
git -C /path/to/out.git fsck --full # zero errors expected