Skip to content

Commit

Permalink
✨ implement ListContributors for Azure DevOps (#4437)
Browse files Browse the repository at this point in the history
* ✨ implement `ListContributors` for Azure DevOps

Signed-off-by: Jamie Magee <[email protected]>

* Fix tests

Signed-off-by: Jamie Magee <[email protected]>

---------

Signed-off-by: Jamie Magee <[email protected]>
  • Loading branch information
JamieMagee authored Dec 10, 2024
1 parent 687b739 commit b0dfb70
Show file tree
Hide file tree
Showing 4 changed files with 250 additions and 14 deletions.
32 changes: 19 additions & 13 deletions clients/azuredevopsrepo/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,19 @@ var (
)

type Client struct {
azdoClient git.Client
ctx context.Context
repourl *Repo
repo *git.GitRepository
audit *auditHandler
branches *branchesHandler
commits *commitsHandler
languages *languagesHandler
search *searchHandler
workItems *workItemsHandler
zip *zipHandler
commitDepth int
azdoClient git.Client
ctx context.Context
repourl *Repo
repo *git.GitRepository
audit *auditHandler
branches *branchesHandler
commits *commitsHandler
contributors *contributorsHandler
languages *languagesHandler
search *searchHandler
workItems *workItemsHandler
zip *zipHandler
commitDepth int
}

func (c *Client) InitRepo(inputRepo clients.Repo, commitSHA string, commitDepth int) error {
Expand Down Expand Up @@ -95,6 +96,8 @@ func (c *Client) InitRepo(inputRepo clients.Repo, commitSHA string, commitDepth

c.commits.init(c.ctx, c.repourl, c.commitDepth)

c.contributors.init(c.ctx, c.repourl)

c.languages.init(c.ctx, c.repourl)

c.search.init(c.ctx, c.repourl)
Expand Down Expand Up @@ -176,7 +179,7 @@ func (c *Client) ListReleases() ([]clients.Release, error) {
}

func (c *Client) ListContributors() ([]clients.User, error) {
return nil, clients.ErrUnsupportedFeature
return c.contributors.listContributors()
}

func (c *Client) ListSuccessfulWorkflowRuns(filename string) ([]clients.WorkflowRun, error) {
Expand Down Expand Up @@ -260,6 +263,9 @@ func CreateAzureDevOpsClientWithToken(ctx context.Context, token string, repo cl
commits: &commitsHandler{
gitClient: gitClient,
},
contributors: &contributorsHandler{
gitClient: gitClient,
},
languages: &languagesHandler{
projectAnalysisClient: projectAnalysisClient,
},
Expand Down
100 changes: 100 additions & 0 deletions clients/azuredevopsrepo/contributors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright 2024 OpenSSF Scorecard Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package azuredevopsrepo

import (
"context"
"sync"

"github.com/microsoft/azure-devops-go-api/azuredevops/v7/git"

"github.com/ossf/scorecard/v5/clients"
)

type contributorsHandler struct {
ctx context.Context
once *sync.Once
repourl *Repo
gitClient git.Client
errSetup error
getCommits fnGetCommits
contributors []clients.User
}

func (c *contributorsHandler) init(ctx context.Context, repourl *Repo) {
c.ctx = ctx
c.once = new(sync.Once)
c.repourl = repourl
c.errSetup = nil
c.getCommits = c.gitClient.GetCommits
c.contributors = nil
}

func (c *contributorsHandler) setup() error {
c.once.Do(func() {
contributors := make(map[string]clients.User)
commitsPageSize := 1000
skip := 0
for {
args := git.GetCommitsArgs{
RepositoryId: &c.repourl.id,
SearchCriteria: &git.GitQueryCommitsCriteria{
Top: &commitsPageSize,
Skip: &skip,
},
}
commits, err := c.getCommits(c.ctx, args)
if err != nil {
c.errSetup = err
return
}

if commits == nil || len(*commits) == 0 {
break
}

for i := range *commits {
commit := (*commits)[i]
email := *commit.Author.Email
if _, ok := contributors[email]; ok {
user := contributors[email]
user.NumContributions++
contributors[email] = user
} else {
contributors[email] = clients.User{
Login: email,
NumContributions: 1,
Companies: []string{c.repourl.organization},
}
}
}

skip += commitsPageSize
}

for _, contributor := range contributors {
c.contributors = append(c.contributors, contributor)
}
})
return c.errSetup
}

func (c *contributorsHandler) listContributors() ([]clients.User, error) {
if err := c.setup(); err != nil {
return nil, err
}

return c.contributors, nil
}
130 changes: 130 additions & 0 deletions clients/azuredevopsrepo/contributors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright 2024 OpenSSF Scorecard Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package azuredevopsrepo

import (
"context"
"sync"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/git"

"github.com/ossf/scorecard/v5/clients"
)

func Test_listContributors(t *testing.T) {
t.Parallel()
tests := []struct {
name string
getCommits fnGetCommits
wantContribs []clients.User
wantErr bool
}{
{
name: "no commits",
getCommits: func(ctx context.Context, args git.GetCommitsArgs) (*[]git.GitCommitRef, error) {
return &[]git.GitCommitRef{}, nil
},
wantContribs: nil,
wantErr: false,
},
{
name: "single contributor",
getCommits: func(ctx context.Context, args git.GetCommitsArgs) (*[]git.GitCommitRef, error) {
if *args.SearchCriteria.Skip == 0 {
return &[]git.GitCommitRef{
{
Author: &git.GitUserDate{
Email: toPtr("[email protected]"),
},
},
}, nil
} else {
return &[]git.GitCommitRef{}, nil
}
},
wantContribs: []clients.User{
{
Login: "[email protected]",
Companies: []string{"testOrg"},
NumContributions: 1,
},
},
wantErr: false,
},
{
name: "multiple contributors",
getCommits: func(ctx context.Context, args git.GetCommitsArgs) (*[]git.GitCommitRef, error) {
if *args.SearchCriteria.Skip == 0 {
return &[]git.GitCommitRef{
{
Author: &git.GitUserDate{
Email: toPtr("[email protected]"),
},
},
{
Author: &git.GitUserDate{
Email: toPtr("[email protected]"),
},
},
{
Author: &git.GitUserDate{
Email: toPtr("[email protected]"),
},
},
}, nil
} else {
return &[]git.GitCommitRef{}, nil
}
},
wantContribs: []clients.User{
{
Login: "[email protected]",
Companies: []string{"testOrg"},
NumContributions: 1,
},
{
Login: "[email protected]",
Companies: []string{"testOrg"},
NumContributions: 2,
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
c := contributorsHandler{
ctx: context.Background(),
once: new(sync.Once),
repourl: &Repo{
organization: "testOrg",
},
getCommits: tt.getCommits,
}
err := c.setup()
if (err != nil) != tt.wantErr {
t.Errorf("contributorsHandler.setup() error = %v, wantErr %v", err, tt.wantErr)
return
}
if diff := cmp.Diff(tt.wantContribs, c.contributors, cmpopts.SortSlices(func(a, b string) bool { return a < b })); diff != "" {
t.Errorf("contributorsHandler.setup() mismatch (-want +got):\n%s", diff)
}
})
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ require (
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/google/uuid v1.6.0
github.com/google/wire v0.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
Expand Down

0 comments on commit b0dfb70

Please sign in to comment.