pull request import (#2121)

* bypass lint
* bump golang version
* using types from migrator
* moving Importing to the RepositoryOutput
* minor changes; improved unit tests
* pr comments
* pr import
* pull request import
unified-ui
Marko Gaćeša 2024-07-06 00:46:36 +00:00 committed by Harness
parent 30dfa92023
commit 5427130a42
34 changed files with 843 additions and 77 deletions

View File

@ -35,7 +35,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: '1.20'
go-version: '1.22'
- name: get dependencies
run: |
mkdir -p ./web/dist

View File

@ -18,7 +18,7 @@ RUN yarn && yarn build && yarn cache clean
# ---------------------------------------------------------#
# Build gitness image #
# ---------------------------------------------------------#
FROM --platform=$BUILDPLATFORM golang:1.20-alpine3.18 as builder
FROM --platform=$BUILDPLATFORM golang:1.22-alpine3.18 as builder
RUN apk update \
&& apk add --no-cache protoc build-base git

View File

@ -47,6 +47,11 @@ func (c *Controller) PreReceive(
return hook.Output{}, err
}
if !in.Internal && repo.State != enum.RepoStateActive && repo.State != enum.RepoStateMigrateGitPush {
output.Error = ptr.String("Push not allowed in the current repository state")
return output, nil
}
if err := c.limiter.RepoSize(ctx, in.RepoID); err != nil {
return hook.Output{}, fmt.Errorf(
"resource limit exceeded: %w",

View File

@ -25,6 +25,7 @@ import (
pullreqevents "github.com/harness/gitness/app/events/pullreq"
"github.com/harness/gitness/app/services/codecomments"
"github.com/harness/gitness/app/services/codeowners"
"github.com/harness/gitness/app/services/importer"
locker "github.com/harness/gitness/app/services/locker"
"github.com/harness/gitness/app/services/protection"
"github.com/harness/gitness/app/services/pullreq"
@ -62,6 +63,7 @@ type Controller struct {
sseStreamer sse.Streamer
codeOwners *codeowners.Service
locker *locker.Locker
importer *importer.PullReq
}
func NewController(
@ -87,6 +89,7 @@ func NewController(
sseStreamer sse.Streamer,
codeowners *codeowners.Service,
locker *locker.Locker,
importer *importer.PullReq,
) *Controller {
return &Controller{
tx: tx,
@ -111,6 +114,7 @@ func NewController(
sseStreamer: sseStreamer,
codeOwners: codeowners,
locker: locker,
importer: importer,
}
}
@ -152,8 +156,8 @@ func (c *Controller) getRepoCheckAccess(ctx context.Context,
return nil, fmt.Errorf("failed to find repository: %w", err)
}
if repo.Importing {
return nil, usererror.BadRequest("Repository import is in progress.")
if repo.State != enum.RepoStateActive {
return nil, usererror.BadRequest("Repository is not ready to use.")
}
if err = apiauth.CheckRepo(ctx, c.authorizer, session, repo, reqPermission); err != nil {

View File

@ -19,6 +19,7 @@ import (
pullreqevents "github.com/harness/gitness/app/events/pullreq"
"github.com/harness/gitness/app/services/codecomments"
"github.com/harness/gitness/app/services/codeowners"
"github.com/harness/gitness/app/services/importer"
"github.com/harness/gitness/app/services/locker"
"github.com/harness/gitness/app/services/protection"
"github.com/harness/gitness/app/services/pullreq"
@ -45,7 +46,7 @@ func ProvideController(tx dbtx.Transactor, urlProvider url.Provider, authorizer
checkStore store.CheckStore,
rpcClient git.Interface, eventReporter *pullreqevents.Reporter, codeCommentMigrator *codecomments.Migrator,
pullreqService *pullreq.Service, ruleManager *protection.Manager, sseStreamer sse.Streamer,
codeOwners *codeowners.Service, locker *locker.Locker,
codeOwners *codeowners.Service, locker *locker.Locker, importer *importer.PullReq,
) *Controller {
return NewController(tx, urlProvider, authorizer,
pullReqStore, pullReqActivityStore,
@ -56,5 +57,5 @@ func ProvideController(tx dbtx.Transactor, urlProvider url.Provider, authorizer
checkStore,
rpcClient, eventReporter,
codeCommentMigrator,
pullreqService, ruleManager, sseStreamer, codeOwners, locker)
pullreqService, ruleManager, sseStreamer, codeOwners, locker, importer)
}

View File

@ -49,7 +49,8 @@ var errPublicRepoCreationDisabled = usererror.BadRequestf("Public repository cre
type RepositoryOutput struct {
types.Repository
IsPublic bool `json:"is_public" yaml:"is_public"`
IsPublic bool `json:"is_public" yaml:"is_public"`
Importing bool `json:"importing" yaml:"-"`
}
// TODO [CODE-1363]: remove after identifier migration.
@ -156,6 +157,7 @@ func (c *Controller) getRepo(
ctx,
c.repoStore,
repoRef,
ActiveRepoStates,
)
}
@ -174,6 +176,26 @@ func (c *Controller) getRepoCheckAccess(
session,
repoRef,
reqPermission,
ActiveRepoStates,
)
}
// getRepoCheckAccessForGit fetches a repo
// and checks if the current user has permission to access it.
func (c *Controller) getRepoCheckAccessForGit(
ctx context.Context,
session *auth.Session,
repoRef string,
reqPermission enum.Permission,
) (*types.Repository, error) {
return GetRepoCheckAccess(
ctx,
c.repoStore,
c.authorizer,
session,
repoRef,
reqPermission,
nil, // Any state allowed - we'll block in the pre-receive hook.
)
}

View File

@ -149,10 +149,7 @@ func (c *Controller) Create(ctx context.Context, session *auth.Session, in *Crea
repo.GitURL = c.urlProvider.GenerateGITCloneURL(repo.Path)
repo.GitSSHURL = c.urlProvider.GenerateGITCloneSSHURL(repo.Path)
repoOutput := &RepositoryOutput{
Repository: *repo,
IsPublic: in.IsPublic,
}
repoOutput := GetRepoOutputWithAccess(ctx, in.IsPublic, repo)
err = c.auditService.Log(ctx,
session.Principal,

View File

@ -33,7 +33,7 @@ func (c *Controller) GitInfoRefs(
gitProtocol string,
w io.Writer,
) error {
repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoView)
repo, err := c.getRepoCheckAccessForGit(ctx, session, repoRef, enum.PermissionRepoView)
if err != nil {
return fmt.Errorf("failed to verify repo access: %w", err)
}

View File

@ -40,7 +40,7 @@ func (c *Controller) GitServicePack(
permission = enum.PermissionRepoPush
}
repo, err := c.getRepoCheckAccess(ctx, session, repoRef, permission)
repo, err := c.getRepoCheckAccessForGit(ctx, session, repoRef, permission)
if err != nil {
return fmt.Errorf("failed to verify repo access: %w", err)
}

View File

@ -26,13 +26,18 @@ import (
"github.com/harness/gitness/app/store"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
"golang.org/x/exp/slices"
)
// GetRepo fetches an active repo (not one that is currently being imported).
var ActiveRepoStates = []enum.RepoState{enum.RepoStateActive}
// GetRepo fetches an repository.
func GetRepo(
ctx context.Context,
repoStore store.RepoStore,
repoRef string,
allowedStates []enum.RepoState,
) (*types.Repository, error) {
if repoRef == "" {
return nil, usererror.BadRequest("A valid repository reference must be provided.")
@ -43,8 +48,8 @@ func GetRepo(
return nil, fmt.Errorf("failed to find repository: %w", err)
}
if repo.Importing {
return nil, usererror.BadRequest("Repository import is in progress.")
if len(allowedStates) > 0 && !slices.Contains(allowedStates, repo.State) {
return nil, usererror.BadRequest("Repository is not ready to use.")
}
return repo, nil
@ -59,8 +64,9 @@ func GetRepoCheckAccess(
session *auth.Session,
repoRef string,
reqPermission enum.Permission,
allowedStates []enum.RepoState,
) (*types.Repository, error) {
repo, err := GetRepo(ctx, repoStore, repoRef)
repo, err := GetRepo(ctx, repoStore, repoRef, allowedStates)
if err != nil {
return nil, fmt.Errorf("failed to find repo: %w", err)
}
@ -85,5 +91,18 @@ func GetRepoOutput(
return &RepositoryOutput{
Repository: *repo,
IsPublic: isPublic,
Importing: repo.State != enum.RepoStateActive,
}, nil
}
func GetRepoOutputWithAccess(
_ context.Context,
isPublic bool,
repo *types.Repository,
) *RepositoryOutput {
return &RepositoryOutput{
Repository: *repo,
IsPublic: isPublic,
Importing: repo.State != enum.RepoStateActive,
}
}

View File

@ -114,10 +114,7 @@ func (c *Controller) Import(ctx context.Context, session *auth.Session, in *Impo
log.Warn().Msgf("failed to insert audit log for import repository operation: %s", err)
}
return &RepositoryOutput{
Repository: *repo,
IsPublic: false,
}, nil
return GetRepoOutputWithAccess(ctx, false, repo), nil
}
func (c *Controller) sanitizeImportInput(in *ImportInput) error {

View File

@ -58,8 +58,8 @@ func (c *Controller) Move(ctx context.Context,
return nil, err
}
if repo.Importing {
return nil, usererror.BadRequest("can't move a repo that is being imported")
if repo.State != enum.RepoStateActive {
return nil, usererror.BadRequest("Can't move a repo that isn't ready to use.")
}
if err = apiauth.CheckRepo(ctx, c.authorizer, session, repo, enum.PermissionRepoEdit); err != nil {

View File

@ -64,7 +64,7 @@ func (c *Controller) PurgeNoAuth(
session *auth.Session,
repo *types.Repository,
) error {
if repo.Importing {
if repo.State == enum.RepoStateGitImport {
log.Ctx(ctx).Info().Msg("repository is importing. cancelling the import job.")
err := c.importer.Cancel(ctx, repo)
if err != nil {

View File

@ -94,8 +94,5 @@ func (c *Controller) RestoreNoAuth(
}
// Repos restored as private since public access data has been deleted upon deletion.
return &RepositoryOutput{
Repository: *repo,
IsPublic: false,
}, nil
return GetRepoOutputWithAccess(ctx, false, repo), nil
}

View File

@ -97,7 +97,7 @@ func (c *Controller) SoftDeleteNoAuth(
return fmt.Errorf("failed to delete public access for repo: %w", err)
}
if repo.Importing {
if repo.State != enum.RepoStateActive {
return c.PurgeNoAuth(ctx, session, repo)
}

View File

@ -63,10 +63,7 @@ func (c *Controller) UpdatePublicAccess(ctx context.Context,
// no op
if isPublic == in.IsPublic {
return &RepositoryOutput{
Repository: *repo,
IsPublic: isPublic,
}, nil
return GetRepoOutputWithAccess(ctx, isPublic, repo), nil
}
if err = c.publicAccess.Set(ctx, enum.PublicResourceTypeRepo, repo.Path, in.IsPublic); err != nil {
@ -95,8 +92,5 @@ func (c *Controller) UpdatePublicAccess(ctx context.Context,
log.Ctx(ctx).Warn().Msgf("failed to insert audit log for update repository operation: %s", err)
}
return &RepositoryOutput{
Repository: *repo,
IsPublic: in.IsPublic,
}, nil
return GetRepoOutputWithAccess(ctx, in.IsPublic, repo), nil
}

View File

@ -63,5 +63,6 @@ func (c *Controller) getRepoCheckAccess(
session,
repoRef,
reqPermission,
repo.ActiveRepoStates,
)
}

View File

@ -21,7 +21,7 @@ import (
apiauth "github.com/harness/gitness/app/api/auth"
"github.com/harness/gitness/app/api/controller/limiter"
repoCtrl "github.com/harness/gitness/app/api/controller/repo"
repoctrl "github.com/harness/gitness/app/api/controller/repo"
"github.com/harness/gitness/app/api/usererror"
"github.com/harness/gitness/app/auth"
"github.com/harness/gitness/app/paths"
@ -39,8 +39,8 @@ type ImportRepositoriesInput struct {
}
type ImportRepositoriesOutput struct {
ImportingRepos []*repoCtrl.RepositoryOutput `json:"importing_repos"`
DuplicateRepos []*repoCtrl.RepositoryOutput `json:"duplicate_repos"` // repos which already exist in the space.
ImportingRepos []*repoctrl.RepositoryOutput `json:"importing_repos"`
DuplicateRepos []*repoctrl.RepositoryOutput `json:"duplicate_repos"` // repos which already exist in the space.
}
// getSpaceCheckAuthRepoCreation checks whether the user has permissions to create repos
@ -165,12 +165,9 @@ func (c *Controller) ImportRepositories(
return ImportRepositoriesOutput{}, err
}
reposOut := make([]*repoCtrl.RepositoryOutput, len(repos))
reposOut := make([]*repoctrl.RepositoryOutput, len(repos))
for i, repo := range repos {
reposOut[i] = &repoCtrl.RepositoryOutput{
Repository: *repo,
IsPublic: false,
}
reposOut[i] = repoctrl.GetRepoOutputWithAccess(ctx, false, repo)
err = c.auditService.Log(ctx,
session.Principal,
@ -187,12 +184,9 @@ func (c *Controller) ImportRepositories(
}
}
duplicateReposOut := make([]*repoCtrl.RepositoryOutput, len(duplicateRepos))
duplicateReposOut := make([]*repoctrl.RepositoryOutput, len(duplicateRepos))
for i, dupRepo := range duplicateRepos {
duplicateReposOut[i] = &repoCtrl.RepositoryOutput{
Repository: *dupRepo,
IsPublic: false,
}
duplicateReposOut[i] = repoctrl.GetRepoOutputWithAccess(ctx, false, dupRepo)
}
return ImportRepositoriesOutput{ImportingRepos: reposOut, DuplicateRepos: duplicateReposOut}, nil

View File

@ -27,6 +27,7 @@ import (
"github.com/harness/gitness/app/api/usererror"
"github.com/harness/gitness/app/paths"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
"github.com/drone/go-scm/scm"
"github.com/drone/go-scm/scm/driver/azure"
@ -100,7 +101,7 @@ func (r *RepositoryInfo) ToRepo(
Updated: now,
ForkID: 0,
DefaultBranch: r.DefaultBranch,
Importing: true,
State: enum.RepoStateGitImport,
Path: paths.Concatenate(spacePath, identifier),
}, r.IsPublic
}

View File

@ -0,0 +1,538 @@
// Copyright 2023 Harness, Inc.
//
// 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 importer
import (
"context"
"encoding/json"
"fmt"
"sort"
"strings"
"time"
"github.com/harness/gitness/app/store"
"github.com/harness/gitness/app/url"
"github.com/harness/gitness/errors"
"github.com/harness/gitness/git"
"github.com/harness/gitness/git/parser"
"github.com/harness/gitness/store/database/dbtx"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
"github.com/rs/zerolog/log"
)
// PullReq is pull request importer.
type PullReq struct {
urlProvider url.Provider
git git.Interface
principalStore store.PrincipalStore
repoStore store.RepoStore
pullReqStore store.PullReqStore
pullReqActStore store.PullReqActivityStore
tx dbtx.Transactor
}
type repoImportState struct {
git git.Interface
readParams git.ReadParams
principalStore store.PrincipalStore
pullReqActivityStore store.PullReqActivityStore
branchCheck map[string]*git.Branch
principals map[string]*types.Principal
}
// Import load provided pull requests in go-scm format and imports them.
//
//nolint:gocognit
func (importer PullReq) Import(
ctx context.Context,
repo *types.Repository,
extPullReqs []*ExternalPullRequest,
) ([]*types.PullReq, error) {
if repo.State != enum.RepoStateMigrateDataImport {
return nil, errors.PreconditionFailed("Repository data can't be imported at this point")
}
readParams := git.ReadParams{RepoUID: repo.GitUID}
repoState := repoImportState{
git: importer.git,
readParams: readParams,
principalStore: importer.principalStore,
pullReqActivityStore: importer.pullReqActStore,
branchCheck: map[string]*git.Branch{},
principals: map[string]*types.Principal{},
}
pullReqUnique := map[int]struct{}{}
pullReqComments := map[*types.PullReq][]ExternalComment{}
pullReqs := make([]*types.PullReq, 0, len(extPullReqs))
// create the PR objects, one by one. Each pull request will mutate the repository object (to update the counters).
for _, extPullReqData := range extPullReqs {
extPullReq := &extPullReqData.PullRequest
if _, exists := pullReqUnique[extPullReq.Number]; exists {
return nil, errors.Conflict("duplicate pull request number %d", extPullReq.Number)
}
pullReqUnique[extPullReq.Number] = struct{}{}
pr, err := repoState.convertPullReq(ctx, repo, extPullReqData)
if err != nil {
return nil, fmt.Errorf("failed to import pull request %d: %w", extPullReq.Number, err)
}
pullReqs = append(pullReqs, pr)
pullReqComments[pr] = extPullReqData.Comments
}
if len(pullReqs) == 0 { // nothing to do: exit early to avoid accessing the database
return nil, nil
}
err := importer.tx.WithTx(ctx, func(ctx context.Context) error {
var deltaOpen, deltaClosed, deltaMerged int
var maxNumber int64
// Store the pull request objects and the comments.
for _, pullReq := range pullReqs {
if err := importer.pullReqStore.Create(ctx, pullReq); err != nil {
return fmt.Errorf("failed to import the pull request %d: %w", pullReq.Number, err)
}
switch pullReq.State {
case enum.PullReqStateOpen:
deltaOpen++
case enum.PullReqStateClosed:
deltaClosed++
case enum.PullReqStateMerged:
deltaMerged++
}
if maxNumber < pullReq.Number {
maxNumber = pullReq.Number
}
comments, err := repoState.createComments(ctx, repo, pullReq, pullReqComments[pullReq])
if err != nil {
return fmt.Errorf("failed to import pull request comments: %w", err)
}
if len(comments) == 0 { // no need to update the pull request object in the DB if there are no comments.
continue
}
if err := importer.pullReqStore.Update(ctx, pullReq); err != nil {
return fmt.Errorf("failed to update pull request after importing of the comments: %w", err)
}
}
// Update the repository
repoUpdate, err := importer.repoStore.Find(ctx, repo.ID)
if err != nil {
return fmt.Errorf("failed to fetch repo in pull request import: %w", err)
}
if repoUpdate.PullReqSeq < maxNumber {
repoUpdate.PullReqSeq = maxNumber
}
repoUpdate.NumPulls += len(pullReqs)
repoUpdate.NumOpenPulls += deltaOpen
repoUpdate.NumClosedPulls += deltaClosed
repoUpdate.NumMergedPulls += deltaMerged
if err := importer.repoStore.Update(ctx, repoUpdate); err != nil {
return fmt.Errorf("failed to update repo in pull request import: %w", err)
}
return nil
})
if err != nil {
return nil, err
}
return pullReqs, nil
}
// convertPullReq analyses external pull request object and creates types.PullReq object out of it.
func (r *repoImportState) convertPullReq(
ctx context.Context,
repo *types.Repository,
extPullReqData *ExternalPullRequest,
) (*types.PullReq, error) {
extPullReq := extPullReqData.PullRequest
log := log.Ctx(ctx).With().
Str("repo.identifier", repo.Identifier).
Int("pullreq.number", extPullReq.Number).
Logger()
author, err := r.getPrincipalByEmail(ctx, extPullReq.Author.Email)
if err != nil {
return nil, fmt.Errorf("failed to get pull request author: %w", err)
}
now := time.Now().UnixMilli()
createdAt := timestampMillis(extPullReq.Created, now)
updatedAt := timestampMillis(extPullReq.Updated, now)
pr := &types.PullReq{
ID: 0, // the ID will be populated in the data layer
Version: 0,
Number: int64(extPullReq.Number),
CreatedBy: author.ID,
Created: createdAt,
Updated: updatedAt,
Edited: updatedAt,
Closed: nil,
State: enum.PullReqStateOpen,
IsDraft: false,
CommentCount: 0,
UnresolvedCount: 0,
Title: extPullReq.Title,
Description: extPullReq.Body,
SourceRepoID: repo.ID,
SourceBranch: extPullReq.Head.Name,
SourceSHA: extPullReq.Head.SHA,
TargetRepoID: repo.ID,
TargetBranch: extPullReq.Base.Name,
ActivitySeq: 0,
// Merge related fields are all left unset and will be set depending on the PR state
}
params := git.ReadParams{RepoUID: repo.GitUID}
// Set the state of the PR
switch {
case extPullReq.Merged:
pr.State = enum.PullReqStateMerged
case extPullReq.Closed:
pr.State = enum.PullReqStateClosed
default:
pr.State = enum.PullReqStateOpen
}
// Update the PR depending on its state
switch pr.State {
case enum.PullReqStateMerged:
// For merged PR's assume the Head.Sha and Base.Sha point to commits at the time of merging.
pr.Merged = &pr.Edited
pr.MergedBy = &author.ID // Don't have real info for this - use the author.
mergeMethod := enum.MergeMethodMerge // Don't know
pr.MergeMethod = &mergeMethod
pr.MergeCheckStatus = enum.MergeCheckStatusMergeable
pr.SourceSHA = extPullReq.Head.SHA
pr.MergeTargetSHA = &extPullReq.Base.SHA
pr.MergeBaseSHA = extPullReq.Head.SHA // Don't have the real value. Set the value to SourceSHA.
pr.MergeSHA = nil // Don't have this.
pr.MergeConflicts = nil
case enum.PullReqStateClosed:
// For closed PR's it's not important to verify existence of branches and commits.
// If these don't exist the PR will be impossible to open.
pr.MergeCheckStatus = enum.MergeCheckStatusUnchecked
pr.MergeSHA = nil
pr.MergeConflicts = nil
pr.MergeTargetSHA = nil
pr.Closed = &pr.Edited
case enum.PullReqStateOpen:
// For open PR we need to verify existence of branches and find to merge base.
sourceBranch, err := r.git.GetBranch(ctx, &git.GetBranchParams{
ReadParams: params,
BranchName: extPullReq.Head.Name,
})
if err != nil {
return nil, fmt.Errorf("failed to fetch source branch of an open pull request: %w", err)
}
// TODO: Cache this in the repoImportState - it's very likely that it will be the same for other PRs
targetBranch, err := r.git.GetBranch(ctx, &git.GetBranchParams{
ReadParams: params,
BranchName: extPullReq.Base.Name,
})
if err != nil {
return nil, fmt.Errorf("failed to fetch target branch of an open pull request: %w", err)
}
mergeBase, err := r.git.MergeBase(ctx, git.MergeBaseParams{
ReadParams: params,
Ref1: sourceBranch.Branch.SHA.String(),
Ref2: targetBranch.Branch.SHA.String(),
})
if err != nil {
return nil, fmt.Errorf("failed to find merge base an open pull request: %w", err)
}
sourceSHA := sourceBranch.Branch.SHA.String()
targetSHA := targetBranch.Branch.SHA.String()
pr.SourceSHA = sourceSHA
pr.MergeTargetSHA = &targetSHA
pr.MergeBaseSHA = mergeBase.MergeBaseSHA.String()
pr.MergeCheckStatus = enum.MergeCheckStatusUnchecked
}
log.Debug().Str("pullreq.state", string(pr.State)).Msg("importing pull request")
return pr, nil
}
// createComments analyses external pull request comment objects and stores types.PullReqActivity object to the DB.
// It will mutate the pull request object to update counter fields.
func (r *repoImportState) createComments(
ctx context.Context,
repo *types.Repository,
pullReq *types.PullReq,
extComments []ExternalComment,
) ([]*types.PullReqActivity, error) {
log := log.Ctx(ctx).With().
Str("repo.id", repo.Identifier).
Int("pullreq.number", int(pullReq.Number)).
Logger()
extThreads := generateThreads(extComments)
comments := make([]*types.PullReqActivity, 0, len(extComments))
for idxTopLevel, extThread := range extThreads {
order := idxTopLevel + 1
// Create the top level comment with the correct value of Order, SubOrder and ReplySeq.
commentTopLevel, err := r.createComment(ctx, repo, pullReq, nil,
order, 0, len(extThread.Replies), &extThread.TopLevel)
if err != nil {
return nil, fmt.Errorf("failed to create top level comment: %w", err)
}
comments = append(comments, commentTopLevel)
for idxReply, extReply := range extThread.Replies {
subOrder := idxReply + 1
// Create the reply comment with the correct value of Order, SubOrder and ReplySeq.
//nolint:gosec
commentReply, err := r.createComment(ctx, repo, pullReq, &commentTopLevel.ID,
order, subOrder, 0, &extReply)
if err != nil {
return nil, fmt.Errorf("failed to create reply comment: %w", err)
}
comments = append(comments, commentReply)
}
}
log.Debug().Int("count", len(comments)).Msg("imported pull request comments")
return comments, nil
}
// createComment analyses an external pull request comment object and creates types.PullReqActivity object out of it.
// It will mutate the pull request object to update counter fields.
func (r *repoImportState) createComment(
ctx context.Context,
repo *types.Repository,
pullReq *types.PullReq,
parentID *int64,
order, subOrder, replySeq int,
extComment *ExternalComment,
) (*types.PullReqActivity, error) {
commenter, err := r.getPrincipalByEmail(ctx, extComment.Author.Email)
if err != nil {
return nil, fmt.Errorf("failed to get comment ID=%d author: %w", extComment.ID, err)
}
commentedAt := extComment.Created.UnixMilli()
// Mark comments as resolved if the PR is merged, otherwise they are unresolved.
var resolved, resolvedBy *int64
if pullReq.State == enum.PullReqStateMerged {
resolved = &commentedAt
resolvedBy = &commenter.ID
}
comment := &types.PullReqActivity{
CreatedBy: commenter.ID,
Created: commentedAt,
Updated: commentedAt,
Edited: commentedAt,
Deleted: nil,
ParentID: parentID,
RepoID: repo.ID,
PullReqID: pullReq.ID,
Order: int64(order),
SubOrder: int64(subOrder),
ReplySeq: int64(replySeq),
Type: enum.PullReqActivityTypeComment,
Kind: enum.PullReqActivityKindComment,
Text: extComment.Body,
PayloadRaw: json.RawMessage("{}"),
Metadata: nil,
ResolvedBy: resolvedBy,
Resolved: resolved,
CodeComment: nil,
Mentions: nil,
}
if cc := extComment.CodeComment; cc != nil && cc.HunkHeader != "" && extComment.ParentID == 0 {
// a code comment must have a valid HunkHeader and must not be a reply
hunkHeader, ok := parser.ParseDiffHunkHeader(cc.HunkHeader)
if !ok {
return nil, errors.InvalidArgument("Invalid hunk header for code comment: %s", cc.HunkHeader)
}
comment.Kind = enum.PullReqActivityKindChangeComment
comment.Type = enum.PullReqActivityTypeCodeComment
comment.CodeComment = &types.CodeCommentFields{
Outdated: cc.SourceSHA != pullReq.SourceSHA,
MergeBaseSHA: cc.MergeBaseSHA,
SourceSHA: cc.SourceSHA,
Path: cc.Path,
LineNew: hunkHeader.NewLine,
SpanNew: hunkHeader.NewSpan,
LineOld: hunkHeader.OldLine,
SpanOld: hunkHeader.OldSpan,
}
sideNew := !strings.EqualFold(cc.Side, "OLD") // cc.Side can be either OLD or NEW
_ = comment.SetPayload(&types.PullRequestActivityPayloadCodeComment{
Title: cc.CodeSnippet.Header,
Lines: cc.CodeSnippet.Lines,
LineStartNew: sideNew,
LineEndNew: sideNew,
})
}
// store the comment
if err := r.pullReqActivityStore.Create(ctx, comment); err != nil {
return nil, fmt.Errorf("failed to store the external comment ID=%d author: %w", extComment.ID, err)
}
// update the pull request's counter fields
pullReq.CommentCount++
if comment.IsBlocking() {
pullReq.UnresolvedCount++
}
if pullReq.ActivitySeq < comment.Order {
pullReq.ActivitySeq = comment.Order
}
return comment, nil
}
func (r *repoImportState) getPrincipalByEmail(ctx context.Context, emailAddress string) (*types.Principal, error) {
if principal, exists := r.principals[emailAddress]; exists {
return principal, nil
}
principal, err := r.principalStore.FindByEmail(ctx, emailAddress)
if err != nil {
return nil, fmt.Errorf("failed to load principal by email: %w", err)
}
return principal, nil
}
func timestampMillis(t time.Time, def int64) int64 {
if t.IsZero() {
return def
}
return t.UnixMilli()
}
func generateThreads(extComments []ExternalComment) []*externalCommentThread {
extCommentParents := make(map[int]int, len(extComments))
extCommentMap := make(map[int]ExternalComment, len(extComments))
for _, extComment := range extComments {
extCommentParents[extComment.ID] = extComment.ParentID
extCommentMap[extComment.ID] = extComment
}
// Make flat list of reply comment IDs: create map[topLevelCommentID]->[]commentID
extCommentIDReplyMap := make(map[int][]int)
for _, extComment := range extComments {
topLevelParentID := getTopLevelParentID(extComment.ID, extCommentParents)
if topLevelParentID < 0 {
continue
}
if topLevelParentID == extComment.ID {
// Make sure the item with topLevelParentID exist in the map, at least as a nil entry.
extCommentIDReplyMap[topLevelParentID] = extCommentIDReplyMap[topLevelParentID] //nolint:staticcheck
continue
}
extCommentIDReplyMap[topLevelParentID] = append(extCommentIDReplyMap[topLevelParentID], extComment.ID)
}
countTopLevel := len(extCommentIDReplyMap)
if countTopLevel == 0 {
return nil
}
extCommentThreads := make([]*externalCommentThread, 0, countTopLevel)
for topLevelID, replyIDs := range extCommentIDReplyMap {
expReplyComments := make([]ExternalComment, len(replyIDs))
for i, replyID := range replyIDs {
expReplyComments[i] = extCommentMap[replyID]
}
thread := &externalCommentThread{
TopLevel: extCommentMap[topLevelID],
Replies: expReplyComments,
}
extCommentThreads = append(extCommentThreads, thread)
}
// order top level comments
sort.Slice(extCommentThreads, func(i, j int) bool {
created1 := extCommentThreads[i].TopLevel.Created
created2 := extCommentThreads[j].TopLevel.Created
return created1.Before(created2)
})
// order reply comments
for _, thread := range extCommentThreads {
sort.Slice(thread.Replies, func(i, j int) bool {
created1 := thread.Replies[i].Created
created2 := thread.Replies[j].Created
return created1.Before(created2)
})
}
return extCommentThreads
}
func getTopLevelParentID(id int, tree map[int]int) int {
const maxDepth = 20
for currID, depth := id, 0; depth < maxDepth; depth++ {
parentID := tree[currID]
if parentID == 0 {
return currID
}
currID = parentID
}
return -1
}

View File

@ -0,0 +1,72 @@
// Copyright 2023 Harness, Inc.
//
// 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 importer
import (
"testing"
"time"
"github.com/google/go-cmp/cmp"
)
func TestGenerateThreads(t *testing.T) {
// comments with treelike structure
t0 := time.Now()
comments := []ExternalComment{
/* 0 */ {ID: 1, Body: "A", ParentID: 0},
/* 1 */ {ID: 2, Body: "B", ParentID: 0},
/* 2 */ {ID: 3, Body: "A1", ParentID: 1},
/* 3 */ {ID: 4, Body: "B1", ParentID: 2},
/* 4 */ {ID: 5, Body: "A2", ParentID: 1},
/* 5 */ {ID: 6, Body: "A2X", ParentID: 5},
/* 6 */ {ID: 7, Body: "A1X", ParentID: 3},
/* 7 */ {ID: 8, Body: "B1X", ParentID: 4},
/* 8 */ {ID: 9, Body: "C", ParentID: 0},
/* 9 */ {ID: 10, Body: "D1", ParentID: 11}, // Wrong order - a reply before its parent
/* 10 */ {ID: 11, Body: "D", ParentID: 0},
{ID: 20, Body: "Self-parent", ParentID: 20}, // Invalid
{ID: 30, Body: "Crosslinked-X", ParentID: 31}, // Invalid
{ID: 31, Body: "Crosslinked-Y", ParentID: 30}, // Invalid
}
for i := range comments {
comments[i].Created = t0.Add(time.Duration(i) * time.Minute)
}
// flattened threads with top level comments and a list of replies to each of them
wantThreads := []*externalCommentThread{
{
TopLevel: comments[0], // A
Replies: []ExternalComment{comments[2], comments[4], comments[5], comments[6]}, // A1, A2, A2X, A1X
},
{
TopLevel: comments[1], // B
Replies: []ExternalComment{comments[3], comments[7]}, // B1, B1X
},
{
TopLevel: comments[8], // C
Replies: []ExternalComment{},
},
{
TopLevel: comments[10], // D
Replies: []ExternalComment{comments[9]}, // D1
},
}
gotThreads := generateThreads(comments)
if diff := cmp.Diff(gotThreads, wantThreads); diff != "" {
t.Errorf(diff)
}
}

View File

@ -0,0 +1,27 @@
// Copyright 2023 Harness, Inc.
//
// 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 importer
import (
migratetypes "github.com/harness/harness-migrate/types"
)
type ExternalPullRequest = migratetypes.PullRequestData
type ExternalComment = migratetypes.Comment
type externalCommentThread struct {
TopLevel ExternalComment
Replies []ExternalComment
}

View File

@ -238,7 +238,7 @@ func (r *Repository) Handle(ctx context.Context, data string, _ job.ProgressRepo
return "", fmt.Errorf("failed to find repo by id: %w", err)
}
if !repo.Importing {
if repo.State != enum.RepoStateGitImport {
return "", fmt.Errorf("repository %s is not being imported", repo.Identifier)
}
@ -302,7 +302,7 @@ func (r *Repository) Handle(ctx context.Context, data string, _ job.ProgressRepo
err = func() error {
repo, err = r.repoStore.UpdateOptLock(ctx, repo, func(repo *types.Repository) error {
if !repo.Importing {
if repo.State != enum.RepoStateGitImport {
return errors.New("repository has already finished importing")
}
repo.GitUID = gitUID
@ -328,13 +328,13 @@ func (r *Repository) Handle(ctx context.Context, data string, _ job.ProgressRepo
log.Info().Msg("update repo in DB")
repo, err = r.repoStore.UpdateOptLock(ctx, repo, func(repo *types.Repository) error {
if !repo.Importing {
if repo.State != enum.RepoStateGitImport {
return errors.New("repository has already finished importing")
}
repo.GitUID = gitUID
repo.DefaultBranch = defaultBranch
repo.Importing = false
repo.State = enum.RepoStateActive
return nil
})
@ -385,7 +385,7 @@ func (r *Repository) Handle(ctx context.Context, data string, _ job.ProgressRepo
func (r *Repository) GetProgress(ctx context.Context, repo *types.Repository) (job.Progress, error) {
progress, err := r.scheduler.GetJobProgress(ctx, JobIDFromRepoID(repo.ID))
if errors.Is(err, gitness_store.ErrResourceNotFound) {
if repo.Importing {
if repo.State == enum.RepoStateGitImport {
// if the job is not found but repo is marked as importing, return state=failed
return job.FailProgress(), nil
}
@ -401,7 +401,7 @@ func (r *Repository) GetProgress(ctx context.Context, repo *types.Repository) (j
}
func (r *Repository) Cancel(ctx context.Context, repo *types.Repository) error {
if !repo.Importing {
if repo.State != enum.RepoStateGitImport {
return nil
}

View File

@ -32,6 +32,7 @@ import (
var WireSet = wire.NewSet(
ProvideRepoImporter,
ProvidePullReqImporter,
)
func ProvideRepoImporter(
@ -73,3 +74,25 @@ func ProvideRepoImporter(
return importer, nil
}
func ProvidePullReqImporter(
urlProvider url.Provider,
git git.Interface,
principalStore store.PrincipalStore,
repoStore store.RepoStore,
pullReqStore store.PullReqStore,
pullReqActStore store.PullReqActivityStore,
tx dbtx.Transactor,
) *PullReq {
importer := &PullReq{
urlProvider: urlProvider,
git: git,
principalStore: principalStore,
repoStore: repoStore,
pullReqStore: pullReqStore,
pullReqActStore: pullReqActStore,
tx: tx,
}
return importer
}

View File

@ -0,0 +1,7 @@
ALTER TABLE repositories
ADD COLUMN repo_importing BOOLEAN DEFAULT FALSE;
UPDATE repositories SET repo_importing = TRUE WHERE repo_state = 1;
ALTER TABLE repositories
DROP COLUMN repo_state;

View File

@ -0,0 +1,7 @@
ALTER TABLE repositories
ADD COLUMN repo_state INTEGER DEFAULT 0;
UPDATE repositories SET repo_state = 1 WHERE repo_importing = TRUE;
ALTER TABLE repositories
DROP COLUMN repo_importing;

View File

@ -0,0 +1,7 @@
ALTER TABLE repositories
ADD COLUMN repo_importing BOOLEAN DEFAULT FALSE;
UPDATE repositories SET repo_importing = TRUE WHERE repo_state = 1;
ALTER TABLE repositories
DROP COLUMN repo_state;

View File

@ -0,0 +1,7 @@
ALTER TABLE repositories
ADD COLUMN repo_state INTEGER DEFAULT 0;
UPDATE repositories SET repo_state = 1 WHERE repo_importing = TRUE;
ALTER TABLE repositories
DROP COLUMN repo_importing;

View File

@ -86,8 +86,8 @@ type repository struct {
NumOpenPulls int `db:"repo_num_open_pulls"`
NumMergedPulls int `db:"repo_num_merged_pulls"`
Importing bool `db:"repo_importing"`
IsEmpty bool `db:"repo_is_empty"`
State enum.RepoState `db:"repo_state"`
IsEmpty bool `db:"repo_is_empty"`
}
const (
@ -112,7 +112,7 @@ const (
,repo_num_closed_pulls
,repo_num_open_pulls
,repo_num_merged_pulls
,repo_importing
,repo_state
,repo_is_empty`
)
@ -236,7 +236,7 @@ func (s *RepoStore) Create(ctx context.Context, repo *types.Repository) error {
,repo_num_closed_pulls
,repo_num_open_pulls
,repo_num_merged_pulls
,repo_importing
,repo_state
,repo_is_empty
) values (
:repo_version
@ -258,7 +258,7 @@ func (s *RepoStore) Create(ctx context.Context, repo *types.Repository) error {
,:repo_num_closed_pulls
,:repo_num_open_pulls
,:repo_num_merged_pulls
,:repo_importing
,:repo_state
,:repo_is_empty
) RETURNING repo_id`
@ -301,7 +301,7 @@ func (s *RepoStore) Update(ctx context.Context, repo *types.Repository) error {
,repo_num_closed_pulls = :repo_num_closed_pulls
,repo_num_open_pulls = :repo_num_open_pulls
,repo_num_merged_pulls = :repo_num_merged_pulls
,repo_importing = :repo_importing
,repo_state = :repo_state
,repo_is_empty = :repo_is_empty
WHERE repo_id = :repo_id AND repo_version = :repo_version - 1`
@ -744,7 +744,7 @@ func (s *RepoStore) mapToRepo(
NumClosedPulls: in.NumClosedPulls,
NumOpenPulls: in.NumOpenPulls,
NumMergedPulls: in.NumMergedPulls,
Importing: in.Importing,
State: in.State,
IsEmpty: in.IsEmpty,
// Path: is set below
}
@ -827,7 +827,7 @@ func mapToInternalRepo(in *types.Repository) *repository {
NumClosedPulls: in.NumClosedPulls,
NumOpenPulls: in.NumOpenPulls,
NumMergedPulls: in.NumMergedPulls,
Importing: in.Importing,
State: in.State,
IsEmpty: in.IsEmpty,
}
}
@ -857,7 +857,7 @@ func applySortFilter(stmt squirrel.SelectBuilder, filter *types.RepoFilter) squi
// NOTE: string concatenation is safe because the
// order attribute is an enum and is not user-defined,
// and is therefore not subject to injection attacks.
stmt = stmt.OrderBy("repo_importing desc, repo_uid " + filter.Order.String())
stmt = stmt.OrderBy("repo_state desc, repo_uid " + filter.Order.String())
case enum.RepoAttrCreated:
stmt = stmt.OrderBy("repo_created " + filter.Order.String())
case enum.RepoAttrUpdated:

View File

@ -266,7 +266,8 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
if err != nil {
return nil, err
}
pullreqController := pullreq2.ProvideController(transactor, provider, authorizer, pullReqStore, pullReqActivityStore, codeCommentView, pullReqReviewStore, pullReqReviewerStore, repoStore, principalStore, principalInfoCache, pullReqFileViewStore, membershipStore, checkStore, gitInterface, eventsReporter, migrator, pullreqService, protectionManager, streamer, codeownersService, lockerLocker)
pullReq := importer.ProvidePullReqImporter(provider, gitInterface, principalStore, repoStore, pullReqStore, pullReqActivityStore, transactor)
pullreqController := pullreq2.ProvideController(transactor, provider, authorizer, pullReqStore, pullReqActivityStore, codeCommentView, pullReqReviewStore, pullReqReviewerStore, repoStore, principalStore, principalInfoCache, pullReqFileViewStore, membershipStore, checkStore, gitInterface, eventsReporter, migrator, pullreqService, protectionManager, streamer, codeownersService, lockerLocker, pullReq)
webhookConfig := server.ProvideWebhookConfig(config)
webhookStore := database.ProvideWebhookStore(db)
webhookExecutionStore := database.ProvideWebhookExecutionStore(db)

8
go.mod
View File

@ -1,6 +1,6 @@
module github.com/harness/gitness
go 1.20
go 1.22
replace github.com/docker/docker => github.com/docker/engine v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible
@ -12,6 +12,8 @@ require (
github.com/bmatcuk/doublestar/v4 v4.6.0
github.com/coreos/go-semver v0.3.0
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5
github.com/docker/docker v23.0.3+incompatible
github.com/docker/go-connections v0.4.0
github.com/drone-runners/drone-runner-docker v1.8.4-0.20240109154718-47375e234554
github.com/drone/drone-go v1.7.1
github.com/drone/drone-yaml v1.2.3
@ -36,7 +38,7 @@ require (
github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75
github.com/gotidy/ptr v1.4.0
github.com/guregu/null v4.0.0+incompatible
github.com/harness/harness-migrate v0.21.1-0.20240624210736-65c7e9fbe930
github.com/harness/harness-migrate v0.21.1-0.20240703163651-0641dc7290d8
github.com/hashicorp/go-multierror v1.1.1
github.com/jmoiron/sqlx v1.3.3
github.com/joho/godotenv v1.3.0
@ -88,8 +90,6 @@ require (
github.com/containerd/containerd v1.7.6 // indirect
github.com/distribution/reference v0.5.0 // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker v23.0.3+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/drone/envsubst v1.0.3 // indirect
github.com/fatih/semgroup v1.2.0 // indirect

22
go.sum
View File

@ -56,6 +56,7 @@ github.com/99designs/basicauth-go v0.0.0-20160802081356-2a93ba0f464d/go.mod h1:3
github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e h1:rl2Aq4ZODqTDkeSqQBy+fzpZPamacO1Srp8zq7jf2Sc=
github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e/go.mod h1:Xa6lInWHNQnuWoF0YPSsx+INFA9qk7/7pTjwb3PInkY=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/BobuSumisu/aho-corasick v1.0.3 h1:uuf+JHwU9CHP2Vx+wAy6jcksJThhJS9ehR8a+4nPE9g=
github.com/BobuSumisu/aho-corasick v1.0.3/go.mod h1:hm4jLcvZKI2vRF2WDU1N4p/jpWtpOzp3nLmi9AzX/XE=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
@ -81,6 +82,7 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF
github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 h1:ez/4by2iGztzR4L0zgAOR8lTQK9VlyBVVd7G4omaOQs=
github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
@ -111,7 +113,9 @@ github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9
github.com/bmatcuk/doublestar/v4 v4.6.0 h1:HTuxyug8GyFbRkrffIpzNCSK4luc0TY3wzXvzIZhEXc=
github.com/bmatcuk/doublestar/v4 v4.6.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bool64/dev v0.2.32 h1:DRZtloaoH1Igky3zphaUHV9+SLIV2H3lsf78JsJHFg0=
github.com/bool64/dev v0.2.32/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E=
github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs=
github.com/buildkite/yaml v2.1.0+incompatible h1:xirI+ql5GzfikVNDmt+yeiXpf/v1Gt03qXTtT5WXdr8=
github.com/buildkite/yaml v2.1.0+incompatible/go.mod h1:UoU8vbcwu1+vjZq01+KrpSeLBgQQIjL/H7Y6KwikUrI=
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
@ -268,6 +272,7 @@ github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@ -298,6 +303,7 @@ github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@ -346,6 +352,7 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw=
github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
@ -402,9 +409,11 @@ github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/h2non/gock v1.0.9/go.mod h1:CZMcB0Lg5IWnr9bF79pPMg9WeV6WumxQiUJ1UvdO1iE=
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
github.com/harness/harness-migrate v0.21.1-0.20240624210736-65c7e9fbe930 h1:NboKpEf78oc/8bOmh7Gk8uILbIYrD5nswyX9+K6iuKg=
github.com/harness/harness-migrate v0.21.1-0.20240624210736-65c7e9fbe930/go.mod h1:HiUX39ZfhBGY+OkEEN6JpglbodNJlbzayOt/l3nkk6w=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/harness/harness-migrate v0.21.1-0.20240703163651-0641dc7290d8 h1:X0/Nk9aCkIKoF3kavxZcr9EtfARSBqnJK33c59m+xmw=
github.com/harness/harness-migrate v0.21.1-0.20240703163651-0641dc7290d8/go.mod h1:HiUX39ZfhBGY+OkEEN6JpglbodNJlbzayOt/l3nkk6w=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
@ -437,6 +446,7 @@ github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/J
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
github.com/iancoleman/orderedmap v0.2.0 h1:sq1N/TFpYH++aViPcaKjys3bDClUEU7s5B+z6jq8pNA=
github.com/iancoleman/orderedmap v0.2.0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
@ -531,6 +541,7 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
@ -607,6 +618,7 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68 h1:y1p/ycavWjGT9FnmSjdbWUlLGvcxrY0Rw3ATltrxOhk=
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
@ -733,6 +745,7 @@ github.com/sercand/kuberesolver/v5 v5.1.0 h1:YLqreB1vUFbZHSidcqI5ChMp+RIRmop0brQ
github.com/sercand/kuberesolver/v5 v5.1.0/go.mod h1:Fs1KbKhVRnB2aDWN12NjKCB+RgYMWZJ294T3BtmVCpQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
@ -785,6 +798,7 @@ github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKN
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/swaggest/assertjson v1.7.0 h1:SKw5Rn0LQs6UvmGrIdaKQbMR1R3ncXm5KNon+QJ7jtw=
github.com/swaggest/assertjson v1.7.0/go.mod h1:vxMJMehbSVJd+dDWFCKv3QRZKNTpy/ktZKTz9LOEDng=
github.com/swaggest/jsonschema-go v0.3.40 h1:9EqQ9RvtdW69xfYODmyEKWOSZ12x5eiK+wGD2EVh/L4=
github.com/swaggest/jsonschema-go v0.3.40/go.mod h1:ipIOmoFP64QyRUgyPyU/P9tayq2m2TlvUhyZHrhe3S4=
github.com/swaggest/openapi-go v0.2.23 h1:DYUezSTyw180z1bL51wUnalYYbTMwHBjp1Itvji8/rs=
@ -805,7 +819,9 @@ github.com/vearutop/statigz v1.4.0/go.mod h1:LYTolBLiz9oJISwiVKnOQoIwhO1LWX1A7OE
github.com/vinzenz/yaml v0.0.0-20170920082545-91409cdd725d/go.mod h1:mb5taDqMnJiZNRQ3+02W2IFG+oEz1+dTuCXkp4jpkfo=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -1307,6 +1323,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
@ -1335,6 +1352,7 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -18,10 +18,10 @@ import (
"strings"
)
// Defines repo attributes that can be used for sorting and filtering.
// RepoAttr defines repo attributes that can be used for sorting and filtering.
type RepoAttr int
// Order enumeration.
// RepoAttr enumeration.
const (
RepoAttrNone RepoAttr = iota
// TODO [CODE-1363]: remove after identifier migration.
@ -72,3 +72,30 @@ func (a RepoAttr) String() string {
return undefined
}
}
// RepoState defines repo state.
type RepoState int
// RepoState enumeration.
const (
RepoStateActive RepoState = iota
RepoStateGitImport
RepoStateMigrateGitPush
RepoStateMigrateDataImport
)
// String returns the string representation of the RepoState.
func (state RepoState) String() string {
switch state {
case RepoStateActive:
return "active"
case RepoStateGitImport:
return "git-import"
case RepoStateMigrateGitPush:
return "migrate-git-push"
case RepoStateMigrateDataImport:
return "migrate-data-import"
default:
return undefined
}
}

View File

@ -48,8 +48,8 @@ type Repository struct {
NumOpenPulls int `json:"num_open_pulls" yaml:"num_open_pulls"`
NumMergedPulls int `json:"num_merged_pulls" yaml:"num_merged_pulls"`
Importing bool `json:"importing" yaml:"-"`
IsEmpty bool `json:"is_empty,omitempty" yaml:"is_empty"`
State enum.RepoState `json:"state" yaml:"-"`
IsEmpty bool `json:"is_empty,omitempty" yaml:"is_empty"`
// git urls
GitURL string `json:"git_url" yaml:"-"`