Skip to content

Commit 5433a8c

Browse files
committed
Add pinned mod for repo
similar with github, use card ui to show pinned repos. Signed-off-by: a1012112796 <[email protected]>
1 parent 4027c5d commit 5433a8c

File tree

19 files changed

+496
-0
lines changed

19 files changed

+496
-0
lines changed

models/error.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,38 @@ func (err ErrReachLimitOfRepo) Error() string {
267267
return fmt.Sprintf("user has reached maximum limit of repositories [limit: %d]", err.Limit)
268268
}
269269

270+
// ErrUserPinnedRepoAlreadyExist represents a "UserPinnedRepo already exist" kind of error.
271+
type ErrUserPinnedRepoAlreadyExist struct {
272+
UID int64
273+
RepoID int64
274+
}
275+
276+
// IsErrUserPinnedRepoAlreadyExist checks if an error is a ErrUserPinnedRepoAlreadyExist.
277+
func IsErrUserPinnedRepoAlreadyExist(err error) bool {
278+
_, ok := err.(ErrUserPinnedRepoAlreadyExist)
279+
return ok
280+
}
281+
282+
func (err ErrUserPinnedRepoAlreadyExist) Error() string {
283+
return fmt.Sprintf("user has pinned this repo [uid: %d, repo_id: %d]", err.UID, err.RepoID)
284+
}
285+
286+
// ErrUserPinnedRepoNotExist represents a "UserPinnedRepo not exist" kind of error.
287+
type ErrUserPinnedRepoNotExist struct {
288+
UID int64
289+
RepoID int64
290+
}
291+
292+
// IsErrUserPinnedRepoNotExist checks if an error is a ErrUserPinnedRepoNotExist.
293+
func IsErrUserPinnedRepoNotExist(err error) bool {
294+
_, ok := err.(ErrUserPinnedRepoNotExist)
295+
return ok
296+
}
297+
298+
func (err ErrUserPinnedRepoNotExist) Error() string {
299+
return fmt.Sprintf("user hasn't pinned this repo [uid: %d, repo_id: %d]", err.UID, err.RepoID)
300+
}
301+
270302
// __ __.__ __ .__
271303
// / \ / \__| | _|__|
272304
// \ \/\/ / | |/ / |

models/fixtures/user_pinned_repo.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
-
2+
id: 1
3+
uid: 2
4+
repo_id: 1
5+
is_owned: true
6+
7+
-
8+
id: 2
9+
uid: 6
10+
repo_id: 1
11+
is_owned: false
12+
13+
-
14+
id: 3
15+
uid: 2
16+
repo_id: 4
17+
is_owned: false

models/migrations/migrations.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,8 @@ var migrations = []Migration{
226226
NewMigration("Increase Language field to 50 in LanguageStats", increaseLanguageField),
227227
// v146 -> v147
228228
NewMigration("Add projects info to repository table", addProjectsInfo),
229+
// v147 ->v148
230+
NewMigration("Add user_pinned_repo table", addUserPinnedRepoTable),
229231
}
230232

231233
// GetCurrentDBVersion returns the current db version

models/migrations/v147.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright 2020 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package migrations
6+
7+
import (
8+
"xorm.io/xorm"
9+
)
10+
11+
func addUserPinnedRepoTable(x *xorm.Engine) error {
12+
13+
type UserPinnedRepo struct {
14+
ID int64 `xorm:"pk autoincr"`
15+
UID int64 `xorm:"INDEX NOT NULL"`
16+
RepoID int64 `xorm:"INDEX NOT NULL"`
17+
IsOwned bool `xorm:"INDEX NOT NULL"`
18+
}
19+
20+
return x.Sync2(new(UserPinnedRepo))
21+
}

models/models.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ func init() {
129129
new(Project),
130130
new(ProjectBoard),
131131
new(ProjectIssue),
132+
new(UserPinnedRepo),
132133
)
133134

