• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jfrog / froggit-go / 15140282759

20 May 2025 02:29PM UTC coverage: 85.324%. First build
15140282759

Pull #153

github

eyalk007
added merge pull request
Pull Request #153: Added support for all needed functions of global installation of Frogbot

177 of 327 new or added lines in 5 files covered. (54.13%)

4378 of 5131 relevant lines covered (85.32%)

6.49 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.69
/vcsclient/azurerepos.go
1
package vcsclient
2

3
import (
4
        "bufio"
5
        "context"
6
        "errors"
7
        "fmt"
8
        "github.com/go-git/go-git/v5/plumbing"
9
        "github.com/jfrog/froggit-go/vcsutils"
10
        "github.com/jfrog/gofrog/datastructures"
11
        "github.com/microsoft/azure-devops-go-api/azuredevops/v7"
12
        "github.com/microsoft/azure-devops-go-api/azuredevops/v7/core"
13
        "github.com/microsoft/azure-devops-go-api/azuredevops/v7/git"
14
        "io"
15
        "net/http"
16
        "os"
17
        "sort"
18
        "strconv"
19
        "strings"
20
        "time"
21
)
22

23
const (
24
        notSupportedOnAzure              = "currently not supported on Azure"
25
        defaultAzureBaseUrl              = "https://dev.azure.com/"
26
        azurePullRequestDetailsSizeLimit = 4000
27
        azurePullRequestCommentSizeLimit = 150000
28
)
29

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

32
// Azure Devops API version 6
33
type AzureReposClient struct {
34
        vcsInfo           VcsInfo
35
        connectionDetails *azuredevops.Connection
36
        logger            vcsutils.Log
37
}
38

39
// NewAzureReposClient create a new AzureReposClient
40
func NewAzureReposClient(vcsInfo VcsInfo, logger vcsutils.Log) (*AzureReposClient, error) {
58✔
41
        client := &AzureReposClient{vcsInfo: vcsInfo, logger: logger}
58✔
42
        baseUrl := strings.TrimSuffix(client.vcsInfo.APIEndpoint, "/")
58✔
43
        client.connectionDetails = azuredevops.NewPatConnection(baseUrl, client.vcsInfo.Token)
58✔
44
        return client, nil
58✔
45
}
58✔
46

47
func (client *AzureReposClient) buildAzureReposClient(ctx context.Context) (git.Client, error) {
46✔
48
        if client.connectionDetails == nil {
46✔
49
                return nil, errors.New("connection details wasn't initialized")
×
50
        }
×
51
        return git.NewClient(ctx, client.connectionDetails)
46✔
52
}
53

54
// TestConnection on Azure Repos
55
func (client *AzureReposClient) TestConnection(ctx context.Context) error {
1✔
56
        buildClient := azuredevops.NewClient(client.connectionDetails, client.connectionDetails.BaseUrl)
1✔
57
        _, err := buildClient.GetResourceAreas(ctx)
1✔
58
        return err
1✔
59
}
1✔
60

61
// ListRepositories on Azure Repos
62
func (client *AzureReposClient) ListRepositories(ctx context.Context) (map[string][]string, error) {
2✔
63
        azureReposGitClient, err := client.buildAzureReposClient(ctx)
2✔
64
        if err != nil {
2✔
65
                return nil, err
×
66
        }
×
67
        repositories := make(map[string][]string)
2✔
68
        resp, err := azureReposGitClient.GetRepositories(ctx, git.GetRepositoriesArgs{Project: &client.vcsInfo.Project})
2✔
69
        if err != nil {
3✔
70
                return repositories, err
1✔
71
        }
1✔
72
        for _, repo := range *resp {
3✔
73
                repositories[client.vcsInfo.Project] = append(repositories[client.vcsInfo.Project], *repo.Name)
2✔
74
        }
2✔
75
        return repositories, nil
1✔
76
}
77

78
// ListBranches on Azure Repos
79
func (client *AzureReposClient) ListBranches(ctx context.Context, _, repository string) ([]string, error) {
2✔
80
        azureReposGitClient, err := client.buildAzureReposClient(ctx)
2✔
81
        if err != nil {
2✔
82
                return nil, err
×
83
        }
×
84
        var branches []string
2✔
85
        gitBranchStats, err := azureReposGitClient.GetBranches(ctx, git.GetBranchesArgs{Project: &client.vcsInfo.Project, RepositoryId: &repository})
2✔
86
        if err != nil {
3✔
87
                return nil, err
1✔
88
        }
1✔
89
        for _, branch := range *gitBranchStats {
3✔
90
                branches = append(branches, *branch.Name)
2✔
91
        }
2✔
92
        return branches, nil
1✔
93
}
94

95
// DownloadRepository on Azure Repos
96
func (client *AzureReposClient) DownloadRepository(ctx context.Context, owner, repository, branch, localPath string) (err error) {
2✔
97
        wd, err := os.Getwd()
2✔
98
        if err != nil {
2✔
99
                return
×
100
        }
×
101
        // Changing dir to localPath will download the repository there.
102
        if err = os.Chdir(localPath); err != nil {
2✔
103
                return
×
104
        }
×
105
        defer func() {
4✔
106
                err = errors.Join(err, os.Chdir(wd))
2✔
107
        }()
2✔
108
        res, err := client.sendDownloadRepoRequest(ctx, repository, branch)
2✔
109
        defer func() {
4✔
110
                if res.Body != nil {
3✔
111
                        err = errors.Join(err, res.Body.Close())
1✔
112
                }
1✔
113
        }()
114
        if err != nil {
3✔
115
                return
1✔
116
        }
1✔
117
        zipFileContent, err := io.ReadAll(res.Body)
1✔
118
        if err != nil {
1✔
119
                return
×
120
        }
×
121
        err = vcsutils.Unzip(zipFileContent, localPath)
1✔
122
        if err != nil {
1✔
123
                return err
×
124
        }
×
125
        client.logger.Info(vcsutils.SuccessfulRepoExtraction)
1✔
126
        repoInfo, err := client.GetRepositoryInfo(ctx, owner, repository)
1✔
127
        if err != nil {
1✔
128
                return err
×
129
        }
×
130
        httpsCloneUrl := repoInfo.CloneInfo.HTTP
1✔
131
        // Generate .git folder with remote details
1✔
132
        return vcsutils.CreateDotGitFolderWithRemote(
1✔
133
                localPath,
1✔
134
                vcsutils.RemoteName,
1✔
135
                httpsCloneUrl)
1✔
136
}
137

