diff --git a/go.sum b/go.sum index 4f3a4d2d..6b7de472 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,6 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= -github.com/Yakwilik/go-yamlvalidator v0.1.0 h1:mj+HF3jIUKhPwFp1izfKWB0FUAn32C0Vxp/g7jQplDs= -github.com/Yakwilik/go-yamlvalidator v0.1.0/go.mod h1:/2WJvPxGxWLfbdKaKEPzYkiuFjLuvCrqsMb2NdQSdWI= github.com/Yakwilik/go-yamlvalidator v0.2.1 h1:9j9pj6pXf5KOgxKzoxhxaN2/Ed4ummIoDtOGj69TBK0= github.com/Yakwilik/go-yamlvalidator v0.2.1/go.mod h1:/2WJvPxGxWLfbdKaKEPzYkiuFjLuvCrqsMb2NdQSdWI= github.com/a8m/envsubst v1.4.3 h1:kDF7paGK8QACWYaQo6KtyYBozY2jhQrTuNNuUxQkhJY= diff --git a/internal/adapters/go_git/get_dir_walker.go b/internal/adapters/go_git/get_dir_walker.go index 7d1a32d9..ce65796a 100644 --- a/internal/adapters/go_git/get_dir_walker.go +++ b/internal/adapters/go_git/get_dir_walker.go @@ -3,8 +3,10 @@ package go_git import ( "errors" "fmt" + pathpkg "path" + "path/filepath" - "github.com/go-git/go-git/v5" + gogit "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/easyp-tech/easyp/internal/core" @@ -12,14 +14,38 @@ import ( ) func (g *GoGit) GetDirWalker(workingDir, gitRef, path string) (core.DirWalker, error) { - repository, err := git.PlainOpen(workingDir) + // Open the repository by searching upward from workingDir, so --root can be + // any subdirectory of the git repository (not only the config directory). + repository, err := gogit.PlainOpenWithOptions(workingDir, &gogit.PlainOpenOptions{ + DetectDotGit: true, + }) if err != nil { - if errors.Is(err, git.ErrRepositoryNotExists) { + if errors.Is(err, gogit.ErrRepositoryNotExists) { return nil, core.ErrRepositoryDoesNotExist } - return nil, fmt.Errorf("git.PlainOpen: %w", err) + return nil, fmt.Errorf("git.PlainOpenWithOptions: %w", err) } + + // Determine the repo root so we can compute the correct git-tree path. + wt, err := repository.Worktree() + if err != nil { + return nil, fmt.Errorf("repository.Worktree: %w", err) + } + repoRoot := wt.Filesystem.Root() + + // Compute the path from the repo root to workingDir. + rel, err := filepath.Rel(repoRoot, workingDir) + if err != nil { + return nil, fmt.Errorf("filepath.Rel: %w", err) + } + if !filepath.IsLocal(rel) { + return nil, fmt.Errorf("%w: %q is outside the git repository %q", core.ErrRootOutsideProject, workingDir, repoRoot) + } + + relSlash := filepath.ToSlash(rel) + gitPath := pathpkg.Join(relSlash, filepath.ToSlash(path)) + refName := plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", gitRef)) refAgainst, err := repository.Reference(refName, false) @@ -37,6 +63,6 @@ func (g *GoGit) GetDirWalker(workingDir, gitRef, path string) (core.DirWalker, e return nil, fmt.Errorf("commitAgainst.Tree: %w", err) } - gitTreeWalker := go_git.NewGitTreeWalker(treeAgainst, path) + gitTreeWalker := go_git.NewGitTreeWalker(treeAgainst, relSlash, gitPath) return gitTreeWalker, nil } diff --git a/internal/api/breaking_check.go b/internal/api/breaking_check.go index f964ed1c..584956b2 100644 --- a/internal/api/breaking_check.go +++ b/internal/api/breaking_check.go @@ -29,6 +29,15 @@ var ( Value: "master", } + flagBreakingCheckRoot = &cli.StringFlag{ + Name: "root", + Usage: "set root directory for file search (default: current working directory)", + Required: false, + HasBeenSet: false, + Value: "", + Aliases: []string{"r"}, + } + ErrBreakingCheckIssue = errors.New("has breaking check issue") ) @@ -49,6 +58,7 @@ func (b BreakingCheck) Command() *cli.Command { Flags: []cli.Flag{ flagLintDirectoryPath, flagAgainstBranchName, + flagBreakingCheckRoot, }, SkipFlagParsing: false, HideHelp: false, @@ -86,12 +96,12 @@ func (b BreakingCheck) Action(ctx *cli.Context) error { } func (b BreakingCheck) action(ctx *cli.Context, log logger.Logger) error { - workingDir, err := os.Getwd() + configPath, projectRoot, breakingCheckRoot, err := resolveRoots(ctx, flagBreakingCheckRoot.Name) if err != nil { - return fmt.Errorf("os.Getwd: %w", err) + return fmt.Errorf("resolveRoots: %w", err) } - cfg, err := config.New(ctx.Context, ctx.String(flags.Config.Name)) + cfg, err := config.New(ctx.Context, configPath) if err != nil { return fmt.Errorf("config.New: %w", err) } @@ -102,13 +112,14 @@ func (b BreakingCheck) action(ctx *cli.Context, log logger.Logger) error { cfg.BreakingCheck.AgainstGitRef = against } - dirWalker := fs.NewFSWalker(workingDir, ".") - app, err := buildCore(ctx.Context, log, *cfg, dirWalker) + // Walker for Core (lockfile etc) - strictly based on project root + projectWalker := fs.NewFSWalker(projectRoot, ".") + app, err := buildCore(ctx.Context, log, *cfg, projectWalker) if err != nil { return fmt.Errorf("buildCore: %w", err) } - issues, err := app.BreakingCheck(ctx.Context, workingDir, path) + issues, err := app.BreakingCheck(ctx.Context, projectRoot, breakingCheckRoot, path) if err != nil { return fmt.Errorf("app.BreakingCheck: %w", err) } diff --git a/internal/core/breaking_check.go b/internal/core/breaking_check.go index 7925a10b..ac6242fb 100644 --- a/internal/core/breaking_check.go +++ b/internal/core/breaking_check.go @@ -3,6 +3,7 @@ package core import ( "context" "fmt" + "log/slog" "path/filepath" "github.com/yoheimuta/go-protoparser/v4/interpret/unordered" @@ -12,6 +13,8 @@ import ( "github.com/easyp-tech/easyp/internal/fs/fs" ) +var ErrRootOutsideProject = fmt.Errorf("breaking check root must be inside the git repository") + type BreakingCheckConfig struct { // branch name to compare with AgainstGitRef string @@ -19,14 +22,26 @@ type BreakingCheckConfig struct { IgnoreDirs []string } -func (c *Core) BreakingCheck(ctx context.Context, workingDir, path string) ([]IssueInfo, error) { - fsWalker := fs.NewFSWalker(workingDir, path) +func (c *Core) BreakingCheck(ctx context.Context, projectRoot, workingDir, path string) ([]IssueInfo, error) { + c.logger.Debug( + ctx, "Paths for breaking check", + slog.String("projectRoot", projectRoot), + slog.String("workingDir", workingDir), + slog.String("path", path), + ) + if err := c.Download(ctx); err != nil { + return nil, fmt.Errorf("c.Download: %w", err) + } + + // read current state + fsWalker := fs.NewFSWalker(workingDir, path) currentProtoFiles, err := c.readProtoFiles(ctx, fsWalker) if err != nil { return nil, fmt.Errorf("c.readCurrentProtoFiles: %w", err) } + // read from ref branch againstFSWalker, err := c.currentProjectGitWalker.GetDirWalker( workingDir, c.breakingCheckConfig.AgainstGitRef, path, ) @@ -38,6 +53,8 @@ func (c *Core) BreakingCheck(ctx context.Context, workingDir, path string) ([]Is return nil, fmt.Errorf("c.readAgainstProtoFiles: %w", err) } + // --- + currentProtoData, err := collect(currentProtoFiles) if err != nil { return nil, fmt.Errorf("collect(current): %w", err) diff --git a/internal/fs/go_git/adapter.go b/internal/fs/go_git/adapter.go index 7fb9392d..f1150771 100644 --- a/internal/fs/go_git/adapter.go +++ b/internal/fs/go_git/adapter.go @@ -2,16 +2,35 @@ package go_git import ( "errors" + "fmt" "io" + "path" + "path/filepath" + "strings" "github.com/go-git/go-git/v5/plumbing/object" ) type GitTreeDiskAdapter struct { *object.Tree + root string } func (a *GitTreeDiskAdapter) Open(name string) (io.ReadCloser, error) { + if a.root != "" { + // Reject non-local import paths (e.g., those containing ".." or absolute paths) + if !isLocalPath(name) { + return nil, fmt.Errorf("non-local import path: %q", name) + } + // Prefer root-prefixed lookup to match the on-disk walker behaviour + withRoot := path.Join(a.root, name) + gitFile, err := a.File(withRoot) + if err == nil { + return gitFile.Reader() + } + } + + // Fall back to unrooted lookup (e.g., caller already passed a fully-qualified path) gitFile, err := a.File(name) if err != nil { return nil, err @@ -20,6 +39,20 @@ func (a *GitTreeDiskAdapter) Open(name string) (io.ReadCloser, error) { return gitFile.Reader() } +// isLocalPath reports whether p does not escape its base directory +// (no ".." components, not an absolute path). +func isLocalPath(p string) bool { + if filepath.IsAbs(p) { + return false + } + for _, segment := range strings.Split(p, "/") { + if segment == ".." { + return false + } + } + return true +} + func (a *GitTreeDiskAdapter) Create(name string) (io.WriteCloser, error) { return nil, errors.New("not implemented") } diff --git a/internal/fs/go_git/dir_walker.go b/internal/fs/go_git/dir_walker.go index 642307d0..7fb22436 100644 --- a/internal/fs/go_git/dir_walker.go +++ b/internal/fs/go_git/dir_walker.go @@ -6,9 +6,9 @@ import ( "github.com/easyp-tech/easyp/internal/core/path_helpers" ) -func NewGitTreeWalker(tree *object.Tree, path string) *GitTreeWalker { +func NewGitTreeWalker(tree *object.Tree, root, path string) *GitTreeWalker { return &GitTreeWalker{ - GitTreeDiskAdapter: &GitTreeDiskAdapter{tree}, + GitTreeDiskAdapter: &GitTreeDiskAdapter{tree, root}, tree: tree, path: path, }