134135
gonicNames := []string{"SSL", "UID"}

models/repo.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,8 @@ type Repository struct {
200200
// Avatar: ID(10-20)-md5(32) - must fit into 64 symbols
201201
Avatar string `xorm:"VARCHAR(64)"`
202202

203+
IsPinned bool `xorm:"-"`
204+
203205
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
204206
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
205207
}

models/user_pinnedrepo.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Copyright 2020 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package models
6+
7+
import (
8+
"code.gitea.io/gitea/modules/structs"
9+
10+
"xorm.io/builder"
11+
)
12+
13+
// UserPinnedRepo represents a pinned repo by an user or org.
14+
type UserPinnedRepo struct {
15+
ID int64 `xorm:"pk autoincr"`
16+
UID int64 `xorm:"INDEX NOT NULL"`
17+
RepoID int64 `xorm:"INDEX NOT NULL"`
18+
IsOwned bool `xorm:"INDEX NOT NULL"`
19+
}
20+
21+
// AddPinnedRepo add a pinned repo
22+
func (u *User) AddPinnedRepo(repo *Repository) (err error) {
23+
exist := false
24+
if exist, err = u.IsPinnedRepoExist(repo.ID); err != nil {
25+
return
26+
}
27+
28+
if exist {
29+
return ErrUserPinnedRepoAlreadyExist{UID: u.ID, RepoID: repo.ID}
30+
}
31+
32+
r := &UserPinnedRepo{
33+
UID: u.ID,
34+
RepoID: repo.ID,
35+
IsOwned: u.ID == repo.OwnerID,
36+
}
37+
38+
_, err = x.Insert(r)
39+
return
40+
}
41+
42+
// RemovePinnedRepo remove a pinned repo
43+
func (u *User) RemovePinnedRepo(repoID int64) (err error) {
44+
exist := false
45+
if exist, err = u.IsPinnedRepoExist(repoID); err != nil {
46+
return
47+
}
48+
49+
if !exist {
50+
return ErrUserPinnedRepoNotExist{UID: u.ID, RepoID: repoID}
51+
}
52+
53+
_, err = x.Delete(&UserPinnedRepo{UID: u.ID, RepoID: repoID})
54+
return
55+
}
56+
57+
// IsPinnedRepoExist check if this repo is pinned
58+
func (u *User) IsPinnedRepoExist(repoID int64) (isExist bool, err error) {
59+
return x.Exist(&UserPinnedRepo{UID: u.ID, RepoID: repoID})
60+
}
61+
62+
// GetPinnedRepos get pinned repos
63+
func (u *User) GetPinnedRepos(actor *User, onlyIncludeNotOwned, loadAttributes bool) (repos RepositoryList, err error) {
64+
var cond = builder.NewCond()
65+
repos = make(RepositoryList, 0, 10)
66+
67+
if actor == nil {
68+
if u.IsOrganization() && u.Visibility != structs.VisibleTypePublic {
69+
return
70+
}
71+
cond = cond.And(builder.Eq{"is_private": false})
72+
} else if actor.ID != u.ID && !actor.IsAdmin {
73+
// OK we're in the context of a User
74+
cond = cond.And(accessibleRepositoryCondition(actor))
75+
}
76+
77+
var idBuilder *builder.Builder
78+
79+
if onlyIncludeNotOwned {
80+
idBuilder = builder.Select("repo_id").
81+
From("user_pinned_repo").
82+
Where(builder.Eq{"uid": u.ID, "is_owned": false})
83+
} else {
84+
idBuilder = builder.Select("repo_id").
85+
From("user_pinned_repo").
86+
Where(builder.Eq{"uid": u.ID})
87+
}
88+
89+
cond = cond.And(builder.In("id", idBuilder))
90+
if err = x.Where(cond).Find(&repos); err != nil {
91+
return nil, err
92+
}
93+
94+
if loadAttributes {
95+
if err = repos.LoadAttributes(); err != nil {
96+
return nil, err
97+
}
98+
}
99+
100+
return
101+
}

