mirror of https://github.com/harness/drone.git
391 lines
11 KiB
Go
391 lines
11 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"
|
|
"time"
|
|
|
|
"github.com/harness/gitness/errors"
|
|
"github.com/harness/gitness/git/api"
|
|
"github.com/harness/gitness/git/enum"
|
|
"github.com/harness/gitness/git/hook"
|
|
"github.com/harness/gitness/git/merge"
|
|
"github.com/harness/gitness/git/parser"
|
|
"github.com/harness/gitness/git/sha"
|
|
)
|
|
|
|
// MergeParams is input structure object for merging operation.
|
|
type MergeParams struct {
|
|
WriteParams
|
|
|
|
BaseSHA sha.SHA
|
|
BaseBranch string
|
|
|
|
// HeadRepoUID specifies the UID of the repo that contains the head branch (required for forking).
|
|
// WARNING: This field is currently not supported yet!
|
|
HeadRepoUID string
|
|
HeadBranch string
|
|
|
|
Title string // Deprecated
|
|
Message string
|
|
|
|
// Committer overwrites the git committer used for committing the files
|
|
// (optional, default: actor)
|
|
Committer *Identity
|
|
// CommitterDate overwrites the git committer date used for committing the files
|
|
// (optional, default: current time on server)
|
|
CommitterDate *time.Time
|
|
// Author overwrites the git author used for committing the files
|
|
// (optional, default: committer)
|
|
Author *Identity
|
|
// AuthorDate overwrites the git author date used for committing the files
|
|
// (optional, default: committer date)
|
|
AuthorDate *time.Time
|
|
|
|
RefType enum.RefType
|
|
RefName string
|
|
|
|
// HeadExpectedSHA is commit sha on the head branch, if HeadExpectedSHA is older
|
|
// than the HeadBranch latest sha then merge will fail.
|
|
HeadExpectedSHA sha.SHA
|
|
|
|
Force bool
|
|
DeleteHeadBranch bool
|
|
|
|
Method enum.MergeMethod
|
|
}
|
|
|
|
func (p *MergeParams) Validate() error {
|
|
if err := p.WriteParams.Validate(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if p.BaseBranch == "" && p.BaseSHA.IsEmpty() {
|
|
return errors.InvalidArgument("either base branch or commit SHA is mandatory")
|
|
}
|
|
|
|
if p.HeadBranch == "" {
|
|
return errors.InvalidArgument("head branch is mandatory")
|
|
}
|
|
|
|
if p.RefType != enum.RefTypeUndefined && p.RefName == "" {
|
|
return errors.InvalidArgument("ref name has to be provided if type is defined")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// MergeOutput is result object from merging and returns
|
|
// base, head and commit sha.
|
|
type MergeOutput struct {
|
|
// BaseSHA is the sha of the latest commit on the base branch that was used for merging.
|
|
BaseSHA sha.SHA
|
|
// HeadSHA is the sha of the latest commit on the head branch that was used for merging.
|
|
HeadSHA sha.SHA
|
|
// MergeBaseSHA is the sha of the merge base of the HeadSHA and BaseSHA
|
|
MergeBaseSHA sha.SHA
|
|
// MergeSHA is the sha of the commit after merging HeadSHA with BaseSHA.
|
|
MergeSHA sha.SHA
|
|
|
|
CommitCount int
|
|
ChangedFileCount int
|
|
Additions int
|
|
Deletions int
|
|
ConflictFiles []string
|
|
}
|
|
|
|
// Merge method executes git merge operation. Refs can be sha, branch or tag.
|
|
// Based on input params.RefType merge can do checking or final merging of two refs.
|
|
// some examples:
|
|
//
|
|
// params.RefType = Undefined -> discard merge commit (only performs a merge check).
|
|
// params.RefType = Raw and params.RefName = refs/pull/1/ref will push to refs/pullreq/1/ref
|
|
// params.RefType = RefTypeBranch and params.RefName = "somebranch" -> merge and push to refs/heads/somebranch
|
|
// params.RefType = RefTypePullReqHead and params.RefName = "1" -> merge and push to refs/pullreq/1/head
|
|
// params.RefType = RefTypePullReqMerge and params.RefName = "1" -> merge and push to refs/pullreq/1/merge
|
|
//
|
|
// There are cases when you want to block merging and for that you will need to provide
|
|
// params.HeadExpectedSHA which will be compared with the latest sha from head branch
|
|
// if they are not the same error will be returned.
|
|
//
|
|
//nolint:gocognit,gocyclo,cyclop
|
|
func (s *Service) Merge(ctx context.Context, params *MergeParams) (MergeOutput, error) {
|
|
err := params.Validate()
|
|
if err != nil {
|
|
return MergeOutput{}, fmt.Errorf("params not valid: %w", err)
|
|
}
|
|
|
|
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
|
|
|
|
// prepare the merge method function
|
|
|
|
mergeMethod, ok := params.Method.Sanitize()
|
|
if !ok && params.Method != "" {
|
|
return MergeOutput{}, errors.InvalidArgument("Unsupported merge method: %s", params.Method)
|
|
}
|
|
|
|
var mergeFunc merge.Func
|
|
|
|
switch mergeMethod {
|
|
case enum.MergeMethodMerge:
|
|
mergeFunc = merge.Merge
|
|
case enum.MergeMethodSquash:
|
|
mergeFunc = merge.Squash
|
|
case enum.MergeMethodRebase:
|
|
mergeFunc = merge.Rebase
|
|
case enum.MergeMethodFastForward:
|
|
mergeFunc = merge.FastForward
|
|
default:
|
|
// should not happen, the call to Sanitize above should handle this case.
|
|
panic("unsupported merge method")
|
|
}
|
|
|
|
// set up the target reference
|
|
|
|
var refPath string
|
|
var refOldValue sha.SHA
|
|
|
|
if params.RefType != enum.RefTypeUndefined {
|
|
refPath, err = GetRefPath(params.RefName, params.RefType)
|
|
if err != nil {
|
|
return MergeOutput{}, fmt.Errorf(
|
|
"failed to generate full reference for type '%s' and name '%s' for merge operation: %w",
|
|
params.RefType, params.RefName, err)
|
|
}
|
|
|
|
refOldValue, err = s.git.GetFullCommitID(ctx, repoPath, refPath)
|
|
if errors.IsNotFound(err) {
|
|
refOldValue = sha.Nil
|
|
} else if err != nil {
|
|
return MergeOutput{}, fmt.Errorf("failed to resolve %q: %w", refPath, err)
|
|
}
|
|
}
|
|
|
|
// find the commit SHAs
|
|
|
|
baseCommitSHA := params.BaseSHA
|
|
if baseCommitSHA.IsEmpty() {
|
|
baseCommitSHA, err = s.git.GetFullCommitID(ctx, repoPath, params.BaseBranch)
|
|
if err != nil {
|
|
return MergeOutput{}, fmt.Errorf("failed to get base branch commit SHA: %w", err)
|
|
}
|
|
}
|
|
|
|
headCommitSHA, err := s.git.GetFullCommitID(ctx, repoPath, params.HeadBranch)
|
|
if err != nil {
|
|
return MergeOutput{}, fmt.Errorf("failed to get head branch commit SHA: %w", err)
|
|
}
|
|
|
|
if !params.HeadExpectedSHA.IsEmpty() && !params.HeadExpectedSHA.Equal(headCommitSHA) {
|
|
return MergeOutput{}, errors.PreconditionFailed(
|
|
"head branch '%s' is on SHA '%s' which doesn't match expected SHA '%s'.",
|
|
params.HeadBranch,
|
|
headCommitSHA,
|
|
params.HeadExpectedSHA)
|
|
}
|
|
|
|
mergeBaseCommitSHA, _, err := s.git.GetMergeBase(ctx, repoPath, "origin",
|
|
baseCommitSHA.String(), headCommitSHA.String())
|
|
if err != nil {
|
|
return MergeOutput{}, fmt.Errorf("failed to get merge base: %w", err)
|
|
}
|
|
|
|
if headCommitSHA.Equal(mergeBaseCommitSHA) {
|
|
return MergeOutput{}, errors.InvalidArgument("head branch doesn't contain any new commits.")
|
|
}
|
|
|
|
// find short stat and number of commits
|
|
|
|
shortStat, err := s.git.DiffShortStat(ctx, repoPath, baseCommitSHA.String(), headCommitSHA.String(), true)
|
|
if err != nil {
|
|
return MergeOutput{}, errors.Internal(err,
|
|
"failed to find short stat between %s and %s", baseCommitSHA, headCommitSHA)
|
|
}
|
|
|
|
commitCount, err := merge.CommitCount(ctx, repoPath, baseCommitSHA.String(), headCommitSHA.String())
|
|
if err != nil {
|
|
return MergeOutput{}, fmt.Errorf("failed to find commit count for merge check: %w", err)
|
|
}
|
|
|
|
// author and committer
|
|
|
|
now := time.Now().UTC()
|
|
|
|
committer := api.Signature{Identity: api.Identity(params.Actor), When: now}
|
|
|
|
if params.Committer != nil {
|
|
committer.Identity = api.Identity(*params.Committer)
|
|
}
|
|
if params.CommitterDate != nil {
|
|
committer.When = *params.CommitterDate
|
|
}
|
|
|
|
author := committer
|
|
|
|
if params.Author != nil {
|
|
author.Identity = api.Identity(*params.Author)
|
|
}
|
|
if params.AuthorDate != nil {
|
|
author.When = *params.AuthorDate
|
|
}
|
|
|
|
// merge message
|
|
|
|
var message string
|
|
if params.Title != "" {
|
|
// Title is deprecated and should not be sent, but if it's sent assume we need to generate the full message.
|
|
message = parser.CleanUpWhitespace(CommitMessage(params.Title, params.Message))
|
|
} else {
|
|
message = parser.CleanUpWhitespace(params.Message)
|
|
}
|
|
|
|
// merge
|
|
|
|
var refUpdater *hook.RefUpdater
|
|
|
|
if params.RefType != enum.RefTypeUndefined {
|
|
refUpdater, err = hook.CreateRefUpdater(s.hookClientFactory, params.EnvVars, repoPath, refPath)
|
|
if err != nil {
|
|
return MergeOutput{}, errors.Internal(err, "failed to create ref updater object")
|
|
}
|
|
|
|
if err := refUpdater.InitOld(ctx, refOldValue); err != nil {
|
|
return MergeOutput{}, errors.Internal(err, "failed to set old reference value for ref updater")
|
|
}
|
|
}
|
|
|
|
mergeCommitSHA, conflicts, err := mergeFunc(
|
|
ctx,
|
|
refUpdater,
|
|
repoPath, s.tmpDir,
|
|
&author, &committer,
|
|
message,
|
|
mergeBaseCommitSHA, baseCommitSHA, headCommitSHA)
|
|
if errors.IsConflict(err) {
|
|
return MergeOutput{}, fmt.Errorf("failed to merge %q to %q in %q using the %q merge method: %w",
|
|
params.HeadBranch, params.BaseBranch, params.RepoUID, mergeMethod, err)
|
|
}
|
|
if err != nil {
|
|
return MergeOutput{}, errors.Internal(err, "failed to merge %q to %q in %q using the %q merge method",
|
|
params.HeadBranch, params.BaseBranch, params.RepoUID, mergeMethod)
|
|
}
|
|
if len(conflicts) > 0 {
|
|
return MergeOutput{
|
|
BaseSHA: baseCommitSHA,
|
|
HeadSHA: headCommitSHA,
|
|
MergeBaseSHA: mergeBaseCommitSHA,
|
|
MergeSHA: sha.None,
|
|
CommitCount: commitCount,
|
|
ChangedFileCount: shortStat.Files,
|
|
Additions: shortStat.Additions,
|
|
Deletions: shortStat.Deletions,
|
|
ConflictFiles: conflicts,
|
|
}, nil
|
|
}
|
|
|
|
return MergeOutput{
|
|
BaseSHA: baseCommitSHA,
|
|
HeadSHA: headCommitSHA,
|
|
MergeBaseSHA: mergeBaseCommitSHA,
|
|
MergeSHA: mergeCommitSHA,
|
|
CommitCount: commitCount,
|
|
ChangedFileCount: shortStat.Files,
|
|
Additions: shortStat.Additions,
|
|
Deletions: shortStat.Deletions,
|
|
ConflictFiles: nil,
|
|
}, nil
|
|
}
|
|
|
|
type MergeBaseParams struct {
|
|
ReadParams
|
|
Ref1 string
|
|
Ref2 string
|
|
}
|
|
|
|
func (p *MergeBaseParams) Validate() error {
|
|
if err := p.ReadParams.Validate(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if p.Ref1 == "" {
|
|
// needs better naming
|
|
return errors.InvalidArgument("first reference cannot be empty")
|
|
}
|
|
|
|
if p.Ref2 == "" {
|
|
// needs better naming
|
|
return errors.InvalidArgument("second reference cannot be empty")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type MergeBaseOutput struct {
|
|
MergeBaseSHA sha.SHA
|
|
}
|
|
|
|
func (s *Service) MergeBase(
|
|
ctx context.Context,
|
|
params MergeBaseParams,
|
|
) (MergeBaseOutput, error) {
|
|
if err := params.Validate(); err != nil {
|
|
return MergeBaseOutput{}, err
|
|
}
|
|
|
|
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
|
|
|
|
result, _, err := s.git.GetMergeBase(ctx, repoPath, "", params.Ref1, params.Ref2)
|
|
if err != nil {
|
|
return MergeBaseOutput{}, err
|
|
}
|
|
|
|
return MergeBaseOutput{
|
|
MergeBaseSHA: result,
|
|
}, nil
|
|
}
|
|
|
|
type IsAncestorParams struct {
|
|
ReadParams
|
|
AncestorCommitSHA sha.SHA
|
|
DescendantCommitSHA sha.SHA
|
|
}
|
|
|
|
type IsAncestorOutput struct {
|
|
Ancestor bool
|
|
}
|
|
|
|
func (s *Service) IsAncestor(
|
|
ctx context.Context,
|
|
params IsAncestorParams,
|
|
) (IsAncestorOutput, error) {
|
|
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
|
|
|
|
result, err := s.git.IsAncestor(
|
|
ctx,
|
|
repoPath,
|
|
params.AlternateObjectDirs,
|
|
params.AncestorCommitSHA,
|
|
params.DescendantCommitSHA,
|
|
)
|
|
if err != nil {
|
|
return IsAncestorOutput{}, err
|
|
}
|
|
|
|
return IsAncestorOutput{
|
|
Ancestor: result,
|
|
}, nil
|
|
}
|