{"record":{"id":"b90b668e3f61093d","repo":"gastownhall/beads","slug":"missing-source-path-for-porcelain-rename-copy-reco","errorCode":null,"errorMessage":"missing source path for porcelain rename/copy record %q","messagePattern":"missing source path for porcelain rename/copy record %q","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cmd/bd/worktree_cmd.go","lineNumber":1137,"sourceCode":"}\n\nfunc fingerprintWorktreeStatusPaths(worktreePath, status string) (string, error) {\n\trecords := strings.Split(status, \"\\x00\")\n\tpathSet := make(map[string]struct{})\n\tfor index := 0; index < len(records); index++ {\n\t\trecord := records[index]\n\t\tif record == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif len(record) < 4 || record[2] != ' ' {\n\t\t\treturn \"\", fmt.Errorf(\"unexpected porcelain status record %q\", record)\n\t\t}\n\t\tcode := record[:2]\n\t\tpathSet[record[3:]] = struct{}{}\n\t\tif strings.ContainsAny(code, \"RC\") {\n\t\t\tindex++\n\t\t\tif index >= len(records) || records[index] == \"\" {\n\t\t\t\treturn \"\", fmt.Errorf(\"missing source path for porcelain rename/copy record %q\", record)\n\t\t\t}\n\t\t\tpathSet[records[index]] = struct{}{}\n\t\t}\n\t}\n\n\tpaths := make([]string, 0, len(pathSet))\n\tfor path := range pathSet {\n\t\tpaths = append(paths, path)\n\t}\n\tsort.Strings(paths)\n\n\thasher := sha256.New()\n\tfor _, gitPath := range paths {\n\t\tcleanPath := filepath.Clean(filepath.FromSlash(gitPath))\n\t\tif cleanPath == \".\" ||\n\t\t\tfilepath.IsAbs(cleanPath) ||\n\t\t\tcleanPath == \"..\" ||\n\t\t\tstrings.HasPrefix(cleanPath, \"..\"+string(filepath.Separator)) {","sourceCodeStart":1119,"sourceCodeEnd":1155,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/cmd/bd/worktree_cmd.go#L1119-L1155","documentation":"In porcelain -z output, rename (R) and copy (C) records are followed by a second NUL-separated field: the original (source) path. bd consumes that next record to include both endpoints in the fingerprint; this error means the expected source path was absent or empty, so the status stream is truncated or malformed.","triggerScenarios":"fingerprintWorktreeStatusPaths sees an R/C status code but the next NUL-separated record is empty or missing — truncated `git status` output or a caller passing a hand-built/partial status string.","commonSituations":"Renamed files in the index combined with output truncation; testing/mocking code that fabricates porcelain records without the rename source; a git build emitting non-standard rename serialization.","solutions":["Regenerate status directly from git to confirm output is complete: `git -C <worktree> status --porcelain=v1 -z --untracked-files=all --ignored=matching`","Check for R/C entries (`git status --porcelain=v1`) and verify each has a preceding `->` source in non-z form; upgrade git if output looks truncated","If this comes from a test/mock, fix the fixture to include the NUL-separated source path after each R/C record"],"exampleFix":"// before (bad fixture)\n\"R  new.txt\\x00\"\n// after\n\"R  new.txt\\x00old.txt\\x00\"","handlingStrategy":"validation","validationCode":"records := strings.Split(status, \"\\x00\")\nfor i, r := range records {\n    if len(r) > 2 && strings.ContainsAny(r[:2], \"RC\") {\n        if i+1 >= len(records) || records[i+1] == \"\" {\n            return fmt.Errorf(\"fixture/truncated output: R/C record %q lacks source path\", r)\n        }\n    }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["When mocking porcelain output in tests, always append the NUL-separated source path after R/C records","Never pass hand-trimmed status text into worktree tooling","Verify rename serialization with real `git status -z` output"],"tags":["git","parsing","porcelain"],"backgroundTag":"porcelain-status-parse-error","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}