drone/git/branch.go

294 lines
7.6 KiB
Go

// 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 git
import (
"context"
"fmt"
"strings"
"github.com/harness/gitness/errors"
"github.com/harness/gitness/git/api"
"github.com/harness/gitness/git/check"
"github.com/harness/gitness/git/hook"
"github.com/harness/gitness/git/sha"
"github.com/rs/zerolog/log"
)
type BranchSortOption int
const (
BranchSortOptionDefault BranchSortOption = iota
BranchSortOptionName
BranchSortOptionDate
)
var listBranchesRefFields = []api.GitReferenceField{
api.GitReferenceFieldRefName,
api.GitReferenceFieldObjectName,
}
type Branch struct {
Name string
SHA sha.SHA
Commit *Commit
}
type CreateBranchParams struct {
WriteParams
// BranchName is the name of the branch
BranchName string
// Target is a git reference (branch / tag / commit SHA)
Target string
}
type CreateBranchOutput struct {
Branch Branch
}
type GetBranchParams struct {
ReadParams
// BranchName is the name of the branch
BranchName string
}
type GetBranchOutput struct {
Branch Branch
}
type DeleteBranchParams struct {
WriteParams
// BranchName is the name of the branch
BranchName string
}
type ListBranchesParams struct {
ReadParams
IncludeCommit bool
Query string
Sort BranchSortOption
Order SortOrder
Page int32
PageSize int32
}
type ListBranchesOutput struct {
Branches []Branch
}
func (s *Service) CreateBranch(ctx context.Context, params *CreateBranchParams) (*CreateBranchOutput, error) {
if err := params.Validate(); err != nil {
return nil, err
}
if err := check.BranchName(params.BranchName); err != nil {
return nil, errors.InvalidArgument(err.Error())
}
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
targetCommit, err := s.git.GetCommit(ctx, repoPath, strings.TrimSpace(params.Target))
if err != nil {
return nil, fmt.Errorf("failed to get target commit: %w", err)
}
branchRef := api.GetReferenceFromBranchName(params.BranchName)
refUpdater, err := hook.CreateRefUpdater(s.hookClientFactory, params.EnvVars, repoPath, branchRef)
if err != nil {
return nil, fmt.Errorf("failed to create ref updater to create the branch: %w", err)
}
err = refUpdater.Do(ctx, sha.Nil, targetCommit.SHA)
if errors.IsConflict(err) {
return nil, errors.Conflict("branch %q already exists", params.BranchName)
}
if err != nil {
return nil, fmt.Errorf("failed to create branch reference: %w", err)
}
commit, err := mapCommit(targetCommit)
if err != nil {
return nil, fmt.Errorf("failed to map git commit: %w", err)
}
return &CreateBranchOutput{
Branch: Branch{
Name: params.BranchName,
SHA: commit.SHA,
Commit: commit,
},
}, nil
}
func (s *Service) GetBranch(ctx context.Context, params *GetBranchParams) (*GetBranchOutput, error) {
if params == nil {
return nil, ErrNoParamsProvided
}
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
sanitizedBranchName := strings.TrimPrefix(params.BranchName, gitReferenceNamePrefixBranch)
gitBranch, err := s.git.GetBranch(ctx, repoPath, sanitizedBranchName)
if err != nil {
return nil, err
}
branch, err := mapBranch(gitBranch)
if err != nil {
return nil, fmt.Errorf("failed to map rpc branch %v: %w", gitBranch.Name, err)
}
return &GetBranchOutput{
Branch: *branch,
}, nil
}
func (s *Service) DeleteBranch(ctx context.Context, params *DeleteBranchParams) error {
if params == nil {
return ErrNoParamsProvided
}
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
branchRef := api.GetReferenceFromBranchName(params.BranchName)
refUpdater, err := hook.CreateRefUpdater(s.hookClientFactory, params.EnvVars, repoPath, branchRef)
if err != nil {
return fmt.Errorf("failed to create ref updater to create the branch: %w", err)
}
err = refUpdater.Do(ctx, sha.None, sha.Nil) // delete whatever is there
if errors.IsNotFound(err) {
return errors.NotFound("branch %q does not exist", params.BranchName)
}
if err != nil {
return fmt.Errorf("failed to delete branch reference: %w", err)
}
return nil
}
func (s *Service) ListBranches(ctx context.Context, params *ListBranchesParams) (*ListBranchesOutput, error) {
if params == nil {
return nil, ErrNoParamsProvided
}
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
gitBranches, err := s.listBranchesLoadReferenceData(ctx, repoPath, api.BranchFilter{
IncludeCommit: params.IncludeCommit,
Query: params.Query,
Sort: mapBranchesSortOption(params.Sort),
Order: mapToSortOrder(params.Order),
Page: params.Page,
PageSize: params.PageSize,
})
if err != nil {
return nil, err
}
// get commits if needed (single call for perf savings: 1s-4s vs 5s-20s)
if params.IncludeCommit {
commitSHAs := make([]string, len(gitBranches))
for i := range gitBranches {
commitSHAs[i] = gitBranches[i].SHA.String()
}
var gitCommits []*api.Commit
gitCommits, err = s.git.GetCommits(ctx, repoPath, commitSHAs)
if err != nil {
return nil, fmt.Errorf("failed to get commit: %w", err)
}
for i := range gitCommits {
gitBranches[i].Commit = gitCommits[i]
}
}
branches := make([]Branch, len(gitBranches))
for i, branch := range gitBranches {
b, err := mapBranch(branch)
if err != nil {
return nil, err
}
branches[i] = *b
}
return &ListBranchesOutput{
Branches: branches,
}, nil
}
func (s *Service) listBranchesLoadReferenceData(
ctx context.Context,
repoPath string,
filter api.BranchFilter,
) ([]*api.Branch, error) {
// TODO: can we be smarter with slice allocation
branches := make([]*api.Branch, 0, 16)
handler := listBranchesWalkReferencesHandler(&branches)
instructor, endsAfter, err := wrapInstructorWithOptionalPagination(
api.DefaultInstructor, // branches only have one target type, default instructor is enough
filter.Page,
filter.PageSize,
)
if err != nil {
return nil, errors.InvalidArgument("invalid pagination details: %v", err)
}
opts := &api.WalkReferencesOptions{
Patterns: createReferenceWalkPatternsFromQuery(gitReferenceNamePrefixBranch, filter.Query),
Sort: filter.Sort,
Order: filter.Order,
Fields: listBranchesRefFields,
Instructor: instructor,
// we don't do any post-filtering, restrict git to only return as many elements as pagination needs.
MaxWalkDistance: endsAfter,
}
err = s.git.WalkReferences(ctx, repoPath, handler, opts)
if err != nil {
return nil, fmt.Errorf("failed to walk branch references: %w", err)
}
log.Ctx(ctx).Trace().Msgf("git api returned %d branches", len(branches))
return branches, nil
}
func listBranchesWalkReferencesHandler(
branches *[]*api.Branch,
) api.WalkReferencesHandler {
return func(e api.WalkReferencesEntry) error {
fullRefName, ok := e[api.GitReferenceFieldRefName]
if !ok {
return fmt.Errorf("entry missing reference name")
}
objectSHA, ok := e[api.GitReferenceFieldObjectName]
if !ok {
return fmt.Errorf("entry missing object sha")
}
branch := &api.Branch{
Name: fullRefName[len(gitReferenceNamePrefixBranch):],
SHA: sha.Must(objectSHA),
}
// TODO: refactor to not use slice pointers?
*branches = append(*branches, branch)
return nil
}
}