aboutsummaryrefslogtreecommitdiff
path: root/internal/db
diff options
context:
space:
mode:
Diffstat (limited to 'internal/db')
-rw-r--r--internal/db/access.go241
-rw-r--r--internal/db/action.go767
-rw-r--r--internal/db/admin.go118
-rw-r--r--internal/db/attachment.go183
-rw-r--r--internal/db/comment.go534
-rw-r--r--internal/db/error.go575
-rw-r--r--internal/db/errors/errors.go14
-rw-r--r--internal/db/errors/issue.go35
-rw-r--r--internal/db/errors/login_source.go60
-rw-r--r--internal/db/errors/org.go21
-rw-r--r--internal/db/errors/repo.go87
-rw-r--r--internal/db/errors/token.go16
-rw-r--r--internal/db/errors/two_factor.go33
-rw-r--r--internal/db/errors/user.go45
-rw-r--r--internal/db/errors/user_mail.go33
-rw-r--r--internal/db/errors/webhook.go34
-rw-r--r--internal/db/git_diff.go194
-rw-r--r--internal/db/git_diff_test.go41
-rw-r--r--internal/db/issue.go1440
-rw-r--r--internal/db/issue_label.go374
-rw-r--r--internal/db/issue_mail.go180
-rw-r--r--internal/db/login_source.go866
-rw-r--r--internal/db/migrations/migrations.go390
-rw-r--r--internal/db/migrations/v13.go52
-rw-r--r--internal/db/migrations/v14.go24
-rw-r--r--internal/db/migrations/v15.go104
-rw-r--r--internal/db/migrations/v16.go77
-rw-r--r--internal/db/migrations/v17.go22
-rw-r--r--internal/db/migrations/v18.go34
-rw-r--r--internal/db/migrations/v19.go18
-rw-r--r--internal/db/milestone.go402
-rw-r--r--internal/db/mirror.go498
-rw-r--r--internal/db/mirror_test.go108
-rw-r--r--internal/db/models.go401
-rw-r--r--internal/db/models_sqlite.go15
-rw-r--r--internal/db/models_test.go33
-rw-r--r--internal/db/org.go563
-rw-r--r--internal/db/org_team.go666
-rw-r--r--internal/db/pull.go851
-rw-r--r--internal/db/release.go352
-rw-r--r--internal/db/repo.go2458
-rw-r--r--internal/db/repo_branch.go257
-rw-r--r--internal/db/repo_collaboration.go226
-rw-r--r--internal/db/repo_editor.go518
-rw-r--r--internal/db/repo_editor_test.go34
-rw-r--r--internal/db/repo_test.go63
-rw-r--r--internal/db/ssh_key.go771
-rw-r--r--internal/db/ssh_key_test.go56
-rw-r--r--internal/db/token.go102
-rw-r--r--internal/db/two_factor.go201
-rw-r--r--internal/db/update.go142
-rw-r--r--internal/db/user.go1146
-rw-r--r--internal/db/user_cache.go16
-rw-r--r--internal/db/user_mail.go210
-rw-r--r--internal/db/webhook.go771
-rw-r--r--internal/db/webhook_dingtalk.go261
-rw-r--r--internal/db/webhook_discord.go409
-rw-r--r--internal/db/webhook_slack.go326
-rw-r--r--internal/db/wiki.go179
59 files changed, 18647 insertions, 0 deletions
diff --git a/internal/db/access.go b/internal/db/access.go
new file mode 100644
index 00000000..e9c8b5b7
--- /dev/null
+++ b/internal/db/access.go
@@ -0,0 +1,241 @@
+// Copyright 2014 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 db
+
+import (
+ "fmt"
+
+ log "gopkg.in/clog.v1"
+
+ "gogs.io/gogs/internal/db/errors"
+)
+
+type AccessMode int
+
+const (
+ ACCESS_MODE_NONE AccessMode = iota // 0
+ ACCESS_MODE_READ // 1
+ ACCESS_MODE_WRITE // 2
+ ACCESS_MODE_ADMIN // 3
+ ACCESS_MODE_OWNER // 4
+)
+
+func (mode AccessMode) String() string {
+ switch mode {
+ case ACCESS_MODE_READ:
+ return "read"
+ case ACCESS_MODE_WRITE:
+ return "write"
+ case ACCESS_MODE_ADMIN:
+ return "admin"
+ case ACCESS_MODE_OWNER:
+ return "owner"
+ default:
+ return "none"
+ }
+}
+
+// ParseAccessMode returns corresponding access mode to given permission string.
+func ParseAccessMode(permission string) AccessMode {
+ switch permission {
+ case "write":
+ return ACCESS_MODE_WRITE
+ case "admin":
+ return ACCESS_MODE_ADMIN
+ default:
+ return ACCESS_MODE_READ
+ }
+}
+
+// Access represents the highest access level of a user to the repository. The only access type
+// that is not in this table is the real owner of a repository. In case of an organization
+// repository, the members of the owners team are in this table.
+type Access struct {
+ ID int64
+ UserID int64 `xorm:"UNIQUE(s)"`
+ RepoID int64 `xorm:"UNIQUE(s)"`
+ Mode AccessMode
+}
+
+func userAccessMode(e Engine, userID int64, repo *Repository) (AccessMode, error) {
+ mode := ACCESS_MODE_NONE
+ // Everyone has read access to public repository
+ if !repo.IsPrivate {
+ mode = ACCESS_MODE_READ
+ }
+
+ if userID <= 0 {
+ return mode, nil
+ }
+
+ if userID == repo.OwnerID {
+ return ACCESS_MODE_OWNER, nil
+ }
+
+ access := &Access{
+ UserID: userID,
+ RepoID: repo.ID,
+ }
+ if has, err := e.Get(access); !has || err != nil {
+ return mode, err
+ }
+ return access.Mode, nil
+}
+
+// UserAccessMode returns the access mode of given user to the repository.
+func UserAccessMode(userID int64, repo *Repository) (AccessMode, error) {
+ return userAccessMode(x, userID, repo)
+}
+
+func hasAccess(e Engine, userID int64, repo *Repository, testMode AccessMode) (bool, error) {
+ mode, err := userAccessMode(e, userID, repo)
+ return mode >= testMode, err
+}
+
+// HasAccess returns true if someone has the request access level. User can be nil!
+func HasAccess(userID int64, repo *Repository, testMode AccessMode) (bool, error) {
+ return hasAccess(x, userID, repo, testMode)
+}
+
+// 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
+ }
+
+ repos := make(map[*Repository]AccessMode, len(accesses))
+ for _, access := range accesses {
+ repo, err := GetRepositoryByID(access.RepoID)
+ if err != nil {
+ if errors.IsRepoNotExist(err) {
+ log.Error(2, "GetRepositoryByID: %v", err)
+ continue
+ }
+ return nil, err
+ }
+ if repo.OwnerID == u.ID {
+ continue
+ }
+ repos[repo] = access.Mode
+ }
+ return repos, nil
+}
+
+// GetAccessibleRepositories finds repositories which the user has access but does not own.
+// If limit is smaller than 1 means returns all found results.
+func (user *User) GetAccessibleRepositories(limit int) (repos []*Repository, _ error) {
+ sess := x.Where("owner_id !=? ", user.ID).Desc("updated_unix")
+ if limit > 0 {
+ sess.Limit(limit)
+ repos = make([]*Repository, 0, limit)
+ } else {
+ repos = make([]*Repository, 0, 10)
+ }
+ return repos, sess.Join("INNER", "access", "access.user_id = ? AND access.repo_id = repository.id", user.ID).Find(&repos)
+}
+
+func maxAccessMode(modes ...AccessMode) AccessMode {
+ max := ACCESS_MODE_NONE
+ for _, mode := range modes {
+ if mode > max {
+ max = mode
+ }
+ }
+ return max
+}
+
+// FIXME: do corss-comparison so reduce deletions and additions to the minimum?
+func (repo *Repository) refreshAccesses(e Engine, accessMap map[int64]AccessMode) (err error) {
+ newAccesses := make([]Access, 0, len(accessMap))
+ for userID, mode := range accessMap {
+ newAccesses = append(newAccesses, Access{
+ UserID: userID,
+ RepoID: repo.ID,
+ Mode: mode,
+ })
+ }
+
+ // Delete old accesses and insert new ones for repository.
+ if _, err = e.Delete(&Access{RepoID: repo.ID}); err != nil {
+ return fmt.Errorf("delete old accesses: %v", err)
+ } else if _, err = e.Insert(newAccesses); err != nil {
+ return fmt.Errorf("insert new accesses: %v", err)
+ }
+ return nil
+}
+
+// refreshCollaboratorAccesses retrieves repository collaborations with their access modes.
+func (repo *Repository) refreshCollaboratorAccesses(e Engine, accessMap map[int64]AccessMode) error {
+ collaborations, err := repo.getCollaborations(e)
+ if err != nil {
+ return fmt.Errorf("getCollaborations: %v", err)
+ }
+ for _, c := range collaborations {
+ accessMap[c.UserID] = c.Mode
+ }
+ return nil
+}
+
+// recalculateTeamAccesses recalculates new accesses for teams of an organization
+// except the team whose ID is given. It is used to assign a team ID when
+// remove repository from that team.
+func (repo *Repository) recalculateTeamAccesses(e Engine, ignTeamID int64) (err error) {
+ accessMap := make(map[int64]AccessMode, 20)
+
+ if err = repo.getOwner(e); err != nil {
+ return err
+ } else if !repo.Owner.IsOrganization() {
+ return fmt.Errorf("owner is not an organization: %d", repo.OwnerID)
+ }
+
+ if err = repo.refreshCollaboratorAccesses(e, accessMap); err != nil {
+ return fmt.Errorf("refreshCollaboratorAccesses: %v", err)
+ }
+
+ if err = repo.Owner.getTeams(e); err != nil {
+ return err
+ }
+
+ for _, t := range repo.Owner.Teams {
+ if t.ID == ignTeamID {
+ continue
+ }
+
+ // Owner team gets owner access, and skip for teams that do not
+ // have relations with repository.
+ if t.IsOwnerTeam() {
+ t.Authorize = ACCESS_MODE_OWNER
+ } else if !t.hasRepository(e, repo.ID) {
+ continue
+ }
+
+ if err = t.getMembers(e); err != nil {
+ return fmt.Errorf("getMembers '%d': %v", t.ID, err)
+ }
+ for _, m := range t.Members {
+ accessMap[m.ID] = maxAccessMode(accessMap[m.ID], t.Authorize)
+ }
+ }
+
+ return repo.refreshAccesses(e, accessMap)
+}
+
+func (repo *Repository) recalculateAccesses(e Engine) error {
+ if repo.Owner.IsOrganization() {
+ return repo.recalculateTeamAccesses(e, 0)
+ }
+
+ accessMap := make(map[int64]AccessMode, 10)
+ if err := repo.refreshCollaboratorAccesses(e, accessMap); err != nil {
+ return fmt.Errorf("refreshCollaboratorAccesses: %v", err)
+ }
+ return repo.refreshAccesses(e, accessMap)
+}
+
+// RecalculateAccesses recalculates all accesses for repository.
+func (repo *Repository) RecalculateAccesses() error {
+ return repo.recalculateAccesses(x)
+}
diff --git a/internal/db/action.go b/internal/db/action.go
new file mode 100644
index 00000000..d6006410
--- /dev/null
+++ b/internal/db/action.go
@@ -0,0 +1,767 @@
+// Copyright 2014 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 db
+
+import (
+ "fmt"
+ "path"
+ "regexp"
+ "strings"
+ "time"
+ "unicode"
+
+ "github.com/json-iterator/go"
+ "github.com/unknwon/com"
+ log "gopkg.in/clog.v1"
+ "xorm.io/xorm"
+
+ "github.com/gogs/git-module"
+ api "github.com/gogs/go-gogs-client"
+
+ "gogs.io/gogs/internal/db/errors"
+ "gogs.io/gogs/internal/setting"
+ "gogs.io/gogs/internal/tool"
+)
+
+type ActionType int
+
+// Note: To maintain backward compatibility only append to the end of list
+const (
+ ACTION_CREATE_REPO ActionType = iota + 1 // 1
+ ACTION_RENAME_REPO // 2
+ ACTION_STAR_REPO // 3
+ ACTION_WATCH_REPO // 4
+ ACTION_COMMIT_REPO // 5
+ ACTION_CREATE_ISSUE // 6
+ ACTION_CREATE_PULL_REQUEST // 7
+ ACTION_TRANSFER_REPO // 8
+ ACTION_PUSH_TAG // 9
+ ACTION_COMMENT_ISSUE // 10
+ ACTION_MERGE_PULL_REQUEST // 11
+ ACTION_CLOSE_ISSUE // 12
+ ACTION_REOPEN_ISSUE // 13
+ ACTION_CLOSE_PULL_REQUEST // 14
+ ACTION_REOPEN_PULL_REQUEST // 15
+ ACTION_CREATE_BRANCH // 16
+ ACTION_DELETE_BRANCH // 17
+ ACTION_DELETE_TAG // 18
+ ACTION_FORK_REPO // 19
+ ACTION_MIRROR_SYNC_PUSH // 20
+ ACTION_MIRROR_SYNC_CREATE // 21
+ ACTION_MIRROR_SYNC_DELETE // 22
+)
+
+var (
+ // Same as Github. See https://help.github.com/articles/closing-issues-via-commit-messages
+ IssueCloseKeywords = []string{"close", "closes", "closed", "fix", "fixes", "fixed", "resolve", "resolves", "resolved"}
+ IssueReopenKeywords = []string{"reopen", "reopens", "reopened"}
+
+ IssueCloseKeywordsPat = regexp.MustCompile(assembleKeywordsPattern(IssueCloseKeywords))
+ IssueReopenKeywordsPat = regexp.MustCompile(assembleKeywordsPattern(IssueReopenKeywords))
+ IssueReferenceKeywordsPat = regexp.MustCompile(`(?i)(?:)(^| )\S+`)
+)
+
+func assembleKeywordsPattern(words []string) string {
+ return fmt.Sprintf(`(?i)(?:%s) \S+`, strings.Join(words, "|"))
+}
+
+// Action represents user operation type and other information to repository,
+// it implemented interface base.Actioner so that can be used in template render.
+type Action struct {
+ ID int64
+ UserID int64 // Receiver user ID
+ OpType ActionType
+ ActUserID int64 // Doer user ID
+ ActUserName string // Doer user name
+ ActAvatar string `xorm:"-" json:"-"`
+ RepoID int64 `xorm:"INDEX"`
+ RepoUserName string
+ RepoName string
+ RefName string
+ IsPrivate bool `xorm:"NOT NULL DEFAULT false"`
+ Content string `xorm:"TEXT"`
+ Created time.Time `xorm:"-" json:"-"`
+ CreatedUnix int64
+}
+
+func (a *Action) BeforeInsert() {
+ a.CreatedUnix = time.Now().Unix()
+}
+
+func (a *Action) AfterSet(colName string, _ xorm.Cell) {
+ switch colName {
+ case "created_unix":
+ a.Created = time.Unix(a.CreatedUnix, 0).Local()
+ }
+}
+
+func (a *Action) GetOpType() int {
+ return int(a.OpType)
+}
+
+func (a *Action) GetActUserName() string {
+ return a.ActUserName
+}
+
+func (a *Action) ShortActUserName() string {
+ return tool.EllipsisString(a.ActUserName, 20)
+}
+
+func (a *Action) GetRepoUserName() string {
+ return a.RepoUserName
+}
+
+func (a *Action) ShortRepoUserName() string {
+ return tool.EllipsisString(a.RepoUserName, 20)
+}
+
+func (a *Action) GetRepoName() string {
+ return a.RepoName
+}
+
+func (a *Action) ShortRepoName() string {
+ return tool.EllipsisString(a.RepoName, 33)
+}
+
+func (a *Action) GetRepoPath() string {
+ return path.Join(a.RepoUserName, a.RepoName)
+}
+
+func (a *Action) ShortRepoPath() string {
+ return path.Join(a.ShortRepoUserName(), a.ShortRepoName())
+}
+
+func (a *Action) GetRepoLink() string {
+ if len(setting.AppSubURL) > 0 {
+ return path.Join(setting.AppSubURL, a.GetRepoPath())
+ }
+ return "/" + a.GetRepoPath()
+}
+
+func (a *Action) GetBranch() string {
+ return a.RefName
+}
+
+func (a *Action) GetContent() string {
+ return a.Content
+}
+
+func (a *Action) GetCreate() time.Time {
+ return a.Created
+}
+
+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.Title
+}
+
+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, doer, owner *User, repo *Repository) (err error) {
+ opType := ACTION_CREATE_REPO
+ if repo.IsFork {
+ opType = ACTION_FORK_REPO
+ }
+
+ return notifyWatchers(e, &Action{
+ ActUserID: doer.ID,
+ ActUserName: doer.Name,
+ OpType: opType,
+ RepoID: repo.ID,
+ RepoUserName: repo.Owner.Name,
+ RepoName: repo.Name,
+ IsPrivate: repo.IsPrivate,
+ })
+}
+
+// NewRepoAction adds new action for creating repository.
+func NewRepoAction(doer, owner *User, repo *Repository) (err error) {
+ return newRepoAction(x, doer, owner, repo)
+}
+
+func renameRepoAction(e Engine, actUser *User, oldRepoName string, repo *Repository) (err error) {
+ if err = notifyWatchers(e, &Action{
+ ActUserID: actUser.ID,
+ ActUserName: actUser.Name,
+ OpType: ACTION_RENAME_REPO,
+ RepoID: repo.ID,
+ RepoUserName: repo.Owner.Name,
+ RepoName: repo.Name,
+ IsPrivate: repo.IsPrivate,
+ Content: oldRepoName,
+ }); err != nil {
+ return fmt.Errorf("notify watchers: %v", err)
+ }
+
+ log.Trace("action.renameRepoAction: %s/%s", actUser.Name, repo.Name)
+ return nil
+}
+
+// RenameRepoAction adds new action for renaming a repository.
+func RenameRepoAction(actUser *User, oldRepoName string, repo *Repository) error {
+ return renameRepoAction(x, actUser, oldRepoName, repo)
+}
+
+func issueIndexTrimRight(c rune) bool {
+ return !unicode.IsDigit(c)
+}
+
+type PushCommit struct {
+ Sha1 string
+ Message string
+ AuthorEmail string
+ AuthorName string
+ CommitterEmail string
+ CommitterName string
+ Timestamp time.Time
+}
+
+type PushCommits struct {
+ Len int
+ Commits []*PushCommit
+ CompareURL string
+
+ avatars map[string]string
+}
+
+func NewPushCommits() *PushCommits {
+ return &PushCommits{
+ avatars: make(map[string]string),
+ }
+}
+
+func (pc *PushCommits) ToApiPayloadCommits(repoPath, repoURL string) ([]*api.PayloadCommit, error) {
+ commits := make([]*api.PayloadCommit, len(pc.Commits))
+ for i, commit := range pc.Commits {
+ authorUsername := ""
+ author, err := GetUserByEmail(commit.AuthorEmail)
+ if err == nil {
+ authorUsername = author.Name
+ } else if !errors.IsUserNotExist(err) {
+ return nil, fmt.Errorf("GetUserByEmail: %v", err)
+ }
+
+ committerUsername := ""
+ committer, err := GetUserByEmail(commit.CommitterEmail)
+ if err == nil {
+ committerUsername = committer.Name
+ } else if !errors.IsUserNotExist(err) {
+ return nil, fmt.Errorf("GetUserByEmail: %v", err)
+ }
+
+ fileStatus, err := git.GetCommitFileStatus(repoPath, commit.Sha1)
+ if err != nil {
+ return nil, fmt.Errorf("FileStatus [commit_sha1: %s]: %v", commit.Sha1, err)
+ }
+
+ commits[i] = &api.PayloadCommit{
+ ID: commit.Sha1,
+ Message: commit.Message,
+ URL: fmt.Sprintf("%s/commit/%s", repoURL, commit.Sha1),
+ Author: &api.PayloadUser{
+ Name: commit.AuthorName,
+ Email: commit.AuthorEmail,
+ UserName: authorUsername,
+ },
+ Committer: &api.PayloadUser{
+ Name: commit.CommitterName,
+ Email: commit.CommitterEmail,
+ UserName: committerUsername,
+ },
+ Added: fileStatus.Added,
+ Removed: fileStatus.Removed,
+ Modified: fileStatus.Modified,
+ Timestamp: commit.Timestamp,
+ }
+ }
+ return commits, nil
+}
+
+// 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] = tool.AvatarLink(email)
+ if !errors.IsUserNotExist(err) {
+ log.Error(4, "GetUserByEmail: %v", err)
+ }
+ } else {
+ push.avatars[email] = u.RelAvatarLink()
+ }
+ }
+
+ return push.avatars[email]
+}
+
+// UpdateIssuesCommit checks if issues are manipulated by commit message.
+func UpdateIssuesCommit(doer *User, repo *Repository, commits []*PushCommit) error {
+ // Commits are appended in the reverse order.
+ for i := len(commits) - 1; i >= 0; i-- {
+ c := commits[i]
+
+ refMarked := make(map[int64]bool)
+ for _, ref := range IssueReferenceKeywordsPat.FindAllString(c.Message, -1) {
+ ref = ref[strings.IndexByte(ref, byte(' '))+1:]
+ ref = strings.TrimRightFunc(ref, issueIndexTrimRight)
+
+ if len(ref) == 0 {
+ continue
+ }
+
+ // Add repo name if missing
+ if ref[0] == '#' {
+ ref = fmt.Sprintf("%s%s", repo.FullName(), ref)
+ } else if !strings.Contains(ref, "/") {
+ // FIXME: We don't support User#ID syntax yet
+ // return ErrNotImplemented
+ continue
+ }
+
+ issue, err := GetIssueByRef(ref)
+ if err != nil {
+ if errors.IsIssueNotExist(err) {
+ continue
+ }
+ return err
+ }
+
+ if refMarked[issue.ID] {
+ continue
+ }
+ refMarked[issue.ID] = true
+
+ msgLines := strings.Split(c.Message, "\n")
+ shortMsg := msgLines[0]
+ if len(msgLines) > 2 {
+ shortMsg += "..."
+ }
+ message := fmt.Sprintf(`<a href="%s/commit/%s">%s</a>`, repo.Link(), c.Sha1, shortMsg)
+ if err = CreateRefComment(doer, repo, issue, message, c.Sha1); err != nil {
+ return err
+ }
+ }
+
+ refMarked = make(map[int64]bool)
+ // FIXME: can merge this one and next one to a common function.
+ for _, ref := range IssueCloseKeywordsPat.FindAllString(c.Message, -1) {
+ ref = ref[strings.IndexByte(ref, byte(' '))+1:]
+ ref = strings.TrimRightFunc(ref, issueIndexTrimRight)
+
+ if len(ref) == 0 {
+ continue
+ }
+
+ // Add repo name if missing
+ if ref[0] == '#' {
+ ref = fmt.Sprintf("%s%s", repo.FullName(), ref)
+ } else if !strings.Contains(ref, "/") {
+ // FIXME: We don't support User#ID syntax yet
+ continue
+ }
+
+ issue, err := GetIssueByRef(ref)
+ if err != nil {
+ if errors.IsIssueNotExist(err) {
+ continue
+ }
+ return err
+ }
+
+ if refMarked[issue.ID] {
+ continue
+ }
+ refMarked[issue.ID] = true
+
+ if issue.RepoID != repo.ID || issue.IsClosed {
+ continue
+ }
+
+ if err = issue.ChangeStatus(doer, repo, true); err != nil {
+ return err
+ }
+ }
+
+ // It is conflict to have close and reopen at same time, so refsMarkd doesn't need to reinit here.
+ for _, ref := range IssueReopenKeywordsPat.FindAllString(c.Message, -1) {
+ ref = ref[strings.IndexByte(ref, byte(' '))+1:]
+ ref = strings.TrimRightFunc(ref, issueIndexTrimRight)
+
+ if len(ref) == 0 {
+ continue
+ }
+
+ // Add repo name if missing
+ if ref[0] == '#' {
+ ref = fmt.Sprintf("%s%s", repo.FullName(), ref)
+ } else if !strings.Contains(ref, "/") {
+ // We don't support User#ID syntax yet
+ // return ErrNotImplemented
+ continue
+ }
+
+ issue, err := GetIssueByRef(ref)
+ if err != nil {
+ if errors.IsIssueNotExist(err) {
+ continue
+ }
+ return err
+ }
+
+ if refMarked[issue.ID] {
+ continue
+ }
+ refMarked[issue.ID] = true
+
+ if issue.RepoID != repo.ID || !issue.IsClosed {
+ continue
+ }
+
+ if err = issue.ChangeStatus(doer, repo, false); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+type CommitRepoActionOptions struct {
+ PusherName string
+ RepoOwnerID int64
+ RepoName string
+ RefFullName string
+ OldCommitID string
+ NewCommitID string
+ Commits *PushCommits
+}
+
+// CommitRepoAction adds new commit actio to the repository, and prepare corresponding webhooks.
+func CommitRepoAction(opts CommitRepoActionOptions) error {
+ pusher, err := GetUserByName(opts.PusherName)
+ if err != nil {
+ return fmt.Errorf("GetUserByName [%s]: %v", opts.PusherName, err)
+ }
+
+ repo, err := GetRepositoryByName(opts.RepoOwnerID, opts.RepoName)
+ if err != nil {
+ return fmt.Errorf("GetRepositoryByName [owner_id: %d, name: %s]: %v", opts.RepoOwnerID, opts.RepoName, 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)
+ }
+
+ isNewRef := opts.OldCommitID == git.EMPTY_SHA
+ isDelRef := opts.NewCommitID == git.EMPTY_SHA
+
+ opType := ACTION_COMMIT_REPO
+ // Check if it's tag push or branch.
+ if strings.HasPrefix(opts.RefFullName, git.TAG_PREFIX) {
+ opType = ACTION_PUSH_TAG
+ } else {
+ // if not the first commit, set the compare URL.
+ if !isNewRef && !isDelRef {
+ opts.Commits.CompareURL = repo.ComposeCompareURL(opts.OldCommitID, opts.NewCommitID)
+ }
+
+ // Only update issues via commits when internal issue tracker is enabled
+ if repo.EnableIssues && !repo.EnableExternalTracker {
+ if err = UpdateIssuesCommit(pusher, repo, opts.Commits.Commits); err != nil {
+ log.Error(2, "UpdateIssuesCommit: %v", err)
+ }
+ }
+ }
+
+ if len(opts.Commits.Commits) > setting.UI.FeedMaxCommitNum {
+ opts.Commits.Commits = opts.Commits.Commits[:setting.UI.FeedMaxCommitNum]
+ }
+
+ data, err := jsoniter.Marshal(opts.Commits)
+ if err != nil {
+ return fmt.Errorf("Marshal: %v", err)
+ }
+
+ refName := git.RefEndName(opts.RefFullName)
+ action := &Action{
+ ActUserID: pusher.ID,
+ ActUserName: pusher.Name,
+ Content: string(data),
+ RepoID: repo.ID,
+ RepoUserName: repo.MustOwner().Name,
+ RepoName: repo.Name,
+ RefName: refName,
+ IsPrivate: repo.IsPrivate,
+ }
+
+ apiRepo := repo.APIFormat(nil)
+ apiPusher := pusher.APIFormat()
+ switch opType {
+ case ACTION_COMMIT_REPO: // Push
+ if isDelRef {
+ if err = PrepareWebhooks(repo, HOOK_EVENT_DELETE, &api.DeletePayload{
+ Ref: refName,
+ RefType: "branch",
+ PusherType: api.PUSHER_TYPE_USER,
+ Repo: apiRepo,
+ Sender: apiPusher,
+ }); err != nil {
+ return fmt.Errorf("PrepareWebhooks.(delete branch): %v", err)
+ }
+
+ action.OpType = ACTION_DELETE_BRANCH
+ if err = NotifyWatchers(action); err != nil {
+ return fmt.Errorf("NotifyWatchers.(delete branch): %v", err)
+ }
+
+ // Delete branch doesn't have anything to push or compare
+ return nil
+ }
+
+ compareURL := setting.AppURL + opts.Commits.CompareURL
+ if isNewRef {
+ compareURL = ""
+ if err = PrepareWebhooks(repo, HOOK_EVENT_CREATE, &api.CreatePayload{
+ Ref: refName,
+ RefType: "branch",
+ DefaultBranch: repo.DefaultBranch,
+ Repo: apiRepo,
+ Sender: apiPusher,
+ }); err != nil {
+ return fmt.Errorf("PrepareWebhooks.(new branch): %v", err)
+ }
+
+ action.OpType = ACTION_CREATE_BRANCH
+ if err = NotifyWatchers(action); err != nil {
+ return fmt.Errorf("NotifyWatchers.(new branch): %v", err)
+ }
+ }
+
+ commits, err := opts.Commits.ToApiPayloadCommits(repo.RepoPath(), repo.HTMLURL())
+ if err != nil {
+ return fmt.Errorf("ToApiPayloadCommits: %v", err)
+ }
+
+ if err = PrepareWebhooks(repo, HOOK_EVENT_PUSH, &api.PushPayload{
+ Ref: opts.RefFullName,
+ Before: opts.OldCommitID,
+ After: opts.NewCommitID,
+ CompareURL: compareURL,
+ Commits: commits,
+ Repo: apiRepo,
+ Pusher: apiPusher,
+ Sender: apiPusher,
+ }); err != nil {
+ return fmt.Errorf("PrepareWebhooks.(new commit): %v", err)
+ }
+
+ action.OpType = ACTION_COMMIT_REPO
+ if err = NotifyWatchers(action); err != nil {
+ return fmt.Errorf("NotifyWatchers.(new commit): %v", err)
+ }
+
+ case ACTION_PUSH_TAG: // Tag
+ if isDelRef {
+ if err = PrepareWebhooks(repo, HOOK_EVENT_DELETE, &api.DeletePayload{
+ Ref: refName,
+ RefType: "tag",
+ PusherType: api.PUSHER_TYPE_USER,
+ Repo: apiRepo,
+ Sender: apiPusher,
+ }); err != nil {
+ return fmt.Errorf("PrepareWebhooks.(delete tag): %v", err)
+ }
+
+ action.OpType = ACTION_DELETE_TAG
+ if err = NotifyWatchers(action); err != nil {
+ return fmt.Errorf("NotifyWatchers.(delete tag): %v", err)
+ }
+ return nil
+ }
+
+ if err = PrepareWebhooks(repo, HOOK_EVENT_CREATE, &api.CreatePayload{
+ Ref: refName,
+ RefType: "tag",
+ Sha: opts.NewCommitID,
+ DefaultBranch: repo.DefaultBranch,
+ Repo: apiRepo,
+ Sender: apiPusher,
+ }); err != nil {
+ return fmt.Errorf("PrepareWebhooks.(new tag): %v", err)
+ }
+
+ action.OpType = ACTION_PUSH_TAG
+ if err = NotifyWatchers(action); err != nil {
+ return fmt.Errorf("NotifyWatchers.(new tag): %v", err)
+ }
+ }
+
+ return nil
+}
+
+func transferRepoAction(e Engine, doer, oldOwner *User, repo *Repository) (err error) {
+ if err = notifyWatchers(e, &Action{
+ ActUserID: doer.ID,
+ ActUserName: doer.Name,
+ OpType: ACTION_TRANSFER_REPO,
+ RepoID: repo.ID,
+ RepoUserName: repo.Owner.Name,
+ RepoName: repo.Name,
+ IsPrivate: repo.IsPrivate,
+ Content: path.Join(oldOwner.Name, repo.Name),
+ }); err != nil {
+ return fmt.Errorf("notifyWatchers: %v", err)
+ }
+
+ // Remove watch for organization.
+ if oldOwner.IsOrganization() {
+ if err = watchRepo(e, oldOwner.ID, repo.ID, false); err != nil {
+ return fmt.Errorf("watchRepo [false]: %v", err)
+ }
+ }
+
+ return nil
+}
+
+// TransferRepoAction adds new action for transferring repository,
+// the Owner field of repository is assumed to be new owner.
+func TransferRepoAction(doer, oldOwner *User, repo *Repository) error {
+ return transferRepoAction(x, doer, oldOwner, repo)
+}
+
+func mergePullRequestAction(e Engine, doer *User, repo *Repository, issue *Issue) error {
+ return notifyWatchers(e, &Action{
+ ActUserID: doer.ID,
+ ActUserName: doer.Name,
+ OpType: ACTION_MERGE_PULL_REQUEST,
+ Content: fmt.Sprintf("%d|%s", issue.Index, issue.Title),
+ RepoID: repo.ID,
+ RepoUserName: repo.Owner.Name,
+ RepoName: repo.Name,
+ IsPrivate: repo.IsPrivate,
+ })
+}
+
+// MergePullRequestAction adds new action for merging pull request.
+func MergePullRequestAction(actUser *User, repo *Repository, pull *Issue) error {
+ return mergePullRequestAction(x, actUser, repo, pull)
+}
+
+func mirrorSyncAction(opType ActionType, repo *Repository, refName string, data []byte) error {
+ return NotifyWatchers(&Action{
+ ActUserID: repo.OwnerID,
+ ActUserName: repo.MustOwner().Name,
+ OpType: opType,
+ Content: string(data),
+ RepoID: repo.ID,
+ RepoUserName: repo.MustOwner().Name,
+ RepoName: repo.Name,
+ RefName: refName,
+ IsPrivate: repo.IsPrivate,
+ })
+}
+
+type MirrorSyncPushActionOptions struct {
+ RefName string
+ OldCommitID string
+ NewCommitID string
+ Commits *PushCommits
+}
+
+// MirrorSyncPushAction adds new action for mirror synchronization of pushed commits.
+func MirrorSyncPushAction(repo *Repository, opts MirrorSyncPushActionOptions) error {
+ if len(opts.Commits.Commits) > setting.UI.FeedMaxCommitNum {
+ opts.Commits.Commits = opts.Commits.Commits[:setting.UI.FeedMaxCommitNum]
+ }
+
+ apiCommits, err := opts.Commits.ToApiPayloadCommits(repo.RepoPath(), repo.HTMLURL())
+ if err != nil {
+ return fmt.Errorf("ToApiPayloadCommits: %v", err)
+ }
+
+ opts.Commits.CompareURL = repo.ComposeCompareURL(opts.OldCommitID, opts.NewCommitID)
+ apiPusher := repo.MustOwner().APIFormat()
+ if err := PrepareWebhooks(repo, HOOK_EVENT_PUSH, &api.PushPayload{
+ Ref: opts.RefName,
+ Before: opts.OldCommitID,
+ After: opts.NewCommitID,
+ CompareURL: setting.AppURL + opts.Commits.CompareURL,
+ Commits: apiCommits,
+ Repo: repo.APIFormat(nil),
+ Pusher: apiPusher,
+ Sender: apiPusher,
+ }); err != nil {
+ return fmt.Errorf("PrepareWebhooks: %v", err)
+ }
+
+ data, err := jsoniter.Marshal(opts.Commits)
+ if err != nil {
+ return err
+ }
+
+ return mirrorSyncAction(ACTION_MIRROR_SYNC_PUSH, repo, opts.RefName, data)
+}
+
+// MirrorSyncCreateAction adds new action for mirror synchronization of new reference.
+func MirrorSyncCreateAction(repo *Repository, refName string) error {
+ return mirrorSyncAction(ACTION_MIRROR_SYNC_CREATE, repo, refName, nil)
+}
+
+// MirrorSyncCreateAction adds new action for mirror synchronization of delete reference.
+func MirrorSyncDeleteAction(repo *Repository, refName string) error {
+ return mirrorSyncAction(ACTION_MIRROR_SYNC_DELETE, repo, refName, nil)
+}
+
+// GetFeeds returns action list of given user in given context.
+// actorID is the user who's requesting, ctxUserID is the user/org that is requested.
+// actorID can be -1 when isProfile is true or to skip the permission check.
+func GetFeeds(ctxUser *User, actorID, afterID int64, isProfile bool) ([]*Action, error) {
+ actions := make([]*Action, 0, setting.UI.User.NewsFeedPagingNum)
+ sess := x.Limit(setting.UI.User.NewsFeedPagingNum).Where("user_id = ?", ctxUser.ID).Desc("id")
+ if afterID > 0 {
+ sess.And("id < ?", afterID)
+ }
+ if isProfile {
+ sess.And("is_private = ?", false).And("act_user_id = ?", ctxUser.ID)
+ } else if actorID != -1 && ctxUser.IsOrganization() {
+ // FIXME: only need to get IDs here, not all fields of repository.
+ repos, _, err := ctxUser.GetUserRepositories(actorID, 1, ctxUser.NumRepos)
+ if err != nil {
+ return nil, fmt.Errorf("GetUserRepositories: %v", err)
+ }
+
+ var repoIDs []int64
+ for _, repo := range repos {
+ repoIDs = append(repoIDs, repo.ID)
+ }
+
+ if len(repoIDs) > 0 {
+ sess.In("repo_id", repoIDs)
+ }
+ }
+
+ err := sess.Find(&actions)
+ return actions, err
+}
diff --git a/internal/db/admin.go b/internal/db/admin.go
new file mode 100644
index 00000000..1fe13002
--- /dev/null
+++ b/internal/db/admin.go
@@ -0,0 +1,118 @@
+// Copyright 2014 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 db
+
+import (
+ "fmt"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/unknwon/com"
+ log "gopkg.in/clog.v1"
+ "xorm.io/xorm"
+
+ "gogs.io/gogs/internal/tool"
+)
+
+type NoticeType int
+
+const (
+ NOTICE_REPOSITORY NoticeType = iota + 1
+)
+
+// Notice represents a system notice for admin.
+type Notice struct {
+ ID int64
+ Type NoticeType
+ Description string `xorm:"TEXT"`
+ Created time.Time `xorm:"-" json:"-"`
+ CreatedUnix int64
+}
+
+func (n *Notice) BeforeInsert() {
+ n.CreatedUnix = time.Now().Unix()
+}
+
+func (n *Notice) AfterSet(colName string, _ xorm.Cell) {
+ switch colName {
+ case "created_unix":
+ n.Created = time.Unix(n.CreatedUnix, 0).Local()
+ }
+}
+
+// TrStr returns a translation format string.
+func (n *Notice) TrStr() string {
+ return "admin.notices.type_" + com.ToStr(n.Type)
+}
+
+// CreateNotice creates new system notice.
+func CreateNotice(tp NoticeType, desc string) error {
+ // Prevent panic if database connection is not available at this point
+ if x == nil {
+ return fmt.Errorf("could not save notice due database connection not being available: %d %s", tp, desc)
+ }
+
+ n := &Notice{
+ Type: tp,
+ Description: desc,
+ }
+ _, err := x.Insert(n)
+ return err
+}
+
+// CreateRepositoryNotice creates new system notice with type NOTICE_REPOSITORY.
+func CreateRepositoryNotice(desc string) error {
+ return CreateNotice(NOTICE_REPOSITORY, desc)
+}
+
+// RemoveAllWithNotice removes all directories in given path and
+// creates a system notice when error occurs.
+func RemoveAllWithNotice(title, path string) {
+ if err := os.RemoveAll(path); err != nil {
+ desc := fmt.Sprintf("%s [%s]: %v", title, path, err)
+ log.Warn(desc)
+ if err = CreateRepositoryNotice(desc); err != nil {
+ log.Error(2, "CreateRepositoryNotice: %v", err)
+ }
+ }
+}
+
+// CountNotices returns number of notices.
+func CountNotices() int64 {
+ count, _ := x.Count(new(Notice))
+ return count
+}
+
+// Notices returns number of notices in given page.
+func Notices(page, pageSize int) ([]*Notice, error) {
+ notices := make([]*Notice, 0, pageSize)
+ return notices, x.Limit(pageSize, (page-1)*pageSize).Desc("id").Find(&notices)
+}
+
+// DeleteNotice deletes a system notice by given ID.
+func DeleteNotice(id int64) error {
+ _, err := x.Id(id).Delete(new(Notice))
+ return err
+}
+
+// DeleteNotices deletes all notices with ID from start to end (inclusive).
+func DeleteNotices(start, end int64) error {
+ sess := x.Where("id >= ?", start)
+ if end > 0 {
+ sess.And("id <= ?", end)
+ }
+ _, err := sess.Delete(new(Notice))
+ return err
+}
+
+// DeleteNoticesByIDs deletes notices by given IDs.
+func DeleteNoticesByIDs(ids []int64) error {
+ if len(ids) == 0 {
+ return nil
+ }
+ _, err := x.Where("id IN (" + strings.Join(tool.Int64sToStrings(ids), ",") + ")").Delete(new(Notice))
+ return err
+}
diff --git a/internal/db/attachment.go b/internal/db/attachment.go
new file mode 100644
index 00000000..1494002d
--- /dev/null
+++ b/internal/db/attachment.go
@@ -0,0 +1,183 @@
+// Copyright 2017 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 db
+
+import (
+ "fmt"
+ "io"
+ "mime/multipart"
+ "os"
+ "path"
+ "time"
+
+ gouuid "github.com/satori/go.uuid"
+ "xorm.io/xorm"
+
+ "gogs.io/gogs/internal/setting"
+)
+
+// Attachment represent a attachment of issue/comment/release.
+type Attachment struct {
+ ID int64
+ UUID string `xorm:"uuid UNIQUE"`
+ IssueID int64 `xorm:"INDEX"`
+ CommentID int64
+ ReleaseID int64 `xorm:"INDEX"`
+ Name string
+
+ Created time.Time `xorm:"-" json:"-"`
+ CreatedUnix int64
+}
+
+func (a *Attachment) BeforeInsert() {
+ a.CreatedUnix = time.Now().Unix()
+}
+
+func (a *Attachment) AfterSet(colName string, _ xorm.Cell) {
+ switch colName {
+ case "created_unix":
+ a.Created = time.Unix(a.CreatedUnix, 0).Local()
+ }
+}
+
+// AttachmentLocalPath returns where attachment is stored in local file system based on given UUID.
+func AttachmentLocalPath(uuid string) string {
+ return path.Join(setting.AttachmentPath, uuid[0:1], uuid[1:2], uuid)
+}
+
+// LocalPath returns where attachment is stored in local file system.
+func (attach *Attachment) LocalPath() string {
+ return AttachmentLocalPath(attach.UUID)
+}
+
+// NewAttachment creates a new attachment object.
+func NewAttachment(name string, buf []byte, file multipart.File) (_ *Attachment, err error) {
+ attach := &Attachment{
+ UUID: gouuid.NewV4().String(),
+ Name: name,
+ }
+
+ localPath := attach.LocalPath()
+ if err = os.MkdirAll(path.Dir(localPath), os.ModePerm); err != nil {
+ return nil, fmt.Errorf("MkdirAll: %v", err)
+ }
+
+ fw, err := os.Create(localPath)
+ if err != nil {
+ return nil, fmt.Errorf("Create: %v", err)
+ }
+ defer fw.Close()
+
+ if _, err = fw.Write(buf); err != nil {
+ return nil, fmt.Errorf("Write: %v", err)
+ } else if _, err = io.Copy(fw, file); err != nil {
+ return nil, fmt.Errorf("Copy: %v", err)
+ }
+
+ if _, err := x.Insert(attach); err != nil {
+ return nil, err
+ }
+
+ return attach, nil
+}
+
+func getAttachmentByUUID(e Engine, uuid string) (*Attachment, error) {
+ attach := &Attachment{UUID: uuid}
+ has, err := x.Get(attach)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, ErrAttachmentNotExist{0, uuid}
+ }
+ return attach, nil
+}
+
+func getAttachmentsByUUIDs(e Engine, uuids []string) ([]*Attachment, error) {
+ if len(uuids) == 0 {
+ return []*Attachment{}, nil
+ }
+
+ // Silently drop invalid uuids.
+ attachments := make([]*Attachment, 0, len(uuids))
+ return attachments, e.In("uuid", uuids).Find(&attachments)
+}
+
+// GetAttachmentByUUID returns attachment by given UUID.
+func GetAttachmentByUUID(uuid string) (*Attachment, error) {
+ return getAttachmentByUUID(x, uuid)
+}
+
+func getAttachmentsByIssueID(e Engine, issueID int64) ([]*Attachment, error) {
+ attachments := make([]*Attachment, 0, 5)
+ return attachments, e.Where("issue_id = ? AND comment_id = 0", issueID).Find(&attachments)
+}
+
+// GetAttachmentsByIssueID returns all attachments of an issue.
+func GetAttachmentsByIssueID(issueID int64) ([]*Attachment, error) {
+ return getAttachmentsByIssueID(x, issueID)
+}
+
+func getAttachmentsByCommentID(e Engine, commentID int64) ([]*Attachment, error) {
+ attachments := make([]*Attachment, 0, 5)
+ return attachments, e.Where("comment_id=?", commentID).Find(&attachments)
+}
+
+// GetAttachmentsByCommentID returns all attachments of a comment.
+func GetAttachmentsByCommentID(commentID int64) ([]*Attachment, error) {
+ return getAttachmentsByCommentID(x, commentID)
+}
+
+func getAttachmentsByReleaseID(e Engine, releaseID int64) ([]*Attachment, error) {
+ attachments := make([]*Attachment, 0, 10)
+ return attachments, e.Where("release_id = ?", releaseID).Find(&attachments)
+}
+
+// GetAttachmentsByReleaseID returns all attachments of a release.
+func GetAttachmentsByReleaseID(releaseID int64) ([]*Attachment, error) {
+ return getAttachmentsByReleaseID(x, releaseID)
+}
+
+// DeleteAttachment deletes the given attachment and optionally the associated file.
+func DeleteAttachment(a *Attachment, remove bool) error {
+ _, err := DeleteAttachments([]*Attachment{a}, remove)
+ return err
+}
+
+// DeleteAttachments deletes the given attachments and optionally the associated files.
+func DeleteAttachments(attachments []*Attachment, remove bool) (int, error) {
+ for i, a := range attachments {
+ if remove {
+ if err := os.Remove(a.LocalPath()); err != nil {
+ return i, err
+ }
+ }
+
+ if _, err := x.Delete(a); err != nil {
+ return i, err
+ }
+ }
+
+ return len(attachments), nil
+}
+
+// DeleteAttachmentsByIssue deletes all attachments associated with the given issue.
+func DeleteAttachmentsByIssue(issueId int64, remove bool) (int, error) {
+ attachments, err := GetAttachmentsByIssueID(issueId)
+ if err != nil {
+ return 0, err
+ }
+
+ return DeleteAttachments(attachments, remove)
+}
+
+// DeleteAttachmentsByComment deletes all attachments associated with the given comment.
+func DeleteAttachmentsByComment(commentId int64, remove bool) (int, error) {
+ attachments, err := GetAttachmentsByCommentID(commentId)
+ if err != nil {
+ return 0, err
+ }
+
+ return DeleteAttachments(attachments, remove)
+}
diff --git a/internal/db/comment.go b/internal/db/comment.go
new file mode 100644
index 00000000..b2f19508
--- /dev/null
+++ b/internal/db/comment.go
@@ -0,0 +1,534 @@
+// Copyright 2016 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 db
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/unknwon/com"
+ log "gopkg.in/clog.v1"
+ "xorm.io/xorm"
+
+ api "github.com/gogs/go-gogs-client"
+
+ "gogs.io/gogs/internal/db/errors"
+ "gogs.io/gogs/internal/markup"
+)
+
+// CommentType defines whether a comment is just a simple comment, an action (like close) or a reference.
+type CommentType int
+
+const (
+ // Plain comment, can be associated with a commit (CommitID > 0) and a line (LineNum > 0)
+ COMMENT_TYPE_COMMENT CommentType = iota
+ COMMENT_TYPE_REOPEN
+ COMMENT_TYPE_CLOSE
+
+ // References.
+ COMMENT_TYPE_ISSUE_REF
+ // Reference from a commit (not part of a pull request)
+ COMMENT_TYPE_COMMIT_REF
+ // Reference from a comment
+ COMMENT_TYPE_COMMENT_REF
+ // Reference from a pull request
+ COMMENT_TYPE_PULL_REF
+)
+
+type CommentTag int
+
+const (
+ COMMENT_TAG_NONE CommentTag = iota
+ COMMENT_TAG_POSTER
+ COMMENT_TAG_WRITER
+ COMMENT_TAG_OWNER
+)
+
+// Comment represents a comment in commit and issue page.
+type Comment struct {
+ ID int64
+ Type CommentType
+ PosterID int64
+ Poster *User `xorm:"-" json:"-"`
+ IssueID int64 `xorm:"INDEX"`
+ Issue *Issue `xorm:"-" json:"-"`
+ CommitID int64
+ Line int64
+ Content string `xorm:"TEXT"`
+ RenderedContent string `xorm:"-" json:"-"`
+
+ Created time.Time `xorm:"-" json:"-"`
+ CreatedUnix int64
+ Updated time.Time `xorm:"-" json:"-"`
+ UpdatedUnix int64
+
+ // Reference issue in commit message
+ CommitSHA string `xorm:"VARCHAR(40)"`
+
+ Attachments []*Attachment `xorm:"-" json:"-"`
+
+ // For view issue page.
+ ShowTag CommentTag `xorm:"-" json:"-"`
+}
+
+func (c *Comment) BeforeInsert() {
+ c.CreatedUnix = time.Now().Unix()
+ c.UpdatedUnix = c.CreatedUnix
+}
+
+func (c *Comment) BeforeUpdate() {
+ c.UpdatedUnix = time.Now().Unix()
+}
+
+func (c *Comment) AfterSet(colName string, _ xorm.Cell) {
+ switch colName {
+ case "created_unix":
+ c.Created = time.Unix(c.CreatedUnix, 0).Local()
+ case "updated_unix":
+ c.Updated = time.Unix(c.UpdatedUnix, 0).Local()
+ }
+}
+
+func (c *Comment) loadAttributes(e Engine) (err error) {
+ if c.Poster == nil {
+ c.Poster, err = GetUserByID(c.PosterID)
+ if err != nil {
+ if errors.IsUserNotExist(err) {
+ c.PosterID = -1
+ c.Poster = NewGhostUser()
+ } else {
+ return fmt.Errorf("getUserByID.(Poster) [%d]: %v", c.PosterID, err)
+ }
+ }
+ }
+
+ if c.Issue == nil {
+ c.Issue, err = getRawIssueByID(e, c.IssueID)
+ if err != nil {
+ return fmt.Errorf("getIssueByID [%d]: %v", c.IssueID, err)
+ }
+ if c.Issue.Repo == nil {
+ c.Issue.Repo, err = getRepositoryByID(e, c.Issue.RepoID)
+ if err != nil {
+ return fmt.Errorf("getRepositoryByID [%d]: %v", c.Issue.RepoID, err)
+ }
+ }
+ }
+
+ if c.Attachments == nil {
+ c.Attachments, err = getAttachmentsByCommentID(e, c.ID)
+ if err != nil {
+ return fmt.Errorf("getAttachmentsByCommentID [%d]: %v", c.ID, err)
+ }
+ }
+
+ return nil
+}
+
+func (c *Comment) LoadAttributes() error {
+ return c.loadAttributes(x)
+}
+
+func (c *Comment) HTMLURL() string {
+ return fmt.Sprintf("%s#issuecomment-%d", c.Issue.HTMLURL(), c.ID)
+}
+
+// This method assumes following fields have been assigned with valid values:
+// Required - Poster, Issue
+func (c *Comment) APIFormat() *api.Comment {
+ return &api.Comment{
+ ID: c.ID,
+ HTMLURL: c.HTMLURL(),
+ Poster: c.Poster.APIFormat(),
+ Body: c.Content,
+ Created: c.Created,
+ Updated: c.Updated,
+ }
+}
+
+func CommentHashTag(id int64) string {
+ return "issuecomment-" + com.ToStr(id)
+}
+
+// HashTag returns unique hash tag for comment.
+func (c *Comment) HashTag() string {
+ return CommentHashTag(c.ID)
+}
+
+// EventTag returns unique event hash tag for comment.
+func (c *Comment) EventTag() string {
+ return "event-" + com.ToStr(c.ID)
+}
+
+// mailParticipants sends new comment emails to repository watchers
+// and mentioned people.
+func (cmt *Comment) mailParticipants(e Engine, opType ActionType, issue *Issue) (err error) {
+ mentions := markup.FindAllMentions(cmt.Content)
+ if err = updateIssueMentions(e, cmt.IssueID, mentions); err != nil {
+ return fmt.Errorf("UpdateIssueMentions [%d]: %v", cmt.IssueID, err)
+ }
+
+ switch opType {
+ case ACTION_COMMENT_ISSUE:
+ issue.Content = cmt.Content
+ case ACTION_CLOSE_ISSUE:
+ issue.Content = fmt.Sprintf("Closed #%d", issue.Index)
+ case ACTION_REOPEN_ISSUE:
+ issue.Content = fmt.Sprintf("Reopened #%d", issue.Index)
+ }
+ if err = mailIssueCommentToParticipants(issue, cmt.Poster, mentions); err != nil {
+ log.Error(2, "mailIssueCommentToParticipants: %v", err)
+ }
+
+ return nil
+}
+
+func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err error) {
+ comment := &Comment{
+ Type: opts.Type,
+ PosterID: opts.Doer.ID,
+ Poster: opts.Doer,
+ IssueID: opts.Issue.ID,
+ CommitID: opts.CommitID,
+ CommitSHA: opts.CommitSHA,
+ Line: opts.LineNum,
+ Content: opts.Content,
+ }
+ if _, err = e.Insert(comment); err != nil {
+ return nil, err
+ }
+
+ // 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{
+ ActUserID: opts.Doer.ID,
+ ActUserName: opts.Doer.Name,
+ Content: fmt.Sprintf("%d|%s", opts.Issue.Index, strings.Split(opts.Content, "\n")[0]),
+ RepoID: opts.Repo.ID,
+ RepoUserName: opts.Repo.Owner.Name,
+ RepoName: opts.Repo.Name,
+ IsPrivate: opts.Repo.IsPrivate,
+ }
+
+ // Check comment type.
+ switch opts.Type {
+ case COMMENT_TYPE_COMMENT:
+ act.OpType = ACTION_COMMENT_ISSUE
+
+ if _, err = e.Exec("UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", opts.Issue.ID); err != nil {
+ return nil, err
+ }
+
+ // Check attachments
+ attachments := make([]*Attachment, 0, len(opts.Attachments))
+ for _, uuid := range opts.Attachments {
+ attach, err := getAttachmentByUUID(e, uuid)
+ if err != nil {
+ if IsErrAttachmentNotExist(err) {
+ continue
+ }
+ return nil, fmt.Errorf("getAttachmentByUUID [%s]: %v", uuid, err)
+ }
+ attachments = append(attachments, attach)
+ }
+
+ for i := range attachments {
+ attachments[i].IssueID = opts.Issue.ID
+ attachments[i].CommentID = comment.ID
+ // No assign value could be 0, so ignore AllCols().
+ if _, err = e.Id(attachments[i].ID).Update(attachments[i]); err != nil {
+ return nil, fmt.Errorf("update attachment [%d]: %v", attachments[i].ID, err)
+ }
+ }
+
+ case COMMENT_TYPE_REOPEN:
+ act.OpType = ACTION_REOPEN_ISSUE
+ if opts.Issue.IsPull {
+ act.OpType = ACTION_REOPEN_PULL_REQUEST
+ }
+
+ if opts.Issue.IsPull {
+ _, err = e.Exec("UPDATE `repository` SET num_closed_pulls=num_closed_pulls-1 WHERE id=?", opts.Repo.ID)
+ } else {
+ _, err = e.Exec("UPDATE `repository` SET num_closed_issues=num_closed_issues-1 WHERE id=?", opts.Repo.ID)
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ case COMMENT_TYPE_CLOSE:
+ act.OpType = ACTION_CLOSE_ISSUE
+ if opts.Issue.IsPull {
+ act.OpType = ACTION_CLOSE_PULL_REQUEST
+ }
+
+ if opts.Issue.IsPull {
+ _, err = e.Exec("UPDATE `repository` SET num_closed_pulls=num_closed_pulls+1 WHERE id=?", opts.Repo.ID)
+ } else {
+ _, err = e.Exec("UPDATE `repository` SET num_closed_issues=num_closed_issues+1 WHERE id=?", opts.Repo.ID)
+ }
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ if _, err = e.Exec("UPDATE `issue` SET updated_unix = ? WHERE id = ?", time.Now().Unix(), opts.Issue.ID); err != nil {
+ return nil, fmt.Errorf("update issue 'updated_unix': %v", err)
+ }
+
+ // Notify watchers for whatever action comes in, ignore if no action type.
+ if act.OpType > 0 {
+ if err = notifyWatchers(e, act); err != nil {
+ log.Error(2, "notifyWatchers: %v", err)
+ }
+ if err = comment.mailParticipants(e, act.OpType, opts.Issue); err != nil {
+ log.Error(2, "MailParticipants: %v", err)
+ }
+ }
+
+ return comment, comment.loadAttributes(e)
+}
+
+func createStatusComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue) (*Comment, error) {
+ cmtType := COMMENT_TYPE_CLOSE
+ if !issue.IsClosed {
+ cmtType = COMMENT_TYPE_REOPEN
+ }
+ return createComment(e, &CreateCommentOptions{
+ Type: cmtType,
+ Doer: doer,
+ Repo: repo,
+ Issue: issue,
+ })
+}
+
+type CreateCommentOptions struct {
+ Type CommentType
+ Doer *User
+ Repo *Repository
+ Issue *Issue
+
+ CommitID int64
+ CommitSHA string
+ LineNum int64
+ Content string
+ Attachments []string // UUIDs of attachments
+}
+
+// CreateComment creates comment of issue or commit.
+func CreateComment(opts *CreateCommentOptions) (comment *Comment, err error) {
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return nil, err
+ }
+
+ comment, err = createComment(sess, opts)
+ if err != nil {
+ return nil, err
+ }
+
+ return comment, sess.Commit()
+}
+
+// CreateIssueComment creates a plain issue comment.
+func CreateIssueComment(doer *User, repo *Repository, issue *Issue, content string, attachments []string) (*Comment, error) {
+ comment, err := CreateComment(&CreateCommentOptions{
+ Type: COMMENT_TYPE_COMMENT,
+ Doer: doer,
+ Repo: repo,
+ Issue: issue,
+ Content: content,
+ Attachments: attachments,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("CreateComment: %v", err)
+ }
+
+ comment.Issue = issue
+ if err = PrepareWebhooks(repo, HOOK_EVENT_ISSUE_COMMENT, &api.IssueCommentPayload{
+ Action: api.HOOK_ISSUE_COMMENT_CREATED,
+ Issue: issue.APIFormat(),
+ Comment: comment.APIFormat(),
+ Repository: repo.APIFormat(nil),
+ Sender: doer.APIFormat(),
+ }); err != nil {
+ log.Error(2, "PrepareWebhooks [comment_id: %d]: %v", comment.ID, err)
+ }
+
+ return comment, nil
+}
+
+// CreateRefComment creates a commit reference comment to issue.
+func CreateRefComment(doer *User, repo *Repository, issue *Issue, content, commitSHA string) error {
+ if len(commitSHA) == 0 {
+ return fmt.Errorf("cannot create reference with empty commit SHA")
+ }
+
+ // Check if same reference from same commit has already existed.
+ has, err := x.Get(&Comment{
+ Type: COMMENT_TYPE_COMMIT_REF,
+ IssueID: issue.ID,
+ CommitSHA: commitSHA,
+ })
+ if err != nil {
+ return fmt.Errorf("check reference comment: %v", err)
+ } else if has {
+ return nil
+ }
+
+ _, err = CreateComment(&CreateCommentOptions{
+ Type: COMMENT_TYPE_COMMIT_REF,
+ Doer: doer,
+ Repo: repo,
+ Issue: issue,
+ CommitSHA: commitSHA,
+ Content: content,
+ })
+ return err
+}
+
+// GetCommentByID returns the comment by given ID.
+func GetCommentByID(id int64) (*Comment, error) {
+ c := new(Comment)
+ has, err := x.Id(id).Get(c)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, ErrCommentNotExist{id, 0}
+ }
+ return c, c.LoadAttributes()
+}
+
+// FIXME: use CommentList to improve performance.
+func loadCommentsAttributes(e Engine, comments []*Comment) (err error) {
+ for i := range comments {
+ if err = comments[i].loadAttributes(e); err != nil {
+ return fmt.Errorf("loadAttributes [%d]: %v", comments[i].ID, err)
+ }
+ }
+
+ return nil
+}
+
+func getCommentsByIssueIDSince(e Engine, issueID, since int64) ([]*Comment, error) {
+ comments := make([]*Comment, 0, 10)
+ sess := e.Where("issue_id = ?", issueID).Asc("created_unix")
+ if since > 0 {
+ sess.And("updated_unix >= ?", since)
+ }
+
+ if err := sess.Find(&comments); err != nil {
+ return nil, err
+ }
+ return comments, loadCommentsAttributes(e, comments)
+}
+
+func getCommentsByRepoIDSince(e Engine, repoID, since int64) ([]*Comment, error) {
+ comments := make([]*Comment, 0, 10)
+ sess := e.Where("issue.repo_id = ?", repoID).Join("INNER", "issue", "issue.id = comment.issue_id").Asc("comment.created_unix")
+ if since > 0 {
+ sess.And("comment.updated_unix >= ?", since)
+ }
+ if err := sess.Find(&comments); err != nil {
+ return nil, err
+ }
+ return comments, loadCommentsAttributes(e, comments)
+}
+
+func getCommentsByIssueID(e Engine, issueID int64) ([]*Comment, error) {
+ return getCommentsByIssueIDSince(e, issueID, -1)
+}
+
+// GetCommentsByIssueID returns all comments of an issue.
+func GetCommentsByIssueID(issueID int64) ([]*Comment, error) {
+ return getCommentsByIssueID(x, issueID)
+}
+
+// GetCommentsByIssueIDSince returns a list of comments of an issue since a given time point.
+func GetCommentsByIssueIDSince(issueID, since int64) ([]*Comment, error) {
+ return getCommentsByIssueIDSince(x, issueID, since)
+}
+
+// GetCommentsByRepoIDSince returns a list of comments for all issues in a repo since a given time point.
+func GetCommentsByRepoIDSince(repoID, since int64) ([]*Comment, error) {
+ return getCommentsByRepoIDSince(x, repoID, since)
+}
+
+// UpdateComment updates information of comment.
+func UpdateComment(doer *User, c *Comment, oldContent string) (err error) {
+ if _, err = x.Id(c.ID).AllCols().Update(c); err != nil {
+ return err
+ }
+
+ if err = c.Issue.LoadAttributes(); err != nil {
+ log.Error(2, "Issue.LoadAttributes [issue_id: %d]: %v", c.IssueID, err)
+ } else if err = PrepareWebhooks(c.Issue.Repo, HOOK_EVENT_ISSUE_COMMENT, &api.IssueCommentPayload{
+ Action: api.HOOK_ISSUE_COMMENT_EDITED,
+ Issue: c.Issue.APIFormat(),
+ Comment: c.APIFormat(),
+ Changes: &api.ChangesPayload{
+ Body: &api.ChangesFromPayload{
+ From: oldContent,
+ },
+ },
+ Repository: c.Issue.Repo.APIFormat(nil),
+ Sender: doer.APIFormat(),
+ }); err != nil {
+ log.Error(2, "PrepareWebhooks [comment_id: %d]: %v", c.ID, err)
+ }
+
+ return nil
+}
+
+// DeleteCommentByID deletes the comment by given ID.
+func DeleteCommentByID(doer *User, id int64) error {
+ comment, err := GetCommentByID(id)
+ if err != nil {
+ if IsErrCommentNotExist(err) {
+ return nil
+ }
+ return err
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if _, err = sess.ID(comment.ID).Delete(new(Comment)); err != nil {
+ return err
+ }
+
+ if comment.Type == COMMENT_TYPE_COMMENT {
+ if _, err = sess.Exec("UPDATE `issue` SET num_comments = num_comments - 1 WHERE id = ?", comment.IssueID); err != nil {
+ return err
+ }
+ }
+
+ if err = sess.Commit(); err != nil {
+ return fmt.Errorf("commit: %v", err)
+ }
+
+ _, err = DeleteAttachmentsByComment(comment.ID, true)
+ if err != nil {
+ log.Error(2, "Failed to delete attachments by comment[%d]: %v", comment.ID, err)
+ }
+
+ if err = comment.Issue.LoadAttributes(); err != nil {
+ log.Error(2, "Issue.LoadAttributes [issue_id: %d]: %v", comment.IssueID, err)
+ } else if err = PrepareWebhooks(comment.Issue.Repo, HOOK_EVENT_ISSUE_COMMENT, &api.IssueCommentPayload{
+ Action: api.HOOK_ISSUE_COMMENT_DELETED,
+ Issue: comment.Issue.APIFormat(),
+ Comment: comment.APIFormat(),
+ Repository: comment.Issue.Repo.APIFormat(nil),
+ Sender: doer.APIFormat(),
+ }); err != nil {
+ log.Error(2, "PrepareWebhooks [comment_id: %d]: %v", comment.ID, err)
+ }
+ return nil
+}
diff --git a/internal/db/error.go b/internal/db/error.go
new file mode 100644
index 00000000..033d631b
--- /dev/null
+++ b/internal/db/error.go
@@ -0,0 +1,575 @@
+// 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 db
+
+import (
+ "fmt"
+)
+
+type ErrNameReserved struct {
+ Name string
+}
+
+func IsErrNameReserved(err error) bool {
+ _, ok := err.(ErrNameReserved)
+ return ok
+}
+
+func (err ErrNameReserved) Error() string {
+ return fmt.Sprintf("name is reserved [name: %s]", err.Name)
+}
+
+type ErrNamePatternNotAllowed struct {
+ Pattern string
+}
+
+func IsErrNamePatternNotAllowed(err error) bool {
+ _, ok := err.(ErrNamePatternNotAllowed)
+ return ok
+}
+
+func (err ErrNamePatternNotAllowed) Error() string {
+ return fmt.Sprintf("name pattern is not allowed [pattern: %s]", err.Pattern)
+}
+
+// ____ ___
+// | | \______ ___________
+// | | / ___// __ \_ __ \
+// | | /\___ \\ ___/| | \/
+// |______//____ >\___ >__|
+// \/ \/
+
+type ErrUserAlreadyExist struct {
+ Name string
+}
+
+func IsErrUserAlreadyExist(err error) bool {
+ _, ok := err.(ErrUserAlreadyExist)
+ return ok
+}
+
+func (err ErrUserAlreadyExist) Error() string {
+ return fmt.Sprintf("user already exists [name: %s]", err.Name)
+}
+
+type ErrEmailAlreadyUsed struct {
+ Email string
+}
+
+func IsErrEmailAlreadyUsed(err error) bool {
+ _, ok := err.(ErrEmailAlreadyUsed)
+ return ok
+}
+
+func (err ErrEmailAlreadyUsed) Error() string {
+ return fmt.Sprintf("e-mail has been used [email: %s]", err.Email)
+}
+
+type ErrUserOwnRepos struct {
+ UID int64
+}
+
+func IsErrUserOwnRepos(err error) bool {
+ _, ok := err.(ErrUserOwnRepos)
+ return ok
+}
+
+func (err ErrUserOwnRepos) Error() string {
+ return fmt.Sprintf("user still has ownership of repositories [uid: %d]", err.UID)
+}
+
+type ErrUserHasOrgs struct {
+ UID int64
+}
+
+func IsErrUserHasOrgs(err error) bool {
+ _, ok := err.(ErrUserHasOrgs)
+ return ok
+}
+
+func (err ErrUserHasOrgs) Error() string {
+ return fmt.Sprintf("user still has membership of organizations [uid: %d]", err.UID)
+}
+
+// __ __.__ __ .__
+// / \ / \__| | _|__|
+// \ \/\/ / | |/ / |
+// \ /| | <| |
+// \__/\ / |__|__|_ \__|
+// \/ \/
+
+type ErrWikiAlreadyExist struct {
+ Title string
+}
+
+func IsErrWikiAlreadyExist(err error) bool {
+ _, ok := err.(ErrWikiAlreadyExist)
+ return ok
+}
+
+func (err ErrWikiAlreadyExist) Error() string {
+ return fmt.Sprintf("wiki page already exists [title: %s]", err.Title)
+}
+
+// __________ ___. .__ .__ ____ __.
+// \______ \__ _\_ |__ | | |__| ____ | |/ _|____ ___.__.
+// | ___/ | \ __ \| | | |/ ___\ | <_/ __ < | |
+// | | | | / \_\ \ |_| \ \___ | | \ ___/\___ |
+// |____| |____/|___ /____/__|\___ > |____|__ \___ > ____|
+// \/ \/ \/ \/\/
+
+type ErrKeyUnableVerify struct {
+ Result string
+}
+
+func IsErrKeyUnableVerify(err error) bool {
+ _, ok := err.(ErrKeyUnableVerify)
+ return ok
+}
+
+func (err ErrKeyUnableVerify) Error() string {
+ return fmt.Sprintf("Unable to verify key content [result: %s]", err.Result)
+}
+
+type ErrKeyNotExist struct {
+ ID int64
+}
+
+func IsErrKeyNotExist(err error) bool {
+ _, ok := err.(ErrKeyNotExist)
+ return ok
+}
+
+func (err ErrKeyNotExist) Error() string {
+ return fmt.Sprintf("public key does not exist [id: %d]", err.ID)
+}
+
+type ErrKeyAlreadyExist struct {
+ OwnerID int64
+ Content string
+}
+
+func IsErrKeyAlreadyExist(err error) bool {
+ _, ok := err.(ErrKeyAlreadyExist)
+ return ok
+}
+
+func (err ErrKeyAlreadyExist) Error() string {
+ return fmt.Sprintf("public key already exists [owner_id: %d, content: %s]", err.OwnerID, err.Content)
+}
+
+type ErrKeyNameAlreadyUsed struct {
+ OwnerID int64
+ Name string
+}
+
+func IsErrKeyNameAlreadyUsed(err error) bool {
+ _, ok := err.(ErrKeyNameAlreadyUsed)
+ return ok
+}
+
+func (err ErrKeyNameAlreadyUsed) Error() string {
+ return fmt.Sprintf("public key already exists [owner_id: %d, name: %s]", err.OwnerID, err.Name)
+}
+
+type ErrKeyAccessDenied struct {
+ UserID int64
+ KeyID int64
+ Note string
+}
+
+func IsErrKeyAccessDenied(err error) bool {
+ _, ok := err.(ErrKeyAccessDenied)
+ return ok
+}
+
+func (err ErrKeyAccessDenied) Error() string {
+ return fmt.Sprintf("user does not have access to the key [user_id: %d, key_id: %d, note: %s]",
+ err.UserID, err.KeyID, err.Note)
+}
+
+type ErrDeployKeyNotExist struct {
+ ID int64
+ KeyID int64
+ RepoID int64
+}
+
+func IsErrDeployKeyNotExist(err error) bool {
+ _, ok := err.(ErrDeployKeyNotExist)
+ return ok
+}
+
+func (err ErrDeployKeyNotExist) Error() string {
+ return fmt.Sprintf("Deploy key does not exist [id: %d, key_id: %d, repo_id: %d]", err.ID, err.KeyID, err.RepoID)
+}
+
+type ErrDeployKeyAlreadyExist struct {
+ KeyID int64
+ RepoID int64
+}
+
+func IsErrDeployKeyAlreadyExist(err error) bool {
+ _, ok := err.(ErrDeployKeyAlreadyExist)
+ return ok
+}
+
+func (err ErrDeployKeyAlreadyExist) Error() string {
+ return fmt.Sprintf("public key already exists [key_id: %d, repo_id: %d]", err.KeyID, err.RepoID)
+}
+
+type ErrDeployKeyNameAlreadyUsed struct {
+ RepoID int64
+ Name string
+}
+
+func IsErrDeployKeyNameAlreadyUsed(err error) bool {
+ _, ok := err.(ErrDeployKeyNameAlreadyUsed)
+ return ok
+}
+
+func (err ErrDeployKeyNameAlreadyUsed) Error() string {
+ return fmt.Sprintf("public key already exists [repo_id: %d, name: %s]", err.RepoID, err.Name)
+}
+
+// _____ ___________ __
+// / _ \ ____ ____ ____ ______ _____\__ ___/___ | | __ ____ ____
+// / /_\ \_/ ___\/ ___\/ __ \ / ___// ___/ | | / _ \| |/ // __ \ / \
+// / | \ \__\ \__\ ___/ \___ \ \___ \ | |( <_> ) <\ ___/| | \
+// \____|__ /\___ >___ >___ >____ >____ > |____| \____/|__|_ \\___ >___| /
+// \/ \/ \/ \/ \/ \/ \/ \/ \/
+
+type ErrAccessTokenNotExist struct {
+ SHA string
+}
+
+func IsErrAccessTokenNotExist(err error) bool {
+ _, ok := err.(ErrAccessTokenNotExist)
+ return ok
+}
+
+func (err ErrAccessTokenNotExist) Error() string {
+ return fmt.Sprintf("access token does not exist [sha: %s]", err.SHA)
+}
+
+type ErrAccessTokenEmpty struct {
+}
+
+func IsErrAccessTokenEmpty(err error) bool {
+ _, ok := err.(ErrAccessTokenEmpty)
+ return ok
+}
+
+func (err ErrAccessTokenEmpty) Error() string {
+ return fmt.Sprintf("access token is empty")
+}
+
+// ________ .__ __ .__
+// \_____ \_______ _________ ____ |__|____________ _/ |_|__| ____ ____
+// / | \_ __ \/ ___\__ \ / \| \___ /\__ \\ __\ |/ _ \ / \
+// / | \ | \/ /_/ > __ \| | \ |/ / / __ \| | | ( <_> ) | \
+// \_______ /__| \___ (____ /___| /__/_____ \(____ /__| |__|\____/|___| /
+// \/ /_____/ \/ \/ \/ \/ \/
+
+type ErrLastOrgOwner struct {
+ UID int64
+}
+
+func IsErrLastOrgOwner(err error) bool {
+ _, ok := err.(ErrLastOrgOwner)
+ return ok
+}
+
+func (err ErrLastOrgOwner) Error() string {
+ return fmt.Sprintf("user is the last member of owner team [uid: %d]", err.UID)
+}
+
+// __________ .__ __
+// \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__.
+// | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | |
+// | | \ ___/| |_> > <_> )___ \| || | ( <_> ) | \/\___ |
+// |____|_ /\___ > __/ \____/____ >__||__| \____/|__| / ____|
+// \/ \/|__| \/ \/
+
+type ErrRepoAlreadyExist struct {
+ Uname string
+ Name string
+}
+
+func IsErrRepoAlreadyExist(err error) bool {
+ _, ok := err.(ErrRepoAlreadyExist)
+ return ok
+}
+
+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 {
+ ID int64
+ 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 [id: %d, tag_name: %s]", err.ID, err.TagName)
+}
+
+type ErrInvalidTagName struct {
+ TagName string
+}
+
+func IsErrInvalidTagName(err error) bool {
+ _, ok := err.(ErrInvalidTagName)
+ return ok
+}
+
+func (err ErrInvalidTagName) Error() string {
+ return fmt.Sprintf("release tag name is not valid [tag_name: %s]", err.TagName)
+}
+
+type ErrRepoFileAlreadyExist struct {
+ FileName string
+}
+
+func IsErrRepoFileAlreadyExist(err error) bool {
+ _, ok := err.(ErrRepoFileAlreadyExist)
+ return ok
+}
+
+func (err ErrRepoFileAlreadyExist) Error() string {
+ return fmt.Sprintf("repository file already exists [file_name: %s]", err.FileName)
+}
+
+// __________ .__ .__ __________ __
+// \______ \__ __| | | |\______ \ ____ ________ __ ____ _______/ |_
+// | ___/ | \ | | | | _// __ \/ ____/ | \_/ __ \ / ___/\ __\
+// | | | | / |_| |_| | \ ___< <_| | | /\ ___/ \___ \ | |
+// |____| |____/|____/____/____|_ /\___ >__ |____/ \___ >____ > |__|
+// \/ \/ |__| \/ \/
+
+type ErrPullRequestNotExist struct {
+ ID int64
+ IssueID int64
+ HeadRepoID int64
+ BaseRepoID int64
+ HeadBarcnh string
+ BaseBranch string
+}
+
+func IsErrPullRequestNotExist(err error) bool {
+ _, ok := err.(ErrPullRequestNotExist)
+ return ok
+}
+
+func (err ErrPullRequestNotExist) Error() string {
+ 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)
+}
+
+// _________ __
+// \_ ___ \ ____ _____ _____ ____ _____/ |_
+// / \ \/ / _ \ / \ / \_/ __ \ / \ __\
+// \ \___( <_> ) Y Y \ Y Y \ ___/| | \ |
+// \______ /\____/|__|_| /__|_| /\___ >___| /__|
+// \/ \/ \/ \/ \/
+
+type ErrCommentNotExist struct {
+ ID int64
+ IssueID int64
+}
+
+func IsErrCommentNotExist(err error) bool {
+ _, ok := err.(ErrCommentNotExist)
+ return ok
+}
+
+func (err ErrCommentNotExist) Error() string {
+ return fmt.Sprintf("comment does not exist [id: %d, issue_id: %d]", err.ID, err.IssueID)
+}
+
+// .____ ___. .__
+// | | _____ \_ |__ ____ | |
+// | | \__ \ | __ \_/ __ \| |
+// | |___ / __ \| \_\ \ ___/| |__
+// |_______ (____ /___ /\___ >____/
+// \/ \/ \/ \/
+
+type ErrLabelNotExist struct {
+ LabelID int64
+ RepoID int64
+}
+
+func IsErrLabelNotExist(err error) bool {
+ _, ok := err.(ErrLabelNotExist)
+ return ok
+}
+
+func (err ErrLabelNotExist) Error() string {
+ return fmt.Sprintf("label does not exist [label_id: %d, repo_id: %d]", err.LabelID, err.RepoID)
+}
+
+// _____ .__.__ __
+// / \ |__| | ____ _______/ |_ ____ ____ ____
+// / \ / \| | | _/ __ \ / ___/\ __\/ _ \ / \_/ __ \
+// / Y \ | |_\ ___/ \___ \ | | ( <_> ) | \ ___/
+// \____|__ /__|____/\___ >____ > |__| \____/|___| /\___ >
+// \/ \/ \/ \/ \/
+
+type ErrMilestoneNotExist struct {
+ ID int64
+ RepoID int64
+}
+
+func IsErrMilestoneNotExist(err error) bool {
+ _, ok := err.(ErrMilestoneNotExist)
+ return ok
+}
+
+func (err ErrMilestoneNotExist) Error() string {
+ return fmt.Sprintf("milestone does not exist [id: %d, repo_id: %d]", err.ID, err.RepoID)
+}
+
+// _____ __ __ .__ __
+// / _ \_/ |__/ |______ ____ | |__ _____ ____ _____/ |_
+// / /_\ \ __\ __\__ \ _/ ___\| | \ / \_/ __ \ / \ __\
+// / | \ | | | / __ \\ \___| Y \ Y Y \ ___/| | \ |
+// \____|__ /__| |__| (____ /\___ >___| /__|_| /\___ >___| /__|
+// \/ \/ \/ \/ \/ \/ \/
+
+type ErrAttachmentNotExist struct {
+ ID int64
+ UUID string
+}
+
+func IsErrAttachmentNotExist(err error) bool {
+ _, ok := err.(ErrAttachmentNotExist)
+ return ok
+}
+
+func (err ErrAttachmentNotExist) Error() string {
+ return fmt.Sprintf("attachment does not exist [id: %d, uuid: %s]", err.ID, err.UUID)
+}
+
+// .____ .__ _________
+// | | ____ ____ |__| ____ / _____/ ____ __ _________ ____ ____
+// | | / _ \ / ___\| |/ \ \_____ \ / _ \| | \_ __ \_/ ___\/ __ \
+// | |__( <_> ) /_/ > | | \ / ( <_> ) | /| | \/\ \__\ ___/
+// |_______ \____/\___ /|__|___| / /_______ /\____/|____/ |__| \___ >___ >
+// \/ /_____/ \/ \/ \/ \/
+
+type ErrLoginSourceAlreadyExist struct {
+ Name string
+}
+
+func IsErrLoginSourceAlreadyExist(err error) bool {
+ _, ok := err.(ErrLoginSourceAlreadyExist)
+ return ok
+}
+
+func (err ErrLoginSourceAlreadyExist) Error() string {
+ return fmt.Sprintf("login source already exists [name: %s]", err.Name)
+}
+
+type ErrLoginSourceInUse struct {
+ ID int64
+}
+
+func IsErrLoginSourceInUse(err error) bool {
+ _, ok := err.(ErrLoginSourceInUse)
+ return ok
+}
+
+func (err ErrLoginSourceInUse) Error() string {
+ return fmt.Sprintf("login source is still used by some users [id: %d]", err.ID)
+}
+
+// ___________
+// \__ ___/___ _____ _____
+// | |_/ __ \\__ \ / \
+// | |\ ___/ / __ \| Y Y \
+// |____| \___ >____ /__|_| /
+// \/ \/ \/
+
+type ErrTeamAlreadyExist struct {
+ OrgID int64
+ Name string
+}
+
+func IsErrTeamAlreadyExist(err error) bool {
+ _, ok := err.(ErrTeamAlreadyExist)
+ return ok
+}
+
+func (err ErrTeamAlreadyExist) Error() string {
+ return fmt.Sprintf("team already exists [org_id: %d, name: %s]", err.OrgID, err.Name)
+}
+
+// ____ ___ .__ .___
+// | | \______ | | _________ __| _/
+// | | /\____ \| | / _ \__ \ / __ |
+// | | / | |_> > |_( <_> ) __ \_/ /_/ |
+// |______/ | __/|____/\____(____ /\____ |
+// |__| \/ \/
+//
+
+type ErrUploadNotExist struct {
+ ID int64
+ UUID string
+}
+
+func IsErrUploadNotExist(err error) bool {
+ _, ok := err.(ErrAttachmentNotExist)
+ return ok
+}
+
+func (err ErrUploadNotExist) Error() string {
+ return fmt.Sprintf("attachment does not exist [id: %d, uuid: %s]", err.ID, err.UUID)
+}
diff --git a/internal/db/errors/errors.go b/internal/db/errors/errors.go
new file mode 100644
index 00000000..cc231436
--- /dev/null
+++ b/internal/db/errors/errors.go
@@ -0,0 +1,14 @@
+// Copyright 2017 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 errors
+
+import "errors"
+
+var InternalServerError = errors.New("internal server error")
+
+// New is a wrapper of real errors.New function.
+func New(text string) error {
+ return errors.New(text)
+}
diff --git a/internal/db/errors/issue.go b/internal/db/errors/issue.go
new file mode 100644
index 00000000..903cc977
--- /dev/null
+++ b/internal/db/errors/issue.go
@@ -0,0 +1,35 @@
+// Copyright 2017 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 errors
+
+import "fmt"
+
+type IssueNotExist struct {
+ ID int64
+ RepoID int64
+ Index int64
+}
+
+func IsIssueNotExist(err error) bool {
+ _, ok := err.(IssueNotExist)
+ return ok
+}
+
+func (err IssueNotExist) Error() string {
+ return fmt.Sprintf("issue does not exist [id: %d, repo_id: %d, index: %d]", err.ID, err.RepoID, err.Index)
+}
+
+type InvalidIssueReference struct {
+ Ref string
+}
+
+func IsInvalidIssueReference(err error) bool {
+ _, ok := err.(InvalidIssueReference)
+ return ok
+}
+
+func (err InvalidIssueReference) Error() string {
+ return fmt.Sprintf("invalid issue reference [ref: %s]", err.Ref)
+}
diff --git a/internal/db/errors/login_source.go b/internal/db/errors/login_source.go
new file mode 100644
index 00000000..dd18664e
--- /dev/null
+++ b/internal/db/errors/login_source.go
@@ -0,0 +1,60 @@
+// Copyright 2017 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 errors
+
+import "fmt"
+
+type LoginSourceNotExist struct {
+ ID int64
+}
+
+func IsLoginSourceNotExist(err error) bool {
+ _, ok := err.(LoginSourceNotExist)
+ return ok
+}
+
+func (err LoginSourceNotExist) Error() string {
+ return fmt.Sprintf("login source does not exist [id: %d]", err.ID)
+}
+
+type LoginSourceNotActivated struct {
+ SourceID int64
+}
+
+func IsLoginSourceNotActivated(err error) bool {
+ _, ok := err.(LoginSourceNotActivated)
+ return ok
+}
+
+func (err LoginSourceNotActivated) Error() string {
+ return fmt.Sprintf("login source is not activated [source_id: %d]", err.SourceID)
+}
+
+type InvalidLoginSourceType struct {
+ Type interface{}
+}
+
+func IsInvalidLoginSourceType(err error) bool {
+ _, ok := err.(InvalidLoginSourceType)
+ return ok
+}
+
+func (err InvalidLoginSourceType) Error() string {
+ return fmt.Sprintf("invalid login source type [type: %v]", err.Type)
+}
+
+type LoginSourceMismatch struct {
+ Expect int64
+ Actual int64
+}
+
+func IsLoginSourceMismatch(err error) bool {
+ _, ok := err.(LoginSourceMismatch)
+ return ok
+}
+
+func (err LoginSourceMismatch) Error() string {
+ return fmt.Sprintf("login source mismatch [expect: %d, actual: %d]", err.Expect, err.Actual)
+}
diff --git a/internal/db/errors/org.go b/internal/db/errors/org.go
new file mode 100644
index 00000000..56532746
--- /dev/null
+++ b/internal/db/errors/org.go
@@ -0,0 +1,21 @@
+// Copyright 2018 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 errors
+
+import "fmt"
+
+type TeamNotExist struct {
+ TeamID int64
+ Name string
+}
+
+func IsTeamNotExist(err error) bool {
+ _, ok := err.(TeamNotExist)
+ return ok
+}
+
+func (err TeamNotExist) Error() string {
+ return fmt.Sprintf("team does not exist [team_id: %d, name: %s]", err.TeamID, err.Name)
+}
diff --git a/internal/db/errors/repo.go b/internal/db/errors/repo.go
new file mode 100644
index 00000000..c9894af9
--- /dev/null
+++ b/internal/db/errors/repo.go
@@ -0,0 +1,87 @@
+// Copyright 2017 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 errors
+
+import "fmt"
+
+type RepoNotExist struct {
+ ID int64
+ UserID int64
+ Name string
+}
+
+func IsRepoNotExist(err error) bool {
+ _, ok := err.(RepoNotExist)
+ return ok
+}
+
+func (err RepoNotExist) Error() string {
+ return fmt.Sprintf("repository does not exist [id: %d, user_id: %d, name: %s]", err.ID, err.UserID, err.Name)
+}
+
+type ReachLimitOfRepo struct {
+ Limit int
+}
+
+func IsReachLimitOfRepo(err error) bool {
+ _, ok := err.(ReachLimitOfRepo)
+ return ok
+}
+
+func (err ReachLimitOfRepo) Error() string {
+ return fmt.Sprintf("user has reached maximum limit of repositories [limit: %d]", err.Limit)
+}
+
+type InvalidRepoReference struct {
+ Ref string
+}
+
+func IsInvalidRepoReference(err error) bool {
+ _, ok := err.(InvalidRepoReference)
+ return ok
+}
+
+func (err InvalidRepoReference) Error() string {
+ return fmt.Sprintf("invalid repository reference [ref: %s]", err.Ref)
+}
+
+type MirrorNotExist struct {
+ RepoID int64
+}
+
+func IsMirrorNotExist(err error) bool {
+ _, ok := err.(MirrorNotExist)
+ return ok
+}
+
+func (err MirrorNotExist) Error() string {
+ return fmt.Sprintf("mirror does not exist [repo_id: %d]", err.RepoID)
+}
+
+type BranchAlreadyExists struct {
+ Name string
+}
+
+func IsBranchAlreadyExists(err error) bool {
+ _, ok := err.(BranchAlreadyExists)
+ return ok
+}
+
+func (err BranchAlreadyExists) Error() string {
+ return fmt.Sprintf("branch already exists [name: %s]", err.Name)
+}
+
+type ErrBranchNotExist struct {
+ Name string
+}
+
+func IsErrBranchNotExist(err error) bool {
+ _, ok := err.(ErrBranchNotExist)
+ return ok
+}
+
+func (err ErrBranchNotExist) Error() string {
+ return fmt.Sprintf("branch does not exist [name: %s]", err.Name)
+}
diff --git a/internal/db/errors/token.go b/internal/db/errors/token.go
new file mode 100644
index 00000000..d6a4577a
--- /dev/null
+++ b/internal/db/errors/token.go
@@ -0,0 +1,16 @@
+package errors
+
+import "fmt"
+
+type AccessTokenNameAlreadyExist struct {
+ Name string
+}
+
+func IsAccessTokenNameAlreadyExist(err error) bool {
+ _, ok := err.(AccessTokenNameAlreadyExist)
+ return ok
+}
+
+func (err AccessTokenNameAlreadyExist) Error() string {
+ return fmt.Sprintf("access token already exist [name: %s]", err.Name)
+}
diff --git a/internal/db/errors/two_factor.go b/internal/db/errors/two_factor.go
new file mode 100644
index 00000000..02cdcf5c
--- /dev/null
+++ b/internal/db/errors/two_factor.go
@@ -0,0 +1,33 @@
+// Copyright 2017 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 errors
+
+import "fmt"
+
+type TwoFactorNotFound struct {
+ UserID int64
+}
+
+func IsTwoFactorNotFound(err error) bool {
+ _, ok := err.(TwoFactorNotFound)
+ return ok
+}
+
+func (err TwoFactorNotFound) Error() string {
+ return fmt.Sprintf("two-factor authentication does not found [user_id: %d]", err.UserID)
+}
+
+type TwoFactorRecoveryCodeNotFound struct {
+ Code string
+}
+
+func IsTwoFactorRecoveryCodeNotFound(err error) bool {
+ _, ok := err.(TwoFactorRecoveryCodeNotFound)
+ return ok
+}
+
+func (err TwoFactorRecoveryCodeNotFound) Error() string {
+ return fmt.Sprintf("two-factor recovery code does not found [code: %s]", err.Code)
+}
diff --git a/internal/db/errors/user.go b/internal/db/errors/user.go
new file mode 100644
index 00000000..526d4b2d
--- /dev/null
+++ b/internal/db/errors/user.go
@@ -0,0 +1,45 @@
+// Copyright 2017 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 errors
+
+import "fmt"
+
+type EmptyName struct{}
+
+func IsEmptyName(err error) bool {
+ _, ok := err.(EmptyName)
+ return ok
+}
+
+func (err EmptyName) Error() string {
+ return "empty name"
+}
+
+type UserNotExist struct {
+ UserID int64
+ Name string
+}
+
+func IsUserNotExist(err error) bool {
+ _, ok := err.(UserNotExist)
+ return ok
+}
+
+func (err UserNotExist) Error() string {
+ return fmt.Sprintf("user does not exist [user_id: %d, name: %s]", err.UserID, err.Name)
+}
+
+type UserNotKeyOwner struct {
+ KeyID int64
+}
+
+func IsUserNotKeyOwner(err error) bool {
+ _, ok := err.(UserNotKeyOwner)
+ return ok
+}
+
+func (err UserNotKeyOwner) Error() string {
+ return fmt.Sprintf("user is not the owner of public key [key_id: %d]", err.KeyID)
+}
diff --git a/internal/db/errors/user_mail.go b/internal/db/errors/user_mail.go
new file mode 100644
index 00000000..fcdeb78c
--- /dev/null
+++ b/internal/db/errors/user_mail.go
@@ -0,0 +1,33 @@
+// Copyright 2017 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 errors
+
+import "fmt"
+
+type EmailNotFound struct {
+ Email string
+}
+
+func IsEmailNotFound(err error) bool {
+ _, ok := err.(EmailNotFound)
+ return ok
+}
+
+func (err EmailNotFound) Error() string {
+ return fmt.Sprintf("email is not found [email: %s]", err.Email)
+}
+
+type EmailNotVerified struct {
+ Email string
+}
+
+func IsEmailNotVerified(err error) bool {
+ _, ok := err.(EmailNotVerified)
+ return ok
+}
+
+func (err EmailNotVerified) Error() string {
+ return fmt.Sprintf("email has not been verified [email: %s]", err.Email)
+}
diff --git a/internal/db/errors/webhook.go b/internal/db/errors/webhook.go
new file mode 100644
index 00000000..76cf8cb4
--- /dev/null
+++ b/internal/db/errors/webhook.go
@@ -0,0 +1,34 @@
+// Copyright 2017 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 errors
+
+import "fmt"
+
+type WebhookNotExist struct {
+ ID int64
+}
+
+func IsWebhookNotExist(err error) bool {
+ _, ok := err.(WebhookNotExist)
+ return ok
+}
+
+func (err WebhookNotExist) Error() string {
+ return fmt.Sprintf("webhook does not exist [id: %d]", err.ID)
+}
+
+type HookTaskNotExist struct {
+ HookID int64
+ UUID string
+}
+
+func IsHookTaskNotExist(err error) bool {
+ _, ok := err.(HookTaskNotExist)
+ return ok
+}
+
+func (err HookTaskNotExist) Error() string {
+ return fmt.Sprintf("hook task does not exist [hook_id: %d, uuid: %s]", err.HookID, err.UUID)
+}
diff --git a/internal/db/git_diff.go b/internal/db/git_diff.go
new file mode 100644
index 00000000..040c472b
--- /dev/null
+++ b/internal/db/git_diff.go
@@ -0,0 +1,194 @@
+// Copyright 2014 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 db
+
+import (
+ "bytes"
+ "fmt"
+ "html"
+ "html/template"
+ "io"
+
+ "github.com/sergi/go-diff/diffmatchpatch"
+ "golang.org/x/net/html/charset"
+ "golang.org/x/text/transform"
+
+ "github.com/gogs/git-module"
+
+ "gogs.io/gogs/internal/setting"
+ "gogs.io/gogs/internal/template/highlight"
+ "gogs.io/gogs/internal/tool"
+)
+
+type DiffSection struct {
+ *git.DiffSection
+}
+
+var (
+ addedCodePrefix = []byte("<span class=\"added-code\">")
+ removedCodePrefix = []byte("<span class=\"removed-code\">")
+ codeTagSuffix = []byte("</span>")
+)
+
+func diffToHTML(diffs []diffmatchpatch.Diff, lineType git.DiffLineType) template.HTML {
+ buf := bytes.NewBuffer(nil)
+
+ // Reproduce signs which are cutted for inline diff before.
+ switch lineType {
+ case git.DIFF_LINE_ADD:
+ buf.WriteByte('+')
+ case git.DIFF_LINE_DEL:
+ buf.WriteByte('-')
+ }
+
+ for i := range diffs {
+ switch {
+ case diffs[i].Type == diffmatchpatch.DiffInsert && lineType == git.DIFF_LINE_ADD:
+ buf.Write(addedCodePrefix)
+ buf.WriteString(html.EscapeString(diffs[i].Text))
+ buf.Write(codeTagSuffix)
+ case diffs[i].Type == diffmatchpatch.DiffDelete && lineType == git.DIFF_LINE_DEL:
+ buf.Write(removedCodePrefix)
+ buf.WriteString(html.EscapeString(diffs[i].Text))
+ buf.Write(codeTagSuffix)
+ case diffs[i].Type == diffmatchpatch.DiffEqual:
+ buf.WriteString(html.EscapeString(diffs[i].Text))
+ }
+ }
+
+ return template.HTML(buf.Bytes())
+}
+
+var diffMatchPatch = diffmatchpatch.New()
+
+func init() {
+ diffMatchPatch.DiffEditCost = 100
+}
+
+// ComputedInlineDiffFor computes inline diff for the given line.
+func (diffSection *DiffSection) ComputedInlineDiffFor(diffLine *git.DiffLine) template.HTML {
+ if setting.Git.DisableDiffHighlight {
+ return template.HTML(html.EscapeString(diffLine.Content[1:]))
+ }
+ var (
+ compareDiffLine *git.DiffLine
+ diff1 string
+ diff2 string
+ )
+
+ // try to find equivalent diff line. ignore, otherwise
+ switch diffLine.Type {
+ case git.DIFF_LINE_ADD:
+ compareDiffLine = diffSection.Line(git.DIFF_LINE_DEL, diffLine.RightIdx)
+ if compareDiffLine == nil {
+ return template.HTML(html.EscapeString(diffLine.Content))
+ }
+ diff1 = compareDiffLine.Content
+ diff2 = diffLine.Content
+ case git.DIFF_LINE_DEL:
+ compareDiffLine = diffSection.Line(git.DIFF_LINE_ADD, diffLine.LeftIdx)
+ if compareDiffLine == nil {
+ return template.HTML(html.EscapeString(diffLine.Content))
+ }
+ diff1 = diffLine.Content
+ diff2 = compareDiffLine.Content
+ default:
+ return template.HTML(html.EscapeString(diffLine.Content))
+ }
+
+ diffRecord := diffMatchPatch.DiffMain(diff1[1:], diff2[1:], true)
+ diffRecord = diffMatchPatch.DiffCleanupEfficiency(diffRecord)
+
+ return diffToHTML(diffRecord, diffLine.Type)
+}
+
+type DiffFile struct {
+ *git.DiffFile
+ Sections []*DiffSection
+}
+
+func (diffFile *DiffFile) HighlightClass() string {
+ return highlight.FileNameToHighlightClass(diffFile.Name)
+}
+
+type Diff struct {
+ *git.Diff
+ Files []*DiffFile
+}
+
+func NewDiff(gitDiff *git.Diff) *Diff {
+ diff := &Diff{
+ Diff: gitDiff,
+ Files: make([]*DiffFile, gitDiff.NumFiles()),
+ }
+
+ // FIXME: detect encoding while parsing.
+ var buf bytes.Buffer
+ for i := range gitDiff.Files {
+ buf.Reset()
+
+ diff.Files[i] = &DiffFile{
+ DiffFile: gitDiff.Files[i],
+ Sections: make([]*DiffSection, gitDiff.Files[i].NumSections()),
+ }
+
+ for j := range gitDiff.Files[i].Sections {
+ diff.Files[i].Sections[j] = &DiffSection{
+ DiffSection: gitDiff.Files[i].Sections[j],
+ }
+
+ for k := range diff.Files[i].Sections[j].Lines {
+ buf.WriteString(diff.Files[i].Sections[j].Lines[k].Content)
+ buf.WriteString("\n")
+ }
+ }
+
+ charsetLabel, err := tool.DetectEncoding(buf.Bytes())
+ if charsetLabel != "UTF-8" && err == nil {
+ encoding, _ := charset.Lookup(charsetLabel)
+ if encoding != nil {
+ d := encoding.NewDecoder()
+ for j := range diff.Files[i].Sections {
+ for k := range diff.Files[i].Sections[j].Lines {
+ if c, _, err := transform.String(d, diff.Files[i].Sections[j].Lines[k].Content); err == nil {
+ diff.Files[i].Sections[j].Lines[k].Content = c
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return diff
+}
+
+func ParsePatch(maxLines, maxLineCharacteres, maxFiles int, reader io.Reader) (*Diff, error) {
+ done := make(chan error)
+ var gitDiff *git.Diff
+ go func() {
+ gitDiff = git.ParsePatch(done, maxLines, maxLineCharacteres, maxFiles, reader)
+ }()
+
+ if err := <-done; err != nil {
+ return nil, fmt.Errorf("ParsePatch: %v", err)
+ }
+ return NewDiff(gitDiff), nil
+}
+
+func GetDiffRange(repoPath, beforeCommitID, afterCommitID string, maxLines, maxLineCharacteres, maxFiles int) (*Diff, error) {
+ gitDiff, err := git.GetDiffRange(repoPath, beforeCommitID, afterCommitID, maxLines, maxLineCharacteres, maxFiles)
+ if err != nil {
+ return nil, fmt.Errorf("GetDiffRange: %v", err)
+ }
+ return NewDiff(gitDiff), nil
+}
+
+func GetDiffCommit(repoPath, commitID string, maxLines, maxLineCharacteres, maxFiles int) (*Diff, error) {
+ gitDiff, err := git.GetDiffCommit(repoPath, commitID, maxLines, maxLineCharacteres, maxFiles)
+ if err != nil {
+ return nil, fmt.Errorf("GetDiffCommit: %v", err)
+ }
+ return NewDiff(gitDiff), nil
+}
diff --git a/internal/db/git_diff_test.go b/internal/db/git_diff_test.go
new file mode 100644
index 00000000..d92afe9e
--- /dev/null
+++ b/internal/db/git_diff_test.go
@@ -0,0 +1,41 @@
+// Copyright 2016 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 db
+
+import (
+ "html/template"
+ "testing"
+
+ "github.com/gogs/git-module"
+ dmp "github.com/sergi/go-diff/diffmatchpatch"
+)
+
+func assertEqual(t *testing.T, s1 string, s2 template.HTML) {
+ if s1 != string(s2) {
+ t.Errorf("%s should be equal %s", s2, s1)
+ }
+}
+
+func assertLineEqual(t *testing.T, d1 *git.DiffLine, d2 *git.DiffLine) {
+ if d1 != d2 {
+ t.Errorf("%v should be equal %v", d1, d2)
+ }
+}
+
+func Test_diffToHTML(t *testing.T) {
+ assertEqual(t, "+foo <span class=\"added-code\">bar</span> biz", diffToHTML([]dmp.Diff{
+ dmp.Diff{dmp.DiffEqual, "foo "},
+ dmp.Diff{dmp.DiffInsert, "bar"},
+ dmp.Diff{dmp.DiffDelete, " baz"},
+ dmp.Diff{dmp.DiffEqual, " biz"},
+ }, git.DIFF_LINE_ADD))
+
+ assertEqual(t, "-foo <span class=\"removed-code\">bar</span> biz", diffToHTML([]dmp.Diff{
+ dmp.Diff{dmp.DiffEqual, "foo "},
+ dmp.Diff{dmp.DiffDelete, "bar"},
+ dmp.Diff{dmp.DiffInsert, " baz"},
+ dmp.Diff{dmp.DiffEqual, " biz"},
+ }, git.DIFF_LINE_DEL))
+}
diff --git a/internal/db/issue.go b/internal/db/issue.go
new file mode 100644
index 00000000..f176f876
--- /dev/null
+++ b/internal/db/issue.go
@@ -0,0 +1,1440 @@
+// Copyright 2014 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 db
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/unknwon/com"
+ log "gopkg.in/clog.v1"
+ "xorm.io/xorm"
+
+ api "github.com/gogs/go-gogs-client"
+
+ "gogs.io/gogs/internal/db/errors"
+ "gogs.io/gogs/internal/setting"
+ "gogs.io/gogs/internal/tool"
+)
+
+var (
+ ErrMissingIssueNumber = errors.New("No issue number specified")
+)
+
+// Issue represents an issue or pull request of repository.
+type Issue struct {
+ ID int64
+ RepoID int64 `xorm:"INDEX UNIQUE(repo_index)"`
+ Repo *Repository `xorm:"-" json:"-"`
+ Index int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository.
+ PosterID int64
+ Poster *User `xorm:"-" json:"-"`
+ Title string `xorm:"name"`
+ Content string `xorm:"TEXT"`
+ RenderedContent string `xorm:"-" json:"-"`
+ Labels []*Label `xorm:"-" json:"-"`
+ MilestoneID int64
+ Milestone *Milestone `xorm:"-" json:"-"`
+ Priority int
+ AssigneeID int64
+ Assignee *User `xorm:"-" json:"-"`
+ IsClosed bool
+ IsRead bool `xorm:"-" json:"-"`
+ IsPull bool // Indicates whether is a pull request or not.
+ PullRequest *PullRequest `xorm:"-" json:"-"`
+ NumComments int
+
+ Deadline time.Time `xorm:"-" json:"-"`
+ DeadlineUnix int64
+ Created time.Time `xorm:"-" json:"-"`
+ CreatedUnix int64
+ Updated time.Time `xorm:"-" json:"-"`
+ UpdatedUnix int64
+
+ Attachments []*Attachment `xorm:"-" json:"-"`
+ Comments []*Comment `xorm:"-" json:"-"`
+}
+
+func (issue *Issue) BeforeInsert() {
+ issue.CreatedUnix = time.Now().Unix()
+ issue.UpdatedUnix = issue.CreatedUnix
+}
+
+func (issue *Issue) BeforeUpdate() {
+ issue.UpdatedUnix = time.Now().Unix()
+ issue.DeadlineUnix = issue.Deadline.Unix()
+}
+
+func (issue *Issue) AfterSet(colName string, _ xorm.Cell) {
+ switch colName {
+ case "deadline_unix":
+ issue.Deadline = time.Unix(issue.DeadlineUnix, 0).Local()
+ case "created_unix":
+ issue.Created = time.Unix(issue.CreatedUnix, 0).Local()
+ case "updated_unix":
+ issue.Updated = time.Unix(issue.UpdatedUnix, 0).Local()
+ }
+}
+
+func (issue *Issue) loadAttributes(e Engine) (err error) {
+ if issue.Repo == nil {
+ issue.Repo, err = getRepositoryByID(e, issue.RepoID)
+ if err != nil {
+ return fmt.Errorf("getRepositoryByID [%d]: %v", issue.RepoID, err)
+ }
+ }
+
+ if issue.Poster == nil {
+ issue.Poster, err = getUserByID(e, issue.PosterID)
+ if err != nil {
+ if errors.IsUserNotExist(err) {
+ issue.PosterID = -1
+ issue.Poster = NewGhostUser()
+ } else {
+ return fmt.Errorf("getUserByID.(Poster) [%d]: %v", issue.PosterID, err)
+ }
+ }
+ }
+
+ if issue.Labels == nil {
+ issue.Labels, err = getLabelsByIssueID(e, issue.ID)
+ if err != nil {
+ return fmt.Errorf("getLabelsByIssueID [%d]: %v", issue.ID, err)
+ }
+ }
+
+ if issue.Milestone == nil && issue.MilestoneID > 0 {
+ issue.Milestone, err = getMilestoneByRepoID(e, issue.RepoID, issue.MilestoneID)
+ if err != nil {
+ return fmt.Errorf("getMilestoneByRepoID [repo_id: %d, milestone_id: %d]: %v", issue.RepoID, issue.MilestoneID, err)
+ }
+ }
+
+ if issue.Assignee == nil && issue.AssigneeID > 0 {
+ issue.Assignee, err = getUserByID(e, issue.AssigneeID)
+ if err != nil {
+ return fmt.Errorf("getUserByID.(assignee) [%d]: %v", issue.AssigneeID, err)
+ }
+ }
+
+ if issue.IsPull && issue.PullRequest == nil {
+ // It is possible pull request is not yet created.
+ issue.PullRequest, err = getPullRequestByIssueID(e, issue.ID)
+ if err != nil && !IsErrPullRequestNotExist(err) {
+ return fmt.Errorf("getPullRequestByIssueID [%d]: %v", issue.ID, err)
+ }
+ }
+
+ if issue.Attachments == nil {
+ issue.Attachments, err = getAttachmentsByIssueID(e, issue.ID)
+ if err != nil {
+ return fmt.Errorf("getAttachmentsByIssueID [%d]: %v", issue.ID, err)
+ }
+ }
+
+ if issue.Comments == nil {
+ issue.Comments, err = getCommentsByIssueID(e, issue.ID)
+ if err != nil {
+ return fmt.Errorf("getCommentsByIssueID [%d]: %v", issue.ID, err)
+ }
+ }
+
+ return nil
+}
+
+func (issue *Issue) LoadAttributes() error {
+ return issue.loadAttributes(x)
+}
+
+func (issue *Issue) HTMLURL() string {
+ var path string
+ if issue.IsPull {
+ path = "pulls"
+ } else {
+ path = "issues"
+ }
+ return fmt.Sprintf("%s/%s/%d", issue.Repo.HTMLURL(), path, issue.Index)
+}
+
+// State returns string representation of issue status.
+func (issue *Issue) State() api.StateType {
+ if issue.IsClosed {
+ return api.STATE_CLOSED
+ }
+ return api.STATE_OPEN
+}
+
+// This method assumes some fields assigned with values:
+// Required - Poster, Labels,
+// Optional - Milestone, Assignee, PullRequest
+func (issue *Issue) APIFormat() *api.Issue {
+ apiLabels := make([]*api.Label, len(issue.Labels))
+ for i := range issue.Labels {
+ apiLabels[i] = issue.Labels[i].APIFormat()
+ }
+
+ apiIssue := &api.Issue{
+ ID: issue.ID,
+ Index: issue.Index,
+ Poster: issue.Poster.APIFormat(),
+ Title: issue.Title,
+ Body: issue.Content,
+ Labels: apiLabels,
+ State: issue.State(),
+ Comments: issue.NumComments,
+ Created: issue.Created,
+ Updated: issue.Updated,
+ }
+
+ if issue.Milestone != nil {
+ apiIssue.Milestone = issue.Milestone.APIFormat()
+ }
+ if issue.Assignee != nil {
+ apiIssue.Assignee = issue.Assignee.APIFormat()
+ }
+ if issue.IsPull {
+ apiIssue.PullRequest = &api.PullRequestMeta{
+ HasMerged: issue.PullRequest.HasMerged,
+ }
+ if issue.PullRequest.HasMerged {
+ apiIssue.PullRequest.Merged = &issue.PullRequest.Merged
+ }
+ }
+
+ return apiIssue
+}
+
+// HashTag returns unique hash tag for issue.
+func (issue *Issue) HashTag() string {
+ return "issue-" + com.ToStr(issue.ID)
+}
+
+// IsPoster returns true if given user by ID is the poster.
+func (issue *Issue) IsPoster(uid int64) bool {
+ return issue.PosterID == uid
+}
+
+func (issue *Issue) hasLabel(e Engine, labelID int64) bool {
+ return hasIssueLabel(e, issue.ID, labelID)
+}
+
+// HasLabel returns true if issue has been labeled by given ID.
+func (issue *Issue) HasLabel(labelID int64) bool {
+ return issue.hasLabel(x, labelID)
+}
+
+func (issue *Issue) sendLabelUpdatedWebhook(doer *User) {
+ var err error
+ if issue.IsPull {
+ err = issue.PullRequest.LoadIssue()
+ if err != nil {
+ log.Error(2, "LoadIssue: %v", err)
+ return
+ }
+ err = PrepareWebhooks(issue.Repo, HOOK_EVENT_PULL_REQUEST, &api.PullRequestPayload{
+ Action: api.HOOK_ISSUE_LABEL_UPDATED,
+ Index: issue.Index,
+ PullRequest: issue.PullRequest.APIFormat(),
+ Repository: issue.Repo.APIFormat(nil),
+ Sender: doer.APIFormat(),
+ })
+ } else {
+ err = PrepareWebhooks(issue.Repo, HOOK_EVENT_ISSUES, &api.IssuesPayload{
+ Action: api.HOOK_ISSUE_LABEL_UPDATED,
+ Index: issue.Index,
+ Issue: issue.APIFormat(),
+ Repository: issue.Repo.APIFormat(nil),
+ Sender: doer.APIFormat(),
+ })
+ }
+ if err != nil {
+ log.Error(2, "PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err)
+ }
+}
+
+func (issue *Issue) addLabel(e *xorm.Session, label *Label) error {
+ return newIssueLabel(e, issue, label)
+}
+
+// AddLabel adds a new label to the issue.
+func (issue *Issue) AddLabel(doer *User, label *Label) error {
+ if err := NewIssueLabel(issue, label); err != nil {
+ return err
+ }
+
+ issue.sendLabelUpdatedWebhook(doer)
+ return nil
+}
+
+func (issue *Issue) addLabels(e *xorm.Session, labels []*Label) error {
+ return newIssueLabels(e, issue, labels)
+}
+
+// AddLabels adds a list of new labels to the issue.
+func (issue *Issue) AddLabels(doer *User, labels []*Label) error {
+ if err := NewIssueLabels(issue, labels); err != nil {
+ return err
+ }
+
+ issue.sendLabelUpdatedWebhook(doer)
+ return nil
+}
+
+func (issue *Issue) getLabels(e Engine) (err error) {
+ if len(issue.Labels) > 0 {
+ return nil
+ }
+
+ issue.Labels, err = getLabelsByIssueID(e, issue.ID)
+ if err != nil {
+ return fmt.Errorf("getLabelsByIssueID: %v", err)
+ }
+ return nil
+}
+
+func (issue *Issue) removeLabel(e *xorm.Session, label *Label) error {
+ return deleteIssueLabel(e, issue, label)
+}
+
+// RemoveLabel removes a label from issue by given ID.
+func (issue *Issue) RemoveLabel(doer *User, label *Label) error {
+ if err := DeleteIssueLabel(issue, label); err != nil {
+ return err
+ }
+
+ issue.sendLabelUpdatedWebhook(doer)
+ return nil
+}
+
+func (issue *Issue) clearLabels(e *xorm.Session) (err error) {
+ if err = issue.getLabels(e); err != nil {
+ return fmt.Errorf("getLabels: %v", err)
+ }
+
+ // NOTE: issue.removeLabel slices issue.Labels, so we need to create another slice to be unaffected.
+ labels := make([]*Label, len(issue.Labels))
+ for i := range issue.Labels {
+ labels[i] = issue.Labels[i]
+ }
+ for i := range labels {
+ if err = issue.removeLabel(e, labels[i]); err != nil {
+ return fmt.Errorf("removeLabel: %v", err)
+ }
+ }
+
+ return nil
+}
+
+func (issue *Issue) ClearLabels(doer *User) (err error) {
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if err = issue.clearLabels(sess); err != nil {
+ return err
+ }
+
+ if err = sess.Commit(); err != nil {
+ return fmt.Errorf("Commit: %v", err)
+ }
+
+ if issue.IsPull {
+ err = issue.PullRequest.LoadIssue()
+ if err != nil {
+ log.Error(2, "LoadIssue: %v", err)
+ return
+ }
+ err = PrepareWebhooks(issue.Repo, HOOK_EVENT_PULL_REQUEST, &api.PullRequestPayload{
+ Action: api.HOOK_ISSUE_LABEL_CLEARED,
+ Index: issue.Index,
+ PullRequest: issue.PullRequest.APIFormat(),
+ Repository: issue.Repo.APIFormat(nil),
+ Sender: doer.APIFormat(),
+ })
+ } else {
+ err = PrepareWebhooks(issue.Repo, HOOK_EVENT_ISSUES, &api.IssuesPayload{
+ Action: api.HOOK_ISSUE_LABEL_CLEARED,
+ Index: issue.Index,
+ Issue: issue.APIFormat(),
+ Repository: issue.Repo.APIFormat(nil),
+ Sender: doer.APIFormat(),
+ })
+ }
+ if err != nil {
+ log.Error(2, "PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err)
+ }
+
+ return nil
+}
+
+// ReplaceLabels removes all current labels and add new labels to the issue.
+func (issue *Issue) ReplaceLabels(labels []*Label) (err error) {
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if err = issue.clearLabels(sess); err != nil {
+ return fmt.Errorf("clearLabels: %v", err)
+ } else if err = issue.addLabels(sess, labels); err != nil {
+ return fmt.Errorf("addLabels: %v", err)
+ }
+
+ return sess.Commit()
+}
+
+func (issue *Issue) GetAssignee() (err error) {
+ if issue.AssigneeID == 0 || issue.Assignee != nil {
+ return nil
+ }
+
+ issue.Assignee, err = GetUserByID(issue.AssigneeID)
+ if errors.IsUserNotExist(err) {
+ return nil
+ }
+ return err
+}
+
+// ReadBy sets issue to be read by given user.
+func (issue *Issue) ReadBy(uid int64) error {
+ return UpdateIssueUserByRead(uid, issue.ID)
+}
+
+func updateIssueCols(e Engine, issue *Issue, cols ...string) error {
+ _, err := e.ID(issue.ID).Cols(cols...).Update(issue)
+ return err
+}
+
+// UpdateIssueCols only updates values of specific columns for given issue.
+func UpdateIssueCols(issue *Issue, cols ...string) error {
+ return updateIssueCols(x, issue, cols...)
+}
+
+func (issue *Issue) changeStatus(e *xorm.Session, doer *User, repo *Repository, isClosed bool) (err error) {
+ // Nothing should be performed if current status is same as target status
+ if issue.IsClosed == isClosed {
+ return nil
+ }
+ issue.IsClosed = isClosed
+
+ if err = updateIssueCols(e, issue, "is_closed"); err != nil {
+ return err
+ } else if err = updateIssueUsersByStatus(e, issue.ID, isClosed); err != nil {
+ return err
+ }
+
+ // Update issue count of labels
+ if err = issue.getLabels(e); err != nil {
+ return err
+ }
+ for idx := range issue.Labels {
+ if issue.IsClosed {
+ issue.Labels[idx].NumClosedIssues++
+ } else {
+ issue.Labels[idx].NumClosedIssues--
+ }
+ if err = updateLabel(e, issue.Labels[idx]); err != nil {
+ return err
+ }
+ }
+
+ // Update issue count of milestone
+ if err = changeMilestoneIssueStats(e, issue); err != nil {
+ return err
+ }
+
+ // New action comment
+ if _, err = createStatusComment(e, doer, repo, issue); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// ChangeStatus changes issue status to open or closed.
+func (issue *Issue) ChangeStatus(doer *User, repo *Repository, isClosed bool) (err error) {
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if err = issue.changeStatus(sess, doer, repo, isClosed); err != nil {
+ return err
+ }
+
+ if err = sess.Commit(); err != nil {
+ return fmt.Errorf("Commit: %v", err)
+ }
+
+ if issue.IsPull {
+ // Merge pull request calls issue.changeStatus so we need to handle separately.
+ issue.PullRequest.Issue = issue
+ apiPullRequest := &api.PullRequestPayload{
+ Index: issue.Index,
+ PullRequest: issue.PullRequest.APIFormat(),
+ Repository: repo.APIFormat(nil),
+ Sender: doer.APIFormat(),
+ }
+ if isClosed {
+ apiPullRequest.Action = api.HOOK_ISSUE_CLOSED
+ } else {
+ apiPullRequest.Action = api.HOOK_ISSUE_REOPENED
+ }
+ err = PrepareWebhooks(repo, HOOK_EVENT_PULL_REQUEST, apiPullRequest)
+ } else {
+ apiIssues := &api.IssuesPayload{
+ Index: issue.Index,
+ Issue: issue.APIFormat(),
+ Repository: repo.APIFormat(nil),
+ Sender: doer.APIFormat(),
+ }
+ if isClosed {
+ apiIssues.Action = api.HOOK_ISSUE_CLOSED
+ } else {
+ apiIssues.Action = api.HOOK_ISSUE_REOPENED
+ }
+ err = PrepareWebhooks(repo, HOOK_EVENT_ISSUES, apiIssues)
+ }
+ if err != nil {
+ log.Error(2, "PrepareWebhooks [is_pull: %v, is_closed: %v]: %v", issue.IsPull, isClosed, err)
+ }
+
+ return nil
+}
+
+func (issue *Issue) ChangeTitle(doer *User, title string) (err error) {
+ oldTitle := issue.Title
+ issue.Title = title
+ if err = UpdateIssueCols(issue, "name"); err != nil {
+ return fmt.Errorf("UpdateIssueCols: %v", err)
+ }
+
+ if issue.IsPull {
+ issue.PullRequest.Issue = issue
+ err = PrepareWebhooks(issue.Repo, HOOK_EVENT_PULL_REQUEST, &api.PullRequestPayload{
+ Action: api.HOOK_ISSUE_EDITED,
+ Index: issue.Index,
+ PullRequest: issue.PullRequest.APIFormat(),
+ Changes: &api.ChangesPayload{
+ Title: &api.ChangesFromPayload{
+ From: oldTitle,
+ },
+ },
+ Repository: issue.Repo.APIFormat(nil),
+ Sender: doer.APIFormat(),
+ })
+ } else {
+ err = PrepareWebhooks(issue.Repo, HOOK_EVENT_ISSUES, &api.IssuesPayload{
+ Action: api.HOOK_ISSUE_EDITED,
+ Index: issue.Index,
+ Issue: issue.APIFormat(),
+ Changes: &api.ChangesPayload{
+ Title: &api.ChangesFromPayload{
+ From: oldTitle,
+ },
+ },
+ Repository: issue.Repo.APIFormat(nil),
+ Sender: doer.APIFormat(),
+ })
+ }
+ if err != nil {
+ log.Error(2, "PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err)
+ }
+
+ return nil
+}
+
+func (issue *Issue) ChangeContent(doer *User, content string) (err error) {
+ oldContent := issue.Content
+ issue.Content = content
+ if err = UpdateIssueCols(issue, "content"); err != nil {
+ return fmt.Errorf("UpdateIssueCols: %v", err)
+ }
+
+ if issue.IsPull {
+ issue.PullRequest.Issue = issue
+ err = PrepareWebhooks(issue.Repo, HOOK_EVENT_PULL_REQUEST, &api.PullRequestPayload{
+ Action: api.HOOK_ISSUE_EDITED,
+ Index: issue.Index,
+ PullRequest: issue.PullRequest.APIFormat(),
+ Changes: &api.ChangesPayload{
+ Body: &api.ChangesFromPayload{
+ From: oldContent,
+ },
+ },
+ Repository: issue.Repo.APIFormat(nil),
+ Sender: doer.APIFormat(),
+ })
+ } else {
+ err = PrepareWebhooks(issue.Repo, HOOK_EVENT_ISSUES, &api.IssuesPayload{
+ Action: api.HOOK_ISSUE_EDITED,
+ Index: issue.Index,
+ Issue: issue.APIFormat(),
+ Changes: &api.ChangesPayload{
+ Body: &api.ChangesFromPayload{
+ From: oldContent,
+ },
+ },
+ Repository: issue.Repo.APIFormat(nil),
+ Sender: doer.APIFormat(),
+ })
+ }
+ if err != nil {
+ log.Error(2, "PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err)
+ }
+
+ return nil
+}
+
+func (issue *Issue) ChangeAssignee(doer *User, assigneeID int64) (err error) {
+ issue.AssigneeID = assigneeID
+ if err = UpdateIssueUserByAssignee(issue); err != nil {
+ return fmt.Errorf("UpdateIssueUserByAssignee: %v", err)
+ }
+
+ issue.Assignee, err = GetUserByID(issue.AssigneeID)
+ if err != nil && !errors.IsUserNotExist(err) {
+ log.Error(4, "GetUserByID [assignee_id: %v]: %v", issue.AssigneeID, err)
+ return nil
+ }
+
+ // Error not nil here means user does not exist, which is remove assignee.
+ isRemoveAssignee := err != nil
+ if issue.IsPull {
+ issue.PullRequest.Issue = issue
+ apiPullRequest := &api.PullRequestPayload{
+ Index: issue.Index,
+ PullRequest: issue.PullRequest.APIFormat(),
+ Repository: issue.Repo.APIFormat(nil),
+ Sender: doer.APIFormat(),
+ }
+ if isRemoveAssignee {
+ apiPullRequest.Action = api.HOOK_ISSUE_UNASSIGNED
+ } else {
+ apiPullRequest.Action = api.HOOK_ISSUE_ASSIGNED
+ }
+ err = PrepareWebhooks(issue.Repo, HOOK_EVENT_PULL_REQUEST, apiPullRequest)
+ } else {
+ apiIssues := &api.IssuesPayload{
+ Index: issue.Index,
+ Issue: issue.APIFormat(),
+ Repository: issue.Repo.APIFormat(nil),
+ Sender: doer.APIFormat(),
+ }
+ if isRemoveAssignee {
+ apiIssues.Action = api.HOOK_ISSUE_UNASSIGNED
+ } else {
+ apiIssues.Action = api.HOOK_ISSUE_ASSIGNED
+ }
+ err = PrepareWebhooks(issue.Repo, HOOK_EVENT_ISSUES, apiIssues)
+ }
+ if err != nil {
+ log.Error(4, "PrepareWebhooks [is_pull: %v, remove_assignee: %v]: %v", issue.IsPull, isRemoveAssignee, err)
+ }
+
+ return nil
+}
+
+type NewIssueOptions struct {
+ Repo *Repository
+ Issue *Issue
+ LableIDs []int64
+ Attachments []string // In UUID format.
+ IsPull bool
+}
+
+func newIssue(e *xorm.Session, opts NewIssueOptions) (err error) {
+ opts.Issue.Title = strings.TrimSpace(opts.Issue.Title)
+ opts.Issue.Index = opts.Repo.NextIssueIndex()
+
+ if opts.Issue.MilestoneID > 0 {
+ milestone, err := getMilestoneByRepoID(e, opts.Issue.RepoID, opts.Issue.MilestoneID)
+ if err != nil && !IsErrMilestoneNotExist(err) {
+ return fmt.Errorf("getMilestoneByID: %v", err)
+ }
+
+ // Assume milestone is invalid and drop silently.
+ opts.Issue.MilestoneID = 0
+ if milestone != nil {
+ opts.Issue.MilestoneID = milestone.ID
+ opts.Issue.Milestone = milestone
+ if err = changeMilestoneAssign(e, opts.Issue, -1); err != nil {
+ return err
+ }
+ }
+ }
+
+ if opts.Issue.AssigneeID > 0 {
+ assignee, err := getUserByID(e, opts.Issue.AssigneeID)
+ if err != nil && !errors.IsUserNotExist(err) {
+ return fmt.Errorf("getUserByID: %v", err)
+ }
+
+ // Assume assignee is invalid and drop silently.
+ opts.Issue.AssigneeID = 0
+ if assignee != nil {
+ valid, err := hasAccess(e, assignee.ID, opts.Repo, ACCESS_MODE_READ)
+ if err != nil {
+ return fmt.Errorf("hasAccess [user_id: %d, repo_id: %d]: %v", assignee.ID, opts.Repo.ID, err)
+ }
+ if valid {
+ opts.Issue.AssigneeID = assignee.ID
+ opts.Issue.Assignee = assignee
+ }
+ }
+ }
+
+ // Milestone and assignee validation should happen before insert actual object.
+ if _, err = e.Insert(opts.Issue); err != nil {
+ return err
+ }
+
+ if opts.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)
+ }
+ if err != nil {
+ return err
+ }
+
+ if len(opts.LableIDs) > 0 {
+ // During the session, SQLite3 driver cannot handle retrieve objects after update something.
+ // So we have to get all needed labels first.
+ labels := make([]*Label, 0, len(opts.LableIDs))
+ if err = e.In("id", opts.LableIDs).Find(&labels); err != nil {
+ return fmt.Errorf("find all labels [label_ids: %v]: %v", opts.LableIDs, err)
+ }
+
+ for _, label := range labels {
+ // Silently drop invalid labels.
+ if label.RepoID != opts.Repo.ID {
+ continue
+ }
+
+ if err = opts.Issue.addLabel(e, label); err != nil {
+ return fmt.Errorf("addLabel [id: %d]: %v", label.ID, err)
+ }
+ }
+ }
+
+ if err = newIssueUsers(e, opts.Repo, opts.Issue); err != nil {
+ return err
+ }
+
+ if len(opts.Attachments) > 0 {
+ attachments, err := getAttachmentsByUUIDs(e, opts.Attachments)
+ if err != nil {
+ return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %v", opts.Attachments, err)
+ }
+
+ for i := 0; i < len(attachments); i++ {
+ attachments[i].IssueID = opts.Issue.ID
+ if _, err = e.Id(attachments[i].ID).Update(attachments[i]); err != nil {
+ return fmt.Errorf("update attachment [id: %d]: %v", attachments[i].ID, err)
+ }
+ }
+ }
+
+ return opts.Issue.loadAttributes(e)
+}
+
+// NewIssue creates new issue with labels and attachments for repository.
+func NewIssue(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, NewIssueOptions{
+ Repo: repo,
+ Issue: issue,
+ LableIDs: labelIDs,
+ Attachments: uuids,
+ }); err != nil {
+ return fmt.Errorf("newIssue: %v", err)
+ }
+
+ if err = sess.Commit(); err != nil {
+ return fmt.Errorf("Commit: %v", err)
+ }
+
+ if err = NotifyWatchers(&Action{
+ ActUserID: issue.Poster.ID,
+ ActUserName: issue.Poster.Name,
+ OpType: ACTION_CREATE_ISSUE,
+ Content: fmt.Sprintf("%d|%s", issue.Index, issue.Title),
+ RepoID: repo.ID,
+ RepoUserName: repo.Owner.Name,
+ RepoName: repo.Name,
+ IsPrivate: repo.IsPrivate,
+ }); err != nil {
+ log.Error(2, "NotifyWatchers: %v", err)
+ }
+ if err = issue.MailParticipants(); err != nil {
+ log.Error(2, "MailParticipants: %v", err)
+ }
+
+ if err = PrepareWebhooks(repo, HOOK_EVENT_ISSUES, &api.IssuesPayload{
+ Action: api.HOOK_ISSUE_OPENED,
+ Index: issue.Index,
+ Issue: issue.APIFormat(),
+ Repository: repo.APIFormat(nil),
+ Sender: issue.Poster.APIFormat(),
+ }); err != nil {
+ log.Error(2, "PrepareWebhooks: %v", err)
+ }
+
+ return nil
+}
+
+// GetIssueByRef returns an Issue specified by a GFM reference.
+// See https://help.github.com/articles/writing-on-github#references for more information on the syntax.
+func GetIssueByRef(ref string) (*Issue, error) {
+ n := strings.IndexByte(ref, byte('#'))
+ if n == -1 {
+ return nil, errors.InvalidIssueReference{ref}
+ }
+
+ index := com.StrTo(ref[n+1:]).MustInt64()
+ if index == 0 {
+ return nil, errors.IssueNotExist{}
+ }
+
+ repo, err := GetRepositoryByRef(ref[:n])
+ if err != nil {
+ return nil, err
+ }
+
+ issue, err := GetIssueByIndex(repo.ID, index)
+ if err != nil {
+ return nil, err
+ }
+
+ return issue, issue.LoadAttributes()
+}
+
+// GetIssueByIndex returns raw issue without loading attributes by index in a repository.
+func GetRawIssueByIndex(repoID, index int64) (*Issue, error) {
+ issue := &Issue{
+ RepoID: repoID,
+ Index: index,
+ }
+ has, err := x.Get(issue)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, errors.IssueNotExist{0, repoID, index}
+ }
+ return issue, nil
+}
+
+// GetIssueByIndex returns issue by index in a repository.
+func GetIssueByIndex(repoID, index int64) (*Issue, error) {
+ issue, err := GetRawIssueByIndex(repoID, index)
+ if err != nil {
+ return nil, err
+ }
+ return issue, issue.LoadAttributes()
+}
+
+func getRawIssueByID(e Engine, id int64) (*Issue, error) {
+ issue := new(Issue)
+ has, err := e.ID(id).Get(issue)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, errors.IssueNotExist{id, 0, 0}
+ }
+ return issue, nil
+}
+
+func getIssueByID(e Engine, id int64) (*Issue, error) {
+ issue, err := getRawIssueByID(e, id)
+ if err != nil {
+ return nil, err
+ }
+ return issue, issue.loadAttributes(e)
+}
+
+// GetIssueByID returns an issue by given ID.
+func GetIssueByID(id int64) (*Issue, error) {
+ return getIssueByID(x, id)
+}
+
+type IssuesOptions struct {
+ UserID int64
+ AssigneeID int64
+ RepoID int64
+ PosterID int64
+ MilestoneID int64
+ RepoIDs []int64
+ Page int
+ IsClosed bool
+ IsMention bool
+ IsPull bool
+ Labels string
+ SortType string
+}
+
+// buildIssuesQuery returns nil if it foresees there won't be any value returned.
+func buildIssuesQuery(opts *IssuesOptions) *xorm.Session {
+ sess := x.NewSession()
+
+ if opts.Page <= 0 {
+ opts.Page = 1
+ }
+
+ if opts.RepoID > 0 {
+ sess.Where("issue.repo_id=?", opts.RepoID).And("issue.is_closed=?", opts.IsClosed)
+ } else if opts.RepoIDs != nil {
+ // In case repository IDs are provided but actually no repository has issue.
+ if len(opts.RepoIDs) == 0 {
+ return nil
+ }
+ sess.In("issue.repo_id", opts.RepoIDs).And("issue.is_closed=?", opts.IsClosed)
+ } else {
+ sess.Where("issue.is_closed=?", opts.IsClosed)
+ }
+
+ if opts.AssigneeID > 0 {
+ sess.And("issue.assignee_id=?", opts.AssigneeID)
+ } else if opts.PosterID > 0 {
+ sess.And("issue.poster_id=?", opts.PosterID)
+ }
+
+ if opts.MilestoneID > 0 {
+ sess.And("issue.milestone_id=?", opts.MilestoneID)
+ }
+
+ sess.And("issue.is_pull=?", opts.IsPull)
+
+ switch opts.SortType {
+ case "oldest":
+ sess.Asc("issue.created_unix")
+ case "recentupdate":
+ sess.Desc("issue.updated_unix")
+ case "leastupdate":
+ sess.Asc("issue.updated_unix")
+ case "mostcomment":
+ sess.Desc("issue.num_comments")
+ case "leastcomment":
+ sess.Asc("issue.num_comments")
+ case "priority":
+ sess.Desc("issue.priority")
+ default:
+ sess.Desc("issue.created_unix")
+ }
+
+ if len(opts.Labels) > 0 && opts.Labels != "0" {
+ labelIDs := strings.Split(opts.Labels, ",")
+ if len(labelIDs) > 0 {
+ sess.Join("INNER", "issue_label", "issue.id = issue_label.issue_id").In("issue_label.label_id", labelIDs)
+ }
+ }
+
+ if opts.IsMention {
+ sess.Join("INNER", "issue_user", "issue.id = issue_user.issue_id").And("issue_user.is_mentioned = ?", true)
+
+ if opts.UserID > 0 {
+ sess.And("issue_user.uid = ?", opts.UserID)
+ }
+ }
+
+ return sess
+}
+
+// IssuesCount returns the number of issues by given conditions.
+func IssuesCount(opts *IssuesOptions) (int64, error) {
+ sess := buildIssuesQuery(opts)
+ if sess == nil {
+ return 0, nil
+ }
+
+ return sess.Count(&Issue{})
+}
+
+// Issues returns a list of issues by given conditions.
+func Issues(opts *IssuesOptions) ([]*Issue, error) {
+ sess := buildIssuesQuery(opts)
+ if sess == nil {
+ return make([]*Issue, 0), nil
+ }
+
+ sess.Limit(setting.UI.IssuePagingNum, (opts.Page-1)*setting.UI.IssuePagingNum)
+
+ issues := make([]*Issue, 0, setting.UI.IssuePagingNum)
+ if err := sess.Find(&issues); err != nil {
+ return nil, fmt.Errorf("Find: %v", err)
+ }
+
+ // FIXME: use IssueList to improve performance.
+ for i := range issues {
+ if err := issues[i].LoadAttributes(); err != nil {
+ return nil, fmt.Errorf("LoadAttributes [%d]: %v", issues[i].ID, err)
+ }
+ }
+
+ return issues, nil
+}
+
+// GetParticipantsByIssueID returns all users who are participated in comments of an issue.
+func GetParticipantsByIssueID(issueID int64) ([]*User, error) {
+ userIDs := make([]int64, 0, 5)
+ if err := x.Table("comment").Cols("poster_id").
+ Where("issue_id = ?", issueID).
+ Distinct("poster_id").
+ Find(&userIDs); err != nil {
+ return nil, fmt.Errorf("get poster IDs: %v", err)
+ }
+ if len(userIDs) == 0 {
+ return nil, nil
+ }
+
+ users := make([]*User, 0, len(userIDs))
+ return users, x.In("id", userIDs).Find(&users)
+}
+
+// .___ ____ ___
+// | | ______ ________ __ ____ | | \______ ___________
+// | |/ ___// ___/ | \_/ __ \| | / ___// __ \_ __ \
+// | |\___ \ \___ \| | /\ ___/| | /\___ \\ ___/| | \/
+// |___/____ >____ >____/ \___ >______//____ >\___ >__|
+// \/ \/ \/ \/ \/
+
+// IssueUser represents an issue-user relation.
+type IssueUser struct {
+ ID int64
+ UID int64 `xorm:"INDEX"` // User ID.
+ IssueID int64
+ RepoID int64 `xorm:"INDEX"`
+ MilestoneID int64
+ IsRead bool
+ IsAssigned bool
+ IsMentioned bool
+ IsPoster bool
+ IsClosed bool
+}
+
+func newIssueUsers(e *xorm.Session, repo *Repository, issue *Issue) error {
+ assignees, err := repo.getAssignees(e)
+ if err != nil {
+ return fmt.Errorf("getAssignees: %v", err)
+ }
+
+ // Poster can be anyone, append later if not one of assignees.
+ isPosterAssignee := false
+
+ // Leave a seat for poster itself to append later, but if poster is one of assignee
+ // and just waste 1 unit is cheaper than re-allocate memory once.
+ issueUsers := make([]*IssueUser, 0, len(assignees)+1)
+ for _, assignee := range assignees {
+ isPoster := assignee.ID == issue.PosterID
+ issueUsers = append(issueUsers, &IssueUser{
+ IssueID: issue.ID,
+ RepoID: repo.ID,
+ UID: assignee.ID,
+ IsPoster: isPoster,
+ IsAssigned: assignee.ID == issue.AssigneeID,
+ })
+ if !isPosterAssignee && isPoster {
+ isPosterAssignee = true
+ }
+ }
+ if !isPosterAssignee {
+ issueUsers = append(issueUsers, &IssueUser{
+ IssueID: issue.ID,
+ RepoID: repo.ID,
+ UID: issue.PosterID,
+ IsPoster: true,
+ })
+ }
+
+ if _, err = e.Insert(issueUsers); err != nil {
+ return err
+ }
+ return nil
+}
+
+// NewIssueUsers adds new issue-user relations for new issue of repository.
+func NewIssueUsers(repo *Repository, issue *Issue) (err error) {
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if err = newIssueUsers(sess, repo, issue); err != nil {
+ return err
+ }
+
+ return sess.Commit()
+}
+
+// PairsContains returns true when pairs list contains given issue.
+func PairsContains(ius []*IssueUser, issueId, uid int64) int {
+ for i := range ius {
+ if ius[i].IssueID == issueId &&
+ ius[i].UID == uid {
+ return i
+ }
+ }
+ return -1
+}
+
+// GetIssueUsers returns issue-user pairs by given repository and user.
+func GetIssueUsers(rid, uid int64, isClosed bool) ([]*IssueUser, error) {
+ ius := make([]*IssueUser, 0, 10)
+ err := x.Where("is_closed=?", isClosed).Find(&ius, &IssueUser{RepoID: rid, UID: uid})
+ return ius, err
+}
+
+// GetIssueUserPairsByRepoIds returns issue-user pairs by given repository IDs.
+func GetIssueUserPairsByRepoIds(rids []int64, isClosed bool, page int) ([]*IssueUser, error) {
+ if len(rids) == 0 {
+ return []*IssueUser{}, nil
+ }
+
+ ius := make([]*IssueUser, 0, 10)
+ sess := x.Limit(20, (page-1)*20).Where("is_closed=?", isClosed).In("repo_id", rids)
+ err := sess.Find(&ius)
+ return ius, err
+}
+
+// GetIssueUserPairsByMode returns issue-user pairs by given repository and user.
+func GetIssueUserPairsByMode(userID, repoID int64, filterMode FilterMode, isClosed bool, page int) ([]*IssueUser, error) {
+ ius := make([]*IssueUser, 0, 10)
+ sess := x.Limit(20, (page-1)*20).Where("uid=?", userID).And("is_closed=?", isClosed)
+ if repoID > 0 {
+ sess.And("repo_id=?", repoID)
+ }
+
+ switch filterMode {
+ case FILTER_MODE_ASSIGN:
+ sess.And("is_assigned=?", true)
+ case FILTER_MODE_CREATE:
+ sess.And("is_poster=?", true)
+ default:
+ return ius, nil
+ }
+ err := sess.Find(&ius)
+ return ius, err
+}
+
+// updateIssueMentions extracts mentioned people from content and
+// updates issue-user relations for them.
+func updateIssueMentions(e Engine, issueID int64, mentions []string) error {
+ if len(mentions) == 0 {
+ return nil
+ }
+
+ for i := range mentions {
+ mentions[i] = strings.ToLower(mentions[i])
+ }
+ users := make([]*User, 0, len(mentions))
+
+ if err := e.In("lower_name", mentions).Asc("lower_name").Find(&users); err != nil {
+ return fmt.Errorf("find mentioned users: %v", err)
+ }
+
+ ids := make([]int64, 0, len(mentions))
+ for _, user := range users {
+ ids = append(ids, user.ID)
+ if !user.IsOrganization() || user.NumMembers == 0 {
+ continue
+ }
+
+ memberIDs := make([]int64, 0, user.NumMembers)
+ orgUsers, err := getOrgUsersByOrgID(e, user.ID)
+ if err != nil {
+ return fmt.Errorf("getOrgUsersByOrgID [%d]: %v", user.ID, err)
+ }
+
+ for _, orgUser := range orgUsers {
+ memberIDs = append(memberIDs, orgUser.ID)
+ }
+
+ ids = append(ids, memberIDs...)
+ }
+
+ if err := updateIssueUsersByMentions(e, issueID, ids); err != nil {
+ return fmt.Errorf("UpdateIssueUsersByMentions: %v", err)
+ }
+
+ return nil
+}
+
+// IssueStats represents issue statistic information.
+type IssueStats struct {
+ OpenCount, ClosedCount int64
+ YourReposCount int64
+ AssignCount int64
+ CreateCount int64
+ MentionCount int64
+}
+
+type FilterMode string
+
+const (
+ FILTER_MODE_YOUR_REPOS FilterMode = "your_repositories"
+ FILTER_MODE_ASSIGN FilterMode = "assigned"
+ FILTER_MODE_CREATE FilterMode = "created_by"
+ FILTER_MODE_MENTION FilterMode = "mentioned"
+)
+
+func parseCountResult(results []map[string][]byte) int64 {
+ if len(results) == 0 {
+ return 0
+ }
+ for _, result := range results[0] {
+ return com.StrTo(string(result)).MustInt64()
+ }
+ return 0
+}
+
+type IssueStatsOptions struct {
+ RepoID int64
+ UserID int64
+ Labels string
+ MilestoneID int64
+ AssigneeID int64
+ FilterMode FilterMode
+ IsPull bool
+}
+
+// GetIssueStats returns issue statistic information by given conditions.
+func GetIssueStats(opts *IssueStatsOptions) *IssueStats {
+ stats := &IssueStats{}
+
+ countSession := func(opts *IssueStatsOptions) *xorm.Session {
+ sess := x.Where("issue.repo_id = ?", opts.RepoID).And("is_pull = ?", opts.IsPull)
+
+ if len(opts.Labels) > 0 && opts.Labels != "0" {
+ labelIDs := tool.StringsToInt64s(strings.Split(opts.Labels, ","))
+ if len(labelIDs) > 0 {
+ sess.Join("INNER", "issue_label", "issue.id = issue_id").In("label_id", labelIDs)
+ }
+ }
+
+ if opts.MilestoneID > 0 {
+ sess.And("issue.milestone_id = ?", opts.MilestoneID)
+ }
+
+ if opts.AssigneeID > 0 {
+ sess.And("assignee_id = ?", opts.AssigneeID)
+ }
+
+ return sess
+ }
+
+ switch opts.FilterMode {
+ case FILTER_MODE_YOUR_REPOS, FILTER_MODE_ASSIGN:
+ stats.OpenCount, _ = countSession(opts).
+ And("is_closed = ?", false).
+ Count(new(Issue))
+
+ stats.ClosedCount, _ = countSession(opts).
+ And("is_closed = ?", true).
+ Count(new(Issue))
+ case FILTER_MODE_CREATE:
+ stats.OpenCount, _ = countSession(opts).
+ And("poster_id = ?", opts.UserID).
+ And("is_closed = ?", false).
+ Count(new(Issue))
+
+ stats.ClosedCount, _ = countSession(opts).
+ And("poster_id = ?", opts.UserID).
+ And("is_closed = ?", true).
+ Count(new(Issue))
+ case FILTER_MODE_MENTION:
+ stats.OpenCount, _ = countSession(opts).
+ Join("INNER", "issue_user", "issue.id = issue_user.issue_id").
+ And("issue_user.uid = ?", opts.UserID).
+ And("issue_user.is_mentioned = ?", true).
+ And("issue.is_closed = ?", false).
+ Count(new(Issue))
+
+ stats.ClosedCount, _ = countSession(opts).
+ Join("INNER", "issue_user", "issue.id = issue_user.issue_id").
+ And("issue_user.uid = ?", opts.UserID).
+ And("issue_user.is_mentioned = ?", true).
+ And("issue.is_closed = ?", true).
+ Count(new(Issue))
+ }
+ return stats
+}
+
+// GetUserIssueStats returns issue statistic information for dashboard by given conditions.
+func GetUserIssueStats(repoID, userID int64, repoIDs []int64, filterMode FilterMode, isPull bool) *IssueStats {
+ stats := &IssueStats{}
+ hasAnyRepo := repoID > 0 || len(repoIDs) > 0
+ countSession := func(isClosed, isPull bool, repoID int64, repoIDs []int64) *xorm.Session {
+ sess := x.Where("issue.is_closed = ?", isClosed).And("issue.is_pull = ?", isPull)
+
+ if repoID > 0 {
+ sess.And("repo_id = ?", repoID)
+ } else if len(repoIDs) > 0 {
+ sess.In("repo_id", repoIDs)
+ }
+
+ return sess
+ }
+
+ stats.AssignCount, _ = countSession(false, isPull, repoID, nil).
+ And("assignee_id = ?", userID).
+ Count(new(Issue))
+
+ stats.CreateCount, _ = countSession(false, isPull, repoID, nil).
+ And("poster_id = ?", userID).
+ Count(new(Issue))
+
+ if hasAnyRepo {
+ stats.YourReposCount, _ = countSession(false, isPull, repoID, repoIDs).
+ Count(new(Issue))
+ }
+
+ switch filterMode {
+ case FILTER_MODE_YOUR_REPOS:
+ if !hasAnyRepo {
+ break
+ }
+
+ stats.OpenCount, _ = countSession(false, isPull, repoID, repoIDs).
+ Count(new(Issue))
+ stats.ClosedCount, _ = countSession(true, isPull, repoID, repoIDs).
+ Count(new(Issue))
+ case FILTER_MODE_ASSIGN:
+ stats.OpenCount, _ = countSession(false, isPull, repoID, nil).
+ And("assignee_id = ?", userID).
+ Count(new(Issue))
+ stats.ClosedCount, _ = countSession(true, isPull, repoID, nil).
+ And("assignee_id = ?", userID).
+ Count(new(Issue))
+ case FILTER_MODE_CREATE:
+ stats.OpenCount, _ = countSession(false, isPull, repoID, nil).
+ And("poster_id = ?", userID).
+ Count(new(Issue))
+ stats.ClosedCount, _ = countSession(true, isPull, repoID, nil).
+ And("poster_id = ?", userID).
+ Count(new(Issue))
+ }
+
+ return stats
+}
+
+// GetRepoIssueStats returns number of open and closed repository issues by given filter mode.
+func GetRepoIssueStats(repoID, userID int64, filterMode FilterMode, isPull bool) (numOpen int64, numClosed int64) {
+ countSession := func(isClosed, isPull bool, repoID int64) *xorm.Session {
+ sess := x.Where("issue.repo_id = ?", isClosed).
+ And("is_pull = ?", isPull).
+ And("repo_id = ?", repoID)
+
+ return sess
+ }
+
+ openCountSession := countSession(false, isPull, repoID)
+ closedCountSession := countSession(true, isPull, repoID)
+
+ switch filterMode {
+ case FILTER_MODE_ASSIGN:
+ openCountSession.And("assignee_id = ?", userID)
+ closedCountSession.And("assignee_id = ?", userID)
+ case FILTER_MODE_CREATE:
+ openCountSession.And("poster_id = ?", userID)
+ closedCountSession.And("poster_id = ?", userID)
+ }
+
+ openResult, _ := openCountSession.Count(new(Issue))
+ closedResult, _ := closedCountSession.Count(new(Issue))
+
+ return openResult, closedResult
+}
+
+func updateIssue(e Engine, issue *Issue) error {
+ _, err := e.ID(issue.ID).AllCols().Update(issue)
+ return err
+}
+
+// UpdateIssue updates all fields of given issue.
+func UpdateIssue(issue *Issue) error {
+ return updateIssue(x, issue)
+}
+
+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
+}
+
+// UpdateIssueUsersByStatus updates issue-user relations by issue status.
+func UpdateIssueUsersByStatus(issueID int64, isClosed bool) error {
+ return updateIssueUsersByStatus(x, issueID, isClosed)
+}
+
+func updateIssueUserByAssignee(e *xorm.Session, issue *Issue) (err error) {
+ if _, err = e.Exec("UPDATE `issue_user` SET is_assigned = ? WHERE issue_id = ?", false, issue.ID); err != nil {
+ return err
+ }
+
+ // Assignee ID equals to 0 means clear assignee.
+ if issue.AssigneeID > 0 {
+ if _, err = e.Exec("UPDATE `issue_user` SET is_assigned = ? WHERE uid = ? AND issue_id = ?", true, issue.AssigneeID, issue.ID); err != nil {
+ return err
+ }
+ }
+
+ return updateIssue(e, issue)
+}
+
+// UpdateIssueUserByAssignee updates issue-user relation for assignee.
+func UpdateIssueUserByAssignee(issue *Issue) (err error) {
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if err = updateIssueUserByAssignee(sess, issue); err != nil {
+ return err
+ }
+
+ return sess.Commit()
+}
+
+// UpdateIssueUserByRead updates issue-user relation for reading.
+func UpdateIssueUserByRead(uid, issueID int64) error {
+ _, err := x.Exec("UPDATE `issue_user` SET is_read=? WHERE uid=? AND issue_id=?", true, uid, issueID)
+ return err
+}
+
+// updateIssueUsersByMentions updates issue-user pairs by mentioning.
+func updateIssueUsersByMentions(e Engine, issueID int64, uids []int64) error {
+ for _, uid := range uids {
+ iu := &IssueUser{
+ UID: uid,
+ IssueID: issueID,
+ }
+ has, err := e.Get(iu)
+ if err != nil {
+ return err
+ }
+
+ iu.IsMentioned = true
+ if has {
+ _, err = e.ID(iu.ID).AllCols().Update(iu)
+ } else {
+ _, err = e.Insert(iu)
+ }
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/internal/db/issue_label.go b/internal/db/issue_label.go
new file mode 100644
index 00000000..ab875771
--- /dev/null
+++ b/internal/db/issue_label.go
@@ -0,0 +1,374 @@
+// Copyright 2016 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 db
+
+import (
+ "fmt"
+ "html/template"
+ "regexp"
+ "strconv"
+ "strings"
+
+ "xorm.io/xorm"
+
+ api "github.com/gogs/go-gogs-client"
+
+ "gogs.io/gogs/internal/tool"
+)
+
+var labelColorPattern = regexp.MustCompile("#([a-fA-F0-9]{6})")
+
+// GetLabelTemplateFile loads the label template file by given name,
+// then parses and returns a list of name-color pairs.
+func GetLabelTemplateFile(name string) ([][2]string, error) {
+ data, err := getRepoInitFile("label", name)
+ if err != nil {
+ return nil, fmt.Errorf("getRepoInitFile: %v", err)
+ }
+
+ lines := strings.Split(string(data), "\n")
+ list := make([][2]string, 0, len(lines))
+ for i := 0; i < len(lines); i++ {
+ line := strings.TrimSpace(lines[i])
+ if len(line) == 0 {
+ continue
+ }
+
+ fields := strings.SplitN(line, " ", 2)
+ if len(fields) != 2 {
+ return nil, fmt.Errorf("line is malformed: %s", line)
+ }
+
+ if !labelColorPattern.MatchString(fields[0]) {
+ return nil, fmt.Errorf("bad HTML color code in line: %s", line)
+ }
+
+ fields[1] = strings.TrimSpace(fields[1])
+ list = append(list, [2]string{fields[1], fields[0]})
+ }
+
+ return list, nil
+}
+
+// Label represents a label of repository for issues.
+type Label struct {
+ ID int64
+ RepoID int64 `xorm:"INDEX"`
+ Name string
+ Color string `xorm:"VARCHAR(7)"`
+ NumIssues int
+ NumClosedIssues int
+ NumOpenIssues int `xorm:"-" json:"-"`
+ IsChecked bool `xorm:"-" json:"-"`
+}
+
+func (label *Label) APIFormat() *api.Label {
+ return &api.Label{
+ ID: label.ID,
+ Name: label.Name,
+ Color: strings.TrimLeft(label.Color, "#"),
+ }
+}
+
+// CalOpenIssues calculates the open issues of label.
+func (label *Label) CalOpenIssues() {
+ label.NumOpenIssues = label.NumIssues - label.NumClosedIssues
+}
+
+// ForegroundColor calculates the text color for labels based
+// on their background color.
+func (l *Label) ForegroundColor() template.CSS {
+ if strings.HasPrefix(l.Color, "#") {
+ if color, err := strconv.ParseUint(l.Color[1:], 16, 64); err == nil {
+ r := float32(0xFF & (color >> 16))
+ g := float32(0xFF & (color >> 8))
+ b := float32(0xFF & color)
+ luminance := (0.2126*r + 0.7152*g + 0.0722*b) / 255
+
+ if luminance < 0.66 {
+ return template.CSS("#fff")
+ }
+ }
+ }
+
+ // default to black
+ return template.CSS("#000")
+}
+
+// NewLabels creates new label(s) for a repository.
+func NewLabels(labels ...*Label) error {
+ _, err := x.Insert(labels)
+ return err
+}
+
+// getLabelOfRepoByName returns a label by Name in given repository.
+// If pass repoID as 0, then ORM will ignore limitation of repository
+// and can return arbitrary label with any valid ID.
+func getLabelOfRepoByName(e Engine, repoID int64, labelName string) (*Label, error) {
+ if len(labelName) <= 0 {
+ return nil, ErrLabelNotExist{0, repoID}
+ }
+
+ l := &Label{
+ Name: labelName,
+ RepoID: repoID,
+ }
+ has, err := x.Get(l)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, ErrLabelNotExist{0, l.RepoID}
+ }
+ return l, nil
+}
+
+// getLabelInRepoByID returns a label by ID in given repository.
+// If pass repoID as 0, then ORM will ignore limitation of repository
+// and can return arbitrary label with any valid ID.
+func getLabelOfRepoByID(e Engine, repoID, labelID int64) (*Label, error) {
+ if labelID <= 0 {
+ return nil, ErrLabelNotExist{labelID, repoID}
+ }
+
+ l := &Label{
+ ID: labelID,
+ RepoID: repoID,
+ }
+ has, err := x.Get(l)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, ErrLabelNotExist{l.ID, l.RepoID}
+ }
+ return l, nil
+}
+
+// GetLabelByID returns a label by given ID.
+func GetLabelByID(id int64) (*Label, error) {
+ return getLabelOfRepoByID(x, 0, id)
+}
+
+// GetLabelOfRepoByID returns a label by ID in given repository.
+func GetLabelOfRepoByID(repoID, labelID int64) (*Label, error) {
+ return getLabelOfRepoByID(x, repoID, labelID)
+}
+
+// GetLabelOfRepoByName returns a label by name in given repository.
+func GetLabelOfRepoByName(repoID int64, labelName string) (*Label, error) {
+ return getLabelOfRepoByName(x, repoID, labelName)
+}
+
+// GetLabelsInRepoByIDs returns a list of labels by IDs in given repository,
+// it silently ignores label IDs that are not belong to the repository.
+func GetLabelsInRepoByIDs(repoID int64, labelIDs []int64) ([]*Label, error) {
+ labels := make([]*Label, 0, len(labelIDs))
+ return labels, x.Where("repo_id = ?", repoID).In("id", tool.Int64sToStrings(labelIDs)).Asc("name").Find(&labels)
+}
+
+// GetLabelsByRepoID returns all labels that belong to given repository by ID.
+func GetLabelsByRepoID(repoID int64) ([]*Label, error) {
+ labels := make([]*Label, 0, 10)
+ return labels, x.Where("repo_id = ?", repoID).Asc("name").Find(&labels)
+}
+
+func getLabelsByIssueID(e Engine, issueID int64) ([]*Label, error) {
+ issueLabels, err := getIssueLabels(e, issueID)
+ if err != nil {
+ return nil, fmt.Errorf("getIssueLabels: %v", err)
+ } else if len(issueLabels) == 0 {
+ return []*Label{}, nil
+ }
+
+ labelIDs := make([]int64, len(issueLabels))
+ for i := range issueLabels {
+ labelIDs[i] = issueLabels[i].LabelID
+ }
+
+ labels := make([]*Label, 0, len(labelIDs))
+ return labels, e.Where("id > 0").In("id", tool.Int64sToStrings(labelIDs)).Asc("name").Find(&labels)
+}
+
+// GetLabelsByIssueID returns all labels that belong to given issue by ID.
+func GetLabelsByIssueID(issueID int64) ([]*Label, error) {
+ return getLabelsByIssueID(x, issueID)
+}
+
+func updateLabel(e Engine, l *Label) error {
+ _, err := e.ID(l.ID).AllCols().Update(l)
+ return err
+}
+
+// UpdateLabel updates label information.
+func UpdateLabel(l *Label) error {
+ return updateLabel(x, l)
+}
+
+// DeleteLabel delete a label of given repository.
+func DeleteLabel(repoID, labelID int64) error {
+ _, err := GetLabelOfRepoByID(repoID, labelID)
+ if err != nil {
+ if IsErrLabelNotExist(err) {
+ return nil
+ }
+ return err
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if _, err = sess.ID(labelID).Delete(new(Label)); err != nil {
+ return err
+ } else if _, err = sess.Where("label_id = ?", labelID).Delete(new(IssueLabel)); err != nil {
+ return err
+ }
+
+ return sess.Commit()
+}
+
+// .___ .____ ___. .__
+// | | ______ ________ __ ____ | | _____ \_ |__ ____ | |
+// | |/ ___// ___/ | \_/ __ \| | \__ \ | __ \_/ __ \| |
+// | |\___ \ \___ \| | /\ ___/| |___ / __ \| \_\ \ ___/| |__
+// |___/____ >____ >____/ \___ >_______ (____ /___ /\___ >____/
+// \/ \/ \/ \/ \/ \/ \/
+
+// IssueLabel represetns an issue-lable relation.
+type IssueLabel struct {
+ ID int64
+ IssueID int64 `xorm:"UNIQUE(s)"`
+ LabelID int64 `xorm:"UNIQUE(s)"`
+}
+
+func hasIssueLabel(e Engine, issueID, labelID int64) bool {
+ has, _ := e.Where("issue_id = ? AND label_id = ?", issueID, labelID).Get(new(IssueLabel))
+ return has
+}
+
+// HasIssueLabel returns true if issue has been labeled.
+func HasIssueLabel(issueID, labelID int64) bool {
+ return hasIssueLabel(x, issueID, labelID)
+}
+
+func newIssueLabel(e *xorm.Session, issue *Issue, label *Label) (err error) {
+ if _, err = e.Insert(&IssueLabel{
+ IssueID: issue.ID,
+ LabelID: label.ID,
+ }); err != nil {
+ return err
+ }
+
+ label.NumIssues++
+ if issue.IsClosed {
+ label.NumClosedIssues++
+ }
+
+ if err = updateLabel(e, label); err != nil {
+ return fmt.Errorf("updateLabel: %v", err)
+ }
+
+ issue.Labels = append(issue.Labels, label)
+ return nil
+}
+
+// NewIssueLabel creates a new issue-label relation.
+func NewIssueLabel(issue *Issue, label *Label) (err error) {
+ if HasIssueLabel(issue.ID, label.ID) {
+ return nil
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if err = newIssueLabel(sess, issue, label); err != nil {
+ return err
+ }
+
+ return sess.Commit()
+}
+
+func newIssueLabels(e *xorm.Session, issue *Issue, labels []*Label) (err error) {
+ for i := range labels {
+ if hasIssueLabel(e, issue.ID, labels[i].ID) {
+ continue
+ }
+
+ if err = newIssueLabel(e, issue, labels[i]); err != nil {
+ return fmt.Errorf("newIssueLabel: %v", err)
+ }
+ }
+
+ return nil
+}
+
+// NewIssueLabels creates a list of issue-label relations.
+func NewIssueLabels(issue *Issue, labels []*Label) (err error) {
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if err = newIssueLabels(sess, issue, labels); err != nil {
+ return err
+ }
+
+ return sess.Commit()
+}
+
+func getIssueLabels(e Engine, issueID int64) ([]*IssueLabel, error) {
+ issueLabels := make([]*IssueLabel, 0, 10)
+ return issueLabels, e.Where("issue_id=?", issueID).Asc("label_id").Find(&issueLabels)
+}
+
+// GetIssueLabels returns all issue-label relations of given issue by ID.
+func GetIssueLabels(issueID int64) ([]*IssueLabel, error) {
+ return getIssueLabels(x, issueID)
+}
+
+func deleteIssueLabel(e *xorm.Session, issue *Issue, label *Label) (err error) {
+ if _, err = e.Delete(&IssueLabel{
+ IssueID: issue.ID,
+ LabelID: label.ID,
+ }); err != nil {
+ return err
+ }
+
+ label.NumIssues--
+ if issue.IsClosed {
+ label.NumClosedIssues--
+ }
+ if err = updateLabel(e, label); err != nil {
+ return fmt.Errorf("updateLabel: %v", err)
+ }
+
+ for i := range issue.Labels {
+ if issue.Labels[i].ID == label.ID {
+ issue.Labels = append(issue.Labels[:i], issue.Labels[i+1:]...)
+ break
+ }
+ }
+ return nil
+}
+
+// DeleteIssueLabel deletes issue-label relation.
+func DeleteIssueLabel(issue *Issue, label *Label) (err error) {
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if err = deleteIssueLabel(sess, issue, label); err != nil {
+ return err
+ }
+
+ return sess.Commit()
+}
diff --git a/internal/db/issue_mail.go b/internal/db/issue_mail.go
new file mode 100644
index 00000000..b1f81cc8
--- /dev/null
+++ b/internal/db/issue_mail.go
@@ -0,0 +1,180 @@
+// Copyright 2016 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 db
+
+import (
+ "fmt"
+
+ "github.com/unknwon/com"
+ log "gopkg.in/clog.v1"
+
+ "gogs.io/gogs/internal/mailer"
+ "gogs.io/gogs/internal/markup"
+ "gogs.io/gogs/internal/setting"
+)
+
+func (issue *Issue) MailSubject() string {
+ return fmt.Sprintf("[%s] %s (#%d)", issue.Repo.Name, issue.Title, issue.Index)
+}
+
+// mailerUser is a wrapper for satisfying mailer.User interface.
+type mailerUser struct {
+ user *User
+}
+
+func (this mailerUser) ID() int64 {
+ return this.user.ID
+}
+
+func (this mailerUser) DisplayName() string {
+ return this.user.DisplayName()
+}
+
+func (this mailerUser) Email() string {
+ return this.user.Email
+}
+
+func (this mailerUser) GenerateActivateCode() string {
+ return this.user.GenerateActivateCode()
+}
+
+func (this mailerUser) GenerateEmailActivateCode(email string) string {
+ return this.user.GenerateEmailActivateCode(email)
+}
+
+func NewMailerUser(u *User) mailer.User {
+ return mailerUser{u}
+}
+
+// mailerRepo is a wrapper for satisfying mailer.Repository interface.
+type mailerRepo struct {
+ repo *Repository
+}
+
+func (this mailerRepo) FullName() string {
+ return this.repo.FullName()
+}
+
+func (this mailerRepo) HTMLURL() string {
+ return this.repo.HTMLURL()
+}
+
+func (this mailerRepo) ComposeMetas() map[string]string {
+ return this.repo.ComposeMetas()
+}
+
+func NewMailerRepo(repo *Repository) mailer.Repository {
+ return mailerRepo{repo}
+}
+
+// mailerIssue is a wrapper for satisfying mailer.Issue interface.
+type mailerIssue struct {
+ issue *Issue
+}
+
+func (this mailerIssue) MailSubject() string {
+ return this.issue.MailSubject()
+}
+
+func (this mailerIssue) Content() string {
+ return this.issue.Content
+}
+
+func (this mailerIssue) HTMLURL() string {
+ return this.issue.HTMLURL()
+}
+
+func NewMailerIssue(issue *Issue) mailer.Issue {
+ return mailerIssue{issue}
+}
+
+// mailIssueCommentToParticipants can be used for both new issue creation and comment.
+// This functions sends two list of emails:
+// 1. Repository watchers, users who participated in comments and the assignee.
+// 2. Users who are not in 1. but get mentioned in current issue/comment.
+func mailIssueCommentToParticipants(issue *Issue, doer *User, mentions []string) error {
+ if !setting.Service.EnableNotifyMail {
+ return nil
+ }
+
+ watchers, err := GetWatchers(issue.RepoID)
+ if err != nil {
+ return fmt.Errorf("GetWatchers [repo_id: %d]: %v", issue.RepoID, err)
+ }
+ participants, err := GetParticipantsByIssueID(issue.ID)
+ if err != nil {
+ return fmt.Errorf("GetParticipantsByIssueID [issue_id: %d]: %v", issue.ID, err)
+ }
+
+ // In case the issue poster is not watching the repository,
+ // even if we have duplicated in watchers, can be safely filtered out.
+ if issue.PosterID != doer.ID {
+ participants = append(participants, issue.Poster)
+ }
+
+ tos := make([]string, 0, len(watchers)) // List of email addresses
+ names := make([]string, 0, len(watchers))
+ for i := range watchers {
+ if watchers[i].UserID == doer.ID {
+ continue
+ }
+
+ to, err := GetUserByID(watchers[i].UserID)
+ if err != nil {
+ return fmt.Errorf("GetUserByID [%d]: %v", watchers[i].UserID, err)
+ }
+ if to.IsOrganization() || !to.IsActive {
+ continue
+ }
+
+ tos = append(tos, to.Email)
+ names = append(names, to.Name)
+ }
+ for i := range participants {
+ if participants[i].ID == doer.ID {
+ continue
+ } else if com.IsSliceContainsStr(names, participants[i].Name) {
+ continue
+ }
+
+ tos = append(tos, participants[i].Email)
+ names = append(names, participants[i].Name)
+ }
+ if issue.Assignee != nil && issue.Assignee.ID != doer.ID {
+ if !com.IsSliceContainsStr(names, issue.Assignee.Name) {
+ tos = append(tos, issue.Assignee.Email)
+ names = append(names, issue.Assignee.Name)
+ }
+ }
+ mailer.SendIssueCommentMail(NewMailerIssue(issue), NewMailerRepo(issue.Repo), NewMailerUser(doer), tos)
+
+ // Mail mentioned people and exclude watchers.
+ names = append(names, doer.Name)
+ tos = make([]string, 0, len(mentions)) // list of user names.
+ for i := range mentions {
+ if com.IsSliceContainsStr(names, mentions[i]) {
+ continue
+ }
+
+ tos = append(tos, mentions[i])
+ }
+ mailer.SendIssueMentionMail(NewMailerIssue(issue), NewMailerRepo(issue.Repo), NewMailerUser(doer), GetUserEmailsByNames(tos))
+ return nil
+}
+
+// MailParticipants sends new issue thread created emails to repository watchers
+// and mentioned people.
+func (issue *Issue) MailParticipants() (err error) {
+ mentions := markup.FindAllMentions(issue.Content)
+ if err = updateIssueMentions(x, issue.ID, mentions); err != nil {
+ return fmt.Errorf("UpdateIssueMentions [%d]: %v", issue.ID, err)
+ }
+
+ if err = mailIssueCommentToParticipants(issue, issue.Poster, mentions); err != nil {
+ log.Error(2, "mailIssueCommentToParticipants: %v", err)
+ }
+
+ return nil
+}
diff --git a/internal/db/login_source.go b/internal/db/login_source.go
new file mode 100644
index 00000000..c9e5dcc9
--- /dev/null
+++ b/internal/db/login_source.go
@@ -0,0 +1,866 @@
+// Copyright 2014 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.
+
+// FIXME: Put this file into its own package and separate into different files based on login sources.
+package db
+
+import (
+ "crypto/tls"
+ "fmt"
+ "net/smtp"
+ "net/textproto"
+ "os"
+ "path"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/go-macaron/binding"
+ "github.com/json-iterator/go"
+ "github.com/unknwon/com"
+ log "gopkg.in/clog.v1"
+ "gopkg.in/ini.v1"
+ "xorm.io/core"
+ "xorm.io/xorm"
+
+ "gogs.io/gogs/internal/auth/github"
+ "gogs.io/gogs/internal/auth/ldap"
+ "gogs.io/gogs/internal/auth/pam"
+ "gogs.io/gogs/internal/db/errors"
+ "gogs.io/gogs/internal/setting"
+)
+
+type LoginType int
+
+// Note: new type must append to the end of list to maintain compatibility.
+const (
+ LOGIN_NOTYPE LoginType = iota
+ LOGIN_PLAIN // 1
+ LOGIN_LDAP // 2
+ LOGIN_SMTP // 3
+ LOGIN_PAM // 4
+ LOGIN_DLDAP // 5
+ LOGIN_GITHUB // 6
+)
+
+var LoginNames = map[LoginType]string{
+ LOGIN_LDAP: "LDAP (via BindDN)",
+ LOGIN_DLDAP: "LDAP (simple auth)", // Via direct bind
+ LOGIN_SMTP: "SMTP",
+ LOGIN_PAM: "PAM",
+ LOGIN_GITHUB: "GitHub",
+}
+
+var SecurityProtocolNames = map[ldap.SecurityProtocol]string{
+ ldap.SECURITY_PROTOCOL_UNENCRYPTED: "Unencrypted",
+ ldap.SECURITY_PROTOCOL_LDAPS: "LDAPS",
+ ldap.SECURITY_PROTOCOL_START_TLS: "StartTLS",
+}
+
+// Ensure structs implemented interface.
+var (
+ _ core.Conversion = &LDAPConfig{}
+ _ core.Conversion = &SMTPConfig{}
+ _ core.Conversion = &PAMConfig{}
+ _ core.Conversion = &GitHubConfig{}
+)
+
+type LDAPConfig struct {
+ *ldap.Source `ini:"config"`
+}
+
+func (cfg *LDAPConfig) FromDB(bs []byte) error {
+ return jsoniter.Unmarshal(bs, &cfg)
+}
+
+func (cfg *LDAPConfig) ToDB() ([]byte, error) {
+ return jsoniter.Marshal(cfg)
+}
+
+func (cfg *LDAPConfig) SecurityProtocolName() string {
+ return SecurityProtocolNames[cfg.SecurityProtocol]
+}
+
+type SMTPConfig struct {
+ Auth string
+ Host string
+ Port int
+ AllowedDomains string `xorm:"TEXT"`
+ TLS bool `ini:"tls"`
+ SkipVerify bool
+}
+
+func (cfg *SMTPConfig) FromDB(bs []byte) error {
+ return jsoniter.Unmarshal(bs, cfg)
+}
+
+func (cfg *SMTPConfig) ToDB() ([]byte, error) {
+ return jsoniter.Marshal(cfg)
+}
+
+type PAMConfig struct {
+ ServiceName string // PAM service (e.g. system-auth)
+}
+
+func (cfg *PAMConfig) FromDB(bs []byte) error {
+ return jsoniter.Unmarshal(bs, &cfg)
+}
+
+func (cfg *PAMConfig) ToDB() ([]byte, error) {
+ return jsoniter.Marshal(cfg)
+}
+
+type GitHubConfig struct {
+ APIEndpoint string // GitHub service (e.g. https://api.github.com/)
+}
+
+func (cfg *GitHubConfig) FromDB(bs []byte) error {
+ return jsoniter.Unmarshal(bs, &cfg)
+}
+
+func (cfg *GitHubConfig) ToDB() ([]byte, error) {
+ return jsoniter.Marshal(cfg)
+}
+
+// AuthSourceFile contains information of an authentication source file.
+type AuthSourceFile struct {
+ abspath string
+ file *ini.File
+}
+
+// SetGeneral sets new value to the given key in the general (default) section.
+func (f *AuthSourceFile) SetGeneral(name, value string) {
+ f.file.Section("").Key(name).SetValue(value)
+}
+
+// SetConfig sets new values to the "config" section.
+func (f *AuthSourceFile) SetConfig(cfg core.Conversion) error {
+ return f.file.Section("config").ReflectFrom(cfg)
+}
+
+// Save writes updates into file system.
+func (f *AuthSourceFile) Save() error {
+ return f.file.SaveTo(f.abspath)
+}
+
+// LoginSource represents an external way for authorizing users.
+type LoginSource struct {
+ ID int64
+ Type LoginType
+ Name string `xorm:"UNIQUE"`
+ IsActived bool `xorm:"NOT NULL DEFAULT false"`
+ IsDefault bool `xorm:"DEFAULT false"`
+ Cfg core.Conversion `xorm:"TEXT"`
+
+ Created time.Time `xorm:"-" json:"-"`
+ CreatedUnix int64
+ Updated time.Time `xorm:"-" json:"-"`
+ UpdatedUnix int64
+
+ LocalFile *AuthSourceFile `xorm:"-" json:"-"`
+}
+
+func (s *LoginSource) BeforeInsert() {
+ s.CreatedUnix = time.Now().Unix()
+ s.UpdatedUnix = s.CreatedUnix
+}
+
+func (s *LoginSource) BeforeUpdate() {
+ s.UpdatedUnix = time.Now().Unix()
+}
+
+// Cell2Int64 converts a xorm.Cell type to int64,
+// and handles possible irregular cases.
+func Cell2Int64(val xorm.Cell) int64 {
+ switch (*val).(type) {
+ case []uint8:
+ log.Trace("Cell2Int64 ([]uint8): %v", *val)
+ return com.StrTo(string((*val).([]uint8))).MustInt64()
+ }
+ return (*val).(int64)
+}
+
+func (s *LoginSource) BeforeSet(colName string, val xorm.Cell) {
+ switch colName {
+ case "type":
+ switch LoginType(Cell2Int64(val)) {
+ case LOGIN_LDAP, LOGIN_DLDAP:
+ s.Cfg = new(LDAPConfig)
+ case LOGIN_SMTP:
+ s.Cfg = new(SMTPConfig)
+ case LOGIN_PAM:
+ s.Cfg = new(PAMConfig)
+ case LOGIN_GITHUB:
+ s.Cfg = new(GitHubConfig)
+ default:
+ panic("unrecognized login source type: " + com.ToStr(*val))
+ }
+ }
+}
+
+func (s *LoginSource) AfterSet(colName string, _ xorm.Cell) {
+ switch colName {
+ case "created_unix":
+ s.Created = time.Unix(s.CreatedUnix, 0).Local()
+ case "updated_unix":
+ s.Updated = time.Unix(s.UpdatedUnix, 0).Local()
+ }
+}
+
+func (s *LoginSource) TypeName() string {
+ return LoginNames[s.Type]
+}
+
+func (s *LoginSource) IsLDAP() bool {
+ return s.Type == LOGIN_LDAP
+}
+
+func (s *LoginSource) IsDLDAP() bool {
+ return s.Type == LOGIN_DLDAP
+}
+
+func (s *LoginSource) IsSMTP() bool {
+ return s.Type == LOGIN_SMTP
+}
+
+func (s *LoginSource) IsPAM() bool {
+ return s.Type == LOGIN_PAM
+}
+
+func (s *LoginSource) IsGitHub() bool {
+ return s.Type == LOGIN_GITHUB
+}
+
+func (s *LoginSource) HasTLS() bool {
+ return ((s.IsLDAP() || s.IsDLDAP()) &&
+ s.LDAP().SecurityProtocol > ldap.SECURITY_PROTOCOL_UNENCRYPTED) ||
+ s.IsSMTP()
+}
+
+func (s *LoginSource) UseTLS() bool {
+ switch s.Type {
+ case LOGIN_LDAP, LOGIN_DLDAP:
+ return s.LDAP().SecurityProtocol != ldap.SECURITY_PROTOCOL_UNENCRYPTED
+ case LOGIN_SMTP:
+ return s.SMTP().TLS
+ }
+
+ return false
+}
+
+func (s *LoginSource) SkipVerify() bool {
+ switch s.Type {
+ case LOGIN_LDAP, LOGIN_DLDAP:
+ return s.LDAP().SkipVerify
+ case LOGIN_SMTP:
+ return s.SMTP().SkipVerify
+ }
+
+ return false
+}
+
+func (s *LoginSource) LDAP() *LDAPConfig {
+ return s.Cfg.(*LDAPConfig)
+}
+
+func (s *LoginSource) SMTP() *SMTPConfig {
+ return s.Cfg.(*SMTPConfig)
+}
+
+func (s *LoginSource) PAM() *PAMConfig {
+ return s.Cfg.(*PAMConfig)
+}
+
+func (s *LoginSource) GitHub() *GitHubConfig {
+ return s.Cfg.(*GitHubConfig)
+}
+
+func CreateLoginSource(source *LoginSource) error {
+ has, err := x.Get(&LoginSource{Name: source.Name})
+ if err != nil {
+ return err
+ } else if has {
+ return ErrLoginSourceAlreadyExist{source.Name}
+ }
+
+ _, err = x.Insert(source)
+ if err != nil {
+ return err
+ } else if source.IsDefault {
+ return ResetNonDefaultLoginSources(source)
+ }
+ return nil
+}
+
+// LoginSources returns all login sources defined.
+func LoginSources() ([]*LoginSource, error) {
+ sources := make([]*LoginSource, 0, 2)
+ if err := x.Find(&sources); err != nil {
+ return nil, err
+ }
+
+ return append(sources, localLoginSources.List()...), nil
+}
+
+// ActivatedLoginSources returns login sources that are currently activated.
+func ActivatedLoginSources() ([]*LoginSource, error) {
+ sources := make([]*LoginSource, 0, 2)
+ if err := x.Where("is_actived = ?", true).Find(&sources); err != nil {
+ return nil, fmt.Errorf("find activated login sources: %v", err)
+ }
+ return append(sources, localLoginSources.ActivatedList()...), nil
+}
+
+// GetLoginSourceByID returns login source by given ID.
+func GetLoginSourceByID(id int64) (*LoginSource, error) {
+ source := new(LoginSource)
+ has, err := x.Id(id).Get(source)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return localLoginSources.GetLoginSourceByID(id)
+ }
+ return source, nil
+}
+
+// ResetNonDefaultLoginSources clean other default source flag
+func ResetNonDefaultLoginSources(source *LoginSource) error {
+ // update changes to DB
+ if _, err := x.NotIn("id", []int64{source.ID}).Cols("is_default").Update(&LoginSource{IsDefault: false}); err != nil {
+ return err
+ }
+ // write changes to local authentications
+ for i := range localLoginSources.sources {
+ if localLoginSources.sources[i].LocalFile != nil && localLoginSources.sources[i].ID != source.ID {
+ localLoginSources.sources[i].LocalFile.SetGeneral("is_default", "false")
+ if err := localLoginSources.sources[i].LocalFile.SetConfig(source.Cfg); err != nil {
+ return fmt.Errorf("LocalFile.SetConfig: %v", err)
+ } else if err = localLoginSources.sources[i].LocalFile.Save(); err != nil {
+ return fmt.Errorf("LocalFile.Save: %v", err)
+ }
+ }
+ }
+ // flush memory so that web page can show the same behaviors
+ localLoginSources.UpdateLoginSource(source)
+ return nil
+}
+
+// UpdateLoginSource updates information of login source to database or local file.
+func UpdateLoginSource(source *LoginSource) error {
+ if source.LocalFile == nil {
+ if _, err := x.Id(source.ID).AllCols().Update(source); err != nil {
+ return err
+ } else {
+ return ResetNonDefaultLoginSources(source)
+ }
+
+ }
+
+ source.LocalFile.SetGeneral("name", source.Name)
+ source.LocalFile.SetGeneral("is_activated", com.ToStr(source.IsActived))
+ source.LocalFile.SetGeneral("is_default", com.ToStr(source.IsDefault))
+ if err := source.LocalFile.SetConfig(source.Cfg); err != nil {
+ return fmt.Errorf("LocalFile.SetConfig: %v", err)
+ } else if err = source.LocalFile.Save(); err != nil {
+ return fmt.Errorf("LocalFile.Save: %v", err)
+ }
+ return ResetNonDefaultLoginSources(source)
+}
+
+func DeleteSource(source *LoginSource) error {
+ count, err := x.Count(&User{LoginSource: source.ID})
+ if err != nil {
+ return err
+ } else if count > 0 {
+ return ErrLoginSourceInUse{source.ID}
+ }
+ _, err = x.Id(source.ID).Delete(new(LoginSource))
+ return err
+}
+
+// CountLoginSources returns total number of login sources.
+func CountLoginSources() int64 {
+ count, _ := x.Count(new(LoginSource))
+ return count + int64(localLoginSources.Len())
+}
+
+// LocalLoginSources contains authentication sources configured and loaded from local files.
+// Calling its methods is thread-safe; otherwise, please maintain the mutex accordingly.
+type LocalLoginSources struct {
+ sync.RWMutex
+ sources []*LoginSource
+}
+
+func (s *LocalLoginSources) Len() int {
+ return len(s.sources)
+}
+
+// List returns full clone of login sources.
+func (s *LocalLoginSources) List() []*LoginSource {
+ s.RLock()
+ defer s.RUnlock()
+
+ list := make([]*LoginSource, s.Len())
+ for i := range s.sources {
+ list[i] = &LoginSource{}
+ *list[i] = *s.sources[i]
+ }
+ return list
+}
+
+// ActivatedList returns clone of activated login sources.
+func (s *LocalLoginSources) ActivatedList() []*LoginSource {
+ s.RLock()
+ defer s.RUnlock()
+
+ list := make([]*LoginSource, 0, 2)
+ for i := range s.sources {
+ if !s.sources[i].IsActived {
+ continue
+ }
+ source := &LoginSource{}
+ *source = *s.sources[i]
+ list = append(list, source)
+ }
+ return list
+}
+
+// GetLoginSourceByID returns a clone of login source by given ID.
+func (s *LocalLoginSources) GetLoginSourceByID(id int64) (*LoginSource, error) {
+ s.RLock()
+ defer s.RUnlock()
+
+ for i := range s.sources {
+ if s.sources[i].ID == id {
+ source := &LoginSource{}
+ *source = *s.sources[i]
+ return source, nil
+ }
+ }
+
+ return nil, errors.LoginSourceNotExist{id}
+}
+
+// UpdateLoginSource updates in-memory copy of the authentication source.
+func (s *LocalLoginSources) UpdateLoginSource(source *LoginSource) {
+ s.Lock()
+ defer s.Unlock()
+
+ source.Updated = time.Now()
+ for i := range s.sources {
+ if s.sources[i].ID == source.ID {
+ *s.sources[i] = *source
+ } else if source.IsDefault {
+ s.sources[i].IsDefault = false
+ }
+ }
+}
+
+var localLoginSources = &LocalLoginSources{}
+
+// LoadAuthSources loads authentication sources from local files
+// and converts them into login sources.
+func LoadAuthSources() {
+ authdPath := path.Join(setting.CustomPath, "conf/auth.d")
+ if !com.IsDir(authdPath) {
+ return
+ }
+
+ paths, err := com.GetFileListBySuffix(authdPath, ".conf")
+ if err != nil {
+ log.Fatal(2, "Failed to list authentication sources: %v", err)
+ }
+
+ localLoginSources.sources = make([]*LoginSource, 0, len(paths))
+
+ for _, fpath := range paths {
+ authSource, err := ini.Load(fpath)
+ if err != nil {
+ log.Fatal(2, "Failed to load authentication source: %v", err)
+ }
+ authSource.NameMapper = ini.TitleUnderscore
+
+ // Set general attributes
+ s := authSource.Section("")
+ loginSource := &LoginSource{
+ ID: s.Key("id").MustInt64(),
+ Name: s.Key("name").String(),
+ IsActived: s.Key("is_activated").MustBool(),
+ IsDefault: s.Key("is_default").MustBool(),
+ LocalFile: &AuthSourceFile{
+ abspath: fpath,
+ file: authSource,
+ },
+ }
+
+ fi, err := os.Stat(fpath)
+ if err != nil {
+ log.Fatal(2, "Failed to load authentication source: %v", err)
+ }
+ loginSource.Updated = fi.ModTime()
+
+ // Parse authentication source file
+ authType := s.Key("type").String()
+ switch authType {
+ case "ldap_bind_dn":
+ loginSource.Type = LOGIN_LDAP
+ loginSource.Cfg = &LDAPConfig{}
+ case "ldap_simple_auth":
+ loginSource.Type = LOGIN_DLDAP
+ loginSource.Cfg = &LDAPConfig{}
+ case "smtp":
+ loginSource.Type = LOGIN_SMTP
+ loginSource.Cfg = &SMTPConfig{}
+ case "pam":
+ loginSource.Type = LOGIN_PAM
+ loginSource.Cfg = &PAMConfig{}
+ case "github":
+ loginSource.Type = LOGIN_GITHUB
+ loginSource.Cfg = &GitHubConfig{}
+ default:
+ log.Fatal(2, "Failed to load authentication source: unknown type '%s'", authType)
+ }
+
+ if err = authSource.Section("config").MapTo(loginSource.Cfg); err != nil {
+ log.Fatal(2, "Failed to parse authentication source 'config': %v", err)
+ }
+
+ localLoginSources.sources = append(localLoginSources.sources, loginSource)
+ }
+}
+
+// .____ ________ _____ __________
+// | | \______ \ / _ \\______ \
+// | | | | \ / /_\ \| ___/
+// | |___ | ` \/ | \ |
+// |_______ \/_______ /\____|__ /____|
+// \/ \/ \/
+
+func composeFullName(firstname, surname, username string) string {
+ switch {
+ case len(firstname) == 0 && len(surname) == 0:
+ return username
+ case len(firstname) == 0:
+ return surname
+ case len(surname) == 0:
+ return firstname
+ default:
+ return firstname + " " + surname
+ }
+}
+
+// LoginViaLDAP queries if login/password is valid against the LDAP directory pool,
+// and create a local user if success when enabled.
+func LoginViaLDAP(user *User, login, password string, source *LoginSource, autoRegister bool) (*User, error) {
+ username, fn, sn, mail, isAdmin, succeed := source.Cfg.(*LDAPConfig).SearchEntry(login, password, source.Type == LOGIN_DLDAP)
+ if !succeed {
+ // User not in LDAP, do nothing
+ return nil, errors.UserNotExist{0, login}
+ }
+
+ if !autoRegister {
+ return user, nil
+ }
+
+ // Fallback.
+ if len(username) == 0 {
+ username = login
+ }
+ // Validate username make sure it satisfies requirement.
+ if binding.AlphaDashDotPattern.MatchString(username) {
+ return nil, fmt.Errorf("Invalid pattern for attribute 'username' [%s]: must be valid alpha or numeric or dash(-_) or dot characters", username)
+ }
+
+ if len(mail) == 0 {
+ mail = fmt.Sprintf("%s@localhost", username)
+ }
+
+ user = &User{
+ LowerName: strings.ToLower(username),
+ Name: username,
+ FullName: composeFullName(fn, sn, username),
+ Email: mail,
+ LoginType: source.Type,
+ LoginSource: source.ID,
+ LoginName: login,
+ IsActive: true,
+ IsAdmin: isAdmin,
+ }
+
+ ok, err := IsUserExist(0, user.Name)
+ if err != nil {
+ return user, err
+ }
+
+ if ok {
+ return user, UpdateUser(user)
+ }
+
+ return user, CreateUser(user)
+}
+
+// _________ __________________________
+// / _____/ / \__ ___/\______ \
+// \_____ \ / \ / \| | | ___/
+// / \/ Y \ | | |
+// /_______ /\____|__ /____| |____|
+// \/ \/
+
+type smtpLoginAuth struct {
+ username, password string
+}
+
+func (auth *smtpLoginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
+ return "LOGIN", []byte(auth.username), nil
+}
+
+func (auth *smtpLoginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
+ if more {
+ switch string(fromServer) {
+ case "Username:":
+ return []byte(auth.username), nil
+ case "Password:":
+ return []byte(auth.password), nil
+ }
+ }
+ return nil, nil
+}
+
+const (
+ SMTP_PLAIN = "PLAIN"
+ SMTP_LOGIN = "LOGIN"
+)
+
+var SMTPAuths = []string{SMTP_PLAIN, SMTP_LOGIN}
+
+func SMTPAuth(a smtp.Auth, cfg *SMTPConfig) error {
+ c, err := smtp.Dial(fmt.Sprintf("%s:%d", cfg.Host, cfg.Port))
+ if err != nil {
+ return err
+ }
+ defer c.Close()
+
+ if err = c.Hello("gogs"); err != nil {
+ return err
+ }
+
+ if cfg.TLS {
+ if ok, _ := c.Extension("STARTTLS"); ok {
+ if err = c.StartTLS(&tls.Config{
+ InsecureSkipVerify: cfg.SkipVerify,
+ ServerName: cfg.Host,
+ }); err != nil {
+ return err
+ }
+ } else {
+ return errors.New("SMTP server unsupports TLS")
+ }
+ }
+
+ if ok, _ := c.Extension("AUTH"); ok {
+ if err = c.Auth(a); err != nil {
+ return err
+ }
+ return nil
+ }
+ return errors.New("Unsupported SMTP authentication method")
+}
+
+// LoginViaSMTP queries if login/password is valid against the SMTP,
+// and create a local user if success when enabled.
+func LoginViaSMTP(user *User, login, password string, sourceID int64, cfg *SMTPConfig, autoRegister bool) (*User, error) {
+ // Verify allowed domains.
+ if len(cfg.AllowedDomains) > 0 {
+ idx := strings.Index(login, "@")
+ if idx == -1 {
+ return nil, errors.UserNotExist{0, login}
+ } else if !com.IsSliceContainsStr(strings.Split(cfg.AllowedDomains, ","), login[idx+1:]) {
+ return nil, errors.UserNotExist{0, login}
+ }
+ }
+
+ var auth smtp.Auth
+ if cfg.Auth == SMTP_PLAIN {
+ auth = smtp.PlainAuth("", login, password, cfg.Host)
+ } else if cfg.Auth == SMTP_LOGIN {
+ auth = &smtpLoginAuth{login, password}
+ } else {
+ return nil, errors.New("Unsupported SMTP authentication type")
+ }
+
+ if err := SMTPAuth(auth, cfg); err != nil {
+ // Check standard error format first,
+ // then fallback to worse case.
+ tperr, ok := err.(*textproto.Error)
+ if (ok && tperr.Code == 535) ||
+ strings.Contains(err.Error(), "Username and Password not accepted") {
+ return nil, errors.UserNotExist{0, login}
+ }
+ return nil, err
+ }
+
+ if !autoRegister {
+ return user, nil
+ }
+
+ username := login
+ idx := strings.Index(login, "@")
+ if idx > -1 {
+ username = login[:idx]
+ }
+
+ user = &User{
+ LowerName: strings.ToLower(username),
+ Name: strings.ToLower(username),
+ Email: login,
+ Passwd: password,
+ LoginType: LOGIN_SMTP,
+ LoginSource: sourceID,
+ LoginName: login,
+ IsActive: true,
+ }
+ return user, CreateUser(user)
+}
+
+// __________ _____ _____
+// \______ \/ _ \ / \
+// | ___/ /_\ \ / \ / \
+// | | / | \/ Y \
+// |____| \____|__ /\____|__ /
+// \/ \/
+
+// LoginViaPAM queries if login/password is valid against the PAM,
+// and create a local user if success when enabled.
+func LoginViaPAM(user *User, login, password string, sourceID int64, cfg *PAMConfig, autoRegister bool) (*User, error) {
+ if err := pam.PAMAuth(cfg.ServiceName, login, password); err != nil {
+ if strings.Contains(err.Error(), "Authentication failure") {
+ return nil, errors.UserNotExist{0, login}
+ }
+ return nil, err
+ }
+
+ if !autoRegister {
+ return user, nil
+ }
+
+ user = &User{
+ LowerName: strings.ToLower(login),
+ Name: login,
+ Email: login,
+ Passwd: password,
+ LoginType: LOGIN_PAM,
+ LoginSource: sourceID,
+ LoginName: login,
+ IsActive: true,
+ }
+ return user, CreateUser(user)
+}
+
+//________.__ __ ___ ___ ___.
+/// _____/|__|/ |_ / | \ __ _\_ |__
+/// \ ___| \ __\/ ~ \ | \ __ \
+//\ \_\ \ || | \ Y / | / \_\ \
+//\______ /__||__| \___|_ /|____/|___ /
+//\/ \/ \/
+
+func LoginViaGitHub(user *User, login, password string, sourceID int64, cfg *GitHubConfig, autoRegister bool) (*User, error) {
+ fullname, email, url, location, err := github.Authenticate(cfg.APIEndpoint, login, password)
+ if err != nil {
+ if strings.Contains(err.Error(), "401") {
+ return nil, errors.UserNotExist{0, login}
+ }
+ return nil, err
+ }
+
+ if !autoRegister {
+ return user, nil
+ }
+ user = &User{
+ LowerName: strings.ToLower(login),
+ Name: login,
+ FullName: fullname,
+ Email: email,
+ Website: url,
+ Passwd: password,
+ LoginType: LOGIN_GITHUB,
+ LoginSource: sourceID,
+ LoginName: login,
+ IsActive: true,
+ Location: location,
+ }
+ return user, CreateUser(user)
+}
+
+func remoteUserLogin(user *User, login, password string, source *LoginSource, autoRegister bool) (*User, error) {
+ if !source.IsActived {
+ return nil, errors.LoginSourceNotActivated{source.ID}
+ }
+
+ switch source.Type {
+ case LOGIN_LDAP, LOGIN_DLDAP:
+ return LoginViaLDAP(user, login, password, source, autoRegister)
+ case LOGIN_SMTP:
+ return LoginViaSMTP(user, login, password, source.ID, source.Cfg.(*SMTPConfig), autoRegister)
+ case LOGIN_PAM:
+ return LoginViaPAM(user, login, password, source.ID, source.Cfg.(*PAMConfig), autoRegister)
+ case LOGIN_GITHUB:
+ return LoginViaGitHub(user, login, password, source.ID, source.Cfg.(*GitHubConfig), autoRegister)
+ }
+
+ return nil, errors.InvalidLoginSourceType{source.Type}
+}
+
+// UserLogin validates user name and password via given login source ID.
+// If the loginSourceID is negative, it will abort login process if user is not found.
+func UserLogin(username, password string, loginSourceID int64) (*User, error) {
+ var user *User
+ if strings.Contains(username, "@") {
+ user = &User{Email: strings.ToLower(username)}
+ } else {
+ user = &User{LowerName: strings.ToLower(username)}
+ }
+
+ hasUser, err := x.Get(user)
+ if err != nil {
+ return nil, fmt.Errorf("get user record: %v", err)
+ }
+
+ if hasUser {
+ // Note: This check is unnecessary but to reduce user confusion at login page
+ // and make it more consistent at user's perspective.
+ if loginSourceID >= 0 && user.LoginSource != loginSourceID {
+ return nil, errors.LoginSourceMismatch{loginSourceID, user.LoginSource}
+ }
+
+ // Validate password hash fetched from database for local accounts
+ if user.LoginType == LOGIN_NOTYPE ||
+ user.LoginType == LOGIN_PLAIN {
+ if user.ValidatePassword(password) {
+ return user, nil
+ }
+
+ return nil, errors.UserNotExist{user.ID, user.Name}
+ }
+
+ // Remote login to the login source the user is associated with
+ source, err := GetLoginSourceByID(user.LoginSource)
+ if err != nil {
+ return nil, err
+ }
+
+ return remoteUserLogin(user, user.LoginName, password, source, false)
+ }
+
+ // Non-local login source is always greater than 0
+ if loginSourceID <= 0 {
+ return nil, errors.UserNotExist{-1, username}
+ }
+
+ source, err := GetLoginSourceByID(loginSourceID)
+ if err != nil {
+ return nil, err
+ }
+
+ return remoteUserLogin(nil, username, password, source, true)
+}
diff --git a/internal/db/migrations/migrations.go b/internal/db/migrations/migrations.go
new file mode 100644
index 00000000..79534484
--- /dev/null
+++ b/internal/db/migrations/migrations.go
@@ -0,0 +1,390 @@
+// 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 migrations
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/unknwon/com"
+ log "gopkg.in/clog.v1"
+ "xorm.io/xorm"
+
+ "gogs.io/gogs/internal/tool"
+)
+
+const _MIN_DB_VER = 10
+
+type Migration interface {
+ Description() string
+ Migrate(*xorm.Engine) error
+}
+
+type migration struct {
+ description string
+ migrate func(*xorm.Engine) error
+}
+
+func NewMigration(desc string, fn func(*xorm.Engine) error) Migration {
+ return &migration{desc, fn}
+}
+
+func (m *migration) Description() string {
+ return m.description
+}
+
+func (m *migration) Migrate(x *xorm.Engine) error {
+ return m.migrate(x)
+}
+
+// The version table. Should have only one row with id==1
+type Version struct {
+ ID int64
+ Version int64
+}
+
+// This is a sequence of migrations. Add new migrations to the bottom of the list.
+// If you want to "retire" a migration, remove it from the top of the list and
+// update _MIN_VER_DB accordingly
+var migrations = []Migration{
+ // v0 -> v4 : before 0.6.0 -> last support 0.7.33
+ // v4 -> v10: before 0.7.0 -> last support 0.9.141
+ NewMigration("generate rands and salt for organizations", generateOrgRandsAndSalt), // V10 -> V11:v0.8.5
+ NewMigration("convert date to unix timestamp", convertDateToUnix), // V11 -> V12:v0.9.2
+ NewMigration("convert LDAP UseSSL option to SecurityProtocol", ldapUseSSLToSecurityProtocol), // V12 -> V13:v0.9.37
+
+ // v13 -> v14:v0.9.87
+ NewMigration("set comment updated with created", setCommentUpdatedWithCreated),
+ // v14 -> v15:v0.9.147
+ NewMigration("generate and migrate Git hooks", generateAndMigrateGitHooks),
+ // v15 -> v16:v0.10.16
+ NewMigration("update repository sizes", updateRepositorySizes),
+ // v16 -> v17:v0.10.31
+ NewMigration("remove invalid protect branch whitelist", removeInvalidProtectBranchWhitelist),
+ // v17 -> v18:v0.11.48
+ NewMigration("store long text in repository description field", updateRepositoryDescriptionField),
+ // v18 -> v19:v0.11.55
+ NewMigration("clean unlinked webhook and hook_tasks", cleanUnlinkedWebhookAndHookTasks),
+}
+
+// Migrate database to current version
+func Migrate(x *xorm.Engine) error {
+ if err := x.Sync(new(Version)); err != nil {
+ return fmt.Errorf("sync: %v", err)
+ }
+
+ currentVersion := &Version{ID: 1}
+ has, err := x.Get(currentVersion)
+ if err != nil {
+ return fmt.Errorf("get: %v", err)
+ } else if !has {
+ // If the version record does not exist we think
+ // it is a fresh installation and we can skip all migrations.
+ currentVersion.ID = 0
+ currentVersion.Version = int64(_MIN_DB_VER + len(migrations))
+
+ if _, err = x.InsertOne(currentVersion); err != nil {
+ return fmt.Errorf("insert: %v", err)
+ }
+ }
+
+ v := currentVersion.Version
+ if _MIN_DB_VER > v {
+ log.Fatal(0, `
+Hi there, thank you for using Gogs for so long!
+However, Gogs has stopped supporting auto-migration from your previously installed version.
+But the good news is, it's very easy to fix this problem!
+You can migrate your older database using a previous release, then you can upgrade to the newest version.
+
+Please save following instructions to somewhere and start working:
+
+- If you were using below 0.6.0 (e.g. 0.5.x), download last supported archive from following link:
+ https://gogs.io/gogs/releases/tag/v0.7.33
+- If you were using below 0.7.0 (e.g. 0.6.x), download last supported archive from following link:
+ https://gogs.io/gogs/releases/tag/v0.9.141
+
+Once finished downloading,
+
+1. Extract the archive and to upgrade steps as usual.
+2. Run it once. To verify, you should see some migration traces.
+3. Once it starts web server successfully, stop it.
+4. Now it's time to put back the release archive you originally intent to upgrade.
+5. Enjoy!
+
+In case you're stilling getting this notice, go through instructions again until it disappears.`)
+ return nil
+ }
+
+ if int(v-_MIN_DB_VER) > len(migrations) {
+ // User downgraded Gogs.
+ currentVersion.Version = int64(len(migrations) + _MIN_DB_VER)
+ _, err = x.Id(1).Update(currentVersion)
+ return err
+ }
+ for i, m := range migrations[v-_MIN_DB_VER:] {
+ log.Info("Migration: %s", m.Description())
+ if err = m.Migrate(x); err != nil {
+ return fmt.Errorf("do migrate: %v", err)
+ }
+ currentVersion.Version = v + int64(i) + 1
+ if _, err = x.Id(1).Update(currentVersion); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func generateOrgRandsAndSalt(x *xorm.Engine) (err error) {
+ type User struct {
+ ID int64 `xorm:"pk autoincr"`
+ Rands string `xorm:"VARCHAR(10)"`
+ Salt string `xorm:"VARCHAR(10)"`
+ }
+
+ orgs := make([]*User, 0, 10)
+ if err = x.Where("type=1").And("rands=''").Find(&orgs); err != nil {
+ return fmt.Errorf("select all organizations: %v", err)
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ for _, org := range orgs {
+ if org.Rands, err = tool.RandomString(10); err != nil {
+ return err
+ }
+ if org.Salt, err = tool.RandomString(10); err != nil {
+ return err
+ }
+ if _, err = sess.ID(org.ID).Update(org); err != nil {
+ return err
+ }
+ }
+
+ return sess.Commit()
+}
+
+type TAction struct {
+ ID int64 `xorm:"pk autoincr"`
+ CreatedUnix int64
+}
+
+func (t *TAction) TableName() string { return "action" }
+
+type TNotice struct {
+ ID int64 `xorm:"pk autoincr"`
+ CreatedUnix int64
+}
+
+func (t *TNotice) TableName() string { return "notice" }
+
+type TComment struct {
+ ID int64 `xorm:"pk autoincr"`
+ CreatedUnix int64
+}
+
+func (t *TComment) TableName() string { return "comment" }
+
+type TIssue struct {
+ ID int64 `xorm:"pk autoincr"`
+ DeadlineUnix int64
+ CreatedUnix int64
+ UpdatedUnix int64
+}
+
+func (t *TIssue) TableName() string { return "issue" }
+
+type TMilestone struct {
+ ID int64 `xorm:"pk autoincr"`
+ DeadlineUnix int64
+ ClosedDateUnix int64
+}
+
+func (t *TMilestone) TableName() string { return "milestone" }
+
+type TAttachment struct {
+ ID int64 `xorm:"pk autoincr"`
+ CreatedUnix int64
+}
+
+func (t *TAttachment) TableName() string { return "attachment" }
+
+type TLoginSource struct {
+ ID int64 `xorm:"pk autoincr"`
+ CreatedUnix int64
+ UpdatedUnix int64
+}
+
+func (t *TLoginSource) TableName() string { return "login_source" }
+
+type TPull struct {
+ ID int64 `xorm:"pk autoincr"`
+ MergedUnix int64
+}
+
+func (t *TPull) TableName() string { return "pull_request" }
+
+type TRelease struct {
+ ID int64 `xorm:"pk autoincr"`
+ CreatedUnix int64
+}
+
+func (t *TRelease) TableName() string { return "release" }
+
+type TRepo struct {
+ ID int64 `xorm:"pk autoincr"`
+ CreatedUnix int64
+ UpdatedUnix int64
+}
+
+func (t *TRepo) TableName() string { return "repository" }
+
+type TMirror struct {
+ ID int64 `xorm:"pk autoincr"`
+ UpdatedUnix int64
+ NextUpdateUnix int64
+}
+
+func (t *TMirror) TableName() string { return "mirror" }
+
+type TPublicKey struct {
+ ID int64 `xorm:"pk autoincr"`
+ CreatedUnix int64
+ UpdatedUnix int64
+}
+
+func (t *TPublicKey) TableName() string { return "public_key" }
+
+type TDeployKey struct {
+ ID int64 `xorm:"pk autoincr"`
+ CreatedUnix int64
+ UpdatedUnix int64
+}
+
+func (t *TDeployKey) TableName() string { return "deploy_key" }
+
+type TAccessToken struct {
+ ID int64 `xorm:"pk autoincr"`
+ CreatedUnix int64
+ UpdatedUnix int64
+}
+
+func (t *TAccessToken) TableName() string { return "access_token" }
+
+type TUser struct {
+ ID int64 `xorm:"pk autoincr"`
+ CreatedUnix int64
+ UpdatedUnix int64
+}
+
+func (t *TUser) TableName() string { return "user" }
+
+type TWebhook struct {
+ ID int64 `xorm:"pk autoincr"`
+ CreatedUnix int64
+ UpdatedUnix int64
+}
+
+func (t *TWebhook) TableName() string { return "webhook" }
+
+func convertDateToUnix(x *xorm.Engine) (err error) {
+ log.Info("This migration could take up to minutes, please be patient.")
+ type Bean struct {
+ ID int64 `xorm:"pk autoincr"`
+ Created time.Time
+ Updated time.Time
+ Merged time.Time
+ Deadline time.Time
+ ClosedDate time.Time
+ NextUpdate time.Time
+ }
+
+ var tables = []struct {
+ name string
+ cols []string
+ bean interface{}
+ }{
+ {"action", []string{"created"}, new(TAction)},
+ {"notice", []string{"created"}, new(TNotice)},
+ {"comment", []string{"created"}, new(TComment)},
+ {"issue", []string{"deadline", "created", "updated"}, new(TIssue)},
+ {"milestone", []string{"deadline", "closed_date"}, new(TMilestone)},
+ {"attachment", []string{"created"}, new(TAttachment)},
+ {"login_source", []string{"created", "updated"}, new(TLoginSource)},
+ {"pull_request", []string{"merged"}, new(TPull)},
+ {"release", []string{"created"}, new(TRelease)},
+ {"repository", []string{"created", "updated"}, new(TRepo)},
+ {"mirror", []string{"updated", "next_update"}, new(TMirror)},
+ {"public_key", []string{"created", "updated"}, new(TPublicKey)},
+ {"deploy_key", []string{"created", "updated"}, new(TDeployKey)},
+ {"access_token", []string{"created", "updated"}, new(TAccessToken)},
+ {"user", []string{"created", "updated"}, new(TUser)},
+ {"webhook", []string{"created", "updated"}, new(TWebhook)},
+ }
+
+ for _, table := range tables {
+ log.Info("Converting table: %s", table.name)
+ if err = x.Sync2(table.bean); err != nil {
+ return fmt.Errorf("Sync [table: %s]: %v", table.name, err)
+ }
+
+ offset := 0
+ for {
+ beans := make([]*Bean, 0, 100)
+ if err = x.Sql(fmt.Sprintf("SELECT * FROM `%s` ORDER BY id ASC LIMIT 100 OFFSET %d",
+ table.name, offset)).Find(&beans); err != nil {
+ return fmt.Errorf("select beans [table: %s, offset: %d]: %v", table.name, offset, err)
+ }
+ log.Trace("Table [%s]: offset: %d, beans: %d", table.name, offset, len(beans))
+ if len(beans) == 0 {
+ break
+ }
+ offset += 100
+
+ baseSQL := "UPDATE `" + table.name + "` SET "
+ for _, bean := range beans {
+ valSQLs := make([]string, 0, len(table.cols))
+ for _, col := range table.cols {
+ fieldSQL := ""
+ fieldSQL += col + "_unix = "
+
+ switch col {
+ case "deadline":
+ if bean.Deadline.IsZero() {
+ continue
+ }
+ fieldSQL += com.ToStr(bean.Deadline.Unix())
+ case "created":
+ fieldSQL += com.ToStr(bean.Created.Unix())
+ case "updated":
+ fieldSQL += com.ToStr(bean.Updated.Unix())
+ case "closed_date":
+ fieldSQL += com.ToStr(bean.ClosedDate.Unix())
+ case "merged":
+ fieldSQL += com.ToStr(bean.Merged.Unix())
+ case "next_update":
+ fieldSQL += com.ToStr(bean.NextUpdate.Unix())
+ }
+
+ valSQLs = append(valSQLs, fieldSQL)
+ }
+
+ if len(valSQLs) == 0 {
+ continue
+ }
+
+ if _, err = x.Exec(baseSQL + strings.Join(valSQLs, ",") + " WHERE id = " + com.ToStr(bean.ID)); err != nil {
+ return fmt.Errorf("update bean [table: %s, id: %d]: %v", table.name, bean.ID, err)
+ }
+ }
+ }
+ }
+
+ return nil
+}
diff --git a/internal/db/migrations/v13.go b/internal/db/migrations/v13.go
new file mode 100644
index 00000000..1097956e
--- /dev/null
+++ b/internal/db/migrations/v13.go
@@ -0,0 +1,52 @@
+// Copyright 2016 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 migrations
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/unknwon/com"
+ "xorm.io/xorm"
+ "github.com/json-iterator/go"
+)
+
+func ldapUseSSLToSecurityProtocol(x *xorm.Engine) error {
+ results, err := x.Query("SELECT `id`,`cfg` FROM `login_source` WHERE `type` = 2 OR `type` = 5")
+ if err != nil {
+ if strings.Contains(err.Error(), "no such column") {
+ return nil
+ }
+ return fmt.Errorf("select LDAP login sources: %v", err)
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ for _, result := range results {
+ cfg := map[string]interface{}{}
+ if err = jsoniter.Unmarshal(result["cfg"], &cfg); err != nil {
+ return fmt.Errorf("unmarshal JSON config: %v", err)
+ }
+ if com.ToStr(cfg["UseSSL"]) == "true" {
+ cfg["SecurityProtocol"] = 1 // LDAPS
+ }
+ delete(cfg, "UseSSL")
+
+ data, err := jsoniter.Marshal(&cfg)
+ if err != nil {
+ return fmt.Errorf("marshal JSON config: %v", err)
+ }
+
+ if _, err = sess.Exec("UPDATE `login_source` SET `cfg`=? WHERE `id`=?",
+ string(data), com.StrTo(result["id"]).MustInt64()); err != nil {
+ return fmt.Errorf("update config column: %v", err)
+ }
+ }
+ return sess.Commit()
+}
diff --git a/internal/db/migrations/v14.go b/internal/db/migrations/v14.go
new file mode 100644
index 00000000..de8babed
--- /dev/null
+++ b/internal/db/migrations/v14.go
@@ -0,0 +1,24 @@
+// Copyright 2016 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 migrations
+
+import (
+ "fmt"
+
+ "xorm.io/xorm"
+)
+
+func setCommentUpdatedWithCreated(x *xorm.Engine) (err error) {
+ type Comment struct {
+ UpdatedUnix int64
+ }
+
+ if err = x.Sync2(new(Comment)); err != nil {
+ return fmt.Errorf("Sync2: %v", err)
+ } else if _, err = x.Exec("UPDATE comment SET updated_unix = created_unix"); err != nil {
+ return fmt.Errorf("set update_unix: %v", err)
+ }
+ return nil
+}
diff --git a/internal/db/migrations/v15.go b/internal/db/migrations/v15.go
new file mode 100644
index 00000000..7f3b9504
--- /dev/null
+++ b/internal/db/migrations/v15.go
@@ -0,0 +1,104 @@
+// Copyright 2017 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 migrations
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/unknwon/com"
+ "xorm.io/xorm"
+ log "gopkg.in/clog.v1"
+
+ "gogs.io/gogs/internal/setting"
+)
+
+func generateAndMigrateGitHooks(x *xorm.Engine) (err error) {
+ type Repository struct {
+ ID int64
+ OwnerID int64
+ Name string
+ }
+ type User struct {
+ ID int64
+ Name string
+ }
+ var (
+ hookNames = []string{"pre-receive", "update", "post-receive"}
+ hookTpls = []string{
+ fmt.Sprintf("#!/usr/bin/env %s\n\"%s\" hook --config='%s' pre-receive\n", setting.ScriptType, setting.AppPath, setting.CustomConf),
+ fmt.Sprintf("#!/usr/bin/env %s\n\"%s\" hook --config='%s' update $1 $2 $3\n", setting.ScriptType, setting.AppPath, setting.CustomConf),
+ fmt.Sprintf("#!/usr/bin/env %s\n\"%s\" hook --config='%s' post-receive\n", setting.ScriptType, setting.AppPath, setting.CustomConf),
+ }
+ )
+
+ // Cleanup old update.log and http.log files.
+ filepath.Walk(setting.LogRootPath, func(path string, info os.FileInfo, err error) error {
+ if !info.IsDir() &&
+ (strings.HasPrefix(filepath.Base(path), "update.log") ||
+ strings.HasPrefix(filepath.Base(path), "http.log")) {
+ os.Remove(path)
+ }
+ return nil
+ })
+
+ return x.Where("id > 0").Iterate(new(Repository),
+ func(idx int, bean interface{}) error {
+ repo := bean.(*Repository)
+ if repo.Name == "." || repo.Name == ".." {
+ return nil
+ }
+
+ user := new(User)
+ has, err := x.Where("id = ?", repo.OwnerID).Get(user)
+ if err != nil {
+ return fmt.Errorf("query owner of repository [repo_id: %d, owner_id: %d]: %v", repo.ID, repo.OwnerID, err)
+ } else if !has {
+ return nil
+ }
+
+ repoBase := filepath.Join(setting.RepoRootPath, strings.ToLower(user.Name), strings.ToLower(repo.Name))
+ repoPath := repoBase + ".git"
+ wikiPath := repoBase + ".wiki.git"
+ log.Trace("[%04d]: %s", idx, repoPath)
+
+ // Note: we should not create hookDir here because update hook file should already exists inside this direcotry,
+ // if this directory does not exist, the current setup is not correct anyway.
+ hookDir := filepath.Join(repoPath, "hooks")
+ customHookDir := filepath.Join(repoPath, "custom_hooks")
+ wikiHookDir := filepath.Join(wikiPath, "hooks")
+
+ for i, hookName := range hookNames {
+ oldHookPath := filepath.Join(hookDir, hookName)
+ newHookPath := filepath.Join(customHookDir, hookName)
+
+ // Gogs didn't allow user to set custom update hook thus no migration for it.
+ // In case user runs this migration multiple times, and custom hook exists,
+ // we assume it's been migrated already.
+ if hookName != "update" && com.IsFile(oldHookPath) && !com.IsExist(customHookDir) {
+ os.MkdirAll(customHookDir, os.ModePerm)
+ if err = os.Rename(oldHookPath, newHookPath); err != nil {
+ return fmt.Errorf("move hook file to custom directory '%s' -> '%s': %v", oldHookPath, newHookPath, err)
+ }
+ }
+
+ if err = ioutil.WriteFile(oldHookPath, []byte(hookTpls[i]), os.ModePerm); err != nil {
+ return fmt.Errorf("write hook file '%s': %v", oldHookPath, err)
+ }
+
+ if com.IsDir(wikiPath) {
+ os.MkdirAll(wikiHookDir, os.ModePerm)
+ wikiHookPath := filepath.Join(wikiHookDir, hookName)
+ if err = ioutil.WriteFile(wikiHookPath, []byte(hookTpls[i]), os.ModePerm); err != nil {
+ return fmt.Errorf("write wiki hook file '%s': %v", wikiHookPath, err)
+ }
+ }
+ }
+ return nil
+ })
+}
diff --git a/internal/db/migrations/v16.go b/internal/db/migrations/v16.go
new file mode 100644
index 00000000..b374be39
--- /dev/null
+++ b/internal/db/migrations/v16.go
@@ -0,0 +1,77 @@
+// Copyright 2017 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 migrations
+
+import (
+ "fmt"
+ "path/filepath"
+ "strings"
+
+ "xorm.io/xorm"
+ log "gopkg.in/clog.v1"
+
+ "github.com/gogs/git-module"
+
+ "gogs.io/gogs/internal/setting"
+)
+
+func updateRepositorySizes(x *xorm.Engine) (err error) {
+ log.Info("This migration could take up to minutes, please be patient.")
+ type Repository struct {
+ ID int64
+ OwnerID int64
+ Name string
+ Size int64
+ }
+ type User struct {
+ ID int64
+ Name string
+ }
+ if err = x.Sync2(new(Repository)); err != nil {
+ return fmt.Errorf("Sync2: %v", err)
+ }
+
+ // For the sake of SQLite3, we can't use x.Iterate here.
+ offset := 0
+ for {
+ repos := make([]*Repository, 0, 10)
+ if err = x.Sql(fmt.Sprintf("SELECT * FROM `repository` ORDER BY id ASC LIMIT 10 OFFSET %d", offset)).
+ Find(&repos); err != nil {
+ return fmt.Errorf("select repos [offset: %d]: %v", offset, err)
+ }
+ log.Trace("Select [offset: %d, repos: %d]", offset, len(repos))
+ if len(repos) == 0 {
+ break
+ }
+ offset += 10
+
+ for _, repo := range repos {
+ if repo.Name == "." || repo.Name == ".." {
+ continue
+ }
+
+ user := new(User)
+ has, err := x.Where("id = ?", repo.OwnerID).Get(user)
+ if err != nil {
+ return fmt.Errorf("query owner of repository [repo_id: %d, owner_id: %d]: %v", repo.ID, repo.OwnerID, err)
+ } else if !has {
+ continue
+ }
+
+ repoPath := filepath.Join(setting.RepoRootPath, strings.ToLower(user.Name), strings.ToLower(repo.Name)) + ".git"
+ countObject, err := git.GetRepoSize(repoPath)
+ if err != nil {
+ log.Warn("GetRepoSize: %v", err)
+ continue
+ }
+
+ repo.Size = countObject.Size + countObject.SizePack
+ if _, err = x.Id(repo.ID).Cols("size").Update(repo); err != nil {
+ return fmt.Errorf("update size: %v", err)
+ }
+ }
+ }
+ return nil
+}
diff --git a/internal/db/migrations/v17.go b/internal/db/migrations/v17.go
new file mode 100644
index 00000000..279ddf25
--- /dev/null
+++ b/internal/db/migrations/v17.go
@@ -0,0 +1,22 @@
+// Copyright 2017 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 migrations
+
+import (
+ "fmt"
+
+ "xorm.io/xorm"
+)
+
+func removeInvalidProtectBranchWhitelist(x *xorm.Engine) error {
+ exist, err := x.IsTableExist("protect_branch_whitelist")
+ if err != nil {
+ return fmt.Errorf("IsTableExist: %v", err)
+ } else if !exist {
+ return nil
+ }
+ _, err = x.Exec("DELETE FROM protect_branch_whitelist WHERE protect_branch_id = 0")
+ return err
+}
diff --git a/internal/db/migrations/v18.go b/internal/db/migrations/v18.go
new file mode 100644
index 00000000..9ebb46ed
--- /dev/null
+++ b/internal/db/migrations/v18.go
@@ -0,0 +1,34 @@
+// Copyright 2018 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 migrations
+
+import (
+ "fmt"
+
+ "xorm.io/xorm"
+
+ "gogs.io/gogs/internal/setting"
+)
+
+func updateRepositoryDescriptionField(x *xorm.Engine) error {
+ exist, err := x.IsTableExist("repository")
+ if err != nil {
+ return fmt.Errorf("IsTableExist: %v", err)
+ } else if !exist {
+ return nil
+ }
+ switch {
+ case setting.UseMySQL:
+ _, err = x.Exec("ALTER TABLE `repository` MODIFY `description` VARCHAR(512);")
+ case setting.UseMSSQL:
+ _, err = x.Exec("ALTER TABLE `repository` ALTER COLUMN `description` VARCHAR(512);")
+ case setting.UsePostgreSQL:
+ _, err = x.Exec("ALTER TABLE `repository` ALTER COLUMN `description` TYPE VARCHAR(512);")
+ case setting.UseSQLite3:
+ // Sqlite3 uses TEXT type by default for any string type field.
+ // Keep this comment to mention that we don't missed any option.
+ }
+ return err
+}
diff --git a/internal/db/migrations/v19.go b/internal/db/migrations/v19.go
new file mode 100644
index 00000000..bae2e355
--- /dev/null
+++ b/internal/db/migrations/v19.go
@@ -0,0 +1,18 @@
+// Copyright 2018 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 migrations
+
+import (
+ "xorm.io/xorm"
+)
+
+func cleanUnlinkedWebhookAndHookTasks(x *xorm.Engine) error {
+ _, err := x.Exec(`DELETE FROM webhook WHERE repo_id NOT IN (SELECT id FROM repository);`)
+ if err != nil {
+ return err
+ }
+ _, err = x.Exec(`DELETE FROM hook_task WHERE repo_id NOT IN (SELECT id FROM repository);`)
+ return err
+}
diff --git a/internal/db/milestone.go b/internal/db/milestone.go
new file mode 100644
index 00000000..10c5c556
--- /dev/null
+++ b/internal/db/milestone.go
@@ -0,0 +1,402 @@
+// Copyright 2017 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 db
+
+import (
+ "fmt"
+ "time"
+
+ log "gopkg.in/clog.v1"
+ "xorm.io/xorm"
+
+ api "github.com/gogs/go-gogs-client"
+
+ "gogs.io/gogs/internal/setting"
+)
+
+// Milestone represents a milestone of repository.
+type Milestone struct {
+ ID int64
+ RepoID int64 `xorm:"INDEX"`
+ Name string
+ Content string `xorm:"TEXT"`
+ RenderedContent string `xorm:"-" json:"-"`
+ IsClosed bool
+ NumIssues int
+ NumClosedIssues int
+ NumOpenIssues int `xorm:"-" json:"-"`
+ Completeness int // Percentage(1-100).
+ IsOverDue bool `xorm:"-" json:"-"`
+
+ DeadlineString string `xorm:"-" json:"-"`
+ Deadline time.Time `xorm:"-" json:"-"`
+ DeadlineUnix int64
+ ClosedDate time.Time `xorm:"-" json:"-"`
+ ClosedDateUnix int64
+}
+
+func (m *Milestone) BeforeInsert() {
+ m.DeadlineUnix = m.Deadline.Unix()
+}
+
+func (m *Milestone) BeforeUpdate() {
+ if m.NumIssues > 0 {
+ m.Completeness = m.NumClosedIssues * 100 / m.NumIssues
+ } else {
+ m.Completeness = 0
+ }
+
+ m.DeadlineUnix = m.Deadline.Unix()
+ m.ClosedDateUnix = m.ClosedDate.Unix()
+}
+
+func (m *Milestone) AfterSet(colName string, _ xorm.Cell) {
+ switch colName {
+ case "num_closed_issues":
+ m.NumOpenIssues = m.NumIssues - m.NumClosedIssues
+
+ case "deadline_unix":
+ m.Deadline = time.Unix(m.DeadlineUnix, 0).Local()
+ if m.Deadline.Year() == 9999 {
+ return
+ }
+
+ m.DeadlineString = m.Deadline.Format("2006-01-02")
+ if time.Now().Local().After(m.Deadline) {
+ m.IsOverDue = true
+ }
+
+ case "closed_date_unix":
+ m.ClosedDate = time.Unix(m.ClosedDateUnix, 0).Local()
+ }
+}
+
+// State returns string representation of milestone status.
+func (m *Milestone) State() api.StateType {
+ if m.IsClosed {
+ return api.STATE_CLOSED
+ }
+ return api.STATE_OPEN
+}
+
+func (m *Milestone) ChangeStatus(isClosed bool) error {
+ return ChangeMilestoneStatus(m, isClosed)
+}
+
+func (m *Milestone) APIFormat() *api.Milestone {
+ apiMilestone := &api.Milestone{
+ ID: m.ID,
+ State: m.State(),
+ Title: m.Name,
+ Description: m.Content,
+ OpenIssues: m.NumOpenIssues,
+ ClosedIssues: m.NumClosedIssues,
+ }
+ if m.IsClosed {
+ apiMilestone.Closed = &m.ClosedDate
+ }
+ if m.Deadline.Year() < 9999 {
+ apiMilestone.Deadline = &m.Deadline
+ }
+ return apiMilestone
+}
+
+func (m *Milestone) CountIssues(isClosed, includePulls bool) int64 {
+ sess := x.Where("milestone_id = ?", m.ID).And("is_closed = ?", isClosed)
+ if !includePulls {
+ sess.And("is_pull = ?", false)
+ }
+ count, _ := sess.Count(new(Issue))
+ return count
+}
+
+// NewMilestone creates new milestone of repository.
+func NewMilestone(m *Milestone) (err error) {
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if _, err = sess.Insert(m); err != nil {
+ return err
+ }
+
+ if _, err = sess.Exec("UPDATE `repository` SET num_milestones = num_milestones + 1 WHERE id = ?", m.RepoID); err != nil {
+ return err
+ }
+ return sess.Commit()
+}
+
+func getMilestoneByRepoID(e Engine, repoID, id int64) (*Milestone, error) {
+ m := &Milestone{
+ ID: id,
+ RepoID: repoID,
+ }
+ has, err := e.Get(m)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, ErrMilestoneNotExist{id, repoID}
+ }
+ return m, nil
+}
+
+// GetWebhookByRepoID returns the milestone in a repository.
+func GetMilestoneByRepoID(repoID, id int64) (*Milestone, error) {
+ return getMilestoneByRepoID(x, repoID, id)
+}
+
+// GetMilestonesByRepoID returns all milestones of a repository.
+func GetMilestonesByRepoID(repoID int64) ([]*Milestone, error) {
+ miles := make([]*Milestone, 0, 10)
+ return miles, x.Where("repo_id = ?", repoID).Find(&miles)
+}
+
+// GetMilestones returns a list of milestones of given repository and status.
+func GetMilestones(repoID int64, page int, isClosed bool) ([]*Milestone, error) {
+ miles := make([]*Milestone, 0, setting.UI.IssuePagingNum)
+ sess := x.Where("repo_id = ? AND is_closed = ?", repoID, isClosed)
+ if page > 0 {
+ sess = sess.Limit(setting.UI.IssuePagingNum, (page-1)*setting.UI.IssuePagingNum)
+ }
+ return miles, sess.Find(&miles)
+}
+
+func updateMilestone(e Engine, m *Milestone) error {
+ _, err := e.ID(m.ID).AllCols().Update(m)
+ return err
+}
+
+// UpdateMilestone updates information of given milestone.
+func UpdateMilestone(m *Milestone) error {
+ return updateMilestone(x, m)
+}
+
+func countRepoMilestones(e Engine, repoID int64) int64 {
+ count, _ := e.Where("repo_id=?", repoID).Count(new(Milestone))
+ return count
+}
+
+// CountRepoMilestones returns number of milestones in given repository.
+func CountRepoMilestones(repoID int64) int64 {
+ return countRepoMilestones(x, repoID)
+}
+
+func countRepoClosedMilestones(e Engine, repoID int64) int64 {
+ closed, _ := e.Where("repo_id=? AND is_closed=?", repoID, true).Count(new(Milestone))
+ return closed
+}
+
+// CountRepoClosedMilestones returns number of closed milestones in given repository.
+func CountRepoClosedMilestones(repoID int64) int64 {
+ return countRepoClosedMilestones(x, repoID)
+}
+
+// MilestoneStats returns number of open and closed milestones of given repository.
+func MilestoneStats(repoID int64) (open int64, closed int64) {
+ open, _ = x.Where("repo_id=? AND is_closed=?", repoID, false).Count(new(Milestone))
+ return open, CountRepoClosedMilestones(repoID)
+}
+
+// ChangeMilestoneStatus changes the milestone open/closed status.
+// If milestone passes with changed values, those values will be
+// updated to database as well.
+func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) {
+ repo, err := GetRepositoryByID(m.RepoID)
+ if err != nil {
+ return err
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ m.IsClosed = isClosed
+ if err = updateMilestone(sess, m); err != nil {
+ return err
+ }
+
+ repo.NumMilestones = int(countRepoMilestones(sess, repo.ID))
+ repo.NumClosedMilestones = int(countRepoClosedMilestones(sess, repo.ID))
+ if _, err = sess.ID(repo.ID).AllCols().Update(repo); err != nil {
+ return err
+ }
+ return sess.Commit()
+}
+
+func changeMilestoneIssueStats(e *xorm.Session, issue *Issue) error {
+ if issue.MilestoneID == 0 {
+ return nil
+ }
+
+ m, err := getMilestoneByRepoID(e, issue.RepoID, issue.MilestoneID)
+ if err != nil {
+ return err
+ }
+
+ if issue.IsClosed {
+ m.NumOpenIssues--
+ m.NumClosedIssues++
+ } else {
+ m.NumOpenIssues++
+ m.NumClosedIssues--
+ }
+
+ return updateMilestone(e, m)
+}
+
+// ChangeMilestoneIssueStats updates the open/closed issues counter and progress
+// for the milestone associated with the given issue.
+func ChangeMilestoneIssueStats(issue *Issue) (err error) {
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if err = changeMilestoneIssueStats(sess, issue); err != nil {
+ return err
+ }
+
+ return sess.Commit()
+}
+
+func changeMilestoneAssign(e *xorm.Session, issue *Issue, oldMilestoneID int64) error {
+ if oldMilestoneID > 0 {
+ m, err := getMilestoneByRepoID(e, issue.RepoID, oldMilestoneID)
+ if err != nil {
+ return err
+ }
+
+ m.NumIssues--
+ if issue.IsClosed {
+ m.NumClosedIssues--
+ }
+
+ if err = updateMilestone(e, m); err != nil {
+ return err
+ } else if _, err = e.Exec("UPDATE `issue_user` SET milestone_id = 0 WHERE issue_id = ?", issue.ID); err != nil {
+ return err
+ }
+
+ issue.Milestone = nil
+ }
+
+ if issue.MilestoneID > 0 {
+ m, err := getMilestoneByRepoID(e, issue.RepoID, issue.MilestoneID)
+ if err != nil {
+ return err
+ }
+
+ m.NumIssues++
+ if issue.IsClosed {
+ m.NumClosedIssues++
+ }
+
+ if err = updateMilestone(e, m); err != nil {
+ return err
+ } else if _, err = e.Exec("UPDATE `issue_user` SET milestone_id = ? WHERE issue_id = ?", m.ID, issue.ID); err != nil {
+ return err
+ }
+
+ issue.Milestone = m
+ }
+
+ return updateIssue(e, issue)
+}
+
+// ChangeMilestoneAssign changes assignment of milestone for issue.
+func ChangeMilestoneAssign(doer *User, issue *Issue, oldMilestoneID int64) (err error) {
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if err = changeMilestoneAssign(sess, issue, oldMilestoneID); err != nil {
+ return err
+ }
+
+ if err = sess.Commit(); err != nil {
+ return fmt.Errorf("Commit: %v", err)
+ }
+
+ var hookAction api.HookIssueAction
+ if issue.MilestoneID > 0 {
+ hookAction = api.HOOK_ISSUE_MILESTONED
+ } else {
+ hookAction = api.HOOK_ISSUE_DEMILESTONED
+ }
+
+ if issue.IsPull {
+ err = issue.PullRequest.LoadIssue()
+ if err != nil {
+ log.Error(2, "LoadIssue: %v", err)
+ return
+ }
+ err = PrepareWebhooks(issue.Repo, HOOK_EVENT_PULL_REQUEST, &api.PullRequestPayload{
+ Action: hookAction,
+ Index: issue.Index,
+ PullRequest: issue.PullRequest.APIFormat(),
+ Repository: issue.Repo.APIFormat(nil),
+ Sender: doer.APIFormat(),
+ })
+ } else {
+ err = PrepareWebhooks(issue.Repo, HOOK_EVENT_ISSUES, &api.IssuesPayload{
+ Action: hookAction,
+ Index: issue.Index,
+ Issue: issue.APIFormat(),
+ Repository: issue.Repo.APIFormat(nil),
+ Sender: doer.APIFormat(),
+ })
+ }
+ if err != nil {
+ log.Error(2, "PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err)
+ }
+
+ return nil
+}
+
+// DeleteMilestoneOfRepoByID deletes a milestone from a repository.
+func DeleteMilestoneOfRepoByID(repoID, id int64) error {
+ m, err := GetMilestoneByRepoID(repoID, id)
+ if err != nil {
+ if IsErrMilestoneNotExist(err) {
+ return nil
+ }
+ return err
+ }
+
+ repo, err := GetRepositoryByID(m.RepoID)
+ if err != nil {
+ return err
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if _, err = sess.ID(m.ID).Delete(new(Milestone)); err != nil {
+ return err
+ }
+
+ repo.NumMilestones = int(countRepoMilestones(sess, repo.ID))
+ repo.NumClosedMilestones = int(countRepoClosedMilestones(sess, repo.ID))
+ if _, err = sess.ID(repo.ID).AllCols().Update(repo); err != nil {
+ return err
+ }
+
+ if _, err = sess.Exec("UPDATE `issue` SET milestone_id = 0 WHERE milestone_id = ?", m.ID); err != nil {
+ return err
+ } else if _, err = sess.Exec("UPDATE `issue_user` SET milestone_id = 0 WHERE milestone_id = ?", m.ID); err != nil {
+ return err
+ }
+ return sess.Commit()
+}
diff --git a/internal/db/mirror.go b/internal/db/mirror.go
new file mode 100644
index 00000000..b165cbfc
--- /dev/null
+++ b/internal/db/mirror.go
@@ -0,0 +1,498 @@
+// Copyright 2016 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 db
+
+import (
+ "container/list"
+ "fmt"
+ "net/url"
+ "strings"
+ "time"
+
+ "github.com/unknwon/com"
+ log "gopkg.in/clog.v1"
+ "gopkg.in/ini.v1"
+ "xorm.io/xorm"
+
+ "github.com/gogs/git-module"
+
+ "gogs.io/gogs/internal/db/errors"
+ "gogs.io/gogs/internal/process"
+ "gogs.io/gogs/internal/setting"
+ "gogs.io/gogs/internal/sync"
+)
+
+var MirrorQueue = sync.NewUniqueQueue(setting.Repository.MirrorQueueLength)
+
+// Mirror represents mirror information of a repository.
+type Mirror struct {
+ ID int64
+ RepoID int64
+ Repo *Repository `xorm:"-" json:"-"`
+ Interval int // Hour.
+ EnablePrune bool `xorm:"NOT NULL DEFAULT true"`
+
+ // Last and next sync time of Git data from upstream
+ LastSync time.Time `xorm:"-" json:"-"`
+ LastSyncUnix int64 `xorm:"updated_unix"`
+ NextSync time.Time `xorm:"-" json:"-"`
+ NextSyncUnix int64 `xorm:"next_update_unix"`
+
+ address string `xorm:"-" json:"-"`
+}
+
+func (m *Mirror) BeforeInsert() {
+ m.NextSyncUnix = m.NextSync.Unix()
+}
+
+func (m *Mirror) BeforeUpdate() {
+ m.LastSyncUnix = m.LastSync.Unix()
+ m.NextSyncUnix = m.NextSync.Unix()
+}
+
+func (m *Mirror) AfterSet(colName string, _ xorm.Cell) {
+ var err error
+ switch colName {
+ case "repo_id":
+ m.Repo, err = GetRepositoryByID(m.RepoID)
+ if err != nil {
+ log.Error(3, "GetRepositoryByID [%d]: %v", m.ID, err)
+ }
+ case "updated_unix":
+ m.LastSync = time.Unix(m.LastSyncUnix, 0).Local()
+ case "next_update_unix":
+ m.NextSync = time.Unix(m.NextSyncUnix, 0).Local()
+ }
+}
+
+// ScheduleNextSync calculates and sets next sync time based on repostiroy mirror setting.
+func (m *Mirror) ScheduleNextSync() {
+ m.NextSync = time.Now().Add(time.Duration(m.Interval) * time.Hour)
+}
+
+// findPasswordInMirrorAddress returns start (inclusive) and end index (exclusive)
+// of password portion of credentials in given mirror address.
+// It returns a boolean value to indicate whether password portion is found.
+func findPasswordInMirrorAddress(addr string) (start int, end int, found bool) {
+ // Find end of credentials (start of path)
+ end = strings.LastIndex(addr, "@")
+ if end == -1 {
+ return -1, -1, false
+ }
+
+ // Find delimiter of credentials (end of username)
+ start = strings.Index(addr, "://")
+ if start == -1 {
+ return -1, -1, false
+ }
+ start += 3
+ delim := strings.Index(addr[start:], ":")
+ if delim == -1 {
+ return -1, -1, false
+ }
+ delim += 1
+
+ if start+delim >= end {
+ return -1, -1, false // No password portion presented
+ }
+
+ return start + delim, end, true
+}
+
+// unescapeMirrorCredentials returns mirror address with unescaped credentials.
+func unescapeMirrorCredentials(addr string) string {
+ start, end, found := findPasswordInMirrorAddress(addr)
+ if !found {
+ return addr
+ }
+
+ password, _ := url.QueryUnescape(addr[start:end])
+ return addr[:start] + password + addr[end:]
+}
+
+func (m *Mirror) readAddress() {
+ if len(m.address) > 0 {
+ return
+ }
+
+ cfg, err := ini.Load(m.Repo.GitConfigPath())
+ if err != nil {
+ log.Error(2, "Load: %v", err)
+ return
+ }
+ m.address = cfg.Section("remote \"origin\"").Key("url").Value()
+}
+
+// HandleMirrorCredentials replaces user credentials from HTTP/HTTPS URL
+// with placeholder <credentials>.
+// It returns original string if protocol is not HTTP/HTTPS.
+func HandleMirrorCredentials(url string, mosaics bool) string {
+ i := strings.Index(url, "@")
+ if i == -1 {
+ return url
+ }
+ start := strings.Index(url, "://")
+ if start == -1 {
+ return url
+ }
+ if mosaics {
+ return url[:start+3] + "<credentials>" + url[i:]
+ }
+ return url[:start+3] + url[i+1:]
+}
+
+// Address returns mirror address from Git repository config without credentials.
+func (m *Mirror) Address() string {
+ m.readAddress()
+ return HandleMirrorCredentials(m.address, false)
+}
+
+// MosaicsAddress returns mirror address from Git repository config with credentials under mosaics.
+func (m *Mirror) MosaicsAddress() string {
+ m.readAddress()
+ return HandleMirrorCredentials(m.address, true)
+}
+
+// RawAddress returns raw mirror address directly from Git repository config.
+func (m *Mirror) RawAddress() string {
+ m.readAddress()
+ return m.address
+}
+
+// FullAddress returns mirror address from Git repository config with unescaped credentials.
+func (m *Mirror) FullAddress() string {
+ m.readAddress()
+ return unescapeMirrorCredentials(m.address)
+}
+
+// escapeCredentials returns mirror address with escaped credentials.
+func escapeMirrorCredentials(addr string) string {
+ start, end, found := findPasswordInMirrorAddress(addr)
+ if !found {
+ return addr
+ }
+
+ return addr[:start] + url.QueryEscape(addr[start:end]) + addr[end:]
+}
+
+// SaveAddress writes new address to Git repository config.
+func (m *Mirror) SaveAddress(addr string) error {
+ configPath := m.Repo.GitConfigPath()
+ cfg, err := ini.Load(configPath)
+ if err != nil {
+ return fmt.Errorf("Load: %v", err)
+ }
+
+ cfg.Section(`remote "origin"`).Key("url").SetValue(escapeMirrorCredentials(addr))
+ return cfg.SaveToIndent(configPath, "\t")
+}
+
+const GIT_SHORT_EMPTY_SHA = "0000000"
+
+// mirrorSyncResult contains information of a updated reference.
+// If the oldCommitID is "0000000", it means a new reference, the value of newCommitID is empty.
+// If the newCommitID is "0000000", it means the reference is deleted, the value of oldCommitID is empty.
+type mirrorSyncResult struct {
+ refName string
+ oldCommitID string
+ newCommitID string
+}
+
+// parseRemoteUpdateOutput detects create, update and delete operations of references from upstream.
+func parseRemoteUpdateOutput(output string) []*mirrorSyncResult {
+ results := make([]*mirrorSyncResult, 0, 3)
+ lines := strings.Split(output, "\n")
+ for i := range lines {
+ // Make sure reference name is presented before continue
+ idx := strings.Index(lines[i], "-> ")
+ if idx == -1 {
+ continue
+ }
+
+ refName := lines[i][idx+3:]
+ switch {
+ case strings.HasPrefix(lines[i], " * "): // New reference
+ results = append(results, &mirrorSyncResult{
+ refName: refName,
+ oldCommitID: GIT_SHORT_EMPTY_SHA,
+ })
+ case strings.HasPrefix(lines[i], " - "): // Delete reference
+ results = append(results, &mirrorSyncResult{
+ refName: refName,
+ newCommitID: GIT_SHORT_EMPTY_SHA,
+ })
+ case strings.HasPrefix(lines[i], " "): // New commits of a reference
+ delimIdx := strings.Index(lines[i][3:], " ")
+ if delimIdx == -1 {
+ log.Error(2, "SHA delimiter not found: %q", lines[i])
+ continue
+ }
+ shas := strings.Split(lines[i][3:delimIdx+3], "..")
+ if len(shas) != 2 {
+ log.Error(2, "Expect two SHAs but not what found: %q", lines[i])
+ continue
+ }
+ results = append(results, &mirrorSyncResult{
+ refName: refName,
+ oldCommitID: shas[0],
+ newCommitID: shas[1],
+ })
+
+ default:
+ log.Warn("parseRemoteUpdateOutput: unexpected update line %q", lines[i])
+ }
+ }
+ return results
+}
+
+// runSync returns true if sync finished without error.
+func (m *Mirror) runSync() ([]*mirrorSyncResult, bool) {
+ repoPath := m.Repo.RepoPath()
+ wikiPath := m.Repo.WikiPath()
+ timeout := time.Duration(setting.Git.Timeout.Mirror) * time.Second
+
+ // Do a fast-fail testing against on repository URL to ensure it is accessible under
+ // good condition to prevent long blocking on URL resolution without syncing anything.
+ if !git.IsRepoURLAccessible(git.NetworkOptions{
+ URL: m.RawAddress(),
+ Timeout: 10 * time.Second,
+ }) {
+ desc := fmt.Sprintf("Source URL of mirror repository '%s' is not accessible: %s", m.Repo.FullName(), m.MosaicsAddress())
+ if err := CreateRepositoryNotice(desc); err != nil {
+ log.Error(2, "CreateRepositoryNotice: %v", err)
+ }
+ return nil, false
+ }
+
+ gitArgs := []string{"remote", "update"}
+ if m.EnablePrune {
+ gitArgs = append(gitArgs, "--prune")
+ }
+ _, stderr, err := process.ExecDir(
+ timeout, repoPath, fmt.Sprintf("Mirror.runSync: %s", repoPath),
+ "git", gitArgs...)
+ if err != nil {
+ desc := fmt.Sprintf("Fail to update mirror repository '%s': %s", repoPath, stderr)
+ log.Error(2, desc)
+ if err = CreateRepositoryNotice(desc); err != nil {
+ log.Error(2, "CreateRepositoryNotice: %v", err)
+ }
+ return nil, false
+ }
+ output := stderr
+
+ if err := m.Repo.UpdateSize(); err != nil {
+ log.Error(2, "UpdateSize [repo_id: %d]: %v", m.Repo.ID, err)
+ }
+
+ if m.Repo.HasWiki() {
+ // Even if wiki sync failed, we still want results from the main repository
+ if _, stderr, err := process.ExecDir(
+ timeout, wikiPath, fmt.Sprintf("Mirror.runSync: %s", wikiPath),
+ "git", "remote", "update", "--prune"); err != nil {
+ desc := fmt.Sprintf("Fail to update mirror wiki repository '%s': %s", wikiPath, stderr)
+ log.Error(2, desc)
+ if err = CreateRepositoryNotice(desc); err != nil {
+ log.Error(2, "CreateRepositoryNotice: %v", err)
+ }
+ }
+ }
+
+ return parseRemoteUpdateOutput(output), true
+}
+
+func getMirrorByRepoID(e Engine, repoID int64) (*Mirror, error) {
+ m := &Mirror{RepoID: repoID}
+ has, err := e.Get(m)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, errors.MirrorNotExist{repoID}
+ }
+ return m, nil
+}
+
+// GetMirrorByRepoID returns mirror information of a repository.
+func GetMirrorByRepoID(repoID int64) (*Mirror, error) {
+ return getMirrorByRepoID(x, repoID)
+}
+
+func updateMirror(e Engine, m *Mirror) error {
+ _, err := e.ID(m.ID).AllCols().Update(m)
+ return err
+}
+
+func UpdateMirror(m *Mirror) error {
+ return updateMirror(x, m)
+}
+
+func DeleteMirrorByRepoID(repoID int64) error {
+ _, err := x.Delete(&Mirror{RepoID: repoID})
+ return err
+}
+
+// MirrorUpdate checks and updates mirror repositories.
+func MirrorUpdate() {
+ if taskStatusTable.IsRunning(_MIRROR_UPDATE) {
+ return
+ }
+ taskStatusTable.Start(_MIRROR_UPDATE)
+ defer taskStatusTable.Stop(_MIRROR_UPDATE)
+
+ log.Trace("Doing: MirrorUpdate")
+
+ if err := x.Where("next_update_unix<=?", time.Now().Unix()).Iterate(new(Mirror), func(idx int, bean interface{}) error {
+ m := bean.(*Mirror)
+ if m.Repo == nil {
+ log.Error(2, "Disconnected mirror repository found: %d", m.ID)
+ return nil
+ }
+
+ MirrorQueue.Add(m.RepoID)
+ return nil
+ }); err != nil {
+ log.Error(2, "MirrorUpdate: %v", err)
+ }
+}
+
+// SyncMirrors checks and syncs mirrors.
+// TODO: sync more mirrors at same time.
+func SyncMirrors() {
+ // Start listening on new sync requests.
+ for repoID := range MirrorQueue.Queue() {
+ log.Trace("SyncMirrors [repo_id: %s]", repoID)
+ MirrorQueue.Remove(repoID)
+
+ m, err := GetMirrorByRepoID(com.StrTo(repoID).MustInt64())
+ if err != nil {
+ log.Error(2, "GetMirrorByRepoID [%d]: %v", m.RepoID, err)
+ continue
+ }
+
+ results, ok := m.runSync()
+ if !ok {
+ continue
+ }
+
+ m.ScheduleNextSync()
+ if err = UpdateMirror(m); err != nil {
+ log.Error(2, "UpdateMirror [%d]: %v", m.RepoID, err)
+ continue
+ }
+
+ // TODO:
+ // - Create "Mirror Sync" webhook event
+ // - Create mirror sync (create, push and delete) events and trigger the "mirror sync" webhooks
+
+ var gitRepo *git.Repository
+ if len(results) == 0 {
+ log.Trace("SyncMirrors [repo_id: %d]: no commits fetched", m.RepoID)
+ } else {
+ gitRepo, err = git.OpenRepository(m.Repo.RepoPath())
+ if err != nil {
+ log.Error(2, "OpenRepository [%d]: %v", m.RepoID, err)
+ continue
+ }
+ }
+
+ for _, result := range results {
+ // Discard GitHub pull requests, i.e. refs/pull/*
+ if strings.HasPrefix(result.refName, "refs/pull/") {
+ continue
+ }
+
+ // Delete reference
+ if result.newCommitID == GIT_SHORT_EMPTY_SHA {
+ if err = MirrorSyncDeleteAction(m.Repo, result.refName); err != nil {
+ log.Error(2, "MirrorSyncDeleteAction [repo_id: %d]: %v", m.RepoID, err)
+ }
+ continue
+ }
+
+ // New reference
+ isNewRef := false
+ if result.oldCommitID == GIT_SHORT_EMPTY_SHA {
+ if err = MirrorSyncCreateAction(m.Repo, result.refName); err != nil {
+ log.Error(2, "MirrorSyncCreateAction [repo_id: %d]: %v", m.RepoID, err)
+ continue
+ }
+ isNewRef = true
+ }
+
+ // Push commits
+ var commits *list.List
+ var oldCommitID string
+ var newCommitID string
+ if !isNewRef {
+ oldCommitID, err = git.GetFullCommitID(gitRepo.Path, result.oldCommitID)
+ if err != nil {
+ log.Error(2, "GetFullCommitID [%d]: %v", m.RepoID, err)
+ continue
+ }
+ newCommitID, err = git.GetFullCommitID(gitRepo.Path, result.newCommitID)
+ if err != nil {
+ log.Error(2, "GetFullCommitID [%d]: %v", m.RepoID, err)
+ continue
+ }
+ commits, err = gitRepo.CommitsBetweenIDs(newCommitID, oldCommitID)
+ if err != nil {
+ log.Error(2, "CommitsBetweenIDs [repo_id: %d, new_commit_id: %s, old_commit_id: %s]: %v", m.RepoID, newCommitID, oldCommitID, err)
+ continue
+ }
+ } else {
+ refNewCommitID, err := gitRepo.GetBranchCommitID(result.refName)
+ if err != nil {
+ log.Error(2, "GetFullCommitID [%d]: %v", m.RepoID, err)
+ continue
+ }
+ if newCommit, err := gitRepo.GetCommit(refNewCommitID); err != nil {
+ log.Error(2, "GetCommit [repo_id: %d, commit_id: %s]: %v", m.RepoID, refNewCommitID, err)
+ continue
+ } else {
+ // TODO: Get the commits for the new ref until the closest ancestor branch like Github does
+ commits, err = newCommit.CommitsBeforeLimit(10)
+ if err != nil {
+ log.Error(2, "CommitsBeforeLimit [repo_id: %d, commit_id: %s]: %v", m.RepoID, refNewCommitID, err)
+ }
+ oldCommitID = git.EMPTY_SHA
+ newCommitID = refNewCommitID
+ }
+ }
+ if err = MirrorSyncPushAction(m.Repo, MirrorSyncPushActionOptions{
+ RefName: result.refName,
+ OldCommitID: oldCommitID,
+ NewCommitID: newCommitID,
+ Commits: ListToPushCommits(commits),
+ }); err != nil {
+ log.Error(2, "MirrorSyncPushAction [repo_id: %d]: %v", m.RepoID, err)
+ continue
+ }
+ }
+
+ if _, err = x.Exec("UPDATE mirror SET updated_unix = ? WHERE repo_id = ?", time.Now().Unix(), m.RepoID); err != nil {
+ log.Error(2, "Update 'mirror.updated_unix' [%d]: %v", m.RepoID, err)
+ continue
+ }
+
+ // Get latest commit date and compare to current repository updated time,
+ // update if latest commit date is newer.
+ commitDate, err := git.GetLatestCommitDate(m.Repo.RepoPath(), "")
+ if err != nil {
+ log.Error(2, "GetLatestCommitDate [%d]: %v", m.RepoID, err)
+ continue
+ } else if commitDate.Before(m.Repo.Updated) {
+ continue
+ }
+
+ if _, err = x.Exec("UPDATE repository SET updated_unix = ? WHERE id = ?", commitDate.Unix(), m.RepoID); err != nil {
+ log.Error(2, "Update 'repository.updated_unix' [%d]: %v", m.RepoID, err)
+ continue
+ }
+ }
+}
+
+func InitSyncMirrors() {
+ go SyncMirrors()
+}
diff --git a/internal/db/mirror_test.go b/internal/db/mirror_test.go
new file mode 100644
index 00000000..cc85546a
--- /dev/null
+++ b/internal/db/mirror_test.go
@@ -0,0 +1,108 @@
+// Copyright 2017 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 db
+
+import (
+ "testing"
+
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+func Test_parseRemoteUpdateOutput(t *testing.T) {
+ Convey("Parse mirror remote update output", t, func() {
+ testCases := []struct {
+ output string
+ results []*mirrorSyncResult
+ }{
+ {
+ `
+From https://try.gogs.io/unknwon/upsteam
+ * [new branch] develop -> develop
+ b0bb24f..1d85a4f master -> master
+ - [deleted] (none) -> bugfix
+`,
+ []*mirrorSyncResult{
+ {"develop", GIT_SHORT_EMPTY_SHA, ""},
+ {"master", "b0bb24f", "1d85a4f"},
+ {"bugfix", "", GIT_SHORT_EMPTY_SHA},
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ results := parseRemoteUpdateOutput(tc.output)
+ So(len(results), ShouldEqual, len(tc.results))
+
+ for i := range tc.results {
+ So(tc.results[i].refName, ShouldEqual, results[i].refName)
+ So(tc.results[i].oldCommitID, ShouldEqual, results[i].oldCommitID)
+ So(tc.results[i].newCommitID, ShouldEqual, results[i].newCommitID)
+ }
+ }
+ })
+}
+
+func Test_findPasswordInMirrorAddress(t *testing.T) {
+ Convey("Find password portion in mirror address", t, func() {
+ testCases := []struct {
+ addr string
+ start, end int
+ found bool
+ password string
+ }{
+ {"http://localhost:3000/user/repo.git", -1, -1, false, ""},
+ {"http://user@localhost:3000/user/repo.git", -1, -1, false, ""},
+ {"http://user:@localhost:3000/user/repo.git", -1, -1, false, ""},
+ {"http://user:password@localhost:3000/user/repo.git", 12, 20, true, "password"},
+ {"http://username:my%3Asecure%3Bpassword@localhost:3000/user/repo.git", 16, 38, true, "my%3Asecure%3Bpassword"},
+ {"http://username:my%40secure%23password@localhost:3000/user/repo.git", 16, 38, true, "my%40secure%23password"},
+ {"http://username:@@localhost:3000/user/repo.git", 16, 17, true, "@"},
+ }
+
+ for _, tc := range testCases {
+ start, end, found := findPasswordInMirrorAddress(tc.addr)
+ So(start, ShouldEqual, tc.start)
+ So(end, ShouldEqual, tc.end)
+ So(found, ShouldEqual, tc.found)
+ if found {
+ So(tc.addr[start:end], ShouldEqual, tc.password)
+ }
+ }
+ })
+}
+
+func Test_unescapeMirrorCredentials(t *testing.T) {
+ Convey("Escape credentials in mirror address", t, func() {
+ testCases := []string{
+ "http://localhost:3000/user/repo.git", "http://localhost:3000/user/repo.git",
+ "http://user@localhost:3000/user/repo.git", "http://user@localhost:3000/user/repo.git",
+ "http://user:@localhost:3000/user/repo.git", "http://user:@localhost:3000/user/repo.git",
+ "http://user:password@localhost:3000/user/repo.git", "http://user:password@localhost:3000/user/repo.git",
+ "http://user:my%3Asecure%3Bpassword@localhost:3000/user/repo.git", "http://user:my:secure;password@localhost:3000/user/repo.git",
+ "http://user:my%40secure%23password@localhost:3000/user/repo.git", "http://user:my@secure#password@localhost:3000/user/repo.git",
+ }
+
+ for i := 0; i < len(testCases); i += 2 {
+ So(unescapeMirrorCredentials(testCases[i]), ShouldEqual, testCases[i+1])
+ }
+ })
+}
+
+func Test_escapeMirrorCredentials(t *testing.T) {
+ Convey("Escape credentials in mirror address", t, func() {
+ testCases := []string{
+ "http://localhost:3000/user/repo.git", "http://localhost:3000/user/repo.git",
+ "http://user@localhost:3000/user/repo.git", "http://user@localhost:3000/user/repo.git",
+ "http://user:@localhost:3000/user/repo.git", "http://user:@localhost:3000/user/repo.git",
+ "http://user:password@localhost:3000/user/repo.git", "http://user:password@localhost:3000/user/repo.git",
+ "http://user:my:secure;password@localhost:3000/user/repo.git", "http://user:my%3Asecure%3Bpassword@localhost:3000/user/repo.git",
+ "http://user:my@secure#password@localhost:3000/user/repo.git", "http://user:my%40secure%23password@localhost:3000/user/repo.git",
+ }
+
+ for i := 0; i < len(testCases); i += 2 {
+ So(escapeMirrorCredentials(testCases[i]), ShouldEqual, testCases[i+1])
+ }
+ })
+}
diff --git a/internal/db/models.go b/internal/db/models.go
new file mode 100644
index 00000000..75aeaf3a
--- /dev/null
+++ b/internal/db/models.go
@@ -0,0 +1,401 @@
+// Copyright 2014 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 db
+
+import (
+ "bufio"
+ "database/sql"
+ "errors"
+ "fmt"
+ "net/url"
+ "os"
+ "path"
+ "strings"
+ "time"
+
+ _ "github.com/denisenkom/go-mssqldb"
+ _ "github.com/go-sql-driver/mysql"
+ "github.com/json-iterator/go"
+ _ "github.com/lib/pq"
+ "github.com/unknwon/com"
+ log "gopkg.in/clog.v1"
+ "xorm.io/core"
+ "xorm.io/xorm"
+
+ "gogs.io/gogs/internal/db/migrations"
+ "gogs.io/gogs/internal/setting"
+)
+
+// Engine represents a XORM engine or session.
+type Engine interface {
+ Delete(interface{}) (int64, error)
+ Exec(...interface{}) (sql.Result, error)
+ Find(interface{}, ...interface{}) error
+ Get(interface{}) (bool, error)
+ ID(interface{}) *xorm.Session
+ In(string, ...interface{}) *xorm.Session
+ Insert(...interface{}) (int64, error)
+ InsertOne(interface{}) (int64, error)
+ Iterate(interface{}, xorm.IterFunc) error
+ Sql(string, ...interface{}) *xorm.Session
+ Table(interface{}) *xorm.Session
+ Where(interface{}, ...interface{}) *xorm.Session
+}
+
+var (
+ x *xorm.Engine
+ tables []interface{}
+ HasEngine bool
+
+ DbCfg struct {
+ Type, Host, Name, User, Passwd, Path, SSLMode string
+ }
+
+ EnableSQLite3 bool
+)
+
+func init() {
+ tables = append(tables,
+ new(User), new(PublicKey), new(AccessToken), new(TwoFactor), new(TwoFactorRecoveryCode),
+ new(Repository), new(DeployKey), new(Collaboration), new(Access), new(Upload),
+ new(Watch), new(Star), new(Follow), new(Action),
+ new(Issue), new(PullRequest), new(Comment), new(Attachment), new(IssueUser),
+ new(Label), new(IssueLabel), new(Milestone),
+ new(Mirror), new(Release), new(LoginSource), new(Webhook), new(HookTask),
+ new(ProtectBranch), new(ProtectBranchWhitelist),
+ new(Team), new(OrgUser), new(TeamUser), new(TeamRepo),
+ new(Notice), new(EmailAddress))
+
+ gonicNames := []string{"SSL"}
+ for _, name := range gonicNames {
+ core.LintGonicMapper[name] = true
+ }
+}
+
+func LoadConfigs() {
+ sec := setting.Cfg.Section("database")
+ DbCfg.Type = sec.Key("DB_TYPE").String()
+ switch DbCfg.Type {
+ case "sqlite3":
+ setting.UseSQLite3 = true
+ case "mysql":
+ setting.UseMySQL = true
+ case "postgres":
+ setting.UsePostgreSQL = true
+ case "mssql":
+ setting.UseMSSQL = true
+ }
+ DbCfg.Host = sec.Key("HOST").String()
+ DbCfg.Name = sec.Key("NAME").String()
+ DbCfg.User = sec.Key("USER").String()
+ if len(DbCfg.Passwd) == 0 {
+ DbCfg.Passwd = sec.Key("PASSWD").String()
+ }
+ DbCfg.SSLMode = sec.Key("SSL_MODE").String()
+ DbCfg.Path = sec.Key("PATH").MustString("data/gogs.db")
+}
+
+// parsePostgreSQLHostPort parses given input in various forms defined in
+// https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING
+// and returns proper host and port number.
+func parsePostgreSQLHostPort(info string) (string, string) {
+ host, port := "127.0.0.1", "5432"
+ if strings.Contains(info, ":") && !strings.HasSuffix(info, "]") {
+ idx := strings.LastIndex(info, ":")
+ host = info[:idx]
+ port = info[idx+1:]
+ } else if len(info) > 0 {
+ host = info
+ }
+ return host, port
+}
+
+func parseMSSQLHostPort(info string) (string, string) {
+ host, port := "127.0.0.1", "1433"
+ if strings.Contains(info, ":") {
+ host = strings.Split(info, ":")[0]
+ port = strings.Split(info, ":")[1]
+ } else if strings.Contains(info, ",") {
+ host = strings.Split(info, ",")[0]
+ port = strings.TrimSpace(strings.Split(info, ",")[1])
+ } else if len(info) > 0 {
+ host = info
+ }
+ return host, port
+}
+
+func getEngine() (*xorm.Engine, error) {
+ connStr := ""
+ var Param string = "?"
+ if strings.Contains(DbCfg.Name, Param) {
+ Param = "&"
+ }
+ switch DbCfg.Type {
+ case "mysql":
+ if DbCfg.Host[0] == '/' { // looks like a unix socket
+ connStr = fmt.Sprintf("%s:%s@unix(%s)/%s%scharset=utf8mb4&parseTime=true",
+ DbCfg.User, DbCfg.Passwd, DbCfg.Host, DbCfg.Name, Param)
+ } else {
+ connStr = fmt.Sprintf("%s:%s@tcp(%s)/%s%scharset=utf8mb4&parseTime=true",
+ DbCfg.User, DbCfg.Passwd, DbCfg.Host, DbCfg.Name, Param)
+ }
+ var engineParams = map[string]string{"rowFormat": "DYNAMIC"}
+ return xorm.NewEngineWithParams(DbCfg.Type, connStr, engineParams)
+ case "postgres":
+ host, port := parsePostgreSQLHostPort(DbCfg.Host)
+ if host[0] == '/' { // looks like a unix socket
+ connStr = fmt.Sprintf("postgres://%s:%s@:%s/%s%ssslmode=%s&host=%s",
+ url.QueryEscape(DbCfg.User), url.QueryEscape(DbCfg.Passwd), port, DbCfg.Name, Param, DbCfg.SSLMode, host)
+ } else {
+ connStr = fmt.Sprintf("postgres://%s:%s@%s:%s/%s%ssslmode=%s",
+ url.QueryEscape(DbCfg.User), url.QueryEscape(DbCfg.Passwd), host, port, DbCfg.Name, Param, DbCfg.SSLMode)
+ }
+ case "mssql":
+ host, port := parseMSSQLHostPort(DbCfg.Host)
+ connStr = fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;", host, port, DbCfg.Name, DbCfg.User, DbCfg.Passwd)
+ case "sqlite3":
+ if !EnableSQLite3 {
+ return nil, errors.New("this binary version does not build support for SQLite3")
+ }
+ if err := os.MkdirAll(path.Dir(DbCfg.Path), os.ModePerm); err != nil {
+ return nil, fmt.Errorf("create directories: %v", err)
+ }
+ connStr = "file:" + DbCfg.Path + "?cache=shared&mode=rwc"
+ default:
+ return nil, fmt.Errorf("unknown database type: %s", DbCfg.Type)
+ }
+ return xorm.NewEngine(DbCfg.Type, connStr)
+}
+
+func NewTestEngine(x *xorm.Engine) (err error) {
+ x, err = getEngine()
+ if err != nil {
+ return fmt.Errorf("connect to database: %v", err)
+ }
+
+ x.SetMapper(core.GonicMapper{})
+ return x.StoreEngine("InnoDB").Sync2(tables...)
+}
+
+func SetEngine() (err error) {
+ x, err = getEngine()
+ if err != nil {
+ return fmt.Errorf("connect to database: %v", err)
+ }
+
+ x.SetMapper(core.GonicMapper{})
+
+ // WARNING: for serv command, MUST remove the output to os.stdout,
+ // so use log file to instead print to stdout.
+ sec := setting.Cfg.Section("log.xorm")
+ logger, err := log.NewFileWriter(path.Join(setting.LogRootPath, "xorm.log"),
+ log.FileRotationConfig{
+ Rotate: sec.Key("ROTATE").MustBool(true),
+ Daily: sec.Key("ROTATE_DAILY").MustBool(true),
+ MaxSize: sec.Key("MAX_SIZE").MustInt64(100) * 1024 * 1024,
+ MaxDays: sec.Key("MAX_DAYS").MustInt64(3),
+ })
+ if err != nil {
+ return fmt.Errorf("create 'xorm.log': %v", err)
+ }
+
+ // To prevent mystery "MySQL: invalid connection" error,
+ // see https://gogs.io/gogs/issues/5532.
+ x.SetMaxIdleConns(0)
+ x.SetConnMaxLifetime(time.Second)
+
+ if setting.ProdMode {
+ x.SetLogger(xorm.NewSimpleLogger3(logger, xorm.DEFAULT_LOG_PREFIX, xorm.DEFAULT_LOG_FLAG, core.LOG_WARNING))
+ } else {
+ x.SetLogger(xorm.NewSimpleLogger(logger))
+ }
+ x.ShowSQL(true)
+ return nil
+}
+
+func NewEngine() (err error) {
+ if err = SetEngine(); err != nil {
+ return err
+ }
+
+ if err = migrations.Migrate(x); err != nil {
+ return fmt.Errorf("migrate: %v", err)
+ }
+
+ if err = x.StoreEngine("InnoDB").Sync2(tables...); err != nil {
+ return fmt.Errorf("sync structs to database tables: %v\n", err)
+ }
+
+ return nil
+}
+
+type Statistic struct {
+ Counter struct {
+ User, Org, PublicKey,
+ Repo, Watch, Star, Action, Access,
+ Issue, Comment, Oauth, Follow,
+ Mirror, Release, LoginSource, Webhook,
+ Milestone, Label, HookTask,
+ Team, UpdateTask, Attachment int64
+ }
+}
+
+func GetStatistic() (stats Statistic) {
+ stats.Counter.User = CountUsers()
+ stats.Counter.Org = CountOrganizations()
+ stats.Counter.PublicKey, _ = x.Count(new(PublicKey))
+ stats.Counter.Repo = CountRepositories(true)
+ stats.Counter.Watch, _ = x.Count(new(Watch))
+ stats.Counter.Star, _ = x.Count(new(Star))
+ stats.Counter.Action, _ = x.Count(new(Action))
+ stats.Counter.Access, _ = x.Count(new(Access))
+ stats.Counter.Issue, _ = x.Count(new(Issue))
+ stats.Counter.Comment, _ = x.Count(new(Comment))
+ stats.Counter.Oauth = 0
+ stats.Counter.Follow, _ = x.Count(new(Follow))
+ stats.Counter.Mirror, _ = x.Count(new(Mirror))
+ stats.Counter.Release, _ = x.Count(new(Release))
+ stats.Counter.LoginSource = CountLoginSources()
+ stats.Counter.Webhook, _ = x.Count(new(Webhook))
+ stats.Counter.Milestone, _ = x.Count(new(Milestone))
+ stats.Counter.Label, _ = x.Count(new(Label))
+ stats.Counter.HookTask, _ = x.Count(new(HookTask))
+ stats.Counter.Team, _ = x.Count(new(Team))
+ stats.Counter.Attachment, _ = x.Count(new(Attachment))
+ return
+}
+
+func Ping() error {
+ return x.Ping()
+}
+
+// The version table. Should have only one row with id==1
+type Version struct {
+ ID int64
+ Version int64
+}
+
+// DumpDatabase dumps all data from database to file system in JSON format.
+func DumpDatabase(dirPath string) (err error) {
+ os.MkdirAll(dirPath, os.ModePerm)
+ // Purposely create a local variable to not modify global variable
+ tables := append(tables, new(Version))
+ for _, table := range tables {
+ tableName := strings.TrimPrefix(fmt.Sprintf("%T", table), "*db.")
+ tableFile := path.Join(dirPath, tableName+".json")
+ f, err := os.Create(tableFile)
+ if err != nil {
+ return fmt.Errorf("create JSON file: %v", err)
+ }
+
+ if err = x.Asc("id").Iterate(table, func(idx int, bean interface{}) (err error) {
+ return jsoniter.NewEncoder(f).Encode(bean)
+ }); err != nil {
+ f.Close()
+ return fmt.Errorf("dump table '%s': %v", tableName, err)
+ }
+ f.Close()
+ }
+ return nil
+}
+
+// ImportDatabase imports data from backup archive.
+func ImportDatabase(dirPath string, verbose bool) (err error) {
+ snakeMapper := core.SnakeMapper{}
+
+ skipInsertProcessors := map[string]bool{
+ "mirror": true,
+ "milestone": true,
+ }
+
+ // Purposely create a local variable to not modify global variable
+ tables := append(tables, new(Version))
+ for _, table := range tables {
+ tableName := strings.TrimPrefix(fmt.Sprintf("%T", table), "*db.")
+ tableFile := path.Join(dirPath, tableName+".json")
+ if !com.IsExist(tableFile) {
+ continue
+ }
+
+ if verbose {
+ log.Trace("Importing table '%s'...", tableName)
+ }
+
+ if err = x.DropTables(table); err != nil {
+ return fmt.Errorf("drop table '%s': %v", tableName, err)
+ } else if err = x.Sync2(table); err != nil {
+ return fmt.Errorf("sync table '%s': %v", tableName, err)
+ }
+
+ f, err := os.Open(tableFile)
+ if err != nil {
+ return fmt.Errorf("open JSON file: %v", err)
+ }
+ rawTableName := x.TableName(table)
+ _, isInsertProcessor := table.(xorm.BeforeInsertProcessor)
+ scanner := bufio.NewScanner(f)
+ for scanner.Scan() {
+ switch bean := table.(type) {
+ case *LoginSource:
+ meta := make(map[string]interface{})
+ if err = jsoniter.Unmarshal(scanner.Bytes(), &meta); err != nil {
+ return fmt.Errorf("unmarshal to map: %v", err)
+ }
+
+ tp := LoginType(com.StrTo(com.ToStr(meta["Type"])).MustInt64())
+ switch tp {
+ case LOGIN_LDAP, LOGIN_DLDAP:
+ bean.Cfg = new(LDAPConfig)
+ case LOGIN_SMTP:
+ bean.Cfg = new(SMTPConfig)
+ case LOGIN_PAM:
+ bean.Cfg = new(PAMConfig)
+ case LOGIN_GITHUB:
+ bean.Cfg = new(GitHubConfig)
+ default:
+ return fmt.Errorf("unrecognized login source type:: %v", tp)
+ }
+ table = bean
+ }
+
+ if err = jsoniter.Unmarshal(scanner.Bytes(), table); err != nil {
+ return fmt.Errorf("unmarshal to struct: %v", err)
+ }
+
+ if _, err = x.Insert(table); err != nil {
+ return fmt.Errorf("insert strcut: %v", err)
+ }
+
+ meta := make(map[string]interface{})
+ if err = jsoniter.Unmarshal(scanner.Bytes(), &meta); err != nil {
+ log.Error(2, "Failed to unmarshal to map: %v", err)
+ }
+
+ // Reset created_unix back to the date save in archive because Insert method updates its value
+ if isInsertProcessor && !skipInsertProcessors[rawTableName] {
+ if _, err = x.Exec("UPDATE "+rawTableName+" SET created_unix=? WHERE id=?", meta["CreatedUnix"], meta["ID"]); err != nil {
+ log.Error(2, "Failed to reset 'created_unix': %v", err)
+ }
+ }
+
+ switch rawTableName {
+ case "milestone":
+ if _, err = x.Exec("UPDATE "+rawTableName+" SET deadline_unix=?, closed_date_unix=? WHERE id=?", meta["DeadlineUnix"], meta["ClosedDateUnix"], meta["ID"]); err != nil {
+ log.Error(2, "Failed to reset 'milestone.deadline_unix', 'milestone.closed_date_unix': %v", err)
+ }
+ }
+ }
+
+ // PostgreSQL needs manually reset table sequence for auto increment keys
+ if setting.UsePostgreSQL {
+ rawTableName := snakeMapper.Obj2Table(tableName)
+ seqName := rawTableName + "_id_seq"
+ if _, err = x.Exec(fmt.Sprintf(`SELECT setval('%s', COALESCE((SELECT MAX(id)+1 FROM "%s"), 1), false);`, seqName, rawTableName)); err != nil {
+ return fmt.Errorf("reset table '%s' sequence: %v", rawTableName, err)
+ }
+ }
+ }
+ return nil
+}
diff --git a/internal/db/models_sqlite.go b/internal/db/models_sqlite.go
new file mode 100644
index 00000000..c462cc5d
--- /dev/null
+++ b/internal/db/models_sqlite.go
@@ -0,0 +1,15 @@
+// +build sqlite
+
+// Copyright 2014 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 db
+
+import (
+ _ "github.com/mattn/go-sqlite3"
+)
+
+func init() {
+ EnableSQLite3 = true
+}
diff --git a/internal/db/models_test.go b/internal/db/models_test.go
new file mode 100644
index 00000000..53f8b4f0
--- /dev/null
+++ b/internal/db/models_test.go
@@ -0,0 +1,33 @@
+// Copyright 2016 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 db
+
+import (
+ "testing"
+
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+func Test_parsePostgreSQLHostPort(t *testing.T) {
+ testSuites := []struct {
+ input string
+ host, port string
+ }{
+ {"127.0.0.1:1234", "127.0.0.1", "1234"},
+ {"127.0.0.1", "127.0.0.1", "5432"},
+ {"[::1]:1234", "[::1]", "1234"},
+ {"[::1]", "[::1]", "5432"},
+ {"/tmp/pg.sock:1234", "/tmp/pg.sock", "1234"},
+ {"/tmp/pg.sock", "/tmp/pg.sock", "5432"},
+ }
+
+ Convey("Parse PostgreSQL host and port", t, func() {
+ for _, suite := range testSuites {
+ host, port := parsePostgreSQLHostPort(suite.input)
+ So(host, ShouldEqual, suite.host)
+ So(port, ShouldEqual, suite.port)
+ }
+ })
+}
diff --git a/internal/db/org.go b/internal/db/org.go
new file mode 100644
index 00000000..fb16c830
--- /dev/null
+++ b/internal/db/org.go
@@ -0,0 +1,563 @@
+// Copyright 2014 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 db
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/go-xorm/builder"
+ "xorm.io/xorm"
+)
+
+var (
+ ErrOrgNotExist = errors.New("Organization does not exist")
+)
+
+// IsOwnedBy returns true if given user is in the owner team.
+func (org *User) IsOwnedBy(userID int64) bool {
+ return IsOrganizationOwner(org.ID, userID)
+}
+
+// IsOrgMember returns true if given user is member of organization.
+func (org *User) IsOrgMember(uid int64) bool {
+ return org.IsOrganization() && IsOrganizationMember(org.ID, uid)
+}
+
+func (org *User) getTeam(e Engine, name string) (*Team, error) {
+ return getTeamOfOrgByName(e, org.ID, name)
+}
+
+// GetTeamOfOrgByName returns named team of organization.
+func (org *User) GetTeam(name string) (*Team, error) {
+ return org.getTeam(x, name)
+}
+
+func (org *User) getOwnerTeam(e Engine) (*Team, error) {
+ return org.getTeam(e, OWNER_TEAM)
+}
+
+// GetOwnerTeam returns owner team of organization.
+func (org *User) GetOwnerTeam() (*Team, error) {
+ return org.getOwnerTeam(x)
+}
+
+func (org *User) getTeams(e Engine) (err error) {
+ org.Teams, err = getTeamsByOrgID(e, org.ID)
+ return err
+}
+
+// GetTeams returns all teams that belong to organization.
+func (org *User) GetTeams() error {
+ return org.getTeams(x)
+}
+
+// TeamsHaveAccessToRepo returns all teamsthat have given access level to the repository.
+func (org *User) TeamsHaveAccessToRepo(repoID int64, mode AccessMode) ([]*Team, error) {
+ return GetTeamsHaveAccessToRepo(org.ID, repoID, mode)
+}
+
+// GetMembers returns all members of organization.
+func (org *User) GetMembers() error {
+ ous, err := GetOrgUsersByOrgID(org.ID)
+ if err != nil {
+ return err
+ }
+
+ org.Members = make([]*User, len(ous))
+ for i, ou := range ous {
+ org.Members[i], err = GetUserByID(ou.Uid)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// AddMember adds new member to organization.
+func (org *User) AddMember(uid int64) error {
+ return AddOrgUser(org.ID, uid)
+}
+
+// RemoveMember removes member from organization.
+func (org *User) RemoveMember(uid int64) error {
+ return RemoveOrgUser(org.ID, uid)
+}
+
+func (org *User) removeOrgRepo(e Engine, repoID int64) error {
+ return removeOrgRepo(e, org.ID, repoID)
+}
+
+// RemoveOrgRepo removes all team-repository relations of organization.
+func (org *User) RemoveOrgRepo(repoID int64) error {
+ return org.removeOrgRepo(x, repoID)
+}
+
+// CreateOrganization creates record of a new organization.
+func CreateOrganization(org, owner *User) (err error) {
+ if err = IsUsableUsername(org.Name); err != nil {
+ return err
+ }
+
+ isExist, err := IsUserExist(0, org.Name)
+ if err != nil {
+ return err
+ } else if isExist {
+ return ErrUserAlreadyExist{org.Name}
+ }
+
+ org.LowerName = strings.ToLower(org.Name)
+ if org.Rands, err = GetUserSalt(); err != nil {
+ return err
+ }
+ if org.Salt, err = GetUserSalt(); err != nil {
+ return err
+ }
+ org.UseCustomAvatar = true
+ org.MaxRepoCreation = -1
+ org.NumTeams = 1
+ org.NumMembers = 1
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if _, err = sess.Insert(org); err != nil {
+ return fmt.Errorf("insert organization: %v", err)
+ }
+ org.GenerateRandomAvatar()
+
+ // Add initial creator to organization and owner team.
+ if _, err = sess.Insert(&OrgUser{
+ Uid: owner.ID,
+ OrgID: org.ID,
+ IsOwner: true,
+ NumTeams: 1,
+ }); err != nil {
+ return fmt.Errorf("insert org-user relation: %v", err)
+ }
+
+ // Create default owner team.
+ t := &Team{
+ OrgID: org.ID,
+ LowerName: strings.ToLower(OWNER_TEAM),
+ Name: OWNER_TEAM,
+ Authorize: ACCESS_MODE_OWNER,
+ NumMembers: 1,
+ }
+ if _, err = sess.Insert(t); err != nil {
+ return fmt.Errorf("insert owner team: %v", err)
+ }
+
+ if _, err = sess.Insert(&TeamUser{
+ UID: owner.ID,
+ OrgID: org.ID,
+ TeamID: t.ID,
+ }); err != nil {
+ return fmt.Errorf("insert team-user relation: %v", err)
+ }
+
+ if err = os.MkdirAll(UserPath(org.Name), os.ModePerm); err != nil {
+ return fmt.Errorf("create directory: %v", err)
+ }
+
+ return sess.Commit()
+}
+
+// GetOrgByName returns organization by given name.
+func GetOrgByName(name string) (*User, error) {
+ if len(name) == 0 {
+ return nil, ErrOrgNotExist
+ }
+ u := &User{
+ LowerName: strings.ToLower(name),
+ Type: USER_TYPE_ORGANIZATION,
+ }
+ has, err := x.Get(u)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, ErrOrgNotExist
+ }
+ return u, nil
+}
+
+// CountOrganizations returns number of organizations.
+func CountOrganizations() int64 {
+ count, _ := x.Where("type=1").Count(new(User))
+ return count
+}
+
+// Organizations returns number of organizations in given page.
+func Organizations(page, pageSize int) ([]*User, error) {
+ orgs := make([]*User, 0, pageSize)
+ return orgs, x.Limit(pageSize, (page-1)*pageSize).Where("type=1").Asc("id").Find(&orgs)
+}
+
+// DeleteOrganization completely and permanently deletes everything of organization.
+func DeleteOrganization(org *User) (err error) {
+ if err := DeleteUser(org); err != nil {
+ return err
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if err = deleteBeans(sess,
+ &Team{OrgID: org.ID},
+ &OrgUser{OrgID: org.ID},
+ &TeamUser{OrgID: org.ID},
+ ); err != nil {
+ return fmt.Errorf("deleteBeans: %v", err)
+ }
+
+ if err = deleteUser(sess, org); err != nil {
+ return fmt.Errorf("deleteUser: %v", err)
+ }
+
+ return sess.Commit()
+}
+
+// ________ ____ ___
+// \_____ \_______ ____ | | \______ ___________
+// / | \_ __ \/ ___\| | / ___// __ \_ __ \
+// / | \ | \/ /_/ > | /\___ \\ ___/| | \/
+// \_______ /__| \___ /|______//____ >\___ >__|
+// \/ /_____/ \/ \/
+
+// OrgUser represents an organization-user relation.
+type OrgUser struct {
+ ID int64
+ Uid int64 `xorm:"INDEX UNIQUE(s)"`
+ OrgID int64 `xorm:"INDEX UNIQUE(s)"`
+ IsPublic bool
+ IsOwner bool
+ NumTeams int
+}
+
+// IsOrganizationOwner returns true if given user is in the owner team.
+func IsOrganizationOwner(orgID, userID int64) bool {
+ has, _ := x.Where("is_owner = ?", true).And("uid = ?", userID).And("org_id = ?", orgID).Get(new(OrgUser))
+ return has
+}
+
+// IsOrganizationMember returns true if given user is member of organization.
+func IsOrganizationMember(orgId, uid int64) bool {
+ has, _ := x.Where("uid=?", uid).And("org_id=?", orgId).Get(new(OrgUser))
+ return has
+}
+
+// IsPublicMembership returns true if given user public his/her membership.
+func IsPublicMembership(orgId, uid int64) bool {
+ has, _ := x.Where("uid=?", uid).And("org_id=?", orgId).And("is_public=?", true).Get(new(OrgUser))
+ return has
+}
+
+func getOrgsByUserID(sess *xorm.Session, userID int64, showAll bool) ([]*User, error) {
+ orgs := make([]*User, 0, 10)
+ if !showAll {
+ sess.And("`org_user`.is_public=?", true)
+ }
+ return orgs, sess.And("`org_user`.uid=?", userID).
+ Join("INNER", "`org_user`", "`org_user`.org_id=`user`.id").Find(&orgs)
+}
+
+// GetOrgsByUserID returns a list of organizations that the given user ID
+// has joined.
+func GetOrgsByUserID(userID int64, showAll bool) ([]*User, error) {
+ return getOrgsByUserID(x.NewSession(), userID, showAll)
+}
+
+// GetOrgsByUserIDDesc returns a list of organizations that the given user ID
+// has joined, ordered descending by the given condition.
+func GetOrgsByUserIDDesc(userID int64, desc string, showAll bool) ([]*User, error) {
+ return getOrgsByUserID(x.NewSession().Desc(desc), userID, showAll)
+}
+
+func getOwnedOrgsByUserID(sess *xorm.Session, userID int64) ([]*User, error) {
+ orgs := make([]*User, 0, 10)
+ return orgs, sess.Where("`org_user`.uid=?", userID).And("`org_user`.is_owner=?", true).
+ Join("INNER", "`org_user`", "`org_user`.org_id=`user`.id").Find(&orgs)
+}
+
+// GetOwnedOrgsByUserID returns a list of organizations are owned by given user ID.
+func GetOwnedOrgsByUserID(userID int64) ([]*User, error) {
+ sess := x.NewSession()
+ return getOwnedOrgsByUserID(sess, userID)
+}
+
+// GetOwnedOrganizationsByUserIDDesc returns a list of organizations are owned by
+// given user ID, ordered descending by the given condition.
+func GetOwnedOrgsByUserIDDesc(userID int64, desc string) ([]*User, error) {
+ sess := x.NewSession()
+ return getOwnedOrgsByUserID(sess.Desc(desc), userID)
+}
+
+// GetOrgIDsByUserID returns a list of organization IDs that user belongs to.
+// The showPrivate indicates whether to include private memberships.
+func GetOrgIDsByUserID(userID int64, showPrivate bool) ([]int64, error) {
+ orgIDs := make([]int64, 0, 5)
+ sess := x.Table("org_user").Where("uid = ?", userID)
+ if !showPrivate {
+ sess.And("is_public = ?", true)
+ }
+ return orgIDs, sess.Distinct("org_id").Find(&orgIDs)
+}
+
+func getOrgUsersByOrgID(e Engine, orgID int64) ([]*OrgUser, error) {
+ orgUsers := make([]*OrgUser, 0, 10)
+ return orgUsers, e.Where("org_id=?", orgID).Find(&orgUsers)
+}
+
+// GetOrgUsersByOrgID returns all organization-user relations by organization ID.
+func GetOrgUsersByOrgID(orgID int64) ([]*OrgUser, error) {
+ return getOrgUsersByOrgID(x, orgID)
+}
+
+// ChangeOrgUserStatus changes public or private membership status.
+func ChangeOrgUserStatus(orgID, uid int64, public bool) error {
+ ou := new(OrgUser)
+ has, err := x.Where("uid=?", uid).And("org_id=?", orgID).Get(ou)
+ if err != nil {
+ return err
+ } else if !has {
+ return nil
+ }
+
+ ou.IsPublic = public
+ _, err = x.Id(ou.ID).AllCols().Update(ou)
+ return err
+}
+
+// AddOrgUser adds new user to given organization.
+func AddOrgUser(orgID, uid int64) error {
+ if IsOrganizationMember(orgID, uid) {
+ return nil
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err := sess.Begin(); err != nil {
+ return err
+ }
+
+ ou := &OrgUser{
+ Uid: uid,
+ OrgID: orgID,
+ }
+
+ if _, err := sess.Insert(ou); err != nil {
+ sess.Rollback()
+ return err
+ } else if _, err = sess.Exec("UPDATE `user` SET num_members = num_members + 1 WHERE id = ?", orgID); err != nil {
+ sess.Rollback()
+ return err
+ }
+
+ return sess.Commit()
+}
+
+// RemoveOrgUser removes user from given organization.
+func RemoveOrgUser(orgID, userID int64) error {
+ ou := new(OrgUser)
+
+ has, err := x.Where("uid=?", userID).And("org_id=?", orgID).Get(ou)
+ if err != nil {
+ return fmt.Errorf("get org-user: %v", err)
+ } else if !has {
+ return nil
+ }
+
+ user, err := GetUserByID(userID)
+ if err != nil {
+ return fmt.Errorf("GetUserByID [%d]: %v", userID, err)
+ }
+ org, err := GetUserByID(orgID)
+ if err != nil {
+ return fmt.Errorf("GetUserByID [%d]: %v", orgID, err)
+ }
+
+ // FIXME: only need to get IDs here, not all fields of repository.
+ repos, _, err := org.GetUserRepositories(user.ID, 1, org.NumRepos)
+ if err != nil {
+ return fmt.Errorf("GetUserRepositories [%d]: %v", user.ID, err)
+ }
+
+ // Check if the user to delete is the last member in owner team.
+ if IsOrganizationOwner(orgID, userID) {
+ t, err := org.GetOwnerTeam()
+ if err != nil {
+ return err
+ }
+ if t.NumMembers == 1 {
+ return ErrLastOrgOwner{UID: userID}
+ }
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err := sess.Begin(); err != nil {
+ return err
+ }
+
+ if _, err := sess.ID(ou.ID).Delete(ou); err != nil {
+ return err
+ } else if _, err = sess.Exec("UPDATE `user` SET num_members=num_members-1 WHERE id=?", orgID); err != nil {
+ return err
+ }
+
+ // Delete all repository accesses and unwatch them.
+ repoIDs := make([]int64, len(repos))
+ for i := range repos {
+ repoIDs = append(repoIDs, repos[i].ID)
+ if err = watchRepo(sess, user.ID, repos[i].ID, false); err != nil {
+ return err
+ }
+ }
+
+ if len(repoIDs) > 0 {
+ if _, err = sess.Where("user_id = ?", user.ID).In("repo_id", repoIDs).Delete(new(Access)); err != nil {
+ return err
+ }
+ }
+
+ // Delete member in his/her teams.
+ teams, err := getUserTeams(sess, org.ID, user.ID)
+ if err != nil {
+ return err
+ }
+ for _, t := range teams {
+ if err = removeTeamMember(sess, org.ID, t.ID, user.ID); err != nil {
+ return err
+ }
+ }
+
+ return sess.Commit()
+}
+
+func removeOrgRepo(e Engine, orgID, repoID int64) error {
+ _, err := e.Delete(&TeamRepo{
+ OrgID: orgID,
+ RepoID: repoID,
+ })
+ return err
+}
+
+// RemoveOrgRepo removes all team-repository relations of given organization.
+func RemoveOrgRepo(orgID, repoID int64) error {
+ return removeOrgRepo(x, orgID, repoID)
+}
+
+func (org *User) getUserTeams(e Engine, userID int64, cols ...string) ([]*Team, error) {
+ teams := make([]*Team, 0, org.NumTeams)
+ return teams, e.Where("team_user.org_id = ?", org.ID).
+ And("team_user.uid = ?", userID).
+ Join("INNER", "team_user", "team_user.team_id = team.id").
+ Cols(cols...).Find(&teams)
+}
+
+// GetUserTeamIDs returns of all team IDs of the organization that user is memeber of.
+func (org *User) GetUserTeamIDs(userID int64) ([]int64, error) {
+ teams, err := org.getUserTeams(x, userID, "team.id")
+ if err != nil {
+ return nil, fmt.Errorf("getUserTeams [%d]: %v", userID, err)
+ }
+
+ teamIDs := make([]int64, len(teams))
+ for i := range teams {
+ teamIDs[i] = teams[i].ID
+ }
+ return teamIDs, nil
+}
+
+// GetTeams returns all teams that belong to organization,
+// and that the user has joined.
+func (org *User) GetUserTeams(userID int64) ([]*Team, error) {
+ return org.getUserTeams(x, userID)
+}
+
+// GetUserRepositories returns a range of repositories in organization which the user has access to,
+// and total number of records based on given condition.
+func (org *User) GetUserRepositories(userID int64, page, pageSize int) ([]*Repository, int64, error) {
+ teamIDs, err := org.GetUserTeamIDs(userID)
+ if err != nil {
+ return nil, 0, fmt.Errorf("GetUserTeamIDs: %v", err)
+ }
+ if len(teamIDs) == 0 {
+ // user has no team but "IN ()" is invalid SQL
+ teamIDs = []int64{-1} // there is no team with id=-1
+ }
+
+ var teamRepoIDs []int64
+ if err = x.Table("team_repo").In("team_id", teamIDs).Distinct("repo_id").Find(&teamRepoIDs); err != nil {
+ return nil, 0, fmt.Errorf("get team repository IDs: %v", err)
+ }
+ if len(teamRepoIDs) == 0 {
+ // team has no repo but "IN ()" is invalid SQL
+ teamRepoIDs = []int64{-1} // there is no repo with id=-1
+ }
+
+ if page <= 0 {
+ page = 1
+ }
+ repos := make([]*Repository, 0, pageSize)
+ if err = x.Where("owner_id = ?", org.ID).
+ And("is_private = ?", false).
+ Or(builder.In("id", teamRepoIDs)).
+ Desc("updated_unix").
+ Limit(pageSize, (page-1)*pageSize).
+ Find(&repos); err != nil {
+ return nil, 0, fmt.Errorf("get user repositories: %v", err)
+ }
+
+ repoCount, err := x.Where("owner_id = ?", org.ID).
+ And("is_private = ?", false).
+ Or(builder.In("id", teamRepoIDs)).
+ Count(new(Repository))
+ if err != nil {
+ return nil, 0, fmt.Errorf("count user repositories: %v", err)
+ }
+
+ return repos, repoCount, nil
+}
+
+// GetUserMirrorRepositories returns mirror repositories of the organization which the user has access to.
+func (org *User) GetUserMirrorRepositories(userID int64) ([]*Repository, error) {
+ teamIDs, err := org.GetUserTeamIDs(userID)
+ if err != nil {
+ return nil, fmt.Errorf("GetUserTeamIDs: %v", err)
+ }
+ if len(teamIDs) == 0 {
+ teamIDs = []int64{-1}
+ }
+
+ var teamRepoIDs []int64
+ err = x.Table("team_repo").In("team_id", teamIDs).Distinct("repo_id").Find(&teamRepoIDs)
+ if err != nil {
+ return nil, fmt.Errorf("get team repository ids: %v", err)
+ }
+ if len(teamRepoIDs) == 0 {
+ // team has no repo but "IN ()" is invalid SQL
+ teamRepoIDs = []int64{-1} // there is no repo with id=-1
+ }
+
+ repos := make([]*Repository, 0, 10)
+ if err = x.Where("owner_id = ?", org.ID).
+ And("is_private = ?", false).
+ Or(builder.In("id", teamRepoIDs)).
+ And("is_mirror = ?", true). // Don't move up because it's an independent condition
+ Desc("updated_unix").
+ Find(&repos); err != nil {
+ return nil, fmt.Errorf("get user repositories: %v", err)
+ }
+ return repos, nil
+}
diff --git a/internal/db/org_team.go b/internal/db/org_team.go
new file mode 100644
index 00000000..7021e42d
--- /dev/null
+++ b/internal/db/org_team.go
@@ -0,0 +1,666 @@
+// Copyright 2016 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 db
+
+import (
+ "fmt"
+ "strings"
+
+ "xorm.io/xorm"
+
+ "gogs.io/gogs/internal/db/errors"
+)
+
+const OWNER_TEAM = "Owners"
+
+// Team represents a organization team.
+type Team struct {
+ ID int64
+ OrgID int64 `xorm:"INDEX"`
+ LowerName string
+ Name string
+ Description string
+ Authorize AccessMode
+ Repos []*Repository `xorm:"-" json:"-"`
+ Members []*User `xorm:"-" json:"-"`
+ NumRepos int
+ NumMembers int
+}
+
+func (t *Team) AfterSet(colName string, _ xorm.Cell) {
+ switch colName {
+ case "num_repos":
+ // LEGACY [1.0]: this is backward compatibility bug fix for https://gogs.io/gogs/issues/3671
+ if t.NumRepos < 0 {
+ t.NumRepos = 0
+ }
+ }
+}
+
+// IsOwnerTeam returns true if team is owner team.
+func (t *Team) IsOwnerTeam() bool {
+ return t.Name == OWNER_TEAM
+}
+
+// HasWriteAccess returns true if team has at least write level access mode.
+func (t *Team) HasWriteAccess() bool {
+ return t.Authorize >= ACCESS_MODE_WRITE
+}
+
+// IsTeamMember returns true if given user is a member of team.
+func (t *Team) IsMember(userID int64) bool {
+ return IsTeamMember(t.OrgID, t.ID, userID)
+}
+
+func (t *Team) getRepositories(e Engine) (err error) {
+ teamRepos := make([]*TeamRepo, 0, t.NumRepos)
+ if err = x.Where("team_id=?", t.ID).Find(&teamRepos); err != nil {
+ return fmt.Errorf("get team-repos: %v", err)
+ }
+
+ t.Repos = make([]*Repository, 0, len(teamRepos))
+ for i := range teamRepos {
+ repo, err := getRepositoryByID(e, teamRepos[i].RepoID)
+ if err != nil {
+ return fmt.Errorf("getRepositoryById(%d): %v", teamRepos[i].RepoID, err)
+ }
+ t.Repos = append(t.Repos, repo)
+ }
+ return nil
+}
+
+// GetRepositories returns all repositories in team of organization.
+func (t *Team) GetRepositories() error {
+ return t.getRepositories(x)
+}
+
+func (t *Team) getMembers(e Engine) (err error) {
+ t.Members, err = getTeamMembers(e, t.ID)
+ return err
+}
+
+// GetMembers returns all members in team of organization.
+func (t *Team) GetMembers() (err error) {
+ return t.getMembers(x)
+}
+
+// AddMember adds new membership of the team to the organization,
+// the user will have membership to the organization automatically when needed.
+func (t *Team) AddMember(uid int64) error {
+ return AddTeamMember(t.OrgID, t.ID, uid)
+}
+
+// RemoveMember removes member from team of organization.
+func (t *Team) RemoveMember(uid int64) error {
+ return RemoveTeamMember(t.OrgID, t.ID, uid)
+}
+
+func (t *Team) hasRepository(e Engine, repoID int64) bool {
+ return hasTeamRepo(e, t.OrgID, t.ID, repoID)
+}
+
+// HasRepository returns true if given repository belong to team.
+func (t *Team) HasRepository(repoID int64) bool {
+ return t.hasRepository(x, repoID)
+}
+
+func (t *Team) addRepository(e Engine, repo *Repository) (err error) {
+ if err = addTeamRepo(e, t.OrgID, t.ID, repo.ID); err != nil {
+ return err
+ }
+
+ t.NumRepos++
+ if _, err = e.ID(t.ID).AllCols().Update(t); err != nil {
+ return fmt.Errorf("update team: %v", err)
+ }
+
+ if err = repo.recalculateTeamAccesses(e, 0); err != nil {
+ return fmt.Errorf("recalculateAccesses: %v", err)
+ }
+
+ if err = t.getMembers(e); err != nil {
+ return fmt.Errorf("getMembers: %v", err)
+ }
+ for _, u := range t.Members {
+ if err = watchRepo(e, u.ID, repo.ID, true); err != nil {
+ return fmt.Errorf("watchRepo: %v", err)
+ }
+ }
+ return nil
+}
+
+// AddRepository adds new repository to team of organization.
+func (t *Team) AddRepository(repo *Repository) (err error) {
+ if repo.OwnerID != t.OrgID {
+ return errors.New("Repository does not belong to organization")
+ } else if t.HasRepository(repo.ID) {
+ return nil
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if err = t.addRepository(sess, repo); err != nil {
+ return err
+ }
+
+ return sess.Commit()
+}
+
+func (t *Team) removeRepository(e Engine, repo *Repository, recalculate bool) (err error) {
+ if err = removeTeamRepo(e, t.ID, repo.ID); err != nil {
+ return err
+ }
+
+ t.NumRepos--
+ if _, err = e.ID(t.ID).AllCols().Update(t); err != nil {
+ return err
+ }
+
+ // Don't need to recalculate when delete a repository from organization.
+ if recalculate {
+ if err = repo.recalculateTeamAccesses(e, t.ID); err != nil {
+ return err
+ }
+ }
+
+ if err = t.getMembers(e); err != nil {
+ return fmt.Errorf("get team members: %v", err)
+ }
+ for _, member := range t.Members {
+ has, err := hasAccess(e, member.ID, repo, ACCESS_MODE_READ)
+ if err != nil {
+ return err
+ } else if has {
+ continue
+ }
+
+ if err = watchRepo(e, member.ID, repo.ID, false); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// RemoveRepository removes repository from team of organization.
+func (t *Team) RemoveRepository(repoID int64) error {
+ if !t.HasRepository(repoID) {
+ return nil
+ }
+
+ repo, err := GetRepositoryByID(repoID)
+ if err != nil {
+ return err
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if err = t.removeRepository(sess, repo, true); err != nil {
+ return err
+ }
+
+ return sess.Commit()
+}
+
+var reservedTeamNames = []string{"new"}
+
+// IsUsableTeamName return an error if given name is a reserved name or pattern.
+func IsUsableTeamName(name string) error {
+ return isUsableName(reservedTeamNames, nil, name)
+}
+
+// NewTeam creates a record of new team.
+// It's caller's responsibility to assign organization ID.
+func NewTeam(t *Team) error {
+ if len(t.Name) == 0 {
+ return errors.New("empty team name")
+ } else if t.OrgID == 0 {
+ return errors.New("OrgID is not assigned")
+ }
+
+ if err := IsUsableTeamName(t.Name); err != nil {
+ return err
+ }
+
+ has, err := x.Id(t.OrgID).Get(new(User))
+ if err != nil {
+ return err
+ } else if !has {
+ return ErrOrgNotExist
+ }
+
+ t.LowerName = strings.ToLower(t.Name)
+ has, err = x.Where("org_id=?", t.OrgID).And("lower_name=?", t.LowerName).Get(new(Team))
+ if err != nil {
+ return err
+ } else if has {
+ return ErrTeamAlreadyExist{t.OrgID, t.LowerName}
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if _, err = sess.Insert(t); err != nil {
+ sess.Rollback()
+ return err
+ }
+
+ // Update organization number of teams.
+ if _, err = sess.Exec("UPDATE `user` SET num_teams=num_teams+1 WHERE id = ?", t.OrgID); err != nil {
+ sess.Rollback()
+ return err
+ }
+ return sess.Commit()
+}
+
+func getTeamOfOrgByName(e Engine, orgID int64, name string) (*Team, error) {
+ t := &Team{
+ OrgID: orgID,
+ LowerName: strings.ToLower(name),
+ }
+ has, err := e.Get(t)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, errors.TeamNotExist{0, name}
+ }
+ return t, nil
+}
+
+// GetTeamOfOrgByName returns team by given team name and organization.
+func GetTeamOfOrgByName(orgID int64, name string) (*Team, error) {
+ return getTeamOfOrgByName(x, orgID, name)
+}
+
+func getTeamByID(e Engine, teamID int64) (*Team, error) {
+ t := new(Team)
+ has, err := e.ID(teamID).Get(t)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, errors.TeamNotExist{teamID, ""}
+ }
+ return t, nil
+}
+
+// GetTeamByID returns team by given ID.
+func GetTeamByID(teamID int64) (*Team, error) {
+ return getTeamByID(x, teamID)
+}
+
+func getTeamsByOrgID(e Engine, orgID int64) ([]*Team, error) {
+ teams := make([]*Team, 0, 3)
+ return teams, e.Where("org_id = ?", orgID).Find(&teams)
+}
+
+// GetTeamsByOrgID returns all teams belong to given organization.
+func GetTeamsByOrgID(orgID int64) ([]*Team, error) {
+ return getTeamsByOrgID(x, orgID)
+}
+
+// UpdateTeam updates information of team.
+func UpdateTeam(t *Team, authChanged bool) (err error) {
+ if len(t.Name) == 0 {
+ return errors.New("empty team name")
+ }
+
+ if len(t.Description) > 255 {
+ t.Description = t.Description[:255]
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ t.LowerName = strings.ToLower(t.Name)
+ has, err := x.Where("org_id=?", t.OrgID).And("lower_name=?", t.LowerName).And("id!=?", t.ID).Get(new(Team))
+ if err != nil {
+ return err
+ } else if has {
+ return ErrTeamAlreadyExist{t.OrgID, t.LowerName}
+ }
+
+ if _, err = sess.ID(t.ID).AllCols().Update(t); err != nil {
+ return fmt.Errorf("update: %v", err)
+ }
+
+ // Update access for team members if needed.
+ if authChanged {
+ if err = t.getRepositories(sess); err != nil {
+ return fmt.Errorf("getRepositories:%v", err)
+ }
+
+ for _, repo := range t.Repos {
+ if err = repo.recalculateTeamAccesses(sess, 0); err != nil {
+ return fmt.Errorf("recalculateTeamAccesses: %v", err)
+ }
+ }
+ }
+
+ return sess.Commit()
+}
+
+// DeleteTeam deletes given team.
+// It's caller's responsibility to assign organization ID.
+func DeleteTeam(t *Team) error {
+ if err := t.GetRepositories(); err != nil {
+ return err
+ }
+
+ // Get organization.
+ org, err := GetUserByID(t.OrgID)
+ if err != nil {
+ return err
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ // Delete all accesses.
+ for _, repo := range t.Repos {
+ if err = repo.recalculateTeamAccesses(sess, t.ID); err != nil {
+ return err
+ }
+ }
+
+ // Delete team-user.
+ if _, err = sess.Where("org_id=?", org.ID).Where("team_id=?", t.ID).Delete(new(TeamUser)); err != nil {
+ return err
+ }
+
+ // Delete team.
+ if _, err = sess.ID(t.ID).Delete(new(Team)); err != nil {
+ return err
+ }
+ // Update organization number of teams.
+ if _, err = sess.Exec("UPDATE `user` SET num_teams=num_teams-1 WHERE id=?", t.OrgID); err != nil {
+ return err
+ }
+
+ return sess.Commit()
+}
+
+// ___________ ____ ___
+// \__ ___/___ _____ _____ | | \______ ___________
+// | |_/ __ \\__ \ / \| | / ___// __ \_ __ \
+// | |\ ___/ / __ \| Y Y \ | /\___ \\ ___/| | \/
+// |____| \___ >____ /__|_| /______//____ >\___ >__|
+// \/ \/ \/ \/ \/
+
+// TeamUser represents an team-user relation.
+type TeamUser struct {
+ ID int64
+ OrgID int64 `xorm:"INDEX"`
+ TeamID int64 `xorm:"UNIQUE(s)"`
+ UID int64 `xorm:"UNIQUE(s)"`
+}
+
+func isTeamMember(e Engine, orgID, teamID, uid int64) bool {
+ has, _ := e.Where("org_id=?", orgID).And("team_id=?", teamID).And("uid=?", uid).Get(new(TeamUser))
+ return has
+}
+
+// IsTeamMember returns true if given user is a member of team.
+func IsTeamMember(orgID, teamID, uid int64) bool {
+ return isTeamMember(x, orgID, teamID, uid)
+}
+
+func getTeamMembers(e Engine, teamID int64) (_ []*User, err error) {
+ teamUsers := make([]*TeamUser, 0, 10)
+ if err = e.Sql("SELECT `id`, `org_id`, `team_id`, `uid` FROM `team_user` WHERE team_id = ?", teamID).
+ Find(&teamUsers); err != nil {
+ return nil, fmt.Errorf("get team-users: %v", err)
+ }
+ members := make([]*User, 0, len(teamUsers))
+ for i := range teamUsers {
+ member := new(User)
+ if _, err = e.ID(teamUsers[i].UID).Get(member); err != nil {
+ return nil, fmt.Errorf("get user '%d': %v", teamUsers[i].UID, err)
+ }
+ members = append(members, member)
+ }
+ return members, nil
+}
+
+// GetTeamMembers returns all members in given team of organization.
+func GetTeamMembers(teamID int64) ([]*User, error) {
+ return getTeamMembers(x, teamID)
+}
+
+func getUserTeams(e Engine, orgID, userID int64) ([]*Team, error) {
+ teamUsers := make([]*TeamUser, 0, 5)
+ if err := e.Where("uid = ?", userID).And("org_id = ?", orgID).Find(&teamUsers); err != nil {
+ return nil, err
+ }
+
+ teamIDs := make([]int64, len(teamUsers)+1)
+ for i := range teamUsers {
+ teamIDs[i] = teamUsers[i].TeamID
+ }
+ teamIDs[len(teamUsers)] = -1
+
+ teams := make([]*Team, 0, len(teamIDs))
+ return teams, e.Where("org_id = ?", orgID).In("id", teamIDs).Find(&teams)
+}
+
+// GetUserTeams returns all teams that user belongs to in given organization.
+func GetUserTeams(orgID, userID int64) ([]*Team, error) {
+ return getUserTeams(x, orgID, userID)
+}
+
+// AddTeamMember adds new membership of given team to given organization,
+// the user will have membership to given organization automatically when needed.
+func AddTeamMember(orgID, teamID, userID int64) error {
+ if IsTeamMember(orgID, teamID, userID) {
+ return nil
+ }
+
+ if err := AddOrgUser(orgID, userID); err != nil {
+ return err
+ }
+
+ // Get team and its repositories.
+ t, err := GetTeamByID(teamID)
+ if err != nil {
+ return err
+ }
+ t.NumMembers++
+
+ if err = t.GetRepositories(); err != nil {
+ return err
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ tu := &TeamUser{
+ UID: userID,
+ OrgID: orgID,
+ TeamID: teamID,
+ }
+ if _, err = sess.Insert(tu); err != nil {
+ return err
+ } else if _, err = sess.ID(t.ID).Update(t); err != nil {
+ return err
+ }
+
+ // Give access to team repositories.
+ for _, repo := range t.Repos {
+ if err = repo.recalculateTeamAccesses(sess, 0); err != nil {
+ return err
+ }
+ }
+
+ // We make sure it exists before.
+ ou := new(OrgUser)
+ if _, err = sess.Where("uid = ?", userID).And("org_id = ?", orgID).Get(ou); err != nil {
+ return err
+ }
+ ou.NumTeams++
+ if t.IsOwnerTeam() {
+ ou.IsOwner = true
+ }
+ if _, err = sess.ID(ou.ID).AllCols().Update(ou); err != nil {
+ return err
+ }
+
+ return sess.Commit()
+}
+
+func removeTeamMember(e Engine, orgID, teamID, uid int64) error {
+ if !isTeamMember(e, orgID, teamID, uid) {
+ return nil
+ }
+
+ // Get team and its repositories.
+ t, err := getTeamByID(e, teamID)
+ if err != nil {
+ return err
+ }
+
+ // Check if the user to delete is the last member in owner team.
+ if t.IsOwnerTeam() && t.NumMembers == 1 {
+ return ErrLastOrgOwner{UID: uid}
+ }
+
+ t.NumMembers--
+
+ if err = t.getRepositories(e); err != nil {
+ return err
+ }
+
+ // Get organization.
+ org, err := getUserByID(e, orgID)
+ if err != nil {
+ return err
+ }
+
+ tu := &TeamUser{
+ UID: uid,
+ OrgID: orgID,
+ TeamID: teamID,
+ }
+ if _, err := e.Delete(tu); err != nil {
+ return err
+ } else if _, err = e.ID(t.ID).AllCols().Update(t); err != nil {
+ return err
+ }
+
+ // Delete access to team repositories.
+ for _, repo := range t.Repos {
+ if err = repo.recalculateTeamAccesses(e, 0); err != nil {
+ return err
+ }
+ }
+
+ // This must exist.
+ ou := new(OrgUser)
+ _, err = e.Where("uid = ?", uid).And("org_id = ?", org.ID).Get(ou)
+ if err != nil {
+ return err
+ }
+ ou.NumTeams--
+ if t.IsOwnerTeam() {
+ ou.IsOwner = false
+ }
+ if _, err = e.ID(ou.ID).AllCols().Update(ou); err != nil {
+ return err
+ }
+ return nil
+}
+
+// RemoveTeamMember removes member from given team of given organization.
+func RemoveTeamMember(orgID, teamID, uid int64) error {
+ sess := x.NewSession()
+ defer sess.Close()
+ if err := sess.Begin(); err != nil {
+ return err
+ }
+ if err := removeTeamMember(sess, orgID, teamID, uid); err != nil {
+ return err
+ }
+ return sess.Commit()
+}
+
+// ___________ __________
+// \__ ___/___ _____ _____\______ \ ____ ______ ____
+// | |_/ __ \\__ \ / \| _// __ \\____ \ / _ \
+// | |\ ___/ / __ \| Y Y \ | \ ___/| |_> > <_> )
+// |____| \___ >____ /__|_| /____|_ /\___ > __/ \____/
+// \/ \/ \/ \/ \/|__|
+
+// TeamRepo represents an team-repository relation.
+type TeamRepo struct {
+ ID int64
+ OrgID int64 `xorm:"INDEX"`
+ TeamID int64 `xorm:"UNIQUE(s)"`
+ RepoID int64 `xorm:"UNIQUE(s)"`
+}
+
+func hasTeamRepo(e Engine, orgID, teamID, repoID int64) bool {
+ has, _ := e.Where("org_id = ?", orgID).And("team_id = ?", teamID).And("repo_id = ?", repoID).Get(new(TeamRepo))
+ return has
+}
+
+// HasTeamRepo returns true if given team has access to the repository of the organization.
+func HasTeamRepo(orgID, teamID, repoID int64) bool {
+ return hasTeamRepo(x, orgID, teamID, repoID)
+}
+
+func addTeamRepo(e Engine, orgID, teamID, repoID int64) error {
+ _, err := e.InsertOne(&TeamRepo{
+ OrgID: orgID,
+ TeamID: teamID,
+ RepoID: repoID,
+ })
+ return err
+}
+
+// AddTeamRepo adds new repository relation to team.
+func AddTeamRepo(orgID, teamID, repoID int64) error {
+ return addTeamRepo(x, orgID, teamID, repoID)
+}
+
+func removeTeamRepo(e Engine, teamID, repoID int64) error {
+ _, err := e.Delete(&TeamRepo{
+ TeamID: teamID,
+ RepoID: repoID,
+ })
+ return err
+}
+
+// RemoveTeamRepo deletes repository relation to team.
+func RemoveTeamRepo(teamID, repoID int64) error {
+ return removeTeamRepo(x, teamID, repoID)
+}
+
+// GetTeamsHaveAccessToRepo returns all teams in an organization that have given access level to the repository.
+func GetTeamsHaveAccessToRepo(orgID, repoID int64, mode AccessMode) ([]*Team, error) {
+ teams := make([]*Team, 0, 5)
+ return teams, x.Where("team.authorize >= ?", mode).
+ Join("INNER", "team_repo", "team_repo.team_id = team.id").
+ And("team_repo.org_id = ?", orgID).
+ And("team_repo.repo_id = ?", repoID).
+ Find(&teams)
+}
diff --git a/internal/db/pull.go b/internal/db/pull.go
new file mode 100644
index 00000000..30179eb2
--- /dev/null
+++ b/internal/db/pull.go
@@ -0,0 +1,851 @@
+// 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 db
+
+import (
+ "fmt"
+ "os"
+ "path"
+ "strings"
+ "time"
+
+ "github.com/unknwon/com"
+ log "gopkg.in/clog.v1"
+ "xorm.io/xorm"
+
+ "github.com/gogs/git-module"
+ api "github.com/gogs/go-gogs-client"
+
+ "gogs.io/gogs/internal/db/errors"
+ "gogs.io/gogs/internal/process"
+ "gogs.io/gogs/internal/setting"
+ "gogs.io/gogs/internal/sync"
+)
+
+var PullRequestQueue = sync.NewUniqueQueue(setting.Repository.PullRequestQueueLength)
+
+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
+ Type PullRequestType
+ Status PullRequestStatus
+
+ IssueID int64 `xorm:"INDEX"`
+ Issue *Issue `xorm:"-" json:"-"`
+ Index int64
+
+ HeadRepoID int64
+ HeadRepo *Repository `xorm:"-" json:"-"`
+ BaseRepoID int64
+ BaseRepo *Repository `xorm:"-" json:"-"`
+ HeadUserName string
+ HeadBranch string
+ BaseBranch string
+ MergeBase string `xorm:"VARCHAR(40)"`
+
+ HasMerged bool
+ MergedCommitID string `xorm:"VARCHAR(40)"`
+ MergerID int64
+ Merger *User `xorm:"-" json:"-"`
+ Merged time.Time `xorm:"-" json:"-"`
+ MergedUnix int64
+}
+
+func (pr *PullRequest) BeforeUpdate() {
+ pr.MergedUnix = pr.Merged.Unix()
+}
+
+// Note: don't try to get Issue because will end up recursive querying.
+func (pr *PullRequest) AfterSet(colName string, _ xorm.Cell) {
+ switch colName {
+ case "merged_unix":
+ if !pr.HasMerged {
+ return
+ }
+
+ pr.Merged = time.Unix(pr.MergedUnix, 0).Local()
+ }
+}
+
+// Note: don't try to get Issue because will end up recursive querying.
+func (pr *PullRequest) loadAttributes(e Engine) (err error) {
+ if pr.HeadRepo == nil {
+ pr.HeadRepo, err = getRepositoryByID(e, pr.HeadRepoID)
+ if err != nil && !errors.IsRepoNotExist(err) {
+ return fmt.Errorf("getRepositoryByID.(HeadRepo) [%d]: %v", pr.HeadRepoID, err)
+ }
+ }
+
+ if pr.BaseRepo == nil {
+ pr.BaseRepo, err = getRepositoryByID(e, pr.BaseRepoID)
+ if err != nil {
+ return fmt.Errorf("getRepositoryByID.(BaseRepo) [%d]: %v", pr.BaseRepoID, err)
+ }
+ }
+
+ if pr.HasMerged && pr.Merger == nil {
+ pr.Merger, err = getUserByID(e, pr.MergerID)
+ if errors.IsUserNotExist(err) {
+ pr.MergerID = -1
+ pr.Merger = NewGhostUser()
+ } else if err != nil {
+ return fmt.Errorf("getUserByID [%d]: %v", pr.MergerID, err)
+ }
+ }
+
+ return nil
+}
+
+func (pr *PullRequest) LoadAttributes() error {
+ return pr.loadAttributes(x)
+}
+
+func (pr *PullRequest) LoadIssue() (err error) {
+ if pr.Issue != nil {
+ return nil
+ }
+
+ pr.Issue, err = GetIssueByID(pr.IssueID)
+ return err
+}
+
+// This method assumes following fields have been assigned with valid values:
+// Required - Issue, BaseRepo
+// Optional - HeadRepo, Merger
+func (pr *PullRequest) APIFormat() *api.PullRequest {
+ // In case of head repo has been deleted.
+ var apiHeadRepo *api.Repository
+ if pr.HeadRepo == nil {
+ apiHeadRepo = &api.Repository{
+ Name: "deleted",
+ }
+ } else {
+ apiHeadRepo = pr.HeadRepo.APIFormat(nil)
+ }
+
+ apiIssue := pr.Issue.APIFormat()
+ apiPullRequest := &api.PullRequest{
+ ID: pr.ID,
+ Index: pr.Index,
+ Poster: apiIssue.Poster,
+ Title: apiIssue.Title,
+ Body: apiIssue.Body,
+ Labels: apiIssue.Labels,
+ Milestone: apiIssue.Milestone,
+ Assignee: apiIssue.Assignee,
+ State: apiIssue.State,
+ Comments: apiIssue.Comments,
+ HeadBranch: pr.HeadBranch,
+ HeadRepo: apiHeadRepo,
+ BaseBranch: pr.BaseBranch,
+ BaseRepo: pr.BaseRepo.APIFormat(nil),
+ HTMLURL: pr.Issue.HTMLURL(),
+ HasMerged: pr.HasMerged,
+ }
+
+ if pr.Status != PULL_REQUEST_STATUS_CHECKING {
+ mergeable := pr.Status != PULL_REQUEST_STATUS_CONFLICT
+ apiPullRequest.Mergeable = &mergeable
+ }
+ if pr.HasMerged {
+ apiPullRequest.Merged = &pr.Merged
+ apiPullRequest.MergedCommitID = &pr.MergedCommitID
+ apiPullRequest.MergedBy = pr.Merger.APIFormat()
+ }
+
+ return apiPullRequest
+}
+
+// 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
+}
+
+// MergeStyle represents the approach to merge commits into base branch.
+type MergeStyle string
+
+const (
+ MERGE_STYLE_REGULAR MergeStyle = "create_merge_commit"
+ MERGE_STYLE_REBASE MergeStyle = "rebase_before_merging"
+)
+
+// Merge merges pull request to base repository.
+// FIXME: add repoWorkingPull make sure two merges does not happen at same time.
+func (pr *PullRequest) Merge(doer *User, baseGitRepo *git.Repository, mergeStyle MergeStyle, commitDescription string) (err error) {
+ defer func() {
+ go HookQueue.Add(pr.BaseRepo.ID)
+ go AddTestPullRequestTask(doer, pr.BaseRepo.ID, pr.BaseBranch, false)
+ }()
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if err = pr.Issue.changeStatus(sess, doer, pr.Issue.Repo, true); err != nil {
+ return fmt.Errorf("Issue.changeStatus: %v", err)
+ }
+
+ headRepoPath := RepoPath(pr.HeadUserName, pr.HeadRepo.Name)
+ headGitRepo, err := git.OpenRepository(headRepoPath)
+ if err != nil {
+ return fmt.Errorf("OpenRepository: %v", err)
+ }
+
+ // Create temporary directory to store temporary copy of the base repository,
+ // and clean it up when operation finished regardless of succeed or not.
+ 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))
+
+ // Clone the base repository to the defined temporary directory,
+ // and checks out to base branch directly.
+ var stderr string
+ if _, stderr, err = process.ExecTimeout(5*time.Minute,
+ fmt.Sprintf("PullRequest.Merge (git clone): %s", tmpBasePath),
+ "git", "clone", "-b", pr.BaseBranch, baseGitRepo.Path, tmpBasePath); err != nil {
+ return fmt.Errorf("git clone: %s", stderr)
+ }
+
+ // Add remote which points to the head repository.
+ 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)
+ }
+
+ // Fetch information from head repository to the temporary copy.
+ 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)
+ }
+
+ remoteHeadBranch := "head_repo/" + pr.HeadBranch
+
+ // Check if merge style is allowed, reset to default style if not
+ if mergeStyle == MERGE_STYLE_REBASE && !pr.BaseRepo.PullsAllowRebase {
+ mergeStyle = MERGE_STYLE_REGULAR
+ }
+
+ switch mergeStyle {
+ case MERGE_STYLE_REGULAR: // Create merge commit
+
+ // Merge changes from head branch.
+ if _, stderr, err = process.ExecDir(-1, tmpBasePath,
+ fmt.Sprintf("PullRequest.Merge (git merge --no-ff --no-commit): %s", tmpBasePath),
+ "git", "merge", "--no-ff", "--no-commit", remoteHeadBranch); err != nil {
+ return fmt.Errorf("git merge --no-ff --no-commit [%s]: %v - %s", tmpBasePath, err, stderr)
+ }
+
+ // Create a merge commit for the base branch.
+ sig := doer.NewGitSig()
+ if _, stderr, err = process.ExecDir(-1, tmpBasePath,
+ fmt.Sprintf("PullRequest.Merge (git merge): %s", tmpBasePath),
+ "git", "commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email),
+ "-m", fmt.Sprintf("Merge branch '%s' of %s/%s into %s", pr.HeadBranch, pr.HeadUserName, pr.HeadRepo.Name, pr.BaseBranch),
+ "-m", commitDescription); err != nil {
+ return fmt.Errorf("git commit [%s]: %v - %s", tmpBasePath, err, stderr)
+ }
+
+ case MERGE_STYLE_REBASE: // Rebase before merging
+
+ // Rebase head branch based on base branch, this creates a non-branch commit state.
+ if _, stderr, err = process.ExecDir(-1, tmpBasePath,
+ fmt.Sprintf("PullRequest.Merge (git rebase): %s", tmpBasePath),
+ "git", "rebase", "--quiet", pr.BaseBranch, remoteHeadBranch); err != nil {
+ return fmt.Errorf("git rebase [%s on %s]: %s", remoteHeadBranch, pr.BaseBranch, stderr)
+ }
+
+ // Name non-branch commit state to a new temporary branch in order to save changes.
+ tmpBranch := com.ToStr(time.Now().UnixNano(), 10)
+ if _, stderr, err = process.ExecDir(-1, tmpBasePath,
+ fmt.Sprintf("PullRequest.Merge (git checkout): %s", tmpBasePath),
+ "git", "checkout", "-b", tmpBranch); err != nil {
+ return fmt.Errorf("git checkout '%s': %s", tmpBranch, stderr)
+ }
+
+ // Check out the base branch to be operated on.
+ 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': %s", pr.BaseBranch, stderr)
+ }
+
+ // Merge changes from temporary branch to the base branch.
+ if _, stderr, err = process.ExecDir(-1, tmpBasePath,
+ fmt.Sprintf("PullRequest.Merge (git merge): %s", tmpBasePath),
+ "git", "merge", tmpBranch); err != nil {
+ return fmt.Errorf("git merge [%s]: %v - %s", tmpBasePath, err, stderr)
+ }
+
+ default:
+ return fmt.Errorf("unknown merge style: %s", mergeStyle)
+ }
+
+ // Push changes on base branch 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)
+ }
+
+ pr.MergedCommitID, err = headGitRepo.GetBranchCommitID(pr.HeadBranch)
+ if err != nil {
+ return fmt.Errorf("GetBranchCommit: %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)
+ }
+
+ if err = sess.Commit(); err != nil {
+ return fmt.Errorf("Commit: %v", err)
+ }
+
+ if err = MergePullRequestAction(doer, pr.Issue.Repo, pr.Issue); err != nil {
+ log.Error(2, "MergePullRequestAction [%d]: %v", pr.ID, err)
+ }
+
+ // Reload pull request information.
+ if err = pr.LoadAttributes(); err != nil {
+ log.Error(2, "LoadAttributes: %v", err)
+ return nil
+ }
+ if err = PrepareWebhooks(pr.Issue.Repo, HOOK_EVENT_PULL_REQUEST, &api.PullRequestPayload{
+ Action: api.HOOK_ISSUE_CLOSED,
+ Index: pr.Index,
+ PullRequest: pr.APIFormat(),
+ Repository: pr.Issue.Repo.APIFormat(nil),
+ Sender: doer.APIFormat(),
+ }); err != nil {
+ log.Error(2, "PrepareWebhooks: %v", err)
+ return nil
+ }
+
+ l, err := headGitRepo.CommitsBetweenIDs(pr.MergedCommitID, pr.MergeBase)
+ if err != nil {
+ log.Error(2, "CommitsBetweenIDs: %v", err)
+ return nil
+ }
+
+ // It is possible that head branch is not fully sync with base branch for merge commits,
+ // so we need to get latest head commit and append merge commit manully
+ // to avoid strange diff commits produced.
+ mergeCommit, err := baseGitRepo.GetBranchCommit(pr.BaseBranch)
+ if err != nil {
+ log.Error(2, "GetBranchCommit: %v", err)
+ return nil
+ }
+ if mergeStyle == MERGE_STYLE_REGULAR {
+ l.PushFront(mergeCommit)
+ }
+
+ commits, err := ListToPushCommits(l).ToApiPayloadCommits(pr.BaseRepo.RepoPath(), pr.BaseRepo.HTMLURL())
+ if err != nil {
+ log.Error(2, "ToApiPayloadCommits: %v", err)
+ return nil
+ }
+
+ p := &api.PushPayload{
+ Ref: git.BRANCH_PREFIX + pr.BaseBranch,
+ Before: pr.MergeBase,
+ After: mergeCommit.ID.String(),
+ CompareURL: setting.AppURL + pr.BaseRepo.ComposeCompareURL(pr.MergeBase, pr.MergedCommitID),
+ Commits: commits,
+ Repo: pr.BaseRepo.APIFormat(nil),
+ Pusher: pr.HeadRepo.MustOwner().APIFormat(),
+ Sender: doer.APIFormat(),
+ }
+ if err = PrepareWebhooks(pr.BaseRepo, HOOK_EVENT_PUSH, p); err != nil {
+ log.Error(2, "PrepareWebhooks: %v", err)
+ return nil
+ }
+ return nil
+}
+
+// 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
+ }
+
+ repoWorkingPool.CheckIn(com.ToStr(pr.BaseRepoID))
+ defer repoWorkingPool.CheckOut(com.ToStr(pr.BaseRepoID))
+
+ log.Trace("PullRequest[%d].testPatch (patchPath): %s", pr.ID, patchPath)
+
+ if err := pr.BaseRepo.UpdateLocalCopyBranch(pr.BaseBranch); err != nil {
+ return fmt.Errorf("UpdateLocalCopy [%d]: %v", pr.BaseRepoID, err)
+ }
+
+ args := []string{"apply", "--check"}
+ if pr.BaseRepo.PullsIgnoreWhitespace {
+ args = append(args, "--ignore-whitespace")
+ }
+ args = append(args, patchPath)
+
+ 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", args...)
+ if err != nil {
+ log.Trace("PullRequest[%d].testPatch (apply): has conflit\n%s", pr.ID, stderr)
+ pr.Status = PULL_REQUEST_STATUS_CONFLICT
+ return nil
+ }
+ 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 sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if err = newIssue(sess, NewIssueOptions{
+ Repo: repo,
+ Issue: pull,
+ LableIDs: labelIDs,
+ Attachments: uuids,
+ IsPull: true,
+ }); err != nil {
+ return fmt.Errorf("newIssue: %v", 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)
+ }
+ // No conflict appears after test means mergeable.
+ 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)
+ }
+
+ if err = sess.Commit(); err != nil {
+ return fmt.Errorf("Commit: %v", err)
+ }
+
+ if err = NotifyWatchers(&Action{
+ ActUserID: pull.Poster.ID,
+ ActUserName: pull.Poster.Name,
+ OpType: ACTION_CREATE_PULL_REQUEST,
+ Content: fmt.Sprintf("%d|%s", pull.Index, pull.Title),
+ RepoID: repo.ID,
+ RepoUserName: repo.Owner.Name,
+ RepoName: repo.Name,
+ IsPrivate: repo.IsPrivate,
+ }); err != nil {
+ log.Error(2, "NotifyWatchers: %v", err)
+ }
+ if err = pull.MailParticipants(); err != nil {
+ log.Error(2, "MailParticipants: %v", err)
+ }
+
+ pr.Issue = pull
+ pull.PullRequest = pr
+ if err = PrepareWebhooks(repo, HOOK_EVENT_PULL_REQUEST, &api.PullRequestPayload{
+ Action: api.HOOK_ISSUE_OPENED,
+ Index: pull.Index,
+ PullRequest: pr.APIFormat(),
+ Repository: repo.APIFormat(nil),
+ Sender: pull.Poster.APIFormat(),
+ }); err != nil {
+ log.Error(2, "PrepareWebhooks: %v", err)
+ }
+
+ return nil
+}
+
+// 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)
+}
+
+func getPullRequestByID(e Engine, id int64) (*PullRequest, error) {
+ pr := new(PullRequest)
+ has, err := e.ID(id).Get(pr)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, ErrPullRequestNotExist{id, 0, 0, 0, "", ""}
+ }
+ return pr, pr.loadAttributes(e)
+}
+
+// GetPullRequestByID returns a pull request by given ID.
+func GetPullRequestByID(id int64) (*PullRequest, error) {
+ return getPullRequestByID(x, id)
+}
+
+func getPullRequestByIssueID(e Engine, issueID int64) (*PullRequest, error) {
+ pr := &PullRequest{
+ IssueID: issueID,
+ }
+ has, err := e.Get(pr)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, ErrPullRequestNotExist{0, issueID, 0, 0, "", ""}
+ }
+ return pr, pr.loadAttributes(e)
+}
+
+// GetPullRequestByIssueID returns pull request by given issue ID.
+func GetPullRequestByIssueID(issueID int64) (*PullRequest, error) {
+ return getPullRequestByIssueID(x, issueID)
+}
+
+// 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
+}
+
+// UpdatePatch generates and saves a new patch.
+func (pr *PullRequest) UpdatePatch() (err error) {
+ if pr.HeadRepo == nil {
+ log.Trace("PullRequest[%d].UpdatePatch: ignored cruppted data", pr.ID)
+ return nil
+ }
+
+ headGitRepo, err := git.OpenRepository(pr.HeadRepo.RepoPath())
+ 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.MustOwner().Name, pr.BaseRepo.Name), true); 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
+}
+
+// PushToBaseRepo pushes commits from branches of head repository to
+// corresponding branches of base repository.
+// FIXME: Only push branches that are actually updates?
+func (pr *PullRequest) PushToBaseRepo() (err error) {
+ log.Trace("PushToBaseRepo[%d]: pushing commits to base repo 'refs/pull/%d/head'", pr.BaseRepoID, pr.Index)
+
+ headRepoPath := pr.HeadRepo.RepoPath()
+ headGitRepo, err := git.OpenRepository(headRepoPath)
+ if err != nil {
+ return fmt.Errorf("OpenRepository: %v", err)
+ }
+
+ tmpRemoteName := fmt.Sprintf("tmp-pull-%d", pr.ID)
+ if err = headGitRepo.AddRemote(tmpRemoteName, pr.BaseRepo.RepoPath(), false); err != nil {
+ return fmt.Errorf("headGitRepo.AddRemote: %v", err)
+ }
+ // Make sure to remove the remote even if the push fails
+ defer headGitRepo.RemoveRemote(tmpRemoteName)
+
+ headFile := fmt.Sprintf("refs/pull/%d/head", pr.Index)
+
+ // Remove head in case there is a conflict.
+ os.Remove(path.Join(pr.BaseRepo.RepoPath(), headFile))
+
+ if err = git.Push(headRepoPath, tmpRemoteName, fmt.Sprintf("%s:%s", pr.HeadBranch, headFile)); err != nil {
+ return fmt.Errorf("Push: %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(3, "AddToTaskQueue.UpdateCols[%d].(add to queue): %v", pr.ID, err)
+ }
+ })
+}
+
+type PullRequestList []*PullRequest
+
+func (prs PullRequestList) loadAttributes(e Engine) (err error) {
+ if len(prs) == 0 {
+ return nil
+ }
+
+ // Load issues
+ set := make(map[int64]*Issue)
+ for i := range prs {
+ set[prs[i].IssueID] = nil
+ }
+ issueIDs := make([]int64, 0, len(prs))
+ for issueID := range set {
+ issueIDs = append(issueIDs, issueID)
+ }
+ issues := make([]*Issue, 0, len(issueIDs))
+ if err = e.Where("id > 0").In("id", issueIDs).Find(&issues); err != nil {
+ return fmt.Errorf("find issues: %v", err)
+ }
+ for i := range issues {
+ set[issues[i].ID] = issues[i]
+ }
+ for i := range prs {
+ prs[i].Issue = set[prs[i].IssueID]
+ }
+
+ // Load attributes
+ for i := range prs {
+ if err = prs[i].loadAttributes(e); err != nil {
+ return fmt.Errorf("loadAttributes [%d]: %v", prs[i].ID, err)
+ }
+ }
+
+ return nil
+}
+
+func (prs PullRequestList) LoadAttributes() error {
+ return prs.loadAttributes(x)
+}
+
+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
+ } else if err := pr.PushToBaseRepo(); err != nil {
+ log.Error(4, "PushToBaseRepo: %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(doer *User, repoID int64, branch string, isSync bool) {
+ 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(2, "Find pull requests [head_repo_id: %d, head_branch: %s]: %v", repoID, branch, err)
+ return
+ }
+
+ if isSync {
+ if err = PullRequestList(prs).LoadAttributes(); err != nil {
+ log.Error(2, "PullRequestList.LoadAttributes: %v", err)
+ }
+
+ if err == nil {
+ for _, pr := range prs {
+ pr.Issue.PullRequest = pr
+ if err = pr.Issue.LoadAttributes(); err != nil {
+ log.Error(2, "LoadAttributes: %v", err)
+ continue
+ }
+ if err = PrepareWebhooks(pr.Issue.Repo, HOOK_EVENT_PULL_REQUEST, &api.PullRequestPayload{
+ Action: api.HOOK_ISSUE_SYNCHRONIZED,
+ Index: pr.Issue.Index,
+ PullRequest: pr.Issue.PullRequest.APIFormat(),
+ Repository: pr.Issue.Repo.APIFormat(nil),
+ Sender: doer.APIFormat(),
+ }); err != nil {
+ log.Error(2, "PrepareWebhooks [pull_id: %v]: %v", pr.ID, err)
+ continue
+ }
+ }
+ }
+ }
+
+ 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(2, "Find pull requests [base_repo_id: %d, base_branch: %s]: %v", repoID, branch, err)
+ return
+ }
+ for _, pr := range prs {
+ pr.AddToTaskQueue()
+ }
+}
+
+func ChangeUsernameInPullRequests(oldUserName, newUserName string) error {
+ pr := PullRequest{
+ HeadUserName: strings.ToLower(newUserName),
+ }
+ _, err := x.Cols("head_user_name").Where("head_user_name = ?", strings.ToLower(oldUserName)).Update(pr)
+ return err
+}
+
+// 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.LoadAttributes(); err != nil {
+ log.Error(3, "LoadAttributes: %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[%s]: %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/internal/db/release.go b/internal/db/release.go
new file mode 100644
index 00000000..7712a0af
--- /dev/null
+++ b/internal/db/release.go
@@ -0,0 +1,352 @@
+// Copyright 2014 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 db
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+ "time"
+
+ log "gopkg.in/clog.v1"
+ "xorm.io/xorm"
+
+ "github.com/gogs/git-module"
+ api "github.com/gogs/go-gogs-client"
+
+ "gogs.io/gogs/internal/db/errors"
+ "gogs.io/gogs/internal/process"
+)
+
+// Release represents a release of repository.
+type Release struct {
+ ID int64
+ RepoID int64
+ Repo *Repository `xorm:"-" json:"-"`
+ PublisherID int64
+ Publisher *User `xorm:"-" json:"-"`
+ TagName string
+ LowerTagName string
+ Target string
+ Title string
+ Sha1 string `xorm:"VARCHAR(40)"`
+ NumCommits int64
+ NumCommitsBehind int64 `xorm:"-" json:"-"`
+ Note string `xorm:"TEXT"`
+ IsDraft bool `xorm:"NOT NULL DEFAULT false"`
+ IsPrerelease bool
+
+ Created time.Time `xorm:"-" json:"-"`
+ CreatedUnix int64
+
+ Attachments []*Attachment `xorm:"-" json:"-"`
+}
+
+func (r *Release) BeforeInsert() {
+ if r.CreatedUnix == 0 {
+ r.CreatedUnix = time.Now().Unix()
+ }
+}
+
+func (r *Release) AfterSet(colName string, _ xorm.Cell) {
+ switch colName {
+ case "created_unix":
+ r.Created = time.Unix(r.CreatedUnix, 0).Local()
+ }
+}
+
+func (r *Release) loadAttributes(e Engine) (err error) {
+ if r.Repo == nil {
+ r.Repo, err = getRepositoryByID(e, r.RepoID)
+ if err != nil {
+ return fmt.Errorf("getRepositoryByID [repo_id: %d]: %v", r.RepoID, err)
+ }
+ }
+
+ if r.Publisher == nil {
+ r.Publisher, err = getUserByID(e, r.PublisherID)
+ if err != nil {
+ if errors.IsUserNotExist(err) {
+ r.PublisherID = -1
+ r.Publisher = NewGhostUser()
+ } else {
+ return fmt.Errorf("getUserByID.(Publisher) [publisher_id: %d]: %v", r.PublisherID, err)
+ }
+ }
+ }
+
+ if r.Attachments == nil {
+ r.Attachments, err = getAttachmentsByReleaseID(e, r.ID)
+ if err != nil {
+ return fmt.Errorf("getAttachmentsByReleaseID [%d]: %v", r.ID, err)
+ }
+ }
+
+ return nil
+}
+
+func (r *Release) LoadAttributes() error {
+ return r.loadAttributes(x)
+}
+
+// This method assumes some fields assigned with values:
+// Required - Publisher
+func (r *Release) APIFormat() *api.Release {
+ return &api.Release{
+ ID: r.ID,
+ TagName: r.TagName,
+ TargetCommitish: r.Target,
+ Name: r.Title,
+ Body: r.Note,
+ Draft: r.IsDraft,
+ Prerelease: r.IsPrerelease,
+ Author: r.Publisher.APIFormat(),
+ Created: r.Created,
+ }
+}
+
+// IsReleaseExist returns true if release with given tag name already exists.
+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)})
+}
+
+func createTag(gitRepo *git.Repository, r *Release) error {
+ // Only actual create when publish.
+ if !r.IsDraft {
+ if !gitRepo.IsTagExist(r.TagName) {
+ commit, err := gitRepo.GetBranchCommit(r.Target)
+ if err != nil {
+ return fmt.Errorf("GetBranchCommit: %v", err)
+ }
+
+ // Trim '--' prefix to prevent command line argument vulnerability.
+ r.TagName = strings.TrimPrefix(r.TagName, "--")
+ if err = gitRepo.CreateTag(r.TagName, commit.ID.String()); err != nil {
+ if strings.Contains(err.Error(), "is not a valid tag name") {
+ return ErrInvalidTagName{r.TagName}
+ }
+ return err
+ }
+ } else {
+ commit, err := gitRepo.GetTagCommit(r.TagName)
+ if err != nil {
+ return fmt.Errorf("GetTagCommit: %v", err)
+ }
+
+ r.Sha1 = commit.ID.String()
+ r.NumCommits, err = commit.CommitsCount()
+ if err != nil {
+ return fmt.Errorf("CommitsCount: %v", err)
+ }
+ }
+ }
+ return nil
+}
+
+func (r *Release) preparePublishWebhooks() {
+ if err := PrepareWebhooks(r.Repo, HOOK_EVENT_RELEASE, &api.ReleasePayload{
+ Action: api.HOOK_RELEASE_PUBLISHED,
+ Release: r.APIFormat(),
+ Repository: r.Repo.APIFormat(nil),
+ Sender: r.Publisher.APIFormat(),
+ }); err != nil {
+ log.Error(2, "PrepareWebhooks: %v", err)
+ }
+}
+
+// NewRelease creates a new release with attachments for repository.
+func NewRelease(gitRepo *git.Repository, r *Release, uuids []string) error {
+ isExist, err := IsReleaseExist(r.RepoID, r.TagName)
+ if err != nil {
+ return err
+ } else if isExist {
+ return ErrReleaseAlreadyExist{r.TagName}
+ }
+
+ if err = createTag(gitRepo, r); err != nil {
+ return err
+ }
+ r.LowerTagName = strings.ToLower(r.TagName)
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if _, err = sess.Insert(r); err != nil {
+ return fmt.Errorf("Insert: %v", err)
+ }
+
+ if len(uuids) > 0 {
+ if _, err = sess.In("uuid", uuids).Cols("release_id").Update(&Attachment{ReleaseID: r.ID}); err != nil {
+ return fmt.Errorf("link attachments: %v", err)
+ }
+ }
+
+ if err = sess.Commit(); err != nil {
+ return fmt.Errorf("Commit: %v", err)
+ }
+
+ // Only send webhook when actually published, skip drafts
+ if r.IsDraft {
+ return nil
+ }
+ r, err = GetReleaseByID(r.ID)
+ if err != nil {
+ return fmt.Errorf("GetReleaseByID: %v", err)
+ }
+ r.preparePublishWebhooks()
+ return nil
+}
+
+// GetRelease returns release by given ID.
+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{0, tagName}
+ }
+
+ r := &Release{RepoID: repoID, LowerTagName: strings.ToLower(tagName)}
+ if _, err = x.Get(r); err != nil {
+ return nil, fmt.Errorf("Get: %v", err)
+ }
+
+ return r, r.LoadAttributes()
+}
+
+// GetReleaseByID returns release with given ID.
+func GetReleaseByID(id int64) (*Release, error) {
+ r := new(Release)
+ has, err := x.Id(id).Get(r)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, ErrReleaseNotExist{id, ""}
+ }
+
+ return r, r.LoadAttributes()
+}
+
+// GetPublishedReleasesByRepoID returns a list of published releases of repository.
+// If matches is not empty, only published releases in matches will be returned.
+// In any case, drafts won't be returned by this function.
+func GetPublishedReleasesByRepoID(repoID int64, matches ...string) ([]*Release, error) {
+ sess := x.Where("repo_id = ?", repoID).And("is_draft = ?", false).Desc("created_unix")
+ if len(matches) > 0 {
+ sess.In("tag_name", matches)
+ }
+ releases := make([]*Release, 0, 5)
+ return releases, sess.Find(&releases, new(Release))
+}
+
+// GetDraftReleasesByRepoID returns all draft releases of repository.
+func GetDraftReleasesByRepoID(repoID int64) ([]*Release, error) {
+ releases := make([]*Release, 0)
+ return releases, x.Where("repo_id = ?", repoID).And("is_draft = ?", true).Find(&releases)
+}
+
+type ReleaseSorter struct {
+ releases []*Release
+}
+
+func (rs *ReleaseSorter) Len() int {
+ return len(rs.releases)
+}
+
+func (rs *ReleaseSorter) Less(i, j int) bool {
+ diffNum := rs.releases[i].NumCommits - rs.releases[j].NumCommits
+ if diffNum != 0 {
+ return diffNum > 0
+ }
+ return rs.releases[i].Created.After(rs.releases[j].Created)
+}
+
+func (rs *ReleaseSorter) Swap(i, j int) {
+ rs.releases[i], rs.releases[j] = rs.releases[j], rs.releases[i]
+}
+
+// SortReleases sorts releases by number of commits and created time.
+func SortReleases(rels []*Release) {
+ sorter := &ReleaseSorter{releases: rels}
+ sort.Sort(sorter)
+}
+
+// UpdateRelease updates information of a release.
+func UpdateRelease(doer *User, gitRepo *git.Repository, r *Release, isPublish bool, uuids []string) (err error) {
+ if err = createTag(gitRepo, r); err != nil {
+ return fmt.Errorf("createTag: %v", err)
+ }
+
+ r.PublisherID = doer.ID
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+ if _, err = sess.ID(r.ID).AllCols().Update(r); err != nil {
+ return fmt.Errorf("Update: %v", err)
+ }
+
+ // Unlink all current attachments and link back later if still valid
+ if _, err = sess.Exec("UPDATE attachment SET release_id = 0 WHERE release_id = ?", r.ID); err != nil {
+ return fmt.Errorf("unlink current attachments: %v", err)
+ }
+
+ if len(uuids) > 0 {
+ if _, err = sess.In("uuid", uuids).Cols("release_id").Update(&Attachment{ReleaseID: r.ID}); err != nil {
+ return fmt.Errorf("link attachments: %v", err)
+ }
+ }
+
+ if err = sess.Commit(); err != nil {
+ return fmt.Errorf("Commit: %v", err)
+ }
+
+ if !isPublish {
+ return nil
+ }
+ r.Publisher = doer
+ r.preparePublishWebhooks()
+ return nil
+}
+
+// DeleteReleaseOfRepoByID deletes a release and corresponding Git tag by given ID.
+func DeleteReleaseOfRepoByID(repoID, id int64) error {
+ rel, err := GetReleaseByID(id)
+ if err != nil {
+ return fmt.Errorf("GetReleaseByID: %v", err)
+ }
+
+ // Mark sure the delete operation againsts same repository.
+ if repoID != rel.RepoID {
+ return nil
+ }
+
+ repo, err := GetRepositoryByID(rel.RepoID)
+ if err != nil {
+ return fmt.Errorf("GetRepositoryByID: %v", err)
+ }
+
+ _, stderr, err := process.ExecDir(-1, repo.RepoPath(),
+ fmt.Sprintf("DeleteReleaseByID (git tag -d): %d", rel.ID),
+ "git", "tag", "-d", rel.TagName)
+ if err != nil && !strings.Contains(stderr, "not found") {
+ return fmt.Errorf("git tag -d: %v - %s", err, stderr)
+ }
+
+ if _, err = x.Id(rel.ID).Delete(new(Release)); err != nil {
+ return fmt.Errorf("Delete: %v", err)
+ }
+
+ return nil
+}
diff --git a/internal/db/repo.go b/internal/db/repo.go
new file mode 100644
index 00000000..8e839ef6
--- /dev/null
+++ b/internal/db/repo.go
@@ -0,0 +1,2458 @@
+// Copyright 2014 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 db
+
+import (
+ "bytes"
+ "fmt"
+ "image"
+ _ "image/jpeg"
+ "image/png"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path"
+ "path/filepath"
+ "sort"
+ "strings"
+ "time"
+
+ "github.com/mcuadros/go-version"
+ "github.com/nfnt/resize"
+ "github.com/unknwon/cae/zip"
+ "github.com/unknwon/com"
+ log "gopkg.in/clog.v1"
+ "gopkg.in/ini.v1"
+ "xorm.io/xorm"
+
+ "github.com/gogs/git-module"
+ api "github.com/gogs/go-gogs-client"
+
+ "gogs.io/gogs/internal/avatar"
+ "gogs.io/gogs/internal/bindata"
+ "gogs.io/gogs/internal/db/errors"
+ "gogs.io/gogs/internal/markup"
+ "gogs.io/gogs/internal/process"
+ "gogs.io/gogs/internal/setting"
+ "gogs.io/gogs/internal/sync"
+)
+
+// REPO_AVATAR_URL_PREFIX is used to identify a URL is to access repository avatar.
+const REPO_AVATAR_URL_PREFIX = "repo-avatars"
+
+var repoWorkingPool = sync.NewExclusivePool()
+
+var (
+ Gitignores, Licenses, Readmes, LabelTemplates []string
+
+ // Maximum items per page in forks, watchers and stars of a repo
+ ItemsPerPage = 40
+)
+
+func LoadRepoConfig() {
+ // Load .gitignore and license files and readme templates.
+ types := []string{"gitignore", "license", "readme", "label"}
+ typeFiles := make([][]string, 4)
+ for i, t := range types {
+ files, err := bindata.AssetDir("conf/" + t)
+ if err != nil {
+ log.Fatal(4, "Fail to get %s files: %v", t, err)
+ }
+ customPath := path.Join(setting.CustomPath, "conf", t)
+ if com.IsDir(customPath) {
+ customFiles, err := com.StatDir(customPath)
+ if err != nil {
+ log.Fatal(4, "Fail to get custom %s files: %v", t, err)
+ }
+
+ for _, f := range customFiles {
+ if !com.IsSliceContainsStr(files, f) {
+ files = append(files, f)
+ }
+ }
+ }
+ typeFiles[i] = files
+ }
+
+ Gitignores = typeFiles[0]
+ Licenses = typeFiles[1]
+ Readmes = typeFiles[2]
+ LabelTemplates = typeFiles[3]
+ sort.Strings(Gitignores)
+ sort.Strings(Licenses)
+ sort.Strings(Readmes)
+ sort.Strings(LabelTemplates)
+
+ // Filter out invalid names and promote preferred licenses.
+ sortedLicenses := make([]string, 0, len(Licenses))
+ for _, name := range setting.Repository.PreferredLicenses {
+ if com.IsSliceContainsStr(Licenses, name) {
+ sortedLicenses = append(sortedLicenses, name)
+ }
+ }
+ for _, name := range Licenses {
+ if !com.IsSliceContainsStr(setting.Repository.PreferredLicenses, name) {
+ sortedLicenses = append(sortedLicenses, name)
+ }
+ }
+ Licenses = sortedLicenses
+}
+
+func NewRepoContext() {
+ zip.Verbose = false
+
+ // Check Git installation.
+ if _, err := exec.LookPath("git"); err != nil {
+ log.Fatal(4, "Fail to test 'git' command: %v (forgotten install?)", err)
+ }
+
+ // Check Git version.
+ var err error
+ setting.Git.Version, err = git.BinVersion()
+ if err != nil {
+ log.Fatal(4, "Fail to get Git version: %v", err)
+ }
+
+ log.Info("Git Version: %s", setting.Git.Version)
+ if version.Compare("1.7.1", setting.Git.Version, ">") {
+ log.Fatal(4, "Gogs requires Git version greater or equal to 1.7.1")
+ }
+ git.HookDir = "custom_hooks"
+ git.HookSampleDir = "hooks"
+ git.DefaultCommitsPageSize = setting.UI.User.CommitsPagingNum
+
+ // 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"} {
+ if stdout, stderr, err := process.Exec("NewRepoContext(get setting)", "git", "config", "--get", configKey); err != nil || strings.TrimSpace(stdout) == "" {
+ // ExitError indicates this config is not set
+ if _, ok := err.(*exec.ExitError); ok || strings.TrimSpace(stdout) == "" {
+ if _, stderr, gerr := process.Exec("NewRepoContext(set "+configKey+")", "git", "config", "--global", configKey, defaultValue); gerr != nil {
+ log.Fatal(4, "Fail to set git %s(%s): %s", configKey, gerr, stderr)
+ }
+ log.Info("Git config %s set to %s", configKey, defaultValue)
+ } else {
+ log.Fatal(4, "Fail to get git %s(%s): %s", configKey, err, stderr)
+ }
+ }
+ }
+
+ // Set git some configurations.
+ if _, stderr, err := process.Exec("NewRepoContext(git config --global core.quotepath false)",
+ "git", "config", "--global", "core.quotepath", "false"); err != nil {
+ log.Fatal(4, "Fail to execute 'git config --global core.quotepath false': %s", stderr)
+ }
+
+ RemoveAllWithNotice("Clean up repository temporary data", filepath.Join(setting.AppDataPath, "tmp"))
+}
+
+// Repository contains information of a repository.
+type Repository struct {
+ ID int64
+ OwnerID int64 `xorm:"UNIQUE(s)"`
+ Owner *User `xorm:"-" json:"-"`
+ LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"`
+ Name string `xorm:"INDEX NOT NULL"`
+ Description string `xorm:"VARCHAR(512)"`
+ Website string
+ DefaultBranch string
+ Size int64 `xorm:"NOT NULL DEFAULT 0"`
+ UseCustomAvatar bool
+
+ // Counters
+ NumWatches int
+ NumStars int
+ NumForks int
+ NumIssues int
+ NumClosedIssues int
+ NumOpenIssues int `xorm:"-" json:"-"`
+ NumPulls int
+ NumClosedPulls int
+ NumOpenPulls int `xorm:"-" json:"-"`
+ NumMilestones int `xorm:"NOT NULL DEFAULT 0"`
+ NumClosedMilestones int `xorm:"NOT NULL DEFAULT 0"`
+ NumOpenMilestones int `xorm:"-" json:"-"`
+ NumTags int `xorm:"-" json:"-"`
+
+ IsPrivate bool
+ IsBare bool
+
+ IsMirror bool
+ *Mirror `xorm:"-" json:"-"`
+
+ // Advanced settings
+ EnableWiki bool `xorm:"NOT NULL DEFAULT true"`
+ AllowPublicWiki bool
+ EnableExternalWiki bool
+ ExternalWikiURL string
+ EnableIssues bool `xorm:"NOT NULL DEFAULT true"`
+ AllowPublicIssues bool
+ EnableExternalTracker bool
+ ExternalTrackerURL string
+ ExternalTrackerFormat string
+ ExternalTrackerStyle string
+ ExternalMetas map[string]string `xorm:"-" json:"-"`
+ EnablePulls bool `xorm:"NOT NULL DEFAULT true"`
+ PullsIgnoreWhitespace bool `xorm:"NOT NULL DEFAULT false"`
+ PullsAllowRebase bool `xorm:"NOT NULL DEFAULT false"`
+
+ IsFork bool `xorm:"NOT NULL DEFAULT false"`
+ ForkID int64
+ BaseRepo *Repository `xorm:"-" json:"-"`
+
+ Created time.Time `xorm:"-" json:"-"`
+ CreatedUnix int64
+ Updated time.Time `xorm:"-" json:"-"`
+ UpdatedUnix int64
+}
+
+func (repo *Repository) BeforeInsert() {
+ repo.CreatedUnix = time.Now().Unix()
+ repo.UpdatedUnix = repo.CreatedUnix
+}
+
+func (repo *Repository) BeforeUpdate() {
+ repo.UpdatedUnix = time.Now().Unix()
+}
+
+func (repo *Repository) AfterSet(colName string, _ xorm.Cell) {
+ switch colName {
+ case "default_branch":
+ // FIXME: use db migration to solve all at once.
+ if len(repo.DefaultBranch) == 0 {
+ repo.DefaultBranch = "master"
+ }
+ case "num_closed_issues":
+ repo.NumOpenIssues = repo.NumIssues - repo.NumClosedIssues
+ case "num_closed_pulls":
+ repo.NumOpenPulls = repo.NumPulls - repo.NumClosedPulls
+ case "num_closed_milestones":
+ repo.NumOpenMilestones = repo.NumMilestones - repo.NumClosedMilestones
+ case "external_tracker_style":
+ if len(repo.ExternalTrackerStyle) == 0 {
+ repo.ExternalTrackerStyle = markup.ISSUE_NAME_STYLE_NUMERIC
+ }
+ case "created_unix":
+ repo.Created = time.Unix(repo.CreatedUnix, 0).Local()
+ case "updated_unix":
+ repo.Updated = time.Unix(repo.UpdatedUnix, 0)
+ }
+}
+
+func (repo *Repository) loadAttributes(e Engine) (err error) {
+ if repo.Owner == nil {
+ repo.Owner, err = getUserByID(e, repo.OwnerID)
+ if err != nil {
+ return fmt.Errorf("getUserByID [%d]: %v", repo.OwnerID, err)
+ }
+ }
+
+ if repo.IsFork && repo.BaseRepo == nil {
+ repo.BaseRepo, err = getRepositoryByID(e, repo.ForkID)
+ if err != nil {
+ if errors.IsRepoNotExist(err) {
+ repo.IsFork = false
+ repo.ForkID = 0
+ } else {
+ return fmt.Errorf("getRepositoryByID [%d]: %v", repo.ForkID, err)
+ }
+ }
+ }
+
+ return nil
+}
+
+func (repo *Repository) LoadAttributes() error {
+ return repo.loadAttributes(x)
+}
+
+// IsPartialPublic returns true if repository is public or allow public access to wiki or issues.
+func (repo *Repository) IsPartialPublic() bool {
+ return !repo.IsPrivate || repo.AllowPublicWiki || repo.AllowPublicIssues
+}
+
+func (repo *Repository) CanGuestViewWiki() bool {
+ return repo.EnableWiki && !repo.EnableExternalWiki && repo.AllowPublicWiki
+}
+
+func (repo *Repository) CanGuestViewIssues() bool {
+ return repo.EnableIssues && !repo.EnableExternalTracker && repo.AllowPublicIssues
+}
+
+// MustOwner always returns a valid *User object to avoid conceptually impossible error handling.
+// It creates a fake object that contains error deftail when error occurs.
+func (repo *Repository) MustOwner() *User {
+ return repo.mustOwner(x)
+}
+
+func (repo *Repository) FullName() string {
+ return repo.MustOwner().Name + "/" + repo.Name
+}
+
+func (repo *Repository) HTMLURL() string {
+ return setting.AppURL + repo.FullName()
+}
+
+// CustomAvatarPath returns repository custom avatar file path.
+func (repo *Repository) CustomAvatarPath() string {
+ return filepath.Join(setting.RepositoryAvatarUploadPath, com.ToStr(repo.ID))
+}
+
+// RelAvatarLink returns relative avatar link to the site domain,
+// which includes app sub-url as prefix.
+// Since Gravatar support not needed here - just check for image path.
+func (repo *Repository) RelAvatarLink() string {
+ defaultImgUrl := ""
+ if !com.IsExist(repo.CustomAvatarPath()) {
+ return defaultImgUrl
+ }
+ return fmt.Sprintf("%s/%s/%d", setting.AppSubURL, REPO_AVATAR_URL_PREFIX, repo.ID)
+}
+
+// AvatarLink returns repository avatar absolute link.
+func (repo *Repository) AvatarLink() string {
+ link := repo.RelAvatarLink()
+ if link[0] == '/' && link[1] != '/' {
+ return setting.AppURL + strings.TrimPrefix(link, setting.AppSubURL)[1:]
+ }
+ return link
+}
+
+// UploadAvatar saves custom avatar for repository.
+// FIXME: split uploads to different subdirs in case we have massive number of repositories.
+func (repo *Repository) UploadAvatar(data []byte) error {
+ img, _, err := image.Decode(bytes.NewReader(data))
+ if err != nil {
+ return fmt.Errorf("decode image: %v", err)
+ }
+
+ os.MkdirAll(setting.RepositoryAvatarUploadPath, os.ModePerm)
+ fw, err := os.Create(repo.CustomAvatarPath())
+ if err != nil {
+ return fmt.Errorf("create custom avatar directory: %v", err)
+ }
+ defer fw.Close()
+
+ m := resize.Resize(avatar.AVATAR_SIZE, avatar.AVATAR_SIZE, img, resize.NearestNeighbor)
+ if err = png.Encode(fw, m); err != nil {
+ return fmt.Errorf("encode image: %v", err)
+ }
+
+ return nil
+}
+
+// DeleteAvatar deletes the repository custom avatar.
+func (repo *Repository) DeleteAvatar() error {
+ log.Trace("DeleteAvatar [%d]: %s", repo.ID, repo.CustomAvatarPath())
+ if err := os.Remove(repo.CustomAvatarPath()); err != nil {
+ return err
+ }
+
+ repo.UseCustomAvatar = false
+ return UpdateRepository(repo, false)
+}
+
+// This method assumes following fields have been assigned with valid values:
+// Required - BaseRepo (if fork)
+// Arguments that are allowed to be nil: permission
+func (repo *Repository) APIFormat(permission *api.Permission, user ...*User) *api.Repository {
+ cloneLink := repo.CloneLink()
+ apiRepo := &api.Repository{
+ ID: repo.ID,
+ Owner: repo.Owner.APIFormat(),
+ Name: repo.Name,
+ FullName: repo.FullName(),
+ Description: repo.Description,
+ Private: repo.IsPrivate,
+ Fork: repo.IsFork,
+ Empty: repo.IsBare,
+ Mirror: repo.IsMirror,
+ Size: repo.Size,
+ HTMLURL: repo.HTMLURL(),
+ SSHURL: cloneLink.SSH,
+ CloneURL: cloneLink.HTTPS,
+ Website: repo.Website,
+ Stars: repo.NumStars,
+ Forks: repo.NumForks,
+ Watchers: repo.NumWatches,
+ OpenIssues: repo.NumOpenIssues,
+ DefaultBranch: repo.DefaultBranch,
+ Created: repo.Created,
+ Updated: repo.Updated,
+ Permissions: permission,
+ // Reserved for go-gogs-client change
+ // AvatarUrl: repo.AvatarLink(),
+ }
+ if repo.IsFork {
+ p := &api.Permission{Pull: true}
+ if len(user) != 0 {
+ p.Admin = user[0].IsAdminOfRepo(repo)
+ p.Push = user[0].IsWriterOfRepo(repo)
+ }
+ apiRepo.Parent = repo.BaseRepo.APIFormat(p)
+ }
+ return apiRepo
+}
+
+func (repo *Repository) getOwner(e Engine) (err error) {
+ if repo.Owner != nil {
+ return nil
+ }
+
+ repo.Owner, err = getUserByID(e, repo.OwnerID)
+ return err
+}
+
+func (repo *Repository) GetOwner() error {
+ return repo.getOwner(x)
+}
+
+func (repo *Repository) mustOwner(e Engine) *User {
+ if err := repo.getOwner(e); err != nil {
+ return &User{
+ Name: "error",
+ FullName: err.Error(),
+ }
+ }
+
+ return repo.Owner
+}
+
+func (repo *Repository) UpdateSize() error {
+ countObject, err := git.GetRepoSize(repo.RepoPath())
+ if err != nil {
+ return fmt.Errorf("GetRepoSize: %v", err)
+ }
+
+ repo.Size = countObject.Size + countObject.SizePack
+ if _, err = x.Id(repo.ID).Cols("size").Update(repo); err != nil {
+ return fmt.Errorf("update size: %v", err)
+ }
+ return nil
+}
+
+// ComposeMetas composes a map of metas for rendering external issue tracker URL.
+func (repo *Repository) ComposeMetas() map[string]string {
+ if !repo.EnableExternalTracker {
+ return nil
+ } else if repo.ExternalMetas == nil {
+ repo.ExternalMetas = map[string]string{
+ "format": repo.ExternalTrackerFormat,
+ "user": repo.MustOwner().Name,
+ "repo": repo.Name,
+ }
+ switch repo.ExternalTrackerStyle {
+ case markup.ISSUE_NAME_STYLE_ALPHANUMERIC:
+ repo.ExternalMetas["style"] = markup.ISSUE_NAME_STYLE_ALPHANUMERIC
+ default:
+ repo.ExternalMetas["style"] = markup.ISSUE_NAME_STYLE_NUMERIC
+ }
+
+ }
+ return repo.ExternalMetas
+}
+
+// DeleteWiki removes the actual and local copy of repository wiki.
+func (repo *Repository) DeleteWiki() {
+ wikiPaths := []string{repo.WikiPath(), repo.LocalWikiPath()}
+ for _, wikiPath := range wikiPaths {
+ RemoveAllWithNotice("Delete repository wiki", wikiPath)
+ }
+}
+
+// getUsersWithAccesMode returns users that have at least given access mode to the repository.
+func (repo *Repository) getUsersWithAccesMode(e Engine, mode AccessMode) (_ []*User, err error) {
+ if err = repo.getOwner(e); err != nil {
+ return nil, err
+ }
+
+ accesses := make([]*Access, 0, 10)
+ if err = e.Where("repo_id = ? AND mode >= ?", repo.ID, mode).Find(&accesses); err != nil {
+ return nil, err
+ }
+
+ // Leave a seat for owner itself to append later, but if owner is an organization
+ // and just waste 1 unit is cheaper than re-allocate memory once.
+ users := make([]*User, 0, len(accesses)+1)
+ if len(accesses) > 0 {
+ userIDs := make([]int64, len(accesses))
+ for i := 0; i < len(accesses); i++ {
+ userIDs[i] = accesses[i].UserID
+ }
+
+ if err = e.In("id", userIDs).Find(&users); err != nil {
+ return nil, err
+ }
+ }
+ if !repo.Owner.IsOrganization() {
+ users = append(users, repo.Owner)
+ }
+
+ return users, nil
+}
+
+// getAssignees returns a list of users who can be assigned to issues in this repository.
+func (repo *Repository) getAssignees(e Engine) (_ []*User, err error) {
+ return repo.getUsersWithAccesMode(e, ACCESS_MODE_READ)
+}
+
+// GetAssignees returns all users that have read access and can be assigned to issues
+// of the repository,
+func (repo *Repository) GetAssignees() (_ []*User, err error) {
+ return repo.getAssignees(x)
+}
+
+// GetAssigneeByID returns the user that has write access of repository by given ID.
+func (repo *Repository) GetAssigneeByID(userID int64) (*User, error) {
+ return GetAssigneeByID(repo, userID)
+}
+
+// GetWriters returns all users that have write access to the repository.
+func (repo *Repository) GetWriters() (_ []*User, err error) {
+ return repo.getUsersWithAccesMode(x, ACCESS_MODE_WRITE)
+}
+
+// GetMilestoneByID returns the milestone belongs to repository by given ID.
+func (repo *Repository) GetMilestoneByID(milestoneID int64) (*Milestone, error) {
+ return GetMilestoneByRepoID(repo.ID, milestoneID)
+}
+
+// IssueStats returns number of open and closed repository issues by given filter mode.
+func (repo *Repository) IssueStats(userID int64, filterMode FilterMode, isPull bool) (int64, int64) {
+ return GetRepoIssueStats(repo.ID, userID, filterMode, isPull)
+}
+
+func (repo *Repository) GetMirror() (err error) {
+ repo.Mirror, err = GetMirrorByRepoID(repo.ID)
+ return err
+}
+
+func (repo *Repository) repoPath(e Engine) string {
+ return RepoPath(repo.mustOwner(e).Name, repo.Name)
+}
+
+func (repo *Repository) RepoPath() string {
+ return repo.repoPath(x)
+}
+
+func (repo *Repository) GitConfigPath() string {
+ return filepath.Join(repo.RepoPath(), "config")
+}
+
+func (repo *Repository) RelLink() string {
+ return "/" + repo.FullName()
+}
+
+func (repo *Repository) Link() string {
+ return setting.AppSubURL + "/" + repo.FullName()
+}
+
+func (repo *Repository) ComposeCompareURL(oldCommitID, newCommitID string) string {
+ return fmt.Sprintf("%s/%s/compare/%s...%s", repo.MustOwner().Name, repo.Name, oldCommitID, newCommitID)
+}
+
+func (repo *Repository) HasAccess(userID int64) bool {
+ has, _ := HasAccess(userID, repo, ACCESS_MODE_READ)
+ return has
+}
+
+func (repo *Repository) IsOwnedBy(userID int64) bool {
+ return repo.OwnerID == userID
+}
+
+// CanBeForked returns true if repository meets the requirements of being forked.
+func (repo *Repository) CanBeForked() bool {
+ return !repo.IsBare
+}
+
+// CanEnablePulls returns true if repository meets the requirements of accepting pulls.
+func (repo *Repository) CanEnablePulls() bool {
+ return !repo.IsMirror && !repo.IsBare
+}
+
+// AllowPulls returns true if repository meets the requirements of accepting pulls and has them enabled.
+func (repo *Repository) AllowsPulls() bool {
+ return repo.CanEnablePulls() && repo.EnablePulls
+}
+
+func (repo *Repository) IsBranchRequirePullRequest(name string) bool {
+ return IsBranchOfRepoRequirePullRequest(repo.ID, name)
+}
+
+// CanEnableEditor returns true if repository meets the requirements of web editor.
+func (repo *Repository) CanEnableEditor() bool {
+ return !repo.IsMirror
+}
+
+// FIXME: should have a mutex to prevent producing same index for two issues that are created
+// closely enough.
+func (repo *Repository) NextIssueIndex() int64 {
+ return int64(repo.NumIssues+repo.NumPulls) + 1
+}
+
+func (repo *Repository) LocalCopyPath() string {
+ return path.Join(setting.AppDataPath, "tmp/local-repo", com.ToStr(repo.ID))
+}
+
+// UpdateLocalCopy fetches latest changes of given branch from repoPath to localPath.
+// It creates a new clone if local copy does not exist, but does not checks out to a
+// specific branch if the local copy belongs to a wiki.
+// For existing local copy, it checks out to target branch by default, and safe to
+// assume subsequent operations are against target branch when caller has confidence
+// about no race condition.
+func UpdateLocalCopyBranch(repoPath, localPath, branch string, isWiki bool) (err error) {
+ if !com.IsExist(localPath) {
+ // Checkout to a specific branch fails when wiki is an empty repository.
+ if isWiki {
+ branch = ""
+ }
+ if err = git.Clone(repoPath, localPath, git.CloneRepoOptions{
+ Timeout: time.Duration(setting.Git.Timeout.Clone) * time.Second,
+ Branch: branch,
+ }); err != nil {
+ return fmt.Errorf("git clone %s: %v", branch, err)
+ }
+ } else {
+ if err = git.Fetch(localPath, git.FetchRemoteOptions{
+ Prune: true,
+ }); err != nil {
+ return fmt.Errorf("git fetch: %v", err)
+ }
+ if err = git.Checkout(localPath, git.CheckoutOptions{
+ Branch: branch,
+ }); err != nil {
+ return fmt.Errorf("git checkout %s: %v", branch, err)
+ }
+
+ // Reset to align with remote in case of force push.
+ if err = git.ResetHEAD(localPath, true, "origin/"+branch); err != nil {
+ return fmt.Errorf("git reset --hard origin/%s: %v", branch, err)
+ }
+ }
+ return nil
+}
+
+// UpdateLocalCopyBranch makes sure local copy of repository in given branch is up-to-date.
+func (repo *Repository) UpdateLocalCopyBranch(branch string) error {
+ return UpdateLocalCopyBranch(repo.RepoPath(), repo.LocalCopyPath(), branch, false)
+}
+
+// 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(filepath.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,
+ LowerName: strings.ToLower(repoName),
+ })
+ return has && com.IsDir(RepoPath(u.Name, repoName)), err
+}
+
+// IsRepositoryExist returns true if the repository with given name under user has already existed.
+func IsRepositoryExist(u *User, repoName string) (bool, error) {
+ return isRepositoryExist(x, u, repoName)
+}
+
+// CloneLink represents different types of clone URLs of repository.
+type CloneLink struct {
+ SSH string
+ HTTPS string
+ Git string
+}
+
+// ComposeHTTPSCloneURL returns HTTPS clone URL based on given owner and repository name.
+func ComposeHTTPSCloneURL(owner, repo string) string {
+ return fmt.Sprintf("%s%s/%s.git", setting.AppURL, owner, repo)
+}
+
+func (repo *Repository) cloneLink(isWiki bool) *CloneLink {
+ repoName := repo.Name
+ if isWiki {
+ repoName += ".wiki"
+ }
+
+ repo.Owner = repo.MustOwner()
+ cl := new(CloneLink)
+ if setting.SSH.Port != 22 {
+ cl.SSH = fmt.Sprintf("ssh://%s@%s:%d/%s/%s.git", setting.RunUser, setting.SSH.Domain, setting.SSH.Port, repo.Owner.Name, repoName)
+ } else {
+ cl.SSH = fmt.Sprintf("%s@%s:%s/%s.git", setting.RunUser, setting.SSH.Domain, repo.Owner.Name, repoName)
+ }
+ cl.HTTPS = ComposeHTTPSCloneURL(repo.Owner.Name, repoName)
+ return cl
+}
+
+// CloneLink returns clone URLs of repository.
+func (repo *Repository) CloneLink() (cl *CloneLink) {
+ return repo.cloneLink(false)
+}
+
+type MigrateRepoOptions struct {
+ Name string
+ Description string
+ IsPrivate bool
+ IsMirror bool
+ RemoteAddr string
+}
+
+/*
+ GitHub, GitLab, Gogs: *.wiki.git
+ BitBucket: *.git/wiki
+*/
+var commonWikiURLSuffixes = []string{".wiki.git", ".git/wiki"}
+
+// wikiRemoteURL returns accessible repository URL for wiki if exists.
+// Otherwise, it returns an empty string.
+func wikiRemoteURL(remote string) string {
+ remote = strings.TrimSuffix(remote, ".git")
+ for _, suffix := range commonWikiURLSuffixes {
+ wikiURL := remote + suffix
+ if git.IsRepoURLAccessible(git.NetworkOptions{
+ URL: wikiURL,
+ }) {
+ return wikiURL
+ }
+ }
+ return ""
+}
+
+// MigrateRepository migrates a existing repository from other project hosting.
+func MigrateRepository(doer, owner *User, opts MigrateRepoOptions) (*Repository, error) {
+ repo, err := CreateRepository(doer, owner, CreateRepoOptions{
+ Name: opts.Name,
+ Description: opts.Description,
+ IsPrivate: opts.IsPrivate,
+ IsMirror: opts.IsMirror,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ repoPath := RepoPath(owner.Name, opts.Name)
+ wikiPath := WikiPath(owner.Name, opts.Name)
+
+ if owner.IsOrganization() {
+ t, err := owner.GetOwnerTeam()
+ if err != nil {
+ return nil, err
+ }
+ repo.NumWatches = t.NumMembers
+ } else {
+ repo.NumWatches = 1
+ }
+
+ migrateTimeout := time.Duration(setting.Git.Timeout.Migrate) * time.Second
+
+ RemoveAllWithNotice("Repository path erase before creation", repoPath)
+ if err = git.Clone(opts.RemoteAddr, repoPath, git.CloneRepoOptions{
+ Mirror: true,
+ Quiet: true,
+ Timeout: migrateTimeout,
+ }); err != nil {
+ return repo, fmt.Errorf("Clone: %v", err)
+ }
+
+ wikiRemotePath := wikiRemoteURL(opts.RemoteAddr)
+ if len(wikiRemotePath) > 0 {
+ RemoveAllWithNotice("Repository wiki path erase before creation", wikiPath)
+ if err = git.Clone(wikiRemotePath, wikiPath, git.CloneRepoOptions{
+ Mirror: true,
+ Quiet: true,
+ Timeout: migrateTimeout,
+ }); err != nil {
+ log.Trace("Fail to clone wiki: %v", err)
+ RemoveAllWithNotice("Delete repository wiki for initialization failure", wikiPath)
+ }
+ }
+
+ // Check if repository is empty.
+ _, stderr, err := com.ExecCmdDir(repoPath, "git", "log", "-1")
+ if err != nil {
+ if strings.Contains(stderr, "fatal: bad default revision 'HEAD'") {
+ repo.IsBare = true
+ } else {
+ return repo, fmt.Errorf("check bare: %v - %s", err, stderr)
+ }
+ }
+
+ if !repo.IsBare {
+ // Try to get HEAD branch and set it as default branch.
+ gitRepo, err := git.OpenRepository(repoPath)
+ if err != nil {
+ return repo, fmt.Errorf("OpenRepository: %v", err)
+ }
+ headBranch, err := gitRepo.GetHEADBranch()
+ if err != nil {
+ return repo, fmt.Errorf("GetHEADBranch: %v", err)
+ }
+ if headBranch != nil {
+ repo.DefaultBranch = headBranch.Name
+ }
+
+ if err = repo.UpdateSize(); err != nil {
+ log.Error(2, "UpdateSize [repo_id: %d]: %v", repo.ID, err)
+ }
+ }
+
+ if opts.IsMirror {
+ if _, err = x.InsertOne(&Mirror{
+ RepoID: repo.ID,
+ Interval: setting.Mirror.DefaultInterval,
+ EnablePrune: true,
+ NextSync: time.Now().Add(time.Duration(setting.Mirror.DefaultInterval) * time.Hour),
+ }); err != nil {
+ return repo, fmt.Errorf("InsertOne: %v", err)
+ }
+
+ repo.IsMirror = true
+ return repo, UpdateRepository(repo, false)
+ }
+
+ return CleanUpMigrateInfo(repo)
+}
+
+// cleanUpMigrateGitConfig removes mirror info which prevents "push --all".
+// This also removes possible user credentials.
+func cleanUpMigrateGitConfig(configPath string) error {
+ 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
+}
+
+var hooksTpls = map[string]string{
+ "pre-receive": "#!/usr/bin/env %s\n\"%s\" hook --config='%s' pre-receive\n",
+ "update": "#!/usr/bin/env %s\n\"%s\" hook --config='%s' update $1 $2 $3\n",
+ "post-receive": "#!/usr/bin/env %s\n\"%s\" hook --config='%s' post-receive\n",
+}
+
+func createDelegateHooks(repoPath string) (err error) {
+ for _, name := range git.HookNames {
+ hookPath := filepath.Join(repoPath, "hooks", name)
+ if err = ioutil.WriteFile(hookPath,
+ []byte(fmt.Sprintf(hooksTpls[name], setting.ScriptType, setting.AppPath, setting.CustomConf)),
+ os.ModePerm); err != nil {
+ return fmt.Errorf("create delegate hook '%s': %v", hookPath, err)
+ }
+ }
+ return nil
+}
+
+// Finish migrating repository and/or wiki with things that don't need to be done for mirrors.
+func CleanUpMigrateInfo(repo *Repository) (*Repository, error) {
+ repoPath := repo.RepoPath()
+ if err := createDelegateHooks(repoPath); err != nil {
+ return repo, fmt.Errorf("createDelegateHooks: %v", err)
+ }
+ if repo.HasWiki() {
+ if err := createDelegateHooks(repo.WikiPath()); err != nil {
+ return repo, fmt.Errorf("createDelegateHooks.(wiki): %v", err)
+ }
+ }
+
+ if err := cleanUpMigrateGitConfig(repo.GitConfigPath()); err != nil {
+ return repo, fmt.Errorf("cleanUpMigrateGitConfig: %v", err)
+ }
+ if repo.HasWiki() {
+ if err := cleanUpMigrateGitConfig(path.Join(repo.WikiPath(), "config")); err != nil {
+ return repo, fmt.Errorf("cleanUpMigrateGitConfig.(wiki): %v", err)
+ }
+ }
+
+ return repo, UpdateRepository(repo, false)
+}
+
+// initRepoCommit temporarily changes with work directory.
+func initRepoCommit(tmpPath string, sig *git.Signature) (err error) {
+ var stderr string
+ if _, stderr, err = process.ExecDir(-1,
+ tmpPath, fmt.Sprintf("initRepoCommit (git add): %s", tmpPath),
+ "git", "add", "--all"); err != nil {
+ return fmt.Errorf("git add: %s", stderr)
+ }
+
+ if _, stderr, err = process.ExecDir(-1,
+ tmpPath, fmt.Sprintf("initRepoCommit (git commit): %s", tmpPath),
+ "git", "commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email),
+ "-m", "Initial commit"); err != nil {
+ return fmt.Errorf("git commit: %s", stderr)
+ }
+
+ if _, stderr, err = process.ExecDir(-1,
+ tmpPath, fmt.Sprintf("initRepoCommit (git push): %s", tmpPath),
+ "git", "push", "origin", "master"); err != nil {
+ return fmt.Errorf("git push: %s", stderr)
+ }
+ return nil
+}
+
+type CreateRepoOptions struct {
+ Name string
+ Description string
+ Gitignores string
+ License string
+ Readme string
+ IsPrivate bool
+ IsMirror bool
+ AutoInit bool
+}
+
+func getRepoInitFile(tp, name string) ([]byte, error) {
+ relPath := path.Join("conf", tp, strings.TrimLeft(path.Clean("/"+name), "/"))
+
+ // Use custom file when available.
+ customPath := path.Join(setting.CustomPath, relPath)
+ if com.IsFile(customPath) {
+ return ioutil.ReadFile(customPath)
+ }
+ return bindata.Asset(relPath)
+}
+
+func prepareRepoCommit(repo *Repository, tmpDir, repoPath string, opts CreateRepoOptions) error {
+ // Clone to temprory path and do the init commit.
+ _, stderr, err := process.Exec(
+ fmt.Sprintf("initRepository(git clone): %s", repoPath), "git", "clone", repoPath, tmpDir)
+ if err != nil {
+ return fmt.Errorf("git clone: %v - %s", err, stderr)
+ }
+
+ // README
+ data, err := getRepoInitFile("readme", opts.Readme)
+ if err != nil {
+ return fmt.Errorf("getRepoInitFile[%s]: %v", opts.Readme, err)
+ }
+
+ cloneLink := repo.CloneLink()
+ match := map[string]string{
+ "Name": repo.Name,
+ "Description": repo.Description,
+ "CloneURL.SSH": cloneLink.SSH,
+ "CloneURL.HTTPS": cloneLink.HTTPS,
+ }
+ if err = ioutil.WriteFile(filepath.Join(tmpDir, "README.md"),
+ []byte(com.Expand(string(data), match)), 0644); err != nil {
+ return fmt.Errorf("write README.md: %v", err)
+ }
+
+ // .gitignore
+ if len(opts.Gitignores) > 0 {
+ var buf bytes.Buffer
+ names := strings.Split(opts.Gitignores, ",")
+ for _, name := range names {
+ data, err = getRepoInitFile("gitignore", name)
+ if err != nil {
+ return fmt.Errorf("getRepoInitFile[%s]: %v", name, err)
+ }
+ buf.WriteString("# ---> " + name + "\n")
+ buf.Write(data)
+ buf.WriteString("\n")
+ }
+
+ if buf.Len() > 0 {
+ if err = ioutil.WriteFile(filepath.Join(tmpDir, ".gitignore"), buf.Bytes(), 0644); err != nil {
+ return fmt.Errorf("write .gitignore: %v", err)
+ }
+ }
+ }
+
+ // LICENSE
+ if len(opts.License) > 0 {
+ data, err = getRepoInitFile("license", opts.License)
+ if err != nil {
+ return fmt.Errorf("getRepoInitFile[%s]: %v", opts.License, err)
+ }
+
+ if err = ioutil.WriteFile(filepath.Join(tmpDir, "LICENSE"), data, 0644); err != nil {
+ return fmt.Errorf("write LICENSE: %v", err)
+ }
+ }
+
+ return nil
+}
+
+// initRepository performs initial commit with chosen setup files on behave of doer.
+func initRepository(e Engine, repoPath string, doer *User, repo *Repository, opts CreateRepoOptions) (err error) {
+ // Somehow the directory could exist.
+ if com.IsExist(repoPath) {
+ return fmt.Errorf("initRepository: path already exists: %s", repoPath)
+ }
+
+ // Init bare new repository.
+ if err = git.InitRepository(repoPath, true); err != nil {
+ return fmt.Errorf("InitRepository: %v", err)
+ } else if err = createDelegateHooks(repoPath); err != nil {
+ return fmt.Errorf("createDelegateHooks: %v", err)
+ }
+
+ tmpDir := filepath.Join(os.TempDir(), "gogs-"+repo.Name+"-"+com.ToStr(time.Now().Nanosecond()))
+
+ // Initialize repository according to user's choice.
+ if opts.AutoInit {
+ os.MkdirAll(tmpDir, os.ModePerm)
+ defer RemoveAllWithNotice("Delete repository for auto-initialization", tmpDir)
+
+ if err = prepareRepoCommit(repo, tmpDir, repoPath, opts); err != nil {
+ return fmt.Errorf("prepareRepoCommit: %v", err)
+ }
+
+ // Apply changes and commit.
+ if err = initRepoCommit(tmpDir, doer.NewGitSig()); err != nil {
+ return fmt.Errorf("initRepoCommit: %v", err)
+ }
+ }
+
+ // Re-fetch the repository from database before updating it (else it would
+ // override changes that were done earlier with sql)
+ if repo, err = getRepositoryByID(e, repo.ID); err != nil {
+ return fmt.Errorf("getRepositoryByID: %v", err)
+ }
+
+ if !opts.AutoInit {
+ repo.IsBare = true
+ }
+
+ repo.DefaultBranch = "master"
+ if err = updateRepository(e, repo, false); err != nil {
+ return fmt.Errorf("updateRepository: %v", err)
+ }
+
+ return nil
+}
+
+var (
+ reservedRepoNames = []string{".", ".."}
+ reservedRepoPatterns = []string{"*.git", "*.wiki"}
+)
+
+// IsUsableRepoName return an error if given name is a reserved name or pattern.
+func IsUsableRepoName(name string) error {
+ return isUsableName(reservedRepoNames, reservedRepoPatterns, name)
+}
+
+func createRepository(e *xorm.Session, doer, owner *User, repo *Repository) (err error) {
+ if err = IsUsableRepoName(repo.Name); err != nil {
+ return err
+ }
+
+ has, err := isRepositoryExist(e, owner, repo.Name)
+ if err != nil {
+ return fmt.Errorf("IsRepositoryExist: %v", err)
+ } else if has {
+ return ErrRepoAlreadyExist{owner.Name, repo.Name}
+ }
+
+ if _, err = e.Insert(repo); err != nil {
+ return err
+ }
+
+ owner.NumRepos++
+ // Remember visibility preference.
+ owner.LastRepoVisibility = repo.IsPrivate
+ if err = updateUser(e, owner); err != nil {
+ return fmt.Errorf("updateUser: %v", err)
+ }
+
+ // Give access to all members in owner team.
+ if owner.IsOrganization() {
+ t, err := owner.getOwnerTeam(e)
+ if err != nil {
+ return fmt.Errorf("getOwnerTeam: %v", err)
+ } else if err = t.addRepository(e, repo); err != nil {
+ return fmt.Errorf("addRepository: %v", err)
+ }
+ } else {
+ // Organization automatically called this in addRepository method.
+ if err = repo.recalculateAccesses(e); err != nil {
+ return fmt.Errorf("recalculateAccesses: %v", err)
+ }
+ }
+
+ if err = watchRepo(e, owner.ID, repo.ID, true); err != nil {
+ return fmt.Errorf("watchRepo: %v", err)
+ } else if err = newRepoAction(e, doer, owner, repo); err != nil {
+ return fmt.Errorf("newRepoAction: %v", err)
+ }
+
+ return repo.loadAttributes(e)
+}
+
+// CreateRepository creates a repository for given user or organization.
+func CreateRepository(doer, owner *User, opts CreateRepoOptions) (_ *Repository, err error) {
+ if !owner.CanCreateRepo() {
+ return nil, errors.ReachLimitOfRepo{owner.RepoCreationNum()}
+ }
+
+ repo := &Repository{
+ OwnerID: owner.ID,
+ Owner: owner,
+ Name: opts.Name,
+ LowerName: strings.ToLower(opts.Name),
+ Description: opts.Description,
+ IsPrivate: opts.IsPrivate,
+ EnableWiki: true,
+ EnableIssues: true,
+ EnablePulls: true,
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return nil, err
+ }
+
+ if err = createRepository(sess, doer, owner, repo); err != nil {
+ return nil, err
+ }
+
+ // No need for init mirror.
+ if !opts.IsMirror {
+ repoPath := RepoPath(owner.Name, repo.Name)
+ if err = initRepository(sess, repoPath, doer, repo, opts); err != nil {
+ RemoveAllWithNotice("Delete repository for initialization failure", repoPath)
+ return nil, fmt.Errorf("initRepository: %v", err)
+ }
+
+ _, stderr, err := process.ExecDir(-1,
+ repoPath, fmt.Sprintf("CreateRepository 'git update-server-info': %s", repoPath),
+ "git", "update-server-info")
+ if err != nil {
+ return nil, fmt.Errorf("CreateRepository 'git update-server-info': %s", stderr)
+ }
+ }
+
+ return repo, sess.Commit()
+}
+
+func countRepositories(userID int64, private bool) int64 {
+ sess := x.Where("id > 0")
+
+ if userID > 0 {
+ sess.And("owner_id = ?", userID)
+ }
+ if !private {
+ sess.And("is_private=?", false)
+ }
+
+ count, err := sess.Count(new(Repository))
+ if err != nil {
+ log.Error(4, "countRepositories: %v", err)
+ }
+ return count
+}
+
+// CountRepositories returns number of repositories.
+// Argument private only takes effect when it is false,
+// set it true to count all repositories.
+func CountRepositories(private bool) int64 {
+ return countRepositories(-1, private)
+}
+
+// CountUserRepositories returns number of repositories user owns.
+// Argument private only takes effect when it is false,
+// set it true to count all repositories.
+func CountUserRepositories(userID int64, private bool) int64 {
+ return countRepositories(userID, private)
+}
+
+func Repositories(page, pageSize int) (_ []*Repository, err error) {
+ repos := make([]*Repository, 0, pageSize)
+ return repos, x.Limit(pageSize, (page-1)*pageSize).Asc("id").Find(&repos)
+}
+
+// RepositoriesWithUsers returns number of repos in given page.
+func RepositoriesWithUsers(page, pageSize int) (_ []*Repository, err error) {
+ repos, err := Repositories(page, pageSize)
+ if err != nil {
+ return nil, fmt.Errorf("Repositories: %v", err)
+ }
+
+ for i := range repos {
+ if err = repos[i].GetOwner(); err != nil {
+ return nil, err
+ }
+ }
+
+ return repos, nil
+}
+
+// FilterRepositoryWithIssues selects repositories that are using interal issue tracker
+// and has disabled external tracker from given set.
+// It returns nil if result set is empty.
+func FilterRepositoryWithIssues(repoIDs []int64) ([]int64, error) {
+ if len(repoIDs) == 0 {
+ return nil, nil
+ }
+
+ repos := make([]*Repository, 0, len(repoIDs))
+ if err := x.Where("enable_issues=?", true).
+ And("enable_external_tracker=?", false).
+ In("id", repoIDs).
+ Cols("id").
+ Find(&repos); err != nil {
+ return nil, fmt.Errorf("filter valid repositories %v: %v", repoIDs, err)
+ }
+
+ if len(repos) == 0 {
+ return nil, nil
+ }
+
+ repoIDs = make([]int64, len(repos))
+ for i := range repos {
+ repoIDs[i] = repos[i].ID
+ }
+ return repoIDs, nil
+}
+
+// RepoPath returns repository path by given user and repository name.
+func RepoPath(userName, repoName string) string {
+ return filepath.Join(UserPath(userName), strings.ToLower(repoName)+".git")
+}
+
+// TransferOwnership transfers all corresponding setting from old user to new one.
+func TransferOwnership(doer *User, newOwnerName string, repo *Repository) error {
+ newOwner, err := GetUserByName(newOwnerName)
+ if err != nil {
+ return fmt.Errorf("get new owner '%s': %v", newOwnerName, err)
+ }
+
+ // Check if new owner has repository with same name.
+ has, err := IsRepositoryExist(newOwner, repo.Name)
+ if err != nil {
+ return fmt.Errorf("IsRepositoryExist: %v", err)
+ } else if has {
+ return ErrRepoAlreadyExist{newOwnerName, repo.Name}
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return fmt.Errorf("sess.Begin: %v", err)
+ }
+
+ owner := repo.Owner
+
+ // Note: we have to set value here to make sure recalculate accesses is based on
+ // new owner.
+ repo.OwnerID = newOwner.ID
+ repo.Owner = newOwner
+
+ // Update repository.
+ if _, err := sess.ID(repo.ID).Update(repo); err != nil {
+ return fmt.Errorf("update owner: %v", err)
+ }
+
+ // Remove redundant collaborators.
+ collaborators, err := repo.getCollaborators(sess)
+ if err != nil {
+ return fmt.Errorf("getCollaborators: %v", err)
+ }
+
+ // Dummy object.
+ collaboration := &Collaboration{RepoID: repo.ID}
+ for _, c := range collaborators {
+ collaboration.UserID = c.ID
+ if c.ID == newOwner.ID || newOwner.IsOrgMember(c.ID) {
+ if _, err = sess.Delete(collaboration); err != nil {
+ return fmt.Errorf("remove collaborator '%d': %v", c.ID, err)
+ }
+ }
+ }
+
+ // Remove old team-repository relations.
+ if owner.IsOrganization() {
+ if err = owner.getTeams(sess); err != nil {
+ return fmt.Errorf("getTeams: %v", err)
+ }
+ for _, t := range owner.Teams {
+ if !t.hasRepository(sess, repo.ID) {
+ continue
+ }
+
+ t.NumRepos--
+ if _, err := sess.ID(t.ID).AllCols().Update(t); err != nil {
+ return fmt.Errorf("decrease team repository count '%d': %v", t.ID, err)
+ }
+ }
+
+ if err = owner.removeOrgRepo(sess, repo.ID); err != nil {
+ return fmt.Errorf("removeOrgRepo: %v", err)
+ }
+ }
+
+ if newOwner.IsOrganization() {
+ t, err := newOwner.getOwnerTeam(sess)
+ if err != nil {
+ return fmt.Errorf("getOwnerTeam: %v", err)
+ } else if err = t.addRepository(sess, repo); err != nil {
+ return fmt.Errorf("add to owner team: %v", err)
+ }
+ } else {
+ // Organization called this in addRepository method.
+ if err = repo.recalculateAccesses(sess); err != nil {
+ return fmt.Errorf("recalculateAccesses: %v", err)
+ }
+ }
+
+ // Update repository count.
+ if _, err = sess.Exec("UPDATE `user` SET num_repos=num_repos+1 WHERE id=?", newOwner.ID); err != nil {
+ return fmt.Errorf("increase new owner repository count: %v", err)
+ } else if _, err = sess.Exec("UPDATE `user` SET num_repos=num_repos-1 WHERE id=?", owner.ID); err != nil {
+ return fmt.Errorf("decrease old owner repository count: %v", err)
+ }
+
+ if err = watchRepo(sess, newOwner.ID, repo.ID, true); err != nil {
+ return fmt.Errorf("watchRepo: %v", err)
+ } else if err = transferRepoAction(sess, doer, owner, repo); err != nil {
+ return fmt.Errorf("transferRepoAction: %v", err)
+ }
+
+ // Rename remote repository to new path and delete local copy.
+ os.MkdirAll(UserPath(newOwner.Name), os.ModePerm)
+ if err = os.Rename(RepoPath(owner.Name, repo.Name), RepoPath(newOwner.Name, repo.Name)); err != nil {
+ return fmt.Errorf("rename repository directory: %v", err)
+ }
+ RemoveAllWithNotice("Delete repository local copy", repo.LocalCopyPath())
+
+ // Rename remote wiki repository to new path and delete local copy.
+ wikiPath := WikiPath(owner.Name, repo.Name)
+ if com.IsExist(wikiPath) {
+ RemoveAllWithNotice("Delete repository wiki local copy", repo.LocalWikiPath())
+ if err = os.Rename(wikiPath, WikiPath(newOwner.Name, repo.Name)); err != nil {
+ return fmt.Errorf("rename repository wiki: %v", err)
+ }
+ }
+
+ return sess.Commit()
+}
+
+// ChangeRepositoryName changes all corresponding setting from old repository name to new one.
+func ChangeRepositoryName(u *User, oldRepoName, newRepoName string) (err error) {
+ oldRepoName = strings.ToLower(oldRepoName)
+ newRepoName = strings.ToLower(newRepoName)
+ if err = IsUsableRepoName(newRepoName); err != nil {
+ return err
+ }
+
+ has, err := IsRepositoryExist(u, newRepoName)
+ if err != nil {
+ return fmt.Errorf("IsRepositoryExist: %v", err)
+ } else if has {
+ return ErrRepoAlreadyExist{u.Name, newRepoName}
+ }
+
+ repo, err := GetRepositoryByName(u.ID, oldRepoName)
+ if err != nil {
+ return fmt.Errorf("GetRepositoryByName: %v", err)
+ }
+
+ // Change repository directory name
+ if err = os.Rename(repo.RepoPath(), RepoPath(u.Name, newRepoName)); err != nil {
+ return fmt.Errorf("rename repository directory: %v", err)
+ }
+
+ wikiPath := repo.WikiPath()
+ if com.IsExist(wikiPath) {
+ if err = os.Rename(wikiPath, WikiPath(u.Name, newRepoName)); err != nil {
+ return fmt.Errorf("rename repository wiki: %v", err)
+ }
+ RemoveAllWithNotice("Delete repository wiki local copy", repo.LocalWikiPath())
+ }
+
+ RemoveAllWithNotice("Delete repository local copy", repo.LocalCopyPath())
+ return nil
+}
+
+func getRepositoriesByForkID(e Engine, forkID int64) ([]*Repository, error) {
+ repos := make([]*Repository, 0, 10)
+ return repos, e.Where("fork_id=?", forkID).Find(&repos)
+}
+
+// GetRepositoriesByForkID returns all repositories with given fork ID.
+func GetRepositoriesByForkID(forkID int64) ([]*Repository, error) {
+ return getRepositoriesByForkID(x, forkID)
+}
+
+func updateRepository(e Engine, repo *Repository, visibilityChanged bool) (err error) {
+ repo.LowerName = strings.ToLower(repo.Name)
+
+ if len(repo.Description) > 512 {
+ repo.Description = repo.Description[:512]
+ }
+ if len(repo.Website) > 255 {
+ repo.Website = repo.Website[:255]
+ }
+
+ if _, err = e.ID(repo.ID).AllCols().Update(repo); err != nil {
+ return fmt.Errorf("update: %v", err)
+ }
+
+ if visibilityChanged {
+ if err = repo.getOwner(e); err != nil {
+ return fmt.Errorf("getOwner: %v", err)
+ }
+ if repo.Owner.IsOrganization() {
+ // Organization repository need to recalculate access table when visivility is changed
+ if err = repo.recalculateTeamAccesses(e, 0); err != nil {
+ return fmt.Errorf("recalculateTeamAccesses: %v", err)
+ }
+ }
+
+ // Create/Remove git-daemon-export-ok for git-daemon
+ daemonExportFile := path.Join(repo.RepoPath(), "git-daemon-export-ok")
+ if repo.IsPrivate && com.IsExist(daemonExportFile) {
+ if err = os.Remove(daemonExportFile); err != nil {
+ log.Error(4, "Failed to remove %s: %v", daemonExportFile, err)
+ }
+ } else if !repo.IsPrivate && !com.IsExist(daemonExportFile) {
+ if f, err := os.Create(daemonExportFile); err != nil {
+ log.Error(4, "Failed to create %s: %v", daemonExportFile, err)
+ } else {
+ f.Close()
+ }
+ }
+
+ forkRepos, err := getRepositoriesByForkID(e, repo.ID)
+ if err != nil {
+ return fmt.Errorf("getRepositoriesByForkID: %v", err)
+ }
+ for i := range forkRepos {
+ forkRepos[i].IsPrivate = repo.IsPrivate
+ if err = updateRepository(e, forkRepos[i], true); err != nil {
+ return fmt.Errorf("updateRepository[%d]: %v", forkRepos[i].ID, err)
+ }
+ }
+
+ // Change visibility of generated actions
+ if _, err = e.Where("repo_id = ?", repo.ID).Cols("is_private").Update(&Action{IsPrivate: repo.IsPrivate}); err != nil {
+ return fmt.Errorf("change action visibility of repository: %v", err)
+ }
+ }
+
+ return nil
+}
+
+func UpdateRepository(repo *Repository, visibilityChanged bool) (err error) {
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if err = updateRepository(x, repo, visibilityChanged); err != nil {
+ return fmt.Errorf("updateRepository: %v", err)
+ }
+
+ return sess.Commit()
+}
+
+// DeleteRepository deletes a repository for a user or organization.
+func DeleteRepository(uid, repoID int64) error {
+ repo := &Repository{ID: repoID, OwnerID: uid}
+ has, err := x.Get(repo)
+ if err != nil {
+ return err
+ } else if !has {
+ return errors.RepoNotExist{repoID, uid, ""}
+ }
+
+ // In case is a organization.
+ org, err := GetUserByID(uid)
+ if err != nil {
+ return err
+ }
+ if org.IsOrganization() {
+ if err = org.GetTeams(); err != nil {
+ return err
+ }
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if org.IsOrganization() {
+ for _, t := range org.Teams {
+ if !t.hasRepository(sess, repoID) {
+ continue
+ } else if err = t.removeRepository(sess, repo, false); err != nil {
+ return err
+ }
+ }
+ }
+
+ if err = deleteBeans(sess,
+ &Repository{ID: repoID},
+ &Access{RepoID: repo.ID},
+ &Action{RepoID: repo.ID},
+ &Watch{RepoID: repoID},
+ &Star{RepoID: repoID},
+ &Mirror{RepoID: repoID},
+ &IssueUser{RepoID: repoID},
+ &Milestone{RepoID: repoID},
+ &Release{RepoID: repoID},
+ &Collaboration{RepoID: repoID},
+ &PullRequest{BaseRepoID: repoID},
+ &ProtectBranch{RepoID: repoID},
+ &ProtectBranchWhitelist{RepoID: repoID},
+ &Webhook{RepoID: repoID},
+ &HookTask{RepoID: repoID},
+ ); err != nil {
+ return fmt.Errorf("deleteBeans: %v", err)
+ }
+
+ // Delete comments and attachments.
+ issues := make([]*Issue, 0, 25)
+ attachmentPaths := make([]string, 0, len(issues))
+ if err = sess.Where("repo_id=?", repoID).Find(&issues); err != nil {
+ return err
+ }
+ for i := range issues {
+ if _, err = sess.Delete(&Comment{IssueID: issues[i].ID}); err != nil {
+ return err
+ }
+
+ attachments := make([]*Attachment, 0, 5)
+ if err = sess.Where("issue_id=?", issues[i].ID).Find(&attachments); err != nil {
+ return err
+ }
+ for j := range attachments {
+ attachmentPaths = append(attachmentPaths, attachments[j].LocalPath())
+ }
+
+ if _, err = sess.Delete(&Attachment{IssueID: issues[i].ID}); err != nil {
+ return err
+ }
+ }
+
+ if _, err = sess.Delete(&Issue{RepoID: repoID}); err != nil {
+ return err
+ }
+
+ if repo.IsFork {
+ if _, err = sess.Exec("UPDATE `repository` SET num_forks=num_forks-1 WHERE id=?", repo.ForkID); err != nil {
+ return fmt.Errorf("decrease fork count: %v", err)
+ }
+ }
+
+ if _, err = sess.Exec("UPDATE `user` SET num_repos=num_repos-1 WHERE id=?", uid); err != nil {
+ return err
+ }
+
+ if err = sess.Commit(); err != nil {
+ return fmt.Errorf("Commit: %v", err)
+ }
+
+ // Remove repository files.
+ repoPath := repo.RepoPath()
+ RemoveAllWithNotice("Delete repository files", repoPath)
+
+ repo.DeleteWiki()
+
+ // Remove attachment files.
+ for i := range attachmentPaths {
+ RemoveAllWithNotice("Delete attachment", attachmentPaths[i])
+ }
+
+ if repo.NumForks > 0 {
+ if _, err = x.Exec("UPDATE `repository` SET fork_id=0,is_fork=? WHERE fork_id=?", false, repo.ID); err != nil {
+ log.Error(4, "reset 'fork_id' and 'is_fork': %v", err)
+ }
+ }
+
+ return nil
+}
+
+// GetRepositoryByRef returns a Repository specified by a GFM reference.
+// See https://help.github.com/articles/writing-on-github#references for more information on the syntax.
+func GetRepositoryByRef(ref string) (*Repository, error) {
+ n := strings.IndexByte(ref, byte('/'))
+ if n < 2 {
+ return nil, errors.InvalidRepoReference{ref}
+ }
+
+ userName, repoName := ref[:n], ref[n+1:]
+ user, err := GetUserByName(userName)
+ if err != nil {
+ return nil, err
+ }
+
+ return GetRepositoryByName(user.ID, repoName)
+}
+
+// GetRepositoryByName returns the repository by given name under user if exists.
+func GetRepositoryByName(ownerID int64, name string) (*Repository, error) {
+ repo := &Repository{
+ OwnerID: ownerID,
+ LowerName: strings.ToLower(name),
+ }
+ has, err := x.Get(repo)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, errors.RepoNotExist{0, ownerID, name}
+ }
+ return repo, repo.LoadAttributes()
+}
+
+func getRepositoryByID(e Engine, id int64) (*Repository, error) {
+ repo := new(Repository)
+ has, err := e.ID(id).Get(repo)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, errors.RepoNotExist{id, 0, ""}
+ }
+ return repo, repo.loadAttributes(e)
+}
+
+// GetRepositoryByID returns the repository by given id if exists.
+func GetRepositoryByID(id int64) (*Repository, error) {
+ return getRepositoryByID(x, id)
+}
+
+type UserRepoOptions struct {
+ UserID int64
+ Private bool
+ Page int
+ PageSize int
+}
+
+// GetUserRepositories returns a list of repositories of given user.
+func GetUserRepositories(opts *UserRepoOptions) ([]*Repository, error) {
+ sess := x.Where("owner_id=?", opts.UserID).Desc("updated_unix")
+ if !opts.Private {
+ sess.And("is_private=?", false)
+ }
+
+ if opts.Page <= 0 {
+ opts.Page = 1
+ }
+ sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize)
+
+ repos := make([]*Repository, 0, opts.PageSize)
+ return repos, sess.Find(&repos)
+}
+
+// GetUserRepositories returns a list of mirror repositories of given user.
+func GetUserMirrorRepositories(userID int64) ([]*Repository, error) {
+ repos := make([]*Repository, 0, 10)
+ return repos, x.Where("owner_id = ?", userID).And("is_mirror = ?", true).Find(&repos)
+}
+
+// GetRecentUpdatedRepositories returns the list of repositories that are recently updated.
+func GetRecentUpdatedRepositories(page, pageSize int) (repos []*Repository, err error) {
+ return repos, x.Limit(pageSize, (page-1)*pageSize).
+ Where("is_private=?", false).Limit(pageSize).Desc("updated_unix").Find(&repos)
+}
+
+// GetUserAndCollaborativeRepositories returns list of repositories the user owns and collaborates.
+func GetUserAndCollaborativeRepositories(userID int64) ([]*Repository, error) {
+ repos := make([]*Repository, 0, 10)
+ if err := x.Alias("repo").
+ Join("INNER", "collaboration", "collaboration.repo_id = repo.id").
+ Where("collaboration.user_id = ?", userID).
+ Find(&repos); err != nil {
+ return nil, fmt.Errorf("select collaborative repositories: %v", err)
+ }
+
+ ownRepos := make([]*Repository, 0, 10)
+ if err := x.Where("owner_id = ?", userID).Find(&ownRepos); err != nil {
+ return nil, fmt.Errorf("select own repositories: %v", err)
+ }
+
+ return append(repos, ownRepos...), nil
+}
+
+func getRepositoryCount(e Engine, u *User) (int64, error) {
+ return x.Count(&Repository{OwnerID: u.ID})
+}
+
+// GetRepositoryCount returns the total number of repositories of user.
+func GetRepositoryCount(u *User) (int64, error) {
+ return getRepositoryCount(x, u)
+}
+
+type SearchRepoOptions struct {
+ Keyword string
+ OwnerID int64
+ UserID int64 // When set results will contain all public/private repositories user has access to
+ OrderBy string
+ Private bool // Include private repositories in results
+ Page int
+ PageSize int // Can be smaller than or equal to setting.ExplorePagingNum
+}
+
+// SearchRepositoryByName takes keyword and part of repository name to search,
+// it returns results in given range and number of total results.
+func SearchRepositoryByName(opts *SearchRepoOptions) (repos []*Repository, count int64, err error) {
+ if opts.Page <= 0 {
+ opts.Page = 1
+ }
+
+ repos = make([]*Repository, 0, opts.PageSize)
+ sess := x.Alias("repo")
+ // Attempt to find repositories that opts.UserID has access to,
+ // this does not include other people's private repositories even if opts.UserID is an admin.
+ if !opts.Private && opts.UserID > 0 {
+ sess.Join("LEFT", "access", "access.repo_id = repo.id").
+ Where("repo.owner_id = ? OR access.user_id = ? OR repo.is_private = ? OR (repo.is_private = ? AND (repo.allow_public_wiki = ? OR repo.allow_public_issues = ?))", opts.UserID, opts.UserID, false, true, true, true)
+ } else {
+ // Only return public repositories if opts.Private is not set
+ if !opts.Private {
+ sess.And("repo.is_private = ? OR (repo.is_private = ? AND (repo.allow_public_wiki = ? OR repo.allow_public_issues = ?))", false, true, true, true)
+ }
+ }
+ if len(opts.Keyword) > 0 {
+ sess.And("repo.lower_name LIKE ? OR repo.description LIKE ?", "%"+strings.ToLower(opts.Keyword)+"%", "%"+strings.ToLower(opts.Keyword)+"%")
+ }
+ if opts.OwnerID > 0 {
+ sess.And("repo.owner_id = ?", opts.OwnerID)
+ }
+
+ // We need all fields (repo.*) in final list but only ID (repo.id) is good enough for counting.
+ count, err = sess.Clone().Distinct("repo.id").Count(new(Repository))
+ if err != nil {
+ return nil, 0, fmt.Errorf("Count: %v", err)
+ }
+
+ if len(opts.OrderBy) > 0 {
+ sess.OrderBy("repo." + opts.OrderBy)
+ }
+ return repos, count, sess.Distinct("repo.*").Limit(opts.PageSize, (opts.Page-1)*opts.PageSize).Find(&repos)
+}
+
+func DeleteOldRepositoryArchives() {
+ if taskStatusTable.IsRunning(_CLEAN_OLD_ARCHIVES) {
+ return
+ }
+ taskStatusTable.Start(_CLEAN_OLD_ARCHIVES)
+ defer taskStatusTable.Stop(_CLEAN_OLD_ARCHIVES)
+
+ log.Trace("Doing: DeleteOldRepositoryArchives")
+
+ formats := []string{"zip", "targz"}
+ oldestTime := time.Now().Add(-setting.Cron.RepoArchiveCleanup.OlderThan)
+ if err := x.Where("id > 0").Iterate(new(Repository),
+ func(idx int, bean interface{}) error {
+ repo := bean.(*Repository)
+ basePath := filepath.Join(repo.RepoPath(), "archives")
+ for _, format := range formats {
+ dirPath := filepath.Join(basePath, format)
+ if !com.IsDir(dirPath) {
+ continue
+ }
+
+ dir, err := os.Open(dirPath)
+ if err != nil {
+ log.Error(3, "Fail to open directory '%s': %v", dirPath, err)
+ continue
+ }
+
+ fis, err := dir.Readdir(0)
+ dir.Close()
+ if err != nil {
+ log.Error(3, "Fail to read directory '%s': %v", dirPath, err)
+ continue
+ }
+
+ for _, fi := range fis {
+ if fi.IsDir() || fi.ModTime().After(oldestTime) {
+ continue
+ }
+
+ archivePath := filepath.Join(dirPath, fi.Name())
+ if err = os.Remove(archivePath); err != nil {
+ desc := fmt.Sprintf("Fail to health delete archive '%s': %v", archivePath, err)
+ log.Warn(desc)
+ if err = CreateRepositoryNotice(desc); err != nil {
+ log.Error(3, "CreateRepositoryNotice: %v", err)
+ }
+ }
+ }
+ }
+
+ return nil
+ }); err != nil {
+ log.Error(2, "DeleteOldRepositoryArchives: %v", err)
+ }
+}
+
+// DeleteRepositoryArchives deletes all repositories' archives.
+func DeleteRepositoryArchives() error {
+ if taskStatusTable.IsRunning(_CLEAN_OLD_ARCHIVES) {
+ return nil
+ }
+ taskStatusTable.Start(_CLEAN_OLD_ARCHIVES)
+ defer taskStatusTable.Stop(_CLEAN_OLD_ARCHIVES)
+
+ return x.Where("id > 0").Iterate(new(Repository),
+ func(idx int, bean interface{}) error {
+ repo := bean.(*Repository)
+ return os.RemoveAll(filepath.Join(repo.RepoPath(), "archives"))
+ })
+}
+
+func gatherMissingRepoRecords() ([]*Repository, error) {
+ repos := make([]*Repository, 0, 10)
+ if err := x.Where("id > 0").Iterate(new(Repository),
+ func(idx int, bean interface{}) error {
+ repo := bean.(*Repository)
+ if !com.IsDir(repo.RepoPath()) {
+ repos = append(repos, repo)
+ }
+ return nil
+ }); err != nil {
+ if err2 := CreateRepositoryNotice(fmt.Sprintf("gatherMissingRepoRecords: %v", err)); err2 != nil {
+ return nil, fmt.Errorf("CreateRepositoryNotice: %v", err)
+ }
+ }
+ return repos, nil
+}
+
+// DeleteMissingRepositories deletes all repository records that lost Git files.
+func DeleteMissingRepositories() error {
+ repos, err := gatherMissingRepoRecords()
+ if err != nil {
+ return fmt.Errorf("gatherMissingRepoRecords: %v", err)
+ }
+
+ if len(repos) == 0 {
+ return nil
+ }
+
+ for _, repo := range repos {
+ log.Trace("Deleting %d/%d...", repo.OwnerID, repo.ID)
+ if err := DeleteRepository(repo.OwnerID, repo.ID); err != nil {
+ if err2 := CreateRepositoryNotice(fmt.Sprintf("DeleteRepository [%d]: %v", repo.ID, err)); err2 != nil {
+ return fmt.Errorf("CreateRepositoryNotice: %v", err)
+ }
+ }
+ }
+ return nil
+}
+
+// ReinitMissingRepositories reinitializes all repository records that lost Git files.
+func ReinitMissingRepositories() error {
+ repos, err := gatherMissingRepoRecords()
+ if err != nil {
+ return fmt.Errorf("gatherMissingRepoRecords: %v", err)
+ }
+
+ if len(repos) == 0 {
+ return nil
+ }
+
+ for _, repo := range repos {
+ log.Trace("Initializing %d/%d...", repo.OwnerID, repo.ID)
+ if err := git.InitRepository(repo.RepoPath(), true); err != nil {
+ if err2 := CreateRepositoryNotice(fmt.Sprintf("InitRepository [%d]: %v", repo.ID, err)); err2 != nil {
+ return fmt.Errorf("CreateRepositoryNotice: %v", err)
+ }
+ }
+ }
+ return nil
+}
+
+// SyncRepositoryHooks rewrites all repositories' pre-receive, update and post-receive hooks
+// to make sure the binary and custom conf path are up-to-date.
+func SyncRepositoryHooks() error {
+ return x.Where("id > 0").Iterate(new(Repository),
+ func(idx int, bean interface{}) error {
+ repo := bean.(*Repository)
+ if err := createDelegateHooks(repo.RepoPath()); err != nil {
+ return err
+ }
+
+ if repo.HasWiki() {
+ return createDelegateHooks(repo.WikiPath())
+ }
+ return nil
+ })
+}
+
+// Prevent duplicate running tasks.
+var taskStatusTable = sync.NewStatusTable()
+
+const (
+ _MIRROR_UPDATE = "mirror_update"
+ _GIT_FSCK = "git_fsck"
+ _CHECK_REPO_STATS = "check_repos_stats"
+ _CLEAN_OLD_ARCHIVES = "clean_old_archives"
+)
+
+// GitFsck calls 'git fsck' to check repository health.
+func GitFsck() {
+ if taskStatusTable.IsRunning(_GIT_FSCK) {
+ return
+ }
+ taskStatusTable.Start(_GIT_FSCK)
+ defer taskStatusTable.Stop(_GIT_FSCK)
+
+ log.Trace("Doing: GitFsck")
+
+ if err := x.Where("id>0").Iterate(new(Repository),
+ func(idx int, bean interface{}) error {
+ repo := bean.(*Repository)
+ repoPath := repo.RepoPath()
+ if err := git.Fsck(repoPath, setting.Cron.RepoHealthCheck.Timeout, setting.Cron.RepoHealthCheck.Args...); err != nil {
+ desc := fmt.Sprintf("Failed to perform health check on repository '%s': %v", repoPath, err)
+ log.Warn(desc)
+ if err = CreateRepositoryNotice(desc); err != nil {
+ log.Error(3, "CreateRepositoryNotice: %v", err)
+ }
+ }
+ return nil
+ }); err != nil {
+ log.Error(2, "GitFsck: %v", err)
+ }
+}
+
+func GitGcRepos() error {
+ args := append([]string{"gc"}, setting.Git.GCArgs...)
+ 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
+ }
+ _, stderr, err := process.ExecDir(
+ time.Duration(setting.Git.Timeout.GC)*time.Second,
+ RepoPath(repo.Owner.Name, repo.Name), "Repository garbage collection",
+ "git", args...)
+ if err != nil {
+ return fmt.Errorf("%v: %v", err, stderr)
+ }
+ return nil
+ })
+}
+
+type repoChecker struct {
+ querySQL, correctSQL string
+ desc string
+}
+
+func repoStatsCheck(checker *repoChecker) {
+ results, err := x.Query(checker.querySQL)
+ if err != nil {
+ log.Error(2, "Select %s: %v", checker.desc, err)
+ return
+ }
+ for _, result := range results {
+ id := com.StrTo(result["id"]).MustInt64()
+ log.Trace("Updating %s: %d", checker.desc, id)
+ _, err = x.Exec(checker.correctSQL, id, id)
+ if err != nil {
+ log.Error(2, "Update %s[%d]: %v", checker.desc, id, err)
+ }
+ }
+}
+
+func CheckRepoStats() {
+ if taskStatusTable.IsRunning(_CHECK_REPO_STATS) {
+ return
+ }
+ taskStatusTable.Start(_CHECK_REPO_STATS)
+ defer taskStatusTable.Stop(_CHECK_REPO_STATS)
+
+ log.Trace("Doing: CheckRepoStats")
+
+ checkers := []*repoChecker{
+ // Repository.NumWatches
+ {
+ "SELECT repo.id FROM `repository` repo WHERE repo.num_watches!=(SELECT COUNT(*) FROM `watch` WHERE repo_id=repo.id)",
+ "UPDATE `repository` SET num_watches=(SELECT COUNT(*) FROM `watch` WHERE repo_id=?) WHERE id=?",
+ "repository count 'num_watches'",
+ },
+ // Repository.NumStars
+ {
+ "SELECT repo.id FROM `repository` repo WHERE repo.num_stars!=(SELECT COUNT(*) FROM `star` WHERE repo_id=repo.id)",
+ "UPDATE `repository` SET num_stars=(SELECT COUNT(*) FROM `star` WHERE repo_id=?) WHERE id=?",
+ "repository count 'num_stars'",
+ },
+ // Label.NumIssues
+ {
+ "SELECT label.id FROM `label` WHERE label.num_issues!=(SELECT COUNT(*) FROM `issue_label` WHERE label_id=label.id)",
+ "UPDATE `label` SET num_issues=(SELECT COUNT(*) FROM `issue_label` WHERE label_id=?) WHERE id=?",
+ "label count 'num_issues'",
+ },
+ // User.NumRepos
+ {
+ "SELECT `user`.id FROM `user` WHERE `user`.num_repos!=(SELECT COUNT(*) FROM `repository` WHERE owner_id=`user`.id)",
+ "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])
+ }
+
+ // ***** START: Repository.NumClosedIssues *****
+ desc := "repository count 'num_closed_issues'"
+ results, err := x.Query("SELECT repo.id FROM `repository` repo WHERE repo.num_closed_issues!=(SELECT COUNT(*) FROM `issue` WHERE repo_id=repo.id AND is_closed=? AND is_pull=?)", true, false)
+ if err != nil {
+ log.Error(2, "Select %s: %v", desc, err)
+ } else {
+ for _, result := range results {
+ id := com.StrTo(result["id"]).MustInt64()
+ log.Trace("Updating %s: %d", desc, id)
+ _, err = x.Exec("UPDATE `repository` SET num_closed_issues=(SELECT COUNT(*) FROM `issue` WHERE repo_id=? AND is_closed=? AND is_pull=?) WHERE id=?", id, true, false, id)
+ if err != nil {
+ log.Error(2, "Update %s[%d]: %v", desc, id, err)
+ }
+ }
+ }
+ // ***** END: Repository.NumClosedIssues *****
+
+ // FIXME: use checker when stop supporting old fork repo format.
+ // ***** START: Repository.NumForks *****
+ results, err = x.Query("SELECT repo.id FROM `repository` repo WHERE repo.num_forks!=(SELECT COUNT(*) FROM `repository` WHERE fork_id=repo.id)")
+ if err != nil {
+ log.Error(2, "Select repository count 'num_forks': %v", err)
+ } else {
+ for _, result := range results {
+ id := com.StrTo(result["id"]).MustInt64()
+ log.Trace("Updating repository count 'num_forks': %d", id)
+
+ repo, err := GetRepositoryByID(id)
+ if err != nil {
+ log.Error(2, "GetRepositoryByID[%d]: %v", id, err)
+ continue
+ }
+
+ rawResult, err := x.Query("SELECT COUNT(*) FROM `repository` WHERE fork_id=?", repo.ID)
+ if err != nil {
+ log.Error(2, "Select count of forks[%d]: %v", repo.ID, err)
+ continue
+ }
+ repo.NumForks = int(parseCountResult(rawResult))
+
+ if err = UpdateRepository(repo, false); err != nil {
+ log.Error(2, "UpdateRepository[%d]: %v", id, err)
+ continue
+ }
+ }
+ }
+ // ***** END: Repository.NumForks *****
+}
+
+type RepositoryList []*Repository
+
+func (repos RepositoryList) loadAttributes(e Engine) error {
+ if len(repos) == 0 {
+ return nil
+ }
+
+ // Load owners
+ userSet := make(map[int64]*User)
+ for i := range repos {
+ userSet[repos[i].OwnerID] = nil
+ }
+ userIDs := make([]int64, 0, len(userSet))
+ for userID := range userSet {
+ userIDs = append(userIDs, userID)
+ }
+ users := make([]*User, 0, len(userIDs))
+ if err := e.Where("id > 0").In("id", userIDs).Find(&users); err != nil {
+ return fmt.Errorf("find users: %v", err)
+ }
+ for i := range users {
+ userSet[users[i].ID] = users[i]
+ }
+ for i := range repos {
+ repos[i].Owner = userSet[repos[i].OwnerID]
+ }
+
+ // Load base repositories
+ repoSet := make(map[int64]*Repository)
+ for i := range repos {
+ if repos[i].IsFork {
+ repoSet[repos[i].ForkID] = nil
+ }
+ }
+ baseIDs := make([]int64, 0, len(repoSet))
+ for baseID := range repoSet {
+ baseIDs = append(baseIDs, baseID)
+ }
+ baseRepos := make([]*Repository, 0, len(baseIDs))
+ if err := e.Where("id > 0").In("id", baseIDs).Find(&baseRepos); err != nil {
+ return fmt.Errorf("find base repositories: %v", err)
+ }
+ for i := range baseRepos {
+ repoSet[baseRepos[i].ID] = baseRepos[i]
+ }
+ for i := range repos {
+ if repos[i].IsFork {
+ repos[i].BaseRepo = repoSet[repos[i].ForkID]
+ }
+ }
+
+ return nil
+}
+
+func (repos RepositoryList) LoadAttributes() error {
+ return repos.loadAttributes(x)
+}
+
+type MirrorRepositoryList []*Repository
+
+func (repos MirrorRepositoryList) loadAttributes(e Engine) error {
+ if len(repos) == 0 {
+ return nil
+ }
+
+ // Load mirrors.
+ repoIDs := make([]int64, 0, len(repos))
+ for i := range repos {
+ if !repos[i].IsMirror {
+ continue
+ }
+
+ repoIDs = append(repoIDs, repos[i].ID)
+ }
+ mirrors := make([]*Mirror, 0, len(repoIDs))
+ if err := e.Where("id > 0").In("repo_id", repoIDs).Find(&mirrors); err != nil {
+ return fmt.Errorf("find mirrors: %v", err)
+ }
+
+ set := make(map[int64]*Mirror)
+ for i := range mirrors {
+ set[mirrors[i].RepoID] = mirrors[i]
+ }
+ for i := range repos {
+ repos[i].Mirror = set[repos[i].ID]
+ }
+ return nil
+}
+
+func (repos MirrorRepositoryList) LoadAttributes() error {
+ return repos.loadAttributes(x)
+}
+
+// __ __ __ .__
+// / \ / \_____ _/ |_ ____ | |__
+// \ \/\/ /\__ \\ __\/ ___\| | \
+// \ / / __ \| | \ \___| Y \
+// \__/\ / (____ /__| \___ >___| /
+// \/ \/ \/ \/
+
+// Watch is connection request for receiving repository notification.
+type Watch struct {
+ ID int64
+ UserID int64 `xorm:"UNIQUE(watch)"`
+ RepoID int64 `xorm:"UNIQUE(watch)"`
+}
+
+func isWatching(e Engine, userID, repoID int64) bool {
+ has, _ := e.Get(&Watch{0, userID, repoID})
+ return has
+}
+
+// IsWatching checks if user has watched given repository.
+func IsWatching(userID, repoID int64) bool {
+ return isWatching(x, userID, repoID)
+}
+
+func watchRepo(e Engine, userID, repoID int64, watch bool) (err error) {
+ if watch {
+ if isWatching(e, userID, repoID) {
+ return nil
+ }
+ if _, err = e.Insert(&Watch{RepoID: repoID, UserID: userID}); err != nil {
+ return err
+ }
+ _, err = e.Exec("UPDATE `repository` SET num_watches = num_watches + 1 WHERE id = ?", repoID)
+ } else {
+ if !isWatching(e, userID, repoID) {
+ return nil
+ }
+ if _, err = e.Delete(&Watch{0, userID, repoID}); err != nil {
+ return err
+ }
+ _, err = e.Exec("UPDATE `repository` SET num_watches = num_watches - 1 WHERE id = ?", repoID)
+ }
+ return err
+}
+
+// Watch or unwatch repository.
+func WatchRepo(userID, repoID int64, watch bool) (err error) {
+ return watchRepo(x, userID, repoID, watch)
+}
+
+func getWatchers(e Engine, repoID int64) ([]*Watch, error) {
+ watches := make([]*Watch, 0, 10)
+ return watches, e.Find(&watches, &Watch{RepoID: repoID})
+}
+
+// GetWatchers returns all watchers of given repository.
+func GetWatchers(repoID int64) ([]*Watch, error) {
+ return getWatchers(x, repoID)
+}
+
+// Repository.GetWatchers returns range of users watching given repository.
+func (repo *Repository) GetWatchers(page int) ([]*User, error) {
+ users := make([]*User, 0, ItemsPerPage)
+ sess := x.Limit(ItemsPerPage, (page-1)*ItemsPerPage).Where("watch.repo_id=?", repo.ID)
+ if setting.UsePostgreSQL {
+ sess = sess.Join("LEFT", "watch", `"user".id=watch.user_id`)
+ } else {
+ sess = sess.Join("LEFT", "watch", "user.id=watch.user_id")
+ }
+ return users, sess.Find(&users)
+}
+
+func notifyWatchers(e Engine, act *Action) error {
+ // Add feeds for user self and all watchers.
+ watchers, err := getWatchers(e, act.RepoID)
+ if err != nil {
+ return fmt.Errorf("getWatchers: %v", err)
+ }
+
+ // Reset ID to reuse Action object
+ act.ID = 0
+
+ // Add feed for actioner.
+ act.UserID = act.ActUserID
+ if _, err = e.Insert(act); err != nil {
+ return fmt.Errorf("insert new action: %v", err)
+ }
+
+ for i := range watchers {
+ if act.ActUserID == watchers[i].UserID {
+ continue
+ }
+
+ act.ID = 0
+ act.UserID = watchers[i].UserID
+ if _, err = e.Insert(act); err != nil {
+ return fmt.Errorf("insert new action: %v", err)
+ }
+ }
+ return nil
+}
+
+// NotifyWatchers creates batch of actions for every watcher.
+func NotifyWatchers(act *Action) error {
+ return notifyWatchers(x, act)
+}
+
+// _________ __
+// / _____// |______ _______
+// \_____ \\ __\__ \\_ __ \
+// / \| | / __ \| | \/
+// /_______ /|__| (____ /__|
+// \/ \/
+
+type Star struct {
+ ID int64
+ UID int64 `xorm:"UNIQUE(s)"`
+ RepoID int64 `xorm:"UNIQUE(s)"`
+}
+
+// Star or unstar repository.
+func StarRepo(userID, repoID int64, star bool) (err error) {
+ if star {
+ if IsStaring(userID, repoID) {
+ return nil
+ }
+ if _, err = x.Insert(&Star{UID: userID, RepoID: repoID}); err != nil {
+ return err
+ } else if _, err = x.Exec("UPDATE `repository` SET num_stars = num_stars + 1 WHERE id = ?", repoID); err != nil {
+ return err
+ }
+ _, err = x.Exec("UPDATE `user` SET num_stars = num_stars + 1 WHERE id = ?", userID)
+ } else {
+ if !IsStaring(userID, repoID) {
+ return nil
+ }
+ if _, err = x.Delete(&Star{0, userID, repoID}); err != nil {
+ return err
+ } else if _, err = x.Exec("UPDATE `repository` SET num_stars = num_stars - 1 WHERE id = ?", repoID); err != nil {
+ return err
+ }
+ _, err = x.Exec("UPDATE `user` SET num_stars = num_stars - 1 WHERE id = ?", userID)
+ }
+ return err
+}
+
+// IsStaring checks if user has starred given repository.
+func IsStaring(userID, repoID int64) bool {
+ has, _ := x.Get(&Star{0, userID, repoID})
+ return has
+}
+
+func (repo *Repository) GetStargazers(page int) ([]*User, error) {
+ users := make([]*User, 0, ItemsPerPage)
+ sess := x.Limit(ItemsPerPage, (page-1)*ItemsPerPage).Where("star.repo_id=?", repo.ID)
+ if setting.UsePostgreSQL {
+ sess = sess.Join("LEFT", "star", `"user".id=star.uid`)
+ } else {
+ sess = sess.Join("LEFT", "star", "user.id=star.uid")
+ }
+ return users, sess.Find(&users)
+}
+
+// ___________ __
+// \_ _____/__________| | __
+// | __)/ _ \_ __ \ |/ /
+// | \( <_> ) | \/ <
+// \___ / \____/|__| |__|_ \
+// \/ \/
+
+// HasForkedRepo checks if given user has already forked a repository.
+// When user has already forked, it returns true along with the repository.
+func HasForkedRepo(ownerID, repoID int64) (*Repository, bool, error) {
+ repo := new(Repository)
+ has, err := x.Where("owner_id = ? AND fork_id = ?", ownerID, repoID).Get(repo)
+ if err != nil {
+ return nil, false, err
+ } else if !has {
+ return nil, false, nil
+ }
+ return repo, true, repo.LoadAttributes()
+}
+
+// ForkRepository creates a fork of target repository under another user domain.
+func ForkRepository(doer, owner *User, baseRepo *Repository, name, desc string) (_ *Repository, err error) {
+ if !owner.CanCreateRepo() {
+ return nil, errors.ReachLimitOfRepo{owner.RepoCreationNum()}
+ }
+
+ repo := &Repository{
+ OwnerID: owner.ID,
+ Owner: owner,
+ Name: name,
+ LowerName: strings.ToLower(name),
+ Description: desc,
+ DefaultBranch: baseRepo.DefaultBranch,
+ IsPrivate: baseRepo.IsPrivate,
+ IsFork: true,
+ ForkID: baseRepo.ID,
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return nil, err
+ }
+
+ if err = createRepository(sess, doer, owner, repo); err != nil {
+ return nil, err
+ } else if _, err = sess.Exec("UPDATE `repository` SET num_forks=num_forks+1 WHERE id=?", baseRepo.ID); err != nil {
+ return nil, err
+ }
+
+ repoPath := repo.repoPath(sess)
+ RemoveAllWithNotice("Repository path erase before creation", repoPath)
+
+ _, stderr, err := process.ExecTimeout(10*time.Minute,
+ fmt.Sprintf("ForkRepository 'git clone': %s/%s", owner.Name, repo.Name),
+ "git", "clone", "--bare", baseRepo.RepoPath(), repoPath)
+ if err != nil {
+ return nil, fmt.Errorf("git clone: %v", stderr)
+ }
+
+ _, stderr, err = process.ExecDir(-1,
+ repoPath, fmt.Sprintf("ForkRepository 'git update-server-info': %s", repoPath),
+ "git", "update-server-info")
+ if err != nil {
+ return nil, fmt.Errorf("git update-server-info: %v", err)
+ }
+
+ if err = createDelegateHooks(repoPath); err != nil {
+ return nil, fmt.Errorf("createDelegateHooks: %v", err)
+ }
+
+ if err = sess.Commit(); err != nil {
+ return nil, fmt.Errorf("Commit: %v", err)
+ }
+
+ if err = repo.UpdateSize(); err != nil {
+ log.Error(2, "UpdateSize [repo_id: %d]: %v", repo.ID, err)
+ }
+ if err = PrepareWebhooks(baseRepo, HOOK_EVENT_FORK, &api.ForkPayload{
+ Forkee: repo.APIFormat(nil),
+ Repo: baseRepo.APIFormat(nil),
+ Sender: doer.APIFormat(),
+ }); err != nil {
+ log.Error(2, "PrepareWebhooks [repo_id: %d]: %v", baseRepo.ID, err)
+ }
+ return repo, nil
+}
+
+func (repo *Repository) GetForks() ([]*Repository, error) {
+ forks := make([]*Repository, 0, repo.NumForks)
+ if err := x.Find(&forks, &Repository{ForkID: repo.ID}); err != nil {
+ return nil, err
+ }
+
+ for _, fork := range forks {
+ fork.BaseRepo = repo
+ }
+ return forks, nil
+}
+
+// __________ .__
+// \______ \____________ ____ ____ | |__
+// | | _/\_ __ \__ \ / \_/ ___\| | \
+// | | \ | | \// __ \| | \ \___| Y \
+// |______ / |__| (____ /___| /\___ >___| /
+// \/ \/ \/ \/ \/
+//
+
+func (repo *Repository) CreateNewBranch(doer *User, oldBranchName, branchName string) (err error) {
+ repoWorkingPool.CheckIn(com.ToStr(repo.ID))
+ defer repoWorkingPool.CheckOut(com.ToStr(repo.ID))
+
+ localPath := repo.LocalCopyPath()
+
+ if err = discardLocalRepoBranchChanges(localPath, oldBranchName); err != nil {
+ return fmt.Errorf("discardLocalRepoChanges: %v", err)
+ } else if err = repo.UpdateLocalCopyBranch(oldBranchName); err != nil {
+ return fmt.Errorf("UpdateLocalCopyBranch: %v", err)
+ }
+
+ if err = repo.CheckoutNewBranch(oldBranchName, branchName); err != nil {
+ return fmt.Errorf("CreateNewBranch: %v", err)
+ }
+
+ if err = git.Push(localPath, "origin", branchName); err != nil {
+ return fmt.Errorf("Push: %v", err)
+ }
+
+ return nil
+}
diff --git a/internal/db/repo_branch.go b/internal/db/repo_branch.go
new file mode 100644
index 00000000..3eb15fef
--- /dev/null
+++ b/internal/db/repo_branch.go
@@ -0,0 +1,257 @@
+// Copyright 2016 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 db
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/gogs/git-module"
+ "github.com/unknwon/com"
+
+ "gogs.io/gogs/internal/db/errors"
+ "gogs.io/gogs/internal/tool"
+)
+
+type Branch struct {
+ RepoPath string
+ Name string
+
+ IsProtected bool
+ Commit *git.Commit
+}
+
+func GetBranchesByPath(path string) ([]*Branch, error) {
+ gitRepo, err := git.OpenRepository(path)
+ if err != nil {
+ return nil, err
+ }
+
+ brs, err := gitRepo.GetBranches()
+ if err != nil {
+ return nil, err
+ }
+
+ branches := make([]*Branch, len(brs))
+ for i := range brs {
+ branches[i] = &Branch{
+ RepoPath: path,
+ Name: brs[i],
+ }
+ }
+ return branches, nil
+}
+
+func (repo *Repository) GetBranch(br string) (*Branch, error) {
+ if !git.IsBranchExist(repo.RepoPath(), br) {
+ return nil, errors.ErrBranchNotExist{br}
+ }
+ return &Branch{
+ RepoPath: repo.RepoPath(),
+ Name: br,
+ }, nil
+}
+
+func (repo *Repository) GetBranches() ([]*Branch, error) {
+ return GetBranchesByPath(repo.RepoPath())
+}
+
+func (br *Branch) GetCommit() (*git.Commit, error) {
+ gitRepo, err := git.OpenRepository(br.RepoPath)
+ if err != nil {
+ return nil, err
+ }
+ return gitRepo.GetBranchCommit(br.Name)
+}
+
+type ProtectBranchWhitelist struct {
+ ID int64
+ ProtectBranchID int64
+ RepoID int64 `xorm:"UNIQUE(protect_branch_whitelist)"`
+ Name string `xorm:"UNIQUE(protect_branch_whitelist)"`
+ UserID int64 `xorm:"UNIQUE(protect_branch_whitelist)"`
+}
+
+// IsUserInProtectBranchWhitelist returns true if given user is in the whitelist of a branch in a repository.
+func IsUserInProtectBranchWhitelist(repoID, userID int64, branch string) bool {
+ has, err := x.Where("repo_id = ?", repoID).And("user_id = ?", userID).And("name = ?", branch).Get(new(ProtectBranchWhitelist))
+ return has && err == nil
+}
+
+// ProtectBranch contains options of a protected branch.
+type ProtectBranch struct {
+ ID int64
+ RepoID int64 `xorm:"UNIQUE(protect_branch)"`
+ Name string `xorm:"UNIQUE(protect_branch)"`
+ Protected bool
+ RequirePullRequest bool
+ EnableWhitelist bool
+ WhitelistUserIDs string `xorm:"TEXT"`
+ WhitelistTeamIDs string `xorm:"TEXT"`
+}
+
+// GetProtectBranchOfRepoByName returns *ProtectBranch by branch name in given repostiory.
+func GetProtectBranchOfRepoByName(repoID int64, name string) (*ProtectBranch, error) {
+ protectBranch := &ProtectBranch{
+ RepoID: repoID,
+ Name: name,
+ }
+ has, err := x.Get(protectBranch)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, errors.ErrBranchNotExist{name}
+ }
+ return protectBranch, nil
+}
+
+// IsBranchOfRepoRequirePullRequest returns true if branch requires pull request in given repository.
+func IsBranchOfRepoRequirePullRequest(repoID int64, name string) bool {
+ protectBranch, err := GetProtectBranchOfRepoByName(repoID, name)
+ if err != nil {
+ return false
+ }
+ return protectBranch.Protected && protectBranch.RequirePullRequest
+}
+
+// UpdateProtectBranch saves branch protection options.
+// If ID is 0, it creates a new record. Otherwise, updates existing record.
+func UpdateProtectBranch(protectBranch *ProtectBranch) (err error) {
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if protectBranch.ID == 0 {
+ if _, err = sess.Insert(protectBranch); err != nil {
+ return fmt.Errorf("Insert: %v", err)
+ }
+ }
+
+ if _, err = sess.ID(protectBranch.ID).AllCols().Update(protectBranch); err != nil {
+ return fmt.Errorf("Update: %v", err)
+ }
+
+ return sess.Commit()
+}
+
+// UpdateOrgProtectBranch saves branch protection options of organizational repository.
+// If ID is 0, it creates a new record. Otherwise, updates existing record.
+// This function also performs check if whitelist user and team's IDs have been changed
+// to avoid unnecessary whitelist delete and regenerate.
+func UpdateOrgProtectBranch(repo *Repository, protectBranch *ProtectBranch, whitelistUserIDs, whitelistTeamIDs string) (err error) {
+ if err = repo.GetOwner(); err != nil {
+ return fmt.Errorf("GetOwner: %v", err)
+ } else if !repo.Owner.IsOrganization() {
+ return fmt.Errorf("expect repository owner to be an organization")
+ }
+
+ hasUsersChanged := false
+ validUserIDs := tool.StringsToInt64s(strings.Split(protectBranch.WhitelistUserIDs, ","))
+ if protectBranch.WhitelistUserIDs != whitelistUserIDs {
+ hasUsersChanged = true
+ userIDs := tool.StringsToInt64s(strings.Split(whitelistUserIDs, ","))
+ validUserIDs = make([]int64, 0, len(userIDs))
+ for _, userID := range userIDs {
+ has, err := HasAccess(userID, repo, ACCESS_MODE_WRITE)
+ if err != nil {
+ return fmt.Errorf("HasAccess [user_id: %d, repo_id: %d]: %v", userID, protectBranch.RepoID, err)
+ } else if !has {
+ continue // Drop invalid user ID
+ }
+
+ validUserIDs = append(validUserIDs, userID)
+ }
+
+ protectBranch.WhitelistUserIDs = strings.Join(tool.Int64sToStrings(validUserIDs), ",")
+ }
+
+ hasTeamsChanged := false
+ validTeamIDs := tool.StringsToInt64s(strings.Split(protectBranch.WhitelistTeamIDs, ","))
+ if protectBranch.WhitelistTeamIDs != whitelistTeamIDs {
+ hasTeamsChanged = true
+ teamIDs := tool.StringsToInt64s(strings.Split(whitelistTeamIDs, ","))
+ teams, err := GetTeamsHaveAccessToRepo(repo.OwnerID, repo.ID, ACCESS_MODE_WRITE)
+ if err != nil {
+ return fmt.Errorf("GetTeamsHaveAccessToRepo [org_id: %d, repo_id: %d]: %v", repo.OwnerID, repo.ID, err)
+ }
+ validTeamIDs = make([]int64, 0, len(teams))
+ for i := range teams {
+ if teams[i].HasWriteAccess() && com.IsSliceContainsInt64(teamIDs, teams[i].ID) {
+ validTeamIDs = append(validTeamIDs, teams[i].ID)
+ }
+ }
+
+ protectBranch.WhitelistTeamIDs = strings.Join(tool.Int64sToStrings(validTeamIDs), ",")
+ }
+
+ // Make sure protectBranch.ID is not 0 for whitelists
+ if protectBranch.ID == 0 {
+ if _, err = x.Insert(protectBranch); err != nil {
+ return fmt.Errorf("Insert: %v", err)
+ }
+ }
+
+ // Merge users and members of teams
+ var whitelists []*ProtectBranchWhitelist
+ if hasUsersChanged || hasTeamsChanged {
+ mergedUserIDs := make(map[int64]bool)
+ for _, userID := range validUserIDs {
+ // Empty whitelist users can cause an ID with 0
+ if userID != 0 {
+ mergedUserIDs[userID] = true
+ }
+ }
+
+ for _, teamID := range validTeamIDs {
+ members, err := GetTeamMembers(teamID)
+ if err != nil {
+ return fmt.Errorf("GetTeamMembers [team_id: %d]: %v", teamID, err)
+ }
+
+ for i := range members {
+ mergedUserIDs[members[i].ID] = true
+ }
+ }
+
+ whitelists = make([]*ProtectBranchWhitelist, 0, len(mergedUserIDs))
+ for userID := range mergedUserIDs {
+ whitelists = append(whitelists, &ProtectBranchWhitelist{
+ ProtectBranchID: protectBranch.ID,
+ RepoID: repo.ID,
+ Name: protectBranch.Name,
+ UserID: userID,
+ })
+ }
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if _, err = sess.ID(protectBranch.ID).AllCols().Update(protectBranch); err != nil {
+ return fmt.Errorf("Update: %v", err)
+ }
+
+ // Refresh whitelists
+ if hasUsersChanged || hasTeamsChanged {
+ if _, err = sess.Delete(&ProtectBranchWhitelist{ProtectBranchID: protectBranch.ID}); err != nil {
+ return fmt.Errorf("delete old protect branch whitelists: %v", err)
+ } else if _, err = sess.Insert(whitelists); err != nil {
+ return fmt.Errorf("insert new protect branch whitelists: %v", err)
+ }
+ }
+
+ return sess.Commit()
+}
+
+// GetProtectBranchesByRepoID returns a list of *ProtectBranch in given repostiory.
+func GetProtectBranchesByRepoID(repoID int64) ([]*ProtectBranch, error) {
+ protectBranches := make([]*ProtectBranch, 0, 2)
+ return protectBranches, x.Where("repo_id = ? and protected = ?", repoID, true).Asc("name").Find(&protectBranches)
+}
diff --git a/internal/db/repo_collaboration.go b/internal/db/repo_collaboration.go
new file mode 100644
index 00000000..84059c0a
--- /dev/null
+++ b/internal/db/repo_collaboration.go
@@ -0,0 +1,226 @@
+// Copyright 2016 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 db
+
+import (
+ "fmt"
+
+ log "gopkg.in/clog.v1"
+
+ api "github.com/gogs/go-gogs-client"
+)
+
+// Collaboration represent the relation between an individual and a repository.
+type Collaboration struct {
+ ID int64
+ RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
+ UserID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
+ Mode AccessMode `xorm:"DEFAULT 2 NOT NULL"`
+}
+
+func (c *Collaboration) ModeI18nKey() string {
+ switch c.Mode {
+ case ACCESS_MODE_READ:
+ return "repo.settings.collaboration.read"
+ case ACCESS_MODE_WRITE:
+ return "repo.settings.collaboration.write"
+ case ACCESS_MODE_ADMIN:
+ return "repo.settings.collaboration.admin"
+ default:
+ return "repo.settings.collaboration.undefined"
+ }
+}
+
+// IsCollaborator returns true if the user is a collaborator of the repository.
+func IsCollaborator(repoID, userID int64) bool {
+ collaboration := &Collaboration{
+ RepoID: repoID,
+ UserID: userID,
+ }
+ has, err := x.Get(collaboration)
+ if err != nil {
+ log.Error(2, "get collaboration [repo_id: %d, user_id: %d]: %v", repoID, userID, err)
+ return false
+ }
+ return has
+}
+
+func (repo *Repository) IsCollaborator(userID int64) bool {
+ return IsCollaborator(repo.ID, userID)
+}
+
+// AddCollaborator adds new collaboration to a repository with default access mode.
+func (repo *Repository) AddCollaborator(u *User) error {
+ collaboration := &Collaboration{
+ RepoID: repo.ID,
+ UserID: u.ID,
+ }
+
+ has, err := x.Get(collaboration)
+ if err != nil {
+ return err
+ } else if has {
+ return nil
+ }
+ collaboration.Mode = ACCESS_MODE_WRITE
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if _, err = sess.Insert(collaboration); err != nil {
+ return err
+ } else if err = repo.recalculateAccesses(sess); err != nil {
+ return fmt.Errorf("recalculateAccesses [repo_id: %v]: %v", repo.ID, err)
+ }
+
+ return sess.Commit()
+}
+
+func (repo *Repository) getCollaborations(e Engine) ([]*Collaboration, error) {
+ collaborations := make([]*Collaboration, 0)
+ return collaborations, e.Find(&collaborations, &Collaboration{RepoID: repo.ID})
+}
+
+// Collaborator represents a user with collaboration details.
+type Collaborator struct {
+ *User
+ Collaboration *Collaboration
+}
+
+func (c *Collaborator) APIFormat() *api.Collaborator {
+ return &api.Collaborator{
+ User: c.User.APIFormat(),
+ Permissions: api.Permission{
+ Admin: c.Collaboration.Mode >= ACCESS_MODE_ADMIN,
+ Push: c.Collaboration.Mode >= ACCESS_MODE_WRITE,
+ Pull: c.Collaboration.Mode >= ACCESS_MODE_READ,
+ },
+ }
+}
+
+func (repo *Repository) getCollaborators(e Engine) ([]*Collaborator, error) {
+ collaborations, err := repo.getCollaborations(e)
+ if err != nil {
+ return nil, fmt.Errorf("getCollaborations: %v", err)
+ }
+
+ collaborators := make([]*Collaborator, len(collaborations))
+ for i, c := range collaborations {
+ user, err := getUserByID(e, c.UserID)
+ if err != nil {
+ return nil, err
+ }
+ collaborators[i] = &Collaborator{
+ User: user,
+ Collaboration: c,
+ }
+ }
+ return collaborators, nil
+}
+
+// GetCollaborators returns the collaborators for a repository
+func (repo *Repository) GetCollaborators() ([]*Collaborator, error) {
+ return repo.getCollaborators(x)
+}
+
+// ChangeCollaborationAccessMode sets new access mode for the collaboration.
+func (repo *Repository) ChangeCollaborationAccessMode(userID int64, mode AccessMode) error {
+ // Discard invalid input
+ if mode <= ACCESS_MODE_NONE || mode > ACCESS_MODE_OWNER {
+ return nil
+ }
+
+ collaboration := &Collaboration{
+ RepoID: repo.ID,
+ UserID: userID,
+ }
+ has, err := x.Get(collaboration)
+ if err != nil {
+ return fmt.Errorf("get collaboration: %v", err)
+ } else if !has {
+ return nil
+ }
+
+ if collaboration.Mode == mode {
+ return nil
+ }
+ collaboration.Mode = mode
+
+ // If it's an organizational repository, merge with team access level for highest permission
+ if repo.Owner.IsOrganization() {
+ teams, err := GetUserTeams(repo.OwnerID, userID)
+ if err != nil {
+ return fmt.Errorf("GetUserTeams: [org_id: %d, user_id: %d]: %v", repo.OwnerID, userID, err)
+ }
+ for i := range teams {
+ if mode < teams[i].Authorize {
+ mode = teams[i].Authorize
+ }
+ }
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if _, err = sess.ID(collaboration.ID).AllCols().Update(collaboration); err != nil {
+ return fmt.Errorf("update collaboration: %v", err)
+ }
+
+ access := &Access{
+ UserID: userID,
+ RepoID: repo.ID,
+ }
+ has, err = sess.Get(access)
+ if err != nil {
+ return fmt.Errorf("get access record: %v", err)
+ }
+ if has {
+ _, err = sess.Exec("UPDATE access SET mode = ? WHERE user_id = ? AND repo_id = ?", mode, userID, repo.ID)
+ } else {
+ access.Mode = mode
+ _, err = sess.Insert(access)
+ }
+ if err != nil {
+ return fmt.Errorf("update/insert access table: %v", err)
+ }
+
+ return sess.Commit()
+}
+
+// DeleteCollaboration removes collaboration relation between the user and repository.
+func DeleteCollaboration(repo *Repository, userID int64) (err error) {
+ if !IsCollaborator(repo.ID, userID) {
+ return nil
+ }
+
+ collaboration := &Collaboration{
+ RepoID: repo.ID,
+ UserID: userID,
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if has, err := sess.Delete(collaboration); err != nil || has == 0 {
+ return err
+ } else if err = repo.recalculateAccesses(sess); err != nil {
+ return err
+ }
+
+ return sess.Commit()
+}
+
+func (repo *Repository) DeleteCollaboration(userID int64) error {
+ return DeleteCollaboration(repo, userID)
+}
diff --git a/internal/db/repo_editor.go b/internal/db/repo_editor.go
new file mode 100644
index 00000000..f224ac49
--- /dev/null
+++ b/internal/db/repo_editor.go
@@ -0,0 +1,518 @@
+// Copyright 2016 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 db
+
+import (
+ "fmt"
+ "io"
+ "io/ioutil"
+ "mime/multipart"
+ "os"
+ "os/exec"
+ "path"
+ "path/filepath"
+ "strings"
+ "time"
+
+ gouuid "github.com/satori/go.uuid"
+ "github.com/unknwon/com"
+
+ "github.com/gogs/git-module"
+
+ "gogs.io/gogs/internal/db/errors"
+ "gogs.io/gogs/internal/process"
+ "gogs.io/gogs/internal/setting"
+ "gogs.io/gogs/internal/tool"
+)
+
+const (
+ ENV_AUTH_USER_ID = "GOGS_AUTH_USER_ID"
+ ENV_AUTH_USER_NAME = "GOGS_AUTH_USER_NAME"
+ ENV_AUTH_USER_EMAIL = "GOGS_AUTH_USER_EMAIL"
+ ENV_REPO_OWNER_NAME = "GOGS_REPO_OWNER_NAME"
+ ENV_REPO_OWNER_SALT_MD5 = "GOGS_REPO_OWNER_SALT_MD5"
+ ENV_REPO_ID = "GOGS_REPO_ID"
+ ENV_REPO_NAME = "GOGS_REPO_NAME"
+ ENV_REPO_CUSTOM_HOOKS_PATH = "GOGS_REPO_CUSTOM_HOOKS_PATH"
+)
+
+type ComposeHookEnvsOptions struct {
+ AuthUser *User
+ OwnerName string
+ OwnerSalt string
+ RepoID int64
+ RepoName string
+ RepoPath string
+}
+
+func ComposeHookEnvs(opts ComposeHookEnvsOptions) []string {
+ envs := []string{
+ "SSH_ORIGINAL_COMMAND=1",
+ ENV_AUTH_USER_ID + "=" + com.ToStr(opts.AuthUser.ID),
+ ENV_AUTH_USER_NAME + "=" + opts.AuthUser.Name,
+ ENV_AUTH_USER_EMAIL + "=" + opts.AuthUser.Email,
+ ENV_REPO_OWNER_NAME + "=" + opts.OwnerName,
+ ENV_REPO_OWNER_SALT_MD5 + "=" + tool.MD5(opts.OwnerSalt),
+ ENV_REPO_ID + "=" + com.ToStr(opts.RepoID),
+ ENV_REPO_NAME + "=" + opts.RepoName,
+ ENV_REPO_CUSTOM_HOOKS_PATH + "=" + path.Join(opts.RepoPath, "custom_hooks"),
+ }
+ return envs
+}
+
+// ___________ .___.__ __ ___________.__.__
+// \_ _____/ __| _/|__|/ |_ \_ _____/|__| | ____
+// | __)_ / __ | | \ __\ | __) | | | _/ __ \
+// | \/ /_/ | | || | | \ | | |_\ ___/
+// /_______ /\____ | |__||__| \___ / |__|____/\___ >
+// \/ \/ \/ \/
+
+// discardLocalRepoBranchChanges discards local commits/changes of
+// given branch to make sure it is even to remote branch.
+func discardLocalRepoBranchChanges(localPath, branch string) error {
+ if !com.IsExist(localPath) {
+ return nil
+ }
+ // No need to check if nothing in the repository.
+ if !git.IsBranchExist(localPath, branch) {
+ return nil
+ }
+
+ refName := "origin/" + branch
+ if err := git.ResetHEAD(localPath, true, refName); err != nil {
+ return fmt.Errorf("git reset --hard %s: %v", refName, err)
+ }
+ return nil
+}
+
+func (repo *Repository) DiscardLocalRepoBranchChanges(branch string) error {
+ return discardLocalRepoBranchChanges(repo.LocalCopyPath(), branch)
+}
+
+// checkoutNewBranch checks out to a new branch from the a branch name.
+func checkoutNewBranch(repoPath, localPath, oldBranch, newBranch string) error {
+ if err := git.Checkout(localPath, git.CheckoutOptions{
+ Timeout: time.Duration(setting.Git.Timeout.Pull) * time.Second,
+ Branch: newBranch,
+ OldBranch: oldBranch,
+ }); err != nil {
+ return fmt.Errorf("git checkout -b %s %s: %v", newBranch, oldBranch, err)
+ }
+ return nil
+}
+
+func (repo *Repository) CheckoutNewBranch(oldBranch, newBranch string) error {
+ return checkoutNewBranch(repo.RepoPath(), repo.LocalCopyPath(), oldBranch, newBranch)
+}
+
+type UpdateRepoFileOptions struct {
+ LastCommitID string
+ OldBranch string
+ NewBranch string
+ OldTreeName string
+ NewTreeName string
+ Message string
+ Content string
+ IsNewFile bool
+}
+
+// UpdateRepoFile adds or updates a file in repository.
+func (repo *Repository) UpdateRepoFile(doer *User, opts UpdateRepoFileOptions) (err error) {
+ repoWorkingPool.CheckIn(com.ToStr(repo.ID))
+ defer repoWorkingPool.CheckOut(com.ToStr(repo.ID))
+
+ if err = repo.DiscardLocalRepoBranchChanges(opts.OldBranch); err != nil {
+ return fmt.Errorf("discard local repo branch[%s] changes: %v", opts.OldBranch, err)
+ } else if err = repo.UpdateLocalCopyBranch(opts.OldBranch); err != nil {
+ return fmt.Errorf("update local copy branch[%s]: %v", opts.OldBranch, err)
+ }
+
+ repoPath := repo.RepoPath()
+ localPath := repo.LocalCopyPath()
+
+ if opts.OldBranch != opts.NewBranch {
+ // Directly return error if new branch already exists in the server
+ if git.IsBranchExist(repoPath, opts.NewBranch) {
+ return errors.BranchAlreadyExists{opts.NewBranch}
+ }
+
+ // Otherwise, delete branch from local copy in case out of sync
+ if git.IsBranchExist(localPath, opts.NewBranch) {
+ if err = git.DeleteBranch(localPath, opts.NewBranch, git.DeleteBranchOptions{
+ Force: true,
+ }); err != nil {
+ return fmt.Errorf("delete branch[%s]: %v", opts.NewBranch, err)
+ }
+ }
+
+ if err := repo.CheckoutNewBranch(opts.OldBranch, opts.NewBranch); err != nil {
+ return fmt.Errorf("checkout new branch[%s] from old branch[%s]: %v", opts.NewBranch, opts.OldBranch, err)
+ }
+ }
+
+ oldFilePath := path.Join(localPath, opts.OldTreeName)
+ filePath := path.Join(localPath, opts.NewTreeName)
+ os.MkdirAll(path.Dir(filePath), os.ModePerm)
+
+ // If it's meant to be a new file, make sure it doesn't exist.
+ if opts.IsNewFile {
+ if com.IsExist(filePath) {
+ return ErrRepoFileAlreadyExist{filePath}
+ }
+ }
+
+ // Ignore move step if it's a new file under a directory.
+ // Otherwise, move the file when name changed.
+ if com.IsFile(oldFilePath) && opts.OldTreeName != opts.NewTreeName {
+ if err = git.MoveFile(localPath, opts.OldTreeName, opts.NewTreeName); err != nil {
+ return fmt.Errorf("git mv %q %q: %v", opts.OldTreeName, opts.NewTreeName, err)
+ }
+ }
+
+ if err = ioutil.WriteFile(filePath, []byte(opts.Content), 0666); err != nil {
+ return fmt.Errorf("write file: %v", err)
+ }
+
+ if err = git.AddChanges(localPath, true); err != nil {
+ return fmt.Errorf("git add --all: %v", err)
+ } else if err = git.CommitChanges(localPath, git.CommitChangesOptions{
+ Committer: doer.NewGitSig(),
+ Message: opts.Message,
+ }); err != nil {
+ return fmt.Errorf("commit changes on %q: %v", localPath, err)
+ } else if err = git.PushWithEnvs(localPath, "origin", opts.NewBranch,
+ ComposeHookEnvs(ComposeHookEnvsOptions{
+ AuthUser: doer,
+ OwnerName: repo.MustOwner().Name,
+ OwnerSalt: repo.MustOwner().Salt,
+ RepoID: repo.ID,
+ RepoName: repo.Name,
+ RepoPath: repo.RepoPath(),
+ })); err != nil {
+ return fmt.Errorf("git push origin %s: %v", opts.NewBranch, err)
+ }
+ return nil
+}
+
+// GetDiffPreview produces and returns diff result of a file which is not yet committed.
+func (repo *Repository) GetDiffPreview(branch, treePath, content string) (diff *Diff, err error) {
+ repoWorkingPool.CheckIn(com.ToStr(repo.ID))
+ defer repoWorkingPool.CheckOut(com.ToStr(repo.ID))
+
+ if err = repo.DiscardLocalRepoBranchChanges(branch); err != nil {
+ return nil, fmt.Errorf("discard local repo branch[%s] changes: %v", branch, err)
+ } else if err = repo.UpdateLocalCopyBranch(branch); err != nil {
+ return nil, fmt.Errorf("update local copy branch[%s]: %v", branch, err)
+ }
+
+ localPath := repo.LocalCopyPath()
+ filePath := path.Join(localPath, treePath)
+ os.MkdirAll(filepath.Dir(filePath), os.ModePerm)
+ if err = ioutil.WriteFile(filePath, []byte(content), 0666); err != nil {
+ return nil, fmt.Errorf("write file: %v", err)
+ }
+
+ cmd := exec.Command("git", "diff", treePath)
+ cmd.Dir = localPath
+ cmd.Stderr = os.Stderr
+
+ stdout, err := cmd.StdoutPipe()
+ if err != nil {
+ return nil, fmt.Errorf("get stdout pipe: %v", err)
+ }
+
+ if err = cmd.Start(); err != nil {
+ return nil, fmt.Errorf("start: %v", err)
+ }
+
+ pid := process.Add(fmt.Sprintf("GetDiffPreview [repo_path: %s]", repo.RepoPath()), cmd)
+ defer process.Remove(pid)
+
+ diff, err = ParsePatch(setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, stdout)
+ if err != nil {
+ return nil, fmt.Errorf("parse path: %v", err)
+ }
+
+ if err = cmd.Wait(); err != nil {
+ return nil, fmt.Errorf("wait: %v", err)
+ }
+
+ return diff, nil
+}
+
+// ________ .__ __ ___________.__.__
+// \______ \ ____ | | _____/ |_ ____ \_ _____/|__| | ____
+// | | \_/ __ \| | _/ __ \ __\/ __ \ | __) | | | _/ __ \
+// | ` \ ___/| |_\ ___/| | \ ___/ | \ | | |_\ ___/
+// /_______ /\___ >____/\___ >__| \___ > \___ / |__|____/\___ >
+// \/ \/ \/ \/ \/ \/
+//
+
+type DeleteRepoFileOptions struct {
+ LastCommitID string
+ OldBranch string
+ NewBranch string
+ TreePath string
+ Message string
+}
+
+func (repo *Repository) DeleteRepoFile(doer *User, opts DeleteRepoFileOptions) (err error) {
+ repoWorkingPool.CheckIn(com.ToStr(repo.ID))
+ defer repoWorkingPool.CheckOut(com.ToStr(repo.ID))
+
+ if err = repo.DiscardLocalRepoBranchChanges(opts.OldBranch); err != nil {
+ return fmt.Errorf("discard local repo branch[%s] changes: %v", opts.OldBranch, err)
+ } else if err = repo.UpdateLocalCopyBranch(opts.OldBranch); err != nil {
+ return fmt.Errorf("update local copy branch[%s]: %v", opts.OldBranch, err)
+ }
+
+ if opts.OldBranch != opts.NewBranch {
+ if err := repo.CheckoutNewBranch(opts.OldBranch, opts.NewBranch); err != nil {
+ return fmt.Errorf("checkout new branch[%s] from old branch[%s]: %v", opts.NewBranch, opts.OldBranch, err)
+ }
+ }
+
+ localPath := repo.LocalCopyPath()
+ if err = os.Remove(path.Join(localPath, opts.TreePath)); err != nil {
+ return fmt.Errorf("remove file %q: %v", opts.TreePath, err)
+ }
+
+ if err = git.AddChanges(localPath, true); err != nil {
+ return fmt.Errorf("git add --all: %v", err)
+ } else if err = git.CommitChanges(localPath, git.CommitChangesOptions{
+ Committer: doer.NewGitSig(),
+ Message: opts.Message,
+ }); err != nil {
+ return fmt.Errorf("commit changes to %q: %v", localPath, err)
+ } else if err = git.PushWithEnvs(localPath, "origin", opts.NewBranch,
+ ComposeHookEnvs(ComposeHookEnvsOptions{
+ AuthUser: doer,
+ OwnerName: repo.MustOwner().Name,
+ OwnerSalt: repo.MustOwner().Salt,
+ RepoID: repo.ID,
+ RepoName: repo.Name,
+ RepoPath: repo.RepoPath(),
+ })); err != nil {
+ return fmt.Errorf("git push origin %s: %v", opts.NewBranch, err)
+ }
+ return nil
+}
+
+// ____ ___ .__ .___ ___________.___.__
+// | | \______ | | _________ __| _/ \_ _____/| | | ____ ______
+// | | /\____ \| | / _ \__ \ / __ | | __) | | | _/ __ \ / ___/
+// | | / | |_> > |_( <_> ) __ \_/ /_/ | | \ | | |_\ ___/ \___ \
+// |______/ | __/|____/\____(____ /\____ | \___ / |___|____/\___ >____ >
+// |__| \/ \/ \/ \/ \/
+//
+
+// Upload represent a uploaded file to a repo to be deleted when moved
+type Upload struct {
+ ID int64
+ UUID string `xorm:"uuid UNIQUE"`
+ Name string
+}
+
+// UploadLocalPath returns where uploads is stored in local file system based on given UUID.
+func UploadLocalPath(uuid string) string {
+ return path.Join(setting.Repository.Upload.TempPath, uuid[0:1], uuid[1:2], uuid)
+}
+
+// LocalPath returns where uploads are temporarily stored in local file system.
+func (upload *Upload) LocalPath() string {
+ return UploadLocalPath(upload.UUID)
+}
+
+// NewUpload creates a new upload object.
+func NewUpload(name string, buf []byte, file multipart.File) (_ *Upload, err error) {
+ if tool.IsMaliciousPath(name) {
+ return nil, fmt.Errorf("malicious path detected: %s", name)
+ }
+
+ upload := &Upload{
+ UUID: gouuid.NewV4().String(),
+ Name: name,
+ }
+
+ localPath := upload.LocalPath()
+ if err = os.MkdirAll(path.Dir(localPath), os.ModePerm); err != nil {
+ return nil, fmt.Errorf("mkdir all: %v", err)
+ }
+
+ fw, err := os.Create(localPath)
+ if err != nil {
+ return nil, fmt.Errorf("create: %v", err)
+ }
+ defer fw.Close()
+
+ if _, err = fw.Write(buf); err != nil {
+ return nil, fmt.Errorf("write: %v", err)
+ } else if _, err = io.Copy(fw, file); err != nil {
+ return nil, fmt.Errorf("copy: %v", err)
+ }
+
+ if _, err := x.Insert(upload); err != nil {
+ return nil, err
+ }
+
+ return upload, nil
+}
+
+func GetUploadByUUID(uuid string) (*Upload, error) {
+ upload := &Upload{UUID: uuid}
+ has, err := x.Get(upload)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, ErrUploadNotExist{0, uuid}
+ }
+ return upload, nil
+}
+
+func GetUploadsByUUIDs(uuids []string) ([]*Upload, error) {
+ if len(uuids) == 0 {
+ return []*Upload{}, nil
+ }
+
+ // Silently drop invalid uuids.
+ uploads := make([]*Upload, 0, len(uuids))
+ return uploads, x.In("uuid", uuids).Find(&uploads)
+}
+
+func DeleteUploads(uploads ...*Upload) (err error) {
+ if len(uploads) == 0 {
+ return nil
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ ids := make([]int64, len(uploads))
+ for i := 0; i < len(uploads); i++ {
+ ids[i] = uploads[i].ID
+ }
+ if _, err = sess.In("id", ids).Delete(new(Upload)); err != nil {
+ return fmt.Errorf("delete uploads: %v", err)
+ }
+
+ for _, upload := range uploads {
+ localPath := upload.LocalPath()
+ if !com.IsFile(localPath) {
+ continue
+ }
+
+ if err := os.Remove(localPath); err != nil {
+ return fmt.Errorf("remove upload: %v", err)
+ }
+ }
+
+ return sess.Commit()
+}
+
+func DeleteUpload(u *Upload) error {
+ return DeleteUploads(u)
+}
+
+func DeleteUploadByUUID(uuid string) error {
+ upload, err := GetUploadByUUID(uuid)
+ if err != nil {
+ if IsErrUploadNotExist(err) {
+ return nil
+ }
+ return fmt.Errorf("get upload by UUID[%s]: %v", uuid, err)
+ }
+
+ if err := DeleteUpload(upload); err != nil {
+ return fmt.Errorf("delete upload: %v", err)
+ }
+
+ return nil
+}
+
+type UploadRepoFileOptions struct {
+ LastCommitID string
+ OldBranch string
+ NewBranch string
+ TreePath string
+ Message string
+ Files []string // In UUID format
+}
+
+// isRepositoryGitPath returns true if given path is or resides inside ".git" path of the repository.
+func isRepositoryGitPath(path string) bool {
+ return strings.HasSuffix(path, ".git") || strings.Contains(path, ".git"+string(os.PathSeparator))
+}
+
+func (repo *Repository) UploadRepoFiles(doer *User, opts UploadRepoFileOptions) (err error) {
+ if len(opts.Files) == 0 {
+ return nil
+ }
+
+ uploads, err := GetUploadsByUUIDs(opts.Files)
+ if err != nil {
+ return fmt.Errorf("get uploads by UUIDs[%v]: %v", opts.Files, err)
+ }
+
+ repoWorkingPool.CheckIn(com.ToStr(repo.ID))
+ defer repoWorkingPool.CheckOut(com.ToStr(repo.ID))
+
+ if err = repo.DiscardLocalRepoBranchChanges(opts.OldBranch); err != nil {
+ return fmt.Errorf("discard local repo branch[%s] changes: %v", opts.OldBranch, err)
+ } else if err = repo.UpdateLocalCopyBranch(opts.OldBranch); err != nil {
+ return fmt.Errorf("update local copy branch[%s]: %v", opts.OldBranch, err)
+ }
+
+ if opts.OldBranch != opts.NewBranch {
+ if err = repo.CheckoutNewBranch(opts.OldBranch, opts.NewBranch); err != nil {
+ return fmt.Errorf("checkout new branch[%s] from old branch[%s]: %v", opts.NewBranch, opts.OldBranch, err)
+ }
+ }
+
+ localPath := repo.LocalCopyPath()
+ dirPath := path.Join(localPath, opts.TreePath)
+ os.MkdirAll(dirPath, os.ModePerm)
+
+ // Copy uploaded files into repository
+ for _, upload := range uploads {
+ tmpPath := upload.LocalPath()
+ if !com.IsFile(tmpPath) {
+ continue
+ }
+
+ // Prevent copying files into .git directory, see https://gogs.io/gogs/issues/5558.
+ if isRepositoryGitPath(upload.Name) {
+ continue
+ }
+
+ targetPath := path.Join(dirPath, upload.Name)
+ if err = com.Copy(tmpPath, targetPath); err != nil {
+ return fmt.Errorf("copy: %v", err)
+ }
+ }
+
+ if err = git.AddChanges(localPath, true); err != nil {
+ return fmt.Errorf("git add --all: %v", err)
+ } else if err = git.CommitChanges(localPath, git.CommitChangesOptions{
+ Committer: doer.NewGitSig(),
+ Message: opts.Message,
+ }); err != nil {
+ return fmt.Errorf("commit changes on %q: %v", localPath, err)
+ } else if err = git.PushWithEnvs(localPath, "origin", opts.NewBranch,
+ ComposeHookEnvs(ComposeHookEnvsOptions{
+ AuthUser: doer,
+ OwnerName: repo.MustOwner().Name,
+ OwnerSalt: repo.MustOwner().Salt,
+ RepoID: repo.ID,
+ RepoName: repo.Name,
+ RepoPath: repo.RepoPath(),
+ })); err != nil {
+ return fmt.Errorf("git push origin %s: %v", opts.NewBranch, err)
+ }
+
+ return DeleteUploads(uploads...)
+}
diff --git a/internal/db/repo_editor_test.go b/internal/db/repo_editor_test.go
new file mode 100644
index 00000000..18e844d0
--- /dev/null
+++ b/internal/db/repo_editor_test.go
@@ -0,0 +1,34 @@
+// Copyright 2018 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 db
+
+import (
+ "os"
+ "testing"
+
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+func Test_isRepositoryGitPath(t *testing.T) {
+ Convey("Check if path is or resides inside '.git'", t, func() {
+ sep := string(os.PathSeparator)
+ testCases := []struct {
+ path string
+ expect bool
+ }{
+ {"." + sep + ".git", true},
+ {"." + sep + ".git" + sep + "", true},
+ {"." + sep + ".git" + sep + "hooks" + sep + "pre-commit", true},
+ {".git" + sep + "hooks", true},
+ {"dir" + sep + ".git", true},
+
+ {".gitignore", false},
+ {"dir" + sep + ".gitkeep", false},
+ }
+ for _, tc := range testCases {
+ So(isRepositoryGitPath(tc.path), ShouldEqual, tc.expect)
+ }
+ })
+}
diff --git a/internal/db/repo_test.go b/internal/db/repo_test.go
new file mode 100644
index 00000000..c239e74d
--- /dev/null
+++ b/internal/db/repo_test.go
@@ -0,0 +1,63 @@
+package db_test
+
+import (
+ "testing"
+
+ . "github.com/smartystreets/goconvey/convey"
+
+ "gogs.io/gogs/internal/db"
+ "gogs.io/gogs/internal/markup"
+)
+
+func TestRepo(t *testing.T) {
+ Convey("The metas map", t, func() {
+ var repo = new(db.Repository)
+ repo.Name = "testrepo"
+ repo.Owner = new(db.User)
+ repo.Owner.Name = "testuser"
+ repo.ExternalTrackerFormat = "https://someurl.com/{user}/{repo}/{issue}"
+
+ Convey("When no external tracker is configured", func() {
+ Convey("It should be nil", func() {
+ repo.EnableExternalTracker = false
+ So(repo.ComposeMetas(), ShouldEqual, map[string]string(nil))
+ })
+ Convey("It should be nil even if other settings are present", func() {
+ repo.EnableExternalTracker = false
+ repo.ExternalTrackerFormat = "http://someurl.com/{user}/{repo}/{issue}"
+ repo.ExternalTrackerStyle = markup.ISSUE_NAME_STYLE_NUMERIC
+ So(repo.ComposeMetas(), ShouldEqual, map[string]string(nil))
+ })
+ })
+
+ Convey("When an external issue tracker is configured", func() {
+ repo.EnableExternalTracker = true
+ Convey("It should default to numeric issue style", func() {
+ metas := repo.ComposeMetas()
+ So(metas["style"], ShouldEqual, markup.ISSUE_NAME_STYLE_NUMERIC)
+ })
+ Convey("It should pass through numeric issue style setting", func() {
+ repo.ExternalTrackerStyle = markup.ISSUE_NAME_STYLE_NUMERIC
+ metas := repo.ComposeMetas()
+ So(metas["style"], ShouldEqual, markup.ISSUE_NAME_STYLE_NUMERIC)
+ })
+ Convey("It should pass through alphanumeric issue style setting", func() {
+ repo.ExternalTrackerStyle = markup.ISSUE_NAME_STYLE_ALPHANUMERIC
+ metas := repo.ComposeMetas()
+ So(metas["style"], ShouldEqual, markup.ISSUE_NAME_STYLE_ALPHANUMERIC)
+ })
+ Convey("It should contain the user name", func() {
+ metas := repo.ComposeMetas()
+ So(metas["user"], ShouldEqual, "testuser")
+ })
+ Convey("It should contain the repo name", func() {
+ metas := repo.ComposeMetas()
+ So(metas["repo"], ShouldEqual, "testrepo")
+ })
+ Convey("It should contain the URL format", func() {
+ metas := repo.ComposeMetas()
+ So(metas["format"], ShouldEqual, "https://someurl.com/{user}/{repo}/{issue}")
+ })
+ })
+ })
+}
diff --git a/internal/db/ssh_key.go b/internal/db/ssh_key.go
new file mode 100644
index 00000000..317d90d3
--- /dev/null
+++ b/internal/db/ssh_key.go
@@ -0,0 +1,771 @@
+// Copyright 2014 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 db
+
+import (
+ "encoding/base64"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "math/big"
+ "os"
+ "path"
+ "path/filepath"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/unknwon/com"
+ "golang.org/x/crypto/ssh"
+ log "gopkg.in/clog.v1"
+ "xorm.io/xorm"
+
+ "gogs.io/gogs/internal/process"
+ "gogs.io/gogs/internal/setting"
+)
+
+const (
+ _TPL_PUBLICK_KEY = `command="%s serv key-%d --config='%s'",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty %s` + "\n"
+)
+
+var sshOpLocker sync.Mutex
+
+type KeyType int
+
+const (
+ KEY_TYPE_USER = iota + 1
+ KEY_TYPE_DEPLOY
+)
+
+// PublicKey represents a user or deploy SSH public key.
+type PublicKey struct {
+ ID int64
+ OwnerID int64 `xorm:"INDEX NOT NULL"`
+ Name string `xorm:"NOT NULL"`
+ Fingerprint string `xorm:"NOT NULL"`
+ Content string `xorm:"TEXT NOT NULL"`
+ Mode AccessMode `xorm:"NOT NULL DEFAULT 2"`
+ Type KeyType `xorm:"NOT NULL DEFAULT 1"`
+
+ Created time.Time `xorm:"-" json:"-"`
+ CreatedUnix int64
+ Updated time.Time `xorm:"-" json:"-"` // Note: Updated must below Created for AfterSet.
+ UpdatedUnix int64
+ HasRecentActivity bool `xorm:"-" json:"-"`
+ HasUsed bool `xorm:"-" json:"-"`
+}
+
+func (k *PublicKey) BeforeInsert() {
+ k.CreatedUnix = time.Now().Unix()
+}
+
+func (k *PublicKey) BeforeUpdate() {
+ k.UpdatedUnix = time.Now().Unix()
+}
+
+func (k *PublicKey) AfterSet(colName string, _ xorm.Cell) {
+ switch colName {
+ case "created_unix":
+ k.Created = time.Unix(k.CreatedUnix, 0).Local()
+ case "updated_unix":
+ k.Updated = time.Unix(k.UpdatedUnix, 0).Local()
+ k.HasUsed = k.Updated.After(k.Created)
+ k.HasRecentActivity = k.Updated.Add(7 * 24 * time.Hour).After(time.Now())
+ }
+}
+
+// OmitEmail returns content of public key without email address.
+func (k *PublicKey) OmitEmail() string {
+ return strings.Join(strings.Split(k.Content, " ")[:2], " ")
+}
+
+// AuthorizedString returns formatted public key string for authorized_keys file.
+func (k *PublicKey) AuthorizedString() string {
+ return fmt.Sprintf(_TPL_PUBLICK_KEY, setting.AppPath, k.ID, setting.CustomConf, k.Content)
+}
+
+// IsDeployKey returns true if the public key is used as deploy key.
+func (k *PublicKey) IsDeployKey() bool {
+ return k.Type == KEY_TYPE_DEPLOY
+}
+
+func extractTypeFromBase64Key(key string) (string, error) {
+ b, err := base64.StdEncoding.DecodeString(key)
+ if err != nil || len(b) < 4 {
+ return "", fmt.Errorf("invalid key format: %v", err)
+ }
+
+ keyLength := int(binary.BigEndian.Uint32(b))
+ if len(b) < 4+keyLength {
+ return "", fmt.Errorf("invalid key format: not enough length %d", keyLength)
+ }
+
+ return string(b[4 : 4+keyLength]), nil
+}
+
+// parseKeyString parses any key string in OpenSSH or SSH2 format to clean OpenSSH string (RFC4253).
+func parseKeyString(content string) (string, error) {
+ // Transform all legal line endings to a single "\n"
+
+ // Replace all windows full new lines ("\r\n")
+ content = strings.Replace(content, "\r\n", "\n", -1)
+
+ // Replace all windows half new lines ("\r"), if it happen not to match replace above
+ content = strings.Replace(content, "\r", "\n", -1)
+
+ // Replace ending new line as its may cause unwanted behaviour (extra line means not a single line key | OpenSSH key)
+ content = strings.TrimRight(content, "\n")
+
+ // split lines
+ lines := strings.Split(content, "\n")
+
+ var keyType, keyContent, keyComment string
+
+ if len(lines) == 1 {
+ // Parse OpenSSH format.
+ parts := strings.SplitN(lines[0], " ", 3)
+ switch len(parts) {
+ case 0:
+ return "", errors.New("empty key")
+ case 1:
+ keyContent = parts[0]
+ case 2:
+ keyType = parts[0]
+ keyContent = parts[1]
+ default:
+ keyType = parts[0]
+ keyContent = parts[1]
+ keyComment = parts[2]
+ }
+
+ // If keyType is not given, extract it from content. If given, validate it.
+ t, err := extractTypeFromBase64Key(keyContent)
+ if err != nil {
+ return "", fmt.Errorf("extractTypeFromBase64Key: %v", err)
+ }
+ if len(keyType) == 0 {
+ keyType = t
+ } else if keyType != t {
+ return "", fmt.Errorf("key type and content does not match: %s - %s", keyType, t)
+ }
+ } else {
+ // Parse SSH2 file format.
+ continuationLine := false
+
+ for _, line := range lines {
+ // Skip lines that:
+ // 1) are a continuation of the previous line,
+ // 2) contain ":" as that are comment lines
+ // 3) contain "-" as that are begin and end tags
+ if continuationLine || strings.ContainsAny(line, ":-") {
+ continuationLine = strings.HasSuffix(line, "\\")
+ } else {
+ keyContent = keyContent + line
+ }
+ }
+
+ t, err := extractTypeFromBase64Key(keyContent)
+ if err != nil {
+ return "", fmt.Errorf("extractTypeFromBase64Key: %v", err)
+ }
+ keyType = t
+ }
+ return keyType + " " + keyContent + " " + keyComment, nil
+}
+
+// writeTmpKeyFile writes key content to a temporary file
+// and returns the name of that file, along with any possible errors.
+func writeTmpKeyFile(content string) (string, error) {
+ tmpFile, err := ioutil.TempFile(setting.SSH.KeyTestPath, "gogs_keytest")
+ if err != nil {
+ return "", fmt.Errorf("TempFile: %v", err)
+ }
+ defer tmpFile.Close()
+
+ if _, err = tmpFile.WriteString(content); err != nil {
+ return "", fmt.Errorf("WriteString: %v", err)
+ }
+ return tmpFile.Name(), nil
+}
+
+// SSHKeyGenParsePublicKey extracts key type and length using ssh-keygen.
+func SSHKeyGenParsePublicKey(key string) (string, int, error) {
+ tmpName, err := writeTmpKeyFile(key)
+ if err != nil {
+ return "", 0, fmt.Errorf("writeTmpKeyFile: %v", err)
+ }
+ defer os.Remove(tmpName)
+
+ stdout, stderr, err := process.Exec("SSHKeyGenParsePublicKey", setting.SSH.KeygenPath, "-lf", tmpName)
+ if err != nil {
+ return "", 0, fmt.Errorf("fail to parse public key: %s - %s", err, stderr)
+ }
+ if strings.Contains(stdout, "is not a public key file") {
+ return "", 0, ErrKeyUnableVerify{stdout}
+ }
+
+ fields := strings.Split(stdout, " ")
+ if len(fields) < 4 {
+ return "", 0, fmt.Errorf("invalid public key line: %s", stdout)
+ }
+
+ keyType := strings.Trim(fields[len(fields)-1], "()\r\n")
+ return strings.ToLower(keyType), com.StrTo(fields[0]).MustInt(), nil
+}
+
+// SSHNativeParsePublicKey extracts the key type and length using the golang SSH library.
+func SSHNativeParsePublicKey(keyLine string) (string, int, error) {
+ fields := strings.Fields(keyLine)
+ if len(fields) < 2 {
+ return "", 0, fmt.Errorf("not enough fields in public key line: %s", string(keyLine))
+ }
+
+ raw, err := base64.StdEncoding.DecodeString(fields[1])
+ if err != nil {
+ return "", 0, err
+ }
+
+ pkey, err := ssh.ParsePublicKey(raw)
+ if err != nil {
+ if strings.Contains(err.Error(), "ssh: unknown key algorithm") {
+ return "", 0, ErrKeyUnableVerify{err.Error()}
+ }
+ return "", 0, fmt.Errorf("ParsePublicKey: %v", err)
+ }
+
+ // The ssh library can parse the key, so next we find out what key exactly we have.
+ switch pkey.Type() {
+ case ssh.KeyAlgoDSA:
+ rawPub := struct {
+ Name string
+ P, Q, G, Y *big.Int
+ }{}
+ if err := ssh.Unmarshal(pkey.Marshal(), &rawPub); err != nil {
+ return "", 0, err
+ }
+ // as per https://bugzilla.mindrot.org/show_bug.cgi?id=1647 we should never
+ // see dsa keys != 1024 bit, but as it seems to work, we will not check here
+ return "dsa", rawPub.P.BitLen(), nil // use P as per crypto/dsa/dsa.go (is L)
+ case ssh.KeyAlgoRSA:
+ rawPub := struct {
+ Name string
+ E *big.Int
+ N *big.Int
+ }{}
+ if err := ssh.Unmarshal(pkey.Marshal(), &rawPub); err != nil {
+ return "", 0, err
+ }
+ return "rsa", rawPub.N.BitLen(), nil // use N as per crypto/rsa/rsa.go (is bits)
+ case ssh.KeyAlgoECDSA256:
+ return "ecdsa", 256, nil
+ case ssh.KeyAlgoECDSA384:
+ return "ecdsa", 384, nil
+ case ssh.KeyAlgoECDSA521:
+ return "ecdsa", 521, nil
+ case ssh.KeyAlgoED25519:
+ return "ed25519", 256, nil
+ }
+ return "", 0, fmt.Errorf("unsupported key length detection for type: %s", pkey.Type())
+}
+
+// CheckPublicKeyString checks if the given public key string is recognized by SSH.
+// It returns the actual public key line on success.
+func CheckPublicKeyString(content string) (_ string, err error) {
+ if setting.SSH.Disabled {
+ return "", errors.New("SSH is disabled")
+ }
+
+ content, err = parseKeyString(content)
+ if err != nil {
+ return "", err
+ }
+
+ content = strings.TrimRight(content, "\n\r")
+ if strings.ContainsAny(content, "\n\r") {
+ return "", errors.New("only a single line with a single key please")
+ }
+
+ // Remove any unnecessary whitespace
+ content = strings.TrimSpace(content)
+
+ if !setting.SSH.MinimumKeySizeCheck {
+ return content, nil
+ }
+
+ var (
+ fnName string
+ keyType string
+ length int
+ )
+ if setting.SSH.StartBuiltinServer {
+ fnName = "SSHNativeParsePublicKey"
+ keyType, length, err = SSHNativeParsePublicKey(content)
+ } else {
+ fnName = "SSHKeyGenParsePublicKey"
+ keyType, length, err = SSHKeyGenParsePublicKey(content)
+ }
+ if err != nil {
+ return "", fmt.Errorf("%s: %v", fnName, err)
+ }
+ log.Trace("Key info [native: %v]: %s-%d", setting.SSH.StartBuiltinServer, keyType, length)
+
+ if minLen, found := setting.SSH.MinimumKeySizes[keyType]; found && length >= minLen {
+ return content, nil
+ } else if found && length < minLen {
+ return "", fmt.Errorf("key length is not enough: got %d, needs %d", length, minLen)
+ }
+ return "", fmt.Errorf("key type is not allowed: %s", keyType)
+}
+
+// appendAuthorizedKeysToFile appends new SSH keys' content to authorized_keys file.
+func appendAuthorizedKeysToFile(keys ...*PublicKey) error {
+ sshOpLocker.Lock()
+ defer sshOpLocker.Unlock()
+
+ fpath := filepath.Join(setting.SSH.RootPath, "authorized_keys")
+ f, err := os.OpenFile(fpath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ // Note: chmod command does not support in Windows.
+ if !setting.IsWindows {
+ fi, err := f.Stat()
+ if err != nil {
+ return err
+ }
+
+ // .ssh directory should have mode 700, and authorized_keys file should have mode 600.
+ if fi.Mode().Perm() > 0600 {
+ log.Error(4, "authorized_keys file has unusual permission flags: %s - setting to -rw-------", fi.Mode().Perm().String())
+ if err = f.Chmod(0600); err != nil {
+ return err
+ }
+ }
+ }
+
+ for _, key := range keys {
+ if _, err = f.WriteString(key.AuthorizedString()); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// checkKeyContent onlys checks if key content has been used as public key,
+// it is OK to use same key as deploy key for multiple repositories/users.
+func checkKeyContent(content string) error {
+ has, err := x.Get(&PublicKey{
+ Content: content,
+ Type: KEY_TYPE_USER,
+ })
+ if err != nil {
+ return err
+ } else if has {
+ return ErrKeyAlreadyExist{0, content}
+ }
+ return nil
+}
+
+func addKey(e Engine, key *PublicKey) (err error) {
+ // Calculate fingerprint.
+ tmpPath := strings.Replace(path.Join(os.TempDir(), fmt.Sprintf("%d", time.Now().Nanosecond()),
+ "id_rsa.pub"), "\\", "/", -1)
+ os.MkdirAll(path.Dir(tmpPath), os.ModePerm)
+ if err = ioutil.WriteFile(tmpPath, []byte(key.Content), 0644); err != nil {
+ return err
+ }
+
+ stdout, stderr, err := process.Exec("AddPublicKey", setting.SSH.KeygenPath, "-lf", tmpPath)
+ if err != nil {
+ return fmt.Errorf("fail to parse public key: %s - %s", err, stderr)
+ } else if len(stdout) < 2 {
+ return errors.New("not enough output for calculating fingerprint: " + stdout)
+ }
+ key.Fingerprint = strings.Split(stdout, " ")[1]
+
+ // Save SSH key.
+ if _, err = e.Insert(key); err != nil {
+ return err
+ }
+
+ // Don't need to rewrite this file if builtin SSH server is enabled.
+ if setting.SSH.StartBuiltinServer {
+ return nil
+ }
+ return appendAuthorizedKeysToFile(key)
+}
+
+// AddPublicKey adds new public key to database and authorized_keys file.
+func AddPublicKey(ownerID int64, name, content string) (*PublicKey, error) {
+ log.Trace(content)
+ if err := checkKeyContent(content); err != nil {
+ return nil, err
+ }
+
+ // Key name of same user cannot be duplicated.
+ has, err := x.Where("owner_id = ? AND name = ?", ownerID, name).Get(new(PublicKey))
+ if err != nil {
+ return nil, err
+ } else if has {
+ return nil, ErrKeyNameAlreadyUsed{ownerID, name}
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return nil, err
+ }
+
+ key := &PublicKey{
+ OwnerID: ownerID,
+ Name: name,
+ Content: content,
+ Mode: ACCESS_MODE_WRITE,
+ Type: KEY_TYPE_USER,
+ }
+ if err = addKey(sess, key); err != nil {
+ return nil, fmt.Errorf("addKey: %v", err)
+ }
+
+ return key, sess.Commit()
+}
+
+// GetPublicKeyByID returns public key by given ID.
+func GetPublicKeyByID(keyID int64) (*PublicKey, error) {
+ key := new(PublicKey)
+ has, err := x.Id(keyID).Get(key)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, ErrKeyNotExist{keyID}
+ }
+ 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)
+ return keys, x.Where("owner_id = ?", uid).Find(&keys)
+}
+
+// UpdatePublicKey updates given public key.
+func UpdatePublicKey(key *PublicKey) error {
+ _, err := x.Id(key.ID).AllCols().Update(key)
+ return err
+}
+
+// deletePublicKeys does the actual key deletion but does not update authorized_keys file.
+func deletePublicKeys(e *xorm.Session, keyIDs ...int64) error {
+ if len(keyIDs) == 0 {
+ return nil
+ }
+
+ _, err := e.In("id", keyIDs).Delete(new(PublicKey))
+ return err
+}
+
+// DeletePublicKey deletes SSH key information both in database and authorized_keys file.
+func DeletePublicKey(doer *User, id int64) (err error) {
+ key, err := GetPublicKeyByID(id)
+ if err != nil {
+ if IsErrKeyNotExist(err) {
+ return nil
+ }
+ return fmt.Errorf("GetPublicKeyByID: %v", err)
+ }
+
+ // Check if user has access to delete this key.
+ if !doer.IsAdmin && doer.ID != key.OwnerID {
+ return ErrKeyAccessDenied{doer.ID, key.ID, "public"}
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if err = deletePublicKeys(sess, id); err != nil {
+ return err
+ }
+
+ if err = sess.Commit(); err != nil {
+ return err
+ }
+
+ return RewriteAuthorizedKeys()
+}
+
+// RewriteAuthorizedKeys removes any authorized key and rewrite all keys from database again.
+// Note: x.Iterate does not get latest data after insert/delete, so we have to call this function
+// outsite any session scope independently.
+func RewriteAuthorizedKeys() error {
+ sshOpLocker.Lock()
+ defer sshOpLocker.Unlock()
+
+ log.Trace("Doing: RewriteAuthorizedKeys")
+
+ os.MkdirAll(setting.SSH.RootPath, os.ModePerm)
+ fpath := filepath.Join(setting.SSH.RootPath, "authorized_keys")
+ tmpPath := fpath + ".tmp"
+ f, err := os.OpenFile(tmpPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
+ if err != nil {
+ return err
+ }
+ defer os.Remove(tmpPath)
+
+ err = x.Iterate(new(PublicKey), func(idx int, bean interface{}) (err error) {
+ _, err = f.WriteString((bean.(*PublicKey)).AuthorizedString())
+ return err
+ })
+ f.Close()
+ if err != nil {
+ return err
+ }
+
+ if com.IsExist(fpath) {
+ if err = os.Remove(fpath); err != nil {
+ return err
+ }
+ }
+ if err = os.Rename(tmpPath, fpath); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// ________ .__ ____ __.
+// \______ \ ____ ______ | | ____ ___.__.| |/ _|____ ___.__.
+// | | \_/ __ \\____ \| | / _ < | || <_/ __ < | |
+// | ` \ ___/| |_> > |_( <_> )___ || | \ ___/\___ |
+// /_______ /\___ > __/|____/\____// ____||____|__ \___ > ____|
+// \/ \/|__| \/ \/ \/\/
+
+// DeployKey represents deploy key information and its relation with repository.
+type DeployKey struct {
+ ID int64
+ KeyID int64 `xorm:"UNIQUE(s) INDEX"`
+ RepoID int64 `xorm:"UNIQUE(s) INDEX"`
+ Name string
+ Fingerprint string
+ Content string `xorm:"-" json:"-"`
+
+ Created time.Time `xorm:"-" json:"-"`
+ CreatedUnix int64
+ Updated time.Time `xorm:"-" json:"-"` // Note: Updated must below Created for AfterSet.
+ UpdatedUnix int64
+ HasRecentActivity bool `xorm:"-" json:"-"`
+ HasUsed bool `xorm:"-" json:"-"`
+}
+
+func (k *DeployKey) BeforeInsert() {
+ k.CreatedUnix = time.Now().Unix()
+}
+
+func (k *DeployKey) BeforeUpdate() {
+ k.UpdatedUnix = time.Now().Unix()
+}
+
+func (k *DeployKey) AfterSet(colName string, _ xorm.Cell) {
+ switch colName {
+ case "created_unix":
+ k.Created = time.Unix(k.CreatedUnix, 0).Local()
+ case "updated_unix":
+ k.Updated = time.Unix(k.UpdatedUnix, 0).Local()
+ k.HasUsed = k.Updated.After(k.Created)
+ k.HasRecentActivity = k.Updated.Add(7 * 24 * time.Hour).After(time.Now())
+ }
+}
+
+// GetContent gets associated public key content.
+func (k *DeployKey) GetContent() error {
+ pkey, err := GetPublicKeyByID(k.KeyID)
+ if err != nil {
+ return err
+ }
+ k.Content = pkey.Content
+ return nil
+}
+
+func checkDeployKey(e Engine, keyID, repoID int64, name string) error {
+ // Note: We want error detail, not just true or false here.
+ has, err := e.Where("key_id = ? AND repo_id = ?", keyID, repoID).Get(new(DeployKey))
+ if err != nil {
+ return err
+ } else if has {
+ return ErrDeployKeyAlreadyExist{keyID, repoID}
+ }
+
+ has, err = e.Where("repo_id = ? AND name = ?", repoID, name).Get(new(DeployKey))
+ if err != nil {
+ return err
+ } else if has {
+ return ErrDeployKeyNameAlreadyUsed{repoID, name}
+ }
+
+ return nil
+}
+
+// addDeployKey adds new key-repo relation.
+func addDeployKey(e *xorm.Session, keyID, repoID int64, name, fingerprint string) (*DeployKey, error) {
+ if err := checkDeployKey(e, keyID, repoID, name); err != nil {
+ return nil, err
+ }
+
+ key := &DeployKey{
+ KeyID: keyID,
+ RepoID: repoID,
+ Name: name,
+ Fingerprint: fingerprint,
+ }
+ _, err := e.Insert(key)
+ return key, err
+}
+
+// HasDeployKey returns true if public key is a deploy key of given repository.
+func HasDeployKey(keyID, repoID int64) bool {
+ has, _ := x.Where("key_id = ? AND repo_id = ?", keyID, repoID).Get(new(DeployKey))
+ return has
+}
+
+// AddDeployKey add new deploy key to database and authorized_keys file.
+func AddDeployKey(repoID int64, name, content string) (*DeployKey, error) {
+ if err := checkKeyContent(content); err != nil {
+ return nil, err
+ }
+
+ pkey := &PublicKey{
+ Content: content,
+ Mode: ACCESS_MODE_READ,
+ Type: KEY_TYPE_DEPLOY,
+ }
+ has, err := x.Get(pkey)
+ if err != nil {
+ return nil, err
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return nil, err
+ }
+
+ // First time use this deploy key.
+ if !has {
+ if err = addKey(sess, pkey); err != nil {
+ return nil, fmt.Errorf("addKey: %v", err)
+ }
+ }
+
+ key, err := addDeployKey(sess, pkey.ID, repoID, name, pkey.Fingerprint)
+ if err != nil {
+ return nil, fmt.Errorf("addDeployKey: %v", err)
+ }
+
+ return key, sess.Commit()
+}
+
+// GetDeployKeyByID returns deploy key by given ID.
+func GetDeployKeyByID(id int64) (*DeployKey, error) {
+ key := new(DeployKey)
+ has, err := x.Id(id).Get(key)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, ErrDeployKeyNotExist{id, 0, 0}
+ }
+ return key, nil
+}
+
+// GetDeployKeyByRepo returns deploy key by given public key ID and repository ID.
+func GetDeployKeyByRepo(keyID, repoID int64) (*DeployKey, error) {
+ key := &DeployKey{
+ KeyID: keyID,
+ RepoID: repoID,
+ }
+ has, err := x.Get(key)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, ErrDeployKeyNotExist{0, keyID, repoID}
+ }
+ return key, nil
+}
+
+// UpdateDeployKey updates deploy key information.
+func UpdateDeployKey(key *DeployKey) error {
+ _, err := x.Id(key.ID).AllCols().Update(key)
+ return err
+}
+
+// DeleteDeployKey deletes deploy key from its repository authorized_keys file if needed.
+func DeleteDeployKey(doer *User, id int64) error {
+ key, err := GetDeployKeyByID(id)
+ if err != nil {
+ if IsErrDeployKeyNotExist(err) {
+ return nil
+ }
+ return fmt.Errorf("GetDeployKeyByID: %v", err)
+ }
+
+ // Check if user has access to delete this key.
+ if !doer.IsAdmin {
+ repo, err := GetRepositoryByID(key.RepoID)
+ if err != nil {
+ return fmt.Errorf("GetRepositoryByID: %v", err)
+ }
+ yes, err := HasAccess(doer.ID, repo, ACCESS_MODE_ADMIN)
+ if err != nil {
+ return fmt.Errorf("HasAccess: %v", err)
+ } else if !yes {
+ return ErrKeyAccessDenied{doer.ID, key.ID, "deploy"}
+ }
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if _, err = sess.ID(key.ID).Delete(new(DeployKey)); err != nil {
+ return fmt.Errorf("delete deploy key [%d]: %v", key.ID, err)
+ }
+
+ // Check if this is the last reference to same key content.
+ has, err := sess.Where("key_id = ?", key.KeyID).Get(new(DeployKey))
+ if err != nil {
+ return err
+ } else if !has {
+ if err = deletePublicKeys(sess, key.KeyID); err != nil {
+ return err
+ }
+ }
+
+ return sess.Commit()
+}
+
+// ListDeployKeys returns all deploy keys by given repository ID.
+func ListDeployKeys(repoID int64) ([]*DeployKey, error) {
+ keys := make([]*DeployKey, 0, 5)
+ return keys, x.Where("repo_id = ?", repoID).Find(&keys)
+}
diff --git a/internal/db/ssh_key_test.go b/internal/db/ssh_key_test.go
new file mode 100644
index 00000000..d4c06488
--- /dev/null
+++ b/internal/db/ssh_key_test.go
@@ -0,0 +1,56 @@
+// Copyright 2016 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 db
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+
+ . "github.com/smartystreets/goconvey/convey"
+
+ "gogs.io/gogs/internal/setting"
+)
+
+func init() {
+ setting.NewContext()
+}
+
+func Test_SSHParsePublicKey(t *testing.T) {
+ testKeys := map[string]struct {
+ typeName string
+ length int
+ content string
+ }{
+ "dsa-1024": {"dsa", 1024, "ssh-dss AAAAB3NzaC1kc3MAAACBAOChCC7lf6Uo9n7BmZ6M8St19PZf4Tn59NriyboW2x/DZuYAz3ibZ2OkQ3S0SqDIa0HXSEJ1zaExQdmbO+Ux/wsytWZmCczWOVsaszBZSl90q8UnWlSH6P+/YA+RWJm5SFtuV9PtGIhyZgoNuz5kBQ7K139wuQsecdKktISwTakzAAAAFQCzKsO2JhNKlL+wwwLGOcLffoAmkwAAAIBpK7/3xvduajLBD/9vASqBQIHrgK2J+wiQnIb/Wzy0UsVmvfn8A+udRbBo+csM8xrSnlnlJnjkJS3qiM5g+eTwsLIV1IdKPEwmwB+VcP53Cw6lSyWyJcvhFb0N6s08NZysLzvj0N+ZC/FnhKTLzIyMtkHf/IrPCwlM+pV/M/96YgAAAIEAqQcGn9CKgzgPaguIZooTAOQdvBLMI5y0bQjOW6734XOpqQGf/Kra90wpoasLKZjSYKNPjE+FRUOrStLrxcNs4BeVKhy2PYTRnybfYVk1/dmKgH6P1YSRONsGKvTsH6c5IyCRG0ncCgYeF8tXppyd642982daopE7zQ/NPAnJfag= nocomment"},
+ "rsa-1024": {"rsa", 1024, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDAu7tvIvX6ZHrRXuZNfkR3XLHSsuCK9Zn3X58lxBcQzuo5xZgB6vRwwm/QtJuF+zZPtY5hsQILBLmF+BZ5WpKZp1jBeSjH2G7lxet9kbcH+kIVj0tPFEoyKI9wvWqIwC4prx/WVk2wLTJjzBAhyNxfEq7C9CeiX9pQEbEqJfkKCQ== nocomment\n"},
+ "rsa-2048": {"rsa", 2048, "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDMZXh+1OBUwSH9D45wTaxErQIN9IoC9xl7MKJkqvTvv6O5RR9YW/IK9FbfjXgXsppYGhsCZo1hFOOsXHMnfOORqu/xMDx4yPuyvKpw4LePEcg4TDipaDFuxbWOqc/BUZRZcXu41QAWfDLrInwsltWZHSeG7hjhpacl4FrVv9V1pS6Oc5Q1NxxEzTzuNLS/8diZrTm/YAQQ/+B+mzWI3zEtF4miZjjAljWd1LTBPvU23d29DcBmmFahcZ441XZsTeAwGxG/Q6j8NgNXj9WxMeWwxXV2jeAX/EBSpZrCVlCQ1yJswT6xCp8TuBnTiGWYMBNTbOZvPC4e0WI2/yZW/s5F nocomment"},
+ "ecdsa-256": {"ecdsa", 256, "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFQacN3PrOll7PXmN5B/ZNVahiUIqI05nbBlZk1KXsO3d06ktAWqbNflv2vEmA38bTFTfJ2sbn2B5ksT52cDDbA= nocomment"},
+ "ecdsa-384": {"ecdsa", 384, "ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBINmioV+XRX1Fm9Qk2ehHXJ2tfVxW30ypUWZw670Zyq5GQfBAH6xjygRsJ5wWsHXBsGYgFUXIHvMKVAG1tpw7s6ax9oA+dJOJ7tj+vhn8joFqT+sg3LYHgZkHrfqryRasQ== nocomment"},
+ // "ecdsa-521": {"ecdsa", 521, "ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBACGt3UG3EzRwNOI17QR84l6PgiAcvCE7v6aXPj/SC6UWKg4EL8vW9ZBcdYL9wzs4FZXh4MOV8jAzu3KRWNTwb4k2wFNUpGOt7l28MztFFEtH5BDDrtAJSPENPy8pvPLMfnPg5NhvWycqIBzNcHipem5wSJFN5PdpNOC2xMrPWKNqj+ZjQ== nocomment"},
+ }
+
+ Convey("Parse public keys in both native and ssh-keygen", t, func() {
+ for name, key := range testKeys {
+ fmt.Println("\nTesting key:", name)
+
+ keyTypeN, lengthN, errN := SSHNativeParsePublicKey(key.content)
+ So(errN, ShouldBeNil)
+ So(keyTypeN, ShouldEqual, key.typeName)
+ So(lengthN, ShouldEqual, key.length)
+
+ keyTypeK, lengthK, errK := SSHKeyGenParsePublicKey(key.content)
+ if errK != nil {
+ // Some server just does not support ecdsa format.
+ if strings.Contains(errK.Error(), "line 1 too long:") {
+ continue
+ }
+ So(errK, ShouldBeNil)
+ }
+ So(keyTypeK, ShouldEqual, key.typeName)
+ So(lengthK, ShouldEqual, key.length)
+ }
+ })
+}
diff --git a/internal/db/token.go b/internal/db/token.go
new file mode 100644
index 00000000..2e2f3492
--- /dev/null
+++ b/internal/db/token.go
@@ -0,0 +1,102 @@
+// Copyright 2014 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 db
+
+import (
+ "time"
+
+ gouuid "github.com/satori/go.uuid"
+ "xorm.io/xorm"
+
+ "gogs.io/gogs/internal/db/errors"
+ "gogs.io/gogs/internal/tool"
+)
+
+// AccessToken represents a personal access token.
+type AccessToken struct {
+ ID int64
+ UID int64 `xorm:"INDEX"`
+ Name string
+ Sha1 string `xorm:"UNIQUE VARCHAR(40)"`
+
+ Created time.Time `xorm:"-" json:"-"`
+ CreatedUnix int64
+ Updated time.Time `xorm:"-" json:"-"` // Note: Updated must below Created for AfterSet.
+ UpdatedUnix int64
+ HasRecentActivity bool `xorm:"-" json:"-"`
+ HasUsed bool `xorm:"-" json:"-"`
+}
+
+func (t *AccessToken) BeforeInsert() {
+ t.CreatedUnix = time.Now().Unix()
+}
+
+func (t *AccessToken) BeforeUpdate() {
+ t.UpdatedUnix = time.Now().Unix()
+}
+
+func (t *AccessToken) AfterSet(colName string, _ xorm.Cell) {
+ switch colName {
+ case "created_unix":
+ t.Created = time.Unix(t.CreatedUnix, 0).Local()
+ case "updated_unix":
+ t.Updated = time.Unix(t.UpdatedUnix, 0).Local()
+ t.HasUsed = t.Updated.After(t.Created)
+ t.HasRecentActivity = t.Updated.Add(7 * 24 * time.Hour).After(time.Now())
+ }
+}
+
+// NewAccessToken creates new access token.
+func NewAccessToken(t *AccessToken) error {
+ t.Sha1 = tool.SHA1(gouuid.NewV4().String())
+ has, err := x.Get(&AccessToken{
+ UID: t.UID,
+ Name: t.Name,
+ })
+ if err != nil {
+ return err
+ } else if has {
+ return errors.AccessTokenNameAlreadyExist{t.Name}
+ }
+
+ _, err = x.Insert(t)
+ return err
+}
+
+// GetAccessTokenBySHA returns access token by given sha1.
+func GetAccessTokenBySHA(sha string) (*AccessToken, error) {
+ if sha == "" {
+ return nil, ErrAccessTokenEmpty{}
+ }
+ t := &AccessToken{Sha1: sha}
+ has, err := x.Get(t)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, ErrAccessTokenNotExist{sha}
+ }
+ return t, nil
+}
+
+// ListAccessTokens returns a list of access tokens belongs to given user.
+func ListAccessTokens(uid int64) ([]*AccessToken, error) {
+ tokens := make([]*AccessToken, 0, 5)
+ return tokens, x.Where("uid=?", uid).Desc("id").Find(&tokens)
+}
+
+// UpdateAccessToken updates information of access token.
+func UpdateAccessToken(t *AccessToken) error {
+ _, err := x.Id(t.ID).AllCols().Update(t)
+ return err
+}
+
+// DeleteAccessTokenOfUserByID deletes access token by given ID.
+func DeleteAccessTokenOfUserByID(userID, id int64) error {
+ _, err := x.Delete(&AccessToken{
+ ID: id,
+ UID: userID,
+ })
+ return err
+}
diff --git a/internal/db/two_factor.go b/internal/db/two_factor.go
new file mode 100644
index 00000000..dcc1c16c
--- /dev/null
+++ b/internal/db/two_factor.go
@@ -0,0 +1,201 @@
+// Copyright 2017 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 db
+
+import (
+ "encoding/base64"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/pquerna/otp/totp"
+ "github.com/unknwon/com"
+ log "gopkg.in/clog.v1"
+ "xorm.io/xorm"
+
+ "gogs.io/gogs/internal/db/errors"
+ "gogs.io/gogs/internal/setting"
+ "gogs.io/gogs/internal/tool"
+)
+
+// TwoFactor represents a two-factor authentication token.
+type TwoFactor struct {
+ ID int64
+ UserID int64 `xorm:"UNIQUE"`
+ Secret string
+ Created time.Time `xorm:"-" json:"-"`
+ CreatedUnix int64
+}
+
+func (t *TwoFactor) BeforeInsert() {
+ t.CreatedUnix = time.Now().Unix()
+}
+
+func (t *TwoFactor) AfterSet(colName string, _ xorm.Cell) {
+ switch colName {
+ case "created_unix":
+ t.Created = time.Unix(t.CreatedUnix, 0).Local()
+ }
+}
+
+// ValidateTOTP returns true if given passcode is valid for two-factor authentication token.
+// It also returns possible validation error.
+func (t *TwoFactor) ValidateTOTP(passcode string) (bool, error) {
+ secret, err := base64.StdEncoding.DecodeString(t.Secret)
+ if err != nil {
+ return false, fmt.Errorf("DecodeString: %v", err)
+ }
+ decryptSecret, err := com.AESGCMDecrypt(tool.MD5Bytes(setting.SecretKey), secret)
+ if err != nil {
+ return false, fmt.Errorf("AESGCMDecrypt: %v", err)
+ }
+ return totp.Validate(passcode, string(decryptSecret)), nil
+}
+
+// IsUserEnabledTwoFactor returns true if user has enabled two-factor authentication.
+func IsUserEnabledTwoFactor(userID int64) bool {
+ has, err := x.Where("user_id = ?", userID).Get(new(TwoFactor))
+ if err != nil {
+ log.Error(2, "IsUserEnabledTwoFactor [user_id: %d]: %v", userID, err)
+ }
+ return has
+}
+
+func generateRecoveryCodes(userID int64) ([]*TwoFactorRecoveryCode, error) {
+ recoveryCodes := make([]*TwoFactorRecoveryCode, 10)
+ for i := 0; i < 10; i++ {
+ code, err := tool.RandomString(10)
+ if err != nil {
+ return nil, fmt.Errorf("RandomString: %v", err)
+ }
+ recoveryCodes[i] = &TwoFactorRecoveryCode{
+ UserID: userID,
+ Code: strings.ToLower(code[:5] + "-" + code[5:]),
+ }
+ }
+ return recoveryCodes, nil
+}
+
+// NewTwoFactor creates a new two-factor authentication token and recovery codes for given user.
+func NewTwoFactor(userID int64, secret string) error {
+ t := &TwoFactor{
+ UserID: userID,
+ }
+
+ // Encrypt secret
+ encryptSecret, err := com.AESGCMEncrypt(tool.MD5Bytes(setting.SecretKey), []byte(secret))
+ if err != nil {
+ return fmt.Errorf("AESGCMEncrypt: %v", err)
+ }
+ t.Secret = base64.StdEncoding.EncodeToString(encryptSecret)
+
+ recoveryCodes, err := generateRecoveryCodes(userID)
+ if err != nil {
+ return fmt.Errorf("generateRecoveryCodes: %v", err)
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if _, err = sess.Insert(t); err != nil {
+ return fmt.Errorf("insert two-factor: %v", err)
+ } else if _, err = sess.Insert(recoveryCodes); err != nil {
+ return fmt.Errorf("insert recovery codes: %v", err)
+ }
+
+ return sess.Commit()
+}
+
+// GetTwoFactorByUserID returns two-factor authentication token of given user.
+func GetTwoFactorByUserID(userID int64) (*TwoFactor, error) {
+ t := new(TwoFactor)
+ has, err := x.Where("user_id = ?", userID).Get(t)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, errors.TwoFactorNotFound{userID}
+ }
+
+ return t, nil
+}
+
+// DeleteTwoFactor removes two-factor authentication token and recovery codes of given user.
+func DeleteTwoFactor(userID int64) (err error) {
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if _, err = sess.Where("user_id = ?", userID).Delete(new(TwoFactor)); err != nil {
+ return fmt.Errorf("delete two-factor: %v", err)
+ } else if err = deleteRecoveryCodesByUserID(sess, userID); err != nil {
+ return fmt.Errorf("deleteRecoveryCodesByUserID: %v", err)
+ }
+
+ return sess.Commit()
+}
+
+// TwoFactorRecoveryCode represents a two-factor authentication recovery code.
+type TwoFactorRecoveryCode struct {
+ ID int64
+ UserID int64
+ Code string `xorm:"VARCHAR(11)"`
+ IsUsed bool
+}
+
+// GetRecoveryCodesByUserID returns all recovery codes of given user.
+func GetRecoveryCodesByUserID(userID int64) ([]*TwoFactorRecoveryCode, error) {
+ recoveryCodes := make([]*TwoFactorRecoveryCode, 0, 10)
+ return recoveryCodes, x.Where("user_id = ?", userID).Find(&recoveryCodes)
+}
+
+func deleteRecoveryCodesByUserID(e Engine, userID int64) error {
+ _, err := e.Where("user_id = ?", userID).Delete(new(TwoFactorRecoveryCode))
+ return err
+}
+
+// RegenerateRecoveryCodes regenerates new set of recovery codes for given user.
+func RegenerateRecoveryCodes(userID int64) error {
+ recoveryCodes, err := generateRecoveryCodes(userID)
+ if err != nil {
+ return fmt.Errorf("generateRecoveryCodes: %v", err)
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if err = deleteRecoveryCodesByUserID(sess, userID); err != nil {
+ return fmt.Errorf("deleteRecoveryCodesByUserID: %v", err)
+ } else if _, err = sess.Insert(recoveryCodes); err != nil {
+ return fmt.Errorf("insert new recovery codes: %v", err)
+ }
+
+ return sess.Commit()
+}
+
+// UseRecoveryCode validates recovery code of given user and marks it is used if valid.
+func UseRecoveryCode(userID int64, code string) error {
+ recoveryCode := new(TwoFactorRecoveryCode)
+ has, err := x.Where("code = ?", code).And("is_used = ?", false).Get(recoveryCode)
+ if err != nil {
+ return fmt.Errorf("get unused code: %v", err)
+ } else if !has {
+ return errors.TwoFactorRecoveryCodeNotFound{code}
+ }
+
+ recoveryCode.IsUsed = true
+ if _, err = x.Id(recoveryCode.ID).Cols("is_used").Update(recoveryCode); err != nil {
+ return fmt.Errorf("mark code as used: %v", err)
+ }
+
+ return nil
+}
diff --git a/internal/db/update.go b/internal/db/update.go
new file mode 100644
index 00000000..6555a479
--- /dev/null
+++ b/internal/db/update.go
@@ -0,0 +1,142 @@
+// Copyright 2014 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 db
+
+import (
+ "container/list"
+ "fmt"
+ "os/exec"
+ "strings"
+
+ git "github.com/gogs/git-module"
+)
+
+// CommitToPushCommit transforms a git.Commit to PushCommit type.
+func CommitToPushCommit(commit *git.Commit) *PushCommit {
+ return &PushCommit{
+ Sha1: commit.ID.String(),
+ Message: commit.Message(),
+ AuthorEmail: commit.Author.Email,
+ AuthorName: commit.Author.Name,
+ CommitterEmail: commit.Committer.Email,
+ CommitterName: commit.Committer.Name,
+ Timestamp: commit.Committer.When,
+ }
+}
+
+func ListToPushCommits(l *list.List) *PushCommits {
+ if l == nil {
+ return &PushCommits{}
+ }
+
+ commits := make([]*PushCommit, 0)
+ var actEmail string
+ for e := l.Front(); e != nil; e = e.Next() {
+ commit := e.Value.(*git.Commit)
+ if actEmail == "" {
+ actEmail = commit.Committer.Email
+ }
+ commits = append(commits, CommitToPushCommit(commit))
+ }
+ return &PushCommits{l.Len(), commits, "", nil}
+}
+
+type PushUpdateOptions struct {
+ OldCommitID string
+ NewCommitID string
+ RefFullName string
+ PusherID int64
+ PusherName string
+ RepoUserName string
+ RepoName string
+}
+
+// PushUpdate must be called for any push actions in order to
+// generates necessary push action history feeds.
+func PushUpdate(opts PushUpdateOptions) (err error) {
+ isNewRef := opts.OldCommitID == git.EMPTY_SHA
+ isDelRef := opts.NewCommitID == git.EMPTY_SHA
+ if isNewRef && isDelRef {
+ return fmt.Errorf("Old and new revisions are both %s", git.EMPTY_SHA)
+ }
+
+ repoPath := RepoPath(opts.RepoUserName, opts.RepoName)
+
+ gitUpdate := exec.Command("git", "update-server-info")
+ gitUpdate.Dir = repoPath
+ if err = gitUpdate.Run(); err != nil {
+ return fmt.Errorf("Fail to call 'git update-server-info': %v", err)
+ }
+
+ gitRepo, err := git.OpenRepository(repoPath)
+ if err != nil {
+ return fmt.Errorf("OpenRepository: %v", err)
+ }
+
+ owner, err := GetUserByName(opts.RepoUserName)
+ if err != nil {
+ return fmt.Errorf("GetUserByName: %v", err)
+ }
+
+ repo, err := GetRepositoryByName(owner.ID, opts.RepoName)
+ if err != nil {
+ return fmt.Errorf("GetRepositoryByName: %v", err)
+ }
+
+ if err = repo.UpdateSize(); err != nil {
+ return fmt.Errorf("UpdateSize: %v", err)
+ }
+
+ // Push tags
+ if strings.HasPrefix(opts.RefFullName, git.TAG_PREFIX) {
+ if err := CommitRepoAction(CommitRepoActionOptions{
+ PusherName: opts.PusherName,
+ RepoOwnerID: owner.ID,
+ RepoName: repo.Name,
+ RefFullName: opts.RefFullName,
+ OldCommitID: opts.OldCommitID,
+ NewCommitID: opts.NewCommitID,
+ Commits: &PushCommits{},
+ }); err != nil {
+ return fmt.Errorf("CommitRepoAction.(tag): %v", err)
+ }
+ return nil
+ }
+
+ var l *list.List
+ // Skip read parent commits when delete branch
+ if !isDelRef {
+ // Push new branch
+ newCommit, err := gitRepo.GetCommit(opts.NewCommitID)
+ if err != nil {
+ return fmt.Errorf("GetCommit [commit_id: %s]: %v", opts.NewCommitID, err)
+ }
+
+ if isNewRef {
+ l, err = newCommit.CommitsBeforeLimit(10)
+ if err != nil {
+ return fmt.Errorf("CommitsBeforeLimit [commit_id: %s]: %v", newCommit.ID, err)
+ }
+ } else {
+ l, err = newCommit.CommitsBeforeUntil(opts.OldCommitID)
+ if err != nil {
+ return fmt.Errorf("CommitsBeforeUntil [commit_id: %s]: %v", opts.OldCommitID, err)
+ }
+ }
+ }
+
+ if err := CommitRepoAction(CommitRepoActionOptions{
+ PusherName: opts.PusherName,
+ RepoOwnerID: owner.ID,
+ RepoName: repo.Name,
+ RefFullName: opts.RefFullName,
+ OldCommitID: opts.OldCommitID,
+ NewCommitID: opts.NewCommitID,
+ Commits: ListToPushCommits(l),
+ }); err != nil {
+ return fmt.Errorf("CommitRepoAction.(branch): %v", err)
+ }
+ return nil
+}
diff --git a/internal/db/user.go b/internal/db/user.go
new file mode 100644
index 00000000..3318a4be
--- /dev/null
+++ b/internal/db/user.go
@@ -0,0 +1,1146 @@
+// Copyright 2014 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 db
+
+import (
+ "bytes"
+ "container/list"
+ "crypto/sha256"
+ "crypto/subtle"
+ "encoding/hex"
+ "fmt"
+ "image"
+ _ "image/jpeg"
+ "image/png"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+ "unicode/utf8"
+
+ "github.com/nfnt/resize"
+ "github.com/unknwon/com"
+ "golang.org/x/crypto/pbkdf2"
+ log "gopkg.in/clog.v1"
+ "xorm.io/xorm"
+
+ "github.com/gogs/git-module"
+ api "github.com/gogs/go-gogs-client"
+
+ "gogs.io/gogs/internal/avatar"
+ "gogs.io/gogs/internal/db/errors"
+ "gogs.io/gogs/internal/setting"
+ "gogs.io/gogs/internal/tool"
+)
+
+// USER_AVATAR_URL_PREFIX is used to identify a URL is to access user avatar.
+const USER_AVATAR_URL_PREFIX = "avatars"
+
+type UserType int
+
+const (
+ USER_TYPE_INDIVIDUAL UserType = iota // Historic reason to make it starts at 0.
+ USER_TYPE_ORGANIZATION
+)
+
+// User represents the object of individual and member of organization.
+type User struct {
+ ID int64
+ LowerName string `xorm:"UNIQUE NOT NULL"`
+ Name string `xorm:"UNIQUE NOT NULL"`
+ FullName string
+ // Email is the primary email address (to be used for communication)
+ Email string `xorm:"NOT NULL"`
+ Passwd string `xorm:"NOT NULL"`
+ LoginType LoginType
+ LoginSource int64 `xorm:"NOT NULL DEFAULT 0"`
+ LoginName string
+ Type UserType
+ OwnedOrgs []*User `xorm:"-" json:"-"`
+ Orgs []*User `xorm:"-" json:"-"`
+ Repos []*Repository `xorm:"-" json:"-"`
+ Location string
+ Website string
+ Rands string `xorm:"VARCHAR(10)"`
+ Salt string `xorm:"VARCHAR(10)"`
+
+ Created time.Time `xorm:"-" json:"-"`
+ CreatedUnix int64
+ Updated time.Time `xorm:"-" json:"-"`
+ UpdatedUnix int64
+
+ // Remember visibility choice for convenience, true for private
+ LastRepoVisibility bool
+ // Maximum repository creation limit, -1 means use gloabl default
+ MaxRepoCreation int `xorm:"NOT NULL DEFAULT -1"`
+
+ // Permissions
+ IsActive bool // Activate primary email
+ IsAdmin bool
+ AllowGitHook bool
+ AllowImportLocal bool // Allow migrate repository by local path
+ ProhibitLogin bool
+
+ // Avatar
+ Avatar string `xorm:"VARCHAR(2048) NOT NULL"`
+ AvatarEmail string `xorm:"NOT NULL"`
+ UseCustomAvatar bool
+
+ // Counters
+ NumFollowers int
+ NumFollowing int `xorm:"NOT NULL DEFAULT 0"`
+ NumStars int
+ NumRepos int
+
+ // For organization
+ Description string
+ NumTeams int
+ NumMembers int
+ Teams []*Team `xorm:"-" json:"-"`
+ Members []*User `xorm:"-" json:"-"`
+}
+
+func (u *User) BeforeInsert() {
+ u.CreatedUnix = time.Now().Unix()
+ u.UpdatedUnix = u.CreatedUnix
+}
+
+func (u *User) BeforeUpdate() {
+ if u.MaxRepoCreation < -1 {
+ u.MaxRepoCreation = -1
+ }
+ u.UpdatedUnix = time.Now().Unix()
+}
+
+func (u *User) AfterSet(colName string, _ xorm.Cell) {
+ switch colName {
+ case "created_unix":
+ u.Created = time.Unix(u.CreatedUnix, 0).Local()
+ case "updated_unix":
+ u.Updated = time.Unix(u.UpdatedUnix, 0).Local()
+ }
+}
+
+// IDStr returns string representation of user's ID.
+func (u *User) IDStr() string {
+ return com.ToStr(u.ID)
+}
+
+func (u *User) APIFormat() *api.User {
+ return &api.User{
+ ID: u.ID,
+ UserName: u.Name,
+ Login: u.Name,
+ FullName: u.FullName,
+ Email: u.Email,
+ AvatarUrl: u.AvatarLink(),
+ }
+}
+
+// returns true if user login type is LOGIN_PLAIN.
+func (u *User) IsLocal() bool {
+ return u.LoginType <= LOGIN_PLAIN
+}
+
+// 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
+}
+
+func (u *User) RepoCreationNum() int {
+ if u.MaxRepoCreation <= -1 {
+ return setting.Repository.MaxCreationLimit
+ }
+ return u.MaxRepoCreation
+}
+
+func (u *User) CanCreateRepo() bool {
+ if u.MaxRepoCreation <= -1 {
+ if setting.Repository.MaxCreationLimit <= -1 {
+ return true
+ }
+ return u.NumRepos < setting.Repository.MaxCreationLimit
+ }
+ return u.NumRepos < u.MaxRepoCreation
+}
+
+func (u *User) CanCreateOrganization() bool {
+ return !setting.Admin.DisableRegularOrgCreation || u.IsAdmin
+}
+
+// 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 setting.Repository.EnableLocalPathMigration && (u.IsAdmin || u.AllowImportLocal)
+}
+
+// DashboardLink returns the user dashboard page link.
+func (u *User) DashboardLink() string {
+ if u.IsOrganization() {
+ return setting.AppSubURL + "/org/" + u.Name + "/dashboard/"
+ }
+ return setting.AppSubURL + "/"
+}
+
+// HomeLink returns the user or organization home page link.
+func (u *User) HomeLink() string {
+ return setting.AppSubURL + "/" + u.Name
+}
+
+func (u *User) HTMLURL() string {
+ return setting.AppURL + u.Name
+}
+
+// GenerateEmailActivateCode generates an activate code based on user information and given e-mail.
+func (u *User) GenerateEmailActivateCode(email string) string {
+ code := tool.CreateTimeLimitCode(
+ com.ToStr(u.ID)+email+u.LowerName+u.Passwd+u.Rands,
+ setting.Service.ActiveCodeLives, nil)
+
+ // Add tail hex username
+ code += hex.EncodeToString([]byte(u.LowerName))
+ return code
+}
+
+// GenerateActivateCode generates an activate code based on user information.
+func (u *User) GenerateActivateCode() string {
+ return u.GenerateEmailActivateCode(u.Email)
+}
+
+// CustomAvatarPath returns user custom avatar file path.
+func (u *User) CustomAvatarPath() string {
+ return filepath.Join(setting.AvatarUploadPath, com.ToStr(u.ID))
+}
+
+// GenerateRandomAvatar generates a random avatar for user.
+func (u *User) GenerateRandomAvatar() error {
+ seed := u.Email
+ if len(seed) == 0 {
+ seed = u.Name
+ }
+
+ img, err := avatar.RandomImage([]byte(seed))
+ if err != nil {
+ return fmt.Errorf("RandomImage: %v", err)
+ }
+ if err = os.MkdirAll(filepath.Dir(u.CustomAvatarPath()), os.ModePerm); err != nil {
+ return fmt.Errorf("MkdirAll: %v", err)
+ }
+ fw, err := os.Create(u.CustomAvatarPath())
+ if err != nil {
+ return fmt.Errorf("Create: %v", err)
+ }
+ defer fw.Close()
+
+ if err = png.Encode(fw, img); err != nil {
+ return fmt.Errorf("Encode: %v", err)
+ }
+
+ log.Info("New random avatar created: %d", u.ID)
+ return nil
+}
+
+// RelAvatarLink returns relative avatar link to the site domain,
+// which includes app sub-url as prefix. However, it is possible
+// to return full URL if user enables Gravatar-like service.
+func (u *User) RelAvatarLink() string {
+ defaultImgUrl := setting.AppSubURL + "/img/avatar_default.png"
+ if u.ID == -1 {
+ return defaultImgUrl
+ }
+
+ switch {
+ case u.UseCustomAvatar:
+ if !com.IsExist(u.CustomAvatarPath()) {
+ return defaultImgUrl
+ }
+ return fmt.Sprintf("%s/%s/%d", setting.AppSubURL, USER_AVATAR_URL_PREFIX, u.ID)
+ case setting.DisableGravatar, setting.OfflineMode:
+ if !com.IsExist(u.CustomAvatarPath()) {
+ if err := u.GenerateRandomAvatar(); err != nil {
+ log.Error(3, "GenerateRandomAvatar: %v", err)
+ }
+ }
+
+ return fmt.Sprintf("%s/%s/%d", setting.AppSubURL, USER_AVATAR_URL_PREFIX, u.ID)
+ }
+ return tool.AvatarLink(u.AvatarEmail)
+}
+
+// AvatarLink returns user avatar absolute link.
+func (u *User) AvatarLink() string {
+ link := u.RelAvatarLink()
+ if link[0] == '/' && link[1] != '/' {
+ return setting.AppURL + strings.TrimPrefix(link, setting.AppSubURL)[1:]
+ }
+ return link
+}
+
+// User.GetFollwoers returns range of user's followers.
+func (u *User) GetFollowers(page int) ([]*User, error) {
+ users := make([]*User, 0, ItemsPerPage)
+ sess := x.Limit(ItemsPerPage, (page-1)*ItemsPerPage).Where("follow.follow_id=?", u.ID)
+ if setting.UsePostgreSQL {
+ sess = sess.Join("LEFT", "follow", `"user".id=follow.user_id`)
+ } else {
+ sess = sess.Join("LEFT", "follow", "user.id=follow.user_id")
+ }
+ return users, sess.Find(&users)
+}
+
+func (u *User) IsFollowing(followID int64) bool {
+ return IsFollowing(u.ID, followID)
+}
+
+// GetFollowing returns range of user's following.
+func (u *User) GetFollowing(page int) ([]*User, error) {
+ users := make([]*User, 0, ItemsPerPage)
+ sess := x.Limit(ItemsPerPage, (page-1)*ItemsPerPage).Where("follow.user_id=?", u.ID)
+ if setting.UsePostgreSQL {
+ sess = sess.Join("LEFT", "follow", `"user".id=follow.follow_id`)
+ } else {
+ sess = sess.Join("LEFT", "follow", "user.id=follow.follow_id")
+ }
+ return users, sess.Find(&users)
+}
+
+// NewGitSig generates and returns the signature of given user.
+func (u *User) NewGitSig() *git.Signature {
+ return &git.Signature{
+ Name: u.DisplayName(),
+ Email: u.Email,
+ When: time.Now(),
+ }
+}
+
+// EncodePasswd encodes password to safe format.
+func (u *User) EncodePasswd() {
+ newPasswd := pbkdf2.Key([]byte(u.Passwd), []byte(u.Salt), 10000, 50, sha256.New)
+ u.Passwd = fmt.Sprintf("%x", newPasswd)
+}
+
+// ValidatePassword checks if given password matches the one belongs to the user.
+func (u *User) ValidatePassword(passwd string) bool {
+ newUser := &User{Passwd: passwd, Salt: u.Salt}
+ newUser.EncodePasswd()
+ return subtle.ConstantTimeCompare([]byte(u.Passwd), []byte(newUser.Passwd)) == 1
+}
+
+// UploadAvatar saves custom avatar for user.
+// FIXME: split uploads to different subdirs in case we have massive number of users.
+func (u *User) UploadAvatar(data []byte) error {
+ img, _, err := image.Decode(bytes.NewReader(data))
+ if err != nil {
+ return fmt.Errorf("decode image: %v", err)
+ }
+
+ os.MkdirAll(setting.AvatarUploadPath, os.ModePerm)
+ fw, err := os.Create(u.CustomAvatarPath())
+ if err != nil {
+ return fmt.Errorf("create custom avatar directory: %v", err)
+ }
+ defer fw.Close()
+
+ m := resize.Resize(avatar.AVATAR_SIZE, avatar.AVATAR_SIZE, img, resize.NearestNeighbor)
+ if err = png.Encode(fw, m); err != nil {
+ return fmt.Errorf("encode image: %v", err)
+ }
+
+ return nil
+}
+
+// DeleteAvatar deletes the user's custom avatar.
+func (u *User) DeleteAvatar() error {
+ log.Trace("DeleteAvatar [%d]: %s", u.ID, u.CustomAvatarPath())
+ if err := os.Remove(u.CustomAvatarPath()); err != nil {
+ return err
+ }
+
+ u.UseCustomAvatar = false
+ return UpdateUser(u)
+}
+
+// IsAdminOfRepo returns true if user has admin or higher access of repository.
+func (u *User) IsAdminOfRepo(repo *Repository) bool {
+ has, err := HasAccess(u.ID, repo, ACCESS_MODE_ADMIN)
+ if err != nil {
+ log.Error(2, "HasAccess: %v", err)
+ }
+ return has
+}
+
+// IsWriterOfRepo returns true if user has write access to given repository.
+func (u *User) IsWriterOfRepo(repo *Repository) bool {
+ has, err := HasAccess(u.ID, repo, ACCESS_MODE_WRITE)
+ if err != nil {
+ log.Error(2, "HasAccess: %v", err)
+ }
+ return has
+}
+
+// IsOrganization returns true if user is actually a organization.
+func (u *User) IsOrganization() bool {
+ return u.Type == USER_TYPE_ORGANIZATION
+}
+
+// IsUserOrgOwner returns true if user is in the owner team of given organization.
+func (u *User) IsUserOrgOwner(orgId int64) bool {
+ return IsOrganizationOwner(orgId, u.ID)
+}
+
+// IsPublicMember returns true if user public his/her membership in give organization.
+func (u *User) IsPublicMember(orgId int64) bool {
+ return IsPublicMembership(orgId, u.ID)
+}
+
+// IsEnabledTwoFactor returns true if user has enabled two-factor authentication.
+func (u *User) IsEnabledTwoFactor() bool {
+ return IsUserEnabledTwoFactor(u.ID)
+}
+
+func (u *User) getOrganizationCount(e Engine) (int64, error) {
+ return e.Where("uid=?", u.ID).Count(new(OrgUser))
+}
+
+// GetOrganizationCount returns count of membership of organization of user.
+func (u *User) GetOrganizationCount() (int64, error) {
+ return u.getOrganizationCount(x)
+}
+
+// GetRepositories returns repositories that user owns, including private repositories.
+func (u *User) GetRepositories(page, pageSize int) (err error) {
+ u.Repos, err = GetUserRepositories(&UserRepoOptions{
+ UserID: u.ID,
+ Private: true,
+ Page: page,
+ PageSize: pageSize,
+ })
+ return err
+}
+
+// GetRepositories returns mirror repositories that user owns, including private repositories.
+func (u *User) GetMirrorRepositories() ([]*Repository, error) {
+ return GetUserMirrorRepositories(u.ID)
+}
+
+// GetOwnedOrganizations returns all organizations that user owns.
+func (u *User) GetOwnedOrganizations() (err error) {
+ u.OwnedOrgs, err = GetOwnedOrgsByUserID(u.ID)
+ return err
+}
+
+// GetOrganizations returns all organizations that user belongs to.
+func (u *User) GetOrganizations(showPrivate bool) error {
+ orgIDs, err := GetOrgIDsByUserID(u.ID, showPrivate)
+ if err != nil {
+ return fmt.Errorf("GetOrgIDsByUserID: %v", err)
+ }
+ if len(orgIDs) == 0 {
+ return nil
+ }
+
+ u.Orgs = make([]*User, 0, len(orgIDs))
+ if err = x.Where("type = ?", USER_TYPE_ORGANIZATION).In("id", orgIDs).Find(&u.Orgs); err != nil {
+ return err
+ }
+ return nil
+}
+
+// DisplayName returns full name if it's not empty,
+// returns username otherwise.
+func (u *User) DisplayName() string {
+ if len(u.FullName) > 0 {
+ return u.FullName
+ }
+ return u.Name
+}
+
+func (u *User) ShortName(length int) string {
+ return tool.EllipsisString(u.Name, length)
+}
+
+// IsMailable checks if a user is elegible
+// to receive emails.
+func (u *User) IsMailable() bool {
+ return u.IsActive
+}
+
+// IsUserExist checks if given user name exist,
+// the user name should be noncased unique.
+// If uid is presented, then check will rule out that one,
+// it is used when update a user name in settings page.
+func IsUserExist(uid int64, name string) (bool, error) {
+ if len(name) == 0 {
+ return false, nil
+ }
+ return x.Where("id != ?", uid).Get(&User{LowerName: strings.ToLower(name)})
+}
+
+// GetUserSalt returns a ramdom user salt token.
+func GetUserSalt() (string, error) {
+ return tool.RandomString(10)
+}
+
+// NewGhostUser creates and returns a fake user for someone who has deleted his/her account.
+func NewGhostUser() *User {
+ return &User{
+ ID: -1,
+ Name: "Ghost",
+ LowerName: "ghost",
+ }
+}
+
+var (
+ reservedUsernames = []string{"explore", "create", "assets", "css", "img", "js", "less", "plugins", "debug", "raw", "install", "api", "avatar", "user", "org", "help", "stars", "issues", "pulls", "commits", "repo", "template", "admin", "new", ".", ".."}
+ reservedUserPatterns = []string{"*.keys"}
+)
+
+// isUsableName checks if name is reserved or pattern of name is not allowed
+// based on given reserved names and patterns.
+// Names are exact match, patterns can be prefix or suffix match with placeholder '*'.
+func isUsableName(names, patterns []string, name string) error {
+ name = strings.TrimSpace(strings.ToLower(name))
+ if utf8.RuneCountInString(name) == 0 {
+ return errors.EmptyName{}
+ }
+
+ for i := range names {
+ if name == names[i] {
+ return ErrNameReserved{name}
+ }
+ }
+
+ for _, pat := range patterns {
+ if pat[0] == '*' && strings.HasSuffix(name, pat[1:]) ||
+ (pat[len(pat)-1] == '*' && strings.HasPrefix(name, pat[:len(pat)-1])) {
+ return ErrNamePatternNotAllowed{pat}
+ }
+ }
+
+ return nil
+}
+
+func IsUsableUsername(name string) error {
+ return isUsableName(reservedUsernames, reservedUserPatterns, name)
+}
+
+// CreateUser creates record of a new user.
+func CreateUser(u *User) (err error) {
+ if err = IsUsableUsername(u.Name); err != nil {
+ return err
+ }
+
+ isExist, err := IsUserExist(0, u.Name)
+ if err != nil {
+ return err
+ } else if isExist {
+ return ErrUserAlreadyExist{u.Name}
+ }
+
+ u.Email = strings.ToLower(u.Email)
+ isExist, err = IsEmailUsed(u.Email)
+ if err != nil {
+ return err
+ } else if isExist {
+ return ErrEmailAlreadyUsed{u.Email}
+ }
+
+ u.LowerName = strings.ToLower(u.Name)
+ u.AvatarEmail = u.Email
+ u.Avatar = tool.HashEmail(u.AvatarEmail)
+ if u.Rands, err = GetUserSalt(); err != nil {
+ return err
+ }
+ if u.Salt, err = GetUserSalt(); err != nil {
+ return err
+ }
+ u.EncodePasswd()
+ u.MaxRepoCreation = -1
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if _, err = sess.Insert(u); err != nil {
+ return err
+ } else if err = os.MkdirAll(UserPath(u.Name), os.ModePerm); err != nil {
+ return err
+ }
+
+ return sess.Commit()
+}
+
+func countUsers(e Engine) int64 {
+ count, _ := e.Where("type=0").Count(new(User))
+ return count
+}
+
+// CountUsers returns number of users.
+func CountUsers() int64 {
+ return countUsers(x)
+}
+
+// Users returns number of users in given page.
+func Users(page, pageSize int) ([]*User, error) {
+ users := make([]*User, 0, pageSize)
+ return users, x.Limit(pageSize, (page-1)*pageSize).Where("type=0").Asc("id").Find(&users)
+}
+
+// parseUserFromCode returns user by username encoded in code.
+// It returns nil if code or username is invalid.
+func parseUserFromCode(code string) (user *User) {
+ if len(code) <= tool.TIME_LIMIT_CODE_LENGTH {
+ return nil
+ }
+
+ // Use tail hex username to query user
+ hexStr := code[tool.TIME_LIMIT_CODE_LENGTH:]
+ if b, err := hex.DecodeString(hexStr); err == nil {
+ if user, err = GetUserByName(string(b)); user != nil {
+ return user
+ } else if !errors.IsUserNotExist(err) {
+ log.Error(2, "GetUserByName: %v", err)
+ }
+ }
+
+ return nil
+}
+
+// verify active code when active account
+func VerifyUserActiveCode(code string) (user *User) {
+ minutes := setting.Service.ActiveCodeLives
+
+ if user = parseUserFromCode(code); user != nil {
+ // time limit code
+ prefix := code[:tool.TIME_LIMIT_CODE_LENGTH]
+ data := com.ToStr(user.ID) + user.Email + user.LowerName + user.Passwd + user.Rands
+
+ if tool.VerifyTimeLimitCode(data, minutes, prefix) {
+ return user
+ }
+ }
+ return nil
+}
+
+// verify active code when active account
+func VerifyActiveEmailCode(code, email string) *EmailAddress {
+ minutes := setting.Service.ActiveCodeLives
+
+ if user := parseUserFromCode(code); user != nil {
+ // time limit code
+ prefix := code[:tool.TIME_LIMIT_CODE_LENGTH]
+ data := com.ToStr(user.ID) + email + user.LowerName + user.Passwd + user.Rands
+
+ if tool.VerifyTimeLimitCode(data, minutes, prefix) {
+ emailAddress := &EmailAddress{Email: email}
+ if has, _ := x.Get(emailAddress); has {
+ return emailAddress
+ }
+ }
+ }
+ return nil
+}
+
+// ChangeUserName changes all corresponding setting from old user name to new one.
+func ChangeUserName(u *User, newUserName string) (err error) {
+ if err = IsUsableUsername(newUserName); err != nil {
+ return err
+ }
+
+ isExist, err := IsUserExist(0, newUserName)
+ if err != nil {
+ return err
+ } else if isExist {
+ return ErrUserAlreadyExist{newUserName}
+ }
+
+ if err = ChangeUsernameInPullRequests(u.Name, newUserName); err != nil {
+ return fmt.Errorf("ChangeUsernameInPullRequests: %v", err)
+ }
+
+ // Delete all local copies of repository wiki that user owns.
+ if err = x.Where("owner_id=?", u.ID).Iterate(new(Repository), func(idx int, bean interface{}) error {
+ repo := bean.(*Repository)
+ RemoveAllWithNotice("Delete repository wiki local copy", repo.LocalWikiPath())
+ return nil
+ }); err != nil {
+ return fmt.Errorf("Delete repository wiki local copy: %v", err)
+ }
+
+ // Rename or create user base directory
+ baseDir := UserPath(u.Name)
+ newBaseDir := UserPath(newUserName)
+ if com.IsExist(baseDir) {
+ return os.Rename(baseDir, newBaseDir)
+ }
+ return os.MkdirAll(newBaseDir, os.ModePerm)
+}
+
+func updateUser(e Engine, u *User) error {
+ // Organization does not need email
+ if !u.IsOrganization() {
+ u.Email = strings.ToLower(u.Email)
+ has, err := e.Where("id!=?", u.ID).And("type=?", u.Type).And("email=?", u.Email).Get(new(User))
+ if err != nil {
+ return err
+ } else if has {
+ return ErrEmailAlreadyUsed{u.Email}
+ }
+
+ if len(u.AvatarEmail) == 0 {
+ u.AvatarEmail = u.Email
+ }
+ u.Avatar = tool.HashEmail(u.AvatarEmail)
+ }
+
+ u.LowerName = strings.ToLower(u.Name)
+ u.Location = tool.TruncateString(u.Location, 255)
+ u.Website = tool.TruncateString(u.Website, 255)
+ u.Description = tool.TruncateString(u.Description, 255)
+
+ _, err := e.ID(u.ID).AllCols().Update(u)
+ return err
+}
+
+// UpdateUser updates user's information.
+func UpdateUser(u *User) error {
+ return updateUser(x, u)
+}
+
+// deleteBeans deletes all given beans, beans should contain delete conditions.
+func deleteBeans(e Engine, beans ...interface{}) (err error) {
+ for i := range beans {
+ if _, err = e.Delete(beans[i]); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// FIXME: need some kind of mechanism to record failure. HINT: system notice
+func deleteUser(e *xorm.Session, u *User) error {
+ // Note: A user owns any repository or belongs to any organization
+ // cannot perform delete operation.
+
+ // Check ownership of repository.
+ count, err := getRepositoryCount(e, u)
+ if err != nil {
+ return fmt.Errorf("GetRepositoryCount: %v", err)
+ } else if count > 0 {
+ return ErrUserOwnRepos{UID: u.ID}
+ }
+
+ // Check membership of organization.
+ count, err = u.getOrganizationCount(e)
+ if err != nil {
+ return fmt.Errorf("GetOrganizationCount: %v", err)
+ } else if count > 0 {
+ return ErrUserHasOrgs{UID: u.ID}
+ }
+
+ // ***** START: Watch *****
+ watches := make([]*Watch, 0, 10)
+ if err = e.Find(&watches, &Watch{UserID: u.ID}); err != nil {
+ return fmt.Errorf("get all watches: %v", err)
+ }
+ for i := range watches {
+ if _, err = e.Exec("UPDATE `repository` SET num_watches=num_watches-1 WHERE id=?", watches[i].RepoID); err != nil {
+ return fmt.Errorf("decrease repository watch number[%d]: %v", watches[i].RepoID, err)
+ }
+ }
+ // ***** END: Watch *****
+
+ // ***** START: Star *****
+ stars := make([]*Star, 0, 10)
+ if err = e.Find(&stars, &Star{UID: u.ID}); err != nil {
+ return fmt.Errorf("get all stars: %v", err)
+ }
+ for i := range stars {
+ if _, err = e.Exec("UPDATE `repository` SET num_stars=num_stars-1 WHERE id=?", stars[i].RepoID); err != nil {
+ return fmt.Errorf("decrease repository star number[%d]: %v", stars[i].RepoID, err)
+ }
+ }
+ // ***** END: Star *****
+
+ // ***** START: Follow *****
+ followers := make([]*Follow, 0, 10)
+ if err = e.Find(&followers, &Follow{UserID: u.ID}); err != nil {
+ return fmt.Errorf("get all followers: %v", err)
+ }
+ for i := range followers {
+ if _, err = e.Exec("UPDATE `user` SET num_followers=num_followers-1 WHERE id=?", followers[i].UserID); err != nil {
+ return fmt.Errorf("decrease user follower number[%d]: %v", followers[i].UserID, err)
+ }
+ }
+ // ***** END: Follow *****
+
+ if err = deleteBeans(e,
+ &AccessToken{UID: u.ID},
+ &Collaboration{UserID: u.ID},
+ &Access{UserID: u.ID},
+ &Watch{UserID: u.ID},
+ &Star{UID: u.ID},
+ &Follow{FollowID: u.ID},
+ &Action{UserID: u.ID},
+ &IssueUser{UID: u.ID},
+ &EmailAddress{UID: u.ID},
+ ); err != nil {
+ return fmt.Errorf("deleteBeans: %v", err)
+ }
+
+ // ***** START: PublicKey *****
+ keys := make([]*PublicKey, 0, 10)
+ if err = e.Find(&keys, &PublicKey{OwnerID: u.ID}); err != nil {
+ return fmt.Errorf("get all public keys: %v", err)
+ }
+
+ keyIDs := make([]int64, len(keys))
+ for i := range keys {
+ keyIDs[i] = keys[i].ID
+ }
+ if err = deletePublicKeys(e, keyIDs...); err != nil {
+ return fmt.Errorf("deletePublicKeys: %v", err)
+ }
+ // ***** END: PublicKey *****
+
+ // Clear assignee.
+ if _, err = e.Exec("UPDATE `issue` SET assignee_id=0 WHERE assignee_id=?", u.ID); err != nil {
+ return fmt.Errorf("clear assignee: %v", err)
+ }
+
+ if _, err = e.Id(u.ID).Delete(new(User)); err != nil {
+ return fmt.Errorf("Delete: %v", err)
+ }
+
+ // FIXME: system notice
+ // Note: There are something just cannot be roll back,
+ // so just keep error logs of those operations.
+
+ os.RemoveAll(UserPath(u.Name))
+ os.Remove(u.CustomAvatarPath())
+
+ return nil
+}
+
+// DeleteUser completely and permanently deletes everything of a user,
+// but issues/comments/pulls will be kept and shown as someone has been deleted.
+func DeleteUser(u *User) (err error) {
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if err = deleteUser(sess, u); err != nil {
+ // Note: don't wrapper error here.
+ return err
+ }
+
+ if err = sess.Commit(); err != nil {
+ return err
+ }
+
+ return RewriteAuthorizedKeys()
+}
+
+// DeleteInactivateUsers deletes all inactivate users and email addresses.
+func DeleteInactivateUsers() (err error) {
+ users := make([]*User, 0, 10)
+ if err = x.Where("is_active = ?", false).Find(&users); err != nil {
+ return fmt.Errorf("get all inactive users: %v", err)
+ }
+ // FIXME: should only update authorized_keys file once after all deletions.
+ for _, u := range users {
+ if err = DeleteUser(u); err != nil {
+ // Ignore users that were set inactive by admin.
+ if IsErrUserOwnRepos(err) || IsErrUserHasOrgs(err) {
+ continue
+ }
+ return err
+ }
+ }
+
+ _, err = x.Where("is_activated = ?", false).Delete(new(EmailAddress))
+ return err
+}
+
+// UserPath returns the path absolute path of user repositories.
+func UserPath(userName string) string {
+ return filepath.Join(setting.RepoRootPath, strings.ToLower(userName))
+}
+
+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)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, errors.UserNotKeyOwner{keyID}
+ }
+ return user, nil
+}
+
+func getUserByID(e Engine, id int64) (*User, error) {
+ u := new(User)
+ has, err := e.ID(id).Get(u)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, errors.UserNotExist{id, ""}
+ }
+ return u, nil
+}
+
+// GetUserByID returns the user object by given ID if exists.
+func GetUserByID(id int64) (*User, error) {
+ return getUserByID(x, id)
+}
+
+// GetAssigneeByID returns the user with write access of repository by given ID.
+func GetAssigneeByID(repo *Repository, userID int64) (*User, error) {
+ has, err := HasAccess(userID, repo, ACCESS_MODE_READ)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, errors.UserNotExist{userID, ""}
+ }
+ return GetUserByID(userID)
+}
+
+// GetUserByName returns a user by given name.
+func GetUserByName(name string) (*User, error) {
+ if len(name) == 0 {
+ return nil, errors.UserNotExist{0, name}
+ }
+ u := &User{LowerName: strings.ToLower(name)}
+ has, err := x.Get(u)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, errors.UserNotExist{0, name}
+ }
+ return u, nil
+}
+
+// GetUserEmailsByNames returns a list of e-mails corresponds to names.
+func GetUserEmailsByNames(names []string) []string {
+ mails := make([]string, 0, len(names))
+ for _, name := range names {
+ u, err := GetUserByName(name)
+ if err != nil {
+ continue
+ }
+ if u.IsMailable() {
+ mails = append(mails, u.Email)
+ }
+ }
+ return mails
+}
+
+// GetUserIDsByNames returns a slice of ids corresponds to names.
+func GetUserIDsByNames(names []string) []int64 {
+ ids := make([]int64, 0, len(names))
+ for _, name := range names {
+ u, err := GetUserByName(name)
+ if err != nil {
+ continue
+ }
+ ids = append(ids, u.ID)
+ }
+ return ids
+}
+
+// UserCommit represents a commit with validation of user.
+type UserCommit struct {
+ User *User
+ *git.Commit
+}
+
+// ValidateCommitWithEmail chceck if author's e-mail of commit is corresponsind to a user.
+func ValidateCommitWithEmail(c *git.Commit) *User {
+ u, err := GetUserByEmail(c.Author.Email)
+ if err != nil {
+ return nil
+ }
+ return u
+}
+
+// ValidateCommitsWithEmails checks if authors' e-mails of commits are corresponding to users.
+func ValidateCommitsWithEmails(oldCommits *list.List) *list.List {
+ var (
+ u *User
+ emails = map[string]*User{}
+ newCommits = list.New()
+ e = oldCommits.Front()
+ )
+ for e != nil {
+ c := e.Value.(*git.Commit)
+
+ if v, ok := emails[c.Author.Email]; !ok {
+ u, _ = GetUserByEmail(c.Author.Email)
+ emails[c.Author.Email] = u
+ } else {
+ u = v
+ }
+
+ newCommits.PushBack(UserCommit{
+ User: u,
+ Commit: c,
+ })
+ e = e.Next()
+ }
+ return newCommits
+}
+
+// GetUserByEmail returns the user object by given e-mail if exists.
+func GetUserByEmail(email string) (*User, error) {
+ if len(email) == 0 {
+ return nil, errors.UserNotExist{0, "email"}
+ }
+
+ email = strings.ToLower(email)
+ // First try to find the user by primary email
+ user := &User{Email: email}
+ has, err := x.Get(user)
+ if err != nil {
+ return nil, err
+ }
+ if has {
+ return user, nil
+ }
+
+ // Otherwise, check in alternative list for activated email addresses
+ emailAddress := &EmailAddress{Email: email, IsActivated: true}
+ has, err = x.Get(emailAddress)
+ if err != nil {
+ return nil, err
+ }
+ if has {
+ return GetUserByID(emailAddress.UID)
+ }
+
+ return nil, errors.UserNotExist{0, email}
+}
+
+type SearchUserOptions struct {
+ Keyword string
+ Type UserType
+ OrderBy string
+ Page int
+ PageSize int // Can be smaller than or equal to setting.UI.ExplorePagingNum
+}
+
+// SearchUserByName takes keyword and part of user name to search,
+// it returns results in given range and number of total results.
+func SearchUserByName(opts *SearchUserOptions) (users []*User, _ int64, _ error) {
+ if len(opts.Keyword) == 0 {
+ return users, 0, nil
+ }
+ opts.Keyword = strings.ToLower(opts.Keyword)
+
+ if opts.PageSize <= 0 || opts.PageSize > setting.UI.ExplorePagingNum {
+ opts.PageSize = setting.UI.ExplorePagingNum
+ }
+ if opts.Page <= 0 {
+ opts.Page = 1
+ }
+
+ searchQuery := "%" + opts.Keyword + "%"
+ users = make([]*User, 0, opts.PageSize)
+ // Append conditions
+ sess := x.Where("LOWER(lower_name) LIKE ?", searchQuery).
+ Or("LOWER(full_name) LIKE ?", searchQuery).
+ And("type = ?", opts.Type)
+
+ var countSess xorm.Session
+ countSess = *sess
+ count, err := countSess.Count(new(User))
+ if err != nil {
+ return nil, 0, fmt.Errorf("Count: %v", err)
+ }
+
+ if len(opts.OrderBy) > 0 {
+ sess.OrderBy(opts.OrderBy)
+ }
+ return users, count, sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize).Find(&users)
+}
+
+// ___________ .__ .__
+// \_ _____/___ | | | | ______ _ __
+// | __)/ _ \| | | | / _ \ \/ \/ /
+// | \( <_> ) |_| |_( <_> ) /
+// \___ / \____/|____/____/\____/ \/\_/
+// \/
+
+// Follow represents relations of user and his/her followers.
+type Follow struct {
+ ID int64
+ UserID int64 `xorm:"UNIQUE(follow)"`
+ FollowID int64 `xorm:"UNIQUE(follow)"`
+}
+
+func IsFollowing(userID, followID int64) bool {
+ has, _ := x.Get(&Follow{UserID: userID, FollowID: followID})
+ return has
+}
+
+// FollowUser marks someone be another's follower.
+func FollowUser(userID, followID int64) (err error) {
+ if userID == followID || IsFollowing(userID, followID) {
+ return nil
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if _, err = sess.Insert(&Follow{UserID: userID, FollowID: followID}); err != nil {
+ return err
+ }
+
+ if _, err = sess.Exec("UPDATE `user` SET num_followers = num_followers + 1 WHERE id = ?", followID); err != nil {
+ return err
+ }
+
+ if _, err = sess.Exec("UPDATE `user` SET num_following = num_following + 1 WHERE id = ?", userID); err != nil {
+ return err
+ }
+ return sess.Commit()
+}
+
+// UnfollowUser unmarks someone be another's follower.
+func UnfollowUser(userID, followID int64) (err error) {
+ if userID == followID || !IsFollowing(userID, followID) {
+ return nil
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if _, err = sess.Delete(&Follow{UserID: userID, FollowID: followID}); err != nil {
+ return err
+ }
+
+ if _, err = sess.Exec("UPDATE `user` SET num_followers = num_followers - 1 WHERE id = ?", followID); err != nil {
+ return err
+ }
+
+ if _, err = sess.Exec("UPDATE `user` SET num_following = num_following - 1 WHERE id = ?", userID); err != nil {
+ return err
+ }
+ return sess.Commit()
+}
diff --git a/internal/db/user_cache.go b/internal/db/user_cache.go
new file mode 100644
index 00000000..314ea3b1
--- /dev/null
+++ b/internal/db/user_cache.go
@@ -0,0 +1,16 @@
+// Copyright 2018 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 db
+
+// MailResendCacheKey returns key used for cache mail resend.
+func (u *User) MailResendCacheKey() string {
+ return "MailResend_" + u.IDStr()
+}
+
+// TwoFactorCacheKey returns key used for cache two factor passcode.
+// e.g. TwoFactor_1_012664
+func (u *User) TwoFactorCacheKey(passcode string) string {
+ return "TwoFactor_" + u.IDStr() + "_" + passcode
+}
diff --git a/internal/db/user_mail.go b/internal/db/user_mail.go
new file mode 100644
index 00000000..5304f13b
--- /dev/null
+++ b/internal/db/user_mail.go
@@ -0,0 +1,210 @@
+// Copyright 2016 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 db
+
+import (
+ "fmt"
+ "strings"
+
+ "gogs.io/gogs/internal/db/errors"
+)
+
+// EmailAdresses is the list of all email addresses of a user. Can contain the
+// primary email address, but is not obligatory.
+type EmailAddress struct {
+ ID int64
+ UID int64 `xorm:"INDEX NOT NULL"`
+ Email string `xorm:"UNIQUE NOT NULL"`
+ IsActivated bool
+ IsPrimary bool `xorm:"-" json:"-"`
+}
+
+// GetEmailAddresses returns all email addresses belongs to given user.
+func GetEmailAddresses(uid int64) ([]*EmailAddress, error) {
+ emails := make([]*EmailAddress, 0, 5)
+ if err := x.Where("uid=?", uid).Find(&emails); err != nil {
+ return nil, err
+ }
+
+ u, err := GetUserByID(uid)
+ if err != nil {
+ return nil, err
+ }
+
+ isPrimaryFound := false
+ for _, email := range emails {
+ if email.Email == u.Email {
+ isPrimaryFound = true
+ email.IsPrimary = true
+ } else {
+ email.IsPrimary = false
+ }
+ }
+
+ // We alway want the primary email address displayed, even if it's not in
+ // the emailaddress table (yet).
+ if !isPrimaryFound {
+ emails = append(emails, &EmailAddress{
+ Email: u.Email,
+ IsActivated: true,
+ IsPrimary: true,
+ })
+ }
+ return emails, nil
+}
+
+func isEmailUsed(e Engine, email string) (bool, error) {
+ if len(email) == 0 {
+ return true, nil
+ }
+
+ has, err := e.Get(&EmailAddress{Email: email})
+ if err != nil {
+ return false, err
+ } else if has {
+ return true, nil
+ }
+
+ // We need to check primary email of users as well.
+ return e.Where("type=?", USER_TYPE_INDIVIDUAL).And("email=?", email).Get(new(User))
+}
+
+// IsEmailUsed returns true if the email has been used.
+func IsEmailUsed(email string) (bool, error) {
+ return isEmailUsed(x, email)
+}
+
+func addEmailAddress(e Engine, email *EmailAddress) error {
+ email.Email = strings.ToLower(strings.TrimSpace(email.Email))
+ used, err := isEmailUsed(e, email.Email)
+ if err != nil {
+ return err
+ } else if used {
+ return ErrEmailAlreadyUsed{email.Email}
+ }
+
+ _, err = e.Insert(email)
+ return err
+}
+
+func AddEmailAddress(email *EmailAddress) error {
+ return addEmailAddress(x, email)
+}
+
+func AddEmailAddresses(emails []*EmailAddress) error {
+ if len(emails) == 0 {
+ return nil
+ }
+
+ // Check if any of them has been used
+ for i := range emails {
+ emails[i].Email = strings.ToLower(strings.TrimSpace(emails[i].Email))
+ used, err := IsEmailUsed(emails[i].Email)
+ if err != nil {
+ return err
+ } else if used {
+ return ErrEmailAlreadyUsed{emails[i].Email}
+ }
+ }
+
+ if _, err := x.Insert(emails); err != nil {
+ return fmt.Errorf("Insert: %v", err)
+ }
+
+ return nil
+}
+
+func (email *EmailAddress) Activate() error {
+ user, err := GetUserByID(email.UID)
+ if err != nil {
+ return err
+ }
+ if user.Rands, err = GetUserSalt(); err != nil {
+ return err
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ email.IsActivated = true
+ if _, err := sess.ID(email.ID).AllCols().Update(email); err != nil {
+ return err
+ } else if err = updateUser(sess, user); err != nil {
+ return err
+ }
+
+ return sess.Commit()
+}
+
+func DeleteEmailAddress(email *EmailAddress) (err error) {
+ if email.ID > 0 {
+ _, err = x.Id(email.ID).Delete(new(EmailAddress))
+ } else {
+ _, err = x.Where("email=?", email.Email).Delete(new(EmailAddress))
+ }
+ return err
+}
+
+func DeleteEmailAddresses(emails []*EmailAddress) (err error) {
+ for i := range emails {
+ if err = DeleteEmailAddress(emails[i]); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func MakeEmailPrimary(email *EmailAddress) error {
+ has, err := x.Get(email)
+ if err != nil {
+ return err
+ } else if !has {
+ return errors.EmailNotFound{email.Email}
+ }
+
+ if !email.IsActivated {
+ return errors.EmailNotVerified{email.Email}
+ }
+
+ user := &User{ID: email.UID}
+ has, err = x.Get(user)
+ if err != nil {
+ return err
+ } else if !has {
+ return errors.UserNotExist{email.UID, ""}
+ }
+
+ // Make sure the former primary email doesn't disappear.
+ formerPrimaryEmail := &EmailAddress{Email: user.Email}
+ has, err = x.Get(formerPrimaryEmail)
+ if err != nil {
+ return err
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if !has {
+ formerPrimaryEmail.UID = user.ID
+ formerPrimaryEmail.IsActivated = user.IsActive
+ if _, err = sess.Insert(formerPrimaryEmail); err != nil {
+ return err
+ }
+ }
+
+ user.Email = email.Email
+ if _, err = sess.ID(user.ID).AllCols().Update(user); err != nil {
+ return err
+ }
+
+ return sess.Commit()
+}
diff --git a/internal/db/webhook.go b/internal/db/webhook.go
new file mode 100644
index 00000000..750cc613
--- /dev/null
+++ b/internal/db/webhook.go
@@ -0,0 +1,771 @@
+// Copyright 2014 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 db
+
+import (
+ "crypto/hmac"
+ "crypto/sha256"
+ "crypto/tls"
+ "encoding/hex"
+ "fmt"
+ "io/ioutil"
+ "strings"
+ "time"
+
+ "github.com/json-iterator/go"
+ gouuid "github.com/satori/go.uuid"
+ log "gopkg.in/clog.v1"
+ "xorm.io/xorm"
+
+ api "github.com/gogs/go-gogs-client"
+
+ "gogs.io/gogs/internal/db/errors"
+ "gogs.io/gogs/internal/httplib"
+ "gogs.io/gogs/internal/setting"
+ "gogs.io/gogs/internal/sync"
+)
+
+var HookQueue = sync.NewUniqueQueue(setting.Webhook.QueueLength)
+
+type HookContentType int
+
+const (
+ JSON HookContentType = iota + 1
+ FORM
+)
+
+var hookContentTypes = map[string]HookContentType{
+ "json": JSON,
+ "form": FORM,
+}
+
+// ToHookContentType returns HookContentType by given name.
+func ToHookContentType(name string) HookContentType {
+ return hookContentTypes[name]
+}
+
+func (t HookContentType) Name() string {
+ switch t {
+ case JSON:
+ return "json"
+ case FORM:
+ return "form"
+ }
+ return ""
+}
+
+// IsValidHookContentType returns true if given name is a valid hook content type.
+func IsValidHookContentType(name string) bool {
+ _, ok := hookContentTypes[name]
+ return ok
+}
+
+type HookEvents struct {
+ Create bool `json:"create"`
+ Delete bool `json:"delete"`
+ Fork bool `json:"fork"`
+ Push bool `json:"push"`
+ Issues bool `json:"issues"`
+ PullRequest bool `json:"pull_request"`
+ IssueComment bool `json:"issue_comment"`
+ Release bool `json:"release"`
+}
+
+// HookEvent represents events that will delivery hook.
+type HookEvent struct {
+ PushOnly bool `json:"push_only"`
+ SendEverything bool `json:"send_everything"`
+ ChooseEvents bool `json:"choose_events"`
+
+ HookEvents `json:"events"`
+}
+
+type HookStatus int
+
+const (
+ HOOK_STATUS_NONE = iota
+ HOOK_STATUS_SUCCEED
+ HOOK_STATUS_FAILED
+)
+
+// Webhook represents a web hook object.
+type Webhook struct {
+ ID int64
+ RepoID int64
+ OrgID int64
+ URL string `xorm:"url TEXT"`
+ ContentType HookContentType
+ Secret string `xorm:"TEXT"`
+ Events string `xorm:"TEXT"`
+ *HookEvent `xorm:"-"` // LEGACY [1.0]: Cannot ignore JSON here, it breaks old backup archive
+ IsSSL bool `xorm:"is_ssl"`
+ IsActive bool
+ HookTaskType HookTaskType
+ Meta string `xorm:"TEXT"` // store hook-specific attributes
+ LastStatus HookStatus // Last delivery status
+
+ Created time.Time `xorm:"-" json:"-"`
+ CreatedUnix int64
+ Updated time.Time `xorm:"-" json:"-"`
+ UpdatedUnix int64
+}
+
+func (w *Webhook) BeforeInsert() {
+ w.CreatedUnix = time.Now().Unix()
+ w.UpdatedUnix = w.CreatedUnix
+}
+
+func (w *Webhook) BeforeUpdate() {
+ w.UpdatedUnix = time.Now().Unix()
+}
+
+func (w *Webhook) AfterSet(colName string, _ xorm.Cell) {
+ var err error
+ switch colName {
+ case "events":
+ w.HookEvent = &HookEvent{}
+ if err = jsoniter.Unmarshal([]byte(w.Events), w.HookEvent); err != nil {
+ log.Error(3, "Unmarshal [%d]: %v", w.ID, err)
+ }
+ case "created_unix":
+ w.Created = time.Unix(w.CreatedUnix, 0).Local()
+ case "updated_unix":
+ w.Updated = time.Unix(w.UpdatedUnix, 0).Local()
+ }
+}
+
+func (w *Webhook) GetSlackHook() *SlackMeta {
+ s := &SlackMeta{}
+ if err := jsoniter.Unmarshal([]byte(w.Meta), s); err != nil {
+ log.Error(2, "GetSlackHook [%d]: %v", w.ID, err)
+ }
+ return s
+}
+
+// History returns history of webhook by given conditions.
+func (w *Webhook) History(page int) ([]*HookTask, error) {
+ return HookTasks(w.ID, page)
+}
+
+// UpdateEvent handles conversion from HookEvent to Events.
+func (w *Webhook) UpdateEvent() error {
+ data, err := jsoniter.Marshal(w.HookEvent)
+ w.Events = string(data)
+ return err
+}
+
+// HasCreateEvent returns true if hook enabled create event.
+func (w *Webhook) HasCreateEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.Create)
+}
+
+// HasDeleteEvent returns true if hook enabled delete event.
+func (w *Webhook) HasDeleteEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.Delete)
+}
+
+// HasForkEvent returns true if hook enabled fork event.
+func (w *Webhook) HasForkEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.Fork)
+}
+
+// HasPushEvent returns true if hook enabled push event.
+func (w *Webhook) HasPushEvent() bool {
+ return w.PushOnly || w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.Push)
+}
+
+// HasIssuesEvent returns true if hook enabled issues event.
+func (w *Webhook) HasIssuesEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.Issues)
+}
+
+// HasPullRequestEvent returns true if hook enabled pull request event.
+func (w *Webhook) HasPullRequestEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.PullRequest)
+}
+
+// HasIssueCommentEvent returns true if hook enabled issue comment event.
+func (w *Webhook) HasIssueCommentEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.IssueComment)
+}
+
+// HasReleaseEvent returns true if hook enabled release event.
+func (w *Webhook) HasReleaseEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.Release)
+}
+
+type eventChecker struct {
+ checker func() bool
+ typ HookEventType
+}
+
+func (w *Webhook) EventsArray() []string {
+ events := make([]string, 0, 8)
+ eventCheckers := []eventChecker{
+ {w.HasCreateEvent, HOOK_EVENT_CREATE},
+ {w.HasDeleteEvent, HOOK_EVENT_DELETE},
+ {w.HasForkEvent, HOOK_EVENT_FORK},
+ {w.HasPushEvent, HOOK_EVENT_PUSH},
+ {w.HasIssuesEvent, HOOK_EVENT_ISSUES},
+ {w.HasPullRequestEvent, HOOK_EVENT_PULL_REQUEST},
+ {w.HasIssueCommentEvent, HOOK_EVENT_ISSUE_COMMENT},
+ {w.HasReleaseEvent, HOOK_EVENT_RELEASE},
+ }
+ for _, c := range eventCheckers {
+ if c.checker() {
+ events = append(events, string(c.typ))
+ }
+ }
+ return events
+}
+
+// CreateWebhook creates a new web hook.
+func CreateWebhook(w *Webhook) error {
+ _, err := x.Insert(w)
+ return err
+}
+
+// getWebhook uses argument bean as query condition,
+// ID must be specified and do not assign unnecessary fields.
+func getWebhook(bean *Webhook) (*Webhook, error) {
+ has, err := x.Get(bean)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, errors.WebhookNotExist{bean.ID}
+ }
+ return bean, nil
+}
+
+// GetWebhookByID returns webhook by given ID.
+// Use this function with caution of accessing unauthorized webhook,
+// which means should only be used in non-user interactive functions.
+func GetWebhookByID(id int64) (*Webhook, error) {
+ return getWebhook(&Webhook{
+ ID: id,
+ })
+}
+
+// GetWebhookOfRepoByID returns webhook of repository by given ID.
+func GetWebhookOfRepoByID(repoID, id int64) (*Webhook, error) {
+ return getWebhook(&Webhook{
+ ID: id,
+ RepoID: repoID,
+ })
+}
+
+// GetWebhookByOrgID returns webhook of organization by given ID.
+func GetWebhookByOrgID(orgID, id int64) (*Webhook, error) {
+ return getWebhook(&Webhook{
+ ID: id,
+ OrgID: orgID,
+ })
+}
+
+// getActiveWebhooksByRepoID returns all active webhooks of repository.
+func getActiveWebhooksByRepoID(e Engine, repoID int64) ([]*Webhook, error) {
+ webhooks := make([]*Webhook, 0, 5)
+ return webhooks, e.Where("repo_id = ?", repoID).And("is_active = ?", true).Find(&webhooks)
+}
+
+// GetWebhooksByRepoID returns all webhooks of a repository.
+func GetWebhooksByRepoID(repoID int64) ([]*Webhook, error) {
+ webhooks := make([]*Webhook, 0, 5)
+ return webhooks, x.Find(&webhooks, &Webhook{RepoID: repoID})
+}
+
+// UpdateWebhook updates information of webhook.
+func UpdateWebhook(w *Webhook) error {
+ _, err := x.Id(w.ID).AllCols().Update(w)
+ return err
+}
+
+// deleteWebhook uses argument bean as query condition,
+// ID must be specified and do not assign unnecessary fields.
+func deleteWebhook(bean *Webhook) (err error) {
+ sess := x.NewSession()
+ defer sess.Close()
+ if err = sess.Begin(); err != nil {
+ return err
+ }
+
+ if _, err = sess.Delete(bean); err != nil {
+ return err
+ } else if _, err = sess.Delete(&HookTask{HookID: bean.ID}); err != nil {
+ return err
+ }
+
+ return sess.Commit()
+}
+
+// DeleteWebhookOfRepoByID deletes webhook of repository by given ID.
+func DeleteWebhookOfRepoByID(repoID, id int64) error {
+ return deleteWebhook(&Webhook{
+ ID: id,
+ RepoID: repoID,
+ })
+}
+
+// DeleteWebhookOfOrgByID deletes webhook of organization by given ID.
+func DeleteWebhookOfOrgByID(orgID, id int64) error {
+ return deleteWebhook(&Webhook{
+ ID: id,
+ OrgID: orgID,
+ })
+}
+
+// GetWebhooksByOrgID returns all webhooks for an organization.
+func GetWebhooksByOrgID(orgID int64) (ws []*Webhook, err error) {
+ err = x.Find(&ws, &Webhook{OrgID: orgID})
+ return ws, err
+}
+
+// getActiveWebhooksByOrgID returns all active webhooks for an organization.
+func getActiveWebhooksByOrgID(e Engine, orgID int64) ([]*Webhook, error) {
+ ws := make([]*Webhook, 0, 3)
+ return ws, e.Where("org_id=?", orgID).And("is_active=?", true).Find(&ws)
+}
+
+// ___ ___ __ ___________ __
+// / | \ ____ ____ | | _\__ ___/____ _____| | __
+// / ~ \/ _ \ / _ \| |/ / | | \__ \ / ___/ |/ /
+// \ Y ( <_> | <_> ) < | | / __ \_\___ \| <
+// \___|_ / \____/ \____/|__|_ \ |____| (____ /____ >__|_ \
+// \/ \/ \/ \/ \/
+
+type HookTaskType int
+
+const (
+ GOGS HookTaskType = iota + 1
+ SLACK
+ DISCORD
+ DINGTALK
+)
+
+var hookTaskTypes = map[string]HookTaskType{
+ "gogs": GOGS,
+ "slack": SLACK,
+ "discord": DISCORD,
+ "dingtalk": DINGTALK,
+}
+
+// ToHookTaskType returns HookTaskType by given name.
+func ToHookTaskType(name string) HookTaskType {
+ return hookTaskTypes[name]
+}
+
+func (t HookTaskType) Name() string {
+ switch t {
+ case GOGS:
+ return "gogs"
+ case SLACK:
+ return "slack"
+ case DISCORD:
+ return "discord"
+ case DINGTALK:
+ return "dingtalk"
+ }
+ return ""
+}
+
+// IsValidHookTaskType returns true if given name is a valid hook task type.
+func IsValidHookTaskType(name string) bool {
+ _, ok := hookTaskTypes[name]
+ return ok
+}
+
+type HookEventType string
+
+const (
+ HOOK_EVENT_CREATE HookEventType = "create"
+ HOOK_EVENT_DELETE HookEventType = "delete"
+ HOOK_EVENT_FORK HookEventType = "fork"
+ HOOK_EVENT_PUSH HookEventType = "push"
+ HOOK_EVENT_ISSUES HookEventType = "issues"
+ HOOK_EVENT_PULL_REQUEST HookEventType = "pull_request"
+ HOOK_EVENT_ISSUE_COMMENT HookEventType = "issue_comment"
+ HOOK_EVENT_RELEASE HookEventType = "release"
+)
+
+// HookRequest represents hook task request information.
+type HookRequest struct {
+ Headers map[string]string `json:"headers"`
+}
+
+// HookResponse represents hook task response information.
+type HookResponse struct {
+ Status int `json:"status"`
+ Headers map[string]string `json:"headers"`
+ Body string `json:"body"`
+}
+
+// HookTask represents a hook task.
+type HookTask struct {
+ ID int64
+ RepoID int64 `xorm:"INDEX"`
+ HookID int64
+ UUID string
+ Type HookTaskType
+ URL string `xorm:"TEXT"`
+ Signature string `xorm:"TEXT"`
+ api.Payloader `xorm:"-" json:"-"`
+ PayloadContent string `xorm:"TEXT"`
+ ContentType HookContentType
+ EventType HookEventType
+ IsSSL bool
+ IsDelivered bool
+ Delivered int64
+ DeliveredString string `xorm:"-" json:"-"`
+
+ // History info.
+ IsSucceed bool
+ RequestContent string `xorm:"TEXT"`
+ RequestInfo *HookRequest `xorm:"-" json:"-"`
+ ResponseContent string `xorm:"TEXT"`
+ ResponseInfo *HookResponse `xorm:"-" json:"-"`
+}
+
+func (t *HookTask) BeforeUpdate() {
+ if t.RequestInfo != nil {
+ t.RequestContent = t.MarshalJSON(t.RequestInfo)
+ }
+ if t.ResponseInfo != nil {
+ t.ResponseContent = t.MarshalJSON(t.ResponseInfo)
+ }
+}
+
+func (t *HookTask) AfterSet(colName string, _ xorm.Cell) {
+ var err error
+ switch colName {
+ case "delivered":
+ t.DeliveredString = time.Unix(0, t.Delivered).Format("2006-01-02 15:04:05 MST")
+
+ case "request_content":
+ if len(t.RequestContent) == 0 {
+ return
+ }
+
+ t.RequestInfo = &HookRequest{}
+ if err = jsoniter.Unmarshal([]byte(t.RequestContent), t.RequestInfo); err != nil {
+ log.Error(3, "Unmarshal[%d]: %v", t.ID, err)
+ }
+
+ case "response_content":
+ if len(t.ResponseContent) == 0 {
+ return
+ }
+
+ t.ResponseInfo = &HookResponse{}
+ if err = jsoniter.Unmarshal([]byte(t.ResponseContent), t.ResponseInfo); err != nil {
+ log.Error(3, "Unmarshal [%d]: %v", t.ID, err)
+ }
+ }
+}
+
+func (t *HookTask) MarshalJSON(v interface{}) string {
+ p, err := jsoniter.Marshal(v)
+ if err != nil {
+ log.Error(3, "Marshal [%d]: %v", t.ID, err)
+ }
+ return string(p)
+}
+
+// HookTasks returns a list of hook tasks by given conditions.
+func HookTasks(hookID int64, page int) ([]*HookTask, error) {
+ tasks := make([]*HookTask, 0, setting.Webhook.PagingNum)
+ return tasks, x.Limit(setting.Webhook.PagingNum, (page-1)*setting.Webhook.PagingNum).Where("hook_id=?", hookID).Desc("id").Find(&tasks)
+}
+
+// createHookTask creates a new hook task,
+// it handles conversion from Payload to PayloadContent.
+func createHookTask(e Engine, t *HookTask) error {
+ data, err := t.Payloader.JSONPayload()
+ if err != nil {
+ return err
+ }
+ t.UUID = gouuid.NewV4().String()
+ t.PayloadContent = string(data)
+ _, err = e.Insert(t)
+ return err
+}
+
+// GetHookTaskOfWebhookByUUID returns hook task of given webhook by UUID.
+func GetHookTaskOfWebhookByUUID(webhookID int64, uuid string) (*HookTask, error) {
+ hookTask := &HookTask{
+ HookID: webhookID,
+ UUID: uuid,
+ }
+ has, err := x.Get(hookTask)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, errors.HookTaskNotExist{webhookID, uuid}
+ }
+ return hookTask, nil
+}
+
+// UpdateHookTask updates information of hook task.
+func UpdateHookTask(t *HookTask) error {
+ _, err := x.Id(t.ID).AllCols().Update(t)
+ return err
+}
+
+// prepareHookTasks adds list of webhooks to task queue.
+func prepareHookTasks(e Engine, repo *Repository, event HookEventType, p api.Payloader, webhooks []*Webhook) (err error) {
+ if len(webhooks) == 0 {
+ return nil
+ }
+
+ var payloader api.Payloader
+ for _, w := range webhooks {
+ switch event {
+ case HOOK_EVENT_CREATE:
+ if !w.HasCreateEvent() {
+ continue
+ }
+ case HOOK_EVENT_DELETE:
+ if !w.HasDeleteEvent() {
+ continue
+ }
+ case HOOK_EVENT_FORK:
+ if !w.HasForkEvent() {
+ continue
+ }
+ case HOOK_EVENT_PUSH:
+ if !w.HasPushEvent() {
+ continue
+ }
+ case HOOK_EVENT_ISSUES:
+ if !w.HasIssuesEvent() {
+ continue
+ }
+ case HOOK_EVENT_PULL_REQUEST:
+ if !w.HasPullRequestEvent() {
+ continue
+ }
+ case HOOK_EVENT_ISSUE_COMMENT:
+ if !w.HasIssueCommentEvent() {
+ continue
+ }
+ case HOOK_EVENT_RELEASE:
+ if !w.HasReleaseEvent() {
+ continue
+ }
+ }
+
+ // Use separate objects so modifcations won't be made on payload on non-Gogs type hooks.
+ switch w.HookTaskType {
+ case SLACK:
+ payloader, err = GetSlackPayload(p, event, w.Meta)
+ if err != nil {
+ return fmt.Errorf("GetSlackPayload: %v", err)
+ }
+ case DISCORD:
+ payloader, err = GetDiscordPayload(p, event, w.Meta)
+ if err != nil {
+ return fmt.Errorf("GetDiscordPayload: %v", err)
+ }
+ case DINGTALK:
+ payloader, err = GetDingtalkPayload(p, event)
+ if err != nil {
+ return fmt.Errorf("GetDingtalkPayload: %v", err)
+ }
+ default:
+ payloader = p
+ }
+
+ var signature string
+ if len(w.Secret) > 0 {
+ data, err := payloader.JSONPayload()
+ if err != nil {
+ log.Error(2, "prepareWebhooks.JSONPayload: %v", err)
+ }
+ sig := hmac.New(sha256.New, []byte(w.Secret))
+ sig.Write(data)
+ signature = hex.EncodeToString(sig.Sum(nil))
+ }
+
+ if err = createHookTask(e, &HookTask{
+ RepoID: repo.ID,
+ HookID: w.ID,
+ Type: w.HookTaskType,
+ URL: w.URL,
+ Signature: signature,
+ Payloader: payloader,
+ ContentType: w.ContentType,
+ EventType: event,
+ IsSSL: w.IsSSL,
+ }); err != nil {
+ return fmt.Errorf("createHookTask: %v", err)
+ }
+ }
+
+ // It's safe to fail when the whole function is called during hook execution
+ // because resource released after exit. Also, there is no process started to
+ // consume this input during hook execution.
+ go HookQueue.Add(repo.ID)
+ return nil
+}
+
+func prepareWebhooks(e Engine, repo *Repository, event HookEventType, p api.Payloader) error {
+ webhooks, err := getActiveWebhooksByRepoID(e, repo.ID)
+ if err != nil {
+ return fmt.Errorf("getActiveWebhooksByRepoID [%d]: %v", repo.ID, err)
+ }
+
+ // check if repo belongs to org and append additional webhooks
+ if repo.mustOwner(e).IsOrganization() {
+ // get hooks for org
+ orgws, err := getActiveWebhooksByOrgID(e, repo.OwnerID)
+ if err != nil {
+ return fmt.Errorf("getActiveWebhooksByOrgID [%d]: %v", repo.OwnerID, err)
+ }
+ webhooks = append(webhooks, orgws...)
+ }
+ return prepareHookTasks(e, repo, event, p, webhooks)
+}
+
+// PrepareWebhooks adds all active webhooks to task queue.
+func PrepareWebhooks(repo *Repository, event HookEventType, p api.Payloader) error {
+ return prepareWebhooks(x, repo, event, p)
+}
+
+// TestWebhook adds the test webhook matches the ID to task queue.
+func TestWebhook(repo *Repository, event HookEventType, p api.Payloader, webhookID int64) error {
+ webhook, err := GetWebhookOfRepoByID(repo.ID, webhookID)
+ if err != nil {
+ return fmt.Errorf("GetWebhookOfRepoByID [repo_id: %d, id: %d]: %v", repo.ID, webhookID, err)
+ }
+ return prepareHookTasks(x, repo, event, p, []*Webhook{webhook})
+}
+
+func (t *HookTask) deliver() {
+ t.IsDelivered = true
+
+ timeout := time.Duration(setting.Webhook.DeliverTimeout) * time.Second
+ req := httplib.Post(t.URL).SetTimeout(timeout, timeout).
+ Header("X-Github-Delivery", t.UUID).
+ Header("X-Github-Event", string(t.EventType)).
+ Header("X-Gogs-Delivery", t.UUID).
+ Header("X-Gogs-Signature", t.Signature).
+ Header("X-Gogs-Event", string(t.EventType)).
+ SetTLSClientConfig(&tls.Config{InsecureSkipVerify: setting.Webhook.SkipTLSVerify})
+
+ switch t.ContentType {
+ case JSON:
+ req = req.Header("Content-Type", "application/json").Body(t.PayloadContent)
+ case FORM:
+ req.Param("payload", t.PayloadContent)
+ }
+
+ // Record delivery information.
+ t.RequestInfo = &HookRequest{
+ Headers: map[string]string{},
+ }
+ for k, vals := range req.Headers() {
+ t.RequestInfo.Headers[k] = strings.Join(vals, ",")
+ }
+
+ t.ResponseInfo = &HookResponse{
+ Headers: map[string]string{},
+ }
+
+ defer func() {
+ t.Delivered = time.Now().UnixNano()
+ if t.IsSucceed {
+ log.Trace("Hook delivered: %s", t.UUID)
+ } else {
+ log.Trace("Hook delivery failed: %s", t.UUID)
+ }
+
+ // Update webhook last delivery status.
+ w, err := GetWebhookByID(t.HookID)
+ if err != nil {
+ log.Error(3, "GetWebhookByID: %v", err)
+ return
+ }
+ if t.IsSucceed {
+ w.LastStatus = HOOK_STATUS_SUCCEED
+ } else {
+ w.LastStatus = HOOK_STATUS_FAILED
+ }
+ if err = UpdateWebhook(w); err != nil {
+ log.Error(3, "UpdateWebhook: %v", err)
+ return
+ }
+ }()
+
+ resp, err := req.Response()
+ if err != nil {
+ t.ResponseInfo.Body = fmt.Sprintf("Delivery: %v", err)
+ return
+ }
+ defer resp.Body.Close()
+
+ // Status code is 20x can be seen as succeed.
+ t.IsSucceed = resp.StatusCode/100 == 2
+ t.ResponseInfo.Status = resp.StatusCode
+ for k, vals := range resp.Header {
+ t.ResponseInfo.Headers[k] = strings.Join(vals, ",")
+ }
+
+ p, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ t.ResponseInfo.Body = fmt.Sprintf("read body: %s", err)
+ return
+ }
+ t.ResponseInfo.Body = string(p)
+}
+
+// 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)
+ t.deliver()
+ tasks = append(tasks, t)
+ return nil
+ })
+
+ // Update hook task status.
+ for _, t := range tasks {
+ if err := UpdateHookTask(t); err != nil {
+ log.Error(4, "UpdateHookTask [%d]: %v", t.ID, err)
+ }
+ }
+
+ // Start listening on new hook requests.
+ for repoID := range HookQueue.Queue() {
+ log.Trace("DeliverHooks [repo_id: %v]", repoID)
+ HookQueue.Remove(repoID)
+
+ tasks = make([]*HookTask, 0, 5)
+ if err := x.Where("repo_id = ?", repoID).And("is_delivered = ?", false).Find(&tasks); err != nil {
+ log.Error(4, "Get repository [%s] hook tasks: %v", repoID, err)
+ continue
+ }
+ for _, t := range tasks {
+ t.deliver()
+ if err := UpdateHookTask(t); err != nil {
+ log.Error(4, "UpdateHookTask [%d]: %v", t.ID, err)
+ continue
+ }
+ }
+ }
+}
+
+func InitDeliverHooks() {
+ go DeliverHooks()
+}
diff --git a/internal/db/webhook_dingtalk.go b/internal/db/webhook_dingtalk.go
new file mode 100644
index 00000000..4382803d
--- /dev/null
+++ b/internal/db/webhook_dingtalk.go
@@ -0,0 +1,261 @@
+// Copyright 2017 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 db
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/json-iterator/go"
+
+ "github.com/gogs/git-module"
+ api "github.com/gogs/go-gogs-client"
+)
+
+const (
+ DingtalkNotificationTitle = "Gogs Notification"
+)
+
+//Refer: https://open-doc.dingtalk.com/docs/doc.htm?treeId=257&articleId=105735&docType=1
+type DingtalkActionCard struct {
+ Title string `json:"title"`
+ Text string `json:"text"`
+ HideAvatar string `json:"hideAvatar"`
+ BtnOrientation string `json:"btnOrientation"`
+ SingleTitle string `json:"singleTitle"`
+ SingleURL string `json:"singleURL"`
+}
+
+//Refer: https://open-doc.dingtalk.com/docs/doc.htm?treeId=257&articleId=105735&docType=1
+type DingtalkAtObject struct {
+ AtMobiles []string `json:"atMobiles"`
+ IsAtAll bool `json:"isAtAll"`
+}
+
+//Refer: https://open-doc.dingtalk.com/docs/doc.htm?treeId=257&articleId=105735&docType=1
+type DingtalkPayload struct {
+ MsgType string `json:"msgtype"`
+ At DingtalkAtObject `json:"at"`
+ ActionCard DingtalkActionCard `json:"actionCard"`
+}
+
+func (p *DingtalkPayload) JSONPayload() ([]byte, error) {
+ data, err := jsoniter.MarshalIndent(p, "", " ")
+ if err != nil {
+ return []byte{}, err
+ }
+ return data, nil
+}
+
+func NewDingtalkActionCard(singleTitle, singleURL string) DingtalkActionCard {
+ return DingtalkActionCard{
+ Title: DingtalkNotificationTitle,
+ SingleURL: singleURL,
+ SingleTitle: singleTitle,
+ }
+}
+
+//TODO: add content
+func GetDingtalkPayload(p api.Payloader, event HookEventType) (payload *DingtalkPayload, err error) {
+ switch event {
+ case HOOK_EVENT_CREATE:
+ payload, err = getDingtalkCreatePayload(p.(*api.CreatePayload))
+ case HOOK_EVENT_DELETE:
+ payload, err = getDingtalkDeletePayload(p.(*api.DeletePayload))
+ case HOOK_EVENT_FORK:
+ payload, err = getDingtalkForkPayload(p.(*api.ForkPayload))
+ case HOOK_EVENT_PUSH:
+ payload, err = getDingtalkPushPayload(p.(*api.PushPayload))
+ case HOOK_EVENT_ISSUES:
+ payload, err = getDingtalkIssuesPayload(p.(*api.IssuesPayload))
+ case HOOK_EVENT_ISSUE_COMMENT:
+ payload, err = getDingtalkIssueCommentPayload(p.(*api.IssueCommentPayload))
+ case HOOK_EVENT_PULL_REQUEST:
+ payload, err = getDingtalkPullRequestPayload(p.(*api.PullRequestPayload))
+ case HOOK_EVENT_RELEASE:
+ payload, err = getDingtalkReleasePayload(p.(*api.ReleasePayload))
+ }
+
+ if err != nil {
+ return nil, fmt.Errorf("event '%s': %v", event, err)
+ }
+
+ return payload, nil
+}
+
+func getDingtalkCreatePayload(p *api.CreatePayload) (*DingtalkPayload, error) {
+ refName := git.RefEndName(p.Ref)
+ refType := strings.Title(p.RefType)
+
+ actionCard := NewDingtalkActionCard("View "+refType, p.Repo.HTMLURL+"/src/"+refName)
+
+ actionCard.Text += "# New " + refType + " Create Event"
+ actionCard.Text += "\n- Repo: **" + MarkdownLinkFormatter(p.Repo.HTMLURL, p.Repo.Name) + "**"
+ actionCard.Text += "\n- New " + refType + ": **" + MarkdownLinkFormatter(p.Repo.HTMLURL+"/src/"+refName, refName) + "**"
+
+ return &DingtalkPayload{MsgType: "actionCard", ActionCard: actionCard}, nil
+}
+
+func getDingtalkDeletePayload(p *api.DeletePayload) (*DingtalkPayload, error) {
+ refName := git.RefEndName(p.Ref)
+ refType := strings.Title(p.RefType)
+
+ actionCard := NewDingtalkActionCard("View Repo", p.Repo.HTMLURL)
+
+ actionCard.Text += "# " + refType + " Delete Event"
+ actionCard.Text += "\n- Repo: **" + MarkdownLinkFormatter(p.Repo.HTMLURL, p.Repo.Name) + "**"
+ actionCard.Text += "\n- " + refType + ": **" + refName + "**"
+
+ return &DingtalkPayload{MsgType: "actionCard", ActionCard: actionCard}, nil
+}
+
+func getDingtalkForkPayload(p *api.ForkPayload) (*DingtalkPayload, error) {
+ actionCard := NewDingtalkActionCard("View Forkee", p.Forkee.HTMLURL)
+
+ actionCard.Text += "# Repo Fork Event"
+ actionCard.Text += "\n- From Repo: **" + MarkdownLinkFormatter(p.Repo.HTMLURL, p.Repo.Name) + "**"
+ actionCard.Text += "\n- To Repo: **" + MarkdownLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName) + "**"
+
+ return &DingtalkPayload{MsgType: "actionCard", ActionCard: actionCard}, nil
+}
+
+func getDingtalkPushPayload(p *api.PushPayload) (*DingtalkPayload, error) {
+ refName := git.RefEndName(p.Ref)
+
+ pusher := p.Pusher.FullName
+ if pusher == "" {
+ pusher = p.Pusher.UserName
+ }
+
+ var detail string
+ for i, commit := range p.Commits {
+ msg := strings.Split(commit.Message, "\n")[0]
+ commitLink := MarkdownLinkFormatter(commit.URL, commit.ID[:7])
+ detail += fmt.Sprintf("> %d. %s %s - %s\n", i, commitLink, commit.Author.Name, msg)
+ }
+
+ actionCard := NewDingtalkActionCard("View Changes", p.CompareURL)
+
+ actionCard.Text += "# Repo Push Event"
+ actionCard.Text += "\n- Repo: **" + MarkdownLinkFormatter(p.Repo.HTMLURL, p.Repo.Name) + "**"
+ actionCard.Text += "\n- Ref: **" + MarkdownLinkFormatter(p.Repo.HTMLURL+"/src/"+refName, refName) + "**"
+ actionCard.Text += "\n- Pusher: **" + pusher + "**"
+ actionCard.Text += "\n## " + fmt.Sprintf("Total %d commits(s)", len(p.Commits))
+ actionCard.Text += "\n" + detail
+
+ return &DingtalkPayload{MsgType: "actionCard", ActionCard: actionCard}, nil
+}
+
+func getDingtalkIssuesPayload(p *api.IssuesPayload) (*DingtalkPayload, error) {
+ issueName := fmt.Sprintf("#%d %s", p.Index, p.Issue.Title)
+ issueURL := fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Index)
+
+ actionCard := NewDingtalkActionCard("View Issue", issueURL)
+
+ actionCard.Text += "# Issue Event " + strings.Title(string(p.Action))
+ actionCard.Text += "\n- Issue: **" + MarkdownLinkFormatter(issueURL, issueName) + "**"
+
+ if p.Action == api.HOOK_ISSUE_ASSIGNED {
+ actionCard.Text += "\n- New Assignee: **" + p.Issue.Assignee.UserName + "**"
+ } else if p.Action == api.HOOK_ISSUE_MILESTONED {
+ actionCard.Text += "\n- New Milestone: **" + p.Issue.Milestone.Title + "**"
+ } else if p.Action == api.HOOK_ISSUE_LABEL_UPDATED {
+ if len(p.Issue.Labels) > 0 {
+ labels := make([]string, len(p.Issue.Labels))
+ for i, label := range p.Issue.Labels {
+ labels[i] = "**" + label.Name + "**"
+ }
+ actionCard.Text += "\n- Labels: " + strings.Join(labels, ",")
+ } else {
+ actionCard.Text += "\n- Labels: **empty**"
+ }
+ }
+
+ if p.Issue.Body != "" {
+ actionCard.Text += "\n> " + p.Issue.Body
+ }
+
+ return &DingtalkPayload{MsgType: "actionCard", ActionCard: actionCard}, nil
+}
+
+func getDingtalkIssueCommentPayload(p *api.IssueCommentPayload) (*DingtalkPayload, error) {
+ issueName := fmt.Sprintf("#%d %s", p.Issue.Index, p.Issue.Title)
+ commentURL := fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index)
+ if p.Action != api.HOOK_ISSUE_COMMENT_DELETED {
+ commentURL += "#" + CommentHashTag(p.Comment.ID)
+ }
+
+ issueURL := fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index)
+
+ actionCard := NewDingtalkActionCard("View Issue Comment", commentURL)
+
+ actionCard.Text += "# Issue Comment " + strings.Title(string(p.Action))
+ actionCard.Text += "\n- Issue: " + MarkdownLinkFormatter(issueURL, issueName)
+ actionCard.Text += "\n- Comment content: "
+ actionCard.Text += "\n> " + p.Comment.Body
+
+ return &DingtalkPayload{MsgType: "actionCard", ActionCard: actionCard}, nil
+}
+
+func getDingtalkPullRequestPayload(p *api.PullRequestPayload) (*DingtalkPayload, error) {
+ title := "# Pull Request " + strings.Title(string(p.Action))
+ if p.Action == api.HOOK_ISSUE_CLOSED && p.PullRequest.HasMerged {
+ title = "# Pull Request Merged"
+ }
+
+ pullRequestURL := fmt.Sprintf("%s/pulls/%d", p.Repository.HTMLURL, p.Index)
+
+ content := "- PR: " + MarkdownLinkFormatter(pullRequestURL, fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title))
+ if p.Action == api.HOOK_ISSUE_ASSIGNED {
+ content += "\n- New Assignee: **" + p.PullRequest.Assignee.UserName + "**"
+ } else if p.Action == api.HOOK_ISSUE_MILESTONED {
+ content += "\n- New Milestone: *" + p.PullRequest.Milestone.Title + "*"
+ } else if p.Action == api.HOOK_ISSUE_LABEL_UPDATED {
+ labels := make([]string, len(p.PullRequest.Labels))
+ for i, label := range p.PullRequest.Labels {
+ labels[i] = "**" + label.Name + "**"
+ }
+ content += "\n- New Labels: " + strings.Join(labels, ",")
+ }
+
+ actionCard := NewDingtalkActionCard("View Pull Request", pullRequestURL)
+ actionCard.Text += title + "\n" + content
+
+ if p.Action == api.HOOK_ISSUE_OPENED || p.Action == api.HOOK_ISSUE_EDITED {
+ actionCard.Text += "\n> " + p.PullRequest.Body
+ }
+
+ return &DingtalkPayload{MsgType: "actionCard", ActionCard: actionCard}, nil
+}
+
+func getDingtalkReleasePayload(p *api.ReleasePayload) (*DingtalkPayload, error) {
+ releaseURL := p.Repository.HTMLURL + "/src/" + p.Release.TagName
+
+ author := p.Release.Author.FullName
+ if author == "" {
+ author = p.Release.Author.UserName
+ }
+
+ actionCard := NewDingtalkActionCard("View Release", releaseURL)
+
+ actionCard.Text += "# New Release Published"
+ actionCard.Text += "\n- Repo: " + MarkdownLinkFormatter(p.Repository.HTMLURL, p.Repository.Name)
+ actionCard.Text += "\n- Tag: " + MarkdownLinkFormatter(releaseURL, p.Release.TagName)
+ actionCard.Text += "\n- Author: " + author
+ actionCard.Text += fmt.Sprintf("\n- Draft?: %t", p.Release.Draft)
+ actionCard.Text += fmt.Sprintf("\n- Pre Release?: %t", p.Release.Prerelease)
+ actionCard.Text += "\n- Title: " + p.Release.Name
+
+ if p.Release.Body != "" {
+ actionCard.Text += "\n- Note: " + p.Release.Body
+ }
+
+ return &DingtalkPayload{MsgType: "actionCard", ActionCard: actionCard}, nil
+}
+
+//Format link addr and title into markdown style
+func MarkdownLinkFormatter(link, text string) string {
+ return "[" + text + "](" + link + ")"
+}
diff --git a/internal/db/webhook_discord.go b/internal/db/webhook_discord.go
new file mode 100644
index 00000000..35b7d9b1
--- /dev/null
+++ b/internal/db/webhook_discord.go
@@ -0,0 +1,409 @@
+// Copyright 2017 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 db
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+
+ "github.com/json-iterator/go"
+
+ "github.com/gogs/git-module"
+ api "github.com/gogs/go-gogs-client"
+
+ "gogs.io/gogs/internal/setting"
+)
+
+type DiscordEmbedFooterObject struct {
+ Text string `json:"text"`
+}
+
+type DiscordEmbedAuthorObject struct {
+ Name string `json:"name"`
+ URL string `json:"url"`
+ IconURL string `json:"icon_url"`
+}
+
+type DiscordEmbedFieldObject struct {
+ Name string `json:"name"`
+ Value string `json:"value"`
+}
+
+type DiscordEmbedObject struct {
+ Title string `json:"title"`
+ Description string `json:"description"`
+ URL string `json:"url"`
+ Color int `json:"color"`
+ Footer *DiscordEmbedFooterObject `json:"footer"`
+ Author *DiscordEmbedAuthorObject `json:"author"`
+ Fields []*DiscordEmbedFieldObject `json:"fields"`
+}
+
+type DiscordPayload struct {
+ Content string `json:"content"`
+ Username string `json:"username"`
+ AvatarURL string `json:"avatar_url"`
+ Embeds []*DiscordEmbedObject `json:"embeds"`
+}
+
+func (p *DiscordPayload) JSONPayload() ([]byte, error) {
+ data, err := jsoniter.MarshalIndent(p, "", " ")
+ if err != nil {
+ return []byte{}, err
+ }
+ return data, nil
+}
+
+func DiscordTextFormatter(s string) string {
+ return strings.Split(s, "\n")[0]
+}
+
+func DiscordLinkFormatter(url string, text string) string {
+ return fmt.Sprintf("[%s](%s)", text, url)
+}
+
+func DiscordSHALinkFormatter(url string, text string) string {
+ return fmt.Sprintf("[`%s`](%s)", text, url)
+}
+
+// getDiscordCreatePayload composes Discord payload for create new branch or tag.
+func getDiscordCreatePayload(p *api.CreatePayload) (*DiscordPayload, error) {
+ refName := git.RefEndName(p.Ref)
+ repoLink := DiscordLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
+ refLink := DiscordLinkFormatter(p.Repo.HTMLURL+"/src/"+refName, refName)
+ content := fmt.Sprintf("Created new %s: %s/%s", p.RefType, repoLink, refLink)
+ return &DiscordPayload{
+ Embeds: []*DiscordEmbedObject{{
+ Description: content,
+ URL: setting.AppURL + p.Sender.UserName,
+ Author: &DiscordEmbedAuthorObject{
+ Name: p.Sender.UserName,
+ IconURL: p.Sender.AvatarUrl,
+ },
+ }},
+ }, nil
+}
+
+// getDiscordDeletePayload composes Discord payload for delete a branch or tag.
+func getDiscordDeletePayload(p *api.DeletePayload) (*DiscordPayload, error) {
+ refName := git.RefEndName(p.Ref)
+ repoLink := DiscordLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
+ content := fmt.Sprintf("Deleted %s: %s/%s", p.RefType, repoLink, refName)
+ return &DiscordPayload{
+ Embeds: []*DiscordEmbedObject{{
+ Description: content,
+ URL: setting.AppURL + p.Sender.UserName,
+ Author: &DiscordEmbedAuthorObject{
+ Name: p.Sender.UserName,
+ IconURL: p.Sender.AvatarUrl,
+ },
+ }},
+ }, nil
+}
+
+// getDiscordForkPayload composes Discord payload for forked by a repository.
+func getDiscordForkPayload(p *api.ForkPayload) (*DiscordPayload, error) {
+ baseLink := DiscordLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
+ forkLink := DiscordLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName)
+ content := fmt.Sprintf("%s is forked to %s", baseLink, forkLink)
+ return &DiscordPayload{
+ Embeds: []*DiscordEmbedObject{{
+ Description: content,
+ URL: setting.AppURL + p.Sender.UserName,
+ Author: &DiscordEmbedAuthorObject{
+ Name: p.Sender.UserName,
+ IconURL: p.Sender.AvatarUrl,
+ },
+ }},
+ }, nil
+}
+
+func getDiscordPushPayload(p *api.PushPayload, slack *SlackMeta) (*DiscordPayload, error) {
+ // n new commits
+ var (
+ branchName = git.RefEndName(p.Ref)
+ commitDesc string
+ commitString string
+ )
+
+ if len(p.Commits) == 1 {
+ commitDesc = "1 new commit"
+ } else {
+ commitDesc = fmt.Sprintf("%d new commits", len(p.Commits))
+ }
+
+ if len(p.CompareURL) > 0 {
+ commitString = DiscordLinkFormatter(p.CompareURL, commitDesc)
+ } else {
+ commitString = commitDesc
+ }
+
+ repoLink := DiscordLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
+ branchLink := DiscordLinkFormatter(p.Repo.HTMLURL+"/src/"+branchName, branchName)
+ content := fmt.Sprintf("Pushed %s to %s/%s\n", commitString, repoLink, branchLink)
+
+ // for each commit, generate attachment text
+ for i, commit := range p.Commits {
+ content += fmt.Sprintf("%s %s - %s", DiscordSHALinkFormatter(commit.URL, commit.ID[:7]), DiscordTextFormatter(commit.Message), commit.Author.Name)
+ // add linebreak to each commit but the last
+ if i < len(p.Commits)-1 {
+ content += "\n"
+ }
+ }
+
+ color, _ := strconv.ParseInt(strings.TrimLeft(slack.Color, "#"), 16, 32)
+ return &DiscordPayload{
+ Username: slack.Username,
+ AvatarURL: slack.IconURL,
+ Embeds: []*DiscordEmbedObject{{
+ Description: content,
+ URL: setting.AppURL + p.Sender.UserName,
+ Color: int(color),
+ Author: &DiscordEmbedAuthorObject{
+ Name: p.Sender.UserName,
+ IconURL: p.Sender.AvatarUrl,
+ },
+ }},
+ }, nil
+}
+
+func getDiscordIssuesPayload(p *api.IssuesPayload, slack *SlackMeta) (*DiscordPayload, error) {
+ title := fmt.Sprintf("#%d %s", p.Index, p.Issue.Title)
+ url := fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Index)
+ content := ""
+ fields := make([]*DiscordEmbedFieldObject, 0, 1)
+ switch p.Action {
+ case api.HOOK_ISSUE_OPENED:
+ title = "New issue: " + title
+ content = p.Issue.Body
+ case api.HOOK_ISSUE_CLOSED:
+ title = "Issue closed: " + title
+ case api.HOOK_ISSUE_REOPENED:
+ title = "Issue re-opened: " + title
+ case api.HOOK_ISSUE_EDITED:
+ title = "Issue edited: " + title
+ content = p.Issue.Body
+ case api.HOOK_ISSUE_ASSIGNED:
+ title = "Issue assigned: " + title
+ fields = []*DiscordEmbedFieldObject{{
+ Name: "New Assignee",
+ Value: p.Issue.Assignee.UserName,
+ }}
+ case api.HOOK_ISSUE_UNASSIGNED:
+ title = "Issue unassigned: " + title
+ case api.HOOK_ISSUE_LABEL_UPDATED:
+ title = "Issue labels updated: " + title
+ labels := make([]string, len(p.Issue.Labels))
+ for i := range p.Issue.Labels {
+ labels[i] = p.Issue.Labels[i].Name
+ }
+ if len(labels) == 0 {
+ labels = []string{"<empty>"}
+ }
+ fields = []*DiscordEmbedFieldObject{{
+ Name: "Labels",
+ Value: strings.Join(labels, ", "),
+ }}
+ case api.HOOK_ISSUE_LABEL_CLEARED:
+ title = "Issue labels cleared: " + title
+ case api.HOOK_ISSUE_SYNCHRONIZED:
+ title = "Issue synchronized: " + title
+ case api.HOOK_ISSUE_MILESTONED:
+ title = "Issue milestoned: " + title
+ fields = []*DiscordEmbedFieldObject{{
+ Name: "New Milestone",
+ Value: p.Issue.Milestone.Title,
+ }}
+ case api.HOOK_ISSUE_DEMILESTONED:
+ title = "Issue demilestoned: " + title
+ }
+
+ color, _ := strconv.ParseInt(strings.TrimLeft(slack.Color, "#"), 16, 32)
+ return &DiscordPayload{
+ Username: slack.Username,
+ AvatarURL: slack.IconURL,
+ Embeds: []*DiscordEmbedObject{{
+ Title: title,
+ Description: content,
+ URL: url,
+ Color: int(color),
+ Footer: &DiscordEmbedFooterObject{
+ Text: p.Repository.FullName,
+ },
+ Author: &DiscordEmbedAuthorObject{
+ Name: p.Sender.UserName,
+ IconURL: p.Sender.AvatarUrl,
+ },
+ Fields: fields,
+ }},
+ }, nil
+}
+
+func getDiscordIssueCommentPayload(p *api.IssueCommentPayload, slack *SlackMeta) (*DiscordPayload, error) {
+ title := fmt.Sprintf("#%d %s", p.Issue.Index, p.Issue.Title)
+ url := fmt.Sprintf("%s/issues/%d#%s", p.Repository.HTMLURL, p.Issue.Index, CommentHashTag(p.Comment.ID))
+ content := ""
+ fields := make([]*DiscordEmbedFieldObject, 0, 1)
+ switch p.Action {
+ case api.HOOK_ISSUE_COMMENT_CREATED:
+ title = "New comment: " + title
+ content = p.Comment.Body
+ case api.HOOK_ISSUE_COMMENT_EDITED:
+ title = "Comment edited: " + title
+ content = p.Comment.Body
+ case api.HOOK_ISSUE_COMMENT_DELETED:
+ title = "Comment deleted: " + title
+ url = fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index)
+ content = p.Comment.Body
+ }
+
+ color, _ := strconv.ParseInt(strings.TrimLeft(slack.Color, "#"), 16, 32)
+ return &DiscordPayload{
+ Username: slack.Username,
+ AvatarURL: slack.IconURL,
+ Embeds: []*DiscordEmbedObject{{
+ Title: title,
+ Description: content,
+ URL: url,
+ Color: int(color),
+ Footer: &DiscordEmbedFooterObject{
+ Text: p.Repository.FullName,
+ },
+ Author: &DiscordEmbedAuthorObject{
+ Name: p.Sender.UserName,
+ IconURL: p.Sender.AvatarUrl,
+ },
+ Fields: fields,
+ }},
+ }, nil
+}
+
+func getDiscordPullRequestPayload(p *api.PullRequestPayload, slack *SlackMeta) (*DiscordPayload, error) {
+ title := fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title)
+ url := fmt.Sprintf("%s/pulls/%d", p.Repository.HTMLURL, p.Index)
+ content := ""
+ fields := make([]*DiscordEmbedFieldObject, 0, 1)
+ switch p.Action {
+ case api.HOOK_ISSUE_OPENED:
+ title = "New pull request: " + title
+ content = p.PullRequest.Body
+ case api.HOOK_ISSUE_CLOSED:
+ if p.PullRequest.HasMerged {
+ title = "Pull request merged: " + title
+ } else {
+ title = "Pull request closed: " + title
+ }
+ case api.HOOK_ISSUE_REOPENED:
+ title = "Pull request re-opened: " + title
+ case api.HOOK_ISSUE_EDITED:
+ title = "Pull request edited: " + title
+ content = p.PullRequest.Body
+ case api.HOOK_ISSUE_ASSIGNED:
+ title = "Pull request assigned: " + title
+ fields = []*DiscordEmbedFieldObject{{
+ Name: "New Assignee",
+ Value: p.PullRequest.Assignee.UserName,
+ }}
+ case api.HOOK_ISSUE_UNASSIGNED:
+ title = "Pull request unassigned: " + title
+ case api.HOOK_ISSUE_LABEL_UPDATED:
+ title = "Pull request labels updated: " + title
+ labels := make([]string, len(p.PullRequest.Labels))
+ for i := range p.PullRequest.Labels {
+ labels[i] = p.PullRequest.Labels[i].Name
+ }
+ fields = []*DiscordEmbedFieldObject{{
+ Name: "Labels",
+ Value: strings.Join(labels, ", "),
+ }}
+ case api.HOOK_ISSUE_LABEL_CLEARED:
+ title = "Pull request labels cleared: " + title
+ case api.HOOK_ISSUE_SYNCHRONIZED:
+ title = "Pull request synchronized: " + title
+ case api.HOOK_ISSUE_MILESTONED:
+ title = "Pull request milestoned: " + title
+ fields = []*DiscordEmbedFieldObject{{
+ Name: "New Milestone",
+ Value: p.PullRequest.Milestone.Title,
+ }}
+ case api.HOOK_ISSUE_DEMILESTONED:
+ title = "Pull request demilestoned: " + title
+ }
+
+ color, _ := strconv.ParseInt(strings.TrimLeft(slack.Color, "#"), 16, 32)
+ return &DiscordPayload{
+ Username: slack.Username,
+ AvatarURL: slack.IconURL,
+ Embeds: []*DiscordEmbedObject{{
+ Title: title,
+ Description: content,
+ URL: url,
+ Color: int(color),
+ Footer: &DiscordEmbedFooterObject{
+ Text: p.Repository.FullName,
+ },
+ Author: &DiscordEmbedAuthorObject{
+ Name: p.Sender.UserName,
+ IconURL: p.Sender.AvatarUrl,
+ },
+ Fields: fields,
+ }},
+ }, nil
+}
+
+func getDiscordReleasePayload(p *api.ReleasePayload) (*DiscordPayload, error) {
+ repoLink := DiscordLinkFormatter(p.Repository.HTMLURL, p.Repository.Name)
+ refLink := DiscordLinkFormatter(p.Repository.HTMLURL+"/src/"+p.Release.TagName, p.Release.TagName)
+ content := fmt.Sprintf("Published new release %s of %s", refLink, repoLink)
+ return &DiscordPayload{
+ Embeds: []*DiscordEmbedObject{{
+ Description: content,
+ URL: setting.AppURL + p.Sender.UserName,
+ Author: &DiscordEmbedAuthorObject{
+ Name: p.Sender.UserName,
+ IconURL: p.Sender.AvatarUrl,
+ },
+ }},
+ }, nil
+}
+
+func GetDiscordPayload(p api.Payloader, event HookEventType, meta string) (payload *DiscordPayload, err error) {
+ slack := &SlackMeta{}
+ if err := jsoniter.Unmarshal([]byte(meta), &slack); err != nil {
+ return nil, fmt.Errorf("jsoniter.Unmarshal: %v", err)
+ }
+
+ switch event {
+ case HOOK_EVENT_CREATE:
+ payload, err = getDiscordCreatePayload(p.(*api.CreatePayload))
+ case HOOK_EVENT_DELETE:
+ payload, err = getDiscordDeletePayload(p.(*api.DeletePayload))
+ case HOOK_EVENT_FORK:
+ payload, err = getDiscordForkPayload(p.(*api.ForkPayload))
+ case HOOK_EVENT_PUSH:
+ payload, err = getDiscordPushPayload(p.(*api.PushPayload), slack)
+ case HOOK_EVENT_ISSUES:
+ payload, err = getDiscordIssuesPayload(p.(*api.IssuesPayload), slack)
+ case HOOK_EVENT_ISSUE_COMMENT:
+ payload, err = getDiscordIssueCommentPayload(p.(*api.IssueCommentPayload), slack)
+ case HOOK_EVENT_PULL_REQUEST:
+ payload, err = getDiscordPullRequestPayload(p.(*api.PullRequestPayload), slack)
+ case HOOK_EVENT_RELEASE:
+ payload, err = getDiscordReleasePayload(p.(*api.ReleasePayload))
+ }
+ if err != nil {
+ return nil, fmt.Errorf("event '%s': %v", event, err)
+ }
+
+ payload.Username = slack.Username
+ payload.AvatarURL = slack.IconURL
+ if len(payload.Embeds) > 0 {
+ color, _ := strconv.ParseInt(strings.TrimLeft(slack.Color, "#"), 16, 32)
+ payload.Embeds[0].Color = int(color)
+ }
+
+ return payload, nil
+}
diff --git a/internal/db/webhook_slack.go b/internal/db/webhook_slack.go
new file mode 100644
index 00000000..ae547dd7
--- /dev/null
+++ b/internal/db/webhook_slack.go
@@ -0,0 +1,326 @@
+// Copyright 2014 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 db
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/json-iterator/go"
+
+ "github.com/gogs/git-module"
+ api "github.com/gogs/go-gogs-client"
+
+ "gogs.io/gogs/internal/setting"
+)
+
+type SlackMeta struct {
+ Channel string `json:"channel"`
+ Username string `json:"username"`
+ IconURL string `json:"icon_url"`
+ Color string `json:"color"`
+}
+
+type SlackAttachment struct {
+ Fallback string `json:"fallback"`
+ Color string `json:"color"`
+ Title string `json:"title"`
+ Text string `json:"text"`
+}
+
+type SlackPayload struct {
+ Channel string `json:"channel"`
+ Text string `json:"text"`
+ Username string `json:"username"`
+ IconURL string `json:"icon_url"`
+ UnfurlLinks int `json:"unfurl_links"`
+ LinkNames int `json:"link_names"`
+ Attachments []*SlackAttachment `json:"attachments"`
+}
+
+func (p *SlackPayload) JSONPayload() ([]byte, error) {
+ data, err := jsoniter.MarshalIndent(p, "", " ")
+ if err != nil {
+ return []byte{}, err
+ }
+ return data, nil
+}
+
+// see: https://api.slack.com/docs/formatting
+func SlackTextFormatter(s string) string {
+ // replace & < >
+ s = strings.Replace(s, "&", "&amp;", -1)
+ s = strings.Replace(s, "<", "&lt;", -1)
+ s = strings.Replace(s, ">", "&gt;", -1)
+ return s
+}
+
+func SlackShortTextFormatter(s string) string {
+ s = strings.Split(s, "\n")[0]
+ // replace & < >
+ s = strings.Replace(s, "&", "&amp;", -1)
+ s = strings.Replace(s, "<", "&lt;", -1)
+ s = strings.Replace(s, ">", "&gt;", -1)
+ return s
+}
+
+func SlackLinkFormatter(url string, text string) string {
+ return fmt.Sprintf("<%s|%s>", url, SlackTextFormatter(text))
+}
+
+// getSlackCreatePayload composes Slack payload for create new branch or tag.
+func getSlackCreatePayload(p *api.CreatePayload) (*SlackPayload, error) {
+ refName := git.RefEndName(p.Ref)
+ repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
+ refLink := SlackLinkFormatter(p.Repo.HTMLURL+"/src/"+refName, refName)
+ text := fmt.Sprintf("[%s:%s] %s created by %s", repoLink, refLink, p.RefType, p.Sender.UserName)
+ return &SlackPayload{
+ Text: text,
+ }, nil
+}
+
+// getSlackDeletePayload composes Slack payload for delete a branch or tag.
+func getSlackDeletePayload(p *api.DeletePayload) (*SlackPayload, error) {
+ refName := git.RefEndName(p.Ref)
+ repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
+ text := fmt.Sprintf("[%s:%s] %s deleted by %s", repoLink, refName, p.RefType, p.Sender.UserName)
+ return &SlackPayload{
+ Text: text,
+ }, nil
+}
+
+// getSlackForkPayload composes Slack payload for forked by a repository.
+func getSlackForkPayload(p *api.ForkPayload) (*SlackPayload, error) {
+ baseLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
+ forkLink := SlackLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName)
+ text := fmt.Sprintf("%s is forked to %s", baseLink, forkLink)
+ return &SlackPayload{
+ Text: text,
+ }, nil
+}
+
+func getSlackPushPayload(p *api.PushPayload, slack *SlackMeta) (*SlackPayload, error) {
+ // n new commits
+ var (
+ branchName = git.RefEndName(p.Ref)
+ commitDesc string
+ commitString string
+ )
+
+ if len(p.Commits) == 1 {
+ commitDesc = "1 new commit"
+ } else {
+ commitDesc = fmt.Sprintf("%d new commits", len(p.Commits))
+ }
+ if len(p.CompareURL) > 0 {
+ commitString = SlackLinkFormatter(p.CompareURL, commitDesc)
+ } else {
+ commitString = commitDesc
+ }
+
+ repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
+ branchLink := SlackLinkFormatter(p.Repo.HTMLURL+"/src/"+branchName, branchName)
+ text := fmt.Sprintf("[%s:%s] %s pushed by %s", repoLink, branchLink, commitString, p.Pusher.UserName)
+
+ var attachmentText string
+ // for each commit, generate attachment text
+ for i, commit := range p.Commits {
+ attachmentText += fmt.Sprintf("%s: %s - %s", SlackLinkFormatter(commit.URL, commit.ID[:7]), SlackShortTextFormatter(commit.Message), SlackTextFormatter(commit.Author.Name))
+ // add linebreak to each commit but the last
+ if i < len(p.Commits)-1 {
+ attachmentText += "\n"
+ }
+ }
+
+ return &SlackPayload{
+ Channel: slack.Channel,
+ Text: text,
+ Username: slack.Username,
+ IconURL: slack.IconURL,
+ Attachments: []*SlackAttachment{{
+ Color: slack.Color,
+ Text: attachmentText,
+ }},
+ }, nil
+}
+
+func getSlackIssuesPayload(p *api.IssuesPayload, slack *SlackMeta) (*SlackPayload, error) {
+ senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
+ titleLink := SlackLinkFormatter(fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Index),
+ fmt.Sprintf("#%d %s", p.Index, p.Issue.Title))
+ var text, title, attachmentText string
+ switch p.Action {
+ case api.HOOK_ISSUE_OPENED:
+ text = fmt.Sprintf("[%s] New issue created by %s", p.Repository.FullName, senderLink)
+ title = titleLink
+ attachmentText = SlackTextFormatter(p.Issue.Body)
+ case api.HOOK_ISSUE_CLOSED:
+ text = fmt.Sprintf("[%s] Issue closed: %s by %s", p.Repository.FullName, titleLink, senderLink)
+ case api.HOOK_ISSUE_REOPENED:
+ text = fmt.Sprintf("[%s] Issue re-opened: %s by %s", p.Repository.FullName, titleLink, senderLink)
+ case api.HOOK_ISSUE_EDITED:
+ text = fmt.Sprintf("[%s] Issue edited: %s by %s", p.Repository.FullName, titleLink, senderLink)
+ attachmentText = SlackTextFormatter(p.Issue.Body)
+ case api.HOOK_ISSUE_ASSIGNED:
+ text = fmt.Sprintf("[%s] Issue assigned to %s: %s by %s", p.Repository.FullName,
+ SlackLinkFormatter(setting.AppURL+p.Issue.Assignee.UserName, p.Issue.Assignee.UserName),
+ titleLink, senderLink)
+ case api.HOOK_ISSUE_UNASSIGNED:
+ text = fmt.Sprintf("[%s] Issue unassigned: %s by %s", p.Repository.FullName, titleLink, senderLink)
+ case api.HOOK_ISSUE_LABEL_UPDATED:
+ text = fmt.Sprintf("[%s] Issue labels updated: %s by %s", p.Repository.FullName, titleLink, senderLink)
+ case api.HOOK_ISSUE_LABEL_CLEARED:
+ text = fmt.Sprintf("[%s] Issue labels cleared: %s by %s", p.Repository.FullName, titleLink, senderLink)
+ case api.HOOK_ISSUE_MILESTONED:
+ text = fmt.Sprintf("[%s] Issue milestoned: %s by %s", p.Repository.FullName, titleLink, senderLink)
+ case api.HOOK_ISSUE_DEMILESTONED:
+ text = fmt.Sprintf("[%s] Issue demilestoned: %s by %s", p.Repository.FullName, titleLink, senderLink)
+ }
+
+ return &SlackPayload{
+ Channel: slack.Channel,
+ Text: text,
+ Username: slack.Username,
+ IconURL: slack.IconURL,
+ Attachments: []*SlackAttachment{{
+ Color: slack.Color,
+ Title: title,
+ Text: attachmentText,
+ }},
+ }, nil
+}
+
+func getSlackIssueCommentPayload(p *api.IssueCommentPayload, slack *SlackMeta) (*SlackPayload, error) {
+ senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
+ titleLink := SlackLinkFormatter(fmt.Sprintf("%s/issues/%d#%s", p.Repository.HTMLURL, p.Issue.Index, CommentHashTag(p.Comment.ID)),
+ fmt.Sprintf("#%d %s", p.Issue.Index, p.Issue.Title))
+ var text, title, attachmentText string
+ switch p.Action {
+ case api.HOOK_ISSUE_COMMENT_CREATED:
+ text = fmt.Sprintf("[%s] New comment created by %s", p.Repository.FullName, senderLink)
+ title = titleLink
+ attachmentText = SlackTextFormatter(p.Comment.Body)
+ case api.HOOK_ISSUE_COMMENT_EDITED:
+ text = fmt.Sprintf("[%s] Comment edited by %s", p.Repository.FullName, senderLink)
+ title = titleLink
+ attachmentText = SlackTextFormatter(p.Comment.Body)
+ case api.HOOK_ISSUE_COMMENT_DELETED:
+ text = fmt.Sprintf("[%s] Comment deleted by %s", p.Repository.FullName, senderLink)
+ title = SlackLinkFormatter(fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index),
+ fmt.Sprintf("#%d %s", p.Issue.Index, p.Issue.Title))
+ attachmentText = SlackTextFormatter(p.Comment.Body)
+ }
+
+ return &SlackPayload{
+ Channel: slack.Channel,
+ Text: text,
+ Username: slack.Username,
+ IconURL: slack.IconURL,
+ Attachments: []*SlackAttachment{{
+ Color: slack.Color,
+ Title: title,
+ Text: attachmentText,
+ }},
+ }, nil
+}
+
+func getSlackPullRequestPayload(p *api.PullRequestPayload, slack *SlackMeta) (*SlackPayload, error) {
+ senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
+ titleLink := SlackLinkFormatter(fmt.Sprintf("%s/pulls/%d", p.Repository.HTMLURL, p.Index),
+ fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title))
+ var text, title, attachmentText string
+ switch p.Action {
+ case api.HOOK_ISSUE_OPENED:
+ text = fmt.Sprintf("[%s] Pull request submitted by %s", p.Repository.FullName, senderLink)
+ title = titleLink
+ attachmentText = SlackTextFormatter(p.PullRequest.Body)
+ case api.HOOK_ISSUE_CLOSED:
+ if p.PullRequest.HasMerged {
+ text = fmt.Sprintf("[%s] Pull request merged: %s by %s", p.Repository.FullName, titleLink, senderLink)
+ } else {
+ text = fmt.Sprintf("[%s] Pull request closed: %s by %s", p.Repository.FullName, titleLink, senderLink)
+ }
+ case api.HOOK_ISSUE_REOPENED:
+ text = fmt.Sprintf("[%s] Pull request re-opened: %s by %s", p.Repository.FullName, titleLink, senderLink)
+ case api.HOOK_ISSUE_EDITED:
+ text = fmt.Sprintf("[%s] Pull request edited: %s by %s", p.Repository.FullName, titleLink, senderLink)
+ attachmentText = SlackTextFormatter(p.PullRequest.Body)
+ case api.HOOK_ISSUE_ASSIGNED:
+ text = fmt.Sprintf("[%s] Pull request assigned to %s: %s by %s", p.Repository.FullName,
+ SlackLinkFormatter(setting.AppURL+p.PullRequest.Assignee.UserName, p.PullRequest.Assignee.UserName),
+ titleLink, senderLink)
+ case api.HOOK_ISSUE_UNASSIGNED:
+ text = fmt.Sprintf("[%s] Pull request unassigned: %s by %s", p.Repository.FullName, titleLink, senderLink)
+ case api.HOOK_ISSUE_LABEL_UPDATED:
+ text = fmt.Sprintf("[%s] Pull request labels updated: %s by %s", p.Repository.FullName, titleLink, senderLink)
+ case api.HOOK_ISSUE_LABEL_CLEARED:
+ text = fmt.Sprintf("[%s] Pull request labels cleared: %s by %s", p.Repository.FullName, titleLink, senderLink)
+ case api.HOOK_ISSUE_SYNCHRONIZED:
+ text = fmt.Sprintf("[%s] Pull request synchronized: %s by %s", p.Repository.FullName, titleLink, senderLink)
+ case api.HOOK_ISSUE_MILESTONED:
+ text = fmt.Sprintf("[%s] Pull request milestoned: %s by %s", p.Repository.FullName, titleLink, senderLink)
+ case api.HOOK_ISSUE_DEMILESTONED:
+ text = fmt.Sprintf("[%s] Pull request demilestoned: %s by %s", p.Repository.FullName, titleLink, senderLink)
+ }
+
+ return &SlackPayload{
+ Channel: slack.Channel,
+ Text: text,
+ Username: slack.Username,
+ IconURL: slack.IconURL,
+ Attachments: []*SlackAttachment{{
+ Color: slack.Color,
+ Title: title,
+ Text: attachmentText,
+ }},
+ }, nil
+}
+
+func getSlackReleasePayload(p *api.ReleasePayload) (*SlackPayload, error) {
+ repoLink := SlackLinkFormatter(p.Repository.HTMLURL, p.Repository.Name)
+ refLink := SlackLinkFormatter(p.Repository.HTMLURL+"/src/"+p.Release.TagName, p.Release.TagName)
+ text := fmt.Sprintf("[%s] new release %s published by %s", repoLink, refLink, p.Sender.UserName)
+ return &SlackPayload{
+ Text: text,
+ }, nil
+}
+
+func GetSlackPayload(p api.Payloader, event HookEventType, meta string) (payload *SlackPayload, err error) {
+ slack := &SlackMeta{}
+ if err := jsoniter.Unmarshal([]byte(meta), &slack); err != nil {
+ return nil, fmt.Errorf("Unmarshal: %v", err)
+ }
+
+ switch event {
+ case HOOK_EVENT_CREATE:
+ payload, err = getSlackCreatePayload(p.(*api.CreatePayload))
+ case HOOK_EVENT_DELETE:
+ payload, err = getSlackDeletePayload(p.(*api.DeletePayload))
+ case HOOK_EVENT_FORK:
+ payload, err = getSlackForkPayload(p.(*api.ForkPayload))
+ case HOOK_EVENT_PUSH:
+ payload, err = getSlackPushPayload(p.(*api.PushPayload), slack)
+ case HOOK_EVENT_ISSUES:
+ payload, err = getSlackIssuesPayload(p.(*api.IssuesPayload), slack)
+ case HOOK_EVENT_ISSUE_COMMENT:
+ payload, err = getSlackIssueCommentPayload(p.(*api.IssueCommentPayload), slack)
+ case HOOK_EVENT_PULL_REQUEST:
+ payload, err = getSlackPullRequestPayload(p.(*api.PullRequestPayload), slack)
+ case HOOK_EVENT_RELEASE:
+ payload, err = getSlackReleasePayload(p.(*api.ReleasePayload))
+ }
+ if err != nil {
+ return nil, fmt.Errorf("event '%s': %v", event, err)
+ }
+
+ payload.Channel = slack.Channel
+ payload.Username = slack.Username
+ payload.IconURL = slack.IconURL
+ if len(payload.Attachments) > 0 {
+ payload.Attachments[0].Color = slack.Color
+ }
+
+ return payload, nil
+}
diff --git a/internal/db/wiki.go b/internal/db/wiki.go
new file mode 100644
index 00000000..a7e27418
--- /dev/null
+++ b/internal/db/wiki.go
@@ -0,0 +1,179 @@
+// 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 db
+
+import (
+ "fmt"
+ "io/ioutil"
+ "net/url"
+ "os"
+ "path"
+ "path/filepath"
+ "strings"
+
+ "github.com/unknwon/com"
+
+ "github.com/gogs/git-module"
+
+ "gogs.io/gogs/internal/setting"
+ "gogs.io/gogs/internal/sync"
+)
+
+var wikiWorkingPool = sync.NewExclusivePool()
+
+// ToWikiPageURL formats a string to corresponding wiki URL name.
+func ToWikiPageURL(name string) string {
+ return url.QueryEscape(name)
+}
+
+// ToWikiPageName formats a URL back to corresponding wiki page name,
+// and removes leading characters './' to prevent changing files
+// that are not belong to wiki repository.
+func ToWikiPageName(urlString string) string {
+ name, _ := url.QueryUnescape(urlString)
+ return strings.Replace(strings.TrimLeft(path.Clean("/"+name), "/"), "/", " ", -1)
+}
+
+// WikiCloneLink returns clone URLs of repository wiki.
+func (repo *Repository) WikiCloneLink() (cl *CloneLink) {
+ return repo.cloneLink(true)
+}
+
+// WikiPath returns wiki data path by given user and repository name.
+func WikiPath(userName, repoName string) string {
+ return filepath.Join(UserPath(userName), strings.ToLower(repoName)+".wiki.git")
+}
+
+func (repo *Repository) WikiPath() string {
+ return WikiPath(repo.MustOwner().Name, repo.Name)
+}
+
+// HasWiki returns true if repository has wiki.
+func (repo *Repository) HasWiki() bool {
+ return com.IsDir(repo.WikiPath())
+}
+
+// InitWiki initializes a wiki for repository,
+// it does nothing when repository already has wiki.
+func (repo *Repository) InitWiki() error {
+ if repo.HasWiki() {
+ return nil
+ }
+
+ if err := git.InitRepository(repo.WikiPath(), true); err != nil {
+ return fmt.Errorf("InitRepository: %v", err)
+ } else if err = createDelegateHooks(repo.WikiPath()); err != nil {
+ return fmt.Errorf("createDelegateHooks: %v", err)
+ }
+ return nil
+}
+
+func (repo *Repository) LocalWikiPath() string {
+ return path.Join(setting.AppDataPath, "tmp/local-wiki", com.ToStr(repo.ID))
+}
+
+// UpdateLocalWiki makes sure the local copy of repository wiki is up-to-date.
+func (repo *Repository) UpdateLocalWiki() error {
+ return UpdateLocalCopyBranch(repo.WikiPath(), repo.LocalWikiPath(), "master", true)
+}
+
+func discardLocalWikiChanges(localPath string) error {
+ return discardLocalRepoBranchChanges(localPath, "master")
+}
+
+// updateWikiPage adds new page to repository wiki.
+func (repo *Repository) updateWikiPage(doer *User, oldTitle, title, content, message string, isNew bool) (err error) {
+ wikiWorkingPool.CheckIn(com.ToStr(repo.ID))
+ defer wikiWorkingPool.CheckOut(com.ToStr(repo.ID))
+
+ if err = repo.InitWiki(); err != nil {
+ return fmt.Errorf("InitWiki: %v", err)
+ }
+
+ localPath := repo.LocalWikiPath()
+ if err = discardLocalWikiChanges(localPath); err != nil {
+ return fmt.Errorf("discardLocalWikiChanges: %v", err)
+ } else if err = repo.UpdateLocalWiki(); err != nil {
+ return fmt.Errorf("UpdateLocalWiki: %v", err)
+ }
+
+ title = ToWikiPageName(title)
+ filename := path.Join(localPath, title+".md")
+
+ // If not a new file, show perform update not create.
+ if isNew {
+ if com.IsExist(filename) {
+ return ErrWikiAlreadyExist{filename}
+ }
+ } else {
+ os.Remove(path.Join(localPath, oldTitle+".md"))
+ }
+
+ // SECURITY: if new file is a symlink to non-exist critical file,
+ // attack content can be written to the target file (e.g. authorized_keys2)
+ // as a new page operation.
+ // So we want to make sure the symlink is removed before write anything.
+ // The new file we created will be in normal text format.
+ os.Remove(filename)
+
+ if err = ioutil.WriteFile(filename, []byte(content), 0666); err != nil {
+ return fmt.Errorf("WriteFile: %v", err)
+ }
+
+ if len(message) == 0 {
+ message = "Update page '" + title + "'"
+ }
+ if err = git.AddChanges(localPath, true); err != nil {
+ return fmt.Errorf("AddChanges: %v", err)
+ } else if err = git.CommitChanges(localPath, git.CommitChangesOptions{
+ Committer: doer.NewGitSig(),
+ Message: message,
+ }); err != nil {
+ return fmt.Errorf("CommitChanges: %v", err)
+ } else if err = git.Push(localPath, "origin", "master"); err != nil {
+ return fmt.Errorf("Push: %v", err)
+ }
+
+ return nil
+}
+
+func (repo *Repository) AddWikiPage(doer *User, title, content, message string) error {
+ return repo.updateWikiPage(doer, "", title, content, message, true)
+}
+
+func (repo *Repository) EditWikiPage(doer *User, oldTitle, title, content, message string) error {
+ return repo.updateWikiPage(doer, oldTitle, title, content, message, false)
+}
+
+func (repo *Repository) DeleteWikiPage(doer *User, title string) (err error) {
+ wikiWorkingPool.CheckIn(com.ToStr(repo.ID))
+ defer wikiWorkingPool.CheckOut(com.ToStr(repo.ID))
+
+ localPath := repo.LocalWikiPath()
+ if err = discardLocalWikiChanges(localPath); err != nil {
+ return fmt.Errorf("discardLocalWikiChanges: %v", err)
+ } else if err = repo.UpdateLocalWiki(); err != nil {
+ return fmt.Errorf("UpdateLocalWiki: %v", err)
+ }
+
+ title = ToWikiPageName(title)
+ filename := path.Join(localPath, title+".md")
+ os.Remove(filename)
+
+ message := "Delete page '" + title + "'"
+
+ if err = git.AddChanges(localPath, true); err != nil {
+ return fmt.Errorf("AddChanges: %v", err)
+ } else if err = git.CommitChanges(localPath, git.CommitChangesOptions{
+ Committer: doer.NewGitSig(),
+ Message: message,
+ }); err != nil {
+ return fmt.Errorf("CommitChanges: %v", err)
+ } else if err = git.Push(localPath, "origin", "master"); err != nil {
+ return fmt.Errorf("Push: %v", err)
+ }
+
+ return nil
+}