-
Notifications
You must be signed in to change notification settings - Fork 500
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨ Initial experimental Azure DevOps client (#4377)
* ✨ Initial Azure DevOps client Signed-off-by: Jamie Magee <[email protected]> * Address PR comments Signed-off-by: Jamie Magee <[email protected]> * Address PR comments Signed-off-by: Jamie Magee <[email protected]> * Fix lint Signed-off-by: Jamie Magee <[email protected]> * simplify IsArchived call Signed-off-by: Jamie Magee <[email protected]> --------- Signed-off-by: Jamie Magee <[email protected]>
- Loading branch information
1 parent
cf30f20
commit fee8bcf
Showing
12 changed files
with
669 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
// 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" | ||
"fmt" | ||
"sync" | ||
|
||
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/git" | ||
|
||
"github.com/ossf/scorecard/v5/clients" | ||
) | ||
|
||
type branchesHandler struct { | ||
gitClient git.Client | ||
ctx context.Context | ||
once *sync.Once | ||
errSetup error | ||
repourl *Repo | ||
defaultBranchRef *clients.BranchRef | ||
queryBranch fnQueryBranch | ||
} | ||
|
||
func (handler *branchesHandler) init(ctx context.Context, repourl *Repo) { | ||
handler.ctx = ctx | ||
handler.repourl = repourl | ||
handler.errSetup = nil | ||
handler.once = new(sync.Once) | ||
handler.queryBranch = handler.gitClient.GetBranch | ||
} | ||
|
||
type ( | ||
fnQueryBranch func(ctx context.Context, args git.GetBranchArgs) (*git.GitBranchStats, error) | ||
) | ||
|
||
func (handler *branchesHandler) setup() error { | ||
handler.once.Do(func() { | ||
branch, err := handler.queryBranch(handler.ctx, git.GetBranchArgs{ | ||
RepositoryId: &handler.repourl.id, | ||
Name: &handler.repourl.defaultBranch, | ||
}) | ||
if err != nil { | ||
handler.errSetup = fmt.Errorf("request for default branch failed with error %w", err) | ||
return | ||
} | ||
handler.defaultBranchRef = &clients.BranchRef{ | ||
Name: branch.Name, | ||
} | ||
|
||
handler.errSetup = nil | ||
}) | ||
return handler.errSetup | ||
} | ||
|
||
func (handler *branchesHandler) getDefaultBranch() (*clients.BranchRef, error) { | ||
err := handler.setup() | ||
if err != nil { | ||
return nil, fmt.Errorf("error during branchesHandler.setup: %w", err) | ||
} | ||
|
||
return handler.defaultBranchRef, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
// 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" | ||
"fmt" | ||
"sync" | ||
"testing" | ||
|
||
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/git" | ||
) | ||
|
||
func TestGetDefaultBranch(t *testing.T) { | ||
t.Parallel() | ||
tests := []struct { | ||
setupMock func() fnQueryBranch | ||
name string | ||
expectedName string | ||
expectedError bool | ||
}{ | ||
{ | ||
name: "successful branch retrieval", | ||
setupMock: func() fnQueryBranch { | ||
return func(ctx context.Context, args git.GetBranchArgs) (*git.GitBranchStats, error) { | ||
return &git.GitBranchStats{Name: args.Name}, nil | ||
} | ||
}, | ||
expectedError: false, | ||
expectedName: "main", | ||
}, | ||
{ | ||
name: "error during branch retrieval", | ||
setupMock: func() fnQueryBranch { | ||
return func(ctx context.Context, args git.GetBranchArgs) (*git.GitBranchStats, error) { | ||
return nil, fmt.Errorf("error") | ||
} | ||
}, | ||
expectedError: true, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
t.Parallel() | ||
handler := &branchesHandler{ | ||
queryBranch: tt.setupMock(), | ||
once: new(sync.Once), | ||
repourl: &Repo{ | ||
id: "repo-id", | ||
defaultBranch: "main", | ||
}, | ||
} | ||
|
||
branch, err := handler.getDefaultBranch() | ||
if (err != nil) != tt.expectedError { | ||
t.Errorf("expected error: %v, got: %v", tt.expectedError, err) | ||
} | ||
if branch != nil && *branch.Name != tt.expectedName { | ||
t.Errorf("expected branch name: %v, got: %v", tt.expectedName, *branch.Name) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,211 @@ | ||
// 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" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"os" | ||
"strings" | ||
"time" | ||
|
||
"github.com/microsoft/azure-devops-go-api/azuredevops/v7" | ||
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/git" | ||
|
||
"github.com/ossf/scorecard/v5/clients" | ||
) | ||
|
||
var ( | ||
_ clients.RepoClient = &Client{} | ||
errInputRepoType = errors.New("input repo should be of type azuredevopsrepo.Repo") | ||
errDefaultBranchNotFound = errors.New("default branch not found") | ||
) | ||
|
||
type Client struct { | ||
azdoClient git.Client | ||
ctx context.Context | ||
repourl *Repo | ||
repo *git.GitRepository | ||
branches *branchesHandler | ||
commits *commitsHandler | ||
commitDepth int | ||
} | ||
|
||
func (c *Client) InitRepo(inputRepo clients.Repo, commitSHA string, commitDepth int) error { | ||
azdoRepo, ok := inputRepo.(*Repo) | ||
if !ok { | ||
return fmt.Errorf("%w: %v", errInputRepoType, inputRepo) | ||
} | ||
|
||
repo, err := c.azdoClient.GetRepository(c.ctx, git.GetRepositoryArgs{ | ||
Project: &azdoRepo.project, | ||
RepositoryId: &azdoRepo.name, | ||
}) | ||
if err != nil { | ||
return fmt.Errorf("could not get repository with error: %w", err) | ||
} | ||
|
||
c.repo = repo | ||
|
||
if commitDepth <= 0 { | ||
c.commitDepth = 30 // default | ||
} else { | ||
c.commitDepth = commitDepth | ||
} | ||
|
||
branch := strings.TrimPrefix(*repo.DefaultBranch, "refs/heads/") | ||
|
||
c.repourl = &Repo{ | ||
scheme: azdoRepo.scheme, | ||
host: azdoRepo.host, | ||
organization: azdoRepo.organization, | ||
project: azdoRepo.project, | ||
name: azdoRepo.name, | ||
id: fmt.Sprint(*repo.Id), | ||
defaultBranch: branch, | ||
commitSHA: commitSHA, | ||
} | ||
|
||
c.branches.init(c.ctx, c.repourl) | ||
|
||
c.commits.init(c.ctx, c.repourl, c.commitDepth) | ||
|
||
return nil | ||
} | ||
|
||
func (c *Client) URI() string { | ||
return c.repourl.URI() | ||
} | ||
|
||
func (c *Client) IsArchived() (bool, error) { | ||
return *c.repo.IsDisabled, nil | ||
} | ||
|
||
func (c *Client) ListFiles(predicate func(string) (bool, error)) ([]string, error) { | ||
return []string{}, clients.ErrUnsupportedFeature | ||
} | ||
|
||
func (c *Client) LocalPath() (string, error) { | ||
return "", clients.ErrUnsupportedFeature | ||
} | ||
|
||
func (c *Client) GetFileReader(filename string) (io.ReadCloser, error) { | ||
return nil, clients.ErrUnsupportedFeature | ||
} | ||
|
||
func (c *Client) GetBranch(branch string) (*clients.BranchRef, error) { | ||
return nil, clients.ErrUnsupportedFeature | ||
} | ||
|
||
func (c *Client) GetCreatedAt() (time.Time, error) { | ||
return time.Time{}, clients.ErrUnsupportedFeature | ||
} | ||
|
||
func (c *Client) GetDefaultBranchName() (string, error) { | ||
if len(c.repourl.defaultBranch) > 0 { | ||
return c.repourl.defaultBranch, nil | ||
} | ||
|
||
return "", errDefaultBranchNotFound | ||
} | ||
|
||
func (c *Client) GetDefaultBranch() (*clients.BranchRef, error) { | ||
return c.branches.getDefaultBranch() | ||
} | ||
|
||
func (c *Client) GetOrgRepoClient(context.Context) (clients.RepoClient, error) { | ||
return nil, clients.ErrUnsupportedFeature | ||
} | ||
|
||
func (c *Client) ListCommits() ([]clients.Commit, error) { | ||
return c.commits.listCommits() | ||
} | ||
|
||
func (c *Client) ListIssues() ([]clients.Issue, error) { | ||
return nil, clients.ErrUnsupportedFeature | ||
} | ||
|
||
func (c *Client) ListLicenses() ([]clients.License, error) { | ||
return nil, clients.ErrUnsupportedFeature | ||
} | ||
|
||
func (c *Client) ListReleases() ([]clients.Release, error) { | ||
return nil, clients.ErrUnsupportedFeature | ||
} | ||
|
||
func (c *Client) ListContributors() ([]clients.User, error) { | ||
return nil, clients.ErrUnsupportedFeature | ||
} | ||
|
||
func (c *Client) ListSuccessfulWorkflowRuns(filename string) ([]clients.WorkflowRun, error) { | ||
return nil, clients.ErrUnsupportedFeature | ||
} | ||
|
||
func (c *Client) ListCheckRunsForRef(ref string) ([]clients.CheckRun, error) { | ||
return nil, clients.ErrUnsupportedFeature | ||
} | ||
|
||
func (c *Client) ListStatuses(ref string) ([]clients.Status, error) { | ||
return nil, clients.ErrUnsupportedFeature | ||
} | ||
|
||
func (c *Client) ListWebhooks() ([]clients.Webhook, error) { | ||
return nil, clients.ErrUnsupportedFeature | ||
} | ||
|
||
func (c *Client) ListProgrammingLanguages() ([]clients.Language, error) { | ||
return nil, clients.ErrUnsupportedFeature | ||
} | ||
|
||
func (c *Client) Search(request clients.SearchRequest) (clients.SearchResponse, error) { | ||
return clients.SearchResponse{}, clients.ErrUnsupportedFeature | ||
} | ||
|
||
func (c *Client) SearchCommits(request clients.SearchCommitsOptions) ([]clients.Commit, error) { | ||
return nil, clients.ErrUnsupportedFeature | ||
} | ||
|
||
func (c *Client) Close() error { | ||
return nil | ||
} | ||
|
||
func CreateAzureDevOpsClient(ctx context.Context, repo clients.Repo) (*Client, error) { | ||
token := os.Getenv("AZURE_DEVOPS_AUTH_TOKEN") | ||
return CreateAzureDevOpsClientWithToken(ctx, token, repo) | ||
} | ||
|
||
func CreateAzureDevOpsClientWithToken(ctx context.Context, token string, repo clients.Repo) (*Client, error) { | ||
// https://dev.azure.com/<org> | ||
url := "https://" + repo.Host() + "/" + strings.Split(repo.Path(), "/")[0] | ||
connection := azuredevops.NewPatConnection(url, token) | ||
|
||
gitClient, err := git.NewClient(ctx, connection) | ||
if err != nil { | ||
return nil, fmt.Errorf("could not create azure devops git client with error: %w", err) | ||
} | ||
|
||
return &Client{ | ||
ctx: ctx, | ||
azdoClient: gitClient, | ||
branches: &branchesHandler{ | ||
gitClient: gitClient, | ||
}, | ||
commits: &commitsHandler{ | ||
gitClient: gitClient, | ||
}, | ||
}, nil | ||
} |
Oops, something went wrong.