mirror of
https://github.com/harness/drone.git
synced 2025-05-17 01:20:13 +08:00
feat: [CODE-1277]: Add support for PR target branch change (#3579)
* Merge remote-tracking branch 'origin/main' into dd/change-target-branch * Merge remote-tracking branch 'origin/main' into dd/change-target-branch * Add openapi spec for opChangeTargetBranch * Fix updateMergeData args * Refactor ChangeTargetBranch to use verifyBranchExistence and git.GetRefPath * Merge remote-tracking branch 'origin/main' into dd/change-target-branch * Refactor instumentation and event payload * Change instrumentation and event payload * Add target-branch-changed event * Get new merge base and clear merge related fields * Merge remote-tracking branch 'origin/main' into dd/change-target-branch * Merge remote-tracking branch 'origin/main' into dd/change-target-branch * Add merge check to target branch change * Merge remote-tracking branch 'origin/main' into dd/change-target-branch * Add new PullReqActivityType and EventType for audit track * Add support for PR target branch change
This commit is contained in:
parent
cc2a63b797
commit
60a182e515
148
app/api/controller/pullreq/branch_change_target.go
Normal file
148
app/api/controller/pullreq/branch_change_target.go
Normal file
@ -0,0 +1,148 @@
|
||||
// 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 pullreq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/harness/gitness/app/api/usererror"
|
||||
"github.com/harness/gitness/app/auth"
|
||||
pullreqevents "github.com/harness/gitness/app/events/pullreq"
|
||||
"github.com/harness/gitness/app/services/instrument"
|
||||
"github.com/harness/gitness/errors"
|
||||
"github.com/harness/gitness/git"
|
||||
gitenum "github.com/harness/gitness/git/enum"
|
||||
"github.com/harness/gitness/types"
|
||||
"github.com/harness/gitness/types/enum"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type ChangeTargetBranchInput struct {
|
||||
BranchName string `json:"branch_name"`
|
||||
}
|
||||
|
||||
func (c *Controller) ChangeTargetBranch(ctx context.Context,
|
||||
session *auth.Session,
|
||||
repoRef string,
|
||||
pullreqNum int64,
|
||||
in *ChangeTargetBranchInput,
|
||||
) (*types.PullReq, error) {
|
||||
repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoPush)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to acquire access to repo: %w", err)
|
||||
}
|
||||
|
||||
pr, err := c.pullreqStore.FindByNumber(ctx, repo.ID, pullreqNum)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get pull request by number: %w", err)
|
||||
}
|
||||
|
||||
if _, err = c.verifyBranchExistence(ctx, repo, in.BranchName); err != nil {
|
||||
return nil,
|
||||
fmt.Errorf("failed to verify branch existence: %w", err)
|
||||
}
|
||||
|
||||
if pr.TargetBranch == in.BranchName {
|
||||
return pr, nil
|
||||
}
|
||||
if pr.SourceBranch == in.BranchName {
|
||||
return nil,
|
||||
errors.InvalidArgument("source branch %q is same as new target branch", pr.SourceBranch)
|
||||
}
|
||||
|
||||
ref1, err := git.GetRefPath(pr.SourceBranch, gitenum.RefTypeBranch)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get ref path: %w", err)
|
||||
}
|
||||
ref2, err := git.GetRefPath(in.BranchName, gitenum.RefTypeBranch)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get ref path: %w", err)
|
||||
}
|
||||
mergeBase, err := c.git.MergeBase(ctx, git.MergeBaseParams{
|
||||
ReadParams: git.ReadParams{RepoUID: repo.GitUID},
|
||||
Ref1: ref1,
|
||||
Ref2: ref2,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find merge base: %w", err)
|
||||
}
|
||||
|
||||
if mergeBase.MergeBaseSHA.String() == pr.SourceSHA {
|
||||
return nil,
|
||||
usererror.BadRequest("The source branch doesn't contain any new commits")
|
||||
}
|
||||
|
||||
oldTargetBranch := pr.TargetBranch
|
||||
oldMergeBaseSHA := pr.MergeBaseSHA
|
||||
|
||||
_, err = c.pullreqStore.UpdateOptLock(ctx, pr, func(pr *types.PullReq) error {
|
||||
// clear merge and stats related fields
|
||||
pr.MergeSHA = nil
|
||||
pr.MergeTargetSHA = nil
|
||||
pr.Stats.DiffStats = types.DiffStats{}
|
||||
|
||||
pr.MergeBaseSHA = mergeBase.MergeBaseSHA.String()
|
||||
pr.TargetBranch = in.BranchName
|
||||
|
||||
pr.MarkAsMergeUnchecked()
|
||||
|
||||
pr.ActivitySeq++
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update PR target branch in db with error: %w", err)
|
||||
}
|
||||
|
||||
_, err = c.activityStore.CreateWithPayload(
|
||||
ctx, pr, session.Principal.ID,
|
||||
&types.PullRequestActivityPayloadBranchChangeTarget{
|
||||
Old: oldTargetBranch,
|
||||
New: in.BranchName,
|
||||
},
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
log.Ctx(ctx).Err(err).Msgf("failed to write pull request activity for successful branch restore")
|
||||
}
|
||||
|
||||
err = c.instrumentation.Track(ctx, instrument.Event{
|
||||
Type: instrument.EventTypeChangeTargetBranch,
|
||||
Principal: session.Principal.ToPrincipalInfo(),
|
||||
Path: repo.Path,
|
||||
Properties: map[instrument.Property]any{
|
||||
instrument.PropertyRepositoryID: repo.ID,
|
||||
instrument.PropertyRepositoryName: repo.Identifier,
|
||||
instrument.PropertyTargetBranch: in.BranchName,
|
||||
instrument.PropertyPullRequestID: pr.Number,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Ctx(ctx).Warn().Msgf("failed to insert instrumentation record for create branch operation: %s", err)
|
||||
}
|
||||
|
||||
c.eventReporter.TargetBranchChanged(ctx, &pullreqevents.TargetBranchChangedPayload{
|
||||
Base: eventBase(pr, &session.Principal),
|
||||
SourceSHA: pr.SourceSHA,
|
||||
OldTargetBranch: oldTargetBranch,
|
||||
NewTargetBranch: in.BranchName,
|
||||
OldMergeBaseSHA: oldMergeBaseSHA,
|
||||
NewMergeBaseSHA: mergeBase.MergeBaseSHA.String(),
|
||||
})
|
||||
|
||||
return pr, nil
|
||||
}
|
60
app/api/handler/pullreq/branch_change_target.go
Normal file
60
app/api/handler/pullreq/branch_change_target.go
Normal file
@ -0,0 +1,60 @@
|
||||
// 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 pullreq
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/harness/gitness/app/api/controller/pullreq"
|
||||
"github.com/harness/gitness/app/api/render"
|
||||
"github.com/harness/gitness/app/api/request"
|
||||
)
|
||||
|
||||
func HandleChangeTargetBranch(pullreqCtrl *pullreq.Controller) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
session, _ := request.AuthSessionFrom(ctx)
|
||||
|
||||
repoRef, err := request.GetRepoRefFromPath(r)
|
||||
if err != nil {
|
||||
render.TranslatedUserError(ctx, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
pullreqNumber, err := request.GetPullReqNumberFromPath(r)
|
||||
if err != nil {
|
||||
render.TranslatedUserError(ctx, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
in := new(pullreq.ChangeTargetBranchInput)
|
||||
err = json.NewDecoder(r.Body).Decode(in)
|
||||
if err != nil {
|
||||
render.BadRequestf(ctx, w, "Invalid request body: %s.", err)
|
||||
return
|
||||
}
|
||||
|
||||
out, err := pullreqCtrl.ChangeTargetBranch(
|
||||
ctx, session, repoRef, pullreqNumber, in,
|
||||
)
|
||||
if err != nil {
|
||||
render.TranslatedUserError(ctx, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.JSON(w, http.StatusOK, out)
|
||||
}
|
||||
}
|
@ -757,6 +757,21 @@ func pullReqOperations(reflector *openapi3.Reflector) {
|
||||
_ = reflector.SetJSONResponse(&opDeleteBranch, new(types.RulesViolations), http.StatusUnprocessableEntity)
|
||||
_ = reflector.Spec.AddOperation(http.MethodDelete, "/repos/{repo_ref}/pullreq/{pullreq_number}/branch", opDeleteBranch)
|
||||
|
||||
opChangeTargetBranch := openapi3.Operation{}
|
||||
opChangeTargetBranch.WithTags("pullreq")
|
||||
opChangeTargetBranch.WithMapOfAnything(map[string]interface{}{"operationId": "changeTargetBranch"})
|
||||
_ = reflector.SetRequest(&opChangeTargetBranch, struct {
|
||||
pullReqRequest
|
||||
pullreq.ChangeTargetBranchInput
|
||||
}{}, http.MethodPost)
|
||||
_ = reflector.SetJSONResponse(&opChangeTargetBranch, new(types.PullReq), http.StatusOK)
|
||||
_ = reflector.SetJSONResponse(&opChangeTargetBranch, new(usererror.Error), http.StatusBadRequest)
|
||||
_ = reflector.SetJSONResponse(&opChangeTargetBranch, new(usererror.Error), http.StatusInternalServerError)
|
||||
_ = reflector.SetJSONResponse(&opChangeTargetBranch, new(usererror.Error), http.StatusUnauthorized)
|
||||
_ = reflector.SetJSONResponse(&opChangeTargetBranch, new(usererror.Error), http.StatusForbidden)
|
||||
_ = reflector.Spec.AddOperation(http.MethodPut,
|
||||
"/repos/{repo_ref}/pullreq/{pullreq_number}/branch", opChangeTargetBranch)
|
||||
|
||||
fileViewAdd := openapi3.Operation{}
|
||||
fileViewAdd.WithTags("pullreq")
|
||||
fileViewAdd.WithMapOfAnything(map[string]interface{}{"operationId": "fileViewAddPullReq"})
|
||||
|
@ -47,7 +47,46 @@ func (r *Reporter) BranchUpdated(ctx context.Context, payload *BranchUpdatedPayl
|
||||
log.Ctx(ctx).Debug().Msgf("reported pull request branch updated event with id '%s'", eventID)
|
||||
}
|
||||
|
||||
func (r *Reader) RegisterBranchUpdated(fn events.HandlerFunc[*BranchUpdatedPayload],
|
||||
func (r *Reader) RegisterBranchUpdated(
|
||||
fn events.HandlerFunc[*BranchUpdatedPayload],
|
||||
opts ...events.HandlerOption) error {
|
||||
return events.ReaderRegisterEvent(r.innerReader, BranchUpdatedEvent, fn, opts...)
|
||||
}
|
||||
|
||||
const TargetBranchChangedEvent events.EventType = "target-branch-changed"
|
||||
|
||||
type TargetBranchChangedPayload struct {
|
||||
Base
|
||||
SourceSHA string `json:"source_sha"`
|
||||
OldTargetBranch string `json:"old_target_branch"`
|
||||
NewTargetBranch string `json:"new_target_branch"`
|
||||
OldMergeBaseSHA string `json:"old_merge_base_sha"`
|
||||
NewMergeBaseSHA string `json:"new_merge_base_sha"`
|
||||
}
|
||||
|
||||
func (r *Reporter) TargetBranchChanged(
|
||||
ctx context.Context,
|
||||
payload *TargetBranchChangedPayload,
|
||||
) {
|
||||
if payload == nil {
|
||||
return
|
||||
}
|
||||
|
||||
eventID, err := events.ReporterSendEvent(
|
||||
r.innerReporter, ctx, TargetBranchChangedEvent, payload,
|
||||
)
|
||||
if err != nil {
|
||||
log.Ctx(ctx).Err(err).Msgf("failed to send pull request target branch changed event")
|
||||
return
|
||||
}
|
||||
|
||||
log.Ctx(ctx).Debug().Msgf(
|
||||
"reported pull request target branch changed event with id '%s'", eventID,
|
||||
)
|
||||
}
|
||||
|
||||
func (r *Reader) RegisterTargetBranchChanged(
|
||||
fn events.HandlerFunc[*TargetBranchChangedPayload],
|
||||
opts ...events.HandlerOption) error {
|
||||
return events.ReaderRegisterEvent(r.innerReader, TargetBranchChangedEvent, fn, opts...)
|
||||
}
|
||||
|
@ -694,6 +694,7 @@ func SetupPullReq(r chi.Router, pullreqCtrl *pullreq.Controller) {
|
||||
r.Route("/branch", func(r chi.Router) {
|
||||
r.Post("/", handlerpullreq.HandleRestoreBranch(pullreqCtrl))
|
||||
r.Delete("/", handlerpullreq.HandleDeleteBranch(pullreqCtrl))
|
||||
r.Put("/", handlerpullreq.HandleChangeTargetBranch(pullreqCtrl))
|
||||
})
|
||||
|
||||
r.Route("/file-views", func(r chi.Router) {
|
||||
|
@ -42,6 +42,7 @@ const (
|
||||
PropertyIsDefaultBranch Property = "is_default_branch"
|
||||
PropertyDecision Property = "decision"
|
||||
PropertyRepositories Property = "repositories"
|
||||
PropertyTargetBranch Property = "target_branch"
|
||||
|
||||
PropertySpaceID Property = "space_id"
|
||||
PropertySpaceName Property = "space_name"
|
||||
@ -55,6 +56,7 @@ const (
|
||||
EventTypeCommitCount EventType = "Commit count"
|
||||
EventTypeCreateCommit EventType = "Create commit"
|
||||
EventTypeCreateBranch EventType = "Create branch"
|
||||
EventTypeChangeTargetBranch EventType = "Change target branch"
|
||||
EventTypeCreateTag EventType = "Create tag"
|
||||
EventTypeCreatePullRequest EventType = "Create pull request"
|
||||
EventTypeMergePullRequest EventType = "Merge pull request"
|
||||
|
@ -65,6 +65,20 @@ func (s *Service) mergeCheckOnBranchUpdate(ctx context.Context,
|
||||
)
|
||||
}
|
||||
|
||||
// mergeCheckOnTargetBranchChange handles pull request target branch changed events.
|
||||
func (s *Service) mergeCheckOnTargetBranchChange(
|
||||
ctx context.Context,
|
||||
event *events.Event[*pullreqevents.TargetBranchChangedPayload],
|
||||
) error {
|
||||
return s.updateMergeData(
|
||||
ctx,
|
||||
event.Payload.TargetRepoID,
|
||||
event.Payload.Number,
|
||||
sha.None.String(),
|
||||
event.Payload.SourceSHA,
|
||||
)
|
||||
}
|
||||
|
||||
// mergeCheckOnReopen handles pull request StateChanged events.
|
||||
// It updates the PR head git ref to point to the source branch commit SHA.
|
||||
func (s *Service) mergeCheckOnReopen(ctx context.Context,
|
||||
|
@ -195,6 +195,7 @@ func New(ctx context.Context,
|
||||
_ = r.RegisterCreated(service.mergeCheckOnCreated)
|
||||
_ = r.RegisterBranchUpdated(service.mergeCheckOnBranchUpdate)
|
||||
_ = r.RegisterReopened(service.mergeCheckOnReopen)
|
||||
_ = r.RegisterTargetBranchChanged(service.mergeCheckOnTargetBranchChange)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
@ -302,6 +302,7 @@ func (s *PullReqStore) Update(ctx context.Context, pr *types.PullReq) error {
|
||||
,pullreq_description = :pullreq_description
|
||||
,pullreq_activity_seq = :pullreq_activity_seq
|
||||
,pullreq_source_sha = :pullreq_source_sha
|
||||
,pullreq_target_branch = :pullreq_target_branch
|
||||
,pullreq_merged_by = :pullreq_merged_by
|
||||
,pullreq_merged = :pullreq_merged
|
||||
,pullreq_merge_method = :pullreq_merge_method
|
||||
|
@ -88,6 +88,7 @@ const (
|
||||
PullReqActivityTypeBranchUpdate PullReqActivityType = "branch-update"
|
||||
PullReqActivityTypeBranchDelete PullReqActivityType = "branch-delete"
|
||||
PullReqActivityTypeBranchRestore PullReqActivityType = "branch-restore"
|
||||
PullReqActivityTypeTargetBranchChange PullReqActivityType = "target-branch-change"
|
||||
PullReqActivityTypeMerge PullReqActivityType = "merge"
|
||||
PullReqActivityTypeLabelModify PullReqActivityType = "label-modify"
|
||||
)
|
||||
@ -103,6 +104,7 @@ var pullReqActivityTypes = sortEnum([]PullReqActivityType{
|
||||
PullReqActivityTypeBranchUpdate,
|
||||
PullReqActivityTypeBranchDelete,
|
||||
PullReqActivityTypeBranchRestore,
|
||||
PullReqActivityTypeTargetBranchChange,
|
||||
PullReqActivityTypeMerge,
|
||||
PullReqActivityTypeLabelModify,
|
||||
})
|
||||
|
@ -179,6 +179,15 @@ func (a *PullRequestActivityPayloadBranchRestore) ActivityType() enum.PullReqAct
|
||||
return enum.PullReqActivityTypeBranchRestore
|
||||
}
|
||||
|
||||
type PullRequestActivityPayloadBranchChangeTarget struct {
|
||||
Old string `json:"old"`
|
||||
New string `json:"new"`
|
||||
}
|
||||
|
||||
func (a *PullRequestActivityPayloadBranchChangeTarget) ActivityType() enum.PullReqActivityType {
|
||||
return enum.PullReqActivityTypeTargetBranchChange
|
||||
}
|
||||
|
||||
type PullRequestActivityLabelBase struct {
|
||||
Label string `json:"label"`
|
||||
LabelColor enum.LabelColor `json:"label_color"`
|
||||
@ -188,6 +197,7 @@ type PullRequestActivityLabelBase struct {
|
||||
OldValue *string `json:"old_value,omitempty"`
|
||||
OldValueColor *enum.LabelColor `json:"old_value_color,omitempty"`
|
||||
}
|
||||
|
||||
type PullRequestActivityLabel struct {
|
||||
PullRequestActivityLabelBase
|
||||
Type enum.PullReqLabelActivityType `json:"type"`
|
||||
|
Loading…
Reference in New Issue
Block a user