From b274e3ec944664e3c2f123a371170f9904fd49e9 Mon Sep 17 00:00:00 2001 From: Rodrigo Bernardi Date: Thu, 16 Jul 2026 10:59:37 -0300 Subject: [PATCH] feat(boss): resolve same-name repo conflicts and support custom project paths (issues #114, #80) --- internal/core/domain/cacheInfo_test.go | 4 +- internal/core/domain/dependency.go | 26 ++++++++-- internal/core/domain/dependency_test.go | 12 ++--- .../core/services/cache/cache_service_test.go | 4 +- internal/core/services/installer/core.go | 4 +- internal/core/services/paths/paths_test.go | 10 ++-- utils/librarypath/dproj_util.go | 52 +++++++++++++------ 7 files changed, 76 insertions(+), 36 deletions(-) diff --git a/internal/core/domain/cacheInfo_test.go b/internal/core/domain/cacheInfo_test.go index 2032091e..08207f5d 100644 --- a/internal/core/domain/cacheInfo_test.go +++ b/internal/core/domain/cacheInfo_test.go @@ -16,8 +16,8 @@ func TestNewRepoInfo(t *testing.T) { t.Errorf("NewRepoInfo().Key = %q, want %q", info.Key, dep.HashName()) } - if info.Name != "horse" { - t.Errorf("NewRepoInfo().Name = %q, want %q", info.Name, "horse") + if info.Name != "github_com_hashload_horse" { + t.Errorf("NewRepoInfo().Name = %q, want %q", info.Name, "github_com_hashload_horse") } if len(info.Versions) != 3 { diff --git a/internal/core/domain/dependency.go b/internal/core/domain/dependency.go index 0a29cb37..df0878aa 100644 --- a/internal/core/domain/dependency.go +++ b/internal/core/domain/dependency.go @@ -21,7 +21,6 @@ var ( reHasHTTPS = regexp.MustCompile(`(?m)^https?:\/\/`) reVersionMajorMinor = regexp.MustCompile(`(?m)^(.|)(\d+)\.(\d+)$`) reVersionMajor = regexp.MustCompile(`(?m)^(.|)(\d+)$`) - reDepName = regexp.MustCompile(`[^/]+(:?/$|$)`) ) // Dependency represents a package dependency. @@ -124,9 +123,30 @@ func GetDependenciesNames(deps []Dependency) []string { return dependencies } -// Name returns the name of the dependency extracted from the repository URL. +// Name returns the unique, collision-free name of the dependency based on its repository URL. func (p *Dependency) Name() string { - return reDepName.FindString(p.Repository) + repo := p.Repository + // Trim protocol and credentials + repo = strings.TrimPrefix(repo, "https://") + repo = strings.TrimPrefix(repo, "http://") + if idx := strings.Index(repo, "@"); idx != -1 { + repo = repo[idx+1:] + } + // Replace colon with slash (for SSH format like git@github.com:owner/repo) + repo = strings.ReplaceAll(repo, ":", "/") + // Trim trailing .git and slashes + repo = strings.TrimSuffix(repo, ".git") + repo = strings.TrimSuffix(repo, "/") + + parts := strings.Split(repo, "/") + if len(parts) <= 1 { + return repo + } + + // Join with underscore and replace dots in host/domain to get a clean, safe folder name + res := strings.Join(parts, "_") + res = strings.ReplaceAll(res, ".", "_") + return res } // GetKey returns the normalized key for the dependency (lowercase repository). diff --git a/internal/core/domain/dependency_test.go b/internal/core/domain/dependency_test.go index cc2d0bd9..4d7b214e 100644 --- a/internal/core/domain/dependency_test.go +++ b/internal/core/domain/dependency_test.go @@ -15,27 +15,27 @@ func TestDependency_Name(t *testing.T) { { name: "github repository", repository: "github.com/hashload/boss", - expected: "boss", + expected: "github_com_hashload_boss", }, { name: "gitlab repository", repository: "gitlab.com/user/project", - expected: "project", + expected: "gitlab_com_user_project", }, { name: "bitbucket repository", repository: "bitbucket.org/team/repo", - expected: "repo", + expected: "bitbucket_org_team_repo", }, { name: "nested path repository", repository: "github.com/org/group/subgroup/repo", - expected: "repo", + expected: "github_com_org_group_subgroup_repo", }, { name: "repository with trailing slash", repository: "github.com/hashload/boss/", - expected: "boss/", + expected: "github_com_hashload_boss", }, { name: "simple name", @@ -263,7 +263,7 @@ func TestGetDependenciesNames(t *testing.T) { t.Errorf("GetDependenciesNames() returned %d names, want 3", len(names)) } - expectedNames := []string{"boss", "horse", "repo"} + expectedNames := []string{"github_com_hashload_boss", "github_com_hashload_horse", "github_com_user_repo"} for i, expected := range expectedNames { if names[i] != expected { t.Errorf("GetDependenciesNames()[%d] = %q, want %q", i, names[i], expected) diff --git a/internal/core/services/cache/cache_service_test.go b/internal/core/services/cache/cache_service_test.go index e8692485..a4ee0773 100644 --- a/internal/core/services/cache/cache_service_test.go +++ b/internal/core/services/cache/cache_service_test.go @@ -115,8 +115,8 @@ func TestService_SaveAndLoadRepositoryDetails(t *testing.T) { t.Fatalf("LoadRepositoryData() error = %v", err) } - if info.Name != "horse" { - t.Errorf("LoadRepositoryData().Name = %q, want %q", info.Name, "horse") + if info.Name != "github_com_hashload_horse" { + t.Errorf("LoadRepositoryData().Name = %q, want %q", info.Name, "github_com_hashload_horse") } if len(info.Versions) != 3 { diff --git a/internal/core/services/installer/core.go b/internal/core/services/installer/core.go index c4198223..3a1ca212 100644 --- a/internal/core/services/installer/core.go +++ b/internal/core/services/installer/core.go @@ -445,7 +445,9 @@ func (ic *installContext) reportInstallResult(depName, warning string) { } func (ic *installContext) shouldSkipDependency(dep domain.Dependency) bool { - if utils.Contains(ic.options.ForceUpdate, dep.Name()) { + if utils.Contains(ic.options.ForceUpdate, dep.Repository) || + utils.Contains(ic.options.ForceUpdate, dep.Name()) || + utils.Contains(ic.options.ForceUpdate, ParseDependency(dep.Name())) { return false } diff --git a/internal/core/services/paths/paths_test.go b/internal/core/services/paths/paths_test.go index 688bd27a..56465fa9 100644 --- a/internal/core/services/paths/paths_test.go +++ b/internal/core/services/paths/paths_test.go @@ -94,6 +94,10 @@ func TestEnsureCleanModulesDir_RemovesOldDependencies(t *testing.T) { t.Fatalf("Failed to create modules dir: %v", err) } + // Define current dependencies + dep := domain.ParseDependency("github.com/hashload/horse", "^1.0.0") + deps := []domain.Dependency{dep} + // Create an old dependency directory that should be removed oldDepDir := filepath.Join(modulesDir, "old-dependency") if err := os.MkdirAll(oldDepDir, 0755); err != nil { @@ -101,14 +105,10 @@ func TestEnsureCleanModulesDir_RemovesOldDependencies(t *testing.T) { } // Create a current dependency directory that should be kept - currentDepDir := filepath.Join(modulesDir, "horse") + currentDepDir := filepath.Join(modulesDir, dep.Name()) if err := os.MkdirAll(currentDepDir, 0755); err != nil { t.Fatalf("Failed to create current dependency dir: %v", err) } - - // Define current dependencies - dep := domain.ParseDependency("github.com/hashload/horse", "^1.0.0") - deps := []domain.Dependency{dep} lock := domain.PackageLock{ Installed: map[string]domain.LockedDependency{}, } diff --git a/utils/librarypath/dproj_util.go b/utils/librarypath/dproj_util.go index 9daf0778..1ead4b07 100644 --- a/utils/librarypath/dproj_util.go +++ b/utils/librarypath/dproj_util.go @@ -25,10 +25,9 @@ var ( // updateDprojLibraryPath updates the library path in the project file. func updateDprojLibraryPath(pkg *domain.Package) { - var isLazarus = isLazarus() var projectNames = GetProjectNames(pkg) for _, projectName := range projectNames { - if isLazarus { + if isLazarusFile(projectName) { updateOtherUnitFilesProject(projectName) } else { updateLibraryPathProject(projectName) @@ -36,6 +35,13 @@ func updateDprojLibraryPath(pkg *domain.Package) { } } +// isLazarusFile checks if a specific project file is a Lazarus project or package. +func isLazarusFile(filename string) bool { + lower := strings.ToLower(filename) + return strings.HasSuffix(lower, consts.FileExtensionLpi) || + strings.HasSuffix(lower, consts.FileExtensionLpk) +} + // updateOtherUnitFilesProject updates the other unit files in the project file. func updateOtherUnitFilesProject(lpiName string) { doc := etree.NewDocument() @@ -114,10 +120,9 @@ func createTagOtherUnitFiles(node *etree.Element) *etree.Element { // updateGlobalBrowsingPath updates the global browsing path. func updateGlobalBrowsingPath(pkg *domain.Package) { - var isLazarus = isLazarus() var projectNames = GetProjectNames(pkg) for i, projectName := range projectNames { - if !isLazarus { + if !isLazarusFile(projectName) { updateGlobalBrowsingByProject(projectName, i == 0) } } @@ -171,24 +176,37 @@ func createTagLibraryPath(node *etree.Element) *etree.Element { // GetProjectNames returns the project names. func GetProjectNames(pkg *domain.Package) []string { - var result []string - if len(pkg.Projects) > 0 { - result = pkg.Projects - } else { - files, err := os.ReadDir(env.GetCurrentDir()) - if err != nil { - msg.Err("❌ Failed to read directory: %v", err) - return result - } + return getExplicitProjectNames(pkg.Projects) + } + return discoverProjectNames() +} - for _, file := range files { - if reProjectFile.MatchString(file.Name()) { - result = append(result, filepath.Join(env.GetCurrentDir(), file.Name())) - } +func getExplicitProjectNames(projects []string) []string { + result := make([]string, 0, len(projects)) + for _, project := range projects { + if filepath.IsAbs(project) { + result = append(result, project) + } else { + result = append(result, filepath.Join(env.GetCurrentDir(), project)) } } + return result +} + +func discoverProjectNames() []string { + files, err := os.ReadDir(env.GetCurrentDir()) + if err != nil { + msg.Err("❌ Failed to read directory: %v", err) + return nil + } + var result []string + for _, file := range files { + if reProjectFile.MatchString(file.Name()) { + result = append(result, filepath.Join(env.GetCurrentDir(), file.Name())) + } + } return result }