138
func (client *AzureReposClient) sendDownloadRepoRequest(ctx context.Context, repository string, branch string) (res *http.Response, err error) {
2✔
139
        downloadRepoUrl := fmt.Sprintf("%s/%s/_apis/git/repositories/%s/items/items?path=/&versionDescriptor[version]=%s&$format=zip",
2✔
140
                client.connectionDetails.BaseUrl,
2✔
141
                client.vcsInfo.Project,
2✔
142
                repository,
2✔
143
                branch)
2✔
144
        client.logger.Debug("Download url:", downloadRepoUrl)
2✔
145
        headers := map[string]string{
2✔
146
                "Authorization":  client.connectionDetails.AuthorizationString,
2✔
147
                "download":       "true",
2✔
148
                "resolveLfs":     "true",
2✔
149
                "includeContent": "true",
2✔
150
        }
2✔
151
        httpClient := &http.Client{}
2✔
152
        var req *http.Request
2✔
153
        if req, err = http.NewRequestWithContext(ctx, http.MethodGet, downloadRepoUrl, nil); err != nil {
2✔
154
                return
×
155
        }
×
156
        for key, val := range headers {
10✔
157
                req.Header.Add(key, val)
8✔
158
        }
8✔
159
        if res, err = httpClient.Do(req); err != nil {
2✔
160
                return
×
161
        }
×
162
        if err = vcsutils.CheckResponseStatusWithBody(res, http.StatusOK); err != nil {
3✔
163
                return &http.Response{}, err
1✔
164
        }
1✔
165
        client.logger.Info(repository, vcsutils.SuccessfulRepoDownload)
1✔
166
        return
1✔
167
}
168

169
func (client *AzureReposClient) GetPullRequestCommentSizeLimit() int {
×
170
        return azurePullRequestCommentSizeLimit
×
171
}
×
172

173
func (client *AzureReposClient) GetPullRequestDetailsSizeLimit() int {
×
174
        return azurePullRequestDetailsSizeLimit
×
175
}
×
176

177
// CreatePullRequest on Azure Repos
178
func (client *AzureReposClient) CreatePullRequest(ctx context.Context, _, repository, sourceBranch, targetBranch, title, description string) error {
2✔
179
        azureReposGitClient, err := client.buildAzureReposClient(ctx)
2✔
180
        if err != nil {
2✔
181
                return err
×
182
        }
×
183
        sourceBranch = vcsutils.AddBranchPrefix(sourceBranch)
2✔
184
        targetBranch = vcsutils.AddBranchPrefix(targetBranch)
2✔
185
        client.logger.Debug(vcsutils.CreatingPullRequest, title)
2✔
186
        _, err = azureReposGitClient.CreatePullRequest(ctx, git.CreatePullRequestArgs{
2✔
187
                GitPullRequestToCreate: &git.GitPullRequest{
2✔
188
                        Description:   &description,
2✔
189
                        SourceRefName: &sourceBranch,
2✔
190
                        TargetRefName: &targetBranch,
2✔
191
                        Title:         &title,
2✔
192
                },
2✔
193
                RepositoryId: &repository,
2✔
194
                Project:      &client.vcsInfo.Project,
2✔
195
        })
2✔
196
        return err
2✔
197
}
198

199
// UpdatePullRequest on Azure Repos
200
func (client *AzureReposClient) UpdatePullRequest(ctx context.Context, _, repository, title, body, targetBranchName string, prId int, state vcsutils.PullRequestState) error {
4✔
201
        azureReposGitClient, err := client.buildAzureReposClient(ctx)
4✔
202
        if err != nil {
4✔
203
                return err
×
204
        }
×
205
        targetBranchName = vcsutils.AddBranchPrefix(targetBranchName)
4✔
206
        client.logger.Debug(vcsutils.UpdatingPullRequest, prId)
4✔
207
        _, err = azureReposGitClient.UpdatePullRequest(ctx, git.UpdatePullRequestArgs{
4✔
208
                GitPullRequestToUpdate: &git.GitPullRequest{
4✔
209
                        Description:   vcsutils.GetNilIfZeroVal(body),
4✔
210
                        Status:        azureMapPullRequestState(state),
4✔
211
                        TargetRefName: vcsutils.GetNilIfZeroVal(targetBranchName),
4✔
212
                        Title:         vcsutils.GetNilIfZeroVal(title),
4✔
213
                },
4✔
214
                RepositoryId:  vcsutils.GetNilIfZeroVal(repository),
4✔
215
                PullRequestId: vcsutils.GetNilIfZeroVal(prId),
4✔
216
                Project:       vcsutils.GetNilIfZeroVal(client.vcsInfo.Project),
4✔
217
        })
4✔
218
        return err
4✔
219
}
220

221
// AddPullRequestComment on Azure Repos
222
func (client *AzureReposClient) AddPullRequestComment(ctx context.Context, _, repository, content string, pullRequestID int) error {
2✔
223
        return client.addPullRequestComment(ctx, repository, pullRequestID, PullRequestComment{CommentInfo: CommentInfo{Content: content}})
2✔
224
}
2✔
225

