| @@ -17,7 +17,7 @@ import ( | |||
| "github.com/gogits/gogs/modules/base" | |||
| ) | |||
| const APP_VER = "0.3.3.0506 Alpha" | |||
| const APP_VER = "0.3.3.0507 Alpha" | |||
| func init() { | |||
| base.AppVer = APP_VER | |||
| @@ -8,8 +8,6 @@ import ( | |||
| "errors" | |||
| "strings" | |||
| "time" | |||
| "github.com/gogits/gogs/modules/base" | |||
| ) | |||
| var ( | |||
| @@ -21,7 +19,7 @@ type Issue struct { | |||
| Id int64 | |||
| Index int64 // Index in one repository. | |||
| Name string | |||
| RepoId int64 `xorm:"index"` | |||
| RepoId int64 `xorm:"INDEX"` | |||
| Repo *Repository `xorm:"-"` | |||
| PosterId int64 | |||
| Poster *User `xorm:"-"` | |||
| @@ -35,44 +33,51 @@ type Issue struct { | |||
| Priority int | |||
| NumComments int | |||
| Deadline time.Time | |||
| Created time.Time `xorm:"created"` | |||
| Updated time.Time `xorm:"updated"` | |||
| Created time.Time `xorm:"CREATED"` | |||
| Updated time.Time `xorm:"UPDATED"` | |||
| } | |||
| func (i *Issue) GetPoster() (err error) { | |||
| i.Poster, err = GetUserById(i.PosterId) | |||
| return err | |||
| } | |||
| // IssseUser represents an issue-user relation. | |||
| type IssseUser struct { | |||
| Id int64 | |||
| Iid int64 // Issue ID. | |||
| Rid int64 // Repository ID. | |||
| Uid int64 // User ID. | |||
| IsRead bool | |||
| IsAssigned bool | |||
| IsMentioned bool | |||
| IsClosed bool | |||
| } | |||
| // CreateIssue creates new issue for repository. | |||
| func CreateIssue(userId, repoId, milestoneId, assigneeId int64, issueCount int, name, labels, content string, isPull bool) (issue *Issue, err error) { | |||
| func NewIssue(issue *Issue) (err error) { | |||
| sess := orm.NewSession() | |||
| defer sess.Close() | |||
| sess.Begin() | |||
| issue = &Issue{ | |||
| Index: int64(issueCount) + 1, | |||
| Name: name, | |||
| RepoId: repoId, | |||
| PosterId: userId, | |||
| MilestoneId: milestoneId, | |||
| AssigneeId: assigneeId, | |||
| IsPull: isPull, | |||
| Labels: labels, | |||
| Content: content, | |||
| if err = sess.Begin(); err != nil { | |||
| return err | |||
| } | |||
| if _, err = sess.Insert(issue); err != nil { | |||
| sess.Rollback() | |||
| return nil, err | |||
| return err | |||
| } | |||
| rawSql := "UPDATE `repository` SET num_issues = num_issues + 1 WHERE id = ?" | |||
| if _, err = sess.Exec(rawSql, repoId); err != nil { | |||
| if _, err = sess.Exec(rawSql, issue.RepoId); err != nil { | |||
| sess.Rollback() | |||
| return nil, err | |||
| return err | |||
| } | |||
| return issue, sess.Commit() | |||
| return sess.Commit() | |||
| } | |||
| // GetIssueById returns issue object by given id. | |||
| func GetIssueByIndex(repoId, index int64) (*Issue, error) { | |||
| issue := &Issue{RepoId: repoId, Index: index} | |||
| // GetIssueByIndex returns issue by given index in repository. | |||
| func GetIssueByIndex(rid, index int64) (*Issue, error) { | |||
| issue := &Issue{RepoId: rid, Index: index} | |||
| has, err := orm.Get(issue) | |||
| if err != nil { | |||
| return nil, err | |||
| @@ -83,30 +88,28 @@ func GetIssueByIndex(repoId, index int64) (*Issue, error) { | |||
| } | |||
| // GetIssues returns a list of issues by given conditions. | |||
| func GetIssues(userId, repoId, posterId, milestoneId int64, page int, isClosed, isMention bool, labels, sortType string) ([]Issue, error) { | |||
| func GetIssues(uid, rid, pid, mid int64, page int, isClosed bool, labels, sortType string) ([]Issue, error) { | |||
| sess := orm.Limit(20, (page-1)*20) | |||
| if repoId > 0 { | |||
| sess.Where("repo_id=?", repoId).And("is_closed=?", isClosed) | |||
| if rid > 0 { | |||
| sess.Where("repo_id=?", rid).And("is_closed=?", isClosed) | |||
| } else { | |||
| sess.Where("is_closed=?", isClosed) | |||
| } | |||
| if userId > 0 { | |||
| sess.And("assignee_id=?", userId) | |||
| } else if posterId > 0 { | |||
| sess.And("poster_id=?", posterId) | |||
| } else if isMention { | |||
| sess.And("mentions like '%$" + base.ToStr(userId) + "|%'") | |||
| if uid > 0 { | |||
| sess.And("assignee_id=?", uid) | |||
| } else if pid > 0 { | |||
| sess.And("poster_id=?", pid) | |||
| } | |||
| if milestoneId > 0 { | |||
| sess.And("milestone_id=?", milestoneId) | |||
| if mid > 0 { | |||
| sess.And("milestone_id=?", mid) | |||
| } | |||
| if len(labels) > 0 { | |||
| for _, label := range strings.Split(labels, ",") { | |||
| sess.And("mentions like '%$" + label + "|%'") | |||
| sess.And("labels like '%$" + label + "|%'") | |||
| } | |||
| } | |||
| @@ -130,22 +133,133 @@ func GetIssues(userId, repoId, posterId, milestoneId int64, page int, isClosed, | |||
| return issues, err | |||
| } | |||
| // PairsContains returns true when pairs list contains given issue. | |||
| func PairsContains(ius []*IssseUser, issueId int64) bool { | |||
| for i := range ius { | |||
| if ius[i].Iid == issueId { | |||
| return true | |||
| } | |||
| } | |||
| return false | |||
| } | |||
| // GetIssueUserPairs returns all issue-user pairs by given repository and user. | |||
| func GetIssueUserPairs(rid, uid int64, isClosed bool) ([]*IssseUser, error) { | |||
| ius := make([]*IssseUser, 0, 10) | |||
| err := orm.Find(&ius, &IssseUser{Rid: rid, Uid: uid, IsClosed: isClosed}) | |||
| return ius, err | |||
| } | |||
| // GetUserIssueCount returns the number of issues that were created by given user in repository. | |||
| func GetUserIssueCount(userId, repoId int64) int64 { | |||
| count, _ := orm.Where("poster_id=?", userId).And("repo_id=?", repoId).Count(new(Issue)) | |||
| func GetUserIssueCount(uid, rid int64) int64 { | |||
| count, _ := orm.Where("poster_id=?", uid).And("repo_id=?", rid).Count(new(Issue)) | |||
| return count | |||
| } | |||
| // IssueStats represents issue statistic information. | |||
| type IssueStats struct { | |||
| OpenCount, ClosedCount int64 | |||
| AllCount int64 | |||
| AssignCount int64 | |||
| CreateCount int64 | |||
| MentionCount int64 | |||
| } | |||
| // Filter modes. | |||
| const ( | |||
| FM_ASSIGN = iota + 1 | |||
| FM_CREATE | |||
| FM_MENTION | |||
| ) | |||
| // GetIssueStats returns issue statistic information by given condition. | |||
| func GetIssueStats(rid, uid int64, isShowClosed bool, filterMode int) *IssueStats { | |||
| stats := &IssueStats{} | |||
| issue := new(Issue) | |||
| sess := orm.Where("repo_id=?", rid) | |||
| tmpSess := sess | |||
| stats.OpenCount, _ = tmpSess.And("is_closed=?", false).Count(issue) | |||
| *tmpSess = *sess | |||
| stats.ClosedCount, _ = tmpSess.And("is_closed=?", true).Count(issue) | |||
| if isShowClosed { | |||
| stats.AllCount = stats.ClosedCount | |||
| } else { | |||
| stats.AllCount = stats.OpenCount | |||
| } | |||
| if filterMode != FM_MENTION { | |||
| sess = orm.Where("repo_id=?", rid) | |||
| switch filterMode { | |||
| case FM_ASSIGN: | |||
| sess.And("assignee_id=?", uid) | |||
| case FM_CREATE: | |||
| sess.And("poster_id=?", uid) | |||
| default: | |||
| goto nofilter | |||
| } | |||
| *tmpSess = *sess | |||
| stats.OpenCount, _ = tmpSess.And("is_closed=?", false).Count(issue) | |||
| *tmpSess = *sess | |||
| stats.ClosedCount, _ = tmpSess.And("is_closed=?", true).Count(issue) | |||
| } else { | |||
| sess := orm.Where("rid=?", rid).And("uid=?", uid).And("is_mentioned=?", true) | |||
| tmpSess := sess | |||
| stats.OpenCount, _ = tmpSess.And("is_closed=?", false).Count(new(IssseUser)) | |||
| *tmpSess = *sess | |||
| stats.ClosedCount, _ = tmpSess.And("is_closed=?", true).Count(new(IssseUser)) | |||
| } | |||
| nofilter: | |||
| stats.AssignCount, _ = orm.Where("repo_id=?", rid).And("is_closed=?", isShowClosed).And("assignee_id=?", uid).Count(issue) | |||
| stats.CreateCount, _ = orm.Where("repo_id=?", rid).And("is_closed=?", isShowClosed).And("poster_id=?", uid).Count(issue) | |||
| stats.MentionCount, _ = orm.Where("rid=?", rid).And("uid=?", uid).And("is_closed=?", isShowClosed).And("is_mentioned=?", true).Count(new(IssseUser)) | |||
| return stats | |||
| } | |||
| // GetUserIssueStats returns issue statistic information for dashboard by given condition. | |||
| func GetUserIssueStats(uid int64, filterMode int) *IssueStats { | |||
| stats := &IssueStats{} | |||
| issue := new(Issue) | |||
| iu := new(IssseUser) | |||
| sess := orm.Where("uid=?", uid) | |||
| tmpSess := sess | |||
| if filterMode == 0 { | |||
| stats.OpenCount, _ = tmpSess.And("is_closed=?", false).Count(iu) | |||
| *tmpSess = *sess | |||
| stats.ClosedCount, _ = tmpSess.And("is_closed=?", true).Count(iu) | |||
| } | |||
| switch filterMode { | |||
| case FM_ASSIGN: | |||
| sess.And("is_assigned=?", true) | |||
| *tmpSess = *sess | |||
| stats.OpenCount, _ = tmpSess.And("is_closed=?", false).Count(iu) | |||
| *tmpSess = *sess | |||
| stats.ClosedCount, _ = tmpSess.And("is_closed=?", true).Count(iu) | |||
| case FM_CREATE: | |||
| sess.Where("poster_id=?", uid) | |||
| *tmpSess = *sess | |||
| stats.OpenCount, _ = tmpSess.And("is_closed=?", false).Count(issue) | |||
| *tmpSess = *sess | |||
| stats.ClosedCount, _ = tmpSess.And("is_closed=?", true).Count(issue) | |||
| } | |||
| stats.AssignCount, _ = orm.Where("assignee_id=?", uid).And("is_closed=?", false).Count(issue) | |||
| stats.CreateCount, _ = orm.Where("poster_id=?", uid).And("is_closed=?", false).Count(issue) | |||
| return stats | |||
| } | |||
| // UpdateIssue updates information of issue. | |||
| func UpdateIssue(issue *Issue) error { | |||
| _, err := orm.Id(issue.Id).AllCols().Update(issue) | |||
| _, err := orm.AllCols().Update(issue) | |||
| return err | |||
| } | |||
| // Label represents a list of labels of repository for issues. | |||
| type Label struct { | |||
| Id int64 | |||
| RepoId int64 `xorm:"index"` | |||
| RepoId int64 `xorm:"INDEX"` | |||
| Names string | |||
| Colors string | |||
| } | |||
| @@ -154,12 +268,12 @@ type Label struct { | |||
| type Milestone struct { | |||
| Id int64 | |||
| Name string | |||
| RepoId int64 `xorm:"index"` | |||
| RepoId int64 `xorm:"INDEX"` | |||
| IsClosed bool | |||
| Content string | |||
| NumIssues int | |||
| DueDate time.Time | |||
| Created time.Time `xorm:"created"` | |||
| Created time.Time `xorm:"CREATED"` | |||
| } | |||
| // Issue types. | |||
| @@ -179,7 +293,7 @@ type Comment struct { | |||
| CommitId int64 | |||
| Line int64 | |||
| Content string | |||
| Created time.Time `xorm:"created"` | |||
| Created time.Time `xorm:"CREATED"` | |||
| } | |||
| // CreateComment creates comment of issue or commit. | |||
| @@ -34,7 +34,7 @@ var ( | |||
| func init() { | |||
| tables = append(tables, new(User), new(PublicKey), new(Repository), new(Watch), | |||
| new(Action), new(Access), new(Issue), new(Comment), new(Oauth2), new(Follow), | |||
| new(Mirror), new(Release), new(LoginSource), new(Webhook)) | |||
| new(Mirror), new(Release), new(LoginSource), new(Webhook), new(IssseUser)) | |||
| } | |||
| func LoadModelsConfig() { | |||
| @@ -6,9 +6,9 @@ package models | |||
| import ( | |||
| "bufio" | |||
| "bytes" | |||
| "errors" | |||
| "fmt" | |||
| "io" | |||
| "io/ioutil" | |||
| "os" | |||
| "path" | |||
| @@ -26,7 +26,7 @@ import ( | |||
| const ( | |||
| // "### autogenerated by gitgos, DO NOT EDIT\n" | |||
| TPL_PUBLICK_KEY = `command="%s serv key-%d",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty %s` | |||
| _TPL_PUBLICK_KEY = `command="%s serv key-%d",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty %s` + "\n" | |||
| ) | |||
| var ( | |||
| @@ -64,7 +64,7 @@ func init() { | |||
| } | |||
| } | |||
| // PublicKey represents a SSH key of user. | |||
| // PublicKey represents a SSH key. | |||
| type PublicKey struct { | |||
| Id int64 | |||
| OwnerId int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` | |||
| @@ -75,14 +75,29 @@ type PublicKey struct { | |||
| Updated time.Time `xorm:"UPDATED"` | |||
| } | |||
| // GenAuthorizedKey returns formatted public key string. | |||
| func GenAuthorizedKey(keyId int64, key string) string { | |||
| return fmt.Sprintf(TPL_PUBLICK_KEY+"\n", appPath, keyId, key) | |||
| // GetAuthorizedString generates and returns formatted public key string for authorized_keys file. | |||
| func (key *PublicKey) GetAuthorizedString() string { | |||
| return fmt.Sprintf(_TPL_PUBLICK_KEY, appPath, key.Id, key.Content) | |||
| } | |||
| // AddPublicKey adds new public key to database and SSH key file. | |||
| // saveAuthorizedKeyFile writes SSH key content to authorized_keys file. | |||
| func saveAuthorizedKeyFile(key *PublicKey) error { | |||
| sshOpLocker.Lock() | |||
| defer sshOpLocker.Unlock() | |||
| fpath := filepath.Join(sshPath, "authorized_keys") | |||
| f, err := os.OpenFile(fpath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| defer f.Close() | |||
| _, err = f.WriteString(key.GetAuthorizedString()) | |||
| return err | |||
| } | |||
| // AddPublicKey adds new public key to database and authorized_keys file. | |||
| func AddPublicKey(key *PublicKey) (err error) { | |||
| // Check if public key name has been used. | |||
| has, err := orm.Get(key) | |||
| if err != nil { | |||
| return err | |||
| @@ -91,7 +106,7 @@ func AddPublicKey(key *PublicKey) (err error) { | |||
| } | |||
| // Calculate fingerprint. | |||
| tmpPath := strings.Replace(filepath.Join(os.TempDir(), fmt.Sprintf("%d", time.Now().Nanosecond()), | |||
| tmpPath := strings.Replace(path.Join(os.TempDir(), fmt.Sprintf("%d", time.Now().Nanosecond()), | |||
| "id_rsa.pub"), "\\", "/", -1) | |||
| os.MkdirAll(path.Dir(tmpPath), os.ModePerm) | |||
| if err = ioutil.WriteFile(tmpPath, []byte(key.Content), os.ModePerm); err != nil { | |||
| @@ -108,8 +123,8 @@ func AddPublicKey(key *PublicKey) (err error) { | |||
| // Save SSH key. | |||
| if _, err = orm.Insert(key); err != nil { | |||
| return err | |||
| } | |||
| if err = SaveAuthorizedKeyFile(key); err != nil { | |||
| } else if err = saveAuthorizedKeyFile(key); err != nil { | |||
| // Roll back. | |||
| if _, err2 := orm.Delete(key); err2 != nil { | |||
| return err2 | |||
| } | |||
| @@ -119,6 +134,13 @@ func AddPublicKey(key *PublicKey) (err error) { | |||
| return nil | |||
| } | |||
| // ListPublicKey returns a list of all public keys that user has. | |||
| func ListPublicKey(uid int64) ([]PublicKey, error) { | |||
| keys := make([]PublicKey, 0, 5) | |||
| err := orm.Find(&keys, &PublicKey{OwnerId: uid}) | |||
| return keys, err | |||
| } | |||
| // rewriteAuthorizedKeys finds and deletes corresponding line in authorized_keys file. | |||
| func rewriteAuthorizedKeys(key *PublicKey, p, tmpP string) error { | |||
| sshOpLocker.Lock() | |||
| @@ -137,28 +159,38 @@ func rewriteAuthorizedKeys(key *PublicKey, p, tmpP string) error { | |||
| defer fw.Close() | |||
| isFound := false | |||
| keyword := []byte(fmt.Sprintf("key-%d", key.Id)) | |||
| content := []byte(key.Content) | |||
| snr := bufio.NewScanner(fr) | |||
| for snr.Scan() { | |||
| line := append(bytes.TrimSpace(snr.Bytes()), '\n') | |||
| if len(line) == 0 { | |||
| continue | |||
| keyword := fmt.Sprintf("key-%d", key.Id) | |||
| buf := bufio.NewReader(fr) | |||
| for { | |||
| line, errRead := buf.ReadString('\n') | |||
| line = strings.TrimSpace(line) | |||
| if errRead != nil { | |||
| if errRead != io.EOF { | |||
| return errRead | |||
| } | |||
| // Reached end of file, if nothing to read then break, | |||
| // otherwise handle the last line. | |||
| if len(line) == 0 { | |||
| break | |||
| } | |||
| } | |||
| // Found the line and copy rest of file. | |||
| if !isFound && bytes.Contains(line, keyword) && bytes.Contains(line, content) { | |||
| if !isFound && strings.Contains(line, keyword) && strings.Contains(line, key.Content) { | |||
| isFound = true | |||
| continue | |||
| } | |||
| // Still finding the line, copy the line that currently read. | |||
| if _, err = fw.Write(line); err != nil { | |||
| if _, err = fw.WriteString(line + "\n"); err != nil { | |||
| return err | |||
| } | |||
| } | |||
| if errRead == io.EOF { | |||
| break | |||
| } | |||
| } | |||
| return nil | |||
| } | |||
| @@ -175,37 +207,14 @@ func DeletePublicKey(key *PublicKey) error { | |||
| return err | |||
| } | |||
| p := filepath.Join(sshPath, "authorized_keys") | |||
| tmpP := filepath.Join(sshPath, "authorized_keys.tmp") | |||
| log.Trace("publickey.DeletePublicKey(authorized_keys): %s", p) | |||
| fpath := filepath.Join(sshPath, "authorized_keys") | |||
| tmpPath := filepath.Join(sshPath, "authorized_keys.tmp") | |||
| log.Trace("publickey.DeletePublicKey(authorized_keys): %s", fpath) | |||
| if err = rewriteAuthorizedKeys(key, p, tmpP); err != nil { | |||
| if err = rewriteAuthorizedKeys(key, fpath, tmpPath); err != nil { | |||
| return err | |||
| } else if err = os.Remove(p); err != nil { | |||
| } else if err = os.Remove(fpath); err != nil { | |||
| return err | |||
| } | |||
| return os.Rename(tmpP, p) | |||
| } | |||
| // ListPublicKey returns a list of public keys that user has. | |||
| func ListPublicKey(userId int64) ([]PublicKey, error) { | |||
| keys := make([]PublicKey, 0) | |||
| err := orm.Find(&keys, &PublicKey{OwnerId: userId}) | |||
| return keys, err | |||
| } | |||
| // SaveAuthorizedKeyFile writes SSH key content to SSH key file. | |||
| func SaveAuthorizedKeyFile(key *PublicKey) error { | |||
| sshOpLocker.Lock() | |||
| defer sshOpLocker.Unlock() | |||
| p := filepath.Join(sshPath, "authorized_keys") | |||
| f, err := os.OpenFile(p, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| defer f.Close() | |||
| _, err = f.WriteString(GenAuthorizedKey(key.Id, key.Content)) | |||
| return err | |||
| return os.Rename(tmpPath, fpath) | |||
| } | |||
| @@ -694,8 +694,8 @@ func GetRepositoryById(id int64) (*Repository, error) { | |||
| } | |||
| // GetRepositories returns the list of repositories of given user. | |||
| func GetRepositories(user *User, private bool) ([]Repository, error) { | |||
| repos := make([]Repository, 0, 10) | |||
| func GetRepositories(user *User, private bool) ([]*Repository, error) { | |||
| repos := make([]*Repository, 0, 10) | |||
| sess := orm.Desc("updated") | |||
| if !private { | |||
| sess.Where("is_private=?", false) | |||
| @@ -24,61 +24,84 @@ func Issues(ctx *middleware.Context) { | |||
| ctx.Data["Title"] = "Issues" | |||
| ctx.Data["IsRepoToolbarIssues"] = true | |||
| ctx.Data["IsRepoToolbarIssuesList"] = true | |||
| ctx.Data["ViewType"] = "all" | |||
| milestoneId, _ := base.StrTo(ctx.Query("milestone")).Int() | |||
| page, _ := base.StrTo(ctx.Query("page")).Int() | |||
| viewType := ctx.Query("type") | |||
| types := []string{"assigned", "created_by", "mentioned"} | |||
| if !com.IsSliceContainsStr(types, viewType) { | |||
| viewType = "all" | |||
| } | |||
| ctx.Data["IssueCreatedCount"] = 0 | |||
| isShowClosed := ctx.Query("state") == "closed" | |||
| var posterId int64 = 0 | |||
| isCreatedBy := ctx.Query("type") == "created_by" | |||
| if isCreatedBy { | |||
| if viewType != "all" { | |||
| if !ctx.IsSigned { | |||
| ctx.SetCookie("redirect_to", "/"+url.QueryEscape(ctx.Req.RequestURI)) | |||
| ctx.Redirect("/user/login/", 302) | |||
| ctx.Redirect("/user/login") | |||
| return | |||
| } | |||
| ctx.Data["ViewType"] = "created_by" | |||
| } | |||
| var assigneeId, posterId int64 | |||
| var filterMode int | |||
| switch viewType { | |||
| case "assigned": | |||
| assigneeId = ctx.User.Id | |||
| filterMode = models.FM_ASSIGN | |||
| case "created_by": | |||
| posterId = ctx.User.Id | |||
| filterMode = models.FM_CREATE | |||
| case "mentioned": | |||
| filterMode = models.FM_MENTION | |||
| } | |||
| mid, _ := base.StrTo(ctx.Query("milestone")).Int64() | |||
| page, _ := base.StrTo(ctx.Query("page")).Int() | |||
| // Get issues. | |||
| issues, err := models.GetIssues(0, ctx.Repo.Repository.Id, posterId, int64(milestoneId), page, | |||
| ctx.Query("state") == "closed", false, ctx.Query("labels"), ctx.Query("sortType")) | |||
| issues, err := models.GetIssues(assigneeId, ctx.Repo.Repository.Id, posterId, mid, page, | |||
| isShowClosed, ctx.Query("labels"), ctx.Query("sortType")) | |||
| if err != nil { | |||
| ctx.Handle(200, "issue.Issues: %v", err) | |||
| ctx.Handle(500, "issue.Issues(GetIssues): %v", err) | |||
| return | |||
| } | |||
| if ctx.IsSigned { | |||
| posterId = ctx.User.Id | |||
| var pairs []*models.IssseUser | |||
| if filterMode == models.FM_MENTION { | |||
| // Get issue-user pairs. | |||
| pairs, err = models.GetIssueUserPairs(ctx.Repo.Repository.Id, ctx.User.Id, isShowClosed) | |||
| if err != nil { | |||
| ctx.Handle(500, "issue.Issues(GetIssueUserPairs): %v", err) | |||
| return | |||
| } | |||
| } | |||
| var createdByCount int | |||
| showIssues := make([]models.Issue, 0, len(issues)) | |||
| // Get posters. | |||
| for i := range issues { | |||
| u, err := models.GetUserById(issues[i].PosterId) | |||
| if err != nil { | |||
| ctx.Handle(200, "issue.Issues(get poster): %v", err) | |||
| return | |||
| } | |||
| if isCreatedBy && u.Id != posterId { | |||
| if filterMode == models.FM_MENTION && !models.PairsContains(pairs, issues[i].Id) { | |||
| continue | |||
| } | |||
| if u.Id == posterId { | |||
| createdByCount++ | |||
| if err = issues[i].GetPoster(); err != nil { | |||
| ctx.Handle(500, "issue.Issues(GetPoster): %v", err) | |||
| return | |||
| } | |||
| issues[i].Poster = u | |||
| showIssues = append(showIssues, issues[i]) | |||
| } | |||
| ctx.Data["Issues"] = showIssues | |||
| ctx.Data["IssueCount"] = ctx.Repo.Repository.NumIssues | |||
| ctx.Data["OpenCount"] = ctx.Repo.Repository.NumOpenIssues | |||
| ctx.Data["ClosedCount"] = ctx.Repo.Repository.NumClosedIssues | |||
| ctx.Data["IssueCreatedCount"] = createdByCount | |||
| ctx.Data["IsShowClosed"] = ctx.Query("state") == "closed" | |||
| var uid int64 = -1 | |||
| if ctx.User != nil { | |||
| uid = ctx.User.Id | |||
| } | |||
| issueStats := models.GetIssueStats(ctx.Repo.Repository.Id, uid, isShowClosed, filterMode) | |||
| ctx.Data["IssueStats"] = issueStats | |||
| ctx.Data["ViewType"] = viewType | |||
| ctx.Data["Issues"] = issues | |||
| ctx.Data["IsShowClosed"] = isShowClosed | |||
| if isShowClosed { | |||
| ctx.Data["State"] = "closed" | |||
| ctx.Data["ShowCount"] = issueStats.ClosedCount | |||
| } else { | |||
| ctx.Data["ShowCount"] = issueStats.OpenCount | |||
| } | |||
| ctx.HTML(200, "issue/list") | |||
| } | |||
| @@ -99,15 +122,23 @@ func CreateIssuePost(ctx *middleware.Context, params martini.Params, form auth.C | |||
| return | |||
| } | |||
| issue, err := models.CreateIssue(ctx.User.Id, ctx.Repo.Repository.Id, form.MilestoneId, form.AssigneeId, | |||
| ctx.Repo.Repository.NumIssues, form.IssueName, form.Labels, form.Content, false) | |||
| if err != nil { | |||
| ctx.Handle(500, "issue.CreateIssue(CreateIssue)", err) | |||
| issue := &models.Issue{ | |||
| Index: int64(ctx.Repo.Repository.NumIssues) + 1, | |||
| Name: form.IssueName, | |||
| RepoId: ctx.Repo.Repository.Id, | |||
| PosterId: ctx.User.Id, | |||
| MilestoneId: form.MilestoneId, | |||
| AssigneeId: form.AssigneeId, | |||
| Labels: form.Labels, | |||
| Content: form.Content, | |||
| } | |||
| if err := models.NewIssue(issue); err != nil { | |||
| ctx.Handle(500, "issue.CreateIssue(NewIssue)", err) | |||
| return | |||
| } | |||
| // Notify watchers. | |||
| if err = models.NotifyWatchers(&models.Action{ActUserId: ctx.User.Id, ActUserName: ctx.User.Name, ActEmail: ctx.User.Email, | |||
| if err := models.NotifyWatchers(&models.Action{ActUserId: ctx.User.Id, ActUserName: ctx.User.Name, ActEmail: ctx.User.Email, | |||
| OpType: models.OP_CREATE_ISSUE, Content: fmt.Sprintf("%d|%s", issue.Index, issue.Name), | |||
| RepoId: ctx.Repo.Repository.Id, RepoName: ctx.Repo.Repository.Name, RefName: ""}); err != nil { | |||
| ctx.Handle(500, "issue.CreateIssue(NotifyWatchers)", err) | |||
| @@ -144,13 +175,13 @@ func CreateIssuePost(ctx *middleware.Context, params martini.Params, form auth.C | |||
| } | |||
| func ViewIssue(ctx *middleware.Context, params martini.Params) { | |||
| index, err := base.StrTo(params["index"]).Int() | |||
| if err != nil { | |||
| ctx.Handle(404, "issue.ViewIssue", err) | |||
| idx, _ := base.StrTo(params["index"]).Int64() | |||
| if idx == 0 { | |||
| ctx.Handle(404, "issue.ViewIssue", nil) | |||
| return | |||
| } | |||
| issue, err := models.GetIssueByIndex(ctx.Repo.Repository.Id, int64(index)) | |||
| issue, err := models.GetIssueByIndex(ctx.Repo.Repository.Id, idx) | |||
| if err != nil { | |||
| if err == models.ErrIssueNotExist { | |||
| ctx.Handle(404, "issue.ViewIssue", err) | |||
| @@ -160,10 +191,10 @@ func ViewIssue(ctx *middleware.Context, params martini.Params) { | |||
| return | |||
| } | |||
| // Get posters. | |||
| // Get poster. | |||
| u, err := models.GetUserById(issue.PosterId) | |||
| if err != nil { | |||
| ctx.Handle(200, "issue.ViewIssue(get poster): %v", err) | |||
| ctx.Handle(500, "issue.ViewIssue(GetUserById): %v", err) | |||
| return | |||
| } | |||
| issue.Poster = u | |||
| @@ -172,7 +203,7 @@ func ViewIssue(ctx *middleware.Context, params martini.Params) { | |||
| // Get comments. | |||
| comments, err := models.GetIssueComments(issue.Id) | |||
| if err != nil { | |||
| ctx.Handle(200, "issue.ViewIssue(get comments): %v", err) | |||
| ctx.Handle(500, "issue.ViewIssue(GetIssueComments): %v", err) | |||
| return | |||
| } | |||
| @@ -180,7 +211,7 @@ func ViewIssue(ctx *middleware.Context, params martini.Params) { | |||
| for i := range comments { | |||
| u, err := models.GetUserById(comments[i].PosterId) | |||
| if err != nil { | |||
| ctx.Handle(200, "issue.ViewIssue(get poster): %v", err) | |||
| ctx.Handle(500, "issue.ViewIssue(get poster of comment): %v", err) | |||
| return | |||
| } | |||
| comments[i].Poster = u | |||
| @@ -7,6 +7,7 @@ package user | |||
| import ( | |||
| "fmt" | |||
| "github.com/Unknwon/com" | |||
| "github.com/go-martini/martini" | |||
| "github.com/gogits/gogs/models" | |||
| @@ -105,85 +106,132 @@ func Feeds(ctx *middleware.Context, form auth.FeedsForm) { | |||
| func Issues(ctx *middleware.Context) { | |||
| ctx.Data["Title"] = "Your Issues" | |||
| ctx.Data["ViewType"] = "all" | |||
| page, _ := base.StrTo(ctx.Query("page")).Int() | |||
| repoId, _ := base.StrTo(ctx.Query("repoid")).Int64() | |||
| viewType := ctx.Query("type") | |||
| types := []string{"assigned", "created_by"} | |||
| if !com.IsSliceContainsStr(types, viewType) { | |||
| viewType = "all" | |||
| } | |||
| ctx.Data["RepoId"] = repoId | |||
| isShowClosed := ctx.Query("state") == "closed" | |||
| var posterId int64 = 0 | |||
| if ctx.Query("type") == "created_by" { | |||
| var assigneeId, posterId int64 | |||
| var filterMode int | |||
| switch viewType { | |||
| case "assigned": | |||
| assigneeId = ctx.User.Id | |||
| filterMode = models.FM_ASSIGN | |||
| case "created_by": | |||
| posterId = ctx.User.Id | |||
| ctx.Data["ViewType"] = "created_by" | |||
| filterMode = models.FM_CREATE | |||
| } | |||
| _, _ = assigneeId, posterId | |||
| // page, _ := base.StrTo(ctx.Query("page")).Int() | |||
| // repoId, _ := base.StrTo(ctx.Query("repoid")).Int64() | |||
| // ctx.Data["RepoId"] = repoId | |||
| // var posterId int64 = 0 | |||
| // if ctx.Query("type") == "created_by" { | |||
| // posterId = ctx.User.Id | |||
| // ctx.Data["ViewType"] = "created_by" | |||
| // } | |||
| rid, _ := base.StrTo(ctx.Query("repoid")).Int64() | |||
| issueStats := models.GetUserIssueStats(ctx.User.Id, filterMode) | |||
| // Get all repositories. | |||
| repos, err := models.GetRepositories(ctx.User, true) | |||
| if err != nil { | |||
| ctx.Handle(200, "user.Issues(get repositories)", err) | |||
| ctx.Handle(500, "user.Issues(get repositories)", err) | |||
| return | |||
| } | |||
| showRepos := make([]models.Repository, 0, len(repos)) | |||
| isShowClosed := ctx.Query("state") == "closed" | |||
| var closedIssueCount, createdByCount, allIssueCount int | |||
| showRepos := make([]*models.Repository, 0, len(repos)) | |||
| // Get all issues. | |||
| allIssues := make([]models.Issue, 0, 5*len(repos)) | |||
| for i, repo := range repos { | |||
| issues, err := models.GetIssues(0, repo.Id, posterId, 0, page, isShowClosed, false, "", "") | |||
| if err != nil { | |||
| ctx.Handle(200, "user.Issues(get issues)", err) | |||
| return | |||
| } | |||
| allIssueCount += repo.NumIssues | |||
| closedIssueCount += repo.NumClosedIssues | |||
| // Set repository information to issues. | |||
| for j := range issues { | |||
| issues[j].Repo = &repos[i] | |||
| // allIssues := make([]models.Issue, 0, 5*len(repos)) | |||
| for _, repo := range repos { | |||
| if repo.NumIssues == 0 { | |||
| continue | |||
| } | |||
| allIssues = append(allIssues, issues...) | |||
| repos[i].NumOpenIssues = repo.NumIssues - repo.NumClosedIssues | |||
| if repos[i].NumOpenIssues > 0 { | |||
| showRepos = append(showRepos, repos[i]) | |||
| } | |||
| } | |||
| repo.NumOpenIssues = repo.NumIssues - repo.NumClosedIssues | |||
| issueStats.AllCount += int64(repo.NumOpenIssues) | |||
| showIssues := make([]models.Issue, 0, len(allIssues)) | |||
| ctx.Data["IsShowClosed"] = isShowClosed | |||
| // switch filterMode{ | |||
| // case models.FM_ASSIGN: | |||
| // Get posters and filter issues. | |||
| for i := range allIssues { | |||
| u, err := models.GetUserById(allIssues[i].PosterId) | |||
| if err != nil { | |||
| ctx.Handle(200, "user.Issues(get poster): %v", err) | |||
| return | |||
| } | |||
| allIssues[i].Poster = u | |||
| if u.Id == ctx.User.Id { | |||
| createdByCount++ | |||
| } | |||
| // } | |||
| if repoId > 0 && repoId != allIssues[i].Repo.Id { | |||
| continue | |||
| if isShowClosed { | |||
| if repo.NumClosedIssues > 0 { | |||
| showRepos = append(showRepos, repo) | |||
| } | |||
| } else { | |||
| if repo.NumOpenIssues > 0 { | |||
| showRepos = append(showRepos, repo) | |||
| } | |||
| } | |||
| if isShowClosed == allIssues[i].IsClosed { | |||
| showIssues = append(showIssues, allIssues[i]) | |||
| } | |||
| // issues, err := models.GetIssues(0, repo.Id, posterId, 0, page, isShowClosed, "", "") | |||
| // if err != nil { | |||
| // ctx.Handle(200, "user.Issues(get issues)", err) | |||
| // return | |||
| // } | |||
| } | |||
| // allIssueCount += repo.NumIssues | |||
| // closedIssueCount += repo.NumClosedIssues | |||
| // // Set repository information to issues. | |||
| // for j := range issues { | |||
| // issues[j].Repo = &repos[i] | |||
| // } | |||
| // allIssues = append(allIssues, issues...) | |||
| // repos[i].NumOpenIssues = repo.NumIssues - repo.NumClosedIssues | |||
| // if repos[i].NumOpenIssues > 0 { | |||
| // showRepos = append(showRepos, repos[i]) | |||
| // } | |||
| // } | |||
| // showIssues := make([]models.Issue, 0, len(allIssues)) | |||
| // ctx.Data["IsShowClosed"] = isShowClosed | |||
| // // Get posters and filter issues. | |||
| // for i := range allIssues { | |||
| // u, err := models.GetUserById(allIssues[i].PosterId) | |||
| // if err != nil { | |||
| // ctx.Handle(200, "user.Issues(get poster): %v", err) | |||
| // return | |||
| // } | |||
| // allIssues[i].Poster = u | |||
| // if u.Id == ctx.User.Id { | |||
| // createdByCount++ | |||
| // } | |||
| // if repoId > 0 && repoId != allIssues[i].Repo.Id { | |||
| // continue | |||
| // } | |||
| // if isShowClosed == allIssues[i].IsClosed { | |||
| // showIssues = append(showIssues, allIssues[i]) | |||
| // } | |||
| // } | |||
| ctx.Data["RepoId"] = rid | |||
| ctx.Data["Repos"] = showRepos | |||
| ctx.Data["Issues"] = showIssues | |||
| ctx.Data["AllIssueCount"] = allIssueCount | |||
| ctx.Data["ClosedIssueCount"] = closedIssueCount | |||
| ctx.Data["OpenIssueCount"] = allIssueCount - closedIssueCount | |||
| ctx.Data["CreatedByCount"] = createdByCount | |||
| ctx.Data["ViewType"] = viewType | |||
| ctx.Data["IssueStats"] = issueStats | |||
| ctx.Data["IsShowClosed"] = isShowClosed | |||
| if isShowClosed { | |||
| ctx.Data["State"] = "closed" | |||
| ctx.Data["ShowCount"] = issueStats.ClosedCount | |||
| } else { | |||
| ctx.Data["ShowCount"] = issueStats.OpenCount | |||
| } | |||
| ctx.HTML(200, "issue/user") | |||
| } | |||
| @@ -17,6 +17,7 @@ | |||
| <th>Name</th> | |||
| <th>Private</th> | |||
| <th>Watches</th> | |||
| <th>Issues</th> | |||
| <th>Forks</th> | |||
| <th>Created</th> | |||
| </tr> | |||
| @@ -29,6 +30,7 @@ | |||
| <td><a href="/{{.UserName}}/{{.Name}}">{{.Name}}</a></td> | |||
| <td><i class="fa fa{{if .IsPrivate}}-check{{end}}-square-o"></i></td> | |||
| <td>{{.NumWatches}}</td> | |||
| <td>{{.NumIssues}}</td> | |||
| <td>{{.NumForks}}</td> | |||
| <td>{{DateFormat .Created "M d, Y"}}</td> | |||
| </tr> | |||
| @@ -6,21 +6,21 @@ | |||
| <div id="issue"> | |||
| <div class="col-md-3 filter-list"> | |||
| <ul class="list-unstyled"> | |||
| <li><a href="{{.RepoLink}}/issues"{{if eq .ViewType "all"}} class="active"{{end}}>All Issues <strong class="pull-right">{{.IssueCount}}</strong></a></li> | |||
| <!-- <li><a href="#">Assigned to you</a></li> --> | |||
| <li><a href="{{.RepoLink}}/issues?type=created_by"{{if eq .ViewType "created_by"}} class="active"{{end}}>Created by you <strong class="pull-right">{{.IssueCreatedCount}}</strong></a></li> | |||
| <!-- <li><a href="#">Mentioned</a></li> --> | |||
| <li><a href="{{.RepoLink}}/issues?state={{.State}}"{{if eq .ViewType "all"}} class="active"{{end}}>All Issues <strong class="pull-right">{{..IssueStats.AllCount}}</strong></a></li> | |||
| <li><a href="{{.RepoLink}}/issues?type=assigned&state={{.State}}"{{if eq .ViewType "assigned"}} class="active"{{end}}>Assigned to you <strong class="pull-right">{{.IssueStats.AssignCount}}</strong></a></li> | |||
| <li><a href="{{.RepoLink}}/issues?type=created_by&state={{.State}}"{{if eq .ViewType "created_by"}} class="active"{{end}}>Created by you <strong class="pull-right">{{.IssueStats.CreateCount}}</strong></a></li> | |||
| <li><a href="{{.RepoLink}}/issues?type=mentioned&state={{.State}}"{{if eq .ViewType "mentioned"}} class="active"{{end}}>Mentioning you <strong class="pull-right">{{.IssueStats.MentionCount}}</strong></a></li> | |||
| </ul> | |||
| </div> | |||
| <div class="col-md-9"> | |||
| <div class="filter-option"> | |||
| <div class="btn-group"> | |||
| <a class="btn btn-default issue-open{{if not .IsShowClosed}} active{{end}}" href="{{.RepoLink}}/issues?type={{.ViewType}}">{{.OpenCount}} Open</a> | |||
| <a class="btn btn-default issue-close{{if .IsShowClosed}} active{{end}}" href="{{.RepoLink}}/issues?state=closed&type={{.ViewType}}">{{.ClosedCount}} Closed</a> | |||
| <a class="btn btn-default issue-open{{if not .IsShowClosed}} active{{end}}" href="{{.RepoLink}}/issues?type={{.ViewType}}">{{..IssueStats.OpenCount}} Open</a> | |||
| <a class="btn btn-default issue-close{{if .IsShowClosed}} active{{end}}" href="{{.RepoLink}}/issues?type={{.ViewType}}&state=closed">{{.IssueStats.ClosedCount}} Closed</a> | |||
| </div> | |||
| </div> | |||
| <div class="issues list-group"> | |||
| {{range .Issues}} | |||
| {{range .Issues}}{{if .Poster}} | |||
| <div class="list-group-item issue-item" id="issue-{{.Id}}"> | |||
| <span class="number pull-right">#{{.Index}}</span> | |||
| <h5 class="title"><a href="{{$.RepoLink}}/issues/{{.Index}}">{{.Name}}</a></h5> | |||
| @@ -31,7 +31,7 @@ | |||
| <span class="comment"><i class="fa fa-comments"></i> {{.NumComments}}</span> | |||
| </p> | |||
| </div> | |||
| {{end}} | |||
| {{end}}{{end}} | |||
| </div> | |||
| </div> | |||
| </div> | |||
| @@ -16,9 +16,9 @@ | |||
| <div id="issue"> | |||
| <div class="col-md-3 filter-list"> | |||
| <ul class="list-unstyled"> | |||
| <li><a href="/issues"{{if eq .ViewType "all"}} class="active"{{end}}>In your repositories <strong class="pull-right">{{.AllIssueCount}}</strong></a></li> | |||
| <!-- <li><a href="#">Assigned to you</a></li> --> | |||
| <li><a href="/issues?type=created_by"{{if eq .ViewType "created_by"}} class="active"{{end}}>Created by you <strong class="pull-right">{{.CreatedByCount}}</strong></a></li> | |||
| <li><a href="/issues?state={{.State}}&repoid={{.RepoId}}"{{if eq .ViewType "all"}} class="active"{{end}}>In your repositories <strong class="pull-right">{{.IssueStats.AllCount}}</strong></a></li> | |||
| <li><a href="/issues?type=assigned&repoid={{.RepoId}}&state={{.State}}"{{if eq .ViewType "assigned"}} class="active"{{end}}>Assigned to you <strong class="pull-right">{{.IssueStats.AssignCount}}</strong></a></li> | |||
| <li><a href="/issues?type=created_by&repoid={{.RepoId}}&state={{.State}}"{{if eq .ViewType "created_by"}} class="active"{{end}}>Created by you <strong class="pull-right">{{.IssueStats.CreateCount}}</strong></a></li> | |||
| <li><hr/></li> | |||
| {{range .Repos}} | |||
| <li><a href="/issues?type={{$.ViewType}}{{if eq $.RepoId .Id}}{{else}}&repoid={{.Id}}{{end}}" class="sm{{if eq $.RepoId .Id}} active{{end}}">{{$.SignedUser.Name}}/{{.Name}} <strong class="pull-right">{{.NumOpenIssues}}</strong></a></li> | |||
| @@ -28,8 +28,8 @@ | |||
| <div class="col-md-9"> | |||
| <div class="filter-option"> | |||
| <div class="btn-group"> | |||
| <a class="btn btn-default issue-open{{if not .IsShowClosed}} active{{end}}" href="/issues?type={{.ViewType}}&repoid={{.RepoId}}">{{.OpenIssueCount}} Open</a> | |||
| <a class="btn btn-default issue-close{{if .IsShowClosed}} active{{end}}" href="/issues?state=closed&type={{.ViewType}}&repoid={{.RepoId}}">{{.ClosedIssueCount}} Closed</a> | |||
| <a class="btn btn-default issue-open{{if not .IsShowClosed}} active{{end}}" href="/issues?type={{.ViewType}}">{{..IssueStats.OpenCount}} Open</a> | |||
| <a class="btn btn-default issue-close{{if .IsShowClosed}} active{{end}}" href="/issues?type={{.ViewType}}&state=closed">{{.IssueStats.ClosedCount}} Closed</a> | |||
| </div> | |||
| </div> | |||
| <div class="issues list-group"> | |||