diff --git a/.github/workflows/go.yaml b/.github/workflows/go.yaml new file mode 100644 index 0000000..47e220f --- /dev/null +++ b/.github/workflows/go.yaml @@ -0,0 +1,25 @@ +name: Go + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.19' + + - name: Build + run: make compile + + - name: Test + run: make test diff --git a/cmd/client.go b/cmd/client.go index 9ef9cf4..09de10a 100644 --- a/cmd/client.go +++ b/cmd/client.go @@ -80,21 +80,20 @@ func (c *Client) initGitlabClient() error { } /* This will fetch the project ID and merge request ID using the client */ -func (c *Client) initProjectSettings(remoteUrl string, namespace string, projectName string, branchName string) error { +func (c *Client) initProjectSettings(g GitProjectInfo) error { - idStr := namespace + "/" + projectName opt := gitlab.GetProjectOptions{} - project, _, err := c.git.Projects.GetProject(idStr, &opt) + project, _, err := c.git.Projects.GetProject(g.projectPath(), &opt) if err != nil { - return fmt.Errorf(fmt.Sprintf("Error getting project at %s", remoteUrl), err) + return fmt.Errorf(fmt.Sprintf("Error getting project at %s", g.RemoteUrl), err) } if project == nil { - return fmt.Errorf(fmt.Sprintf("Could not find project at %s", remoteUrl), err) + return fmt.Errorf(fmt.Sprintf("Could not find project at %s", g.RemoteUrl), err) } if project == nil { - return fmt.Errorf("No projects you are a member of contained remote URL %s", remoteUrl) + return fmt.Errorf("No projects you are a member of contained remote URL %s", g.RemoteUrl) } c.projectId = fmt.Sprint(project.ID) @@ -102,7 +101,7 @@ func (c *Client) initProjectSettings(remoteUrl string, namespace string, project options := gitlab.ListProjectMergeRequestsOptions{ Scope: gitlab.String("all"), State: gitlab.String("opened"), - SourceBranch: &branchName, + SourceBranch: &g.BranchName, } mergeRequests, _, err := c.git.MergeRequests.ListProjectMergeRequests(c.projectId, &options) diff --git a/cmd/git.go b/cmd/git.go index d7ba70b..e7156aa 100644 --- a/cmd/git.go +++ b/cmd/git.go @@ -7,37 +7,58 @@ import ( "strings" ) +type GitProjectInfo struct { + RemoteUrl string + Namespace string + ProjectName string + BranchName string +} + +/* +projectPath returns the Gitlab project full path, which isn't necessarily the same as its name. +See https://docs.gitlab.com/ee/api/rest/index.html#namespaced-path-encoding for more information. +*/ +func (g GitProjectInfo) projectPath() string { + return g.Namespace + "/" + g.ProjectName +} + /* Extracts information about the current repository and returns it to the client for initialization. The current directory must be a valid Gitlab project and the branch must be a feature branch */ -func ExtractGitInfo() (string, string, string, string, error) { - - url, err := getProjectUrl() +func ExtractGitInfo(getProjectRemoteUrl func() (string, error), getCurrentBranchName func() (string, error)) (GitProjectInfo, error) { + url, err := getProjectRemoteUrl() if err != nil { - return "", "", "", "", fmt.Errorf("Could not get project Url: %v", err) + return GitProjectInfo{}, fmt.Errorf("Could not get project Url: %v", err) } - re := regexp.MustCompile(`(?:[:\/])([^\/]+)\/([^\/]+)\.git$`) + // play with regex at: https://regex101.com/r/P2jSGh/1 + re := regexp.MustCompile(`(?:^git@.+:|^https?:\/\/.+?[^\/:]\/)(.+)\/([^\/]+)\.git$`) matches := re.FindStringSubmatch(url) if len(matches) != 3 { - return "", "", "", "", fmt.Errorf("Invalid Git URL format: %s", url) + return GitProjectInfo{}, fmt.Errorf("Invalid Git URL format: %s", url) } namespace := matches[1] projectName := matches[2] - branch, err := getCurrentBranch() + branchName, err := getCurrentBranchName() if err != nil { - return "", "", "", "", fmt.Errorf("Failed to get current branch: %v", err) + return GitProjectInfo{}, fmt.Errorf("Failed to get current branch: %v", err) } - return url, namespace, projectName, branch, nil + return GitProjectInfo{ + RemoteUrl: url, + Namespace: namespace, + ProjectName: projectName, + BranchName: branchName, + }, + nil } -/* Gets the current branch */ -func getCurrentBranch() (res string, e error) { +/* Gets the current branch name */ +func GetCurrentBranchNameFromNativeGitCmd() (res string, e error) { gitCmd := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD") output, err := gitCmd.Output() @@ -55,10 +76,9 @@ func getCurrentBranch() (res string, e error) { } /* Gets the project SSH or HTTPS url */ -func getProjectUrl() (res string, e error) { +func GetProjectUrlFromNativeGitCmd() (string, error) { cmd := exec.Command("git", "remote", "get-url", "origin") url, err := cmd.Output() - if err != nil { return "", fmt.Errorf("Could not get origin remote") } diff --git a/cmd/git_test.go b/cmd/git_test.go new file mode 100644 index 0000000..340d82f --- /dev/null +++ b/cmd/git_test.go @@ -0,0 +1,157 @@ +package main + +import ( + "errors" + "fmt" + "testing" +) + +func TestExtractGitInfo_Success(t *testing.T) { + getCurrentBranchName := func() (string, error) { + return "feature/abc", nil + } + testCases := []struct { + getProjectRemoteUrl func() (string, error) + expected GitProjectInfo + desc string + }{ + { + desc: "Project configured in SSH under a single folder", + getProjectRemoteUrl: func() (string, error) { + return "git@custom-gitlab.com:namespace-1/project-name.git", nil + }, + expected: GitProjectInfo{ + RemoteUrl: "git@custom-gitlab.com:namespace-1/project-name.git", + BranchName: "feature/abc", + ProjectName: "project-name", + Namespace: "namespace-1", + }, + }, + { + desc: "Project configured in SSH under one nested folder", + getProjectRemoteUrl: func() (string, error) { + return "git@custom-gitlab.com:namespace-1/namespace-2/project-name.git", nil + }, + expected: GitProjectInfo{ + RemoteUrl: "git@custom-gitlab.com:namespace-1/namespace-2/project-name.git", + BranchName: "feature/abc", + ProjectName: "project-name", + Namespace: "namespace-1/namespace-2", + }, + }, + { + desc: "Project configured in SSH under two nested folders", + getProjectRemoteUrl: func() (string, error) { + return "git@custom-gitlab.com:namespace-1/namespace-2/namespace-3/project-name.git", nil + }, + expected: GitProjectInfo{ + RemoteUrl: "git@custom-gitlab.com:namespace-1/namespace-2/namespace-3/project-name.git", + BranchName: "feature/abc", + ProjectName: "project-name", + Namespace: "namespace-1/namespace-2/namespace-3", + }, + }, + { + desc: "Project configured in HTTPS and under a single folder", + getProjectRemoteUrl: func() (string, error) { + return "https://custom-gitlab.com/namespace-1/project-name.git", nil + }, + expected: GitProjectInfo{ + RemoteUrl: "https://custom-gitlab.com/namespace-1/project-name.git", + BranchName: "feature/abc", + ProjectName: "project-name", + Namespace: "namespace-1", + }, + }, + { + desc: "Project configured in HTTPS and under a nested folder", + getProjectRemoteUrl: func() (string, error) { + return "https://custom-gitlab.com/namespace-1/namespace-2/project-name.git", nil + }, + expected: GitProjectInfo{ + RemoteUrl: "https://custom-gitlab.com/namespace-1/namespace-2/project-name.git", + BranchName: "feature/abc", + ProjectName: "project-name", + Namespace: "namespace-1/namespace-2", + }, + }, + { + desc: "Project configured in HTTPS and under two nested folders", + getProjectRemoteUrl: func() (string, error) { + return "https://custom-gitlab.com/namespace-1/namespace-2/namespace-3/project-name.git", nil + }, + expected: GitProjectInfo{ + RemoteUrl: "https://custom-gitlab.com/namespace-1/namespace-2/namespace-3/project-name.git", + BranchName: "feature/abc", + ProjectName: "project-name", + Namespace: "namespace-1/namespace-2/namespace-3", + }, + }, + } + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + actual, err := ExtractGitInfo(tC.getProjectRemoteUrl, getCurrentBranchName) + if err != nil { + t.Errorf("No error was expected, got %s", err) + } + if actual != tC.expected { + t.Errorf("\nExpected: %s\nActual: %s", tC.expected, actual) + } + }) + } +} + +func TestExtractGitInfo_FailToGetProjectRemoteUrl(t *testing.T) { + getCurrentBranchName := func() (string, error) { + return "feature/abc", nil + } + + testCases := []struct { + getProjectRemoteUrl func() (string, error) + expectedErrorMessage string + desc string + }{ + { + desc: "Error returned by function to get the project remote url", + getProjectRemoteUrl: func() (string, error) { + return "", errors.New("error when getting project remote url") + }, + expectedErrorMessage: "Could not get project Url: error when getting project remote url", + }, + { + desc: "Invalid project remote url", + getProjectRemoteUrl: func() (string, error) { + return "git@invalid", nil + }, + expectedErrorMessage: "Invalid Git URL format: git@invalid", + }, + } + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + _, actualErr := ExtractGitInfo(tC.getProjectRemoteUrl, getCurrentBranchName) + if actualErr == nil { + t.Errorf("Expected an error, got none") + } + if actualErr.Error() != tC.expectedErrorMessage { + t.Errorf("\nExpected: %s\nActual: %s", tC.expectedErrorMessage, actualErr.Error()) + } + }) + } +} + +func TestExtractGitInfo_FailToGetCurrentBranchName(t *testing.T) { + expectedErrNestedMsg := "error when getting current branch name" + _, actualErr := ExtractGitInfo(func() (string, error) { + return "git@custom-gitlab.com:namespace/project.git", nil + }, func() (string, error) { + return "", errors.New(expectedErrNestedMsg) + }) + + if actualErr == nil { + t.Errorf("Expected an error, got none") + } + expectedErr := fmt.Errorf("Failed to get current branch: %s", expectedErrNestedMsg) + if actualErr.Error() != expectedErr.Error() { + t.Errorf("\nExpected: %s\nActual: %s", expectedErr, actualErr) + } +} diff --git a/cmd/main.go b/cmd/main.go index f4b7b40..a476c1f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -11,7 +11,7 @@ import ( ) func main() { - url, namespace, projectName, branchName, err := ExtractGitInfo() + g, err := ExtractGitInfo(GetProjectUrlFromNativeGitCmd, GetCurrentBranchNameFromNativeGitCmd) if err != nil { log.Fatalf("Failed to get git namespace, project, branch, or url: %v", err) } @@ -21,7 +21,7 @@ func main() { log.Fatalf("Failed to initialize Gitlab client: %v", err) } - if err := c.initProjectSettings(url, namespace, projectName, branchName); err != nil { + if err := c.initProjectSettings(g); err != nil { log.Fatalf("Failed to initialize project settings: %v", err) } @@ -82,7 +82,7 @@ func main() { } } -func PingHandler(w http.ResponseWriter, r *http.Request) { +func PingHandler(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) fmt.Fprintln(w, "pong") } diff --git a/makefile b/makefile index 71b607f..f077924 100644 --- a/makefile +++ b/makefile @@ -1,2 +1,19 @@ -compile: - cd cmd && go build -o bin && mv bin ../bin +default: help + +PROJECTNAME=$(shell basename "$(PWD)") + +## compile: build golang project +compile: + @cd cmd && go build -o bin && mv bin ../bin +## test: run golang project tests +test: + @cd cmd && go test -v + +.PHONY: help +all: help +help: makefile + @echo + @echo " Choose a command run in "$(PROJECTNAME)":" + @echo + @sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /' + @echo