226
// AddPullRequestReviewComments on Azure Repos
227
func (client *AzureReposClient) AddPullRequestReviewComments(ctx context.Context, _, repository string, pullRequestID int, comments ...PullRequestComment) error {
2✔
228
        if len(comments) == 0 {
2✔
229
                return errors.New(vcsutils.ErrNoCommentsProvided)
×
230
        }
×
231
        for _, comment := range comments {
4✔
232
                if err := client.addPullRequestComment(ctx, repository, pullRequestID, comment); err != nil {
3✔
233
                        return err
1✔
234
                }
1✔
235
        }
236
        return nil
1✔
237
}
238

239
func (client *AzureReposClient) addPullRequestComment(ctx context.Context, repository string, pullRequestID int, comment PullRequestComment) error {
4✔
240
        azureReposGitClient, err := client.buildAzureReposClient(ctx)
4✔
241
        if err != nil {
4✔
242
                return err
×
243
        }
×
244
        threadArgs := getThreadArgs(repository, client.vcsInfo.Project, pullRequestID, comment)
4✔
245
        _, err = azureReposGitClient.CreateThread(ctx, threadArgs)
4✔
246
        return err
4✔
247
}
248

249
func getThreadArgs(repository, project string, prId int, comment PullRequestComment) git.CreateThreadArgs {
4✔
250
        filePath := vcsutils.GetPullRequestFilePath(comment.NewFilePath)
4✔
251
        return git.CreateThreadArgs{
4✔
252
                CommentThread: &git.GitPullRequestCommentThread{
4✔
253
                        Comments: &[]git.Comment{{Content: &comment.Content}},
4✔
254
                        Status:   &git.CommentThreadStatusValues.Active,
4✔
255
                        ThreadContext: &git.CommentThreadContext{
4✔
256
                                FilePath:       &filePath,
4✔
257
                                LeftFileStart:  &git.CommentPosition{Line: &comment.OriginalStartLine, Offset: &comment.OriginalStartColumn},
4✔
258
                                LeftFileEnd:    &git.CommentPosition{Line: &comment.OriginalEndLine, Offset: &comment.OriginalEndColumn},
4✔
259
                                RightFileStart: &git.CommentPosition{Line: &comment.NewStartLine, Offset: &comment.NewStartColumn},
4✔
260
                                RightFileEnd:   &git.CommentPosition{Line: &comment.NewEndLine, Offset: &comment.NewEndColumn},
4✔
261
                        },
4✔
262
                },
4✔
263
                RepositoryId:  &repository,
4✔
264
                PullRequestId: &prId,
4✔
265
                Project:       &project,
4✔
266
        }
4✔
267
}
4✔
268

269
func (client *AzureReposClient) ListPullRequestReviews(ctx context.Context, owner, repository string, pullRequestID int) ([]PullRequestReviewDetails, error) {
1✔
270
        azureReposGitClient, err := client.buildAzureReposClient(ctx)
1✔
271
        if err != nil {
1✔
272
                return nil, err
×
273
        }
×
274

275
        reviewers, err := azureReposGitClient.GetPullRequestReviewers(ctx, git.GetPullRequestReviewersArgs{
1✔
276
                RepositoryId:  &repository,
1✔
277
                PullRequestId: &pullRequestID,
1✔
278
                Project:       &client.vcsInfo.Project,
1✔
279
        })
1✔
280
        if err != nil {
1✔
281
                return nil, err
×
282
        }
×
283

284
        var reviews []PullRequestReviewDetails
1✔
285
        for _, reviewer := range *reviewers {
3✔
286
                id, err := strconv.ParseInt(*reviewer.Id, 10, 64)
2✔
287
                if err != nil {
2✔
288
                        return nil, err
×
289
                }
×
290
                reviews = append(reviews, PullRequestReviewDetails{
2✔
291
                        ID:       id,
2✔
292
                        Reviewer: *reviewer.DisplayName,
2✔
293
                        State:    mapVoteToState(*reviewer.Vote),
2✔
294
                })
2✔
295
        }
296

297
        return reviews, nil
1✔
298
}
299

300
func (client *AzureReposClient) ListPullRequestsAssociatedWithCommit(ctx context.Context, owner, repository string, commitSHA string) ([]PullRequestInfo, error) {
1✔
301
        return nil, getUnsupportedInAzureError("list pull requests associated with commit")
1✔
302
}
1✔
303

304
// ListPullRequestReviewComments on Azure Repos
305
func (client *AzureReposClient) ListPullRequestReviewComments(ctx context.Context, owner, repository string, pullRequestID int) ([]CommentInfo, error) {
×
306
        return client.ListPullRequestComments(ctx, owner, repository, pullRequestID)
×
307
}
×
308

309
// ListPullRequestComments on Azure Repos
310
func (client *AzureReposClient) ListPullRequestComments(ctx context.Context, _, repository string, pullRequestID int) ([]CommentInfo, error) {
4✔
311
        azureReposGitClient, err := client.buildAzureReposClient(ctx)
4✔
312
        if err != nil {
4✔
313
                return nil, err
×
314
        }
×
315
        threads, err := azureReposGitClient.GetThreads(ctx, git.GetThreadsArgs{
4✔
316
                RepositoryId:  &repository,
4✔
317
                PullRequestId: &pullRequestID,
4✔
318
                Project:       &client.vcsInfo.Project,
4✔
319
        })
4✔
320
        if err != nil {
6✔
321
                return nil, err
2✔
322
        }
2✔
323
        var commentInfo []CommentInfo
2✔
324
        for _, thread := range *threads {
4✔
325
                if thread.IsDeleted != nil && *thread.IsDeleted {
2✔
326
                        continue
×
327
                }
328
                var commentsAggregator strings.Builder
2✔
329
                for _, comment := range *thread.Comments {
6✔
330
                        if comment.IsDeleted != nil && *comment.IsDeleted {
4✔
331
                                continue
×
332
                        }
333
                        _, err = commentsAggregator.WriteString(
4✔
334
                                fmt.Sprintf("Author: %s, Id: %d, Content:%s\n",
4✔
335
                                        *comment.Author.DisplayName,
4✔
336
                                        *comment.Id,
4✔
337
                                        *comment.Content))
4✔
338
                        if err != nil {
4✔
339
                                return nil, err
×
340
                        }
×
341
                }
342
                commentInfo = append(commentInfo, CommentInfo{
2✔
343
                        ID:      int64(*thread.Id),
2✔
344
                        Created: thread.PublishedDate.Time,
2✔
345
                        Content: commentsAggregator.String(),
2✔
346
                })
2✔
347
        }
348
        return commentInfo, nil
2✔
349
}
350

