Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "GetCommitsWithQueryOptions" new API. #135

Merged
merged 14 commits into from
Jun 30, 2024
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Currently supported providers are: [GitHub](#github), [Bitbucket Server](#bitbuc
- [Delete Pull Request Comment](#delete-pull-request-comment)
- [Delete Pull Request Review Comments](#delete-pull-request-review-comments)
- [Get Commits](#get-commits)
- [Get Commits With Options](#get-commits-with-options)
- [Get Latest Commit](#get-latest-commit)
- [Get Commit By SHA](#get-commit-by-sha)
- [Get List of Modified Files](#get-list-of-modified-files)
Expand Down Expand Up @@ -544,6 +545,29 @@ branch := "dev"
commitInfo, err := client.GetCommits(ctx, owner, repository, branch)
```

#### Get Commits With Options

```go
// Go context
ctx := context.Background()
// Organization or username
owner := "jfrog"
// VCS repository
repository := "jfrog-cli"

// Commits query options
options := GitCommitsQueryOptions{
Since: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
Until: time.Now(),
ListOptions: ListOptions{
Page: 1,
PerPage: 30,
},
}

result, err := client.GetCommitsWithQueryOptions(ctx, owner, repository, options)
```

#### Get Latest Commit

```go
Expand Down
7 changes: 7 additions & 0 deletions vcsclient/azurerepos.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@ import (
)

const (
notSupportedOnAzure = "currently not supported on Azure"
defaultAzureBaseUrl = "https://dev.azure.com/"
azurePullRequestDetailsSizeLimit = 4000
azurePullRequestCommentSizeLimit = 150000
)

var errAzureGetCommitsWithOptionsNotSupported = fmt.Errorf("get commits with options is %s", notSupportedOnAzure)

// Azure Devops API version 6
type AzureReposClient struct {
vcsInfo VcsInfo
Expand Down Expand Up @@ -425,6 +428,10 @@ func (client *AzureReposClient) GetCommits(ctx context.Context, _, repository, b
return commitsInfo, nil
}

func (client *AzureReposClient) GetCommitsWithQueryOptions(ctx context.Context, _, repository string, listOptions GitCommitsQueryOptions) ([]CommitInfo, error) {
return nil, errAzureGetCommitsWithOptionsNotSupported
}

func mapAzureReposCommitsToCommitInfo(commit git.GitCommitRef) CommitInfo {
var authorName, authorEmail string
if commit.Author != nil {
Expand Down
4 changes: 4 additions & 0 deletions vcsclient/bitbucketcloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,10 @@ func (client *BitbucketCloudClient) GetCommits(_ context.Context, _, _, _ string
return nil, errBitbucketGetCommitsNotSupported
}

func (client *BitbucketCloudClient) GetCommitsWithQueryOptions(ctx context.Context, owner, repository string, listOptions GitCommitsQueryOptions) ([]CommitInfo, error) {
return nil, errBitbucketGetCommitsWithOptionsNotSupported
}

// GetRepositoryInfo on Bitbucket cloud
func (client *BitbucketCloudClient) GetRepositoryInfo(ctx context.Context, owner, repository string) (RepositoryInfo, error) {
if err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository}); err != nil {
Expand Down
1 change: 1 addition & 0 deletions vcsclient/bitbucketcommon.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ var (
errBitbucketCodeScanningNotSupported = fmt.Errorf("code scanning is %s", notSupportedOnBitbucket)
errBitbucketDownloadFileFromRepoNotSupported = fmt.Errorf("download file from repo is %s", notSupportedOnBitbucket)
errBitbucketGetCommitsNotSupported = fmt.Errorf("get commits is %s", notSupportedOnBitbucket)
errBitbucketGetCommitsWithOptionsNotSupported = fmt.Errorf("get commits with options is %s", notSupportedOnBitbucket)
errBitbucketGetRepoEnvironmentInfoNotSupported = fmt.Errorf("get repository environment info is %s", notSupportedOnBitbucket)
errBitbucketListPullRequestReviewCommentsNotSupported = fmt.Errorf("list pull request review comments is %s", notSupportedOnBitbucket)
errBitbucketAddPullRequestReviewCommentsNotSupported = fmt.Errorf("add pull request review comment is %s", notSupportedOnBitbucket)
Expand Down
62 changes: 58 additions & 4 deletions vcsclient/bitbucketserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,52 @@ func (client *BitbucketServerClient) GetCommits(ctx context.Context, owner, repo
"limit": vcsutils.NumberOfCommitsToFetch,
"until": branch,
}
return client.getCommitsWithQueryOptions(ctx, owner, repository, options)
}

func (client *BitbucketServerClient) GetCommitsWithQueryOptions(ctx context.Context, owner, repository string, listOptions GitCommitsQueryOptions) ([]CommitInfo, error) {
err := validateParametersNotBlank(map[string]string{
"owner": owner,
"repository": repository,
})
if err != nil {
return nil, err
}
commits, err := client.getCommitsWithQueryOptions(ctx, owner, repository, convertToBitbucketOptionsMap(listOptions))
if err != nil {
return nil, err
}
return getCommitsInDateRate(commits, listOptions), nil
}

// Bitbucket doesn't support filtering by date, so we need to filter the commits by date ourselves.
func getCommitsInDateRate(commits []CommitInfo, options GitCommitsQueryOptions) []CommitInfo {
commitsNumber := len(commits)
if commitsNumber == 0 {
return commits
}

firstCommit := time.Unix(commits[0].Timestamp, 0).UTC()
lastCommit := time.Unix(commits[commitsNumber-1].Timestamp, 0).UTC()

// If all commits are in the range return all.
if lastCommit.After(options.Since) || lastCommit.Equal(options.Since) {
return commits
}
// If the first commit is older than the "since" timestamp, all commits are out of range, return an empty list.
if firstCommit.Before(options.Since) {
return []CommitInfo{}
}
// Find the first commit that is older than the "since" timestamp.
for i, commit := range commits {
if time.Unix(commit.Timestamp, 0).UTC().Before(options.Since) {
return commits[:i]
}
}
return []CommitInfo{}
}

func (client *BitbucketServerClient) getCommitsWithQueryOptions(ctx context.Context, owner, repository string, options map[string]interface{}) ([]CommitInfo, error) {
bitbucketClient := client.buildBitbucketClient(ctx)

apiResponse, err := bitbucketClient.GetCommits(owner, repository, options)
Expand All @@ -571,6 +617,13 @@ func (client *BitbucketServerClient) GetCommits(ctx context.Context, owner, repo
return commitsInfo, nil
}

func convertToBitbucketOptionsMap(listOptions GitCommitsQueryOptions) map[string]interface{} {
return map[string]interface{}{
"limit": listOptions.PerPage,
"start": (listOptions.Page - 1) * listOptions.PerPage,
}
}

// GetRepositoryInfo on Bitbucket server
func (client *BitbucketServerClient) GetRepositoryInfo(ctx context.Context, owner, repository string) (RepositoryInfo, error) {
if err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository}); err != nil {
Expand Down Expand Up @@ -767,10 +820,11 @@ func (client *BitbucketServerClient) mapBitbucketServerCommitToCommitInfo(commit
AuthorName: commit.Author.Name,
CommitterName: commit.Committer.Name,
Url: url,
Timestamp: commit.CommitterTimestamp,
Message: commit.Message,
ParentHashes: parents,
AuthorEmail: commit.Author.EmailAddress,
// Convert from bitbucket millisecond timestamp to CommitInfo seconds timestamp.
Timestamp: commit.CommitterTimestamp / 1000,
Message: commit.Message,
ParentHashes: parents,
AuthorEmail: commit.Author.EmailAddress,
}
}

Expand Down
131 changes: 127 additions & 4 deletions vcsclient/bitbucketserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ func TestBitbucketServer_GetLatestCommit(t *testing.T) {
AuthorName: "charlie",
CommitterName: "mark",
Url: expectedUrl,
Timestamp: 1548720847610,
Timestamp: 1548720847,
Message: "More work on feature 1",
ParentHashes: []string{"abcdef0123abcdef4567abcdef8987abcdef6543", "qwerty0123abcdef4567abcdef8987abcdef6543"},
AuthorEmail: "charlie@example.com",
Expand Down Expand Up @@ -371,7 +371,7 @@ func TestBitbucketServer_GetCommits(t *testing.T) {
AuthorName: "charlie",
CommitterName: "mark",
Url: expectedUrl,
Timestamp: 1548720847610,
Timestamp: 1548720847,
Message: "More work on feature 1",
ParentHashes: []string{"abcdef0123abcdef4567abcdef8987abcdef6543", "qwerty0123abcdef4567abcdef8987abcdef6543"},
AuthorEmail: "charlie@example.com",
Expand All @@ -381,7 +381,7 @@ func TestBitbucketServer_GetCommits(t *testing.T) {
AuthorName: "marly",
CommitterName: "marly",
Url: expectedUrl,
Timestamp: 1548720847610,
Timestamp: 1548720847,
Message: "More work on feature 2",
ParentHashes: []string{"abcdef0123abcdef4567abcdef8987abcdef6543", "qwerty0123abcdef4567abcdef8987abcdef6543"},
AuthorEmail: "marly@example.com",
Expand All @@ -391,6 +391,54 @@ func TestBitbucketServer_GetCommits(t *testing.T) {
assert.Error(t, err)
}

func TestBitbucketServer_GetCommitsWithQueryOptions(t *testing.T) {
ctx := context.Background()
response, err := os.ReadFile(filepath.Join("testdata", "bitbucketserver", "commit_list_response.json"))
assert.NoError(t, err)
client, serverUrl, cleanUp := createServerWithUrlAndClientReturningStatus(t, vcsutils.BitbucketServer, false,
response,
fmt.Sprintf("/rest/api/1.0/projects/%s/repos/%s/commits?limit=30&limit=30&start=0", owner, repo1),
http.StatusOK, createBitbucketServerHandler)
defer cleanUp()

options := GitCommitsQueryOptions{
Since: time.Date(2017, 1, 1, 0, 0, 0, 0, time.UTC),
ListOptions: ListOptions{
Page: 1,
PerPage: 30,
},
}

result, err := client.GetCommitsWithQueryOptions(ctx, owner, repo1, options)

assert.NoError(t, err)
expectedUrl := fmt.Sprintf("%s/projects/jfrog/repos/repo-1"+
"/commits/def0123abcdef4567abcdef8987abcdef6543abc", serverUrl)
assert.Equal(t, CommitInfo{
Hash: "def0123abcdef4567abcdef8987abcdef6543abc",
AuthorName: "charlie",
CommitterName: "mark",
Url: expectedUrl,
Timestamp: 1548720847,
Message: "More work on feature 1",
ParentHashes: []string{"abcdef0123abcdef4567abcdef8987abcdef6543", "qwerty0123abcdef4567abcdef8987abcdef6543"},
AuthorEmail: "charlie@example.com",
}, result[0])
assert.Equal(t, CommitInfo{
Hash: "def0123abcdef4567abcdef8987abcdef6543abc",
AuthorName: "marly",
CommitterName: "marly",
Url: expectedUrl,
Timestamp: 1548720847,
Message: "More work on feature 2",
ParentHashes: []string{"abcdef0123abcdef4567abcdef8987abcdef6543", "qwerty0123abcdef4567abcdef8987abcdef6543"},
AuthorEmail: "marly@example.com",
}, result[1])

_, err = createBadBitbucketServerClient(t).GetCommitsWithQueryOptions(ctx, owner, repo1, options)
assert.Error(t, err)
}

func TestBitbucketServer_GetLatestCommitNotFound(t *testing.T) {
ctx := context.Background()
response := []byte(`{
Expand Down Expand Up @@ -603,7 +651,7 @@ func TestBitbucketServer_GetCommitBySha(t *testing.T) {
AuthorName: "charlie",
CommitterName: "mark",
Url: expectedUrl,
Timestamp: 1636089306104,
Timestamp: 1636089306,
Message: "WIP on feature 1",
ParentHashes: []string{"bbcdef0123abcdef4567abcdef8987abcdef6543"},
AuthorEmail: "charlie@example.com",
Expand Down Expand Up @@ -882,3 +930,78 @@ func createBadBitbucketServerClient(t *testing.T) VcsClient {
assert.NoError(t, err)
return client
}

func TestGetCommitsInDateRate(t *testing.T) {
tests := []struct {
name string
commits []CommitInfo
options GitCommitsQueryOptions
expected []CommitInfo
}{
{
name: "All commits within range",
commits: []CommitInfo{
{Timestamp: 1717396600}, // Mon, 03 Jun 2024 09:56:40 GMT (Within range)
{Timestamp: 1717396500}, // Mon, 03 Jun 2024 09:55:00 GMT (Within range)
{Timestamp: 1717396400}, // Mon, 03 Jun 2024 09:53:20 GMT (Within range)
},
options: GitCommitsQueryOptions{
Since: time.Unix(1717396300, 0), // Mon, 03 Jun 2024 09:51:40 GMT (Set since timestamp in seconds)
},
expected: []CommitInfo{
{Timestamp: 1717396600},
{Timestamp: 1717396500},
{Timestamp: 1717396400},
},
},
{
name: "All commits within range or equal",
commits: []CommitInfo{
{Timestamp: 1717396600}, // Mon, 03 Jun 2024 09:56:40 GMT (Within range)
{Timestamp: 1717396500}, // Mon, 03 Jun 2024 09:55:00 GMT (Within range)
{Timestamp: 1717396400}, // Mon, 03 Jun 2024 09:53:20 GMT (Within range)
},
options: GitCommitsQueryOptions{
Since: time.Unix(1717396400, 0), // Mon, 03 Jun 2024 09:53:20 GMT (Set since timestamp in seconds)
},
expected: []CommitInfo{
{Timestamp: 1717396600},
{Timestamp: 1717396500},
{Timestamp: 1717396400},
},
},
{
name: "No commits within range",
commits: []CommitInfo{
{Timestamp: 1717396500}, // Mon, 03 Jun 2024 09:55:00 GMT (Older than range)
{Timestamp: 1717396400}, // Mon, 03 Jun 2024 09:53:20 GMT (Older than range)
},
options: GitCommitsQueryOptions{
Since: time.Unix(1717396600, 0), // Mon, 03 Jun 2024 09:56:40 GMT (Set since timestamp in seconds)
},
expected: []CommitInfo{},
},
{
name: "Partial commits within range",
commits: []CommitInfo{
{Timestamp: 1717396600}, // Mon, 03 Jun 2024 09:56:40 GMT (Within range)
{Timestamp: 1717396500}, // Mon, 03 Jun 2024 09:55:00 GMT (Within range)
{Timestamp: 1717396400}, // Mon, 03 Jun 2024 09:53:20 GMT (Older than range)
},
options: GitCommitsQueryOptions{
Since: time.Unix(1717396500, 0), // Mon, 03 Jun 2024 09:55:00 GMT (Set since timestamp in seconds)
},
expected: []CommitInfo{
{Timestamp: 1717396600},
{Timestamp: 1717396500},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getCommitsInDateRate(tt.commits, tt.options)
assert.ElementsMatch(t, result, tt.expected)
})
}
}
41 changes: 35 additions & 6 deletions vcsclient/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"sort"
"strconv"
"strings"
"time"
)

const (
Expand Down Expand Up @@ -690,21 +691,49 @@ func (client *GitHubClient) GetCommits(ctx context.Context, owner, repository, b
var commitsInfo []CommitInfo
err = client.runWithRateLimitRetries(func() (*github.Response, error) {
var ghResponse *github.Response
commitsInfo, ghResponse, err = client.executeGetCommits(ctx, owner, repository, branch)
listOptions := &github.51.almitsListOptions{
SHA: branch,
ListOptions: github.ListOptions{
Page: 1,
PerPage: vcsutils.NumberOfCommitsToFetch,
},
}
commitsInfo, ghResponse, err = client.executeGetCommits(ctx, owner, repository, listOptions)
return ghResponse, err
})
return commitsInfo, err
}

func (client *GitHubClient) executeGetCommits(ctx context.Context, owner, repository, branch string) ([]CommitInfo, *github.Response, error) {
listOptions := &github.51.almitsListOptions{
SHA: branch,
// GetCommitsWithQueryOptions on GitHub
func (client *GitHubClient) GetCommitsWithQueryOptions(ctx context.Context, owner, repository string, listOptions GitCommitsQueryOptions) ([]CommitInfo, error) {
err := validateParametersNotBlank(map[string]string{
"owner": owner,
"repository": repository,
})
if err != nil {
return nil, err
}
var commitsInfo []CommitInfo
err = client.runWithRateLimitRetries(func() (*github.Response, error) {
var ghResponse *github.Response
commitsInfo, ghResponse, err = client.executeGetCommits(ctx, owner, repository, convertToGitHubCommitsListOptions(listOptions))
return ghResponse, err
})
return commitsInfo, err
}

func convertToGitHubCommitsListOptions(listOptions GitCommitsQueryOptions) *github.51.almitsListOptions {
return &github.51.almitsListOptions{
Since: listOptions.Since,
Until: time.Now(),
ListOptions: github.ListOptions{
Page: 1,
PerPage: vcsutils.NumberOfCommitsToFetch,
Page: listOptions.Page,
PerPage: listOptions.PerPage,
},
}
}

func (client *GitHubClient) executeGetCommits(ctx context.Context, owner, repository string, listOptions *github.51.almitsListOptions) ([]CommitInfo, *github.Response, error) {
commits, ghResponse, err := client.ghClient.Repositories.ListCommits(ctx, owner, repository, listOptions)
if err != nil {
return nil, ghResponse, err
Expand Down
Loading
Loading