From 91d1a3df8bd2a55c330b36bffd1193bb4f135efd Mon Sep 17 00:00:00 2001 From: Isaque Pinheiro Date: Thu, 25 Jun 2026 09:06:20 -0300 Subject: [PATCH] fix(boss): support SSH git@ URLs and resolve version parsing conflicts (issues #170, #129) --- .../secondary/repository/repository_test.go | 8 +++ internal/core/domain/dependency.go | 3 + internal/core/services/installer/utils.go | 58 ++++++++++++---- .../core/services/installer/utils_test.go | 69 +++++++++++++++++++ 4 files changed, 125 insertions(+), 13 deletions(-) diff --git a/internal/adapters/secondary/repository/repository_test.go b/internal/adapters/secondary/repository/repository_test.go index 813a305b..6098cec2 100644 --- a/internal/adapters/secondary/repository/repository_test.go +++ b/internal/adapters/secondary/repository/repository_test.go @@ -6,6 +6,7 @@ import ( "errors" "io" "os" + "path/filepath" "testing" "time" @@ -27,6 +28,7 @@ func NewMockFileSystem() *MockFileSystem { } func (m *MockFileSystem) ReadFile(name string) ([]byte, error) { + name = filepath.ToSlash(name) if data, ok := m.files[name]; ok { return data, nil } @@ -34,6 +36,7 @@ func (m *MockFileSystem) ReadFile(name string) ([]byte, error) { } func (m *MockFileSystem) WriteFile(name string, data []byte, _ os.FileMode) error { + name = filepath.ToSlash(name) m.files[name] = data return nil } @@ -43,6 +46,7 @@ func (m *MockFileSystem) MkdirAll(_ string, _ os.FileMode) error { } func (m *MockFileSystem) Stat(name string) (os.FileInfo, error) { + name = filepath.ToSlash(name) if _, ok := m.files[name]; ok { //nolint:nilnil // Mock for testing return nil, nil @@ -51,6 +55,7 @@ func (m *MockFileSystem) Stat(name string) (os.FileInfo, error) { } func (m *MockFileSystem) Remove(name string) error { + name = filepath.ToSlash(name) delete(m.files, name) return nil } @@ -60,6 +65,8 @@ func (m *MockFileSystem) RemoveAll(_ string) error { } func (m *MockFileSystem) Rename(oldpath, newpath string) error { + oldpath = filepath.ToSlash(oldpath) + newpath = filepath.ToSlash(newpath) if data, ok := m.files[oldpath]; ok { m.files[newpath] = data delete(m.files, oldpath) @@ -82,6 +89,7 @@ func (m *MockFileSystem) Create(_ string) (io.WriteCloser, error) { } func (m *MockFileSystem) Exists(name string) bool { + name = filepath.ToSlash(name) _, ok := m.files[name] return ok } diff --git a/internal/core/domain/dependency.go b/internal/core/domain/dependency.go index d041c69e..0a29cb37 100644 --- a/internal/core/domain/dependency.go +++ b/internal/core/domain/dependency.go @@ -64,6 +64,9 @@ func (p *Dependency) GetURLPrefix() string { // GetURL returns the full URL for the repository, handling SSH and HTTPS. func (p *Dependency) GetURL() string { + if strings.HasPrefix(p.Repository, "git@") { + return p.Repository + } prefix := p.GetURLPrefix() auth := env.GlobalConfiguration().Auth[prefix] if auth != nil { diff --git a/internal/core/services/installer/utils.go b/internal/core/services/installer/utils.go index 0eb090e4..e0109efb 100644 --- a/internal/core/services/installer/utils.go +++ b/internal/core/services/installer/utils.go @@ -18,26 +18,55 @@ var ( reHasMultiSlash = regexp.MustCompile(`(?m)([?^/].*)(([?^/]).*)`) ) -// EnsureDependency ensures that the dependencies are added to the package. -func EnsureDependency(pkg *domain.Package, args []string) { - for _, dependency := range args { - dependency = ParseDependency(dependency) +// parseUrlAndVersion parses the dependency URL and version, handling SSH git@ URLs safely. +func parseUrlAndVersion(input string) (url string, version string) { + if strings.HasPrefix(input, "git@") { + // SSH URL format: git@host:owner/repo[.git][:version] or git@host:owner/repo[.git]@version + firstColon := strings.Index(input, ":") + if firstColon == -1 { + return input, "" + } - match := make(map[string]string) - split := reURLVersion.FindStringSubmatch(dependency) + remainder := input[firstColon+1:] + // Look for last colon or @ in remainder to see if a version is specified + lastSep := strings.LastIndexAny(remainder, ":@") + if lastSep != -1 { + possibleVersion := remainder[lastSep+1:] + // Version shouldn't contain slashes (which paths do) and shouldn't be empty + if !strings.Contains(possibleVersion, "/") && possibleVersion != "" { + url = input[:firstColon+1+lastSep] + version = possibleVersion + return url, version + } + } + return input, "" + } + + // For standard HTTP/HTTPS/standard paths, use the original regex + match := make(map[string]string) + split := reURLVersion.FindStringSubmatch(input) + if len(split) > 0 { for i, name := range reURLVersion.SubexpNames() { - if i != 0 && name != "" { + if i != 0 && name != "" && i < len(split) { match[name] = split[i] } } - var ver string - var dep string - dep = match["url"] - if len(match["version"]) == 0 { + } + + url = match["url"] + version = match["version"] + return url, version +} + +// EnsureDependency ensures that the dependencies are added to the package. +func EnsureDependency(pkg *domain.Package, args []string) { + for _, dependency := range args { + dependency = ParseDependency(dependency) + + dep, ver := parseUrlAndVersion(dependency) + if ver == "" { ver = consts.MinimalDependencyVersion - } else { - ver = match["version"] } if strings.HasSuffix(strings.ToLower(dep), ".git") { @@ -50,6 +79,9 @@ func EnsureDependency(pkg *domain.Package, args []string) { // ParseDependency parses the dependency name and returns the full URL if needed. func ParseDependency(dependencyName string) string { + if strings.HasPrefix(dependencyName, "git@") || strings.HasPrefix(dependencyName, "http://") || strings.HasPrefix(dependencyName, "https://") { + return dependencyName + } if !reHasSlash.MatchString(dependencyName) { return "github.com/hashload/" + dependencyName } diff --git a/internal/core/services/installer/utils_test.go b/internal/core/services/installer/utils_test.go index 06a06892..44fb6dd5 100644 --- a/internal/core/services/installer/utils_test.go +++ b/internal/core/services/installer/utils_test.go @@ -157,3 +157,72 @@ func TestEnsureDependency_HTTPSUrl(t *testing.T) { t.Error("Should add dependency for HTTPS URL") } } + +func TestEnsureDependency_SSHUrl(t *testing.T) { + tests := []struct { + name string + input string + expectedDep string + expectedVer string + }{ + { + name: "SSH URL with version tag", + input: "git@github.com:hashload/boss.git:v1.0.0", + expectedDep: "git@github.com:hashload/boss", + expectedVer: "v1.0.0", + }, + { + name: "SSH URL without version tag", + input: "git@github.com:hashload/boss.git", + expectedDep: "git@github.com:hashload/boss", + expectedVer: ">0.0.0", + }, + { + name: "Self-hosted gitlab SSH URL without version", + input: "git@mygitlab.domain.de:delphi/libraries/mylib.git", + expectedDep: "git@mygitlab.domain.de:delphi/libraries/mylib", + expectedVer: ">0.0.0", + }, + { + name: "Self-hosted gitlab SSH URL with version", + input: "git@mygitlab.domain.de:delphi/libraries/mylib.git:1.2.3", + expectedDep: "git@mygitlab.domain.de:delphi/libraries/mylib", + expectedVer: "1.2.3", + }, + { + name: "SSH URL with @ version tag", + input: "git@github.com:hashload/boss.git@v2.5.0", + expectedDep: "git@github.com:hashload/boss", + expectedVer: "v2.5.0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pkg := &domain.Package{ + Dependencies: make(map[string]string), + } + + installer.EnsureDependency(pkg, []string{tt.input}) + + if len(pkg.Dependencies) != 1 { + t.Fatalf("EnsureDependency() did not add exactly 1 dependency for %s, got %d", tt.input, len(pkg.Dependencies)) + } + + actualVer, exists := pkg.Dependencies[tt.expectedDep] + if !exists { + // Print keys for debugging + var keys []string + for k := range pkg.Dependencies { + keys = append(keys, k) + } + t.Fatalf("Expected dependency %q not found, got keys: %v", tt.expectedDep, keys) + } + + if actualVer != tt.expectedVer { + t.Errorf("pkg.Dependencies[%q] = %q, want %q", tt.expectedDep, actualVer, tt.expectedVer) + } + }) + } +} +