models/user_pinnedrepo_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright 2017 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package models
6+
7+
import (
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func TestAddPinnedRepo(t *testing.T) {
14+
assert.NoError(t, PrepareTestDatabase())
15+
16+
user2 := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
17+
repo1 := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
18+
repo10 := AssertExistsAndLoadBean(t, &Repository{ID: 10}).(*Repository)
19+
20+
assert.NoError(t, user2.AddPinnedRepo(repo10))
21+
AssertExistsAndLoadBean(t, &UserPinnedRepo{UID: user2.ID, RepoID: repo10.ID})
22+
23+
assert.EqualError(t, user2.AddPinnedRepo(repo1),
24+
ErrUserPinnedRepoAlreadyExist{UID: user2.ID, RepoID: repo1.ID}.Error())
25+
}
26+
27+
func TestRemovePinnedRepo(t *testing.T) {
28+
assert.NoError(t, PrepareTestDatabase())
29+
30+
user2 := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
31+
32+
assert.NoError(t, user2.RemovePinnedRepo(1))
33+
34+
assert.EqualError(t, user2.RemovePinnedRepo(3),
35+
ErrUserPinnedRepoNotExist{UID: user2.ID, RepoID: 3}.Error())
36+
}
37+
38+
func TestIsPinnedRepoExist(t *testing.T) {
39+
assert.NoError(t, PrepareTestDatabase())
40+
41+
user2 := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
42+
43+
exist, err := user2.IsPinnedRepoExist(1)
44+
assert.NoError(t, err)
45+
assert.Equal(t, true, exist)
46+
47+
exist, err = user2.IsPinnedRepoExist(5)
48+
assert.NoError(t, err)
49+
assert.Equal(t, false, exist)
50+
}
51+
52+
func TestGetPinnedRepos(t *testing.T) {
53+
assert.NoError(t, PrepareTestDatabase())
54+
55+
user2 := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
56+
57+
repos, err := user2.GetPinnedRepos(user2, true, true)
58+
assert.NoError(t, err)
59+
assert.Equal(t, 1, len(repos))
60+
}

modules/auth/user_form.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,3 +360,9 @@ type U2FDeleteForm struct {
360360
func (f *U2FDeleteForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
361361
return validate(errs, ctx.Data, f, ctx.Locale)
362362
}
363+
364+
// RepoPinnedForm form for changing repository pinned settings
365+
type RepoPinnedForm struct {
366+
RepoFullName string `binding:"Required" form:"name"`
367+
Status string `binding:"Required;In(pinned,unpinned)"`
368+
}

options/locale/locale_en-US.ini

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ issues.in_your_repos = In your repositories
226226
227227
[explore]
228228
repos = Repositories
229+
pinned_repos = Pinned repositories
229230
users = Users
230231
organizations = Organizations
231232
search = Search
@@ -450,6 +451,13 @@ uploaded_avatar_not_a_image = The uploaded file is not an image.
450451
uploaded_avatar_is_too_big = The uploaded file has exceeded the maximum size.
451452
update_avatar_success = Your avatar has been updated.
452453
454+
pinned_repo = pinned this repo
455+
unpinned_repo = unpinned this repo
456+
pinned_repo_success = pinned repo success
457+
unpinned_repo_success = unpinned repo success
458+
pinned_other_repo_des = Repo full name
459+
pinned_other_repo = Pinned other repo
460+
453461
change_password = Update Password
454462
old_password = Current Password
455463
new_password = New Password

routers/org/home.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,27 @@ func Home(ctx *context.Context) {
119119
return
120120
}
121121

122+
pinnedRepos := make([]*models.Repository, 0, 10)
123+
has := false
124+
for _, repo := range repos {
125+
if has, err = org.IsPinnedRepoExist(repo.ID); err != nil {
126+
ctx.ServerError("IsPinnedRepoExist", err)
127+
return
128+
}
129+
130+
if has {
131+
pinnedRepos = append(pinnedRepos, repo)
132+
repo.IsPinned = true
133+
}
134+
}
135+
136+
ctx.Data["PinnedRepos"] = pinnedRepos
137+
ctx.Data["PinnedReposNum"] = len(pinnedRepos)
138+
ctx.Data["CanConfigPinnedRepos"] = ctx.IsSigned && ctx.Org.IsOwner
139+
if ctx.IsSigned && ctx.Org.IsOwner {
140+
ctx.Data["ConfigPinnedReposLink"] = ctx.Org.OrgLink + "/settings/pinned_repo"
141+
}
142+
122143
ctx.Data["Repos"] = repos
123144
ctx.Data["Total"] = count
124145
ctx.Data["MembersTotal"] = membersCount

routers/org/setting.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,34 @@ func Labels(ctx *context.Context) {
206206
ctx.Data["LabelTemplates"] = models.LabelTemplates
207207
ctx.HTML(200, tplSettingsLabels)
208208
}
209+
210+
// PinnedRepoPost response pinned repo settings
211+
func PinnedRepoPost(ctx *context.Context, form auth.RepoPinnedForm) {
212+
repo, err := models.GetRepositoryByOwnerAndName(ctx.Org.Organization.Name, form.RepoFullName)
213+
if err != nil {
214+
if models.IsErrRepoNotExist(err) {
215+
ctx.Status(404)
216+
return
217+
}
218+
219+
ctx.ServerError("models.GetRepositoryByOwnerAndName", err)
220+
return
221+
}
222+
223+
if form.Status == "unpinned" {
224+
if err = ctx.Org.Organization.RemovePinnedRepo(repo.ID); err != nil && !models.IsErrUserPinnedRepoNotExist(err) {
225+
ctx.ServerError("ctx.Org.Organization.RemovePinnedRepo", err)
226+
return
227+
}
228+
229+
ctx.Redirect(ctx.Org.Organization.HomeLink())
230+
return
231+
}
232+
233+
if err = ctx.Org.Organization.AddPinnedRepo(repo); err != nil && !models.IsErrUserPinnedRepoAlreadyExist(err) {
234+
ctx.ServerError("ctx.Org.Organization.AddPinnedRepo", err)
235+
return
236+
}
237+
238+
ctx.Redirect(ctx.Org.Organization.HomeLink())
239+
}

routers/routes/routes.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,7 @@ func RegisterRoutes(m *macaron.Macaron) {
355355
m.Group("/user/settings", func() {
356356
m.Get("", userSetting.Profile)
357357
m.Post("", bindIgnErr(auth.UpdateProfileForm{}), userSetting.ProfilePost)
358+
m.Post("/pinned_repo", bindIgnErr(auth.RepoPinnedForm{}), userSetting.PinnedRepoPost)
358359
m.Get("/change_password", user.MustChangePassword)
359360
m.Post("/change_password", bindIgnErr(auth.MustChangePasswordForm{}), user.MustChangePasswordPost)
360361
m.Post("/avatar", binding.MultipartForm(auth.AvatarForm{}), userSetting.AvatarPost)
@@ -570,6 +571,7 @@ func RegisterRoutes(m *macaron.Macaron) {
570571
m.Group("/settings", func() {
571572
m.Combo("").Get(org.Settings).
572573
Post(bindIgnErr(auth.UpdateOrgSettingForm{}), org.SettingsPost)
574+
m.Post("/pinned_repo", bindIgnErr(auth.RepoPinnedForm{}), org.PinnedRepoPost)
573575
m.Post("/avatar", binding.MultipartForm(auth.AvatarForm{}), org.SettingsAvatar)
574576
m.Post("/avatar/delete", org.SettingsDeleteAvatar)
575577

0 commit comments

Comments
 (0)