351
// DeletePullRequestReviewComments on Azure Repos
352
func (client *AzureReposClient) DeletePullRequestReviewComments(ctx context.Context, owner, repository string, pullRequestID int, comments ...CommentInfo) error {
2✔
353
        for _, comment := range comments {
5✔
354
                if err := client.DeletePullRequestComment(ctx, owner, repository, pullRequestID, int(comment.ID)); err != nil {
4✔
355
                        return err
1✔
356
                }
1✔
357
        }
358
        return nil
1✔
359
}
360

361
// DeletePullRequestComment on Azure Repos
362
func (client *AzureReposClient) DeletePullRequestComment(ctx context.Context, _, repository string, pullRequestID, commentID int) error {
5✔
363
        azureReposGitClient, err := client.buildAzureReposClient(ctx)
5✔
364
        if err != nil {
5✔
365
                return err
×
366
        }
×
367
        firstCommentInThreadID := 1
5✔
368
        return azureReposGitClient.DeleteComment(ctx, git.DeleteCommentArgs{
5✔
369
                RepositoryId:  &repository,
5✔
370
                PullRequestId: &pullRequestID,
5✔
371
                ThreadId:      &commentID,
5✔
372
                Project:       &client.vcsInfo.Project,
5✔
373
                CommentId:     &firstCommentInThreadID,
5✔
374
        })
5✔
375
}
376

377
// ListOpenPullRequestsWithBody on Azure Repos
378
func (client *AzureReposClient) ListOpenPullRequestsWithBody(ctx context.Context, owner, repository string) ([]PullRequestInfo, error) {
1✔
379
        return client.getOpenPullRequests(ctx, owner, repository, true)
1✔
380
}
1✔
381

382
// ListOpenPullRequests on Azure Repos
383
func (client *AzureReposClient) ListOpenPullRequests(ctx context.Context, owner, repository string) ([]PullRequestInfo, error) {
3✔
384
        return client.getOpenPullRequests(ctx, owner, repository, false)
3✔
385
}
3✔
386

387
func (client *AzureReposClient) getOpenPullRequests(ctx context.Context, owner, repository string, withBody bool) ([]PullRequestInfo, error) {
4✔
388
        azureReposGitClient, err := client.buildAzureReposClient(ctx)
4✔
389
        if err != nil {
4✔
390
                return nil, err
×
391
        }
×
392
        client.logger.Debug(vcsutils.FetchingOpenPullRequests, repository)
4✔
393
        pullRequests, err := azureReposGitClient.GetPullRequests(ctx, git.GetPullRequestsArgs{
4✔
394
                RepositoryId:   &repository,
4✔
395
                Project:        &client.vcsInfo.Project,
4✔
396
                SearchCriteria: &git.GitPullRequestSearchCriteria{Status: &git.PullRequestStatusValues.Active},
4✔
397
        })
4✔
398
        if err != nil {
6✔
399
                return nil, err
2✔
400
        }
2✔
401
        var pullRequestsInfo []PullRequestInfo
2✔
402
        for _, pullRequest := range *pullRequests {
4✔
403
                pullRequestDetails := parsePullRequestDetails(client, pullRequest, owner, repository, withBody)
2✔
404
                pullRequestsInfo = append(pullRequestsInfo, pullRequestDetails)
2✔
405
        }
2✔
406
        return pullRequestsInfo, nil
2✔
407
}
408

409
// GetPullRequestById in Azure Repos
410
func (client *AzureReposClient) GetPullRequestByID(ctx context.Context, owner, repository string, pullRequestId int) (pullRequestInfo PullRequestInfo, err error) {
3✔
411
        azureReposGitClient, err := client.buildAzureReposClient(ctx)
3✔
412
        if err != nil {
3✔
413
                return
×
414
        }
×
415
        client.logger.Debug(vcsutils.FetchingPullRequestById, repository)
3✔
416
        pullRequest, err := azureReposGitClient.GetPullRequestById(ctx, git.GetPullRequestByIdArgs{
3✔
417
                PullRequestId: &pullRequestId,
3✔
418
                Project:       &client.vcsInfo.Project,
3✔
419
        })
3✔
420
        if err != nil {
4✔
421
                return
1✔
422
        }
1✔
423
        pullRequestInfo = parsePullRequestDetails(client, *pullRequest, owner, repository, false)
2✔
424
        return
2✔
425
}
426

427
// GetLatestCommit on Azure Repos
428
func (client *AzureReposClient) GetLatestCommit(ctx context.Context, _, repository, branch string) (CommitInfo, error) {
2✔
429
        commitsInfo, err := client.GetCommits(ctx, "", repository, branch)
2✔
430
        if err != nil {
3✔
431
                return CommitInfo{}, err
1✔
432
        }
1✔
433

434
        var latestCommit CommitInfo
1✔
435
        if len(commitsInfo) > 0 {
2✔
436
                latestCommit = commitsInfo[0]
1✔
437
        }
1✔
438
        return latestCommit, nil
1✔
439
}
440

