drone/gitrpc/internal/service/branch.go

278 lines
8.9 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 service
import (
"context"
"fmt"
"strings"
"github.com/harness/gitness/gitrpc/check"
"github.com/harness/gitness/gitrpc/internal/gitea"
"github.com/harness/gitness/gitrpc/internal/types"
"github.com/harness/gitness/gitrpc/rpc"
"code.gitea.io/gitea/modules/git"
"github.com/rs/zerolog/log"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
var listBranchesRefFields = []types.GitReferenceField{types.GitReferenceFieldRefName, types.GitReferenceFieldObjectName}
func (s ReferenceService) CreateBranch(
ctx context.Context,
request *rpc.CreateBranchRequest,
) (*rpc.CreateBranchResponse, error) {
if err := check.BranchName(request.BranchName); err != nil {
return nil, ErrInvalidArgument(err)
}
base := request.GetBase()
if base == nil {
return nil, types.ErrBaseCannotBeEmpty
}
repoPath := getFullPathForRepo(s.reposRoot, base.GetRepoUid())
if ok, err := repoIsEmpty(ctx, repoPath); ok {
return nil, ErrInvalidArgumentf("branch cannot be created on empty repository", err)
}
sharedRepo, err := NewSharedRepo(s.tmpDir, base.GetRepoUid(), repoPath)
if err != nil {
return nil, processGitErrorf(err, "failed to create new shared repo")
}
defer sharedRepo.Close(ctx)
// clone repo (with HEAD branch - target might be anything)
err = sharedRepo.Clone(ctx, "")
if err != nil {
return nil, processGitErrorf(err, "failed to clone shared repo with branch '%s'", request.GetBranchName())
}
_, err = sharedRepo.GetBranchCommit(request.GetBranchName())
// return an error if branch alredy exists (push doesn't fail if it's a noop or fast forward push)
if err == nil {
return nil, ErrAlreadyExistsf("branch '%s' already exists", request.GetBranchName())
}
if !git.IsErrNotExist(err) {
return nil, processGitErrorf(err, "branch creation of '%s' failed", request.GetBranchName())
}
// get target commit (as target could be branch/tag/commit, and tag can't be pushed using source:destination syntax)
targetCommit, err := s.adapter.GetCommit(ctx, sharedRepo.tmpPath, strings.TrimSpace(request.GetTarget()))
if git.IsErrNotExist(err) {
return nil, ErrNotFoundf("target '%s' doesn't exist", request.GetTarget())
}
if err != nil {
return nil, processGitErrorf(err, "failed to get commit id for target '%s'", request.GetTarget())
}
// push to new branch (all changes should go through push flow for hooks and other safety meassures)
err = sharedRepo.PushCommitToBranch(ctx, base, targetCommit.SHA, request.GetBranchName())
if err != nil {
return nil, processGitErrorf(err, "failed to push new branch '%s'", request.GetBranchName())
}
// get branch
// TODO: get it from shared repo to avoid opening another gitea repo and having to strip here.
gitBranch, err := s.adapter.GetBranch(ctx, repoPath,
strings.TrimPrefix(request.GetBranchName(), gitReferenceNamePrefixBranch))
if err != nil {
return nil, processGitErrorf(err, "failed to get gitea branch '%s'", request.GetBranchName())
}
branch, err := mapGitBranch(gitBranch)
if err != nil {
return nil, err
}
return &rpc.CreateBranchResponse{
Branch: branch,
}, nil
}
func (s ReferenceService) GetBranch(ctx context.Context,
request *rpc.GetBranchRequest) (*rpc.GetBranchResponse, error) {
base := request.GetBase()
if base == nil {
return nil, types.ErrBaseCannotBeEmpty
}
repoPath := getFullPathForRepo(s.reposRoot, base.GetRepoUid())
gitBranch, err := s.adapter.GetBranch(ctx, repoPath,
strings.TrimPrefix(request.GetBranchName(), gitReferenceNamePrefixBranch))
if err != nil {
return nil, processGitErrorf(err, "failed to get gitea branch '%s'", request.GetBranchName())
}
branch, err := mapGitBranch(gitBranch)
if err != nil {
return nil, err
}
return &rpc.GetBranchResponse{
Branch: branch,
}, nil
}
func (s ReferenceService) DeleteBranch(ctx context.Context,
request *rpc.DeleteBranchRequest) (*rpc.DeleteBranchResponse, error) {
base := request.GetBase()
if base == nil {
return nil, types.ErrBaseCannotBeEmpty
}
repoPath := getFullPathForRepo(s.reposRoot, base.GetRepoUid())
sharedRepo, err := NewSharedRepo(s.tmpDir, base.GetRepoUid(), repoPath)
if err != nil {
return nil, processGitErrorf(err, "failed to create new shared repo")
}
defer sharedRepo.Close(ctx)
// clone repo (technically we don't care about which branch we clone)
err = sharedRepo.Clone(ctx, request.GetBranchName())
if err != nil {
return nil, processGitErrorf(err, "failed to clone shared repo with branch '%s'", request.GetBranchName())
}
// get latest branch commit before we delete
gitCommit, err := sharedRepo.GetBranchCommit(request.GetBranchName())
if err != nil {
return nil, processGitErrorf(err, "failed to get gitea commit for branch '%s'", request.GetBranchName())
}
// push to new branch (all changes should go through push flow for hooks and other safety meassures)
// NOTE: setting sourceRef to empty will delete the remote branch when pushing:
// https://git-scm.com/docs/git-push#Documentation/git-push.txt-ltrefspecgt82308203
err = sharedRepo.PushDeleteBranch(ctx, base, request.GetBranchName())
if err != nil {
return nil, processGitErrorf(err, "failed to delete branch '%s' from remote repo", request.GetBranchName())
}
return &rpc.DeleteBranchResponse{
Sha: gitCommit.ID.String(),
}, nil
}
func (s ReferenceService) ListBranches(request *rpc.ListBranchesRequest,
stream rpc.ReferenceService_ListBranchesServer) error {
base := request.GetBase()
if base == nil {
return types.ErrBaseCannotBeEmpty
}
ctx := stream.Context()
repoPath := getFullPathForRepo(s.reposRoot, base.GetRepoUid())
// get all required information from git references
branches, err := s.listBranchesLoadReferenceData(ctx, repoPath, request)
if err != nil {
return err
}
// get commits if needed (single call for perf savings: 1s-4s vs 5s-20s)
if request.GetIncludeCommit() {
commitSHAs := make([]string, len(branches))
for i := range branches {
commitSHAs[i] = branches[i].Sha
}
var gitCommits []types.Commit
gitCommits, err = s.adapter.GetCommits(ctx, repoPath, commitSHAs)
if err != nil {
return status.Errorf(codes.Internal, "failed to get commits: %v", err)
}
for i := range gitCommits {
branches[i].Commit, err = mapGitCommit(&gitCommits[i])
if err != nil {
return err
}
}
}
// send out all branches
for _, branch := range branches {
err = stream.Send(&rpc.ListBranchesResponse{
Branch: branch,
})
if err != nil {
return status.Errorf(codes.Internal, "failed to send branch: %v", err)
}
}
return nil
}
func (s ReferenceService) listBranchesLoadReferenceData(ctx context.Context,
repoPath string, request *rpc.ListBranchesRequest) ([]*rpc.Branch, error) {
// TODO: can we be smarter with slice allocation
branches := make([]*rpc.Branch, 0, 16)
handler := listBranchesWalkReferencesHandler(&branches)
instructor, endsAfter, err := wrapInstructorWithOptionalPagination(
gitea.DefaultInstructor, // branches only have one target type, default instructor is enough
request.GetPage(),
request.GetPageSize())
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid pagination details: %v", err)
}
opts := &types.WalkReferencesOptions{
Patterns: createReferenceWalkPatternsFromQuery(gitReferenceNamePrefixBranch, request.GetQuery()),
Sort: mapListBranchesSortOption(request.Sort),
Order: mapSortOrder(request.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.adapter.WalkReferences(ctx, repoPath, handler, opts)
if err != nil {
return nil, processGitErrorf(err, "failed to walk branch references")
}
log.Ctx(ctx).Trace().Msgf("git adapter returned %d branches", len(branches))
return branches, nil
}
func listBranchesWalkReferencesHandler(branches *[]*rpc.Branch) types.WalkReferencesHandler {
return func(e types.WalkReferencesEntry) error {
fullRefName, ok := e[types.GitReferenceFieldRefName]
if !ok {
return fmt.Errorf("entry missing reference name")
}
objectSHA, ok := e[types.GitReferenceFieldObjectName]
if !ok {
return fmt.Errorf("entry missing object sha")
}
branch := &rpc.Branch{
Name: fullRefName[len(gitReferenceNamePrefixBranch):],
Sha: objectSHA,
}
// TODO: refactor to not use slice pointers?
*branches = append(*branches, branch)
return nil
}
}