diff --git a/models/context.go b/models/context.go index 5f47c595a25d7..6b3c261dd484c 100644 --- a/models/context.go +++ b/models/context.go @@ -53,3 +53,9 @@ func WithTx(f func(ctx DBContext) error) error { sess.Close() return err } + +// Insert inserts an object to database +func Insert(ctx DBContext, bean interface{}) error { + _, err := ctx.e.Insert(bean) + return err +} diff --git a/models/issue.go b/models/issue.go index 0a08a97fdd35b..f290626ab57c8 100644 --- a/models/issue.go +++ b/models/issue.go @@ -75,7 +75,6 @@ var ( const issueTasksRegexpStr = `(^\s*[-*]\s\[[\sx]\]\s.)|(\n\s*[-*]\s\[[\sx]\]\s.)` const issueTasksDoneRegexpStr = `(^\s*[-*]\s\[[x]\]\s.)|(\n\s*[-*]\s\[[x]\]\s.)` -const issueMaxDupIndexAttempts = 3 func init() { issueTasksPat = regexp.MustCompile(issueTasksRegexpStr) @@ -447,7 +446,7 @@ func (issue *Issue) ReplyReference() string { return fmt.Sprintf("%s/%s/%d@%s", issue.Repo.FullName(), path, issue.Index, setting.Domain) } -func (issue *Issue) addLabel(e *xorm.Session, label *Label, doer *User) error { +func (issue *Issue) addLabel(e Engine, label *Label, doer *User) error { return newIssueLabel(e, issue, label, doer) } @@ -843,10 +842,9 @@ type NewIssueOptions struct { Issue *Issue LabelIDs []int64 Attachments []string // In UUID format. - IsPull bool } -func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) { +func newIssue(e Engine, doer *User, opts NewIssueOptions) (err error) { opts.Issue.Title = strings.TrimSpace(opts.Issue.Title) if opts.Issue.MilestoneID > 0 { @@ -864,8 +862,8 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) { } // Milestone validation should happen before insert actual object. - if _, err := e.SetExpr("`index`", "coalesce(MAX(`index`),0)+1"). - Where("repo_id=?", opts.Issue.RepoID). + if _, err := e.Where("repo_id=?", opts.Issue.RepoID). + SetExpr("`index`", "coalesce(MAX(`index`),0)+1"). Insert(opts.Issue); err != nil { return ErrNewIssueInsert{err} } @@ -896,7 +894,7 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) { } } - if opts.IsPull { + if opts.Issue.IsPull { _, err = e.Exec("UPDATE `repository` SET num_pulls = num_pulls + 1 WHERE id = ?", opts.Issue.RepoID) } else { _, err = e.Exec("UPDATE `repository` SET num_issues = num_issues + 1 WHERE id = ?", opts.Issue.RepoID) @@ -953,48 +951,13 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) { } // NewIssue creates new issue with labels for repository. -func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) { - // Retry several times in case INSERT fails due to duplicate key for (repo_id, index); see #7887 - i := 0 - for { - if err = newIssueAttempt(repo, issue, labelIDs, uuids); err == nil { - return nil - } - if !IsErrNewIssueInsert(err) { - return err - } - if i++; i == issueMaxDupIndexAttempts { - break - } - log.Error("NewIssue: error attempting to insert the new issue; will retry. Original error: %v", err) - } - return fmt.Errorf("NewIssue: too many errors attempting to insert the new issue. Last error was: %v", err) -} - -func newIssueAttempt(repo *Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) { - sess := x.NewSession() - defer sess.Close() - if err = sess.Begin(); err != nil { - return err - } - - if err = newIssue(sess, issue.Poster, NewIssueOptions{ +func NewIssue(ctx DBContext, repo *Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) { + return newIssue(ctx.e, issue.Poster, NewIssueOptions{ Repo: repo, Issue: issue, LabelIDs: labelIDs, Attachments: uuids, - }); err != nil { - if IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) { - return err - } - return fmt.Errorf("newIssue: %v", err) - } - - if err = sess.Commit(); err != nil { - return fmt.Errorf("Commit: %v", err) - } - - return nil + }) } // GetIssueByIndex returns raw issue without loading attributes by index in a repository. diff --git a/models/issue_comment.go b/models/issue_comment.go index 5843689f1bbb4..93116b30ec948 100644 --- a/models/issue_comment.go +++ b/models/issue_comment.go @@ -495,7 +495,7 @@ func (c *Comment) CodeCommentURL() string { return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag()) } -func createCommentWithNoAction(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err error) { +func createCommentWithNoAction(e Engine, opts *CreateCommentOptions) (_ *Comment, err error) { var LabelID int64 if opts.Label != nil { LabelID = opts.Label.ID @@ -546,7 +546,7 @@ func createCommentWithNoAction(e *xorm.Session, opts *CreateCommentOptions) (_ * return comment, nil } -func updateCommentInfos(e *xorm.Session, opts *CreateCommentOptions, comment *Comment) (err error) { +func updateCommentInfos(e Engine, opts *CreateCommentOptions, comment *Comment) (err error) { // Check comment type. switch opts.Type { case CommentTypeCode: @@ -596,7 +596,7 @@ func updateCommentInfos(e *xorm.Session, opts *CreateCommentOptions, comment *Co return updateIssueCols(e, opts.Issue, "updated_unix") } -func sendCreateCommentAction(e *xorm.Session, opts *CreateCommentOptions, comment *Comment) (err error) { +func sendCreateCommentAction(e Engine, opts *CreateCommentOptions, comment *Comment) (err error) { // Compose comment action, could be plain comment, close or reopen issue/pull request. // This object will be used to notify watchers in the end of function. act := &Action{ diff --git a/models/issue_label.go b/models/issue_label.go index 1ea0ed85cc14f..f9920a88f28e2 100644 --- a/models/issue_label.go +++ b/models/issue_label.go @@ -413,7 +413,7 @@ func HasIssueLabel(issueID, labelID int64) bool { return hasIssueLabel(x, issueID, labelID) } -func newIssueLabel(e *xorm.Session, issue *Issue, label *Label, doer *User) (err error) { +func newIssueLabel(e Engine, issue *Issue, label *Label, doer *User) (err error) { if _, err = e.Insert(&IssueLabel{ IssueID: issue.ID, LabelID: label.ID, diff --git a/models/issue_test.go b/models/issue_test.go index d369b0acf5c3b..eb53e6188c525 100644 --- a/models/issue_test.go +++ b/models/issue_test.go @@ -296,7 +296,7 @@ func testInsertIssue(t *testing.T, title, content string) { Title: title, Content: content, } - err := NewIssue(repo, &issue, nil, nil) + err := NewIssue(DefaultDBContext(), repo, &issue, nil, nil) assert.NoError(t, err) var newIssue Issue diff --git a/models/issue_xref.go b/models/issue_xref.go index 5cf8f58a0f256..dbbbc0527f460 100644 --- a/models/issue_xref.go +++ b/models/issue_xref.go @@ -11,7 +11,6 @@ import ( "code.gitea.io/gitea/modules/references" "github.com/unknwon/com" - "xorm.io/xorm" ) type crossReference struct { @@ -61,7 +60,7 @@ func neuterCrossReferencesIds(e Engine, ids []int64) error { // \/ \/ \/ // -func (issue *Issue) addCrossReferences(e *xorm.Session, doer *User, removeOld bool) error { +func (issue *Issue) addCrossReferences(e Engine, doer *User, removeOld bool) error { var commentType CommentType if issue.IsPull { commentType = CommentTypePullRef @@ -77,7 +76,7 @@ func (issue *Issue) addCrossReferences(e *xorm.Session, doer *User, removeOld bo return issue.createCrossReferences(e, ctx, issue.Title, issue.Content) } -func (issue *Issue) createCrossReferences(e *xorm.Session, ctx *crossReferencesContext, plaincontent, mdcontent string) error { +func (issue *Issue) createCrossReferences(e Engine, ctx *crossReferencesContext, plaincontent, mdcontent string) error { xreflist, err := ctx.OrigIssue.getCrossReferences(e, ctx, plaincontent, mdcontent) if err != nil { return err @@ -138,7 +137,7 @@ func (issue *Issue) createCrossReferences(e *xorm.Session, ctx *crossReferencesC return nil } -func (issue *Issue) getCrossReferences(e *xorm.Session, ctx *crossReferencesContext, plaincontent, mdcontent string) ([]*crossReference, error) { +func (issue *Issue) getCrossReferences(e Engine, ctx *crossReferencesContext, plaincontent, mdcontent string) ([]*crossReference, error) { xreflist := make([]*crossReference, 0, 5) var ( refRepo *Repository @@ -246,7 +245,7 @@ func (issue *Issue) verifyReferencedIssue(e Engine, ctx *crossReferencesContext, // \/ \/ \/ \/ \/ // -func (comment *Comment) addCrossReferences(e *xorm.Session, doer *User, removeOld bool) error { +func (comment *Comment) addCrossReferences(e Engine, doer *User, removeOld bool) error { if comment.Type != CommentTypeCode && comment.Type != CommentTypeComment { return nil } diff --git a/models/pull.go b/models/pull.go index 2bd79202f094b..a463a0c6e00fd 100644 --- a/models/pull.go +++ b/models/pull.go @@ -6,22 +6,16 @@ package models import ( - "bufio" "fmt" "os" "path" - "path/filepath" - "strconv" "strings" - "time" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" - - "github.com/unknwon/com" ) // PullRequestType defines pull request type @@ -101,14 +95,14 @@ func (pr *PullRequest) LoadAttributes() error { } // LoadBaseRepo loads pull request base repository from database -func (pr *PullRequest) LoadBaseRepo() error { +func (pr *PullRequest) LoadBaseRepo(ctx DBContext) error { if pr.BaseRepo == nil { if pr.HeadRepoID == pr.BaseRepoID && pr.HeadRepo != nil { pr.BaseRepo = pr.HeadRepo return nil } var repo Repository - if has, err := x.ID(pr.BaseRepoID).Get(&repo); err != nil { + if has, err := ctx.e.ID(pr.BaseRepoID).Get(&repo); err != nil { return err } else if !has { return ErrRepoNotExist{ID: pr.BaseRepoID} @@ -389,7 +383,7 @@ func (pr *PullRequest) GetLastCommitStatus() (status *CommitStatus, err error) { return nil, err } - err = pr.LoadBaseRepo() + err = pr.LoadBaseRepo(DefaultDBContext()) if err != nil { return nil, err } @@ -481,187 +475,6 @@ func (pr *PullRequest) SetMerged() (err error) { return nil } -// patchConflicts is a list of conflict description from Git. -var patchConflicts = []string{ - "patch does not apply", - "already exists in working directory", - "unrecognized input", - "error:", -} - -// TestPatch checks if patch can be merged to base repository without conflict. -func (pr *PullRequest) TestPatch() error { - return pr.testPatch(x) -} - -// testPatch checks if patch can be merged to base repository without conflict. -func (pr *PullRequest) testPatch(e Engine) (err error) { - if pr.BaseRepo == nil { - pr.BaseRepo, err = getRepositoryByID(e, pr.BaseRepoID) - if err != nil { - return fmt.Errorf("GetRepositoryByID: %v", err) - } - } - - patchPath, err := pr.BaseRepo.patchPath(e, pr.Index) - if err != nil { - return fmt.Errorf("BaseRepo.PatchPath: %v", err) - } - - // Fast fail if patch does not exist, this assumes data is corrupted. - if !com.IsFile(patchPath) { - log.Trace("PullRequest[%d].testPatch: ignored corrupted data", pr.ID) - return nil - } - - RepoWorkingPool.CheckIn(com.ToStr(pr.BaseRepoID)) - defer RepoWorkingPool.CheckOut(com.ToStr(pr.BaseRepoID)) - - log.Trace("PullRequest[%d].testPatch (patchPath): %s", pr.ID, patchPath) - - pr.Status = PullRequestStatusChecking - - indexTmpPath := filepath.Join(os.TempDir(), "gitea-"+pr.BaseRepo.Name+"-"+strconv.Itoa(time.Now().Nanosecond())) - defer os.Remove(indexTmpPath) - - _, err = git.NewCommand("read-tree", pr.BaseBranch).RunInDirWithEnv("", []string{"GIT_DIR=" + pr.BaseRepo.RepoPath(), "GIT_INDEX_FILE=" + indexTmpPath}) - if err != nil { - return fmt.Errorf("git read-tree --index-output=%s %s: %v", indexTmpPath, pr.BaseBranch, err) - } - - prUnit, err := pr.BaseRepo.getUnit(e, UnitTypePullRequests) - if err != nil { - return err - } - prConfig := prUnit.PullRequestsConfig() - - args := []string{"apply", "--check", "--cached"} - if prConfig.IgnoreWhitespaceConflicts { - args = append(args, "--ignore-whitespace") - } - args = append(args, patchPath) - pr.ConflictedFiles = []string{} - - stderrBuilder := new(strings.Builder) - err = git.NewCommand(args...).RunInDirTimeoutEnvPipeline( - []string{"GIT_INDEX_FILE=" + indexTmpPath, "GIT_DIR=" + pr.BaseRepo.RepoPath()}, - -1, - "", - nil, - stderrBuilder) - stderr := stderrBuilder.String() - - if err != nil { - for i := range patchConflicts { - if strings.Contains(stderr, patchConflicts[i]) { - log.Trace("PullRequest[%d].testPatch (apply): has conflict: %s", pr.ID, stderr) - const prefix = "error: patch failed:" - pr.Status = PullRequestStatusConflict - pr.ConflictedFiles = make([]string, 0, 5) - scanner := bufio.NewScanner(strings.NewReader(stderr)) - for scanner.Scan() { - line := scanner.Text() - - if strings.HasPrefix(line, prefix) { - var found bool - var filepath = strings.TrimSpace(strings.Split(line[len(prefix):], ":")[0]) - for _, f := range pr.ConflictedFiles { - if f == filepath { - found = true - break - } - } - if !found { - pr.ConflictedFiles = append(pr.ConflictedFiles, filepath) - } - } - // only list 10 conflicted files - if len(pr.ConflictedFiles) >= 10 { - break - } - } - - if len(pr.ConflictedFiles) > 0 { - log.Trace("Found %d files conflicted: %v", len(pr.ConflictedFiles), pr.ConflictedFiles) - } - - return nil - } - } - - return fmt.Errorf("git apply --check: %v - %s", err, stderr) - } - return nil -} - -// NewPullRequest creates new pull request with labels for repository. -func NewPullRequest(repo *Repository, pull *Issue, labelIDs []int64, uuids []string, pr *PullRequest, patch []byte) (err error) { - // Retry several times in case INSERT fails due to duplicate key for (repo_id, index); see #7887 - i := 0 - for { - if err = newPullRequestAttempt(repo, pull, labelIDs, uuids, pr, patch); err == nil { - return nil - } - if !IsErrNewIssueInsert(err) { - return err - } - if i++; i == issueMaxDupIndexAttempts { - break - } - log.Error("NewPullRequest: error attempting to insert the new issue; will retry. Original error: %v", err) - } - return fmt.Errorf("NewPullRequest: too many errors attempting to insert the new issue. Last error was: %v", err) -} - -func newPullRequestAttempt(repo *Repository, pull *Issue, labelIDs []int64, uuids []string, pr *PullRequest, patch []byte) (err error) { - sess := x.NewSession() - defer sess.Close() - if err = sess.Begin(); err != nil { - return err - } - - if err = newIssue(sess, pull.Poster, NewIssueOptions{ - Repo: repo, - Issue: pull, - LabelIDs: labelIDs, - Attachments: uuids, - IsPull: true, - }); err != nil { - if IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) { - return err - } - return fmt.Errorf("newIssue: %v", err) - } - - pr.Index = pull.Index - pr.BaseRepo = repo - pr.Status = PullRequestStatusChecking - if len(patch) > 0 { - if err = repo.savePatch(sess, pr.Index, patch); err != nil { - return fmt.Errorf("SavePatch: %v", err) - } - - if err = pr.testPatch(sess); err != nil { - return fmt.Errorf("testPatch: %v", err) - } - } - // No conflict appears after test means mergeable. - if pr.Status == PullRequestStatusChecking { - pr.Status = PullRequestStatusMergeable - } - - pr.IssueID = pull.ID - if _, err = sess.Insert(pr); err != nil { - return fmt.Errorf("insert pull repo: %v", err) - } - - if err = sess.Commit(); err != nil { - return fmt.Errorf("Commit: %v", err) - } - - return nil -} - // GetUnmergedPullRequest returns a pull request that is open and has not been merged // by given head/base and repo/branch. func GetUnmergedPullRequest(headRepoID, baseRepoID int64, headBranch, baseBranch string) (*PullRequest, error) { @@ -764,54 +577,6 @@ func (pr *PullRequest) UpdateCols(cols ...string) error { return err } -// UpdatePatch generates and saves a new patch. -func (pr *PullRequest) UpdatePatch() (err error) { - if err = pr.GetHeadRepo(); err != nil { - return fmt.Errorf("GetHeadRepo: %v", err) - } else if pr.HeadRepo == nil { - log.Trace("PullRequest[%d].UpdatePatch: ignored corrupted data", pr.ID) - return nil - } - - if err = pr.GetBaseRepo(); err != nil { - return fmt.Errorf("GetBaseRepo: %v", err) - } - - headGitRepo, err := git.OpenRepository(pr.HeadRepo.RepoPath()) - if err != nil { - return fmt.Errorf("OpenRepository: %v", err) - } - defer headGitRepo.Close() - - // Add a temporary remote. - tmpRemote := com.ToStr(time.Now().UnixNano()) - if err = headGitRepo.AddRemote(tmpRemote, RepoPath(pr.BaseRepo.MustOwner().Name, pr.BaseRepo.Name), true); err != nil { - return fmt.Errorf("AddRemote: %v", err) - } - defer func() { - if err := headGitRepo.RemoveRemote(tmpRemote); err != nil { - log.Error("UpdatePatch: RemoveRemote: %s", err) - } - }() - pr.MergeBase, _, err = headGitRepo.GetMergeBase(tmpRemote, pr.BaseBranch, pr.HeadBranch) - if err != nil { - return fmt.Errorf("GetMergeBase: %v", err) - } else if err = pr.Update(); err != nil { - return fmt.Errorf("Update: %v", err) - } - - patch, err := headGitRepo.GetPatch(pr.MergeBase, pr.HeadBranch) - if err != nil { - return fmt.Errorf("GetPatch: %v", err) - } - - if err = pr.BaseRepo.SavePatch(pr.Index, patch); err != nil { - return fmt.Errorf("BaseRepo.SavePatch: %v", err) - } - - return nil -} - // PushToBaseRepo pushes commits from branches of head repository to // corresponding branches of base repository. // FIXME: Only push branches that are actually updates? diff --git a/models/repo.go b/models/repo.go index 2fd4df92060e0..c9808132fb1e3 100644 --- a/models/repo.go +++ b/models/repo.go @@ -496,6 +496,11 @@ func (repo *Repository) GetUnit(tp UnitType) (*RepoUnit, error) { return repo.getUnit(x, tp) } +// GetRepoUnit returns a RepoUnit object +func GetRepoUnit(ctx DBContext, repo *Repository, tp UnitType) (*RepoUnit, error) { + return repo.getUnit(ctx.e, tp) +} + func (repo *Repository) getUnit(e Engine, tp UnitType) (*RepoUnit, error) { if err := repo.getUnits(e); err != nil { return nil, err @@ -888,8 +893,8 @@ func (repo *Repository) DescriptionHTML() template.HTML { } // PatchPath returns corresponding patch file path of repository by given issue ID. -func (repo *Repository) PatchPath(index int64) (string, error) { - return repo.patchPath(x, index) +func (repo *Repository) PatchPath(ctx DBContext, index int64) (string, error) { + return repo.patchPath(ctx.e, index) } func (repo *Repository) patchPath(e Engine, index int64) (string, error) { @@ -900,29 +905,6 @@ func (repo *Repository) patchPath(e Engine, index int64) (string, error) { return filepath.Join(RepoPath(repo.Owner.Name, repo.Name), "pulls", com.ToStr(index)+".patch"), nil } -// SavePatch saves patch data to corresponding location by given issue ID. -func (repo *Repository) SavePatch(index int64, patch []byte) error { - return repo.savePatch(x, index, patch) -} - -func (repo *Repository) savePatch(e Engine, index int64, patch []byte) error { - patchPath, err := repo.patchPath(e, index) - if err != nil { - return fmt.Errorf("PatchPath: %v", err) - } - dir := filepath.Dir(patchPath) - - if err := os.MkdirAll(dir, os.ModePerm); err != nil { - return fmt.Errorf("Failed to create dir %s: %v", dir, err) - } - - if err = ioutil.WriteFile(patchPath, patch, 0644); err != nil { - return fmt.Errorf("WriteFile: %v", err) - } - - return nil -} - func isRepositoryExist(e Engine, u *User, repoName string) (bool, error) { has, err := e.Get(&Repository{ OwnerID: u.ID, diff --git a/routers/repo/issue.go b/routers/repo/issue.go index c79ea02e86f37..af18c75d4f6e8 100644 --- a/routers/repo/issue.go +++ b/routers/repo/issue.go @@ -1282,7 +1282,7 @@ func NewComment(ctx *context.Context, form auth.CreateCommentForm) { // Regenerate patch and test conflict. if pr == nil { - if err = issue.PullRequest.UpdatePatch(); err != nil { + if err = pull_service.UpdatePatch(issue.PullRequest); err != nil { ctx.ServerError("UpdatePatch", err) return } diff --git a/routers/repo/pull.go b/routers/repo/pull.go index 78406de8acdc8..a746ea124d684 100644 --- a/routers/repo/pull.go +++ b/routers/repo/pull.go @@ -1008,7 +1008,7 @@ func DownloadPullDiff(ctx *context.Context) { ctx.ServerError("GetBaseRepo", err) return } - patch, err := pr.BaseRepo.PatchPath(pr.Index) + patch, err := pull_service.PatchPath(pr.BaseRepo, pr.Index) if err != nil { ctx.ServerError("PatchPath", err) return diff --git a/services/issue/issue.go b/services/issue/issue.go index aa06ba409730f..fc748af025fe3 100644 --- a/services/issue/issue.go +++ b/services/issue/issue.go @@ -5,14 +5,30 @@ package issue import ( + "fmt" + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/notification" ) +const issueMaxDupIndexAttempts = 3 + // NewIssue creates new issue with labels for repository. -func NewIssue(repo *models.Repository, issue *models.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64) error { - if err := models.NewIssue(repo, issue, labelIDs, uuids); err != nil { - return err +func NewIssue(repo *models.Repository, issue *models.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64) (err error) { + // Retry several times in case INSERT fails due to duplicate key for (repo_id, index); see #7887 + i := 0 + for ; i < issueMaxDupIndexAttempts; i++ { + if err = newIssueAttempt(repo, issue, labelIDs, uuids); err == nil { + break + } + if !models.IsErrNewIssueInsert(err) { + return err + } + log.Error("NewIssue: error attempting to insert the new issue; will retry. Original error: %v", err) + } + if i >= issueMaxDupIndexAttempts { + return fmt.Errorf("NewIssue: too many errors attempting to insert the new issue. Last error was: %v", err) } for _, assigneeID := range assigneeIDs { @@ -26,6 +42,27 @@ func NewIssue(repo *models.Repository, issue *models.Issue, labelIDs []int64, uu return nil } +func newIssueAttempt(repo *models.Repository, issue *models.Issue, labelIDs []int64, uuids []string) (err error) { + ctx, commiter, err := models.TxDBContext() + if err != nil { + return err + } + defer commiter.Close() + + if err = models.NewIssue(ctx, repo, issue, labelIDs, uuids); err != nil { + if models.IsErrUserDoesNotHaveAccessToRepo(err) || models.IsErrNewIssueInsert(err) { + return err + } + return fmt.Errorf("newIssue: %v", err) + } + + if err = commiter.Commit(); err != nil { + return fmt.Errorf("Commit: %v", err) + } + + return nil +} + // ChangeTitle changes the title of this issue, as the given user. func ChangeTitle(issue *models.Issue, doer *models.User, title string) (err error) { oldTitle := issue.Title diff --git a/services/pull/check.go b/services/pull/check.go index 0fd3e2a76f47b..d13b028cebad5 100644 --- a/services/pull/check.go +++ b/services/pull/check.go @@ -6,6 +6,7 @@ package pull import ( + "bufio" "fmt" "io/ioutil" "os" @@ -149,6 +150,111 @@ func manuallyMerged(pr *models.PullRequest) bool { return false } +// patchConflicts is a list of conflict description from Git. +var patchConflicts = []string{ + "patch does not apply", + "already exists in working directory", + "unrecognized input", + "error:", +} + +// testPatch checks if patch can be merged to base repository without conflict. +func testPatch(pr *models.PullRequest, ctx models.DBContext) (err error) { + if err := pr.LoadBaseRepo(ctx); err != nil { + return fmt.Errorf("LoadBaseRepo: %v", err) + } + + patchPath, err := pr.BaseRepo.PatchPath(ctx, pr.Index) + if err != nil { + return fmt.Errorf("BaseRepo.PatchPath: %v", err) + } + + // Fast fail if patch does not exist, this assumes data is corrupted. + if !com.IsFile(patchPath) { + log.Trace("PullRequest[%d].testPatch: ignored corrupted data", pr.ID) + return nil + } + + models.RepoWorkingPool.CheckIn(com.ToStr(pr.BaseRepoID)) + defer models.RepoWorkingPool.CheckOut(com.ToStr(pr.BaseRepoID)) + + log.Trace("PullRequest[%d].testPatch (patchPath): %s", pr.ID, patchPath) + + pr.Status = models.PullRequestStatusChecking + + indexTmpPath := filepath.Join(os.TempDir(), "gitea-"+pr.BaseRepo.Name+"-"+strconv.Itoa(time.Now().Nanosecond())) + defer os.Remove(indexTmpPath) + + _, err = git.NewCommand("read-tree", pr.BaseBranch).RunInDirWithEnv("", []string{"GIT_DIR=" + pr.BaseRepo.RepoPath(), "GIT_INDEX_FILE=" + indexTmpPath}) + if err != nil { + return fmt.Errorf("git read-tree --index-output=%s %s: %v", indexTmpPath, pr.BaseBranch, err) + } + + prUnit, err := models.GetRepoUnit(ctx, pr.BaseRepo, models.UnitTypePullRequests) + if err != nil { + return err + } + prConfig := prUnit.PullRequestsConfig() + + args := []string{"apply", "--check", "--cached"} + if prConfig.IgnoreWhitespaceConflicts { + args = append(args, "--ignore-whitespace") + } + args = append(args, patchPath) + pr.ConflictedFiles = []string{} + + stderrBuilder := new(strings.Builder) + err = git.NewCommand(args...).RunInDirTimeoutEnvPipeline( + []string{"GIT_INDEX_FILE=" + indexTmpPath, "GIT_DIR=" + pr.BaseRepo.RepoPath()}, + -1, + "", + nil, + stderrBuilder) + stderr := stderrBuilder.String() + + if err != nil { + for i := range patchConflicts { + if strings.Contains(stderr, patchConflicts[i]) { + log.Trace("PullRequest[%d].testPatch (apply): has conflict: %s", pr.ID, stderr) + const prefix = "error: patch failed:" + pr.Status = models.PullRequestStatusConflict + pr.ConflictedFiles = make([]string, 0, 5) + scanner := bufio.NewScanner(strings.NewReader(stderr)) + for scanner.Scan() { + line := scanner.Text() + + if strings.HasPrefix(line, prefix) { + var found bool + var filepath = strings.TrimSpace(strings.Split(line[len(prefix):], ":")[0]) + for _, f := range pr.ConflictedFiles { + if f == filepath { + found = true + break + } + } + if !found { + pr.ConflictedFiles = append(pr.ConflictedFiles, filepath) + } + } + // only list 10 conflicted files + if len(pr.ConflictedFiles) >= 10 { + break + } + } + + if len(pr.ConflictedFiles) > 0 { + log.Trace("Found %d files conflicted: %v", len(pr.ConflictedFiles), pr.ConflictedFiles) + } + + return nil + } + } + + return fmt.Errorf("git apply --check: %v - %s", err, stderr) + } + return nil +} + // TestPullRequests checks and tests untested patches of pull requests. // TODO: test more pull requests at same time. func TestPullRequests() { @@ -170,7 +276,7 @@ func TestPullRequests() { if manuallyMerged(pr) { continue } - if err := pr.TestPatch(); err != nil { + if err := testPatch(pr, models.DefaultDBContext()); err != nil { log.Error("testPatch: %v", err) continue } @@ -194,7 +300,7 @@ func TestPullRequests() { continue } else if manuallyMerged(pr) { continue - } else if err = pr.TestPatch(); err != nil { + } else if err = testPatch(pr, models.DefaultDBContext()); err != nil { log.Error("testPatch[%d]: %v", pr.ID, err) continue } diff --git a/services/pull/commit_status.go b/services/pull/commit_status.go index ca00cdaad9bc4..c6962b0785dd2 100644 --- a/services/pull/commit_status.go +++ b/services/pull/commit_status.go @@ -66,7 +66,7 @@ func IsPullCommitStatusPass(pr *models.PullRequest) (bool, error) { return false, errors.Wrap(err, "GetBranchCommitID") } - if err := pr.LoadBaseRepo(); err != nil { + if err := pr.LoadBaseRepo(models.DefaultDBContext()); err != nil { return false, errors.Wrap(err, "LoadBaseRepo") } diff --git a/services/pull/patch.go b/services/pull/patch.go new file mode 100644 index 0000000000000..26ba70234e282 --- /dev/null +++ b/services/pull/patch.go @@ -0,0 +1,96 @@ +// 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 pull + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "time" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + + "github.com/unknwon/com" +) + +// PatchPath returns corresponding patch file path of repository by given issue index. +func PatchPath(repo *models.Repository, index int64) (string, error) { + return repo.PatchPath(models.DefaultDBContext(), index) +} + +// SavePatch saves patch data to corresponding location by given issue ID. +func SavePatch(repo *models.Repository, index int64, patch []byte) error { + return savePatch(models.DefaultDBContext(), repo, index, patch) +} + +// savePatch saves patch data to corresponding location by given issue ID. +func savePatch(ctx models.DBContext, repo *models.Repository, index int64, patch []byte) error { + patchPath, err := repo.PatchPath(ctx, index) + if err != nil { + return fmt.Errorf("PatchPath: %v", err) + } + dir := filepath.Dir(patchPath) + + if err := os.MkdirAll(dir, os.ModePerm); err != nil { + return fmt.Errorf("Failed to create dir %s: %v", dir, err) + } + + if err = ioutil.WriteFile(patchPath, patch, 0644); err != nil { + return fmt.Errorf("WriteFile: %v", err) + } + + return nil +} + +// UpdatePatch generates and saves a new patch. +func UpdatePatch(pr *models.PullRequest) (err error) { + if err = pr.GetHeadRepo(); err != nil { + return fmt.Errorf("GetHeadRepo: %v", err) + } else if pr.HeadRepo == nil { + log.Trace("PullRequest[%d].UpdatePatch: ignored corrupted data", pr.ID) + return nil + } + + if err = pr.GetBaseRepo(); err != nil { + return fmt.Errorf("GetBaseRepo: %v", err) + } + + headGitRepo, err := git.OpenRepository(pr.HeadRepo.RepoPath()) + if err != nil { + return fmt.Errorf("OpenRepository: %v", err) + } + defer headGitRepo.Close() + + // Add a temporary remote. + tmpRemote := com.ToStr(time.Now().UnixNano()) + if err = headGitRepo.AddRemote(tmpRemote, models.RepoPath(pr.BaseRepo.MustOwner().Name, pr.BaseRepo.Name), true); err != nil { + return fmt.Errorf("AddRemote: %v", err) + } + defer func() { + if err := headGitRepo.RemoveRemote(tmpRemote); err != nil { + log.Error("UpdatePatch: RemoveRemote: %s", err) + } + }() + pr.MergeBase, _, err = headGitRepo.GetMergeBase(tmpRemote, pr.BaseBranch, pr.HeadBranch) + if err != nil { + return fmt.Errorf("GetMergeBase: %v", err) + } else if err = pr.Update(); err != nil { + return fmt.Errorf("Update: %v", err) + } + + patch, err := headGitRepo.GetPatch(pr.MergeBase, pr.HeadBranch) + if err != nil { + return fmt.Errorf("GetPatch: %v", err) + } + + if err = SavePatch(pr.BaseRepo, pr.Index, patch); err != nil { + return fmt.Errorf("BaseRepo.SavePatch: %v", err) + } + + return nil +} diff --git a/services/pull/pull.go b/services/pull/pull.go index 2650dacc116da..5d555ba5c2b9e 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -14,9 +14,73 @@ import ( issue_service "code.gitea.io/gitea/services/issue" ) +const issueMaxDupIndexAttempts = 3 + +// newPullRequest creates new pull request with labels for repository. +func newPullRequest(repo *models.Repository, pull *models.Issue, labelIDs []int64, uuids []string, pr *models.PullRequest, patch []byte) (err error) { + // Retry several times in case INSERT fails due to duplicate key for (repo_id, index); see #7887 + i := 0 + for { + if err = newPullRequestAttempt(repo, pull, labelIDs, uuids, pr, patch); err == nil { + return nil + } + if !models.IsErrNewIssueInsert(err) { + return err + } + if i++; i == issueMaxDupIndexAttempts { + break + } + log.Error("NewPullRequest: error attempting to insert the new issue; will retry. Original error: %v", err) + } + return fmt.Errorf("NewPullRequest: too many errors attempting to insert the new issue. Last error was: %v", err) +} + +func newPullRequestAttempt(repo *models.Repository, pull *models.Issue, labelIDs []int64, uuids []string, pr *models.PullRequest, patch []byte) (err error) { + ctx, commiter, err := models.TxDBContext() + if err != nil { + return err + } + defer commiter.Close() + + if err = models.NewIssue(ctx, repo, pull, labelIDs, uuids); err != nil { + if models.IsErrUserDoesNotHaveAccessToRepo(err) || models.IsErrNewIssueInsert(err) { + return err + } + return fmt.Errorf("newIssue: %v", err) + } + + pr.Index = pull.Index + pr.BaseRepo = repo + pr.Status = models.PullRequestStatusChecking + if len(patch) > 0 { + if err = savePatch(ctx, repo, pr.Index, patch); err != nil { + return fmt.Errorf("SavePatch: %v", err) + } + + if err = testPatch(pr, ctx); err != nil { + return fmt.Errorf("testPatch: %v", err) + } + } + // No conflict appears after test means mergeable. + if pr.Status == models.PullRequestStatusChecking { + pr.Status = models.PullRequestStatusMergeable + } + + pr.IssueID = pull.ID + if err = models.Insert(ctx, pr); err != nil { + return fmt.Errorf("insert pull repo: %v", err) + } + + if err = commiter.Commit(); err != nil { + return fmt.Errorf("Commit: %v", err) + } + + return nil +} + // NewPullRequest creates new pull request with labels for repository. func NewPullRequest(repo *models.Repository, pull *models.Issue, labelIDs []int64, uuids []string, pr *models.PullRequest, patch []byte, assigneeIDs []int64) error { - if err := models.NewPullRequest(repo, pull, labelIDs, uuids, pr, patch); err != nil { + if err := newPullRequest(repo, pull, labelIDs, uuids, pr, patch); err != nil { return err } @@ -56,7 +120,7 @@ func checkForInvalidation(requests models.PullRequestList, repoID int64, doer *m func addHeadRepoTasks(prs []*models.PullRequest) { for _, pr := range prs { log.Trace("addHeadRepoTasks[%d]: composing new test task", pr.ID) - if err := pr.UpdatePatch(); err != nil { + if err := UpdatePatch(pr); err != nil { log.Error("UpdatePatch: %v", err) continue } else if err := pr.PushToBaseRepo(); err != nil {