mirror of
https://github.com/harness/drone.git
synced 2025-05-11 06:30:06 +08:00
[GIT] Fix commit API for empty repo, remove unused functions
This commit is contained in:
parent
81dd8a5908
commit
e448c6f763
@ -7,58 +7,12 @@ package gitea
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/harness/gitness/gitrpc/internal/types"
|
"github.com/harness/gitness/gitrpc/internal/types"
|
||||||
|
|
||||||
gitea "code.gitea.io/gitea/modules/git"
|
gitea "code.gitea.io/gitea/modules/git"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateBranch creates a new branch.
|
|
||||||
// Note: target is the commit (or points to the commit) the branch will be pointing to.
|
|
||||||
func (g Adapter) CreateBranch(ctx context.Context, repoPath string,
|
|
||||||
branchName string, target string) (*types.Branch, error) {
|
|
||||||
giteaRepo, err := gitea.OpenRepository(ctx, repoPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer giteaRepo.Close()
|
|
||||||
|
|
||||||
// Get the commit object for the ref
|
|
||||||
giteaCommit, err := giteaRepo.GetCommit(target)
|
|
||||||
if err != nil {
|
|
||||||
return nil, processGiteaErrorf(err, "error getting commit for ref '%s'", target)
|
|
||||||
}
|
|
||||||
|
|
||||||
// In case of target being an annotated tag, gitea is overwriting the commit message with the tag message.
|
|
||||||
// Reload the commit explicitly in case it's a tag (independent of whether it's annotated or not to simplify code)
|
|
||||||
// NOTE: we also allow passing refs/tags/tagName or tags/tagName, which is not covered by IsTagExist.
|
|
||||||
// Worst case we have a false positive and reload the same commit, we don't want false negatives though!
|
|
||||||
if strings.HasPrefix(target, gitea.TagPrefix) || strings.HasPrefix(target, "tags") || giteaRepo.IsTagExist(target) {
|
|
||||||
giteaCommit, err = giteaRepo.GetCommit(giteaCommit.ID.String())
|
|
||||||
if err != nil {
|
|
||||||
return nil, processGiteaErrorf(err, "error getting commit for annotated tag '%s' (commitId '%s')",
|
|
||||||
target, giteaCommit.ID.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = giteaRepo.CreateBranch(branchName, giteaCommit.ID.String())
|
|
||||||
if err != nil {
|
|
||||||
return nil, processGiteaErrorf(err, "failed to create branch '%s'", branchName)
|
|
||||||
}
|
|
||||||
|
|
||||||
commit, err := mapGiteaCommit(giteaCommit)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to map gitea commit: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &types.Branch{
|
|
||||||
Name: branchName,
|
|
||||||
SHA: giteaCommit.ID.String(),
|
|
||||||
Commit: commit,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBranch gets an existing branch.
|
// GetBranch gets an existing branch.
|
||||||
func (g Adapter) GetBranch(ctx context.Context, repoPath string,
|
func (g Adapter) GetBranch(ctx context.Context, repoPath string,
|
||||||
branchName string) (*types.Branch, error) {
|
branchName string) (*types.Branch, error) {
|
||||||
@ -89,24 +43,3 @@ func (g Adapter) GetBranch(ctx context.Context, repoPath string,
|
|||||||
Commit: commit,
|
Commit: commit,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteBranch deletes an existing branch.
|
|
||||||
func (g Adapter) DeleteBranch(ctx context.Context, repoPath string, branchName string, force bool) (string, error) {
|
|
||||||
giteaRepo, err := gitea.OpenRepository(ctx, repoPath)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer giteaRepo.Close()
|
|
||||||
|
|
||||||
sha, err := giteaRepo.GetRefCommitID(branchName)
|
|
||||||
if err != nil {
|
|
||||||
return "", processGiteaErrorf(err, "failed to read sha of branch '%s'", branchName)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = giteaRepo.DeleteBranch(branchName, gitea.DeleteBranchOptions{Force: force})
|
|
||||||
if err != nil {
|
|
||||||
return "", processGiteaErrorf(err, "failed to delete branch '%s'", branchName)
|
|
||||||
}
|
|
||||||
|
|
||||||
return sha, nil
|
|
||||||
}
|
|
||||||
|
@ -93,12 +93,6 @@ func (g Adapter) ListCommits(ctx context.Context, repoPath string,
|
|||||||
}
|
}
|
||||||
defer giteaRepo.Close()
|
defer giteaRepo.Close()
|
||||||
|
|
||||||
// Get the refCommitSHA object for the ref
|
|
||||||
refCommitSHA, err := giteaRepo.GetRefCommitID(ref)
|
|
||||||
if err != nil {
|
|
||||||
return nil, processGiteaErrorf(err, "error getting commit ID for ref '%s'", ref)
|
|
||||||
}
|
|
||||||
|
|
||||||
args := []string{"rev-list"}
|
args := []string{"rev-list"}
|
||||||
|
|
||||||
// add pagination if requested
|
// add pagination if requested
|
||||||
@ -112,17 +106,12 @@ func (g Adapter) ListCommits(ctx context.Context, repoPath string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// add refCommitSHA as starting point
|
// add refCommitSHA as starting point
|
||||||
args = append(args, refCommitSHA)
|
args = append(args, ref)
|
||||||
|
|
||||||
// return commits only up to a certain reference if requested
|
// return commits only up to a certain reference if requested
|
||||||
if afterRef != "" {
|
if afterRef != "" {
|
||||||
var afterRefCommitSHA string
|
// ^REF tells the rev-list command to return only commits that aren't reachable by SHA
|
||||||
afterRefCommitSHA, err = giteaRepo.GetRefCommitID(afterRef)
|
args = append(args, fmt.Sprintf("^%s", afterRef))
|
||||||
if err != nil {
|
|
||||||
return nil, processGiteaErrorf(err, "error getting commit ID for afterRef '%s'", afterRef)
|
|
||||||
}
|
|
||||||
// ^SHA tells the rev-list command to return only commits that aren't reachable by SHA
|
|
||||||
args = append(args, fmt.Sprintf("^%s", afterRefCommitSHA))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stdout, _, runErr := gitea.NewCommand(giteaRepo.Ctx, args...).RunStdBytes(&gitea.RunOpts{Dir: giteaRepo.Path})
|
stdout, _, runErr := gitea.NewCommand(giteaRepo.Ctx, args...).RunStdBytes(&gitea.RunOpts{Dir: giteaRepo.Path})
|
||||||
|
@ -7,6 +7,7 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/harness/gitness/gitrpc/internal/gitea"
|
"github.com/harness/gitness/gitrpc/internal/gitea"
|
||||||
"github.com/harness/gitness/gitrpc/internal/types"
|
"github.com/harness/gitness/gitrpc/internal/types"
|
||||||
@ -48,14 +49,15 @@ func (s ReferenceService) CreateBranch(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// push to new branch (all changes should go through push flow for hooks and other safety meassures)
|
// push to new branch (all changes should go through push flow for hooks and other safety meassures)
|
||||||
err = sharedRepo.Push(ctx, base, request.GetTarget(), request.GetBranchName())
|
err = sharedRepo.PushBranch(ctx, base, request.GetTarget(), request.GetBranchName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, processGitErrorf(err, "failed to push new branch '%s'", request.GetBranchName())
|
return nil, processGitErrorf(err, "failed to push new branch '%s'", request.GetBranchName())
|
||||||
}
|
}
|
||||||
|
|
||||||
// get branch
|
// get branch
|
||||||
// TODO: get it from shared repo to avoid opening another gitea repo
|
// TODO: get it from shared repo to avoid opening another gitea repo and having to strip here.
|
||||||
gitBranch, err := s.adapter.GetBranch(ctx, repoPath, request.GetBranchName())
|
gitBranch, err := s.adapter.GetBranch(ctx, repoPath,
|
||||||
|
strings.TrimPrefix(request.GetBranchName(), gitReferenceNamePrefixBranch))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, processGitErrorf(err, "failed to get gitea branch '%s'", request.GetBranchName())
|
return nil, processGitErrorf(err, "failed to get gitea branch '%s'", request.GetBranchName())
|
||||||
}
|
}
|
||||||
@ -106,7 +108,7 @@ func (s ReferenceService) DeleteBranch(ctx context.Context,
|
|||||||
// push to new branch (all changes should go through push flow for hooks and other safety meassures)
|
// 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:
|
// NOTE: setting sourceRef to empty will delete the remote branch when pushing:
|
||||||
// https://git-scm.com/docs/git-push#Documentation/git-push.txt-ltrefspecgt82308203
|
// https://git-scm.com/docs/git-push#Documentation/git-push.txt-ltrefspecgt82308203
|
||||||
err = sharedRepo.Push(ctx, base, "", request.GetBranchName())
|
err = sharedRepo.PushDeleteBranch(ctx, base, request.GetBranchName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, processGitErrorf(err, "failed to delete branch '%s' from remote repo", request.GetBranchName())
|
return nil, processGitErrorf(err, "failed to delete branch '%s' from remote repo", request.GetBranchName())
|
||||||
}
|
}
|
||||||
|
@ -36,9 +36,7 @@ type GitAdapter interface {
|
|||||||
GetFullCommitID(ctx context.Context, repoPath, shortID string) (string, error)
|
GetFullCommitID(ctx context.Context, repoPath, shortID string) (string, error)
|
||||||
GetAnnotatedTag(ctx context.Context, repoPath string, sha string) (*types.Tag, error)
|
GetAnnotatedTag(ctx context.Context, repoPath string, sha string) (*types.Tag, error)
|
||||||
GetAnnotatedTags(ctx context.Context, repoPath string, shas []string) ([]types.Tag, error)
|
GetAnnotatedTags(ctx context.Context, repoPath string, shas []string) ([]types.Tag, error)
|
||||||
CreateBranch(ctx context.Context, repoPath string, branchName string, target string) (*types.Branch, error)
|
|
||||||
GetBranch(ctx context.Context, repoPath string, branchName string) (*types.Branch, error)
|
GetBranch(ctx context.Context, repoPath string, branchName string) (*types.Branch, error)
|
||||||
DeleteBranch(ctx context.Context, repoPath string, branchName string, force bool) (string, error)
|
|
||||||
GetCommitDivergences(ctx context.Context, repoPath string,
|
GetCommitDivergences(ctx context.Context, repoPath string,
|
||||||
requests []types.CommitDivergenceRequest, max int32) ([]types.CommitDivergence, error)
|
requests []types.CommitDivergenceRequest, max int32) ([]types.CommitDivergence, error)
|
||||||
GetRef(ctx context.Context, repoPath string, name string, refType types.RefType) (string, error)
|
GetRef(ctx context.Context, repoPath string, name string, refType types.RefType) (string, error)
|
||||||
|
@ -23,7 +23,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
filePrefix = "file://"
|
filePrefix = "file://"
|
||||||
|
defaultFilePermission = "100644" // 0o644 default file permission
|
||||||
)
|
)
|
||||||
|
|
||||||
type CommitFilesService struct {
|
type CommitFilesService struct {
|
||||||
@ -81,35 +82,44 @@ func (s *CommitFilesService) CommitFiles(stream rpc.CommitFilesService_CommitFil
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = s.validateHeader(repo, header); err != nil {
|
// check if repo is empty
|
||||||
|
// IMPORTANT: we don't use gitea's repo.IsEmpty() as that only checks whether the default branch exists (in HEAD).
|
||||||
|
// This can be an issue in case someone created a branch already in the repo (just default branch is missing).
|
||||||
|
// In that case the user can accidentaly create separate git histories (which most likely is unintended).
|
||||||
|
// If the user wants to actually build a disconnected commit graph they can use the cli.
|
||||||
|
isEmpty, err := repoHasBranches(ctx, repo)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to determine if repo is empty: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure input data is valid
|
||||||
|
if err = s.validateAndPrepareHeader(repo, isEmpty, header); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// collect all file actions from grpc stream
|
||||||
actions := make([]fileAction, 0, 16)
|
actions := make([]fileAction, 0, 16)
|
||||||
if err = s.collectActions(stream, &actions); err != nil {
|
if err = s.collectActions(stream, &actions); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// create a shared repo
|
// create a new shared repo
|
||||||
shared, err := NewSharedRepo(s.reposTempDir, base.GetRepoUid(), repo)
|
shared, err := NewSharedRepo(s.reposTempDir, base.GetRepoUid(), repo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer shared.Close(ctx)
|
defer shared.Close(ctx)
|
||||||
|
|
||||||
if err = s.clone(ctx, repo, shared, header.GetBranchName()); err != nil {
|
// handle empty repo separately (as branch doesn't exist, no commit exists, ...)
|
||||||
return err
|
var parentCommitSHA string
|
||||||
}
|
if isEmpty {
|
||||||
|
err = s.prepareTreeEmptyRepo(ctx, shared, actions)
|
||||||
// Get the commit of the original branch
|
if err != nil {
|
||||||
commit, err := shared.GetBranchCommit(header.GetBranchName())
|
return err
|
||||||
if err != nil {
|
}
|
||||||
return err
|
} else {
|
||||||
}
|
parentCommitSHA, err = s.prepareTree(ctx, shared, header.GetBranchName(), actions)
|
||||||
|
if err != nil {
|
||||||
for _, action := range actions {
|
|
||||||
action := action
|
|
||||||
if err = s.processAction(ctx, shared, &action, commit); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -125,16 +135,16 @@ func (s *CommitFilesService) CommitFiles(stream rpc.CommitFilesService_CommitFil
|
|||||||
message += "\n\n" + strings.TrimSpace(header.GetMessage())
|
message += "\n\n" + strings.TrimSpace(header.GetMessage())
|
||||||
}
|
}
|
||||||
// Now commit the tree
|
// Now commit the tree
|
||||||
commitHash, err := shared.CommitTree(ctx, commit.ID.String(), author, committer, treeHash, message, false)
|
commitSHA, err := shared.CommitTree(ctx, parentCommitSHA, author, committer, treeHash, message, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = shared.Push(ctx, base, commitHash, header.GetNewBranchName()); err != nil {
|
if err = shared.PushCommit(ctx, base, commitSHA, header.GetNewBranchName()); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
commit, err = shared.GetCommit(commitHash)
|
commit, err := shared.GetCommit(commitSHA)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -144,23 +154,86 @@ func (s *CommitFilesService) CommitFiles(stream rpc.CommitFilesService_CommitFil
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *CommitFilesService) validateHeader(repo *git.Repository, header *rpc.CommitFilesRequestHeader) error {
|
func (s *CommitFilesService) prepareTree(ctx context.Context, shared *SharedRepo,
|
||||||
|
branchName string, actions []fileAction) (string, error) {
|
||||||
|
// clone original branch from repo
|
||||||
|
if err := s.clone(ctx, shared, branchName); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the latest commit of the original branch
|
||||||
|
commit, err := shared.GetBranchCommit(branchName)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// execute all actions
|
||||||
|
for _, action := range actions {
|
||||||
|
action := action
|
||||||
|
if err = s.processAction(ctx, shared, &action, commit); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return commit.ID.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CommitFilesService) prepareTreeEmptyRepo(ctx context.Context, shared *SharedRepo,
|
||||||
|
actions []fileAction) error {
|
||||||
|
// init a new repo (full clone would cause risk that by time of push someone wrote to the remote repo!)
|
||||||
|
err := shared.Init(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to init shared tmp repo: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, action := range actions {
|
||||||
|
if action.header.Action != rpc.CommitFilesActionHeader_CREATE {
|
||||||
|
return types.ErrActionNotAllowedOnEmptyRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := files.CleanUploadFileName(action.header.GetPath())
|
||||||
|
if filePath == "" {
|
||||||
|
return types.ErrInvalidPath
|
||||||
|
}
|
||||||
|
|
||||||
|
reader := bytes.NewReader(action.content)
|
||||||
|
if err = createFile(ctx, shared, nil, filePath, defaultFilePermission, reader); err != nil {
|
||||||
|
return fmt.Errorf("failed to create file '%s': %w", action.header.Path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CommitFilesService) validateAndPrepareHeader(repo *git.Repository, isEmpty bool,
|
||||||
|
header *rpc.CommitFilesRequestHeader) error {
|
||||||
if header.GetBranchName() == "" {
|
if header.GetBranchName() == "" {
|
||||||
branch, err := repo.GetDefaultBranch()
|
defaultBranchRef, err := repo.GetDefaultBranch()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
header.BranchName = branch
|
header.BranchName = defaultBranchRef
|
||||||
}
|
}
|
||||||
|
|
||||||
if header.GetNewBranchName() == "" {
|
if header.GetNewBranchName() == "" {
|
||||||
header.NewBranchName = header.GetBranchName()
|
header.NewBranchName = header.GetBranchName()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// trim refs/heads/ prefixes to avoid issues when calling gitea API
|
||||||
|
header.BranchName = strings.TrimPrefix(strings.TrimSpace(header.GetBranchName()), gitReferenceNamePrefixBranch)
|
||||||
|
header.NewBranchName = strings.TrimPrefix(strings.TrimSpace(header.GetNewBranchName()), gitReferenceNamePrefixBranch)
|
||||||
|
|
||||||
|
// if the repo is empty then we can skip branch existance checks
|
||||||
|
if isEmpty {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure source branch exists
|
||||||
if _, err := repo.GetBranch(header.GetBranchName()); err != nil {
|
if _, err := repo.GetBranch(header.GetBranchName()); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ensure new branch doesn't exist yet (if new branch creation was requested)
|
||||||
if header.GetBranchName() != header.GetNewBranchName() {
|
if header.GetBranchName() != header.GetNewBranchName() {
|
||||||
existingBranch, err := repo.GetBranch(header.GetNewBranchName())
|
existingBranch, err := repo.GetBranch(header.GetNewBranchName())
|
||||||
if existingBranch != nil {
|
if existingBranch != nil {
|
||||||
@ -175,21 +248,18 @@ func (s *CommitFilesService) validateHeader(repo *git.Repository, header *rpc.Co
|
|||||||
|
|
||||||
func (s *CommitFilesService) clone(
|
func (s *CommitFilesService) clone(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
repo *git.Repository,
|
|
||||||
shared *SharedRepo,
|
shared *SharedRepo,
|
||||||
branch string,
|
branch string,
|
||||||
) error {
|
) error {
|
||||||
if err := shared.Clone(ctx, branch); err != nil {
|
if err := shared.Clone(ctx, branch); err != nil {
|
||||||
empty, _ := repo.IsEmpty()
|
return fmt.Errorf("failed to clone branch '%s': %w", branch, err)
|
||||||
if !git.IsErrBranchNotExist(err) || !empty {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if errInit := shared.Init(ctx); errInit != nil {
|
|
||||||
return errInit
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
return shared.SetDefaultIndex(ctx)
|
|
||||||
|
if err := shared.SetDefaultIndex(ctx); err != nil {
|
||||||
|
return fmt.Errorf("failed to set default index: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *CommitFilesService) collectActions(
|
func (s *CommitFilesService) collectActions(
|
||||||
@ -254,16 +324,15 @@ func (s *CommitFilesService) processAction(
|
|||||||
return types.ErrInvalidPath
|
return types.ErrInvalidPath
|
||||||
}
|
}
|
||||||
|
|
||||||
mode := "100644" // 0o644 default file permission
|
|
||||||
reader := bytes.NewReader(action.content)
|
reader := bytes.NewReader(action.content)
|
||||||
|
|
||||||
switch header.Action {
|
switch header.Action {
|
||||||
case rpc.CommitFilesActionHeader_CREATE:
|
case rpc.CommitFilesActionHeader_CREATE:
|
||||||
err = createFile(ctx, shared, commit, filePath, mode, reader)
|
err = createFile(ctx, shared, commit, filePath, defaultFilePermission, reader)
|
||||||
case rpc.CommitFilesActionHeader_UPDATE:
|
case rpc.CommitFilesActionHeader_UPDATE:
|
||||||
err = updateFile(ctx, shared, commit, filePath, header.GetSha(), mode, reader)
|
err = updateFile(ctx, shared, commit, filePath, header.GetSha(), defaultFilePermission, reader)
|
||||||
case rpc.CommitFilesActionHeader_MOVE:
|
case rpc.CommitFilesActionHeader_MOVE:
|
||||||
err = moveFile(ctx, shared, commit, filePath, mode, reader)
|
err = moveFile(ctx, shared, commit, filePath, defaultFilePermission, reader)
|
||||||
case rpc.CommitFilesActionHeader_DELETE:
|
case rpc.CommitFilesActionHeader_DELETE:
|
||||||
err = deleteFile(ctx, shared, filePath)
|
err = deleteFile(ctx, shared, filePath)
|
||||||
}
|
}
|
||||||
@ -271,22 +340,20 @@ func (s *CommitFilesService) processAction(
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func createFile(ctx context.Context, repo *SharedRepo, commit *git.Commit, filePath,
|
func createFile(ctx context.Context, repo *SharedRepo, commit *git.Commit,
|
||||||
mode string, reader io.Reader) error {
|
filePath, mode string, reader io.Reader) error {
|
||||||
if err := checkPath(commit, filePath, true); err != nil {
|
// only check path availability if a source commit is available (empty repo won't have such a commit)
|
||||||
return err
|
if commit != nil {
|
||||||
|
if err := checkPathAvailability(commit, filePath, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
filesInIndex, err := repo.LsFiles(ctx, filePath)
|
|
||||||
if err != nil {
|
hash, err := repo.WriteGitObject(ctx, reader)
|
||||||
return fmt.Errorf("listing files error %w", err)
|
|
||||||
}
|
|
||||||
if slices.Contains(filesInIndex, filePath) {
|
|
||||||
return fmt.Errorf("%s %w", filePath, types.ErrAlreadyExists)
|
|
||||||
}
|
|
||||||
hash, err := repo.HashObject(ctx, reader)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error hashing object %w", err)
|
return fmt.Errorf("error hashing object %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the object to the index
|
// Add the object to the index
|
||||||
if err = repo.AddObjectToIndex(ctx, mode, hash, filePath); err != nil {
|
if err = repo.AddObjectToIndex(ctx, mode, hash, filePath); err != nil {
|
||||||
return fmt.Errorf("error creating object: %w", err)
|
return fmt.Errorf("error creating object: %w", err)
|
||||||
@ -296,27 +363,20 @@ func createFile(ctx context.Context, repo *SharedRepo, commit *git.Commit, fileP
|
|||||||
|
|
||||||
func updateFile(ctx context.Context, repo *SharedRepo, commit *git.Commit, filePath, sha,
|
func updateFile(ctx context.Context, repo *SharedRepo, commit *git.Commit, filePath, sha,
|
||||||
mode string, reader io.Reader) error {
|
mode string, reader io.Reader) error {
|
||||||
filesInIndex, err := repo.LsFiles(ctx, filePath)
|
// get file mode from existing file (default unless executable)
|
||||||
|
entry, err := getFileEntry(commit, sha, filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("listing files error %w", err)
|
return fmt.Errorf("failed to get file entry: %w", err)
|
||||||
}
|
}
|
||||||
if !slices.Contains(filesInIndex, filePath) {
|
if entry.IsExecutable() {
|
||||||
return fmt.Errorf("%s %w", filePath, types.ErrNotFound)
|
mode = "100755"
|
||||||
}
|
}
|
||||||
if commit != nil {
|
|
||||||
var entry *git.TreeEntry
|
hash, err := repo.WriteGitObject(ctx, reader)
|
||||||
entry, err = getFileEntry(commit, sha, filePath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if entry.IsExecutable() {
|
|
||||||
mode = "100755"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hash, err := repo.HashObject(ctx, reader)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error hashing object %w", err)
|
return fmt.Errorf("error hashing object %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = repo.AddObjectToIndex(ctx, mode, hash, filePath); err != nil {
|
if err = repo.AddObjectToIndex(ctx, mode, hash, filePath); err != nil {
|
||||||
return fmt.Errorf("error updating object: %w", err)
|
return fmt.Errorf("error updating object: %w", err)
|
||||||
}
|
}
|
||||||
@ -338,9 +398,10 @@ func moveFile(ctx context.Context, repo *SharedRepo, commit *git.Commit,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = checkPath(commit, newPath, false); err != nil {
|
if err = checkPathAvailability(commit, newPath, false); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
filesInIndex, err := repo.LsFiles(ctx, filePath)
|
filesInIndex, err := repo.LsFiles(ctx, filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("listing files error %w", err)
|
return fmt.Errorf("listing files error %w", err)
|
||||||
@ -348,16 +409,16 @@ func moveFile(ctx context.Context, repo *SharedRepo, commit *git.Commit,
|
|||||||
if !slices.Contains(filesInIndex, filePath) {
|
if !slices.Contains(filesInIndex, filePath) {
|
||||||
return fmt.Errorf("%s %w", filePath, types.ErrNotFound)
|
return fmt.Errorf("%s %w", filePath, types.ErrNotFound)
|
||||||
}
|
}
|
||||||
if slices.Contains(filesInIndex, newPath) {
|
|
||||||
return fmt.Errorf("%s %w", filePath, types.ErrAlreadyExists)
|
hash, err := repo.WriteGitObject(ctx, buffer)
|
||||||
}
|
|
||||||
hash, err := repo.HashObject(ctx, buffer)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error hashing object %w", err)
|
return fmt.Errorf("error hashing object %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = repo.AddObjectToIndex(ctx, mode, hash, newPath); err != nil {
|
if err = repo.AddObjectToIndex(ctx, mode, hash, newPath); err != nil {
|
||||||
return fmt.Errorf("created object: %w", err)
|
return fmt.Errorf("add object: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = repo.RemoveFilesFromIndex(ctx, filePath); err != nil {
|
if err = repo.RemoveFilesFromIndex(ctx, filePath); err != nil {
|
||||||
return fmt.Errorf("remove object: %w", err)
|
return fmt.Errorf("remove object: %w", err)
|
||||||
}
|
}
|
||||||
@ -372,6 +433,7 @@ func deleteFile(ctx context.Context, repo *SharedRepo, filePath string) error {
|
|||||||
if !slices.Contains(filesInIndex, filePath) {
|
if !slices.Contains(filesInIndex, filePath) {
|
||||||
return fmt.Errorf("%s %w", filePath, types.ErrNotFound)
|
return fmt.Errorf("%s %w", filePath, types.ErrNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = repo.RemoveFilesFromIndex(ctx, filePath); err != nil {
|
if err = repo.RemoveFilesFromIndex(ctx, filePath); err != nil {
|
||||||
return fmt.Errorf("remove object: %w", err)
|
return fmt.Errorf("remove object: %w", err)
|
||||||
}
|
}
|
||||||
@ -384,22 +446,28 @@ func getFileEntry(
|
|||||||
path string,
|
path string,
|
||||||
) (*git.TreeEntry, error) {
|
) (*git.TreeEntry, error) {
|
||||||
entry, err := commit.GetTreeEntryByPath(path)
|
entry, err := commit.GetTreeEntryByPath(path)
|
||||||
|
if git.IsErrNotExist(err) {
|
||||||
|
return nil, fmt.Errorf("%s %w", path, types.ErrNotFound)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a SHA was given and the SHA given doesn't match the SHA of the fromTreePath, throw error
|
// If a SHA was given and the SHA given doesn't match the SHA of the fromTreePath, throw error
|
||||||
if sha == "" || sha != entry.ID.String() {
|
if sha == "" || sha != entry.ID.String() {
|
||||||
return nil, fmt.Errorf("%w for path %s [given: %s, expected: %s]",
|
return nil, fmt.Errorf("%w for path %s [given: %s, expected: %s]",
|
||||||
types.ErrSHADoesNotMatch, path, sha, entry.ID.String())
|
types.ErrSHADoesNotMatch, path, sha, entry.ID.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
return entry, nil
|
return entry, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkPath(commit *git.Commit, filePath string, isNewFile bool) error {
|
// checkPathAvailability ensures that the path is available for the requested operation.
|
||||||
// For the path where this file will be created/updated, we need to make
|
// For the path where this file will be created/updated, we need to make
|
||||||
// sure no parts of the path are existing files or links except for the last
|
// sure no parts of the path are existing files or links except for the last
|
||||||
// item in the path which is the file name, and that shouldn't exist IF it is
|
// item in the path which is the file name, and that shouldn't exist IF it is
|
||||||
// a new file OR is being moved to a new path.
|
// a new file OR is being moved to a new path.
|
||||||
|
func checkPathAvailability(commit *git.Commit, filePath string, isNewFile bool) error {
|
||||||
parts := strings.Split(filePath, "/")
|
parts := strings.Split(filePath, "/")
|
||||||
subTreePath := ""
|
subTreePath := ""
|
||||||
for index, part := range parts {
|
for index, part := range parts {
|
||||||
@ -431,6 +499,21 @@ func checkPath(commit *git.Commit, filePath string, isNewFile bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// repoHasBranches returns true iff there's at least one branch in the repo (any branch)
|
||||||
|
// NOTE: This is different from repo.Empty(),
|
||||||
|
// as it doesn't care whether the existing branch is the default branch or not.
|
||||||
|
func repoHasBranches(ctx context.Context, repo *git.Repository) (bool, error) {
|
||||||
|
// repo has branches IFF there's at least one commit that is reachable via a branch
|
||||||
|
// (every existing branch points to a commit)
|
||||||
|
stdout, _, runErr := git.NewCommand(ctx, "rev-list", "--max-count", "1", "--branches").
|
||||||
|
RunStdBytes(&git.RunOpts{Dir: repo.Path})
|
||||||
|
if runErr != nil {
|
||||||
|
return false, fmt.Errorf("failed to trigger rev-list command: %w", runErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimSpace(string(stdout)) == "", nil
|
||||||
|
}
|
||||||
|
|
||||||
func parsePayload(payload io.Reader, content io.Writer) (string, error) {
|
func parsePayload(payload io.Reader, content io.Writer) (string, error) {
|
||||||
newPath := ""
|
newPath := ""
|
||||||
reader := bufio.NewReader(payload)
|
reader := bufio.NewReader(payload)
|
||||||
|
@ -54,13 +54,14 @@ func (r *SharedRepo) Close(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clone the base repository to our path and set branch as the HEAD.
|
// Clone the base repository to our path and set branch as the HEAD.
|
||||||
func (r *SharedRepo) Clone(ctx context.Context, branch string) error {
|
func (r *SharedRepo) Clone(ctx context.Context, branchName string) error {
|
||||||
if _, _, err := git.NewCommand(ctx, "clone", "-s", "--bare", "-b",
|
if _, _, err := git.NewCommand(ctx, "clone", "-s", "--bare", "-b",
|
||||||
branch, r.remoteRepo.Path, r.tmpPath).RunStdString(nil); err != nil {
|
strings.TrimPrefix(branchName, gitReferenceNamePrefixBranch),
|
||||||
|
r.remoteRepo.Path, r.tmpPath).RunStdString(nil); err != nil {
|
||||||
stderr := err.Error()
|
stderr := err.Error()
|
||||||
if matched, _ := regexp.MatchString(".*Remote branch .* not found in upstream origin.*", stderr); matched {
|
if matched, _ := regexp.MatchString(".*Remote branch .* not found in upstream origin.*", stderr); matched {
|
||||||
return git.ErrBranchNotExist{
|
return git.ErrBranchNotExist{
|
||||||
Name: branch,
|
Name: branchName,
|
||||||
}
|
}
|
||||||
} else if matched, _ = regexp.MatchString(".* repository .* does not exist.*", stderr); matched {
|
} else if matched, _ = regexp.MatchString(".* repository .* does not exist.*", stderr); matched {
|
||||||
return fmt.Errorf("%s %w", r.repoUID, types.ErrNotFound)
|
return fmt.Errorf("%s %w", r.repoUID, types.ErrNotFound)
|
||||||
@ -154,8 +155,8 @@ func (r *SharedRepo) RemoveFilesFromIndex(ctx context.Context, filenames ...stri
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// HashObject writes the provided content to the object db and returns its hash.
|
// WriteGitObject writes the provided content to the object db and returns its hash.
|
||||||
func (r *SharedRepo) HashObject(ctx context.Context, content io.Reader) (string, error) {
|
func (r *SharedRepo) WriteGitObject(ctx context.Context, content io.Reader) (string, error) {
|
||||||
stdOut := new(bytes.Buffer)
|
stdOut := new(bytes.Buffer)
|
||||||
stdErr := new(bytes.Buffer)
|
stdErr := new(bytes.Buffer)
|
||||||
|
|
||||||
@ -296,13 +297,29 @@ func (r *SharedRepo) CommitTreeWithDate(
|
|||||||
return strings.TrimSpace(stdout.String()), nil
|
return strings.TrimSpace(stdout.String()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push the provided commitHash to the repository branch by the provided user.
|
func (r *SharedRepo) PushDeleteBranch(ctx context.Context, writeRequest *rpc.WriteRequest,
|
||||||
func (r *SharedRepo) Push(ctx context.Context, writeRequest *rpc.WriteRequest, sourceRef, branch string) error {
|
branch string) error {
|
||||||
|
return r.push(ctx, writeRequest, "", branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SharedRepo) PushCommit(ctx context.Context, writeRequest *rpc.WriteRequest,
|
||||||
|
commitSHA string, branch string) error {
|
||||||
|
return r.push(ctx, writeRequest, commitSHA, branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SharedRepo) PushBranch(ctx context.Context, writeRequest *rpc.WriteRequest,
|
||||||
|
sourceBranch string, branch string) error {
|
||||||
|
return r.push(ctx, writeRequest, GetReferenceFromBranchName(sourceBranch), branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// push the provided commitHash to the repository branch by the provided user.
|
||||||
|
func (r *SharedRepo) push(ctx context.Context, writeRequest *rpc.WriteRequest,
|
||||||
|
sourceRef, branch string) error {
|
||||||
// Because calls hooks we need to pass in the environment
|
// Because calls hooks we need to pass in the environment
|
||||||
env := CreateEnvironmentForPush(ctx, writeRequest)
|
env := CreateEnvironmentForPush(ctx, writeRequest)
|
||||||
if err := git.Push(ctx, r.tmpPath, git.PushOptions{
|
if err := git.Push(ctx, r.tmpPath, git.PushOptions{
|
||||||
Remote: r.remoteRepo.Path,
|
Remote: r.remoteRepo.Path,
|
||||||
Branch: strings.TrimSpace(sourceRef) + ":" + gitReferenceNamePrefixBranch + strings.TrimSpace(branch),
|
Branch: sourceRef + ":" + GetReferenceFromBranchName(branch),
|
||||||
Env: env,
|
Env: env,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
if git.IsErrPushOutOfDate(err) {
|
if git.IsErrPushOutOfDate(err) {
|
||||||
@ -327,7 +344,8 @@ func (r *SharedRepo) GetBranchCommit(branch string) (*git.Commit, error) {
|
|||||||
if r.repo == nil {
|
if r.repo == nil {
|
||||||
return nil, fmt.Errorf("repository has not been cloned")
|
return nil, fmt.Errorf("repository has not been cloned")
|
||||||
}
|
}
|
||||||
return r.repo.GetBranchCommit(branch)
|
|
||||||
|
return r.repo.GetBranchCommit(strings.TrimPrefix(branch, gitReferenceNamePrefixBranch))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCommit Gets the commit object of the given commit ID.
|
// GetCommit Gets the commit object of the given commit ID.
|
||||||
@ -359,3 +377,18 @@ func CreateEnvironmentForPush(ctx context.Context, writeRequest *rpc.WriteReques
|
|||||||
|
|
||||||
return environ
|
return environ
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetReferenceFromBranchName assumes the provided value is the branch name (not the ref!)
|
||||||
|
// and first sanitizes the branch name (remove any spaces or 'refs/heads/' prefix)
|
||||||
|
// It then returns the full form of the branch reference.
|
||||||
|
func GetReferenceFromBranchName(branchName string) string {
|
||||||
|
// remove spaces
|
||||||
|
branchName = strings.TrimSpace(branchName)
|
||||||
|
// remove `refs/heads/` prefix (shouldn't be there, but if it is remove it to try to avoid complications)
|
||||||
|
// NOTE: This is used to reduce missconfigurations via api
|
||||||
|
// TODO: block via CLI, too
|
||||||
|
branchName = strings.TrimPrefix(branchName, gitReferenceNamePrefixBranch)
|
||||||
|
|
||||||
|
// return reference
|
||||||
|
return gitReferenceNamePrefixBranch + branchName
|
||||||
|
}
|
||||||
|
@ -10,18 +10,19 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrAlreadyExists = errors.New("already exists")
|
ErrAlreadyExists = errors.New("already exists")
|
||||||
ErrInvalidArgument = errors.New("invalid argument")
|
ErrInvalidArgument = errors.New("invalid argument")
|
||||||
ErrNotFound = errors.New("not found")
|
ErrNotFound = errors.New("not found")
|
||||||
ErrInvalidPath = errors.New("path is invalid")
|
ErrInvalidPath = errors.New("path is invalid")
|
||||||
ErrUndefinedAction = errors.New("undefined action")
|
ErrUndefinedAction = errors.New("undefined action")
|
||||||
ErrContentSentBeforeAction = errors.New("content sent before action")
|
ErrActionNotAllowedOnEmptyRepo = errors.New("action not allowed on empty repository")
|
||||||
ErrActionListEmpty = errors.New("no commit actions to perform on repository")
|
ErrContentSentBeforeAction = errors.New("content sent before action")
|
||||||
ErrHeaderCannotBeEmpty = errors.New("header field cannot be empty")
|
ErrActionListEmpty = errors.New("no commit actions to perform on repository")
|
||||||
ErrBaseCannotBeEmpty = errors.New("base field cannot be empty")
|
ErrHeaderCannotBeEmpty = errors.New("header field cannot be empty")
|
||||||
ErrSHADoesNotMatch = errors.New("sha does not match")
|
ErrBaseCannotBeEmpty = errors.New("base field cannot be empty")
|
||||||
ErrEmptyLeftCommitID = errors.New("empty LeftCommitId")
|
ErrSHADoesNotMatch = errors.New("sha does not match")
|
||||||
ErrEmptyRightCommitID = errors.New("empty RightCommitId")
|
ErrEmptyLeftCommitID = errors.New("empty LeftCommitId")
|
||||||
|
ErrEmptyRightCommitID = errors.New("empty RightCommitId")
|
||||||
)
|
)
|
||||||
|
|
||||||
// MergeConflictsError represents an error if merging fails with a conflict.
|
// MergeConflictsError represents an error if merging fails with a conflict.
|
||||||
|
Loading…
Reference in New Issue
Block a user