mirror of https://github.com/harness/drone.git
379 lines
11 KiB
Go
379 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 githook
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/harness/gitness/app/api/usererror"
|
|
"github.com/harness/gitness/app/auth"
|
|
"github.com/harness/gitness/app/bootstrap"
|
|
events "github.com/harness/gitness/app/events/git"
|
|
repoevents "github.com/harness/gitness/app/events/repo"
|
|
"github.com/harness/gitness/git/hook"
|
|
"github.com/harness/gitness/types"
|
|
"github.com/harness/gitness/types/enum"
|
|
|
|
"github.com/gotidy/ptr"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
const (
|
|
// gitReferenceNamePrefixBranch is the prefix of references of type branch.
|
|
gitReferenceNamePrefixBranch = "refs/heads/"
|
|
|
|
// gitReferenceNamePrefixTag is the prefix of references of type tag.
|
|
gitReferenceNamePrefixTag = "refs/tags/"
|
|
|
|
// gitReferenceNamePrefixTag is the prefix of pull req references.
|
|
gitReferenceNamePullReq = "refs/pullreq/"
|
|
)
|
|
|
|
// PostReceive executes the post-receive hook for a git repository.
|
|
func (c *Controller) PostReceive(
|
|
ctx context.Context,
|
|
rgit RestrictedGIT,
|
|
session *auth.Session,
|
|
in types.GithookPostReceiveInput,
|
|
) (hook.Output, error) {
|
|
repoCore, err := c.getRepoCheckAccess(ctx, session, in.RepoID, enum.PermissionRepoPush)
|
|
if err != nil {
|
|
return hook.Output{}, err
|
|
}
|
|
|
|
repo, err := c.repoStore.Find(ctx, repoCore.ID)
|
|
if err != nil {
|
|
return hook.Output{}, err
|
|
}
|
|
|
|
// create output object and have following messages fill its messages
|
|
out := hook.Output{}
|
|
|
|
// update default branch based on ref update info on empty repos.
|
|
// as the branch could be different than the configured default value.
|
|
c.handleEmptyRepoPush(ctx, repo, in.PostReceiveInput, &out)
|
|
|
|
// always update last git push time - best effort
|
|
c.updateLastGITPushTime(ctx, repo, in)
|
|
|
|
// report ref events if repo is in an active state - best effort
|
|
if repo.State == enum.RepoStateActive {
|
|
c.reportReferenceEvents(ctx, rgit, repo, in.PrincipalID, in.PostReceiveInput)
|
|
}
|
|
|
|
// handle branch updates related to PRs - best effort
|
|
c.handlePRMessaging(ctx, repo, in.PostReceiveInput, &out)
|
|
|
|
err = c.postReceiveExtender.Extend(ctx, rgit, session, repo.Core(), in, &out)
|
|
if err != nil {
|
|
return hook.Output{}, fmt.Errorf("failed to extend post-receive hook: %w", err)
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
// reportReferenceEvents is reporting reference events to the event system.
|
|
// NOTE: keep best effort for now as it doesn't change the outcome of the git operation.
|
|
// TODO: in the future we might want to think about propagating errors so user is aware of events not being triggered.
|
|
func (c *Controller) reportReferenceEvents(
|
|
ctx context.Context,
|
|
rgit RestrictedGIT,
|
|
repo *types.Repository,
|
|
principalID int64,
|
|
in hook.PostReceiveInput,
|
|
) {
|
|
for _, refUpdate := range in.RefUpdates {
|
|
switch {
|
|
case strings.HasPrefix(refUpdate.Ref, gitReferenceNamePrefixBranch):
|
|
c.reportBranchEvent(ctx, rgit, repo, principalID, in.Environment, refUpdate)
|
|
case strings.HasPrefix(refUpdate.Ref, gitReferenceNamePrefixTag):
|
|
c.reportTagEvent(ctx, repo, principalID, refUpdate)
|
|
default:
|
|
// Ignore any other references in post-receive
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Controller) reportBranchEvent(
|
|
ctx context.Context,
|
|
rgit RestrictedGIT,
|
|
repo *types.Repository,
|
|
principalID int64,
|
|
env hook.Environment,
|
|
branchUpdate hook.ReferenceUpdate,
|
|
) {
|
|
switch {
|
|
case branchUpdate.Old.IsNil():
|
|
payload := &events.BranchCreatedPayload{
|
|
RepoID: repo.ID,
|
|
PrincipalID: principalID,
|
|
Ref: branchUpdate.Ref,
|
|
SHA: branchUpdate.New.String(),
|
|
}
|
|
|
|
c.gitReporter.BranchCreated(ctx, payload)
|
|
|
|
c.sseStreamer.Publish(ctx, repo.ParentID, enum.SSETypeBranchCreated, payload)
|
|
|
|
case branchUpdate.New.IsNil():
|
|
payload := &events.BranchDeletedPayload{
|
|
RepoID: repo.ID,
|
|
PrincipalID: principalID,
|
|
Ref: branchUpdate.Ref,
|
|
SHA: branchUpdate.Old.String(),
|
|
}
|
|
|
|
c.gitReporter.BranchDeleted(ctx, payload)
|
|
|
|
c.sseStreamer.Publish(ctx, repo.ParentID, enum.SSETypeBranchDeleted, payload)
|
|
|
|
default:
|
|
// A force update event might trigger some additional operations that aren't required
|
|
// for ordinary updates (force pushes alter the commit history of a branch).
|
|
forced, err := isForcePush(ctx, rgit, repo.GitUID, env.AlternateObjectDirs, branchUpdate)
|
|
if err != nil {
|
|
// In case of an error consider this a forced update. In post-update the branch has already been updated,
|
|
// so there's less harm in declaring the update as forced.
|
|
forced = true
|
|
log.Ctx(ctx).Warn().Err(err).
|
|
Str("ref", branchUpdate.Ref).
|
|
Msg("failed to check ancestor")
|
|
}
|
|
|
|
payload := &events.BranchUpdatedPayload{
|
|
RepoID: repo.ID,
|
|
PrincipalID: principalID,
|
|
Ref: branchUpdate.Ref,
|
|
OldSHA: branchUpdate.Old.String(),
|
|
NewSHA: branchUpdate.New.String(),
|
|
Forced: forced,
|
|
}
|
|
|
|
c.gitReporter.BranchUpdated(ctx, payload)
|
|
|
|
c.sseStreamer.Publish(ctx, repo.ParentID, enum.SSETypeBranchUpdated, payload)
|
|
}
|
|
}
|
|
|
|
func (c *Controller) reportTagEvent(
|
|
ctx context.Context,
|
|
repo *types.Repository,
|
|
principalID int64,
|
|
tagUpdate hook.ReferenceUpdate,
|
|
) {
|
|
switch {
|
|
case tagUpdate.Old.IsNil():
|
|
payload := &events.TagCreatedPayload{
|
|
RepoID: repo.ID,
|
|
PrincipalID: principalID,
|
|
Ref: tagUpdate.Ref,
|
|
SHA: tagUpdate.New.String(),
|
|
}
|
|
|
|
c.gitReporter.TagCreated(ctx, payload)
|
|
|
|
c.sseStreamer.Publish(ctx, repo.ParentID, enum.SSETypeTagCreated, payload)
|
|
|
|
case tagUpdate.New.IsNil():
|
|
payload := &events.TagDeletedPayload{
|
|
RepoID: repo.ID,
|
|
PrincipalID: principalID,
|
|
Ref: tagUpdate.Ref,
|
|
SHA: tagUpdate.Old.String(),
|
|
}
|
|
|
|
c.gitReporter.TagDeleted(ctx, payload)
|
|
|
|
c.sseStreamer.Publish(ctx, repo.ParentID, enum.SSETypeTagDeleted, payload)
|
|
|
|
default:
|
|
payload := &events.TagUpdatedPayload{
|
|
RepoID: repo.ID,
|
|
PrincipalID: principalID,
|
|
Ref: tagUpdate.Ref,
|
|
OldSHA: tagUpdate.Old.String(),
|
|
NewSHA: tagUpdate.New.String(),
|
|
// tags can only be force updated!
|
|
Forced: true,
|
|
}
|
|
|
|
c.gitReporter.TagUpdated(ctx, payload)
|
|
|
|
c.sseStreamer.Publish(ctx, repo.ParentID, enum.SSETypeTagUpdated, payload)
|
|
}
|
|
}
|
|
|
|
// handlePRMessaging checks any single branch push for pr information and returns an according response if needed.
|
|
// TODO: If it is a new branch, or an update on a branch without any PR, it also sends out an SSE for pr creation.
|
|
func (c *Controller) handlePRMessaging(
|
|
ctx context.Context,
|
|
repo *types.Repository,
|
|
in hook.PostReceiveInput,
|
|
out *hook.Output,
|
|
) {
|
|
// skip anything that was a batch push / isn't branch related / isn't updating/creating a branch.
|
|
if len(in.RefUpdates) != 1 ||
|
|
!strings.HasPrefix(in.RefUpdates[0].Ref, gitReferenceNamePrefixBranch) ||
|
|
in.RefUpdates[0].New.IsNil() {
|
|
return
|
|
}
|
|
|
|
// for now we only care about first branch that was pushed.
|
|
branchName := in.RefUpdates[0].Ref[len(gitReferenceNamePrefixBranch):]
|
|
|
|
c.suggestPullRequest(ctx, repo, branchName, out)
|
|
|
|
// TODO: store latest pushed branch for user in cache and send out SSE
|
|
}
|
|
|
|
func (c *Controller) suggestPullRequest(
|
|
ctx context.Context,
|
|
repo *types.Repository,
|
|
branchName string,
|
|
out *hook.Output,
|
|
) {
|
|
if branchName == repo.DefaultBranch {
|
|
// Don't suggest a pull request if this is a push to the default branch.
|
|
return
|
|
}
|
|
|
|
// do we have a PR related to it?
|
|
prs, err := c.pullreqStore.List(ctx, &types.PullReqFilter{
|
|
Page: 1,
|
|
// without forks we expect at most one PR (keep 2 to not break when forks are introduced)
|
|
Size: 2,
|
|
SourceRepoID: repo.ID,
|
|
SourceBranch: branchName,
|
|
// we only care about open PRs - merged/closed will lead to "create new PR" message
|
|
States: []enum.PullReqState{enum.PullReqStateOpen},
|
|
Order: enum.OrderAsc,
|
|
Sort: enum.PullReqSortCreated,
|
|
// don't care about the PR description, omit it from the response
|
|
ExcludeDescription: true,
|
|
})
|
|
if err != nil {
|
|
log.Ctx(ctx).Warn().Err(err).Msgf(
|
|
"failed to find pullrequests for branch '%s' originating from repo '%s'",
|
|
branchName,
|
|
repo.Path,
|
|
)
|
|
return
|
|
}
|
|
|
|
// for already existing PRs, print them to users terminal for easier access.
|
|
if len(prs) > 0 {
|
|
msgs := make([]string, 2*len(prs)+1)
|
|
msgs[0] = fmt.Sprintf("Branch %q has open PRs:", branchName)
|
|
for i, pr := range prs {
|
|
msgs[2*i+1] = fmt.Sprintf(" (#%d) %s", pr.Number, pr.Title)
|
|
msgs[2*i+2] = " " + c.urlProvider.GenerateUIPRURL(ctx, repo.Path, pr.Number)
|
|
}
|
|
out.Messages = append(out.Messages, msgs...)
|
|
return
|
|
}
|
|
|
|
// this is a new PR!
|
|
out.Messages = append(out.Messages,
|
|
fmt.Sprintf("Create a pull request for %q by visiting:", branchName),
|
|
" "+c.urlProvider.GenerateUICompareURL(ctx, repo.Path, repo.DefaultBranch, branchName),
|
|
)
|
|
}
|
|
|
|
// handleEmptyRepoPush updates repo default branch on empty repos if push contains branches.
|
|
func (c *Controller) handleEmptyRepoPush(
|
|
ctx context.Context,
|
|
repo *types.Repository,
|
|
in hook.PostReceiveInput,
|
|
out *hook.Output,
|
|
) {
|
|
if !repo.IsEmpty {
|
|
return
|
|
}
|
|
|
|
var newDefaultBranch string
|
|
// update default branch if corresponding branch does not exist
|
|
for _, refUpdate := range in.RefUpdates {
|
|
if strings.HasPrefix(refUpdate.Ref, gitReferenceNamePrefixBranch) && !refUpdate.New.IsNil() {
|
|
branchName := refUpdate.Ref[len(gitReferenceNamePrefixBranch):]
|
|
if branchName == repo.DefaultBranch {
|
|
newDefaultBranch = branchName
|
|
break
|
|
}
|
|
// use the first pushed branch if default branch is not present.
|
|
if newDefaultBranch == "" {
|
|
newDefaultBranch = branchName
|
|
}
|
|
}
|
|
}
|
|
if newDefaultBranch == "" {
|
|
out.Error = ptr.String(usererror.ErrEmptyRepoNeedsBranch.Error())
|
|
return
|
|
}
|
|
|
|
oldName := repo.DefaultBranch
|
|
var err error
|
|
repo, err = c.repoStore.UpdateOptLock(ctx, repo, func(r *types.Repository) error {
|
|
r.IsEmpty = false
|
|
r.DefaultBranch = newDefaultBranch
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
log.Ctx(ctx).Warn().Err(err).Msgf("failed to update the repo default branch to %s and is_empty to false",
|
|
newDefaultBranch)
|
|
return
|
|
}
|
|
|
|
c.repoFinder.MarkChanged(ctx, repo.Core())
|
|
|
|
if repo.DefaultBranch != oldName {
|
|
c.repoReporter.DefaultBranchUpdated(ctx, &repoevents.DefaultBranchUpdatedPayload{
|
|
RepoID: repo.ID,
|
|
PrincipalID: bootstrap.NewSystemServiceSession().Principal.ID,
|
|
OldName: oldName,
|
|
NewName: repo.DefaultBranch,
|
|
})
|
|
}
|
|
}
|
|
|
|
// updateLastGITPushTime updates the repo's last git push time.
|
|
func (c *Controller) updateLastGITPushTime(
|
|
ctx context.Context,
|
|
repo *types.Repository,
|
|
in types.GithookPostReceiveInput,
|
|
) {
|
|
isNonePRRefFn := func(refUpdate hook.ReferenceUpdate) bool {
|
|
return !strings.HasPrefix(refUpdate.Ref, gitReferenceNamePullReq)
|
|
}
|
|
// ignore push that only contains pr refs for last git push time updates
|
|
if !slices.ContainsFunc(in.RefUpdates, isNonePRRefFn) {
|
|
return
|
|
}
|
|
|
|
newRepo, err := c.repoStore.UpdateOptLock(ctx, repo, func(r *types.Repository) error {
|
|
r.LastGITPush = time.Now().UnixMilli()
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
log.Ctx(ctx).Warn().Err(err).Msgf("failed to update last git push time for repo %q", repo.Path)
|
|
return
|
|
}
|
|
|
|
*repo = *newRepo
|
|
}
|