441
// GetCommits on Azure Repos
442
func (client *AzureReposClient) GetCommits(ctx context.Context, _, repository, branch string) ([]CommitInfo, error) {
4✔
443
        azureReposGitClient, err := client.buildAzureReposClient(ctx)
4✔
444
        if err != nil {
4✔
445
                return nil, err
×
446
        }
×
447
        commits, err := azureReposGitClient.GetCommits(ctx, git.GetCommitsArgs{
4✔
448
                RepositoryId:   &repository,
4✔
449
                Project:        &client.vcsInfo.Project,
4✔
450
                SearchCriteria: &git.GitQueryCommitsCriteria{ItemVersion: &git.GitVersionDescriptor{Version: &branch, VersionType: &git.GitVersionTypeValues.Branch}},
4✔
451
        })
4✔
452
        if err != nil {
6✔
453
                return nil, err
2✔
454
        }
2✔
455
        if commits == nil {
2✔
456
                return nil, fmt.Errorf("could not retrieve commits for <%s/%s>", repository, branch)
×
457
        }
×
458

459
        var commitsInfo []CommitInfo
2✔
460
        for _, commit := range *commits {
8✔
461
                commitInfo := mapAzureReposCommitsToCommitInfo(commit)
6✔
462
                commitsInfo = append(commitsInfo, commitInfo)
6✔
463
        }
6✔
464
        return commitsInfo, nil
2✔
465
}
466

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

471
func mapAzureReposCommitsToCommitInfo(commit git.GitCommitRef) CommitInfo {
6✔
472
        var authorName, authorEmail string
6✔
473
        if commit.Author != nil {
12✔
474
                authorName = vcsutils.DefaultIfNotNil(commit.Author.Name)
6✔
475
                authorEmail = vcsutils.DefaultIfNotNil(commit.Author.Email)
6✔
476
        }
6✔
477
        var committerName string
6✔
478
        var timestamp int64
6✔
479
        if commit.Committer != nil {
12✔
480
                committerName = vcsutils.DefaultIfNotNil(commit.Committer.Name)
6✔
481
                timestamp = vcsutils.DefaultIfNotNil(commit.Committer.Date).Time.Unix()
6✔
482
        }
6✔
483
        return CommitInfo{
6✔
484
                Hash:          vcsutils.DefaultIfNotNil(commit.CommitId),
6✔
485
                AuthorName:    authorName,
6✔
486
                CommitterName: committerName,
6✔
487
                Url:           vcsutils.DefaultIfNotNil(commit.Url),
6✔
488
                Timestamp:     timestamp,
6✔
489
                Message:       vcsutils.DefaultIfNotNil(commit.Comment),
6✔
490
                ParentHashes:  vcsutils.DefaultIfNotNil(commit.Parents),
6✔
491
                AuthorEmail:   authorEmail,
6✔
492
        }
6✔
493
}
494

495
func getUnsupportedInAzureError(functionName string) error {
14✔
496
        return fmt.Errorf("%s is currently not supported for Azure Repos", functionName)
14✔
497
}
14✔
498

499
// AddSshKeyToRepository on Azure Repos
500
func (client *AzureReposClient) AddSshKeyToRepository(ctx context.Context, owner, repository, keyName, publicKey string, permission Permission) error {
1✔
501
        return getUnsupportedInAzureError("add ssh key to repository")
1✔
502
}
1✔
503

504
// GetRepositoryInfo on Azure Repos
505
func (client *AzureReposClient) GetRepositoryInfo(ctx context.Context, owner, repository string) (RepositoryInfo, error) {
2✔
506
        azureReposGitClient, err := client.buildAzureReposClient(ctx)
2✔
507
        if err != nil {
2✔
508
                return RepositoryInfo{}, err
×
509
        }
×
510
        response, err := azureReposGitClient.GetRepository(ctx, git.GetRepositoryArgs{
2✔
511
                RepositoryId: &repository,
2✔
512
                Project:      &client.vcsInfo.Project,
2✔
513
        })
2✔
514
        if err != nil {
2✔
515
                return RepositoryInfo{}, fmt.Errorf("an error occured while retrieving <%s/%s/%s> repository info:\n%s", owner, client.vcsInfo.Project, repository, err.Error())
×
516
        }
×
517
        if response == nil {
2✔
518
                return RepositoryInfo{}, fmt.Errorf("failed to retreive <%s/%s/%s> repository info, received empty response", owner, client.vcsInfo.Project, repository)
×
519
        }
×
520
        if response.Project == nil {
2✔
521
                return RepositoryInfo{}, fmt.Errorf("failed to retreive <%s/%s/%s> repository info, received empty project info", owner, client.vcsInfo.Project, repository)
×
522
        }
×
523

524
        visibility := Private
2✔
525
        visibilityFromResponse := *response.Project.Visibility
2✔
526
        if visibilityFromResponse == core.ProjectVisibilityValues.Public {
4✔
527
                visibility = Public
2✔
528
        }
2✔
529
        return RepositoryInfo{
2✔
530
                CloneInfo:            CloneInfo{HTTP: *response.RemoteUrl, SSH: *response.SshUrl},
2✔
531
                RepositoryVisibility: visibility,
2✔
532
        }, nil
2✔
533
}
534

535
// GetCommitBySha on Azure Repos
536
func (client *AzureReposClient) GetCommitBySha(ctx context.Context, owner, repository, sha string) (CommitInfo, error) {
1✔
537
        return CommitInfo{}, getUnsupportedInAzureError("get commit by sha")
1✔
538
}
1✔
539

