Add --check · Entire

Add --check

95f0f4cmain·

Sessions

Transcript data is unavailable for this checkpoint.

Changes

4

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