Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions cmd/kosli/pullrequest.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,7 @@ func (o *attestPROptions) run(args []string) error {
o.payload.GitProvider, label = getGitProviderAndLabel(o.retriever)

var pullRequestsEvidence []*types.PREvidence
if o.payload.GitProvider == "azure" {
pullRequestsEvidence, err = o.getRetriever().PREvidenceForCommitV1(o.payload.Commit.Sha1)
} else {
pullRequestsEvidence, err = o.getRetriever().PREvidenceForCommitV2(o.payload.Commit.Sha1)
}
pullRequestsEvidence, err = o.getRetriever().PREvidenceForCommitV2(o.payload.Commit.Sha1)
if err != nil {
return err
}
Expand Down
101 changes: 93 additions & 8 deletions internal/azure/azure.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ package azure

import (
"context"
"fmt"
"net/url"
"strconv"
"strings"

"github.com/kosli-dev/cli/internal/types"
"github.com/kosli-dev/cli/internal/utils"
"github.com/microsoft/azure-devops-go-api/azuredevops"
"github.com/microsoft/azure-devops-go-api/azuredevops/git"
)
Expand Down Expand Up @@ -74,7 +74,19 @@ func (c *AzureConfig) PREvidenceForCommitV1(commit string) ([]*types.PREvidence,

// This is the new implementation, it will be used for Azure
func (c *AzureConfig) PREvidenceForCommitV2(commit string) ([]*types.PREvidence, error) {
return []*types.PREvidence{}, nil
pullRequestsEvidence := []*types.PREvidence{}
prs, err := c.PullRequestsForCommit(commit)
if err != nil {
return pullRequestsEvidence, err
}
for _, pr := range prs {
evidence, err := c.newPRAzureEvidenceV2(pr)
if err != nil {
return pullRequestsEvidence, err
}
pullRequestsEvidence = append(pullRequestsEvidence, evidence)
}
return pullRequestsEvidence, nil
}

func (c *AzureConfig) newPRAzureEvidence(pr git.GitPullRequest) (*types.PREvidence, error) {
Expand All @@ -88,14 +100,79 @@ func (c *AzureConfig) newPRAzureEvidence(pr git.GitPullRequest) (*types.PREviden
MergeCommit: *(pr.LastMergeCommit.CommitId),
State: string(*pr.Status),
}
approvers, err := c.GetPullRequestApprovers(*pr.PullRequestId)
evidence.Approvers, err = c.GetPullRequestApprovers(*pr.PullRequestId, 1)
if err != nil {
return evidence, err
}
evidence.Approvers = utils.ConvertStringListToInterfaceList(approvers)
return evidence, nil
}

// newPRAzureEvidenceV2 creates a new PREvidence for a given pull request in V2 format
func (c *AzureConfig) newPRAzureEvidenceV2(pr git.GitPullRequest) (*types.PREvidence, error) {
prID := strconv.Itoa(*pr.PullRequestId)
prURL, err := url.JoinPath(c.OrgURL, c.Project, "_git", c.Repository, "pullrequest", prID)
if err != nil {
return nil, err
}

evidence := &types.PREvidence{
URL: prURL,
MergeCommit: *(pr.LastMergeCommit.CommitId),
State: string(*pr.Status),
Author: fmt.Sprintf("%s (%s)", *pr.CreatedBy.DisplayName, *pr.CreatedBy.UniqueName),
CreatedAt: pr.CreationDate.Time.Unix(),
Title: *pr.Title,
HeadRef: *pr.SourceRefName,
}
if pr.Status != nil && pr.ClosedDate != nil && *pr.Status == git.PullRequestStatusValues.Completed {
evidence.MergedAt = pr.ClosedDate.Time.Unix()
}
commits, err := c.GetPullRequestCommits(pr)
if err != nil {
return evidence, err
}
evidence.Commits = commits
evidence.Approvers, err = c.GetPullRequestApprovers(*pr.PullRequestId, 2)
if err != nil {
return evidence, err
}
return evidence, nil
}

// GetPullRequestCommits returns a list of commits for a given pull request
func (c *AzureConfig) GetPullRequestCommits(pr git.GitPullRequest) ([]types.Commit, error) {
commits := []types.Commit{}

ctx := context.Background()
client, err := NewAzureClientFromToken(ctx, c.Token, c.OrgURL)
if err != nil {
return commits, err
}

prCommitsResponse, err := client.GetPullRequestCommits(ctx, git.GetPullRequestCommitsArgs{
RepositoryId: &c.Repository,
PullRequestId: pr.PullRequestId,
Project: &c.Project,
})
if err != nil {
return commits, err
}

for _, commit := range prCommitsResponse.Value {
commits = append(commits, types.Commit{
SHA: *commit.CommitId,
Message: *commit.Comment,
Committer: *commit.Author.Name,
Timestamp: commit.Committer.Date.Time.Unix(),
URL: *commit.Url,
Branch: *pr.SourceRefName,
CommitterUsername: *commit.Committer.Name,
})
}

return commits, nil
}

// PullRequestsForCommit returns a list of pull requests for a specific commit
func (c *AzureConfig) PullRequestsForCommit(commit string) ([]git.GitPullRequest, error) {
ctx := context.Background()
Expand Down Expand Up @@ -131,8 +208,8 @@ func (c *AzureConfig) PullRequestsForCommit(commit string) ([]git.GitPullRequest
}

// GetPullRequestApprovers returns a list of approvers for a given pull request
func (c *AzureConfig) GetPullRequestApprovers(number int) ([]string, error) {
approvers := []string{}
func (c *AzureConfig) GetPullRequestApprovers(prNumber, version int) ([]any, error) {
var approvers []any
ctx := context.Background()
client, err := NewAzureClientFromToken(ctx, c.Token, c.OrgURL)
if err != nil {
Expand All @@ -141,7 +218,7 @@ func (c *AzureConfig) GetPullRequestApprovers(number int) ([]string, error) {

reviewers, err := client.GetPullRequestReviewers(ctx, git.GetPullRequestReviewersArgs{
RepositoryId: &c.Repository,
PullRequestId: &number,
PullRequestId: &prNumber,
Project: &c.Project,
})
if err != nil {
Expand All @@ -150,7 +227,15 @@ func (c *AzureConfig) GetPullRequestApprovers(number int) ([]string, error) {

for _, r := range *reviewers {
if *r.Vote == 10 {
approvers = append(approvers, *r.DisplayName)
approverName := fmt.Sprintf("%s (%s)", *r.DisplayName, *r.UniqueName)
if version == 1 {
approvers = append(approvers, approverName)
} else {
approvers = append(approvers, types.PRApprovals{
Username: approverName,
State: "APPROVED",
})
}
}
}
return approvers, nil
Expand Down
78 changes: 75 additions & 3 deletions internal/azure/azure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"testing"

"github.com/kosli-dev/cli/internal/testHelpers"
"github.com/kosli-dev/cli/internal/types"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
Expand Down Expand Up @@ -123,7 +124,7 @@ func (suite *AzureTestSuite) TestPullRequestsForCommit() {
}
}

func (suite *AzureTestSuite) TestGetPullRequestApprovers() {
func (suite *AzureTestSuite) TestGetPullRequestApproversV1() {
type result struct {
wantError bool
approvers []string
Expand Down Expand Up @@ -153,7 +154,7 @@ func (suite *AzureTestSuite) TestGetPullRequestApprovers() {
project: "kosli-azure",
number: 1,
result: result{
approvers: []string{"Ewelina"},
approvers: []string{"Ewelina (ewelina@merkely.onmicrosoft.com)"},
},
},
{
Expand All @@ -176,7 +177,78 @@ func (suite *AzureTestSuite) TestGetPullRequestApprovers() {
Repository: t.repository,
Project: t.project,
}
approvers, err := c.GetPullRequestApprovers(t.number)
approvers, err := c.GetPullRequestApprovers(t.number, 1)
if t.result.wantError {
require.Errorf(suite.T(), err, "expected an error but got: %s", err)
} else {
require.NoErrorf(suite.T(), err, "was NOT expecting error but got: %s", err)
require.ElementsMatchf(suite.T(), t.result.approvers, approvers, "want approvers: %v, got approvers: %v",
t.result.approvers, approvers)
}
})
}
}

func (suite *AzureTestSuite) TestGetPullRequestApproversV2() {
type result struct {
wantError bool
approvers []types.PRApprovals
}
for _, t := range []struct {
name string
azOrgURL string
repository string
project string
number int
result result
}{
{
name: "get an empty list for a PR without approvers",
azOrgURL: "https://dev.azure.com/kosli",
repository: "cli",
project: "kosli-azure",
number: 2,
result: result{
approvers: []types.PRApprovals{},
},
},
{
name: "get the list of approvers for an approved PR",
azOrgURL: "https://dev.azure.com/kosli",
repository: "cli",
project: "kosli-azure",
number: 1,
result: result{
approvers: []types.PRApprovals{
{
Username: "Ewelina (ewelina@merkely.onmicrosoft.com)",
State: "APPROVED",
Timestamp: 0,
},
},
},
},
{
name: "non-existing PR causes an error",
azOrgURL: "https://dev.azure.com/kosli",
repository: "cli",
project: "kosli-azure",
number: 666,
result: result{
wantError: true,
},
},
} {
suite.Run(t.name, func() {
testHelpers.SkipIfEnvVarUnset(suite.T(), []string{"KOSLI_AZURE_TOKEN"})
token := os.Getenv("KOSLI_AZURE_TOKEN")
c := &AzureConfig{
Token: token,
OrgURL: t.azOrgURL,
Repository: t.repository,
Project: t.project,
}
approvers, err := c.GetPullRequestApprovers(t.number, 2)
if t.result.wantError {
require.Errorf(suite.T(), err, "expected an error but got: %s", err)
} else {
Expand Down