540
// CreateLabel on Azure Repos
541
func (client *AzureReposClient) CreateLabel(ctx context.Context, owner, repository string, labelInfo LabelInfo) error {
1✔
542
        return getUnsupportedInAzureError("create label")
1✔
543
}
1✔
544

545
// GetLabel on Azure Repos
546
func (client *AzureReposClient) GetLabel(ctx context.Context, owner, repository, name string) (*LabelInfo, error) {
1✔
547
        return nil, getUnsupportedInAzureError("get label")
1✔
548
}
1✔
549

550
// ListPullRequestLabels on Azure Repos
551
func (client *AzureReposClient) ListPullRequestLabels(ctx context.Context, owner, repository string, pullRequestID int) ([]string, error) {
1✔
552
        return nil, getUnsupportedInAzureError("list pull request labels")
1✔
553
}
1✔
554

555
// UnlabelPullRequest on Azure Repos
556
func (client *AzureReposClient) UnlabelPullRequest(ctx context.Context, owner, repository, name string, pullRequestID int) error {
1✔
557
        return getUnsupportedInAzureError("unlabel pull request")
1✔
558
}
1✔
559

560
// UploadCodeScanning on Azure Repos
561
func (client *AzureReposClient) UploadCodeScanning(ctx context.Context, owner, repository, branch, scanResults string) (string, error) {
1✔
562
        return "", getUnsupportedInAzureError("upload code scanning")
1✔
563
}
1✔
564

565
// CreateWebhook on Azure Repos
566
func (client *AzureReposClient) CreateWebhook(ctx context.Context, owner, repository, branch, payloadURL string, webhookEvents ...vcsutils.WebhookEvent) (string, string, error) {
1✔
567
        return "", "", getUnsupportedInAzureError("create webhook")
1✔
568
}
1✔
569

570
// UpdateWebhook on Azure Repos
571
func (client *AzureReposClient) UpdateWebhook(ctx context.Context, owner, repository, branch, payloadURL, token, webhookID string, webhookEvents ...vcsutils.WebhookEvent) error {
1✔
572
        return getUnsupportedInAzureError("update webhook")
1✔
573
}
1✔
574

575
// DeleteWebhook on Azure Repos
576
func (client *AzureReposClient) DeleteWebhook(ctx context.Context, owner, repository, webhookID string) error {
1✔
577
        return getUnsupportedInAzureError("delete webhook")
1✔
578
}
1✔
579

580
// SetCommitStatus on Azure Repos
581
func (client *AzureReposClient) SetCommitStatus(ctx context.Context, commitStatus CommitStatus, owner, repository, ref, title, description, detailsURL string) error {
2✔
582
        azureReposGitClient, err := client.buildAzureReposClient(ctx)
2✔
583
        if err != nil {
2✔
584
                return err
×
585
        }
×
586
        statusState := git.GitStatusState(mapStatusToString(commitStatus))
2✔
587
        commitStatusArgs := git.CreateCommitStatusArgs{
2✔
588
                GitCommitStatusToCreate: &git.GitStatus{
2✔
589
                        Description: &description,
2✔
590
                        State:       &statusState,
2✔
591
                        TargetUrl:   &detailsURL,
2✔
592
                        Context: &git.GitStatusContext{
2✔
593
                                Name:  &owner,
2✔
594
                                Genre: &title,
2✔
595
                        },
2✔
596
                },
2✔
597
                CommitId:     &ref,
2✔
598
                RepositoryId: &repository,
2✔
599
                Project:      &client.vcsInfo.Project,
2✔
600
        }
2✔
601
        _, err = azureReposGitClient.CreateCommitStatus(ctx, commitStatusArgs)
2✔
602
        return err
2✔
603
}
604

605
// GetCommitStatuses on Azure Repos
606
func (client *AzureReposClient) GetCommitStatuses(ctx context.Context, owner, repository, ref string) (status []CommitStatusInfo, err error) {
3✔
607
        azureReposGitClient, err := client.buildAzureReposClient(ctx)
3✔
608
        if err != nil {
3✔
609
                return nil, err
×
610
        }
×
611
        commitStatusArgs := git.GetStatusesArgs{
3✔
612
                CommitId:     &ref,
3✔
613
                RepositoryId: &repository,
3✔
614
                Project:      &client.vcsInfo.Project,
3✔
615
        }
3✔
616
        resGitStatus, err := azureReposGitClient.GetStatuses(ctx, commitStatusArgs)
3✔
617
        if err != nil {
4✔
618
                return nil, err
1✔
619
        }
1✔
620
        results := make([]CommitStatusInfo, 0)
2✔
621
        for _, singleStatus := range *resGitStatus {
5✔
622
                results = append(results, CommitStatusInfo{
3✔
623
                        State:         commitStatusAsStringToStatus(string(*singleStatus.State)),
3✔
624
                        Description:   *singleStatus.Description,
3✔
625
                        DetailsUrl:    *singleStatus.TargetUrl,
3✔
626
                        Creator:       *singleStatus.CreatedBy.DisplayName,
3✔
627
                        LastUpdatedAt: extractTimeFromAzuredevopsTime(singleStatus.UpdatedDate),
3✔
628
                        CreatedAt:     extractTimeFromAzuredevopsTime(singleStatus.CreationDate),
3✔
629
                })
3✔
630
        }
3✔
631
        return results, err
2✔
632
}
633

