fix(tool): resolve file_read paths against git top-level in monorepos (#309)
Some checks are pending
CI / test (push) Waiting to run

* fix(tool): resolve file_read paths against git top-level in monorepos

ocr review from a monorepo subdirectory failed with "file not found" (#287):
git reports diff and `git show HEAD:<path>` paths relative to the repo root,
but RepoDir was scoped to the invocation subdirectory, producing a double
prefix. resolveWorkingDir now anchors RepoDir at `git rev-parse
--show-toplevel` on the review path (requireGit=true); scan keeps the CWD so
its `git ls-files` walk stays scoped.

The top-level lookup uses a stdout-only git helper so stderr notices can't
pollute the path, and fails loudly if --show-toplevel errors or is empty
(e.g. a bare repo) instead of silently reusing the subdirectory. Adds
regression tests for the subdir hoist, the scan-path scoping, git-show
resolution of root-relative paths, and the bare-repo failure.

* docs(rules): document repo-root rule.json resolution in monorepos

Since #287 anchored RepoDir at the git top-level, ocr review from a
monorepo subdirectory loads the repo-root .opencodereview/rule.json
rather than a subdir-local one. Call out this user-visible behavior at
loadProjectRule so the scope change isn't a surprise (review feedback).
This commit is contained in:
chethanuk 2026-07-07 16:06:28 +04:00 committed by GitHub
parent a2e08b77a1
commit c973e581ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 180 additions and 16 deletions

View file

@ -344,6 +344,11 @@ func loadRuleFile(path string) (*ProjectRule, error) {
return &pr, nil
}
// loadProjectRule reads <repoDir>/.opencodereview/rule.json. Since #287 anchored
// RepoDir at the git top-level, `ocr review` from a monorepo subdirectory loads
// the repo-root rule file — which is consistent, since rule entries match against
// root-relative diff paths. A subproject-local rule.json under the subdirectory is
// intentionally not consulted; put shared rules at the repo root, or pass --rule.
func loadProjectRule(repoDir string) (*ProjectRule, error) {
path := filepath.Join(repoDir, ".opencodereview", "rule.json")
data, err := os.ReadFile(path)

View file

@ -3,6 +3,7 @@ package tool
import (
"context"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
@ -215,3 +216,58 @@ func TestFileReader_Read_SubdirectoryFile(t *testing.T) {
t.Errorf("Read() = %q, want %q", got, "package main")
}
}
// TestFileReader_Read_CommitMode_MonorepoSubdirPath reproduces #287 at the
// git-show layer: in a monorepo, git reports paths relative to the repo root
// (e.g. "subproject1/src/models/request_meta.py"). With RepoDir anchored at the
// git top-level (the fix), `git show HEAD:<root-relative-path>` must resolve —
// this is the exact command that failed in the issue.
func TestFileReader_Read_CommitMode_MonorepoSubdirPath(t *testing.T) {
dir := t.TempDir()
git := func(args ...string) {
t.Helper()
cmd := exec.Command("git", args...)
cmd.Dir = dir
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("git %v: %v\n%s", args, err, out)
}
}
git("init")
git("config", "user.email", "t@t.co")
git("config", "user.name", "t")
rel := filepath.Join("subproject1", "src", "models", "request_meta.py")
if err := os.MkdirAll(filepath.Join(dir, filepath.Dir(rel)), 0o755); err != nil {
t.Fatal(err)
}
content := "class RequestMeta:\n id = 1\n"
if err := os.WriteFile(filepath.Join(dir, rel), []byte(content), 0o644); err != nil {
t.Fatal(err)
}
git("add", ".")
git("commit", "-m", "init")
commit := getHeadCommit(t, dir)
// Use the git-style forward-slash path the diff/LLM would supply.
gitPath := "subproject1/src/models/request_meta.py"
// git-show (commit mode): the exact path from the issue error.
frShow := &FileReader{RepoDir: dir, Mode: ModeCommit, Ref: commit}
got, err := frShow.Read(context.Background(), gitPath)
if err != nil {
t.Fatalf("commit-mode Read(%q) error: %v", gitPath, err)
}
if got != content {
t.Errorf("commit-mode Read = %q, want %q", got, content)
}
// disk (workspace mode): same root-relative path resolves too.
frDisk := &FileReader{RepoDir: dir, Mode: ModeWorkspace}
gotDisk, err := frDisk.Read(context.Background(), gitPath)
if err != nil {
t.Fatalf("workspace-mode Read(%q) error: %v", gitPath, err)
}
if gotDisk != content {
t.Errorf("workspace-mode Read = %q, want %q", gotDisk, content)
}
}