mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-28 09:21:13 -05:00
Add notify watcher action
This commit is contained in:
parent
c5ff58272b
commit
d3b8e9daa1
8 changed files with 119 additions and 95 deletions
|
@ -32,7 +32,7 @@ More importantly, Gogs only needs one binary to setup your own project hosting o
|
||||||
- Create/delete/watch public repository.
|
- Create/delete/watch public repository.
|
||||||
- User profile page.
|
- User profile page.
|
||||||
- Repository viewer.
|
- Repository viewer.
|
||||||
- Gravatar support.
|
- Gravatar and cache support.
|
||||||
- Mail service(register).
|
- Mail service(register).
|
||||||
- Administration panel.
|
- Administration panel.
|
||||||
- Supports MySQL, PostgreSQL and SQLite3(binary release only).
|
- Supports MySQL, PostgreSQL and SQLite3(binary release only).
|
||||||
|
|
|
@ -28,7 +28,7 @@ Gogs 完全使用 Go 语言来实现对 Git 数据的操作,实现 **零** 依
|
||||||
- 创建/删除/关注公开仓库
|
- 创建/删除/关注公开仓库
|
||||||
- 用户个人信息页面
|
- 用户个人信息页面
|
||||||
- 仓库浏览器
|
- 仓库浏览器
|
||||||
- Gravatar 支持
|
- Gravatar 以及缓存支持
|
||||||
- 邮件服务(注册)
|
- 邮件服务(注册)
|
||||||
- 管理员面板
|
- 管理员面板
|
||||||
- 支持 MySQL、PostgreSQL 以及 SQLite3(仅限二进制版本)
|
- 支持 MySQL、PostgreSQL 以及 SQLite3(仅限二进制版本)
|
||||||
|
|
|
@ -19,6 +19,7 @@ const (
|
||||||
OP_STAR_REPO
|
OP_STAR_REPO
|
||||||
OP_FOLLOW_REPO
|
OP_FOLLOW_REPO
|
||||||
OP_COMMIT_REPO
|
OP_COMMIT_REPO
|
||||||
|
OP_CREATE_ISSUE
|
||||||
OP_PULL_REQUEST
|
OP_PULL_REQUEST
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -67,34 +68,10 @@ func CommitRepoAction(userId int64, userName string,
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add feeds for user self and all watchers.
|
if err = NotifyWatchers(userId, repoId, OP_COMMIT_REPO, userName, repoName, refName, string(bs)); err != nil {
|
||||||
watches, err := GetWatches(repoId)
|
log.Error("action.CommitRepoAction(notify watchers): %d/%s", userId, repoName)
|
||||||
if err != nil {
|
|
||||||
log.Error("action.CommitRepoAction(get watches): %d/%s", userId, repoName)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
watches = append(watches, Watch{UserId: userId})
|
|
||||||
|
|
||||||
for i := range watches {
|
|
||||||
if userId == watches[i].UserId && i > 0 {
|
|
||||||
continue // Do not add twice in case author watches his/her repository.
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = orm.InsertOne(&Action{
|
|
||||||
UserId: watches[i].UserId,
|
|
||||||
ActUserId: userId,
|
|
||||||
ActUserName: userName,
|
|
||||||
OpType: OP_COMMIT_REPO,
|
|
||||||
Content: string(bs),
|
|
||||||
RepoId: repoId,
|
|
||||||
RepoName: repoName,
|
|
||||||
RefName: refName,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Error("action.CommitRepoAction(notify watches): %d/%s", userId, repoName)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update repository last update time.
|
// Update repository last update time.
|
||||||
repo, err := GetRepositoryByName(userId, repoName)
|
repo, err := GetRepositoryByName(userId, repoName)
|
||||||
|
|
|
@ -23,6 +23,7 @@ type Issue struct {
|
||||||
Name string
|
Name string
|
||||||
RepoId int64 `xorm:"index"`
|
RepoId int64 `xorm:"index"`
|
||||||
PosterId int64
|
PosterId int64
|
||||||
|
Poster *User `xorm:"-"`
|
||||||
MilestoneId int64
|
MilestoneId int64
|
||||||
AssigneeId int64
|
AssigneeId int64
|
||||||
IsPull bool // Indicates whether is a pull request or not.
|
IsPull bool // Indicates whether is a pull request or not.
|
||||||
|
|
|
@ -262,27 +262,27 @@ func initRepository(f string, user *User, repo *Repository, initReadme bool, rep
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
// hook/post-update
|
// hook/post-update
|
||||||
pu, err := os.OpenFile(filepath.Join(repoPath, "hooks", "post-update"), os.O_CREATE|os.O_WRONLY, 0777)
|
pu, err := os.OpenFile(filepath.Join(repoPath, "hooks", "post-update"), os.O_CREATE|os.O_WRONLY, 0777)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer pu.Close()
|
defer pu.Close()
|
||||||
// TODO: Windows .bat
|
// TODO: Windows .bat
|
||||||
if _, err = pu.WriteString(fmt.Sprintf("#!/usr/bin/env bash\n%s update\n", appPath)); err != nil {
|
if _, err = pu.WriteString(fmt.Sprintf("#!/usr/bin/env bash\n%s update\n", appPath)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// hook/post-update
|
// hook/post-update
|
||||||
pu2, err := os.OpenFile(filepath.Join(repoPath, "hooks", "post-receive"), os.O_CREATE|os.O_WRONLY, 0777)
|
pu2, err := os.OpenFile(filepath.Join(repoPath, "hooks", "post-receive"), os.O_CREATE|os.O_WRONLY, 0777)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer pu2.Close()
|
defer pu2.Close()
|
||||||
// TODO: Windows .bat
|
// TODO: Windows .bat
|
||||||
if _, err = pu2.WriteString("#!/usr/bin/env bash\ngit update-server-info\n"); err != nil {
|
if _, err = pu2.WriteString("#!/usr/bin/env bash\ngit update-server-info\n"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Initialize repository according to user's choice.
|
// Initialize repository according to user's choice.
|
||||||
|
@ -506,6 +506,37 @@ func GetWatches(repoId int64) ([]Watch, error) {
|
||||||
return watches, err
|
return watches, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NotifyWatchers creates batch of actions for every watcher.
|
||||||
|
func NotifyWatchers(userId, repoId int64, opType int, userName, repoName, refName, content string) error {
|
||||||
|
// Add feeds for user self and all watchers.
|
||||||
|
watches, err := GetWatches(repoId)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("repo.NotifyWatchers(get watches): " + err.Error())
|
||||||
|
}
|
||||||
|
watches = append(watches, Watch{UserId: userId})
|
||||||
|
|
||||||
|
for i := range watches {
|
||||||
|
if userId == watches[i].UserId && i > 0 {
|
||||||
|
continue // Do not add twice in case author watches his/her repository.
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = orm.InsertOne(&Action{
|
||||||
|
UserId: watches[i].UserId,
|
||||||
|
ActUserId: userId,
|
||||||
|
ActUserName: userName,
|
||||||
|
OpType: opType,
|
||||||
|
Content: content,
|
||||||
|
RepoId: repoId,
|
||||||
|
RepoName: repoName,
|
||||||
|
RefName: refName,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("repo.NotifyWatchers(create action): " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// IsWatching checks if user has watched given repository.
|
// IsWatching checks if user has watched given repository.
|
||||||
func IsWatching(userId, repoId int64) bool {
|
func IsWatching(userId, repoId int64) bool {
|
||||||
has, _ := orm.Get(&Watch{0, repoId, userId})
|
has, _ := orm.Get(&Watch{0, repoId, userId})
|
||||||
|
|
|
@ -486,15 +486,19 @@ func ActionIcon(opType int) string {
|
||||||
return "plus-circle"
|
return "plus-circle"
|
||||||
case 5: // Commit repository.
|
case 5: // Commit repository.
|
||||||
return "arrow-circle-o-right"
|
return "arrow-circle-o-right"
|
||||||
|
case 6: // Create issue.
|
||||||
|
return "exclamation-circle"
|
||||||
default:
|
default:
|
||||||
return "invalid type"
|
return "invalid type"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TPL_CREATE_REPO = `<a href="/user/%s">%s</a> created repository <a href="/%s/%s">%s</a>`
|
TPL_CREATE_REPO = `<a href="/user/%s">%s</a> created repository <a href="/%s">%s</a>`
|
||||||
TPL_COMMIT_REPO = `<a href="/user/%s">%s</a> pushed to <a href="/%s/%s/tree/%s">%s</a> at <a href="/%s/%s">%s/%s</a>%s`
|
TPL_COMMIT_REPO = `<a href="/user/%s">%s</a> pushed to <a href="/%s/src/%s">%s</a> at <a href="/%s">%s</a>%s`
|
||||||
TPL_COMMIT_REPO_LI = `<div><img id="gogs-user-avatar-commit" src="%s?s=16" alt="user-avatar" title="username"/> <a href="/%s/%s/commit/%s">%s</a> %s</div>`
|
TPL_COMMIT_REPO_LI = `<div><img src="%s?s=16" alt="user-avatar"/> <a href="/%s/commit/%s">%s</a> %s</div>`
|
||||||
|
TPL_CREATE_Issue = `<a href="/user/%s">%s</a> opened issue <a href="/%s/issues/%s">%s#%s</a>
|
||||||
|
<div><img src="%s?s=16" alt="user-avatar"/> %s</div>`
|
||||||
)
|
)
|
||||||
|
|
||||||
type PushCommits struct {
|
type PushCommits struct {
|
||||||
|
@ -507,11 +511,12 @@ type PushCommits struct {
|
||||||
func ActionDesc(act Actioner, avatarLink string) string {
|
func ActionDesc(act Actioner, avatarLink string) string {
|
||||||
actUserName := act.GetActUserName()
|
actUserName := act.GetActUserName()
|
||||||
repoName := act.GetRepoName()
|
repoName := act.GetRepoName()
|
||||||
|
repoLink := actUserName + "/" + repoName
|
||||||
branch := act.GetBranch()
|
branch := act.GetBranch()
|
||||||
content := act.GetContent()
|
content := act.GetContent()
|
||||||
switch act.GetOpType() {
|
switch act.GetOpType() {
|
||||||
case 1: // Create repository.
|
case 1: // Create repository.
|
||||||
return fmt.Sprintf(TPL_CREATE_REPO, actUserName, actUserName, actUserName, repoName, repoName)
|
return fmt.Sprintf(TPL_CREATE_REPO, actUserName, actUserName, repoLink, repoName)
|
||||||
case 5: // Commit repository.
|
case 5: // Commit repository.
|
||||||
var push *PushCommits
|
var push *PushCommits
|
||||||
if err := json.Unmarshal([]byte(content), &push); err != nil {
|
if err := json.Unmarshal([]byte(content), &push); err != nil {
|
||||||
|
@ -519,13 +524,17 @@ func ActionDesc(act Actioner, avatarLink string) string {
|
||||||
}
|
}
|
||||||
buf := bytes.NewBuffer([]byte("\n"))
|
buf := bytes.NewBuffer([]byte("\n"))
|
||||||
for _, commit := range push.Commits {
|
for _, commit := range push.Commits {
|
||||||
buf.WriteString(fmt.Sprintf(TPL_COMMIT_REPO_LI, avatarLink, actUserName, repoName, commit[0], commit[0][:7], commit[1]) + "\n")
|
buf.WriteString(fmt.Sprintf(TPL_COMMIT_REPO_LI, avatarLink, repoLink, commit[0], commit[0][:7], commit[1]) + "\n")
|
||||||
}
|
}
|
||||||
if push.Len > 3 {
|
if push.Len > 3 {
|
||||||
buf.WriteString(fmt.Sprintf(`<div><a href="/%s/%s/commits/%s">%d other commits >></a></div>`, actUserName, repoName, branch, push.Len))
|
buf.WriteString(fmt.Sprintf(`<div><a href="/%s/%s/commits/%s">%d other commits >></a></div>`, actUserName, repoName, branch, push.Len))
|
||||||
}
|
}
|
||||||
return fmt.Sprintf(TPL_COMMIT_REPO, actUserName, actUserName, actUserName, repoName, branch, branch, actUserName, repoName, actUserName, repoName,
|
return fmt.Sprintf(TPL_COMMIT_REPO, actUserName, actUserName, repoLink, branch, branch, repoLink, repoLink,
|
||||||
buf.String())
|
buf.String())
|
||||||
|
case 6: // Create issue.
|
||||||
|
infos := strings.SplitN(content, "|", 2)
|
||||||
|
return fmt.Sprintf(TPL_CREATE_Issue, actUserName, actUserName, repoLink, infos[0], repoLink, infos[0],
|
||||||
|
avatarLink, infos[1])
|
||||||
default:
|
default:
|
||||||
return "invalid type"
|
return "invalid type"
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,13 +23,33 @@ func Issues(ctx *middleware.Context, params martini.Params) {
|
||||||
milestoneId, _ := base.StrTo(params["milestone"]).Int()
|
milestoneId, _ := base.StrTo(params["milestone"]).Int()
|
||||||
page, _ := base.StrTo(params["page"]).Int()
|
page, _ := base.StrTo(params["page"]).Int()
|
||||||
|
|
||||||
var err error
|
// Get issues.
|
||||||
ctx.Data["Issues"], err = models.GetIssues(0, ctx.Repo.Repository.Id, 0,
|
issues, err := models.GetIssues(0, ctx.Repo.Repository.Id, 0,
|
||||||
int64(milestoneId), page, params["state"] == "closed", false, params["labels"], params["sortType"])
|
int64(milestoneId), page, params["state"] == "closed", false, params["labels"], params["sortType"])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Handle(200, "issue.Issues: %v", err)
|
ctx.Handle(200, "issue.Issues: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var closedCount int
|
||||||
|
// 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 issues[i].IsClosed {
|
||||||
|
closedCount++
|
||||||
|
}
|
||||||
|
issues[i].Poster = u
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Data["Issues"] = issues
|
||||||
|
ctx.Data["IssueCount"] = len(issues)
|
||||||
|
ctx.Data["OpenCount"] = len(issues) - closedCount
|
||||||
|
ctx.Data["ClosedCount"] = closedCount
|
||||||
ctx.HTML(200, "issue/list")
|
ctx.HTML(200, "issue/list")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,12 +74,20 @@ func CreateIssue(ctx *middleware.Context, params martini.Params, form auth.Creat
|
||||||
|
|
||||||
issue, err := models.CreateIssue(ctx.User.Id, ctx.Repo.Repository.Id, form.MilestoneId, form.AssigneeId,
|
issue, err := models.CreateIssue(ctx.User.Id, ctx.Repo.Repository.Id, form.MilestoneId, form.AssigneeId,
|
||||||
form.IssueName, form.Labels, form.Content, false)
|
form.IssueName, form.Labels, form.Content, false)
|
||||||
if err == nil {
|
if err != nil {
|
||||||
log.Trace("%d Issue created: %d", ctx.Repo.Repository.Id, issue.Id)
|
ctx.Handle(200, "issue.CreateIssue", err)
|
||||||
ctx.Redirect(fmt.Sprintf("/%s/%s/issues/%d", params["username"], params["reponame"], issue.Index))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Handle(200, "issue.CreateIssue", err)
|
|
||||||
|
// Notify watchers.
|
||||||
|
if err = models.NotifyWatchers(ctx.User.Id, ctx.Repo.Repository.Id, models.OP_CREATE_ISSUE,
|
||||||
|
ctx.User.Name, ctx.Repo.Repository.Name, "", fmt.Sprintf("%d|%s", issue.Index, issue.Name)); err != nil {
|
||||||
|
ctx.Handle(200, "issue.CreateIssue", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Trace("%d Issue created: %d", ctx.Repo.Repository.Id, issue.Id)
|
||||||
|
ctx.Redirect(fmt.Sprintf("/%s/%s/issues/%d", params["username"], params["reponame"], issue.Index))
|
||||||
}
|
}
|
||||||
|
|
||||||
func ViewIssue(ctx *middleware.Context, params martini.Params) {
|
func ViewIssue(ctx *middleware.Context, params martini.Params) {
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<div id="issue">
|
<div id="issue">
|
||||||
<div class="col-md-3 filter-list">
|
<div class="col-md-3 filter-list">
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
<li><a href="#" class="active">All Issues <strong class="pull-right">10</strong></a></li>
|
<li><a href="#" class="active">All Issues <strong class="pull-right">{{.IssueCount}}</strong></a></li>
|
||||||
<li><a href="#">My Issues</a></li>
|
<li><a href="#">My Issues</a></li>
|
||||||
<li><a href="#">Mentioned</a></li>
|
<li><a href="#">Mentioned</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -14,46 +14,24 @@
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
<div class="filter-option">
|
<div class="filter-option">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<a class="btn btn-default active issue-open" href="#">27 Open</a>
|
<a class="btn btn-default active issue-open" href="#">{{.OpenCount}} Open</a>
|
||||||
<a class="btn btn-default issue-close" href="#">Close 128</a>
|
<a class="btn btn-default issue-close" href="#">{{.ClosedCount}} Closed</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="issues list-group">
|
<div class="issues list-group">
|
||||||
{{range .Issues}}
|
{{range .Issues}}
|
||||||
<div class="list-group-item issue-item" id="{{.Id}}"></div>
|
<div class="list-group-item issue-item" id="{{.Id}}">
|
||||||
|
<span class="number pull-right">#{{.Index}}</span>
|
||||||
|
<h5 class="title"><a href="/{{$.RepositoryLink}}/issues/{{.Index}}">{{.Name}}</a></h5>
|
||||||
|
<p class="info">
|
||||||
|
<span class="author"><img class="avatar" src="{{.Poster.AvatarLink}}" alt="" width="20"/>
|
||||||
|
<a href="/user/{{.Poster.Name}}">{{.Poster.Name}}</a></span>
|
||||||
|
<span class="time">{{TimeSince .Created}}</span>
|
||||||
|
<span class="comment"><i class="fa fa-comments"></i> {{.NumComments}}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div class="issues list-group">
|
|
||||||
<div class="list-group-item unread issue-item" id="issue-id">
|
|
||||||
<span class="number pull-right">#123</span>
|
|
||||||
<h5 class="title"><a href="#">Bug: When running tests after generating a beego app, templates do not load.</a></h5>
|
|
||||||
<p class="info">
|
|
||||||
<span class="author"><img class="avatar" src="http://tp2.sinaimg.cn/5068084885/50/40050297589/1" alt="" width="20"/>
|
|
||||||
<a href="#">Obama</a></span>
|
|
||||||
<span class="time">3 days ago</span>
|
|
||||||
<span class="comment"><i class="fa fa-comments"></i> 3</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="list-group-item issue-item" id="issue-id2">
|
|
||||||
<span class="number pull-right">#123</span>
|
|
||||||
<h5 class="title"><a href="#">Bug: When running tests after generating a beego app, templates do not load.</a></h5>
|
|
||||||
<p class="info">
|
|
||||||
<span class="author"><img class="avatar" src="http://tp2.sinaimg.cn/5068084885/50/40050297589/1" alt="" width="20"/>
|
|
||||||
<a href="#">Obama</a></span>
|
|
||||||
<span class="time">3 days ago</span>
|
|
||||||
<span class="comment"><i class="fa fa-comments"></i> 3</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="list-group-item issue-item" id="issue-id3">
|
|
||||||
<span class="number pull-right">#123</span>
|
|
||||||
<h5 class="title"><a href="#">Bug: When running tests after generating a beego app, templates do not load.</a></h5>
|
|
||||||
<p class="info">
|
|
||||||
<span class="author"><img class="avatar" src="http://tp2.sinaimg.cn/5068084885/50/40050297589/1" alt="" width="20"/>
|
|
||||||
<a href="#">Obama</a></span>
|
|
||||||
<span class="time">3 days ago</span>
|
|
||||||
<span class="comment"><i class="fa fa-comments"></i> 3</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue