Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cmd/waza/tokens/compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ func compareFilesets(baseRef, headRef, rootDir string, baseFiles, headFiles map[
var hasBase bool
if baseFiles[file] {
var err error
baseContent, err = git.GetFileFromRef(rootDir, file, baseRef)
baseContent, err = git.GetCWDFileFromRef(rootDir, file, baseRef)
if err == nil {
hasBase = true
}
Expand All @@ -392,7 +392,7 @@ func compareFilesets(baseRef, headRef, rootDir string, baseFiles, headFiles map[
}
} else {
var err error
headContent, err = git.GetFileFromRef(rootDir, file, headRef)
headContent, err = git.GetCWDFileFromRef(rootDir, file, headRef)
if err == nil {
hasHead = true
}
Expand Down
73 changes: 73 additions & 0 deletions cmd/waza/tokens/compare_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,38 @@ func TestCompare_Branches(t *testing.T) {
})
}

// TestCompare_Subdirectory verifies that compare works correctly when the
// working directory is a subdirectory of the git repository. This is a
// regression test for a bug where git show HEAD:file treated paths as
// repo-root-relative while ls-tree returned CWD-relative paths.
func TestCompare_Subdirectory(t *testing.T) {
dir := initRepo(t)

// Create files in a subdirectory and commit them
subDir := filepath.Join(dir, "skills", "my-skill")
require.NoError(t, os.MkdirAll(subDir, 0700))
require.NoError(t, os.WriteFile(filepath.Join(subDir, "SKILL.md"), []byte("# My Skill"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(subDir, "README.md"), []byte("# Hello"), 0o644))
commit(t, dir, "add skill files")

// Modify a file without committing
require.NoError(t, os.WriteFile(filepath.Join(subDir, "README.md"), []byte("# Hello with more content added"), 0o644))

// Run compare from the subdirectory
t.Chdir(subDir)

out := new(bytes.Buffer)
cmd := newCompareCmd()
cmd.SetOut(out)

require.NoError(t, cmd.Execute())
output := out.String()

// README.md should be "modified", not "added"
require.Contains(t, output, "πŸ“ˆ", "README.md should show as modified (πŸ“ˆ), not added (πŸ†•)")
require.NotContains(t, output, "πŸ†•", "no files should show as added since all exist at HEAD")
}

Comment thread
chlowell marked this conversation as resolved.
// TestCompare_InvalidRef verifies that an invalid/nonexistent ref does not cause
// a hard error. The TypeScript implementation catches all git errors and returns
// empty results; the Go implementation should behave the same way.
Expand Down Expand Up @@ -589,6 +621,47 @@ func TestCompare_Skills(t *testing.T) {
require.Len(t, report.Files, 1)
require.Equal(t, "custom-skills/delta/SKILL.md", report.Files[0].File)
})

t.Run("compares two git refs with custom roots", func(t *testing.T) {
dir := initRepo(t)

// Set up .waza.yaml with custom skill root and a skill file
cfg := "paths:\n skills: my-skills\n"
require.NoError(t, os.WriteFile(filepath.Join(dir, ".waza.yaml"), []byte(cfg), 0o644))
require.NoError(t, os.MkdirAll(filepath.Join(dir, "my-skills", "echo"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, "my-skills", "echo", "SKILL.md"), []byte("# Echo\nv1 content"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "README.md"), []byte("# Repo readme"), 0o644))
commit(t, dir, "initial")

// Create branch with modified skill
gitCmd := exec.Command("git", "checkout", "-b", "updated")
gitCmd.Dir = dir
require.NoError(t, gitCmd.Run())
require.NoError(t, os.WriteFile(filepath.Join(dir, "my-skills", "echo", "SKILL.md"), []byte("# Echo\nv2 content with extra words added"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "README.md"), []byte("# Updated readme with more text"), 0o644))
commit(t, dir, "update skill")

// Compare main to updated branch with --skills
out := new(bytes.Buffer)
cmd := newCompareCmd()
cmd.SetOut(out)
cmd.SetErr(new(bytes.Buffer))
cmd.SetArgs([]string{"main", "updated", "--skills", "--format", "json"})

require.NoError(t, cmd.Execute())

var report comparisonReport
require.NoError(t, json.Unmarshal(out.Bytes(), &report))
require.Equal(t, "main", report.BaseRef)
require.Equal(t, "updated", report.HeadRef)
// Only SKILL.md under my-skills/ should appear β€” README.md is excluded
require.Len(t, report.Files, 1)
require.Equal(t, "my-skills/echo/SKILL.md", report.Files[0].File)
require.Equal(t, "modified", report.Files[0].Status)
require.NotNil(t, report.Files[0].Before, "Before should be populated from base ref")
require.NotNil(t, report.Files[0].After, "After should be populated from head ref")
require.Greater(t, report.Files[0].After.Tokens, report.Files[0].Before.Tokens)
})
}

func TestCompare_Threshold(t *testing.T) {
Expand Down
22 changes: 22 additions & 0 deletions cmd/waza/tokens/internal/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,33 @@ func RefExists(dir, ref string) bool {
}

// GetFileFromRef retrieves the content of a file at a given git ref.
// The file path is resolved relative to the repository root, regardless
// of the working directory. Use [GetCWDFileFromRef] when the path comes
// from a CWD-relative listing such as git ls-tree run from a subdirectory.
// It returns [ErrFileNotFound] (wrapped) when the path does not exist at the
// given ref, so callers can distinguish "missing file" from other git errors.
func GetFileFromRef(dir, file, ref string) (string, error) {
cmd := exec.Command("git", "-C", dir, "show", ref+":"+file)
out, err := cmd.Output()
if err != nil {
if exitErr, ok := errors.AsType[*exec.ExitError](err); ok {
stderr := string(exitErr.Stderr)
if strings.Contains(stderr, "does not exist") {
return "", fmt.Errorf("reading %q at %s: %w", file, ref, ErrFileNotFound)
}
}
return "", fmt.Errorf("reading %q at %s: %w", file, ref, err)
Comment thread
chlowell marked this conversation as resolved.
}
return string(out), nil
}

// GetCWDFileFromRef retrieves the content of a file at a given git ref,
// resolving the path relative to dir (the working directory) rather than the
// repository root. This matches how [GetFilesFromRef] returns CWD-relative
// paths from git ls-tree when run from a subdirectory.
Comment thread
chlowell marked this conversation as resolved.
func GetCWDFileFromRef(dir, file, ref string) (string, error) {
cmd := exec.Command("git", "-C", dir, "show", ref+":./"+file)
Comment thread
chlowell marked this conversation as resolved.
out, err := cmd.Output()
if err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
Expand Down
53 changes: 53 additions & 0 deletions cmd/waza/tokens/internal/git/git_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,56 @@ func TestGetFileFromRefNotFoundAndOtherErrors(t *testing.T) {
t.Fatalf("did not expect ErrFileNotFound for invalid ref")
}
}

func TestGetCWDFileFromRef(t *testing.T) {
repo := initTestRepo(t)
writeFile(t, repo, "root.md", "root content\n")
writeFile(t, repo, "sub/nested.md", "nested content\n")
_ = commitAll(t, repo, "initial")

t.Run("from repo root", func(t *testing.T) {
content, err := GetCWDFileFromRef(repo, "sub/nested.md", "HEAD")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if content != "nested content\n" {
t.Fatalf("expected nested content, got %q", content)
}
})

t.Run("from subdirectory", func(t *testing.T) {
subDir := filepath.Join(repo, "sub")
// GetCWDFileFromRef resolves paths relative to dir, so "nested.md"
// from the sub/ directory should find sub/nested.md in the tree.
content, err := GetCWDFileFromRef(subDir, "nested.md", "HEAD")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if content != "nested content\n" {
t.Fatalf("expected nested content, got %q", content)
}
})

t.Run("repo-root file not visible from subdirectory", func(t *testing.T) {
subDir := filepath.Join(repo, "sub")
// "root.md" exists at the repo root but not under sub/,
// so GetCWDFileFromRef from sub/ should not find it.
_, err := GetCWDFileFromRef(subDir, "root.md", "HEAD")
if err == nil {
t.Fatalf("expected error reading repo-root file from subdirectory")
}
Comment thread
chlowell marked this conversation as resolved.
})

t.Run("GetFileFromRef still uses repo-root paths", func(t *testing.T) {
subDir := filepath.Join(repo, "sub")
// GetFileFromRef always resolves relative to repo root,
// so "root.md" should be found even when dir is a subdirectory.
content, err := GetFileFromRef(subDir, "root.md", "HEAD")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if content != "root content\n" {
t.Fatalf("expected root content, got %q", content)
}
})
}
Loading