634
// DownloadFileFromRepo on Azure Repos
635
func (client *AzureReposClient) DownloadFileFromRepo(ctx context.Context, owner, repository, branch, path string) ([]byte, int, error) {
3✔
636
        if err := validateParametersNotBlank(map[string]string{
3✔
637
                "owner":      owner,
3✔
638
                "repository": repository,
3✔
639
                "path":       path,
3✔
640
        }); err != nil {
4✔
641
                return nil, 0, err
1✔
642
        }
1✔
643

644
        azureReposGitClient, err := client.buildAzureReposClient(ctx)
2✔
645
        if err != nil {
2✔
646
                return nil, 0, err
×
647
        }
×
648

649
        trueVal := true
2✔
650
        output, err := azureReposGitClient.GetItemContent(ctx, git.GetItemContentArgs{
2✔
651
                RepositoryId:      &repository,
2✔
652
                Path:              &path,
2✔
653
                Project:           &client.vcsInfo.Project,
2✔
654
                VersionDescriptor: &git.GitVersionDescriptor{Version: &branch, VersionType: &git.GitVersionTypeValues.Branch},
2✔
655
                IncludeContent:    &trueVal,
2✔
656
        })
2✔
657
        if err != nil {
3✔
658
                return nil, http.StatusNotFound, err
1✔
659
        }
1✔
660

661
        reader := bufio.NewReader(output)
1✔
662
        // read the contents of the ReadCloser into a byte slice
1✔
663
        contents, err := io.ReadAll(reader)
1✔
664
        if err != nil {
1✔
665
                return nil, 0, err
×
666
        }
×
667
        return contents, http.StatusOK, nil
1✔
668
}
669

670
// GetRepositoryEnvironmentInfo on GitLab
671
func (client *AzureReposClient) GetRepositoryEnvironmentInfo(ctx context.Context, owner, repository, name string) (RepositoryEnvironmentInfo, error) {
1✔
672
        return RepositoryEnvironmentInfo{}, getUnsupportedInAzureError("get repository environment info")
1✔
673
}
1✔
674

675
func (client *AzureReposClient) GetModifiedFiles(ctx context.Context, _, repository, refBefore, refAfter string) ([]string, error) {
5✔
676
        if err := validateParametersNotBlank(map[string]string{
5✔
677
                "repository": repository,
5✔
678
                "refBefore":  refBefore,
5✔
679
                "refAfter":   refAfter,
5✔
680
        }); err != nil {
8✔
681
                return nil, err
3✔
682
        }
3✔
683

684
        azureReposGitClient, err := client.buildAzureReposClient(ctx)
2✔
685
        if err != nil {
2✔
686
                return nil, err
×
687
        }
×
688

689
        fileNamesSet := datastructures.MakeSet[string]()
2✔
690
        changesToReturn := vcsutils.PointerOf(100)
2✔
691
        changesToSkip := vcsutils.PointerOf(0)
2✔
692

2✔
693
        for *changesToReturn >= 0 {
4✔
694
                commitDiffs, err := azureReposGitClient.GetCommitDiffs(ctx, git.GetCommitDiffsArgs{
2✔
695
                        Top:                     changesToReturn,
2✔
696
                        Skip:                    changesToSkip,
2✔
697
                        RepositoryId:            &repository,
2✔
698
                        Project:                 &client.vcsInfo.Project,
2✔
699
                        DiffCommonCommit:        vcsutils.PointerOf(true),
2✔
700
                        BaseVersionDescriptor:   &git.GitBaseVersionDescriptor{BaseVersion: &refBefore},
2✔
701
                        TargetVersionDescriptor: &git.GitTargetVersionDescriptor{TargetVersion: &refAfter},
2✔
702
                })
2✔
703
                if err != nil {
3✔
704
                        return nil, err
1✔
705
                }
1✔
706

707
                changes := vcsutils.DefaultIfNotNil(commitDiffs.Changes)
1✔
708
                if len(changes) < *changesToReturn {
2✔
709
                        changesToReturn = vcsutils.PointerOf(-1)
1✔
710
                } else {
1✔
711
                        changesToSkip = vcsutils.PointerOf(*changesToSkip + *changesToReturn)
×
712
                }
×
713

714
                for _, anyChange := range changes {
31✔
715
                        change, err := vcsutils.RemapFields[git.GitChange](anyChange, "json")
30✔
716
                        if err != nil {
30✔
717
                                return nil, err
×
718
                        }
×
719

720
                        changedItem, err := vcsutils.RemapFields[git.GitItem](change.Item, "json")
30✔
721
                        if err != nil {
30✔
722
                                return nil, err
×
723
                        }
×
724

725
                        if vcsutils.DefaultIfNotNil(changedItem.GitObjectType) != git.GitObjectTypeValues.Blob {
41✔
726
                                // We are not interested in the folders (trees) and other Git types.
11✔
727
                                continue
11✔
728
                        }
729

730
                        // Azure returns all paths with '/' prefix. Other providers doesn't, so let's
731
                        // remove the prefix here to produce output of the same format.
732
                        fileNamesSet.Add(strings.TrimPrefix(vcsutils.DefaultIfNotNil(changedItem.Path), "/"))
19✔
733
                }
734
        }
735
        _ = fileNamesSet.Remove("") // Make sure there are no blank filepath.
1✔
736
        fileNamesList := fileNamesSet.ToSlice()
1✔
737
        sort.Strings(fileNamesList)
1✔
738
        return fileNamesList, nil
1✔
739
}
740

NEW
741
func (client *AzureReposClient) CreateBranch(ctx context.Context, owner, repository, sourceBranch, newBranch string) error {
×
NEW
742
        return getUnsupportedInAzureError("create branch")
×
NEW
743
}
×
744

NEW
745
func (client *AzureReposClient) AllowWorkflows(ctx context.Context, owner string) error {
×
NEW
746
        return getUnsupportedInAzureError("allow workflows")
×
NEW
747
}
×
748

NEW
749
func (client *AzureReposClient) AddOrganizationSecret(ctx context.Context, owner, secretName, secretValue string) error {
×
NEW
750
        return getUnsupportedInAzureError("add organization secret")
×
NEW
751
}
×
752

