diff options
Diffstat (limited to 'models')
-rw-r--r-- | models/access.go | 14 | ||||
-rw-r--r-- | models/action.go | 64 | ||||
-rw-r--r-- | models/error.go | 68 | ||||
-rw-r--r-- | models/issue.go | 1040 | ||||
-rw-r--r-- | models/migrations/migrations.go | 155 | ||||
-rw-r--r-- | models/models.go | 3 | ||||
-rw-r--r-- | models/oauth2.go | 2 | ||||
-rw-r--r-- | models/org.go | 30 | ||||
-rw-r--r-- | models/repo.go | 168 | ||||
-rw-r--r-- | models/update.go | 4 | ||||
-rw-r--r-- | models/user.go | 98 |
11 files changed, 1191 insertions, 455 deletions
diff --git a/models/access.go b/models/access.go index 54d0f099..8d698f15 100644 --- a/models/access.go +++ b/models/access.go @@ -37,11 +37,11 @@ func accessLevel(e Engine, u *User, repo *Repository) (AccessMode, error) { } if u != nil { - if u.Id == repo.OwnerId { + if u.Id == repo.OwnerID { return ACCESS_MODE_OWNER, nil } - a := &Access{UserID: u.Id, RepoID: repo.Id} + a := &Access{UserID: u.Id, RepoID: repo.ID} if has, err := e.Get(a); !has || err != nil { return mode, err } @@ -77,7 +77,7 @@ func (u *User) GetAccessibleRepositories() (map[*Repository]AccessMode, error) { repos := make(map[*Repository]AccessMode, len(accesses)) for _, access := range accesses { - repo, err := GetRepositoryById(access.RepoID) + repo, err := GetRepositoryByID(access.RepoID) if err != nil { if IsErrRepoNotExist(err) { log.Error(4, "%v", err) @@ -87,7 +87,7 @@ func (u *User) GetAccessibleRepositories() (map[*Repository]AccessMode, error) { } if err = repo.GetOwner(); err != nil { return nil, err - } else if repo.OwnerId == u.Id { + } else if repo.OwnerID == u.Id { continue } repos[repo] = access.Mode @@ -121,13 +121,13 @@ func (repo *Repository) refreshAccesses(e Engine, accessMap map[int64]AccessMode } newAccesses = append(newAccesses, Access{ UserID: userID, - RepoID: repo.Id, + RepoID: repo.ID, Mode: mode, }) } // Delete old accesses and insert new ones for repository. - if _, err = e.Delete(&Access{RepoID: repo.Id}); err != nil { + if _, err = e.Delete(&Access{RepoID: repo.ID}); err != nil { return fmt.Errorf("delete old accesses: %v", err) } else if _, err = e.Insert(newAccesses); err != nil { return fmt.Errorf("insert new accesses: %v", err) @@ -193,7 +193,7 @@ func (repo *Repository) recalculateTeamAccesses(e Engine, ignTeamID int64) (err // have relations with repository. if t.IsOwnerTeam() { t.Authorize = ACCESS_MODE_OWNER - } else if !t.hasRepository(e, repo.Id) { + } else if !t.hasRepository(e, repo.ID) { continue } diff --git a/models/action.go b/models/action.go index 99cd1709..de7f93a9 100644 --- a/models/action.go +++ b/models/action.go @@ -124,7 +124,7 @@ func (a Action) GetIssueInfos() []string { return strings.SplitN(a.Content, "|", 2) } -func updateIssuesCommit(userId, repoId int64, repoUserName, repoName string, commits []*base.PushCommit) error { +func updateIssuesCommit(u *User, repo *Repository, repoUserName, repoName string, commits []*base.PushCommit) error { for _, c := range commits { for _, ref := range IssueReferenceKeywordsPat.FindAllString(c.Message, -1) { ref := ref[strings.IndexByte(ref, byte(' '))+1:] @@ -153,7 +153,7 @@ func updateIssuesCommit(userId, repoId int64, repoUserName, repoName string, com url := fmt.Sprintf("%s/%s/%s/commit/%s", setting.AppSubUrl, repoUserName, repoName, c.Sha1) message := fmt.Sprintf(`<a href="%s">%s</a>`, url, c.Message) - if _, err = CreateComment(userId, issue.RepoID, issue.ID, 0, 0, COMMENT_TYPE_COMMIT, message, nil); err != nil { + if _, err = CreateComment(u, repo, issue, 0, 0, COMMENT_TYPE_COMMIT_REF, message, nil); err != nil { return err } } @@ -183,7 +183,7 @@ func updateIssuesCommit(userId, repoId int64, repoUserName, repoName string, com return err } - if issue.RepoID == repoId { + if issue.RepoID == repo.ID { if issue.IsClosed { continue } @@ -202,7 +202,7 @@ func updateIssuesCommit(userId, repoId int64, repoUserName, repoName string, com if err = UpdateIssue(issue); err != nil { return err - } else if err = UpdateIssueUserPairsByStatus(issue.ID, issue.IsClosed); err != nil { + } else if err = UpdateIssueUsersByStatus(issue.ID, issue.IsClosed); err != nil { return err } @@ -211,7 +211,7 @@ func updateIssuesCommit(userId, repoId int64, repoUserName, repoName string, com } // If commit happened in the referenced repository, it means the issue can be closed. - if _, err = CreateComment(userId, repoId, issue.ID, 0, 0, COMMENT_TYPE_CLOSE, "", nil); err != nil { + if _, err = CreateComment(u, repo, issue, 0, 0, COMMENT_TYPE_CLOSE, "", nil); err != nil { return err } } @@ -242,7 +242,7 @@ func updateIssuesCommit(userId, repoId int64, repoUserName, repoName string, com return err } - if issue.RepoID == repoId { + if issue.RepoID == repo.ID { if !issue.IsClosed { continue } @@ -261,7 +261,7 @@ func updateIssuesCommit(userId, repoId int64, repoUserName, repoName string, com if err = UpdateIssue(issue); err != nil { return err - } else if err = UpdateIssueUserPairsByStatus(issue.ID, issue.IsClosed); err != nil { + } else if err = UpdateIssueUsersByStatus(issue.ID, issue.IsClosed); err != nil { return err } @@ -270,7 +270,7 @@ func updateIssuesCommit(userId, repoId int64, repoUserName, repoName string, com } // If commit happened in the referenced repository, it means the issue can be closed. - if _, err = CreateComment(userId, repoId, issue.ID, 0, 0, COMMENT_TYPE_REOPEN, "", nil); err != nil { + if _, err = CreateComment(u, repo, issue, 0, 0, COMMENT_TYPE_REOPEN, "", nil); err != nil { return err } } @@ -280,8 +280,8 @@ func updateIssuesCommit(userId, repoId int64, repoUserName, repoName string, com } // 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, oldCommitId string, newCommitId string) error { +func CommitRepoAction(userID, repoUserID int64, userName, actEmail string, + repoID int64, repoUserName, repoName string, refFullName string, commit *base.PushCommits, oldCommitID string, newCommitID string) error { opType := COMMIT_REPO // Check it's tag push or branch. @@ -292,40 +292,44 @@ func CommitRepoAction(userId, repoUserId int64, userName, actEmail string, repoLink := fmt.Sprintf("%s%s/%s", setting.AppUrl, repoUserName, repoName) // if not the first commit, set the compareUrl - if !strings.HasPrefix(oldCommitId, "0000000") { - commit.CompareUrl = fmt.Sprintf("%s/%s/compare/%s...%s", repoUserName, repoName, oldCommitId, newCommitId) + if !strings.HasPrefix(oldCommitID, "0000000") { + commit.CompareUrl = fmt.Sprintf("%s/%s/compare/%s...%s", repoUserName, repoName, oldCommitID, newCommitID) } bs, err := json.Marshal(commit) if err != nil { - return errors.New("json: " + err.Error()) + return fmt.Errorf("Marshal: %v", err) } refName := git.RefEndName(refFullName) // Change repository bare status and update last updated time. - repo, err := GetRepositoryByName(repoUserId, repoName) + repo, err := GetRepositoryByName(repoUserID, repoName) if err != nil { - return errors.New("GetRepositoryByName: " + err.Error()) + return fmt.Errorf("GetRepositoryByName: %v", err) } repo.IsBare = false if err = UpdateRepository(repo, false); err != nil { - return errors.New("UpdateRepository: " + err.Error()) + return fmt.Errorf("UpdateRepository: %v", err) } - err = updateIssuesCommit(userId, repoId, repoUserName, repoName, commit.Commits) + u, err := GetUserByID(userID) + if err != nil { + return fmt.Errorf("GetUserByID: %v", err) + } + err = updateIssuesCommit(u, repo, repoUserName, repoName, commit.Commits) if err != nil { log.Debug("updateIssuesCommit: ", err) } if err = NotifyWatchers(&Action{ - ActUserID: userId, + ActUserID: u.Id, ActUserName: userName, ActEmail: actEmail, OpType: opType, Content: string(bs), - RepoID: repoId, + RepoID: repo.ID, RepoUserName: repoUserName, RepoName: repoName, RefName: refName, @@ -340,7 +344,7 @@ func CommitRepoAction(userId, repoUserId int64, userName, actEmail string, return errors.New("GetOwner: " + err.Error()) } - ws, err := GetActiveWebhooksByRepoId(repoId) + ws, err := GetActiveWebhooksByRepoId(repo.ID) if err != nil { return errors.New("GetActiveWebhooksByRepoId: " + err.Error()) } @@ -348,7 +352,7 @@ func CommitRepoAction(userId, repoUserId int64, userName, actEmail string, // check if repo belongs to org and append additional webhooks if repo.Owner.IsOrganization() { // get hooks for org - orgws, err := GetActiveWebhooksByOrgId(repo.OwnerId) + orgws, err := GetActiveWebhooksByOrgId(repo.OwnerID) if err != nil { return errors.New("GetActiveWebhooksByOrgId: " + err.Error()) } @@ -388,7 +392,7 @@ func CommitRepoAction(userId, repoUserId int64, userName, actEmail string, Ref: refFullName, Commits: commits, Repo: &PayloadRepo{ - Id: repo.Id, + Id: repo.ID, Name: repo.LowerName, Url: repoLink, Description: repo.Description, @@ -406,8 +410,8 @@ func CommitRepoAction(userId, repoUserId int64, userName, actEmail string, Email: pusher_email, UserName: userName, }, - Before: oldCommitId, - After: newCommitId, + Before: oldCommitID, + After: newCommitID, CompareUrl: setting.AppUrl + commit.CompareUrl, } @@ -431,7 +435,7 @@ func CommitRepoAction(userId, repoUserId int64, userName, actEmail string, } if err = CreateHookTask(&HookTask{ - RepoID: repo.Id, + RepoID: repo.ID, HookID: w.Id, Type: w.HookTaskType, Url: w.Url, @@ -453,12 +457,12 @@ func newRepoAction(e Engine, u *User, repo *Repository) (err error) { ActUserName: u.Name, ActEmail: u.Email, OpType: CREATE_REPO, - RepoID: repo.Id, + RepoID: repo.ID, RepoUserName: repo.Owner.Name, RepoName: repo.Name, IsPrivate: repo.IsPrivate, }); err != nil { - return fmt.Errorf("notify watchers '%d/%s'", u.Id, repo.Id) + return fmt.Errorf("notify watchers '%d/%s'", u.Id, repo.ID) } log.Trace("action.NewRepoAction: %s/%s", u.Name, repo.Name) @@ -476,19 +480,19 @@ func transferRepoAction(e Engine, actUser, oldOwner, newOwner *User, repo *Repos ActUserName: actUser.Name, ActEmail: actUser.Email, OpType: TRANSFER_REPO, - RepoID: repo.Id, + RepoID: repo.ID, RepoUserName: newOwner.Name, RepoName: repo.Name, IsPrivate: repo.IsPrivate, Content: path.Join(oldOwner.LowerName, repo.LowerName), } if err = notifyWatchers(e, action); err != nil { - return fmt.Errorf("notify watchers '%d/%s'", actUser.Id, repo.Id) + return fmt.Errorf("notify watchers '%d/%s'", actUser.Id, repo.ID) } // Remove watch for organization. if repo.Owner.IsOrganization() { - if err = watchRepo(e, repo.Owner.Id, repo.Id, false); err != nil { + if err = watchRepo(e, repo.Owner.Id, repo.ID, false); err != nil { return fmt.Errorf("watch repository: %v", err) } } diff --git a/models/error.go b/models/error.go index 0e554e52..c3e48063 100644 --- a/models/error.go +++ b/models/error.go @@ -239,6 +239,48 @@ func (err ErrRepoAlreadyExist) Error() string { return fmt.Sprintf("repository already exists [uname: %d, name: %s]", err.Uname, err.Name) } +// .___ +// | | ______ ________ __ ____ +// | |/ ___// ___/ | \_/ __ \ +// | |\___ \ \___ \| | /\ ___/ +// |___/____ >____ >____/ \___ > +// \/ \/ \/ + +type ErrIssueNotExist struct { + ID int64 + RepoID int64 + Index int64 +} + +func IsErrIssueNotExist(err error) bool { + _, ok := err.(ErrIssueNotExist) + return ok +} + +func (err ErrIssueNotExist) Error() string { + return fmt.Sprintf("issue does not exist [id: %d, repo_id: %d, index: %4]", err.ID, err.RepoID, err.Index) +} + +// .____ ___. .__ +// | | _____ \_ |__ ____ | | +// | | \__ \ | __ \_/ __ \| | +// | |___ / __ \| \_\ \ ___/| |__ +// |_______ (____ /___ /\___ >____/ +// \/ \/ \/ \/ + +type ErrLabelNotExist struct { + ID int64 +} + +func IsErrLabelNotExist(err error) bool { + _, ok := err.(ErrLabelNotExist) + return ok +} + +func (err ErrLabelNotExist) Error() string { + return fmt.Sprintf("label does not exist [id: %d]", err.ID) +} + // _____ .__.__ __ // / \ |__| | ____ _______/ |_ ____ ____ ____ // / \ / \| | | _/ __ \ / ___/\ __\/ _ \ / \_/ __ \ @@ -247,7 +289,8 @@ func (err ErrRepoAlreadyExist) Error() string { // \/ \/ \/ \/ \/ type ErrMilestoneNotExist struct { - ID int64 + ID int64 + RepoID int64 } func IsErrMilestoneNotExist(err error) bool { @@ -256,5 +299,26 @@ func IsErrMilestoneNotExist(err error) bool { } func (err ErrMilestoneNotExist) Error() string { - return fmt.Sprintf("milestone does not exist [id: %d]", err.ID) + return fmt.Sprintf("milestone does not exist [id: %d, repo_id: %d]", err.ID, err.RepoID) +} + +// _____ __ __ .__ __ +// / _ \_/ |__/ |______ ____ | |__ _____ ____ _____/ |_ +// / /_\ \ __\ __\__ \ _/ ___\| | \ / \_/ __ \ / \ __\ +// / | \ | | | / __ \\ \___| Y \ Y Y \ ___/| | \ | +// \____|__ /__| |__| (____ /\___ >___| /__|_| /\___ >___| /__| +// \/ \/ \/ \/ \/ \/ \/ + +type ErrAttachmentNotExist struct { + ID int64 + UUID string +} + +func IsErrAttachmentNotExist(err error) bool { + _, ok := err.(ErrAttachmentNotExist) + return ok +} + +func (err ErrAttachmentNotExist) Error() string { + return fmt.Sprintf("attachment does not exist [id: %d, uuid: %s]", err.ID, err.UUID) } diff --git a/models/issue.go b/models/issue.go index d423f056..7f16f936 100644 --- a/models/issue.go +++ b/models/issue.go @@ -7,8 +7,11 @@ package models import ( "bytes" "errors" - "html/template" + "fmt" + "io" + "mime/multipart" "os" + "path" "strconv" "strings" "time" @@ -16,15 +19,14 @@ import ( "github.com/Unknwon/com" "github.com/go-xorm/xorm" + "github.com/gogits/gogs/modules/base" "github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/setting" + gouuid "github.com/gogits/gogs/modules/uuid" ) var ( - ErrIssueNotExist = errors.New("Issue does not exist") - ErrLabelNotExist = errors.New("Label 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") ) @@ -38,7 +40,6 @@ type Issue struct { Repo *Repository `xorm:"-"` PosterID int64 Poster *User `xorm:"-"` - LabelIds string `xorm:"TEXT"` Labels []*Label `xorm:"-"` MilestoneID int64 Milestone *Milestone `xorm:"-"` @@ -54,78 +55,196 @@ type Issue struct { Deadline time.Time Created time.Time `xorm:"CREATED"` Updated time.Time `xorm:"UPDATED"` + + Attachments []*Attachment `xorm:"-"` + Comments []*Comment `xorm:"-"` +} + +// HashTag returns unique hash tag for issue. +func (i *Issue) HashTag() string { + return "issue-" + com.ToStr(i.ID) } func (i *Issue) AfterSet(colName string, _ xorm.Cell) { var err error switch colName { + case "id": + i.Attachments, err = GetAttachmentsByIssueID(i.ID) + if err != nil { + log.Error(3, "GetAttachmentsByIssueID[%d]: %v", i.ID, err) + } + + i.Comments, err = GetCommentsByIssueID(i.ID) + if err != nil { + log.Error(3, "GetCommentsByIssueID[%d]: %v", i.ID, err) + } + case "milestone_id": + if i.MilestoneID == 0 { + return + } + i.Milestone, err = GetMilestoneByID(i.MilestoneID) if err != nil { - log.Error(3, "GetMilestoneById: %v", err) + log.Error(3, "GetMilestoneById[%d]: %v", i.ID, err) + } + case "assignee_id": + if i.AssigneeID == 0 { + return + } + + i.Assignee, err = GetUserByID(i.AssigneeID) + if err != nil { + log.Error(3, "GetUserByID[%d]: %v", i.ID, err) } } } +// IsPoster returns true if given user by ID is the poster. +func (i *Issue) IsPoster(uid int64) bool { + return i.PosterID == uid +} + func (i *Issue) GetPoster() (err error) { - i.Poster, err = GetUserById(i.PosterID) + i.Poster, err = GetUserByID(i.PosterID) if IsErrUserNotExist(err) { - i.Poster = &User{Name: "FakeUser"} + i.Poster = &User{Name: "Someone"} return nil } return err } -func (i *Issue) GetLabels() error { - if len(i.LabelIds) < 3 { +func (i *Issue) hasLabel(e Engine, labelID int64) bool { + return hasIssueLabel(e, i.ID, labelID) +} + +// HasLabel returns true if issue has been labeled by given ID. +func (i *Issue) HasLabel(labelID int64) bool { + return i.hasLabel(x, labelID) +} + +func (i *Issue) addLabel(e Engine, labelID int64) error { + return newIssueLabel(e, i.ID, labelID) +} + +// AddLabel adds new label to issue by given ID. +func (i *Issue) AddLabel(labelID int64) error { + return i.addLabel(x, labelID) +} + +func (i *Issue) getLabels(e Engine) (err error) { + if len(i.Labels) > 0 { return nil } - strIds := strings.Split(strings.TrimSuffix(i.LabelIds[1:], "|"), "|$") - i.Labels = make([]*Label, 0, len(strIds)) - for _, strId := range strIds { - id := com.StrTo(strId).MustInt64() - if id > 0 { - l, err := GetLabelById(id) - if err != nil { - if err == ErrLabelNotExist { - continue - } - return err - } - i.Labels = append(i.Labels, l) - } + i.Labels, err = getLabelsByIssueID(e, i.ID) + if err != nil { + return fmt.Errorf("getLabelsByIssueID: %v", err) } return nil } +// GetLabels retrieves all labels of issue and assign to corresponding field. +func (i *Issue) GetLabels() error { + return i.getLabels(x) +} + +func (i *Issue) removeLabel(e Engine, labelID int64) error { + return deleteIssueLabel(e, i.ID, labelID) +} + +// RemoveLabel removes a label from issue by given ID. +func (i *Issue) RemoveLabel(labelID int64) error { + return i.removeLabel(x, labelID) +} + func (i *Issue) GetAssignee() (err error) { - if i.AssigneeID == 0 { + if i.AssigneeID == 0 || i.Assignee != nil { return nil } - i.Assignee, err = GetUserById(i.AssigneeID) + i.Assignee, err = GetUserByID(i.AssigneeID) if IsErrUserNotExist(err) { return nil } return err } -func (i *Issue) Attachments() []*Attachment { - a, _ := GetAttachmentsForIssue(i.ID) - return a +// ReadBy sets issue to be read by given user. +func (i *Issue) ReadBy(uid int64) error { + return UpdateIssueUserByRead(uid, i.ID) +} + +func (i *Issue) changeStatus(e *xorm.Session, doer *User, isClosed bool) (err error) { + if i.IsClosed == isClosed { + return nil + } + i.IsClosed = isClosed + + if err = updateIssue(e, i); err != nil { + return err + } else if err = updateIssueUsersByStatus(e, i.ID, isClosed); err != nil { + return err + } + + // Update labels. + if err = i.getLabels(e); err != nil { + return err + } + for idx := range i.Labels { + if i.IsClosed { + i.Labels[idx].NumClosedIssues++ + } else { + i.Labels[idx].NumClosedIssues-- + } + if err = updateLabel(e, i.Labels[idx]); err != nil { + return err + } + } + + // Update milestone. + if err = changeMilestoneIssueStats(e, i); err != nil { + return err + } + + // New action comment. + if _, err = createStatusComment(e, doer, i.Repo, i); err != nil { + return err + } + + return nil } -func (i *Issue) AfterDelete() { - _, err := DeleteAttachmentsByIssue(i.ID, true) +// ChangeStatus changes issue status to open/closed. +func (i *Issue) ChangeStatus(doer *User, isClosed bool) (err error) { + sess := x.NewSession() + defer sessionRelease(sess) + if err = sess.Begin(); err != nil { + return err + } - if err != nil { - log.Info("Could not delete files for issue #%d: %s", i.ID, err) + if err = i.changeStatus(sess, doer, isClosed); err != nil { + return err } + + return sess.Commit() } -// CreateIssue creates new issue for repository. -func NewIssue(issue *Issue) (err error) { +// CreateIssue creates new issue with labels for repository. +func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) { + // Check attachments. + attachments := make([]*Attachment, 0, len(uuids)) + for _, uuid := range uuids { + attach, err := GetAttachmentByUUID(uuid) + if err != nil { + if IsErrAttachmentNotExist(err) { + continue + } + return fmt.Errorf("GetAttachmentByUUID[%s]: %v", uuid, err) + } + attachments = append(attachments, attach) + } + sess := x.NewSession() defer sessionRelease(sess) if err = sess.Begin(); err != nil { @@ -134,20 +253,51 @@ func NewIssue(issue *Issue) (err error) { if _, err = sess.Insert(issue); err != nil { return err - } else if _, err = sess.Exec("UPDATE `repository` SET num_issues = num_issues + 1 WHERE id = ?", issue.RepoID); err != nil { + } else if _, err = sess.Exec("UPDATE `repository` SET num_issues=num_issues+1 WHERE id=?", issue.RepoID); err != nil { return err } - if err = sess.Commit(); err != nil { - return err + for _, id := range labelIDs { + if err = issue.addLabel(sess, id); err != nil { + return fmt.Errorf("addLabel: %v", err) + } } if issue.MilestoneID > 0 { - // FIXES(280): Update milestone counter. - return ChangeMilestoneAssign(0, issue.MilestoneID, issue) + if err = changeMilestoneAssign(sess, 0, issue); err != nil { + return err + } + } + + if err = newIssueUsers(sess, repo, issue); err != nil { + return err + } + + for i := range attachments { + attachments[i].IssueID = issue.ID + // No assign value could be 0, so ignore AllCols(). + if _, err = sess.Id(attachments[i].ID).Update(attachments[i]); err != nil { + return fmt.Errorf("update attachment[%d]: %v", attachments[i].ID, err) + } } - return + // Notify watchers. + act := &Action{ + ActUserID: issue.Poster.Id, + ActUserName: issue.Poster.Name, + ActEmail: issue.Poster.Email, + OpType: CREATE_ISSUE, + Content: fmt.Sprintf("%d|%s", issue.Index, issue.Name), + RepoID: repo.ID, + RepoUserName: repo.Owner.Name, + RepoName: repo.Name, + IsPrivate: repo.IsPrivate, + } + if err = notifyWatchers(sess, act); err != nil { + return err + } + + return sess.Commit() } // GetIssueByRef returns an Issue specified by a GFM reference. @@ -170,35 +320,38 @@ func GetIssueByRef(ref string) (issue *Issue, err error) { return } - return GetIssueByIndex(repo.Id, issueNumber) + return GetIssueByIndex(repo.ID, issueNumber) } // GetIssueByIndex returns issue by given index in repository. -func GetIssueByIndex(rid, index int64) (*Issue, error) { - issue := &Issue{RepoID: rid, Index: index} +func GetIssueByIndex(repoID, index int64) (*Issue, error) { + issue := &Issue{ + RepoID: repoID, + Index: index, + } has, err := x.Get(issue) if err != nil { return nil, err } else if !has { - return nil, ErrIssueNotExist + return nil, ErrIssueNotExist{0, repoID, index} } return issue, nil } -// GetIssueById returns an issue by ID. -func GetIssueById(id int64) (*Issue, error) { - issue := &Issue{ID: id} - has, err := x.Get(issue) +// GetIssueByID returns an issue by given ID. +func GetIssueByID(id int64) (*Issue, error) { + issue := new(Issue) + has, err := x.Id(id).Get(issue) if err != nil { return nil, err } else if !has { - return nil, ErrIssueNotExist + return nil, ErrIssueNotExist{id, 0, 0} } return issue, nil } // Issues returns a list of issues by given conditions. -func Issues(uid, assigneeID, repoID, posterID, milestoneID int64, page int, isClosed, isMention bool, labelIds, sortType string) ([]*Issue, error) { +func Issues(uid, assigneeID, repoID, posterID, milestoneID int64, page int, isClosed, isMention bool, labels, sortType string) ([]*Issue, error) { sess := x.Limit(setting.IssuePagingNum, (page-1)*setting.IssuePagingNum) if repoID > 0 { @@ -217,14 +370,6 @@ func Issues(uid, assigneeID, repoID, posterID, milestoneID int64, page int, isCl sess.And("issue.milestone_id=?", milestoneID) } - if len(labelIds) > 0 { - for _, label := range strings.Split(labelIds, ",") { - if com.StrTo(label).MustInt() > 0 { - sess.And("label_ids like ?", "%$"+label+"|%") - } - } - } - switch sortType { case "oldest": sess.Asc("created") @@ -242,10 +387,26 @@ func Issues(uid, assigneeID, repoID, posterID, milestoneID int64, page int, isCl sess.Desc("created") } + labelIDs := base.StringsToInt64s(strings.Split(labels, ",")) + if len(labelIDs) > 0 { + validJoin := false + queryStr := "issue.id=issue_label.issue_id" + for _, id := range labelIDs { + if id == 0 { + continue + } + validJoin = true + queryStr += " AND issue_label.label_id=" + com.ToStr(id) + } + if validJoin { + sess.Join("INNER", "issue_label", queryStr) + } + } + if isMention { - queryStr := "issue.id = issue_user.issue_id AND issue_user.is_mentioned=1" + queryStr := "issue.id=issue_user.issue_id AND issue_user.is_mentioned=1" if uid > 0 { - queryStr += " AND issue_user.uid = " + com.ToStr(uid) + queryStr += " AND issue_user.uid=" + com.ToStr(uid) } sess.Join("INNER", "issue_user", queryStr) } @@ -261,12 +422,6 @@ const ( IS_CLOSE ) -// GetIssuesByLabel returns a list of issues by given label and repository. -func GetIssuesByLabel(repoID, labelID int64) ([]*Issue, error) { - issues := make([]*Issue, 0, 10) - return issues, x.Where("repo_id=?", repoID).And("label_ids like '%$" + com.ToStr(labelID) + "|%'").Find(&issues) -} - // GetIssueCountByPoster returns number of issues of repository by poster. func GetIssueCountByPoster(uid, rid int64, isClosed bool) int64 { count, _ := x.Where("repo_id=?", rid).And("poster_id=?", uid).And("is_closed=?", isClosed).Count(new(Issue)) @@ -282,11 +437,11 @@ func GetIssueCountByPoster(uid, rid int64, isClosed bool) int64 { // IssueUser represents an issue-user relation. type IssueUser struct { - Id int64 - Uid int64 `xorm:"INDEX"` // User ID. - IssueId int64 - RepoId int64 `xorm:"INDEX"` - MilestoneId int64 + ID int64 `xorm:"pk autoincr"` + UID int64 `xorm:"uid INDEX"` // User ID. + IssueID int64 + RepoID int64 `xorm:"INDEX"` + MilestoneID int64 IsRead bool IsAssigned bool IsMentioned bool @@ -294,70 +449,72 @@ type IssueUser struct { IsClosed bool } -// FIXME: organization -// NewIssueUserPairs adds new issue-user pairs for new issue of repository. -func NewIssueUserPairs(repo *Repository, issueID, orgID, posterID, assigneeID int64) error { - users, err := repo.GetCollaborators() +func newIssueUsers(e *xorm.Session, repo *Repository, issue *Issue) error { + users, err := repo.GetAssignees() if err != nil { return err } iu := &IssueUser{ - IssueId: issueID, - RepoId: repo.Id, + IssueID: issue.ID, + RepoID: repo.ID, } + // Poster can be anyone. isNeedAddPoster := true for _, u := range users { - iu.Id = 0 - iu.Uid = u.Id - iu.IsPoster = iu.Uid == posterID + iu.ID = 0 + iu.UID = u.Id + iu.IsPoster = iu.UID == issue.PosterID if isNeedAddPoster && iu.IsPoster { isNeedAddPoster = false } - iu.IsAssigned = iu.Uid == assigneeID - if _, err = x.Insert(iu); err != nil { + iu.IsAssigned = iu.UID == issue.AssigneeID + if _, err = e.Insert(iu); err != nil { return err } } if isNeedAddPoster { - iu.Id = 0 - iu.Uid = posterID + iu.ID = 0 + iu.UID = issue.PosterID iu.IsPoster = true - iu.IsAssigned = iu.Uid == assigneeID - if _, err = x.Insert(iu); err != nil { + if _, err = e.Insert(iu); err != nil { return err } } + return nil +} - // Add owner's as well. - if repo.OwnerId != posterID { - iu.Id = 0 - iu.Uid = repo.OwnerId - iu.IsAssigned = iu.Uid == assigneeID - if _, err = x.Insert(iu); err != nil { - return err - } +// NewIssueUsers adds new issue-user relations for new issue of repository. +func NewIssueUsers(repo *Repository, issue *Issue) (err error) { + sess := x.NewSession() + defer sessionRelease(sess) + if err = sess.Begin(); err != nil { + return err } - return nil + if err = newIssueUsers(sess, repo, issue); err != nil { + return err + } + + return sess.Commit() } // PairsContains returns true when pairs list contains given issue. func PairsContains(ius []*IssueUser, issueId, uid int64) int { for i := range ius { - if ius[i].IssueId == issueId && - ius[i].Uid == uid { + if ius[i].IssueID == issueId && + ius[i].UID == uid { return i } } return -1 } -// GetIssueUserPairs returns issue-user pairs by given repository and user. -func GetIssueUserPairs(rid, uid int64, isClosed bool) ([]*IssueUser, error) { +// GetIssueUsers returns issue-user pairs by given repository and user. +func GetIssueUsers(rid, uid int64, isClosed bool) ([]*IssueUser, error) { ius := make([]*IssueUser, 0, 10) - err := x.Where("is_closed=?", isClosed).Find(&ius, &IssueUser{RepoId: rid, Uid: uid}) + err := x.Where("is_closed=?", isClosed).Find(&ius, &IssueUser{RepoID: rid, UID: uid}) return ius, err } @@ -420,50 +577,58 @@ const ( FM_MENTION ) +func parseCountResult(results []map[string][]byte) int64 { + if len(results) == 0 { + return 0 + } + for _, result := range results[0] { + return com.StrTo(string(result)).MustInt64() + } + return 0 +} + // GetIssueStats returns issue statistic information by given conditions. func GetIssueStats(repoID, uid, labelID, milestoneID int64, isShowClosed bool, filterMode int) *IssueStats { stats := &IssueStats{} - issue := new(Issue) + // issue := new(Issue) - queryStr := "issue.repo_id=? AND issue.is_closed=?" + queryStr := "SELECT COUNT(*) FROM `issue` " if labelID > 0 { - queryStr += " AND issue.label_ids like '%$" + com.ToStr(labelID) + "|%'" + queryStr += "INNER JOIN `issue_label` ON `issue`.id=`issue_label`.issue_id AND `issue_label`.label_id=" + com.ToStr(labelID) } + + baseCond := " WHERE issue.repo_id=? AND issue.is_closed=?" if milestoneID > 0 { - queryStr += " AND milestone_id=" + com.ToStr(milestoneID) + baseCond += " AND issue.milestone_id=" + com.ToStr(milestoneID) } switch filterMode { case FM_ALL: - stats.OpenCount, _ = x.Where(queryStr, repoID, false).Count(issue) - stats.ClosedCount, _ = x.Where(queryStr, repoID, true).Count(issue) - return stats + resutls, _ := x.Query(queryStr+baseCond, repoID, false) + stats.OpenCount = parseCountResult(resutls) + resutls, _ = x.Query(queryStr+baseCond, repoID, true) + stats.ClosedCount = parseCountResult(resutls) case FM_ASSIGN: - queryStr += " AND assignee_id=?" - stats.OpenCount, _ = x.Where(queryStr, repoID, false, uid).Count(issue) - stats.ClosedCount, _ = x.Where(queryStr, repoID, true, uid).Count(issue) - return stats + baseCond += " AND assignee_id=?" + resutls, _ := x.Query(queryStr+baseCond, repoID, false, uid) + stats.OpenCount = parseCountResult(resutls) + resutls, _ = x.Query(queryStr+baseCond, repoID, true, uid) + stats.ClosedCount = parseCountResult(resutls) case FM_CREATE: - queryStr += " AND poster_id=?" - stats.OpenCount, _ = x.Where(queryStr, repoID, false, uid).Count(issue) - stats.ClosedCount, _ = x.Where(queryStr, repoID, true, uid).Count(issue) - return stats + baseCond += " AND poster_id=?" + resutls, _ := x.Query(queryStr+baseCond, repoID, false, uid) + stats.OpenCount = parseCountResult(resutls) + resutls, _ = x.Query(queryStr+baseCond, repoID, true, uid) + stats.ClosedCount = parseCountResult(resutls) case FM_MENTION: - queryStr += " AND uid=? AND is_mentioned=?" - if labelID > 0 { - stats.OpenCount, _ = x.Where(queryStr, repoID, false, uid, true). - Join("INNER", "issue", "issue.id = issue_id").Count(new(IssueUser)) - stats.ClosedCount, _ = x.Where(queryStr, repoID, true, uid, true). - Join("INNER", "issue", "issue.id = issue_id").Count(new(IssueUser)) - return stats - } - - queryStr = strings.Replace(queryStr, "issue.", "", 2) - stats.OpenCount, _ = x.Where(queryStr, repoID, false, uid, true).Count(new(IssueUser)) - stats.ClosedCount, _ = x.Where(queryStr, repoID, true, uid, true).Count(new(IssueUser)) - return stats + queryStr += " INNER JOIN `issue_user` ON `issue`.id=`issue_user`.issue_id" + baseCond += " AND `issue_user`.uid=? AND `issue_user`.is_mentioned=?" + resutls, _ := x.Query(queryStr+baseCond, repoID, false, uid, true) + stats.OpenCount = parseCountResult(resutls) + resutls, _ = x.Query(queryStr+baseCond, repoID, true, uid, true) + stats.ClosedCount = parseCountResult(resutls) } return stats } @@ -477,51 +642,64 @@ func GetUserIssueStats(uid int64, filterMode int) *IssueStats { return stats } +func updateIssue(e Engine, issue *Issue) error { + _, err := e.Id(issue.ID).AllCols().Update(issue) + return err +} + // UpdateIssue updates information of issue. func UpdateIssue(issue *Issue) error { - _, err := x.Id(issue.ID).AllCols().Update(issue) - - if err != nil { - return err - } + return updateIssue(x, issue) +} +func updateIssueUsersByStatus(e Engine, issueID int64, isClosed bool) error { + _, err := e.Exec("UPDATE `issue_user` SET is_closed=? WHERE issue_id=?", isClosed, issueID) return err } -// UpdateIssueUserByStatus updates issue-user pairs by issue status. -func UpdateIssueUserPairsByStatus(iid int64, isClosed bool) error { - rawSql := "UPDATE `issue_user` SET is_closed = ? WHERE issue_id = ?" - _, err := x.Exec(rawSql, isClosed, iid) - return err +// UpdateIssueUsersByStatus updates issue-user relations by issue status. +func UpdateIssueUsersByStatus(issueID int64, isClosed bool) error { + return updateIssueUsersByStatus(x, issueID, isClosed) } -// UpdateIssueUserPairByAssignee updates issue-user pair for assigning. -func UpdateIssueUserPairByAssignee(aid, iid int64) error { - rawSql := "UPDATE `issue_user` SET is_assigned = ? WHERE issue_id = ?" - if _, err := x.Exec(rawSql, false, iid); err != nil { +func updateIssueUserByAssignee(e *xorm.Session, issueID, assigneeID int64) (err error) { + if _, err = e.Exec("UPDATE `issue_user` SET is_assigned=? WHERE issue_id=?", false, issueID); err != nil { return err } // Assignee ID equals to 0 means clear assignee. - if aid == 0 { + if assigneeID == 0 { return nil } - rawSql = "UPDATE `issue_user` SET is_assigned = ? WHERE uid = ? AND issue_id = ?" - _, err := x.Exec(rawSql, true, aid, iid) + _, err = e.Exec("UPDATE `issue_user` SET is_assigned=? WHERE uid=? AND issue_id=?", true, assigneeID, issueID) return err } -// UpdateIssueUserPairByRead updates issue-user pair for reading. -func UpdateIssueUserPairByRead(uid, iid int64) error { - rawSql := "UPDATE `issue_user` SET is_read = ? WHERE uid = ? AND issue_id = ?" - _, err := x.Exec(rawSql, true, uid, iid) +// UpdateIssueUserByAssignee updates issue-user relation for assignee. +func UpdateIssueUserByAssignee(issueID, assigneeID int64) (err error) { + sess := x.NewSession() + defer sessionRelease(sess) + if err = sess.Begin(); err != nil { + return err + } + + if err = updateIssueUserByAssignee(sess, issueID, assigneeID); err != nil { + return err + } + + return sess.Commit() +} + +// UpdateIssueUserByRead updates issue-user relation for reading. +func UpdateIssueUserByRead(uid, issueID int64) error { + _, err := x.Exec("UPDATE `issue_user` SET is_read=? WHERE uid=? AND issue_id=?", true, uid, issueID) return err } -// UpdateIssueUserPairsByMentions updates issue-user pairs by mentioning. -func UpdateIssueUserPairsByMentions(uids []int64, iid int64) error { +// UpdateIssueUsersByMentions updates issue-user pairs by mentioning. +func UpdateIssueUsersByMentions(uids []int64, iid int64) error { for _, uid := range uids { - iu := &IssueUser{Uid: uid, IssueId: iid} + iu := &IssueUser{UID: uid, IssueID: iid} has, err := x.Get(iu) if err != nil { return err @@ -529,7 +707,7 @@ func UpdateIssueUserPairsByMentions(uids []int64, iid int64) error { iu.IsMentioned = true if has { - _, err = x.Id(iu.Id).AllCols().Update(iu) + _, err = x.Id(iu.ID).AllCols().Update(iu) } else { _, err = x.Insert(iu) } @@ -550,7 +728,7 @@ func UpdateIssueUserPairsByMentions(uids []int64, iid int64) error { // Label represents a label of repository for issues. type Label struct { ID int64 `xorm:"pk autoincr"` - RepoId int64 `xorm:"INDEX"` + RepoID int64 `xorm:"INDEX"` Name string Color string `xorm:"VARCHAR(7)"` NumIssues int @@ -570,10 +748,9 @@ func NewLabel(l *Label) error { return err } -// GetLabelById returns a label by given ID. -func GetLabelById(id int64) (*Label, error) { +func getLabelByID(e Engine, id int64) (*Label, error) { if id <= 0 { - return nil, ErrLabelNotExist + return nil, ErrLabelNotExist{id} } l := &Label{ID: id} @@ -581,58 +758,143 @@ func GetLabelById(id int64) (*Label, error) { if err != nil { return nil, err } else if !has { - return nil, ErrLabelNotExist + return nil, ErrLabelNotExist{l.ID} } return l, nil } -// GetLabels returns a list of labels of given repository ID. -func GetLabels(repoId int64) ([]*Label, error) { +// GetLabelByID returns a label by given ID. +func GetLabelByID(id int64) (*Label, error) { + return getLabelByID(x, id) +} + +// GetLabelsByRepoID returns all labels that belong to given repository by ID. +func GetLabelsByRepoID(repoID int64) ([]*Label, error) { labels := make([]*Label, 0, 10) - err := x.Where("repo_id=?", repoId).Find(&labels) - return labels, err + return labels, x.Where("repo_id=?", repoID).Find(&labels) +} + +func getLabelsByIssueID(e Engine, issueID int64) ([]*Label, error) { + issueLabels, err := getIssueLabels(e, issueID) + if err != nil { + return nil, fmt.Errorf("getIssueLabels: %v", err) + } + + var label *Label + labels := make([]*Label, 0, len(issueLabels)) + for idx := range issueLabels { + label, err = getLabelByID(e, issueLabels[idx].LabelID) + if err != nil && !IsErrLabelNotExist(err) { + return nil, fmt.Errorf("getLabelByID: %v", err) + } + labels = append(labels, label) + } + return labels, nil +} + +// GetLabelsByIssueID returns all labels that belong to given issue by ID. +func GetLabelsByIssueID(issueID int64) ([]*Label, error) { + return getLabelsByIssueID(x, issueID) +} + +func updateLabel(e Engine, l *Label) error { + _, err := e.Id(l.ID).AllCols().Update(l) + return err } // UpdateLabel updates label information. func UpdateLabel(l *Label) error { - _, err := x.Id(l.ID).AllCols().Update(l) - return err + return updateLabel(x, l) } // DeleteLabel delete a label of given repository. func DeleteLabel(repoID, labelID int64) error { - l, err := GetLabelById(labelID) + l, err := GetLabelByID(labelID) if err != nil { - if err == ErrLabelNotExist { + if IsErrLabelNotExist(err) { return nil } return err } - issues, err := GetIssuesByLabel(repoID, labelID) - if err != nil { - return err - } - sess := x.NewSession() defer sessionRelease(sess) if err = sess.Begin(); err != nil { return err } - for _, issue := range issues { - issue.LabelIds = strings.Replace(issue.LabelIds, "$"+com.ToStr(labelID)+"|", "", -1) - if _, err = sess.Id(issue.ID).AllCols().Update(issue); err != nil { - return err - } - } - - if _, err = sess.Delete(l); err != nil { + if _, err = x.Where("label_id=?", labelID).Delete(new(IssueLabel)); err != nil { + return err + } else if _, err = sess.Delete(l); err != nil { return err } return sess.Commit() } +// .___ .____ ___. .__ +// | | ______ ________ __ ____ | | _____ \_ |__ ____ | | +// | |/ ___// ___/ | \_/ __ \| | \__ \ | __ \_/ __ \| | +// | |\___ \ \___ \| | /\ ___/| |___ / __ \| \_\ \ ___/| |__ +// |___/____ >____ >____/ \___ >_______ (____ /___ /\___ >____/ +// \/ \/ \/ \/ \/ \/ \/ + +// IssueLabel represetns an issue-lable relation. +type IssueLabel struct { + ID int64 `xorm:"pk autoincr"` + IssueID int64 `xorm:"UNIQUE(s)"` + LabelID int64 `xorm:"UNIQUE(s)"` +} + +func hasIssueLabel(e Engine, issueID, labelID int64) bool { + has, _ := e.Where("issue_id=? AND label_id=?", issueID, labelID).Get(new(IssueLabel)) + return has +} + +// HasIssueLabel returns true if issue has been labeled. +func HasIssueLabel(issueID, labelID int64) bool { + return hasIssueLabel(x, issueID, labelID) +} + +func newIssueLabel(e Engine, issueID, labelID int64) error { + if issueID == 0 || labelID == 0 { + return nil + } + + _, err := e.Insert(&IssueLabel{ + IssueID: issueID, + LabelID: labelID, + }) + return err +} + +// NewIssueLabel creates a new issue-label relation. +func NewIssueLabel(issueID, labelID int64) error { + return newIssueLabel(x, issueID, labelID) +} + +func getIssueLabels(e Engine, issueID int64) ([]*IssueLabel, error) { + issueLabels := make([]*IssueLabel, 0, 10) + return issueLabels, e.Where("issue_id=?", issueID).Asc("label_id").Find(&issueLabels) +} + +// GetIssueLabels returns all issue-label relations of given issue by ID. +func GetIssueLabels(issueID int64) ([]*IssueLabel, error) { + return getIssueLabels(x, issueID) +} + +func deleteIssueLabel(e Engine, issueID, labelID int64) error { + _, err := e.Delete(&IssueLabel{ + IssueID: issueID, + LabelID: labelID, + }) + return err +} + +// DeleteIssueLabel deletes issue-label relation. +func DeleteIssueLabel(issueID, labelID int64) error { + return deleteIssueLabel(x, issueID, labelID) +} + // _____ .__.__ __ // / \ |__| | ____ _______/ |_ ____ ____ ____ // / \ / \| | | _/ __ \ / ___/\ __\/ _ \ / \_/ __ \ @@ -658,6 +920,14 @@ type Milestone struct { ClosedDate time.Time } +func (m *Milestone) BeforeUpdate() { + if m.NumIssues > 0 { + m.Completeness = m.NumClosedIssues * 100 / m.NumIssues + } else { + m.Completeness = 0 + } +} + func (m *Milestone) AfterSet(colName string, _ xorm.Cell) { if colName == "deadline" { if m.Deadline.Year() == 9999 { @@ -694,14 +964,30 @@ func NewMilestone(m *Milestone) (err error) { return sess.Commit() } +func getMilestoneByID(e Engine, id int64) (*Milestone, error) { + m := &Milestone{ID: id} + has, err := x.Get(m) + if err != nil { + return nil, err + } else if !has { + return nil, ErrMilestoneNotExist{id, 0} + } + return m, nil +} + // GetMilestoneByID returns the milestone of given ID. func GetMilestoneByID(id int64) (*Milestone, error) { - m := &Milestone{ID: id} + return getMilestoneByID(x, id) +} + +// GetRepoMilestoneByID returns the milestone of given ID and repository. +func GetRepoMilestoneByID(repoID, milestoneID int64) (*Milestone, error) { + m := &Milestone{ID: milestoneID, RepoID: repoID} has, err := x.Get(m) if err != nil { return nil, err } else if !has { - return nil, ErrMilestoneNotExist{id} + return nil, ErrMilestoneNotExist{milestoneID, repoID} } return m, nil } @@ -760,7 +1046,7 @@ func MilestoneStats(repoID int64) (open int64, closed int64) { // ChangeMilestoneStatus changes the milestone open/closed status. func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) { - repo, err := GetRepositoryById(m.RepoID) + repo, err := GetRepositoryByID(m.RepoID) if err != nil { return err } @@ -776,22 +1062,20 @@ func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) { return err } - repo.NumMilestones = int(countRepoMilestones(sess, repo.Id)) - repo.NumClosedMilestones = int(countRepoClosedMilestones(sess, repo.Id)) - if _, err = sess.Id(repo.Id).AllCols().Update(repo); err != nil { + repo.NumMilestones = int(countRepoMilestones(sess, repo.ID)) + repo.NumClosedMilestones = int(countRepoClosedMilestones(sess, repo.ID)) + if _, err = sess.Id(repo.ID).AllCols().Update(repo); err != nil { return err } return sess.Commit() } -// ChangeMilestoneIssueStats updates the open/closed issues counter and progress -// for the milestone associated witht the given issue. -func ChangeMilestoneIssueStats(issue *Issue) error { +func changeMilestoneIssueStats(e *xorm.Session, issue *Issue) error { if issue.MilestoneID == 0 { return nil } - m, err := GetMilestoneByID(issue.MilestoneID) + m, err := getMilestoneByID(e, issue.MilestoneID) if err != nil { return err } @@ -804,21 +1088,28 @@ func ChangeMilestoneIssueStats(issue *Issue) error { m.NumClosedIssues-- } - m.Completeness = m.NumClosedIssues * 100 / m.NumIssues - - return UpdateMilestone(m) + return updateMilestone(e, m) } -// ChangeMilestoneAssign changes assignment of milestone for issue. -func ChangeMilestoneAssign(oldMid, mid int64, issue *Issue) (err error) { +// ChangeMilestoneIssueStats updates the open/closed issues counter and progress +// for the milestone associated witht the given issue. +func ChangeMilestoneIssueStats(issue *Issue) (err error) { sess := x.NewSession() - defer sess.Close() + defer sessionRelease(sess) if err = sess.Begin(); err != nil { return err } + if err = changeMilestoneIssueStats(sess, issue); err != nil { + return err + } + + return sess.Commit() +} + +func changeMilestoneAssign(e *xorm.Session, oldMid int64, issue *Issue) error { if oldMid > 0 { - m, err := GetMilestoneByID(oldMid) + m, err := getMilestoneByID(e, oldMid) if err != nil { return err } @@ -827,26 +1118,16 @@ func ChangeMilestoneAssign(oldMid, mid int64, issue *Issue) (err error) { if issue.IsClosed { m.NumClosedIssues-- } - if m.NumIssues > 0 { - m.Completeness = m.NumClosedIssues * 100 / m.NumIssues - } else { - m.Completeness = 0 - } - if _, err = sess.Id(m.ID).Cols("num_issues,num_completeness,num_closed_issues").Update(m); err != nil { - sess.Rollback() + if err = updateMilestone(e, m); err != nil { return err - } - - rawSql := "UPDATE `issue_user` SET milestone_id = 0 WHERE issue_id = ?" - if _, err = sess.Exec(rawSql, issue.ID); err != nil { - sess.Rollback() + } else if _, err = e.Exec("UPDATE `issue_user` SET milestone_id=0 WHERE issue_id=?", issue.ID); err != nil { return err } } - if mid > 0 { - m, err := GetMilestoneByID(mid) + if issue.MilestoneID > 0 { + m, err := GetMilestoneByID(issue.MilestoneID) if err != nil { return err } @@ -860,19 +1141,28 @@ func ChangeMilestoneAssign(oldMid, mid int64, issue *Issue) (err error) { return ErrWrongIssueCounter } - m.Completeness = m.NumClosedIssues * 100 / m.NumIssues - if _, err = sess.Id(m.ID).Cols("num_issues,num_completeness,num_closed_issues").Update(m); err != nil { - sess.Rollback() + if err = updateMilestone(e, m); err != nil { return err - } - - rawSql := "UPDATE `issue_user` SET milestone_id = ? WHERE issue_id = ?" - if _, err = sess.Exec(rawSql, m.ID, issue.ID); err != nil { - sess.Rollback() + } else if _, err = e.Exec("UPDATE `issue_user` SET milestone_id=? WHERE issue_id=?", m.ID, issue.ID); err != nil { return err } } + return nil +} + +// ChangeMilestoneAssign changes assignment of milestone for issue. +func ChangeMilestoneAssign(oldMid int64, issue *Issue) (err error) { + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return err + } + + if err = changeMilestoneAssign(sess, oldMid, issue); err != nil { + return err + } + return sess.Commit() } @@ -886,7 +1176,7 @@ func DeleteMilestoneByID(mid int64) error { return err } - repo, err := GetRepositoryById(m.RepoID) + repo, err := GetRepositoryByID(m.RepoID) if err != nil { return err } @@ -901,9 +1191,9 @@ func DeleteMilestoneByID(mid int64) error { return err } - repo.NumMilestones = int(countRepoMilestones(sess, repo.Id)) - repo.NumClosedMilestones = int(countRepoClosedMilestones(sess, repo.Id)) - if _, err = sess.Id(repo.Id).AllCols().Update(repo); err != nil { + repo.NumMilestones = int(countRepoMilestones(sess, repo.ID)) + repo.NumClosedMilestones = int(countRepoClosedMilestones(sess, repo.ID)) + if _, err = sess.Id(repo.ID).AllCols().Update(repo); err != nil { return err } @@ -932,173 +1222,279 @@ const ( COMMENT_TYPE_CLOSE // References. - COMMENT_TYPE_ISSUE - // Reference from some commit (not part of a pull request) - COMMENT_TYPE_COMMIT - // Reference from some pull request - COMMENT_TYPE_PULL + COMMENT_TYPE_ISSUE_REF + // Reference from a commit (not part of a pull request) + COMMENT_TYPE_COMMIT_REF + // Reference from a comment + COMMENT_TYPE_COMMENT_REF + // Reference from a pull request + COMMENT_TYPE_PULL_REF +) + +type CommentTag int + +const ( + COMMENT_TAG_NONE CommentTag = iota + COMMENT_TAG_POSTER + COMMENT_TAG_ADMIN + COMMENT_TAG_OWNER ) // Comment represents a comment in commit and issue page. type Comment struct { - Id int64 - Type CommentType - PosterId int64 - Poster *User `xorm:"-"` - IssueId int64 - CommitId int64 - Line int64 - Content string `xorm:"TEXT"` - Created time.Time `xorm:"CREATED"` + ID int64 `xorm:"pk autoincr"` + Type CommentType + PosterID int64 + Poster *User `xorm:"-"` + IssueID int64 `xorm:"INDEX"` + CommitID int64 + Line int64 + Content string `xorm:"TEXT"` + RenderedContent string `xorm:"-"` + Created time.Time `xorm:"CREATED"` + + Attachments []*Attachment `xorm:"-"` + + // For view issue page. + ShowTag CommentTag `xorm:"-"` } -// CreateComment creates comment of issue or commit. -func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType CommentType, content string, attachments []int64) (*Comment, error) { - sess := x.NewSession() - defer sessionRelease(sess) - if err := sess.Begin(); err != nil { - return nil, err - } +// HashTag returns unique hash tag for comment. +func (c *Comment) HashTag() string { + return "issuecomment-" + com.ToStr(c.ID) +} + +// EventTag returns unique event hash tag for comment. +func (c *Comment) EventTag() string { + return "event-" + com.ToStr(c.ID) +} + +func (c *Comment) AfterSet(colName string, _ xorm.Cell) { + var err error + switch colName { + case "id": + c.Attachments, err = GetAttachmentsByCommentID(c.ID) + if err != nil { + log.Error(3, "GetAttachmentsByCommentID[%d]: %v", c.ID, err) + } - comment := &Comment{PosterId: userId, Type: cmtType, IssueId: issueId, - CommitId: commitId, Line: line, Content: content} + case "poster_id": + c.Poster, err = GetUserByID(c.PosterID) + if err != nil { + if IsErrUserNotExist(err) { + c.PosterID = -1 + c.Poster = &User{Name: "someone"} + } else { + log.Error(3, "GetUserByID[%d]: %v", c.ID, err) + } + } + } +} - if _, err := sess.Insert(comment); err != nil { +func createComment(e *xorm.Session, u *User, repo *Repository, issue *Issue, commitID, line int64, cmtType CommentType, content string, uuids []string) (_ *Comment, err error) { + comment := &Comment{ + PosterID: u.Id, + Type: cmtType, + IssueID: issue.ID, + CommitID: commitID, + Line: line, + Content: content, + } + if _, err = e.Insert(comment); err != nil { return nil, err } // Check comment type. switch cmtType { case COMMENT_TYPE_COMMENT: - rawSql := "UPDATE `issue` SET num_comments = num_comments + 1 WHERE id = ?" - if _, err := sess.Exec(rawSql, issueId); err != nil { + if _, err = e.Exec("UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", issue.ID); err != nil { return nil, err } - 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)) + // Check attachments. + attachments := make([]*Attachment, 0, len(uuids)) + for _, uuid := range uuids { + attach, err := getAttachmentByUUID(e, uuid) + if err != nil { + if IsErrAttachmentNotExist(err) { + continue + } + return nil, fmt.Errorf("getAttachmentByUUID[%s]: %v", uuid, err) } + attachments = append(attachments, attach) + } - if _, err := sess.Exec(rawSql, comment.Id, strings.Join(astrs, ",")); err != nil { - return nil, err + for i := range attachments { + attachments[i].IssueID = issue.ID + attachments[i].CommentID = comment.ID + // No assign value could be 0, so ignore AllCols(). + if _, err = e.Id(attachments[i].ID).Update(attachments[i]); err != nil { + return nil, fmt.Errorf("update attachment[%d]: %v", attachments[i].ID, err) } } + + // Notify watchers. + act := &Action{ + ActUserID: u.Id, + ActUserName: u.LowerName, + ActEmail: u.Email, + OpType: COMMENT_ISSUE, + Content: fmt.Sprintf("%d|%s", issue.Index, strings.Split(content, "\n")[0]), + RepoID: repo.ID, + RepoUserName: repo.Owner.LowerName, + RepoName: repo.LowerName, + IsPrivate: repo.IsPrivate, + } + if err = notifyWatchers(e, act); err != nil { + return nil, err + } + case COMMENT_TYPE_REOPEN: - rawSql := "UPDATE `repository` SET num_closed_issues = num_closed_issues - 1 WHERE id = ?" - if _, err := sess.Exec(rawSql, repoId); err != nil { + if _, err = e.Exec("UPDATE `repository` SET num_closed_issues=num_closed_issues-1 WHERE id=?", repo.ID); err != nil { return nil, err } case COMMENT_TYPE_CLOSE: - rawSql := "UPDATE `repository` SET num_closed_issues = num_closed_issues + 1 WHERE id = ?" - if _, err := sess.Exec(rawSql, repoId); err != nil { + if _, err = e.Exec("UPDATE `repository` SET num_closed_issues=num_closed_issues+1 WHERE id=?", repo.ID); err != nil { return nil, err } } - return comment, sess.Commit() + return comment, nil } -// GetCommentById returns the comment with the given id -func GetCommentById(commentId int64) (*Comment, error) { - c := &Comment{Id: commentId} - _, err := x.Get(c) +func createStatusComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue) (*Comment, error) { + cmtType := COMMENT_TYPE_CLOSE + if !issue.IsClosed { + cmtType = COMMENT_TYPE_REOPEN + } + return createComment(e, doer, repo, issue, 0, 0, cmtType, "", nil) +} - return c, err +// CreateComment creates comment of issue or commit. +func CreateComment(doer *User, repo *Repository, issue *Issue, commitID, line int64, cmtType CommentType, content string, attachments []string) (comment *Comment, err error) { + sess := x.NewSession() + defer sessionRelease(sess) + if err = sess.Begin(); err != nil { + return nil, err + } + + comment, err = createComment(sess, doer, repo, issue, commitID, line, cmtType, content, attachments) + if err != nil { + return nil, err + } + + return comment, sess.Commit() } -func (c *Comment) ContentHtml() template.HTML { - return template.HTML(c.Content) +// CreateIssueComment creates a plain issue comment. +func CreateIssueComment(doer *User, repo *Repository, issue *Issue, content string, attachments []string) (*Comment, error) { + return CreateComment(doer, repo, issue, 0, 0, COMMENT_TYPE_COMMENT, content, attachments) } -// GetIssueComments returns list of comment by given issue id. -func GetIssueComments(issueId int64) ([]Comment, error) { - comments := make([]Comment, 0, 10) - err := x.Asc("created").Find(&comments, &Comment{IssueId: issueId}) - return comments, err +// GetCommentById returns the comment with the given id +func GetCommentById(id int64) (*Comment, error) { + c := new(Comment) + _, err := x.Id(id).Get(c) + return c, err } -// Attachments returns the attachments for this comment. -func (c *Comment) Attachments() []*Attachment { - a, _ := GetAttachmentsByComment(c.Id) - return a +// GetCommentsByIssueID returns all comments of issue by given ID. +func GetCommentsByIssueID(issueID int64) ([]*Comment, error) { + comments := make([]*Comment, 0, 10) + return comments, x.Where("issue_id=?", issueID).Asc("created").Find(&comments) } func (c *Comment) AfterDelete() { - _, err := DeleteAttachmentsByComment(c.Id, true) + _, 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) + log.Info("Could not delete files for comment %d on issue #%d: %s", c.ID, c.IssueID, err) } } +// Attachment represent a attachment of issue/comment/release. type Attachment struct { - Id int64 - IssueId int64 - CommentId int64 + ID int64 `xorm:"pk autoincr"` + UUID string `xorm:"uuid UNIQUE"` + IssueID int64 `xorm:"INDEX"` + CommentID int64 + ReleaseID int64 `xorm:"INDEX"` 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() +// AttachmentLocalPath returns where attachment is stored in local file system based on given UUID. +func AttachmentLocalPath(uuid string) string { + return path.Join(setting.AttachmentPath, uuid[0:1], uuid[1:2], uuid) +} + +// LocalPath returns where attachment is stored in local file system. +func (attach *Attachment) LocalPath() string { + return AttachmentLocalPath(attach.UUID) +} +// NewAttachment creates a new attachment object. +func NewAttachment(name string, buf []byte, file multipart.File) (_ *Attachment, err error) { + attach := &Attachment{ + UUID: gouuid.NewV4().String(), + Name: name, + } + + if err = os.MkdirAll(path.Dir(attach.LocalPath()), os.ModePerm); err != nil { + return nil, fmt.Errorf("MkdirAll: %v", err) + } + + fw, err := os.Create(attach.LocalPath()) + if err != nil { + return nil, fmt.Errorf("Create: %v", err) + } + defer fw.Close() + + if _, err = fw.Write(buf); err != nil { + return nil, fmt.Errorf("Write: %v", err) + } else if _, err = io.Copy(fw, file); err != nil { + return nil, fmt.Errorf("Copy: %v", err) + } + + sess := x.NewSession() + defer sessionRelease(sess) 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() + if _, err := sess.Insert(attach); err != nil { return nil, err } - return a, sess.Commit() + return attach, sess.Commit() } -// Attachment returns the attachment by given ID. -func GetAttachmentById(id int64) (*Attachment, error) { - m := &Attachment{Id: id} - - has, err := x.Get(m) - +func getAttachmentByUUID(e Engine, uuid string) (*Attachment, error) { + attach := &Attachment{UUID: uuid} + has, err := x.Get(attach) if err != nil { return nil, err + } else if !has { + return nil, ErrAttachmentNotExist{0, uuid} } - - if !has { - return nil, ErrAttachmentNotExist - } - - return m, nil + return attach, 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 +// GetAttachmentByUUID returns attachment by given UUID. +func GetAttachmentByUUID(uuid string) (*Attachment, error) { + return getAttachmentByUUID(x, uuid) } -// GetAttachmentsByIssue returns a list of attachments for the given issue -func GetAttachmentsByIssue(issueId int64) ([]*Attachment, error) { +// GetAttachmentsByIssueID returns all attachments for given issue by ID. +func GetAttachmentsByIssueID(issueID int64) ([]*Attachment, error) { attachments := make([]*Attachment, 0, 10) - err := x.Where("issue_id = ?", issueId).And("comment_id > 0").Find(&attachments) - return attachments, err + return attachments, x.Where("issue_id=? AND comment_id=0", issueID).Find(&attachments) } -// GetAttachmentsByComment returns a list of attachments for the given comment -func GetAttachmentsByComment(commentId int64) ([]*Attachment, error) { +// GetAttachmentsByCommentID returns all attachments if comment by given ID. +func GetAttachmentsByCommentID(commentID int64) ([]*Attachment, error) { attachments := make([]*Attachment, 0, 10) - err := x.Where("comment_id = ?", commentId).Find(&attachments) - return attachments, err + return attachments, x.Where("comment_id=?", commentID).Find(&attachments) } // DeleteAttachment deletes the given attachment and optionally the associated file. @@ -1111,12 +1507,12 @@ func DeleteAttachment(a *Attachment, remove bool) error { func DeleteAttachments(attachments []*Attachment, remove bool) (int, error) { for i, a := range attachments { if remove { - if err := os.Remove(a.Path); err != nil { + if err := os.Remove(a.LocalPath()); err != nil { return i, err } } - if _, err := x.Delete(a.Id); err != nil { + if _, err := x.Delete(a.ID); err != nil { return i, err } } @@ -1126,7 +1522,7 @@ func DeleteAttachments(attachments []*Attachment, remove bool) (int, error) { // DeleteAttachmentsByIssue deletes all attachments associated with the given issue. func DeleteAttachmentsByIssue(issueId int64, remove bool) (int, error) { - attachments, err := GetAttachmentsByIssue(issueId) + attachments, err := GetAttachmentsByIssueID(issueId) if err != nil { return 0, err @@ -1137,7 +1533,7 @@ func DeleteAttachmentsByIssue(issueId int64, remove bool) (int, error) { // DeleteAttachmentsByComment deletes all attachments associated with the given comment. func DeleteAttachmentsByComment(commentId int64, remove bool) (int, error) { - attachments, err := GetAttachmentsByComment(commentId) + attachments, err := GetAttachmentsByCommentID(commentId) if err != nil { return 0, err diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index c7900743..f1d6c91a 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -5,8 +5,12 @@ package migrations import ( + "bytes" "encoding/json" "fmt" + "io/ioutil" + "os" + "path" "strings" "time" @@ -16,6 +20,7 @@ import ( "github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/setting" + gouuid "github.com/gogits/gogs/modules/uuid" ) const _MIN_DB_VER = 0 @@ -58,6 +63,8 @@ var migrations = []Migration{ NewMigration("generate team-repo from team", teamToTeamRepo), // V3 -> V4:v0.5.13 NewMigration("fix locale file load panic", fixLocaleFileLoadPanic), // V4 -> V5:v0.6.0 NewMigration("trim action compare URL prefix", trimCommitActionAppUrlPrefix), // V5 -> V6:v0.6.3 + NewMigration("generate issue-label from issue", issueToIssueLabel), // V6 -> V7:v0.6.4 + NewMigration("refactor attachment table", attachmentRefactor), // V7 -> V8:v0.6.4 } // Migrate database to current version @@ -96,6 +103,12 @@ func Migrate(x *xorm.Engine) error { } v := currentVersion.Version + if int(v-_MIN_DB_VER) > len(migrations) { + // User downgraded Gogs. + currentVersion.Version = int64(len(migrations) + _MIN_DB_VER) + _, err = x.Id(1).Update(currentVersion) + return err + } for i, m := range migrations[v-_MIN_DB_VER:] { log.Info("Migration: %s", m.Description()) if err = m.Migrate(x); err != nil { @@ -130,6 +143,9 @@ func accessToCollaboration(x *xorm.Engine) (err error) { results, err := x.Query("SELECT u.id AS `uid`, a.repo_name AS `repo`, a.mode AS `mode`, a.created as `created` FROM `access` a JOIN `user` u ON a.user_name=u.lower_name") if err != nil { + if strings.Contains(err.Error(), "no such column") { + return nil + } return err } @@ -278,6 +294,9 @@ func accessRefactor(x *xorm.Engine) (err error) { results, err = x.Query("SELECT `id`,`authorize`,`repo_ids` FROM `team` WHERE org_id=? AND authorize>? ORDER BY `authorize` ASC", ownerID, int(minAccessLevel)) if err != nil { + if strings.Contains(err.Error(), "no such column") { + return nil + } return fmt.Errorf("select teams from org: %v", err) } @@ -339,6 +358,9 @@ func teamToTeamRepo(x *xorm.Engine) error { results, err := x.Query("SELECT `id`,`org_id`,`repo_ids` FROM `team`") if err != nil { + if strings.Contains(err.Error(), "no such column") { + return nil + } return fmt.Errorf("select teams: %v", err) } for _, team := range results { @@ -369,7 +391,7 @@ func teamToTeamRepo(x *xorm.Engine) error { } if err = sess.Sync2(new(TeamRepo)); err != nil { - return fmt.Errorf("sync: %v", err) + return fmt.Errorf("sync2: %v", err) } else if _, err = sess.Insert(teamRepos); err != nil { return fmt.Errorf("insert team-repos: %v", err) } @@ -453,3 +475,134 @@ func trimCommitActionAppUrlPrefix(x *xorm.Engine) error { } return sess.Commit() } + +func issueToIssueLabel(x *xorm.Engine) error { + type IssueLabel struct { + ID int64 `xorm:"pk autoincr"` + IssueID int64 `xorm:"UNIQUE(s)"` + LabelID int64 `xorm:"UNIQUE(s)"` + } + + issueLabels := make([]*IssueLabel, 0, 50) + results, err := x.Query("SELECT `id`,`label_ids` FROM `issue`") + if err != nil { + if strings.Contains(err.Error(), "no such column") { + return nil + } + return fmt.Errorf("select issues: %v", err) + } + for _, issue := range results { + issueID := com.StrTo(issue["id"]).MustInt64() + + // Just in case legacy code can have duplicated IDs for same label. + mark := make(map[int64]bool) + for _, idStr := range strings.Split(string(issue["label_ids"]), "|") { + labelID := com.StrTo(strings.TrimPrefix(idStr, "$")).MustInt64() + if labelID == 0 || mark[labelID] { + continue + } + + mark[labelID] = true + issueLabels = append(issueLabels, &IssueLabel{ + IssueID: issueID, + LabelID: labelID, + }) + } + } + + sess := x.NewSession() + defer sessionRelease(sess) + if err = sess.Begin(); err != nil { + return err + } + + if err = sess.Sync2(new(IssueLabel)); err != nil { + return fmt.Errorf("sync2: %v", err) + } else if _, err = sess.Insert(issueLabels); err != nil { + return fmt.Errorf("insert issue-labels: %v", err) + } + + return sess.Commit() +} + +func attachmentRefactor(x *xorm.Engine) error { + type Attachment struct { + ID int64 `xorm:"pk autoincr"` + UUID string `xorm:"uuid INDEX"` + + // For rename purpose. + Path string `xorm:"-"` + NewPath string `xorm:"-"` + } + + results, err := x.Query("SELECT * FROM `attachment`") + if err != nil { + return fmt.Errorf("select attachments: %v", err) + } + + attachments := make([]*Attachment, 0, len(results)) + for _, attach := range results { + if !com.IsExist(string(attach["path"])) { + // If the attachment is already missing, there is no point to update it. + continue + } + attachments = append(attachments, &Attachment{ + ID: com.StrTo(attach["id"]).MustInt64(), + UUID: gouuid.NewV4().String(), + Path: string(attach["path"]), + }) + } + + sess := x.NewSession() + defer sessionRelease(sess) + if err = sess.Begin(); err != nil { + return err + } + + if err = sess.Sync2(new(Attachment)); err != nil { + return fmt.Errorf("Sync2: %v", err) + } + + // Note: Roll back for rename can be a dead loop, + // so produces a backup file. + var buf bytes.Buffer + buf.WriteString("# old path -> new path\n") + + // Update database first because this is where error happens the most often. + for _, attach := range attachments { + if _, err = sess.Id(attach.ID).Update(attach); err != nil { + return err + } + + attach.NewPath = path.Join(setting.AttachmentPath, attach.UUID[0:1], attach.UUID[1:2], attach.UUID) + buf.WriteString(attach.Path) + buf.WriteString("\t") + buf.WriteString(attach.NewPath) + buf.WriteString("\n") + } + + // Then rename attachments. + isSucceed := true + defer func() { + if isSucceed { + return + } + + dumpPath := path.Join(setting.LogRootPath, "attachment_path.dump") + ioutil.WriteFile(dumpPath, buf.Bytes(), 0666) + fmt.Println("Fail to rename some attachments, old and new paths are saved into:", dumpPath) + }() + for _, attach := range attachments { + if err = os.MkdirAll(path.Dir(attach.NewPath), os.ModePerm); err != nil { + isSucceed = false + return err + } + + if err = os.Rename(attach.Path, attach.NewPath); err != nil { + isSucceed = false + return err + } + } + + return sess.Commit() +} diff --git a/models/models.go b/models/models.go index 01b96c0f..e06d5cf8 100644 --- a/models/models.go +++ b/models/models.go @@ -57,7 +57,8 @@ func init() { new(User), new(PublicKey), new(Oauth2), new(AccessToken), new(Repository), new(DeployKey), new(Collaboration), new(Access), new(Watch), new(Star), new(Follow), new(Action), - new(Issue), new(Comment), new(Attachment), new(IssueUser), new(Label), new(Milestone), + new(Issue), new(Comment), new(Attachment), new(IssueUser), + new(Label), new(IssueLabel), new(Milestone), new(Mirror), new(Release), new(LoginSource), new(Webhook), new(UpdateTask), new(HookTask), new(Team), new(OrgUser), new(TeamUser), new(TeamRepo), diff --git a/models/oauth2.go b/models/oauth2.go index d19e248c..a15f7b6f 100644 --- a/models/oauth2.go +++ b/models/oauth2.go @@ -58,7 +58,7 @@ func GetOauth2(identity string) (oa *Oauth2, err error) { } else if oa.Uid == -1 { return oa, ErrOauth2NotAssociated } - oa.User, err = GetUserById(oa.Uid) + oa.User, err = GetUserByID(oa.Uid) return oa, err } diff --git a/models/org.go b/models/org.go index 3caed30b..b8d19456 100644 --- a/models/org.go +++ b/models/org.go @@ -66,7 +66,7 @@ func (org *User) GetMembers() error { org.Members = make([]*User, len(ous)) for i, ou := range ous { - org.Members[i], err = GetUserById(ou.Uid) + org.Members[i], err = GetUserByID(ou.Uid) if err != nil { return err } @@ -343,11 +343,11 @@ func RemoveOrgUser(orgId, uid int64) error { return nil } - u, err := GetUserById(uid) + u, err := GetUserByID(uid) if err != nil { return fmt.Errorf("GetUserById: %v", err) } - org, err := GetUserById(orgId) + org, err := GetUserByID(orgId) if err != nil { return fmt.Errorf("get organization: %v", err) } else if err = org.GetRepositories(); err != nil { @@ -380,10 +380,10 @@ func RemoveOrgUser(orgId, uid int64) error { // Delete all repository accesses. access := &Access{UserID: u.Id} for _, repo := range org.Repos { - access.RepoID = repo.Id + access.RepoID = repo.ID if _, err = sess.Delete(access); err != nil { return err - } else if err = watchRepo(sess, u.Id, repo.Id, false); err != nil { + } else if err = watchRepo(sess, u.Id, repo.ID, false); err != nil { return err } } @@ -443,7 +443,7 @@ func (t *Team) getRepositories(e Engine) (err error) { t.Repos = make([]*Repository, 0, len(teamRepos)) for i := range teamRepos { - repo, err := getRepositoryById(e, teamRepos[i].RepoID) + repo, err := getRepositoryByID(e, teamRepos[i].RepoID) if err != nil { return fmt.Errorf("getRepositoryById(%d): %v", teamRepos[i].RepoID, err) } @@ -487,7 +487,7 @@ func (t *Team) HasRepository(repoID int64) bool { } func (t *Team) addRepository(e Engine, repo *Repository) (err error) { - if err = addTeamRepo(e, t.OrgID, t.ID, repo.Id); err != nil { + if err = addTeamRepo(e, t.OrgID, t.ID, repo.ID); err != nil { return err } @@ -504,7 +504,7 @@ func (t *Team) addRepository(e Engine, repo *Repository) (err error) { return fmt.Errorf("getMembers: %v", err) } for _, u := range t.Members { - if err = watchRepo(e, u.Id, repo.Id, true); err != nil { + if err = watchRepo(e, u.Id, repo.ID, true); err != nil { return fmt.Errorf("watchRepo: %v", err) } } @@ -513,9 +513,9 @@ func (t *Team) addRepository(e Engine, repo *Repository) (err error) { // AddRepository adds new repository to team of organization. func (t *Team) AddRepository(repo *Repository) (err error) { - if repo.OwnerId != t.OrgID { + if repo.OwnerID != t.OrgID { return errors.New("Repository does not belong to organization") - } else if t.HasRepository(repo.Id) { + } else if t.HasRepository(repo.ID) { return nil } @@ -533,7 +533,7 @@ func (t *Team) AddRepository(repo *Repository) (err error) { } func (t *Team) removeRepository(e Engine, repo *Repository, recalculate bool) (err error) { - if err = removeTeamRepo(e, t.ID, repo.Id); err != nil { + if err = removeTeamRepo(e, t.ID, repo.ID); err != nil { return err } @@ -560,7 +560,7 @@ func (t *Team) removeRepository(e Engine, repo *Repository, recalculate bool) (e continue } - if err = watchRepo(e, u.Id, repo.Id, false); err != nil { + if err = watchRepo(e, u.Id, repo.ID, false); err != nil { return err } } @@ -574,7 +574,7 @@ func (t *Team) RemoveRepository(repoID int64) error { return nil } - repo, err := GetRepositoryById(repoID) + repo, err := GetRepositoryByID(repoID) if err != nil { return err } @@ -713,7 +713,7 @@ func DeleteTeam(t *Team) error { } // Get organization. - org, err := GetUserById(t.OrgID) + org, err := GetUserByID(t.OrgID) if err != nil { return err } @@ -903,7 +903,7 @@ func removeTeamMember(e Engine, orgId, teamId, uid int64) error { } // Get organization. - org, err := getUserById(e, orgId) + org, err := getUserByID(e, orgId) if err != nil { return err } diff --git a/models/repo.go b/models/repo.go index 8135bc57..bad6f386 100644 --- a/models/repo.go +++ b/models/repo.go @@ -129,8 +129,8 @@ func NewRepoContext() { // Repository represents a git repository. type Repository struct { - Id int64 - OwnerId int64 `xorm:"UNIQUE(s)"` + ID int64 `xorm:"pk autoincr"` + OwnerID int64 `xorm:"UNIQUE(s)"` Owner *User `xorm:"-"` LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"` Name string `xorm:"INDEX NOT NULL"` @@ -159,8 +159,8 @@ type Repository struct { *Mirror `xorm:"-"` IsFork bool `xorm:"NOT NULL DEFAULT false"` - ForkId int64 - ForkRepo *Repository `xorm:"-"` + ForkID int64 + BaseRepo *Repository `xorm:"-"` Created time.Time `xorm:"CREATED"` Updated time.Time `xorm:"UPDATED"` @@ -168,7 +168,7 @@ type Repository struct { func (repo *Repository) getOwner(e Engine) (err error) { if repo.Owner == nil { - repo.Owner, err = getUserById(e, repo.OwnerId) + repo.Owner, err = getUserByID(e, repo.OwnerID) } return err } @@ -177,17 +177,54 @@ func (repo *Repository) GetOwner() (err error) { return repo.getOwner(x) } +// GetAssignees returns all users that have write access of repository. +func (repo *Repository) GetAssignees() (_ []*User, err error) { + if err = repo.GetOwner(); err != nil { + return nil, err + } + + accesses := make([]*Access, 0, 10) + if err = x.Where("repo_id=? AND mode>=?", repo.ID, ACCESS_MODE_WRITE).Find(&accesses); err != nil { + return nil, err + } + + users := make([]*User, 0, len(accesses)+1) // Just waste 1 unit does not matter. + if !repo.Owner.IsOrganization() { + users = append(users, repo.Owner) + } + + var u *User + for i := range accesses { + u, err = GetUserByID(accesses[i].UserID) + if err != nil { + return nil, err + } + users = append(users, u) + } + return users, nil +} + +// GetAssigneeByID returns the user that has write access of repository by given ID. +func (repo *Repository) GetAssigneeByID(userID int64) (*User, error) { + return GetAssigneeByID(repo, userID) +} + +// GetMilestoneByID returns the milestone belongs to repository by given ID. +func (repo *Repository) GetMilestoneByID(milestoneID int64) (*Milestone, error) { + return GetRepoMilestoneByID(repo.ID, milestoneID) +} + func (repo *Repository) GetMirror() (err error) { - repo.Mirror, err = GetMirror(repo.Id) + repo.Mirror, err = GetMirror(repo.ID) return err } -func (repo *Repository) GetForkRepo() (err error) { +func (repo *Repository) GetBaseRepo() (err error) { if !repo.IsFork { return nil } - repo.ForkRepo, err = GetRepositoryById(repo.ForkId) + repo.BaseRepo, err = GetRepositoryByID(repo.ForkID) return err } @@ -210,8 +247,8 @@ func (repo *Repository) HasAccess(u *User) bool { return has } -func (repo *Repository) IsOwnedBy(u *User) bool { - return repo.OwnerId == u.Id +func (repo *Repository) IsOwnedBy(userID int64) bool { + return repo.OwnerID == userID } // DescriptionHtml does special handles to description and return HTML string. @@ -224,7 +261,7 @@ func (repo *Repository) DescriptionHtml() template.HTML { func isRepositoryExist(e Engine, u *User, repoName string) (bool, error) { has, err := e.Get(&Repository{ - OwnerId: u.Id, + OwnerID: u.Id, LowerName: strings.ToLower(repoName), }) return has && com.IsDir(RepoPath(u.Name, repoName)), err @@ -287,8 +324,8 @@ func IsUsableName(name string) error { // Mirror represents a mirror information of repository. type Mirror struct { - Id int64 - RepoId int64 + ID int64 `xorm:"pk autoincr"` + RepoID int64 RepoName string // <user name>/<repo name> Interval int // Hour. Updated time.Time `xorm:"UPDATED"` @@ -296,7 +333,7 @@ type Mirror struct { } func getMirror(e Engine, repoId int64) (*Mirror, error) { - m := &Mirror{RepoId: repoId} + m := &Mirror{RepoID: repoId} has, err := e.Get(m) if err != nil { return nil, err @@ -312,7 +349,7 @@ func GetMirror(repoId int64) (*Mirror, error) { } func updateMirror(e Engine, m *Mirror) error { - _, err := e.Id(m.Id).Update(m) + _, err := e.Id(m.ID).Update(m) return err } @@ -330,7 +367,7 @@ func MirrorRepository(repoId int64, userName, repoName, repoPath, url string) er } if _, err = x.InsertOne(&Mirror{ - RepoId: repoId, + RepoID: repoId, RepoName: strings.ToLower(userName + "/" + repoName), Interval: 24, NextUpdate: time.Now().Add(24 * time.Hour), @@ -365,7 +402,7 @@ func MigrateRepository(u *User, name, desc string, private, mirror bool, url str repo.IsBare = false if mirror { - if err = MirrorRepository(repo.Id, u.Name, repo.Name, repoPath, url); err != nil { + if err = MirrorRepository(repo.ID, u.Name, repo.Name, repoPath, url); err != nil { return repo, err } repo.IsMirror = true @@ -517,7 +554,7 @@ func initRepository(e Engine, repoPath string, u *User, repo *Repository, initRe if len(fileName) == 0 { // Re-fetch the repository from database before updating it (else it would // override changes that were done earlier with sql) - if repo, err = getRepositoryById(e, repo.Id); err != nil { + if repo, err = getRepositoryByID(e, repo.ID); err != nil { return err } repo.IsBare = true @@ -562,7 +599,7 @@ func createRepository(e *xorm.Session, u *User, repo *Repository) (err error) { } } - if err = watchRepo(e, u.Id, repo.Id, true); err != nil { + if err = watchRepo(e, u.Id, repo.ID, true); err != nil { return fmt.Errorf("watchRepo: %v", err) } else if err = newRepoAction(e, u, repo); err != nil { return fmt.Errorf("newRepoAction: %v", err) @@ -574,7 +611,7 @@ func createRepository(e *xorm.Session, u *User, repo *Repository) (err error) { // CreateRepository creates a repository for given user or organization. func CreateRepository(u *User, name, desc, lang, license string, isPrivate, isMirror, initReadme bool) (_ *Repository, err error) { repo := &Repository{ - OwnerId: u.Id, + OwnerID: u.Id, Owner: u, Name: name, LowerName: strings.ToLower(name), @@ -630,12 +667,12 @@ func GetRepositoriesWithUsers(num, offset int) ([]*Repository, error) { } for _, repo := range repos { - repo.Owner = &User{Id: repo.OwnerId} + repo.Owner = &User{Id: repo.OwnerID} has, err := x.Get(repo.Owner) if err != nil { return nil, err } else if !has { - return nil, ErrUserNotExist{repo.OwnerId, ""} + return nil, ErrUserNotExist{repo.OwnerID, ""} } } @@ -672,11 +709,11 @@ func TransferOwnership(u *User, newOwnerName string, repo *Repository) error { // Note: we have to set value here to make sure recalculate accesses is based on // new owner. - repo.OwnerId = newOwner.Id + repo.OwnerID = newOwner.Id repo.Owner = newOwner // Update repository. - if _, err := sess.Id(repo.Id).Update(repo); err != nil { + if _, err := sess.Id(repo.ID).Update(repo); err != nil { return fmt.Errorf("update owner: %v", err) } @@ -687,7 +724,7 @@ func TransferOwnership(u *User, newOwnerName string, repo *Repository) error { } // Dummy object. - collaboration := &Collaboration{RepoID: repo.Id} + collaboration := &Collaboration{RepoID: repo.ID} for _, c := range collaborators { collaboration.UserID = c.Id if c.Id == newOwner.Id || newOwner.IsOrgMember(c.Id) { @@ -703,7 +740,7 @@ func TransferOwnership(u *User, newOwnerName string, repo *Repository) error { return fmt.Errorf("getTeams: %v", err) } for _, t := range owner.Teams { - if !t.hasRepository(sess, repo.Id) { + if !t.hasRepository(sess, repo.ID) { continue } @@ -713,7 +750,7 @@ func TransferOwnership(u *User, newOwnerName string, repo *Repository) error { } } - if err = owner.removeOrgRepo(sess, repo.Id); err != nil { + if err = owner.removeOrgRepo(sess, repo.ID); err != nil { return fmt.Errorf("removeOrgRepo: %v", err) } } @@ -739,7 +776,7 @@ func TransferOwnership(u *User, newOwnerName string, repo *Repository) error { return fmt.Errorf("decrease old owner repository count: %v", err) } - if err = watchRepo(sess, newOwner.Id, repo.Id, true); err != nil { + if err = watchRepo(sess, newOwner.Id, repo.ID, true); err != nil { return fmt.Errorf("watchRepo: %v", err) } else if err = transferRepoAction(sess, u, owner, newOwner, repo); err != nil { return fmt.Errorf("transferRepoAction: %v", err) @@ -747,7 +784,7 @@ func TransferOwnership(u *User, newOwnerName string, repo *Repository) error { // Update mirror information. if repo.IsMirror { - mirror, err := getMirror(sess, repo.Id) + mirror, err := getMirror(sess, repo.ID) if err != nil { return fmt.Errorf("getMirror: %v", err) } @@ -794,7 +831,7 @@ func updateRepository(e Engine, repo *Repository, visibilityChanged bool) (err e repo.Website = repo.Website[:255] } - if _, err = e.Id(repo.Id).AllCols().Update(repo); err != nil { + if _, err = e.Id(repo.ID).AllCols().Update(repo); err != nil { return fmt.Errorf("update: %v", err) } @@ -831,7 +868,7 @@ func UpdateRepository(repo *Repository, visibilityChanged bool) (err error) { // DeleteRepository deletes a repository for a user or organization. func DeleteRepository(uid, repoID int64, userName string) error { - repo := &Repository{Id: repoID, OwnerId: uid} + repo := &Repository{ID: repoID, OwnerID: uid} has, err := x.Get(repo) if err != nil { return err @@ -840,7 +877,7 @@ func DeleteRepository(uid, repoID int64, userName string) error { } // In case is a organization. - org, err := GetUserById(uid) + org, err := GetUserByID(uid) if err != nil { return err } @@ -866,17 +903,17 @@ func DeleteRepository(uid, repoID int64, userName string) error { } } - if _, err = sess.Delete(&Repository{Id: repoID}); err != nil { + if _, err = sess.Delete(&Repository{ID: repoID}); err != nil { return err - } else if _, err = sess.Delete(&Access{RepoID: repo.Id}); err != nil { + } else if _, err = sess.Delete(&Access{RepoID: repo.ID}); err != nil { return err - } else if _, err = sess.Delete(&Action{RepoID: repo.Id}); err != nil { + } else if _, err = sess.Delete(&Action{RepoID: repo.ID}); err != nil { return err } else if _, err = sess.Delete(&Watch{RepoID: repoID}); err != nil { return err - } else if _, err = sess.Delete(&Mirror{RepoId: repoID}); err != nil { + } else if _, err = sess.Delete(&Mirror{RepoID: repoID}); err != nil { return err - } else if _, err = sess.Delete(&IssueUser{RepoId: repoID}); err != nil { + } else if _, err = sess.Delete(&IssueUser{RepoID: repoID}); err != nil { return err } else if _, err = sess.Delete(&Milestone{RepoID: repoID}); err != nil { return err @@ -886,13 +923,26 @@ func DeleteRepository(uid, repoID int64, userName string) error { return err } - // Delete comments. + // Delete comments and attachments. issues := make([]*Issue, 0, 25) + attachmentPaths := make([]string, 0, len(issues)) if err = sess.Where("repo_id=?", repoID).Find(&issues); err != nil { return err } for i := range issues { - if _, err = sess.Delete(&Comment{IssueId: issues[i].ID}); err != nil { + if _, err = sess.Delete(&Comment{IssueID: issues[i].ID}); err != nil { + return err + } + + attachments := make([]*Attachment, 0, 5) + if err = sess.Where("issue_id=?", issues[i].ID).Find(&attachments); err != nil { + return err + } + for j := range attachments { + attachmentPaths = append(attachmentPaths, attachments[j].LocalPath()) + } + + if _, err = sess.Delete(&Attachment{IssueID: issues[i].ID}); err != nil { return err } } @@ -902,7 +952,7 @@ func DeleteRepository(uid, repoID int64, userName string) error { } if repo.IsFork { - if _, err = sess.Exec("UPDATE `repository` SET num_forks=num_forks-1 WHERE id=?", repo.ForkId); err != nil { + if _, err = sess.Exec("UPDATE `repository` SET num_forks=num_forks-1 WHERE id=?", repo.ForkID); err != nil { return err } } @@ -920,6 +970,13 @@ func DeleteRepository(uid, repoID int64, userName string) error { } } + // Remove attachment files. + for i := range attachmentPaths { + if err = os.Remove(attachmentPaths[i]); err != nil { + log.Warn("delete attachment: %v", err) + } + } + return sess.Commit() } @@ -946,7 +1003,7 @@ func GetRepositoryByRef(ref string) (*Repository, error) { // GetRepositoryByName returns the repository by given name under user if exists. func GetRepositoryByName(uid int64, repoName string) (*Repository, error) { repo := &Repository{ - OwnerId: uid, + OwnerID: uid, LowerName: strings.ToLower(repoName), } has, err := x.Get(repo) @@ -958,7 +1015,7 @@ func GetRepositoryByName(uid int64, repoName string) (*Repository, error) { return repo, err } -func getRepositoryById(e Engine, id int64) (*Repository, error) { +func getRepositoryByID(e Engine, id int64) (*Repository, error) { repo := new(Repository) has, err := e.Id(id).Get(repo) if err != nil { @@ -969,9 +1026,9 @@ func getRepositoryById(e Engine, id int64) (*Repository, error) { return repo, nil } -// GetRepositoryById returns the repository by given id if exists. -func GetRepositoryById(id int64) (*Repository, error) { - return getRepositoryById(x, id) +// GetRepositoryByID returns the repository by given id if exists. +func GetRepositoryByID(id int64) (*Repository, error) { + return getRepositoryByID(x, id) } // GetRepositories returns a list of repositories of given user. @@ -982,8 +1039,7 @@ func GetRepositories(uid int64, private bool) ([]*Repository, error) { sess.Where("is_private=?", false) } - err := sess.Find(&repos, &Repository{OwnerId: uid}) - return repos, err + return repos, sess.Find(&repos, &Repository{OwnerID: uid}) } // GetRecentUpdatedRepositories returns the list of repositories that are recently updated. @@ -993,8 +1049,8 @@ func GetRecentUpdatedRepositories(num int) (repos []*Repository, err error) { } // GetRepositoryCount returns the total number of repositories of user. -func GetRepositoryCount(user *User) (int64, error) { - return x.Count(&Repository{OwnerId: user.Id}) +func GetRepositoryCount(u *User) (int64, error) { + return x.Count(&Repository{OwnerID: u.Id}) } type SearchOption struct { @@ -1199,7 +1255,7 @@ type Collaboration struct { // Add collaborator and accompanying access func (repo *Repository) AddCollaborator(u *User) error { collaboration := &Collaboration{ - RepoID: repo.Id, + RepoID: repo.ID, UserID: u.Id, } @@ -1238,13 +1294,13 @@ func (repo *Repository) AddCollaborator(u *User) error { func (repo *Repository) getCollaborators(e Engine) ([]*User, error) { collaborations := make([]*Collaboration, 0) - if err := e.Find(&collaborations, &Collaboration{RepoID: repo.Id}); err != nil { + if err := e.Find(&collaborations, &Collaboration{RepoID: repo.ID}); err != nil { return nil, err } users := make([]*User, len(collaborations)) for i, c := range collaborations { - user, err := getUserById(e, c.UserID) + user, err := getUserByID(e, c.UserID) if err != nil { return nil, err } @@ -1261,7 +1317,7 @@ func (repo *Repository) GetCollaborators() ([]*User, error) { // Delete collaborator and accompanying access func (repo *Repository) DeleteCollaborator(u *User) (err error) { collaboration := &Collaboration{ - RepoID: repo.Id, + RepoID: repo.ID, UserID: u.Id, } @@ -1430,14 +1486,14 @@ func HasForkedRepo(ownerID, repoID int64) (*Repository, bool) { func ForkRepository(u *User, oldRepo *Repository, name, desc string) (_ *Repository, err error) { repo := &Repository{ - OwnerId: u.Id, + OwnerID: u.Id, Owner: u, Name: name, LowerName: strings.ToLower(name), Description: desc, IsPrivate: oldRepo.IsPrivate, IsFork: true, - ForkId: oldRepo.Id, + ForkID: oldRepo.ID, } sess := x.NewSession() @@ -1450,7 +1506,7 @@ func ForkRepository(u *User, oldRepo *Repository, name, desc string) (_ *Reposit return nil, err } - if _, err = sess.Exec("UPDATE `repository` SET num_forks=num_forks+1 WHERE id=?", oldRepo.Id); err != nil { + if _, err = sess.Exec("UPDATE `repository` SET num_forks=num_forks+1 WHERE id=?", oldRepo.ID); err != nil { return nil, err } diff --git a/models/update.go b/models/update.go index 33b7733e..f381d6fd 100644 --- a/models/update.go +++ b/models/update.go @@ -105,7 +105,7 @@ func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName commit := &base.PushCommits{} if err = CommitRepoAction(userId, ru.Id, userName, actEmail, - repos.Id, repoUserName, repoName, refName, commit, oldCommitId, newCommitId); err != nil { + repos.ID, repoUserName, repoName, refName, commit, oldCommitId, newCommitId); err != nil { log.GitLogger.Fatal(4, "CommitRepoAction: %s/%s:%v", repoUserName, repoName, err) } return err @@ -154,7 +154,7 @@ func Update(refName, oldCommitId, newCommitId, userName, repoUserName, repoName } if err = CommitRepoAction(userId, ru.Id, userName, actEmail, - repos.Id, repoUserName, repoName, refName, &base.PushCommits{l.Len(), commits, ""}, oldCommitId, newCommitId); err != nil { + repos.ID, repoUserName, repoName, refName, &base.PushCommits{l.Len(), commits, ""}, oldCommitId, newCommitId); err != nil { return fmt.Errorf("runUpdate.models.CommitRepoAction: %s/%s:%v", repoUserName, repoName, err) } return nil diff --git a/models/user.go b/models/user.go index 6dd31536..f2151a70 100644 --- a/models/user.go +++ b/models/user.go @@ -13,7 +13,9 @@ import ( "fmt" "image" "image/jpeg" + _ "image/jpeg" "os" + "path" "path/filepath" "strings" "time" @@ -116,11 +118,40 @@ func (u *User) HomeLink() string { // AvatarLink returns user gravatar link. func (u *User) AvatarLink() string { + defaultImgUrl := setting.AppSubUrl + "/img/avatar_default.jpg" + imgPath := path.Join(setting.AvatarUploadPath, com.ToStr(u.Id)) switch { case u.UseCustomAvatar: + if !com.IsExist(imgPath) { + return defaultImgUrl + } return setting.AppSubUrl + "/avatars/" + com.ToStr(u.Id) case setting.DisableGravatar, setting.OfflineMode: - return setting.AppSubUrl + "/img/avatar_default.jpg" + if !com.IsExist(imgPath) { + img, err := avatar.RandomImage([]byte(u.Email)) + if err != nil { + log.Error(3, "RandomImage: %v", err) + return defaultImgUrl + } + if err = os.MkdirAll(path.Dir(imgPath), os.ModePerm); err != nil { + log.Error(3, "MkdirAll: %v", err) + return defaultImgUrl + } + fw, err := os.Create(imgPath) + if err != nil { + log.Error(3, "Create: %v", err) + return defaultImgUrl + } + defer fw.Close() + + if err = jpeg.Encode(fw, img, nil); err != nil { + log.Error(3, "Encode: %v", err) + return defaultImgUrl + } + log.Info("New random avatar created: %d", u.Id) + } + + return setting.AppSubUrl + "/avatars/" + com.ToStr(u.Id) case setting.Service.EnableCacheAvatar: return setting.AppSubUrl + "/avatar/" + u.Avatar } @@ -163,7 +194,7 @@ func (u *User) UploadAvatar(data []byte) error { if err != nil { return err } - m := resize.Resize(200, 200, img, resize.NearestNeighbor) + m := resize.Resize(234, 234, img, resize.NearestNeighbor) sess := x.NewSession() defer sess.Close() @@ -191,6 +222,25 @@ func (u *User) UploadAvatar(data []byte) error { return sess.Commit() } +// IsAdminOfRepo returns true if user has admin or higher access of repository. +func (u *User) IsAdminOfRepo(repo *Repository) bool { + if err := repo.GetOwner(); err != nil { + log.Error(3, "GetOwner: %v", err) + return false + } + + if repo.Owner.IsOrganization() { + has, err := HasAccess(u, repo, ACCESS_MODE_ADMIN) + if err != nil { + log.Error(3, "HasAccess: %v", err) + return false + } + return has + } + + return repo.IsOwnedBy(u.Id) +} + // IsOrganization returns true if user is actually a organization. func (u *User) IsOrganization() bool { return u.Type == ORGANIZATION @@ -226,7 +276,7 @@ func (u *User) GetOrganizations() error { u.Orgs = make([]*User, len(ous)) for i, ou := range ous { - u.Orgs[i], err = GetUserById(ou.OrgID) + u.Orgs[i], err = GetUserByID(ou.OrgID) if err != nil { return err } @@ -522,10 +572,12 @@ func DeleteUser(u *User) error { return err } - // Delete user directory. + // Delete user data. if err = os.RemoveAll(UserPath(u.Name)); err != nil { return err } + // Delete avatar. + os.Remove(u.CustomAvatarPath()) return sess.Commit() } @@ -555,7 +607,7 @@ func GetUserByKeyId(keyId int64) (*User, error) { return user, nil } -func getUserById(e Engine, id int64) (*User, error) { +func getUserByID(e Engine, id int64) (*User, error) { u := new(User) has, err := e.Id(id).Get(u) if err != nil { @@ -566,9 +618,20 @@ func getUserById(e Engine, id int64) (*User, error) { return u, nil } -// GetUserById returns the user object by given ID if exists. -func GetUserById(id int64) (*User, error) { - return getUserById(x, id) +// GetUserByID returns the user object by given ID if exists. +func GetUserByID(id int64) (*User, error) { + return getUserByID(x, id) +} + +// GetAssigneeByID returns the user with write access of repository by given ID. +func GetAssigneeByID(repo *Repository, userID int64) (*User, error) { + has, err := HasAccess(&User{Id: userID}, repo, ACCESS_MODE_WRITE) + if err != nil { + return nil, err + } else if !has { + return nil, ErrUserNotExist{userID, ""} + } + return GetUserByID(userID) } // GetUserByName returns user by given name. @@ -620,7 +683,7 @@ func GetEmailAddresses(uid int64) ([]*EmailAddress, error) { return nil, err } - u, err := GetUserById(uid) + u, err := GetUserByID(uid) if err != nil { return nil, err } @@ -666,7 +729,7 @@ func (email *EmailAddress) Activate() error { return err } - if user, err := GetUserById(email.Uid); err != nil { + if user, err := GetUserByID(email.Uid); err != nil { return err } else { user.Rands = GetUserSalt() @@ -793,7 +856,7 @@ func GetUserByEmail(email string) (*User, error) { return nil, err } if has { - return GetUserById(emailAddress.Uid) + return GetUserByID(emailAddress.Uid) } return nil, ErrUserNotExist{0, "email"} @@ -869,18 +932,19 @@ func UnFollowUser(userId int64, unFollowId int64) (err error) { } func UpdateMentions(userNames []string, issueId int64) error { + for i := range userNames { + userNames[i] = strings.ToLower(userNames[i]) + } users := make([]*User, 0, len(userNames)) - if err := x.Where("name IN (?)", strings.Join(userNames, "\",\"")).OrderBy("name ASC").Find(&users); err != nil { + if err := x.Where("lower_name IN (?)", strings.Join(userNames, "\",\"")).OrderBy("lower_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 { + if !user.IsOrganization() { continue } @@ -889,9 +953,7 @@ func UpdateMentions(userNames []string, issueId int64) error { } tempIds := make([]int64, 0, user.NumMembers) - orgUsers, err := GetOrgUsersByOrgId(user.Id) - if err != nil { return err } @@ -903,7 +965,7 @@ func UpdateMentions(userNames []string, issueId int64) error { ids = append(ids, tempIds...) } - if err := UpdateIssueUserPairsByMentions(ids, issueId); err != nil { + if err := UpdateIssueUsersByMentions(ids, issueId); err != nil { return err } |