From ecd59deb27437963ea0589f07a97d397a0e5e1ef Mon Sep 17 00:00:00 2001
From: Steven <soud@protonmail.com>
Date: Thu, 1 Oct 2015 15:17:27 +0200
Subject: [PATCH] implemented #1721: see users who forked/starred/watched a
 repository

---
 cmd/web.go                          |  3 +
 models/repo.go                      | 30 ++++++++++
 public/ng/css/gogs.css              | 88 +++++++++++++++++++++++++++++
 public/ng/less/gogs/repository.less | 81 ++++++++++++++++++++++++++
 routers/repo/forks.go               | 37 ++++++++++++
 routers/repo/stars.go               | 44 +++++++++++++++
 routers/repo/watchers.go            | 44 +++++++++++++++
 templates/repo/forks.tmpl           | 27 +++++++++
 templates/repo/stars.tmpl           | 61 ++++++++++++++++++++
 templates/repo/watchers.tmpl        | 61 ++++++++++++++++++++
 10 files changed, 476 insertions(+)
 create mode 100644 routers/repo/forks.go
 create mode 100644 routers/repo/stars.go
 create mode 100644 routers/repo/watchers.go
 create mode 100644 templates/repo/forks.tmpl
 create mode 100644 templates/repo/stars.tmpl
 create mode 100644 templates/repo/watchers.tmpl

