diff options
Diffstat (limited to 'models')
-rw-r--r-- | models/access.go | 28 | ||||
-rw-r--r-- | models/action.go | 85 | ||||
-rw-r--r-- | models/error.go | 89 | ||||
-rw-r--r-- | models/git_diff.go | 50 | ||||
-rw-r--r-- | models/issue.go | 265 | ||||
-rw-r--r-- | models/migrations/migrations.go | 100 | ||||
-rw-r--r-- | models/models.go | 5 | ||||
-rw-r--r-- | models/publickey.go | 65 | ||||
-rw-r--r-- | models/pull.go | 558 | ||||
-rw-r--r-- | models/release.go | 36 | ||||
-rw-r--r-- | models/repo.go | 161 | ||||
-rw-r--r-- | models/update.go | 65 | ||||
-rw-r--r-- | models/user.go | 49 | ||||
-rw-r--r-- | models/webhook.go | 82 |
14 files changed, 1136 insertions, 502 deletions
diff --git a/models/access.go b/models/access.go index fe8bf2c1..8a7729b7 100644 --- a/models/access.go +++ b/models/access.go @@ -67,9 +67,8 @@ func HasAccess(u *User, repo *Repository, testMode AccessMode) (bool, error) { return hasAccess(x, u, repo, testMode) } -// GetAccessibleRepositories finds all repositories where a user has access to, -// besides he/she owns. -func (u *User) GetAccessibleRepositories() (map[*Repository]AccessMode, error) { +// GetRepositoryAccesses finds all repositories with their access mode where a user has access but does not own. +func (u *User) GetRepositoryAccesses() (map[*Repository]AccessMode, error) { accesses := make([]*Access, 0, 10) if err := x.Find(&accesses, &Access{UserID: u.Id}); err != nil { return nil, err @@ -80,7 +79,7 @@ func (u *User) GetAccessibleRepositories() (map[*Repository]AccessMode, error) { repo, err := GetRepositoryByID(access.RepoID) if err != nil { if IsErrRepoNotExist(err) { - log.Error(4, "%v", err) + log.Error(4, "GetRepositoryByID: %v", err) continue } return nil, err @@ -92,11 +91,28 @@ func (u *User) GetAccessibleRepositories() (map[*Repository]AccessMode, error) { } repos[repo] = access.Mode } - - // FIXME: should we generate an ordered list here? Random looks weird. return repos, nil } +// GetAccessibleRepositories finds all repositories where a user has access but does not own. +func (u *User) GetAccessibleRepositories() ([]*Repository, error) { + accesses := make([]*Access, 0, 10) + if err := x.Find(&accesses, &Access{UserID: u.Id}); err != nil { + return nil, err + } + + if len(accesses) == 0 { + return []*Repository{}, nil + } + + repoIDs := make([]int64, 0, len(accesses)) + for _, access := range accesses { + repoIDs = append(repoIDs, access.RepoID) + } + repos := make([]*Repository, 0, len(repoIDs)) + return repos, x.Where("owner_id != ?", u.Id).In("id", repoIDs).Desc("updated").Find(&repos) +} + func maxAccessMode(modes ...AccessMode) AccessMode { max := ACCESS_MODE_NONE for _, mode := range modes { diff --git a/models/action.go b/models/action.go index e38cf593..8dd80074 100644 --- a/models/action.go +++ b/models/action.go @@ -14,6 +14,7 @@ import ( "time" "unicode" + "github.com/Unknwon/com" "github.com/go-xorm/xorm" api "github.com/gogits/go-gogs-client" @@ -136,6 +137,26 @@ func (a Action) GetIssueInfos() []string { return strings.SplitN(a.Content, "|", 2) } +func (a Action) GetIssueTitle() string { + index := com.StrTo(a.GetIssueInfos()[0]).MustInt64() + issue, err := GetIssueByIndex(a.RepoID, index) + if err != nil { + log.Error(4, "GetIssueByIndex: %v", err) + return "500 when get issue" + } + return issue.Name +} + +func (a Action) GetIssueContent() string { + index := com.StrTo(a.GetIssueInfos()[0]).MustInt64() + issue, err := GetIssueByIndex(a.RepoID, index) + if err != nil { + log.Error(4, "GetIssueByIndex: %v", err) + return "500 when get issue" + } + return issue.Content +} + func newRepoAction(e Engine, u *User, repo *Repository) (err error) { if err = notifyWatchers(e, &Action{ ActUserID: u.Id, @@ -147,7 +168,7 @@ func newRepoAction(e Engine, u *User, repo *Repository) (err error) { RepoName: repo.Name, IsPrivate: repo.IsPrivate, }); err != nil { - return fmt.Errorf("notify watchers '%d/%s': %v", u.Id, repo.ID, err) + return fmt.Errorf("notify watchers '%d/%d': %v", u.Id, repo.ID, err) } log.Trace("action.newRepoAction: %s/%s", u.Name, repo.Name) @@ -187,8 +208,48 @@ func issueIndexTrimRight(c rune) bool { return !unicode.IsDigit(c) } +type PushCommit struct { + Sha1 string + Message string + AuthorEmail string + AuthorName string +} + +type PushCommits struct { + Len int + Commits []*PushCommit + CompareUrl string + + avatars map[string]string +} + +func NewPushCommits() *PushCommits { + return &PushCommits{ + avatars: make(map[string]string), + } +} + +// AvatarLink tries to match user in database with e-mail +// in order to show custom avatar, and falls back to general avatar link. +func (push *PushCommits) AvatarLink(email string) string { + _, ok := push.avatars[email] + if !ok { + u, err := GetUserByEmail(email) + if err != nil { + push.avatars[email] = base.AvatarLink(email) + if !IsErrUserNotExist(err) { + log.Error(4, "GetUserByEmail: %v", err) + } + } else { + push.avatars[email] = u.AvatarLink() + } + } + + return push.avatars[email] +} + // updateIssuesCommit checks if issues are manipulated by commit message. -func updateIssuesCommit(u *User, repo *Repository, repoUserName, repoName string, commits []*base.PushCommit) error { +func updateIssuesCommit(u *User, repo *Repository, repoUserName, repoName string, commits []*PushCommit) error { // Commits are appended in the reverse order. for i := len(commits) - 1; i >= 0; i-- { c := commits[i] @@ -322,7 +383,7 @@ func CommitRepoAction( repoID int64, repoUserName, repoName string, refFullName string, - commit *base.PushCommits, + commit *PushCommits, oldCommitID string, newCommitID string) error { u, err := GetUserByID(userID) @@ -337,12 +398,18 @@ func CommitRepoAction( return fmt.Errorf("GetOwner: %v", err) } + // Change repository bare status and update last updated time. + repo.IsBare = false + if err = UpdateRepository(repo, false); err != nil { + return fmt.Errorf("UpdateRepository: %v", err) + } + isNewBranch := false opType := COMMIT_REPO // Check it's tag push or branch. if strings.HasPrefix(refFullName, "refs/tags/") { opType = PUSH_TAG - commit = &base.PushCommits{} + commit = &PushCommits{} } else { // if not the first commit, set the compareUrl if !strings.HasPrefix(oldCommitID, "0000000") { @@ -351,12 +418,10 @@ func CommitRepoAction( isNewBranch = true } - // Change repository bare status and update last updated time. - repo.IsBare = false - if err = UpdateRepository(repo, false); err != nil { - return fmt.Errorf("UpdateRepository: %v", err) + // NOTE: limit to detect latest 100 commits. + if len(commit.Commits) > 100 { + commit.Commits = commit.Commits[len(commit.Commits)-100:] } - if err = updateIssuesCommit(u, repo, repoUserName, repoName, commit.Commits); err != nil { log.Error(4, "updateIssuesCommit: %v", err) } @@ -488,7 +553,7 @@ func transferRepoAction(e Engine, actUser, oldOwner, newOwner *User, repo *Repos IsPrivate: repo.IsPrivate, Content: path.Join(oldOwner.LowerName, repo.LowerName), }); err != nil { - return fmt.Errorf("notify watchers '%d/%s': %v", actUser.Id, repo.ID, err) + return fmt.Errorf("notify watchers '%d/%d': %v", actUser.Id, repo.ID, err) } // Remove watch for organization. diff --git a/models/error.go b/models/error.go index 1986c590..f8fea6f5 100644 --- a/models/error.go +++ b/models/error.go @@ -18,7 +18,7 @@ func IsErrNameReserved(err error) bool { } func (err ErrNameReserved) Error() string { - return fmt.Sprintf("name is reserved: [name: %s]", err.Name) + return fmt.Sprintf("name is reserved [name: %s]", err.Name) } type ErrNamePatternNotAllowed struct { @@ -31,7 +31,7 @@ func IsErrNamePatternNotAllowed(err error) bool { } func (err ErrNamePatternNotAllowed) Error() string { - return fmt.Sprintf("name pattern is not allowed: [pattern: %s]", err.Pattern) + return fmt.Sprintf("name pattern is not allowed [pattern: %s]", err.Pattern) } // ____ ___ @@ -51,7 +51,7 @@ func IsErrUserAlreadyExist(err error) bool { } func (err ErrUserAlreadyExist) Error() string { - return fmt.Sprintf("user already exists: [name: %s]", err.Name) + return fmt.Sprintf("user already exists [name: %s]", err.Name) } type ErrUserNotExist struct { @@ -65,7 +65,7 @@ func IsErrUserNotExist(err error) bool { } func (err ErrUserNotExist) Error() string { - return fmt.Sprintf("user does not exist: [uid: %d, name: %s]", err.UID, err.Name) + return fmt.Sprintf("user does not exist [uid: %d, name: %s]", err.UID, err.Name) } type ErrEmailAlreadyUsed struct { @@ -78,7 +78,7 @@ func IsErrEmailAlreadyUsed(err error) bool { } func (err ErrEmailAlreadyUsed) Error() string { - return fmt.Sprintf("e-mail has been used: [email: %s]", err.Email) + return fmt.Sprintf("e-mail has been used [email: %s]", err.Email) } type ErrUserOwnRepos struct { @@ -91,7 +91,7 @@ func IsErrUserOwnRepos(err error) bool { } func (err ErrUserOwnRepos) Error() string { - return fmt.Sprintf("user still has ownership of repositories: [uid: %d]", err.UID) + return fmt.Sprintf("user still has ownership of repositories [uid: %d]", err.UID) } type ErrUserHasOrgs struct { @@ -104,7 +104,7 @@ func IsErrUserHasOrgs(err error) bool { } func (err ErrUserHasOrgs) Error() string { - return fmt.Sprintf("user still has membership of organizations: [uid: %d]", err.UID) + return fmt.Sprintf("user still has membership of organizations [uid: %d]", err.UID) } // __________ ___. .__ .__ ____ __. @@ -124,7 +124,7 @@ func IsErrKeyNotExist(err error) bool { } func (err ErrKeyNotExist) Error() string { - return fmt.Sprintf("public key does not exist: [id: %d]", err.ID) + return fmt.Sprintf("public key does not exist [id: %d]", err.ID) } type ErrKeyAlreadyExist struct { @@ -138,7 +138,7 @@ func IsErrKeyAlreadyExist(err error) bool { } func (err ErrKeyAlreadyExist) Error() string { - return fmt.Sprintf("public key already exists: [owner_id: %d, content: %s]", err.OwnerID, err.Content) + return fmt.Sprintf("public key already exists [owner_id: %d, content: %s]", err.OwnerID, err.Content) } type ErrKeyNameAlreadyUsed struct { @@ -152,7 +152,7 @@ func IsErrKeyNameAlreadyUsed(err error) bool { } func (err ErrKeyNameAlreadyUsed) Error() string { - return fmt.Sprintf("public key already exists: [owner_id: %d, name: %s]", err.OwnerID, err.Name) + return fmt.Sprintf("public key already exists [owner_id: %d, name: %s]", err.OwnerID, err.Name) } type ErrDeployKeyAlreadyExist struct { @@ -166,7 +166,7 @@ func IsErrDeployKeyAlreadyExist(err error) bool { } func (err ErrDeployKeyAlreadyExist) Error() string { - return fmt.Sprintf("public key already exists: [key_id: %d, repo_id: %d]", err.KeyID, err.RepoID) + return fmt.Sprintf("public key already exists [key_id: %d, repo_id: %d]", err.KeyID, err.RepoID) } type ErrDeployKeyNameAlreadyUsed struct { @@ -180,7 +180,7 @@ func IsErrDeployKeyNameAlreadyUsed(err error) bool { } func (err ErrDeployKeyNameAlreadyUsed) Error() string { - return fmt.Sprintf("public key already exists: [repo_id: %d, name: %s]", err.RepoID, err.Name) + return fmt.Sprintf("public key already exists [repo_id: %d, name: %s]", err.RepoID, err.Name) } // _____ ___________ __ @@ -200,7 +200,7 @@ func IsErrAccessTokenNotExist(err error) bool { } func (err ErrAccessTokenNotExist) Error() string { - return fmt.Sprintf("access token does not exist: [sha: %s]", err.SHA) + return fmt.Sprintf("access token does not exist [sha: %s]", err.SHA) } // ________ .__ __ .__ @@ -220,7 +220,7 @@ func IsErrLastOrgOwner(err error) bool { } func (err ErrLastOrgOwner) Error() string { - return fmt.Sprintf("user is the last member of owner team: [uid: %d]", err.UID) + return fmt.Sprintf("user is the last member of owner team [uid: %d]", err.UID) } // __________ .__ __ @@ -259,6 +259,61 @@ func (err ErrRepoAlreadyExist) Error() string { return fmt.Sprintf("repository already exists [uname: %s, name: %s]", err.Uname, err.Name) } +type ErrInvalidCloneAddr struct { + IsURLError bool + IsInvalidPath bool + IsPermissionDenied bool +} + +func IsErrInvalidCloneAddr(err error) bool { + _, ok := err.(ErrInvalidCloneAddr) + return ok +} + +func (err ErrInvalidCloneAddr) Error() string { + return fmt.Sprintf("invalid clone address [is_url_error: %v, is_invalid_path: %v, is_permission_denied: %v]", + err.IsURLError, err.IsInvalidPath, err.IsPermissionDenied) +} + +type ErrUpdateTaskNotExist struct { + UUID string +} + +func IsErrUpdateTaskNotExist(err error) bool { + _, ok := err.(ErrUpdateTaskNotExist) + return ok +} + +func (err ErrUpdateTaskNotExist) Error() string { + return fmt.Sprintf("update task does not exist [uuid: %s]", err.UUID) +} + +type ErrReleaseAlreadyExist struct { + TagName string +} + +func IsErrReleaseAlreadyExist(err error) bool { + _, ok := err.(ErrReleaseAlreadyExist) + return ok +} + +func (err ErrReleaseAlreadyExist) Error() string { + return fmt.Sprintf("Release tag already exist [tag_name: %s]", err.TagName) +} + +type ErrReleaseNotExist struct { + TagName string +} + +func IsErrReleaseNotExist(err error) bool { + _, ok := err.(ErrReleaseNotExist) + return ok +} + +func (err ErrReleaseNotExist) Error() string { + return fmt.Sprintf("Release tag does not exist [tag_name: %s]", err.TagName) +} + // __ __ ___. .__ __ // / \ / \ ____\_ |__ | |__ ____ ____ | | __ // \ \/\/ // __ \| __ \| | \ / _ \ / _ \| |/ / @@ -310,7 +365,7 @@ func (err ErrIssueNotExist) Error() string { type ErrPullRequestNotExist struct { ID int64 - PullID int64 + IssueID int64 HeadRepoID int64 BaseRepoID int64 HeadBarcnh string @@ -323,8 +378,8 @@ func IsErrPullRequestNotExist(err error) bool { } func (err ErrPullRequestNotExist) Error() string { - return fmt.Sprintf("pull request does not exist [id: %d, pull_id: %d, head_repo_id: %d, base_repo_id: %d, head_branch: %s, base_branch: %s]", - err.ID, err.PullID, err.HeadRepoID, err.BaseRepoID, err.HeadBarcnh, err.BaseBranch) + return fmt.Sprintf("pull request does not exist [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.HeadBarcnh, err.BaseBranch) } // _________ __ diff --git a/models/git_diff.go b/models/git_diff.go index 66985650..4cc9ebf8 100644 --- a/models/git_diff.go +++ b/models/git_diff.go @@ -37,6 +37,7 @@ const ( DIFF_FILE_ADD = iota + 1 DIFF_FILE_CHANGE DIFF_FILE_DEL + DIFF_FILE_RENAME ) type DiffLine struct { @@ -57,12 +58,14 @@ type DiffSection struct { type DiffFile struct { Name string + OldName string Index int Addition, Deletion int Type int IsCreated bool IsDeleted bool IsBin bool + IsRenamed bool Sections []*DiffSection } @@ -86,7 +89,6 @@ func ParsePatch(pid int64, maxlines int, cmd *exec.Cmd, reader io.Reader) (*Diff } leftLine, rightLine int - isTooLong bool // FIXME: Should use cache in the future. buf bytes.Buffer ) @@ -95,7 +97,7 @@ func ParsePatch(pid int64, maxlines int, cmd *exec.Cmd, reader io.Reader) (*Diff var i int for scanner.Scan() { line := scanner.Text() - // fmt.Println(i, line) + if strings.HasPrefix(line, "+++ ") || strings.HasPrefix(line, "--- ") { continue } @@ -107,9 +109,10 @@ func ParsePatch(pid int64, maxlines int, cmd *exec.Cmd, reader io.Reader) (*Diff i = i + 1 // Diff data too large, we only show the first about maxlines lines - if i == maxlines { - isTooLong = true + if i >= maxlines { log.Warn("Diff data too large") + diff.Files = nil + return diff, nil } switch { @@ -120,10 +123,6 @@ func ParsePatch(pid int64, maxlines int, cmd *exec.Cmd, reader io.Reader) (*Diff curSection.Lines = append(curSection.Lines, diffLine) continue case line[0] == '@': - if isTooLong { - break - } - curSection = &DiffSection{} curFile.Sections = append(curFile.Sections, curSection) ss := strings.Split(line, "@@") @@ -162,21 +161,27 @@ func ParsePatch(pid int64, maxlines int, cmd *exec.Cmd, reader io.Reader) (*Diff // Get new file. if strings.HasPrefix(line, DIFF_HEAD) { - if isTooLong { - break + middle := -1 + + // Note: In case file name is surrounded by double quotes(it happens only in git-shell). + hasQuote := strings.Index(line, `\"`) > -1 + if hasQuote { + line = strings.Replace(line, `\"`, `"`, -1) + middle = strings.Index(line, ` "b/`) + } else { + middle = strings.Index(line, " b/") } beg := len(DIFF_HEAD) - a := line[beg : (len(line)-beg)/2+beg] - - // In case file name is surrounded by double quotes(it happens only in git-shell). - if a[0] == '"' { + a := line[beg+2 : middle] + b := line[middle+3:] + if hasQuote { a = a[1 : len(a)-1] - a = strings.Replace(a, `\"`, `"`, -1) + b = b[1 : len(b)-1] } curFile = &DiffFile{ - Name: a[strings.Index(a, "/")+1:], + Name: a, Index: len(diff.Files) + 1, Type: DIFF_FILE_CHANGE, Sections: make([]*DiffSection, 0, 10), @@ -188,16 +193,17 @@ func ParsePatch(pid int64, maxlines int, cmd *exec.Cmd, reader io.Reader) (*Diff switch { case strings.HasPrefix(scanner.Text(), "new file"): curFile.Type = DIFF_FILE_ADD - curFile.IsDeleted = false curFile.IsCreated = true case strings.HasPrefix(scanner.Text(), "deleted"): curFile.Type = DIFF_FILE_DEL - curFile.IsCreated = false curFile.IsDeleted = true case strings.HasPrefix(scanner.Text(), "index"): curFile.Type = DIFF_FILE_CHANGE - curFile.IsCreated = false - curFile.IsDeleted = false + case strings.HasPrefix(scanner.Text(), "similarity index 100%"): + curFile.Type = DIFF_FILE_RENAME + curFile.IsRenamed = true + curFile.OldName = curFile.Name + curFile.Name = b } if curFile.Type > 0 { break @@ -252,10 +258,10 @@ func GetDiffRange(repoPath, beforeCommitId string, afterCommitId string, maxline cmd = exec.Command("git", "show", afterCommitId) } else { c, _ := commit.Parent(0) - cmd = exec.Command("git", "diff", c.Id.String(), afterCommitId) + cmd = exec.Command("git", "diff", "-M", c.ID.String(), afterCommitId) } } else { - cmd = exec.Command("git", "diff", beforeCommitId, afterCommitId) + cmd = exec.Command("git", "diff", "-M", beforeCommitId, afterCommitId) } cmd.Dir = repoPath cmd.Stdout = wr diff --git a/models/issue.go b/models/issue.go index 0dd0b663..077e945c 100644 --- a/models/issue.go +++ b/models/issue.go @@ -9,7 +9,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "mime/multipart" "os" "path" @@ -20,9 +19,7 @@ import ( "github.com/go-xorm/xorm" "github.com/gogits/gogs/modules/base" - "github.com/gogits/gogs/modules/git" "github.com/gogits/gogs/modules/log" - "github.com/gogits/gogs/modules/process" "github.com/gogits/gogs/modules/setting" gouuid "github.com/gogits/gogs/modules/uuid" ) @@ -95,15 +92,6 @@ func (i *Issue) AfterSet(colName string, _ xorm.Cell) { if err != nil { log.Error(3, "GetUserByID[%d]: %v", i.ID, err) } - case "is_pull": - if !i.IsPull { - return - } - - i.PullRequest, err = GetPullRequestByPullID(i.ID) - if err != nil { - log.Error(3, "GetPullRequestByPullID[%d]: %v", i.ID, err) - } case "created": i.Created = regulateTimeZone(i.Created) } @@ -236,7 +224,7 @@ func (i *Issue) changeStatus(e *xorm.Session, doer *User, isClosed bool) (err er } i.IsClosed = isClosed - if err = updateIssue(e, i); err != nil { + if err = updateIssueCols(e, i, "is_closed"); err != nil { return err } else if err = updateIssueUsersByStatus(e, i.ID, isClosed); err != nil { return err @@ -285,6 +273,15 @@ func (i *Issue) ChangeStatus(doer *User, isClosed bool) (err error) { return sess.Commit() } +func (i *Issue) GetPullRequest() (err error) { + if i.PullRequest != nil { + return nil + } + + i.PullRequest, err = GetPullRequestByIssueID(i.ID) + return err +} + // It's caller's responsibility to create action. func newIssue(e *xorm.Session, repo *Repository, issue *Issue, labelIDs []int64, uuids []string, isPull bool) (err error) { if _, err = e.Insert(issue); err != nil { @@ -821,11 +818,17 @@ func updateIssue(e Engine, issue *Issue) error { return err } -// UpdateIssue updates information of issue. +// UpdateIssue updates all fields of given issue. func UpdateIssue(issue *Issue) error { return updateIssue(x, issue) } +// updateIssueCols updates specific fields of given issue. +func updateIssueCols(e Engine, issue *Issue, cols ...string) error { + _, err := e.Id(issue.ID).Cols(cols...).Update(issue) + return err +} + func updateIssueUsersByStatus(e Engine, issueID int64, isClosed bool) error { _, err := e.Exec("UPDATE `issue_user` SET is_closed=? WHERE issue_id=?", isClosed, issueID) return err @@ -894,240 +897,6 @@ func UpdateIssueUsersByMentions(uids []int64, iid int64) error { return nil } -// __________ .__ .__ __________ __ -// \______ \__ __| | | |\______ \ ____ ________ __ ____ _______/ |_ -// | ___/ | \ | | | | _// __ \/ ____/ | \_/ __ \ / ___/\ __\ -// | | | | / |_| |_| | \ ___< <_| | | /\ ___/ \___ \ | | -// |____| |____/|____/____/____|_ /\___ >__ |____/ \___ >____ > |__| -// \/ \/ |__| \/ \/ - -type PullRequestType int - -const ( - PULL_REQUEST_GOGS = iota - PLLL_ERQUEST_GIT -) - -// PullRequest represents relation between pull request and repositories. -type PullRequest struct { - ID int64 `xorm:"pk autoincr"` - PullID int64 `xorm:"INDEX"` - Pull *Issue `xorm:"-"` - PullIndex int64 - HeadRepoID int64 - HeadRepo *Repository `xorm:"-"` - BaseRepoID int64 - HeadUserName string - HeadBarcnh string - BaseBranch string - MergeBase string `xorm:"VARCHAR(40)"` - MergedCommitID string `xorm:"VARCHAR(40)"` - Type PullRequestType - CanAutoMerge bool - HasMerged bool - Merged time.Time - MergerID int64 - Merger *User `xorm:"-"` -} - -// Note: don't try to get Pull because will end up recursive querying. -func (pr *PullRequest) AfterSet(colName string, _ xorm.Cell) { - var err error - switch colName { - case "head_repo_id": - // FIXME: shouldn't show error if it's known that head repository has been removed. - pr.HeadRepo, err = GetRepositoryByID(pr.HeadRepoID) - if err != nil { - log.Error(3, "GetRepositoryByID[%d]: %v", pr.ID, err) - } - case "merger_id": - if !pr.HasMerged { - return - } - - pr.Merger, err = GetUserByID(pr.MergerID) - if err != nil { - if IsErrUserNotExist(err) { - pr.MergerID = -1 - pr.Merger = NewFakeUser() - } else { - log.Error(3, "GetUserByID[%d]: %v", pr.ID, err) - } - } - case "merged": - if !pr.HasMerged { - return - } - - pr.Merged = regulateTimeZone(pr.Merged) - } -} - -// Merge merges pull request to base repository. -func (pr *PullRequest) Merge(doer *User, baseGitRepo *git.Repository) (err error) { - sess := x.NewSession() - defer sessionRelease(sess) - if err = sess.Begin(); err != nil { - return err - } - - if err = pr.Pull.changeStatus(sess, doer, true); err != nil { - return fmt.Errorf("Pull.changeStatus: %v", err) - } - - headRepoPath := RepoPath(pr.HeadUserName, pr.HeadRepo.Name) - headGitRepo, err := git.OpenRepository(headRepoPath) - if err != nil { - return fmt.Errorf("OpenRepository: %v", err) - } - pr.MergedCommitID, err = headGitRepo.GetCommitIdOfBranch(pr.HeadBarcnh) - if err != nil { - return fmt.Errorf("GetCommitIdOfBranch: %v", err) - } - - if err = mergePullRequestAction(sess, doer, pr.Pull.Repo, pr.Pull); err != nil { - return fmt.Errorf("mergePullRequestAction: %v", err) - } - - pr.HasMerged = true - pr.Merged = time.Now() - pr.MergerID = doer.Id - if _, err = sess.Id(pr.ID).AllCols().Update(pr); err != nil { - return fmt.Errorf("update pull request: %v", err) - } - - // Clone base repo. - tmpBasePath := path.Join("data/tmp/repos", com.ToStr(time.Now().Nanosecond())+".git") - os.MkdirAll(path.Dir(tmpBasePath), os.ModePerm) - defer os.RemoveAll(path.Dir(tmpBasePath)) - - var stderr string - if _, stderr, err = process.ExecTimeout(5*time.Minute, - fmt.Sprintf("PullRequest.Merge(git clone): %s", tmpBasePath), - "git", "clone", baseGitRepo.Path, tmpBasePath); err != nil { - return fmt.Errorf("git clone: %s", stderr) - } - - // Check out base branch. - if _, stderr, err = process.ExecDir(-1, tmpBasePath, - fmt.Sprintf("PullRequest.Merge(git checkout): %s", tmpBasePath), - "git", "checkout", pr.BaseBranch); err != nil { - return fmt.Errorf("git checkout: %s", stderr) - } - - // Pull commits. - if _, stderr, err = process.ExecDir(-1, tmpBasePath, - fmt.Sprintf("PullRequest.Merge(git pull): %s", tmpBasePath), - "git", "pull", headRepoPath, pr.HeadBarcnh); err != nil { - return fmt.Errorf("git pull[%s / %s -> %s]: %s", headRepoPath, pr.HeadBarcnh, tmpBasePath, stderr) - } - - // Push back to upstream. - if _, stderr, err = process.ExecDir(-1, tmpBasePath, - fmt.Sprintf("PullRequest.Merge(git push): %s", tmpBasePath), - "git", "push", baseGitRepo.Path, pr.BaseBranch); err != nil { - return fmt.Errorf("git push: %s", stderr) - } - - return sess.Commit() -} - -// 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) { - sess := x.NewSession() - defer sessionRelease(sess) - if err = sess.Begin(); err != nil { - return err - } - - if err = newIssue(sess, repo, pull, labelIDs, uuids, true); err != nil { - return fmt.Errorf("newIssue: %v", err) - } - - // Notify watchers. - act := &Action{ - ActUserID: pull.Poster.Id, - ActUserName: pull.Poster.Name, - ActEmail: pull.Poster.Email, - OpType: CREATE_PULL_REQUEST, - Content: fmt.Sprintf("%d|%s", pull.Index, pull.Name), - RepoID: repo.ID, - RepoUserName: repo.Owner.Name, - RepoName: repo.Name, - IsPrivate: repo.IsPrivate, - } - if err = notifyWatchers(sess, act); err != nil { - return err - } - - // Test apply patch. - if err = repo.UpdateLocalCopy(); err != nil { - return fmt.Errorf("UpdateLocalCopy: %v", err) - } - - repoPath, err := repo.RepoPath() - if err != nil { - return fmt.Errorf("RepoPath: %v", err) - } - patchPath := path.Join(repoPath, "pulls", com.ToStr(pull.ID)+".patch") - - os.MkdirAll(path.Dir(patchPath), os.ModePerm) - if err = ioutil.WriteFile(patchPath, patch, 0644); err != nil { - return fmt.Errorf("save patch: %v", err) - } - - pr.CanAutoMerge = true - _, stderr, err := process.ExecDir(-1, repo.LocalCopyPath(), - fmt.Sprintf("NewPullRequest(git apply --check): %d", repo.ID), - "git", "apply", "--check", patchPath) - if err != nil { - if strings.Contains(stderr, "patch does not apply") { - pr.CanAutoMerge = false - } else { - return fmt.Errorf("git apply --check: %v - %s", err, stderr) - } - } - - pr.PullID = pull.ID - pr.PullIndex = pull.Index - if _, err = sess.Insert(pr); err != nil { - return fmt.Errorf("insert pull repo: %v", err) - } - - return sess.Commit() -} - -// GetUnmergedPullRequest returnss a pull request hasn't been merged by given info. -func GetUnmergedPullRequest(headRepoID, baseRepoID int64, headBranch, baseBranch string) (*PullRequest, error) { - pr := &PullRequest{ - HeadRepoID: headRepoID, - BaseRepoID: baseRepoID, - HeadBarcnh: headBranch, - BaseBranch: baseBranch, - } - - has, err := x.Where("has_merged=?", false).Get(pr) - if err != nil { - return nil, err - } else if !has { - return nil, ErrPullRequestNotExist{0, 0, headRepoID, baseRepoID, headBranch, baseBranch} - } - - return pr, nil -} - -// GetPullRequestByPullID returns pull repo by given pull ID. -func GetPullRequestByPullID(pullID int64) (*PullRequest, error) { - pr := new(PullRequest) - has, err := x.Where("pull_id=?", pullID).Get(pr) - if err != nil { - return nil, err - } else if !has { - return nil, ErrPullRequestNotExist{0, pullID, 0, 0, "", ""} - } - return pr, nil -} - // .____ ___. .__ // | | _____ \_ |__ ____ | | // | | \__ \ | __ \_/ __ \| | diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 08a503e6..0d17cf26 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -11,6 +11,7 @@ import ( "io/ioutil" "os" "path" + "path/filepath" "strings" "time" @@ -65,6 +66,8 @@ var migrations = []Migration{ NewMigration("trim action compare URL prefix", trimCommitActionAppUrlPrefix), // V5 -> V6:v0.6.3 NewMigration("generate issue-label from issue", issueToIssueLabel), // V6 -> V7:v0.6.4 NewMigration("refactor attachment table", attachmentRefactor), // V7 -> V8:v0.6.4 + NewMigration("rename pull request fields", renamePullRequestFields), // V8 -> V9:v0.6.16 + NewMigration("clean up migrate repo info", cleanUpMigrateRepoInfo), // V9 -> V10:v0.6.20 } // Migrate database to current version @@ -453,7 +456,7 @@ func trimCommitActionAppUrlPrefix(x *xorm.Engine) error { pushCommits = new(PushCommits) if err = json.Unmarshal(action["content"], pushCommits); err != nil { - return fmt.Errorf("unmarshal action content[%s]: %v", actID, err) + return fmt.Errorf("unmarshal action content[%d]: %v", actID, err) } infos := strings.Split(pushCommits.CompareUrl, "/") @@ -464,7 +467,7 @@ func trimCommitActionAppUrlPrefix(x *xorm.Engine) error { p, err := json.Marshal(pushCommits) if err != nil { - return fmt.Errorf("marshal action content[%s]: %v", actID, err) + return fmt.Errorf("marshal action content[%d]: %v", actID, err) } if _, err = sess.Id(actID).Update(&Action{ @@ -606,3 +609,96 @@ func attachmentRefactor(x *xorm.Engine) error { return sess.Commit() } + +func renamePullRequestFields(x *xorm.Engine) (err error) { + type PullRequest struct { + ID int64 `xorm:"pk autoincr"` + PullID int64 `xorm:"INDEX"` + PullIndex int64 + HeadBarcnh string + + IssueID int64 `xorm:"INDEX"` + Index int64 + HeadBranch string + } + + if err = x.Sync(new(PullRequest)); err != nil { + return fmt.Errorf("sync: %v", err) + } + + results, err := x.Query("SELECT `id`,`pull_id`,`pull_index`,`head_barcnh` FROM `pull_request`") + if err != nil { + if strings.Contains(err.Error(), "no such column") { + return nil + } + return fmt.Errorf("select pull requests: %v", err) + } + + sess := x.NewSession() + defer sessionRelease(sess) + if err = sess.Begin(); err != nil { + return err + } + + var pull *PullRequest + for _, pr := range results { + pull = &PullRequest{ + ID: com.StrTo(pr["id"]).MustInt64(), + IssueID: com.StrTo(pr["pull_id"]).MustInt64(), + Index: com.StrTo(pr["pull_index"]).MustInt64(), + HeadBranch: string(pr["head_barcnh"]), + } + if _, err = sess.Id(pull.ID).Update(pull); err != nil { + return err + } + } + + return sess.Commit() +} + +func cleanUpMigrateRepoInfo(x *xorm.Engine) (err error) { + type ( + User struct { + ID int64 `xorm:"pk autoincr"` + LowerName string + } + Repository struct { + ID int64 `xorm:"pk autoincr"` + OwnerID int64 + LowerName string + } + ) + + repos := make([]*Repository, 0, 25) + if err = x.Where("is_mirror=?", false).Find(&repos); err != nil { + return fmt.Errorf("select all non-mirror repositories: %v", err) + } + var user *User + for _, repo := range repos { + user = &User{ID: repo.OwnerID} + has, err := x.Get(user) + if err != nil { + return fmt.Errorf("get owner of repository[%d - %d]: %v", repo.ID, repo.OwnerID, err) + } else if !has { + continue + } + + configPath := filepath.Join(setting.RepoRootPath, user.LowerName, repo.LowerName+".git/config") + + // In case repository file is somehow missing. + if !com.IsFile(configPath) { + continue + } + + cfg, err := ini.Load(configPath) + if err != nil { + return fmt.Errorf("open config file: %v", err) + } + cfg.DeleteSection("remote \"origin\"") + if err = cfg.SaveToIndent(configPath, "\t"); err != nil { + return fmt.Errorf("save config file: %v", err) + } + } + + return nil +} diff --git a/models/models.go b/models/models.go index 2410ecd4..2249fee4 100644 --- a/models/models.go +++ b/models/models.go @@ -52,9 +52,8 @@ func regulateTimeZone(t time.Time) time.Time { } zone := t.Local().Format("-0700") - log.Trace("regulateTimeZone: %s - %s", t.Local(), zone) - if len(zone) != 5 { + log.Error(4, "Unprocessable timezone: %s - %s", t.Local(), zone) return t } hour := com.StrTo(zone[2:3]).MustInt() @@ -91,7 +90,7 @@ func init() { new(Team), new(OrgUser), new(TeamUser), new(TeamRepo), new(Notice), new(EmailAddress)) - gonicNames := []string{"UID", "SSL"} + gonicNames := []string{"SSL"} for _, name := range gonicNames { core.LintGonicMapper[name] = true } diff --git a/models/publickey.go b/models/publickey.go index 6c0ffc0c..0f041c09 100644 --- a/models/publickey.go +++ b/models/publickey.go @@ -13,7 +13,6 @@ import ( "io" "io/ioutil" "os" - "os/exec" "path" "path/filepath" "strings" @@ -38,20 +37,7 @@ var ( ) var sshOpLocker = sync.Mutex{} - -var ( - SSHPath string // SSH directory. - appPath string // Execution(binary) path. -) - -// exePath returns the executable path. -func exePath() (string, error) { - file, err := exec.LookPath(os.Args[0]) - if err != nil { - return "", err - } - return filepath.Abs(file) -} +var SSHPath string // SSH directory. // homeDir returns the home directory of current user. func homeDir() string { @@ -63,16 +49,9 @@ func homeDir() string { } func init() { - var err error - - if appPath, err = exePath(); err != nil { - log.Fatal(4, "fail to get app path: %v\n", err) - } - appPath = strings.Replace(appPath, "\\", "/", -1) - // Determine and create .ssh path. SSHPath = filepath.Join(homeDir(), ".ssh") - if err = os.MkdirAll(SSHPath, 0700); err != nil { + if err := os.MkdirAll(SSHPath, 0700); err != nil { log.Fatal(4, "fail to create '%s': %v", SSHPath, err) } } @@ -114,17 +93,7 @@ func (k *PublicKey) OmitEmail() string { // GetAuthorizedString generates and returns formatted public key string for authorized_keys file. func (key *PublicKey) GetAuthorizedString() string { - return fmt.Sprintf(_TPL_PUBLICK_KEY, appPath, key.ID, setting.CustomConf, key.Content) -} - -var minimumKeySizes = map[string]int{ - "(ED25519)": 256, - "(ECDSA)": 256, - "(NTRU)": 1087, - "(MCE)": 1702, - "(McE)": 1702, - "(RSA)": 1024, - "(DSA)": 1024, + return fmt.Sprintf(_TPL_PUBLICK_KEY, setting.AppPath, key.ID, setting.CustomConf, key.Content) } func extractTypeFromBase64Key(key string) (string, error) { @@ -228,9 +197,9 @@ func CheckPublicKeyString(content string) (_ string, err error) { tmpFile.Close() // Check if ssh-keygen recognizes its contents. - stdout, stderr, err := process.Exec("CheckPublicKeyString", "ssh-keygen", "-l", "-f", tmpPath) + stdout, stderr, err := process.Exec("CheckPublicKeyString", "ssh-keygen", "-lf", tmpPath) if err != nil { - return "", errors.New("ssh-keygen -l -f: " + stderr) + return "", errors.New("ssh-keygen -lf: " + stderr) } else if len(stdout) < 2 { return "", errors.New("ssh-keygen returned not enough output to evaluate the key: " + stdout) } @@ -251,9 +220,10 @@ func CheckPublicKeyString(content string) (_ string, err error) { if keySize == 0 { return "", errors.New("cannot get key size of the given key") } - keyType := strings.TrimSpace(sshKeygenOutput[len(sshKeygenOutput)-1]) - if minimumKeySize := minimumKeySizes[keyType]; minimumKeySize == 0 { - return "", errors.New("sorry, unrecognized public key type") + + keyType := strings.Trim(sshKeygenOutput[len(sshKeygenOutput)-1], " ()\n") + if minimumKeySize := setting.Service.MinimumKeySizes[keyType]; minimumKeySize == 0 { + return "", fmt.Errorf("unrecognized public key type: %s", keyType) } else if keySize < minimumKeySize { return "", fmt.Errorf("the minimum accepted size of a public key %s is %d", keyType, minimumKeySize) } @@ -321,9 +291,9 @@ func addKey(e Engine, key *PublicKey) (err error) { if err = ioutil.WriteFile(tmpPath, []byte(key.Content), 0644); err != nil { return err } - stdout, stderr, err := process.Exec("AddPublicKey", "ssh-keygen", "-l", "-f", tmpPath) + stdout, stderr, err := process.Exec("AddPublicKey", "ssh-keygen", "-lf", tmpPath) if err != nil { - return errors.New("ssh-keygen -l -f: " + stderr) + return errors.New("ssh-keygen -lf: " + stderr) } else if len(stdout) < 2 { return errors.New("not enough output for calculating fingerprint: " + stdout) } @@ -382,6 +352,19 @@ func GetPublicKeyByID(keyID int64) (*PublicKey, error) { return key, nil } +// SearchPublicKeyByContent searches content as prefix (leak e-mail part) +// and returns public key found. +func SearchPublicKeyByContent(content string) (*PublicKey, error) { + key := new(PublicKey) + has, err := x.Where("content like ?", content+"%").Get(key) + if err != nil { + return nil, err + } else if !has { + return nil, ErrKeyNotExist{} + } + return key, nil +} + // ListPublicKeys returns a list of public keys belongs to given user. func ListPublicKeys(uid int64) ([]*PublicKey, error) { keys := make([]*PublicKey, 0, 5) diff --git a/models/pull.go b/models/pull.go new file mode 100644 index 00000000..5c7d4a3f --- /dev/null +++ b/models/pull.go @@ -0,0 +1,558 @@ +// Copyright 2015 The Gogs 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 models + +import ( + "fmt" + "os" + "path" + "strings" + "time" + + "github.com/Unknwon/com" + "github.com/go-xorm/xorm" + + "github.com/gogits/gogs/modules/git" + "github.com/gogits/gogs/modules/log" + "github.com/gogits/gogs/modules/process" + "github.com/gogits/gogs/modules/setting" +) + +type PullRequestType int + +const ( + PULL_REQUEST_GOGS PullRequestType = iota + PLLL_ERQUEST_GIT +) + +type PullRequestStatus int + +const ( + PULL_REQUEST_STATUS_CONFLICT PullRequestStatus = iota + PULL_REQUEST_STATUS_CHECKING + PULL_REQUEST_STATUS_MERGEABLE +) + +// PullRequest represents relation between pull request and repositories. +type PullRequest struct { + ID int64 `xorm:"pk autoincr"` + Type PullRequestType + Status PullRequestStatus + + IssueID int64 `xorm:"INDEX"` + Issue *Issue `xorm:"-"` + Index int64 + + HeadRepoID int64 + HeadRepo *Repository `xorm:"-"` + BaseRepoID int64 + BaseRepo *Repository `xorm:"-"` + HeadUserName string + HeadBranch string + BaseBranch string + MergeBase string `xorm:"VARCHAR(40)"` + + HasMerged bool + MergedCommitID string `xorm:"VARCHAR(40)"` + Merged time.Time + MergerID int64 + Merger *User `xorm:"-"` +} + +// Note: don't try to get Pull because will end up recursive querying. +func (pr *PullRequest) AfterSet(colName string, _ xorm.Cell) { + switch colName { + case "merged": + if !pr.HasMerged { + return + } + + pr.Merged = regulateTimeZone(pr.Merged) + } +} + +func (pr *PullRequest) getHeadRepo(e Engine) (err error) { + pr.HeadRepo, err = getRepositoryByID(e, pr.HeadRepoID) + if err != nil && !IsErrRepoNotExist(err) { + return fmt.Errorf("getRepositoryByID(head): %v", err) + } + return nil +} + +func (pr *PullRequest) GetHeadRepo() (err error) { + return pr.getHeadRepo(x) +} + +func (pr *PullRequest) GetBaseRepo() (err error) { + if pr.BaseRepo != nil { + return nil + } + + pr.BaseRepo, err = GetRepositoryByID(pr.BaseRepoID) + if err != nil { + return fmt.Errorf("GetRepositoryByID(base): %v", err) + } + return nil +} + +func (pr *PullRequest) GetMerger() (err error) { + if !pr.HasMerged || pr.Merger != nil { + return nil + } + + pr.Merger, err = GetUserByID(pr.MergerID) + if IsErrUserNotExist(err) { + pr.MergerID = -1 + pr.Merger = NewFakeUser() + } else if err != nil { + return fmt.Errorf("GetUserByID: %v", err) + } + return nil +} + +// IsChecking returns true if this pull request is still checking conflict. +func (pr *PullRequest) IsChecking() bool { + return pr.Status == PULL_REQUEST_STATUS_CHECKING +} + +// CanAutoMerge returns true if this pull request can be merged automatically. +func (pr *PullRequest) CanAutoMerge() bool { + return pr.Status == PULL_REQUEST_STATUS_MERGEABLE +} + +// Merge merges pull request to base repository. +func (pr *PullRequest) Merge(doer *User, baseGitRepo *git.Repository) (err error) { + sess := x.NewSession() + defer sessionRelease(sess) + if err = sess.Begin(); err != nil { + return err + } + + if err = pr.Issue.changeStatus(sess, doer, true); err != nil { + return fmt.Errorf("Issue.changeStatus: %v", err) + } + + if err = pr.getHeadRepo(sess); err != nil { + return fmt.Errorf("getHeadRepo: %v", err) + } + + headRepoPath := RepoPath(pr.HeadUserName, pr.HeadRepo.Name) + headGitRepo, err := git.OpenRepository(headRepoPath) + if err != nil { + return fmt.Errorf("OpenRepository: %v", err) + } + pr.MergedCommitID, err = headGitRepo.GetCommitIdOfBranch(pr.HeadBranch) + if err != nil { + return fmt.Errorf("GetCommitIdOfBranch: %v", err) + } + + if err = mergePullRequestAction(sess, doer, pr.Issue.Repo, pr.Issue); err != nil { + return fmt.Errorf("mergePullRequestAction: %v", err) + } + + pr.HasMerged = true + pr.Merged = time.Now() + pr.MergerID = doer.Id + if _, err = sess.Id(pr.ID).AllCols().Update(pr); err != nil { + return fmt.Errorf("update pull request: %v", err) + } + + // Clone base repo. + tmpBasePath := path.Join(setting.AppDataPath, "tmp/repos", com.ToStr(time.Now().Nanosecond())+".git") + os.MkdirAll(path.Dir(tmpBasePath), os.ModePerm) + defer os.RemoveAll(path.Dir(tmpBasePath)) + + var stderr string + if _, stderr, err = process.ExecTimeout(5*time.Minute, + fmt.Sprintf("PullRequest.Merge(git clone): %s", tmpBasePath), + "git", "clone", baseGitRepo.Path, tmpBasePath); err != nil { + return fmt.Errorf("git clone: %s", stderr) + } + + // Check out base branch. + if _, stderr, err = process.ExecDir(-1, tmpBasePath, + fmt.Sprintf("PullRequest.Merge(git checkout): %s", tmpBasePath), + "git", "checkout", pr.BaseBranch); err != nil { + return fmt.Errorf("git checkout: %s", stderr) + } + + // Add head repo remote. + if _, stderr, err = process.ExecDir(-1, tmpBasePath, + fmt.Sprintf("PullRequest.Merge(git remote add): %s", tmpBasePath), + "git", "remote", "add", "head_repo", headRepoPath); err != nil { + return fmt.Errorf("git remote add[%s -> %s]: %s", headRepoPath, tmpBasePath, stderr) + } + + // Merge commits. + if _, stderr, err = process.ExecDir(-1, tmpBasePath, + fmt.Sprintf("PullRequest.Merge(git fetch): %s", tmpBasePath), + "git", "fetch", "head_repo"); err != nil { + return fmt.Errorf("git fetch[%s -> %s]: %s", headRepoPath, tmpBasePath, stderr) + } + + if _, stderr, err = process.ExecDir(-1, tmpBasePath, + fmt.Sprintf("PullRequest.Merge(git merge): %s", tmpBasePath), + "git", "merge", "--no-ff", "-m", + fmt.Sprintf("Merge branch '%s' of %s/%s into %s", pr.HeadBranch, pr.HeadUserName, pr.HeadRepo.Name, pr.BaseBranch), + "head_repo/"+pr.HeadBranch); err != nil { + return fmt.Errorf("git merge[%s]: %s", tmpBasePath, stderr) + } + + // Push back to upstream. + if _, stderr, err = process.ExecDir(-1, tmpBasePath, + fmt.Sprintf("PullRequest.Merge(git push): %s", tmpBasePath), + "git", "push", baseGitRepo.Path, pr.BaseBranch); err != nil { + return fmt.Errorf("git push: %s", stderr) + } + + return sess.Commit() +} + +// patchConflicts is a list of conflit description from Git. +var patchConflicts = []string{ + "patch does not apply", + "already exists in working directory", + "unrecognized input", +} + +// testPatch checks if patch can be merged to base repository without conflit. +// FIXME: make a mechanism to clean up stable local copies. +func (pr *PullRequest) testPatch() (err error) { + if pr.BaseRepo == nil { + pr.BaseRepo, err = GetRepositoryByID(pr.BaseRepoID) + if err != nil { + return fmt.Errorf("GetRepositoryByID: %v", err) + } + } + + patchPath, err := pr.BaseRepo.PatchPath(pr.Index) + if err != nil { + return fmt.Errorf("BaseRepo.PatchPath: %v", err) + } + + // Fast fail if patch does not exist, this assumes data is cruppted. + if !com.IsFile(patchPath) { + log.Trace("PullRequest[%d].testPatch: ignored cruppted data", pr.ID) + return nil + } + + log.Trace("PullRequest[%d].testPatch(patchPath): %s", pr.ID, patchPath) + + if err := pr.BaseRepo.UpdateLocalCopy(); err != nil { + return fmt.Errorf("UpdateLocalCopy: %v", err) + } + + // Checkout base branch. + _, stderr, err := process.ExecDir(-1, pr.BaseRepo.LocalCopyPath(), + fmt.Sprintf("PullRequest.Merge(git checkout): %s", pr.BaseRepo.ID), + "git", "checkout", pr.BaseBranch) + if err != nil { + return fmt.Errorf("git checkout: %s", stderr) + } + + pr.Status = PULL_REQUEST_STATUS_CHECKING + _, stderr, err = process.ExecDir(-1, pr.BaseRepo.LocalCopyPath(), + fmt.Sprintf("testPatch(git apply --check): %d", pr.BaseRepo.ID), + "git", "apply", "--check", patchPath) + if err != nil { + for i := range patchConflicts { + if strings.Contains(stderr, patchConflicts[i]) { + log.Trace("PullRequest[%d].testPatch(apply): has conflit", pr.ID) + fmt.Println(stderr) + pr.Status = PULL_REQUEST_STATUS_CONFLICT + 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) { + sess := x.NewSession() + defer sessionRelease(sess) + if err = sess.Begin(); err != nil { + return err + } + + if err = newIssue(sess, repo, pull, labelIDs, uuids, true); err != nil { + return fmt.Errorf("newIssue: %v", err) + } + + // Notify watchers. + act := &Action{ + ActUserID: pull.Poster.Id, + ActUserName: pull.Poster.Name, + ActEmail: pull.Poster.Email, + OpType: CREATE_PULL_REQUEST, + Content: fmt.Sprintf("%d|%s", pull.Index, pull.Name), + RepoID: repo.ID, + RepoUserName: repo.Owner.Name, + RepoName: repo.Name, + IsPrivate: repo.IsPrivate, + } + if err = notifyWatchers(sess, act); err != nil { + return err + } + + pr.Index = pull.Index + if err = repo.SavePatch(pr.Index, patch); err != nil { + return fmt.Errorf("SavePatch: %v", err) + } + + pr.BaseRepo = repo + if err = pr.testPatch(); err != nil { + return fmt.Errorf("testPatch: %v", err) + } + if pr.Status == PULL_REQUEST_STATUS_CHECKING { + pr.Status = PULL_REQUEST_STATUS_MERGEABLE + } + + pr.IssueID = pull.ID + if _, err = sess.Insert(pr); err != nil { + return fmt.Errorf("insert pull repo: %v", err) + } + + return sess.Commit() +} + +// GetUnmergedPullRequest returnss 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) { + pr := new(PullRequest) + has, err := x.Where("head_repo_id=? AND head_branch=? AND base_repo_id=? AND base_branch=? AND has_merged=? AND issue.is_closed=?", + headRepoID, headBranch, baseRepoID, baseBranch, false, false). + Join("INNER", "issue", "issue.id=pull_request.issue_id").Get(pr) + if err != nil { + return nil, err + } else if !has { + return nil, ErrPullRequestNotExist{0, 0, headRepoID, baseRepoID, headBranch, baseBranch} + } + + return pr, nil +} + +// GetUnmergedPullRequestsByHeadInfo returnss all pull requests that are open and has not been merged +// by given head information (repo and branch). +func GetUnmergedPullRequestsByHeadInfo(repoID int64, branch string) ([]*PullRequest, error) { + prs := make([]*PullRequest, 0, 2) + return prs, x.Where("head_repo_id=? AND head_branch=? AND has_merged=? AND issue.is_closed=?", + repoID, branch, false, false). + Join("INNER", "issue", "issue.id=pull_request.issue_id").Find(&prs) +} + +// GetUnmergedPullRequestsByBaseInfo returnss all pull requests that are open and has not been merged +// by given base information (repo and branch). +func GetUnmergedPullRequestsByBaseInfo(repoID int64, branch string) ([]*PullRequest, error) { + prs := make([]*PullRequest, 0, 2) + return prs, x.Where("base_repo_id=? AND base_branch=? AND has_merged=? AND issue.is_closed=?", + repoID, branch, false, false). + Join("INNER", "issue", "issue.id=pull_request.issue_id").Find(&prs) +} + +// GetPullRequestByID returns a pull request by given ID. +func GetPullRequestByID(id int64) (*PullRequest, error) { + pr := new(PullRequest) + has, err := x.Id(id).Get(pr) + if err != nil { + return nil, err + } else if !has { + return nil, ErrPullRequestNotExist{id, 0, 0, 0, "", ""} + } + return pr, nil +} + +// GetPullRequestByIssueID returns pull request by given issue ID. +func GetPullRequestByIssueID(issueID int64) (*PullRequest, error) { + pr := &PullRequest{ + IssueID: issueID, + } + has, err := x.Get(pr) + if err != nil { + return nil, err + } else if !has { + return nil, ErrPullRequestNotExist{0, issueID, 0, 0, "", ""} + } + return pr, nil +} + +// Update updates all fields of pull request. +func (pr *PullRequest) Update() error { + _, err := x.Id(pr.ID).AllCols().Update(pr) + return err +} + +// Update updates specific fields of pull request. +func (pr *PullRequest) UpdateCols(cols ...string) error { + _, err := x.Id(pr.ID).Cols(cols...).Update(pr) + return err +} + +var PullRequestQueue = NewUniqueQueue(setting.Repository.PullRequestQueueLength) + +// 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 cruppted data", pr.ID) + return nil + } + + if err = pr.GetBaseRepo(); err != nil { + return fmt.Errorf("GetBaseRepo: %v", err) + } else if err = pr.BaseRepo.GetOwner(); err != nil { + return fmt.Errorf("GetOwner: %v", err) + } + + headRepoPath, err := pr.HeadRepo.RepoPath() + if err != nil { + return fmt.Errorf("HeadRepo.RepoPath: %v", err) + } + + headGitRepo, err := git.OpenRepository(headRepoPath) + if err != nil { + return fmt.Errorf("OpenRepository: %v", err) + } + + // Add a temporary remote. + tmpRemote := com.ToStr(time.Now().UnixNano()) + if err = headGitRepo.AddRemote(tmpRemote, RepoPath(pr.BaseRepo.Owner.Name, pr.BaseRepo.Name)); err != nil { + return fmt.Errorf("AddRemote: %v", err) + } + defer func() { + headGitRepo.RemoveRemote(tmpRemote) + }() + remoteBranch := "remotes/" + tmpRemote + "/" + pr.BaseBranch + pr.MergeBase, err = headGitRepo.GetMergeBase(remoteBranch, 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 +} + +// AddToTaskQueue adds itself to pull request test task queue. +func (pr *PullRequest) AddToTaskQueue() { + go PullRequestQueue.AddFunc(pr.ID, func() { + pr.Status = PULL_REQUEST_STATUS_CHECKING + if err := pr.UpdateCols("status"); err != nil { + log.Error(5, "AddToTaskQueue.UpdateCols[%d].(add to queue): %v", pr.ID, err) + } + }) +} + +func addHeadRepoTasks(prs []*PullRequest) { + for _, pr := range prs { + log.Trace("addHeadRepoTasks[%d]: composing new test task", pr.ID) + if err := pr.UpdatePatch(); err != nil { + log.Error(4, "UpdatePatch: %v", err) + continue + } + + pr.AddToTaskQueue() + } +} + +// AddTestPullRequestTask adds new test tasks by given head/base repository and head/base branch, +// and generate new patch for testing as needed. +func AddTestPullRequestTask(repoID int64, branch string) { + log.Trace("AddTestPullRequestTask[head_repo_id: %d, head_branch: %s]: finding pull requests", repoID, branch) + prs, err := GetUnmergedPullRequestsByHeadInfo(repoID, branch) + if err != nil { + log.Error(4, "Find pull requests[head_repo_id: %d, head_branch: %s]: %v", repoID, branch, err) + return + } + addHeadRepoTasks(prs) + + log.Trace("AddTestPullRequestTask[base_repo_id: %d, base_branch: %s]: finding pull requests", repoID, branch) + prs, err = GetUnmergedPullRequestsByBaseInfo(repoID, branch) + if err != nil { + log.Error(4, "Find pull requests[base_repo_id: %d, base_branch: %s]: %v", repoID, branch, err) + return + } + for _, pr := range prs { + pr.AddToTaskQueue() + } +} + +// checkAndUpdateStatus checks if pull request is possible to levaing checking status, +// and set to be either conflict or mergeable. +func (pr *PullRequest) checkAndUpdateStatus() { + // Status is not changed to conflict means mergeable. + if pr.Status == PULL_REQUEST_STATUS_CHECKING { + pr.Status = PULL_REQUEST_STATUS_MERGEABLE + } + + // Make sure there is no waiting test to process before levaing the checking status. + if !PullRequestQueue.Exist(pr.ID) { + if err := pr.UpdateCols("status"); err != nil { + log.Error(4, "Update[%d]: %v", pr.ID, err) + } + } +} + +// TestPullRequests checks and tests untested patches of pull requests. +// TODO: test more pull requests at same time. +func TestPullRequests() { + prs := make([]*PullRequest, 0, 10) + x.Iterate(PullRequest{ + Status: PULL_REQUEST_STATUS_CHECKING, + }, + func(idx int, bean interface{}) error { + pr := bean.(*PullRequest) + + if err := pr.GetBaseRepo(); err != nil { + log.Error(3, "GetBaseRepo: %v", err) + return nil + } + + if err := pr.testPatch(); err != nil { + log.Error(3, "testPatch: %v", err) + return nil + } + prs = append(prs, pr) + return nil + }) + + // Update pull request status. + for _, pr := range prs { + pr.checkAndUpdateStatus() + } + + // Start listening on new test requests. + for prID := range PullRequestQueue.Queue() { + log.Trace("TestPullRequests[%v]: processing test task", prID) + PullRequestQueue.Remove(prID) + + pr, err := GetPullRequestByID(com.StrTo(prID).MustInt64()) + if err != nil { + log.Error(4, "GetPullRequestByID[%d]: %v", prID, err) + continue + } else if err = pr.testPatch(); err != nil { + log.Error(4, "testPatch[%d]: %v", pr.ID, err) + continue + } + + pr.checkAndUpdateStatus() + } +} + +func InitTestPullRequests() { + go TestPullRequests() +} diff --git a/models/release.go b/models/release.go index 027491d9..1c9c7d60 100644 --- a/models/release.go +++ b/models/release.go @@ -5,7 +5,6 @@ package models import ( - "errors" "sort" "strings" "time" @@ -15,16 +14,11 @@ import ( "github.com/gogits/gogs/modules/git" ) -var ( - ErrReleaseAlreadyExist = errors.New("Release already exist") - ErrReleaseNotExist = errors.New("Release does not exist") -) - // Release represents a release of repository. type Release struct { - Id int64 - RepoId int64 - PublisherId int64 + ID int64 `xorm:"pk autoincr"` + RepoID int64 + PublisherID int64 Publisher *User `xorm:"-"` TagName string LowerTagName string @@ -47,12 +41,12 @@ func (r *Release) AfterSet(colName string, _ xorm.Cell) { } // IsReleaseExist returns true if release with given tag name already exists. -func IsReleaseExist(repoId int64, tagName string) (bool, error) { +func IsReleaseExist(repoID int64, tagName string) (bool, error) { if len(tagName) == 0 { return false, nil } - return x.Get(&Release{RepoId: repoId, LowerTagName: strings.ToLower(tagName)}) + return x.Get(&Release{RepoID: repoID, LowerTagName: strings.ToLower(tagName)}) } func createTag(gitRepo *git.Repository, rel *Release) error { @@ -64,7 +58,7 @@ func createTag(gitRepo *git.Repository, rel *Release) error { return err } - if err = gitRepo.CreateTag(rel.TagName, commit.Id.String()); err != nil { + if err = gitRepo.CreateTag(rel.TagName, commit.ID.String()); err != nil { return err } } else { @@ -84,11 +78,11 @@ func createTag(gitRepo *git.Repository, rel *Release) error { // CreateRelease creates a new release of repository. func CreateRelease(gitRepo *git.Repository, rel *Release) error { - isExist, err := IsReleaseExist(rel.RepoId, rel.TagName) + isExist, err := IsReleaseExist(rel.RepoID, rel.TagName) if err != nil { return err } else if isExist { - return ErrReleaseAlreadyExist + return ErrReleaseAlreadyExist{rel.TagName} } if err = createTag(gitRepo, rel); err != nil { @@ -100,22 +94,22 @@ func CreateRelease(gitRepo *git.Repository, rel *Release) error { } // GetRelease returns release by given ID. -func GetRelease(repoId int64, tagName string) (*Release, error) { - isExist, err := IsReleaseExist(repoId, tagName) +func GetRelease(repoID int64, tagName string) (*Release, error) { + isExist, err := IsReleaseExist(repoID, tagName) if err != nil { return nil, err } else if !isExist { - return nil, ErrReleaseNotExist + return nil, ErrReleaseNotExist{tagName} } - rel := &Release{RepoId: repoId, LowerTagName: strings.ToLower(tagName)} + rel := &Release{RepoID: repoID, LowerTagName: strings.ToLower(tagName)} _, err = x.Get(rel) return rel, err } // GetReleasesByRepoId returns a list of releases of repository. -func GetReleasesByRepoId(repoId int64) (rels []*Release, err error) { - err = x.Desc("created").Find(&rels, Release{RepoId: repoId}) +func GetReleasesByRepoId(repoID int64) (rels []*Release, err error) { + err = x.Desc("created").Find(&rels, Release{RepoID: repoID}) return rels, err } @@ -150,6 +144,6 @@ func UpdateRelease(gitRepo *git.Repository, rel *Release) (err error) { if err = createTag(gitRepo, rel); err != nil { return err } - _, err = x.Id(rel.Id).AllCols().Update(rel) + _, err = x.Id(rel.ID).AllCols().Update(rel) return err } diff --git a/models/repo.go b/models/repo.go index 75e1cd3a..174fbd9d 100644 --- a/models/repo.go +++ b/models/repo.go @@ -23,6 +23,7 @@ import ( "github.com/Unknwon/cae/zip" "github.com/Unknwon/com" "github.com/go-xorm/xorm" + "gopkg.in/ini.v1" "github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/bindata" @@ -48,7 +49,7 @@ var ( Gitignores, Licenses, Readmes []string // Maximum items per page in forks, watchers and stars of a repo - ItemsPerPage = 54 + ItemsPerPage = 40 ) func LoadRepoConfig() { @@ -105,6 +106,7 @@ func NewRepoContext() { if ver.LessThan(reqVer) { log.Fatal(4, "Gogs requires Git version greater or equal to 1.7.1") } + log.Info("Git Version: %s", ver.String()) // Git requires setting user.name and user.email in order to commit changes. for configKey, defaultValue := range map[string]string{"user.name": "Gogs", "user.email": "gogs@fake.local"} { @@ -182,9 +184,11 @@ func (repo *Repository) AfterSet(colName string, _ xorm.Cell) { } func (repo *Repository) getOwner(e Engine) (err error) { - if repo.Owner == nil { - repo.Owner, err = getUserByID(e, repo.OwnerID) + if repo.Owner != nil { + return nil } + + repo.Owner, err = getUserByID(e, repo.OwnerID) return err } @@ -297,7 +301,7 @@ func (repo *Repository) DescriptionHtml() template.HTML { } func (repo *Repository) LocalCopyPath() string { - return path.Join(setting.RepoRootPath, "local", com.ToStr(repo.ID)) + return path.Join(setting.AppDataPath, "tmp/local", com.ToStr(repo.ID)) } // UpdateLocalCopy makes sure the local copy of repository is up-to-date. @@ -316,7 +320,7 @@ func (repo *Repository) UpdateLocalCopy() error { } } else { _, stderr, err := process.ExecDir(-1, localPath, - fmt.Sprintf("UpdateLocalCopy(git pull): %s", repoPath), "git", "pull") + fmt.Sprintf("UpdateLocalCopy(git pull --all): %s", repoPath), "git", "pull", "--all") if err != nil { return fmt.Errorf("git pull: %v - %s", err, stderr) } @@ -325,6 +329,30 @@ func (repo *Repository) UpdateLocalCopy() error { return nil } +// PatchPath returns corresponding patch file path of repository by given issue ID. +func (repo *Repository) PatchPath(index int64) (string, error) { + if err := repo.GetOwner(); err != nil { + return "", err + } + + 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 { + patchPath, err := repo.PatchPath(index) + if err != nil { + return fmt.Errorf("PatchPath: %v", err) + } + + os.MkdirAll(path.Dir(patchPath), os.ModePerm) + 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, @@ -352,11 +380,11 @@ func (repo *Repository) CloneLink() (cl CloneLink, err error) { } if setting.SSHPort != 22 { - cl.SSH = fmt.Sprintf("ssh://%s@%s:%d/%s/%s.git", setting.RunUser, setting.SSHDomain, setting.SSHPort, repo.Owner.LowerName, repo.LowerName) + cl.SSH = fmt.Sprintf("ssh://%s@%s:%d/%s/%s.git", setting.RunUser, setting.SSHDomain, setting.SSHPort, repo.Owner.Name, repo.Name) } else { - cl.SSH = fmt.Sprintf("%s@%s:%s/%s.git", setting.RunUser, setting.SSHDomain, repo.Owner.LowerName, repo.LowerName) + cl.SSH = fmt.Sprintf("%s@%s:%s/%s.git", setting.RunUser, setting.SSHDomain, repo.Owner.Name, repo.Name) } - cl.HTTPS = fmt.Sprintf("%s%s/%s.git", setting.AppUrl, repo.Owner.LowerName, repo.LowerName) + cl.HTTPS = fmt.Sprintf("%s%s/%s.git", setting.AppUrl, repo.Owner.Name, repo.Name) return cl, nil } @@ -453,13 +481,21 @@ func MirrorRepository(repoId int64, userName, repoName, repoPath, url string) er return nil } +type MigrateRepoOptions struct { + Name string + Description string + IsPrivate bool + IsMirror bool + RemoteAddr string +} + // MigrateRepository migrates a existing repository from other project hosting. -func MigrateRepository(u *User, name, desc string, private, mirror bool, url string) (*Repository, error) { +func MigrateRepository(u *User, opts MigrateRepoOptions) (*Repository, error) { repo, err := CreateRepository(u, CreateRepoOptions{ - Name: name, - Description: desc, - IsPrivate: private, - IsMirror: mirror, + Name: opts.Name, + Description: opts.Description, + IsPrivate: opts.IsPrivate, + IsMirror: opts.IsMirror, }) if err != nil { return nil, err @@ -469,7 +505,7 @@ func MigrateRepository(u *User, name, desc string, private, mirror bool, url str tmpDir := filepath.Join(os.TempDir(), fmt.Sprintf("%d", time.Now().Nanosecond())) os.MkdirAll(tmpDir, os.ModePerm) - repoPath := RepoPath(u.Name, name) + repoPath := RepoPath(u.Name, opts.Name) if u.IsOrganization() { t, err := u.GetOwnerTeam() @@ -482,8 +518,8 @@ func MigrateRepository(u *User, name, desc string, private, mirror bool, url str } repo.IsBare = false - if mirror { - if err = MirrorRepository(repo.ID, u.Name, repo.Name, repoPath, url); err != nil { + if opts.IsMirror { + if err = MirrorRepository(repo.ID, u.Name, repo.Name, repoPath, opts.RemoteAddr); err != nil { return repo, err } repo.IsMirror = true @@ -495,13 +531,24 @@ func MigrateRepository(u *User, name, desc string, private, mirror bool, url str // FIXME: this command could for both migrate and mirror _, stderr, err := process.ExecTimeout(10*time.Minute, fmt.Sprintf("MigrateRepository: %s", repoPath), - "git", "clone", "--mirror", "--bare", "--quiet", url, repoPath) + "git", "clone", "--mirror", "--bare", "--quiet", opts.RemoteAddr, repoPath) if err != nil { return repo, fmt.Errorf("git clone --mirror --bare --quiet: %v", stderr) } else if err = createUpdateHook(repoPath); err != nil { return repo, fmt.Errorf("create update hook: %v", err) } + // Clean up mirror info which prevents "push --all". + configPath := filepath.Join(repoPath, "/config") + cfg, err := ini.Load(configPath) + if err != nil { + return repo, fmt.Errorf("open config file: %v", err) + } + cfg.DeleteSection("remote \"origin\"") + if err = cfg.SaveToIndent(configPath, "\t"); err != nil { + return repo, fmt.Errorf("save config file: %v", err) + } + // Check if repository is empty. _, stderr, err = com.ExecCmdDir(repoPath, "git", "log", "-1") if err != nil { @@ -552,7 +599,7 @@ func createUpdateHook(repoPath string) error { hookPath := path.Join(repoPath, "hooks/update") os.MkdirAll(path.Dir(hookPath), os.ModePerm) return ioutil.WriteFile(hookPath, - []byte(fmt.Sprintf(_TPL_UPDATE_HOOK, setting.ScriptType, "\""+appPath+"\"", setting.CustomConf)), 0777) + []byte(fmt.Sprintf(_TPL_UPDATE_HOOK, setting.ScriptType, "\""+setting.AppPath+"\"", setting.CustomConf)), 0777) } type CreateRepoOptions struct { @@ -863,9 +910,9 @@ func TransferOwnership(u *User, newOwnerName string, repo *Repository) error { } // Remove redundant collaborators. - collaborators, err := repo.GetCollaborators() + collaborators, err := repo.getCollaborators(sess) if err != nil { - return fmt.Errorf("GetCollaborators: %v", err) + return fmt.Errorf("getCollaborators: %v", err) } // Dummy object. @@ -901,9 +948,9 @@ func TransferOwnership(u *User, newOwnerName string, repo *Repository) error { } if newOwner.IsOrganization() { - t, err := newOwner.GetOwnerTeam() + t, err := newOwner.getOwnerTeam(sess) if err != nil { - return fmt.Errorf("GetOwnerTeam: %v", err) + return fmt.Errorf("getOwnerTeam: %v", err) } else if err = t.addRepository(sess, repo); err != nil { return fmt.Errorf("add to owner team: %v", err) } @@ -1069,7 +1116,7 @@ func DeleteRepository(uid, repoID int64) error { return err } else if _, err = sess.Delete(&Milestone{RepoID: repoID}); err != nil { return err - } else if _, err = sess.Delete(&Release{RepoId: repoID}); err != nil { + } else if _, err = sess.Delete(&Release{RepoID: repoID}); err != nil { return err } else if _, err = sess.Delete(&Collaboration{RepoID: repoID}); err != nil { return err @@ -1124,7 +1171,7 @@ func DeleteRepository(uid, repoID int64) error { desc := fmt.Sprintf("delete repository files[%s]: %v", repoPath, err) log.Warn(desc) if err = CreateRepositoryNotice(desc); err != nil { - log.Error(4, "add notice: %v", err) + log.Error(4, "CreateRepositoryNotice: %v", err) } } @@ -1267,10 +1314,14 @@ func DeleteRepositoryArchives() error { return x.Where("id > 0").Iterate(new(Repository), func(idx int, bean interface{}) error { repo := bean.(*Repository) - if err := repo.GetOwner(); err != nil { - return err + repoPath, err := repo.RepoPath() + if err != nil { + if err2 := CreateRepositoryNotice(fmt.Sprintf("DeleteRepositoryArchives[%d]: %v", repo.ID, err)); err2 != nil { + log.Error(4, "CreateRepositoryNotice: %v", err2) + } + return nil } - return os.RemoveAll(filepath.Join(RepoPath(repo.Owner.Name, repo.Name), "archives")) + return os.RemoveAll(filepath.Join(repoPath, "archives")) }) } @@ -1279,10 +1330,14 @@ func RewriteRepositoryUpdateHook() error { return x.Where("id > 0").Iterate(new(Repository), func(idx int, bean interface{}) error { repo := bean.(*Repository) - if err := repo.GetOwner(); err != nil { - return err + repoPath, err := repo.RepoPath() + if err != nil { + if err2 := CreateRepositoryNotice(fmt.Sprintf("RewriteRepositoryUpdateHook[%d]: %v", repo.ID, err)); err2 != nil { + log.Error(4, "CreateRepositoryNotice: %v", err2) + } + return nil } - return createUpdateHook(RepoPath(repo.Owner.Name, repo.Name)) + return createUpdateHook(repoPath) }) } @@ -1449,6 +1504,12 @@ func CheckRepoStats() { "UPDATE `user` SET num_repos=(SELECT COUNT(*) FROM `repository` WHERE owner_id=?) WHERE id=?", "user count 'num_repos'", }, + // Issue.NumComments + { + "SELECT `issue`.id FROM `issue` WHERE `issue`.num_comments!=(SELECT COUNT(*) FROM `comment` WHERE issue_id=`issue`.id AND type=0)", + "UPDATE `issue` SET num_comments=(SELECT COUNT(*) FROM `comment` WHERE issue_id=? AND type=0) WHERE id=?", + "issue count 'num_comments'", + }, } for i := range checkers { repoStatsCheck(checkers[i]) @@ -1635,25 +1696,21 @@ func WatchRepo(uid, repoId int64, watch bool) (err error) { return watchRepo(x, uid, repoId, watch) } -func getWatchers(e Engine, rid int64) ([]*Watch, error) { +func getWatchers(e Engine, repoID int64) ([]*Watch, error) { watches := make([]*Watch, 0, 10) - err := e.Find(&watches, &Watch{RepoID: rid}) - return watches, err + return watches, e.Find(&watches, &Watch{RepoID: repoID}) } // GetWatchers returns all watchers of given repository. -func GetWatchers(rid int64) ([]*Watch, error) { - return getWatchers(x, rid) +func GetWatchers(repoID int64) ([]*Watch, error) { + return getWatchers(x, repoID) } -// Repository.GetWatchers returns all users watching given repository. -func (repo *Repository) GetWatchers(offset int) ([]*User, error) { - users := make([]*User, 0, 10) - offset = (offset - 1) * ItemsPerPage - - err := x.Limit(ItemsPerPage, offset).Where("repo_id=?", repo.ID).Join("LEFT", "watch", "user.id=watch.user_id").Find(&users) - - return users, err +// Repository.GetWatchers returns range of users watching given repository. +func (repo *Repository) GetWatchers(page int) ([]*User, error) { + users := make([]*User, 0, ItemsPerPage) + return users, x.Limit(ItemsPerPage, (page-1)*ItemsPerPage). + Where("repo_id=?", repo.ID).Join("LEFT", "watch", "user.id=watch.user_id").Find(&users) } func notifyWatchers(e Engine, act *Action) error { @@ -1733,13 +1790,10 @@ func IsStaring(uid, repoId int64) bool { return has } -func (repo *Repository) GetStars(offset int) ([]*User, error) { - users := make([]*User, 0, 10) - offset = (offset - 1) * ItemsPerPage - - err := x.Limit(ItemsPerPage, offset).Where("repo_id=?", repo.ID).Join("LEFT", "star", "user.id=star.uid").Find(&users) - - return users, err +func (repo *Repository) GetStargazers(page int) ([]*User, error) { + users := make([]*User, 0, ItemsPerPage) + return users, x.Limit(ItemsPerPage, (page-1)*ItemsPerPage). + Where("repo_id=?", repo.ID).Join("LEFT", "star", "user.id=star.uid").Find(&users) } // ___________ __ @@ -1811,9 +1865,6 @@ func ForkRepository(u *User, oldRepo *Repository, name, desc string) (_ *Reposit } func (repo *Repository) GetForks() ([]*Repository, error) { - forks := make([]*Repository, 0, 10) - - err := x.Find(&forks, &Repository{ForkID: repo.ID}) - - return forks, err + forks := make([]*Repository, 0, repo.NumForks) + return forks, x.Find(&forks, &Repository{ForkID: repo.ID}) } diff --git a/models/update.go b/models/update.go index cbaf0e66..14e56ce8 100644 --- a/models/update.go +++ b/models/update.go @@ -10,17 +10,16 @@ import ( "os/exec" "strings" - "github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/git" "github.com/gogits/gogs/modules/log" ) type UpdateTask struct { - Id int64 - Uuid string `xorm:"index"` + ID int64 `xorm:"pk autoincr"` + UUID string `xorm:"index"` RefName string - OldCommitId string - NewCommitId string + OldCommitID string + NewCommitID string } func AddUpdateTask(task *UpdateTask) error { @@ -28,27 +27,29 @@ func AddUpdateTask(task *UpdateTask) error { return err } -func GetUpdateTasksByUuid(uuid string) ([]*UpdateTask, error) { +// GetUpdateTaskByUUID returns update task by given UUID. +func GetUpdateTaskByUUID(uuid string) (*UpdateTask, error) { task := &UpdateTask{ - Uuid: uuid, + UUID: uuid, } - tasks := make([]*UpdateTask, 0) - err := x.Find(&tasks, task) + has, err := x.Get(task) if err != nil { return nil, err + } else if !has { + return nil, ErrUpdateTaskNotExist{uuid} } - return tasks, nil + return task, nil } -func DelUpdateTasksByUuid(uuid string) error { - _, err := x.Delete(&UpdateTask{Uuid: uuid}) +func DeleteUpdateTaskByUUID(uuid string) error { + _, err := x.Delete(&UpdateTask{UUID: uuid}) return err } -func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName string, userId int64) error { - isNew := strings.HasPrefix(oldCommitId, "0000000") +func Update(refName, oldCommitID, newCommitID, userName, repoUserName, repoName string, userID int64) error { + isNew := strings.HasPrefix(oldCommitID, "0000000") if isNew && - strings.HasPrefix(newCommitId, "0000000") { + strings.HasPrefix(newCommitID, "0000000") { return fmt.Errorf("old rev and new rev both 000000") } @@ -58,23 +59,23 @@ func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName gitUpdate.Dir = f gitUpdate.Run() - isDel := strings.HasPrefix(newCommitId, "0000000") + isDel := strings.HasPrefix(newCommitID, "0000000") if isDel { - log.GitLogger.Info("del rev", refName, "from", userName+"/"+repoName+".git", "by", userId) + log.GitLogger.Info("del rev", refName, "from", userName+"/"+repoName+".git", "by", userID) return nil } - repo, err := git.OpenRepository(f) + gitRepo, err := git.OpenRepository(f) if err != nil { return fmt.Errorf("runUpdate.Open repoId: %v", err) } - ru, err := GetUserByName(repoUserName) + user, err := GetUserByName(repoUserName) if err != nil { return fmt.Errorf("runUpdate.GetUserByName: %v", err) } - repos, err := GetRepositoryByName(ru.Id, repoName) + repo, err := GetRepositoryByName(user.Id, repoName) if err != nil { return fmt.Errorf("runUpdate.GetRepositoryByName userId: %v", err) } @@ -82,7 +83,7 @@ func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName // Push tags. if strings.HasPrefix(refName, "refs/tags/") { tagName := git.RefEndName(refName) - tag, err := repo.GetTag(tagName) + tag, err := gitRepo.GetTag(tagName) if err != nil { log.GitLogger.Fatal(4, "runUpdate.GetTag: %v", err) } @@ -98,16 +99,16 @@ func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName actEmail = cmt.Committer.Email } - commit := &base.PushCommits{} + commit := &PushCommits{} - if err = CommitRepoAction(userId, ru.Id, userName, actEmail, - repos.ID, repoUserName, repoName, refName, commit, oldCommitId, newCommitId); err != nil { + if err = CommitRepoAction(userID, user.Id, userName, actEmail, + repo.ID, repoUserName, repoName, refName, commit, oldCommitID, newCommitID); err != nil { log.GitLogger.Fatal(4, "CommitRepoAction: %s/%s:%v", repoUserName, repoName, err) } return err } - newCommit, err := repo.GetCommit(newCommitId) + newCommit, err := gitRepo.GetCommit(newCommitID) if err != nil { return fmt.Errorf("runUpdate GetCommit of newCommitId: %v", err) } @@ -117,12 +118,12 @@ func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName if isNew { l, err = newCommit.CommitsBefore() if err != nil { - return fmt.Errorf("Find CommitsBefore erro: %v", err) + return fmt.Errorf("CommitsBefore: %v", err) } } else { - l, err = newCommit.CommitsBeforeUntil(oldCommitId) + l, err = newCommit.CommitsBeforeUntil(oldCommitID) if err != nil { - return fmt.Errorf("Find CommitsBeforeUntil erro: %v", err) + return fmt.Errorf("CommitsBeforeUntil: %v", err) } } @@ -131,7 +132,7 @@ func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName } // Push commits. - commits := make([]*base.PushCommit, 0) + commits := make([]*PushCommit, 0) var actEmail string for e := l.Front(); e != nil; e = e.Next() { commit := e.Value.(*git.Commit) @@ -139,15 +140,15 @@ func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName actEmail = commit.Committer.Email } commits = append(commits, - &base.PushCommit{commit.Id.String(), + &PushCommit{commit.ID.String(), commit.Message(), commit.Author.Email, commit.Author.Name, }) } - if err = CommitRepoAction(userId, ru.Id, userName, actEmail, - repos.ID, repoUserName, repoName, refName, &base.PushCommits{l.Len(), commits, ""}, oldCommitId, newCommitId); err != nil { + if err = CommitRepoAction(userID, user.Id, userName, actEmail, + repo.ID, repoUserName, repoName, refName, &PushCommits{l.Len(), commits, "", nil}, oldCommitID, newCommitID); err != nil { return fmt.Errorf("runUpdate.models.CommitRepoAction: %s/%s:%v", repoUserName, repoName, err) } return nil diff --git a/models/user.go b/models/user.go index 152ae428..3131a88d 100644 --- a/models/user.go +++ b/models/user.go @@ -14,6 +14,7 @@ import ( "image" "image/jpeg" _ "image/jpeg" + "image/png" "os" "path" "path/filepath" @@ -71,13 +72,14 @@ type User struct { Created time.Time `xorm:"CREATED"` Updated time.Time `xorm:"UPDATED"` - // Remember visibility choice for convenience. + // Remember visibility choice for convenience, true for private LastRepoVisibility bool // Permissions. - IsActive bool - IsAdmin bool - AllowGitHook bool + IsActive bool + IsAdmin bool + AllowGitHook bool + AllowImportLocal bool // Allow migrate repository by local path // Avatar. Avatar string `xorm:"VARCHAR(2048) NOT NULL"` @@ -107,6 +109,22 @@ func (u *User) AfterSet(colName string, _ xorm.Cell) { } } +// HasForkedRepo checks if user has already forked a repository with given ID. +func (u *User) HasForkedRepo(repoID int64) bool { + _, has := HasForkedRepo(u.Id, repoID) + return has +} + +// CanEditGitHook returns true if user can edit Git hooks. +func (u *User) CanEditGitHook() bool { + return u.IsAdmin || u.AllowGitHook +} + +// CanImportLocal returns true if user can migrate repository by local path. +func (u *User) CanImportLocal() bool { + return u.IsAdmin || u.AllowImportLocal +} + // EmailAdresses is the list of all email addresses of a user. Can contain the // primary email address, but is not obligatory type EmailAddress struct { @@ -242,14 +260,12 @@ func (u *User) ValidatePassword(passwd string) bool { // UploadAvatar saves custom avatar for user. // FIXME: split uploads to different subdirs in case we have massive users. func (u *User) UploadAvatar(data []byte) error { - u.UseCustomAvatar = true - img, _, err := image.Decode(bytes.NewReader(data)) if err != nil { - return err + return fmt.Errorf("Decode: %v", err) } - m := resize.Resize(234, 234, img, resize.NearestNeighbor) + m := resize.Resize(290, 290, img, resize.NearestNeighbor) sess := x.NewSession() defer sessionRelease(sess) @@ -257,19 +273,20 @@ func (u *User) UploadAvatar(data []byte) error { return err } - if _, err = sess.Id(u.Id).AllCols().Update(u); err != nil { - return err + u.UseCustomAvatar = true + if err = updateUser(sess, u); err != nil { + return fmt.Errorf("updateUser: %v", err) } os.MkdirAll(setting.AvatarUploadPath, os.ModePerm) fw, err := os.Create(u.CustomAvatarPath()) if err != nil { - return err + return fmt.Errorf("Create: %v", err) } defer fw.Close() - if err = jpeg.Encode(fw, m, nil); err != nil { - return err + if err = png.Encode(fw, m); err != nil { + return fmt.Errorf("Encode: %v", err) } return sess.Commit() @@ -717,9 +734,9 @@ func UserPath(userName string) string { return filepath.Join(setting.RepoRootPath, strings.ToLower(userName)) } -func GetUserByKeyId(keyId int64) (*User, error) { +func GetUserByKeyID(keyID int64) (*User, error) { user := new(User) - has, err := x.Sql("SELECT a.* FROM `user` AS a, public_key AS b WHERE a.id = b.owner_id AND b.id=?", keyId).Get(user) + has, err := x.Sql("SELECT a.* FROM `user` AS a, public_key AS b WHERE a.id = b.owner_id AND b.id=?", keyID).Get(user) if err != nil { return nil, err } else if !has { @@ -980,7 +997,7 @@ func GetUserByEmail(email string) (*User, error) { return GetUserByID(emailAddress.UID) } - return nil, ErrUserNotExist{0, "email"} + return nil, ErrUserNotExist{0, email} } // SearchUserByName returns given number of users whose name contains keyword. diff --git a/models/webhook.go b/models/webhook.go index 75380d17..b4d7dc9c 100644 --- a/models/webhook.go +++ b/models/webhook.go @@ -13,6 +13,7 @@ import ( "sync" "time" + "github.com/Unknwon/com" "github.com/go-xorm/xorm" api "github.com/gogits/go-gogs-client" @@ -435,39 +436,65 @@ func PrepareWebhooks(repo *Repository, event HookEventType, p api.Payloader) err return nil } -type hookQueue struct { - // Make sure one repository only occur once in the queue. - lock sync.Mutex - repoIDs map[int64]bool +// UniqueQueue represents a queue that guarantees only one instance of same ID is in the line. +type UniqueQueue struct { + lock sync.Mutex + ids map[string]bool - queue chan int64 + queue chan string } -func (q *hookQueue) removeRepoID(id int64) { +func (q *UniqueQueue) Queue() <-chan string { + return q.queue +} + +func NewUniqueQueue(queueLength int) *UniqueQueue { + if queueLength <= 0 { + queueLength = 100 + } + + return &UniqueQueue{ + ids: make(map[string]bool), + queue: make(chan string, queueLength), + } +} + +func (q *UniqueQueue) Remove(id interface{}) { q.lock.Lock() defer q.lock.Unlock() - delete(q.repoIDs, id) + delete(q.ids, com.ToStr(id)) } -func (q *hookQueue) addRepoID(id int64) { - q.lock.Lock() - if q.repoIDs[id] { - q.lock.Unlock() +func (q *UniqueQueue) AddFunc(id interface{}, fn func()) { + newid := com.ToStr(id) + + if q.Exist(id) { return } - q.repoIDs[id] = true + + q.lock.Lock() + q.ids[newid] = true + if fn != nil { + fn() + } q.lock.Unlock() - q.queue <- id + q.queue <- newid } -// AddRepoID adds repository ID to hook delivery queue. -func (q *hookQueue) AddRepoID(id int64) { - go q.addRepoID(id) +func (q *UniqueQueue) Add(id interface{}) { + q.AddFunc(id, nil) +} + +func (q *UniqueQueue) Exist(id interface{}) bool { + q.lock.Lock() + defer q.lock.Unlock() + + return q.ids[com.ToStr(id)] } -var HookQueue *hookQueue +var HookQueue = NewUniqueQueue(setting.Webhook.QueueLength) -func deliverHook(t *HookTask) { +func (t *HookTask) deliver() { t.IsDelivered = true timeout := time.Duration(setting.Webhook.DeliverTimeout) * time.Second @@ -549,12 +576,13 @@ func deliverHook(t *HookTask) { } // DeliverHooks checks and delivers undelivered hooks. +// TODO: shoot more hooks at same time. func DeliverHooks() { tasks := make([]*HookTask, 0, 10) x.Where("is_delivered=?", false).Iterate(new(HookTask), func(idx int, bean interface{}) error { t := bean.(*HookTask) - deliverHook(t) + t.deliver() tasks = append(tasks, t) return nil }) @@ -566,15 +594,10 @@ func DeliverHooks() { } } - HookQueue = &hookQueue{ - lock: sync.Mutex{}, - repoIDs: make(map[int64]bool), - queue: make(chan int64, setting.Webhook.QueueLength), - } - // Start listening on new hook requests. - for repoID := range HookQueue.queue { - HookQueue.removeRepoID(repoID) + for repoID := range HookQueue.Queue() { + log.Trace("DeliverHooks[%v]: processing delivery hooks", repoID) + HookQueue.Remove(repoID) tasks = make([]*HookTask, 0, 5) if err := x.Where("repo_id=? AND is_delivered=?", repoID, false).Find(&tasks); err != nil { @@ -582,9 +605,10 @@ func DeliverHooks() { continue } for _, t := range tasks { - deliverHook(t) + t.deliver() if err := UpdateHookTask(t); err != nil { - log.Error(4, "UpdateHookTask(%d): %v", t.ID, err) + log.Error(4, "UpdateHookTask[%d]: %v", t.ID, err) + continue } } } |