Compare commits

...

1 Commits

Author SHA1 Message Date
Nikhil Sonti
8e83d3a670 fix(patch): handle canonical workspace paths 2026-05-04 18:03:36 -07:00
4 changed files with 147 additions and 10 deletions

View File

@@ -1,8 +1,12 @@
BINARY := browseros-patch
PREFIX ?= /usr/local/bin
GOBIN := $(shell go env GOBIN)
ifeq ($(GOBIN),)
GOBIN := $(shell go env GOPATH)/bin
endif
PREFIX ?= $(GOBIN)
VERSION ?= dev
.PHONY: build install clean test fmt
.PHONY: build install uninstall clean test fmt
build:
go build -ldflags "-X github.com/browseros-ai/BrowserOS/packages/browseros/tools/patch/cmd.Version=$(VERSION)" -o $(BINARY) .
@@ -17,6 +21,10 @@ else
endif
@echo "Installed $(BINARY) to $(PREFIX)/$(BINARY)"
uninstall:
rm -f $(PREFIX)/$(BINARY)
@echo "Removed $(PREFIX)/$(BINARY)"
test:
go test ./...

View File

@@ -3,6 +3,7 @@ package workspace
import (
"fmt"
"path/filepath"
"slices"
"strings"
)
@@ -15,21 +16,21 @@ func Detect(reg *Registry, cwd string) (Entry, error) {
return Entry{}, err
}
clean := filepath.Clean(abs)
realClean := canonicalPath(clean)
var best Entry
bestLen := -1
for _, ws := range reg.Workspaces {
base := filepath.Clean(ws.Path)
if clean == base || strings.HasPrefix(clean, base+string(filepath.Separator)) {
if len(base) > bestLen {
realBase := canonicalPath(base)
if containsPath(clean, base) || containsPath(realClean, realBase) {
if len(realBase) > bestLen {
best = ws
bestLen = len(base)
bestLen = len(realBase)
}
}
}
if bestLen == -1 {
return Entry{}, fmt.Errorf(
`not inside a registered workspace; run "browseros-patch list" to inspect workspaces or pass one by name`,
)
return Entry{}, detectError(clean, realClean, reg.Workspaces)
}
return best, nil
}
@@ -47,3 +48,43 @@ func Resolve(reg *Registry, name string, cwd string, src string) (Entry, error)
}
return Detect(reg, cwd)
}
func canonicalPath(path string) string {
realPath, err := filepath.EvalSymlinks(path)
if err != nil {
return filepath.Clean(path)
}
return filepath.Clean(realPath)
}
func containsPath(path string, base string) bool {
return path == base || strings.HasPrefix(path, base+string(filepath.Separator))
}
func detectError(cwd string, resolvedCWD string, workspaces []Entry) error {
var builder strings.Builder
builder.WriteString(`not inside a registered workspace; run "browseros-patch list" to inspect workspaces or pass one by name`)
builder.WriteString("\n")
builder.WriteString("cwd: ")
builder.WriteString(cwd)
if resolvedCWD != cwd {
builder.WriteString("\nresolved cwd: ")
builder.WriteString(resolvedCWD)
}
if len(workspaces) > 0 {
builder.WriteString("\nregistered workspaces:")
sorted := append([]Entry(nil), workspaces...)
slices.SortFunc(sorted, func(a, b Entry) int {
return strings.Compare(a.Name, b.Name)
})
for _, ws := range sorted {
builder.WriteString("\n ")
builder.WriteString(ws.Name)
builder.WriteString(" ")
builder.WriteString(ws.Path)
}
builder.WriteString("\nexample: browseros-patch diff ")
builder.WriteString(sorted[0].Name)
}
return fmt.Errorf("%s", builder.String())
}

View File