diff --git a/cmd/web.go b/cmd/web.go
index e78cb13a37..126a86a35f 100644
--- a/cmd/web.go
+++ b/cmd/web.go
@@ -514,6 +514,9 @@ func runWeb(ctx *cli.Context) {
 		m.Get("/labels/", repo.RetrieveLabels, repo.Labels)
 		m.Get("/milestones", repo.Milestones)
 		m.Get("/branches", repo.Branches)
+		m.Get("/stars/?:index", middleware.RepoRef(), repo.Stars)
+		m.Get("/watchers/?:index", middleware.RepoRef(), repo.Watchers)
+		m.Get("/forks", middleware.RepoRef(), repo.Forks)
 		m.Get("/archive/*", repo.Download)
 
 		m.Group("/pulls/:index", func() {
diff --git a/models/repo.go b/models/repo.go
index f3a32d6877..fc155fa5f5 100644
--- a/models/repo.go
+++ b/models/repo.go
@@ -46,6 +46,9 @@ var (
 
 var (
 	Gitignores, Licenses, Readmes []string
+
+	// Maximum items per page in forks, watchers and stars of a repo
+	ItemsPerPage = 3
 )
 
 func LoadRepoConfig() {
@@ -1612,6 +1615,16 @@ func GetWatchers(rid int64) ([]*Watch, error) {
 	return getWatchers(x, rid)
 }
 
+// Repository.GetWatchers returns all users watching given repository.
+func (repo *Repository) GetWatchers(offset int) ([]*User, error) {
+	users := make([]*User, 0, 10)
+	offset = (offset - 1) * ItemsPerPage
+
+	err := x.Limit(ItemsPerPage, offset).Where("repo_id=?", repo.ID).Join("LEFT", "watch", "user.id=watch.user_id").Find(&users)
+
+	return users, err
+}
+
 func notifyWatchers(e Engine, act *Action) error {
 	// Add feeds for user self and all watchers.
 	watches, err := getWatchers(e, act.RepoID)
@@ -1689,6 +1702,15 @@ func IsStaring(uid, repoId int64) bool {
 	return has
 }
 
+func (repo *Repository) GetStars(offset int) ([]*User, error) {
+	users := make([]*User, 0, 10)
+	offset = (offset - 1) * ItemsPerPage
+
+	err := x.Limit(ItemsPerPage, offset).Where("repo_id=?", repo.ID).Join("LEFT", "star", "user.id=star.uid").Find(&users)
+
+	return users, err
+}
+
 // ___________           __
 // \_   _____/__________|  | __
 //  |    __)/  _ \_  __ \  |/ /
@@ -1756,3 +1778,11 @@ func ForkRepository(u *User, oldRepo *Repository, name, desc string) (_ *Reposit
 
 	return repo, sess.Commit()
 }
+
+func (repo *Repository) GetForks() ([]*Repository, error) {
+	forks := make([]*Repository, 0, 10)
+
+	err := x.Find(&forks, &Repository{ForkID: repo.ID})
+
+	return forks, err
+}
diff --git a/public/ng/css/gogs.css b/public/ng/css/gogs.css
index 73273d4f7e..caecfe2579 100644
--- a/public/ng/css/gogs.css
+++ b/public/ng/css/gogs.css
@@ -1947,6 +1947,94 @@ The register and sign-in page style
 #release #release-new-form {
   padding-top: 15px;
 }
+#stars h4,
+#watchers h4,
+#forks h4 {
+  font-size: 18px;
+  padding-bottom: 20px;
+  text-transform: capitalize;
+  border-bottom: 1px solid #DDD;
+}
+#stars h3,
+#watchers h3,
+#forks h3 {
+  margin: -4px 0 0 0;
+  padding: 0;
+}
+#stars .avatar,
+#watchers .avatar,
+#forks .avatar {
+  width: 75px;
+  height: 75px;
+  float: left;
+  display: block;
+  margin-right: 10px;
+}
+#stars .avatar-small,
+#watchers .avatar-small,
+#forks .avatar-small {
+  width: 24px;
+  height: 24px;
+  float: left;
+  display: block;
+  margin-right: 10px;
+}
+#stars ol,
+#watchers ol,
+#forks ol {
+  margin-top: 10px;
+  list-style: none;
+  width: 100%;
+  overflow: hidden;
+}
+#stars li,
+#watchers li,
+#forks li {
+  width: 32.25%;
+  margin: 10px 10px 10px 0;
+  border-bottom: 1px solid #DDD;
+  float: left;
+  padding-bottom: 10px;
+}
+#stars .pagination,
+#watchers .pagination,
+#forks .pagination {
+  width: 100%;
+  text-align: center;
+  text-transform: capitalize;
+}
+#stars .pagination a,
+#watchers .pagination a,
+#forks .pagination a {
+  border-radius: 3px;
+  border: 1px solid #399ADE;
+  padding: 8px;
+  margin: 0;
+}
+#stars .pagination .active,
+#watchers .pagination .active,
+#forks .pagination .active {
+  border-radius: 3px;
+  border: 1px solid #399ADE;
+  background: #399ADE;
+  cursor: default;
+  padding: 8px;
+  margin: 0;
+  color: #FFFFFF;
+}
+#stars .pagination .disabled,
+#watchers .pagination .disabled,
+#forks .pagination .disabled {
+  border-radius: 3px;
+  border: 1px solid #DDD;
+  color: #D3D3D3;
+  cursor: default;
+  padding: 8px;
+  margin: 0;
+}
+#forks p {
+  padding: 5px 0;
+}
 #admin-wrapper,
 #setting-wrapper {
   padding-bottom: 100px;
diff --git a/public/ng/less/gogs/repository.less b/public/ng/less/gogs/repository.less
index 6b0a927e8f..c403b51fec 100644
--- a/public/ng/less/gogs/repository.less
+++ b/public/ng/less/gogs/repository.less
@@ -795,3 +795,84 @@
         padding-top: 15px;
     }
 }
