diff options
Diffstat (limited to 'models')
-rw-r--r-- | models/access.go | 26 | ||||
-rw-r--r-- | models/action.go | 36 | ||||
-rw-r--r-- | models/error.go | 176 | ||||
-rw-r--r-- | models/git_diff.go | 47 | ||||
-rw-r--r-- | models/issue.go | 406 | ||||
-rw-r--r-- | models/login.go | 71 | ||||
-rw-r--r-- | models/migrations/migrations.go | 74 | ||||
-rw-r--r-- | models/models.go | 14 | ||||
-rw-r--r-- | models/org.go | 38 | ||||
-rw-r--r-- | models/publickey.go | 337 | ||||
-rw-r--r-- | models/repo.go | 265 | ||||
-rw-r--r-- | models/user.go | 82 | ||||
-rw-r--r-- | models/webhook.go | 172 |
13 files changed, 1231 insertions, 513 deletions
diff --git a/models/access.go b/models/access.go index dd856afb..54d0f099 100644 --- a/models/access.go +++ b/models/access.go @@ -145,6 +145,25 @@ func (repo *Repository) refreshCollaboratorAccesses(e Engine, accessMap map[int6 for _, c := range collaborators { accessMap[c.Id] = ACCESS_MODE_WRITE } + + // Adds team members access. + if repo.Owner.IsOrganization() { + if err = repo.Owner.GetTeams(); err != nil { + return fmt.Errorf("GetTeams: %v", err) + } + for _, t := range repo.Owner.Teams { + if err = t.GetMembers(); err != nil { + return fmt.Errorf("GetMembers: %v", err) + } + for _, m := range t.Members { + if t.IsOwnerTeam() { + accessMap[m.Id] = ACCESS_MODE_OWNER + } else { + accessMap[m.Id] = maxAccessMode(accessMap[m.Id], t.Authorize) + } + } + } + } return nil } @@ -154,13 +173,12 @@ func (repo *Repository) refreshCollaboratorAccesses(e Engine, accessMap map[int6 func (repo *Repository) recalculateTeamAccesses(e Engine, ignTeamID int64) (err error) { accessMap := make(map[int64]AccessMode, 20) - if err = repo.refreshCollaboratorAccesses(e, accessMap); err != nil { - return fmt.Errorf("refreshCollaboratorAccesses: %v", err) - } - if err = repo.getOwner(e); err != nil { return err } + if err = repo.refreshCollaboratorAccesses(e, accessMap); err != nil { + return fmt.Errorf("refreshCollaboratorAccesses: %v", err) + } if repo.Owner.IsOrganization() { if err = repo.Owner.getTeams(e); err != nil { return err diff --git a/models/action.go b/models/action.go index d3393728..99cd1709 100644 --- a/models/action.go +++ b/models/action.go @@ -153,7 +153,7 @@ func updateIssuesCommit(userId, repoId int64, repoUserName, repoName string, com url := fmt.Sprintf("%s/%s/%s/commit/%s", setting.AppSubUrl, repoUserName, repoName, c.Sha1) message := fmt.Sprintf(`<a href="%s">%s</a>`, url, c.Message) - if _, err = CreateComment(userId, issue.RepoId, issue.Id, 0, 0, COMMENT_TYPE_COMMIT, message, nil); err != nil { + if _, err = CreateComment(userId, issue.RepoID, issue.ID, 0, 0, COMMENT_TYPE_COMMIT, message, nil); err != nil { return err } } @@ -183,7 +183,7 @@ func updateIssuesCommit(userId, repoId int64, repoUserName, repoName string, com return err } - if issue.RepoId == repoId { + if issue.RepoID == repoId { if issue.IsClosed { continue } @@ -202,7 +202,7 @@ func updateIssuesCommit(userId, repoId int64, repoUserName, repoName string, com if err = UpdateIssue(issue); err != nil { return err - } else if err = UpdateIssueUserPairsByStatus(issue.Id, issue.IsClosed); err != nil { + } else if err = UpdateIssueUserPairsByStatus(issue.ID, issue.IsClosed); err != nil { return err } @@ -211,7 +211,7 @@ func updateIssuesCommit(userId, repoId int64, repoUserName, repoName string, com } // If commit happened in the referenced repository, it means the issue can be closed. - if _, err = CreateComment(userId, repoId, issue.Id, 0, 0, COMMENT_TYPE_CLOSE, "", nil); err != nil { + if _, err = CreateComment(userId, repoId, issue.ID, 0, 0, COMMENT_TYPE_CLOSE, "", nil); err != nil { return err } } @@ -242,7 +242,7 @@ func updateIssuesCommit(userId, repoId int64, repoUserName, repoName string, com return err } - if issue.RepoId == repoId { + if issue.RepoID == repoId { if !issue.IsClosed { continue } @@ -261,7 +261,7 @@ func updateIssuesCommit(userId, repoId int64, repoUserName, repoName string, com if err = UpdateIssue(issue); err != nil { return err - } else if err = UpdateIssueUserPairsByStatus(issue.Id, issue.IsClosed); err != nil { + } else if err = UpdateIssueUserPairsByStatus(issue.ID, issue.IsClosed); err != nil { return err } @@ -270,7 +270,7 @@ func updateIssuesCommit(userId, repoId int64, repoUserName, repoName string, com } // If commit happened in the referenced repository, it means the issue can be closed. - if _, err = CreateComment(userId, repoId, issue.Id, 0, 0, COMMENT_TYPE_REOPEN, "", nil); err != nil { + if _, err = CreateComment(userId, repoId, issue.ID, 0, 0, COMMENT_TYPE_REOPEN, "", nil); err != nil { return err } } @@ -293,12 +293,12 @@ func CommitRepoAction(userId, repoUserId int64, userName, actEmail string, repoLink := fmt.Sprintf("%s%s/%s", setting.AppUrl, repoUserName, repoName) // if not the first commit, set the compareUrl if !strings.HasPrefix(oldCommitId, "0000000") { - commit.CompareUrl = fmt.Sprintf("%s/compare/%s...%s", repoLink, oldCommitId, newCommitId) + commit.CompareUrl = fmt.Sprintf("%s/%s/compare/%s...%s", repoUserName, repoName, oldCommitId, newCommitId) } bs, err := json.Marshal(commit) if err != nil { - return errors.New("action.CommitRepoAction(json): " + err.Error()) + return errors.New("json: " + err.Error()) } refName := git.RefEndName(refFullName) @@ -306,17 +306,17 @@ func CommitRepoAction(userId, repoUserId int64, userName, actEmail string, // Change repository bare status and update last updated time. repo, err := GetRepositoryByName(repoUserId, repoName) if err != nil { - return errors.New("action.CommitRepoAction(GetRepositoryByName): " + err.Error()) + return errors.New("GetRepositoryByName: " + err.Error()) } repo.IsBare = false if err = UpdateRepository(repo, false); err != nil { - return errors.New("action.CommitRepoAction(UpdateRepository): " + err.Error()) + return errors.New("UpdateRepository: " + err.Error()) } err = updateIssuesCommit(userId, repoId, repoUserName, repoName, commit.Commits) if err != nil { - log.Debug("action.CommitRepoAction(updateIssuesCommit): ", err) + log.Debug("updateIssuesCommit: ", err) } if err = NotifyWatchers(&Action{ @@ -331,18 +331,18 @@ func CommitRepoAction(userId, repoUserId int64, userName, actEmail string, RefName: refName, IsPrivate: repo.IsPrivate, }); err != nil { - return errors.New("action.CommitRepoAction(NotifyWatchers): " + err.Error()) + return errors.New("NotifyWatchers: " + err.Error()) } // New push event hook. if err := repo.GetOwner(); err != nil { - return errors.New("action.CommitRepoAction(GetOwner): " + err.Error()) + return errors.New("GetOwner: " + err.Error()) } ws, err := GetActiveWebhooksByRepoId(repoId) if err != nil { - return errors.New("action.CommitRepoAction(GetActiveWebhooksByRepoId): " + err.Error()) + return errors.New("GetActiveWebhooksByRepoId: " + err.Error()) } // check if repo belongs to org and append additional webhooks @@ -350,7 +350,7 @@ func CommitRepoAction(userId, repoUserId int64, userName, actEmail string, // get hooks for org orgws, err := GetActiveWebhooksByOrgId(repo.OwnerId) if err != nil { - return errors.New("action.CommitRepoAction(GetActiveWebhooksByOrgId): " + err.Error()) + return errors.New("GetActiveWebhooksByOrgId: " + err.Error()) } ws = append(ws, orgws...) } @@ -408,7 +408,7 @@ func CommitRepoAction(userId, repoUserId int64, userName, actEmail string, }, Before: oldCommitId, After: newCommitId, - CompareUrl: commit.CompareUrl, + CompareUrl: setting.AppUrl + commit.CompareUrl, } for _, w := range ws { @@ -431,6 +431,8 @@ func CommitRepoAction(userId, repoUserId int64, userName, actEmail string, } if err = CreateHookTask(&HookTask{ + RepoID: repo.Id, + HookID: w.Id, Type: w.HookTaskType, Url: w.Url, BasePayload: payload, diff --git a/models/error.go b/models/error.go index a434b8d6..0e554e52 100644 --- a/models/error.go +++ b/models/error.go @@ -8,6 +8,32 @@ 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) +} + // ____ ___ // | | \______ ___________ // | | / ___// __ \_ __ \ @@ -15,6 +41,46 @@ import ( // |______//____ >\___ >__| // \/ \/ +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 ErrUserNotExist struct { + UID int64 + Name string +} + +func IsErrUserNotExist(err error) bool { + _, ok := err.(ErrUserNotExist) + return ok +} + +func (err ErrUserNotExist) Error() string { + return fmt.Sprintf("user does not exist: [uid: %d, name: %s]", err.UID, 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 } @@ -41,6 +107,82 @@ func (err ErrUserHasOrgs) Error() string { return fmt.Sprintf("user still has membership of organizations: [uid: %d]", err.UID) } +// __________ ___. .__ .__ ____ __. +// \______ \__ _\_ |__ | | |__| ____ | |/ _|____ ___.__. +// | ___/ | \ __ \| | | |/ ___\ | <_/ __ < | | +// | | | | / \_\ \ |_| \ \___ | | \ ___/\___ | +// |____| |____/|___ /____/__|\___ > |____|__ \___ > ____| +// \/ \/ \/ \/\/ + +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 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) +} + // ________ .__ __ .__ // \_____ \_______ _________ ____ |__|____________ _/ |_|__| ____ ____ // / | \_ __ \/ ___\__ \ / \| \___ /\__ \\ __\ |/ _ \ / \ @@ -82,3 +224,37 @@ func IsErrRepoNotExist(err error) bool { func (err ErrRepoNotExist) Error() string { return fmt.Sprintf("repository does not exist [id: %d, uid: %d, name: %s]", err.ID, err.UID, err.Name) } + +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: %d, name: %s]", err.Uname, err.Name) +} + +// _____ .__.__ __ +// / \ |__| | ____ _______/ |_ ____ ____ ____ +// / \ / \| | | _/ __ \ / ___/\ __\/ _ \ / \_/ __ \ +// / Y \ | |_\ ___/ \___ \ | | ( <_> ) | \ ___/ +// \____|__ /__|____/\___ >____ > |__| \____/|___| /\___ > +// \/ \/ \/ \/ \/ + +type ErrMilestoneNotExist struct { + ID 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]", err.ID) +} diff --git a/models/git_diff.go b/models/git_diff.go index 9d34ed62..9681b3f4 100644 --- a/models/git_diff.go +++ b/models/git_diff.go @@ -87,7 +87,7 @@ func ParsePatch(pid int64, maxlines int, cmd *exec.Cmd, reader io.Reader) (*Diff leftLine, rightLine int isTooLong bool - // FIXME: use first 30 lines to detect file encoding. Should use cache in the future. + // FIXME: Should use cache in the future. buf bytes.Buffer ) @@ -106,16 +106,10 @@ func ParsePatch(pid int64, maxlines int, cmd *exec.Cmd, reader io.Reader) (*Diff i = i + 1 - // FIXME: use first 30 lines to detect file encoding. - if i <= 30 { - buf.WriteString(line) - } - // Diff data too large, we only show the first about maxlines lines if i == maxlines { isTooLong = true log.Warn("Diff data too large") - //return &Diff{}, nil } switch { @@ -127,7 +121,7 @@ func ParsePatch(pid int64, maxlines int, cmd *exec.Cmd, reader io.Reader) (*Diff continue case line[0] == '@': if isTooLong { - return diff, nil + break } curSection = &DiffSection{} @@ -137,9 +131,14 @@ func ParsePatch(pid int64, maxlines int, cmd *exec.Cmd, reader io.Reader) (*Diff curSection.Lines = append(curSection.Lines, diffLine) // Parse line number. - ranges := strings.Split(ss[len(ss)-2][1:], " ") + ranges := strings.Split(ss[1][1:], " ") leftLine, _ = com.StrTo(strings.Split(ranges[0], ",")[0][1:]).Int() - rightLine, _ = com.StrTo(strings.Split(ranges[1], ",")[0]).Int() + if len(ranges) > 1 { + rightLine, _ = com.StrTo(strings.Split(ranges[1], ",")[0]).Int() + } else { + log.Warn("Parse line number failed: %v", line) + rightLine = leftLine + } continue case line[0] == '+': curFile.Addition++ @@ -164,11 +163,11 @@ func ParsePatch(pid int64, maxlines int, cmd *exec.Cmd, reader io.Reader) (*Diff // Get new file. if strings.HasPrefix(line, DIFF_HEAD) { if isTooLong { - return diff, nil + break } - fs := strings.Split(line[len(DIFF_HEAD):], " ") - a := fs[0] + beg := len(DIFF_HEAD) + a := line[beg : (len(line)-beg)/2+beg] curFile = &DiffFile{ Name: a[strings.Index(a, "/")+1:], @@ -201,14 +200,19 @@ func ParsePatch(pid int64, maxlines int, cmd *exec.Cmd, reader io.Reader) (*Diff } } - // FIXME: use first 30 lines to detect file encoding. - charsetLabel, err := base.DetectEncoding(buf.Bytes()) - if charsetLabel != "utf8" && err == nil { - encoding, _ := charset.Lookup(charsetLabel) - - if encoding != nil { - d := encoding.NewDecoder() - for _, f := range diff.Files { + for _, f := range diff.Files { + buf.Reset() + for _, sec := range f.Sections { + for _, l := range sec.Lines { + buf.WriteString(l.Content) + buf.WriteString("\n") + } + } + charsetLabel, err := base.DetectEncoding(buf.Bytes()) + if charsetLabel != "UTF-8" && err == nil { + encoding, _ := charset.Lookup(charsetLabel) + if encoding != nil { + d := encoding.NewDecoder() for _, sec := range f.Sections { for _, l := range sec.Lines { if c, _, err := transform.String(d, l.Content); err == nil { @@ -219,7 +223,6 @@ func ParsePatch(pid int64, maxlines int, cmd *exec.Cmd, reader io.Reader) (*Diff } } } - return diff, nil } diff --git a/models/issue.go b/models/issue.go index 2b80e576..d423f056 100644 --- a/models/issue.go +++ b/models/issue.go @@ -17,12 +17,12 @@ import ( "github.com/go-xorm/xorm" "github.com/gogits/gogs/modules/log" + "github.com/gogits/gogs/modules/setting" ) var ( ErrIssueNotExist = errors.New("Issue does not exist") ErrLabelNotExist = errors.New("Label does not exist") - ErrMilestoneNotExist = errors.New("Milestone does not exist") ErrWrongIssueCounter = errors.New("Invalid number of issues for this milestone") ErrAttachmentNotExist = errors.New("Attachment does not exist") ErrAttachmentNotLinked = errors.New("Attachment does not belong to this issue") @@ -31,17 +31,18 @@ var ( // Issue represents an issue or pull request of repository. type Issue struct { - Id int64 - RepoId int64 `xorm:"INDEX"` + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"INDEX"` Index int64 // Index in one repository. Name string Repo *Repository `xorm:"-"` - PosterId int64 + PosterID int64 Poster *User `xorm:"-"` LabelIds string `xorm:"TEXT"` Labels []*Label `xorm:"-"` - MilestoneId int64 - AssigneeId int64 + MilestoneID int64 + Milestone *Milestone `xorm:"-"` + AssigneeID int64 Assignee *User `xorm:"-"` IsRead bool `xorm:"-"` IsPull bool // Indicates whether is a pull request or not. @@ -55,9 +56,20 @@ type Issue struct { Updated time.Time `xorm:"UPDATED"` } +func (i *Issue) AfterSet(colName string, _ xorm.Cell) { + var err error + switch colName { + case "milestone_id": + i.Milestone, err = GetMilestoneByID(i.MilestoneID) + if err != nil { + log.Error(3, "GetMilestoneById: %v", err) + } + } +} + func (i *Issue) GetPoster() (err error) { - i.Poster, err = GetUserById(i.PosterId) - if err == ErrUserNotExist { + i.Poster, err = GetUserById(i.PosterID) + if IsErrUserNotExist(err) { i.Poster = &User{Name: "FakeUser"} return nil } @@ -72,7 +84,7 @@ func (i *Issue) GetLabels() error { strIds := strings.Split(strings.TrimSuffix(i.LabelIds[1:], "|"), "|$") i.Labels = make([]*Label, 0, len(strIds)) for _, strId := range strIds { - id, _ := com.StrTo(strId).Int64() + id := com.StrTo(strId).MustInt64() if id > 0 { l, err := GetLabelById(id) if err != nil { @@ -88,45 +100,41 @@ func (i *Issue) GetLabels() error { } func (i *Issue) GetAssignee() (err error) { - if i.AssigneeId == 0 { + if i.AssigneeID == 0 { return nil } - i.Assignee, err = GetUserById(i.AssigneeId) - if err == ErrUserNotExist { + + i.Assignee, err = GetUserById(i.AssigneeID) + if IsErrUserNotExist(err) { return nil } return err } func (i *Issue) Attachments() []*Attachment { - a, _ := GetAttachmentsForIssue(i.Id) + a, _ := GetAttachmentsForIssue(i.ID) return a } func (i *Issue) AfterDelete() { - _, err := DeleteAttachmentsByIssue(i.Id, true) + _, err := DeleteAttachmentsByIssue(i.ID, true) if err != nil { - log.Info("Could not delete files for issue #%d: %s", i.Id, err) + log.Info("Could not delete files for issue #%d: %s", i.ID, err) } } // CreateIssue creates new issue for repository. func NewIssue(issue *Issue) (err error) { sess := x.NewSession() - defer sess.Close() + defer sessionRelease(sess) if err = sess.Begin(); err != nil { return err } if _, err = sess.Insert(issue); err != nil { - sess.Rollback() return err - } - - rawSql := "UPDATE `repository` SET num_issues = num_issues + 1 WHERE id = ?" - if _, err = sess.Exec(rawSql, issue.RepoId); err != nil { - sess.Rollback() + } else if _, err = sess.Exec("UPDATE `repository` SET num_issues = num_issues + 1 WHERE id = ?", issue.RepoID); err != nil { return err } @@ -134,9 +142,9 @@ func NewIssue(issue *Issue) (err error) { return err } - if issue.MilestoneId > 0 { + if issue.MilestoneID > 0 { // FIXES(280): Update milestone counter. - return ChangeMilestoneAssign(0, issue.MilestoneId, issue) + return ChangeMilestoneAssign(0, issue.MilestoneID, issue) } return @@ -167,7 +175,7 @@ func GetIssueByRef(ref string) (issue *Issue, err error) { // GetIssueByIndex returns issue by given index in repository. func GetIssueByIndex(rid, index int64) (*Issue, error) { - issue := &Issue{RepoId: rid, Index: index} + issue := &Issue{RepoID: rid, Index: index} has, err := x.Get(issue) if err != nil { return nil, err @@ -179,7 +187,7 @@ func GetIssueByIndex(rid, index int64) (*Issue, error) { // GetIssueById returns an issue by ID. func GetIssueById(id int64) (*Issue, error) { - issue := &Issue{Id: id} + issue := &Issue{ID: id} has, err := x.Get(issue) if err != nil { return nil, err @@ -189,31 +197,30 @@ func GetIssueById(id int64) (*Issue, error) { return issue, nil } -// GetIssues returns a list of issues by given conditions. -func GetIssues(uid, rid, pid, mid int64, page int, isClosed bool, labelIds, sortType string) ([]Issue, error) { - sess := x.Limit(20, (page-1)*20) +// Issues returns a list of issues by given conditions. +func Issues(uid, assigneeID, repoID, posterID, milestoneID int64, page int, isClosed, isMention bool, labelIds, sortType string) ([]*Issue, error) { + sess := x.Limit(setting.IssuePagingNum, (page-1)*setting.IssuePagingNum) - if rid > 0 { - sess.Where("repo_id=?", rid).And("is_closed=?", isClosed) + if repoID > 0 { + sess.Where("issue.repo_id=?", repoID).And("issue.is_closed=?", isClosed) } else { - sess.Where("is_closed=?", isClosed) + sess.Where("issue.is_closed=?", isClosed) } - if uid > 0 { - sess.And("assignee_id=?", uid) - } else if pid > 0 { - sess.And("poster_id=?", pid) + if assigneeID > 0 { + sess.And("issue.assignee_id=?", assigneeID) + } else if posterID > 0 { + sess.And("issue.poster_id=?", posterID) } - if mid > 0 { - sess.And("milestone_id=?", mid) + if milestoneID > 0 { + sess.And("issue.milestone_id=?", milestoneID) } if len(labelIds) > 0 { for _, label := range strings.Split(labelIds, ",") { - // Prevent SQL inject. if com.StrTo(label).MustInt() > 0 { - sess.And("label_ids like '%$" + label + "|%'") + sess.And("label_ids like ?", "%$"+label+"|%") } } } @@ -235,9 +242,16 @@ func GetIssues(uid, rid, pid, mid int64, page int, isClosed bool, labelIds, sort sess.Desc("created") } - var issues []Issue - err := sess.Find(&issues) - return issues, err + if isMention { + queryStr := "issue.id = issue_user.issue_id AND issue_user.is_mentioned=1" + if uid > 0 { + queryStr += " AND issue_user.uid = " + com.ToStr(uid) + } + sess.Join("INNER", "issue_user", queryStr) + } + + issues := make([]*Issue, 0, setting.IssuePagingNum) + return issues, sess.Find(&issues) } type IssueStatus int @@ -248,10 +262,9 @@ const ( ) // GetIssuesByLabel returns a list of issues by given label and repository. -func GetIssuesByLabel(repoId int64, label string) ([]*Issue, error) { +func GetIssuesByLabel(repoID, labelID int64) ([]*Issue, error) { issues := make([]*Issue, 0, 10) - err := x.Where("repo_id=?", repoId).And("label_ids like '%$" + label + "|%'").Find(&issues) - return issues, err + return issues, x.Where("repo_id=?", repoID).And("label_ids like '%$" + com.ToStr(labelID) + "|%'").Find(&issues) } // GetIssueCountByPoster returns number of issues of repository by poster. @@ -281,8 +294,9 @@ type IssueUser struct { IsClosed bool } +// FIXME: organization // NewIssueUserPairs adds new issue-user pairs for new issue of repository. -func NewIssueUserPairs(repo *Repository, issueID, orgID, posterID, assigneeID int64) (err error) { +func NewIssueUserPairs(repo *Repository, issueID, orgID, posterID, assigneeID int64) error { users, err := repo.GetCollaborators() if err != nil { return err @@ -295,6 +309,7 @@ func NewIssueUserPairs(repo *Repository, issueID, orgID, posterID, assigneeID in isNeedAddPoster := true for _, u := range users { + iu.Id = 0 iu.Uid = u.Id iu.IsPoster = iu.Uid == posterID if isNeedAddPoster && iu.IsPoster { @@ -306,6 +321,7 @@ func NewIssueUserPairs(repo *Repository, issueID, orgID, posterID, assigneeID in } } if isNeedAddPoster { + iu.Id = 0 iu.Uid = posterID iu.IsPoster = true iu.IsAssigned = iu.Uid == assigneeID @@ -314,13 +330,24 @@ func NewIssueUserPairs(repo *Repository, issueID, orgID, posterID, assigneeID in } } + // Add owner's as well. + if repo.OwnerId != posterID { + iu.Id = 0 + iu.Uid = repo.OwnerId + iu.IsAssigned = iu.Uid == assigneeID + if _, err = x.Insert(iu); err != nil { + return err + } + } + return nil } // PairsContains returns true when pairs list contains given issue. -func PairsContains(ius []*IssueUser, issueId int64) int { +func PairsContains(ius []*IssueUser, issueId, uid int64) int { for i := range ius { - if ius[i].IssueId == issueId { + if ius[i].IssueId == issueId && + ius[i].Uid == uid { return i } } @@ -387,53 +414,57 @@ type IssueStats struct { // Filter modes. const ( - FM_ASSIGN = iota + 1 + FM_ALL = iota + FM_ASSIGN FM_CREATE FM_MENTION ) // GetIssueStats returns issue statistic information by given conditions. -func GetIssueStats(rid, uid int64, isShowClosed bool, filterMode int) *IssueStats { +func GetIssueStats(repoID, uid, labelID, milestoneID int64, isShowClosed bool, filterMode int) *IssueStats { stats := &IssueStats{} issue := new(Issue) - tmpSess := &xorm.Session{} - - sess := x.Where("repo_id=?", rid) - *tmpSess = *sess - stats.OpenCount, _ = tmpSess.And("is_closed=?", false).Count(issue) - *tmpSess = *sess - stats.ClosedCount, _ = tmpSess.And("is_closed=?", true).Count(issue) - if isShowClosed { - stats.AllCount = stats.ClosedCount - } else { - stats.AllCount = stats.OpenCount - } - - if filterMode != FM_MENTION { - sess = x.Where("repo_id=?", rid) - switch filterMode { - case FM_ASSIGN: - sess.And("assignee_id=?", uid) - case FM_CREATE: - sess.And("poster_id=?", uid) - default: - goto nofilter + + queryStr := "issue.repo_id=? AND issue.is_closed=?" + if labelID > 0 { + queryStr += " AND issue.label_ids like '%$" + com.ToStr(labelID) + "|%'" + } + if milestoneID > 0 { + queryStr += " AND milestone_id=" + com.ToStr(milestoneID) + } + switch filterMode { + case FM_ALL: + stats.OpenCount, _ = x.Where(queryStr, repoID, false).Count(issue) + stats.ClosedCount, _ = x.Where(queryStr, repoID, true).Count(issue) + return stats + + case FM_ASSIGN: + queryStr += " AND assignee_id=?" + stats.OpenCount, _ = x.Where(queryStr, repoID, false, uid).Count(issue) + stats.ClosedCount, _ = x.Where(queryStr, repoID, true, uid).Count(issue) + return stats + + case FM_CREATE: + queryStr += " AND poster_id=?" + stats.OpenCount, _ = x.Where(queryStr, repoID, false, uid).Count(issue) + stats.ClosedCount, _ = x.Where(queryStr, repoID, true, uid).Count(issue) + return stats + + case FM_MENTION: + queryStr += " AND uid=? AND is_mentioned=?" + if labelID > 0 { + stats.OpenCount, _ = x.Where(queryStr, repoID, false, uid, true). + Join("INNER", "issue", "issue.id = issue_id").Count(new(IssueUser)) + stats.ClosedCount, _ = x.Where(queryStr, repoID, true, uid, true). + Join("INNER", "issue", "issue.id = issue_id").Count(new(IssueUser)) + return stats } - *tmpSess = *sess - stats.OpenCount, _ = tmpSess.And("is_closed=?", false).Count(issue) - *tmpSess = *sess - stats.ClosedCount, _ = tmpSess.And("is_closed=?", true).Count(issue) - } else { - sess := x.Where("repo_id=?", rid).And("uid=?", uid).And("is_mentioned=?", true) - *tmpSess = *sess - stats.OpenCount, _ = tmpSess.And("is_closed=?", false).Count(new(IssueUser)) - *tmpSess = *sess - stats.ClosedCount, _ = tmpSess.And("is_closed=?", true).Count(new(IssueUser)) - } -nofilter: - stats.AssignCount, _ = x.Where("repo_id=?", rid).And("is_closed=?", isShowClosed).And("assignee_id=?", uid).Count(issue) - stats.CreateCount, _ = x.Where("repo_id=?", rid).And("is_closed=?", isShowClosed).And("poster_id=?", uid).Count(issue) - stats.MentionCount, _ = x.Where("repo_id=?", rid).And("uid=?", uid).And("is_closed=?", isShowClosed).And("is_mentioned=?", true).Count(new(IssueUser)) + + queryStr = strings.Replace(queryStr, "issue.", "", 2) + stats.OpenCount, _ = x.Where(queryStr, repoID, false, uid, true).Count(new(IssueUser)) + stats.ClosedCount, _ = x.Where(queryStr, repoID, true, uid, true).Count(new(IssueUser)) + return stats + } return stats } @@ -448,7 +479,7 @@ func GetUserIssueStats(uid int64, filterMode int) *IssueStats { // UpdateIssue updates information of issue. func UpdateIssue(issue *Issue) error { - _, err := x.Id(issue.Id).AllCols().Update(issue) + _, err := x.Id(issue.ID).AllCols().Update(issue) if err != nil { return err @@ -518,7 +549,7 @@ func UpdateIssueUserPairsByMentions(uids []int64, iid int64) error { // Label represents a label of repository for issues. type Label struct { - Id int64 + ID int64 `xorm:"pk autoincr"` RepoId int64 `xorm:"INDEX"` Name string Color string `xorm:"VARCHAR(7)"` @@ -545,7 +576,7 @@ func GetLabelById(id int64) (*Label, error) { return nil, ErrLabelNotExist } - l := &Label{Id: id} + l := &Label{ID: id} has, err := x.Get(l) if err != nil { return nil, err @@ -564,14 +595,13 @@ func GetLabels(repoId int64) ([]*Label, error) { // UpdateLabel updates label information. func UpdateLabel(l *Label) error { - _, err := x.Id(l.Id).AllCols().Update(l) + _, err := x.Id(l.ID).AllCols().Update(l) return err } // DeleteLabel delete a label of given repository. -func DeleteLabel(repoId int64, strId string) error { - id, _ := com.StrTo(strId).Int64() - l, err := GetLabelById(id) +func DeleteLabel(repoID, labelID int64) error { + l, err := GetLabelById(labelID) if err != nil { if err == ErrLabelNotExist { return nil @@ -579,27 +609,25 @@ func DeleteLabel(repoId int64, strId string) error { return err } - issues, err := GetIssuesByLabel(repoId, strId) + issues, err := GetIssuesByLabel(repoID, labelID) if err != nil { return err } sess := x.NewSession() - defer sess.Close() + defer sessionRelease(sess) if err = sess.Begin(); err != nil { return err } for _, issue := range issues { - issue.LabelIds = strings.Replace(issue.LabelIds, "$"+strId+"|", "", -1) - if _, err = sess.Id(issue.Id).AllCols().Update(issue); err != nil { - sess.Rollback() + issue.LabelIds = strings.Replace(issue.LabelIds, "$"+com.ToStr(labelID)+"|", "", -1) + if _, err = sess.Id(issue.ID).AllCols().Update(issue); err != nil { return err } } if _, err = sess.Delete(l); err != nil { - sess.Rollback() return err } return sess.Commit() @@ -614,9 +642,8 @@ func DeleteLabel(repoId int64, strId string) error { // Milestone represents a milestone of repository. type Milestone struct { - Id int64 - RepoId int64 `xorm:"INDEX"` - Index int64 + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"INDEX"` Name string Content string `xorm:"TEXT"` RenderedContent string `xorm:"-"` @@ -627,9 +654,23 @@ type Milestone struct { Completeness int // Percentage(1-100). Deadline time.Time DeadlineString string `xorm:"-"` + IsOverDue bool `xorm:"-"` ClosedDate time.Time } +func (m *Milestone) AfterSet(colName string, _ xorm.Cell) { + if colName == "deadline" { + if m.Deadline.Year() == 9999 { + return + } + + m.DeadlineString = m.Deadline.Format("2006-01-02") + if time.Now().After(m.Deadline) { + m.IsOverDue = true + } + } +} + // CalOpenIssues calculates the open issues of milestone. func (m *Milestone) CalOpenIssues() { m.NumOpenIssues = m.NumIssues - m.NumClosedIssues @@ -638,101 +679,119 @@ func (m *Milestone) CalOpenIssues() { // NewMilestone creates new milestone of repository. func NewMilestone(m *Milestone) (err error) { sess := x.NewSession() - defer sess.Close() + defer sessionRelease(sess) if err = sess.Begin(); err != nil { return err } if _, err = sess.Insert(m); err != nil { - sess.Rollback() return err } - rawSql := "UPDATE `repository` SET num_milestones = num_milestones + 1 WHERE id = ?" - if _, err = sess.Exec(rawSql, m.RepoId); err != nil { - sess.Rollback() + if _, err = sess.Exec("UPDATE `repository` SET num_milestones=num_milestones+1 WHERE id=?", m.RepoID); err != nil { return err } return sess.Commit() } -// GetMilestoneById returns the milestone by given ID. -func GetMilestoneById(id int64) (*Milestone, error) { - m := &Milestone{Id: id} +// GetMilestoneByID returns the milestone of given ID. +func GetMilestoneByID(id int64) (*Milestone, error) { + m := &Milestone{ID: id} has, err := x.Get(m) if err != nil { return nil, err } else if !has { - return nil, ErrMilestoneNotExist + return nil, ErrMilestoneNotExist{id} } return m, nil } -// GetMilestoneByIndex returns the milestone of given repository and index. -func GetMilestoneByIndex(repoId, idx int64) (*Milestone, error) { - m := &Milestone{RepoId: repoId, Index: idx} - has, err := x.Get(m) - if err != nil { - return nil, err - } else if !has { - return nil, ErrMilestoneNotExist - } - return m, nil +// GetAllRepoMilestones returns all milestones of given repository. +func GetAllRepoMilestones(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, isClosed bool) ([]*Milestone, error) { - miles := make([]*Milestone, 0, 10) - err := x.Where("repo_id=?", repoId).And("is_closed=?", isClosed).Find(&miles) - return miles, err +func GetMilestones(repoID int64, page int, isClosed bool) ([]*Milestone, error) { + miles := make([]*Milestone, 0, setting.IssuePagingNum) + sess := x.Where("repo_id=? AND is_closed=?", repoID, isClosed) + if page > 0 { + sess = sess.Limit(setting.IssuePagingNum, (page-1)*setting.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 { - _, err := x.Id(m.Id).Update(m) - return err + 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. func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) { - repo, err := GetRepositoryById(m.RepoId) + repo, err := GetRepositoryById(m.RepoID) if err != nil { return err } sess := x.NewSession() - defer sess.Close() + defer sessionRelease(sess) if err = sess.Begin(); err != nil { return err } m.IsClosed = isClosed - if _, err = sess.Id(m.Id).AllCols().Update(m); err != nil { - sess.Rollback() + if err = updateMilestone(sess, m); err != nil { return err } - if isClosed { - repo.NumClosedMilestones++ - } else { - repo.NumClosedMilestones-- - } - if _, err = sess.Id(repo.Id).Update(repo); err != nil { - sess.Rollback() + 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() } -// ChangeMilestoneIssueStats updates the open/closed issues counter and progress for the -// milestone associated witht the given issue. +// ChangeMilestoneIssueStats updates the open/closed issues counter and progress +// for the milestone associated witht the given issue. func ChangeMilestoneIssueStats(issue *Issue) error { - if issue.MilestoneId == 0 { + if issue.MilestoneID == 0 { return nil } - m, err := GetMilestoneById(issue.MilestoneId) - + m, err := GetMilestoneByID(issue.MilestoneID) if err != nil { return err } @@ -759,7 +818,7 @@ func ChangeMilestoneAssign(oldMid, mid int64, issue *Issue) (err error) { } if oldMid > 0 { - m, err := GetMilestoneById(oldMid) + m, err := GetMilestoneByID(oldMid) if err != nil { return err } @@ -774,20 +833,20 @@ func ChangeMilestoneAssign(oldMid, mid int64, issue *Issue) (err error) { m.Completeness = 0 } - if _, err = sess.Id(m.Id).Cols("num_issues,num_completeness,num_closed_issues").Update(m); err != nil { + if _, err = sess.Id(m.ID).Cols("num_issues,num_completeness,num_closed_issues").Update(m); err != nil { sess.Rollback() return err } rawSql := "UPDATE `issue_user` SET milestone_id = 0 WHERE issue_id = ?" - if _, err = sess.Exec(rawSql, issue.Id); err != nil { + if _, err = sess.Exec(rawSql, issue.ID); err != nil { sess.Rollback() return err } } if mid > 0 { - m, err := GetMilestoneById(mid) + m, err := GetMilestoneByID(mid) if err != nil { return err } @@ -802,13 +861,13 @@ func ChangeMilestoneAssign(oldMid, mid int64, issue *Issue) (err error) { } m.Completeness = m.NumClosedIssues * 100 / m.NumIssues - if _, err = sess.Id(m.Id).Cols("num_issues,num_completeness,num_closed_issues").Update(m); err != nil { + if _, err = sess.Id(m.ID).Cols("num_issues,num_completeness,num_closed_issues").Update(m); err != nil { sess.Rollback() return err } rawSql := "UPDATE `issue_user` SET milestone_id = ? WHERE issue_id = ?" - if _, err = sess.Exec(rawSql, m.Id, issue.Id); err != nil { + if _, err = sess.Exec(rawSql, m.ID, issue.ID); err != nil { sess.Rollback() return err } @@ -817,34 +876,40 @@ func ChangeMilestoneAssign(oldMid, mid int64, issue *Issue) (err error) { return sess.Commit() } -// DeleteMilestone deletes a milestone. -func DeleteMilestone(m *Milestone) (err error) { - sess := x.NewSession() - defer sess.Close() - if err = sess.Begin(); err != nil { +// DeleteMilestoneByID deletes a milestone by given ID. +func DeleteMilestoneByID(mid int64) error { + m, err := GetMilestoneByID(mid) + if err != nil { + if IsErrMilestoneNotExist(err) { + return nil + } return err } - if _, err = sess.Delete(m); err != nil { - sess.Rollback() + repo, err := GetRepositoryById(m.RepoID) + if err != nil { return err } - rawSql := "UPDATE `repository` SET num_milestones = num_milestones - 1 WHERE id = ?" - if _, err = sess.Exec(rawSql, m.RepoId); err != nil { - sess.Rollback() + sess := x.NewSession() + defer sessionRelease(sess) + if err = sess.Begin(); err != nil { return err } - rawSql = "UPDATE `issue` SET milestone_id = 0 WHERE milestone_id = ?" - if _, err = sess.Exec(rawSql, m.Id); err != nil { - sess.Rollback() + if _, err = sess.Id(m.ID).Delete(m); err != nil { return err } - rawSql = "UPDATE `issue_user` SET milestone_id = 0 WHERE milestone_id = ?" - if _, err = sess.Exec(rawSql, m.Id); err != nil { - sess.Rollback() + 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() @@ -890,7 +955,7 @@ type Comment struct { // CreateComment creates comment of issue or commit. func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType CommentType, content string, attachments []int64) (*Comment, error) { sess := x.NewSession() - defer sess.Close() + defer sessionRelease(sess) if err := sess.Begin(); err != nil { return nil, err } @@ -899,7 +964,6 @@ func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType Commen CommitId: commitId, Line: line, Content: content} if _, err := sess.Insert(comment); err != nil { - sess.Rollback() return nil, err } @@ -908,7 +972,6 @@ func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType Commen case COMMENT_TYPE_COMMENT: rawSql := "UPDATE `issue` SET num_comments = num_comments + 1 WHERE id = ?" if _, err := sess.Exec(rawSql, issueId); err != nil { - sess.Rollback() return nil, err } @@ -922,20 +985,17 @@ func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType Commen } if _, err := sess.Exec(rawSql, comment.Id, strings.Join(astrs, ",")); err != nil { - sess.Rollback() return nil, err } } case COMMENT_TYPE_REOPEN: rawSql := "UPDATE `repository` SET num_closed_issues = num_closed_issues - 1 WHERE id = ?" if _, err := sess.Exec(rawSql, repoId); err != nil { - sess.Rollback() return nil, err } case COMMENT_TYPE_CLOSE: rawSql := "UPDATE `repository` SET num_closed_issues = num_closed_issues + 1 WHERE id = ?" if _, err := sess.Exec(rawSql, repoId); err != nil { - sess.Rollback() return nil, err } } diff --git a/models/login.go b/models/login.go index 916e2731..82cea35f 100644 --- a/models/login.go +++ b/models/login.go @@ -17,6 +17,7 @@ import ( "github.com/go-xorm/xorm" "github.com/gogits/gogs/modules/auth/ldap" + "github.com/gogits/gogs/modules/auth/pam" "github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/uuid" ) @@ -28,6 +29,7 @@ const ( PLAIN LDAP SMTP + PAM ) var ( @@ -39,12 +41,14 @@ var ( var LoginTypes = map[LoginType]string{ LDAP: "LDAP", SMTP: "SMTP", + PAM: "PAM", } // Ensure structs implemented interface. var ( _ core.Conversion = &LDAPConfig{} _ core.Conversion = &SMTPConfig{} + _ core.Conversion = &PAMConfig{} ) type LDAPConfig struct { @@ -74,6 +78,18 @@ func (cfg *SMTPConfig) ToDB() ([]byte, error) { return json.Marshal(cfg) } +type PAMConfig struct { + ServiceName string // pam service (e.g. system-auth) +} + +func (cfg *PAMConfig) FromDB(bs []byte) error { + return json.Unmarshal(bs, &cfg) +} + +func (cfg *PAMConfig) ToDB() ([]byte, error) { + return json.Marshal(cfg) +} + type LoginSource struct { Id int64 Type LoginType @@ -97,6 +113,10 @@ func (source *LoginSource) SMTP() *SMTPConfig { return source.Cfg.(*SMTPConfig) } +func (source *LoginSource) PAM() *PAMConfig { + return source.Cfg.(*PAMConfig) +} + func (source *LoginSource) BeforeSet(colName string, val xorm.Cell) { if colName == "type" { ty := (*val).(int64) @@ -105,6 +125,8 @@ func (source *LoginSource) BeforeSet(colName string, val xorm.Cell) { source.Cfg = new(LDAPConfig) case SMTP: source.Cfg = new(SMTPConfig) + case PAM: + source.Cfg = new(PAMConfig) } } } @@ -169,8 +191,8 @@ func UserSignIn(uname, passwd string) (*User, error) { // For plain login, user must exist to reach this line. // Now verify password. if u.LoginType == PLAIN { - if !u.ValidtePassword(passwd) { - return nil, ErrUserNotExist + if !u.ValidatePassword(passwd) { + return nil, ErrUserNotExist{u.Id, u.Name} } return u, nil } @@ -197,10 +219,17 @@ func UserSignIn(uname, passwd string) (*User, error) { return u, nil } log.Warn("Fail to login(%s) by SMTP(%s): %v", uname, source.Name, err) + } else if source.Type == PAM { + u, err := LoginUserPAMSource(nil, uname, passwd, + source.Id, source.Cfg.(*PAMConfig), true) + if err == nil { + return u, nil + } + log.Warn("Fail to login(%s) by PAM(%s): %v", uname, source.Name, err) } } - return nil, ErrUserNotExist + return nil, ErrUserNotExist{u.Id, u.Name} } var source LoginSource @@ -218,6 +247,8 @@ func UserSignIn(uname, passwd string) (*User, error) { return LoginUserLdapSource(u, u.LoginName, passwd, source.Id, source.Cfg.(*LDAPConfig), false) case SMTP: return LoginUserSMTPSource(u, u.LoginName, passwd, source.Id, source.Cfg.(*SMTPConfig), false) + case PAM: + return LoginUserPAMSource(u, u.LoginName, passwd, source.Id, source.Cfg.(*PAMConfig), false) } return nil, ErrUnsupportedLoginType } @@ -230,7 +261,7 @@ func LoginUserLdapSource(u *User, name, passwd string, sourceId int64, cfg *LDAP name, fn, sn, mail, logged := cfg.Ldapsource.SearchEntry(name, passwd) if !logged { // User not in LDAP, do nothing - return nil, ErrUserNotExist + return nil, ErrUserNotExist{u.Id, u.Name} } if !autoRegister { return u, nil @@ -331,7 +362,7 @@ func LoginUserSMTPSource(u *User, name, passwd string, sourceId int64, cfg *SMTP if err := SmtpAuth(cfg.Host, cfg.Port, auth, cfg.TLS); err != nil { if strings.Contains(err.Error(), "Username and Password not accepted") { - return nil, ErrUserNotExist + return nil, ErrUserNotExist{u.Id, u.Name} } return nil, err } @@ -359,3 +390,33 @@ func LoginUserSMTPSource(u *User, name, passwd string, sourceId int64, cfg *SMTP err := CreateUser(u) return u, err } + +// Query if name/passwd can login against PAM +// Create a local user if success +// Return the same LoginUserPlain semantic +func LoginUserPAMSource(u *User, name, passwd string, sourceId int64, cfg *PAMConfig, autoRegister bool) (*User, error) { + if err := pam.PAMAuth(cfg.ServiceName, name, passwd); err != nil { + if strings.Contains(err.Error(), "Authentication failure") { + return nil, ErrUserNotExist{u.Id, u.Name} + } + return nil, err + } + + if !autoRegister { + return u, nil + } + + // fake a local user creation + u = &User{ + LowerName: strings.ToLower(name), + Name: strings.ToLower(name), + LoginType: PAM, + LoginSource: sourceId, + LoginName: name, + IsActive: true, + Passwd: passwd, + Email: name, + } + err := CreateUser(u) + return u, err +} diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 7ad0cbdd..c7900743 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -5,6 +5,7 @@ package migrations import ( + "encoding/json" "fmt" "strings" "time" @@ -51,11 +52,12 @@ type Version struct { // If you want to "retire" a migration, remove it from the top of the list and // update _MIN_VER_DB accordingly var migrations = []Migration{ - NewMigration("generate collaboration from access", accessToCollaboration), // V0 -> V1:v0.5.13 - NewMigration("make authorize 4 if team is owners", ownerTeamUpdate), // V1 -> V2:v0.5.13 - NewMigration("refactor access table to use id's", accessRefactor), // V2 -> V3:v0.5.13 - NewMigration("generate team-repo from team", teamToTeamRepo), // V3 -> V4:v0.5.13 - NewMigration("fix locale file load panic", fixLocaleFileLoadPanic), // V4 -> V5:v0.6.0 + NewMigration("generate collaboration from access", accessToCollaboration), // V0 -> V1:v0.5.13 + NewMigration("make authorize 4 if team is owners", ownerTeamUpdate), // V1 -> V2:v0.5.13 + NewMigration("refactor access table to use id's", accessRefactor), // V2 -> V3:v0.5.13 + NewMigration("generate team-repo from team", teamToTeamRepo), // V3 -> V4:v0.5.13 + NewMigration("fix locale file load panic", fixLocaleFileLoadPanic), // V4 -> V5:v0.6.0 + NewMigration("trim action compare URL prefix", trimCommitActionAppUrlPrefix), // V5 -> V6:v0.6.3 } // Migrate database to current version @@ -389,3 +391,65 @@ func fixLocaleFileLoadPanic(_ *xorm.Engine) error { setting.Langs = strings.Split(strings.Replace(strings.Join(setting.Langs, ","), "fr-CA", "fr-FR", 1), ",") return nil } + +func trimCommitActionAppUrlPrefix(x *xorm.Engine) error { + type PushCommit struct { + Sha1 string + Message string + AuthorEmail string + AuthorName string + } + + type PushCommits struct { + Len int + Commits []*PushCommit + CompareUrl string + } + + type Action struct { + ID int64 `xorm:"pk autoincr"` + Content string `xorm:"TEXT"` + } + + results, err := x.Query("SELECT `id`,`content` FROM `action` WHERE `op_type`=?", 5) + if err != nil { + return fmt.Errorf("select commit actions: %v", err) + } + + sess := x.NewSession() + defer sessionRelease(sess) + if err = sess.Begin(); err != nil { + return err + } + + var pushCommits *PushCommits + for _, action := range results { + actID := com.StrTo(string(action["id"])).MustInt64() + if actID == 0 { + continue + } + + pushCommits = new(PushCommits) + if err = json.Unmarshal(action["content"], pushCommits); err != nil { + return fmt.Errorf("unmarshal action content[%s]: %v", actID, err) + } + + infos := strings.Split(pushCommits.CompareUrl, "/") + if len(infos) <= 4 { + continue + } + pushCommits.CompareUrl = strings.Join(infos[len(infos)-4:], "/") + + p, err := json.Marshal(pushCommits) + if err != nil { + return fmt.Errorf("marshal action content[%s]: %v", actID, err) + } + + if _, err = sess.Id(actID).Update(&Action{ + Content: string(p), + }); err != nil { + return fmt.Errorf("update action[%s]: %v", actID, err) + } + } + return sess.Commit() +} diff --git a/models/models.go b/models/models.go index b7986fed..01b96c0f 100644 --- a/models/models.go +++ b/models/models.go @@ -55,7 +55,7 @@ var ( func init() { tables = append(tables, new(User), new(PublicKey), new(Oauth2), new(AccessToken), - new(Repository), new(Collaboration), new(Access), + new(Repository), new(DeployKey), new(Collaboration), new(Access), new(Watch), new(Star), new(Follow), new(Action), new(Issue), new(Comment), new(Attachment), new(IssueUser), new(Label), new(Milestone), new(Mirror), new(Release), new(LoginSource), new(Webhook), @@ -90,10 +90,10 @@ func getEngine() (*xorm.Engine, error) { switch DbCfg.Type { case "mysql": if DbCfg.Host[0] == '/' { // looks like a unix socket - cnnstr = fmt.Sprintf("%s:%s@unix(%s)/%s?charset=utf8", + cnnstr = fmt.Sprintf("%s:%s@unix(%s)/%s?charset=utf8&parseTime=true", DbCfg.User, DbCfg.Passwd, DbCfg.Host, DbCfg.Name) } else { - cnnstr = fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8", + cnnstr = fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=true", DbCfg.User, DbCfg.Passwd, DbCfg.Host, DbCfg.Name) } case "postgres": @@ -105,7 +105,7 @@ func getEngine() (*xorm.Engine, error) { if len(fields) > 1 && len(strings.TrimSpace(fields[1])) > 0 { port = fields[1] } - cnnstr = fmt.Sprintf("user=%s password=%s host=%s port=%s dbname=%s sslmode=%s", + cnnstr = fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=%s", DbCfg.User, DbCfg.Passwd, host, port, DbCfg.Name, DbCfg.SSLMode) case "sqlite3": if !EnableSQLite3 { @@ -122,7 +122,7 @@ func getEngine() (*xorm.Engine, error) { func NewTestEngine(x *xorm.Engine) (err error) { x, err = getEngine() if err != nil { - return fmt.Errorf("connect to database: %v", err) + return fmt.Errorf("Connect to database: %v", err) } x.SetMapper(core.GonicMapper{}) @@ -132,7 +132,7 @@ func NewTestEngine(x *xorm.Engine) (err error) { func SetEngine() (err error) { x, err = getEngine() if err != nil { - return fmt.Errorf("connect to database: %v", err) + return fmt.Errorf("Fail to connect to database: %v", err) } x.SetMapper(core.GonicMapper{}) @@ -144,7 +144,7 @@ func SetEngine() (err error) { f, err := os.Create(logPath) if err != nil { - return fmt.Errorf("models.init(fail to create xorm.log): %v", err) + return fmt.Errorf("Fail to create xorm.log: %v", err) } x.SetLogger(xorm.NewSimpleLogger(f)) diff --git a/models/org.go b/models/org.go index 32f135cb..3caed30b 100644 --- a/models/org.go +++ b/models/org.go @@ -105,23 +105,23 @@ func IsOrgEmailUsed(email string) (bool, error) { } // CreateOrganization creates record of a new organization. -func CreateOrganization(org, owner *User) (*User, error) { - if !IsLegalName(org.Name) { - return nil, ErrUserNameIllegal +func CreateOrganization(org, owner *User) (err error) { + if err = IsUsableName(org.Name); err != nil { + return err } isExist, err := IsUserExist(0, org.Name) if err != nil { - return nil, err + return err } else if isExist { - return nil, ErrUserAlreadyExist + return ErrUserAlreadyExist{org.Name} } isExist, err = IsOrgEmailUsed(org.Email) if err != nil { - return nil, err + return err } else if isExist { - return nil, ErrEmailAlreadyUsed + return ErrEmailAlreadyUsed{org.Email} } org.LowerName = strings.ToLower(org.Name) @@ -135,11 +135,11 @@ func CreateOrganization(org, owner *User) (*User, error) { sess := x.NewSession() defer sessionRelease(sess) if err = sess.Begin(); err != nil { - return nil, err + return err } if _, err = sess.Insert(org); err != nil { - return nil, err + return fmt.Errorf("insert organization: %v", err) } // Create default owner team. @@ -151,7 +151,7 @@ func CreateOrganization(org, owner *User) (*User, error) { NumMembers: 1, } if _, err = sess.Insert(t); err != nil { - return nil, err + return fmt.Errorf("insert owner team: %v", err) } // Add initial creator to organization and owner team. @@ -162,7 +162,7 @@ func CreateOrganization(org, owner *User) (*User, error) { NumTeams: 1, } if _, err = sess.Insert(ou); err != nil { - return nil, err + return fmt.Errorf("insert org-user relation: %v", err) } tu := &TeamUser{ @@ -171,14 +171,14 @@ func CreateOrganization(org, owner *User) (*User, error) { TeamID: t.ID, } if _, err = sess.Insert(tu); err != nil { - return nil, err + return fmt.Errorf("insert team-user relation: %v", err) } if err = os.MkdirAll(UserPath(org.Name), os.ModePerm); err != nil { - return nil, err + return fmt.Errorf("create directory: %v", err) } - return org, sess.Commit() + return sess.Commit() } // GetOrgByName returns organization by given name. @@ -594,9 +594,9 @@ func (t *Team) RemoveRepository(repoID int64) error { // NewTeam creates a record of new team. // It's caller's responsibility to assign organization ID. -func NewTeam(t *Team) error { - if !IsLegalName(t.Name) { - return ErrTeamNameIllegal +func NewTeam(t *Team) (err error) { + if err = IsUsableName(t.Name); err != nil { + return err } has, err := x.Id(t.OrgID).Get(new(User)) @@ -670,8 +670,8 @@ func GetTeamById(teamId int64) (*Team, error) { // UpdateTeam updates information of team. func UpdateTeam(t *Team, authChanged bool) (err error) { - if !IsLegalName(t.Name) { - return ErrTeamNameIllegal + if err = IsUsableName(t.Name); err != nil { + return err } if len(t.Description) > 255 { diff --git a/models/publickey.go b/models/publickey.go index 0db9f333..70da8057 100644 --- a/models/publickey.go +++ b/models/publickey.go @@ -21,6 +21,7 @@ import ( "time" "github.com/Unknwon/com" + "github.com/go-xorm/xorm" "github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/process" @@ -33,8 +34,6 @@ const ( ) var ( - ErrKeyAlreadyExist = errors.New("Public key already exists") - ErrKeyNotExist = errors.New("Public key does not exist") ErrKeyUnableVerify = errors.New("Unable to verify public key") ) @@ -78,17 +77,34 @@ func init() { } } -// PublicKey represents a SSH key. +type KeyType int + +const ( + KEY_TYPE_USER = iota + 1 + KEY_TYPE_DEPLOY +) + +// PublicKey represents a SSH or deploy key. type PublicKey struct { - Id int64 - OwnerId int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` - Name string `xorm:"UNIQUE(s) NOT NULL"` - Fingerprint string `xorm:"INDEX NOT NULL"` - Content string `xorm:"TEXT NOT NULL"` - Created time.Time `xorm:"CREATED"` - Updated time.Time - HasRecentActivity bool `xorm:"-"` - HasUsed bool `xorm:"-"` + ID int64 `xorm:"pk autoincr"` + 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:"CREATED"` + Updated time.Time // Note: Updated must below Created for AfterSet. + HasRecentActivity bool `xorm:"-"` + HasUsed bool `xorm:"-"` +} + +func (k *PublicKey) AfterSet(colName string, _ xorm.Cell) { + switch colName { + case "created": + 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 but without e-mail address. @@ -98,7 +114,7 @@ func (k *PublicKey) OmitEmail() string { // GetAuthorizedString generates and returns formatted public key string for authorized_keys file. func (key *PublicKey) GetAuthorizedString() string { - return fmt.Sprintf(_TPL_PUBLICK_KEY, appPath, key.Id, setting.CustomConf, key.Content) + return fmt.Sprintf(_TPL_PUBLICK_KEY, appPath, key.ID, setting.CustomConf, key.Content) } var minimumKeySizes = map[string]int{ @@ -126,8 +142,8 @@ func extractTypeFromBase64Key(key string) (string, error) { return string(b[4 : 4+keyLength]), nil } -// Parse any key string in openssh or ssh2 format to clean openssh string (rfc4253) -func ParseKeyString(content string) (string, error) { +// 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" s := strings.Replace(strings.Replace(strings.TrimSpace(content), "\r\n", "\n", -1), "\r", "\n", -1) @@ -190,16 +206,21 @@ func ParseKeyString(content string) (string, error) { } // CheckPublicKeyString checks if the given public key string is recognized by SSH. -func CheckPublicKeyString(content string) (bool, error) { +func CheckPublicKeyString(content string) (_ string, err error) { + content, err = parseKeyString(content) + if err != nil { + return "", err + } + content = strings.TrimRight(content, "\n\r") if strings.ContainsAny(content, "\n\r") { - return false, errors.New("only a single line with a single key please") + return "", errors.New("only a single line with a single key please") } // write the key to a file… tmpFile, err := ioutil.TempFile(os.TempDir(), "keytest") if err != nil { - return false, err + return "", err } tmpPath := tmpFile.Name() defer os.Remove(tmpPath) @@ -209,37 +230,36 @@ func CheckPublicKeyString(content string) (bool, error) { // Check if ssh-keygen recognizes its contents. stdout, stderr, err := process.Exec("CheckPublicKeyString", "ssh-keygen", "-l", "-f", tmpPath) if err != nil { - return false, errors.New("ssh-keygen -l -f: " + stderr) + return "", errors.New("ssh-keygen -l -f: " + stderr) } else if len(stdout) < 2 { - return false, errors.New("ssh-keygen returned not enough output to evaluate the key: " + stdout) + return "", errors.New("ssh-keygen returned not enough output to evaluate the key: " + stdout) } // The ssh-keygen in Windows does not print key type, so no need go further. if setting.IsWindows { - return true, nil + return content, nil } - fmt.Println(stdout) sshKeygenOutput := strings.Split(stdout, " ") if len(sshKeygenOutput) < 4 { - return false, ErrKeyUnableVerify + return content, ErrKeyUnableVerify } // Check if key type and key size match. if !setting.Service.DisableMinimumKeySizeCheck { keySize := com.StrTo(sshKeygenOutput[0]).MustInt() if keySize == 0 { - return false, errors.New("cannot get key size of the given key") + return "", errors.New("cannot get key size of the given key") } keyType := strings.TrimSpace(sshKeygenOutput[len(sshKeygenOutput)-1]) if minimumKeySize := minimumKeySizes[keyType]; minimumKeySize == 0 { - return false, errors.New("sorry, unrecognized public key type") + return "", errors.New("sorry, unrecognized public key type") } else if keySize < minimumKeySize { - return false, fmt.Errorf("the minimum accepted size of a public key %s is %d", keyType, minimumKeySize) + return "", fmt.Errorf("the minimum accepted size of a public key %s is %d", keyType, minimumKeySize) } } - return true, nil + return content, nil } // saveAuthorizedKeyFile writes SSH key content to authorized_keys file. @@ -278,20 +298,23 @@ func saveAuthorizedKeyFile(keys ...*PublicKey) error { return nil } -// AddPublicKey adds new public key to database and authorized_keys file. -func AddPublicKey(key *PublicKey) (err error) { - has, err := x.Get(key) +func checkKeyContent(content string) error { + // Same key can only be added once. + has, err := x.Where("content=?", content).Get(new(PublicKey)) if err != nil { return err } else if has { - return ErrKeyAlreadyExist + 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), os.ModePerm); err != nil { + if err = ioutil.WriteFile(tmpPath, []byte(key.Content), 0644); err != nil { return err } stdout, stderr, err := process.Exec("AddPublicKey", "ssh-keygen", "-l", "-f", tmpPath) @@ -301,32 +324,56 @@ func AddPublicKey(key *PublicKey) (err error) { return errors.New("not enough output for calculating fingerprint: " + stdout) } key.Fingerprint = strings.Split(stdout, " ")[1] - if has, err := x.Get(&PublicKey{Fingerprint: key.Fingerprint}); err == nil && has { - return ErrKeyAlreadyExist - } // Save SSH key. - if _, err = x.Insert(key); err != nil { + if _, err = e.Insert(key); err != nil { return err - } else if err = saveAuthorizedKeyFile(key); err != nil { - // Roll back. - if _, err2 := x.Delete(key); err2 != nil { - return err2 - } + } + return saveAuthorizedKeyFile(key) +} + +// AddPublicKey adds new public key to database and authorized_keys file. +func AddPublicKey(ownerID int64, name, content string) (err error) { + if err = checkKeyContent(content); err != nil { return err } - return nil + // 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 err + } else if has { + return ErrKeyNameAlreadyUsed{ownerID, name} + } + + sess := x.NewSession() + defer sessionRelease(sess) + if err = sess.Begin(); err != nil { + return 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 fmt.Errorf("addKey: %v", err) + } + + return sess.Commit() } -// GetPublicKeyById returns public key by given ID. -func GetPublicKeyById(keyId int64) (*PublicKey, error) { +// GetPublicKeyByID returns public key by given ID. +func GetPublicKeyByID(keyID int64) (*PublicKey, error) { key := new(PublicKey) - has, err := x.Id(keyId).Get(key) + has, err := x.Id(keyID).Get(key) if err != nil { return nil, err } else if !has { - return nil, ErrKeyNotExist + return nil, ErrKeyNotExist{keyID} } return key, nil } @@ -334,16 +381,7 @@ func GetPublicKeyById(keyId int64) (*PublicKey, error) { // ListPublicKeys returns a list of public keys belongs to given user. func ListPublicKeys(uid int64) ([]*PublicKey, error) { keys := make([]*PublicKey, 0, 5) - err := x.Where("owner_id=?", uid).Find(&keys) - if err != nil { - return nil, err - } - - for _, key := range keys { - key.HasUsed = key.Updated.After(key.Created) - key.HasRecentActivity = key.Updated.Add(7 * 24 * time.Hour).After(time.Now()) - } - return keys, nil + return keys, x.Where("owner_id=?", uid).Find(&keys) } // rewriteAuthorizedKeys finds and deletes corresponding line in authorized_keys file. @@ -364,7 +402,7 @@ func rewriteAuthorizedKeys(key *PublicKey, p, tmpP string) error { defer fw.Close() isFound := false - keyword := fmt.Sprintf("key-%d", key.Id) + keyword := fmt.Sprintf("key-%d", key.ID) buf := bufio.NewReader(fr) for { line, errRead := buf.ReadString('\n') @@ -401,20 +439,19 @@ func rewriteAuthorizedKeys(key *PublicKey, p, tmpP string) error { // UpdatePublicKey updates given public key. func UpdatePublicKey(key *PublicKey) error { - _, err := x.Id(key.Id).AllCols().Update(key) + _, err := x.Id(key.ID).AllCols().Update(key) return err } -// DeletePublicKey deletes SSH key information both in database and authorized_keys file. -func DeletePublicKey(key *PublicKey) error { - has, err := x.Get(key) +func deletePublicKey(e *xorm.Session, key *PublicKey) error { + has, err := e.Get(key) if err != nil { return err } else if !has { - return ErrKeyNotExist + return nil } - if _, err = x.Delete(key); err != nil { + if _, err = e.Id(key.ID).Delete(key); err != nil { return err } @@ -428,6 +465,21 @@ func DeletePublicKey(key *PublicKey) error { return os.Rename(tmpPath, fpath) } +// DeletePublicKey deletes SSH key information both in database and authorized_keys file. +func DeletePublicKey(key *PublicKey) (err error) { + sess := x.NewSession() + defer sessionRelease(sess) + if err = sess.Begin(); err != nil { + return err + } + + if err = deletePublicKey(sess, key); err != nil { + return err + } + + return sess.Commit() +} + // RewriteAllPublicKeys removes any authorized key and rewrite all keys from database again. func RewriteAllPublicKeys() error { sshOpLocker.Lock() @@ -461,3 +513,162 @@ func RewriteAllPublicKeys() error { return nil } + +// ________ .__ ____ __. +// \______ \ ____ ______ | | ____ ___.__.| |/ _|____ ___.__. +// | | \_/ __ \\____ \| | / _ < | || <_/ __ < | | +// | ` \ ___/| |_> > |_( <_> )___ || | \ ___/\___ | +// /_______ /\___ > __/|____/\____// ____||____|__ \___ > ____| +// \/ \/|__| \/ \/ \/\/ + +// DeployKey represents deploy key information and its relation with repository. +type DeployKey struct { + ID int64 `xorm:"pk autoincr"` + KeyID int64 `xorm:"UNIQUE(s) INDEX"` + RepoID int64 `xorm:"UNIQUE(s) INDEX"` + Name string + Fingerprint string + Created time.Time `xorm:"CREATED"` + Updated time.Time // Note: Updated must below Created for AfterSet. + HasRecentActivity bool `xorm:"-"` + HasUsed bool `xorm:"-"` +} + +func (k *DeployKey) AfterSet(colName string, _ xorm.Cell) { + switch colName { + case "created": + k.HasUsed = k.Updated.After(k.Created) + k.HasRecentActivity = k.Updated.Add(7 * 24 * time.Hour).After(time.Now()) + } +} + +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) (err error) { + if err = checkDeployKey(e, keyID, repoID, name); err != nil { + return err + } + + _, err = e.Insert(&DeployKey{ + KeyID: keyID, + RepoID: repoID, + Name: name, + Fingerprint: fingerprint, + }) + return 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) (err error) { + if err = checkKeyContent(content); err != nil { + return err + } + + key := &PublicKey{ + Content: content, + Mode: ACCESS_MODE_READ, + Type: KEY_TYPE_DEPLOY, + } + has, err := x.Get(key) + if err != nil { + return err + } + + sess := x.NewSession() + defer sessionRelease(sess) + if err = sess.Begin(); err != nil { + return err + } + + // First time use this deploy key. + if !has { + if err = addKey(sess, key); err != nil { + return nil + } + } + + if err = addDeployKey(sess, key.ID, repoID, name, key.Fingerprint); err != nil { + return err + } + + return sess.Commit() +} + +// 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, + } + _, err := x.Get(key) + return key, err +} + +// 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(id int64) error { + key := &DeployKey{ID: id} + has, err := x.Id(key.ID).Get(key) + if err != nil { + return err + } else if !has { + return nil + } + + sess := x.NewSession() + defer sessionRelease(sess) + if err = sess.Begin(); err != nil { + return err + } + + if _, err = sess.Id(key.ID).Delete(key); 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 = deletePublicKey(sess, &PublicKey{ID: 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/models/repo.go b/models/repo.go index 91bb0d71..8135bc57 100644 --- a/models/repo.go +++ b/models/repo.go @@ -21,6 +21,7 @@ import ( "github.com/Unknwon/cae/zip" "github.com/Unknwon/com" + "github.com/go-xorm/xorm" "github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/bindata" @@ -35,12 +36,11 @@ const ( ) var ( - ErrRepoAlreadyExist = errors.New("Repository already exist") ErrRepoFileNotExist = errors.New("Repository file does not exist") - ErrRepoNameIllegal = errors.New("Repository name contains illegal characters") ErrRepoFileNotLoaded = errors.New("Repository file not loaded") ErrMirrorNotExist = errors.New("Mirror does not exist") ErrInvalidReference = errors.New("Invalid reference specified") + ErrNameEmpty = errors.New("Name is empty") ) var ( @@ -222,13 +222,17 @@ func (repo *Repository) DescriptionHtml() template.HTML { return template.HTML(DescPattern.ReplaceAllStringFunc(base.Sanitizer.Sanitize(repo.Description), sanitize)) } -// IsRepositoryExist returns true if the repository with given name under user has already existed. -func IsRepositoryExist(u *User, repoName string) bool { - has, _ := x.Get(&Repository{ +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)) + 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. @@ -243,34 +247,42 @@ func (repo *Repository) CloneLink() (cl CloneLink, err error) { if err = repo.GetOwner(); err != nil { return cl, err } + if setting.SSHPort != 22 { - cl.SSH = fmt.Sprintf("ssh://%s@%s:%d/%s/%s.git", setting.RunUser, setting.Domain, setting.SSHPort, repo.Owner.LowerName, repo.LowerName) + cl.SSH = fmt.Sprintf("ssh://%s@%s:%d/%s/%s.git", setting.RunUser, setting.SSHDomain, setting.SSHPort, repo.Owner.LowerName, repo.LowerName) } else { - cl.SSH = fmt.Sprintf("%s@%s:%s/%s.git", setting.RunUser, setting.Domain, repo.Owner.LowerName, repo.LowerName) + cl.SSH = fmt.Sprintf("%s@%s:%s/%s.git", setting.RunUser, setting.SSHDomain, repo.Owner.LowerName, repo.LowerName) } cl.HTTPS = fmt.Sprintf("%s%s/%s.git", setting.AppUrl, repo.Owner.LowerName, repo.LowerName) return cl, nil } var ( - illegalEquals = []string{"debug", "raw", "install", "api", "avatar", "user", "org", "help", "stars", "issues", "pulls", "commits", "repo", "template", "admin", "new"} - illegalSuffixs = []string{".git", ".keys"} + reservedNames = []string{"debug", "raw", "install", "api", "avatar", "user", "org", "help", "stars", "issues", "pulls", "commits", "repo", "template", "admin", "new"} + reservedPatterns = []string{"*.git", "*.keys"} ) -// IsLegalName returns false if name contains illegal characters. -func IsLegalName(repoName string) bool { - repoName = strings.ToLower(repoName) - for _, char := range illegalEquals { - if repoName == char { - return false +// IsUsableName checks if name is reserved or pattern of name is not allowed. +func IsUsableName(name string) error { + name = strings.TrimSpace(strings.ToLower(name)) + if utf8.RuneCountInString(name) == 0 { + return ErrNameEmpty + } + + for i := range reservedNames { + if name == reservedNames[i] { + return ErrNameReserved{name} } } - for _, char := range illegalSuffixs { - if strings.HasSuffix(repoName, char) { - return false + + for _, pat := range reservedPatterns { + if pat[0] == '*' && strings.HasSuffix(name, pat[1:]) || + (pat[len(pat)-1] == '*' && strings.HasPrefix(name, pat[:len(pat)-1])) { + return ErrNamePatternNotAllowed{pat} } } - return true + + return nil } // Mirror represents a mirror information of repository. @@ -365,13 +377,22 @@ func MigrateRepository(u *User, name, desc string, private, mirror bool, url str // FIXME: this command could for both migrate and mirror _, stderr, err := process.ExecTimeout(10*time.Minute, fmt.Sprintf("MigrateRepository: %s", repoPath), - "git", "clone", "--mirror", "--bare", url, repoPath) + "git", "clone", "--mirror", "--bare", "--quiet", url, repoPath) if err != nil { - return repo, fmt.Errorf("git clone --mirror --bare: %v", stderr) + return repo, fmt.Errorf("git clone --mirror --bare --quiet: %v", stderr) } else if err = createUpdateHook(repoPath); err != nil { return repo, fmt.Errorf("create update hook: %v", err) } + // Check if repository has master branch, if so set it to default branch. + gitRepo, err := git.OpenRepository(repoPath) + if err != nil { + return repo, fmt.Errorf("open git repository: %v", err) + } + if gitRepo.IsBranchExist("master") { + repo.DefaultBranch = "master" + } + return repo, UpdateRepository(repo, false) } @@ -406,13 +427,18 @@ func createUpdateHook(repoPath string) error { // InitRepository initializes README and .gitignore if needed. func initRepository(e Engine, repoPath string, u *User, repo *Repository, initReadme bool, repoLang, license string) error { + // Somehow the directory could exist. + if com.IsExist(repoPath) { + return fmt.Errorf("initRepository: path already exists: %s", repoPath) + } + // Init bare new repository. os.MkdirAll(repoPath, os.ModePerm) _, stderr, err := process.ExecDir(-1, repoPath, fmt.Sprintf("initRepository(git init --bare): %s", repoPath), "git", "init", "--bare") if err != nil { - return errors.New("git init --bare: " + stderr) + return fmt.Errorf("git init --bare: %s", err) } if err := createUpdateHook(repoPath); err != nil { @@ -434,6 +460,7 @@ func initRepository(e Engine, repoPath string, u *User, repo *Repository, initRe // Clone to temprory path and do the init commit. tmpDir := filepath.Join(os.TempDir(), com.ToStr(time.Now().Nanosecond())) os.MkdirAll(tmpDir, os.ModePerm) + defer os.RemoveAll(tmpDir) _, stderr, err = process.Exec( fmt.Sprintf("initRepository(git clone): %s", repoPath), @@ -464,7 +491,7 @@ func initRepository(e Engine, repoPath string, u *User, repo *Repository, initRe } } else if com.IsSliceContainsStr(Gitignores, repoLang) { if err = ioutil.WriteFile(targetPath, - bindata.MustAsset(path.Join("conf/gitignore", repoLang)), os.ModePerm); err != nil { + bindata.MustAsset(path.Join("conf/gitignore", repoLang)), 0644); err != nil { return fmt.Errorf("generate gitignore: %v", err) } } else { @@ -480,7 +507,7 @@ func initRepository(e Engine, repoPath string, u *User, repo *Repository, initRe } } else if com.IsSliceContainsStr(Licenses, license) { if err = ioutil.WriteFile(targetPath, - bindata.MustAsset(path.Join("conf/license", license)), os.ModePerm); err != nil { + bindata.MustAsset(path.Join("conf/license", license)), 0644); err != nil { return fmt.Errorf("generate license: %v", err) } } else { @@ -502,16 +529,50 @@ func initRepository(e Engine, repoPath string, u *User, repo *Repository, initRe return initRepoCommit(tmpDir, u.NewGitSig()) } -// CreateRepository creates a repository for given user or organization. -func CreateRepository(u *User, name, desc, lang, license string, isPrivate, isMirror, initReadme bool) (_ *Repository, err error) { - if !IsLegalName(name) { - return nil, ErrRepoNameIllegal +func createRepository(e *xorm.Session, u *User, repo *Repository) (err error) { + if err = IsUsableName(repo.Name); err != nil { + return err } - if IsRepositoryExist(u, name) { - return nil, ErrRepoAlreadyExist + has, err := isRepositoryExist(e, u, repo.Name) + if err != nil { + return fmt.Errorf("IsRepositoryExist: %v", err) + } else if has { + return ErrRepoAlreadyExist{u.Name, repo.Name} + } + + if _, err = e.Insert(repo); err != nil { + return err + } else if _, err = e.Exec("UPDATE `user` SET num_repos=num_repos+1 WHERE id=?", u.Id); err != nil { + return err } + // Give access to all members in owner team. + if u.IsOrganization() { + t, err := u.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, u.Id, repo.Id, true); err != nil { + return fmt.Errorf("watchRepo: %v", err) + } else if err = newRepoAction(e, u, repo); err != nil { + return fmt.Errorf("newRepoAction: %v", err) + } + + return nil +} + +// CreateRepository creates a repository for given user or organization. +func CreateRepository(u *User, name, desc, lang, license string, isPrivate, isMirror, initReadme bool) (_ *Repository, err error) { repo := &Repository{ OwnerId: u.Id, Owner: u, @@ -527,35 +588,10 @@ func CreateRepository(u *User, name, desc, lang, license string, isPrivate, isMi return nil, err } - if _, err = sess.Insert(repo); err != nil { - return nil, err - } else if _, err = sess.Exec("UPDATE `user` SET num_repos = num_repos + 1 WHERE id = ?", u.Id); err != nil { + if err = createRepository(sess, u, repo); err != nil { return nil, err } - // TODO fix code for mirrors? - - // Give access to all members in owner team. - if u.IsOrganization() { - t, err := u.getOwnerTeam(sess) - if err != nil { - return nil, fmt.Errorf("getOwnerTeam: %v", err) - } else if err = t.addRepository(sess, repo); err != nil { - return nil, fmt.Errorf("addRepository: %v", err) - } - } else { - // Organization called this in addRepository method. - if err = repo.recalculateAccesses(sess); err != nil { - return nil, fmt.Errorf("recalculateAccesses: %v", err) - } - } - - if err = watchRepo(sess, u.Id, repo.Id, true); err != nil { - return nil, fmt.Errorf("watchRepo: %v", err) - } else if err = newRepoAction(sess, u, repo); err != nil { - return nil, fmt.Errorf("newRepoAction: %v", err) - } - // No need for init mirror. if !isMirror { repoPath := RepoPath(u.Name, repo.Name) @@ -599,7 +635,7 @@ func GetRepositoriesWithUsers(num, offset int) ([]*Repository, error) { if err != nil { return nil, err } else if !has { - return nil, ErrUserNotExist + return nil, ErrUserNotExist{repo.OwnerId, ""} } } @@ -619,8 +655,11 @@ func TransferOwnership(u *User, newOwnerName string, repo *Repository) error { } // Check if new owner has repository with same name. - if IsRepositoryExist(newOwner, repo.Name) { - return ErrRepoAlreadyExist + 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() @@ -727,16 +766,22 @@ func TransferOwnership(u *User, newOwnerName string, repo *Repository) error { } // ChangeRepositoryName changes all corresponding setting from old repository name to new one. -func ChangeRepositoryName(userName, oldRepoName, newRepoName string) (err error) { - userName = strings.ToLower(userName) +func ChangeRepositoryName(u *User, oldRepoName, newRepoName string) (err error) { oldRepoName = strings.ToLower(oldRepoName) newRepoName = strings.ToLower(newRepoName) - if !IsLegalName(newRepoName) { - return ErrRepoNameIllegal + if err = IsUsableName(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} } // Change repository directory name. - return os.Rename(RepoPath(userName, oldRepoName), RepoPath(userName, newRepoName)) + return os.Rename(RepoPath(u.LowerName, oldRepoName), RepoPath(u.LowerName, newRepoName)) } func updateRepository(e Engine, repo *Repository, visibilityChanged bool) (err error) { @@ -833,7 +878,7 @@ func DeleteRepository(uid, repoID int64, userName string) error { return err } else if _, err = sess.Delete(&IssueUser{RepoId: repoID}); err != nil { return err - } else if _, err = sess.Delete(&Milestone{RepoId: repoID}); err != nil { + } else if _, err = sess.Delete(&Milestone{RepoID: repoID}); err != nil { return err } else if _, err = sess.Delete(&Release{RepoId: repoID}); err != nil { return err @@ -847,12 +892,12 @@ func DeleteRepository(uid, repoID int64, userName string) error { return err } for i := range issues { - if _, err = sess.Delete(&Comment{IssueId: issues[i].Id}); err != nil { + if _, err = sess.Delete(&Comment{IssueId: issues[i].ID}); err != nil { return err } } - if _, err = sess.Delete(&Issue{RepoId: repoID}); err != nil { + if _, err = sess.Delete(&Issue{RepoID: repoID}); err != nil { return err } @@ -1008,6 +1053,7 @@ var ( // Prevent duplicate tasks. isMirrorUpdating = false isGitFscking = false + isCheckingRepos = false ) // MirrorUpdate checks and updates mirror repositories. @@ -1029,7 +1075,7 @@ func MirrorUpdate() { repoPath := filepath.Join(setting.RepoRootPath, m.RepoName+".git") if _, stderr, err := process.ExecDir(10*time.Minute, repoPath, fmt.Sprintf("MirrorUpdate: %s", repoPath), - "git", "remote", "update"); err != nil { + "git", "remote", "update", "--prune"); err != nil { desc := fmt.Sprintf("Fail to update mirror repository(%s): %s", repoPath, stderr) log.Error(4, desc) if err = CreateRepositoryNotice(desc); err != nil { @@ -1099,6 +1145,42 @@ func GitGcRepos() error { }) } +func CheckRepoStats() { + if isCheckingRepos { + return + } + isCheckingRepos = true + defer func() { isCheckingRepos = false }() + + // Check count watchers + results_watch, err := x.Query("SELECT r.id FROM `repository` r WHERE r.num_watches!=(SELECT count(*) FROM `watch` WHERE repo_id=r.id)") + if err != nil { + log.Error(4, "select repository check 'watch': %v", err) + } + for _, repo_id := range results_watch { + log.Info("updating repository count 'watch'") + repoID := com.StrTo(repo_id["id"]).MustInt64() + _, err := x.Exec("UPDATE `repository` SET num_watches=(SELECT count(*) FROM `watch` WHERE repo_id=?) WHERE id=?", repoID, repoID) + if err != nil { + log.Error(4, "update repository check 'watch', repo %v: %v", repo_id, err) + } + } + + // Check count stars + results_star, err := x.Query("SELECT s.id FROM `repository` s WHERE s.num_stars!=(SELECT count(*) FROM `star` WHERE repo_id=s.id)") + if err != nil { + log.Error(4, "select repository check 'star': %v", err) + } + for _, repo_id := range results_star { + log.Info("updating repository count 'star'") + repoID := com.StrTo(repo_id["id"]).MustInt64() + _, err := x.Exec("UPDATE `repository` SET .num_stars=(SELECT count(*) FROM `star` WHERE repo_id=?) WHERE id=?", repoID, repoID) + if err != nil { + log.Error(4, "update repository check 'star', repo %v: %v", repo_id, err) + } + } +} + // _________ .__ .__ ___. __ .__ // \_ ___ \ ____ | | | | _____ \_ |__ ________________ _/ |_|__| ____ ____ // / \ \/ / _ \| | | | \__ \ | __ \ / _ \_ __ \__ \\ __\ |/ _ \ / \ @@ -1339,19 +1421,14 @@ func IsStaring(uid, repoId int64) bool { // \___ / \____/|__| |__|_ \ // \/ \/ -func ForkRepository(u *User, oldRepo *Repository, name, desc string) (_ *Repository, err error) { - if IsRepositoryExist(u, name) { - return nil, ErrRepoAlreadyExist - } - - // In case the old repository is a fork. - if oldRepo.IsFork { - oldRepo, err = GetRepositoryById(oldRepo.ForkId) - if err != nil { - return nil, err - } - } +// HasForkedRepo checks if given user has already forked a repository with given ID. +func HasForkedRepo(ownerID, repoID int64) (*Repository, bool) { + repo := new(Repository) + has, _ := x.Where("owner_id=? AND fork_id=?", ownerID, repoID).Get(repo) + return repo, has +} +func ForkRepository(u *User, oldRepo *Repository, name, desc string) (_ *Repository, err error) { repo := &Repository{ OwnerId: u.Id, Owner: u, @@ -1369,32 +1446,8 @@ func ForkRepository(u *User, oldRepo *Repository, name, desc string) (_ *Reposit return nil, err } - if _, err = sess.Insert(repo); err != nil { - return nil, err - } - - if err = repo.recalculateAccesses(sess); err != nil { + if err = createRepository(sess, u, repo); err != nil { return nil, err - } else if _, err = sess.Exec("UPDATE `user` SET num_repos = num_repos + 1 WHERE id = ?", u.Id); err != nil { - return nil, err - } - - if u.IsOrganization() { - // Update owner team info and count. - t, err := u.getOwnerTeam(sess) - if err != nil { - return nil, fmt.Errorf("getOwnerTeam: %v", err) - } else if err = t.addRepository(sess, repo); err != nil { - return nil, fmt.Errorf("addRepository: %v", err) - } - } else { - if err = watchRepo(sess, u.Id, repo.Id, true); err != nil { - return nil, fmt.Errorf("watchRepo: %v", err) - } - } - - if err = newRepoAction(sess, u, repo); err != nil { - return nil, fmt.Errorf("newRepoAction: %v", err) } if _, err = sess.Exec("UPDATE `repository` SET num_forks=num_forks+1 WHERE id=?", oldRepo.Id); err != nil { diff --git a/models/user.go b/models/user.go index dcfd0dc5..6dd31536 100644 --- a/models/user.go +++ b/models/user.go @@ -36,10 +36,7 @@ const ( ) var ( - ErrUserAlreadyExist = errors.New("User already exist") - ErrUserNotExist = errors.New("User does not exist") ErrUserNotKeyOwner = errors.New("User does not the owner of public key") - ErrEmailAlreadyUsed = errors.New("E-mail already used") ErrEmailNotExist = errors.New("E-mail does not exist") ErrEmailNotActivated = errors.New("E-mail address has not been activated") ErrUserNameIllegal = errors.New("User name contains illegal characters") @@ -145,8 +142,8 @@ func (u *User) EncodePasswd() { u.Passwd = fmt.Sprintf("%x", newPasswd) } -// ValidtePassword checks if given password matches the one belongs to the user. -func (u *User) ValidtePassword(passwd string) bool { +// 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 u.Passwd == newUser.Passwd @@ -261,6 +258,8 @@ func IsEmailUsed(email string) (bool, error) { if len(email) == 0 { return false, nil } + + email = strings.ToLower(email) if has, err := x.Get(&EmailAddress{Email: email}); has || err != nil { return has, err } @@ -273,23 +272,23 @@ func GetUserSalt() string { } // CreateUser creates record of a new user. -func CreateUser(u *User) error { - if !IsLegalName(u.Name) { - return ErrUserNameIllegal +func CreateUser(u *User) (err error) { + if err = IsUsableName(u.Name); err != nil { + return err } isExist, err := IsUserExist(0, u.Name) if err != nil { return err } else if isExist { - return ErrUserAlreadyExist + return ErrUserAlreadyExist{u.Name} } isExist, err = IsEmailUsed(u.Email) if err != nil { return err } else if isExist { - return ErrEmailAlreadyUsed + return ErrEmailAlreadyUsed{u.Email} } u.LowerName = strings.ToLower(u.Name) @@ -315,19 +314,23 @@ func CreateUser(u *User) error { return err } - // Auto-set admin for user whose ID is 1. - if u.Id == 1 { + // Auto-set admin for the first user. + if CountUsers() == 1 { u.IsAdmin = true u.IsActive = true - _, err = x.Id(u.Id).UseBool().Update(u) + _, err = x.Id(u.Id).AllCols().Update(u) } return err } +func countUsers(e Engine) int64 { + count, _ := e.Where("type=0").Count(new(User)) + return count +} + // CountUsers returns number of users. func CountUsers() int64 { - count, _ := x.Where("type=0").Count(new(User)) - return count + return countUsers(x) } // GetUsers returns given number of user objects with offset. @@ -392,8 +395,15 @@ func VerifyActiveEmailCode(code, email string) *EmailAddress { // ChangeUserName changes all corresponding setting from old user name to new one. func ChangeUserName(u *User, newUserName string) (err error) { - if !IsLegalName(newUserName) { - return ErrUserNameIllegal + if err = IsUsableName(newUserName); err != nil { + return err + } + + isExist, err := IsUserExist(0, newUserName) + if err != nil { + return err + } else if isExist { + return ErrUserAlreadyExist{newUserName} } return os.Rename(UserPath(u.LowerName), UserPath(newUserName)) @@ -401,11 +411,12 @@ func ChangeUserName(u *User, newUserName string) (err error) { // UpdateUser updates user's information. func UpdateUser(u *User) error { + u.Email = strings.ToLower(u.Email) has, err := x.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 + return ErrEmailAlreadyUsed{u.Email} } u.LowerName = strings.ToLower(u.Name) @@ -498,7 +509,7 @@ func DeleteUser(u *User) error { // Delete all SSH keys. keys := make([]*PublicKey, 0, 10) - if err = sess.Find(&keys, &PublicKey{OwnerId: u.Id}); err != nil { + if err = sess.Find(&keys, &PublicKey{OwnerID: u.Id}); err != nil { return err } for _, key := range keys { @@ -550,7 +561,7 @@ func getUserById(e Engine, id int64) (*User, error) { if err != nil { return nil, err } else if !has { - return nil, ErrUserNotExist + return nil, ErrUserNotExist{id, ""} } return u, nil } @@ -563,14 +574,14 @@ func GetUserById(id int64) (*User, error) { // GetUserByName returns user by given name. func GetUserByName(name string) (*User, error) { if len(name) == 0 { - return nil, ErrUserNotExist + return nil, ErrUserNotExist{0, name} } u := &User{LowerName: strings.ToLower(name)} has, err := x.Get(u) if err != nil { return nil, err } else if !has { - return nil, ErrUserNotExist + return nil, ErrUserNotExist{0, name} } return u, nil } @@ -637,11 +648,12 @@ func GetEmailAddresses(uid int64) ([]*EmailAddress, error) { } func AddEmailAddress(email *EmailAddress) error { + email.Email = strings.ToLower(email.Email) used, err := IsEmailUsed(email.Email) if err != nil { return err } else if used { - return ErrEmailAlreadyUsed + return ErrEmailAlreadyUsed{email.Email} } _, err = x.Insert(email) @@ -670,7 +682,7 @@ func DeleteEmailAddress(email *EmailAddress) error { return ErrEmailNotExist } - if _, err = x.Delete(email); err != nil { + if _, err = x.Id(email.Id).Delete(email); err != nil { return err } @@ -695,7 +707,7 @@ func MakeEmailPrimary(email *EmailAddress) error { if err != nil { return err } else if !has { - return ErrUserNotExist + return ErrUserNotExist{email.Uid, ""} } // Make sure the former primary email doesn't disappear @@ -732,13 +744,15 @@ func ValidateCommitWithEmail(c *git.Commit) *User { // ValidateCommitsWithEmails checks if authors' e-mails of commits are corresponding to users. func ValidateCommitsWithEmails(oldCommits *list.List) *list.List { - emails := map[string]*User{} - newCommits := list.New() - e := oldCommits.Front() + var ( + u *User + emails = map[string]*User{} + newCommits = list.New() + e = oldCommits.Front() + ) for e != nil { c := e.Value.(*git.Commit) - var u *User if v, ok := emails[c.Author.Email]; !ok { u, _ = GetUserByEmail(c.Author.Email) emails[c.Author.Email] = u @@ -758,10 +772,12 @@ func ValidateCommitsWithEmails(oldCommits *list.List) *list.List { // GetUserByEmail returns the user object by given e-mail if exists. func GetUserByEmail(email string) (*User, error) { if len(email) == 0 { - return nil, ErrUserNotExist + return nil, ErrUserNotExist{0, "email"} } + + email = strings.ToLower(email) // First try to find the user by primary email - user := &User{Email: strings.ToLower(email)} + user := &User{Email: email} has, err := x.Get(user) if err != nil { return nil, err @@ -771,7 +787,7 @@ func GetUserByEmail(email string) (*User, error) { } // Otherwise, check in alternative list for activated email addresses - emailAddress := &EmailAddress{Email: strings.ToLower(email), IsActivated: true} + emailAddress := &EmailAddress{Email: email, IsActivated: true} has, err = x.Get(emailAddress) if err != nil { return nil, err @@ -780,7 +796,7 @@ func GetUserByEmail(email string) (*User, error) { return GetUserById(emailAddress.Uid) } - return nil, ErrUserNotExist + return nil, ErrUserNotExist{0, "email"} } // SearchUserByName returns given number of users whose name contains keyword. diff --git a/models/webhook.go b/models/webhook.go index bfa52b99..18cda748 100644 --- a/models/webhook.go +++ b/models/webhook.go @@ -9,6 +9,7 @@ import ( "encoding/json" "errors" "io/ioutil" + "sync" "time" "github.com/gogits/gogs/modules/httplib" @@ -259,7 +260,9 @@ func (p Payload) GetJSONPayload() ([]byte, error) { // HookTask represents a hook task. type HookTask struct { - Id int64 + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"INDEX"` + HookID int64 Uuid string Type HookTaskType Url string @@ -269,6 +272,7 @@ type HookTask struct { EventType HookEventType IsSsl bool IsDelivered bool + Delivered int64 IsSucceed bool } @@ -287,87 +291,137 @@ func CreateHookTask(t *HookTask) error { // UpdateHookTask updates information of hook task. func UpdateHookTask(t *HookTask) error { - _, err := x.Id(t.Id).AllCols().Update(t) + _, err := x.Id(t.ID).AllCols().Update(t) return err } -var ( - // Prevent duplicate deliveries. - // This happens with massive hook tasks cannot finish delivering - // before next shooting starts. - isShooting = false -) +type hookQueue struct { + // Make sure one repository only occur once in the queue. + lock sync.Mutex + repoIDs map[int64]bool -// DeliverHooks checks and delivers undelivered hooks. -// FIXME: maybe can use goroutine to shoot a number of them at same time? -func DeliverHooks() { - if isShooting { + queue chan int64 +} + +func (q *hookQueue) removeRepoID(id int64) { + q.lock.Lock() + defer q.lock.Unlock() + delete(q.repoIDs, id) +} + +func (q *hookQueue) addRepoID(id int64) { + q.lock.Lock() + if q.repoIDs[id] { + q.lock.Unlock() return } - isShooting = true - defer func() { isShooting = false }() + q.repoIDs[id] = true + q.lock.Unlock() + q.queue <- id +} - tasks := make([]*HookTask, 0, 10) +// AddRepoID adds repository ID to hook delivery queue. +func (q *hookQueue) AddRepoID(id int64) { + go q.addRepoID(id) +} + +var HookQueue *hookQueue + +func deliverHook(t *HookTask) { timeout := time.Duration(setting.Webhook.DeliverTimeout) * time.Second - x.Where("is_delivered=?", false).Iterate(new(HookTask), - func(idx int, bean interface{}) error { - t := bean.(*HookTask) - req := httplib.Post(t.Url).SetTimeout(timeout, timeout). - Header("X-Gogs-Delivery", t.Uuid). - 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) - } + req := httplib.Post(t.Url).SetTimeout(timeout, timeout). + Header("X-Gogs-Delivery", t.Uuid). + Header("X-Gogs-Event", string(t.EventType)). + SetTLSClientConfig(&tls.Config{InsecureSkipVerify: setting.Webhook.SkipTLSVerify}) - t.IsDelivered = true + switch t.ContentType { + case JSON: + req = req.Header("Content-Type", "application/json").Body(t.PayloadContent) + case FORM: + req.Param("payload", t.PayloadContent) + } - // FIXME: record response. - switch t.Type { - case GOGS: - { - if _, err := req.Response(); err != nil { - log.Error(5, "Delivery: %v", err) + t.IsDelivered = true + + // FIXME: record response. + switch t.Type { + case GOGS: + { + if resp, err := req.Response(); err != nil { + log.Error(5, "Delivery: %v", err) + } else { + resp.Body.Close() + t.IsSucceed = true + } + } + case SLACK: + { + if resp, err := req.Response(); err != nil { + log.Error(5, "Delivery: %v", err) + } else { + defer resp.Body.Close() + contents, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Error(5, "%s", err) + } else { + if string(contents) != "ok" { + log.Error(5, "slack failed with: %s", string(contents)) } else { t.IsSucceed = true } } - case SLACK: - { - if res, err := req.Response(); err != nil { - log.Error(5, "Delivery: %v", err) - } else { - defer res.Body.Close() - contents, err := ioutil.ReadAll(res.Body) - if err != nil { - log.Error(5, "%s", err) - } else { - if string(contents) != "ok" { - log.Error(5, "slack failed with: %s", string(contents)) - } else { - t.IsSucceed = true - } - } - } - } } + } + } - tasks = append(tasks, t) + t.Delivered = time.Now().UTC().UnixNano() + if t.IsSucceed { + log.Trace("Hook delivered(%s): %s", t.Uuid, t.PayloadContent) + } +} - if t.IsSucceed { - log.Trace("Hook delivered(%s): %s", t.Uuid, t.PayloadContent) - } +// DeliverHooks checks and delivers undelivered hooks. +func DeliverHooks() { + tasks := make([]*HookTask, 0, 10) + x.Where("is_delivered=?", false).Iterate(new(HookTask), + func(idx int, bean interface{}) error { + t := bean.(*HookTask) + deliverHook(t) + 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) + log.Error(4, "UpdateHookTask(%d): %v", t.ID, err) + } + } + + HookQueue = &hookQueue{ + lock: sync.Mutex{}, + repoIDs: make(map[int64]bool), + queue: make(chan int64, setting.Webhook.QueueLength), + } + + // Start listening on new hook requests. + for repoID := range HookQueue.queue { + HookQueue.removeRepoID(repoID) + + tasks = make([]*HookTask, 0, 5) + if err := x.Where("repo_id=? AND is_delivered=?", repoID, false).Find(&tasks); err != nil { + log.Error(4, "Get repository(%d) hook tasks: %v", repoID, err) + continue + } + for _, t := range tasks { + deliverHook(t) + if err := UpdateHookTask(t); err != nil { + log.Error(4, "UpdateHookTask(%d): %v", t.ID, err) + } } } } + +func InitDeliverHooks() { + go DeliverHooks() +} |