{"record":{"id":"908507b53b7dd868","repo":"gastownhall/beads","slug":"invalid-toref-w","errorCode":null,"errorMessage":"invalid toRef: %w","messagePattern":"invalid toRef: %w","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/diff.go","lineNumber":21,"sourceCode":"import (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\n\t\"github.com/steveyegge/beads/internal/storage\"\n\t\"github.com/steveyegge/beads/internal/types\"\n)\n\n// DiffInTx returns changes between two commits or branches by querying\n// Dolt's dolt_diff() table function.\n//\n// nolint:gosec // G201: refs are validated by ValidateRef() - dolt_diff requires literal refs\nfunc DiffInTx(ctx context.Context, tx *sql.Tx, fromRef, toRef string) ([]*storage.DiffEntry, error) {\n\tif err := ValidateRef(fromRef); err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid fromRef: %w\", err)\n\t}\n\tif err := ValidateRef(toRef); err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid toRef: %w\", err)\n\t}\n\n\tquery := fmt.Sprintf(`\n\t\tSELECT\n\t\t\tCOALESCE(from_id, '') as from_id,\n\t\t\tCOALESCE(to_id, '') as to_id,\n\t\t\tdiff_type,\n\t\t\tfrom_title, to_title,\n\t\t\tfrom_description, to_description,\n\t\t\tfrom_status, to_status,\n\t\t\tfrom_priority, to_priority\n\t\tFROM dolt_diff('%s', '%s', 'issues')\n\t`, fromRef, toRef)\n\n\trows, err := tx.QueryContext(ctx, query)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get diff: %w\", err)\n\t}","sourceCodeStart":3,"sourceCodeEnd":39,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/diff.go#L3-L39","documentation":"Identical guard to the fromRef case but for the toRef argument of DiffInTx: ValidateRef rejected the destination ref before it can be interpolated into dolt_diff(). The library fails fast to keep the dolt_diff query injection-safe. It is a caller-input error.","triggerScenarios":"Calling DiffInTx with an invalid toRef: empty string, embedded quotes/semicolons, illegal characters, or a malformed ref form rejected by ValidateRef.","commonSituations":"User-supplied target branch containing unexpected characters; passing a range string like \"main..dev\" instead of two separate refs; automated pipelines propagating unsanitized ref names.","solutions":["Read the wrapped ValidateRef error for the specific rule violated","Pass plain single refs (branch, tag, or commit hash) as toRef — not ranges or expressions","Pre-validate with ValidateRef at the caller boundary","Strip or reject whitespace and SQL metacharacters from user input"],"exampleFix":"// before\nDiffInTx(ctx, tx, \"main\", \"dev..HEAD\")\n// after\nDiffInTx(ctx, tx, \"main\", \"HEAD\") // one plain ref per argument; ranges are not refs","handlingStrategy":"validation","validationCode":"func validRef(ref string) bool {\n    if ref == \"\" || strings.ContainsAny(ref, \"'\\\"; --\") || strings.Contains(ref, \"..\") {\n        return false\n    }\n    return issueops.ValidateRef(ref) == nil\n}\n// reject ranges like \"main..dev\"; pass two refs instead","typeGuard":null,"tryCatchPattern":"if err := DiffInTx(ctx, tx, from, to); err != nil {\n    if strings.Contains(err.Error(), \"invalid toRef\") {\n        return fmt.Errorf(\"usage: to must be a single plain ref, got %q\", to)\n    }\n    return err\n}","preventionTips":["Reject range expressions (\"a..b\") — split them into two refs before calling","Trim and sanitize target-branch input from users and CI variables","Use the library's ValidateRef as the single source of truth","Document accepted ref forms in your command help"],"tags":["validation","sql-injection","refs","input"],"backgroundTag":"invalid-ref","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}