From 6b6f54b79b54a59d57e6566cb29151c15b455431 Mon Sep 17 00:00:00 2001 From: Unknwon Date: Fri, 10 Feb 2017 16:51:09 -0500 Subject: Some file refactoring --- models/attachment.go | 175 +++++++++++++++++ models/comment.go | 472 +++++++++++++++++++++++++++++++++++++++++++++ models/issue.go | 502 ------------------------------------------------ models/issue_comment.go | 472 --------------------------------------------- models/milestone.go | 349 +++++++++++++++++++++++++++++++++ models/mirror.go | 264 +++++++++++++++++++++++++ models/repo_mirror.go | 264 ------------------------- 7 files changed, 1260 insertions(+), 1238 deletions(-) create mode 100644 models/attachment.go create mode 100644 models/comment.go delete mode 100644 models/issue_comment.go create mode 100644 models/milestone.go create mode 100644 models/mirror.go delete mode 100644 models/repo_mirror.go (limited to 'models') diff --git a/models/attachment.go b/models/attachment.go new file mode 100644 index 00000000..a270b751 --- /dev/null +++ b/models/attachment.go @@ -0,0 +1,175 @@ +// Copyright 2017 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package models + +import ( + "fmt" + "io" + "mime/multipart" + "os" + "path" + "time" + + "github.com/go-xorm/xorm" + gouuid "github.com/satori/go.uuid" + + "github.com/gogits/gogs/modules/setting" +) + +// Attachment represent a attachment of issue/comment/release. +type Attachment struct { + ID int64 `xorm:"pk autoincr"` + UUID string `xorm:"uuid UNIQUE"` + IssueID int64 `xorm:"INDEX"` + CommentID int64 + ReleaseID int64 `xorm:"INDEX"` + Name string + + Created time.Time `xorm:"-"` + CreatedUnix int64 +} + +func (a *Attachment) BeforeInsert() { + a.CreatedUnix = time.Now().Unix() +} + +func (a *Attachment) AfterSet(colName string, _ xorm.Cell) { + switch colName { + case "created_unix": + a.Created = time.Unix(a.CreatedUnix, 0).Local() + } +} + +// AttachmentLocalPath returns where attachment is stored in local file system based on given UUID. +func AttachmentLocalPath(uuid string) string { + return path.Join(setting.AttachmentPath, uuid[0:1], uuid[1:2], uuid) +} + +// LocalPath returns where attachment is stored in local file system. +func (attach *Attachment) LocalPath() string { + return AttachmentLocalPath(attach.UUID) +} + +// NewAttachment creates a new attachment object. +func NewAttachment(name string, buf []byte, file multipart.File) (_ *Attachment, err error) { + attach := &Attachment{ + UUID: gouuid.NewV4().String(), + Name: name, + } + + localPath := attach.LocalPath() + if err = os.MkdirAll(path.Dir(localPath), os.ModePerm); err != nil { + return nil, fmt.Errorf("MkdirAll: %v", err) + } + + fw, err := os.Create(localPath) + if err != nil { + return nil, fmt.Errorf("Create: %v", err) + } + defer fw.Close() + + if _, err = fw.Write(buf); err != nil { + return nil, fmt.Errorf("Write: %v", err) + } else if _, err = io.Copy(fw, file); err != nil { + return nil, fmt.Errorf("Copy: %v", err) + } + + if _, err := x.Insert(attach); err != nil { + return nil, err + } + + return attach, nil +} + +func getAttachmentByUUID(e Engine, uuid string) (*Attachment, error) { + attach := &Attachment{UUID: uuid} + has, err := x.Get(attach) + if err != nil { + return nil, err + } else if !has { + return nil, ErrAttachmentNotExist{0, uuid} + } + return attach, nil +} + +func getAttachmentsByUUIDs(e Engine, uuids []string) ([]*Attachment, error) { + if len(uuids) == 0 { + return []*Attachment{}, nil + } + + // Silently drop invalid uuids. + attachments := make([]*Attachment, 0, len(uuids)) + return attachments, e.In("uuid", uuids).Find(&attachments) +} + +// GetAttachmentByUUID returns attachment by given UUID. +func GetAttachmentByUUID(uuid string) (*Attachment, error) { + return getAttachmentByUUID(x, uuid) +} + +func getAttachmentsByIssueID(e Engine, issueID int64) ([]*Attachment, error) { + attachments := make([]*Attachment, 0, 10) + return attachments, e.Where("issue_id = ? AND comment_id = 0", issueID).Find(&attachments) +} + +// GetAttachmentsByIssueID returns all attachments of an issue. +func GetAttachmentsByIssueID(issueID int64) ([]*Attachment, error) { + return getAttachmentsByIssueID(x, issueID) +} + +func getAttachmentsByCommentID(e Engine, commentID int64) ([]*Attachment, error) { + attachments := make([]*Attachment, 0, 10) + return attachments, e.Where("comment_id=?", commentID).Find(&attachments) +} + +// GetAttachmentsByCommentID returns all attachments if comment by given ID. +func GetAttachmentsByCommentID(commentID int64) ([]*Attachment, error) { + return getAttachmentsByCommentID(x, commentID) +} + +// DeleteAttachment deletes the given attachment and optionally the associated file. +func DeleteAttachment(a *Attachment, remove bool) error { + _, err := DeleteAttachments([]*Attachment{a}, remove) + return err +} + +// DeleteAttachments deletes the given attachments and optionally the associated files. +func DeleteAttachments(attachments []*Attachment, remove bool) (int, error) { + for i, a := range attachments { + if remove { + if err := os.Remove(a.LocalPath()); err != nil { + return i, err + } + } + + if _, err := x.Delete(a); err != nil { + return i, err + } + } + + return len(attachments), nil +} + +// DeleteAttachmentsByIssue deletes all attachments associated with the given issue. +func DeleteAttachmentsByIssue(issueId int64, remove bool) (int, error) { + attachments, err := GetAttachmentsByIssueID(issueId) + + if err != nil { + return 0, err + } + + return DeleteAttachments(attachments, remove) +} + +// DeleteAttachmentsByComment deletes all attachments associated with the given comment. +func DeleteAttachmentsByComment(commentId int64, remove bool) (int, error) { + attachments, err := GetAttachmentsByCommentID(commentId) + + if err != nil { + return 0, err + } + + return DeleteAttachments(attachments, remove) +} diff --git a/models/comment.go b/models/comment.go new file mode 100644 index 00000000..e166efd9 --- /dev/null +++ b/models/comment.go @@ -0,0 +1,472 @@ +// Copyright 2016 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package models + +import ( + "fmt" + "strings" + "time" + + "github.com/Unknwon/com" + "github.com/go-xorm/xorm" + log "gopkg.in/clog.v1" + + api "github.com/gogits/go-gogs-client" + + "github.com/gogits/gogs/modules/markdown" +) + +// CommentType defines whether a comment is just a simple comment, an action (like close) or a reference. +type CommentType int + +const ( + // Plain comment, can be associated with a commit (CommitID > 0) and a line (LineNum > 0) + COMMENT_TYPE_COMMENT CommentType = iota + COMMENT_TYPE_REOPEN + COMMENT_TYPE_CLOSE + + // References. + COMMENT_TYPE_ISSUE_REF + // Reference from a commit (not part of a pull request) + COMMENT_TYPE_COMMIT_REF + // Reference from a comment + COMMENT_TYPE_COMMENT_REF + // Reference from a pull request + COMMENT_TYPE_PULL_REF +) + +type CommentTag int + +const ( + COMMENT_TAG_NONE CommentTag = iota + COMMENT_TAG_POSTER + COMMENT_TAG_WRITER + COMMENT_TAG_OWNER +) + +// Comment represents a comment in commit and issue page. +type Comment struct { + ID int64 `xorm:"pk autoincr"` + Type CommentType + PosterID int64 + Poster *User `xorm:"-"` + IssueID int64 `xorm:"INDEX"` + Issue *Issue `xorm:"-"` + CommitID int64 + Line int64 + Content string `xorm:"TEXT"` + RenderedContent string `xorm:"-"` + + Created time.Time `xorm:"-"` + CreatedUnix int64 + Updated time.Time `xorm:"-"` + UpdatedUnix int64 + + // Reference issue in commit message + CommitSHA string `xorm:"VARCHAR(40)"` + + Attachments []*Attachment `xorm:"-"` + + // For view issue page. + ShowTag CommentTag `xorm:"-"` +} + +func (c *Comment) BeforeInsert() { + c.CreatedUnix = time.Now().Unix() + c.UpdatedUnix = c.CreatedUnix +} + +func (c *Comment) BeforeUpdate() { + c.UpdatedUnix = time.Now().Unix() +} + +func (c *Comment) AfterSet(colName string, _ xorm.Cell) { + switch colName { + case "created_unix": + c.Created = time.Unix(c.CreatedUnix, 0).Local() + case "updated_unix": + c.Updated = time.Unix(c.UpdatedUnix, 0).Local() + } +} + +func (c *Comment) loadAttributes(e Engine) (err error) { + if c.Poster == nil { + c.Poster, err = GetUserByID(c.PosterID) + if err != nil { + if IsErrUserNotExist(err) { + c.PosterID = -1 + c.Poster = NewGhostUser() + } else { + return fmt.Errorf("getUserByID.(Poster) [%d]: %v", c.PosterID, err) + } + } + } + + if c.Issue == nil { + c.Issue, err = getRawIssueByID(e, c.IssueID) + if err != nil { + return fmt.Errorf("getIssueByID [%d]: %v", c.IssueID, err) + } + } + + if c.Attachments == nil { + c.Attachments, err = getAttachmentsByCommentID(e, c.ID) + if err != nil { + return fmt.Errorf("getAttachmentsByCommentID [%d]: %v", c.ID, err) + } + } + + return nil +} + +func (c *Comment) LoadAttributes() error { + return c.loadAttributes(x) +} + +func (c *Comment) AfterDelete() { + _, err := DeleteAttachmentsByComment(c.ID, true) + + if err != nil { + log.Info("Could not delete files for comment %d on issue #%d: %s", c.ID, c.IssueID, err) + } +} + +func (c *Comment) HTMLURL() string { + return fmt.Sprintf("%s#issuecomment-%d", c.Issue.HTMLURL(), c.ID) +} + +// This method assumes following fields have been assigned with valid values: +// Required - Poster, Issue +func (c *Comment) APIFormat() *api.Comment { + return &api.Comment{ + ID: c.ID, + HTMLURL: c.HTMLURL(), + Poster: c.Poster.APIFormat(), + Body: c.Content, + Created: c.Created, + Updated: c.Updated, + } +} + +// 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) +} + +// mailParticipants sends new comment emails to repository watchers +// and mentioned people. +func (cmt *Comment) mailParticipants(e Engine, opType ActionType, issue *Issue) (err error) { + mentions := markdown.FindAllMentions(cmt.Content) + if err = updateIssueMentions(e, cmt.IssueID, mentions); err != nil { + return fmt.Errorf("UpdateIssueMentions [%d]: %v", cmt.IssueID, err) + } + + switch opType { + case ACTION_COMMENT_ISSUE: + issue.Content = cmt.Content + case ACTION_CLOSE_ISSUE: + issue.Content = fmt.Sprintf("Closed #%d", issue.Index) + case ACTION_REOPEN_ISSUE: + issue.Content = fmt.Sprintf("Reopened #%d", issue.Index) + } + if err = mailIssueCommentToParticipants(issue, cmt.Poster, mentions); err != nil { + log.Error(4, "mailIssueCommentToParticipants: %v", err) + } + + return nil +} + +func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err error) { + comment := &Comment{ + Type: opts.Type, + PosterID: opts.Doer.ID, + Poster: opts.Doer, + IssueID: opts.Issue.ID, + CommitID: opts.CommitID, + CommitSHA: opts.CommitSHA, + Line: opts.LineNum, + Content: opts.Content, + } + if _, err = e.Insert(comment); err != nil { + return nil, err + } + + // Compose comment action, could be plain comment, close or reopen issue/pull request. + // This object will be used to notify watchers in the end of function. + act := &Action{ + ActUserID: opts.Doer.ID, + ActUserName: opts.Doer.Name, + Content: fmt.Sprintf("%d|%s", opts.Issue.Index, strings.Split(opts.Content, "\n")[0]), + RepoID: opts.Repo.ID, + RepoUserName: opts.Repo.Owner.Name, + RepoName: opts.Repo.Name, + IsPrivate: opts.Repo.IsPrivate, + } + + // Check comment type. + switch opts.Type { + case COMMENT_TYPE_COMMENT: + act.OpType = ACTION_COMMENT_ISSUE + + if _, err = e.Exec("UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", opts.Issue.ID); err != nil { + return nil, err + } + + // Check attachments + attachments := make([]*Attachment, 0, len(opts.Attachments)) + for _, uuid := range opts.Attachments { + attach, err := getAttachmentByUUID(e, uuid) + if err != nil { + if IsErrAttachmentNotExist(err) { + continue + } + return nil, fmt.Errorf("getAttachmentByUUID [%s]: %v", uuid, err) + } + attachments = append(attachments, attach) + } + + for i := range attachments { + attachments[i].IssueID = opts.Issue.ID + attachments[i].CommentID = comment.ID + // No assign value could be 0, so ignore AllCols(). + if _, err = e.Id(attachments[i].ID).Update(attachments[i]); err != nil { + return nil, fmt.Errorf("update attachment [%d]: %v", attachments[i].ID, err) + } + } + + case COMMENT_TYPE_REOPEN: + act.OpType = ACTION_REOPEN_ISSUE + if opts.Issue.IsPull { + act.OpType = ACTION_REOPEN_PULL_REQUEST + } + + if opts.Issue.IsPull { + _, err = e.Exec("UPDATE `repository` SET num_closed_pulls=num_closed_pulls-1 WHERE id=?", opts.Repo.ID) + } else { + _, err = e.Exec("UPDATE `repository` SET num_closed_issues=num_closed_issues-1 WHERE id=?", opts.Repo.ID) + } + if err != nil { + return nil, err + } + + case COMMENT_TYPE_CLOSE: + act.OpType = ACTION_CLOSE_ISSUE + if opts.Issue.IsPull { + act.OpType = ACTION_CLOSE_PULL_REQUEST + } + + if opts.Issue.IsPull { + _, err = e.Exec("UPDATE `repository` SET num_closed_pulls=num_closed_pulls+1 WHERE id=?", opts.Repo.ID) + } else { + _, err = e.Exec("UPDATE `repository` SET num_closed_issues=num_closed_issues+1 WHERE id=?", opts.Repo.ID) + } + if err != nil { + return nil, err + } + + } + + // Notify watchers for whatever action comes in, ignore if no action type. + if act.OpType > 0 { + if err = notifyWatchers(e, act); err != nil { + log.Error(4, "notifyWatchers: %v", err) + } + if err = comment.mailParticipants(e, act.OpType, opts.Issue); err != nil { + log.Error(4, "MailParticipants: %v", err) + } + } + + return comment, comment.loadAttributes(e) +} + +func createStatusComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue) (*Comment, error) { + cmtType := COMMENT_TYPE_CLOSE + if !issue.IsClosed { + cmtType = COMMENT_TYPE_REOPEN + } + return createComment(e, &CreateCommentOptions{ + Type: cmtType, + Doer: doer, + Repo: repo, + Issue: issue, + }) +} + +type CreateCommentOptions struct { + Type CommentType + Doer *User + Repo *Repository + Issue *Issue + + CommitID int64 + CommitSHA string + LineNum int64 + Content string + Attachments []string // UUIDs of attachments +} + +// CreateComment creates comment of issue or commit. +func CreateComment(opts *CreateCommentOptions) (comment *Comment, err error) { + sess := x.NewSession() + defer sessionRelease(sess) + if err = sess.Begin(); err != nil { + return nil, err + } + + comment, err = createComment(sess, opts) + if err != nil { + return nil, err + } + + return comment, sess.Commit() +} + +// CreateIssueComment creates a plain issue comment. +func CreateIssueComment(doer *User, repo *Repository, issue *Issue, content string, attachments []string) (*Comment, error) { + return CreateComment(&CreateCommentOptions{ + Type: COMMENT_TYPE_COMMENT, + Doer: doer, + Repo: repo, + Issue: issue, + Content: content, + Attachments: attachments, + }) +} + +// CreateRefComment creates a commit reference comment to issue. +func CreateRefComment(doer *User, repo *Repository, issue *Issue, content, commitSHA string) error { + if len(commitSHA) == 0 { + return fmt.Errorf("cannot create reference with empty commit SHA") + } + + // Check if same reference from same commit has already existed. + has, err := x.Get(&Comment{ + Type: COMMENT_TYPE_COMMIT_REF, + IssueID: issue.ID, + CommitSHA: commitSHA, + }) + if err != nil { + return fmt.Errorf("check reference comment: %v", err) + } else if has { + return nil + } + + _, err = CreateComment(&CreateCommentOptions{ + Type: COMMENT_TYPE_COMMIT_REF, + Doer: doer, + Repo: repo, + Issue: issue, + CommitSHA: commitSHA, + Content: content, + }) + return err +} + +// GetCommentByID returns the comment by given ID. +func GetCommentByID(id int64) (*Comment, error) { + c := new(Comment) + has, err := x.Id(id).Get(c) + if err != nil { + return nil, err + } else if !has { + return nil, ErrCommentNotExist{id, 0} + } + return c, c.LoadAttributes() +} + +// FIXME: use CommentList to improve performance. +func loadCommentsAttributes(e Engine, comments []*Comment) (err error) { + for i := range comments { + if err = comments[i].loadAttributes(e); err != nil { + return fmt.Errorf("loadAttributes [%d]: %v", comments[i].ID, err) + } + } + + return nil +} + +func getCommentsByIssueIDSince(e Engine, issueID, since int64) ([]*Comment, error) { + comments := make([]*Comment, 0, 10) + sess := e.Where("issue_id = ?", issueID).Asc("created_unix") + if since > 0 { + sess.And("updated_unix >= ?", since) + } + + if err := sess.Find(&comments); err != nil { + return nil, err + } + return comments, loadCommentsAttributes(e, comments) +} + +func getCommentsByRepoIDSince(e Engine, repoID, since int64) ([]*Comment, error) { + comments := make([]*Comment, 0, 10) + sess := e.Where("issue.repo_id = ?", repoID).Join("INNER", "issue", "issue.id = comment.issue_id", repoID).Asc("created_unix") + if since > 0 { + sess.And("updated_unix >= ?", since) + } + if err := sess.Find(&comments); err != nil { + return nil, err + } + return comments, loadCommentsAttributes(e, comments) +} + +func getCommentsByIssueID(e Engine, issueID int64) ([]*Comment, error) { + return getCommentsByIssueIDSince(e, issueID, -1) +} + +// GetCommentsByIssueID returns all comments of an issue. +func GetCommentsByIssueID(issueID int64) ([]*Comment, error) { + return getCommentsByIssueID(x, issueID) +} + +// GetCommentsByIssueIDSince returns a list of comments of an issue since a given time point. +func GetCommentsByIssueIDSince(issueID, since int64) ([]*Comment, error) { + return getCommentsByIssueIDSince(x, issueID, since) +} + +// GetCommentsByRepoIDSince returns a list of comments for all issues in a repo since a given time point. +func GetCommentsByRepoIDSince(repoID, since int64) ([]*Comment, error) { + return getCommentsByRepoIDSince(x, repoID, since) +} + +// UpdateComment updates information of comment. +func UpdateComment(c *Comment) error { + _, err := x.Id(c.ID).AllCols().Update(c) + return err +} + +// DeleteCommentByID deletes the comment by given ID. +func DeleteCommentByID(id int64) error { + comment, err := GetCommentByID(id) + if err != nil { + if IsErrCommentNotExist(err) { + return nil + } + return err + } + + sess := x.NewSession() + defer sessionRelease(sess) + if err = sess.Begin(); err != nil { + return err + } + + if _, err = sess.Id(comment.ID).Delete(new(Comment)); err != nil { + return err + } + + if comment.Type == COMMENT_TYPE_COMMENT { + if _, err = sess.Exec("UPDATE `issue` SET num_comments = num_comments - 1 WHERE id = ?", comment.IssueID); err != nil { + return err + } + } + + return sess.Commit() +} diff --git a/models/issue.go b/models/issue.go index dbe01f6f..0997f3bb 100644 --- a/models/issue.go +++ b/models/issue.go @@ -7,17 +7,12 @@ package models import ( "errors" "fmt" - "io" - "mime/multipart" - "os" - "path" "strings" "time" "github.com/Unknwon/com" "github.com/go-xorm/xorm" api "github.com/gogits/go-gogs-client" - gouuid "github.com/satori/go.uuid" log "gopkg.in/clog.v1" "github.com/gogits/gogs/modules/base" @@ -1332,500 +1327,3 @@ func updateIssueUsersByMentions(e Engine, issueID int64, uids []int64) error { } return nil } - -// _____ .__.__ __ -// / \ |__| | ____ _______/ |_ ____ ____ ____ -// / \ / \| | | _/ __ \ / ___/\ __\/ _ \ / \_/ __ \ -// / Y \ | |_\ ___/ \___ \ | | ( <_> ) | \ ___/ -// \____|__ /__|____/\___ >____ > |__| \____/|___| /\___ > -// \/ \/ \/ \/ \/ - -// Milestone represents a milestone of repository. -type Milestone struct { - ID int64 `xorm:"pk autoincr"` - RepoID int64 `xorm:"INDEX"` - Name string - Content string `xorm:"TEXT"` - RenderedContent string `xorm:"-"` - IsClosed bool - NumIssues int - NumClosedIssues int - NumOpenIssues int `xorm:"-"` - Completeness int // Percentage(1-100). - IsOverDue bool `xorm:"-"` - - DeadlineString string `xorm:"-"` - Deadline time.Time `xorm:"-"` - DeadlineUnix int64 - ClosedDate time.Time `xorm:"-"` - ClosedDateUnix int64 -} - -func (m *Milestone) BeforeInsert() { - m.DeadlineUnix = m.Deadline.Unix() -} - -func (m *Milestone) BeforeUpdate() { - if m.NumIssues > 0 { - m.Completeness = m.NumClosedIssues * 100 / m.NumIssues - } else { - m.Completeness = 0 - } - - m.DeadlineUnix = m.Deadline.Unix() - m.ClosedDateUnix = m.ClosedDate.Unix() -} - -func (m *Milestone) AfterSet(colName string, _ xorm.Cell) { - switch colName { - case "num_closed_issues": - m.NumOpenIssues = m.NumIssues - m.NumClosedIssues - - case "deadline_unix": - m.Deadline = time.Unix(m.DeadlineUnix, 0).Local() - if m.Deadline.Year() == 9999 { - return - } - - m.DeadlineString = m.Deadline.Format("2006-01-02") - if time.Now().Local().After(m.Deadline) { - m.IsOverDue = true - } - - case "closed_date_unix": - m.ClosedDate = time.Unix(m.ClosedDateUnix, 0).Local() - } -} - -// State returns string representation of milestone status. -func (m *Milestone) State() api.StateType { - if m.IsClosed { - return api.STATE_CLOSED - } - return api.STATE_OPEN -} - -func (m *Milestone) ChangeStatus(isClosed bool) error { - return ChangeMilestoneStatus(m, isClosed) -} - -func (m *Milestone) APIFormat() *api.Milestone { - apiMilestone := &api.Milestone{ - ID: m.ID, - State: m.State(), - Title: m.Name, - Description: m.Content, - OpenIssues: m.NumOpenIssues, - ClosedIssues: m.NumClosedIssues, - } - if m.IsClosed { - apiMilestone.Closed = &m.ClosedDate - } - if m.Deadline.Year() < 9999 { - apiMilestone.Deadline = &m.Deadline - } - return apiMilestone -} - -// NewMilestone creates new milestone of repository. -func NewMilestone(m *Milestone) (err error) { - sess := x.NewSession() - defer sessionRelease(sess) - if err = sess.Begin(); err != nil { - return err - } - - if _, err = sess.Insert(m); err != nil { - return err - } - - if _, err = sess.Exec("UPDATE `repository` SET num_milestones = num_milestones + 1 WHERE id = ?", m.RepoID); err != nil { - return err - } - return sess.Commit() -} - -func getMilestoneByRepoID(e Engine, repoID, id int64) (*Milestone, error) { - m := &Milestone{ - ID: id, - RepoID: repoID, - } - has, err := e.Get(m) - if err != nil { - return nil, err - } else if !has { - return nil, ErrMilestoneNotExist{id, repoID} - } - return m, nil -} - -// GetWebhookByRepoID returns the milestone in a repository. -func GetMilestoneByRepoID(repoID, id int64) (*Milestone, error) { - return getMilestoneByRepoID(x, repoID, id) -} - -// GetMilestonesByRepoID returns all milestones of a repository. -func GetMilestonesByRepoID(repoID int64) ([]*Milestone, error) { - miles := make([]*Milestone, 0, 10) - return miles, x.Where("repo_id = ?", repoID).Find(&miles) -} - -// GetMilestones returns a list of milestones of given repository and status. -func GetMilestones(repoID int64, page int, isClosed bool) ([]*Milestone, error) { - miles := make([]*Milestone, 0, setting.UI.IssuePagingNum) - sess := x.Where("repo_id = ? AND is_closed = ?", repoID, isClosed) - if page > 0 { - sess = sess.Limit(setting.UI.IssuePagingNum, (page-1)*setting.UI.IssuePagingNum) - } - return miles, sess.Find(&miles) -} - -func updateMilestone(e Engine, m *Milestone) error { - _, err := e.Id(m.ID).AllCols().Update(m) - return err -} - -// UpdateMilestone updates information of given milestone. -func UpdateMilestone(m *Milestone) error { - return updateMilestone(x, m) -} - -func countRepoMilestones(e Engine, repoID int64) int64 { - count, _ := e.Where("repo_id=?", repoID).Count(new(Milestone)) - return count -} - -// CountRepoMilestones returns number of milestones in given repository. -func CountRepoMilestones(repoID int64) int64 { - return countRepoMilestones(x, repoID) -} - -func countRepoClosedMilestones(e Engine, repoID int64) int64 { - closed, _ := e.Where("repo_id=? AND is_closed=?", repoID, true).Count(new(Milestone)) - return closed -} - -// CountRepoClosedMilestones returns number of closed milestones in given repository. -func CountRepoClosedMilestones(repoID int64) int64 { - return countRepoClosedMilestones(x, repoID) -} - -// MilestoneStats returns number of open and closed milestones of given repository. -func MilestoneStats(repoID int64) (open int64, closed int64) { - open, _ = x.Where("repo_id=? AND is_closed=?", repoID, false).Count(new(Milestone)) - return open, CountRepoClosedMilestones(repoID) -} - -// ChangeMilestoneStatus changes the milestone open/closed status. -// If milestone passes with changed values, those values will be -// updated to database as well. -func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) { - repo, err := GetRepositoryByID(m.RepoID) - if err != nil { - return err - } - - sess := x.NewSession() - defer sessionRelease(sess) - if err = sess.Begin(); err != nil { - return err - } - - m.IsClosed = isClosed - if err = updateMilestone(sess, m); err != nil { - return err - } - - repo.NumMilestones = int(countRepoMilestones(sess, repo.ID)) - repo.NumClosedMilestones = int(countRepoClosedMilestones(sess, repo.ID)) - if _, err = sess.Id(repo.ID).AllCols().Update(repo); err != nil { - return err - } - return sess.Commit() -} - -func changeMilestoneIssueStats(e *xorm.Session, issue *Issue) error { - if issue.MilestoneID == 0 { - return nil - } - - m, err := getMilestoneByRepoID(e, issue.RepoID, issue.MilestoneID) - if err != nil { - return err - } - - if issue.IsClosed { - m.NumOpenIssues-- - m.NumClosedIssues++ - } else { - m.NumOpenIssues++ - m.NumClosedIssues-- - } - - return updateMilestone(e, m) -} - -// ChangeMilestoneIssueStats updates the open/closed issues counter and progress -// for the milestone associated with the given issue. -func ChangeMilestoneIssueStats(issue *Issue) (err error) { - sess := x.NewSession() - defer 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, issue *Issue, oldMilestoneID int64) error { - if oldMilestoneID > 0 { - m, err := getMilestoneByRepoID(e, issue.RepoID, oldMilestoneID) - if err != nil { - return err - } - - m.NumIssues-- - if issue.IsClosed { - m.NumClosedIssues-- - } - - if err = updateMilestone(e, m); err != nil { - return err - } else if _, err = e.Exec("UPDATE `issue_user` SET milestone_id = 0 WHERE issue_id = ?", issue.ID); err != nil { - return err - } - } - - if issue.MilestoneID > 0 { - m, err := getMilestoneByRepoID(e, issue.RepoID, issue.MilestoneID) - if err != nil { - return err - } - - m.NumIssues++ - if issue.IsClosed { - m.NumClosedIssues++ - } - - if err = updateMilestone(e, m); err != nil { - return err - } else if _, err = e.Exec("UPDATE `issue_user` SET milestone_id = ? WHERE issue_id = ?", m.ID, issue.ID); err != nil { - return err - } - } - - return updateIssue(e, issue) -} - -// ChangeMilestoneAssign changes assignment of milestone for issue. -func ChangeMilestoneAssign(issue *Issue, oldMilestoneID int64) (err error) { - sess := x.NewSession() - defer sess.Close() - if err = sess.Begin(); err != nil { - return err - } - - if err = changeMilestoneAssign(sess, issue, oldMilestoneID); err != nil { - return err - } - return sess.Commit() -} - -// DeleteMilestoneOfRepoByID deletes a milestone from a repository. -func DeleteMilestoneOfRepoByID(repoID, id int64) error { - m, err := GetMilestoneByRepoID(repoID, id) - if err != nil { - if IsErrMilestoneNotExist(err) { - return nil - } - return err - } - - repo, err := GetRepositoryByID(m.RepoID) - if err != nil { - return err - } - - sess := x.NewSession() - defer sessionRelease(sess) - if err = sess.Begin(); err != nil { - return err - } - - if _, err = sess.Id(m.ID).Delete(new(Milestone)); err != nil { - return err - } - - repo.NumMilestones = int(countRepoMilestones(sess, repo.ID)) - repo.NumClosedMilestones = int(countRepoClosedMilestones(sess, repo.ID)) - if _, err = sess.Id(repo.ID).AllCols().Update(repo); err != nil { - return err - } - - if _, err = sess.Exec("UPDATE `issue` SET milestone_id = 0 WHERE milestone_id = ?", m.ID); err != nil { - return err - } else if _, err = sess.Exec("UPDATE `issue_user` SET milestone_id = 0 WHERE milestone_id = ?", m.ID); err != nil { - return err - } - return sess.Commit() -} - -// Attachment represent a attachment of issue/comment/release. -type Attachment struct { - ID int64 `xorm:"pk autoincr"` - UUID string `xorm:"uuid UNIQUE"` - IssueID int64 `xorm:"INDEX"` - CommentID int64 - ReleaseID int64 `xorm:"INDEX"` - Name string - - Created time.Time `xorm:"-"` - CreatedUnix int64 -} - -func (a *Attachment) BeforeInsert() { - a.CreatedUnix = time.Now().Unix() -} - -func (a *Attachment) AfterSet(colName string, _ xorm.Cell) { - switch colName { - case "created_unix": - a.Created = time.Unix(a.CreatedUnix, 0).Local() - } -} - -// AttachmentLocalPath returns where attachment is stored in local file system based on given UUID. -func AttachmentLocalPath(uuid string) string { - return path.Join(setting.AttachmentPath, uuid[0:1], uuid[1:2], uuid) -} - -// LocalPath returns where attachment is stored in local file system. -func (attach *Attachment) LocalPath() string { - return AttachmentLocalPath(attach.UUID) -} - -// NewAttachment creates a new attachment object. -func NewAttachment(name string, buf []byte, file multipart.File) (_ *Attachment, err error) { - attach := &Attachment{ - UUID: gouuid.NewV4().String(), - Name: name, - } - - localPath := attach.LocalPath() - if err = os.MkdirAll(path.Dir(localPath), os.ModePerm); err != nil { - return nil, fmt.Errorf("MkdirAll: %v", err) - } - - fw, err := os.Create(localPath) - if err != nil { - return nil, fmt.Errorf("Create: %v", err) - } - defer fw.Close() - - if _, err = fw.Write(buf); err != nil { - return nil, fmt.Errorf("Write: %v", err) - } else if _, err = io.Copy(fw, file); err != nil { - return nil, fmt.Errorf("Copy: %v", err) - } - - if _, err := x.Insert(attach); err != nil { - return nil, err - } - - return attach, nil -} - -func getAttachmentByUUID(e Engine, uuid string) (*Attachment, error) { - attach := &Attachment{UUID: uuid} - has, err := x.Get(attach) - if err != nil { - return nil, err - } else if !has { - return nil, ErrAttachmentNotExist{0, uuid} - } - return attach, nil -} - -func getAttachmentsByUUIDs(e Engine, uuids []string) ([]*Attachment, error) { - if len(uuids) == 0 { - return []*Attachment{}, nil - } - - // Silently drop invalid uuids. - attachments := make([]*Attachment, 0, len(uuids)) - return attachments, e.In("uuid", uuids).Find(&attachments) -} - -// GetAttachmentByUUID returns attachment by given UUID. -func GetAttachmentByUUID(uuid string) (*Attachment, error) { - return getAttachmentByUUID(x, uuid) -} - -func getAttachmentsByIssueID(e Engine, issueID int64) ([]*Attachment, error) { - attachments := make([]*Attachment, 0, 10) - return attachments, e.Where("issue_id = ? AND comment_id = 0", issueID).Find(&attachments) -} - -// GetAttachmentsByIssueID returns all attachments of an issue. -func GetAttachmentsByIssueID(issueID int64) ([]*Attachment, error) { - return getAttachmentsByIssueID(x, issueID) -} - -func getAttachmentsByCommentID(e Engine, commentID int64) ([]*Attachment, error) { - attachments := make([]*Attachment, 0, 10) - return attachments, e.Where("comment_id=?", commentID).Find(&attachments) -} - -// GetAttachmentsByCommentID returns all attachments if comment by given ID. -func GetAttachmentsByCommentID(commentID int64) ([]*Attachment, error) { - return getAttachmentsByCommentID(x, commentID) -} - -// DeleteAttachment deletes the given attachment and optionally the associated file. -func DeleteAttachment(a *Attachment, remove bool) error { - _, err := DeleteAttachments([]*Attachment{a}, remove) - return err -} - -// DeleteAttachments deletes the given attachments and optionally the associated files. -func DeleteAttachments(attachments []*Attachment, remove bool) (int, error) { - for i, a := range attachments { - if remove { - if err := os.Remove(a.LocalPath()); err != nil { - return i, err - } - } - - if _, err := x.Delete(a); err != nil { - return i, err - } - } - - return len(attachments), nil -} - -// DeleteAttachmentsByIssue deletes all attachments associated with the given issue. -func DeleteAttachmentsByIssue(issueId int64, remove bool) (int, error) { - attachments, err := GetAttachmentsByIssueID(issueId) - - if err != nil { - return 0, err - } - - return DeleteAttachments(attachments, remove) -} - -// DeleteAttachmentsByComment deletes all attachments associated with the given comment. -func DeleteAttachmentsByComment(commentId int64, remove bool) (int, error) { - attachments, err := GetAttachmentsByCommentID(commentId) - - if err != nil { - return 0, err - } - - return DeleteAttachments(attachments, remove) -} diff --git a/models/issue_comment.go b/models/issue_comment.go deleted file mode 100644 index e166efd9..00000000 --- a/models/issue_comment.go +++ /dev/null @@ -1,472 +0,0 @@ -// Copyright 2016 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package models - -import ( - "fmt" - "strings" - "time" - - "github.com/Unknwon/com" - "github.com/go-xorm/xorm" - log "gopkg.in/clog.v1" - - api "github.com/gogits/go-gogs-client" - - "github.com/gogits/gogs/modules/markdown" -) - -// CommentType defines whether a comment is just a simple comment, an action (like close) or a reference. -type CommentType int - -const ( - // Plain comment, can be associated with a commit (CommitID > 0) and a line (LineNum > 0) - COMMENT_TYPE_COMMENT CommentType = iota - COMMENT_TYPE_REOPEN - COMMENT_TYPE_CLOSE - - // References. - COMMENT_TYPE_ISSUE_REF - // Reference from a commit (not part of a pull request) - COMMENT_TYPE_COMMIT_REF - // Reference from a comment - COMMENT_TYPE_COMMENT_REF - // Reference from a pull request - COMMENT_TYPE_PULL_REF -) - -type CommentTag int - -const ( - COMMENT_TAG_NONE CommentTag = iota - COMMENT_TAG_POSTER - COMMENT_TAG_WRITER - COMMENT_TAG_OWNER -) - -// Comment represents a comment in commit and issue page. -type Comment struct { - ID int64 `xorm:"pk autoincr"` - Type CommentType - PosterID int64 - Poster *User `xorm:"-"` - IssueID int64 `xorm:"INDEX"` - Issue *Issue `xorm:"-"` - CommitID int64 - Line int64 - Content string `xorm:"TEXT"` - RenderedContent string `xorm:"-"` - - Created time.Time `xorm:"-"` - CreatedUnix int64 - Updated time.Time `xorm:"-"` - UpdatedUnix int64 - - // Reference issue in commit message - CommitSHA string `xorm:"VARCHAR(40)"` - - Attachments []*Attachment `xorm:"-"` - - // For view issue page. - ShowTag CommentTag `xorm:"-"` -} - -func (c *Comment) BeforeInsert() { - c.CreatedUnix = time.Now().Unix() - c.UpdatedUnix = c.CreatedUnix -} - -func (c *Comment) BeforeUpdate() { - c.UpdatedUnix = time.Now().Unix() -} - -func (c *Comment) AfterSet(colName string, _ xorm.Cell) { - switch colName { - case "created_unix": - c.Created = time.Unix(c.CreatedUnix, 0).Local() - case "updated_unix": - c.Updated = time.Unix(c.UpdatedUnix, 0).Local() - } -} - -func (c *Comment) loadAttributes(e Engine) (err error) { - if c.Poster == nil { - c.Poster, err = GetUserByID(c.PosterID) - if err != nil { - if IsErrUserNotExist(err) { - c.PosterID = -1 - c.Poster = NewGhostUser() - } else { - return fmt.Errorf("getUserByID.(Poster) [%d]: %v", c.PosterID, err) - } - } - } - - if c.Issue == nil { - c.Issue, err = getRawIssueByID(e, c.IssueID) - if err != nil { - return fmt.Errorf("getIssueByID [%d]: %v", c.IssueID, err) - } - } - - if c.Attachments == nil { - c.Attachments, err = getAttachmentsByCommentID(e, c.ID) - if err != nil { - return fmt.Errorf("getAttachmentsByCommentID [%d]: %v", c.ID, err) - } - } - - return nil -} - -func (c *Comment) LoadAttributes() error { - return c.loadAttributes(x) -} - -func (c *Comment) AfterDelete() { - _, err := DeleteAttachmentsByComment(c.ID, true) - - if err != nil { - log.Info("Could not delete files for comment %d on issue #%d: %s", c.ID, c.IssueID, err) - } -} - -func (c *Comment) HTMLURL() string { - return fmt.Sprintf("%s#issuecomment-%d", c.Issue.HTMLURL(), c.ID) -} - -// This method assumes following fields have been assigned with valid values: -// Required - Poster, Issue -func (c *Comment) APIFormat() *api.Comment { - return &api.Comment{ - ID: c.ID, - HTMLURL: c.HTMLURL(), - Poster: c.Poster.APIFormat(), - Body: c.Content, - Created: c.Created, - Updated: c.Updated, - } -} - -// 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) -} - -// mailParticipants sends new comment emails to repository watchers -// and mentioned people. -func (cmt *Comment) mailParticipants(e Engine, opType ActionType, issue *Issue) (err error) { - mentions := markdown.FindAllMentions(cmt.Content) - if err = updateIssueMentions(e, cmt.IssueID, mentions); err != nil { - return fmt.Errorf("UpdateIssueMentions [%d]: %v", cmt.IssueID, err) - } - - switch opType { - case ACTION_COMMENT_ISSUE: - issue.Content = cmt.Content - case ACTION_CLOSE_ISSUE: - issue.Content = fmt.Sprintf("Closed #%d", issue.Index) - case ACTION_REOPEN_ISSUE: - issue.Content = fmt.Sprintf("Reopened #%d", issue.Index) - } - if err = mailIssueCommentToParticipants(issue, cmt.Poster, mentions); err != nil { - log.Error(4, "mailIssueCommentToParticipants: %v", err) - } - - return nil -} - -func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err error) { - comment := &Comment{ - Type: opts.Type, - PosterID: opts.Doer.ID, - Poster: opts.Doer, - IssueID: opts.Issue.ID, - CommitID: opts.CommitID, - CommitSHA: opts.CommitSHA, - Line: opts.LineNum, - Content: opts.Content, - } - if _, err = e.Insert(comment); err != nil { - return nil, err - } - - // Compose comment action, could be plain comment, close or reopen issue/pull request. - // This object will be used to notify watchers in the end of function. - act := &Action{ - ActUserID: opts.Doer.ID, - ActUserName: opts.Doer.Name, - Content: fmt.Sprintf("%d|%s", opts.Issue.Index, strings.Split(opts.Content, "\n")[0]), - RepoID: opts.Repo.ID, - RepoUserName: opts.Repo.Owner.Name, - RepoName: opts.Repo.Name, - IsPrivate: opts.Repo.IsPrivate, - } - - // Check comment type. - switch opts.Type { - case COMMENT_TYPE_COMMENT: - act.OpType = ACTION_COMMENT_ISSUE - - if _, err = e.Exec("UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", opts.Issue.ID); err != nil { - return nil, err - } - - // Check attachments - attachments := make([]*Attachment, 0, len(opts.Attachments)) - for _, uuid := range opts.Attachments { - attach, err := getAttachmentByUUID(e, uuid) - if err != nil { - if IsErrAttachmentNotExist(err) { - continue - } - return nil, fmt.Errorf("getAttachmentByUUID [%s]: %v", uuid, err) - } - attachments = append(attachments, attach) - } - - for i := range attachments { - attachments[i].IssueID = opts.Issue.ID - attachments[i].CommentID = comment.ID - // No assign value could be 0, so ignore AllCols(). - if _, err = e.Id(attachments[i].ID).Update(attachments[i]); err != nil { - return nil, fmt.Errorf("update attachment [%d]: %v", attachments[i].ID, err) - } - } - - case COMMENT_TYPE_REOPEN: - act.OpType = ACTION_REOPEN_ISSUE - if opts.Issue.IsPull { - act.OpType = ACTION_REOPEN_PULL_REQUEST - } - - if opts.Issue.IsPull { - _, err = e.Exec("UPDATE `repository` SET num_closed_pulls=num_closed_pulls-1 WHERE id=?", opts.Repo.ID) - } else { - _, err = e.Exec("UPDATE `repository` SET num_closed_issues=num_closed_issues-1 WHERE id=?", opts.Repo.ID) - } - if err != nil { - return nil, err - } - - case COMMENT_TYPE_CLOSE: - act.OpType = ACTION_CLOSE_ISSUE - if opts.Issue.IsPull { - act.OpType = ACTION_CLOSE_PULL_REQUEST - } - - if opts.Issue.IsPull { - _, err = e.Exec("UPDATE `repository` SET num_closed_pulls=num_closed_pulls+1 WHERE id=?", opts.Repo.ID) - } else { - _, err = e.Exec("UPDATE `repository` SET num_closed_issues=num_closed_issues+1 WHERE id=?", opts.Repo.ID) - } - if err != nil { - return nil, err - } - - } - - // Notify watchers for whatever action comes in, ignore if no action type. - if act.OpType > 0 { - if err = notifyWatchers(e, act); err != nil { - log.Error(4, "notifyWatchers: %v", err) - } - if err = comment.mailParticipants(e, act.OpType, opts.Issue); err != nil { - log.Error(4, "MailParticipants: %v", err) - } - } - - return comment, comment.loadAttributes(e) -} - -func createStatusComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue) (*Comment, error) { - cmtType := COMMENT_TYPE_CLOSE - if !issue.IsClosed { - cmtType = COMMENT_TYPE_REOPEN - } - return createComment(e, &CreateCommentOptions{ - Type: cmtType, - Doer: doer, - Repo: repo, - Issue: issue, - }) -} - -type CreateCommentOptions struct { - Type CommentType - Doer *User - Repo *Repository - Issue *Issue - - CommitID int64 - CommitSHA string - LineNum int64 - Content string - Attachments []string // UUIDs of attachments -} - -// CreateComment creates comment of issue or commit. -func CreateComment(opts *CreateCommentOptions) (comment *Comment, err error) { - sess := x.NewSession() - defer sessionRelease(sess) - if err = sess.Begin(); err != nil { - return nil, err - } - - comment, err = createComment(sess, opts) - if err != nil { - return nil, err - } - - return comment, sess.Commit() -} - -// CreateIssueComment creates a plain issue comment. -func CreateIssueComment(doer *User, repo *Repository, issue *Issue, content string, attachments []string) (*Comment, error) { - return CreateComment(&CreateCommentOptions{ - Type: COMMENT_TYPE_COMMENT, - Doer: doer, - Repo: repo, - Issue: issue, - Content: content, - Attachments: attachments, - }) -} - -// CreateRefComment creates a commit reference comment to issue. -func CreateRefComment(doer *User, repo *Repository, issue *Issue, content, commitSHA string) error { - if len(commitSHA) == 0 { - return fmt.Errorf("cannot create reference with empty commit SHA") - } - - // Check if same reference from same commit has already existed. - has, err := x.Get(&Comment{ - Type: COMMENT_TYPE_COMMIT_REF, - IssueID: issue.ID, - CommitSHA: commitSHA, - }) - if err != nil { - return fmt.Errorf("check reference comment: %v", err) - } else if has { - return nil - } - - _, err = CreateComment(&CreateCommentOptions{ - Type: COMMENT_TYPE_COMMIT_REF, - Doer: doer, - Repo: repo, - Issue: issue, - CommitSHA: commitSHA, - Content: content, - }) - return err -} - -// GetCommentByID returns the comment by given ID. -func GetCommentByID(id int64) (*Comment, error) { - c := new(Comment) - has, err := x.Id(id).Get(c) - if err != nil { - return nil, err - } else if !has { - return nil, ErrCommentNotExist{id, 0} - } - return c, c.LoadAttributes() -} - -// FIXME: use CommentList to improve performance. -func loadCommentsAttributes(e Engine, comments []*Comment) (err error) { - for i := range comments { - if err = comments[i].loadAttributes(e); err != nil { - return fmt.Errorf("loadAttributes [%d]: %v", comments[i].ID, err) - } - } - - return nil -} - -func getCommentsByIssueIDSince(e Engine, issueID, since int64) ([]*Comment, error) { - comments := make([]*Comment, 0, 10) - sess := e.Where("issue_id = ?", issueID).Asc("created_unix") - if since > 0 { - sess.And("updated_unix >= ?", since) - } - - if err := sess.Find(&comments); err != nil { - return nil, err - } - return comments, loadCommentsAttributes(e, comments) -} - -func getCommentsByRepoIDSince(e Engine, repoID, since int64) ([]*Comment, error) { - comments := make([]*Comment, 0, 10) - sess := e.Where("issue.repo_id = ?", repoID).Join("INNER", "issue", "issue.id = comment.issue_id", repoID).Asc("created_unix") - if since > 0 { - sess.And("updated_unix >= ?", since) - } - if err := sess.Find(&comments); err != nil { - return nil, err - } - return comments, loadCommentsAttributes(e, comments) -} - -func getCommentsByIssueID(e Engine, issueID int64) ([]*Comment, error) { - return getCommentsByIssueIDSince(e, issueID, -1) -} - -// GetCommentsByIssueID returns all comments of an issue. -func GetCommentsByIssueID(issueID int64) ([]*Comment, error) { - return getCommentsByIssueID(x, issueID) -} - -// GetCommentsByIssueIDSince returns a list of comments of an issue since a given time point. -func GetCommentsByIssueIDSince(issueID, since int64) ([]*Comment, error) { - return getCommentsByIssueIDSince(x, issueID, since) -} - -// GetCommentsByRepoIDSince returns a list of comments for all issues in a repo since a given time point. -func GetCommentsByRepoIDSince(repoID, since int64) ([]*Comment, error) { - return getCommentsByRepoIDSince(x, repoID, since) -} - -// UpdateComment updates information of comment. -func UpdateComment(c *Comment) error { - _, err := x.Id(c.ID).AllCols().Update(c) - return err -} - -// DeleteCommentByID deletes the comment by given ID. -func DeleteCommentByID(id int64) error { - comment, err := GetCommentByID(id) - if err != nil { - if IsErrCommentNotExist(err) { - return nil - } - return err - } - - sess := x.NewSession() - defer sessionRelease(sess) - if err = sess.Begin(); err != nil { - return err - } - - if _, err = sess.Id(comment.ID).Delete(new(Comment)); err != nil { - return err - } - - if comment.Type == COMMENT_TYPE_COMMENT { - if _, err = sess.Exec("UPDATE `issue` SET num_comments = num_comments - 1 WHERE id = ?", comment.IssueID); err != nil { - return err - } - } - - return sess.Commit() -} diff --git a/models/milestone.go b/models/milestone.go new file mode 100644 index 00000000..8607887b --- /dev/null +++ b/models/milestone.go @@ -0,0 +1,349 @@ +// Copyright 2017 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package models + +import ( + "time" + + "github.com/go-xorm/xorm" + + api "github.com/gogits/go-gogs-client" + + "github.com/gogits/gogs/modules/setting" +) + +// Milestone represents a milestone of repository. +type Milestone struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"INDEX"` + Name string + Content string `xorm:"TEXT"` + RenderedContent string `xorm:"-"` + IsClosed bool + NumIssues int + NumClosedIssues int + NumOpenIssues int `xorm:"-"` + Completeness int // Percentage(1-100). + IsOverDue bool `xorm:"-"` + + DeadlineString string `xorm:"-"` + Deadline time.Time `xorm:"-"` + DeadlineUnix int64 + ClosedDate time.Time `xorm:"-"` + ClosedDateUnix int64 +} + +func (m *Milestone) BeforeInsert() { + m.DeadlineUnix = m.Deadline.Unix() +} + +func (m *Milestone) BeforeUpdate() { + if m.NumIssues > 0 { + m.Completeness = m.NumClosedIssues * 100 / m.NumIssues + } else { + m.Completeness = 0 + } + + m.DeadlineUnix = m.Deadline.Unix() + m.ClosedDateUnix = m.ClosedDate.Unix() +} + +func (m *Milestone) AfterSet(colName string, _ xorm.Cell) { + switch colName { + case "num_closed_issues": + m.NumOpenIssues = m.NumIssues - m.NumClosedIssues + + case "deadline_unix": + m.Deadline = time.Unix(m.DeadlineUnix, 0).Local() + if m.Deadline.Year() == 9999 { + return + } + + m.DeadlineString = m.Deadline.Format("2006-01-02") + if time.Now().Local().After(m.Deadline) { + m.IsOverDue = true + } + + case "closed_date_unix": + m.ClosedDate = time.Unix(m.ClosedDateUnix, 0).Local() + } +} + +// State returns string representation of milestone status. +func (m *Milestone) State() api.StateType { + if m.IsClosed { + return api.STATE_CLOSED + } + return api.STATE_OPEN +} + +func (m *Milestone) ChangeStatus(isClosed bool) error { + return ChangeMilestoneStatus(m, isClosed) +} + +func (m *Milestone) APIFormat() *api.Milestone { + apiMilestone := &api.Milestone{ + ID: m.ID, + State: m.State(), + Title: m.Name, + Description: m.Content, + OpenIssues: m.NumOpenIssues, + ClosedIssues: m.NumClosedIssues, + } + if m.IsClosed { + apiMilestone.Closed = &m.ClosedDate + } + if m.Deadline.Year() < 9999 { + apiMilestone.Deadline = &m.Deadline + } + return apiMilestone +} + +// NewMilestone creates new milestone of repository. +func NewMilestone(m *Milestone) (err error) { + sess := x.NewSession() + defer sessionRelease(sess) + if err = sess.Begin(); err != nil { + return err + } + + if _, err = sess.Insert(m); err != nil { + return err + } + + if _, err = sess.Exec("UPDATE `repository` SET num_milestones = num_milestones + 1 WHERE id = ?", m.RepoID); err != nil { + return err + } + return sess.Commit() +} + +func getMilestoneByRepoID(e Engine, repoID, id int64) (*Milestone, error) { + m := &Milestone{ + ID: id, + RepoID: repoID, + } + has, err := e.Get(m) + if err != nil { + return nil, err + } else if !has { + return nil, ErrMilestoneNotExist{id, repoID} + } + return m, nil +} + +// GetWebhookByRepoID returns the milestone in a repository. +func GetMilestoneByRepoID(repoID, id int64) (*Milestone, error) { + return getMilestoneByRepoID(x, repoID, id) +} + +// GetMilestonesByRepoID returns all milestones of a repository. +func GetMilestonesByRepoID(repoID int64) ([]*Milestone, error) { + miles := make([]*Milestone, 0, 10) + return miles, x.Where("repo_id = ?", repoID).Find(&miles) +} + +// GetMilestones returns a list of milestones of given repository and status. +func GetMilestones(repoID int64, page int, isClosed bool) ([]*Milestone, error) { + miles := make([]*Milestone, 0, setting.UI.IssuePagingNum) + sess := x.Where("repo_id = ? AND is_closed = ?", repoID, isClosed) + if page > 0 { + sess = sess.Limit(setting.UI.IssuePagingNum, (page-1)*setting.UI.IssuePagingNum) + } + return miles, sess.Find(&miles) +} + +func updateMilestone(e Engine, m *Milestone) error { + _, err := e.Id(m.ID).AllCols().Update(m) + return err +} + +// UpdateMilestone updates information of given milestone. +func UpdateMilestone(m *Milestone) error { + return updateMilestone(x, m) +} + +func countRepoMilestones(e Engine, repoID int64) int64 { + count, _ := e.Where("repo_id=?", repoID).Count(new(Milestone)) + return count +} + +// CountRepoMilestones returns number of milestones in given repository. +func CountRepoMilestones(repoID int64) int64 { + return countRepoMilestones(x, repoID) +} + +func countRepoClosedMilestones(e Engine, repoID int64) int64 { + closed, _ := e.Where("repo_id=? AND is_closed=?", repoID, true).Count(new(Milestone)) + return closed +} + +// CountRepoClosedMilestones returns number of closed milestones in given repository. +func CountRepoClosedMilestones(repoID int64) int64 { + return countRepoClosedMilestones(x, repoID) +} + +// MilestoneStats returns number of open and closed milestones of given repository. +func MilestoneStats(repoID int64) (open int64, closed int64) { + open, _ = x.Where("repo_id=? AND is_closed=?", repoID, false).Count(new(Milestone)) + return open, CountRepoClosedMilestones(repoID) +} + +// ChangeMilestoneStatus changes the milestone open/closed status. +// If milestone passes with changed values, those values will be +// updated to database as well. +func ChangeMilestoneStatus(m *Milestone, isClosed bool) (err error) { + repo, err := GetRepositoryByID(m.RepoID) + if err != nil { + return err + } + + sess := x.NewSession() + defer sessionRelease(sess) + if err = sess.Begin(); err != nil { + return err + } + + m.IsClosed = isClosed + if err = updateMilestone(sess, m); err != nil { + return err + } + + repo.NumMilestones = int(countRepoMilestones(sess, repo.ID)) + repo.NumClosedMilestones = int(countRepoClosedMilestones(sess, repo.ID)) + if _, err = sess.Id(repo.ID).AllCols().Update(repo); err != nil { + return err + } + return sess.Commit() +} + +func changeMilestoneIssueStats(e *xorm.Session, issue *Issue) error { + if issue.MilestoneID == 0 { + return nil + } + + m, err := getMilestoneByRepoID(e, issue.RepoID, issue.MilestoneID) + if err != nil { + return err + } + + if issue.IsClosed { + m.NumOpenIssues-- + m.NumClosedIssues++ + } else { + m.NumOpenIssues++ + m.NumClosedIssues-- + } + + return updateMilestone(e, m) +} + +// ChangeMilestoneIssueStats updates the open/closed issues counter and progress +// for the milestone associated with the given issue. +func ChangeMilestoneIssueStats(issue *Issue) (err error) { + sess := x.NewSession() + defer 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, issue *Issue, oldMilestoneID int64) error { + if oldMilestoneID > 0 { + m, err := getMilestoneByRepoID(e, issue.RepoID, oldMilestoneID) + if err != nil { + return err + } + + m.NumIssues-- + if issue.IsClosed { + m.NumClosedIssues-- + } + + if err = updateMilestone(e, m); err != nil { + return err + } else if _, err = e.Exec("UPDATE `issue_user` SET milestone_id = 0 WHERE issue_id = ?", issue.ID); err != nil { + return err + } + } + + if issue.MilestoneID > 0 { + m, err := getMilestoneByRepoID(e, issue.RepoID, issue.MilestoneID) + if err != nil { + return err + } + + m.NumIssues++ + if issue.IsClosed { + m.NumClosedIssues++ + } + + if err = updateMilestone(e, m); err != nil { + return err + } else if _, err = e.Exec("UPDATE `issue_user` SET milestone_id = ? WHERE issue_id = ?", m.ID, issue.ID); err != nil { + return err + } + } + + return updateIssue(e, issue) +} + +// ChangeMilestoneAssign changes assignment of milestone for issue. +func ChangeMilestoneAssign(issue *Issue, oldMilestoneID int64) (err error) { + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return err + } + + if err = changeMilestoneAssign(sess, issue, oldMilestoneID); err != nil { + return err + } + return sess.Commit() +} + +// DeleteMilestoneOfRepoByID deletes a milestone from a repository. +func DeleteMilestoneOfRepoByID(repoID, id int64) error { + m, err := GetMilestoneByRepoID(repoID, id) + if err != nil { + if IsErrMilestoneNotExist(err) { + return nil + } + return err + } + + repo, err := GetRepositoryByID(m.RepoID) + if err != nil { + return err + } + + sess := x.NewSession() + defer sessionRelease(sess) + if err = sess.Begin(); err != nil { + return err + } + + if _, err = sess.Id(m.ID).Delete(new(Milestone)); err != nil { + return err + } + + repo.NumMilestones = int(countRepoMilestones(sess, repo.ID)) + repo.NumClosedMilestones = int(countRepoClosedMilestones(sess, repo.ID)) + if _, err = sess.Id(repo.ID).AllCols().Update(repo); err != nil { + return err + } + + if _, err = sess.Exec("UPDATE `issue` SET milestone_id = 0 WHERE milestone_id = ?", m.ID); err != nil { + return err + } else if _, err = sess.Exec("UPDATE `issue_user` SET milestone_id = 0 WHERE milestone_id = ?", m.ID); err != nil { + return err + } + return sess.Commit() +} diff --git a/models/mirror.go b/models/mirror.go new file mode 100644 index 00000000..722d769f --- /dev/null +++ b/models/mirror.go @@ -0,0 +1,264 @@ +// Copyright 2016 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package models + +import ( + "fmt" + "strings" + "time" + + "github.com/Unknwon/com" + "github.com/go-xorm/xorm" + log "gopkg.in/clog.v1" + "gopkg.in/ini.v1" + + "github.com/gogits/git-module" + + "github.com/gogits/gogs/modules/process" + "github.com/gogits/gogs/modules/setting" + "github.com/gogits/gogs/modules/sync" +) + +var MirrorQueue = sync.NewUniqueQueue(setting.Repository.MirrorQueueLength) + +// Mirror represents mirror information of a repository. +type Mirror struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 + Repo *Repository `xorm:"-"` + Interval int // Hour. + EnablePrune bool `xorm:"NOT NULL DEFAULT true"` + + Updated time.Time `xorm:"-"` + UpdatedUnix int64 + NextUpdate time.Time `xorm:"-"` + NextUpdateUnix int64 + + address string `xorm:"-"` +} + +func (m *Mirror) BeforeInsert() { + m.UpdatedUnix = time.Now().Unix() + m.NextUpdateUnix = m.NextUpdate.Unix() +} + +func (m *Mirror) BeforeUpdate() { + m.UpdatedUnix = time.Now().Unix() + m.NextUpdateUnix = m.NextUpdate.Unix() +} + +func (m *Mirror) AfterSet(colName string, _ xorm.Cell) { + var err error + switch colName { + case "repo_id": + m.Repo, err = GetRepositoryByID(m.RepoID) + if err != nil { + log.Error(3, "GetRepositoryByID[%d]: %v", m.ID, err) + } + case "updated_unix": + m.Updated = time.Unix(m.UpdatedUnix, 0).Local() + case "next_updated_unix": + m.NextUpdate = time.Unix(m.NextUpdateUnix, 0).Local() + } +} + +// ScheduleNextUpdate calculates and sets next update time. +func (m *Mirror) ScheduleNextUpdate() { + m.NextUpdate = time.Now().Add(time.Duration(m.Interval) * time.Hour) +} + +func (m *Mirror) readAddress() { + if len(m.address) > 0 { + return + } + + cfg, err := ini.Load(m.Repo.GitConfigPath()) + if err != nil { + log.Error(4, "Load: %v", err) + return + } + m.address = cfg.Section("remote \"origin\"").Key("url").Value() +} + +// HandleCloneUserCredentials replaces user credentials from HTTP/HTTPS URL +// with placeholder . +// It will fail for any other forms of clone addresses. +func HandleCloneUserCredentials(url string, mosaics bool) string { + i := strings.Index(url, "@") + if i == -1 { + return url + } + start := strings.Index(url, "://") + if start == -1 { + return url + } + if mosaics { + return url[:start+3] + "" + url[i:] + } + return url[:start+3] + url[i+1:] +} + +// Address returns mirror address from Git repository config without credentials. +func (m *Mirror) Address() string { + m.readAddress() + return HandleCloneUserCredentials(m.address, false) +} + +// MosaicsAddress returns mirror address from Git repository config with credentials under mosaics. +func (m *Mirror) MosaicsAddress() string { + m.readAddress() + return HandleCloneUserCredentials(m.address, true) +} + +// FullAddress returns mirror address from Git repository config. +func (m *Mirror) FullAddress() string { + m.readAddress() + return m.address +} + +// SaveAddress writes new address to Git repository config. +func (m *Mirror) SaveAddress(addr string) error { + configPath := m.Repo.GitConfigPath() + cfg, err := ini.Load(configPath) + if err != nil { + return fmt.Errorf("Load: %v", err) + } + + cfg.Section("remote \"origin\"").Key("url").SetValue(addr) + return cfg.SaveToIndent(configPath, "\t") +} + +// runSync returns true if sync finished without error. +func (m *Mirror) runSync() bool { + repoPath := m.Repo.RepoPath() + wikiPath := m.Repo.WikiPath() + timeout := time.Duration(setting.Git.Timeout.Mirror) * time.Second + + // Do a fast-fail testing against on repository URL to ensure it is accessible under + // good condition to prevent long blocking on URL resolution without syncing anything. + if !git.IsRepoURLAccessible(git.NetworkOptions{ + URL: m.FullAddress(), + Timeout: 10 * time.Second, + }) { + desc := fmt.Sprintf("Mirror repository URL is not accessible: %s", m.MosaicsAddress()) + if err := CreateRepositoryNotice(desc); err != nil { + log.Error(4, "CreateRepositoryNotice: %v", err) + } + return false + } + + gitArgs := []string{"remote", "update"} + if m.EnablePrune { + gitArgs = append(gitArgs, "--prune") + } + if _, stderr, err := process.ExecDir( + timeout, repoPath, fmt.Sprintf("Mirror.runSync: %s", repoPath), + "git", gitArgs...); err != nil { + desc := fmt.Sprintf("Fail to update mirror repository '%s': %s", repoPath, stderr) + log.Error(4, desc) + if err = CreateRepositoryNotice(desc); err != nil { + log.Error(4, "CreateRepositoryNotice: %v", err) + } + return false + } + if m.Repo.HasWiki() { + if _, stderr, err := process.ExecDir( + timeout, wikiPath, fmt.Sprintf("Mirror.runSync: %s", wikiPath), + "git", "remote", "update", "--prune"); err != nil { + desc := fmt.Sprintf("Fail to update mirror wiki repository '%s': %s", wikiPath, stderr) + log.Error(4, desc) + if err = CreateRepositoryNotice(desc); err != nil { + log.Error(4, "CreateRepositoryNotice: %v", err) + } + return false + } + } + + return true +} + +func getMirrorByRepoID(e Engine, repoID int64) (*Mirror, error) { + m := &Mirror{RepoID: repoID} + has, err := e.Get(m) + if err != nil { + return nil, err + } else if !has { + return nil, ErrMirrorNotExist + } + return m, nil +} + +// GetMirrorByRepoID returns mirror information of a repository. +func GetMirrorByRepoID(repoID int64) (*Mirror, error) { + return getMirrorByRepoID(x, repoID) +} + +func updateMirror(e Engine, m *Mirror) error { + _, err := e.Id(m.ID).AllCols().Update(m) + return err +} + +func UpdateMirror(m *Mirror) error { + return updateMirror(x, m) +} + +func DeleteMirrorByRepoID(repoID int64) error { + _, err := x.Delete(&Mirror{RepoID: repoID}) + return err +} + +// MirrorUpdate checks and updates mirror repositories. +func MirrorUpdate() { + if taskStatusTable.IsRunning(_MIRROR_UPDATE) { + return + } + taskStatusTable.Start(_MIRROR_UPDATE) + defer taskStatusTable.Stop(_MIRROR_UPDATE) + + log.Trace("Doing: MirrorUpdate") + + if err := x.Where("next_update_unix<=?", time.Now().Unix()).Iterate(new(Mirror), func(idx int, bean interface{}) error { + m := bean.(*Mirror) + if m.Repo == nil { + log.Error(4, "Disconnected mirror repository found: %d", m.ID) + return nil + } + + MirrorQueue.Add(m.RepoID) + return nil + }); err != nil { + log.Error(4, "MirrorUpdate: %v", err) + } +} + +// SyncMirrors checks and syncs mirrors. +// TODO: sync more mirrors at same time. +func SyncMirrors() { + // Start listening on new sync requests. + for repoID := range MirrorQueue.Queue() { + log.Trace("SyncMirrors [repo_id: %v]", repoID) + MirrorQueue.Remove(repoID) + + m, err := GetMirrorByRepoID(com.StrTo(repoID).MustInt64()) + if err != nil { + log.Error(4, "GetMirrorByRepoID [%s]: %v", repoID, err) + continue + } + + if !m.runSync() { + continue + } + + m.ScheduleNextUpdate() + if err = UpdateMirror(m); err != nil { + log.Error(4, "UpdateMirror [%s]: %v", repoID, err) + continue + } + } +} + +func InitSyncMirrors() { + go SyncMirrors() +} diff --git a/models/repo_mirror.go b/models/repo_mirror.go deleted file mode 100644 index 722d769f..00000000 --- a/models/repo_mirror.go +++ /dev/null @@ -1,264 +0,0 @@ -// Copyright 2016 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package models - -import ( - "fmt" - "strings" - "time" - - "github.com/Unknwon/com" - "github.com/go-xorm/xorm" - log "gopkg.in/clog.v1" - "gopkg.in/ini.v1" - - "github.com/gogits/git-module" - - "github.com/gogits/gogs/modules/process" - "github.com/gogits/gogs/modules/setting" - "github.com/gogits/gogs/modules/sync" -) - -var MirrorQueue = sync.NewUniqueQueue(setting.Repository.MirrorQueueLength) - -// Mirror represents mirror information of a repository. -type Mirror struct { - ID int64 `xorm:"pk autoincr"` - RepoID int64 - Repo *Repository `xorm:"-"` - Interval int // Hour. - EnablePrune bool `xorm:"NOT NULL DEFAULT true"` - - Updated time.Time `xorm:"-"` - UpdatedUnix int64 - NextUpdate time.Time `xorm:"-"` - NextUpdateUnix int64 - - address string `xorm:"-"` -} - -func (m *Mirror) BeforeInsert() { - m.UpdatedUnix = time.Now().Unix() - m.NextUpdateUnix = m.NextUpdate.Unix() -} - -func (m *Mirror) BeforeUpdate() { - m.UpdatedUnix = time.Now().Unix() - m.NextUpdateUnix = m.NextUpdate.Unix() -} - -func (m *Mirror) AfterSet(colName string, _ xorm.Cell) { - var err error - switch colName { - case "repo_id": - m.Repo, err = GetRepositoryByID(m.RepoID) - if err != nil { - log.Error(3, "GetRepositoryByID[%d]: %v", m.ID, err) - } - case "updated_unix": - m.Updated = time.Unix(m.UpdatedUnix, 0).Local() - case "next_updated_unix": - m.NextUpdate = time.Unix(m.NextUpdateUnix, 0).Local() - } -} - -// ScheduleNextUpdate calculates and sets next update time. -func (m *Mirror) ScheduleNextUpdate() { - m.NextUpdate = time.Now().Add(time.Duration(m.Interval) * time.Hour) -} - -func (m *Mirror) readAddress() { - if len(m.address) > 0 { - return - } - - cfg, err := ini.Load(m.Repo.GitConfigPath()) - if err != nil { - log.Error(4, "Load: %v", err) - return - } - m.address = cfg.Section("remote \"origin\"").Key("url").Value() -} - -// HandleCloneUserCredentials replaces user credentials from HTTP/HTTPS URL -// with placeholder . -// It will fail for any other forms of clone addresses. -func HandleCloneUserCredentials(url string, mosaics bool) string { - i := strings.Index(url, "@") - if i == -1 { - return url - } - start := strings.Index(url, "://") - if start == -1 { - return url - } - if mosaics { - return url[:start+3] + "" + url[i:] - } - return url[:start+3] + url[i+1:] -} - -// Address returns mirror address from Git repository config without credentials. -func (m *Mirror) Address() string { - m.readAddress() - return HandleCloneUserCredentials(m.address, false) -} - -// MosaicsAddress returns mirror address from Git repository config with credentials under mosaics. -func (m *Mirror) MosaicsAddress() string { - m.readAddress() - return HandleCloneUserCredentials(m.address, true) -} - -// FullAddress returns mirror address from Git repository config. -func (m *Mirror) FullAddress() string { - m.readAddress() - return m.address -} - -// SaveAddress writes new address to Git repository config. -func (m *Mirror) SaveAddress(addr string) error { - configPath := m.Repo.GitConfigPath() - cfg, err := ini.Load(configPath) - if err != nil { - return fmt.Errorf("Load: %v", err) - } - - cfg.Section("remote \"origin\"").Key("url").SetValue(addr) - return cfg.SaveToIndent(configPath, "\t") -} - -// runSync returns true if sync finished without error. -func (m *Mirror) runSync() bool { - repoPath := m.Repo.RepoPath() - wikiPath := m.Repo.WikiPath() - timeout := time.Duration(setting.Git.Timeout.Mirror) * time.Second - - // Do a fast-fail testing against on repository URL to ensure it is accessible under - // good condition to prevent long blocking on URL resolution without syncing anything. - if !git.IsRepoURLAccessible(git.NetworkOptions{ - URL: m.FullAddress(), - Timeout: 10 * time.Second, - }) { - desc := fmt.Sprintf("Mirror repository URL is not accessible: %s", m.MosaicsAddress()) - if err := CreateRepositoryNotice(desc); err != nil { - log.Error(4, "CreateRepositoryNotice: %v", err) - } - return false - } - - gitArgs := []string{"remote", "update"} - if m.EnablePrune { - gitArgs = append(gitArgs, "--prune") - } - if _, stderr, err := process.ExecDir( - timeout, repoPath, fmt.Sprintf("Mirror.runSync: %s", repoPath), - "git", gitArgs...); err != nil { - desc := fmt.Sprintf("Fail to update mirror repository '%s': %s", repoPath, stderr) - log.Error(4, desc) - if err = CreateRepositoryNotice(desc); err != nil { - log.Error(4, "CreateRepositoryNotice: %v", err) - } - return false - } - if m.Repo.HasWiki() { - if _, stderr, err := process.ExecDir( - timeout, wikiPath, fmt.Sprintf("Mirror.runSync: %s", wikiPath), - "git", "remote", "update", "--prune"); err != nil { - desc := fmt.Sprintf("Fail to update mirror wiki repository '%s': %s", wikiPath, stderr) - log.Error(4, desc) - if err = CreateRepositoryNotice(desc); err != nil { - log.Error(4, "CreateRepositoryNotice: %v", err) - } - return false - } - } - - return true -} - -func getMirrorByRepoID(e Engine, repoID int64) (*Mirror, error) { - m := &Mirror{RepoID: repoID} - has, err := e.Get(m) - if err != nil { - return nil, err - } else if !has { - return nil, ErrMirrorNotExist - } - return m, nil -} - -// GetMirrorByRepoID returns mirror information of a repository. -func GetMirrorByRepoID(repoID int64) (*Mirror, error) { - return getMirrorByRepoID(x, repoID) -} - -func updateMirror(e Engine, m *Mirror) error { - _, err := e.Id(m.ID).AllCols().Update(m) - return err -} - -func UpdateMirror(m *Mirror) error { - return updateMirror(x, m) -} - -func DeleteMirrorByRepoID(repoID int64) error { - _, err := x.Delete(&Mirror{RepoID: repoID}) - return err -} - -// MirrorUpdate checks and updates mirror repositories. -func MirrorUpdate() { - if taskStatusTable.IsRunning(_MIRROR_UPDATE) { - return - } - taskStatusTable.Start(_MIRROR_UPDATE) - defer taskStatusTable.Stop(_MIRROR_UPDATE) - - log.Trace("Doing: MirrorUpdate") - - if err := x.Where("next_update_unix<=?", time.Now().Unix()).Iterate(new(Mirror), func(idx int, bean interface{}) error { - m := bean.(*Mirror) - if m.Repo == nil { - log.Error(4, "Disconnected mirror repository found: %d", m.ID) - return nil - } - - MirrorQueue.Add(m.RepoID) - return nil - }); err != nil { - log.Error(4, "MirrorUpdate: %v", err) - } -} - -// SyncMirrors checks and syncs mirrors. -// TODO: sync more mirrors at same time. -func SyncMirrors() { - // Start listening on new sync requests. - for repoID := range MirrorQueue.Queue() { - log.Trace("SyncMirrors [repo_id: %v]", repoID) - MirrorQueue.Remove(repoID) - - m, err := GetMirrorByRepoID(com.StrTo(repoID).MustInt64()) - if err != nil { - log.Error(4, "GetMirrorByRepoID [%s]: %v", repoID, err) - continue - } - - if !m.runSync() { - continue - } - - m.ScheduleNextUpdate() - if err = UpdateMirror(m); err != nil { - log.Error(4, "UpdateMirror [%s]: %v", repoID, err) - continue - } - } -} - -func InitSyncMirrors() { - go SyncMirrors() -} -- cgit v1.2.3