* Adds functionality to change target branch of created pull requests Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Use const instead of var in JavaScript additions Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Check if branches are equal and if PR already exists before changing target branch Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Make sure to check all commits Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Print error messages for user as error flash message Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Disallow changing target branch of closed or merged pull requests Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Resolve conflicts after merge of upstream/master Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Change order of branch select fields Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Removes duplicate check Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Use ctx.Tr for translations Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Recompile JS Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Use correct translation namespace Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Remove redundant if condition Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Moves most change branch logic into pull service Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Completes comment Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Add Ref to ChangesPayload for logging changed target branches instead of creating a new struct Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Revert changes to go.mod Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Directly use createComment method Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Return 404 if pull request is not found. Move written check up Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Remove variable declaration Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Return client errors on change pull request target errors Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Return error in commit.HasPreviousCommit Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Adds blank line Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Test patch before persisting new target branch Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Update patch before testing (not working) Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Removes patch calls when changeing pull request target Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Removes unneeded check for base name Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Moves ChangeTargetBranch completely to pull service. Update patch status. Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Set webhook mode after errors were validated Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Update PR in one transaction Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Move logic for check if head is equal with branch to pull model Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Adds missing comment and simplify return Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com> * Adjust CreateComment method call Signed-off-by: Mario Lubenka <mario.lubenka@googlemail.com>tags/v1.11.0-rc1
@@ -953,6 +953,22 @@ func (err ErrBranchNameConflict) Error() string { | |||||
return fmt.Sprintf("branch conflicts with existing branch [name: %s]", err.BranchName) | return fmt.Sprintf("branch conflicts with existing branch [name: %s]", err.BranchName) | ||||
} | } | ||||
// ErrBranchesEqual represents an error that branch name conflicts with other branch. | |||||
type ErrBranchesEqual struct { | |||||
BaseBranchName string | |||||
HeadBranchName string | |||||
} | |||||
// IsErrBranchesEqual checks if an error is an ErrBranchesEqual. | |||||
func IsErrBranchesEqual(err error) bool { | |||||
_, ok := err.(ErrBranchesEqual) | |||||
return ok | |||||
} | |||||
func (err ErrBranchesEqual) Error() string { | |||||
return fmt.Sprintf("branches are equal [head: %sm base: %s]", err.HeadBranchName, err.BaseBranchName) | |||||
} | |||||
// ErrNotAllowedToMerge represents an error that a branch is protected and the current user is not allowed to modify it. | // ErrNotAllowedToMerge represents an error that a branch is protected and the current user is not allowed to modify it. | ||||
type ErrNotAllowedToMerge struct { | type ErrNotAllowedToMerge struct { | ||||
Reason string | Reason string | ||||
@@ -1090,6 +1106,23 @@ func (err ErrIssueNotExist) Error() string { | |||||
return fmt.Sprintf("issue does not exist [id: %d, repo_id: %d, index: %d]", err.ID, err.RepoID, err.Index) | return fmt.Sprintf("issue does not exist [id: %d, repo_id: %d, index: %d]", err.ID, err.RepoID, err.Index) | ||||
} | } | ||||
// ErrIssueIsClosed represents a "IssueIsClosed" kind of error. | |||||
type ErrIssueIsClosed struct { | |||||
ID int64 | |||||
RepoID int64 | |||||
Index int64 | |||||
} | |||||
// IsErrIssueIsClosed checks if an error is a ErrIssueNotExist. | |||||
func IsErrIssueIsClosed(err error) bool { | |||||
_, ok := err.(ErrIssueIsClosed) | |||||
return ok | |||||
} | |||||
func (err ErrIssueIsClosed) Error() string { | |||||
return fmt.Sprintf("issue is closed [id: %d, repo_id: %d, index: %d]", err.ID, err.RepoID, err.Index) | |||||
} | |||||
// ErrIssueLabelTemplateLoad represents a "ErrIssueLabelTemplateLoad" kind of error. | // ErrIssueLabelTemplateLoad represents a "ErrIssueLabelTemplateLoad" kind of error. | ||||
type ErrIssueLabelTemplateLoad struct { | type ErrIssueLabelTemplateLoad struct { | ||||
TemplateFile string | TemplateFile string | ||||
@@ -1326,6 +1359,28 @@ func (err ErrRebaseConflicts) Error() string { | |||||
return fmt.Sprintf("Rebase Error: %v: Whilst Rebasing: %s\n%s\n%s", err.Err, err.CommitSHA, err.StdErr, err.StdOut) | return fmt.Sprintf("Rebase Error: %v: Whilst Rebasing: %s\n%s\n%s", err.Err, err.CommitSHA, err.StdErr, err.StdOut) | ||||
} | } | ||||
// ErrPullRequestHasMerged represents a "PullRequestHasMerged"-error | |||||
type ErrPullRequestHasMerged struct { | |||||
ID int64 | |||||
IssueID int64 | |||||
HeadRepoID int64 | |||||
BaseRepoID int64 | |||||
HeadBranch string | |||||
BaseBranch string | |||||
} | |||||
// IsErrPullRequestHasMerged checks if an error is a ErrPullRequestHasMerged. | |||||
func IsErrPullRequestHasMerged(err error) bool { | |||||
_, ok := err.(ErrPullRequestHasMerged) | |||||
return ok | |||||
} | |||||
// Error does pretty-printing :D | |||||
func (err ErrPullRequestHasMerged) Error() string { | |||||
return fmt.Sprintf("pull request has merged [id: %d, issue_id: %d, head_repo_id: %d, base_repo_id: %d, head_branch: %s, base_branch: %s]", | |||||
err.ID, err.IssueID, err.HeadRepoID, err.BaseRepoID, err.HeadBranch, err.BaseBranch) | |||||
} | |||||
// _________ __ | // _________ __ | ||||
// \_ ___ \ ____ _____ _____ ____ _____/ |_ | // \_ ___ \ ____ _____ _____ ____ _____/ |_ | ||||
// / \ \/ / _ \ / \ / \_/ __ \ / \ __\ | // / \ \/ / _ \ / \ / \_/ __ \ / \ __\ | ||||
@@ -82,6 +82,8 @@ const ( | |||||
CommentTypeLock | CommentTypeLock | ||||
// Unlocks a previously locked issue | // Unlocks a previously locked issue | ||||
CommentTypeUnlock | CommentTypeUnlock | ||||
// Change pull request's target branch | |||||
CommentTypeChangeTargetBranch | |||||
) | ) | ||||
// CommentTag defines comment tag type | // CommentTag defines comment tag type | ||||
@@ -116,6 +118,8 @@ type Comment struct { | |||||
Assignee *User `xorm:"-"` | Assignee *User `xorm:"-"` | ||||
OldTitle string | OldTitle string | ||||
NewTitle string | NewTitle string | ||||
OldRef string | |||||
NewRef string | |||||
DependentIssueID int64 | DependentIssueID int64 | ||||
DependentIssue *Issue `xorm:"-"` | DependentIssue *Issue `xorm:"-"` | ||||
@@ -517,6 +521,8 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err | |||||
Content: opts.Content, | Content: opts.Content, | ||||
OldTitle: opts.OldTitle, | OldTitle: opts.OldTitle, | ||||
NewTitle: opts.NewTitle, | NewTitle: opts.NewTitle, | ||||
OldRef: opts.OldRef, | |||||
NewRef: opts.NewRef, | |||||
DependentIssueID: opts.DependentIssueID, | DependentIssueID: opts.DependentIssueID, | ||||
TreePath: opts.TreePath, | TreePath: opts.TreePath, | ||||
ReviewID: opts.ReviewID, | ReviewID: opts.ReviewID, | ||||
@@ -673,6 +679,8 @@ type CreateCommentOptions struct { | |||||
RemovedAssignee bool | RemovedAssignee bool | ||||
OldTitle string | OldTitle string | ||||
NewTitle string | NewTitle string | ||||
OldRef string | |||||
NewRef string | |||||
CommitID int64 | CommitID int64 | ||||
CommitSHA string | CommitSHA string | ||||
Patch string | Patch string | ||||
@@ -280,6 +280,8 @@ var migrations = []Migration{ | |||||
NewMigration("update branch protection for can push and whitelist enable", addBranchProtectionCanPushAndEnableWhitelist), | NewMigration("update branch protection for can push and whitelist enable", addBranchProtectionCanPushAndEnableWhitelist), | ||||
// v112 -> v113 | // v112 -> v113 | ||||
NewMigration("remove release attachments which repository deleted", removeAttachmentMissedRepo), | NewMigration("remove release attachments which repository deleted", removeAttachmentMissedRepo), | ||||
// v113 -> v114 | |||||
NewMigration("new feature: change target branch of pull requests", featureChangeTargetBranch), | |||||
} | } | ||||
// Migrate database to current version | // Migrate database to current version | ||||
@@ -0,0 +1,23 @@ | |||||
// Copyright 2019 The Gitea Authors. All rights reserved. | |||||
// Use of this source code is governed by a MIT-style | |||||
// license that can be found in the LICENSE file. | |||||
package migrations | |||||
import ( | |||||
"fmt" | |||||
"xorm.io/xorm" | |||||
) | |||||
func featureChangeTargetBranch(x *xorm.Engine) error { | |||||
type Comment struct { | |||||
OldRef string | |||||
NewRef string | |||||
} | |||||
if err := x.Sync2(new(Comment)); err != nil { | |||||
return fmt.Errorf("Sync2: %v", err) | |||||
} | |||||
return nil | |||||
} |
@@ -664,3 +664,32 @@ func (pr *PullRequest) GetWorkInProgressPrefix() string { | |||||
} | } | ||||
return "" | return "" | ||||
} | } | ||||
// IsHeadEqualWithBranch returns if the commits of branchName are available in pull request head | |||||
func (pr *PullRequest) IsHeadEqualWithBranch(branchName string) (bool, error) { | |||||
var err error | |||||
if err = pr.GetBaseRepo(); err != nil { | |||||
return false, err | |||||
} | |||||
baseGitRepo, err := git.OpenRepository(pr.BaseRepo.RepoPath()) | |||||
if err != nil { | |||||
return false, err | |||||
} | |||||
baseCommit, err := baseGitRepo.GetBranchCommit(branchName) | |||||
if err != nil { | |||||
return false, err | |||||
} | |||||
if err = pr.GetHeadRepo(); err != nil { | |||||
return false, err | |||||
} | |||||
headGitRepo, err := git.OpenRepository(pr.HeadRepo.RepoPath()) | |||||
if err != nil { | |||||
return false, err | |||||
} | |||||
headCommit, err := headGitRepo.GetBranchCommit(pr.HeadBranch) | |||||
if err != nil { | |||||
return false, err | |||||
} | |||||
return baseCommit.HasPreviousCommit(headCommit.ID) | |||||
} |
@@ -306,6 +306,27 @@ func (c *Commit) CommitsBefore() (*list.List, error) { | |||||
return c.repo.getCommitsBefore(c.ID) | return c.repo.getCommitsBefore(c.ID) | ||||
} | } | ||||
// HasPreviousCommit returns true if a given commitHash is contained in commit's parents | |||||
func (c *Commit) HasPreviousCommit(commitHash SHA1) (bool, error) { | |||||
for i := 0; i < c.ParentCount(); i++ { | |||||
commit, err := c.Parent(i) | |||||
if err != nil { | |||||
return false, err | |||||
} | |||||
if commit.ID == commitHash { | |||||
return true, nil | |||||
} | |||||
commitInParentCommit, err := commit.HasPreviousCommit(commitHash) | |||||
if err != nil { | |||||
return false, err | |||||
} | |||||
if commitInParentCommit { | |||||
return true, nil | |||||
} | |||||
} | |||||
return false, nil | |||||
} | |||||
// CommitsBeforeLimit returns num commits before current revision | // CommitsBeforeLimit returns num commits before current revision | ||||
func (c *Commit) CommitsBeforeLimit(num int) (*list.List, error) { | func (c *Commit) CommitsBeforeLimit(num int) (*list.List, error) { | ||||
return c.repo.getCommitsBeforeLimit(c.ID, num) | return c.repo.getCommitsBeforeLimit(c.ID, num) | ||||
@@ -34,6 +34,7 @@ type Notifier interface { | |||||
NotifyMergePullRequest(*models.PullRequest, *models.User, *git.Repository) | NotifyMergePullRequest(*models.PullRequest, *models.User, *git.Repository) | ||||
NotifyPullRequestSynchronized(doer *models.User, pr *models.PullRequest) | NotifyPullRequestSynchronized(doer *models.User, pr *models.PullRequest) | ||||
NotifyPullRequestReview(*models.PullRequest, *models.Review, *models.Comment) | NotifyPullRequestReview(*models.PullRequest, *models.Review, *models.Comment) | ||||
NotifyPullRequestChangeTargetBranch(doer *models.User, pr *models.PullRequest, oldBranch string) | |||||
NotifyCreateIssueComment(*models.User, *models.Repository, | NotifyCreateIssueComment(*models.User, *models.Repository, | ||||
*models.Issue, *models.Comment) | *models.Issue, *models.Comment) | ||||
@@ -50,6 +50,10 @@ func (*NullNotifier) NotifyMergePullRequest(pr *models.PullRequest, doer *models | |||||
func (*NullNotifier) NotifyPullRequestSynchronized(doer *models.User, pr *models.PullRequest) { | func (*NullNotifier) NotifyPullRequestSynchronized(doer *models.User, pr *models.PullRequest) { | ||||
} | } | ||||
// NotifyPullRequestChangeTargetBranch places a place holder function | |||||
func (*NullNotifier) NotifyPullRequestChangeTargetBranch(doer *models.User, pr *models.PullRequest, oldBranch string) { | |||||
} | |||||
// NotifyUpdateComment places a place holder function | // NotifyUpdateComment places a place holder function | ||||
func (*NullNotifier) NotifyUpdateComment(doer *models.User, c *models.Comment, oldContent string) { | func (*NullNotifier) NotifyUpdateComment(doer *models.User, c *models.Comment, oldContent string) { | ||||
} | } | ||||
@@ -87,6 +87,13 @@ func NotifyPullRequestReview(pr *models.PullRequest, review *models.Review, comm | |||||
} | } | ||||
} | } | ||||
// NotifyPullRequestChangeTargetBranch notifies when a pull request's target branch was changed | |||||
func NotifyPullRequestChangeTargetBranch(doer *models.User, pr *models.PullRequest, oldBranch string) { | |||||
for _, notifier := range notifiers { | |||||
notifier.NotifyPullRequestChangeTargetBranch(doer, pr, oldBranch) | |||||
} | |||||
} | |||||
// NotifyUpdateComment notifies update comment to notifiers | // NotifyUpdateComment notifies update comment to notifiers | ||||
func NotifyUpdateComment(doer *models.User, c *models.Comment, oldContent string) { | func NotifyUpdateComment(doer *models.User, c *models.Comment, oldContent string) { | ||||
for _, notifier := range notifiers { | for _, notifier := range notifiers { | ||||
@@ -559,6 +559,37 @@ func (*webhookNotifier) NotifyMergePullRequest(pr *models.PullRequest, doer *mod | |||||
} | } | ||||
} | } | ||||
func (m *webhookNotifier) NotifyPullRequestChangeTargetBranch(doer *models.User, pr *models.PullRequest, oldBranch string) { | |||||
issue := pr.Issue | |||||
if !issue.IsPull { | |||||
return | |||||
} | |||||
var err error | |||||
if err = issue.LoadPullRequest(); err != nil { | |||||
log.Error("LoadPullRequest failed: %v", err) | |||||
return | |||||
} | |||||
issue.PullRequest.Issue = issue | |||||
mode, _ := models.AccessLevel(issue.Poster, issue.Repo) | |||||
err = webhook_module.PrepareWebhooks(issue.Repo, models.HookEventPullRequest, &api.PullRequestPayload{ | |||||
Action: api.HookIssueEdited, | |||||
Index: issue.Index, | |||||
Changes: &api.ChangesPayload{ | |||||
Ref: &api.ChangesFromPayload{ | |||||
From: oldBranch, | |||||
}, | |||||
}, | |||||
PullRequest: issue.PullRequest.APIFormat(), | |||||
Repository: issue.Repo.APIFormat(mode), | |||||
Sender: doer.APIFormat(), | |||||
}) | |||||
if err != nil { | |||||
log.Error("PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err) | |||||
} | |||||
} | |||||
func (m *webhookNotifier) NotifyPullRequestReview(pr *models.PullRequest, review *models.Review, comment *models.Comment) { | func (m *webhookNotifier) NotifyPullRequestReview(pr *models.PullRequest, review *models.Review, comment *models.Comment) { | ||||
var reviewHookType models.HookEventType | var reviewHookType models.HookEventType | ||||
@@ -398,10 +398,11 @@ type ChangesFromPayload struct { | |||||
From string `json:"from"` | From string `json:"from"` | ||||
} | } | ||||
// ChangesPayload FIXME | |||||
// ChangesPayload represents the payload information of issue change | |||||
type ChangesPayload struct { | type ChangesPayload struct { | ||||
Title *ChangesFromPayload `json:"title,omitempty"` | Title *ChangesFromPayload `json:"title,omitempty"` | ||||
Body *ChangesFromPayload `json:"body,omitempty"` | Body *ChangesFromPayload `json:"body,omitempty"` | ||||
Ref *ChangesFromPayload `json:"ref,omitempty"` | |||||
} | } | ||||
// __________ .__ .__ __________ __ | // __________ .__ .__ __________ __ | ||||
@@ -1033,8 +1033,9 @@ pulls.no_results = No results found. | |||||
pulls.nothing_to_compare = These branches are equal. There is no need to create a pull request. | pulls.nothing_to_compare = These branches are equal. There is no need to create a pull request. | ||||
pulls.has_pull_request = `A pull request between these branches already exists: <a href="%[1]s/pulls/%[3]d">%[2]s#%[3]d</a>` | pulls.has_pull_request = `A pull request between these branches already exists: <a href="%[1]s/pulls/%[3]d">%[2]s#%[3]d</a>` | ||||
pulls.create = Create Pull Request | pulls.create = Create Pull Request | ||||
pulls.title_desc = wants to merge %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code> | |||||
pulls.title_desc = wants to merge %[1]d commits from <code>%[2]s</code> into <code id="branch_target">%[3]s</code> | |||||
pulls.merged_title_desc = merged %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code> %[4]s | pulls.merged_title_desc = merged %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code> %[4]s | ||||
pulls.change_target_branch_at = `changed target branch from <b>%s</b> to <b>%s</b> %s` | |||||
pulls.tab_conversation = Conversation | pulls.tab_conversation = Conversation | ||||
pulls.tab_commits = Commits | pulls.tab_commits = Commits | ||||
pulls.tab_files = Files Changed | pulls.tab_files = Files Changed | ||||
@@ -1042,6 +1043,7 @@ pulls.reopen_to_merge = Please reopen this pull request to perform a merge. | |||||
pulls.cant_reopen_deleted_branch = This pull request cannot be reopened because the branch was deleted. | pulls.cant_reopen_deleted_branch = This pull request cannot be reopened because the branch was deleted. | ||||
pulls.merged = Merged | pulls.merged = Merged | ||||
pulls.merged_as = The pull request has been merged as <a rel="nofollow" class="ui sha" href="%[1]s"><code>%[2]s</code></a>. | pulls.merged_as = The pull request has been merged as <a rel="nofollow" class="ui sha" href="%[1]s"><code>%[2]s</code></a>. | ||||
pulls.is_closed = The pull request has been closed. | |||||
pulls.has_merged = The pull request has been merged. | pulls.has_merged = The pull request has been merged. | ||||
pulls.title_wip_desc = `<a href="#">Start the title with <strong>%s</strong></a> to prevent the pull request from being merged accidentally.` | pulls.title_wip_desc = `<a href="#">Start the title with <strong>%s</strong></a> to prevent the pull request from being merged accidentally.` | ||||
pulls.cannot_merge_work_in_progress = This pull request is marked as a work in progress. Remove the <strong>%s</strong> prefix from the title when it's ready | pulls.cannot_merge_work_in_progress = This pull request is marked as a work in progress. Remove the <strong>%s</strong> prefix from the title when it's ready | ||||
@@ -605,6 +605,19 @@ func commentTag(repo *models.Repository, poster *models.User, issue *models.Issu | |||||
return models.CommentTagNone, nil | return models.CommentTagNone, nil | ||||
} | } | ||||
func getBranchData(ctx *context.Context, issue *models.Issue) { | |||||
ctx.Data["BaseBranch"] = nil | |||||
ctx.Data["HeadBranch"] = nil | |||||
ctx.Data["HeadUserName"] = nil | |||||
ctx.Data["BaseName"] = ctx.Repo.Repository.OwnerName | |||||
if issue.IsPull { | |||||
pull := issue.PullRequest | |||||
ctx.Data["BaseBranch"] = pull.BaseBranch | |||||
ctx.Data["HeadBranch"] = pull.HeadBranch | |||||
ctx.Data["HeadUserName"] = pull.MustHeadUserName() | |||||
} | |||||
} | |||||
// ViewIssue render issue view page | // ViewIssue render issue view page | ||||
func ViewIssue(ctx *context.Context) { | func ViewIssue(ctx *context.Context) { | ||||
if ctx.Params(":type") == "issues" { | if ctx.Params(":type") == "issues" { | ||||
@@ -885,6 +898,7 @@ func ViewIssue(ctx *context.Context) { | |||||
} | } | ||||
} | } | ||||
getBranchData(ctx, issue) | |||||
if issue.IsPull { | if issue.IsPull { | ||||
pull := issue.PullRequest | pull := issue.PullRequest | ||||
pull.Issue = issue | pull.Issue = issue | ||||
@@ -11,6 +11,7 @@ import ( | |||||
"crypto/subtle" | "crypto/subtle" | ||||
"fmt" | "fmt" | ||||
"html" | "html" | ||||
"net/http" | |||||
"path" | "path" | ||||
"strings" | "strings" | ||||
@@ -467,6 +468,7 @@ func ViewPullCommits(ctx *context.Context) { | |||||
ctx.Data["Commits"] = commits | ctx.Data["Commits"] = commits | ||||
ctx.Data["CommitCount"] = commits.Len() | ctx.Data["CommitCount"] = commits.Len() | ||||
getBranchData(ctx, issue) | |||||
ctx.HTML(200, tplPullCommits) | ctx.HTML(200, tplPullCommits) | ||||
} | } | ||||
@@ -596,6 +598,7 @@ func ViewPullFiles(ctx *context.Context) { | |||||
ctx.ServerError("GetCurrentReview", err) | ctx.ServerError("GetCurrentReview", err) | ||||
return | return | ||||
} | } | ||||
getBranchData(ctx, issue) | |||||
ctx.HTML(200, tplPullFiles) | ctx.HTML(200, tplPullFiles) | ||||
} | } | ||||
@@ -1010,3 +1013,74 @@ func DownloadPullDiffOrPatch(ctx *context.Context, patch bool) { | |||||
return | return | ||||
} | } | ||||
} | } | ||||
// UpdatePullRequestTarget change pull request's target branch | |||||
func UpdatePullRequestTarget(ctx *context.Context) { | |||||
issue := GetActionIssue(ctx) | |||||
pr := issue.PullRequest | |||||
if ctx.Written() { | |||||
return | |||||
} | |||||
if !issue.IsPull { | |||||
ctx.Error(http.StatusNotFound) | |||||
return | |||||
} | |||||
if !ctx.IsSigned || (!issue.IsPoster(ctx.User.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) { | |||||
ctx.Error(http.StatusForbidden) | |||||
return | |||||
} | |||||
targetBranch := ctx.QueryTrim("target_branch") | |||||
if len(targetBranch) == 0 { | |||||
ctx.Error(http.StatusNoContent) | |||||
return | |||||
} | |||||
if err := pull_service.ChangeTargetBranch(pr, ctx.User, targetBranch); err != nil { | |||||
if models.IsErrPullRequestAlreadyExists(err) { | |||||
err := err.(models.ErrPullRequestAlreadyExists) | |||||
RepoRelPath := ctx.Repo.Owner.Name + "/" + ctx.Repo.Repository.Name | |||||
errorMessage := ctx.Tr("repo.pulls.has_pull_request", ctx.Repo.RepoLink, RepoRelPath, err.IssueID) | |||||
ctx.Flash.Error(errorMessage) | |||||
ctx.JSON(http.StatusConflict, map[string]interface{}{ | |||||
"error": err.Error(), | |||||
"user_error": errorMessage, | |||||
}) | |||||
} else if models.IsErrIssueIsClosed(err) { | |||||
errorMessage := ctx.Tr("repo.pulls.is_closed") | |||||
ctx.Flash.Error(errorMessage) | |||||
ctx.JSON(http.StatusConflict, map[string]interface{}{ | |||||
"error": err.Error(), | |||||
"user_error": errorMessage, | |||||
}) | |||||
} else if models.IsErrPullRequestHasMerged(err) { | |||||
errorMessage := ctx.Tr("repo.pulls.has_merged") | |||||
ctx.Flash.Error(errorMessage) | |||||
ctx.JSON(http.StatusConflict, map[string]interface{}{ | |||||
"error": err.Error(), | |||||
"user_error": errorMessage, | |||||
}) | |||||
} else if models.IsErrBranchesEqual(err) { | |||||
errorMessage := ctx.Tr("repo.pulls.nothing_to_compare") | |||||
ctx.Flash.Error(errorMessage) | |||||
ctx.JSON(http.StatusBadRequest, map[string]interface{}{ | |||||
"error": err.Error(), | |||||
"user_error": errorMessage, | |||||
}) | |||||
} else { | |||||
ctx.ServerError("UpdatePullRequestTarget", err) | |||||
} | |||||
return | |||||
} | |||||
notification.NotifyPullRequestChangeTargetBranch(ctx.User, pr, targetBranch) | |||||
ctx.JSON(http.StatusOK, map[string]interface{}{ | |||||
"base_branch": pr.BaseBranch, | |||||
}) | |||||
} |
@@ -770,6 +770,9 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||
m.Combo("/compare/*", repo.MustBeNotEmpty, reqRepoCodeReader, repo.SetEditorconfigIfExists). | m.Combo("/compare/*", repo.MustBeNotEmpty, reqRepoCodeReader, repo.SetEditorconfigIfExists). | ||||
Get(repo.SetDiffViewStyle, repo.CompareDiff). | Get(repo.SetDiffViewStyle, repo.CompareDiff). | ||||
Post(context.RepoMustNotBeArchived(), reqRepoPullsReader, repo.MustAllowPulls, bindIgnErr(auth.CreateIssueForm{}), repo.CompareAndPullRequestPost) | Post(context.RepoMustNotBeArchived(), reqRepoPullsReader, repo.MustAllowPulls, bindIgnErr(auth.CreateIssueForm{}), repo.CompareAndPullRequestPost) | ||||
m.Group("/pull", func() { | |||||
m.Post("/:index/target_branch", repo.UpdatePullRequestTarget) | |||||
}, context.RepoMustNotBeArchived()) | |||||
m.Group("", func() { | m.Group("", func() { | ||||
m.Group("", func() { | m.Group("", func() { | ||||
@@ -46,6 +46,94 @@ func NewPullRequest(repo *models.Repository, pull *models.Issue, labelIDs []int6 | |||||
return nil | return nil | ||||
} | } | ||||
// ChangeTargetBranch changes the target branch of this pull request, as the given user. | |||||
func ChangeTargetBranch(pr *models.PullRequest, doer *models.User, targetBranch string) (err error) { | |||||
// Current target branch is already the same | |||||
if pr.BaseBranch == targetBranch { | |||||
return nil | |||||
} | |||||
if pr.Issue.IsClosed { | |||||
return models.ErrIssueIsClosed{ | |||||
ID: pr.Issue.ID, | |||||
RepoID: pr.Issue.RepoID, | |||||
Index: pr.Issue.Index, | |||||
} | |||||
} | |||||
if pr.HasMerged { | |||||
return models.ErrPullRequestHasMerged{ | |||||
ID: pr.ID, | |||||
IssueID: pr.Index, | |||||
HeadRepoID: pr.HeadRepoID, | |||||
BaseRepoID: pr.BaseRepoID, | |||||
HeadBranch: pr.HeadBranch, | |||||
BaseBranch: pr.BaseBranch, | |||||
} | |||||
} | |||||
// Check if branches are equal | |||||
branchesEqual, err := pr.IsHeadEqualWithBranch(targetBranch) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
if branchesEqual { | |||||
return models.ErrBranchesEqual{ | |||||
HeadBranchName: pr.HeadBranch, | |||||
BaseBranchName: targetBranch, | |||||
} | |||||
} | |||||
// Check if pull request for the new target branch already exists | |||||
existingPr, err := models.GetUnmergedPullRequest(pr.HeadRepoID, pr.BaseRepoID, pr.HeadBranch, targetBranch) | |||||
if existingPr != nil { | |||||
return models.ErrPullRequestAlreadyExists{ | |||||
ID: existingPr.ID, | |||||
IssueID: existingPr.Index, | |||||
HeadRepoID: existingPr.HeadRepoID, | |||||
BaseRepoID: existingPr.BaseRepoID, | |||||
HeadBranch: existingPr.HeadBranch, | |||||
BaseBranch: existingPr.BaseBranch, | |||||
} | |||||
} | |||||
if err != nil && !models.IsErrPullRequestNotExist(err) { | |||||
return err | |||||
} | |||||
// Set new target branch | |||||
oldBranch := pr.BaseBranch | |||||
pr.BaseBranch = targetBranch | |||||
// Refresh patch | |||||
if err := TestPatch(pr); err != nil { | |||||
return err | |||||
} | |||||
// Update target branch, PR diff and status | |||||
// This is the same as checkAndUpdateStatus in check service, but also updates base_branch | |||||
if pr.Status == models.PullRequestStatusChecking { | |||||
pr.Status = models.PullRequestStatusMergeable | |||||
} | |||||
if err := pr.UpdateCols("status, conflicted_files, base_branch"); err != nil { | |||||
return err | |||||
} | |||||
// Create comment | |||||
options := &models.CreateCommentOptions{ | |||||
Type: models.CommentTypeChangeTargetBranch, | |||||
Doer: doer, | |||||
Repo: pr.Issue.Repo, | |||||
Issue: pr.Issue, | |||||
OldRef: oldBranch, | |||||
NewRef: targetBranch, | |||||
} | |||||
if _, err = models.CreateComment(options); err != nil { | |||||
return fmt.Errorf("CreateChangeTargetBranchComment: %v", err) | |||||
} | |||||
return nil | |||||
} | |||||
func checkForInvalidation(requests models.PullRequestList, repoID int64, doer *models.User, branch string) error { | func checkForInvalidation(requests models.PullRequestList, repoID int64, doer *models.User, branch string) error { | ||||
repo, err := models.GetRepositoryByID(repoID) | repo, err := models.GetRepositoryByID(repoID) | ||||
if err != nil { | if err != nil { | ||||
@@ -6,7 +6,7 @@ | |||||
5 = COMMENT_REF, 6 = PULL_REF, 7 = COMMENT_LABEL, 12 = START_TRACKING, | 5 = COMMENT_REF, 6 = PULL_REF, 7 = COMMENT_LABEL, 12 = START_TRACKING, | ||||
13 = STOP_TRACKING, 14 = ADD_TIME_MANUAL, 16 = ADDED_DEADLINE, 17 = MODIFIED_DEADLINE, | 13 = STOP_TRACKING, 14 = ADD_TIME_MANUAL, 16 = ADDED_DEADLINE, 17 = MODIFIED_DEADLINE, | ||||
18 = REMOVED_DEADLINE, 19 = ADD_DEPENDENCY, 20 = REMOVE_DEPENDENCY, 21 = CODE, | 18 = REMOVED_DEADLINE, 19 = ADD_DEPENDENCY, 20 = REMOVE_DEPENDENCY, 21 = CODE, | ||||
22 = REVIEW, 23 = ISSUE_LOCKED, 24 = ISSUE_UNLOCKED --> | |||||
22 = REVIEW, 23 = ISSUE_LOCKED, 24 = ISSUE_UNLOCKED, 25 = TARGET_BRANCH_CHANGED --> | |||||
{{if eq .Type 0}} | {{if eq .Type 0}} | ||||
<div class="comment" id="{{.HashTag}}"> | <div class="comment" id="{{.HashTag}}"> | ||||
{{if .OriginalAuthor }} | {{if .OriginalAuthor }} | ||||
@@ -411,5 +411,15 @@ | |||||
{{$.i18n.Tr "repo.issues.unlock_comment" $createdStr | Safe}} | {{$.i18n.Tr "repo.issues.unlock_comment" $createdStr | Safe}} | ||||
</span> | </span> | ||||
</div> | </div> | ||||
{{else if eq .Type 25}} | |||||
<div class="event"> | |||||
<span class="octicon octicon-primitive-dot"></span> | |||||
<a class="ui avatar image" href="{{.Poster.HomeLink}}"> | |||||
<img src="{{.Poster.RelAvatarLink}}"> | |||||
</a> | |||||
<span class="text grey"><a href="{{.Poster.HomeLink}}">{{.Poster.Name}}</a> | |||||
{{$.i18n.Tr "repo.pulls.change_target_branch_at" (.OldRef|Escape) (.NewRef|Escape) $createdStr | Safe}} | |||||
</span> | |||||
</div> | |||||
{{end}} | {{end}} | ||||
{{end}} | {{end}} |
@@ -11,7 +11,7 @@ | |||||
<div class="edit-zone text right"> | <div class="edit-zone text right"> | ||||
<div id="edit-title" class="ui basic green not-in-edit button">{{.i18n.Tr "repo.issues.edit"}}</div> | <div id="edit-title" class="ui basic green not-in-edit button">{{.i18n.Tr "repo.issues.edit"}}</div> | ||||
<div id="cancel-edit-title" class="ui basic blue in-edit button" style="display: none">{{.i18n.Tr "repo.issues.cancel"}}</div> | <div id="cancel-edit-title" class="ui basic blue in-edit button" style="display: none">{{.i18n.Tr "repo.issues.cancel"}}</div> | ||||
<div id="save-edit-title" class="ui green in-edit button" style="display: none" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/title">{{.i18n.Tr "repo.issues.save"}}</div> | |||||
<div id="save-edit-title" class="ui green in-edit button" style="display: none" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/title" {{if .Issue.IsPull}}data-target-update-url="{{$.RepoLink}}/pull/{{.Issue.Index}}/target_branch"{{end}}>{{.i18n.Tr "repo.issues.save"}}</div> | |||||
</div> | </div> | ||||
</div> | </div> | ||||
{{end}} | {{end}} | ||||
@@ -36,12 +36,42 @@ | |||||
{{end}} | {{end}} | ||||
{{else}} | {{else}} | ||||
{{if .Issue.OriginalAuthor }} | {{if .Issue.OriginalAuthor }} | ||||
{{.Issue.OriginalAuthor}} | |||||
<span class="pull-desc">{{$.i18n.Tr "repo.pulls.title_desc" .NumCommits .HeadTarget .BaseTarget | Str2html}}</span> | |||||
<span id="pull-desc" class="pull-desc">{{.Issue.OriginalAuthor}} {{$.i18n.Tr "repo.pulls.title_desc" .NumCommits .HeadTarget .BaseTarget | Str2html}}</span> | |||||
{{else}} | {{else}} | ||||
<a {{if gt .Issue.Poster.ID 0}}href="{{.Issue.Poster.HomeLink}}"{{end}}>{{.Issue.Poster.GetDisplayName}}</a> | |||||
<span class="pull-desc">{{$.i18n.Tr "repo.pulls.title_desc" .NumCommits .HeadTarget .BaseTarget | Str2html}}</span> | |||||
<span id="pull-desc" class="pull-desc"> | |||||
<a {{if gt .Issue.Poster.ID 0}}href="{{.Issue.Poster.HomeLink}}"{{end}}>{{.Issue.Poster.GetDisplayName}}</a> | |||||
{{$.i18n.Tr "repo.pulls.title_desc" .NumCommits .HeadTarget .BaseTarget | Str2html}} | |||||
</span> | |||||
{{end}} | {{end}} | ||||
<span id="pull-desc-edit" style="display: none"> | |||||
<div class="ui floating filter dropdown"> | |||||
<div class="ui basic small button"> | |||||
<span class="text">{{.i18n.Tr "repo.pulls.compare_compare"}}: {{$.HeadTarget}}</span> | |||||
</div> | |||||
</div> | |||||
<i class="octicon octicon-arrow-right"></i> | |||||
<div class="ui floating filter dropdown" data-no-results="{{.i18n.Tr "repo.pulls.no_results"}}"> | |||||
<div class="ui basic small button"> | |||||
<span class="text" id="pull-target-branch" data-basename="{{$.BaseName}}" data-branch="{{$.BaseBranch}}">{{.i18n.Tr "repo.pulls.compare_base"}}: {{$.BaseName}}:{{$.BaseBranch}}</span> | |||||
<i class="dropdown icon"></i> | |||||
</div> | |||||
<div class="menu"> | |||||
<div class="ui icon search input"> | |||||
<i class="filter icon"></i> | |||||
<input name="search" placeholder="{{.i18n.Tr "repo.pulls.filter_branch"}}..."> | |||||
</div> | |||||
<div class="scrolling menu" id="branch-select"> | |||||
{{range .Branches}} | |||||
{{ $sameBase := ne $.BaseName $.HeadUserName }} | |||||
{{ $differentBranch := ne . $.HeadBranch }} | |||||
{{ if or $sameBase $differentBranch }} | |||||
<div class="item {{if eq $.BaseBranch .}}selected{{end}}" data-branch="{{.}}">{{$.BaseName}}{{if $.HeadRepo}}/{{$.HeadRepo}}{{end}}:{{.}}</div> | |||||
{{ end }} | |||||
{{end}} | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</span> | |||||
{{end}} | {{end}} | ||||
{{else}} | {{else}} | ||||
{{ $createdStr:= TimeSinceUnix .Issue.CreatedUnix $.Lang }} | {{ $createdStr:= TimeSinceUnix .Issue.CreatedUnix $.Lang }} | ||||
@@ -731,27 +731,66 @@ function initRepository() { | |||||
$issueTitle.toggle(); | $issueTitle.toggle(); | ||||
$('.not-in-edit').toggle(); | $('.not-in-edit').toggle(); | ||||
$('#edit-title-input').toggle(); | $('#edit-title-input').toggle(); | ||||
$('#pull-desc').toggle(); | |||||
$('#pull-desc-edit').toggle(); | |||||
$('.in-edit').toggle(); | $('.in-edit').toggle(); | ||||
$editInput.focus(); | $editInput.focus(); | ||||
return false; | return false; | ||||
}; | }; | ||||
const changeBranchSelect = function () { | |||||
const selectionTextField = $('#pull-target-branch'); | |||||
const baseName = selectionTextField.data('basename'); | |||||
const branchNameNew = $(this).data('branch'); | |||||
const branchNameOld = selectionTextField.data('branch'); | |||||
// Replace branch name to keep translation from HTML template | |||||
selectionTextField.html(selectionTextField.html().replace( | |||||
`${baseName}:${branchNameOld}`, | |||||
`${baseName}:${branchNameNew}` | |||||
)); | |||||
selectionTextField.data('branch', branchNameNew); // update branch name in setting | |||||
}; | |||||
$('#branch-select > .item').click(changeBranchSelect); | |||||
$('#edit-title').click(editTitleToggle); | $('#edit-title').click(editTitleToggle); | ||||
$('#cancel-edit-title').click(editTitleToggle); | $('#cancel-edit-title').click(editTitleToggle); | ||||
$('#save-edit-title').click(editTitleToggle).click(function () { | $('#save-edit-title').click(editTitleToggle).click(function () { | ||||
const pullrequest_targetbranch_change = function (update_url) { | |||||
const targetBranch = $('#pull-target-branch').data('branch'); | |||||
const $branchTarget = $('#branch_target'); | |||||
if (targetBranch === $branchTarget.text()) { | |||||
return false; | |||||
} | |||||
$.post(update_url, { | |||||
_csrf: csrf, | |||||
target_branch: targetBranch | |||||
}) | |||||
.success((data) => { | |||||
$branchTarget.text(data.base_branch); | |||||
}) | |||||
.always(() => { | |||||
reload(); | |||||
}); | |||||
}; | |||||
const pullrequest_target_update_url = $(this).data('target-update-url'); | |||||
if ($editInput.val().length === 0 || $editInput.val() === $issueTitle.text()) { | if ($editInput.val().length === 0 || $editInput.val() === $issueTitle.text()) { | ||||
$editInput.val($issueTitle.text()); | $editInput.val($issueTitle.text()); | ||||
return false; | |||||
pullrequest_targetbranch_change(pullrequest_target_update_url); | |||||
} else { | |||||
$.post($(this).data('update-url'), { | |||||
_csrf: csrf, | |||||
title: $editInput.val() | |||||
}, | |||||
(data) => { | |||||
$editInput.val(data.title); | |||||
$issueTitle.text(data.title); | |||||
pullrequest_targetbranch_change(pullrequest_target_update_url); | |||||
reload(); | |||||
}); | |||||
} | } | ||||
$.post($(this).data('update-url'), { | |||||
_csrf: csrf, | |||||
title: $editInput.val() | |||||
}, | |||||
(data) => { | |||||
$editInput.val(data.title); | |||||
$issueTitle.text(data.title); | |||||
reload(); | |||||
}); | |||||
return false; | return false; | ||||
}); | }); | ||||