@@ -70,7 +70,7 @@ func NormalizeWorkspacePath(raw string) (string, error) {
if _, err := os.Stat(filepath.Join(clean, ".git")); err != nil {
return "", fmt.Errorf("workspace is not a git checkout: %s", clean)
}
return clean, nil
return canonicalPath(clean), nil
}
func (r *Registry) Get(name string) (Entry, error) {

View File

@@ -3,6 +3,7 @@ package workspace
import (
"os"
"path/filepath"
"strings"
"testing"
)
@@ -36,6 +37,10 @@ func TestRegistryDetectsLongestMatchingWorkspace(t *testing.T) {
t.Fatalf("mkdir: %v", err)
}
}
detectedPath := filepath.Join(child, "chrome", "browser")
if err := os.MkdirAll(detectedPath, 0o755); err != nil {
t.Fatalf("mkdir detected path: %v", err)
}
reg := &Registry{Version: 1}
if _, err := reg.Add("parent", parent); err != nil {
@@ -45,7 +50,7 @@ func TestRegistryDetectsLongestMatchingWorkspace(t *testing.T) {
t.Fatalf("add child: %v", err)
}
ws, err := Detect(reg, filepath.Join(child, "chrome", "browser"))
ws, err := Detect(reg, detectedPath)
if err != nil {
t.Fatalf("Detect: %v", err)
}
@@ -53,3 +58,86 @@ func TestRegistryDetectsLongestMatchingWorkspace(t *testing.T) {
t.Fatalf("expected child workspace, got %q", ws.Name)
}
}
func TestDetectMatchesSymlinkedWorkingDirectory(t *testing.T) {
root := t.TempDir()
workspacePath := filepath.Join(root, "chromium-1", "src")
if err := os.MkdirAll(filepath.Join(workspacePath, ".git"), 0o755); err != nil {
t.Fatalf("mkdir workspace: %v", err)
}
if err := os.MkdirAll(filepath.Join(workspacePath, "chrome", "browser"), 0o755); err != nil {
t.Fatalf("mkdir workspace child: %v", err)
}
linkPath := filepath.Join(root, "ch-1")
if err := os.Symlink(workspacePath, linkPath); err != nil {
t.Fatalf("symlink workspace: %v", err)
}
reg := &Registry{Version: 1}
if _, err := reg.Add("ch1", workspacePath); err != nil {
t.Fatalf("add workspace: %v", err)
}
ws, err := Detect(reg, filepath.Join(linkPath, "chrome", "browser"))
if err != nil {
t.Fatalf("Detect: %v", err)
}
if ws.Name != "ch1" {
t.Fatalf("expected ch1 workspace, got %q", ws.Name)
}
}
func TestRegistryAddStoresCanonicalWorkspacePath(t *testing.T) {
root := t.TempDir()
workspacePath := filepath.Join(root, "chromium-1", "src")
if err := os.MkdirAll(filepath.Join(workspacePath, ".git"), 0o755); err != nil {
t.Fatalf("mkdir workspace: %v", err)
}
linkPath := filepath.Join(root, "ch-1")
if err := os.Symlink(workspacePath, linkPath); err != nil {
t.Fatalf("symlink workspace: %v", err)
}
reg := &Registry{Version: 1}
entry, err := reg.Add("ch1", linkPath)
if err != nil {
t.Fatalf("add workspace: %v", err)
}
expectedPath := canonicalPath(workspacePath)
if entry.Path != expectedPath {
t.Fatalf("expected canonical path %q, got %q", expectedPath, entry.Path)
}
}
func TestDetectErrorIncludesPathContextAndWorkspaceHint(t *testing.T) {
root := t.TempDir()
workspacePath := filepath.Join(root, "chromium-1", "src")
if err := os.MkdirAll(filepath.Join(workspacePath, ".git"), 0o755); err != nil {
t.Fatalf("mkdir workspace: %v", err)
}
outsidePath := filepath.Join(root, "outside")
if err := os.MkdirAll(outsidePath, 0o755); err != nil {
t.Fatalf("mkdir outside: %v", err)
}
reg := &Registry{Version: 1}
if _, err := reg.Add("ch1", workspacePath); err != nil {
t.Fatalf("add workspace: %v", err)
}
_, err := Detect(reg, outsidePath)
if err == nil {
t.Fatalf("expected Detect to fail")
}
message := err.Error()
for _, want := range []string{
"cwd: " + outsidePath,
"registered workspaces:",
"ch1 " + canonicalPath(workspacePath),
"example: browseros-patch diff ch1",
} {
if !strings.Contains(message, want) {
t.Fatalf("expected error to contain %q, got:\n%s", want, message)
}
}
}