NEW
753
func (client *AzureReposClient) CommitAndPushFiles(ctx context.Context, owner, repo, sourceBranch, commitMessage, authorName, authorEmail string, files []FileToCommit) error {
×
NEW
754
        return getUnsupportedInAzureError("commit and push files")
×
NEW
755
}
×
756

NEW
757
func (client *AzureReposClient) GetRepoCollaborators(ctx context.Context, owner, repo, affiliation, permission string) ([]string, error) {
×
NEW
758
        return nil, getUnsupportedInAzureError("get repo collaborators")
×
NEW
759
}
×
760

NEW
761
func (client *AzureReposClient) GetRepoTeamsByPermissions(ctx context.Context, owner, repo string, permissions []string) ([]int64, error) {
×
NEW
762
        return nil, getUnsupportedInAzureError("get repo teams by permissions")
×
NEW
763
}
×
764

NEW
765
func (client *AzureReposClient) CreateOrUpdateEnvironment(ctx context.Context, owner, repo, envName string, teams []int64, users []string) error {
×
NEW
766
        return getUnsupportedInAzureError("create or update environment")
×
NEW
767
}
×
768

NEW
769
func (client *AzureReposClient) MergePullRequest(ctx context.Context, owner, repo string, prNumber int, commitMessage string) error {
×
NEW
770
        return getUnsupportedInAzureError("merge pull request")
×
NEW
771
}
×
772

773
func parsePullRequestDetails(client *AzureReposClient, pullRequest git.GitPullRequest, owner, repository string, withBody bool) PullRequestInfo {
4✔
774
        // Trim the branches prefix and get the actual branches name
4✔
775
        shortSourceName := plumbing.ReferenceName(*pullRequest.SourceRefName).Short()
4✔
776
        shortTargetName := plumbing.ReferenceName(*pullRequest.TargetRefName).Short()
4✔
777

4✔
778
        var prBody string
4✔
779
        bodyPtr := pullRequest.Description
4✔
780
        if bodyPtr != nil && withBody {
5✔
781
                prBody = *bodyPtr
1✔
782
        }
1✔
783

784
        // When a pull request is from a forked repository, extract the owner.
785
        sourceRepoOwner := owner
4✔
786
        if pullRequest.ForkSource != nil {
6✔
787
                if sourceRepoOwner = extractOwnerFromForkedRepoUrl(pullRequest.ForkSource); sourceRepoOwner == "" {
3✔
788
                        client.logger.Warn(vcsutils.FailedForkedRepositoryExtraction)
1✔
789
                }
1✔
790
        }
791
        return PullRequestInfo{
4✔
792
                ID:     int64(*pullRequest.PullRequestId),
4✔
793
                Title:  vcsutils.DefaultIfNotNil(pullRequest.Title),
4✔
794
                Body:   prBody,
4✔
795
                URL:    vcsutils.DefaultIfNotNil(pullRequest.Url),
4✔
796
                Author: vcsutils.DefaultIfNotNil(pullRequest.CreatedBy.DisplayName),
4✔
797
                Source: BranchInfo{
4✔
798
                        Name:       shortSourceName,
4✔
799
                        Repository: repository,
4✔
800
                        Owner:      sourceRepoOwner,
4✔
801
                },
4✔
802
                Target: BranchInfo{
4✔
803
                        Name:       shortTargetName,
4✔
804
                        Repository: repository,
4✔
805
                        Owner:      owner,
4✔
806
                },
4✔
807
        }
4✔
808
}
809

810
// Extract the repository owner of a forked source
811
func extractOwnerFromForkedRepoUrl(forkedGit *git.GitForkRef) string {
5✔
812
        if forkedGit == nil || forkedGit.Repository == nil || forkedGit.Repository.Url == nil {
6✔
813
                return ""
1✔
814
        }
1✔
815
        url := *forkedGit.Repository.Url
4✔
816
        if !strings.Contains(url, defaultAzureBaseUrl) {
6✔
817
                return ""
2✔
818
        }
2✔
819
        owner := strings.Split(strings.TrimPrefix(url, defaultAzureBaseUrl), "/")[0]
2✔
820
        return owner
2✔
821
}
822

823
// mapStatusToString maps commit status enum to string, specific for azure.
824
func mapStatusToString(status CommitStatus) string {
2✔
825
        conversionMap := map[CommitStatus]string{
2✔
826
                Pass:       "Succeeded",
2✔
827
                Fail:       "Failed",
2✔
828
                Error:      "Error",
2✔
829
                InProgress: "Pending",
2✔
830
        }
2✔
831
        return conversionMap[status]
2✔
832
}
2✔
833

834
func extractTimeFromAzuredevopsTime(rawStatus *azuredevops.Time) time.Time {
6✔
835
        if rawStatus == nil {
9✔
836
                return time.Time{}
3✔
837
        }
3✔
838
        return extractTimeWithFallback(&rawStatus.Time)
3✔
839
}
840

841
func azureMapPullRequestState(state vcsutils.PullRequestState) *git.PullRequestStatus {
4✔
842
        switch state {
4✔
843
        case vcsutils.Open:
2✔
844
                return &git.PullRequestStatusValues.Active
2✔
845
        case vcsutils.Closed:
1✔
846
                return &git.PullRequestStatusValues.Abandoned
1✔
847
        default:
1✔
848
                return nil
1✔
849
        }
850
}
851

852
func mapVoteToState(vote int) string {
2✔
853
        switch vote {
2✔
854
        case 10:
1✔
855
                return "APPROVED"
1✔
856
        case 5:
×
857
                return "APPROVED_WITH_SUGGESTIONS"
×
858
        case -5:
1✔
859
                return "CHANGES_REQUESTED"
1✔
860
        case -10:
×
861
                return "REJECTED"
×
862
        default:
×
863
                return "UNKNOWN"
×
864
        }
865
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc