{"record":{"id":"4b17cf3335bc4484","repo":"gastownhall/beads","slug":"invalid-ref-w-4b17cf","errorCode":null,"errorMessage":"invalid ref: %w","messagePattern":"invalid ref: %w","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/versioncontrolops/fastforward.go","lineNumber":28,"sourceCode":"\n// This file holds additive driver primitives for a fast-forward \"smart\n// migrate\" gate: checking whether local HEAD is a strict ancestor of a\n// cached ref, checking the working set is clean (ignoring wisp tables), and\n// performing the actual fast-forward-only adopt. Nothing in this package or\n// elsewhere calls these yet — they are wired into the smart migrate gate in\n// a later change.\n\n// LocalIsStrictAncestorOf reports whether local HEAD is a STRICT ancestor of\n// ref in the Dolt commit graph: local has zero commits that ref lacks\n// (ahead == 0) and at least one commit that local lacks (behind >= 1). A\n// local HEAD equal to ref (ahead == 0, behind == 0) is NOT a strict\n// ancestor, and returns false.\n//\n// ref must already be present locally (e.g. a cached remote-tracking ref\n// such as \"origin/main\" after a fetch); this performs no fetch of its own.\nfunc LocalIsStrictAncestorOf(ctx context.Context, db DBConn, ref string) (bool, error) {\n\tif err := issueops.ValidateRef(ref); err != nil {\n\t\treturn false, fmt.Errorf(\"invalid ref: %w\", err)\n\t}\n\n\t// Dolt's AS OF requires a literal ref, not a bind parameter; ref was\n\t// validated above via the shared allowlist regex, mirroring the same\n\t// ahead/behind pattern used by EmbeddedDoltStore.SyncStatus\n\t// (internal/storage/embeddeddolt/federation.go).\n\t//nolint:gosec // G201: ref validated by ValidateRef above — AS OF requires a literal\n\tquery := fmt.Sprintf(`\n\t\tSELECT\n\t\t\t(SELECT COUNT(*) FROM dolt_log WHERE commit_hash NOT IN\n\t\t\t\t(SELECT commit_hash FROM dolt_log AS OF '%s')) AS ahead,\n\t\t\t(SELECT COUNT(*) FROM dolt_log AS OF '%s' WHERE commit_hash NOT IN\n\t\t\t\t(SELECT commit_hash FROM dolt_log)) AS behind\n\t`, ref, ref)\n\n\tvar ahead, behind int\n\tif err := db.QueryRowContext(ctx, query).Scan(&ahead, &behind); err != nil {\n\t\treturn false, fmt.Errorf(\"compare local HEAD to %s: %w\", ref, err)","sourceCodeStart":10,"sourceCodeEnd":46,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/versioncontrolops/fastforward.go#L10-L46","documentation":"LocalIsStrictAncestorOf reports whether the given ref is a strict descendant of the local HEAD. Before querying, it validates the ref with issueops.ValidateRef, which requires a non-empty ref of at most 128 chars matching [a-zA-Z0-9_./-]+. The ref fails this allowlist, so the function wraps the validation error with \"invalid ref: %w\" and returns false.","triggerScenarios":"Calling LocalIsStrictAncestorOf(ctx, db, ref) with an empty string, a ref longer than 128 chars, or a ref containing characters outside [a-zA-Z0-9_./-] (e.g. spaces, 'origin..main', shell metacharacters, 'HEAD~1' style suffixes with disallowed chars).","commonSituations":"Passing a user-supplied ref name straight into the API without trimming; constructing a ref by string concatenation that accidentally includes whitespace or newlines; passing a full 'refs/heads/main' style path with characters like ':' or an empty variable due to a failed config lookup.","solutions":["Trim whitespace and re-check the ref string; it must be non-empty, <=128 chars, and match ^[a-zA-Z0-9_./-]+$.","Call issueops.ValidateRef(ref) yourself before invoking LocalIsStrictAncestorOf to get the exact reason.","If the ref should be a commit hash, confirm you are passing the 32-hex-char hash and not a message or label.","If you need the remote value, ensure the ref is a cached remote-tracking name like 'origin/main' after a fetch."],"exampleFix":"// before\nisAncestor, err := versioncontrolops.LocalIsStrictAncestorOf(ctx, db, ref)\n// after\nref = strings.TrimSpace(ref)\nif err := issueops.ValidateRef(ref); err != nil {\n    return fmt.Errorf(\"refusing call: %w\", err)\n}\nisAncestor, err := versioncontrolops.LocalIsStrictAncestorOf(ctx, db, ref)","handlingStrategy":"validation","validationCode":"var validRef = regexp.MustCompile(`^[a-zA-Z0-9_./-]+$`)\nfunc validDoltRef(ref string) bool {\n    return ref != \"\" && len(ref) <= 128 && validRef.MatchString(ref)\n}","typeGuard":"func isSafeRef(s string) bool {\n    if len(s) == 0 || len(s) > 128 { return false }\n    for _, r := range s {\n        ok := r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' ||\n            r == '_' || r == '.' || r == '/' || r == '-'\n        if !ok { return false }\n    }\n    return true\n}","tryCatchPattern":"ok, err := versioncontrolops.LocalIsStrictAncestorOf(ctx, db, ref)\nif err != nil {\n    var validationFailed bool = strings.Contains(err.Error(), \"invalid ref\")\n    if validationFailed {\n        return fmt.Errorf(\"caller bug: bad ref %q: %w\", ref, err)\n    }\n    return err\n}","preventionTips":["Always run issueops.ValidateRef on refs sourced from user input before any versioncontrolops call.","Trim whitespace/newlines when capturing ref names from command output or config.","Use well-known forms only: branch names, 'origin/*' remote-tracking refs, or 32-hex commit hashes."],"tags":["validation","git-ref","dolt","input-validation"],"backgroundTag":"invalid-git-ref","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}