mirror of https://github.com/harness/drone.git
448 lines
12 KiB
Go
448 lines
12 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/hook"
|
|
"github.com/harness/gitness/git/sha"
|
|
"github.com/harness/gitness/git/sharedrepo"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
var (
|
|
listCommitTagsRefFields = []api.GitReferenceField{
|
|
api.GitReferenceFieldRefName,
|
|
api.GitReferenceFieldObjectType,
|
|
api.GitReferenceFieldObjectName,
|
|
}
|
|
listCommitTagsObjectTypeFilter = []api.GitObjectType{
|
|
api.GitObjectTypeCommit,
|
|
api.GitObjectTypeTag,
|
|
}
|
|
)
|
|
|
|
type TagSortOption int
|
|
|
|
const (
|
|
TagSortOptionDefault TagSortOption = iota
|
|
TagSortOptionName
|
|
TagSortOptionDate
|
|
)
|
|
|
|
type ListCommitTagsParams struct {
|
|
ReadParams
|
|
IncludeCommit bool
|
|
Query string
|
|
Sort TagSortOption
|
|
Order SortOrder
|
|
Page int32
|
|
PageSize int32
|
|
}
|
|
|
|
type ListCommitTagsOutput struct {
|
|
Tags []CommitTag
|
|
}
|
|
|
|
type CommitTag struct {
|
|
Name string
|
|
SHA sha.SHA
|
|
IsAnnotated bool
|
|
Title string
|
|
Message string
|
|
Tagger *Signature
|
|
Commit *Commit
|
|
}
|
|
|
|
type CreateCommitTagParams struct {
|
|
WriteParams
|
|
Name string
|
|
|
|
// Target is the commit (or points to the commit) the new tag will be pointing to.
|
|
Target string
|
|
|
|
// Message is the optional message the tag will be created with - if the message is empty
|
|
// the tag will be lightweight, otherwise it'll be annotated
|
|
Message string
|
|
|
|
// Tagger overwrites the git author used in case the tag is annotated
|
|
// (optional, default: actor)
|
|
Tagger *Identity
|
|
// TaggerDate overwrites the git author date used in case the tag is annotated
|
|
// (optional, default: current time on server)
|
|
TaggerDate *time.Time
|
|
}
|
|
|
|
func (p *CreateCommitTagParams) Validate() error {
|
|
if p == nil {
|
|
return ErrNoParamsProvided
|
|
}
|
|
|
|
if p.Name == "" {
|
|
return errors.New("tag name cannot be empty")
|
|
}
|
|
if p.Target == "" {
|
|
return errors.New("target cannot be empty")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type CreateCommitTagOutput struct {
|
|
CommitTag
|
|
}
|
|
|
|
type DeleteTagParams struct {
|
|
WriteParams
|
|
Name string
|
|
}
|
|
|
|
func (p DeleteTagParams) Validate() error {
|
|
if p.Name == "" {
|
|
return errors.New("tag name cannot be empty")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
//nolint:gocognit
|
|
func (s *Service) ListCommitTags(
|
|
ctx context.Context,
|
|
params *ListCommitTagsParams,
|
|
) (*ListCommitTagsOutput, error) {
|
|
if err := params.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
|
|
|
|
// get all required information from git references
|
|
tags, err := s.listCommitTagsLoadReferenceData(ctx, repoPath, params)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ListCommitTags: failed to get git references: %w", err)
|
|
}
|
|
|
|
// get all tag and commit SHAs
|
|
annotatedTagSHAs := make([]string, 0, len(tags))
|
|
commitSHAs := make([]string, len(tags))
|
|
|
|
for i, tag := range tags {
|
|
// always set the commit sha (will be overwritten for annotated tags)
|
|
commitSHAs[i] = tag.SHA.String()
|
|
|
|
if tag.IsAnnotated {
|
|
annotatedTagSHAs = append(annotatedTagSHAs, tag.SHA.String())
|
|
}
|
|
}
|
|
|
|
// populate annotation data for all annotated tags
|
|
if len(annotatedTagSHAs) > 0 {
|
|
var aTags []api.Tag
|
|
aTags, err = s.git.GetAnnotatedTags(ctx, repoPath, annotatedTagSHAs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ListCommitTags: failed to get annotated tag: %w", err)
|
|
}
|
|
|
|
ai := 0 // index for annotated tags
|
|
ri := 0 // read index for all tags
|
|
wi := 0 // write index for all tags (as we might remove some non-commit tags)
|
|
for ; ri < len(tags); ri++ {
|
|
// always copy the current read element to the latest write position (doesn't mean it's kept)
|
|
tags[wi] = tags[ri]
|
|
commitSHAs[wi] = commitSHAs[ri]
|
|
|
|
// keep the tag as is if it's not annotated
|
|
if !tags[ri].IsAnnotated {
|
|
wi++
|
|
continue
|
|
}
|
|
|
|
// filter out annotated tags that don't point to commit objects (blobs, trees, nested tags, ...)
|
|
// we don't actually wanna write it, so keep write index
|
|
// TODO: Support proper pagination: https://harness.atlassian.net/browse/CODE-669
|
|
if aTags[ai].TargetType != api.GitObjectTypeCommit {
|
|
ai++
|
|
continue
|
|
}
|
|
|
|
// correct the commitSHA for the annotated tag (currently it is the tag sha, not the commit sha)
|
|
commitSHAs[wi] = aTags[ai].TargetSha.String()
|
|
|
|
// update tag information with annotation details
|
|
// NOTE: we keep the name from the reference and ignore the annotated name (similar to github)
|
|
tags[wi].Message = aTags[ai].Message
|
|
tags[wi].Title = aTags[ai].Title
|
|
tagger, err := mapSignature(&aTags[ai].Tagger)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("signature mapping error: %w", err)
|
|
}
|
|
tags[wi].Tagger = tagger
|
|
|
|
ai++
|
|
wi++
|
|
}
|
|
|
|
// truncate slices based on what was removed
|
|
tags = tags[:wi]
|
|
commitSHAs = commitSHAs[:wi]
|
|
}
|
|
|
|
// get commits if needed (single call for perf savings: 1s-4s vs 5s-20s)
|
|
if params.IncludeCommit {
|
|
gitCommits, err := s.git.GetCommits(ctx, repoPath, commitSHAs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ListCommitTags: failed to get commits: %w", err)
|
|
}
|
|
|
|
for i := range gitCommits {
|
|
c, err := mapCommit(gitCommits[i])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("commit mapping error: %w", err)
|
|
}
|
|
tags[i].Commit = c
|
|
}
|
|
}
|
|
|
|
return &ListCommitTagsOutput{
|
|
Tags: tags,
|
|
}, nil
|
|
}
|
|
|
|
//nolint:gocognit
|
|
func (s *Service) CreateCommitTag(ctx context.Context, params *CreateCommitTagParams) (*CreateCommitTagOutput, error) {
|
|
if err := params.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
|
|
|
|
targetCommit, err := s.git.GetCommit(ctx, repoPath, params.Target)
|
|
if errors.IsNotFound(err) {
|
|
return nil, errors.NotFound("target '%s' doesn't exist", params.Target)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("CreateCommitTag: failed to get commit id for target '%s': %w", params.Target, err)
|
|
}
|
|
|
|
tagName := params.Name
|
|
tagRef := api.GetReferenceFromTagName(tagName)
|
|
var tag *api.Tag
|
|
|
|
commitSHA, err := s.git.GetRef(ctx, repoPath, tagRef)
|
|
// TODO: Change GetRef to use errors.NotFound and then remove types.IsNotFoundError(err) below.
|
|
if err != nil && !errors.IsNotFound(err) {
|
|
return nil, fmt.Errorf("CreateCommitTag: failed to verify tag existence: %w", err)
|
|
}
|
|
if err == nil && !commitSHA.IsEmpty() {
|
|
return nil, errors.Conflict("tag '%s' already exists", tagName)
|
|
}
|
|
|
|
// create tag request
|
|
|
|
tagger := params.Actor
|
|
if params.Tagger != nil {
|
|
tagger = *params.Tagger
|
|
}
|
|
taggerDate := time.Now().UTC()
|
|
if params.TaggerDate != nil {
|
|
taggerDate = *params.TaggerDate
|
|
}
|
|
|
|
createTagRequest := &api.CreateTagOptions{
|
|
Message: params.Message,
|
|
Tagger: api.Signature{
|
|
Identity: api.Identity{
|
|
Name: tagger.Name,
|
|
Email: tagger.Email,
|
|
},
|
|
When: taggerDate,
|
|
},
|
|
}
|
|
|
|
// ref updater
|
|
|
|
refUpdater, err := hook.CreateRefUpdater(s.hookClientFactory, params.EnvVars, repoPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create ref updater to create the tag: %w", err)
|
|
}
|
|
|
|
// create the tag
|
|
|
|
err = sharedrepo.Run(ctx, refUpdater, s.sharedRepoRoot, repoPath, func(r *sharedrepo.SharedRepo) error {
|
|
if err := s.git.CreateTag(ctx, r.Directory(), tagName, targetCommit.SHA, createTagRequest); err != nil {
|
|
return fmt.Errorf("failed to create tag '%s': %w", tagName, err)
|
|
}
|
|
|
|
tag, err = s.git.GetAnnotatedTag(ctx, r.Directory(), tagName)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read annotated tag after creation: %w", err)
|
|
}
|
|
|
|
ref := hook.ReferenceUpdate{
|
|
Ref: tagRef,
|
|
Old: sha.Nil,
|
|
New: tag.Sha,
|
|
}
|
|
|
|
if err := refUpdater.Init(ctx, []hook.ReferenceUpdate{ref}); err != nil {
|
|
return fmt.Errorf("failed to init ref updater: %w", err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("CreateCommitTag: failed to create tag in shared repository: %w", err)
|
|
}
|
|
|
|
// prepare response
|
|
|
|
var commitTag *CommitTag
|
|
if params.Message != "" {
|
|
tag, err = s.git.GetAnnotatedTag(ctx, repoPath, params.Name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read annotated tag after creation: %w", err)
|
|
}
|
|
commitTag = mapAnnotatedTag(tag)
|
|
} else {
|
|
commitTag = &CommitTag{
|
|
Name: params.Name,
|
|
IsAnnotated: false,
|
|
SHA: targetCommit.SHA,
|
|
}
|
|
}
|
|
|
|
c, err := mapCommit(targetCommit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
commitTag.Commit = c
|
|
|
|
return &CreateCommitTagOutput{CommitTag: *commitTag}, nil
|
|
}
|
|
|
|
func (s *Service) DeleteTag(ctx context.Context, params *DeleteTagParams) error {
|
|
if err := params.Validate(); err != nil {
|
|
return err
|
|
}
|
|
|
|
repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
|
|
|
|
refUpdater, err := hook.CreateRefUpdater(s.hookClientFactory, params.EnvVars, repoPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create ref updater to delete the tag: %w", err)
|
|
}
|
|
|
|
tagRef := api.GetReferenceFromTagName(params.Name)
|
|
|
|
err = refUpdater.DoOne(ctx, tagRef, sha.None, sha.Nil) // delete whatever is there
|
|
if errors.IsNotFound(err) {
|
|
return errors.NotFound("tag %q does not exist", params.Name)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("failed to init ref updater: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) listCommitTagsLoadReferenceData(
|
|
ctx context.Context,
|
|
repoPath string,
|
|
params *ListCommitTagsParams,
|
|
) ([]CommitTag, error) {
|
|
// TODO: can we be smarter with slice allocation
|
|
tags := make([]CommitTag, 0, 16)
|
|
handler := listCommitTagsWalkReferencesHandler(&tags)
|
|
instructor, _, err := wrapInstructorWithOptionalPagination(
|
|
newInstructorWithObjectTypeFilter(listCommitTagsObjectTypeFilter),
|
|
params.Page,
|
|
params.PageSize,
|
|
)
|
|
if err != nil {
|
|
return nil, errors.InvalidArgument("invalid pagination details: %v", err)
|
|
}
|
|
|
|
opts := &api.WalkReferencesOptions{
|
|
Patterns: createReferenceWalkPatternsFromQuery(gitReferenceNamePrefixTag, params.Query),
|
|
Sort: mapListCommitTagsSortOption(params.Sort),
|
|
Order: mapToSortOrder(params.Order),
|
|
Fields: listCommitTagsRefFields,
|
|
Instructor: instructor,
|
|
// we do post-filtering, so we can't restrict the git output ...
|
|
MaxWalkDistance: 0,
|
|
}
|
|
|
|
err = s.git.WalkReferences(ctx, repoPath, handler, opts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to walk tag references: %w", err)
|
|
}
|
|
|
|
log.Ctx(ctx).Trace().Msgf("git api returned %d tags", len(tags))
|
|
|
|
return tags, nil
|
|
}
|
|
|
|
func listCommitTagsWalkReferencesHandler(tags *[]CommitTag) 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")
|
|
}
|
|
objectTypeRaw, ok := e[api.GitReferenceFieldObjectType]
|
|
if !ok {
|
|
return fmt.Errorf("entry missing object type")
|
|
}
|
|
|
|
tag := CommitTag{
|
|
Name: fullRefName[len(gitReferenceNamePrefixTag):],
|
|
SHA: sha.Must(objectSHA),
|
|
IsAnnotated: objectTypeRaw == string(api.GitObjectTypeTag),
|
|
}
|
|
|
|
// TODO: refactor to not use slice pointers?
|
|
*tags = append(*tags, tag)
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func newInstructorWithObjectTypeFilter(filter []api.GitObjectType) api.WalkReferencesInstructor {
|
|
return func(wre api.WalkReferencesEntry) (api.WalkInstruction, error) {
|
|
v, ok := wre[api.GitReferenceFieldObjectType]
|
|
if !ok {
|
|
return api.WalkInstructionStop, fmt.Errorf("ref field for object type is missing")
|
|
}
|
|
|
|
// only handle if any of the filters match
|
|
for _, field := range filter {
|
|
if v == string(field) {
|
|
return api.WalkInstructionHandle, nil
|
|
}
|
|
}
|
|
|
|
// by default skip
|
|
return api.WalkInstructionSkip, nil
|
|
}
|
|
}
|