From 58381a056095c1b4700e4c84bbd8be24931f6d8a Mon Sep 17 00:00:00 2001 From: Daniel Fox Date: Mon, 29 Sep 2025 11:24:04 -0700 Subject: [PATCH 01/11] Add two ways to get the current project --- cmd/get.go | 35 ++++++++++++++++++++++++++- cmd/utils/project.go | 56 +++++++++++++++++++++++++++++++++++++------- 2 files changed, 82 insertions(+), 9 deletions(-) diff --git a/cmd/get.go b/cmd/get.go index b984c8f..04e4cbc 100644 --- a/cmd/get.go +++ b/cmd/get.go @@ -283,6 +283,31 @@ func getAllAgents(client client.AgentApiV1AlphaApi, agentType string) ([]models. return agents, nil } +var GetCurrentProjectCmd = &cobra.Command{ + Use: "current_project", + Short: "Get project for current directory", + Aliases: []string{"cur"}, + Long: ``, + + Run: func(cmd *cobra.Command, args []string) { + c := client.NewProjectV1AlphaApi() + + projectList, err := c.ListProjects() + + utils.Check(err) + + projectName, err := utils.InferProjectName() + utils.Check(err) + + for _, p := range projectList.Projects { + if p.Metadata.Name == projectName { + fmt.Println(p.Metadata.Id, p.Metadata.Name, p.Spec.Repository.Url) + } + } + + }, +} + var GetProjectCmd = &cobra.Command{ Use: "projects [name]", Short: "Get projects.", @@ -298,13 +323,20 @@ var GetProjectCmd = &cobra.Command{ utils.Check(err) + projectName, err := utils.InferProjectName() + utils.Check(err) + const padding = 3 w := tabwriter.NewWriter(os.Stdout, 0, 0, padding, ' ', 0) fmt.Fprintln(w, "NAME\tREPOSITORY") for _, p := range projectList.Projects { - fmt.Fprintf(w, "%s\t%s\n", p.Metadata.Name, p.Spec.Repository.Url) + if p.Metadata.Name == projectName { + fmt.Fprintf(w, "%s\t%s\t%s\n", p.Metadata.Name, p.Spec.Repository.Url, "(active)") + } else { + fmt.Fprintf(w, "%s\t%s\n", p.Metadata.Name, p.Spec.Repository.Url) + } } if err := w.Flush(); err != nil { @@ -529,6 +561,7 @@ func init() { getCmd.AddCommand(GetDashboardCmd) getCmd.AddCommand(getNotificationCmd) getCmd.AddCommand(GetProjectCmd) + getCmd.AddCommand(GetCurrentProjectCmd) getCmd.AddCommand(GetAgentTypeCmd) GetAgentsCmd.Flags().StringP("agent-type", "t", "", diff --git a/cmd/utils/project.go b/cmd/utils/project.go index 9994cdd..993f14c 100644 --- a/cmd/utils/project.go +++ b/cmd/utils/project.go @@ -20,19 +20,26 @@ func GetProjectId(name string) string { } func InferProjectName() (string, error) { - originUrl, err := getGitOriginUrl() + originURLs, err := getAllGitRemoteURLs() if err != nil { return "", err } - log.Printf("Origin url: '%s'\n", originUrl) + var projectName string - projectName, err := getProjectIdFromUrl(originUrl) - if err != nil { - return "", err + for _, originURL := range originURLs { + log.Printf("Origin url: '%s'\n", originURL) + + projectName, err = getProjectIdFromUrl(originURL) + if err != nil { + log.Printf("no project found for remote %s", originURL) + } + if projectName != "" { + return projectName, nil + } } - return projectName, nil + return "", fmt.Errorf("Unable to find project for any configured remotes") } func getProjectIdFromUrl(url string) (string, error) { @@ -58,8 +65,20 @@ func getProjectIdFromUrl(url string) (string, error) { return projectName, nil } -func getGitOriginUrl() (string, error) { - args := []string{"config", "remote.origin.url"} +func getGitRemotes() ([]string, error) { + args := []string{"remote"} + cmd := exec.Command("git", args...) + out, err := cmd.CombinedOutput() + if err != nil { + cmd_string := fmt.Sprintf("'%s %s'", "git", strings.Join(args, " ")) + user_msg := "You are probably not in a git directory?" + return []string{}, fmt.Errorf("%s failed with message: '%s'\n%s", cmd_string, err, user_msg) + } + return strings.Split(strings.TrimSpace(string(out)), "\n"), nil +} + +func getGitRemoteUrl(remote string) (string, error) { + args := []string{"config", fmt.Sprintf("remote.%s.url", remote)} cmd := exec.Command("git", args...) out, err := cmd.CombinedOutput() @@ -71,3 +90,24 @@ func getGitOriginUrl() (string, error) { return strings.TrimSpace(string(out)), nil } + +func getAllGitRemoteURLs() ([]string, error) { + gitRemotes, err := getGitRemotes() + if err != nil { + return gitRemotes, err + } + var gitRemoteURLs []string + for _, remote := range gitRemotes { + remoteURL, err := getGitRemoteUrl(remote) + if err != nil { + log.Printf("could not get URL for remote %s", remote) + } else { + gitRemoteURLs = append(gitRemoteURLs, remoteURL) + } + } + return gitRemoteURLs, nil +} + +func getGitOriginUrl() (string, error) { + return getGitRemoteUrl("origin") +} From 20337e450584671574eded818086d249810a44c1 Mon Sep 17 00:00:00 2001 From: Daniel Fox Date: Mon, 29 Sep 2025 11:45:57 -0700 Subject: [PATCH 02/11] More refactoring --- cmd/get.go | 14 ++----------- cmd/utils/project.go | 50 ++++++++++++++++++++++++-------------------- 2 files changed, 29 insertions(+), 35 deletions(-) diff --git a/cmd/get.go b/cmd/get.go index 04e4cbc..965a3e8 100644 --- a/cmd/get.go +++ b/cmd/get.go @@ -290,20 +290,10 @@ var GetCurrentProjectCmd = &cobra.Command{ Long: ``, Run: func(cmd *cobra.Command, args []string) { - c := client.NewProjectV1AlphaApi() - - projectList, err := c.ListProjects() - + project, err := utils.InferProject() utils.Check(err) - projectName, err := utils.InferProjectName() - utils.Check(err) - - for _, p := range projectList.Projects { - if p.Metadata.Name == projectName { - fmt.Println(p.Metadata.Id, p.Metadata.Name, p.Spec.Repository.Url) - } - } + fmt.Println(project.Metadata.Id, project.Metadata.Name, project.Spec.Repository.Url) }, } diff --git a/cmd/utils/project.go b/cmd/utils/project.go index 993f14c..746c033 100644 --- a/cmd/utils/project.go +++ b/cmd/utils/project.go @@ -2,12 +2,14 @@ package utils import ( "fmt" + "slices" "log" "os/exec" "strings" "github.com/semaphoreci/cli/api/client" + "github.com/semaphoreci/cli/api/models" ) func GetProjectId(name string) string { @@ -20,49 +22,51 @@ func GetProjectId(name string) string { } func InferProjectName() (string, error) { - originURLs, err := getAllGitRemoteURLs() + project, err := InferProject() if err != nil { return "", err } + return project.Metadata.Name, nil +} - var projectName string - - for _, originURL := range originURLs { - log.Printf("Origin url: '%s'\n", originURL) +func InferProject() (models.ProjectV1Alpha, error) { + originURLs, err := getAllGitRemoteURLs() + if err != nil { + return models.ProjectV1Alpha{}, err + } - projectName, err = getProjectIdFromUrl(originURL) - if err != nil { - log.Printf("no project found for remote %s", originURL) - } - if projectName != "" { - return projectName, nil - } + project, err := getProjectFromUrls(originURLs) + if err != nil { + log.Printf("no project found for any configured remotes (%s)", strings.Join(originURLs, ", ")) } - return "", fmt.Errorf("Unable to find project for any configured remotes") + return project, nil } func getProjectIdFromUrl(url string) (string, error) { + project, err := getProjectFromUrls([]string{url}) + if err != nil { + return "", fmt.Errorf("project with url %s not found in this org", url) + } + return project.Metadata.Id, nil + +} + +func getProjectFromUrls(urls []string) (models.ProjectV1Alpha, error) { projectClient := client.NewProjectV1AlphaApi() projects, err := projectClient.ListProjects() if err != nil { - return "", fmt.Errorf("getting project list failed '%s'", err) + return models.ProjectV1Alpha{}, fmt.Errorf("getting project list failed '%s'", err) } - projectName := "" for _, p := range projects.Projects { - if p.Spec.Repository.Url == url { - projectName = p.Metadata.Name - break + if slices.Contains(urls, p.Spec.Repository.Url) { + return p, nil } } - if projectName == "" { - return "", fmt.Errorf("project with url '%s' not found in this org", url) - } - - return projectName, nil + return models.ProjectV1Alpha{}, fmt.Errorf("project with urls '%s' not found in this org", strings.Join(urls, ", ")) } func getGitRemotes() ([]string, error) { From 8e8158486f1a35baf98ba1ec9446c8b8bfd1d28f Mon Sep 17 00:00:00 2001 From: Daniel Fox Date: Mon, 29 Sep 2025 12:05:24 -0700 Subject: [PATCH 03/11] Remove (active) from project list; fix InferProject() --- cmd/get.go | 9 +-------- cmd/utils/project.go | 1 + 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/cmd/get.go b/cmd/get.go index 965a3e8..9f89b50 100644 --- a/cmd/get.go +++ b/cmd/get.go @@ -313,20 +313,13 @@ var GetProjectCmd = &cobra.Command{ utils.Check(err) - projectName, err := utils.InferProjectName() - utils.Check(err) - const padding = 3 w := tabwriter.NewWriter(os.Stdout, 0, 0, padding, ' ', 0) fmt.Fprintln(w, "NAME\tREPOSITORY") for _, p := range projectList.Projects { - if p.Metadata.Name == projectName { - fmt.Fprintf(w, "%s\t%s\t%s\n", p.Metadata.Name, p.Spec.Repository.Url, "(active)") - } else { - fmt.Fprintf(w, "%s\t%s\n", p.Metadata.Name, p.Spec.Repository.Url) - } + fmt.Fprintf(w, "%s\t%s\n", p.Metadata.Name, p.Spec.Repository.Url) } if err := w.Flush(); err != nil { diff --git a/cmd/utils/project.go b/cmd/utils/project.go index 746c033..108fccc 100644 --- a/cmd/utils/project.go +++ b/cmd/utils/project.go @@ -38,6 +38,7 @@ func InferProject() (models.ProjectV1Alpha, error) { project, err := getProjectFromUrls(originURLs) if err != nil { log.Printf("no project found for any configured remotes (%s)", strings.Join(originURLs, ", ")) + return models.ProjectV1Alpha{}, fmt.Errorf("no project found for any configured remotes: %w", err) } return project, nil From dd35574a32e0ca7cb33afdd8a6f4a2cffcd4ac1d Mon Sep 17 00:00:00 2001 From: Daniel Fox Date: Mon, 29 Sep 2025 12:07:52 -0700 Subject: [PATCH 04/11] Better error message --- cmd/utils/project.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/utils/project.go b/cmd/utils/project.go index 108fccc..865131a 100644 --- a/cmd/utils/project.go +++ b/cmd/utils/project.go @@ -67,7 +67,7 @@ func getProjectFromUrls(urls []string) (models.ProjectV1Alpha, error) { } } - return models.ProjectV1Alpha{}, fmt.Errorf("project with urls '%s' not found in this org", strings.Join(urls, ", ")) + return models.ProjectV1Alpha{}, fmt.Errorf("no project found in this org with a url matching any of this repository's remotes") } func getGitRemotes() ([]string, error) { From 01074b3154ccee3462787043a39d1ceffba6543c Mon Sep 17 00:00:00 2001 From: Daniel Fox Date: Mon, 29 Sep 2025 13:22:39 -0700 Subject: [PATCH 05/11] Add --json flag to current_project --- cmd/get.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/cmd/get.go b/cmd/get.go index 9f89b50..8fcaed9 100644 --- a/cmd/get.go +++ b/cmd/get.go @@ -1,6 +1,7 @@ package cmd import ( + "encoding/json" "fmt" "log" "os" @@ -293,7 +294,15 @@ var GetCurrentProjectCmd = &cobra.Command{ project, err := utils.InferProject() utils.Check(err) - fmt.Println(project.Metadata.Id, project.Metadata.Name, project.Spec.Repository.Url) + doJSON, err := cmd.Flags().GetBool("json") + utils.Check(err) + if doJSON { + jsonBody, err := json.MarshalIndent(project, "", " ") + utils.Check(err) + fmt.Println(string(jsonBody)) + } else { + fmt.Println(project.Metadata.Id, project.Metadata.Name, project.Spec.Repository.Url) + } }, } @@ -544,9 +553,11 @@ func init() { getCmd.AddCommand(GetDashboardCmd) getCmd.AddCommand(getNotificationCmd) getCmd.AddCommand(GetProjectCmd) - getCmd.AddCommand(GetCurrentProjectCmd) getCmd.AddCommand(GetAgentTypeCmd) + getCmd.AddCommand(GetCurrentProjectCmd) + GetCurrentProjectCmd.Flags().Bool("json", false, "print project information as json") + GetAgentsCmd.Flags().StringP("agent-type", "t", "", "agent type; if specified, returns only agents for this agent type") getCmd.AddCommand(GetAgentsCmd) From 5e9378303de98c2231d61717f08d3ab4acfeb404 Mon Sep 17 00:00:00 2001 From: Daniel Fox Date: Wed, 1 Oct 2025 11:45:01 -0700 Subject: [PATCH 06/11] Bump minimum go version to 1.21 (for slices) --- Dockerfile.dev | 2 +- go.mod | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile.dev b/Dockerfile.dev index 8d26eb9..f141b22 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,4 +1,4 @@ -FROM golang:1.20 +FROM golang:1.21 RUN go install gotest.tools/gotestsum@v1.12.2 diff --git a/go.mod b/go.mod index d844f4c..e661669 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/semaphoreci/cli -go 1.20 +go 1.21 require ( github.com/ghodss/yaml v1.0.0 From 155bbecc69b8cad8f80ed4eef90d4a27d9812774 Mon Sep 17 00:00:00 2001 From: Daniel Fox Date: Wed, 1 Oct 2025 11:45:15 -0700 Subject: [PATCH 07/11] Add -buildvcs=false to build --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 255d1bf..a4c0a42 100644 --- a/Makefile +++ b/Makefile @@ -47,7 +47,7 @@ test: docker-compose run --rm cli gotestsum --format short-verbose --junitfile results.xml --packages="./..." -- -p 1 build: - docker-compose run --rm cli env GOOS=$(OS) GOARCH=$(ARCH) go build -ldflags "-s -w -X main.version=$(shell git describe --tags --abbrev=0)" -o sem + docker-compose run --rm cli env GOOS=$(OS) GOARCH=$(ARCH) go build -buildvcs=false -ldflags "-s -w -X main.version=$(shell git describe --tags --abbrev=0)" -o sem tar -czvf /tmp/sem.tar.gz sem # Automation of CLI tagging. From e2a67132c3d5dd52009e992ad595c78968631c4e Mon Sep 17 00:00:00 2001 From: Daniel Fox Date: Wed, 22 Oct 2025 12:57:03 -0700 Subject: [PATCH 08/11] Remove -buildvcs=false, add git safe.directory --- Dockerfile.dev | 1 + Makefile | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile.dev b/Dockerfile.dev index f141b22..aef1d89 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,5 +1,6 @@ FROM golang:1.21 RUN go install gotest.tools/gotestsum@v1.12.2 +RUN git config --global --add safe.directory /app WORKDIR /app diff --git a/Makefile b/Makefile index a4c0a42..255d1bf 100644 --- a/Makefile +++ b/Makefile @@ -47,7 +47,7 @@ test: docker-compose run --rm cli gotestsum --format short-verbose --junitfile results.xml --packages="./..." -- -p 1 build: - docker-compose run --rm cli env GOOS=$(OS) GOARCH=$(ARCH) go build -buildvcs=false -ldflags "-s -w -X main.version=$(shell git describe --tags --abbrev=0)" -o sem + docker-compose run --rm cli env GOOS=$(OS) GOARCH=$(ARCH) go build -ldflags "-s -w -X main.version=$(shell git describe --tags --abbrev=0)" -o sem tar -czvf /tmp/sem.tar.gz sem # Automation of CLI tagging. From e275bb0ac48c9e9748d5bd95359e286a8d4c3192 Mon Sep 17 00:00:00 2001 From: Daniel Fox Date: Wed, 22 Oct 2025 15:12:56 -0700 Subject: [PATCH 09/11] Improve "current project" inference In previous implementations, we would infer the current project by getting a list of all remote URLs and then choosing the first project that matched one of them, which isn't necessarily the correct one. In this commit, we add some logic to try to make the best choice possible, using the following logic: 1. Get a list of all remotes, with their names and URLs 2. Get a list of all projects from Semaphore 3. Make a list of all remotes where there is a project configured for that remote's URL (so all remotes wihch have an associated project in Semaphore) Once we have that, we choose one based on the following logic: 1. If there's only one, choose that 2. If the user has run `gh repo set-default` in this repository, use the repository they chose at that point. 3. If there's one named "origin" choose that one. This is git's default remote name when you clone. 4. If there's one named "upstream" choose that one. This is what the `gh` CLI will rename your "origin" repo to when you create a fork for a new PR. 5. If there's more than one remote but they all have the same URL, then just use whichever project matches that URL. 6. If we still can't decide, print an error to the user; they're probably doing something weird. --- cmd/utils/project.go | 151 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 146 insertions(+), 5 deletions(-) diff --git a/cmd/utils/project.go b/cmd/utils/project.go index 865131a..89bde27 100644 --- a/cmd/utils/project.go +++ b/cmd/utils/project.go @@ -12,6 +12,40 @@ import ( "github.com/semaphoreci/cli/api/models" ) +type GitRemote struct { + Name string + URL string + Project models.ProjectV1Alpha +} + +type GitRemoteList []GitRemote + +func (grl GitRemoteList) Contains(remoteNameOrUrl string) bool { + for _, gitRemote := range grl { + if gitRemote.Name == remoteNameOrUrl || gitRemote.URL == remoteNameOrUrl { + return true + } + } + return false +} + +func (grl GitRemoteList) Get(remoteNameOrUrl string) (*GitRemote, error) { + for _, gitRemote := range grl { + if gitRemote.Name == remoteNameOrUrl || gitRemote.URL == remoteNameOrUrl { + return &gitRemote, nil + } + } + return &GitRemote{}, fmt.Errorf("no remote matching %s found in remote list") +} + +func (grl GitRemoteList) URLs() []string { + urls := []string{} + for _, gitRemote := range grl { + urls = append(urls, gitRemote.URL) + } + return urls +} + func GetProjectId(name string) string { projectClient := client.NewProjectV1AlphaApi() project, err := projectClient.GetProject(name) @@ -30,18 +64,64 @@ func InferProjectName() (string, error) { } func InferProject() (models.ProjectV1Alpha, error) { - originURLs, err := getAllGitRemoteURLs() + // Note that getAllGitRemotesAndProjects will only return remotes + // where the URL of that remote is configured in a project we got + // from the API, so this list is a list of remotes valid projects + // configured. All we have to do now is pick one. + gitRemotes, err := getAllGitRemotesAndProjects() if err != nil { return models.ProjectV1Alpha{}, err } - project, err := getProjectFromUrls(originURLs) + // If the user is using GitHub and has run `gh repo set-default`, then + // we can get that 'base' remote name and see if we have a project for + // it. + ghBaseRemoteName, err := getGitHubBaseRemoteName() if err != nil { - log.Printf("no project found for any configured remotes (%s)", strings.Join(originURLs, ", ")) - return models.ProjectV1Alpha{}, fmt.Errorf("no project found for any configured remotes: %w", err) + log.Printf("tried looking for a `gh` base repo configuration, but found none.") + } else { + gitRemote, err := gitRemotes.Get(ghBaseRemoteName) + if err == nil { + return gitRemote.Project, nil + } } - return project, nil + // If we only got one remote with a configured project, return it; + // alternately, if we got multiple, return the alphabetically first + // one. + if len(gitRemotes) == 1 { + return gitRemotes[0].Project, nil + } + + // If we got an "origin" remote or an "upstream" remote, return that (in + // that order of preference) + for _, remoteName := range []string{"origin", "upstream"} { + remote, err := gitRemotes.Get(remoteName) + if err == nil { + return remote.Project, nil + } + } + + // At this point, we have multiple remotes configured, all of which have + // a project configured in Semaphore, none of which are named "origin" or + // "upstream", and none of which are set as the gh base repo. The *most likely* + // explanation here is that the user has the same repo URL configured multiple + // times, or they're doing something extremely unusual. I'm not sure we can + // make the correct decision here. + allUrls := []string{} + for _, url := range gitRemotes.URLs() { + if !slices.Contains(allUrls, url) { + allUrls = append(allUrls, url) + } + } + + // Okay, there's only one URL so we can just pick the relevant project. + if len(allUrls) == 1 { + return gitRemotes[0].Project, nil + } + + // At this point we'd just be guessing, so let's give up + return models.ProjectV1Alpha{}, fmt.Errorf("found %d remotes with %d different URLs but cannot determine the correct one", len(gitRemotes), len(allUrls)) } func getProjectIdFromUrl(url string) (string, error) { @@ -70,6 +150,33 @@ func getProjectFromUrls(urls []string) (models.ProjectV1Alpha, error) { return models.ProjectV1Alpha{}, fmt.Errorf("no project found in this org with a url matching any of this repository's remotes") } +// getGitHubBaseRemoteName checks to see if the `gh` cli tool has set a default +// remote for this repository. If not, or if we're not using Github at all, we +// can just ignore the error. +func getGitHubBaseRemoteName() (string, error) { + args := []string{"config", "--local", "--get-regexp", "gh-resolved"} + cmd := exec.Command("git", args...) + out, err := cmd.CombinedOutput() + if err != nil { + cmd_string := fmt.Sprintf("'%s %s'", "git", strings.Join(args, " ")) + user_msg := "You are probably not in a git directory?" + return "", fmt.Errorf("%s failed with message: '%s'\n%s", cmd_string, err, user_msg) + } + + lines := strings.Split(strings.TrimSpace(string(out)), "\n") + + if len(lines) == 0 { + return "", fmt.Errorf("no GitHub base remote configured for this repository") + } + if len(lines) > 1 { + return "", fmt.Errorf("got multiple lines when looking for GitHub base remote") + } + + fields := strings.Fields(lines[0]) + remoteName := strings.Split(fields[0], ".")[1] + return remoteName, nil +} + func getGitRemotes() ([]string, error) { args := []string{"remote"} cmd := exec.Command("git", args...) @@ -96,6 +203,40 @@ func getGitRemoteUrl(remote string) (string, error) { return strings.TrimSpace(string(out)), nil } +func getAllGitRemotesAndProjects() (GitRemoteList, error) { + args := []string{"config", "--local", "--get-regexp", "remote\\..*\\.url"} + cmd := exec.Command("git", args...) + out, err := cmd.CombinedOutput() + if err != nil { + cmd_string := fmt.Sprintf("'%s %s'", "git", strings.Join(args, " ")) + user_msg := "You are probably not in a git directory?" + return GitRemoteList{}, fmt.Errorf("%s failed with message: '%s'\n%s", cmd_string, err, user_msg) + } + lines := strings.Split(strings.TrimSpace(string(out)), "\n") + + projectClient := client.NewProjectV1AlphaApi() + projects, err := projectClient.ListProjects() + + remotes := GitRemoteList{} + + for _, line := range lines { + fields := strings.Fields(line) + keyFields := strings.Split(line, ".") + remoteName := keyFields[1] + url := fields[1] + + for _, proj := range projects.Projects { + if proj.Spec.Repository.Url == url { + remotes = append(remotes, GitRemote{Name: remoteName, URL: url, Project: proj}) + break + } + } + + } + + return remotes, nil +} + func getAllGitRemoteURLs() ([]string, error) { gitRemotes, err := getGitRemotes() if err != nil { From ddb987855bca0eea9dbe04227cffd14e6bd14130 Mon Sep 17 00:00:00 2001 From: Daniel Fox Date: Wed, 22 Oct 2025 15:22:19 -0700 Subject: [PATCH 10/11] Remove now-dead code --- cmd/utils/project.go | 73 -------------------------------------------- 1 file changed, 73 deletions(-) diff --git a/cmd/utils/project.go b/cmd/utils/project.go index 89bde27..4580234 100644 --- a/cmd/utils/project.go +++ b/cmd/utils/project.go @@ -124,32 +124,6 @@ func InferProject() (models.ProjectV1Alpha, error) { return models.ProjectV1Alpha{}, fmt.Errorf("found %d remotes with %d different URLs but cannot determine the correct one", len(gitRemotes), len(allUrls)) } -func getProjectIdFromUrl(url string) (string, error) { - project, err := getProjectFromUrls([]string{url}) - if err != nil { - return "", fmt.Errorf("project with url %s not found in this org", url) - } - return project.Metadata.Id, nil - -} - -func getProjectFromUrls(urls []string) (models.ProjectV1Alpha, error) { - projectClient := client.NewProjectV1AlphaApi() - projects, err := projectClient.ListProjects() - - if err != nil { - return models.ProjectV1Alpha{}, fmt.Errorf("getting project list failed '%s'", err) - } - - for _, p := range projects.Projects { - if slices.Contains(urls, p.Spec.Repository.Url) { - return p, nil - } - } - - return models.ProjectV1Alpha{}, fmt.Errorf("no project found in this org with a url matching any of this repository's remotes") -} - // getGitHubBaseRemoteName checks to see if the `gh` cli tool has set a default // remote for this repository. If not, or if we're not using Github at all, we // can just ignore the error. @@ -177,32 +151,6 @@ func getGitHubBaseRemoteName() (string, error) { return remoteName, nil } -func getGitRemotes() ([]string, error) { - args := []string{"remote"} - cmd := exec.Command("git", args...) - out, err := cmd.CombinedOutput() - if err != nil { - cmd_string := fmt.Sprintf("'%s %s'", "git", strings.Join(args, " ")) - user_msg := "You are probably not in a git directory?" - return []string{}, fmt.Errorf("%s failed with message: '%s'\n%s", cmd_string, err, user_msg) - } - return strings.Split(strings.TrimSpace(string(out)), "\n"), nil -} - -func getGitRemoteUrl(remote string) (string, error) { - args := []string{"config", fmt.Sprintf("remote.%s.url", remote)} - - cmd := exec.Command("git", args...) - out, err := cmd.CombinedOutput() - if err != nil { - cmd_string := fmt.Sprintf("'%s %s'", "git", strings.Join(args, " ")) - user_msg := "You are probably not in a git directory?" - return "", fmt.Errorf("%s failed with message: '%s'\n%s", cmd_string, err, user_msg) - } - - return strings.TrimSpace(string(out)), nil -} - func getAllGitRemotesAndProjects() (GitRemoteList, error) { args := []string{"config", "--local", "--get-regexp", "remote\\..*\\.url"} cmd := exec.Command("git", args...) @@ -236,24 +184,3 @@ func getAllGitRemotesAndProjects() (GitRemoteList, error) { return remotes, nil } - -func getAllGitRemoteURLs() ([]string, error) { - gitRemotes, err := getGitRemotes() - if err != nil { - return gitRemotes, err - } - var gitRemoteURLs []string - for _, remote := range gitRemotes { - remoteURL, err := getGitRemoteUrl(remote) - if err != nil { - log.Printf("could not get URL for remote %s", remote) - } else { - gitRemoteURLs = append(gitRemoteURLs, remoteURL) - } - } - return gitRemoteURLs, nil -} - -func getGitOriginUrl() (string, error) { - return getGitRemoteUrl("origin") -} From 5adc57eae1ef7b4c0e22d4872759e2f2a1cc90bd Mon Sep 17 00:00:00 2001 From: Daniel Fox Date: Fri, 31 Oct 2025 11:09:55 -0700 Subject: [PATCH 11/11] Update cmd/get.go Co-authored-by: Amir Hasanbasic <43892661+hamir-suspect@users.noreply.github.com> --- cmd/get.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/cmd/get.go b/cmd/get.go index 8fcaed9..240d82d 100644 --- a/cmd/get.go +++ b/cmd/get.go @@ -288,7 +288,14 @@ var GetCurrentProjectCmd = &cobra.Command{ Use: "current_project", Short: "Get project for current directory", Aliases: []string{"cur"}, - Long: ``, +Long: `Determine the Semaphore project associated with this repository. + + Resolution order: + 1. A remote selected by 'gh repo set-default', if present. + 2. The 'origin' remote, when configured. + 3. The 'upstream' remote, when configured. + 4. Any remaining remote whose URL is shared by all candidates. + 5. An explicit error when multiple distinct URLs remain.`, Run: func(cmd *cobra.Command, args []string) { project, err := utils.InferProject()