diff --git a/models/issues/comment.go b/models/issues/comment.go index 4fdb0c1808fb4..65832b9226a50 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -115,6 +115,7 @@ const ( CommentTypeUnpin // 37 unpin Issue/PullRequest CommentTypeChangeTimeEstimate // 38 Change time estimate + CommentTypeChangePRFlowType // 39 Change pull request's flow type ) var commentStrings = []string{ @@ -157,6 +158,7 @@ var commentStrings = []string{ "pin", "unpin", "change_time_estimate", + "change_flow_type", } func (t CommentType) String() string { diff --git a/models/issues/pull.go b/models/issues/pull.go index 4c25b6f0c8494..86ea78f243bdf 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -113,6 +113,30 @@ const ( PullRequestFlowAGit ) +var PullRequestFlowMap map[PullRequestFlow]string = map[PullRequestFlow]string{ + PullRequestFlowGithub: "github", + PullRequestFlowAGit: "agit", +} +var PullRequestFlowTypeUnknown error = errors.New("Unknown pull request type") + +func PullRequestFlowFromString(strtype string) (PullRequestFlow, error) { + for k, v := range PullRequestFlowMap { + if v == strtype { + return k, nil + } + } + + return 0, PullRequestFlowTypeUnknown +} + +func PullRequestFlowTypeToString(flow PullRequestFlow) string { + v, ok := PullRequestFlowMap[flow] + if ok { + return v + } + return "" +} + // PullRequest represents relation between pull request and repositories. type PullRequest struct { ID int64 `xorm:"pk autoincr"` @@ -462,6 +486,18 @@ func (pr *PullRequest) IsFromFork() bool { return pr.HeadRepoID != pr.BaseRepoID } +func (pr *PullRequest) ConvertToAgitPullRequest(ctx context.Context, doer *user_model.User) (err error) { + if pr.IsAgitFlow() { + return nil + } + + pr.Flow = PullRequestFlowAGit + pr.HeadRepoID = pr.BaseRepoID + pr.HeadBranch = doer.LowerName + "/" + pr.HeadBranch + + return pr.UpdateColsIfNotMerged(ctx, "flow", "head_repo_id", "head_branch") +} + // NewPullRequest creates new pull request with labels for repository. func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *Issue, labelIDs []int64, uuids []string, pr *PullRequest) (err error) { ctx, committer, err := db.TxContext(ctx) diff --git a/modules/structs/pull.go b/modules/structs/pull.go index f53d6adafce3a..e4c42fb168d61 100644 --- a/modules/structs/pull.go +++ b/modules/structs/pull.go @@ -60,6 +60,9 @@ type PullRequest struct { Closed *time.Time `json:"closed_at"` PinOrder int `json:"pin_order"` + + // swagger:enum["agit","github"] + Flow string `json:"flow"` } // PRBranchInfo information about a branch @@ -107,6 +110,7 @@ type EditPullRequestOption struct { Deadline *time.Time `json:"due_date"` RemoveDeadline *bool `json:"unset_due_date"` AllowMaintainerEdit *bool `json:"allow_maintainer_edit"` + FlowType string `json:"flow_type"` } // ChangedFile store information about files affected by the pull request diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index e05b9b165c2fc..7bf7c148e690d 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -777,6 +777,46 @@ func EditPullRequest(ctx *context.APIContext) { notify_service.PullRequestChangeTargetBranch(ctx, ctx.Doer, pr, form.Base) } + // change pull request type from branch or from AGit + if !pr.HasMerged && len(form.FlowType) != 0 { + flow, err := issues_model.PullRequestFlowFromString(form.FlowType) + if err != nil { + ctx.APIErrorInternal(err) + return + } + err = nil + if !issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWrite(unit.TypeCode) { + // not implemented + ctx.Status(http.StatusForbidden) + return + } + if flow != pr.Flow { + switch flow { + case issues_model.PullRequestFlowGithub: + // not implemented + ctx.Status(http.StatusForbidden) + return + case issues_model.PullRequestFlowAGit: + err = pull_service.ChangePullRequestFlowToAgit(ctx, pr, ctx.Doer) + } + } + if err != nil { + if issues_model.IsErrPullRequestAlreadyExists(err) { + ctx.APIError(http.StatusConflict, err) + return + } else if issues_model.IsErrIssueIsClosed(err) { + ctx.APIError(http.StatusUnprocessableEntity, err) + return + } else if pull_service.IsErrPullRequestHasMerged(err) { + ctx.APIError(http.StatusConflict, err) + return + } + ctx.APIErrorInternal(err) + return + } + notify_service.PullRequestChangeFlowType(ctx, ctx.Doer, pr) + } + // update allow edits if form.AllowMaintainerEdit != nil { if err := pull_service.SetAllowEdits(ctx, ctx.Doer, pr, *form.AllowMaintainerEdit); err != nil { diff --git a/services/actions/notifier.go b/services/actions/notifier.go index d10cc0ab34419..6220cd8f9323a 100644 --- a/services/actions/notifier.go +++ b/services/actions/notifier.go @@ -688,6 +688,32 @@ func (n *actionsNotifier) PullRequestSynchronized(ctx context.Context, doer *use Notify(ctx) } +func (n *actionsNotifier) PullRequestChangeFlowType(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) { + ctx = withMethod(ctx, "PullRequestChangeFlowType") + + if err := pr.LoadIssue(ctx); err != nil { + log.Error("LoadAttributes: %v", err) + return + } + + if err := pr.Issue.LoadRepo(ctx); err != nil { + log.Error("pr.Issue.LoadRepo: %v", err) + return + } + + permission, _ := access_model.GetUserRepoPermission(ctx, pr.Issue.Repo, pr.Issue.Poster) + newNotifyInput(pr.Issue.Repo, doer, webhook_module.HookEventPullRequest). + WithPayload(&api.PullRequestPayload{ + Action: api.HookIssueEdited, + Index: pr.Issue.Index, + PullRequest: convert.ToAPIPullRequest(ctx, pr, nil), + Repository: convert.ToRepo(ctx, pr.Issue.Repo, permission), + Sender: convert.ToUser(ctx, doer, nil), + }). + WithPullRequest(pr). + Notify(ctx) +} + func (n *actionsNotifier) PullRequestChangeTargetBranch(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest, oldBranch string) { ctx = withMethod(ctx, "PullRequestChangeTargetBranch") diff --git a/services/convert/pull.go b/services/convert/pull.go index 4acbd880dc65c..7732bdccaaffc 100644 --- a/services/convert/pull.go +++ b/services/convert/pull.go @@ -97,6 +97,7 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u Created: pr.Issue.CreatedUnix.AsTimePtr(), Updated: pr.Issue.UpdatedUnix.AsTimePtr(), PinOrder: util.Iif(apiIssue.PinOrder == -1, 0, apiIssue.PinOrder), + Flow: issues_model.PullRequestFlowTypeToString(pr.Flow), // output "[]" rather than null to align to github outputs RequestedReviewers: []*api.User{}, diff --git a/services/forms/user_form_hidden_comments.go b/services/forms/user_form_hidden_comments.go index 76382ddfdde68..1378e9fe2922a 100644 --- a/services/forms/user_form_hidden_comments.go +++ b/services/forms/user_form_hidden_comments.go @@ -36,6 +36,7 @@ var hiddenCommentTypeGroups = hiddenCommentTypeGroupsType{ "branch": { /*11*/ issues_model.CommentTypeDeleteBranch, /*25*/ issues_model.CommentTypeChangeTargetBranch, + /*39*/ issues_model.CommentTypeChangePRFlowType, }, "time_tracking": { /*12*/ issues_model.CommentTypeStartTracking, diff --git a/services/notify/notifier.go b/services/notify/notifier.go index 875a70e5644a7..2433e31e1bd18 100644 --- a/services/notify/notifier.go +++ b/services/notify/notifier.go @@ -49,6 +49,7 @@ type Notifier interface { PullRequestReview(ctx context.Context, pr *issues_model.PullRequest, review *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User) PullRequestCodeComment(ctx context.Context, pr *issues_model.PullRequest, comment *issues_model.Comment, mentions []*user_model.User) PullRequestChangeTargetBranch(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest, oldBranch string) + PullRequestChangeFlowType(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) PullRequestPushCommits(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest, comment *issues_model.Comment) PullReviewDismiss(ctx context.Context, doer *user_model.User, review *issues_model.Review, comment *issues_model.Comment) diff --git a/services/notify/notify.go b/services/notify/notify.go index 2416cbd2e0830..2877c647c6ab3 100644 --- a/services/notify/notify.go +++ b/services/notify/notify.go @@ -155,6 +155,13 @@ func PullRequestChangeTargetBranch(ctx context.Context, doer *user_model.User, p } } +// PullRequestChangeFlowType notifies when a pull request's source branch was changed +func PullRequestChangeFlowType(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) { + for _, notifier := range notifiers { + notifier.PullRequestChangeFlowType(ctx, doer, pr) + } +} + // PullRequestPushCommits notifies when push commits to pull request's head branch func PullRequestPushCommits(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest, comment *issues_model.Comment) { for _, notifier := range notifiers { diff --git a/services/notify/null.go b/services/notify/null.go index c3085d7c9eb0a..bb92fe35d509c 100644 --- a/services/notify/null.go +++ b/services/notify/null.go @@ -70,6 +70,10 @@ func (*NullNotifier) PullRequestSynchronized(ctx context.Context, doer *user_mod func (*NullNotifier) PullRequestChangeTargetBranch(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest, oldBranch string) { } +// PullRequestChangeFlowType places a place holder function +func (*NullNotifier) PullRequestChangeFlowType(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) { +} + // PullRequestPushCommits notifies when push commits to pull request's head branch func (*NullNotifier) PullRequestPushCommits(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest, comment *issues_model.Comment) { } diff --git a/services/pull/pull.go b/services/pull/pull.go index 2829e15441081..eabe0b239c6ba 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -247,6 +247,58 @@ func (err ErrPullRequestHasMerged) Error() string { err.ID, err.IssueID, err.HeadRepoID, err.BaseRepoID, err.HeadBranch, err.BaseBranch) } +// ChangePullRequestFlowToAgit changes the source branch of this pull request, as the given user +func ChangePullRequestFlowToAgit(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User) (err error) { + releaser, err := globallock.Lock(ctx, getPullWorkingLockKey(pr.ID)) + if err != nil { + log.Error("lock.Lock(): %v", err) + return fmt.Errorf("lock.Lock: %w", err) + } + defer releaser() + + // Current target branch is already the same + if pr.Flow == issues_model.PullRequestFlowAGit { + return nil + } + + if pr.Issue.IsClosed { + return issues_model.ErrIssueIsClosed{ + ID: pr.Issue.ID, + RepoID: pr.Issue.RepoID, + Index: pr.Issue.Index, + IsPull: true, + } + } + + if pr.HasMerged { + return ErrPullRequestHasMerged{ + ID: pr.ID, + IssueID: pr.Index, + HeadRepoID: pr.HeadRepoID, + BaseRepoID: pr.BaseRepoID, + HeadBranch: pr.HeadBranch, + BaseBranch: pr.BaseBranch, + } + } + + if err = pr.ConvertToAgitPullRequest(ctx, doer); err != nil { + return fmt.Errorf("Failed to convert PR to AGit: %w", err) + } + + // Create comment + options := &issues_model.CreateCommentOptions{ + Type: issues_model.CommentTypeChangePRFlowType, + Doer: doer, + Repo: pr.Issue.Repo, + Issue: pr.Issue, + } + if _, err = issues_model.CreateComment(ctx, options); err != nil { + return fmt.Errorf("CreateChangeSourceBranchComment: %w", err) + } + + return nil +} + // ChangeTargetBranch changes the target branch of this pull request, as the given user. func ChangeTargetBranch(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, targetBranch string) (err error) { releaser, err := globallock.Lock(ctx, getPullWorkingLockKey(pr.ID)) @@ -1003,6 +1055,10 @@ func getAllCommitStatus(ctx context.Context, gitRepo *git.Repository, pr *issues return statuses, lastStatus, err } +func IsBranchEqual(ctx context.Context, gitRepo *git.Repository, branch1, branch2 string) (bool, error) { + return false, nil +} + // IsHeadEqualWithBranch returns if the commits of branchName are available in pull request head func IsHeadEqualWithBranch(ctx context.Context, pr *issues_model.PullRequest, branchName string) (bool, error) { var err error diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go index 672abd5c95d0e..b4297548bd2f1 100644 --- a/services/webhook/notifier.go +++ b/services/webhook/notifier.go @@ -731,6 +731,26 @@ func (m *webhookNotifier) PullRequestChangeTargetBranch(ctx context.Context, doe } } +func (m *webhookNotifier) PullRequestChangeFlowType(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) { + if err := pr.LoadIssue(ctx); err != nil { + log.Error("LoadIssue: %v", err) + return + } + + issue := pr.Issue + + mode, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster) + if err := PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventPullRequest, &api.PullRequestPayload{ + Action: api.HookIssueEdited, + Index: issue.Index, + PullRequest: convert.ToAPIPullRequest(ctx, pr, doer), + Repository: convert.ToRepo(ctx, issue.Repo, mode), + Sender: convert.ToUser(ctx, doer, nil), + }); err != nil { + log.Error("PrepareWebhooks [pr: %d]: %v", pr.ID, err) + } +} + func (m *webhookNotifier) PullRequestReview(ctx context.Context, pr *issues_model.PullRequest, review *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User) { var reviewHookType webhook_module.HookEventType diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 323e0d64ac567..52caee6922191 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -24173,6 +24173,10 @@ "format": "date-time", "x-go-name": "Deadline" }, + "flow_type": { + "type": "string", + "x-go-name": "FlowType" + }, "labels": { "type": "array", "items": { @@ -26718,6 +26722,10 @@ "format": "date-time", "x-go-name": "Deadline" }, + "flow": { + "type": "string", + "x-go-name": "Flow" + }, "head": { "$ref": "#/definitions/PRBranchInfo" },