diff options
Diffstat (limited to 'models')
-rw-r--r-- | models/access.go | 8 | ||||
-rw-r--r-- | models/action.go | 157 | ||||
-rw-r--r-- | models/git_diff.go | 36 | ||||
-rw-r--r-- | models/issue.go | 322 | ||||
-rw-r--r-- | models/login.go | 27 | ||||
-rw-r--r-- | models/models.go | 35 | ||||
-rw-r--r-- | models/oauth2.go | 36 | ||||
-rw-r--r-- | models/org.go | 871 | ||||
-rw-r--r-- | models/publickey.go | 122 | ||||
-rw-r--r-- | models/release.go | 2 | ||||
-rw-r--r-- | models/repo.go | 526 | ||||
-rw-r--r-- | models/update.go | 15 | ||||
-rw-r--r-- | models/user.go | 191 | ||||
-rw-r--r-- | models/webhook.go | 41 |
14 files changed, 1977 insertions, 412 deletions
diff --git a/models/access.go b/models/access.go index 5238daba..81aa43dc 100644 --- a/models/access.go +++ b/models/access.go @@ -21,10 +21,10 @@ const ( // Access represents the accessibility of user to repository. type Access struct { Id int64 - UserName string `xorm:"unique(s)"` - RepoName string `xorm:"unique(s)"` // <user name>/<repo name> - Mode AccessType `xorm:"unique(s)"` - Created time.Time `xorm:"created"` + UserName string `xorm:"UNIQUE(s)"` + RepoName string `xorm:"UNIQUE(s)"` // <user name>/<repo name> + Mode AccessType `xorm:"UNIQUE(s)"` + Created time.Time `xorm:"CREATED"` } // AddAccess adds new access record. diff --git a/models/action.go b/models/action.go index 55557da2..b5f692c4 100644 --- a/models/action.go +++ b/models/action.go @@ -8,36 +8,53 @@ import ( "encoding/json" "errors" "fmt" + "path" + "regexp" "strings" "time" - - "github.com/gogits/git" + "unicode" "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/git" "github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/setting" ) -// Operation types of user action. +type ActionType int + const ( - OP_CREATE_REPO = iota + 1 - OP_DELETE_REPO - OP_STAR_REPO - OP_FOLLOW_REPO - OP_COMMIT_REPO - OP_CREATE_ISSUE - OP_PULL_REQUEST - OP_TRANSFER_REPO - OP_PUSH_TAG - OP_COMMENT_ISSUE + CREATE_REPO ActionType = iota + 1 // 1 + DELETE_REPO // 2 + STAR_REPO // 3 + FOLLOW_REPO // 4 + COMMIT_REPO // 5 + CREATE_ISSUE // 6 + PULL_REQUEST // 7 + TRANSFER_REPO // 8 + PUSH_TAG // 9 + COMMENT_ISSUE // 10 +) + +var ( + ErrNotImplemented = errors.New("Not implemented yet") +) + +var ( + // Same as Github. See https://help.github.com/articles/closing-issues-via-commit-messages + IssueKeywords = []string{"close", "closes", "closed", "fix", "fixes", "fixed", "resolve", "resolves", "resolved"} + IssueKeywordsPat *regexp.Regexp ) +func init() { + IssueKeywordsPat = regexp.MustCompile(fmt.Sprintf(`(?i)(?:%s) \S+`, strings.Join(IssueKeywords, "|"))) +} + // Action represents user operation type and other information to repository., // it implemented interface base.Actioner so that can be used in template render. type Action struct { Id int64 UserId int64 // Receiver user id. - OpType int + OpType ActionType ActUserId int64 // Action user id. ActUserName string // Action user name. ActEmail string @@ -51,7 +68,7 @@ type Action struct { } func (a Action) GetOpType() int { - return a.OpType + return int(a.OpType) } func (a Action) GetActUserName() string { @@ -70,6 +87,10 @@ func (a Action) GetRepoName() string { return a.RepoName } +func (a Action) GetRepoLink() string { + return path.Join(a.RepoUserName, a.RepoName) +} + func (a Action) GetBranch() string { return a.RefName } @@ -78,15 +99,85 @@ func (a Action) GetContent() string { return a.Content } +func (a Action) GetCreate() time.Time { + return a.Created +} + +func (a Action) GetIssueInfos() []string { + return strings.SplitN(a.Content, "|", 2) +} + +func updateIssuesCommit(userId, repoId int64, repoUserName, repoName string, commits []*base.PushCommit) error { + for _, c := range commits { + refs := IssueKeywordsPat.FindAllString(c.Message, -1) + + for _, ref := range refs { + ref := ref[strings.IndexByte(ref, byte(' '))+1:] + ref = strings.TrimRightFunc(ref, func(c rune) bool { + return !unicode.IsDigit(c) + }) + + if len(ref) == 0 { + continue + } + + // Add repo name if missing + if ref[0] == '#' { + ref = fmt.Sprintf("%s/%s%s", repoUserName, repoName, ref) + } else if strings.Contains(ref, "/") == false { + // We don't support User#ID syntax yet + // return ErrNotImplemented + + continue + } + + issue, err := GetIssueByRef(ref) + + if err != nil { + return err + } + + url := fmt.Sprintf("/%s/%s/commit/%s", 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, COMMIT, message, nil); err != nil { + return err + } + + if issue.RepoId == repoId { + if issue.IsClosed { + continue + } + + issue.IsClosed = true + + if err = UpdateIssue(issue); err != nil { + return err + } + + if err = ChangeMilestoneIssueStats(issue); err != nil { + return err + } + + // If commit happened in the referenced repository, it means the issue can be closed. + if _, err = CreateComment(userId, repoId, issue.Id, 0, 0, CLOSE, "", nil); err != nil { + return err + } + } + } + } + + return nil +} + // CommitRepoAction adds new action for committing repository. func CommitRepoAction(userId, repoUserId int64, userName, actEmail string, repoId int64, repoUserName, repoName string, refFullName string, commit *base.PushCommits) error { - // log.Trace("action.CommitRepoAction(start): %d/%s", userId, repoName) - opType := OP_COMMIT_REPO + opType := COMMIT_REPO // Check it's tag push or branch. if strings.HasPrefix(refFullName, "refs/tags/") { - opType = OP_PUSH_TAG + opType = PUSH_TAG commit = &base.PushCommits{} } @@ -107,6 +198,12 @@ func CommitRepoAction(userId, repoUserId int64, userName, actEmail string, return errors.New("action.CommitRepoAction(UpdateRepository): " + err.Error()) } + err = updateIssuesCommit(userId, repoId, repoUserName, repoName, commit.Commits) + + if err != nil { + log.Debug("action.CommitRepoAction(updateIssuesCommit): ", err) + } + if err = NotifyWatchers(&Action{ActUserId: userId, ActUserName: userName, ActEmail: actEmail, OpType: opType, Content: string(bs), RepoId: repoId, RepoUserName: repoUserName, RepoName: repoName, RefName: refName, @@ -184,37 +281,35 @@ func CommitRepoAction(userId, repoUserId int64, userName, actEmail string, // NewRepoAction adds new action for creating repository. func NewRepoAction(u *User, repo *Repository) (err error) { if err = NotifyWatchers(&Action{ActUserId: u.Id, ActUserName: u.Name, ActEmail: u.Email, - OpType: OP_CREATE_REPO, RepoId: repo.Id, RepoUserName: repo.Owner.Name, RepoName: repo.Name, + OpType: CREATE_REPO, RepoId: repo.Id, RepoUserName: repo.Owner.Name, RepoName: repo.Name, IsPrivate: repo.IsPrivate}); err != nil { - log.Error("action.NewRepoAction(notify watchers): %d/%s", u.Id, repo.Name) + log.Error(4, "NotifyWatchers: %d/%s", u.Id, repo.Name) return err } - log.Trace("action.NewRepoAction: %s/%s", u.LowerName, repo.LowerName) + log.Trace("action.NewRepoAction: %s/%s", u.Name, repo.Name) return err } // TransferRepoAction adds new action for transfering repository. -func TransferRepoAction(user, newUser *User, repo *Repository) (err error) { - if err = NotifyWatchers(&Action{ActUserId: user.Id, ActUserName: user.Name, ActEmail: user.Email, - OpType: OP_TRANSFER_REPO, RepoId: repo.Id, RepoName: repo.Name, Content: newUser.Name, +func TransferRepoAction(u, newUser *User, repo *Repository) (err error) { + if err = NotifyWatchers(&Action{ActUserId: u.Id, ActUserName: u.Name, ActEmail: u.Email, + OpType: TRANSFER_REPO, RepoId: repo.Id, RepoName: repo.Name, Content: newUser.Name, IsPrivate: repo.IsPrivate}); err != nil { - log.Error("action.TransferRepoAction(notify watchers): %d/%s", user.Id, repo.Name) + log.Error(4, "NotifyWatchers: %d/%s", u.Id, repo.Name) return err } - log.Trace("action.TransferRepoAction: %s/%s", user.LowerName, repo.LowerName) + log.Trace("action.TransferRepoAction: %s/%s", u.Name, repo.Name) return err } // GetFeeds returns action list of given user in given context. -func GetFeeds(userid, offset int64, isProfile bool) ([]*Action, error) { +func GetFeeds(uid, offset int64, isProfile bool) ([]*Action, error) { actions := make([]*Action, 0, 20) - sess := x.Limit(20, int(offset)).Desc("id").Where("user_id=?", userid) + sess := x.Limit(20, int(offset)).Desc("id").Where("user_id=?", uid) if isProfile { - sess.Where("is_private=?", false).And("act_user_id=?", userid) - } else { - sess.And("act_user_id!=?", userid) + sess.Where("is_private=?", false).And("act_user_id=?", uid) } err := sess.Find(&actions) return actions, err diff --git a/models/git_diff.go b/models/git_diff.go index ed114b75..4b4d1234 100644 --- a/models/git_diff.go +++ b/models/git_diff.go @@ -11,10 +11,12 @@ import ( "os" "os/exec" "strings" + "time" + + "github.com/Unknwon/com" "github.com/gogits/git" - "github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/process" ) @@ -117,8 +119,8 @@ func ParsePatch(pid int64, cmd *exec.Cmd, reader io.Reader) (*Diff, error) { // Parse line number. ranges := strings.Split(ss[len(ss)-2][1:], " ") - leftLine, _ = base.StrTo(strings.Split(ranges[0], ",")[0][1:]).Int() - rightLine, _ = base.StrTo(strings.Split(ranges[1], ",")[0]).Int() + leftLine, _ = com.StrTo(strings.Split(ranges[0], ",")[0][1:]).Int() + rightLine, _ = com.StrTo(strings.Split(ranges[1], ",")[0]).Int() continue case line[0] == '+': curFile.Addition++ @@ -170,10 +172,6 @@ func ParsePatch(pid int64, cmd *exec.Cmd, reader io.Reader) (*Diff, error) { } } - // In case process became zombie. - if err := process.Kill(pid); err != nil { - log.Error("git_diff.ParsePatch(Kill): %v", err) - } return diff, nil } @@ -201,10 +199,30 @@ func GetDiff(repoPath, commitid string) (*Diff, error) { cmd.Stdout = wr cmd.Stdin = os.Stdin cmd.Stderr = os.Stderr + + done := make(chan error) go func() { - cmd.Run() + cmd.Start() + done <- cmd.Wait() wr.Close() }() defer rd.Close() - return ParsePatch(process.Add(fmt.Sprintf("GetDiff(%s)", repoPath), cmd), cmd, rd) + + desc := fmt.Sprintf("GetDiff(%s)", repoPath) + pid := process.Add(desc, cmd) + go func() { + // In case process became zombie. + select { + case <-time.After(5 * time.Minute): + if errKill := process.Kill(pid); errKill != nil { + log.Error(4, "git_diff.ParsePatch(Kill): %v", err) + } + <-done + // return "", ErrExecTimeout.Error(), ErrExecTimeout + case err = <-done: + process.Remove(pid) + } + }() + + return ParsePatch(pid, cmd, rd) } diff --git a/models/issue.go b/models/issue.go index 6d67a72b..307ace81 100644 --- a/models/issue.go +++ b/models/issue.go @@ -7,18 +7,26 @@ package models import ( "bytes" "errors" + "html/template" + "os" + "strconv" "strings" "time" + "github.com/Unknwon/com" "github.com/go-xorm/xorm" - "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/log" ) var ( - ErrIssueNotExist = errors.New("Issue does not exist") - ErrLabelNotExist = errors.New("Label does not exist") - ErrMilestoneNotExist = errors.New("Milestone does not exist") + 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") + ErrMissingIssueNumber = errors.New("No issue number specified") ) // Issue represents an issue or pull request of repository. @@ -64,7 +72,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, _ := base.StrTo(strId).Int64() + id, _ := com.StrTo(strId).Int64() if id > 0 { l, err := GetLabelById(id) if err != nil { @@ -90,6 +98,19 @@ func (i *Issue) GetAssignee() (err error) { return err } +func (i *Issue) Attachments() []*Attachment { + a, _ := GetAttachmentsForIssue(i.Id) + return a +} + +func (i *Issue) AfterDelete() { + _, err := DeleteAttachmentsByIssue(i.Id, true) + + if err != nil { + 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() @@ -108,7 +129,40 @@ func NewIssue(issue *Issue) (err error) { sess.Rollback() return err } - return sess.Commit() + + if err = sess.Commit(); err != nil { + return err + } + + if issue.MilestoneId > 0 { + // FIXES(280): Update milestone counter. + return ChangeMilestoneAssign(0, issue.MilestoneId, issue) + } + + return +} + +// GetIssueByRef returns an Issue specified by a GFM reference. +// See https://help.github.com/articles/writing-on-github#references for more information on the syntax. +func GetIssueByRef(ref string) (issue *Issue, err error) { + var issueNumber int64 + var repo *Repository + + n := strings.IndexByte(ref, byte('#')) + + if n == -1 { + return nil, ErrMissingIssueNumber + } + + if issueNumber, err = strconv.ParseInt(ref[n+1:], 10, 64); err != nil { + return + } + + if repo, err = GetRepositoryByRef(ref[:n]); err != nil { + return + } + + return GetIssueByIndex(repo.Id, issueNumber) } // GetIssueByIndex returns issue by given index in repository. @@ -276,14 +330,17 @@ func GetIssueUserPairs(rid, uid int64, isClosed bool) ([]*IssueUser, error) { // GetIssueUserPairsByRepoIds returns issue-user pairs by given repository IDs. func GetIssueUserPairsByRepoIds(rids []int64, isClosed bool, page int) ([]*IssueUser, error) { + if len(rids) == 0 { + return []*IssueUser{}, nil + } + buf := bytes.NewBufferString("") for _, rid := range rids { buf.WriteString("repo_id=") - buf.WriteString(base.ToStr(rid)) + buf.WriteString(com.ToStr(rid)) buf.WriteString(" OR ") } cond := strings.TrimSuffix(buf.String(), " OR ") - ius := make([]*IssueUser, 0, 10) sess := x.Limit(20, (page-1)*20).Where("is_closed=?", isClosed) if len(cond) > 0 { @@ -386,6 +443,11 @@ 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) + + if err != nil { + return err + } + return err } @@ -502,7 +564,7 @@ func UpdateLabel(l *Label) error { // DeleteLabel delete a label of given repository. func DeleteLabel(repoId int64, strId string) error { - id, _ := base.StrTo(strId).Int64() + id, _ := com.StrTo(strId).Int64() l, err := GetLabelById(id) if err != nil { if err == ErrLabelNotExist { @@ -656,6 +718,32 @@ func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) { return sess.Commit() } +// 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 { + return nil + } + + m, err := GetMilestoneById(issue.MilestoneId) + + if err != nil { + return err + } + + if issue.IsClosed { + m.NumOpenIssues-- + m.NumClosedIssues++ + } else { + m.NumOpenIssues++ + m.NumClosedIssues-- + } + + m.Completeness = m.NumClosedIssues * 100 / m.NumIssues + + return UpdateMilestone(m) +} + // ChangeMilestoneAssign changes assignment of milestone for issue. func ChangeMilestoneAssign(oldMid, mid int64, issue *Issue) (err error) { sess := x.NewSession() @@ -679,7 +767,8 @@ func ChangeMilestoneAssign(oldMid, mid int64, issue *Issue) (err error) { } else { m.Completeness = 0 } - if _, err = sess.Id(m.Id).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 } @@ -696,12 +785,18 @@ func ChangeMilestoneAssign(oldMid, mid int64, issue *Issue) (err error) { if err != nil { return err } + m.NumIssues++ if issue.IsClosed { m.NumClosedIssues++ } + + if m.NumIssues == 0 { + return ErrWrongIssueCounter + } + m.Completeness = m.NumClosedIssues * 100 / m.NumIssues - if _, err = sess.Id(m.Id).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 } @@ -712,6 +807,7 @@ func ChangeMilestoneAssign(oldMid, mid int64, issue *Issue) (err error) { return err } } + return sess.Commit() } @@ -755,17 +851,33 @@ func DeleteMilestone(m *Milestone) (err error) { // \______ /\____/|__|_| /__|_| /\___ >___| /__| // \/ \/ \/ \/ \/ -// Issue types. +// CommentType defines whether a comment is just a simple comment, an action (like close) or a reference. +type CommentType int + const ( - IT_PLAIN = iota // Pure comment. - IT_REOPEN // Issue reopen status change prompt. - IT_CLOSE // Issue close status change prompt. + // Plain comment, can be associated with a commit (CommitId > 0) and a line (Line > 0) + COMMENT CommentType = iota + + // Reopen action + REOPEN + + // Close action + CLOSE + + // Reference from another issue + ISSUE + + // Reference from some commit (not part of a pull request) + COMMIT + + // Reference from some pull request + PULL ) // Comment represents a comment in commit and issue page. type Comment struct { Id int64 - Type int + Type CommentType PosterId int64 Poster *User `xorm:"-"` IssueId int64 @@ -776,41 +888,71 @@ type Comment struct { } // CreateComment creates comment of issue or commit. -func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType int, content string) error { +func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType CommentType, content string, attachments []int64) (*Comment, error) { sess := x.NewSession() defer sess.Close() if err := sess.Begin(); err != nil { - return err + return nil, err } - if _, err := sess.Insert(&Comment{PosterId: userId, Type: cmtType, IssueId: issueId, - CommitId: commitId, Line: line, Content: content}); err != nil { + comment := &Comment{PosterId: userId, Type: cmtType, IssueId: issueId, + CommitId: commitId, Line: line, Content: content} + + if _, err := sess.Insert(comment); err != nil { sess.Rollback() - return err + return nil, err } // Check comment type. switch cmtType { - case IT_PLAIN: + case COMMENT: rawSql := "UPDATE `issue` SET num_comments = num_comments + 1 WHERE id = ?" if _, err := sess.Exec(rawSql, issueId); err != nil { sess.Rollback() - return err + return nil, err } - case IT_REOPEN: + + if len(attachments) > 0 { + rawSql = "UPDATE `attachment` SET comment_id = ? WHERE id IN (?)" + + astrs := make([]string, 0, len(attachments)) + + for _, a := range attachments { + astrs = append(astrs, strconv.FormatInt(a, 10)) + } + + if _, err := sess.Exec(rawSql, comment.Id, strings.Join(astrs, ",")); err != nil { + sess.Rollback() + return nil, err + } + } + case 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 err + return nil, err } - case IT_CLOSE: + case 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 err + return nil, err } } - return sess.Commit() + + return comment, sess.Commit() +} + +// GetCommentById returns the comment with the given id +func GetCommentById(commentId int64) (*Comment, error) { + c := &Comment{Id: commentId} + _, err := x.Get(c) + + return c, err +} + +func (c *Comment) ContentHtml() template.HTML { + return template.HTML(c.Content) } // GetIssueComments returns list of comment by given issue id. @@ -819,3 +961,127 @@ func GetIssueComments(issueId int64) ([]Comment, error) { err := x.Asc("created").Find(&comments, &Comment{IssueId: issueId}) return comments, err } + +// Attachments returns the attachments for this comment. +func (c *Comment) Attachments() []*Attachment { + a, _ := GetAttachmentsByComment(c.Id) + return a +} + +func (c *Comment) AfterDelete() { + _, err := DeleteAttachmentsByComment(c.Id, true) + + if err != nil { + log.Info("Could not delete files for comment %d on issue #%d: %s", c.Id, c.IssueId, err) + } +} + +type Attachment struct { + Id int64 + IssueId int64 + CommentId int64 + Name string + Path string `xorm:"TEXT"` + Created time.Time `xorm:"CREATED"` +} + +// CreateAttachment creates a new attachment inside the database and +func CreateAttachment(issueId, commentId int64, name, path string) (*Attachment, error) { + sess := x.NewSession() + defer sess.Close() + + if err := sess.Begin(); err != nil { + return nil, err + } + + a := &Attachment{IssueId: issueId, CommentId: commentId, Name: name, Path: path} + + if _, err := sess.Insert(a); err != nil { + sess.Rollback() + return nil, err + } + + return a, sess.Commit() +} + +// Attachment returns the attachment by given ID. +func GetAttachmentById(id int64) (*Attachment, error) { + m := &Attachment{Id: id} + + has, err := x.Get(m) + + if err != nil { + return nil, err + } + + if !has { + return nil, ErrAttachmentNotExist + } + + return m, nil +} + +func GetAttachmentsForIssue(issueId int64) ([]*Attachment, error) { + attachments := make([]*Attachment, 0, 10) + err := x.Where("issue_id = ?", issueId).And("comment_id = 0").Find(&attachments) + return attachments, err +} + +// GetAttachmentsByIssue returns a list of attachments for the given issue +func GetAttachmentsByIssue(issueId int64) ([]*Attachment, error) { + attachments := make([]*Attachment, 0, 10) + err := x.Where("issue_id = ?", issueId).And("comment_id > 0").Find(&attachments) + return attachments, err +} + +// GetAttachmentsByComment returns a list of attachments for the given comment +func GetAttachmentsByComment(commentId int64) ([]*Attachment, error) { + attachments := make([]*Attachment, 0, 10) + err := x.Where("comment_id = ?", commentId).Find(&attachments) + return attachments, err +} + +// DeleteAttachment deletes the given attachment and optionally the associated file. +func DeleteAttachment(a *Attachment, remove bool) error { + _, err := DeleteAttachments([]*Attachment{a}, remove) + return err +} + +// DeleteAttachments deletes the given attachments and optionally the associated files. +func DeleteAttachments(attachments []*Attachment, remove bool) (int, error) { + for i, a := range attachments { + if remove { + if err := os.Remove(a.Path); err != nil { + return i, err + } + } + + if _, err := x.Delete(a.Id); err != nil { + return i, err + } + } + + return len(attachments), nil +} + +// DeleteAttachmentsByIssue deletes all attachments associated with the given issue. +func DeleteAttachmentsByIssue(issueId int64, remove bool) (int, error) { + attachments, err := GetAttachmentsByIssue(issueId) + + if err != nil { + return 0, err + } + + return DeleteAttachments(attachments, remove) +} + +// DeleteAttachmentsByComment deletes all attachments associated with the given comment. +func DeleteAttachmentsByComment(commentId int64, remove bool) (int, error) { + attachments, err := GetAttachmentsByComment(commentId) + + if err != nil { + return 0, err + } + + return DeleteAttachments(attachments, remove) +} diff --git a/models/login.go b/models/login.go index e99b61e7..da7722f2 100644 --- a/models/login.go +++ b/models/login.go @@ -164,15 +164,14 @@ func UserSignIn(uname, passwd string) (*User, error) { if u.LoginType == NOTYPE { if has { u.LoginType = PLAIN + } else { + return nil, ErrUserNotExist } } - // for plain login, user must have existed. + // For plain login, user must exist to reach this line. + // Now verify password. if u.LoginType == PLAIN { - if !has { - return nil, ErrUserNotExist - } - newUser := &User{Passwd: passwd, Salt: u.Salt} newUser.EncodePasswd() if u.Passwd != newUser.Passwd { @@ -233,18 +232,18 @@ func UserSignIn(uname, passwd string) (*User, error) { // Query if name/passwd can login against the LDAP direcotry pool // Create a local user if success // Return the same LoginUserPlain semantic -func LoginUserLdapSource(user *User, name, passwd string, sourceId int64, cfg *LDAPConfig, autoRegister bool) (*User, error) { +func LoginUserLdapSource(u *User, name, passwd string, sourceId int64, cfg *LDAPConfig, autoRegister bool) (*User, error) { mail, logged := cfg.Ldapsource.SearchEntry(name, passwd) if !logged { // user not in LDAP, do nothing return nil, ErrUserNotExist } if !autoRegister { - return user, nil + return u, nil } // fake a local user creation - user = &User{ + u = &User{ LowerName: strings.ToLower(name), Name: strings.ToLower(name), LoginType: LDAP, @@ -255,7 +254,8 @@ func LoginUserLdapSource(user *User, name, passwd string, sourceId int64, cfg *L Email: mail, } - return CreateUser(user) + err := CreateUser(u) + return u, err } type loginAuth struct { @@ -322,7 +322,7 @@ func SmtpAuth(host string, port int, a smtp.Auth, useTls bool) error { // Query if name/passwd can login against the LDAP direcotry pool // Create a local user if success // Return the same LoginUserPlain semantic -func LoginUserSMTPSource(user *User, name, passwd string, sourceId int64, cfg *SMTPConfig, autoRegister bool) (*User, error) { +func LoginUserSMTPSource(u *User, name, passwd string, sourceId int64, cfg *SMTPConfig, autoRegister bool) (*User, error) { var auth smtp.Auth if cfg.Auth == SMTP_PLAIN { auth = smtp.PlainAuth("", name, passwd, cfg.Host) @@ -340,7 +340,7 @@ func LoginUserSMTPSource(user *User, name, passwd string, sourceId int64, cfg *S } if !autoRegister { - return user, nil + return u, nil } var loginName = name @@ -349,7 +349,7 @@ func LoginUserSMTPSource(user *User, name, passwd string, sourceId int64, cfg *S loginName = name[:idx] } // fake a local user creation - user = &User{ + u = &User{ LowerName: strings.ToLower(loginName), Name: strings.ToLower(loginName), LoginType: SMTP, @@ -359,5 +359,6 @@ func LoginUserSMTPSource(user *User, name, passwd string, sourceId int64, cfg *S Passwd: passwd, Email: name, } - return CreateUser(user) + err := CreateUser(u) + return u, err } diff --git a/models/models.go b/models/models.go index 070784f1..4e2e08cf 100644 --- a/models/models.go +++ b/models/models.go @@ -32,11 +32,12 @@ var ( ) func init() { - tables = append(tables, new(User), new(PublicKey), new(Repository), new(Watch), - new(Action), new(Access), new(Issue), new(Comment), new(Oauth2), new(Follow), + tables = append(tables, new(User), new(PublicKey), + new(Repository), new(Watch), new(Star), new(Action), new(Access), + new(Issue), new(Comment), new(Oauth2), new(Follow), new(Mirror), new(Release), new(LoginSource), new(Webhook), new(IssueUser), new(Milestone), new(Label), new(HookTask), new(Team), new(OrgUser), new(TeamUser), - new(UpdateTask)) + new(UpdateTask), new(Attachment)) } func LoadModelsConfig() { @@ -126,6 +127,7 @@ func SetEngine() (err error) { x.ShowSQL = true x.ShowDebug = true x.ShowErr = true + x.ShowWarn = true return nil } @@ -141,30 +143,45 @@ func NewEngine() (err error) { type Statistic struct { Counter struct { - User, PublicKey, Repo, Watch, Action, Access, - Issue, Comment, Mirror, Oauth, Release, - LoginSource, Webhook, Milestone int64 + User, Org, PublicKey, + Repo, Watch, Star, Action, Access, + Issue, Comment, Oauth, Follow, + Mirror, Release, LoginSource, Webhook, + Milestone, Label, HookTask, + Team, UpdateTask, Attachment int64 } } func GetStatistic() (stats Statistic) { - stats.Counter.User, _ = x.Count(new(User)) + stats.Counter.User = CountUsers() + stats.Counter.Org = CountOrganizations() stats.Counter.PublicKey, _ = x.Count(new(PublicKey)) - stats.Counter.Repo, _ = x.Count(new(Repository)) + stats.Counter.Repo = CountRepositories() stats.Counter.Watch, _ = x.Count(new(Watch)) + stats.Counter.Star, _ = x.Count(new(Star)) stats.Counter.Action, _ = x.Count(new(Action)) stats.Counter.Access, _ = x.Count(new(Access)) stats.Counter.Issue, _ = x.Count(new(Issue)) stats.Counter.Comment, _ = x.Count(new(Comment)) - stats.Counter.Mirror, _ = x.Count(new(Mirror)) stats.Counter.Oauth, _ = x.Count(new(Oauth2)) + stats.Counter.Follow, _ = x.Count(new(Follow)) + stats.Counter.Mirror, _ = x.Count(new(Mirror)) stats.Counter.Release, _ = x.Count(new(Release)) stats.Counter.LoginSource, _ = x.Count(new(LoginSource)) stats.Counter.Webhook, _ = x.Count(new(Webhook)) stats.Counter.Milestone, _ = x.Count(new(Milestone)) + stats.Counter.Label, _ = x.Count(new(Label)) + stats.Counter.HookTask, _ = x.Count(new(HookTask)) + stats.Counter.Team, _ = x.Count(new(Team)) + stats.Counter.UpdateTask, _ = x.Count(new(UpdateTask)) + stats.Counter.Attachment, _ = x.Count(new(Attachment)) return } +func Ping() error { + return x.Ping() +} + // DumpDatabase dumps all data from database to file system. func DumpDatabase(filePath string) error { return x.DumpAllToFile(filePath) diff --git a/models/oauth2.go b/models/oauth2.go index 4b024a26..46e8e492 100644 --- a/models/oauth2.go +++ b/models/oauth2.go @@ -6,6 +6,7 @@ package models import ( "errors" + "time" ) type OauthType int @@ -26,12 +27,15 @@ var ( ) type Oauth2 struct { - Id int64 - Uid int64 `xorm:"unique(s)"` // userId - User *User `xorm:"-"` - Type int `xorm:"unique(s) unique(oauth)"` // twitter,github,google... - Identity string `xorm:"unique(s) unique(oauth)"` // id.. - Token string `xorm:"TEXT not null"` + Id int64 + Uid int64 `xorm:"unique(s)"` // userId + User *User `xorm:"-"` + Type int `xorm:"unique(s) unique(oauth)"` // twitter,github,google... + Identity string `xorm:"unique(s) unique(oauth)"` // id.. + Token string `xorm:"TEXT not null"` + Created time.Time `xorm:"CREATED"` + Updated time.Time + HasRecentActivity bool `xorm:"-"` } func BindUserOauth2(userId, oauthId int64) error { @@ -69,10 +73,24 @@ func GetOauth2ById(id int64) (oa *Oauth2, err error) { return oa, nil } +// UpdateOauth2 updates given OAuth2. +func UpdateOauth2(oa *Oauth2) error { + _, err := x.Id(oa.Id).AllCols().Update(oa) + return err +} + // GetOauthByUserId returns list of oauthes that are releated to given user. -func GetOauthByUserId(uid int64) (oas []*Oauth2, err error) { - err = x.Find(&oas, Oauth2{Uid: uid}) - return oas, err +func GetOauthByUserId(uid int64) ([]*Oauth2, error) { + socials := make([]*Oauth2, 0, 5) + err := x.Find(&socials, Oauth2{Uid: uid}) + if err != nil { + return nil, err + } + + for _, social := range socials { + social.HasRecentActivity = social.Updated.Add(7 * 24 * time.Hour).After(time.Now()) + } + return socials, err } // DeleteOauth2ById deletes a oauth2 by ID. diff --git a/models/org.go b/models/org.go index 2625ed42..ce506705 100644 --- a/models/org.go +++ b/models/org.go @@ -6,14 +6,23 @@ package models import ( "errors" + "fmt" + "os" + "path" "strings" + "github.com/Unknwon/com" + "github.com/go-xorm/xorm" + "github.com/gogits/gogs/modules/base" ) var ( ErrOrgNotExist = errors.New("Organization does not exist") ErrTeamAlreadyExist = errors.New("Team already exist") + ErrTeamNotExist = errors.New("Team does not exist") + ErrTeamNameIllegal = errors.New("Team name contains illegal characters") + ErrLastOrgOwner = errors.New("The user to remove is the last member in owner team") ) // IsOrgOwner returns true if given user is in the owner team. @@ -26,14 +35,14 @@ func (org *User) IsOrgMember(uid int64) bool { return IsOrganizationMember(org.Id, uid) } +// GetTeam returns named team of organization. +func (org *User) GetTeam(name string) (*Team, error) { + return GetTeam(org.Id, name) +} + // GetOwnerTeam returns owner team of organization. func (org *User) GetOwnerTeam() (*Team, error) { - t := &Team{ - OrgId: org.Id, - Name: OWNER_TEAM, - } - _, err := x.Get(t) - return t, err + return org.GetTeam(OWNER_TEAM) } // GetTeams returns all teams that belong to organization. @@ -58,6 +67,16 @@ func (org *User) GetMembers() error { return nil } +// AddMember adds new member to organization. +func (org *User) AddMember(uid int64) error { + return AddOrgUser(org.Id, uid) +} + +// RemoveMember removes member from organization. +func (org *User) RemoveMember(uid int64) error { + return RemoveOrgUser(org.Id, uid) +} + // CreateOrganization creates record of a new organization. func CreateOrganization(org, owner *User) (*User, error) { if !IsLegalName(org.Name) { @@ -97,9 +116,15 @@ func CreateOrganization(org, owner *User) (*User, error) { return nil, err } + if err = os.MkdirAll(UserPath(org.Name), os.ModePerm); err != nil { + sess.Rollback() + return nil, err + } + // Create default owner team. t := &Team{ OrgId: org.Id, + LowerName: strings.ToLower(OWNER_TEAM), Name: OWNER_TEAM, Authorize: ORG_ADMIN, NumMembers: 1, @@ -111,10 +136,10 @@ func CreateOrganization(org, owner *User) (*User, error) { // Add initial creator to organization and owner team. ou := &OrgUser{ - Uid: owner.Id, - OrgId: org.Id, - IsOwner: true, - NumTeam: 1, + Uid: owner.Id, + OrgId: org.Id, + IsOwner: true, + NumTeams: 1, } if _, err = sess.Insert(ou); err != nil { sess.Rollback() @@ -134,6 +159,19 @@ func CreateOrganization(org, owner *User) (*User, error) { return org, sess.Commit() } +// CountOrganizations returns number of organizations. +func CountOrganizations() int64 { + count, _ := x.Where("type=1").Count(new(User)) + return count +} + +// GetOrganizations returns given number of organizations with offset. +func GetOrganizations(num, offset int) ([]*User, error) { + orgs := make([]*User, 0, num) + err := x.Limit(num, offset).Where("type=1").Asc("id").Find(&orgs) + return orgs, err +} + // TODO: need some kind of mechanism to record failure. // DeleteOrganization completely and permanently deletes everything of organization. func DeleteOrganization(org *User) (err error) { @@ -162,6 +200,176 @@ func DeleteOrganization(org *User) (err error) { return sess.Commit() } +// ________ ____ ___ +// \_____ \_______ ____ | | \______ ___________ +// / | \_ __ \/ ___\| | / ___// __ \_ __ \ +// / | \ | \/ /_/ > | /\___ \\ ___/| | \/ +// \_______ /__| \___ /|______//____ >\___ >__| +// \/ /_____/ \/ \/ + +// OrgUser represents an organization-user relation. +type OrgUser struct { + Id int64 + Uid int64 `xorm:"INDEX UNIQUE(s)"` + OrgId int64 `xorm:"INDEX UNIQUE(s)"` + IsPublic bool + IsOwner bool + NumTeams int +} + +// IsOrganizationOwner returns true if given user is in the owner team. +func IsOrganizationOwner(orgId, uid int64) bool { + has, _ := x.Where("is_owner=?", true).And("uid=?", uid).And("org_id=?", orgId).Get(new(OrgUser)) + return has +} + +// IsOrganizationMember returns true if given user is member of organization. +func IsOrganizationMember(orgId, uid int64) bool { + has, _ := x.Where("uid=?", uid).And("org_id=?", orgId).Get(new(OrgUser)) + return has +} + +// IsPublicMembership returns ture if given user public his/her membership. +func IsPublicMembership(orgId, uid int64) bool { + has, _ := x.Where("uid=?", uid).And("org_id=?", orgId).And("is_public=?", true).Get(new(OrgUser)) + return has +} + +// GetOrgUsersByUserId returns all organization-user relations by user ID. +func GetOrgUsersByUserId(uid int64) ([]*OrgUser, error) { + ous := make([]*OrgUser, 0, 10) + err := x.Where("uid=?", uid).Find(&ous) + return ous, err +} + +// GetOrgUsersByOrgId returns all organization-user relations by organization ID. +func GetOrgUsersByOrgId(orgId int64) ([]*OrgUser, error) { + ous := make([]*OrgUser, 0, 10) + err := x.Where("org_id=?", orgId).Find(&ous) + return ous, err +} + +// ChangeOrgUserStatus changes public or private membership status. +func ChangeOrgUserStatus(orgId, uid int64, public bool) error { + ou := new(OrgUser) + has, err := x.Where("uid=?", uid).And("org_id=?", orgId).Get(ou) + if err != nil { + return err + } else if !has { + return nil + } + + ou.IsPublic = public + _, err = x.Id(ou.Id).AllCols().Update(ou) + return err +} + +// AddOrgUser adds new user to given organization. +func AddOrgUser(orgId, uid int64) error { + if IsOrganizationMember(orgId, uid) { + return nil + } + + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return err + } + + ou := &OrgUser{ + Uid: uid, + OrgId: orgId, + } + + if _, err := sess.Insert(ou); err != nil { + sess.Rollback() + return err + } else if _, err = sess.Exec("UPDATE `user` SET num_members = num_members + 1 WHERE id = ?", orgId); err != nil { + sess.Rollback() + return err + } + + return sess.Commit() +} + +// RemoveOrgUser removes user from given organization. +func RemoveOrgUser(orgId, uid int64) error { + ou := new(OrgUser) + + has, err := x.Where("uid=?", uid).And("org_id=?", orgId).Get(ou) + if err != nil { + return err + } else if !has { + return nil + } + + u, err := GetUserById(uid) + if err != nil { + return err + } + org, err := GetUserById(orgId) + if err != nil { + return err + } + + // Check if the user to delete is the last member in owner team. + if IsOrganizationOwner(orgId, uid) { + t, err := org.GetOwnerTeam() + if err != nil { + return err + } + if t.NumMembers == 1 { + return ErrLastOrgOwner + } + } + + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return err + } + + if _, err := sess.Id(ou.Id).Delete(ou); err != nil { + sess.Rollback() + return err + } else if _, err = sess.Exec("UPDATE `user` SET num_members = num_members - 1 WHERE id = ?", orgId); err != nil { + sess.Rollback() + return err + } + + // Delete all repository accesses. + if err = org.GetRepositories(); err != nil { + sess.Rollback() + return err + } + access := &Access{ + UserName: u.LowerName, + } + for _, repo := range org.Repos { + access.RepoName = path.Join(org.LowerName, repo.LowerName) + if _, err = sess.Delete(access); err != nil { + sess.Rollback() + return err + } else if err = WatchRepo(u.Id, repo.Id, false); err != nil { + sess.Rollback() + return err + } + } + + // Delete member in his/her teams. + ts, err := GetUserTeams(org.Id, u.Id) + if err != nil { + return err + } + for _, t := range ts { + if err = removeTeamMemberWithSess(org.Id, t.Id, u.Id, sess); err != nil { + return err + } + } + + return sess.Commit() +} + // ___________ // \__ ___/___ _____ _____ // | |_/ __ \\__ \ / \ @@ -177,6 +385,13 @@ const ( ORG_ADMIN ) +func AuthorizeToAccessType(auth AuthorizeType) AccessType { + if auth == ORG_READABLE { + return READABLE + } + return WRITABLE +} + const OWNER_TEAM = "Owners" // Team represents a organization team. @@ -187,10 +402,16 @@ type Team struct { Name string Description string Authorize AuthorizeType - RepoIds string `xorm:"TEXT"` - NumMembers int + RepoIds string `xorm:"TEXT"` + Repos []*Repository `xorm:"-"` + Members []*User `xorm:"-"` NumRepos int - Members []*User `xorm:"-"` + NumMembers int +} + +// IsOwnerTeam returns true if team is owner team. +func (t *Team) IsOwnerTeam() bool { + return t.Name == OWNER_TEAM } // IsTeamMember returns true if given user is a member of team. @@ -198,15 +419,193 @@ func (t *Team) IsMember(uid int64) bool { return IsTeamMember(t.OrgId, t.Id, uid) } -// GetMembers returns all members in given team of organization. +// GetRepositories returns all repositories in team of organization. +func (t *Team) GetRepositories() error { + idStrs := strings.Split(t.RepoIds, "|") + t.Repos = make([]*Repository, 0, len(idStrs)) + for _, str := range idStrs { + if len(str) == 0 { + continue + } + id := com.StrTo(str[1:]).MustInt64() + if id == 0 { + continue + } + repo, err := GetRepositoryById(id) + if err != nil { + return err + } + t.Repos = append(t.Repos, repo) + } + return nil +} + +// GetMembers returns all members in team of organization. func (t *Team) GetMembers() (err error) { t.Members, err = GetTeamMembers(t.OrgId, t.Id) return err } +// AddMember adds new member to team of organization. +func (t *Team) AddMember(uid int64) error { + return AddTeamMember(t.OrgId, t.Id, uid) +} + +// RemoveMember removes member from team of organization. +func (t *Team) RemoveMember(uid int64) error { + return RemoveTeamMember(t.OrgId, t.Id, uid) +} + +// addAccessWithAuthorize inserts or updates access with given mode. +func addAccessWithAuthorize(sess *xorm.Session, access *Access, mode AccessType) error { + has, err := x.Get(access) + if err != nil { + return fmt.Errorf("fail to get access: %v", err) + } + access.Mode = mode + if has { + if _, err = sess.Id(access.Id).Update(access); err != nil { + return fmt.Errorf("fail to update access: %v", err) + } + } else { + if _, err = sess.Insert(access); err != nil { + return fmt.Errorf("fail to insert access: %v", err) + } + } + return nil +} + +// AddRepository adds new repository to team of organization. +func (t *Team) AddRepository(repo *Repository) (err error) { + idStr := "$" + com.ToStr(repo.Id) + "|" + if repo.OwnerId != t.OrgId { + return errors.New("Repository not belong to organization") + } else if strings.Contains(t.RepoIds, idStr) { + return nil + } + + if err = repo.GetOwner(); err != nil { + return err + } else if err = t.GetMembers(); err != nil { + return err + } + + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return err + } + + t.NumRepos++ + t.RepoIds += idStr + if _, err = sess.Id(t.Id).AllCols().Update(t); err != nil { + sess.Rollback() + return err + } + + // Give access to team members. + mode := AuthorizeToAccessType(t.Authorize) + + for _, u := range t.Members { + auth, err := GetHighestAuthorize(t.OrgId, u.Id, t.Id, repo.Id) + if err != nil { + sess.Rollback() + return err + } + + access := &Access{ + UserName: u.LowerName, + RepoName: path.Join(repo.Owner.LowerName, repo.LowerName), + } + if auth == 0 { + access.Mode = mode + if _, err = sess.Insert(access); err != nil { + sess.Rollback() + return fmt.Errorf("fail to insert access: %v", err) + } + } else if auth < t.Authorize { + if err = addAccessWithAuthorize(sess, access, mode); err != nil { + sess.Rollback() + return err + } + } + if err = WatchRepo(u.Id, repo.Id, true); err != nil { + sess.Rollback() + return err + } + } + return sess.Commit() +} + +// RemoveRepository removes repository from team of organization. +func (t *Team) RemoveRepository(repoId int64) error { + idStr := "$" + com.ToStr(repoId) + "|" + if !strings.Contains(t.RepoIds, idStr) { + return nil + } + + repo, err := GetRepositoryById(repoId) + if err != nil { + return err + } + + if err = repo.GetOwner(); err != nil { + return err + } else if err = t.GetMembers(); err != nil { + return err + } + + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return err + } + + t.NumRepos-- + t.RepoIds = strings.Replace(t.RepoIds, idStr, "", 1) + if _, err = sess.Id(t.Id).AllCols().Update(t); err != nil { + sess.Rollback() + return err + } + + // Remove access to team members. + for _, u := range t.Members { + auth, err := GetHighestAuthorize(t.OrgId, u.Id, t.Id, repo.Id) + if err != nil { + sess.Rollback() + return err + } + + access := &Access{ + UserName: u.LowerName, + RepoName: path.Join(repo.Owner.LowerName, repo.LowerName), + } + if auth == 0 { + if _, err = sess.Delete(access); err != nil { + sess.Rollback() + return fmt.Errorf("fail to delete access: %v", err) + } else if err = WatchRepo(u.Id, repo.Id, false); err != nil { + sess.Rollback() + return err + } + } else if auth < t.Authorize { + if err = addAccessWithAuthorize(sess, access, AuthorizeToAccessType(auth)); err != nil { + sess.Rollback() + return err + } + } + } + + return sess.Commit() +} + // 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 + } + has, err := x.Id(t.OrgId).Get(new(User)) if err != nil { return err @@ -234,66 +633,198 @@ func NewTeam(t *Team) error { } // Update organization number of teams. - rawSql := "UPDATE `user` SET num_teams = num_teams + 1 WHERE id = ?" - if _, err = sess.Exec(rawSql, t.OrgId); err != nil { + if _, err = sess.Exec("UPDATE `user` SET num_teams = num_teams + 1 WHERE id = ?", t.OrgId); err != nil { sess.Rollback() return err } return sess.Commit() } +// GetTeam returns team by given team name and organization. +func GetTeam(orgId int64, name string) (*Team, error) { + t := &Team{ + OrgId: orgId, + LowerName: strings.ToLower(name), + } + has, err := x.Get(t) + if err != nil { + return nil, err + } else if !has { + return nil, ErrTeamNotExist + } + return t, nil +} + +// GetTeamById returns team by given ID. +func GetTeamById(teamId int64) (*Team, error) { + t := new(Team) + has, err := x.Id(teamId).Get(t) + if err != nil { + return nil, err + } else if !has { + return nil, ErrTeamNotExist + } + return t, nil +} + +// GetHighestAuthorize returns highest repository authorize level for given user and team. +func GetHighestAuthorize(orgId, uid, teamId, repoId int64) (AuthorizeType, error) { + ts, err := GetUserTeams(orgId, uid) + if err != nil { + return 0, err + } + + var auth AuthorizeType = 0 + for _, t := range ts { + // Not current team and has given repository. + if t.Id != teamId && strings.Contains(t.RepoIds, "$"+com.ToStr(repoId)+"|") { + // Fast return. + if t.Authorize == ORG_WRITABLE { + return ORG_WRITABLE, nil + } + if t.Authorize > auth { + auth = t.Authorize + } + } + } + return auth, nil +} + // UpdateTeam updates information of team. -func UpdateTeam(t *Team) error { +func UpdateTeam(t *Team, authChanged bool) (err error) { + if !IsLegalName(t.Name) { + return ErrTeamNameIllegal + } + if len(t.Description) > 255 { t.Description = t.Description[:255] } + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return err + } + + // Update access for team members if needed. + if authChanged && !t.IsOwnerTeam() { + if err = t.GetRepositories(); err != nil { + return err + } else if err = t.GetMembers(); err != nil { + return err + } + + // Get organization. + org, err := GetUserById(t.OrgId) + if err != nil { + return err + } + + // Update access. + mode := AuthorizeToAccessType(t.Authorize) + + for _, repo := range t.Repos { + for _, u := range t.Members { + // ORG_WRITABLE is the highest authorize level for now. + // Skip checking others if current team has this level. + if t.Authorize < ORG_WRITABLE { + auth, err := GetHighestAuthorize(org.Id, u.Id, t.Id, repo.Id) + if err != nil { + sess.Rollback() + return err + } + if auth >= t.Authorize { + continue // Other team has higher or same authorize level. + } + } + + access := &Access{ + UserName: u.LowerName, + RepoName: path.Join(org.LowerName, repo.LowerName), + } + if err = addAccessWithAuthorize(sess, access, mode); err != nil { + sess.Rollback() + return err + } + } + } + } + t.LowerName = strings.ToLower(t.Name) - _, err := x.Id(t.Id).AllCols().Update(t) - return err + if _, err = sess.Id(t.Id).AllCols().Update(t); err != nil { + sess.Rollback() + return err + } + return sess.Commit() } -// ________ ____ ___ -// \_____ \_______ ____ | | \______ ___________ -// / | \_ __ \/ ___\| | / ___// __ \_ __ \ -// / | \ | \/ /_/ > | /\___ \\ ___/| | \/ -// \_______ /__| \___ /|______//____ >\___ >__| -// \/ /_____/ \/ \/ +// DeleteTeam deletes given team. +// It's caller's responsibility to assign organization ID. +func DeleteTeam(t *Team) error { + if err := t.GetRepositories(); err != nil { + return err + } else if err = t.GetMembers(); err != nil { + return err + } -// OrgUser represents an organization-user relation. -type OrgUser struct { - Id int64 - Uid int64 `xorm:"INDEX"` - OrgId int64 `xorm:"INDEX"` - IsPublic bool - IsOwner bool - NumTeam int -} + // Get organization. + org, err := GetUserById(t.OrgId) + if err != nil { + return err + } -// IsOrganizationOwner returns true if given user is in the owner team. -func IsOrganizationOwner(orgId, uid int64) bool { - has, _ := x.Where("is_owner=?", true).And("uid=?", uid).And("org_id=?", orgId).Get(new(OrgUser)) - return has -} + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return err + } -// IsOrganizationMember returns true if given user is member of organization. -func IsOrganizationMember(orgId, uid int64) bool { - has, _ := x.Where("uid=?", uid).And("org_id=?", orgId).Get(new(OrgUser)) - return has -} + // Delete all accesses. + for _, repo := range t.Repos { + for _, u := range t.Members { + auth, err := GetHighestAuthorize(org.Id, u.Id, t.Id, repo.Id) + if err != nil { + sess.Rollback() + return err + } -// GetOrgUsersByUserId returns all organization-user relations by user ID. -func GetOrgUsersByUserId(uid int64) ([]*OrgUser, error) { - ous := make([]*OrgUser, 0, 10) - err := x.Where("uid=?", uid).Find(&ous) - return ous, err -} + access := &Access{ + UserName: u.LowerName, + RepoName: path.Join(org.LowerName, repo.LowerName), + } + if auth == 0 { + if _, err = sess.Delete(access); err != nil { + sess.Rollback() + return fmt.Errorf("fail to delete access: %v", err) + } + } else if auth < t.Authorize { + // Downgrade authorize level. + if err = addAccessWithAuthorize(sess, access, AuthorizeToAccessType(auth)); err != nil { + sess.Rollback() + return err + } + } + } + } -// GetOrgUsersByOrgId returns all organization-user relations by organization ID. -func GetOrgUsersByOrgId(orgId int64) ([]*OrgUser, error) { - ous := make([]*OrgUser, 0, 10) - err := x.Where("org_id=?", orgId).Find(&ous) - return ous, err + // Delete team-user. + if _, err = sess.Where("org_id=?", org.Id).Where("team_id=?", t.Id).Delete(new(TeamUser)); err != nil { + sess.Rollback() + return err + } + + // Delete team. + if _, err = sess.Id(t.Id).Delete(new(Team)); err != nil { + sess.Rollback() + return err + } + // Update organization number of teams. + if _, err = sess.Exec("UPDATE `user` SET num_teams = num_teams - 1 WHERE id = ?", t.OrgId); err != nil { + sess.Rollback() + return err + } + + return sess.Commit() } // ___________ ____ ___ @@ -334,3 +865,233 @@ func GetTeamMembers(orgId, teamId int64) ([]*User, error) { } return us, nil } + +// GetUserTeams returns all teams that user belongs to in given origanization. +func GetUserTeams(orgId, uid int64) ([]*Team, error) { + tus := make([]*TeamUser, 0, 5) + if err := x.Where("uid=?", uid).And("org_id=?", orgId).Find(&tus); err != nil { + return nil, err + } + + ts := make([]*Team, len(tus)) + for i, tu := range tus { + t := new(Team) + has, err := x.Id(tu.TeamId).Get(t) + if err != nil { + return nil, err + } else if !has { + return nil, ErrTeamNotExist + } + ts[i] = t + } + return ts, nil +} + +// AddTeamMember adds new member to given team of given organization. +func AddTeamMember(orgId, teamId, uid int64) error { + if IsTeamMember(orgId, teamId, uid) { + return nil + } + + if err := AddOrgUser(orgId, uid); err != nil { + return err + } + + // Get team and its repositories. + t, err := GetTeamById(teamId) + if err != nil { + return err + } + t.NumMembers++ + + if err = t.GetRepositories(); err != nil { + return err + } + + // Get organization. + org, err := GetUserById(orgId) + if err != nil { + return err + } + + // Get user. + u, err := GetUserById(uid) + if err != nil { + return err + } + + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return err + } + + tu := &TeamUser{ + Uid: uid, + OrgId: orgId, + TeamId: teamId, + } + + if _, err = sess.Insert(tu); err != nil { + sess.Rollback() + return err + } else if _, err = sess.Id(t.Id).Update(t); err != nil { + sess.Rollback() + return err + } + + // Give access to team repositories. + mode := AuthorizeToAccessType(t.Authorize) + for _, repo := range t.Repos { + auth, err := GetHighestAuthorize(orgId, uid, teamId, repo.Id) + if err != nil { + sess.Rollback() + return err + } + + access := &Access{ + UserName: u.LowerName, + RepoName: path.Join(org.LowerName, repo.LowerName), + } + // Equal 0 means given access doesn't exist. + if auth == 0 { + access.Mode = mode + if _, err = sess.Insert(access); err != nil { + sess.Rollback() + return fmt.Errorf("fail to insert access: %v", err) + } + } else if auth < t.Authorize { + if err = addAccessWithAuthorize(sess, access, mode); err != nil { + sess.Rollback() + return err + } + } + } + + // We make sure it exists before. + ou := new(OrgUser) + _, err = sess.Where("uid=?", uid).And("org_id=?", orgId).Get(ou) + if err != nil { + sess.Rollback() + return err + } + ou.NumTeams++ + if t.IsOwnerTeam() { + ou.IsOwner = true + } + if _, err = sess.Id(ou.Id).AllCols().Update(ou); err != nil { + sess.Rollback() + return err + } + + return sess.Commit() +} + +func removeTeamMemberWithSess(orgId, teamId, uid int64, sess *xorm.Session) error { + if !IsTeamMember(orgId, teamId, uid) { + return nil + } + + // Get team and its repositories. + t, err := GetTeamById(teamId) + if err != nil { + return err + } + + // Check if the user to delete is the last member in owner team. + if t.IsOwnerTeam() && t.NumMembers == 1 { + return ErrLastOrgOwner + } + + t.NumMembers-- + + if err = t.GetRepositories(); err != nil { + return err + } + + // Get organization. + org, err := GetUserById(orgId) + if err != nil { + return err + } + + // Get user. + u, err := GetUserById(uid) + if err != nil { + return err + } + + tu := &TeamUser{ + Uid: uid, + OrgId: orgId, + TeamId: teamId, + } + + if _, err := sess.Delete(tu); err != nil { + sess.Rollback() + return err + } else if _, err = sess.Id(t.Id).AllCols().Update(t); err != nil { + sess.Rollback() + return err + } + + // Delete access to team repositories. + for _, repo := range t.Repos { + auth, err := GetHighestAuthorize(orgId, uid, teamId, repo.Id) + if err != nil { + sess.Rollback() + return err + } + + access := &Access{ + UserName: u.LowerName, + RepoName: path.Join(org.LowerName, repo.LowerName), + } + // Delete access if this is the last team user belongs to. + if auth == 0 { + if _, err = sess.Delete(access); err != nil { + sess.Rollback() + return fmt.Errorf("fail to delete access: %v", err) + } else if err = WatchRepo(u.Id, repo.Id, false); err != nil { + sess.Rollback() + return err + } + } else if auth < t.Authorize { + // Downgrade authorize level. + if err = addAccessWithAuthorize(sess, access, AuthorizeToAccessType(auth)); err != nil { + sess.Rollback() + return err + } + } + } + + // This must exist. + ou := new(OrgUser) + _, err = sess.Where("uid=?", uid).And("org_id=?", org.Id).Get(ou) + if err != nil { + sess.Rollback() + return err + } + ou.NumTeams-- + if t.IsOwnerTeam() { + ou.IsOwner = false + } + if _, err = sess.Id(ou.Id).AllCols().Update(ou); err != nil { + sess.Rollback() + return err + } + return nil +} + +// RemoveTeamMember removes member from given team of given organization. +func RemoveTeamMember(orgId, teamId, uid int64) error { + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return err + } + if err := removeTeamMemberWithSess(orgId, teamId, uid, sess); err != nil { + return err + } + return sess.Commit() +} diff --git a/models/publickey.go b/models/publickey.go index 603ff364..1246cffc 100644 --- a/models/publickey.go +++ b/models/publickey.go @@ -54,7 +54,7 @@ func exePath() (string, error) { func homeDir() string { home, err := com.HomeDir() if err != nil { - log.Fatal("Fail to get home directory: %v", err) + log.Fatal(4, "Fail to get home directory: %v", err) } return home } @@ -63,25 +63,28 @@ func init() { var err error if appPath, err = exePath(); err != nil { - log.Fatal("publickey.init(fail to get app path): %v\n", err) + log.Fatal(4, "fail to get app path: %v\n", err) } + appPath = strings.Replace(appPath, "\\", "/", -1) // Determine and create .ssh path. SshPath = filepath.Join(homeDir(), ".ssh") - if err = os.MkdirAll(SshPath, os.ModePerm); err != nil { - log.Fatal("publickey.init(fail to create SshPath(%s)): %v\n", SshPath, err) + if err = os.MkdirAll(SshPath, 0700); err != nil { + log.Fatal(4, "fail to create SshPath(%s): %v\n", SshPath, err) } } // PublicKey represents a SSH key. type PublicKey struct { - Id int64 - OwnerId int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` - Name string `xorm:"UNIQUE(s) NOT NULL"` - Fingerprint string - Content string `xorm:"TEXT NOT NULL"` - Created time.Time `xorm:"CREATED"` - Updated time.Time `xorm:"UPDATED"` + Id int64 + OwnerId int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` + Name string `xorm:"UNIQUE(s) NOT NULL"` + Fingerprint string + Content string `xorm:"TEXT NOT NULL"` + Created time.Time `xorm:"CREATED"` + Updated time.Time + HasRecentActivity bool `xorm:"-"` + HasUsed bool `xorm:"-"` } // GetAuthorizedString generates and returns formatted public key string for authorized_keys file. @@ -89,6 +92,59 @@ func (key *PublicKey) GetAuthorizedString() string { return fmt.Sprintf(_TPL_PUBLICK_KEY, appPath, key.Id, key.Content) } +var ( + MinimumKeySize = map[string]int{ + "(ED25519)": 256, + "(ECDSA)": 256, + "(NTRU)": 1087, + "(MCE)": 1702, + "(McE)": 1702, + "(RSA)": 2048, + } +) + +// CheckPublicKeyString checks if the given public key string is recognized by SSH. +func CheckPublicKeyString(content string) (bool, error) { + if strings.ContainsAny(content, "\n\r") { + return false, 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 + } + tmpPath := tmpFile.Name() + defer os.Remove(tmpPath) + tmpFile.WriteString(content) + tmpFile.Close() + + // … see 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) + } else if len(stdout) < 2 { + return false, errors.New("ssh-keygen returned not enough output to evaluate the key") + } + sshKeygenOutput := strings.Split(stdout, " ") + if len(sshKeygenOutput) < 4 { + return false, errors.New("Not enough fields returned by ssh-keygen -l -f") + } + keySize, err := com.StrTo(sshKeygenOutput[0]).Int() + if err != nil { + return false, errors.New("Cannot get key size of the given key") + } + keyType := strings.TrimSpace(sshKeygenOutput[len(sshKeygenOutput)-1]) + + if minimumKeySize := MinimumKeySize[keyType]; minimumKeySize == 0 { + return false, 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 true, nil +} + // saveAuthorizedKeyFile writes SSH key content to authorized_keys file. func saveAuthorizedKeyFile(key *PublicKey) error { sshOpLocker.Lock() @@ -100,6 +156,16 @@ func saveAuthorizedKeyFile(key *PublicKey) error { return err } defer f.Close() + finfo, err := f.Stat() + if err != nil { + return err + } + if finfo.Mode().Perm() > 0600 { + log.Error(4, "authorized_keys file has unusual permission flags: %s - setting to -rw-------", finfo.Mode().Perm().String()) + if err = f.Chmod(0600); err != nil { + return err + } + } _, err = f.WriteString(key.GetAuthorizedString()) return err @@ -143,11 +209,31 @@ func AddPublicKey(key *PublicKey) (err error) { return nil } +// GetPublicKeyById returns public key by given ID. +func GetPublicKeyById(keyId int64) (*PublicKey, error) { + key := new(PublicKey) + has, err := x.Id(keyId).Get(key) + if err != nil { + return nil, err + } else if !has { + return nil, ErrKeyNotExist + } + return key, nil +} + // ListPublicKey returns a list of all public keys that user has. -func ListPublicKey(uid int64) ([]PublicKey, error) { - keys := make([]PublicKey, 0, 5) +func ListPublicKey(uid int64) ([]*PublicKey, error) { + keys := make([]*PublicKey, 0, 5) err := x.Find(&keys, &PublicKey{OwnerId: uid}) - return keys, err + 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 } // rewriteAuthorizedKeys finds and deletes corresponding line in authorized_keys file. @@ -203,6 +289,12 @@ func rewriteAuthorizedKeys(key *PublicKey, p, tmpP string) error { return nil } +// UpdatePublicKey updates given public key. +func UpdatePublicKey(key *PublicKey) error { + _, 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) @@ -218,8 +310,6 @@ func DeletePublicKey(key *PublicKey) error { fpath := filepath.Join(SshPath, "authorized_keys") tmpPath := filepath.Join(SshPath, "authorized_keys.tmp") - log.Trace("publickey.DeletePublicKey(authorized_keys): %s", fpath) - if err = rewriteAuthorizedKeys(key, fpath, tmpPath); err != nil { return err } else if err = os.Remove(fpath); err != nil { diff --git a/models/release.go b/models/release.go index 3e1a7811..012b6cc5 100644 --- a/models/release.go +++ b/models/release.go @@ -10,7 +10,7 @@ import ( "strings" "time" - "github.com/gogits/git" + "github.com/gogits/gogs/modules/git" ) var ( diff --git a/models/repo.go b/models/repo.go index 70f9341d..47036966 100644 --- a/models/repo.go +++ b/models/repo.go @@ -7,10 +7,14 @@ package models import ( "errors" "fmt" + "html" + "html/template" "io/ioutil" "os" + "os/exec" "path" "path/filepath" + "regexp" "sort" "strings" "time" @@ -19,10 +23,7 @@ import ( "github.com/Unknwon/cae/zip" "github.com/Unknwon/com" - "github.com/gogits/git" - - "github.com/gogits/gogs/modules/base" - "github.com/gogits/gogs/modules/bin" + "github.com/gogits/gogs/modules/git" "github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/process" "github.com/gogits/gogs/modules/setting" @@ -39,34 +40,31 @@ var ( 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") ) var ( - LanguageIgns, Licenses []string + Gitignores, Licenses []string ) -// getAssetList returns corresponding asset list in 'conf'. -func getAssetList(prefix string) []string { - assets := make([]string, 0, 15) - for _, name := range bin.AssetNames() { - if strings.HasPrefix(name, prefix) { - assets = append(assets, strings.TrimPrefix(name, prefix+"/")) - } - } - return assets -} +var ( + DescriptionPattern = regexp.MustCompile(`https?://\S+`) +) func LoadRepoConfig() { // Load .gitignore and license files. types := []string{"gitignore", "license"} typeFiles := make([][]string, 2) for i, t := range types { - files := getAssetList(path.Join("conf", t)) + files, err := com.StatDir(path.Join("conf", t)) + if err != nil { + log.Fatal(4, "Fail to get %s files: %v", t, err) + } customPath := path.Join(setting.CustomPath, "conf", t) if com.IsDir(customPath) { customFiles, err := com.StatDir(customPath) if err != nil { - log.Fatal("Fail to get custom %s files: %v", t, err) + log.Fatal(4, "Fail to get custom %s files: %v", t, err) } for _, f := range customFiles { @@ -78,36 +76,47 @@ func LoadRepoConfig() { typeFiles[i] = files } - LanguageIgns = typeFiles[0] + Gitignores = typeFiles[0] Licenses = typeFiles[1] - sort.Strings(LanguageIgns) + sort.Strings(Gitignores) sort.Strings(Licenses) } func NewRepoContext() { zip.Verbose = false + // Check Git installation. + if _, err := exec.LookPath("git"); err != nil { + log.Fatal(4, "Fail to test 'git' command: %v (forgotten install?)", err) + } + + // Check Git version. + ver, err := git.GetVersion() + if err != nil { + log.Fatal(4, "Fail to get Git version: %v", err) + } + if ver.Major < 2 && ver.Minor < 8 { + log.Fatal(4, "Gogs requires Git version greater or equal to 1.8.0") + } + // Check if server has basic git setting. stdout, stderr, err := process.Exec("NewRepoContext(get setting)", "git", "config", "--get", "user.name") - if strings.Contains(stderr, "fatal:") { - log.Fatal("repo.NewRepoContext(fail to get git user.name): %s", stderr) + if err != nil { + log.Fatal(4, "Fail to get git user.name: %s", stderr) } else if err != nil || len(strings.TrimSpace(stdout)) == 0 { if _, stderr, err = process.Exec("NewRepoContext(set email)", "git", "config", "--global", "user.email", "gogitservice@gmail.com"); err != nil { - log.Fatal("repo.NewRepoContext(fail to set git user.email): %s", stderr) + log.Fatal(4, "Fail to set git user.email: %s", stderr) } else if _, stderr, err = process.Exec("NewRepoContext(set name)", "git", "config", "--global", "user.name", "Gogs"); err != nil { - log.Fatal("repo.NewRepoContext(fail to set git user.name): %s", stderr) + log.Fatal(4, "Fail to set git user.name: %s", stderr) } } - barePath := path.Join(setting.RepoRootPath, "git-bare.zip") - if !com.IsExist(barePath) { - data, err := bin.Asset("conf/content/git-bare.zip") - if err != nil { - log.Fatal("Fail to get asset 'git-bare.zip': %v", err) - } else if err := ioutil.WriteFile(barePath, data, os.ModePerm); err != nil { - log.Fatal("Fail to write asset 'git-bare.zip': %v", err) - } + // Set git some configurations. + if _, stderr, err = process.Exec("NewRepoContext(git config --global core.quotepath false)", + "git", "config", "--global", "core.quotepath", "false"); err != nil { + log.Fatal(4, "Fail to execute 'git config --global core.quotepath false': %s", stderr) } + } // Repository represents a git repository. @@ -126,12 +135,17 @@ type Repository struct { NumIssues int NumClosedIssues int NumOpenIssues int `xorm:"-"` + NumPulls int + NumClosedPulls int + NumOpenPulls int `xorm:"-"` NumMilestones int `xorm:"NOT NULL DEFAULT 0"` NumClosedMilestones int `xorm:"NOT NULL DEFAULT 0"` NumOpenMilestones int `xorm:"-"` NumTags int `xorm:"-"` IsPrivate bool IsMirror bool + *Mirror `xorm:"-"` + IsFork bool `xorm:"NOT NULL DEFAULT false"` IsBare bool IsGoget bool DefaultBranch string @@ -144,6 +158,21 @@ func (repo *Repository) GetOwner() (err error) { return err } +func (repo *Repository) GetMirror() (err error) { + repo.Mirror, err = GetMirror(repo.Id) + return err +} + +// DescriptionHtml does special handles to description and return HTML string. +func (repo *Repository) DescriptionHtml() template.HTML { + sanitize := func(s string) string { + // TODO(nuss-justin): Improve sanitization. Strip all tags? + ss := html.EscapeString(s) + return fmt.Sprintf(`<a href="%s" target="_blank">%s</a>`, ss, ss) + } + return template.HTML(DescriptionPattern.ReplaceAllStringFunc(repo.Description, sanitize)) +} + // IsRepositoryExist returns true if the repository with given name under user has already existed. func IsRepositoryExist(u *User, repoName string) (bool, error) { repo := Repository{OwnerId: u.Id} @@ -188,10 +217,26 @@ type Mirror struct { NextUpdate time.Time } +func GetMirror(repoId int64) (*Mirror, error) { + m := &Mirror{RepoId: repoId} + has, err := x.Get(m) + if err != nil { + return nil, err + } else if !has { + return nil, ErrMirrorNotExist + } + return m, nil +} + +func UpdateMirror(m *Mirror) error { + _, err := x.Id(m.Id).Update(m) + return err +} + // MirrorRepository creates a mirror repository from source. func MirrorRepository(repoId int64, userName, repoName, repoPath, url string) error { - // TODO: need timeout. - _, stderr, err := process.Exec(fmt.Sprintf("MirrorRepository: %s/%s", userName, repoName), + _, stderr, err := process.ExecTimeout(10*time.Minute, + fmt.Sprintf("MirrorRepository: %s/%s", userName, repoName), "git", "clone", "--mirror", url, repoPath) if err != nil { return errors.New("git clone --mirror: " + stderr) @@ -205,24 +250,7 @@ func MirrorRepository(repoId int64, userName, repoName, repoPath, url string) er }); err != nil { return err } - - return git.UnpackRefs(repoPath) -} - -func GetMirror(repoId int64) (*Mirror, error) { - m := &Mirror{RepoId: repoId} - has, err := x.Get(m) - if err != nil { - return nil, err - } else if !has { - return nil, ErrMirrorNotExist - } - return m, nil -} - -func UpdateMirror(m *Mirror) error { - _, err := x.Id(m.Id).Update(m) - return err + return nil } // MirrorUpdate checks and updates mirror repositories. @@ -233,20 +261,17 @@ func MirrorUpdate() { return nil } - // TODO: need timeout. repoPath := filepath.Join(setting.RepoRootPath, m.RepoName+".git") - if _, stderr, err := process.ExecDir( + if _, stderr, err := process.ExecDir(10*time.Minute, repoPath, fmt.Sprintf("MirrorUpdate: %s", repoPath), "git", "remote", "update"); err != nil { return errors.New("git remote update: " + stderr) - } else if err = git.UnpackRefs(repoPath); err != nil { - return errors.New("UnpackRefs: " + err.Error()) } m.NextUpdate = time.Now().Add(time.Duration(m.Interval) * time.Hour) return UpdateMirror(m) }); err != nil { - log.Error("repo.MirrorUpdate: %v", err) + log.Error(4, "repo.MirrorUpdate: %v", err) } } @@ -263,6 +288,16 @@ func MigrateRepository(u *User, name, desc string, private, mirror bool, url str repoPath := RepoPath(u.Name, name) + if u.IsOrganization() { + t, err := u.GetOwnerTeam() + if err != nil { + return nil, err + } + repo.NumWatches = t.NumMembers + } else { + repo.NumWatches = 1 + } + repo.IsBare = false if mirror { if err = MirrorRepository(repo.Id, u.Name, repo.Name, repoPath, url); err != nil { @@ -272,28 +307,25 @@ func MigrateRepository(u *User, name, desc string, private, mirror bool, url str return repo, UpdateRepository(repo) } - // TODO: need timeout. // Clone from local repository. - _, stderr, err := process.Exec( + _, stderr, err := process.ExecTimeout(10*time.Minute, fmt.Sprintf("MigrateRepository(git clone): %s", repoPath), "git", "clone", repoPath, tmpDir) if err != nil { return repo, errors.New("git clone: " + stderr) } - // TODO: need timeout. - // Pull data from source. - if _, stderr, err = process.ExecDir( + // Add remote and fetch data. + if _, stderr, err = process.ExecDir(3*time.Minute, tmpDir, fmt.Sprintf("MigrateRepository(git pull): %s", repoPath), - "git", "pull", url); err != nil { - return repo, errors.New("git pull: " + stderr) + "git", "remote", "add", "-f", "--tags", "upstream", url); err != nil { + return repo, errors.New("git remote: " + stderr) } - // TODO: need timeout. // Push data to local repository. - if _, stderr, err = process.ExecDir( + if _, stderr, err = process.ExecDir(3*time.Minute, tmpDir, fmt.Sprintf("MigrateRepository(git push): %s", repoPath), - "git", "push", "origin", "master"); err != nil { + "git", "push", "--tags", "origin", "refs/remotes/upstream/*:refs/heads/*"); err != nil { return repo, errors.New("git push: " + stderr) } @@ -302,7 +334,7 @@ func MigrateRepository(u *User, name, desc string, private, mirror bool, url str // extractGitBareZip extracts git-bare.zip to repository path. func extractGitBareZip(repoPath string) error { - z, err := zip.Open(filepath.Join(setting.RepoRootPath, "git-bare.zip")) + z, err := zip.Open(path.Join(setting.ConfRootPath, "content/git-bare.zip")) if err != nil { return err } @@ -314,20 +346,20 @@ func extractGitBareZip(repoPath string) error { // initRepoCommit temporarily changes with work directory. func initRepoCommit(tmpPath string, sig *git.Signature) (err error) { var stderr string - if _, stderr, err = process.ExecDir( + if _, stderr, err = process.ExecDir(-1, tmpPath, fmt.Sprintf("initRepoCommit(git add): %s", tmpPath), "git", "add", "--all"); err != nil { return errors.New("git add: " + stderr) } - if _, stderr, err = process.ExecDir( + if _, stderr, err = process.ExecDir(-1, tmpPath, fmt.Sprintf("initRepoCommit(git commit): %s", tmpPath), "git", "commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email), "-m", "Init commit"); err != nil { return errors.New("git commit: " + stderr) } - if _, stderr, err = process.ExecDir( + if _, stderr, err = process.ExecDir(-1, tmpPath, fmt.Sprintf("initRepoCommit(git push): %s", tmpPath), "git", "push", "origin", "master"); err != nil { return errors.New("git push: " + stderr) @@ -346,28 +378,18 @@ func createHookUpdate(hookPath, content string) error { return err } -// SetRepoEnvs sets environment variables for command update. -func SetRepoEnvs(userId int64, userName, repoName, repoUserName string) { - os.Setenv("userId", base.ToStr(userId)) - os.Setenv("userName", userName) - os.Setenv("repoName", repoName) - os.Setenv("repoUserName", repoUserName) -} - // InitRepository initializes README and .gitignore if needed. -func initRepository(f string, user *User, repo *Repository, initReadme bool, repoLang, license string) error { - repoPath := RepoPath(user.Name, repo.Name) +func initRepository(f string, u *User, repo *Repository, initReadme bool, repoLang, license string) error { + repoPath := RepoPath(u.Name, repo.Name) // Create bare new repository. if err := extractGitBareZip(repoPath); err != nil { return err } - rp := strings.NewReplacer("\\", "/", " ", "\\ ") // hook/post-update if err := createHookUpdate(filepath.Join(repoPath, "hooks", "update"), - fmt.Sprintf(TPL_UPDATE_HOOK, setting.ScriptType, - rp.Replace(appPath))); err != nil { + fmt.Sprintf(TPL_UPDATE_HOOK, setting.ScriptType, "\""+appPath+"\"")); err != nil { return err } @@ -384,7 +406,7 @@ func initRepository(f string, user *User, repo *Repository, initReadme bool, rep } // Clone to temprory path and do the init commit. - tmpDir := filepath.Join(os.TempDir(), base.ToStr(time.Now().Nanosecond())) + tmpDir := filepath.Join(os.TempDir(), com.ToStr(time.Now().Nanosecond())) os.MkdirAll(tmpDir, os.ModePerm) _, stderr, err := process.Exec( @@ -405,12 +427,11 @@ func initRepository(f string, user *User, repo *Repository, initReadme bool, rep } // .gitignore - if repoLang != "" { - filePath := "conf/gitignore/" + repoLang + filePath := "conf/gitignore/" + repoLang + if com.IsFile(filePath) { targetPath := path.Join(tmpDir, fileName["gitign"]) - data, err := bin.Asset(filePath) - if err == nil { - if err = ioutil.WriteFile(targetPath, data, os.ModePerm); err != nil { + if com.IsFile(filePath) { + if err = com.Copy(filePath, targetPath); err != nil { return err } } else { @@ -422,15 +443,16 @@ func initRepository(f string, user *User, repo *Repository, initReadme bool, rep } } } + } else { + delete(fileName, "gitign") } // LICENSE - if license != "" { - filePath := "conf/license/" + license + filePath = "conf/license/" + license + if com.IsFile(filePath) { targetPath := path.Join(tmpDir, fileName["license"]) - data, err := bin.Asset(filePath) - if err == nil { - if err = ioutil.WriteFile(targetPath, data, os.ModePerm); err != nil { + if com.IsFile(filePath) { + if err = com.Copy(filePath, targetPath); err != nil { return err } } else { @@ -442,16 +464,18 @@ func initRepository(f string, user *User, repo *Repository, initReadme bool, rep } } } + } else { + delete(fileName, "license") } if len(fileName) == 0 { - return nil + repo.IsBare = true + repo.DefaultBranch = "master" + return UpdateRepository(repo) } - SetRepoEnvs(user.Id, user.Name, repo.Name, user.Name) - // Apply changes and commit. - return initRepoCommit(tmpDir, user.NewGitSig()) + return initRepoCommit(tmpDir, u.NewGitSig()) } // CreateRepository creates a repository for given user or organization. @@ -480,10 +504,6 @@ func CreateRepository(u *User, name, desc, lang, license string, private, mirror LowerName: strings.ToLower(name), Description: desc, IsPrivate: private, - IsBare: lang == "" && license == "" && !initReadme, - } - if !repo.IsBare { - repo.DefaultBranch = "master" } if _, err = sess.Insert(repo); err != nil { @@ -499,7 +519,7 @@ func CreateRepository(u *User, name, desc, lang, license string, private, mirror } access := &Access{ UserName: u.LowerName, - RepoName: strings.ToLower(path.Join(u.Name, repo.Name)), + RepoName: path.Join(u.LowerName, repo.LowerName), Mode: mode, } // Give access to all members in owner team. @@ -509,12 +529,12 @@ func CreateRepository(u *User, name, desc, lang, license string, private, mirror sess.Rollback() return nil, err } - us, err := GetTeamMembers(u.Id, t.Id) - if err != nil { + if err = t.GetMembers(); err != nil { sess.Rollback() return nil, err } - for _, u := range us { + for _, u := range t.Members { + access.Id = 0 access.UserName = u.LowerName if _, err = sess.Insert(access); err != nil { sess.Rollback() @@ -528,15 +548,15 @@ func CreateRepository(u *User, name, desc, lang, license string, private, mirror } } - rawSql := "UPDATE `user` SET num_repos = num_repos + 1 WHERE id = ?" - if _, err = sess.Exec(rawSql, u.Id); err != nil { + if _, err = sess.Exec( + "UPDATE `user` SET num_repos = num_repos + 1 WHERE id = ?", u.Id); err != nil { sess.Rollback() return nil, err } // Update owner team info and count. if u.IsOrganization() { - t.RepoIds += "$" + base.ToStr(repo.Id) + "|" + t.RepoIds += "$" + com.ToStr(repo.Id) + "|" t.NumRepos++ if _, err = sess.Id(t.Id).AllCols().Update(t); err != nil { sess.Rollback() @@ -549,26 +569,31 @@ func CreateRepository(u *User, name, desc, lang, license string, private, mirror } if u.IsOrganization() { - ous, err := GetOrgUsersByOrgId(u.Id) + t, err := u.GetOwnerTeam() if err != nil { - log.Error("repo.CreateRepository(GetOrgUsersByOrgId): %v", err) + log.Error(4, "GetOwnerTeam: %v", err) } else { - for _, ou := range ous { - if err = WatchRepo(ou.Uid, repo.Id, true); err != nil { - log.Error("repo.CreateRepository(WatchRepo): %v", err) + if err = t.GetMembers(); err != nil { + log.Error(4, "GetMembers: %v", err) + } else { + for _, u := range t.Members { + if err = WatchRepo(u.Id, repo.Id, true); err != nil { + log.Error(4, "WatchRepo2: %v", err) + } } } } - } - if err = WatchRepo(u.Id, repo.Id, true); err != nil { - log.Error("repo.CreateRepository(WatchRepo2): %v", err) + } else { + if err = WatchRepo(u.Id, repo.Id, true); err != nil { + log.Error(4, "WatchRepo3: %v", err) + } } if err = NewRepoAction(u, repo); err != nil { - log.Error("repo.CreateRepository(NewRepoAction): %v", err) + log.Error(4, "NewRepoAction: %v", err) } - // No need for init for mirror. + // No need for init mirror. if mirror { return repo, nil } @@ -576,14 +601,14 @@ func CreateRepository(u *User, name, desc, lang, license string, private, mirror repoPath := RepoPath(u.Name, repo.Name) if err = initRepository(repoPath, u, repo, initReadme, lang, license); err != nil { if err2 := os.RemoveAll(repoPath); err2 != nil { - log.Error("repo.CreateRepository(initRepository): %v", err) - return nil, errors.New(fmt.Sprintf( - "delete repo directory %s/%s failed(2): %v", u.Name, repo.Name, err2)) + log.Error(4, "initRepository: %v", err) + return nil, fmt.Errorf( + "delete repo directory %s/%s failed(2): %v", u.Name, repo.Name, err2) } - return nil, err + return nil, fmt.Errorf("initRepository: %v", err) } - _, stderr, err := process.ExecDir( + _, stderr, err := process.ExecDir(-1, repoPath, fmt.Sprintf("CreateRepository(git update-server-info): %s", repoPath), "git", "update-server-info") if err != nil { @@ -593,6 +618,12 @@ func CreateRepository(u *User, name, desc, lang, license string, private, mirror return repo, nil } +// CountRepositories returns number of repositories. +func CountRepositories() int64 { + count, _ := x.Count(new(Repository)) + return count +} + // GetRepositoriesWithUsers returns given number of repository objects with offset. // It also auto-gets corresponding users. func GetRepositoriesWithUsers(num, offset int) ([]*Repository, error) { @@ -626,26 +657,23 @@ func TransferOwnership(u *User, newOwner string, repo *Repository) (err error) { return err } - // Update accesses. - accesses := make([]Access, 0, 10) - if err = x.Find(&accesses, &Access{RepoName: u.LowerName + "/" + repo.LowerName}); err != nil { - return err - } - sess := x.NewSession() defer sess.Close() if err = sess.Begin(); err != nil { return err } - for i := range accesses { - accesses[i].RepoName = newUser.LowerName + "/" + repo.LowerName - if accesses[i].UserName == u.LowerName { - accesses[i].UserName = newUser.LowerName - } - if err = UpdateAccessWithSession(sess, &accesses[i]); err != nil { - return err - } + if _, err = sess.Where("repo_name = ?", u.LowerName+"/"+repo.LowerName). + And("user_name = ?", u.LowerName).Update(&Access{UserName: newUser.LowerName}); err != nil { + sess.Rollback() + return err + } + + if _, err = sess.Where("repo_name = ?", u.LowerName+"/"+repo.LowerName).Update(&Access{ + RepoName: newUser.LowerName + "/" + repo.LowerName, + }); err != nil { + sess.Rollback() + return err } // Update repository. @@ -656,28 +684,59 @@ func TransferOwnership(u *User, newOwner string, repo *Repository) (err error) { } // Update user repository number. - rawSql := "UPDATE `user` SET num_repos = num_repos + 1 WHERE id = ?" - if _, err = sess.Exec(rawSql, newUser.Id); err != nil { + if _, err = sess.Exec("UPDATE `user` SET num_repos = num_repos + 1 WHERE id = ?", newUser.Id); err != nil { sess.Rollback() return err } - rawSql = "UPDATE `user` SET num_repos = num_repos - 1 WHERE id = ?" - if _, err = sess.Exec(rawSql, u.Id); err != nil { + + if _, err = sess.Exec("UPDATE `user` SET num_repos = num_repos - 1 WHERE id = ?", u.Id); err != nil { sess.Rollback() return err } - // Add watch of new owner to repository. - if !IsWatching(newUser.Id, repo.Id) { - if err = WatchRepo(newUser.Id, repo.Id, true); err != nil { + // New owner is organization. + if newUser.IsOrganization() { + mode := WRITABLE + if repo.IsMirror { + mode = READABLE + } + access := &Access{ + RepoName: path.Join(newUser.LowerName, repo.LowerName), + Mode: mode, + } + + // Give access to all members in owner team. + t, err := newUser.GetOwnerTeam() + if err != nil { sess.Rollback() return err } - } + if err = t.GetMembers(); err != nil { + sess.Rollback() + return err + } + for _, u := range t.Members { + access.Id = 0 + access.UserName = u.LowerName + if _, err = sess.Insert(access); err != nil { + sess.Rollback() + return err + } + } - if err = TransferRepoAction(u, newUser, repo); err != nil { - sess.Rollback() - return err + if _, err = sess.Exec( + "UPDATE `user` SET num_repos = num_repos + 1 WHERE id = ?", u.Id); err != nil { + sess.Rollback() + return err + } + + // Update owner team info and count. + t.RepoIds += "$" + com.ToStr(repo.Id) + "|" + t.NumRepos++ + if _, err = sess.Id(t.Id).AllCols().Update(t); err != nil { + sess.Rollback() + return err + } } // Change repository directory name. @@ -686,11 +745,33 @@ func TransferOwnership(u *User, newOwner string, repo *Repository) (err error) { return err } - return sess.Commit() + if err = sess.Commit(); err != nil { + return err + } + + // Add watch of new owner to repository. + if !newUser.IsOrganization() { + if err = WatchRepo(newUser.Id, repo.Id, true); err != nil { + log.Error(4, "WatchRepo", err) + } + } + if err = WatchRepo(u.Id, repo.Id, false); err != nil { + log.Error(4, "WatchRepo2", err) + } + + if err = TransferRepoAction(u, newUser, repo); err != nil { + return err + } + + return nil } // ChangeRepositoryName changes all corresponding setting from old repository name to new one. func ChangeRepositoryName(userName, oldRepoName, newRepoName string) (err error) { + if !IsLegalName(newRepoName) { + return ErrRepoNameIllegal + } + // Update accesses. accesses := make([]Access, 0, 10) if err = x.Find(&accesses, &Access{RepoName: strings.ToLower(userName + "/" + oldRepoName)}); err != nil { @@ -733,8 +814,8 @@ func UpdateRepository(repo *Repository) error { } // DeleteRepository deletes a repository for a user or orgnaztion. -func DeleteRepository(userId, repoId int64, userName string) error { - repo := &Repository{Id: repoId, OwnerId: userId} +func DeleteRepository(uid, repoId int64, userName string) error { + repo := &Repository{Id: repoId, OwnerId: uid} has, err := x.Get(repo) if err != nil { return err @@ -742,6 +823,17 @@ func DeleteRepository(userId, repoId int64, userName string) error { return ErrRepoNotExist } + // In case is a organization. + org, err := GetUserById(uid) + if err != nil { + return err + } + if org.IsOrganization() { + if err = org.GetTeams(); err != nil { + return err + } + } + sess := x.NewSession() defer sess.Close() if err = sess.Begin(); err != nil { @@ -752,10 +844,27 @@ func DeleteRepository(userId, repoId int64, userName string) error { sess.Rollback() return err } + + // Delete all access. if _, err := sess.Delete(&Access{RepoName: strings.ToLower(path.Join(userName, repo.Name))}); err != nil { sess.Rollback() return err } + if org.IsOrganization() { + idStr := "$" + com.ToStr(repoId) + "|" + for _, t := range org.Teams { + if !strings.Contains(t.RepoIds, idStr) { + continue + } + t.NumRepos-- + t.RepoIds = strings.Replace(t.RepoIds, idStr, "", 1) + if _, err = sess.Id(t.Id).AllCols().Update(t); err != nil { + sess.Rollback() + return err + } + } + } + if _, err := sess.Delete(&Action{RepoId: repo.Id}); err != nil { sess.Rollback() return err @@ -799,8 +908,7 @@ func DeleteRepository(userId, repoId int64, userName string) error { return err } - rawSql := "UPDATE `user` SET num_repos = num_repos - 1 WHERE id = ?" - if _, err = sess.Exec(rawSql, userId); err != nil { + if _, err = sess.Exec("UPDATE `user` SET num_repos = num_repos - 1 WHERE id = ?", uid); err != nil { sess.Rollback() return err } @@ -811,6 +919,26 @@ func DeleteRepository(userId, repoId int64, userName string) error { return sess.Commit() } +// GetRepositoryByRef returns a Repository specified by a GFM reference. +// See https://help.github.com/articles/writing-on-github#references for more information on the syntax. +func GetRepositoryByRef(ref string) (*Repository, error) { + n := strings.IndexByte(ref, byte('/')) + + if n < 2 { + return nil, ErrInvalidReference + } + + userName, repoName := ref[:n], ref[n+1:] + + user, err := GetUserByName(userName) + + if err != nil { + return nil, err + } + + return GetRepositoryByName(user.Id, repoName) +} + // GetRepositoryByName returns the repository by given name under user if exists. func GetRepositoryByName(userId int64, repoName string) (*Repository, error) { repo := &Repository{ @@ -922,6 +1050,37 @@ func GetCollaborators(repoName string) (us []*User, err error) { return us, nil } +type SearchOption struct { + Keyword string + Uid int64 + Limit int +} + +// SearchRepositoryByName returns given number of repositories whose name contains keyword. +func SearchRepositoryByName(opt SearchOption) (repos []*Repository, err error) { + // Prevent SQL inject. + opt.Keyword = strings.TrimSpace(opt.Keyword) + if len(opt.Keyword) == 0 { + return repos, nil + } + + opt.Keyword = strings.Split(opt.Keyword, " ")[0] + if len(opt.Keyword) == 0 { + return repos, nil + } + opt.Keyword = strings.ToLower(opt.Keyword) + + repos = make([]*Repository, 0, opt.Limit) + + // Append conditions. + sess := x.Limit(opt.Limit) + if opt.Uid > 0 { + sess.Where("owner_id=?", opt.Uid) + } + sess.And("lower_name like '%" + opt.Keyword + "%'").Find(&repos) + return repos, err +} + // Watch is connection request for receiving repository notifycation. type Watch struct { Id int64 @@ -930,24 +1089,33 @@ type Watch struct { } // Watch or unwatch repository. -func WatchRepo(uid, rid int64, watch bool) (err error) { +func WatchRepo(uid, repoId int64, watch bool) (err error) { if watch { - if _, err = x.Insert(&Watch{RepoId: rid, UserId: uid}); err != nil { + if IsWatching(uid, repoId) { + return nil + } + if _, err = x.Insert(&Watch{RepoId: repoId, UserId: uid}); err != nil { return err } - - rawSql := "UPDATE `repository` SET num_watches = num_watches + 1 WHERE id = ?" - _, err = x.Exec(rawSql, rid) + _, err = x.Exec("UPDATE `repository` SET num_watches = num_watches + 1 WHERE id = ?", repoId) } else { - if _, err = x.Delete(&Watch{0, uid, rid}); err != nil { + if !IsWatching(uid, repoId) { + return nil + } + if _, err = x.Delete(&Watch{0, uid, repoId}); err != nil { return err } - rawSql := "UPDATE `repository` SET num_watches = num_watches - 1 WHERE id = ?" - _, err = x.Exec(rawSql, rid) + _, err = x.Exec("UPDATE `repository` SET num_watches = num_watches - 1 WHERE id = ?", repoId) } return err } +// IsWatching checks if user has watched given repository. +func IsWatching(uid, rid int64) bool { + has, _ := x.Get(&Watch{0, uid, rid}) + return has +} + // GetWatchers returns all watchers of given repository. func GetWatchers(rid int64) ([]*Watch, error) { watches := make([]*Watch, 0, 10) @@ -983,9 +1151,37 @@ func NotifyWatchers(act *Action) error { return nil } -// IsWatching checks if user has watched given repository. -func IsWatching(uid, rid int64) bool { - has, _ := x.Get(&Watch{0, uid, rid}) +type Star struct { + Id int64 + Uid int64 `xorm:"UNIQUE(s)"` + RepoId int64 `xorm:"UNIQUE(s)"` +} + +// Star or unstar repository. +func StarRepo(uid, repoId int64, star bool) (err error) { + if star { + if IsStaring(uid, repoId) { + return nil + } + if _, err = x.Insert(&Star{Uid: uid, RepoId: repoId}); err != nil { + return err + } + _, err = x.Exec("UPDATE `repository` SET num_stars = num_stars + 1 WHERE id = ?", repoId) + } else { + if !IsStaring(uid, repoId) { + return nil + } + if _, err = x.Delete(&Star{0, uid, repoId}); err != nil { + return err + } + _, err = x.Exec("UPDATE `repository` SET num_stars = num_stars - 1 WHERE id = ?", repoId) + } + return err +} + +// IsStaring checks if user has starred given repository. +func IsStaring(uid, repoId int64) bool { + has, _ := x.Get(&Star{0, uid, repoId}) return has } diff --git a/models/update.go b/models/update.go index cf7f5d2a..68a92ada 100644 --- a/models/update.go +++ b/models/update.go @@ -10,9 +10,8 @@ import ( "os/exec" "strings" - "github.com/gogits/git" - "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/git" "github.com/gogits/gogs/modules/log" ) @@ -47,8 +46,6 @@ func DelUpdateTasksByUuid(uuid string) error { } func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName string, userId int64) error { - //fmt.Println(refName, oldCommitId, newCommitId) - //fmt.Println(userName, repoUserName, repoName) isNew := strings.HasPrefix(oldCommitId, "0000000") if isNew && strings.HasPrefix(newCommitId, "0000000") { @@ -82,12 +79,12 @@ func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName return fmt.Errorf("runUpdate.GetRepositoryByName userId: %v", err) } - // if tags push + // Push tags. if strings.HasPrefix(refName, "refs/tags/") { tagName := git.RefEndName(refName) tag, err := repo.GetTag(tagName) if err != nil { - log.GitLogger.Fatal("runUpdate.GetTag: %v", err) + log.GitLogger.Fatal(4, "runUpdate.GetTag: %v", err) } var actEmail string @@ -96,7 +93,7 @@ func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName } else { cmt, err := tag.Commit() if err != nil { - log.GitLogger.Fatal("runUpdate.GetTag Commit: %v", err) + log.GitLogger.Fatal(4, "runUpdate.GetTag Commit: %v", err) } actEmail = cmt.Committer.Email } @@ -105,7 +102,7 @@ func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName if err = CommitRepoAction(userId, ru.Id, userName, actEmail, repos.Id, repoUserName, repoName, refName, commit); err != nil { - log.GitLogger.Fatal("runUpdate.models.CommitRepoAction: %s/%s:%v", repoUserName, repoName, err) + log.GitLogger.Fatal(4, "runUpdate.models.CommitRepoAction: %s/%s:%v", repoUserName, repoName, err) } return err } @@ -135,7 +132,7 @@ func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName // if commits push commits := make([]*base.PushCommit, 0) - var maxCommits = 3 + var maxCommits = 2 var actEmail string for e := l.Front(); e != nil; e = e.Next() { commit := e.Value.(*git.Commit) diff --git a/models/user.go b/models/user.go index b98e81ba..96881ea3 100644 --- a/models/user.go +++ b/models/user.go @@ -14,9 +14,10 @@ import ( "strings" "time" - "github.com/gogits/git" + "github.com/Unknwon/com" "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/git" "github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/setting" ) @@ -44,30 +45,31 @@ var ( // User represents the object of individual and member of organization. type User struct { Id int64 - LowerName string `xorm:"unique not null"` - Name string `xorm:"unique not null"` + LowerName string `xorm:"UNIQUE NOT NULL"` + Name string `xorm:"UNIQUE NOT NULL"` FullName string - Email string `xorm:"unique not null"` - Passwd string `xorm:"not null"` + Email string `xorm:"UNIQUE NOT NULL"` + Passwd string `xorm:"NOT NULL"` LoginType LoginType - LoginSource int64 `xorm:"not null default 0"` + LoginSource int64 `xorm:"NOT NULL DEFAULT 0"` LoginName string Type UserType - Orgs []*User `xorm:"-"` + Orgs []*User `xorm:"-"` + Repos []*Repository `xorm:"-"` NumFollowers int NumFollowings int NumStars int NumRepos int - Avatar string `xorm:"varchar(2048) not null"` - AvatarEmail string `xorm:"not null"` + Avatar string `xorm:"VARCHAR(2048) NOT NULL"` + AvatarEmail string `xorm:"NOT NULL"` Location string Website string IsActive bool IsAdmin bool Rands string `xorm:"VARCHAR(10)"` Salt string `xorm:"VARCHAR(10)"` - Created time.Time `xorm:"created"` - Updated time.Time `xorm:"updated"` + Created time.Time `xorm:"CREATED"` + Updated time.Time `xorm:"UPDATED"` // For organization. Description string @@ -77,6 +79,14 @@ type User struct { Members []*User `xorm:"-"` } +// DashboardLink returns the user dashboard page link. +func (u *User) DashboardLink() string { + if u.IsOrganization() { + return "/org/" + u.Name + "/dashboard/" + } + return "/" +} + // HomeLink returns the user home page link. func (u *User) HomeLink() string { return "/user/" + u.Name @@ -107,16 +117,39 @@ 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 { + newUser := &User{Passwd: passwd, Salt: u.Salt} + newUser.EncodePasswd() + return u.Passwd == newUser.Passwd +} + // IsOrganization returns true if user is actually a organization. func (u *User) IsOrganization() bool { return u.Type == ORGANIZATION } +// IsUserOrgOwner returns true if user is in the owner team of given organization. +func (u *User) IsUserOrgOwner(orgId int64) bool { + return IsOrganizationOwner(orgId, u.Id) +} + +// IsPublicMember returns true if user public his/her membership in give organization. +func (u *User) IsPublicMember(orgId int64) bool { + return IsPublicMembership(orgId, u.Id) +} + // GetOrganizationCount returns count of membership of organization of user. func (u *User) GetOrganizationCount() (int64, error) { return x.Where("uid=?", u.Id).Count(new(OrgUser)) } +// GetRepositories returns all repositories that user owns, including private repositories. +func (u *User) GetRepositories() (err error) { + u.Repos, err = GetRepositories(u.Id, true) + return err +} + // GetOrganizations returns all organizations that user belongs to. func (u *User) GetOrganizations() error { ous, err := GetOrgUsersByUserId(u.Id) @@ -157,23 +190,23 @@ func GetUserSalt() string { } // CreateUser creates record of a new user. -func CreateUser(u *User) (*User, error) { +func CreateUser(u *User) error { if !IsLegalName(u.Name) { - return nil, ErrUserNameIllegal + return ErrUserNameIllegal } isExist, err := IsUserExist(u.Name) if err != nil { - return nil, err + return err } else if isExist { - return nil, ErrUserAlreadyExist + return ErrUserAlreadyExist } isExist, err = IsEmailUsed(u.Email) if err != nil { - return nil, err + return err } else if isExist { - return nil, ErrEmailAlreadyUsed + return ErrEmailAlreadyUsed } u.LowerName = strings.ToLower(u.Name) @@ -186,21 +219,17 @@ func CreateUser(u *User) (*User, error) { sess := x.NewSession() defer sess.Close() if err = sess.Begin(); err != nil { - return nil, err + return err } if _, err = sess.Insert(u); err != nil { sess.Rollback() - return nil, err - } - - if err = os.MkdirAll(UserPath(u.Name), os.ModePerm); err != nil { + return err + } else if err = os.MkdirAll(UserPath(u.Name), os.ModePerm); err != nil { sess.Rollback() - return nil, err - } - - if err = sess.Commit(); err != nil { - return nil, err + return err + } else if err = sess.Commit(); err != nil { + return err } // Auto-set admin for user whose ID is 1. @@ -209,12 +238,18 @@ func CreateUser(u *User) (*User, error) { u.IsActive = true _, err = x.Id(u.Id).UseBool().Update(u) } - return u, err + return err +} + +// CountUsers returns number of users. +func CountUsers() int64 { + count, _ := x.Where("type=0").Count(new(User)) + return count } // GetUsers returns given number of user objects with offset. -func GetUsers(num, offset int) ([]User, error) { - users := make([]User, 0, num) +func GetUsers(num, offset int) ([]*User, error) { + users := make([]*User, 0, num) err := x.Limit(num, offset).Where("type=0").Asc("id").Find(&users) return users, err } @@ -231,7 +266,7 @@ func getVerifyUser(code string) (user *User) { if user, err = GetUserByName(string(b)); user != nil { return user } - log.Error("user.getVerifyUser: %v", err) + log.Error(4, "user.getVerifyUser: %v", err) } return nil @@ -244,7 +279,7 @@ func VerifyUserActiveCode(code string) (user *User) { if user = getVerifyUser(code); user != nil { // time limit code prefix := code[:base.TimeLimitCodeLength] - data := base.ToStr(user.Id) + user.Email + user.LowerName + user.Passwd + user.Rands + data := com.ToStr(user.Id) + user.Email + user.LowerName + user.Passwd + user.Rands if base.VerifyTimeLimitCode(data, minutes, prefix) { return user @@ -254,12 +289,16 @@ func VerifyUserActiveCode(code string) (user *User) { } // ChangeUserName changes all corresponding setting from old user name to new one. -func ChangeUserName(user *User, newUserName string) (err error) { +func ChangeUserName(u *User, newUserName string) (err error) { + if !IsLegalName(newUserName) { + return ErrUserNameIllegal + } + newUserName = strings.ToLower(newUserName) // Update accesses of user. accesses := make([]Access, 0, 10) - if err = x.Find(&accesses, &Access{UserName: user.LowerName}); err != nil { + if err = x.Find(&accesses, &Access{UserName: u.LowerName}); err != nil { return err } @@ -271,36 +310,38 @@ func ChangeUserName(user *User, newUserName string) (err error) { for i := range accesses { accesses[i].UserName = newUserName - if strings.HasPrefix(accesses[i].RepoName, user.LowerName+"/") { - accesses[i].RepoName = strings.Replace(accesses[i].RepoName, user.LowerName, newUserName, 1) + if strings.HasPrefix(accesses[i].RepoName, u.LowerName+"/") { + accesses[i].RepoName = strings.Replace(accesses[i].RepoName, u.LowerName, newUserName, 1) } if err = UpdateAccessWithSession(sess, &accesses[i]); err != nil { return err } } - repos, err := GetRepositories(user.Id, true) + repos, err := GetRepositories(u.Id, true) if err != nil { return err } for i := range repos { accesses = make([]Access, 0, 10) // Update accesses of user repository. - if err = x.Find(&accesses, &Access{RepoName: user.LowerName + "/" + repos[i].LowerName}); err != nil { + if err = x.Find(&accesses, &Access{RepoName: u.LowerName + "/" + repos[i].LowerName}); err != nil { return err } for j := range accesses { - accesses[j].UserName = newUserName - accesses[j].RepoName = newUserName + "/" + repos[i].LowerName - if err = UpdateAccessWithSession(sess, &accesses[j]); err != nil { - return err + // if the access is not the user's access (already updated above) + if accesses[j].UserName != u.LowerName { + accesses[j].RepoName = newUserName + "/" + repos[i].LowerName + if err = UpdateAccessWithSession(sess, &accesses[j]); err != nil { + return err + } } } } // Change user directory name. - if err = os.Rename(UserPath(user.LowerName), UserPath(newUserName)); err != nil { + if err = os.Rename(UserPath(u.LowerName), UserPath(newUserName)); err != nil { sess.Rollback() return err } @@ -309,7 +350,7 @@ func ChangeUserName(user *User, newUserName string) (err error) { } // UpdateUser updates user's information. -func UpdateUser(u *User) (err error) { +func UpdateUser(u *User) error { u.LowerName = strings.ToLower(u.Name) if len(u.Location) > 255 { @@ -322,7 +363,7 @@ func UpdateUser(u *User) (err error) { u.Description = u.Description[:255] } - _, err = x.Id(u.Id).AllCols().Update(u) + _, err := x.Id(u.Id).AllCols().Update(u) return err } @@ -332,7 +373,7 @@ func DeleteUser(u *User) error { // Check ownership of repository. count, err := GetRepositoryCount(u) if err != nil { - return errors.New("modesl.GetRepositories(GetRepositoryCount): " + err.Error()) + return errors.New("GetRepositoryCount: " + err.Error()) } else if count > 0 { return ErrUserOwnRepos } @@ -480,21 +521,21 @@ func GetUserByEmail(email string) (*User, error) { } // SearchUserByName returns given number of users whose name contains keyword. -func SearchUserByName(key string, limit int) (us []*User, err error) { +func SearchUserByName(opt SearchOption) (us []*User, err error) { // Prevent SQL inject. - key = strings.TrimSpace(key) - if len(key) == 0 { + opt.Keyword = strings.TrimSpace(opt.Keyword) + if len(opt.Keyword) == 0 { return us, nil } - key = strings.Split(key, " ")[0] - if len(key) == 0 { + opt.Keyword = strings.Split(opt.Keyword, " ")[0] + if len(opt.Keyword) == 0 { return us, nil } - key = strings.ToLower(key) + opt.Keyword = strings.ToLower(opt.Keyword) - us = make([]*User, 0, limit) - err = x.Limit(limit).Where("lower_name like '%" + key + "%'").Find(&us) + us = make([]*User, 0, opt.Limit) + err = x.Limit(opt.Limit).Where("type=0").And("lower_name like '%" + opt.Keyword + "%'").Find(&us) return us, err } @@ -554,3 +595,45 @@ func UnFollowUser(userId int64, unFollowId int64) (err error) { } return session.Commit() } + +func UpdateMentions(userNames []string, issueId int64) error { + users := make([]*User, 0, len(userNames)) + + if err := x.Where("name IN (?)", strings.Join(userNames, "\",\"")).OrderBy("name ASC").Find(&users); err != nil { + return err + } + + ids := make([]int64, 0, len(userNames)) + + for _, user := range users { + ids = append(ids, user.Id) + + if user.Type == INDIVIDUAL { + continue + } + + if user.NumMembers == 0 { + continue + } + + tempIds := make([]int64, 0, user.NumMembers) + + orgUsers, err := GetOrgUsersByOrgId(user.Id) + + if err != nil { + return err + } + + for _, orgUser := range orgUsers { + tempIds = append(tempIds, orgUser.Id) + } + + ids = append(ids, tempIds...) + } + + if err := UpdateIssueUserPairsByMentions(ids, issueId); err != nil { + return err + } + + return nil +} diff --git a/models/webhook.go b/models/webhook.go index 9044befb..ced79366 100644 --- a/models/webhook.go +++ b/models/webhook.go @@ -12,6 +12,7 @@ import ( "github.com/gogits/gogs/modules/httplib" "github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/setting" + "github.com/gogits/gogs/modules/uuid" ) var ( @@ -47,7 +48,7 @@ type Webhook struct { func (w *Webhook) GetEvent() { w.HookEvent = &HookEvent{} if err := json.Unmarshal([]byte(w.Events), w.HookEvent); err != nil { - log.Error("webhook.GetEvent(%d): %v", w.Id, err) + log.Error(4, "webhook.GetEvent(%d): %v", w.Id, err) } } @@ -98,7 +99,7 @@ func GetWebhooksByRepoId(repoId int64) (ws []*Webhook, err error) { // UpdateWebhook updates information of webhook. func UpdateWebhook(w *Webhook) error { - _, err := x.AllCols().Update(w) + _, err := x.Id(w.Id).AllCols().Update(w) return err } @@ -122,6 +123,12 @@ const ( SERVICE ) +type HookEventType string + +const ( + PUSH HookEventType = "push" +) + type PayloadAuthor struct { Name string `json:"name"` Email string `json:"email"` @@ -157,13 +164,16 @@ type Payload struct { // HookTask represents a hook task. type HookTask struct { Id int64 + Uuid string Type HookTaskType Url string *Payload `xorm:"-"` PayloadContent string `xorm:"TEXT"` ContentType HookContentType + EventType HookEventType IsSsl bool IsDeliveried bool + IsSucceed bool } // CreateHookTask creates a new hook task, @@ -173,6 +183,7 @@ func CreateHookTask(t *HookTask) error { if err != nil { return err } + t.Uuid = uuid.NewV4().String() t.PayloadContent = string(data) _, err = x.Insert(t) return err @@ -190,20 +201,32 @@ func DeliverHooks() { x.Where("is_deliveried=?", false).Iterate(new(HookTask), func(idx int, bean interface{}) error { t := bean.(*HookTask) - // Only support JSON now. - if _, err := httplib.Post(t.Url).SetTimeout(timeout, timeout). - Body([]byte(t.PayloadContent)).Response(); err != nil { - log.Error("webhook.DeliverHooks(Delivery): %v", err) - return nil + req := httplib.Post(t.Url).SetTimeout(timeout, timeout). + Header("X-Gogs-Delivery", t.Uuid). + Header("X-Gogs-Event", string(t.EventType)) + + switch t.ContentType { + case JSON: + req = req.Header("Content-Type", "application/json").Body(t.PayloadContent) + case FORM: + req.Param("payload", t.PayloadContent) } t.IsDeliveried = true + + // TODO: record response. + if _, err := req.Response(); err != nil { + log.Error(4, "Delivery: %v", err) + } else { + t.IsSucceed = true + } + if err := UpdateHookTask(t); err != nil { - log.Error("webhook.DeliverHooks(UpdateHookTask): %v", err) + log.Error(4, "UpdateHookTask: %v", err) return nil } - log.Trace("Hook delivered: %s", t.PayloadContent) + log.Trace("Hook delivered(%s): %s", t.Uuid, t.PayloadContent) return nil }) } |