diff --git a/README.md b/README.md index 9d05af00..230f1e96 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Currently supported providers are: [GitHub](#github), [Bitbucket Server](#bitbuc - [Delete Webhook](#delete-webhook) - [Set Commit Status](#set-commit-status) - [Create Pull Request](#create-pull-request) + - [Latest Commit Hash](#get-latest-commit-hash) - [Webhook Parser](#webhook-parser) ### VCS Clients @@ -32,6 +33,7 @@ Currently supported providers are: [GitHub](#github), [Bitbucket Server](#bitbuc ##### GitHub +GitHub api v3 is used ```go // The VCS provider. Cannot be changed. vcsProvider := vcsutils.GitHub @@ -47,6 +49,7 @@ client, err := vcsclient.NewClientBuilder(vcsProvider).ApiEndpoint(apiEndpoint). ##### GitLab +GitLab api v4 is used. ```go // The VCS provider. Cannot be changed. vcsProvider := vcsutils.GitLab @@ -62,6 +65,7 @@ client, err := vcsclient.NewClientBuilder(vcsProvider).ApiEndpoint(apiEndpoint). ##### Bitbucket Server +Bitbucket api 1.0 is used. ```go // The VCS provider. Cannot be changed. vcsProvider := vcsclient.BitbucketServer @@ -77,6 +81,7 @@ client, err := vcsclient.NewClientBuilder(vcsProvider).ApiEndpoint(apiEndpoint). ##### Bitbucket Cloud +Bitbucket cloud api version 2.0 is used and the version should be added to the apiEndpoint. ```go // The VCS provider. Cannot be changed. vcsProvider := vcsutils.BitbucketCloud @@ -244,6 +249,22 @@ description := "Pull request description" err := client.CreatePullRequest(ctx, owner, repository, sourceBranch, targetBranch, title, description string) ``` +#### Get Latest Commit Hash + +```go +// Go context +ctx := context.Background() +// Organization or username +owner := "jfrog" +// VCS repository +repository := "jfrog-cli" +// VCS branch +branch := "dev" + +// SHA-1 hash of the latest commit +commitHash, err := client.GetLatestCommitHash(ctx, owner, repository, branch) +``` + ### Webhook Parser ```go diff --git a/vcsclient/bitbucketcloud.go b/vcsclient/bitbucketcloud.go index fc6fe7da..80bc3433 100644 --- a/vcsclient/bitbucketcloud.go +++ b/vcsclient/bitbucketcloud.go @@ -8,43 +8,46 @@ import ( "net/url" "os" "strings" + "time" "github.com/jfrog/froggit-go/vcsutils" "github.com/ktrysmt/go-bitbucket" ) type BitbucketCloudClient struct { - bitbucketClient *bitbucket.Client - username string - token string + vcsInfo VcsInfo } -func NewBitbucketCloudClient(vcsInfo *VcsInfo) (*BitbucketCloudClient, error) { +func NewBitbucketCloudClient(vcsInfo VcsInfo) (*BitbucketCloudClient, error) { err := os.Setenv("BITBUCKET_API_BASE_URL", vcsInfo.ApiEndpoint) if err != nil { return nil, err } bitbucketClient := &BitbucketCloudClient{ - bitbucketClient: bitbucket.NewBasicAuth(vcsInfo.Username, vcsInfo.Token), - username: vcsInfo.Username, - token: vcsInfo.Token, + vcsInfo: vcsInfo, } return bitbucketClient, nil } -func (client *BitbucketCloudClient) TestConnection(_ context.Context) error { - _, err := client.bitbucketClient.User.Profile() +func (client *BitbucketCloudClient) buildBitbucketCloudClient(_ context.Context) *bitbucket.Client { + return bitbucket.NewBasicAuth(client.vcsInfo.Username, client.vcsInfo.Token) +} + +func (client *BitbucketCloudClient) TestConnection(ctx context.Context) error { + bitbucketClient := client.buildBitbucketCloudClient(ctx) + _, err := bitbucketClient.User.Profile() return err } -func (client *BitbucketCloudClient) ListRepositories(_ context.Context) (map[string][]string, error) { +func (client *BitbucketCloudClient) ListRepositories(ctx context.Context) (map[string][]string, error) { + bitbucketClient := client.buildBitbucketCloudClient(ctx) results := make(map[string][]string) - workspaces, err := client.bitbucketClient.Workspaces.List() + workspaces, err := bitbucketClient.Workspaces.List() if err != nil { return nil, err } for _, workspace := range workspaces.Workspaces { - repositoriesRes, err := client.bitbucketClient.Repositories.ListForAccount(&bitbucket.RepositoriesOptions{Owner: workspace.Slug}) + repositoriesRes, err := bitbucketClient.Repositories.ListForAccount(&bitbucket.RepositoriesOptions{Owner: workspace.Slug}) if err != nil { return nil, err } @@ -55,8 +58,9 @@ func (client *BitbucketCloudClient) ListRepositories(_ context.Context) (map[str return results, nil } -func (client *BitbucketCloudClient) ListBranches(_ context.Context, owner, repository string) ([]string, error) { - branches, err := client.bitbucketClient.Repositories.Repository.ListBranches(&bitbucket.RepositoryBranchOptions{Owner: owner, RepoSlug: repository}) +func (client *BitbucketCloudClient) ListBranches(ctx context.Context, owner, repository string) ([]string, error) { + bitbucketClient := client.buildBitbucketCloudClient(ctx) + branches, err := bitbucketClient.Repositories.Repository.ListBranches(&bitbucket.RepositoryBranchOptions{Owner: owner, RepoSlug: repository}) if err != nil { return nil, err } @@ -68,8 +72,9 @@ func (client *BitbucketCloudClient) ListBranches(_ context.Context, owner, repos return results, nil } -func (client *BitbucketCloudClient) CreateWebhook(_ context.Context, owner, repository, _, payloadUrl string, +func (client *BitbucketCloudClient) CreateWebhook(ctx context.Context, owner, repository, _, payloadUrl string, webhookEvents ...vcsutils.WebhookEvent) (string, string, error) { + bitbucketClient := client.buildBitbucketCloudClient(ctx) token := vcsutils.CreateToken() options := &bitbucket.WebhooksOptions{ Active: true, @@ -78,7 +83,7 @@ func (client *BitbucketCloudClient) CreateWebhook(_ context.Context, owner, repo Url: payloadUrl + "?token=" + url.QueryEscape(token), Events: getBitbucketCloudWebhookEvents(webhookEvents...), } - response, err := client.bitbucketClient.Repositories.Webhooks.Create(options) + response, err := bitbucketClient.Repositories.Webhooks.Create(options) if err != nil { return "", "", err } @@ -89,8 +94,9 @@ func (client *BitbucketCloudClient) CreateWebhook(_ context.Context, owner, repo return id, token, err } -func (client *BitbucketCloudClient) UpdateWebhook(_ context.Context, owner, repository, _, payloadUrl, token, +func (client *BitbucketCloudClient) UpdateWebhook(ctx context.Context, owner, repository, _, payloadUrl, token, webhookId string, webhookEvents ...vcsutils.WebhookEvent) error { + bitbucketClient := client.buildBitbucketCloudClient(ctx) options := &bitbucket.WebhooksOptions{ Active: true, Uuid: webhookId, @@ -99,22 +105,24 @@ func (client *BitbucketCloudClient) UpdateWebhook(_ context.Context, owner, repo Url: payloadUrl + "?token=" + url.QueryEscape(token), Events: getBitbucketCloudWebhookEvents(webhookEvents...), } - _, err := client.bitbucketClient.Repositories.Webhooks.Update(options) + _, err := bitbucketClient.Repositories.Webhooks.Update(options) return err } -func (client *BitbucketCloudClient) DeleteWebhook(_ context.Context, owner, repository, webhookId string) error { +func (client *BitbucketCloudClient) DeleteWebhook(ctx context.Context, owner, repository, webhookId string) error { + bitbucketClient := client.buildBitbucketCloudClient(ctx) options := &bitbucket.WebhooksOptions{ Uuid: webhookId, Owner: owner, RepoSlug: repository, } - _, err := client.bitbucketClient.Repositories.Webhooks.Delete(options) + _, err := bitbucketClient.Repositories.Webhooks.Delete(options) return err } -func (client *BitbucketCloudClient) SetCommitStatus(_ context.Context, commitStatus CommitStatus, owner, repository, +func (client *BitbucketCloudClient) SetCommitStatus(ctx context.Context, commitStatus CommitStatus, owner, repository, ref, title, description, detailsUrl string) error { + bitbucketClient := client.buildBitbucketCloudClient(ctx) commitOptions := &bitbucket.CommitsOptions{ Owner: owner, RepoSlug: repository, @@ -126,13 +134,14 @@ func (client *BitbucketCloudClient) SetCommitStatus(_ context.Context, commitSta Description: description, Url: detailsUrl, } - _, err := client.bitbucketClient.Repositories.Commits.CreateCommitStatus(commitOptions, commitStatusOptions) + _, err := bitbucketClient.Repositories.Commits.CreateCommitStatus(commitOptions, commitStatusOptions) return err } -func (client *BitbucketCloudClient) DownloadRepository(_ context.Context, owner, repository, branch, +func (client *BitbucketCloudClient) DownloadRepository(ctx context.Context, owner, repository, branch, localPath string) error { - repo, err := client.bitbucketClient.Repositories.Repository.Get(&bitbucket.RepositoryOptions{ + bitbucketClient := client.buildBitbucketCloudClient(ctx) + repo, err := bitbucketClient.Repositories.Repository.Get(&bitbucket.RepositoryOptions{ Owner: owner, RepoSlug: repository, }) @@ -145,23 +154,24 @@ func (client *BitbucketCloudClient) DownloadRepository(_ context.Context, owner, return err } - getRequest, err := http.NewRequest("GET", downloadLink, nil) + getRequest, err := http.NewRequestWithContext(ctx, "GET", downloadLink, nil) if err != nil { return err } - if len(client.username) > 0 || len(client.token) > 0 { - getRequest.SetBasicAuth(client.username, client.token) + if len(client.vcsInfo.Username) > 0 || len(client.vcsInfo.Token) > 0 { + getRequest.SetBasicAuth(client.vcsInfo.Username, client.vcsInfo.Token) } - response, err := client.bitbucketClient.HttpClient.Do(getRequest) + response, err := bitbucketClient.HttpClient.Do(getRequest) if err != nil { return err } return vcsutils.Untar(localPath, response.Body, true) } -func (client *BitbucketCloudClient) CreatePullRequest(_ context.Context, owner, repository, sourceBranch, +func (client *BitbucketCloudClient) CreatePullRequest(ctx context.Context, owner, repository, sourceBranch, targetBranch, title, description string) error { + bitbucketClient := client.buildBitbucketCloudClient(ctx) options := &bitbucket.PullRequestsOptions{ Owner: owner, SourceRepository: owner + "/" + repository, @@ -171,10 +181,87 @@ func (client *BitbucketCloudClient) CreatePullRequest(_ context.Context, owner, Title: title, Description: description, } - _, err := client.bitbucketClient.Repositories.PullRequests.Create(options) + _, err := bitbucketClient.Repositories.PullRequests.Create(options) return err } +func (client *BitbucketCloudClient) GetLatestCommit(ctx context.Context, owner, repository, branch string) (CommitInfo, error) { + err := validateParametersNotBlank(owner, repository, branch) + if err != nil { + return CommitInfo{}, err + } + bitbucketClient := client.buildBitbucketCloudClient(ctx) + bitbucketClient.Pagelen = 1 + options := &bitbucket.CommitsOptions{ + Owner: owner, + RepoSlug: repository, + Branchortag: branch, + } + commits, err := bitbucketClient.Repositories.Commits.GetCommits(options) + if err != nil { + return CommitInfo{}, err + } + parsedCommits, err := extractCommitFromResponse(commits) + if err != nil { + return CommitInfo{}, err + } + if len(parsedCommits.Values) > 0 { + latestCommit := parsedCommits.Values[0] + parents := make([]string, len(latestCommit.Parents)) + for i, p := range latestCommit.Parents { + parents[i] = p.Hash + } + return CommitInfo{ + Hash: latestCommit.Hash, + AuthorName: latestCommit.Author.User.DisplayName, + CommitterName: "", // not provided + Url: latestCommit.Links.Self.Href, + Timestamp: latestCommit.Date.UTC().Unix(), + Message: latestCommit.Message, + ParentHashes: parents, + }, nil + } + return CommitInfo{}, nil +} + +func extractCommitFromResponse(commits interface{}) (*commitResponse, error) { + var res commitResponse + b, err := json.Marshal(commits) + if err != nil { + return nil, err + } + err = json.Unmarshal(b, &res) + if err != nil { + return nil, err + } + return &res, nil +} + +type commitResponse struct { + Values []commitDetails `json:"values"` +} + +type commitDetails struct { + Hash string `json:"hash"` + Date time.Time `json:"date"` + Message string `json:"message"` + Author struct { + User struct { + DisplayName string `json:"display_name"` + } `json:"user"` + } `json:"author"` + Links struct { + Self link `json:"self"` + } `json:"links"` + Parents []struct { + Hash string `json:"hash"` + } `json:"parents"` +} + +type link struct { + Href string `json:"href"` +} + // Extract the webhook id from the webhook create response func getBitbucketCloudWebhookId(r interface{}) (string, error) { webhook := &bitbucket.WebhooksOptions{} @@ -207,7 +294,7 @@ func getBitbucketCloudWebhookEvents(webhookEvents ...vcsutils.WebhookEvent) []st // The get repository request returns HTTP link to the repository - extract the link from the response. func getDownloadLink(repo *bitbucket.Repository, branch string) (string, error) { - repositoryHtmlLinks := &repositoryHtmlLinks{} + repositoryHtmlLinks := &link{} bytes, err := json.Marshal(repo.Links["html"]) if err != nil { return "", err @@ -222,7 +309,3 @@ func getDownloadLink(repo *bitbucket.Repository, branch string) (string, error) } return htmlLink + "/get/" + branch + ".tar.gz", err } - -type repositoryHtmlLinks struct { - Href string `json:"href,omitempty"` -} diff --git a/vcsclient/bitbucketcloud_test.go b/vcsclient/bitbucketcloud_test.go index 71cadaf0..83c44fe5 100644 --- a/vcsclient/bitbucketcloud_test.go +++ b/vcsclient/bitbucketcloud_test.go @@ -8,6 +8,7 @@ import ( "io/ioutil" "net/http" "os" + "path/filepath" "testing" "time" @@ -17,7 +18,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestBitbucketCloudConnection(t *testing.T) { +func TestBitbucketCloud_Connection(t *testing.T) { ctx := context.Background() mockResponse := map[string][]bitbucket.User{"values": {}} client, cleanUp := createServerAndClient(t, vcsutils.BitbucketCloud, true, mockResponse, "/user", createBitbucketCloudHandler) @@ -27,31 +28,31 @@ func TestBitbucketCloudConnection(t *testing.T) { assert.NoError(t, err) } -func TestBitbucketCloudConnectionWhenContextCancelled(t *testing.T) { +func TestBitbucketCloud_ConnectionWhenContextCancelled(t *testing.T) { t.Skip("Bitbucket cloud does not use the context") ctx := context.Background() ctxWithCancel, cancel := context.WithCancel(ctx) cancel() - client, closeServer := createWaitingServerAndClient(t, vcsutils.BitbucketCloud, 0) - defer closeServer() + client, cleanUp := createWaitingServerAndClient(t, vcsutils.BitbucketCloud, 0) + defer cleanUp() err := client.TestConnection(ctxWithCancel) assert.ErrorIs(t, err, context.Canceled) } -func TestBitbucketCloudConnectionWhenContextTimesOut(t *testing.T) { +func TestBitbucketCloud_ConnectionWhenContextTimesOut(t *testing.T) { t.Skip("Bitbucket cloud does not use the context") ctx := context.Background() ctxWithTimeout, cancel := context.WithTimeout(ctx, 10*time.Millisecond) defer cancel() - client, closeServer := createWaitingServerAndClient(t, vcsutils.BitbucketCloud, 50*time.Millisecond) - defer closeServer() + client, cleanUp := createWaitingServerAndClient(t, vcsutils.BitbucketCloud, 50*time.Millisecond) + defer cleanUp() err := client.TestConnection(ctxWithTimeout) assert.ErrorIs(t, err, context.DeadlineExceeded) } -func TestBitbucketCloudListRepositories(t *testing.T) { +func TestBitbucketCloud_ListRepositories(t *testing.T) { ctx := context.Background() mockResponse := map[string][]bitbucket.Repository{ "values": {{Slug: repo1}, {Slug: repo2}}, @@ -64,7 +65,7 @@ func TestBitbucketCloudListRepositories(t *testing.T) { assert.Equal(t, map[string][]string{username: {repo1, repo2}}, actualRepositories) } -func TestBitbucketCloudListBranches(t *testing.T) { +func TestBitbucketCloud_ListBranches(t *testing.T) { ctx := context.Background() mockResponse := map[string][]bitbucket.BranchModel{ "values": {{Name: branch1}, {Name: branch2}}, @@ -77,7 +78,7 @@ func TestBitbucketCloudListBranches(t *testing.T) { assert.ElementsMatch(t, actualRepositories, []string{branch1, branch2}) } -func TestBitbucketCloudCreateWebhook(t *testing.T) { +func TestBitbucketCloud_CreateWebhook(t *testing.T) { ctx := context.Background() id, err := uuid.NewUUID() assert.NoError(t, err) @@ -92,7 +93,7 @@ func TestBitbucketCloudCreateWebhook(t *testing.T) { assert.Equal(t, id.String(), actualId) } -func TestBitbucketCloudUpdateWebhook(t *testing.T) { +func TestBitbucketCloud_UpdateWebhook(t *testing.T) { ctx := context.Background() id, err := uuid.NewUUID() assert.NoError(t, err) @@ -104,7 +105,7 @@ func TestBitbucketCloudUpdateWebhook(t *testing.T) { assert.NoError(t, err) } -func TestBitbucketCloudDeleteWebhook(t *testing.T) { +func TestBitbucketCloud_DeleteWebhook(t *testing.T) { ctx := context.Background() id, err := uuid.NewUUID() assert.NoError(t, err) @@ -116,7 +117,7 @@ func TestBitbucketCloudDeleteWebhook(t *testing.T) { assert.NoError(t, err) } -func TestBitbucketCloudSetCommitStatus(t *testing.T) { +func TestBitbucketCloud_SetCommitStatus(t *testing.T) { ctx := context.Background() ref := "9caf1c431fb783b669f0f909bd018b40f2ea3808" client, cleanUp := createServerAndClient(t, vcsutils.BitbucketCloud, true, nil, fmt.Sprintf("/repositories/jfrog/repo-1/commit/%s/statuses/build", ref), createBitbucketCloudHandler) @@ -127,7 +128,7 @@ func TestBitbucketCloudSetCommitStatus(t *testing.T) { assert.NoError(t, err) } -func TestBitbucketCloudDownloadRepository(t *testing.T) { +func TestBitbucketCloud_DownloadRepository(t *testing.T) { ctx := context.Background() dir, err := ioutil.TempDir("", "") assert.NoError(t, err) @@ -149,7 +150,7 @@ func TestBitbucketCloudDownloadRepository(t *testing.T) { assert.True(t, readmeFound) } -func TestBitbucketCloudCreatePullRequest(t *testing.T) { +func TestBitbucketCloud_CreatePullRequest(t *testing.T) { ctx := context.Background() client, cleanUp := createServerAndClient(t, vcsutils.BitbucketCloud, true, nil, "/repositories/jfrog/repo-1/pullrequests/", createBitbucketCloudHandler) defer cleanUp() @@ -158,9 +159,93 @@ func TestBitbucketCloudCreatePullRequest(t *testing.T) { assert.NoError(t, err) } -func createBitbucketCloudHandler(t *testing.T, expectedUri string, response []byte) http.HandlerFunc { +func TestBitbucketCloud_GetLatestCommit(t *testing.T) { + ctx := context.Background() + response, err := os.ReadFile(filepath.Join("testdata", "bitbucketcloud", "commit_list_response.json")) + assert.NoError(t, err) + + client, cleanUp := createServerAndClient(t, vcsutils.BitbucketCloud, true, response, + fmt.Sprintf("/repositories/%s/%s/commits/%s?pagelen=1", owner, repo1, "master"), createBitbucketCloudHandler) + defer cleanUp() + + result, err := client.GetLatestCommit(ctx, owner, repo1, "master") + + require.NoError(t, err) + assert.Equal(t, CommitInfo{ + Hash: "ec05bacb91d757b4b6b2a11a0676471020e89fb5", + AuthorName: "user", + CommitterName: "", + Url: "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/ec05bacb91d757b4b6b2a11a0676471020e89fb5", + Timestamp: 1591040823, + Message: "Fix README.md: yaml\n", + ParentHashes: []string{"774aa0fb252bccbc2a7e01060ef4d4be0b0eeaa9", "def26c6128ebe11fac555fe58b59227e9655dc4d"}, + }, result) +} + +func TestBitbucketCloud_GetLatestCommitNotFound(t *testing.T) { + ctx := context.Background() + response := []byte(``) + + client, cleanUp := createServerAndClientReturningStatus(t, vcsutils.BitbucketCloud, true, response, + fmt.Sprintf("/repositories/%s/%s/commits/%s?pagelen=1", owner, repo1, "master"), http.StatusNotFound, + createBitbucketCloudHandler) + defer cleanUp() + + result, err := client.GetLatestCommit(ctx, owner, repo1, "master") + require.EqualError(t, err, "404 Not Found") + assert.Empty(t, result) +} + +func TestBitbucketCloud_GetLatestCommitInvalidPayload(t *testing.T) { + tests := []struct { + name string + owner string + repo string + branch string + }{ + { + name: "all empty", + owner: "", + repo: "", + branch: "", + }, + { + name: "empty owner", + owner: "", + repo: "repo", + branch: "branch", + }, + { + name: "empty repo", + owner: "owner", + repo: "", + branch: "branch", + }, + { + name: "empty branch", + owner: "owner", + repo: "repo", + branch: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + client, err := NewClientBuilder(vcsutils.BitbucketCloud).Build() + require.NoError(t, err) + + result, err := client.GetLatestCommit(ctx, tt.owner, tt.repo, tt.branch) + + require.EqualError(t, err, "required parameter is empty") + assert.Empty(t, result) + }) + } +} + +func createBitbucketCloudHandler(t *testing.T, expectedUri string, response []byte, expectedStatusCode int) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) + w.WriteHeader(expectedStatusCode) if r.RequestURI == "/workspaces" { workspacesResults := make(map[string]interface{}) workspacesResults["values"] = []bitbucket.Workspace{{Slug: username}} diff --git a/vcsclient/bitbucketserver.go b/vcsclient/bitbucketserver.go index 1f740d9f..c54c9587 100644 --- a/vcsclient/bitbucketserver.go +++ b/vcsclient/bitbucketserver.go @@ -13,10 +13,10 @@ import ( ) type BitbucketServerClient struct { - vcsInfo *VcsInfo + vcsInfo VcsInfo } -func NewBitbucketServerClient(vcsInfo *VcsInfo) (*BitbucketServerClient, error) { +func NewBitbucketServerClient(vcsInfo VcsInfo) (*BitbucketServerClient, error) { bitbucketServerClient := &BitbucketServerClient{ vcsInfo: vcsInfo, } @@ -40,7 +40,9 @@ func (client *BitbucketServerClient) TestConnection(ctx context.Context) error { if err != nil { return err } - _, err = bitbucketClient.GetUsers(make(map[string]interface{})) + + options := map[string]interface{}{"limit": 1} + _, err = bitbucketClient.GetUsers(options) return err } @@ -209,6 +211,48 @@ type projectsResponse struct { } `json:"values,omitempty"` } +func (client *BitbucketServerClient) GetLatestCommit(ctx context.Context, owner, repository, branch string) (CommitInfo, error) { + err := validateParametersNotBlank(owner, repository, branch) + if err != nil { + return CommitInfo{}, err + } + + options := map[string]interface{}{ + "limit": 1, + "until": branch, + } + bitbucketClient, err := client.buildBitbucketClient(ctx) + if err != nil { + return CommitInfo{}, err + } + + apiResponse, err := bitbucketClient.GetCommits(owner, repository, options) + if err != nil { + return CommitInfo{}, err + } + commits, err := bitbucketv1.GetCommitsResponse(apiResponse) + if err != nil { + return CommitInfo{}, err + } + if len(commits) > 0 { + latestCommit := commits[0] + parents := make([]string, len(latestCommit.Parents)) + for i, p := range latestCommit.Parents { + parents[i] = p.ID + } + return CommitInfo{ + Hash: latestCommit.ID, + AuthorName: latestCommit.Author.Name, + CommitterName: latestCommit.Committer.Name, + Url: "", // URL not provided + Timestamp: latestCommit.CommitterTimestamp, + Message: latestCommit.Message, + ParentHashes: parents, + }, nil + } + return CommitInfo{}, nil +} + // Get all projects for which the authenticated user has the PROJECT_VIEW permission func (client *BitbucketServerClient) listProjects(bitbucketClient *bitbucketv1.DefaultApiService) ([]string, error) { var apiResponse *bitbucketv1.APIResponse diff --git a/vcsclient/bitbucketserver_test.go b/vcsclient/bitbucketserver_test.go index f1a54789..4d98d93d 100644 --- a/vcsclient/bitbucketserver_test.go +++ b/vcsclient/bitbucketserver_test.go @@ -19,39 +19,40 @@ import ( "github.com/stretchr/testify/assert" ) -func TestBitbucketServerConnection(t *testing.T) { +func TestBitbucketServer_Connection(t *testing.T) { ctx := context.Background() mockResponse := make(map[string][]bitbucketv1.User) - client, cleanUp := createServerAndClient(t, vcsutils.BitbucketServer, true, mockResponse, "/api/1.0/admin/users", createBitbucketServerHandler) + client, cleanUp := createServerAndClient(t, vcsutils.BitbucketServer, true, mockResponse, + "/api/1.0/admin/users?limit=1", createBitbucketServerHandler) defer cleanUp() err := client.TestConnection(ctx) assert.NoError(t, err) } -func TestBitbucketConnectionWhenContextCancelled(t *testing.T) { +func TestBitbucketServer_ConnectionWhenContextCancelled(t *testing.T) { ctx := context.Background() ctxWithCancel, cancel := context.WithCancel(ctx) cancel() - client, closeServer := createWaitingServerAndClient(t, vcsutils.BitbucketServer, 0) - defer closeServer() + client, cleanUp := createWaitingServerAndClient(t, vcsutils.BitbucketServer, 0) + defer cleanUp() err := client.TestConnection(ctxWithCancel) assert.ErrorIs(t, err, context.Canceled) } -func TestBitbucketConnectionWhenContextTimesOut(t *testing.T) { +func TestBitbucketServer_ConnectionWhenContextTimesOut(t *testing.T) { ctx := context.Background() ctxWithTimeout, cancel := context.WithTimeout(ctx, 10*time.Millisecond) defer cancel() - client, closeServer := createWaitingServerAndClient(t, vcsutils.BitbucketServer, 50*time.Millisecond) - defer closeServer() + client, cleanUp := createWaitingServerAndClient(t, vcsutils.BitbucketServer, 50*time.Millisecond) + defer cleanUp() err := client.TestConnection(ctxWithTimeout) assert.ErrorIs(t, err, context.DeadlineExceeded) } -func TestBitbucketServerListRepositories(t *testing.T) { +func TestBitbucketServer_ListRepositories(t *testing.T) { ctx := context.Background() client, cleanUp := createServerAndClient(t, vcsutils.BitbucketServer, false, nil, "", createBitbucketServerListRepositoriesHandler) defer cleanUp() @@ -61,7 +62,7 @@ func TestBitbucketServerListRepositories(t *testing.T) { assert.Equal(t, map[string][]string{"~" + username: {repo1}, username: {repo2}}, actualRepositories) } -func TestBitbucketServerListBranches(t *testing.T) { +func TestBitbucketServer_ListBranches(t *testing.T) { ctx := context.Background() mockResponse := map[string][]bitbucketv1.Branch{ "values": {{ID: branch1}, {ID: branch2}}, @@ -74,7 +75,7 @@ func TestBitbucketServerListBranches(t *testing.T) { assert.ElementsMatch(t, actualRepositories, []string{branch1, branch2}) } -func TestBitbucketServerCreateWebhook(t *testing.T) { +func TestBitbucketServer_CreateWebhook(t *testing.T) { ctx := context.Background() id := rand.Int31() mockResponse := bitbucketv1.Webhook{ID: int(id)} @@ -88,7 +89,7 @@ func TestBitbucketServerCreateWebhook(t *testing.T) { assert.Equal(t, strconv.Itoa(int(id)), actualId) } -func TestBitbucketServerUpdateWebhook(t *testing.T) { +func TestBitbucketServer_UpdateWebhook(t *testing.T) { ctx := context.Background() id := rand.Int31() stringId := strconv.Itoa(int(id)) @@ -101,7 +102,7 @@ func TestBitbucketServerUpdateWebhook(t *testing.T) { assert.NoError(t, err) } -func TestBitbucketServerDeleteWebhook(t *testing.T) { +func TestBitbucketServer_DeleteWebhook(t *testing.T) { ctx := context.Background() id := rand.Int31() stringId := strconv.Itoa(int(id)) @@ -113,7 +114,7 @@ func TestBitbucketServerDeleteWebhook(t *testing.T) { assert.NoError(t, err) } -func TestBitbucketServerSetCommitStatus(t *testing.T) { +func TestBitbucketServer_SetCommitStatus(t *testing.T) { ctx := context.Background() ref := "9caf1c431fb783b669f0f909bd018b40f2ea3808" client, cleanUp := createServerAndClient(t, vcsutils.BitbucketServer, false, nil, fmt.Sprintf("/build-status/1.0/commits/%s", ref), createBitbucketServerHandler) @@ -124,7 +125,7 @@ func TestBitbucketServerSetCommitStatus(t *testing.T) { assert.NoError(t, err) } -func TestBitbucketServerDownloadRepository(t *testing.T) { +func TestBitbucketServer_DownloadRepository(t *testing.T) { ctx := context.Background() dir, err := ioutil.TempDir("", "") assert.NoError(t, err) @@ -140,7 +141,7 @@ func TestBitbucketServerDownloadRepository(t *testing.T) { assert.NoError(t, err) } -func TestBitbucketServerCreatePullRequest(t *testing.T) { +func TestBitbucketServer_CreatePullRequest(t *testing.T) { ctx := context.Background() client, cleanUp := createServerAndClient(t, vcsutils.BitbucketServer, true, nil, "/api/1.0/projects/jfrog/repos/repo-1/pull-requests", createBitbucketServerHandler) defer cleanUp() @@ -149,9 +150,104 @@ func TestBitbucketServerCreatePullRequest(t *testing.T) { assert.NoError(t, err) } -func createBitbucketServerHandler(t *testing.T, expectedUri string, response []byte) http.HandlerFunc { +func TestBitbucketServer_GetLatestCommit(t *testing.T) { + ctx := context.Background() + response, err := os.ReadFile(filepath.Join("testdata", "bitbucketserver", "commit_list_response.json")) + assert.NoError(t, err) + + // limit=1 appears twice because it is added twice by: github.com/gfleury/go-bitbucket-v1@v0.0.0-20210826163055-dff2223adeac/default_api.go:3848 + client, cleanUp := createServerAndClient(t, vcsutils.BitbucketServer, false, response, + fmt.Sprintf("/api/1.0/projects/%s/repos/%s/commits?limit=1&limit=1&until=master", owner, repo1), + createBitbucketServerHandler) + defer cleanUp() + + result, err := client.GetLatestCommit(ctx, owner, repo1, "master") + + require.NoError(t, err) + assert.Equal(t, CommitInfo{ + Hash: "def0123abcdef4567abcdef8987abcdef6543abc", + AuthorName: "charlie", + CommitterName: "mark", + Url: "", + Timestamp: 1548720847610, + Message: "More work on feature 1", + ParentHashes: []string{"abcdef0123abcdef4567abcdef8987abcdef6543", "qwerty0123abcdef4567abcdef8987abcdef6543"}, + }, result) +} + +func TestBitbucketServer_GetLatestCommitNotFound(t *testing.T) { + ctx := context.Background() + response := []byte(`{ + "errors": [ + { + "context": null, + "exceptionName": "com.atlassian.bitbucket.project.NoSuchProjectException", + "message": "Project unknown does not exist." + } + ] + }`) + client, cleanUp := createServerAndClientReturningStatus(t, vcsutils.BitbucketServer, false, response, + fmt.Sprintf("/api/1.0/projects/%s/repos/%s/commits?limit=1&limit=1&until=master", owner, repo1), + http.StatusNotFound, createBitbucketServerHandler) + defer cleanUp() + + result, err := client.GetLatestCommit(ctx, owner, repo1, "master") + + require.Error(t, err) + assert.Contains(t, err.Error(), "Status: 404 Not Found") + assert.Empty(t, result) +} + +func TestBitbucketServer_GetLatestCommitInvalidPayload(t *testing.T) { + tests := []struct { + name string + owner string + repo string + branch string + }{ + { + name: "all empty", + owner: "", + repo: "", + branch: "", + }, + { + name: "empty owner", + owner: "", + repo: "repo", + branch: "branch", + }, + { + name: "empty repo", + owner: "owner", + repo: "", + branch: "branch", + }, + { + name: "empty branch", + owner: "owner", + repo: "repo", + branch: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + client, err := NewClientBuilder(vcsutils.BitbucketServer).Build() + require.NoError(t, err) + + result, err := client.GetLatestCommit(ctx, tt.owner, tt.repo, tt.branch) + + require.EqualError(t, err, "required parameter is empty") + assert.Empty(t, result) + }) + } +} + +func createBitbucketServerHandler(t *testing.T, expectedUri string, response []byte, expectedStatusCode int) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) + w.WriteHeader(expectedStatusCode) _, err := w.Write(response) require.NoError(t, err) assert.Equal(t, expectedUri, r.RequestURI) @@ -159,7 +255,7 @@ func createBitbucketServerHandler(t *testing.T, expectedUri string, response []b } } -func createBitbucketServerListRepositoriesHandler(t *testing.T, _ string, _ []byte) http.HandlerFunc { +func createBitbucketServerListRepositoriesHandler(t *testing.T, _ string, _ []byte, expectedStatusCode int) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var responseObj interface{} if r.RequestURI == "/api/1.0/projects?start=0" { @@ -173,7 +269,7 @@ func createBitbucketServerListRepositoriesHandler(t *testing.T, _ string, _ []by } else { assert.Fail(t, "Unexpected request Uri "+r.RequestURI) } - w.WriteHeader(http.StatusOK) + w.WriteHeader(expectedStatusCode) response, err := json.Marshal(responseObj) require.NoError(t, err) _, err = w.Write(response) diff --git a/vcsclient/common_test.go b/vcsclient/common_test.go index 7fb09e74..b2223498 100644 --- a/vcsclient/common_test.go +++ b/vcsclient/common_test.go @@ -25,10 +25,15 @@ var ( branch2 = "branch-2" ) -type createHandlerFunc func(t *testing.T, expectedUri string, response []byte) http.HandlerFunc +type createHandlerFunc func(t *testing.T, expectedUri string, response []byte, expectedStatusCode int) http.HandlerFunc func createServerAndClient(t *testing.T, vcsProvider vcsutils.VcsProvider, basicAuth bool, response interface{}, expectedUri string, createHandlerFunc createHandlerFunc) (VcsClient, func()) { + return createServerAndClientReturningStatus(t, vcsProvider, basicAuth, response, expectedUri, http.StatusOK, createHandlerFunc) +} + +func createServerAndClientReturningStatus(t *testing.T, vcsProvider vcsutils.VcsProvider, basicAuth bool, response interface{}, + expectedUri string, expectedStatusCode int, createHandlerFunc createHandlerFunc) (VcsClient, func()) { var byteResponse []byte var ok bool if byteResponse, ok = response.([]byte); !ok { @@ -37,18 +42,22 @@ func createServerAndClient(t *testing.T, vcsProvider vcsutils.VcsProvider, basic byteResponse, err = json.Marshal(response) assert.NoError(t, err) } - server := httptest.NewServer(createHandlerFunc(t, expectedUri, byteResponse)) + server := httptest.NewServer(createHandlerFunc(t, expectedUri, byteResponse, expectedStatusCode)) + client := buildClient(t, vcsProvider, basicAuth, server) + return client, server.Close +} + +func buildClient(t *testing.T, vcsProvider vcsutils.VcsProvider, basicAuth bool, server *httptest.Server) VcsClient { clientBuilder := NewClientBuilder(vcsProvider).ApiEndpoint(server.URL).Token(token) if basicAuth { clientBuilder = clientBuilder.Username("frogger") } client, err := clientBuilder.Build() assert.NoError(t, err) - return client, server.Close + return client } -func createWaitingServerAndClient(t *testing.T, provider vcsutils.VcsProvider, - waitDuration time.Duration) (VcsClient, func()) { +func createWaitingServerAndClient(t *testing.T, provider vcsutils.VcsProvider, waitDuration time.Duration) (VcsClient, func()) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if waitDuration > 0 { time.Sleep(waitDuration) diff --git a/vcsclient/factory.go b/vcsclient/factory.go index abf14dc2..3f41cf17 100644 --- a/vcsclient/factory.go +++ b/vcsclient/factory.go @@ -39,13 +39,13 @@ func (builder *ClientBuilder) Logger(logger *log.Logger) *ClientBuilder { func (builder *ClientBuilder) Build() (VcsClient, error) { switch builder.vcsProvider { case vcsutils.GitHub: - return NewGitHubClient(&builder.vcsInfo) + return NewGitHubClient(builder.vcsInfo) case vcsutils.GitLab: - return NewGitLabClient(&builder.vcsInfo) + return NewGitLabClient(builder.vcsInfo) case vcsutils.BitbucketServer: - return NewBitbucketServerClient(&builder.vcsInfo) + return NewBitbucketServerClient(builder.vcsInfo) case vcsutils.BitbucketCloud: - return NewBitbucketCloudClient(&builder.vcsInfo) + return NewBitbucketCloudClient(builder.vcsInfo) } return nil, nil } diff --git a/vcsclient/github.go b/vcsclient/github.go index cdf5c6ec..5067aa60 100644 --- a/vcsclient/github.go +++ b/vcsclient/github.go @@ -13,10 +13,10 @@ import ( ) type GitHubClient struct { - vcsInfo *VcsInfo + vcsInfo VcsInfo } -func NewGitHubClient(vcsInfo *VcsInfo) (*GitHubClient, error) { +func NewGitHubClient(vcsInfo VcsInfo) (*GitHubClient, error) { return &GitHubClient{vcsInfo: vcsInfo}, nil } @@ -182,6 +182,47 @@ func (client *GitHubClient) CreatePullRequest(ctx context.Context, owner, reposi return err } +func (client *GitHubClient) GetLatestCommit(ctx context.Context, owner, repository, branch string) (CommitInfo, error) { + err := validateParametersNotBlank(owner, repository, branch) + if err != nil { + return CommitInfo{}, err + } + + ghClient, err := client.buildGithubClient(ctx) + if err != nil { + return CommitInfo{}, err + } + listOptions := &github.CommitsListOptions{ + SHA: branch, + ListOptions: github.ListOptions{ + Page: 1, + PerPage: 1, + }, + } + commits, _, err := ghClient.Repositories.ListCommits(ctx, owner, repository, listOptions) + if err != nil { + return CommitInfo{}, err + } + if len(commits) > 0 { + latestCommit := commits[0] + parents := make([]string, len(latestCommit.Parents)) + for i, c := range latestCommit.Parents { + parents[i] = c.GetSHA() + } + details := latestCommit.GetCommit() + return CommitInfo{ + Hash: latestCommit.GetSHA(), + AuthorName: details.GetAuthor().GetName(), + CommitterName: details.GetCommitter().GetName(), + Url: latestCommit.GetURL(), + Timestamp: details.GetCommitter().GetDate().UTC().Unix(), + Message: details.GetMessage(), + ParentHashes: parents, + }, nil + } + return CommitInfo{}, nil +} + func createGitHubHook(token, payloadUrl string, webhookEvents ...vcsutils.WebhookEvent) *github.Hook { return &github.Hook{ Events: getGitHubWebhookEvents(webhookEvents...), diff --git a/vcsclient/github_test.go b/vcsclient/github_test.go index eb9f015c..9fc5a7b1 100644 --- a/vcsclient/github_test.go +++ b/vcsclient/github_test.go @@ -8,6 +8,7 @@ import ( "math/rand" "net/http" "os" + "path/filepath" "strconv" "strings" "testing" @@ -18,7 +19,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestGitHubConnection(t *testing.T) { +func TestGitHubClient_Connection(t *testing.T) { ctx := context.Background() client, cleanUp := createServerAndClient(t, vcsutils.GitHub, false, "It's Not Easy Being Green", "/zen", createGitHubHandler) defer cleanUp() @@ -27,29 +28,29 @@ func TestGitHubConnection(t *testing.T) { assert.NoError(t, err) } -func TestGithubConnectionWhenContextCancelled(t *testing.T) { +func TestGitHubClient_ConnectionWhenContextCancelled(t *testing.T) { ctx := context.Background() ctxWithCancel, cancel := context.WithCancel(ctx) cancel() - client, closeServer := createWaitingServerAndClient(t, vcsutils.GitHub, 0) - defer closeServer() + client, cleanUp := createWaitingServerAndClient(t, vcsutils.GitHub, 0) + defer cleanUp() err := client.TestConnection(ctxWithCancel) assert.ErrorIs(t, err, context.Canceled) } -func TestGithubConnectionWhenContextTimesOut(t *testing.T) { +func TestGitHubClient_ConnectionWhenContextTimesOut(t *testing.T) { ctx := context.Background() ctxWithTimeout, cancel := context.WithTimeout(ctx, 10*time.Millisecond) defer cancel() - client, closeServer := createWaitingServerAndClient(t, vcsutils.GitHub, 50*time.Millisecond) - defer closeServer() + client, cleanUp := createWaitingServerAndClient(t, vcsutils.GitHub, 50*time.Millisecond) + defer cleanUp() err := client.TestConnection(ctxWithTimeout) assert.ErrorIs(t, err, context.DeadlineExceeded) } -func TestGitHubListRepositories(t *testing.T) { +func TestGitHubClient_ListRepositories(t *testing.T) { ctx := context.Background() expectedRepo1 := github.Repository{Name: &repo1, Owner: &github.User{Login: &username}} expectedRepo2 := github.Repository{Name: &repo2, Owner: &github.User{Login: &username}} @@ -61,7 +62,7 @@ func TestGitHubListRepositories(t *testing.T) { assert.Equal(t, actualRepositories, map[string][]string{username: {repo1, repo2}}) } -func TestGitHubListBranches(t *testing.T) { +func TestGitHubClient_ListBranches(t *testing.T) { ctx := context.Background() client, cleanUp := createServerAndClient(t, vcsutils.GitHub, false, []github.Branch{{Name: &branch1}, {Name: &branch2}}, fmt.Sprintf("/repos/jfrog/%s/branches", repo1), createGitHubHandler) defer cleanUp() @@ -71,7 +72,7 @@ func TestGitHubListBranches(t *testing.T) { assert.ElementsMatch(t, actualBranches, []string{branch1, branch2}) } -func TestGitHubCreateWebhook(t *testing.T) { +func TestGitHubClient_CreateWebhook(t *testing.T) { ctx := context.Background() id := rand.Int63() client, cleanUp := createServerAndClient(t, vcsutils.GitHub, false, github.Hook{ID: &id}, fmt.Sprintf("/repos/jfrog/%s/hooks", repo1), createGitHubHandler) @@ -83,7 +84,7 @@ func TestGitHubCreateWebhook(t *testing.T) { assert.Equal(t, actualId, strconv.FormatInt(id, 10)) } -func TestGitHubUpdateWebhook(t *testing.T) { +func TestGitHubClient_UpdateWebhook(t *testing.T) { ctx := context.Background() id := rand.Int63() client, cleanUp := createServerAndClient(t, vcsutils.GitHub, false, github.Hook{ID: &id}, fmt.Sprintf("/repos/jfrog/%s/hooks/%s", repo1, strconv.FormatInt(id, 10)), createGitHubHandler) @@ -94,7 +95,7 @@ func TestGitHubUpdateWebhook(t *testing.T) { assert.NoError(t, err) } -func TestGitHubDeleteWebhook(t *testing.T) { +func TestGitHubClient_DeleteWebhook(t *testing.T) { ctx := context.Background() id := rand.Int63() client, cleanUp := createServerAndClient(t, vcsutils.GitHub, false, github.Hook{ID: &id}, fmt.Sprintf("/repos/jfrog/%s/hooks/%s", repo1, strconv.FormatInt(id, 10)), createGitHubHandler) @@ -104,7 +105,7 @@ func TestGitHubDeleteWebhook(t *testing.T) { assert.NoError(t, err) } -func TestGitHubCreateCommitStatus(t *testing.T) { +func TestGitHubClient_CreateCommitStatus(t *testing.T) { ctx := context.Background() ref := "39e5418" client, cleanUp := createServerAndClient(t, vcsutils.GitHub, false, github.RepoStatus{}, fmt.Sprintf("/repos/jfrog/%s/statuses/%s", repo1, ref), createGitHubHandler) @@ -115,13 +116,15 @@ func TestGitHubCreateCommitStatus(t *testing.T) { assert.NoError(t, err) } -func TestGitHubDownloadRepository(t *testing.T) { +func TestGitHubClient_DownloadRepository(t *testing.T) { ctx := context.Background() dir, err := ioutil.TempDir("", "") assert.NoError(t, err) defer func() { _ = os.RemoveAll(dir) }() - client, cleanUp := createServerAndClient(t, vcsutils.GitHub, false, []byte("https://github.com/octocat/Hello-World/archive/refs/heads/master.tar.gz"), "/repos/jfrog/Hello-World/tarball/test", createGitHubHandler) + client, cleanUp := createServerAndClientReturningStatus(t, vcsutils.GitHub, false, + []byte("https://github.com/octocat/Hello-World/archive/refs/heads/master.tar.gz"), + "/repos/jfrog/Hello-World/tarball/test", http.StatusFound, createGitHubHandler) defer cleanUp() assert.NoError(t, err) @@ -133,7 +136,7 @@ func TestGitHubDownloadRepository(t *testing.T) { assert.Equal(t, "README", fileinfo[0].Name()) } -func TestGitHubCreatePullRequest(t *testing.T) { +func TestGitHubClient_CreatePullRequest(t *testing.T) { ctx := context.Background() client, cleanUp := createServerAndClient(t, vcsutils.GitHub, false, github.PullRequest{}, "/repos/jfrog/repo-1/pulls", createGitHubHandler) defer cleanUp() @@ -142,18 +145,104 @@ func TestGitHubCreatePullRequest(t *testing.T) { assert.NoError(t, err) } -func createGitHubHandler(t *testing.T, expectedUri string, response []byte) http.HandlerFunc { +func TestGitHubClient_GetLatestCommit(t *testing.T) { + ctx := context.Background() + response, err := os.ReadFile(filepath.Join("testdata", "github", "commit_list_response.json")) + assert.NoError(t, err) + + client, cleanUp := createServerAndClient(t, vcsutils.GitHub, false, response, + fmt.Sprintf("/repos/%s/%s/commits?page=1&per_page=1&sha=master", owner, repo1), createGitHubHandler) + defer cleanUp() + + result, err := client.GetLatestCommit(ctx, owner, repo1, "master") + + require.NoError(t, err) + assert.Equal(t, CommitInfo{ + Hash: "6dcb09b5b57875f334f61aebed695e2e4193db5e", + AuthorName: "Monalisa Octocat", + CommitterName: "Joconde Octocat", + Url: "https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e", + Timestamp: 1302796850, + Message: "Fix all the bugs", + ParentHashes: []string{"6dcb09b5b57875f334f61aebed695e2e4193db5e"}, + }, result) +} + +func TestGitHubClient_GetLatestCommitNotFound(t *testing.T) { + ctx := context.Background() + response := []byte(`{ + "documentation_url": "https://docs.github.com/rest/reference/repos#list-commits", + "message": "Not Found" + }`) + + client, cleanUp := createServerAndClientReturningStatus(t, vcsutils.GitHub, false, response, + fmt.Sprintf("/repos/%s/%s/commits?page=1&per_page=1&sha=master", owner, repo1), http.StatusNotFound, createGitHubHandler) + defer cleanUp() + + result, err := client.GetLatestCommit(ctx, owner, repo1, "master") + require.Error(t, err) + assert.Contains(t, err.Error(), "404 Not Found") + assert.Empty(t, result) +} + +func TestGitHubClient_GetLatestCommitInvalidPayload(t *testing.T) { + tests := []struct { + name string + owner string + repo string + branch string + }{ + { + name: "all empty", + owner: "", + repo: "", + branch: "", + }, + { + name: "empty owner", + owner: "", + repo: "repo", + branch: "branch", + }, + { + name: "empty repo", + owner: "owner", + repo: "", + branch: "branch", + }, + { + name: "empty branch", + owner: "owner", + repo: "repo", + branch: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + client, err := NewClientBuilder(vcsutils.GitHub).Build() + require.NoError(t, err) + + result, err := client.GetLatestCommit(ctx, tt.owner, tt.repo, tt.branch) + + require.EqualError(t, err, "required parameter is empty") + assert.Empty(t, result) + }) + } +} + +func createGitHubHandler(t *testing.T, expectedUri string, response []byte, expectedStatusCode int) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, expectedUri, r.RequestURI) + assert.Equal(t, "Bearer "+token, r.Header.Get("Authorization")) if strings.Contains(r.RequestURI, "tarball") { w.Header().Add("Location", string(response)) - w.WriteHeader(http.StatusFound) + w.WriteHeader(expectedStatusCode) return - } else { - w.WriteHeader(http.StatusOK) } + w.WriteHeader(expectedStatusCode) _, err := w.Write(response) require.NoError(t, err) - assert.Equal(t, expectedUri, r.RequestURI) - assert.Equal(t, "Bearer "+token, r.Header.Get("Authorization")) } } diff --git a/vcsclient/gitlab.go b/vcsclient/gitlab.go index a49c01f8..2b3bcbdc 100644 --- a/vcsclient/gitlab.go +++ b/vcsclient/gitlab.go @@ -14,7 +14,7 @@ type GitLabClient struct { glClient *gitlab.Client } -func NewGitLabClient(vcsInfo *VcsInfo) (*GitLabClient, error) { +func NewGitLabClient(vcsInfo VcsInfo) (*GitLabClient, error) { var client *gitlab.Client var err error if vcsInfo.ApiEndpoint != "" { @@ -166,6 +166,39 @@ func (client *GitLabClient) CreatePullRequest(ctx context.Context, owner, reposi return err } +func (client *GitLabClient) GetLatestCommit(ctx context.Context, owner, repository, branch string) (CommitInfo, error) { + err := validateParametersNotBlank(owner, repository, branch) + if err != nil { + return CommitInfo{}, err + } + + listOptions := &gitlab.ListCommitsOptions{ + RefName: &branch, + ListOptions: gitlab.ListOptions{ + Page: 1, + PerPage: 1, + }, + } + + commits, _, err := client.glClient.Commits.ListCommits(getProjectId(owner, repository), listOptions, gitlab.WithContext(ctx)) + if err != nil { + return CommitInfo{}, err + } + if len(commits) > 0 { + latestCommit := commits[0] + return CommitInfo{ + Hash: latestCommit.ID, + AuthorName: latestCommit.AuthorName, + CommitterName: latestCommit.CommitterName, + Url: latestCommit.WebURL, + Timestamp: latestCommit.CommittedDate.UTC().Unix(), + Message: latestCommit.Message, + ParentHashes: latestCommit.ParentIDs, + }, nil + } + return CommitInfo{}, nil +} + func getProjectId(owner, project string) string { return fmt.Sprintf("%s/%s", owner, project) } diff --git a/vcsclient/gitlab_test.go b/vcsclient/gitlab_test.go index ae36904d..3417cd16 100644 --- a/vcsclient/gitlab_test.go +++ b/vcsclient/gitlab_test.go @@ -20,7 +20,7 @@ import ( "github.com/xanzy/go-gitlab" ) -func TestGitLabConnection(t *testing.T) { +func TestGitLabClient_Connection(t *testing.T) { ctx := context.Background() client, cleanUp := createServerAndClient(t, vcsutils.GitLab, false, []gitlab.Project{}, "/api/v4/projects", createGitLabHandler) defer cleanUp() @@ -29,30 +29,30 @@ func TestGitLabConnection(t *testing.T) { assert.NoError(t, err) } -func TestGitlabConnectionWhenContextCancelled(t *testing.T) { +func TestGitLabClient_ConnectionWhenContextCancelled(t *testing.T) { ctx := context.Background() ctxWithCancel, cancel := context.WithCancel(ctx) cancel() - client, closeServer := createWaitingServerAndClient(t, vcsutils.GitLab, 0) - defer closeServer() + client, cleanUp := createWaitingServerAndClient(t, vcsutils.GitLab, 0) + defer cleanUp() err := client.TestConnection(ctxWithCancel) assert.ErrorIs(t, err, context.Canceled) } -func TestGitlabConnectionWhenContextTimesOut(t *testing.T) { +func TestGitLabClient_ConnectionWhenContextTimesOut(t *testing.T) { ctx := context.Background() ctxWithTimeout, cancel := context.WithTimeout(ctx, 10*time.Millisecond) defer cancel() - client, closeServer := createWaitingServerAndClient(t, vcsutils.GitLab, 50*time.Millisecond) - defer closeServer() + client, cleanUp := createWaitingServerAndClient(t, vcsutils.GitLab, 50*time.Millisecond) + defer cleanUp() err := client.TestConnection(ctxWithTimeout) assert.ErrorIs(t, err, context.DeadlineExceeded) } -func TestGitLabListRepositories(t *testing.T) { +func TestGitLabClient_ListRepositories(t *testing.T) { ctx := context.Background() client, cleanUp := createServerAndClient(t, vcsutils.GitLab, false, []gitlab.Project{{Path: repo1}, {Path: repo2}}, "/api/v4/groups/frogger/projects?page=1", createGitLabHandler) defer cleanUp() @@ -62,7 +62,7 @@ func TestGitLabListRepositories(t *testing.T) { assert.Equal(t, actualRepositories, map[string][]string{username: {repo1, repo2}}) } -func TestGitLabListBranches(t *testing.T) { +func TestGitLabClient_ListBranches(t *testing.T) { ctx := context.Background() client, cleanUp := createServerAndClient(t, vcsutils.GitLab, false, []gitlab.Branch{{Name: branch1}, {Name: branch2}}, fmt.Sprintf("/api/v4/projects/%s/repository/branches", url.PathEscape(owner+"/"+repo1)), createGitLabHandler) defer cleanUp() @@ -72,7 +72,7 @@ func TestGitLabListBranches(t *testing.T) { assert.ElementsMatch(t, actualRepositories, []string{branch1, branch2}) } -func TestGitLabCreateWebhook(t *testing.T) { +func TestGitLabClient_CreateWebhook(t *testing.T) { ctx := context.Background() id := rand.Int() client, cleanUp := createServerAndClient(t, vcsutils.GitLab, false, gitlab.ProjectHook{ID: id}, fmt.Sprintf("/api/v4/projects/%s/hooks", url.PathEscape(owner+"/"+repo1)), createGitLabHandler) @@ -85,7 +85,7 @@ func TestGitLabCreateWebhook(t *testing.T) { assert.Equal(t, actualId, strconv.Itoa(id)) } -func TestGitLabUpdateWebhook(t *testing.T) { +func TestGitLabClient_UpdateWebhook(t *testing.T) { ctx := context.Background() id := rand.Int() client, cleanUp := createServerAndClient(t, vcsutils.GitLab, false, gitlab.ProjectHook{ID: id}, fmt.Sprintf("/api/v4/projects/%s/hooks/%d", url.PathEscape(owner+"/"+repo1), id), createGitLabHandler) @@ -96,7 +96,7 @@ func TestGitLabUpdateWebhook(t *testing.T) { assert.NoError(t, err) } -func TestGitLabDeleteWebhook(t *testing.T) { +func TestGitLabClient_DeleteWebhook(t *testing.T) { ctx := context.Background() id := rand.Int() client, cleanUp := createServerAndClient(t, vcsutils.GitLab, false, gitlab.ProjectHook{ID: id}, fmt.Sprintf("/api/v4/projects/%s/hooks/%d", url.PathEscape(owner+"/"+repo1), id), createGitLabHandler) @@ -106,7 +106,7 @@ func TestGitLabDeleteWebhook(t *testing.T) { assert.NoError(t, err) } -func TestGitLabCreateCommitStatus(t *testing.T) { +func TestGitLabClient_CreateCommitStatus(t *testing.T) { ctx := context.Background() ref := "5fbf81b31ff7a3b06bd362d1891e2f01bdb2be69" client, cleanUp := createServerAndClient(t, vcsutils.GitLab, false, gitlab.CommitStatus{}, fmt.Sprintf("/api/v4/projects/%s/statuses/%s", url.PathEscape(owner+"/"+repo1), ref), createGitLabHandler) @@ -117,7 +117,7 @@ func TestGitLabCreateCommitStatus(t *testing.T) { assert.NoError(t, err) } -func TestGitLabDownloadRepository(t *testing.T) { +func TestGitLabClient_DownloadRepository(t *testing.T) { ctx := context.Background() dir, err := ioutil.TempDir("", "") assert.NoError(t, err) @@ -138,7 +138,7 @@ func TestGitLabDownloadRepository(t *testing.T) { assert.Equal(t, "README.md", fileinfo[0].Name()) } -func TestGitLabCreatePullRequest(t *testing.T) { +func TestGitLabClient_CreatePullRequest(t *testing.T) { ctx := context.Background() client, cleanUp := createServerAndClient(t, vcsutils.GitLab, false, &gitlab.MergeRequest{}, fmt.Sprintf("/api/v4/projects/%s/merge_requests", url.PathEscape(owner+"/"+repo1)), createGitLabHandler) defer cleanUp() @@ -147,19 +147,110 @@ func TestGitLabCreatePullRequest(t *testing.T) { assert.NoError(t, err) } -func createGitLabHandler(t *testing.T, expectedUri string, response []byte) http.HandlerFunc { +func TestGitLabClient_GetLatestCommit(t *testing.T) { + ctx := context.Background() + response, err := os.ReadFile(filepath.Join("testdata", "gitlab", "commit_list_response.json")) + assert.NoError(t, err) + + client, cleanUp := createServerAndClient(t, vcsutils.GitLab, false, response, + fmt.Sprintf("/api/v4/projects/%s/repository/commits?page=1&per_page=1&ref_name=master", + url.PathEscape(owner+"/"+repo1)), createGitLabHandler) + defer cleanUp() + + result, err := client.GetLatestCommit(ctx, owner, repo1, "master") + + require.NoError(t, err) + assert.Equal(t, CommitInfo{ + Hash: "ed899a2f4b50b4370feeea94676502b42383c746", + AuthorName: "Example User", + CommitterName: "Administrator", + Url: "https://gitlab.example.com/thedude/gitlab-foss/-/commit/ed899a2f4b50b4370feeea94676502b42383c746", + Timestamp: 1348131022, + Message: "Replace sanitize with escape once", + ParentHashes: []string{"6104942438c14ec7bd21c6cd5bd995272b3faff6"}, + }, result) +} + +func TestGitLabClient_GetLatestCommitNotFound(t *testing.T) { + ctx := context.Background() + response := []byte(`{ + "message": "404 Project Not Found" +}`) + + client, cleanUp := createServerAndClientReturningStatus(t, vcsutils.GitLab, false, response, + fmt.Sprintf("/api/v4/projects/%s/repository/commits?page=1&per_page=1&ref_name=master", + url.PathEscape(owner+"/"+repo1)), http.StatusNotFound, createGitLabHandler) + defer cleanUp() + + result, err := client.GetLatestCommit(ctx, owner, repo1, "master") + + require.Error(t, err) + assert.Contains(t, err.Error(), "404 Project Not Found") + assert.Empty(t, result) +} + +func TestGitLabClient_GetLatestCommitInvalidPayload(t *testing.T) { + tests := []struct { + name string + owner string + repo string + branch string + }{ + { + name: "all empty", + owner: "", + repo: "", + branch: "", + }, + { + name: "empty owner", + owner: "", + repo: "repo", + branch: "branch", + }, + { + name: "empty repo", + owner: "owner", + repo: "", + branch: "branch", + }, + { + name: "empty branch", + owner: "owner", + repo: "repo", + branch: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + client, err := NewClientBuilder(vcsutils.GitLab).Build() + require.NoError(t, err) + + result, err := client.GetLatestCommit(ctx, tt.owner, tt.repo, tt.branch) + + require.EqualError(t, err, "required parameter is empty") + assert.Empty(t, result) + }) + } +} + +func createGitLabHandler(t *testing.T, expectedUri string, response []byte, expectedStatusCode int) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) if r.RequestURI == "/api/v4/" { + w.WriteHeader(http.StatusOK) return } if r.RequestURI == "/api/v4/groups" { byteResponse, err := json.Marshal(&[]gitlab.Group{{Path: username}}) assert.NoError(t, err) + w.WriteHeader(http.StatusOK) _, err = w.Write(byteResponse) assert.NoError(t, err) return } + w.WriteHeader(expectedStatusCode) _, err := w.Write(response) assert.NoError(t, err) assert.Equal(t, expectedUri, r.RequestURI) diff --git a/vcsclient/testdata/bitbucketcloud/commit_list_response.json b/vcsclient/testdata/bitbucketcloud/commit_list_response.json new file mode 100644 index 00000000..fe267273 --- /dev/null +++ b/vcsclient/testdata/bitbucketcloud/commit_list_response.json @@ -0,0 +1,1252 @@ +{ + "pagelen": 30, + "values": [ + { + "author": { + "raw": "user ", + "type": "author", + "user": { + "account_id": "5d74edb2098f0b0daa7630de", + "display_name": "user", + "links": { + "avatar": { + "href": "https://secure.gravatar.com/avatar/477cdb6499e58f28541b52307a3c5bbc?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FRN-1.png" + }, + "html": { + "href": "https://bitbucket.org/%7B1658dec0-6727-44ba-b680-15f84e664746%7D/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B1658dec0-6727-44ba-b680-15f84e664746%7D" + } + }, + "nickname": "user", + "type": "user", + "uuid": "{1658dec0-6727-44ba-b680-15f84e664746}" + } + }, + "date": "2020-06-01T19:47:03+00:00", + "hash": "ec05bacb91d757b4b6b2a11a0676471020e89fb5", + "links": { + "approve": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/ec05bacb91d757b4b6b2a11a0676471020e89fb5/approve" + }, + "comments": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/ec05bacb91d757b4b6b2a11a0676471020e89fb5/comments" + }, + "diff": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/diff/ec05bacb91d757b4b6b2a11a0676471020e89fb5" + }, + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli/commits/ec05bacb91d757b4b6b2a11a0676471020e89fb5" + }, + "patch": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/patch/ec05bacb91d757b4b6b2a11a0676471020e89fb5" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/ec05bacb91d757b4b6b2a11a0676471020e89fb5" + }, + "statuses": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/ec05bacb91d757b4b6b2a11a0676471020e89fb5/statuses" + } + }, + "message": "Fix README.md: yaml\n", + "parents": [ + { + "hash": "774aa0fb252bccbc2a7e01060ef4d4be0b0eeaa9", + "links": { + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli/commits/774aa0fb252bccbc2a7e01060ef4d4be0b0eeaa9" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/774aa0fb252bccbc2a7e01060ef4d4be0b0eeaa9" + } + }, + "type": "commit" + }, + { + "hash": "def26c6128ebe11fac555fe58b59227e9655dc4d", + "links": { + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli/commits/def26c6128ebe11fac555fe58b59227e9655dc4d" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/def26c6128ebe11fac555fe58b59227e9655dc4d" + } + }, + "type": "commit" + } + ], + "rendered": { + "message": { + "html": "

Fix README.md: yaml

", + "markup": "markdown", + "raw": "Fix README.md: yaml\n", + "type": "rendered" + } + }, + "repository": { + "full_name": "user2/setup-jfrog-cli", + "links": { + "avatar": { + "href": "https://bytebucket.org/ravatar/%7Bd5f8a3f0-0cf1-43e9-8451-5379417d6c7c%7D?ts=default" + }, + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli" + } + }, + "name": "setup-jfrog-cli", + "type": "repository", + "uuid": "{d5f8a3f0-0cf1-43e9-8451-5379417d6c7c}" + }, + "summary": { + "html": "

Fix README.md: yaml

", + "markup": "markdown", + "raw": "Fix README.md: yaml\n", + "type": "rendered" + }, + "type": "commit" + }, + { + "author": { + "raw": "user ", + "type": "author", + "user": { + "account_id": "5d74edb2098f0b0daa7630de", + "display_name": "user", + "links": { + "avatar": { + "href": "https://secure.gravatar.com/avatar/477cdb6499e58f28541b52307a3c5bbc?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FRN-1.png" + }, + "html": { + "href": "https://bitbucket.org/%7B1658dec0-6727-44ba-b680-15f84e664746%7D/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B1658dec0-6727-44ba-b680-15f84e664746%7D" + } + }, + "nickname": "user", + "type": "user", + "uuid": "{1658dec0-6727-44ba-b680-15f84e664746}" + } + }, + "date": "2020-06-01T19:42:44+00:00", + "hash": "774aa0fb252bccbc2a7e01060ef4d4be0b0eeaa9", + "links": { + "approve": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/774aa0fb252bccbc2a7e01060ef4d4be0b0eeaa9/approve" + }, + "comments": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/774aa0fb252bccbc2a7e01060ef4d4be0b0eeaa9/comments" + }, + "diff": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/diff/774aa0fb252bccbc2a7e01060ef4d4be0b0eeaa9" + }, + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli/commits/774aa0fb252bccbc2a7e01060ef4d4be0b0eeaa9" + }, + "patch": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/patch/774aa0fb252bccbc2a7e01060ef4d4be0b0eeaa9" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/774aa0fb252bccbc2a7e01060ef4d4be0b0eeaa9" + }, + "statuses": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/774aa0fb252bccbc2a7e01060ef4d4be0b0eeaa9/statuses" + } + }, + "message": "Fix new lines in README\n", + "parents": [ + { + "hash": "1807e7d3f7a8f9a7cd3925d321a009f81da0d415", + "links": { + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli/commits/1807e7d3f7a8f9a7cd3925d321a009f81da0d415" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/1807e7d3f7a8f9a7cd3925d321a009f81da0d415" + } + }, + "type": "commit" + } + ], + "rendered": { + "message": { + "html": "

Fix new lines in README

", + "markup": "markdown", + "raw": "Fix new lines in README\n", + "type": "rendered" + } + }, + "repository": { + "full_name": "user2/setup-jfrog-cli", + "links": { + "avatar": { + "href": "https://bytebucket.org/ravatar/%7Bd5f8a3f0-0cf1-43e9-8451-5379417d6c7c%7D?ts=default" + }, + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli" + } + }, + "name": "setup-jfrog-cli", + "type": "repository", + "uuid": "{d5f8a3f0-0cf1-43e9-8451-5379417d6c7c}" + }, + "summary": { + "html": "

Fix new lines in README

", + "markup": "markdown", + "raw": "Fix new lines in README\n", + "type": "rendered" + }, + "type": "commit" + }, + { + "author": { + "raw": "user ", + "type": "author", + "user": { + "account_id": "5d74edb2098f0b0daa7630de", + "display_name": "user", + "links": { + "avatar": { + "href": "https://secure.gravatar.com/avatar/477cdb6499e58f28541b52307a3c5bbc?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FRN-1.png" + }, + "html": { + "href": "https://bitbucket.org/%7B1658dec0-6727-44ba-b680-15f84e664746%7D/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B1658dec0-6727-44ba-b680-15f84e664746%7D" + } + }, + "nickname": "user", + "type": "user", + "uuid": "{1658dec0-6727-44ba-b680-15f84e664746}" + } + }, + "date": "2020-06-01T19:32:48+00:00", + "hash": "1807e7d3f7a8f9a7cd3925d321a009f81da0d415", + "links": { + "approve": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/1807e7d3f7a8f9a7cd3925d321a009f81da0d415/approve" + }, + "comments": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/1807e7d3f7a8f9a7cd3925d321a009f81da0d415/comments" + }, + "diff": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/diff/1807e7d3f7a8f9a7cd3925d321a009f81da0d415" + }, + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli/commits/1807e7d3f7a8f9a7cd3925d321a009f81da0d415" + }, + "patch": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/patch/1807e7d3f7a8f9a7cd3925d321a009f81da0d415" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/1807e7d3f7a8f9a7cd3925d321a009f81da0d415" + }, + "statuses": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/1807e7d3f7a8f9a7cd3925d321a009f81da0d415/statuses" + } + }, + "message": "Update README, remove tests\n", + "parents": [ + { + "hash": "def26c6128ebe11fac555fe58b59227e9655dc4d", + "links": { + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli/commits/def26c6128ebe11fac555fe58b59227e9655dc4d" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/def26c6128ebe11fac555fe58b59227e9655dc4d" + } + }, + "type": "commit" + } + ], + "rendered": { + "message": { + "html": "

Update README, remove tests

", + "markup": "markdown", + "raw": "Update README, remove tests\n", + "type": "rendered" + } + }, + "repository": { + "full_name": "user2/setup-jfrog-cli", + "links": { + "avatar": { + "href": "https://bytebucket.org/ravatar/%7Bd5f8a3f0-0cf1-43e9-8451-5379417d6c7c%7D?ts=default" + }, + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli" + } + }, + "name": "setup-jfrog-cli", + "type": "repository", + "uuid": "{d5f8a3f0-0cf1-43e9-8451-5379417d6c7c}" + }, + "summary": { + "html": "

Update README, remove tests

", + "markup": "markdown", + "raw": "Update README, remove tests\n", + "type": "rendered" + }, + "type": "commit" + }, + { + "author": { + "raw": "user ", + "type": "author", + "user": { + "account_id": "5d74edb2098f0b0daa7630de", + "display_name": "user", + "links": { + "avatar": { + "href": "https://secure.gravatar.com/avatar/477cdb6499e58f28541b52307a3c5bbc?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FRN-1.png" + }, + "html": { + "href": "https://bitbucket.org/%7B1658dec0-6727-44ba-b680-15f84e664746%7D/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B1658dec0-6727-44ba-b680-15f84e664746%7D" + } + }, + "nickname": "user", + "type": "user", + "uuid": "{1658dec0-6727-44ba-b680-15f84e664746}" + } + }, + "date": "2020-06-01T18:46:43+00:00", + "hash": "def26c6128ebe11fac555fe58b59227e9655dc4d", + "links": { + "approve": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/def26c6128ebe11fac555fe58b59227e9655dc4d/approve" + }, + "comments": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/def26c6128ebe11fac555fe58b59227e9655dc4d/comments" + }, + "diff": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/diff/def26c6128ebe11fac555fe58b59227e9655dc4d" + }, + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli/commits/def26c6128ebe11fac555fe58b59227e9655dc4d" + }, + "patch": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/patch/def26c6128ebe11fac555fe58b59227e9655dc4d" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/def26c6128ebe11fac555fe58b59227e9655dc4d" + }, + "statuses": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/def26c6128ebe11fac555fe58b59227e9655dc4d/statuses" + } + }, + "message": "test\n", + "parents": [ + { + "hash": "f1a1201e352814d77a79af9d757dfccc89279f96", + "links": { + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli/commits/f1a1201e352814d77a79af9d757dfccc89279f96" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/f1a1201e352814d77a79af9d757dfccc89279f96" + } + }, + "type": "commit" + } + ], + "rendered": { + "message": { + "html": "

test

", + "markup": "markdown", + "raw": "test\n", + "type": "rendered" + } + }, + "repository": { + "full_name": "user2/setup-jfrog-cli", + "links": { + "avatar": { + "href": "https://bytebucket.org/ravatar/%7Bd5f8a3f0-0cf1-43e9-8451-5379417d6c7c%7D?ts=default" + }, + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli" + } + }, + "name": "setup-jfrog-cli", + "type": "repository", + "uuid": "{d5f8a3f0-0cf1-43e9-8451-5379417d6c7c}" + }, + "summary": { + "html": "

test

", + "markup": "markdown", + "raw": "test\n", + "type": "rendered" + }, + "type": "commit" + }, + { + "author": { + "raw": "user ", + "type": "author", + "user": { + "account_id": "5d74edb2098f0b0daa7630de", + "display_name": "user", + "links": { + "avatar": { + "href": "https://secure.gravatar.com/avatar/477cdb6499e58f28541b52307a3c5bbc?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FRN-1.png" + }, + "html": { + "href": "https://bitbucket.org/%7B1658dec0-6727-44ba-b680-15f84e664746%7D/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B1658dec0-6727-44ba-b680-15f84e664746%7D" + } + }, + "nickname": "user", + "type": "user", + "uuid": "{1658dec0-6727-44ba-b680-15f84e664746}" + } + }, + "date": "2020-06-01T18:43:28+00:00", + "hash": "f1a1201e352814d77a79af9d757dfccc89279f96", + "links": { + "approve": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/f1a1201e352814d77a79af9d757dfccc89279f96/approve" + }, + "comments": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/f1a1201e352814d77a79af9d757dfccc89279f96/comments" + }, + "diff": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/diff/f1a1201e352814d77a79af9d757dfccc89279f96" + }, + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli/commits/f1a1201e352814d77a79af9d757dfccc89279f96" + }, + "patch": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/patch/f1a1201e352814d77a79af9d757dfccc89279f96" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/f1a1201e352814d77a79af9d757dfccc89279f96" + }, + "statuses": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/f1a1201e352814d77a79af9d757dfccc89279f96/statuses" + } + }, + "message": "test\n", + "parents": [ + { + "hash": "140f60dd0dcf50d93d6d35bc38317c523486c15f", + "links": { + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli/commits/140f60dd0dcf50d93d6d35bc38317c523486c15f" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/140f60dd0dcf50d93d6d35bc38317c523486c15f" + } + }, + "type": "commit" + } + ], + "rendered": { + "message": { + "html": "

test

", + "markup": "markdown", + "raw": "test\n", + "type": "rendered" + } + }, + "repository": { + "full_name": "user2/setup-jfrog-cli", + "links": { + "avatar": { + "href": "https://bytebucket.org/ravatar/%7Bd5f8a3f0-0cf1-43e9-8451-5379417d6c7c%7D?ts=default" + }, + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli" + } + }, + "name": "setup-jfrog-cli", + "type": "repository", + "uuid": "{d5f8a3f0-0cf1-43e9-8451-5379417d6c7c}" + }, + "summary": { + "html": "

test

", + "markup": "markdown", + "raw": "test\n", + "type": "rendered" + }, + "type": "commit" + }, + { + "author": { + "raw": "user ", + "type": "author", + "user": { + "account_id": "5d74edb2098f0b0daa7630de", + "display_name": "user", + "links": { + "avatar": { + "href": "https://secure.gravatar.com/avatar/477cdb6499e58f28541b52307a3c5bbc?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FRN-1.png" + }, + "html": { + "href": "https://bitbucket.org/%7B1658dec0-6727-44ba-b680-15f84e664746%7D/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B1658dec0-6727-44ba-b680-15f84e664746%7D" + } + }, + "nickname": "user", + "type": "user", + "uuid": "{1658dec0-6727-44ba-b680-15f84e664746}" + } + }, + "date": "2020-06-01T18:41:48+00:00", + "hash": "140f60dd0dcf50d93d6d35bc38317c523486c15f", + "links": { + "approve": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/140f60dd0dcf50d93d6d35bc38317c523486c15f/approve" + }, + "comments": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/140f60dd0dcf50d93d6d35bc38317c523486c15f/comments" + }, + "diff": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/diff/140f60dd0dcf50d93d6d35bc38317c523486c15f" + }, + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli/commits/140f60dd0dcf50d93d6d35bc38317c523486c15f" + }, + "patch": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/patch/140f60dd0dcf50d93d6d35bc38317c523486c15f" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/140f60dd0dcf50d93d6d35bc38317c523486c15f" + }, + "statuses": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/140f60dd0dcf50d93d6d35bc38317c523486c15f/statuses" + } + }, + "message": "test\n", + "parents": [ + { + "hash": "f8618cbc9ef831d33867f8fd3de6b368060f49d2", + "links": { + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli/commits/f8618cbc9ef831d33867f8fd3de6b368060f49d2" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/f8618cbc9ef831d33867f8fd3de6b368060f49d2" + } + }, + "type": "commit" + } + ], + "rendered": { + "message": { + "html": "

test

", + "markup": "markdown", + "raw": "test\n", + "type": "rendered" + } + }, + "repository": { + "full_name": "user2/setup-jfrog-cli", + "links": { + "avatar": { + "href": "https://bytebucket.org/ravatar/%7Bd5f8a3f0-0cf1-43e9-8451-5379417d6c7c%7D?ts=default" + }, + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli" + } + }, + "name": "setup-jfrog-cli", + "type": "repository", + "uuid": "{d5f8a3f0-0cf1-43e9-8451-5379417d6c7c}" + }, + "summary": { + "html": "

test

", + "markup": "markdown", + "raw": "test\n", + "type": "rendered" + }, + "type": "commit" + }, + { + "author": { + "raw": "user ", + "type": "author", + "user": { + "account_id": "5d74edb2098f0b0daa7630de", + "display_name": "user", + "links": { + "avatar": { + "href": "https://secure.gravatar.com/avatar/477cdb6499e58f28541b52307a3c5bbc?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FRN-1.png" + }, + "html": { + "href": "https://bitbucket.org/%7B1658dec0-6727-44ba-b680-15f84e664746%7D/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B1658dec0-6727-44ba-b680-15f84e664746%7D" + } + }, + "nickname": "user", + "type": "user", + "uuid": "{1658dec0-6727-44ba-b680-15f84e664746}" + } + }, + "date": "2020-06-01T17:56:56+00:00", + "hash": "f8618cbc9ef831d33867f8fd3de6b368060f49d2", + "links": { + "approve": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/f8618cbc9ef831d33867f8fd3de6b368060f49d2/approve" + }, + "comments": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/f8618cbc9ef831d33867f8fd3de6b368060f49d2/comments" + }, + "diff": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/diff/f8618cbc9ef831d33867f8fd3de6b368060f49d2" + }, + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli/commits/f8618cbc9ef831d33867f8fd3de6b368060f49d2" + }, + "patch": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/patch/f8618cbc9ef831d33867f8fd3de6b368060f49d2" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/f8618cbc9ef831d33867f8fd3de6b368060f49d2" + }, + "statuses": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/f8618cbc9ef831d33867f8fd3de6b368060f49d2/statuses" + } + }, + "message": "JFrogDev to jfrog\n", + "parents": [ + { + "hash": "cd07380cb2debd41554568ae99b9e561d6eb3546", + "links": { + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli/commits/cd07380cb2debd41554568ae99b9e561d6eb3546" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/cd07380cb2debd41554568ae99b9e561d6eb3546" + } + }, + "type": "commit" + } + ], + "rendered": { + "message": { + "html": "

JFrogDev to jfrog

", + "markup": "markdown", + "raw": "JFrogDev to jfrog\n", + "type": "rendered" + } + }, + "repository": { + "full_name": "user2/setup-jfrog-cli", + "links": { + "avatar": { + "href": "https://bytebucket.org/ravatar/%7Bd5f8a3f0-0cf1-43e9-8451-5379417d6c7c%7D?ts=default" + }, + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli" + } + }, + "name": "setup-jfrog-cli", + "type": "repository", + "uuid": "{d5f8a3f0-0cf1-43e9-8451-5379417d6c7c}" + }, + "summary": { + "html": "

JFrogDev to jfrog

", + "markup": "markdown", + "raw": "JFrogDev to jfrog\n", + "type": "rendered" + }, + "type": "commit" + }, + { + "author": { + "raw": "user ", + "type": "author", + "user": { + "account_id": "5d74edb2098f0b0daa7630de", + "display_name": "user", + "links": { + "avatar": { + "href": "https://secure.gravatar.com/avatar/477cdb6499e58f28541b52307a3c5bbc?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FRN-1.png" + }, + "html": { + "href": "https://bitbucket.org/%7B1658dec0-6727-44ba-b680-15f84e664746%7D/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B1658dec0-6727-44ba-b680-15f84e664746%7D" + } + }, + "nickname": "user", + "type": "user", + "uuid": "{1658dec0-6727-44ba-b680-15f84e664746}" + } + }, + "date": "2020-06-01T17:34:37+00:00", + "hash": "cd07380cb2debd41554568ae99b9e561d6eb3546", + "links": { + "approve": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/cd07380cb2debd41554568ae99b9e561d6eb3546/approve" + }, + "comments": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/cd07380cb2debd41554568ae99b9e561d6eb3546/comments" + }, + "diff": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/diff/cd07380cb2debd41554568ae99b9e561d6eb3546" + }, + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli/commits/cd07380cb2debd41554568ae99b9e561d6eb3546" + }, + "patch": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/patch/cd07380cb2debd41554568ae99b9e561d6eb3546" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/cd07380cb2debd41554568ae99b9e561d6eb3546" + }, + "statuses": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/cd07380cb2debd41554568ae99b9e561d6eb3546/statuses" + } + }, + "message": "tags trigger\n", + "parents": [ + { + "hash": "566f2a7b82c0e4325e374b1fc2352891fc1b10e2", + "links": { + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli/commits/566f2a7b82c0e4325e374b1fc2352891fc1b10e2" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/566f2a7b82c0e4325e374b1fc2352891fc1b10e2" + } + }, + "type": "commit" + } + ], + "rendered": { + "message": { + "html": "

tags trigger

", + "markup": "markdown", + "raw": "tags trigger\n", + "type": "rendered" + } + }, + "repository": { + "full_name": "user2/setup-jfrog-cli", + "links": { + "avatar": { + "href": "https://bytebucket.org/ravatar/%7Bd5f8a3f0-0cf1-43e9-8451-5379417d6c7c%7D?ts=default" + }, + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli" + } + }, + "name": "setup-jfrog-cli", + "type": "repository", + "uuid": "{d5f8a3f0-0cf1-43e9-8451-5379417d6c7c}" + }, + "summary": { + "html": "

tags trigger

", + "markup": "markdown", + "raw": "tags trigger\n", + "type": "rendered" + }, + "type": "commit" + }, + { + "author": { + "raw": "user ", + "type": "author", + "user": { + "account_id": "5d74edb2098f0b0daa7630de", + "display_name": "user", + "links": { + "avatar": { + "href": "https://secure.gravatar.com/avatar/477cdb6499e58f28541b52307a3c5bbc?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FRN-1.png" + }, + "html": { + "href": "https://bitbucket.org/%7B1658dec0-6727-44ba-b680-15f84e664746%7D/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B1658dec0-6727-44ba-b680-15f84e664746%7D" + } + }, + "nickname": "user", + "type": "user", + "uuid": "{1658dec0-6727-44ba-b680-15f84e664746}" + } + }, + "date": "2020-06-01T17:27:48+00:00", + "hash": "566f2a7b82c0e4325e374b1fc2352891fc1b10e2", + "links": { + "approve": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/566f2a7b82c0e4325e374b1fc2352891fc1b10e2/approve" + }, + "comments": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/566f2a7b82c0e4325e374b1fc2352891fc1b10e2/comments" + }, + "diff": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/diff/566f2a7b82c0e4325e374b1fc2352891fc1b10e2" + }, + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli/commits/566f2a7b82c0e4325e374b1fc2352891fc1b10e2" + }, + "patch": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/patch/566f2a7b82c0e4325e374b1fc2352891fc1b10e2" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/566f2a7b82c0e4325e374b1fc2352891fc1b10e2" + }, + "statuses": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/566f2a7b82c0e4325e374b1fc2352891fc1b10e2/statuses" + } + }, + "message": "tags trigger\n", + "parents": [ + { + "hash": "b41bbad45ce0bf8720521b8422718a29c32bbbf1", + "links": { + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli/commits/b41bbad45ce0bf8720521b8422718a29c32bbbf1" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/b41bbad45ce0bf8720521b8422718a29c32bbbf1" + } + }, + "type": "commit" + } + ], + "rendered": { + "message": { + "html": "

tags trigger

", + "markup": "markdown", + "raw": "tags trigger\n", + "type": "rendered" + } + }, + "repository": { + "full_name": "user2/setup-jfrog-cli", + "links": { + "avatar": { + "href": "https://bytebucket.org/ravatar/%7Bd5f8a3f0-0cf1-43e9-8451-5379417d6c7c%7D?ts=default" + }, + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli" + } + }, + "name": "setup-jfrog-cli", + "type": "repository", + "uuid": "{d5f8a3f0-0cf1-43e9-8451-5379417d6c7c}" + }, + "summary": { + "html": "

tags trigger

", + "markup": "markdown", + "raw": "tags trigger\n", + "type": "rendered" + }, + "type": "commit" + }, + { + "author": { + "raw": "user ", + "type": "author", + "user": { + "account_id": "5d74edb2098f0b0daa7630de", + "display_name": "user", + "links": { + "avatar": { + "href": "https://secure.gravatar.com/avatar/477cdb6499e58f28541b52307a3c5bbc?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FRN-1.png" + }, + "html": { + "href": "https://bitbucket.org/%7B1658dec0-6727-44ba-b680-15f84e664746%7D/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B1658dec0-6727-44ba-b680-15f84e664746%7D" + } + }, + "nickname": "user", + "type": "user", + "uuid": "{1658dec0-6727-44ba-b680-15f84e664746}" + } + }, + "date": "2020-06-01T17:17:59+00:00", + "hash": "b41bbad45ce0bf8720521b8422718a29c32bbbf1", + "links": { + "approve": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/b41bbad45ce0bf8720521b8422718a29c32bbbf1/approve" + }, + "comments": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/b41bbad45ce0bf8720521b8422718a29c32bbbf1/comments" + }, + "diff": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/diff/b41bbad45ce0bf8720521b8422718a29c32bbbf1" + }, + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli/commits/b41bbad45ce0bf8720521b8422718a29c32bbbf1" + }, + "patch": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/patch/b41bbad45ce0bf8720521b8422718a29c32bbbf1" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/b41bbad45ce0bf8720521b8422718a29c32bbbf1" + }, + "statuses": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/b41bbad45ce0bf8720521b8422718a29c32bbbf1/statuses" + } + }, + "message": "tags trigger\n", + "parents": [ + { + "hash": "c66caf690e257f8ec6477c528e455512c9b61dfd", + "links": { + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli/commits/c66caf690e257f8ec6477c528e455512c9b61dfd" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/c66caf690e257f8ec6477c528e455512c9b61dfd" + } + }, + "type": "commit" + } + ], + "rendered": { + "message": { + "html": "

tags trigger

", + "markup": "markdown", + "raw": "tags trigger\n", + "type": "rendered" + } + }, + "repository": { + "full_name": "user2/setup-jfrog-cli", + "links": { + "avatar": { + "href": "https://bytebucket.org/ravatar/%7Bd5f8a3f0-0cf1-43e9-8451-5379417d6c7c%7D?ts=default" + }, + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli" + } + }, + "name": "setup-jfrog-cli", + "type": "repository", + "uuid": "{d5f8a3f0-0cf1-43e9-8451-5379417d6c7c}" + }, + "summary": { + "html": "

tags trigger

", + "markup": "markdown", + "raw": "tags trigger\n", + "type": "rendered" + }, + "type": "commit" + }, + { + "author": { + "raw": "user ", + "type": "author", + "user": { + "account_id": "5d74edb2098f0b0daa7630de", + "display_name": "user", + "links": { + "avatar": { + "href": "https://secure.gravatar.com/avatar/477cdb6499e58f28541b52307a3c5bbc?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FRN-1.png" + }, + "html": { + "href": "https://bitbucket.org/%7B1658dec0-6727-44ba-b680-15f84e664746%7D/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B1658dec0-6727-44ba-b680-15f84e664746%7D" + } + }, + "nickname": "user", + "type": "user", + "uuid": "{1658dec0-6727-44ba-b680-15f84e664746}" + } + }, + "date": "2020-06-01T17:06:53+00:00", + "hash": "c66caf690e257f8ec6477c528e455512c9b61dfd", + "links": { + "approve": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/c66caf690e257f8ec6477c528e455512c9b61dfd/approve" + }, + "comments": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/c66caf690e257f8ec6477c528e455512c9b61dfd/comments" + }, + "diff": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/diff/c66caf690e257f8ec6477c528e455512c9b61dfd" + }, + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli/commits/c66caf690e257f8ec6477c528e455512c9b61dfd" + }, + "patch": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/patch/c66caf690e257f8ec6477c528e455512c9b61dfd" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/c66caf690e257f8ec6477c528e455512c9b61dfd" + }, + "statuses": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/c66caf690e257f8ec6477c528e455512c9b61dfd/statuses" + } + }, + "message": "add release pipeline\n", + "parents": [ + { + "hash": "f62ea5359e7af59880b4a5e23e0ce6c1b32b5d3c", + "links": { + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli/commits/f62ea5359e7af59880b4a5e23e0ce6c1b32b5d3c" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/f62ea5359e7af59880b4a5e23e0ce6c1b32b5d3c" + } + }, + "type": "commit" + } + ], + "rendered": { + "message": { + "html": "

add release pipeline

", + "markup": "markdown", + "raw": "add release pipeline\n", + "type": "rendered" + } + }, + "repository": { + "full_name": "user2/setup-jfrog-cli", + "links": { + "avatar": { + "href": "https://bytebucket.org/ravatar/%7Bd5f8a3f0-0cf1-43e9-8451-5379417d6c7c%7D?ts=default" + }, + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli" + } + }, + "name": "setup-jfrog-cli", + "type": "repository", + "uuid": "{d5f8a3f0-0cf1-43e9-8451-5379417d6c7c}" + }, + "summary": { + "html": "

add release pipeline

", + "markup": "markdown", + "raw": "add release pipeline\n", + "type": "rendered" + }, + "type": "commit" + }, + { + "author": { + "raw": "user ", + "type": "author", + "user": { + "account_id": "5d74edb2098f0b0daa7630de", + "display_name": "user", + "links": { + "avatar": { + "href": "https://secure.gravatar.com/avatar/477cdb6499e58f28541b52307a3c5bbc?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FRN-1.png" + }, + "html": { + "href": "https://bitbucket.org/%7B1658dec0-6727-44ba-b680-15f84e664746%7D/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B1658dec0-6727-44ba-b680-15f84e664746%7D" + } + }, + "nickname": "user", + "type": "user", + "uuid": "{1658dec0-6727-44ba-b680-15f84e664746}" + } + }, + "date": "2020-06-01T16:54:09+00:00", + "hash": "f62ea5359e7af59880b4a5e23e0ce6c1b32b5d3c", + "links": { + "approve": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/f62ea5359e7af59880b4a5e23e0ce6c1b32b5d3c/approve" + }, + "comments": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/f62ea5359e7af59880b4a5e23e0ce6c1b32b5d3c/comments" + }, + "diff": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/diff/f62ea5359e7af59880b4a5e23e0ce6c1b32b5d3c" + }, + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli/commits/f62ea5359e7af59880b4a5e23e0ce6c1b32b5d3c" + }, + "patch": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/patch/f62ea5359e7af59880b4a5e23e0ce6c1b32b5d3c" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/f62ea5359e7af59880b4a5e23e0ce6c1b32b5d3c" + }, + "statuses": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/f62ea5359e7af59880b4a5e23e0ce6c1b32b5d3c/statuses" + } + }, + "message": "Update image name\n", + "parents": [ + { + "hash": "847b70c1c72c59aa7e4c40bbcefdbb44d8b32ae3", + "links": { + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli/commits/847b70c1c72c59aa7e4c40bbcefdbb44d8b32ae3" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/847b70c1c72c59aa7e4c40bbcefdbb44d8b32ae3" + } + }, + "type": "commit" + } + ], + "rendered": { + "message": { + "html": "

Update image name

", + "markup": "markdown", + "raw": "Update image name\n", + "type": "rendered" + } + }, + "repository": { + "full_name": "user2/setup-jfrog-cli", + "links": { + "avatar": { + "href": "https://bytebucket.org/ravatar/%7Bd5f8a3f0-0cf1-43e9-8451-5379417d6c7c%7D?ts=default" + }, + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli" + } + }, + "name": "setup-jfrog-cli", + "type": "repository", + "uuid": "{d5f8a3f0-0cf1-43e9-8451-5379417d6c7c}" + }, + "summary": { + "html": "

Update image name

", + "markup": "markdown", + "raw": "Update image name\n", + "type": "rendered" + }, + "type": "commit" + }, + { + "author": { + "raw": "user ", + "type": "author", + "user": { + "account_id": "5d74edb2098f0b0daa7630de", + "display_name": "user", + "links": { + "avatar": { + "href": "https://secure.gravatar.com/avatar/477cdb6499e58f28541b52307a3c5bbc?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FRN-1.png" + }, + "html": { + "href": "https://bitbucket.org/%7B1658dec0-6727-44ba-b680-15f84e664746%7D/" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B1658dec0-6727-44ba-b680-15f84e664746%7D" + } + }, + "nickname": "user", + "type": "user", + "uuid": "{1658dec0-6727-44ba-b680-15f84e664746}" + } + }, + "date": "2020-06-01T16:20:16+00:00", + "hash": "847b70c1c72c59aa7e4c40bbcefdbb44d8b32ae3", + "links": { + "approve": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/847b70c1c72c59aa7e4c40bbcefdbb44d8b32ae3/approve" + }, + "comments": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/847b70c1c72c59aa7e4c40bbcefdbb44d8b32ae3/comments" + }, + "diff": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/diff/847b70c1c72c59aa7e4c40bbcefdbb44d8b32ae3" + }, + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli/commits/847b70c1c72c59aa7e4c40bbcefdbb44d8b32ae3" + }, + "patch": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/patch/847b70c1c72c59aa7e4c40bbcefdbb44d8b32ae3" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/847b70c1c72c59aa7e4c40bbcefdbb44d8b32ae3" + }, + "statuses": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli/commit/847b70c1c72c59aa7e4c40bbcefdbb44d8b32ae3/statuses" + } + }, + "message": "Initial commit\n", + "parents": [], + "rendered": { + "message": { + "html": "

Initial commit

", + "markup": "markdown", + "raw": "Initial commit\n", + "type": "rendered" + } + }, + "repository": { + "full_name": "user2/setup-jfrog-cli", + "links": { + "avatar": { + "href": "https://bytebucket.org/ravatar/%7Bd5f8a3f0-0cf1-43e9-8451-5379417d6c7c%7D?ts=default" + }, + "html": { + "href": "https://bitbucket.org/user2/setup-jfrog-cli" + }, + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/user2/setup-jfrog-cli" + } + }, + "name": "setup-jfrog-cli", + "type": "repository", + "uuid": "{d5f8a3f0-0cf1-43e9-8451-5379417d6c7c}" + }, + "summary": { + "html": "

Initial commit

", + "markup": "markdown", + "raw": "Initial commit\n", + "type": "rendered" + }, + "type": "commit" + } + ] +} \ No newline at end of file diff --git a/vcsclient/testdata/bitbucketserver/commit_list_response.json b/vcsclient/testdata/bitbucketserver/commit_list_response.json new file mode 100644 index 00000000..3b4a32b0 --- /dev/null +++ b/vcsclient/testdata/bitbucketserver/commit_list_response.json @@ -0,0 +1,35 @@ +{ + "size": 1, + "limit": 25, + "isLastPage": true, + "values": [ + { + "id": "def0123abcdef4567abcdef8987abcdef6543abc", + "displayId": "def0123abcd", + "author": { + "name": "charlie", + "emailAddress": "charlie@example.com" + }, + "authorTimestamp": 1548720847609, + "committer": { + "name": "mark", + "emailAddress": "mark@example.com" + }, + "committerTimestamp": 1548720847610, + "message": "More work on feature 1", + "parents": [ + { + "id": "abcdef0123abcdef4567abcdef8987abcdef6543", + "displayId": "abcdef0" + }, + { + "id": "qwerty0123abcdef4567abcdef8987abcdef6543", + "displayId": "abcdef0" + } + ] + } + ], + "start": 0, + "authorCount": 1, + "totalCount": 1 +} \ No newline at end of file diff --git a/vcsclient/testdata/github/commit_list_response.json b/vcsclient/testdata/github/commit_list_response.json new file mode 100644 index 00000000..ee61f6fc --- /dev/null +++ b/vcsclient/testdata/github/commit_list_response.json @@ -0,0 +1,80 @@ +[ + { + "url": "https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e", + "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e", + "node_id": "MDY6Q29tbWl0NmRjYjA5YjViNTc4NzVmMzM0ZjYxYWViZWQ2OTVlMmU0MTkzZGI1ZQ==", + "html_url": "https://github.com/octocat/Hello-World/commit/6dcb09b5b57875f334f61aebed695e2e4193db5e", + "comments_url": "https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e/comments", + "commit": { + "url": "https://api.github.com/repos/octocat/Hello-World/git/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e", + "author": { + "name": "Monalisa Octocat", + "email": "support@github.com", + "date": "2011-04-14T16:00:49Z" + }, + "committer": { + "name": "Joconde Octocat", + "email": "support2@github.com", + "date": "2011-04-14T16:00:50Z" + }, + "message": "Fix all the bugs", + "tree": { + "url": "https://api.github.com/repos/octocat/Hello-World/tree/6dcb09b5b57875f334f61aebed695e2e4193db5e", + "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e" + }, + "comment_count": 0, + "verification": { + "verified": false, + "reason": "unsigned", + "signature": null, + "payload": null + } + }, + "author": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "committer": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "parents": [ + { + "url": "https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e", + "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e" + } + ] + } +] diff --git a/vcsclient/testdata/gitlab/commit_list_response.json b/vcsclient/testdata/gitlab/commit_list_response.json new file mode 100644 index 00000000..e861317a --- /dev/null +++ b/vcsclient/testdata/gitlab/commit_list_response.json @@ -0,0 +1,34 @@ +[ + { + "id": "ed899a2f4b50b4370feeea94676502b42383c746", + "short_id": "ed899a2f4b5", + "title": "Replace sanitize with escape once", + "author_name": "Example User", + "author_email": "user@example.com", + "authored_date": "2012-09-20T11:50:22+03:00", + "committer_name": "Administrator", + "committer_email": "admin@example.com", + "committed_date": "2012-09-20T11:50:22+03:00", + "created_at": "2012-09-20T11:50:22+03:00", + "message": "Replace sanitize with escape once", + "parent_ids": [ + "6104942438c14ec7bd21c6cd5bd995272b3faff6" + ], + "web_url": "https://gitlab.example.com/thedude/gitlab-foss/-/commit/ed899a2f4b50b4370feeea94676502b42383c746" + }, + { + "id": "6104942438c14ec7bd21c6cd5bd995272b3faff6", + "short_id": "6104942438c", + "title": "Sanitize for network graph", + "author_name": "randx", + "author_email": "user@example.com", + "committer_name": "ExampleName", + "committer_email": "user@example.com", + "created_at": "2012-09-20T09:06:12+03:00", + "message": "Sanitize for network graph", + "parent_ids": [ + "ae1d9fb46aa2b07ee9836d49862ec4e2c46fbbba" + ], + "web_url": "https://gitlab.example.com/thedude/gitlab-foss/-/commit/ed899a2f4b50b4370feeea94676502b42383c746" + } +] \ No newline at end of file diff --git a/vcsclient/vcsclient.go b/vcsclient/vcsclient.go index cea45ab3..f837d707 100644 --- a/vcsclient/vcsclient.go +++ b/vcsclient/vcsclient.go @@ -2,7 +2,9 @@ package vcsclient import ( "context" + "fmt" "github.com/jfrog/froggit-go/vcsutils" + "strings" ) type CommitStatus int @@ -82,4 +84,37 @@ type VcsClient interface { // title - Pull request title // description - Pull request description CreatePullRequest(ctx context.Context, owner, repository, sourceBranch, targetBranch, title, description string) error + + // GetLatestCommit Get the most recent commit of a branch + // owner - User or organization + // repository - VCS repository name + // branch - The name of the branch + GetLatestCommit(ctx context.Context, owner, repository, branch string) (CommitInfo, error) +} + +// CommitInfo contains the details of a commit +type CommitInfo struct { + // The SHA-1 hash of the commit + Hash string + // The author's name + AuthorName string + // The committer's name + CommitterName string + // The commit URL + Url string + // Seconds from epoch + Timestamp int64 + // The commit message + Message string + // The SHA-1 hashes of the parent commits + ParentHashes []string +} + +func validateParametersNotBlank(parameters ...string) error { + for _, parameter := range parameters { + if len(strings.TrimSpace(parameter)) == 0 { + return fmt.Errorf("required parameter is empty") + } + } + return nil }