+
+#stars, #watchers, #forks {
+    h4 {
+        font-size: 18px;
+        padding-bottom: 20px;
+        text-transform: capitalize;
+        border-bottom: 1px solid #DDD;
+    }
+
+    h3 {
+        margin: -4px 0 0 0;
+        padding: 0;
+    }
+
+    .avatar {
+        width: 75px;
+        height: 75px;
+        float: left;
+        display: block;
+        margin-right: 10px;
+    }
+
+    .avatar-small {
+        width: 24px;
+        height: 24px;
+        float: left;
+        display: block;
+        margin-right: 10px;
+    }
+
+    ol {
+        margin-top: 10px;
+        list-style: none;
+        width: 100%;
+        overflow: hidden;
+    }
+
+    li {
+        width: 32.25%;
+        margin: 10px 10px 10px 0;
+        border-bottom: 1px solid #DDD;
+        float: left;
+        padding-bottom: 10px;
+    }
+
+    .pagination {
+        width: 100%;
+        text-align: center;
+        text-transform: capitalize;
+
+        a {
+            border-radius: 3px;
+            border: 1px solid #399ADE;
+            padding: 8px;
+            margin: 0;
+        }
+
+        .active {
+            border-radius: 3px;
+            border: 1px solid #399ADE;
+            background: #399ADE;
+            cursor: default;
+            padding: 8px;
+            margin: 0;
+            color: #FFFFFF;
+        }
+
+        .disabled {
+            border-radius: 3px;
+            border: 1px solid #DDD;
+            color: #D3D3D3;
+            cursor: default;
+            padding: 8px;
+            margin: 0;
+        }
+    }
+}
+
+#forks p {
+    padding: 5px 0;
+}
diff --git a/routers/repo/forks.go b/routers/repo/forks.go
new file mode 100644
index 0000000000..099f0cc4f6
--- /dev/null
+++ b/routers/repo/forks.go
@@ -0,0 +1,37 @@
+// Copyright 2014 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 repo
+
+import (
+	"fmt"
+	"github.com/gogits/gogs/modules/base"
+	"github.com/gogits/gogs/modules/middleware"
+)
+
+const (
+	FORKS base.TplName = "repo/forks"
+)
+
+func Forks(ctx *middleware.Context) {
+	ctx.Data["Title"] = ctx.Tr("repos.forks")
+
+	forks, err := ctx.Repo.Repository.GetForks()
+
+	if err != nil {
+		ctx.Handle(500, "GetForks", err)
+		return
+	}
+
+	for _, fork := range forks {
+		if err = fork.GetOwner(); err != nil {
+			ctx.Handle(500, "GetOwner", fmt.Errorf("%d: %v", fork.ID, err))
+			return
+		}
+	}
+
+	ctx.Data["Forks"] = forks
+
+	ctx.HTML(200, FORKS)
+}
diff --git a/routers/repo/stars.go b/routers/repo/stars.go
new file mode 100644
index 0000000000..ffccd1765b
--- /dev/null
+++ b/routers/repo/stars.go
@@ -0,0 +1,44 @@
+// Copyright 2014 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 repo
+
+import (
+	"github.com/Unknwon/paginater"
+
+	"github.com/gogits/gogs/models"
+	"github.com/gogits/gogs/modules/base"
+	"github.com/gogits/gogs/modules/middleware"
+)
+
+const (
+	STARS base.TplName = "repo/stars"
+)
+
+func Stars(ctx *middleware.Context) {
+	ctx.Data["Title"] = ctx.Tr("repos.stars")
+
+	page := ctx.ParamsInt(":index")
+	if page <= 0 {
+		page = 1
+	}
+
+	ctx.Data["Page"] = paginater.New(ctx.Repo.Repository.NumStars, models.ItemsPerPage, page, 5)
+
+	stars, err := ctx.Repo.Repository.GetStars(ctx.ParamsInt(":index"))
+
+	if err != nil {
+		ctx.Handle(500, "GetStars", err)
+		return
+	}
+
+	if (ctx.ParamsInt(":index")-1)*models.ItemsPerPage > ctx.Repo.Repository.NumStars {
+		ctx.Handle(404, "ctx.Repo.Repository.NumStars", nil)
+		return
+	}
+
+	ctx.Data["Stars"] = stars
+
+	ctx.HTML(200, STARS)
+}
diff --git a/routers/repo/watchers.go b/routers/repo/watchers.go
new file mode 100644
index 0000000000..8765b18376
--- /dev/null
+++ b/routers/repo/watchers.go
@@ -0,0 +1,44 @@
+// Copyright 2014 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 repo
+
+import (
+	"github.com/Unknwon/paginater"
+
+	"github.com/gogits/gogs/models"
+	"github.com/gogits/gogs/modules/base"
+	"github.com/gogits/gogs/modules/middleware"
+)
+
+const (
+	WATCHERS base.TplName = "repo/watchers"
+)
+
+func Watchers(ctx *middleware.Context) {
+	ctx.Data["Title"] = ctx.Tr("repos.watches")
+
+	page := ctx.ParamsInt(":index")
+	if page <= 0 {
+		page = 1
+	}
+
+	ctx.Data["Page"] = paginater.New(ctx.Repo.Repository.NumWatches, models.ItemsPerPage, page, 5)
+
+	watchers, err := ctx.Repo.Repository.GetWatchers(ctx.ParamsInt(":index"))
+
+	if err != nil {
+		ctx.Handle(500, "GetWatchers", err)
+		return
+	}
+
+	if (ctx.ParamsInt(":index")-1)*models.ItemsPerPage > ctx.Repo.Repository.NumWatches {
+		ctx.Handle(404, "ctx.Repo.Repository.NumWatches", nil)
+		return
+	}
+
+	ctx.Data["Watchers"] = watchers
+
+	ctx.HTML(200, WATCHERS)
+}
diff --git a/templates/repo/forks.tmpl b/templates/repo/forks.tmpl
new file mode 100644
index 0000000000..d1fd0320c0
--- /dev/null
+++ b/templates/repo/forks.tmpl
@@ -0,0 +1,27 @@
+{{template "ng/base/head" .}}
+{{template "ng/base/header" .}}
+<div id="repo-wrapper">
+    {{template "repo/header_old" .}}
+    <div id="repo-content" class="clear container">
+        <div id="repo-main" class="left grid-5-6">
+            <div id="forks">
+                <h4>
+                    <strong>{{.i18n.Tr "repos.forks"}}</strong>
+                </h4>
+
+                <ol>
+                {{range .Forks}}
+                    <p>
+                    <img class="avatar-small" src="{{.Owner.AvatarLink}}">
+                    <a href="{{AppSubUrl}}/{{.Owner.Name}}">{{.Owner.Name}}</a>
+                    /
+                    <a href="{{AppSubUrl}}/{{.Owner.Name}}/{{.Name}}">{{.Name}}</a>
+                    </p>
+                {{end}}
+            </div>
+        </div>
+
+        {{template "repo/sidebar" .}}
+    </div>
+</div>
+{{template "ng/base/footer" .}}
diff --git a/templates/repo/stars.tmpl b/templates/repo/stars.tmpl
new file mode 100644
index 0000000000..afdb55d332
--- /dev/null
+++ b/templates/repo/stars.tmpl
@@ -0,0 +1,61 @@
+{{template "ng/base/head" .}}
+{{template "ng/base/header" .}}
+<div id="repo-wrapper">
+    {{template "repo/header_old" .}}
+    <div id="repo-content" class="clear container">
+        <div id="repo-main" class="left grid-5-6">
+            <div id="stars">
+                <h4>
+                    <strong>{{.i18n.Tr "repos.stars"}}</strong>
+                </h4>
+
+                <ol>
+                {{range .Stars}}
+                <li>
+                    <a href="{{AppSubUrl}}/{{.Name}}">
+                    <img class="avatar" src="{{.AvatarLink}}" title="{{.Name}}"/>
+
+                    <h3>{{.Name}}</h3>
+                    </a>
+
+                    <p>
+                    {{if .Website}}
+                    <span class="octicon octicon-link"></span> <a href="{{.Website}}" target="_blank">{{.Website}}</a>
+                    {{else if .Location}}
+                    <span class="octicon octicon-location"></span> {{.Location}}
+                    {{else}}
+                    <span class="octicon octicon-clock"></span> {{$.i18n.Tr "user.join_on"}} {{DateFmtShort .Created}}
+                    {{end}}
+                    </p>
+                </li>
+                {{end}}
+                </ol>
+
+                {{with .Page}}
+				{{if gt .TotalPages 1}}
+                <div class="pagination">
+                    {{if .HasPrevious}}
+                    <a href="{{$.RepoLink}}/stars/{{.Previous}}">{{$.i18n.Tr "issues.previous"}}</a>
+                    {{end}}
+
+                    {{range .Pages}}
+                    {{if eq .Num -1}}
+                    <a class="disabled item">...</a>
+                    {{else}}
+                    <a class="{{if .IsCurrent}}active{{end}} item" {{if not .IsCurrent}}href="{{$.RepoLink}}/stars/{{.Num}}"{{end}}>{{.Num}}</a>
+                    {{end}}
+                    {{end}}
+
+                    {{if .HasNext}}
+                    <a href="{{$.RepoLink}}/stars/{{.Next}}">{{$.i18n.Tr "issues.next"}}</a>
+                    {{end}}
+                </div>
+                {{end}}
+                {{end}}
+            </div>
+        </div>
+
+        {{template "repo/sidebar" .}}
+    </div>
+</div>
+{{template "ng/base/footer" .}}
diff --git a/templates/repo/watchers.tmpl b/templates/repo/watchers.tmpl
new file mode 100644
index 0000000000..3e5db820ca
--- /dev/null
+++ b/templates/repo/watchers.tmpl
@@ -0,0 +1,61 @@
+{{template "ng/base/head" .}}
+{{template "ng/base/header" .}}
+<div id="repo-wrapper">
+    {{template "repo/header_old" .}}
+    <div id="repo-content" class="clear container">
+        <div id="repo-main" class="left grid-5-6">
+            <div id="stars">
+                <h4>
+                    <strong>{{.i18n.Tr "repos.watches"}}</strong>
+                </h4>
+
+                <ol>
+                {{range .Watchers}}
+                <li>
+                    <a href="{{AppSubUrl}}/{{.Name}}">
+                    <img class="avatar" src="{{.AvatarLink}}" title="{{.Name}}"/>
+
+                    <h3>{{.Name}}</h3>
+                    </a>
+
+                    <p>
+                    {{if .Website}}
+                    <span class="octicon octicon-link"></span> <a href="{{.Website}}" target="_blank">{{.Website}}</a>
+                    {{else if .Location}}
+                    <span class="octicon octicon-location"></span> {{.Location}}
+                    {{else}}
+                    <span class="octicon octicon-clock"></span> {{$.i18n.Tr "user.join_on"}} {{DateFmtShort .Created}}
+                    {{end}}
+                    </p>
+                </li>
+                {{end}}
+                </ol>
+
+                {{with .Page}}
+				{{if gt .TotalPages 1}}
+                <div class="pagination">
+                    {{if .HasPrevious}}
+                    <a href="{{$.RepoLink}}/watchers/{{.Previous}}">{{$.i18n.Tr "issues.previous"}}</a>
+                    {{end}}
+
+                    {{range .Pages}}
+                    {{if eq .Num -1}}
+                    <a class="disabled item">...</a>
+                    {{else}}
+                    <a class="{{if .IsCurrent}}active{{end}} item" {{if not .IsCurrent}}href="{{$.RepoLink}}/watchers/{{.Num}}"{{end}}>{{.Num}}</a>
+                    {{end}}
+                    {{end}}
+
+                    {{if .HasNext}}
+                    <a href="{{$.RepoLink}}/watchers/{{.Next}}">{{$.i18n.Tr "issues.next"}}</a>
+                    {{end}}
+                </div>
+                {{end}}
+                {{end}}
+            </div>
+        </div>
+
+        {{template "repo/sidebar" .}}
+    </div>
+</div>
+{{template "ng/base/footer" .}}