mirror of https://github.com/harness/drone.git
278 lines
8.9 KiB
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
|
|
}
|
|
}
|