From 146ab9f2897b1ba0c558174ae16d07280dae5969 Mon Sep 17 00:00:00 2001 From: Vasilii Bliznetsov Date: Wed, 29 Apr 2026 21:18:00 +0300 Subject: [PATCH 01/10] base --- internal/api/breaking_check.go | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/internal/api/breaking_check.go b/internal/api/breaking_check.go index f964ed1c..ee8d9c72 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,13 @@ func (b BreakingCheck) action(ctx *cli.Context, log logger.Logger) error { cfg.BreakingCheck.AgainstGitRef = against } - dirWalker := fs.NewFSWalker(workingDir, ".") + dirWalker := fs.NewFSWalker(breakingCheckRoot, ".") app, err := buildCore(ctx.Context, log, *cfg, dirWalker) if err != nil { return fmt.Errorf("buildCore: %w", err) } - issues, err := app.BreakingCheck(ctx.Context, workingDir, path) + issues, err := app.BreakingCheck(ctx.Context, breakingCheckRoot, path) if err != nil { return fmt.Errorf("app.BreakingCheck: %w", err) } From a0071cd8d30abd7d9648b2c08ee4ce6706e0b02a Mon Sep 17 00:00:00 2001 From: Vasilii Bliznetsov Date: Wed, 29 Apr 2026 21:42:01 +0300 Subject: [PATCH 02/10] base --- internal/api/breaking_check.go | 13 +++++++------ internal/core/breaking_check.go | 4 ++++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/internal/api/breaking_check.go b/internal/api/breaking_check.go index ee8d9c72..a05db671 100644 --- a/internal/api/breaking_check.go +++ b/internal/api/breaking_check.go @@ -106,18 +106,19 @@ func (b BreakingCheck) action(ctx *cli.Context, log logger.Logger) error { return fmt.Errorf("config.New: %w", err) } + // 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) + } + path := ctx.String(flagLintDirectoryPath.Name) against := ctx.String(flagAgainstBranchName.Name) if cfg.BreakingCheck.AgainstGitRef == "" { cfg.BreakingCheck.AgainstGitRef = against } - dirWalker := fs.NewFSWalker(breakingCheckRoot, ".") - app, err := buildCore(ctx.Context, log, *cfg, dirWalker) - if err != nil { - return fmt.Errorf("buildCore: %w", err) - } - issues, err := app.BreakingCheck(ctx.Context, 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..7c21631a 100644 --- a/internal/core/breaking_check.go +++ b/internal/core/breaking_check.go @@ -20,6 +20,10 @@ type BreakingCheckConfig struct { } func (c *Core) BreakingCheck(ctx context.Context, workingDir, path string) ([]IssueInfo, error) { + if err := c.Download(ctx); err != nil { + return nil, fmt.Errorf("c.Download: %w", err) + } + fsWalker := fs.NewFSWalker(workingDir, path) currentProtoFiles, err := c.readProtoFiles(ctx, fsWalker) From 272d8f7c48a95f5eb1305cf7d6924bfa88e1cc0f Mon Sep 17 00:00:00 2001 From: Vasilii Bliznetsov Date: Thu, 30 Apr 2026 09:33:00 +0300 Subject: [PATCH 03/10] base2 --- internal/api/breaking_check.go | 2 +- internal/core/breaking_check.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/api/breaking_check.go b/internal/api/breaking_check.go index a05db671..dab4faa1 100644 --- a/internal/api/breaking_check.go +++ b/internal/api/breaking_check.go @@ -119,7 +119,7 @@ func (b BreakingCheck) action(ctx *cli.Context, log logger.Logger) error { cfg.BreakingCheck.AgainstGitRef = against } - issues, err := app.BreakingCheck(ctx.Context, breakingCheckRoot, 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 7c21631a..58e27cf2 100644 --- a/internal/core/breaking_check.go +++ b/internal/core/breaking_check.go @@ -19,7 +19,7 @@ type BreakingCheckConfig struct { IgnoreDirs []string } -func (c *Core) BreakingCheck(ctx context.Context, workingDir, path string) ([]IssueInfo, error) { +func (c *Core) BreakingCheck(ctx context.Context, projectRoot, workingDir, path string) ([]IssueInfo, error) { if err := c.Download(ctx); err != nil { return nil, fmt.Errorf("c.Download: %w", err) } From e7b7a089bc477198b41008639035b36e9209e758 Mon Sep 17 00:00:00 2001 From: Vasilii Bliznetsov Date: Thu, 30 Apr 2026 23:45:27 +0300 Subject: [PATCH 04/10] 1 --- internal/core/breaking_check.go | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/internal/core/breaking_check.go b/internal/core/breaking_check.go index 58e27cf2..c3475cf9 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" @@ -20,19 +21,25 @@ type BreakingCheckConfig struct { } func (c *Core) BreakingCheck(ctx context.Context, projectRoot, workingDir, path string) ([]IssueInfo, error) { - if err := c.Download(ctx); err != nil { - return nil, fmt.Errorf("c.Download: %w", err) + rel, err := filepath.Rel(projectRoot, workingDir) + if err != nil { + return nil, fmt.Errorf("filepath.Rel: %w", err) } - fsWalker := fs.NewFSWalker(workingDir, path) + gitPath := filepath.Join(rel, path) + c.logger.Debug( + ctx, "Paths for breaking check", + slog.String("rel", rel), + slog.String("path", path), + slog.String("gitPath", gitPath), + ) - currentProtoFiles, err := c.readProtoFiles(ctx, fsWalker) - if err != nil { - return nil, fmt.Errorf("c.readCurrentProtoFiles: %w", err) + if err := c.Download(ctx); err != nil { + return nil, fmt.Errorf("c.Download: %w", err) } againstFSWalker, err := c.currentProjectGitWalker.GetDirWalker( - workingDir, c.breakingCheckConfig.AgainstGitRef, path, + projectRoot, c.breakingCheckConfig.AgainstGitRef, gitPath, ) if err != nil { return nil, fmt.Errorf("c.currentProjectGitWalker.GetDirWalker: %w", err) @@ -42,6 +49,12 @@ func (c *Core) BreakingCheck(ctx context.Context, projectRoot, workingDir, path return nil, fmt.Errorf("c.readAgainstProtoFiles: %w", err) } + fsWalker := fs.NewFSWalker(workingDir, path) + currentProtoFiles, err := c.readProtoFiles(ctx, fsWalker) + if err != nil { + return nil, fmt.Errorf("c.readCurrentProtoFiles: %w", err) + } + currentProtoData, err := collect(currentProtoFiles) if err != nil { return nil, fmt.Errorf("collect(current): %w", err) From f860215483c67de463e533079ef9d962b024cb56 Mon Sep 17 00:00:00 2001 From: Vasilii Bliznetsov Date: Sun, 3 May 2026 18:55:42 +0300 Subject: [PATCH 05/10] implement --- internal/adapters/go_git/get_dir_walker.go | 4 ++-- internal/core/breaking_check.go | 16 ++++++++++------ internal/core/dom.go | 2 +- internal/fs/go_git/adapter.go | 9 +++++++++ internal/fs/go_git/dir_walker.go | 6 ++++-- 5 files changed, 26 insertions(+), 11 deletions(-) diff --git a/internal/adapters/go_git/get_dir_walker.go b/internal/adapters/go_git/get_dir_walker.go index 7d1a32d9..e9eff75c 100644 --- a/internal/adapters/go_git/get_dir_walker.go +++ b/internal/adapters/go_git/get_dir_walker.go @@ -11,7 +11,7 @@ import ( "github.com/easyp-tech/easyp/internal/fs/go_git" ) -func (g *GoGit) GetDirWalker(workingDir, gitRef, path string) (core.DirWalker, error) { +func (g *GoGit) GetDirWalker(workingDir, gitRef, root, path string) (core.DirWalker, error) { repository, err := git.PlainOpen(workingDir) if err != nil { if errors.Is(err, git.ErrRepositoryNotExists) { @@ -37,6 +37,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, root, path) return gitTreeWalker, nil } diff --git a/internal/core/breaking_check.go b/internal/core/breaking_check.go index c3475cf9..50dfe0c2 100644 --- a/internal/core/breaking_check.go +++ b/internal/core/breaking_check.go @@ -38,8 +38,16 @@ func (c *Core) BreakingCheck(ctx context.Context, projectRoot, workingDir, path 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( - projectRoot, c.breakingCheckConfig.AgainstGitRef, gitPath, + projectRoot, c.breakingCheckConfig.AgainstGitRef, rel, gitPath, ) if err != nil { return nil, fmt.Errorf("c.currentProjectGitWalker.GetDirWalker: %w", err) @@ -49,11 +57,7 @@ func (c *Core) BreakingCheck(ctx context.Context, projectRoot, workingDir, path return nil, fmt.Errorf("c.readAgainstProtoFiles: %w", err) } - fsWalker := fs.NewFSWalker(workingDir, path) - currentProtoFiles, err := c.readProtoFiles(ctx, fsWalker) - if err != nil { - return nil, fmt.Errorf("c.readCurrentProtoFiles: %w", err) - } + // --- currentProtoData, err := collect(currentProtoFiles) if err != nil { diff --git a/internal/core/dom.go b/internal/core/dom.go index 2f3531fc..a84286cc 100644 --- a/internal/core/dom.go +++ b/internal/core/dom.go @@ -25,7 +25,7 @@ type ( // CurrentProjectGitWalker is provider for fs walking for current project CurrentProjectGitWalker interface { - GetDirWalker(workingDir, gitRef, path string) (DirWalker, error) + GetDirWalker(workingDir, gitRef, root, path string) (DirWalker, error) } // IssueInfo contains the information of an issue and the path. diff --git a/internal/fs/go_git/adapter.go b/internal/fs/go_git/adapter.go index 7fb9392d..a2fb9a88 100644 --- a/internal/fs/go_git/adapter.go +++ b/internal/fs/go_git/adapter.go @@ -3,16 +3,25 @@ package go_git import ( "errors" "io" + "path" "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) { gitFile, err := a.File(name) + if err == nil { + return gitFile.Reader() + } + + // add root and try to open again + withRoot := path.Join(a.root, name) + gitFile, err = a.File(withRoot) if err != nil { return nil, err } diff --git a/internal/fs/go_git/dir_walker.go b/internal/fs/go_git/dir_walker.go index 642307d0..c8cd59cb 100644 --- a/internal/fs/go_git/dir_walker.go +++ b/internal/fs/go_git/dir_walker.go @@ -6,10 +6,11 @@ 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, + root: root, path: path, } } @@ -18,6 +19,7 @@ type GitTreeWalker struct { *GitTreeDiskAdapter tree *object.Tree + root string path string } From 8c746338ba72e3810a4f17d5fbcbb25bf8e857d7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 3 May 2026 20:29:40 +0000 Subject: [PATCH 06/10] fix: address code review feedback for breaking check root flag Agent-Logs-Url: https://github.com/easyp-tech/easyp/sessions/d72f719b-a9c6-4dcd-9ef5-dc3e3fd34fb3 Co-authored-by: Yakwilik <92339556+Yakwilik@users.noreply.github.com> --- .../core/mocks/CurrentProjectGitWalker.go | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/internal/core/mocks/CurrentProjectGitWalker.go b/internal/core/mocks/CurrentProjectGitWalker.go index 7e0292fb..e1aef656 100644 --- a/internal/core/mocks/CurrentProjectGitWalker.go +++ b/internal/core/mocks/CurrentProjectGitWalker.go @@ -20,9 +20,9 @@ func (_m *CurrentProjectGitWalker) EXPECT() *CurrentProjectGitWalker_Expecter { return &CurrentProjectGitWalker_Expecter{mock: &_m.Mock} } -// GetDirWalker provides a mock function with given fields: workingDir, gitRef, path -func (_m *CurrentProjectGitWalker) GetDirWalker(workingDir string, gitRef string, path string) (core.DirWalker, error) { - ret := _m.Called(workingDir, gitRef, path) +// GetDirWalker provides a mock function with given fields: workingDir, gitRef, root, path +func (_m *CurrentProjectGitWalker) GetDirWalker(workingDir string, gitRef string, root string, path string) (core.DirWalker, error) { + ret := _m.Called(workingDir, gitRef, root, path) if len(ret) == 0 { panic("no return value specified for GetDirWalker") @@ -30,19 +30,19 @@ func (_m *CurrentProjectGitWalker) GetDirWalker(workingDir string, gitRef string var r0 core.DirWalker var r1 error - if rf, ok := ret.Get(0).(func(string, string, string) (core.DirWalker, error)); ok { - return rf(workingDir, gitRef, path) + if rf, ok := ret.Get(0).(func(string, string, string, string) (core.DirWalker, error)); ok { + return rf(workingDir, gitRef, root, path) } - if rf, ok := ret.Get(0).(func(string, string, string) core.DirWalker); ok { - r0 = rf(workingDir, gitRef, path) + if rf, ok := ret.Get(0).(func(string, string, string, string) core.DirWalker); ok { + r0 = rf(workingDir, gitRef, root, path) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(core.DirWalker) } } - if rf, ok := ret.Get(1).(func(string, string, string) error); ok { - r1 = rf(workingDir, gitRef, path) + if rf, ok := ret.Get(1).(func(string, string, string, string) error); ok { + r1 = rf(workingDir, gitRef, root, path) } else { r1 = ret.Error(1) } @@ -58,14 +58,15 @@ type CurrentProjectGitWalker_GetDirWalker_Call struct { // GetDirWalker is a helper method to define mock.On call // - workingDir string // - gitRef string +// - root string // - path string -func (_e *CurrentProjectGitWalker_Expecter) GetDirWalker(workingDir interface{}, gitRef interface{}, path interface{}) *CurrentProjectGitWalker_GetDirWalker_Call { - return &CurrentProjectGitWalker_GetDirWalker_Call{Call: _e.mock.On("GetDirWalker", workingDir, gitRef, path)} +func (_e *CurrentProjectGitWalker_Expecter) GetDirWalker(workingDir interface{}, gitRef interface{}, root interface{}, path interface{}) *CurrentProjectGitWalker_GetDirWalker_Call { + return &CurrentProjectGitWalker_GetDirWalker_Call{Call: _e.mock.On("GetDirWalker", workingDir, gitRef, root, path)} } -func (_c *CurrentProjectGitWalker_GetDirWalker_Call) Run(run func(workingDir string, gitRef string, path string)) *CurrentProjectGitWalker_GetDirWalker_Call { +func (_c *CurrentProjectGitWalker_GetDirWalker_Call) Run(run func(workingDir string, gitRef string, root string, path string)) *CurrentProjectGitWalker_GetDirWalker_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].(string), args[2].(string)) + run(args[0].(string), args[1].(string), args[2].(string), args[3].(string)) }) return _c } @@ -75,7 +76,7 @@ func (_c *CurrentProjectGitWalker_GetDirWalker_Call) Return(_a0 core.DirWalker, return _c } -func (_c *CurrentProjectGitWalker_GetDirWalker_Call) RunAndReturn(run func(string, string, string) (core.DirWalker, error)) *CurrentProjectGitWalker_GetDirWalker_Call { +func (_c *CurrentProjectGitWalker_GetDirWalker_Call) RunAndReturn(run func(string, string, string, string) (core.DirWalker, error)) *CurrentProjectGitWalker_GetDirWalker_Call { _c.Call.Return(run) return _c } From 6315013f03f4cb670b82ae958f4129aa8fe62ef4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 3 May 2026 20:33:28 +0000 Subject: [PATCH 07/10] fix: address all code review feedback for breaking check root flag Agent-Logs-Url: https://github.com/easyp-tech/easyp/sessions/d72f719b-a9c6-4dcd-9ef5-dc3e3fd34fb3 Co-authored-by: Yakwilik <92339556+Yakwilik@users.noreply.github.com> --- go.sum | 2 -- internal/core/breaking_check.go | 15 ++++++++++--- internal/fs/go_git/adapter.go | 36 ++++++++++++++++++++++++++------ internal/fs/go_git/dir_walker.go | 2 -- 4 files changed, 42 insertions(+), 13 deletions(-) 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/core/breaking_check.go b/internal/core/breaking_check.go index 50dfe0c2..5c91c253 100644 --- a/internal/core/breaking_check.go +++ b/internal/core/breaking_check.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log/slog" + pathpkg "path" "path/filepath" "github.com/yoheimuta/go-protoparser/v4/interpret/unordered" @@ -13,6 +14,8 @@ import ( "github.com/easyp-tech/easyp/internal/fs/fs" ) +var ErrRootOutsideProject = fmt.Errorf("breaking check root must be inside the project root") + type BreakingCheckConfig struct { // branch name to compare with AgainstGitRef string @@ -26,10 +29,16 @@ func (c *Core) BreakingCheck(ctx context.Context, projectRoot, workingDir, path return nil, fmt.Errorf("filepath.Rel: %w", err) } - gitPath := filepath.Join(rel, path) + if !filepath.IsLocal(rel) { + return nil, fmt.Errorf("%w: %q is not inside %q", ErrRootOutsideProject, workingDir, projectRoot) + } + + // Git tree paths must use forward slashes regardless of OS + relSlash := filepath.ToSlash(rel) + gitPath := pathpkg.Join(relSlash, filepath.ToSlash(path)) c.logger.Debug( ctx, "Paths for breaking check", - slog.String("rel", rel), + slog.String("rel", relSlash), slog.String("path", path), slog.String("gitPath", gitPath), ) @@ -47,7 +56,7 @@ func (c *Core) BreakingCheck(ctx context.Context, projectRoot, workingDir, path // read from ref branch againstFSWalker, err := c.currentProjectGitWalker.GetDirWalker( - projectRoot, c.breakingCheckConfig.AgainstGitRef, rel, gitPath, + projectRoot, c.breakingCheckConfig.AgainstGitRef, relSlash, gitPath, ) if err != nil { return nil, fmt.Errorf("c.currentProjectGitWalker.GetDirWalker: %w", err) diff --git a/internal/fs/go_git/adapter.go b/internal/fs/go_git/adapter.go index a2fb9a88..5360a9e3 100644 --- a/internal/fs/go_git/adapter.go +++ b/internal/fs/go_git/adapter.go @@ -2,8 +2,11 @@ package go_git import ( "errors" + "fmt" "io" "path" + "path/filepath" + "strings" "github.com/go-git/go-git/v5/plumbing/object" ) @@ -14,14 +17,21 @@ type GitTreeDiskAdapter struct { } func (a *GitTreeDiskAdapter) Open(name string) (io.ReadCloser, error) { - gitFile, err := a.File(name) - if err == nil { - return gitFile.Reader() + 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() + } } - // add root and try to open again - withRoot := path.Join(a.root, name) - gitFile, err = a.File(withRoot) + // 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 } @@ -29,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) || path.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 c8cd59cb..7fb22436 100644 --- a/internal/fs/go_git/dir_walker.go +++ b/internal/fs/go_git/dir_walker.go @@ -10,7 +10,6 @@ func NewGitTreeWalker(tree *object.Tree, root, path string) *GitTreeWalker { return &GitTreeWalker{ GitTreeDiskAdapter: &GitTreeDiskAdapter{tree, root}, tree: tree, - root: root, path: path, } } @@ -19,7 +18,6 @@ type GitTreeWalker struct { *GitTreeDiskAdapter tree *object.Tree - root string path string } From 58dbf67e06a5385359d413fe738bad9eb20ca7d6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 3 May 2026 20:34:43 +0000 Subject: [PATCH 08/10] fix: simplify isLocalPath and improve error message clarity Agent-Logs-Url: https://github.com/easyp-tech/easyp/sessions/d72f719b-a9c6-4dcd-9ef5-dc3e3fd34fb3 Co-authored-by: Yakwilik <92339556+Yakwilik@users.noreply.github.com> --- internal/core/breaking_check.go | 2 +- internal/fs/go_git/adapter.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/core/breaking_check.go b/internal/core/breaking_check.go index 5c91c253..171f49d4 100644 --- a/internal/core/breaking_check.go +++ b/internal/core/breaking_check.go @@ -30,7 +30,7 @@ func (c *Core) BreakingCheck(ctx context.Context, projectRoot, workingDir, path } if !filepath.IsLocal(rel) { - return nil, fmt.Errorf("%w: %q is not inside %q", ErrRootOutsideProject, workingDir, projectRoot) + return nil, fmt.Errorf("%w: root path %q is outside project root %q", ErrRootOutsideProject, workingDir, projectRoot) } // Git tree paths must use forward slashes regardless of OS diff --git a/internal/fs/go_git/adapter.go b/internal/fs/go_git/adapter.go index 5360a9e3..f1150771 100644 --- a/internal/fs/go_git/adapter.go +++ b/internal/fs/go_git/adapter.go @@ -42,7 +42,7 @@ func (a *GitTreeDiskAdapter) Open(name string) (io.ReadCloser, error) { // 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) || path.IsAbs(p) { + if filepath.IsAbs(p) { return false } for _, segment := range strings.Split(p, "/") { From 8148ffe43f968236afb22cc05169110dd1358d25 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 3 May 2026 20:48:22 +0000 Subject: [PATCH 09/10] fix: allow --root outside config dir by discovering git repo root dynamically Agent-Logs-Url: https://github.com/easyp-tech/easyp/sessions/23795cc4-821b-4d33-94e0-ffe6b20db0db Co-authored-by: Yakwilik <92339556+Yakwilik@users.noreply.github.com> --- internal/adapters/go_git/get_dir_walker.go | 38 ++++++++++++++++--- internal/core/breaking_check.go | 21 ++-------- internal/core/dom.go | 2 +- .../core/mocks/CurrentProjectGitWalker.go | 29 +++++++------- 4 files changed, 51 insertions(+), 39 deletions(-) diff --git a/internal/adapters/go_git/get_dir_walker.go b/internal/adapters/go_git/get_dir_walker.go index e9eff75c..ce65796a 100644 --- a/internal/adapters/go_git/get_dir_walker.go +++ b/internal/adapters/go_git/get_dir_walker.go @@ -3,23 +3,49 @@ 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" "github.com/easyp-tech/easyp/internal/fs/go_git" ) -func (g *GoGit) GetDirWalker(workingDir, gitRef, root, path string) (core.DirWalker, error) { - repository, err := git.PlainOpen(workingDir) +func (g *GoGit) GetDirWalker(workingDir, gitRef, path string) (core.DirWalker, error) { + // 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, root, path string) (core.DirWal return nil, fmt.Errorf("commitAgainst.Tree: %w", err) } - gitTreeWalker := go_git.NewGitTreeWalker(treeAgainst, root, path) + gitTreeWalker := go_git.NewGitTreeWalker(treeAgainst, relSlash, gitPath) return gitTreeWalker, nil } diff --git a/internal/core/breaking_check.go b/internal/core/breaking_check.go index 171f49d4..ac6242fb 100644 --- a/internal/core/breaking_check.go +++ b/internal/core/breaking_check.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "log/slog" - pathpkg "path" "path/filepath" "github.com/yoheimuta/go-protoparser/v4/interpret/unordered" @@ -14,7 +13,7 @@ import ( "github.com/easyp-tech/easyp/internal/fs/fs" ) -var ErrRootOutsideProject = fmt.Errorf("breaking check root must be inside the project root") +var ErrRootOutsideProject = fmt.Errorf("breaking check root must be inside the git repository") type BreakingCheckConfig struct { // branch name to compare with @@ -24,23 +23,11 @@ type BreakingCheckConfig struct { } func (c *Core) BreakingCheck(ctx context.Context, projectRoot, workingDir, path string) ([]IssueInfo, error) { - rel, err := filepath.Rel(projectRoot, workingDir) - if err != nil { - return nil, fmt.Errorf("filepath.Rel: %w", err) - } - - if !filepath.IsLocal(rel) { - return nil, fmt.Errorf("%w: root path %q is outside project root %q", ErrRootOutsideProject, workingDir, projectRoot) - } - - // Git tree paths must use forward slashes regardless of OS - relSlash := filepath.ToSlash(rel) - gitPath := pathpkg.Join(relSlash, filepath.ToSlash(path)) c.logger.Debug( ctx, "Paths for breaking check", - slog.String("rel", relSlash), + slog.String("projectRoot", projectRoot), + slog.String("workingDir", workingDir), slog.String("path", path), - slog.String("gitPath", gitPath), ) if err := c.Download(ctx); err != nil { @@ -56,7 +43,7 @@ func (c *Core) BreakingCheck(ctx context.Context, projectRoot, workingDir, path // read from ref branch againstFSWalker, err := c.currentProjectGitWalker.GetDirWalker( - projectRoot, c.breakingCheckConfig.AgainstGitRef, relSlash, gitPath, + workingDir, c.breakingCheckConfig.AgainstGitRef, path, ) if err != nil { return nil, fmt.Errorf("c.currentProjectGitWalker.GetDirWalker: %w", err) diff --git a/internal/core/dom.go b/internal/core/dom.go index a84286cc..2f3531fc 100644 --- a/internal/core/dom.go +++ b/internal/core/dom.go @@ -25,7 +25,7 @@ type ( // CurrentProjectGitWalker is provider for fs walking for current project CurrentProjectGitWalker interface { - GetDirWalker(workingDir, gitRef, root, path string) (DirWalker, error) + GetDirWalker(workingDir, gitRef, path string) (DirWalker, error) } // IssueInfo contains the information of an issue and the path. diff --git a/internal/core/mocks/CurrentProjectGitWalker.go b/internal/core/mocks/CurrentProjectGitWalker.go index e1aef656..7e0292fb 100644 --- a/internal/core/mocks/CurrentProjectGitWalker.go +++ b/internal/core/mocks/CurrentProjectGitWalker.go @@ -20,9 +20,9 @@ func (_m *CurrentProjectGitWalker) EXPECT() *CurrentProjectGitWalker_Expecter { return &CurrentProjectGitWalker_Expecter{mock: &_m.Mock} } -// GetDirWalker provides a mock function with given fields: workingDir, gitRef, root, path -func (_m *CurrentProjectGitWalker) GetDirWalker(workingDir string, gitRef string, root string, path string) (core.DirWalker, error) { - ret := _m.Called(workingDir, gitRef, root, path) +// GetDirWalker provides a mock function with given fields: workingDir, gitRef, path +func (_m *CurrentProjectGitWalker) GetDirWalker(workingDir string, gitRef string, path string) (core.DirWalker, error) { + ret := _m.Called(workingDir, gitRef, path) if len(ret) == 0 { panic("no return value specified for GetDirWalker") @@ -30,19 +30,19 @@ func (_m *CurrentProjectGitWalker) GetDirWalker(workingDir string, gitRef string var r0 core.DirWalker var r1 error - if rf, ok := ret.Get(0).(func(string, string, string, string) (core.DirWalker, error)); ok { - return rf(workingDir, gitRef, root, path) + if rf, ok := ret.Get(0).(func(string, string, string) (core.DirWalker, error)); ok { + return rf(workingDir, gitRef, path) } - if rf, ok := ret.Get(0).(func(string, string, string, string) core.DirWalker); ok { - r0 = rf(workingDir, gitRef, root, path) + if rf, ok := ret.Get(0).(func(string, string, string) core.DirWalker); ok { + r0 = rf(workingDir, gitRef, path) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(core.DirWalker) } } - if rf, ok := ret.Get(1).(func(string, string, string, string) error); ok { - r1 = rf(workingDir, gitRef, root, path) + if rf, ok := ret.Get(1).(func(string, string, string) error); ok { + r1 = rf(workingDir, gitRef, path) } else { r1 = ret.Error(1) } @@ -58,15 +58,14 @@ type CurrentProjectGitWalker_GetDirWalker_Call struct { // GetDirWalker is a helper method to define mock.On call // - workingDir string // - gitRef string -// - root string // - path string -func (_e *CurrentProjectGitWalker_Expecter) GetDirWalker(workingDir interface{}, gitRef interface{}, root interface{}, path interface{}) *CurrentProjectGitWalker_GetDirWalker_Call { - return &CurrentProjectGitWalker_GetDirWalker_Call{Call: _e.mock.On("GetDirWalker", workingDir, gitRef, root, path)} +func (_e *CurrentProjectGitWalker_Expecter) GetDirWalker(workingDir interface{}, gitRef interface{}, path interface{}) *CurrentProjectGitWalker_GetDirWalker_Call { + return &CurrentProjectGitWalker_GetDirWalker_Call{Call: _e.mock.On("GetDirWalker", workingDir, gitRef, path)} } -func (_c *CurrentProjectGitWalker_GetDirWalker_Call) Run(run func(workingDir string, gitRef string, root string, path string)) *CurrentProjectGitWalker_GetDirWalker_Call { +func (_c *CurrentProjectGitWalker_GetDirWalker_Call) Run(run func(workingDir string, gitRef string, path string)) *CurrentProjectGitWalker_GetDirWalker_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].(string), args[2].(string), args[3].(string)) + run(args[0].(string), args[1].(string), args[2].(string)) }) return _c } @@ -76,7 +75,7 @@ func (_c *CurrentProjectGitWalker_GetDirWalker_Call) Return(_a0 core.DirWalker, return _c } -func (_c *CurrentProjectGitWalker_GetDirWalker_Call) RunAndReturn(run func(string, string, string, string) (core.DirWalker, error)) *CurrentProjectGitWalker_GetDirWalker_Call { +func (_c *CurrentProjectGitWalker_GetDirWalker_Call) RunAndReturn(run func(string, string, string) (core.DirWalker, error)) *CurrentProjectGitWalker_GetDirWalker_Call { _c.Call.Return(run) return _c } From 7e92849a6a602782bc4fc0000f68bf2451ad78e1 Mon Sep 17 00:00:00 2001 From: Khasbulat Abdullin Date: Mon, 4 May 2026 00:28:31 +0300 Subject: [PATCH 10/10] Fix breaking against flag handling --- internal/api/breaking_check.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/api/breaking_check.go b/internal/api/breaking_check.go index dab4faa1..584956b2 100644 --- a/internal/api/breaking_check.go +++ b/internal/api/breaking_check.go @@ -106,6 +106,12 @@ func (b BreakingCheck) action(ctx *cli.Context, log logger.Logger) error { return fmt.Errorf("config.New: %w", err) } + path := ctx.String(flagLintDirectoryPath.Name) + against := ctx.String(flagAgainstBranchName.Name) + if cfg.BreakingCheck.AgainstGitRef == "" { + cfg.BreakingCheck.AgainstGitRef = against + } + // Walker for Core (lockfile etc) - strictly based on project root projectWalker := fs.NewFSWalker(projectRoot, ".") app, err := buildCore(ctx.Context, log, *cfg, projectWalker) @@ -113,12 +119,6 @@ func (b BreakingCheck) action(ctx *cli.Context, log logger.Logger) error { return fmt.Errorf("buildCore: %w", err) } - path := ctx.String(flagLintDirectoryPath.Name) - against := ctx.String(flagAgainstBranchName.Name) - if cfg.BreakingCheck.AgainstGitRef == "" { - cfg.BreakingCheck.AgainstGitRef = against - } - issues, err := app.BreakingCheck(ctx.Context, projectRoot, breakingCheckRoot, path) if err != nil { return fmt.Errorf("app.BreakingCheck: %w", err)