From 5f890b55ca46a87721e38bc568fc3ff41d812ecf Mon Sep 17 00:00:00 2001 From: Carlos Felgueiras Date: Fri, 10 May 2024 15:58:31 +0000 Subject: [PATCH 01/16] feat(pin): implemented base code for pins on the backend - Created the pin model, and basic CRUD operations. - Implemented checks for lost of visibility of owner, to automatically delete the repository. - Implemented check for the lack of visibility of a viewer, when requesting the repos of another user, excluding those repositories from the list that is returned. - Added the deletion of all the pins of a repository when it is deleted. - Implemented the ability for a user to pin on the profile of an org user, checking if the user is admin of the org, and the org is owner of the repo. Co-authored-by: Daniel Carvalho --- models/repo/pin.go | 65 ++++++++++++++ models/repo/user_repo.go | 31 +++++++ services/repository/delete.go | 1 + services/user/pin.go | 162 ++++++++++++++++++++++++++++++++++ 4 files changed, 259 insertions(+) create mode 100644 models/repo/pin.go create mode 100644 services/user/pin.go diff --git a/models/repo/pin.go b/models/repo/pin.go new file mode 100644 index 0000000000000..579a31125b1b9 --- /dev/null +++ b/models/repo/pin.go @@ -0,0 +1,65 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "context" + + "code.gitea.io/gitea/models/db" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/timeutil" +) + +type Pin struct { + ID int64 `xorm:"pk autoincr"` + UID int64 `xorm:"UNIQUE(s)"` + RepoID int64 `xorm:"UNIQUE(s)"` + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` +} + +// TableName sets the table name for the pin struct +func (s *Pin) TableName() string { + return "repo_pin" +} + +func init() { + db.RegisterModel(new(Pin)) +} + +func IsPinned(ctx context.Context, userID, repoID int64) bool { + exists, _ := db.GetEngine(ctx).Get(&Pin{UID: userID, RepoID: repoID}) + return exists +} + +func PinRepo(ctx context.Context, doer *user_model.User, repo *Repository, pin bool) error { + ctx, commiter, err := db.TxContext(ctx) + if err != nil { + return err + } + + defer commiter.Close() + pinned := IsPinned(ctx, doer.ID, repo.ID) + + if pin { + // Already pinned, nothing to do + if pinned { + return nil + } + + if err = db.Insert(ctx, &Pin{UID: doer.ID, RepoID: repo.ID}); err != nil { + return err + } + } else { + // Not pinned, nothing to do + if !pinned { + return nil + } + + if _, err = db.DeleteByBean(ctx, &Pin{UID: doer.ID, RepoID: repo.ID}); err != nil { + return err + } + } + + return commiter.Commit() +} diff --git a/models/repo/user_repo.go b/models/repo/user_repo.go index c305603e02070..9af72da11d655 100644 --- a/models/repo/user_repo.go +++ b/models/repo/user_repo.go @@ -54,6 +54,37 @@ func GetStarredRepos(ctx context.Context, opts *StarredReposOptions) ([]*Reposit return db.Find[Repository](ctx, opts) } +type PinnedReposOptions struct { + db.ListOptions + PinnerID int64 + RepoOwnerID int64 +} + +func (opts *PinnedReposOptions) ToConds() builder.Cond { + var cond builder.Cond = builder.Eq{ + "repo_pin.uid": opts.PinnerID, + } + if opts.RepoOwnerID != 0 { + cond = cond.And(builder.Eq{ + "repository.owner_id": opts.RepoOwnerID, + }) + } + return cond +} + +func (opts *PinnedReposOptions) ToJoins() []db.JoinFunc { + return []db.JoinFunc{ + func(e db.Engine) error { + e.Join("INNER", "repo_pin", "`repository`.id=`repo_pin`.repo_id") + return nil + }, + } +} + +func GetPinnedRepos(ctx context.Context, opts *PinnedReposOptions) (RepositoryList, error) { + return db.Find[Repository](ctx, opts) +} + type WatchedReposOptions struct { db.ListOptions WatcherID int64 diff --git a/services/repository/delete.go b/services/repository/delete.go index cd779b05c3501..2805eed3a53f0 100644 --- a/services/repository/delete.go +++ b/services/repository/delete.go @@ -151,6 +151,7 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID &repo_model.Redirect{RedirectRepoID: repoID}, &repo_model.RepoUnit{RepoID: repoID}, &repo_model.Star{RepoID: repoID}, + &repo_model.Pin{RepoID: repoID}, &admin_model.Task{RepoID: repoID}, &repo_model.Watch{RepoID: repoID}, &webhook.Webhook{RepoID: repoID}, diff --git a/services/user/pin.go b/services/user/pin.go new file mode 100644 index 0000000000000..8f9c5f90a1f55 --- /dev/null +++ b/services/user/pin.go @@ -0,0 +1,162 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "errors" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/organization" + access_model "code.gitea.io/gitea/models/perm/access" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/services/context" +) + +// Check if a user have a new pinned repo in it's profile, meaning that it +// has permissions to pin said repo and also has enough space on the pinned list. +func CanPin(ctx *context.Context, u *user_model.User, r *repo_model.Repository) bool { + repos, err := repo_model.GetPinnedRepos(*ctx, &repo_model.PinnedReposOptions{ + ListOptions: db.ListOptions{ + ListAll: true, + }, + PinnerID: u.ID, + }) + if err != nil { + ctx.ServerError("GetPinnedRepos", err) + return false + } + if len(repos) >= 6 { + return false + } + + return HasPermsToPin(ctx, u, r) +} + +// Checks if the user has permission to have the repo pinned in it's profile. +func HasPermsToPin(ctx *context.Context, u *user_model.User, r *repo_model.Repository) bool { + // If user is an organization, it can only pin its own repos + if u.IsOrganization() { + return r.OwnerID == u.ID + } + + // For normal users, anyone that has read access to the repo can pin it + return canSeePin(ctx, u, r) +} + +// Check if a user can see a pin +// A user can see a pin if he has read access to the repo +func canSeePin(ctx *context.Context, u *user_model.User, r *repo_model.Repository) bool { + perm, err := access_model.GetUserRepoPermission(ctx, r, u) + if err != nil { + ctx.ServerError("GetUserRepoPermission", err) + return false + } + return perm.HasAnyUnitAccess() +} + +// CleanupPins iterates over the repos pinned by a user and removes +// the invalid pins. (Needs to be called everytime before we read/write a pin) +func CleanupPins(ctx *context.Context, u *user_model.User) error { + pinnedRepos, err := repo_model.GetPinnedRepos(*ctx, &repo_model.PinnedReposOptions{ + ListOptions: db.ListOptions{ + ListAll: true, + }, + PinnerID: u.ID, + }) + if err != nil { + return err + } + + for _, repo := range pinnedRepos { + if !HasPermsToPin(ctx, u, repo) { + if err := repo_model.PinRepo(*ctx, u, repo, false); err != nil { + return err + } + } + } + + return nil +} + +// Returns the pinned repos of a user that the viewer can see +func GetUserPinnedRepos(ctx *context.Context, user, viewer *user_model.User) ([]*repo_model.Repository, error) { + // Start by cleaning up the invalid pins + err := CleanupPins(ctx, user) + if err != nil { + return nil, err + } + + // Get all of the user's pinned repos + pinnedRepos, err := repo_model.GetPinnedRepos(*ctx, &repo_model.PinnedReposOptions{ + ListOptions: db.ListOptions{ + ListAll: true, + }, + PinnerID: user.ID, + }) + if err != nil { + return nil, err + } + + var repos []*repo_model.Repository + + // Only include the repos that the viewer can see + for _, repo := range pinnedRepos { + if canSeePin(ctx, viewer, repo) { + repos = append(repos, repo) + } + } + + return repos, nil +} + +func PinRepo(ctx *context.Context, doer *user_model.User, repo *repo_model.Repository, pin, toOrg bool) error { + // Determine the user which profile is the target for the pin + var targetUser *user_model.User + if toOrg { + targetUser = repo.Owner + } else { + targetUser = doer + } + + // Start by cleaning up the invalid pins + err := CleanupPins(ctx, targetUser) + if err != nil { + return err + } + + // If target is org profile, need to check if the doer can pin the repo + // on said org profile + if toOrg { + err = assertUserOrgPerms(ctx, doer, repo) + if err != nil { + return err + } + } + + if pin { + if !CanPin(ctx, targetUser, repo) { + return errors.New("user cannot pin this repository") + } + } + + return repo_model.PinRepo(*ctx, targetUser, repo, pin) +} + +func assertUserOrgPerms(ctx *context.Context, doer *user_model.User, repo *repo_model.Repository) error { + if !ctx.Repo.Owner.IsOrganization() { + return errors.New("owner is not an organization") + } + + isAdmin, err := organization.OrgFromUser(repo.Owner).IsOrgAdmin(ctx, doer.ID) + if err != nil { + return err + } + + if !isAdmin { + return errors.New("user is not an admin of this organization") + } + + return nil +} From fd47c820771c33649ed4414e995de9cf2d150f84 Mon Sep 17 00:00:00 2001 From: Carlos Felgueiras Date: Fri, 10 May 2024 16:08:24 +0000 Subject: [PATCH 02/16] feat(pin): implemented the list of pinned repositories on the profile - Added list and count of pinned repos to the context data of the routes of a user and org profile. - Added a template for the list of pinned repo cards. - Included said template in the user and org profile templates. Co-authored-by: Daniel Carvalho --- options/locale/locale_en-US.ini | 1 + routers/web/org/home.go | 20 ++++++-- routers/web/user/profile.go | 21 +++++++-- templates/org/home.tmpl | 3 ++ templates/shared/pinned_repo_cards.tmpl | 62 +++++++++++++++++++++++++ templates/user/profile.tmpl | 3 ++ 6 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 templates/shared/pinned_repo_cards.tmpl diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 6a08041a7c8b6..8964bb456aaf5 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1102,6 +1102,7 @@ transfer.no_permission_to_reject = You do not have permission to reject this tra desc.private = Private desc.public = Public desc.template = Template +desc.private_template = Private Template desc.internal = Internal desc.archived = Archived desc.sha256 = SHA256 diff --git a/routers/web/org/home.go b/routers/web/org/home.go index 846b1de18ab13..b23bbeefa04f5 100644 --- a/routers/web/org/home.go +++ b/routers/web/org/home.go @@ -20,6 +20,7 @@ import ( "code.gitea.io/gitea/modules/util" shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" + user_service "code.gitea.io/gitea/services/user" ) const ( @@ -101,9 +102,11 @@ func Home(ctx *context.Context) { ctx.Data["IsPrivate"] = private var ( - repos []*repo_model.Repository - count int64 - err error + repos []*repo_model.Repository + count int64 + pinnedRepos []*repo_model.Repository + pinnedCount int64 + err error ) repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ @@ -139,8 +142,19 @@ func Home(ctx *context.Context) { return } + // Get pinned repos + pinnedRepos, err = user_service.GetUserPinnedRepos(ctx, org.AsUser(), ctx.Doer) + if err != nil { + ctx.ServerError("GetUserPinnedRepos", err) + return + } + + pinnedCount = int64(len(pinnedRepos)) + ctx.Data["Repos"] = repos ctx.Data["Total"] = count + ctx.Data["PinnedRepos"] = pinnedRepos + ctx.Data["PinnedTotal"] = pinnedCount ctx.Data["Members"] = members ctx.Data["Teams"] = ctx.Org.Teams ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index f0749e10216ee..ef8072f8cf065 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -26,6 +26,7 @@ import ( "code.gitea.io/gitea/routers/web/org" shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" + user_service "code.gitea.io/gitea/services/user" ) const ( @@ -104,10 +105,12 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb pagingNum := setting.UI.User.RepoPagingNum topicOnly := ctx.FormBool("topic") var ( - repos []*repo_model.Repository - count int64 - total int - orderBy db.SearchOrderBy + repos []*repo_model.Repository + pinnedRepos []*repo_model.Repository + count int64 + pinnedCount int64 + total int + orderBy db.SearchOrderBy ) ctx.Data["SortType"] = ctx.FormString("sort") @@ -312,9 +315,19 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb } total = int(count) + + pinnedRepos, err = user_service.GetUserPinnedRepos(ctx, ctx.ContextUser, ctx.Doer) + if err != nil { + ctx.ServerError("GetUserPinnedRepos", err) + return + } + + pinnedCount = int64(len(pinnedRepos)) } ctx.Data["Repos"] = repos ctx.Data["Total"] = total + ctx.Data["PinnedRepos"] = pinnedRepos + ctx.Data["PinnedCount"] = pinnedCount err = shared_user.LoadHeaderCount(ctx) if err != nil { diff --git a/templates/org/home.tmpl b/templates/org/home.tmpl index 4851b6997967b..1f83dddda68c7 100644 --- a/templates/org/home.tmpl +++ b/templates/org/home.tmpl @@ -8,6 +8,9 @@ {{if .ProfileReadme}}
{{.ProfileReadme}}
{{end}} + {{if .PinnedRepos}} + {{template "shared/pinned_repo_cards" .}} + {{end}} {{template "shared/repo_search" .}} {{template "explore/repo_list" .}} {{template "base/paginate" .}} diff --git a/templates/shared/pinned_repo_cards.tmpl b/templates/shared/pinned_repo_cards.tmpl new file mode 100644 index 0000000000000..d32a52a5e9d54 --- /dev/null +++ b/templates/shared/pinned_repo_cards.tmpl @@ -0,0 +1,62 @@ +
+ {{range .PinnedRepos}} +
+
+
+
+ +
+ {{if .IsArchived}} + {{ctx.Locale.Tr "repo.desc.archived"}} + {{end}} + {{if .IsTemplate}} + {{if .IsPrivate}} + {{ctx.Locale.Tr "repo.desc.private_template"}} + {{end}} + {{else}} + {{if .IsPrivate}} + {{ctx.Locale.Tr "repo.desc.private"}} + {{end}} + {{end}} + {{if .IsFork}} + {{svg "octicon-repo-forked"}} + {{else if .IsMirror}} + {{svg "octicon-mirror"}} + {{end}} +
+
+
+
+
+ {{if .PrimaryLanguage}} + {{.PrimaryLanguage.Language}} + {{end}} + {{if not $.DisableStars}} + {{svg "octicon-star" 16 "mr-3"}}{{.NumStars}} + {{end}} + {{svg "octicon-git-branch" 16 "mr-3"}}{{.NumForks}} +
+
+
+ {{$description := .DescriptionHTML $.Context}} + {{if $description}}

{{$description}}

{{end}} + {{if .Topics}} +
+ {{range .Topics}} + {{if ne . ""}} + +
{{.}}
+
+ {{end}} + {{end}} +
+ {{end}} +
+
+
+ {{end}} +
diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl index cf61bb906a172..d38292077d297 100644 --- a/templates/user/profile.tmpl +++ b/templates/user/profile.tmpl @@ -28,6 +28,9 @@ {{else if eq .TabName "overview"}}
{{.ProfileReadme}}
{{else}} + {{if .PinnedRepos}} + {{template "shared/pinned_repo_cards" .}} + {{end}} {{template "shared/repo_search" .}} {{template "explore/repo_list" .}} {{template "base/paginate" .}} From e8dca1ecd4a1b184af6aa1d799d7e9cbdaba13bb Mon Sep 17 00:00:00 2001 From: Carlos Felgueiras Date: Fri, 10 May 2024 16:14:29 +0000 Subject: [PATCH 03/16] feat(pin): implemented the pin/unpin button on repo main page - Added the unpin octicon svg from a previous attempt to implement this feature. - Added the template for the button/set of buttons for pinning/unpinning a repo. - Added the use of said template in the header of a repo main page. - Added the routes for the POST requests for pinning/unpinning to user/org. Co-authored-by: Daniel Carvalho --- options/locale/locale_en-US.ini | 5 +++ .../assets/img/svg/octicon-custom-pin-off.svg | 1 + routers/web/repo/repo.go | 9 +++++ routers/web/repo/view.go | 37 +++++++++++++++++++ templates/repo/header.tmpl | 1 + templates/repo/pin_unpin.tmpl | 30 +++++++++++++++ web_src/svg/octicon-custom-pin-off.svg | 1 + 7 files changed, 84 insertions(+) create mode 100644 public/assets/img/svg/octicon-custom-pin-off.svg create mode 100644 templates/repo/pin_unpin.tmpl create mode 100644 web_src/svg/octicon-custom-pin-off.svg diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 8964bb456aaf5..78c80305fc00d 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1188,10 +1188,15 @@ fork_from_self = You cannot fork a repository you own. fork_guest_user = Sign in to fork this repository. watch_guest_user = Sign in to watch this repository. star_guest_user = Sign in to star this repository. +pin_guest_user = Sign in to pin this repository. unwatch = Unwatch watch = Watch unstar = Unstar star = Star +pin = Pin +unpin = Unpin +pin-org = Pin to %s +unpin-org = Unpin from %s fork = Fork action.blocked_user = Cannot perform action because you are blocked by the repository owner. download_archive = Download Repository diff --git a/public/assets/img/svg/octicon-custom-pin-off.svg b/public/assets/img/svg/octicon-custom-pin-off.svg new file mode 100644 index 0000000000000..763953c2aca17 --- /dev/null +++ b/public/assets/img/svg/octicon-custom-pin-off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index 48be1c22968fb..231047d59fb2b 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -36,6 +36,7 @@ import ( repo_service "code.gitea.io/gitea/services/repository" archiver_service "code.gitea.io/gitea/services/repository/archiver" commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus" + user_service "code.gitea.io/gitea/services/user" ) const ( @@ -321,6 +322,14 @@ func Action(ctx *context.Context) { err = repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, true) case "unstar": err = repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, false) + case "pin": + err = user_service.PinRepo(ctx, ctx.Doer, ctx.Repo.Repository, true, false) + case "unpin": + err = user_service.PinRepo(ctx, ctx.Doer, ctx.Repo.Repository, false, false) + case "pin-org": + err = user_service.PinRepo(ctx, ctx.Doer, ctx.Repo.Repository, true, true) + case "unpin-org": + err = user_service.PinRepo(ctx, ctx.Doer, ctx.Repo.Repository, false, true) case "accept_transfer": err = acceptOrRejectRepoTransfer(ctx, true) case "reject_transfer": diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index e4e6201c24abd..1e85f91d58e75 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -29,6 +29,7 @@ import ( "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" issue_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/organization" repo_model "code.gitea.io/gitea/models/repo" unit_model "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" @@ -50,6 +51,7 @@ import ( "code.gitea.io/gitea/services/context" issue_service "code.gitea.io/gitea/services/issue" files_service "code.gitea.io/gitea/services/repository/files" + user_services "code.gitea.io/gitea/services/user" "github.com/nektos/act/pkg/model" @@ -791,6 +793,13 @@ func Home(ctx *context.Context) { return } + if ctx.IsSigned { + err := loadPinData(ctx) + if err != nil { + ctx.ServerError("loadPinData", err) + } + } + renderHomeCode(ctx) } @@ -1168,3 +1177,31 @@ func Forks(ctx *context.Context) { ctx.HTML(http.StatusOK, tplForks) } + +func loadPinData(ctx *context.Context) error { + // First, cleanup any pins that are no longer valid + err := user_services.CleanupPins(ctx, ctx.Doer) + if err != nil { + return err + } + + ctx.Data["IsPinningRepo"] = repo_model.IsPinned(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) + ctx.Data["CanPinRepo"] = user_services.CanPin(ctx, ctx.Doer, ctx.Repo.Repository) + + if ctx.Repo.Repository.Owner.IsOrganization() { + org := organization.OrgFromUser(ctx.Repo.Repository.Owner) + + isAdmin, err := org.IsOrgAdmin(ctx, ctx.Doer.ID) + if err != nil { + return err + } + + if isAdmin { + ctx.Data["CanUserPinToOrg"] = true + ctx.Data["IsOrgPinningRepo"] = repo_model.IsPinned(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID) + ctx.Data["CanOrgPinRepo"] = user_services.CanPin(ctx, ctx.Repo.Repository.Owner, ctx.Repo.Repository) + } + } + + return nil +} diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index 34f47b7d89a1f..07e607c475bea 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -60,6 +60,7 @@ {{svg "octicon-rss" 16}} {{end}} + {{template "repo/pin_unpin" $}} {{template "repo/watch_unwatch" $}} {{if not $.DisableStars}} {{template "repo/star_unstar" $}} diff --git a/templates/repo/pin_unpin.tmpl b/templates/repo/pin_unpin.tmpl new file mode 100644 index 0000000000000..f06f898fda338 --- /dev/null +++ b/templates/repo/pin_unpin.tmpl @@ -0,0 +1,30 @@ +
+
+
+ {{$buttonText := ctx.Locale.Tr "repo.pin"}} + {{if $.IsPinningRepo}}{{$buttonText = ctx.Locale.Tr "repo.unpin"}}{{end}} + +
+
+ + {{if .CanUserPinToOrg}} + + {{end}} +
diff --git a/web_src/svg/octicon-custom-pin-off.svg b/web_src/svg/octicon-custom-pin-off.svg new file mode 100644 index 0000000000000..a5e6fda34193a --- /dev/null +++ b/web_src/svg/octicon-custom-pin-off.svg @@ -0,0 +1 @@ + \ No newline at end of file From 0754ed7c186f58d9e83e1520b52e7f63f6fe0e00 Mon Sep 17 00:00:00 2001 From: Carlos Felgueiras Date: Fri, 10 May 2024 16:17:42 +0000 Subject: [PATCH 04/16] test(pin): created basic tests for the pin/unpin feature - Created unit tests that check the basic functionality of the model functions - Created integration tests that check the functionality of the POST routes, verifying the effects directly on the database. Co-authored-by: Daniel Carvalho --- models/repo/pin_test.go | 48 ++++++++++++++++++++++++ tests/integration/repo_pin_test.go | 60 ++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 models/repo/pin_test.go create mode 100644 tests/integration/repo_pin_test.go diff --git a/models/repo/pin_test.go b/models/repo/pin_test.go new file mode 100644 index 0000000000000..49ef59344da7a --- /dev/null +++ b/models/repo/pin_test.go @@ -0,0 +1,48 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo_test + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + + "github.com/stretchr/testify/assert" +) + +func TestPinRepoFunctionality(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + + unittest.AssertNotExistsBean(t, &repo_model.Pin{UID: user.ID, RepoID: repo.ID}) + assert.NoError(t, repo_model.PinRepo(db.DefaultContext, user, repo, true)) + unittest.AssertExistsAndLoadBean(t, &repo_model.Pin{UID: user.ID, RepoID: repo.ID}) + assert.NoError(t, repo_model.PinRepo(db.DefaultContext, user, repo, true)) + unittest.AssertExistsAndLoadBean(t, &repo_model.Pin{UID: user.ID, RepoID: repo.ID}) + assert.NoError(t, repo_model.PinRepo(db.DefaultContext, user, repo, false)) + unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID}) +} + +func TestIsPinned(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + + unittest.AssertNotExistsBean(t, &repo_model.Pin{UID: user.ID, RepoID: repo.ID}) + assert.NoError(t, repo_model.PinRepo(db.DefaultContext, user, repo, true)) + unittest.AssertExistsAndLoadBean(t, &repo_model.Pin{UID: user.ID, RepoID: repo.ID}) + + assert.True(t, repo_model.IsPinned(db.DefaultContext, user.ID, repo.ID)) + + assert.NoError(t, repo_model.PinRepo(db.DefaultContext, user, repo, false)) + unittest.AssertNotExistsBean(t, &repo_model.Star{UID: user.ID, RepoID: repo.ID}) + + assert.False(t, repo_model.IsPinned(db.DefaultContext, user.ID, repo.ID)) +} diff --git a/tests/integration/repo_pin_test.go b/tests/integration/repo_pin_test.go new file mode 100644 index 0000000000000..2d3cc40d2d1f6 --- /dev/null +++ b/tests/integration/repo_pin_test.go @@ -0,0 +1,60 @@ +package integration + +import ( + "net/http" + "net/url" + "path" + "testing" + + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" +) + +func TestUserRepoPin(t *testing.T) { + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + session := loginUser(t, "user2") + unittest.AssertNotExistsBean(t, &repo_model.Pin{UID: 2, RepoID: 3}) + testUserPinRepo(t, session, "org3", "repo3", true, false) + unittest.AssertExistsAndLoadBean(t, &repo_model.Pin{UID: 2, RepoID: 3}) + testUserPinRepo(t, session, "org3", "repo3", false, false) + unittest.AssertNotExistsBean(t, &repo_model.Pin{UID: 2, RepoID: 3}) + }) +} + +func TestOrgRepoPin(t *testing.T) { + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + session := loginUser(t, "user2") + unittest.AssertNotExistsBean(t, &repo_model.Pin{UID: 3, RepoID: 3}) + testUserPinRepo(t, session, "org3", "repo3", true, true) + unittest.AssertExistsAndLoadBean(t, &repo_model.Pin{UID: 3, RepoID: 3}) + testUserPinRepo(t, session, "org3", "repo3", false, true) + unittest.AssertNotExistsBean(t, &repo_model.Pin{UID: 3, RepoID: 3}) + }) +} + +func testUserPinRepo(t *testing.T, session *TestSession, user, repo string, pin, org bool) error { + var action string + if pin { + action = "pin" + } else { + action = "unpin" + } + + if org { + action += "-org" + } + + // Get repo page to get the CSRF token + reqPage := NewRequest(t, "GET", path.Join(user, repo)) + respPage := session.MakeRequest(t, reqPage, http.StatusOK) + + htmlDoc := NewHTMLParser(t, respPage.Body) + + reqPath := path.Join(user, repo, "action", action) + req := NewRequestWithValues(t, "POST", reqPath, map[string]string{ + "_csrf": htmlDoc.GetCSRF(), + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + return nil +} From f8b17560bd91d633e4082720d8abb42ae5152934 Mon Sep 17 00:00:00 2001 From: Carlos Felgueiras Date: Mon, 13 May 2024 10:44:12 +0000 Subject: [PATCH 05/16] fix(lint): fixed and added copyright and license comment on top of files --- models/repo/pin_test.go | 2 +- tests/integration/repo_pin_test.go | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/models/repo/pin_test.go b/models/repo/pin_test.go index 49ef59344da7a..d6d9b2e765f85 100644 --- a/models/repo/pin_test.go +++ b/models/repo/pin_test.go @@ -1,4 +1,4 @@ -// Copyright 2017 The Gitea Authors. All rights reserved. +// Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package repo_test diff --git a/tests/integration/repo_pin_test.go b/tests/integration/repo_pin_test.go index 2d3cc40d2d1f6..9d659d385c610 100644 --- a/tests/integration/repo_pin_test.go +++ b/tests/integration/repo_pin_test.go @@ -1,3 +1,6 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + package integration import ( From 5fba27cbbc599c021685e11cc498cdfda6085903 Mon Sep 17 00:00:00 2001 From: Daniel Carvalho Date: Mon, 13 May 2024 16:06:00 +0000 Subject: [PATCH 06/16] fix(table name): Changed repository pins database name --- models/repo/pin.go | 2 +- models/repo/user_repo.go | 4 ++-- routers/web/repo/view.go | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/models/repo/pin.go b/models/repo/pin.go index 579a31125b1b9..8fd1144310203 100644 --- a/models/repo/pin.go +++ b/models/repo/pin.go @@ -20,7 +20,7 @@ type Pin struct { // TableName sets the table name for the pin struct func (s *Pin) TableName() string { - return "repo_pin" + return "repository_pin" } func init() { diff --git a/models/repo/user_repo.go b/models/repo/user_repo.go index 9af72da11d655..4b0e1f4e04cd8 100644 --- a/models/repo/user_repo.go +++ b/models/repo/user_repo.go @@ -62,7 +62,7 @@ type PinnedReposOptions struct { func (opts *PinnedReposOptions) ToConds() builder.Cond { var cond builder.Cond = builder.Eq{ - "repo_pin.uid": opts.PinnerID, + "repository_pin.uid": opts.PinnerID, } if opts.RepoOwnerID != 0 { cond = cond.And(builder.Eq{ @@ -75,7 +75,7 @@ func (opts *PinnedReposOptions) ToConds() builder.Cond { func (opts *PinnedReposOptions) ToJoins() []db.JoinFunc { return []db.JoinFunc{ func(e db.Engine) error { - e.Join("INNER", "repo_pin", "`repository`.id=`repo_pin`.repo_id") + e.Join("INNER", "repository_pin", "`repository`.id=`repository_pin`.repo_id") return nil }, } diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 1e85f91d58e75..d5993a36c67d9 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -51,7 +51,7 @@ import ( "code.gitea.io/gitea/services/context" issue_service "code.gitea.io/gitea/services/issue" files_service "code.gitea.io/gitea/services/repository/files" - user_services "code.gitea.io/gitea/services/user" + user_service "code.gitea.io/gitea/services/user" "github.com/nektos/act/pkg/model" @@ -1180,13 +1180,13 @@ func Forks(ctx *context.Context) { func loadPinData(ctx *context.Context) error { // First, cleanup any pins that are no longer valid - err := user_services.CleanupPins(ctx, ctx.Doer) + err := user_service.CleanupPins(ctx, ctx.Doer) if err != nil { return err } ctx.Data["IsPinningRepo"] = repo_model.IsPinned(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) - ctx.Data["CanPinRepo"] = user_services.CanPin(ctx, ctx.Doer, ctx.Repo.Repository) + ctx.Data["CanPinRepo"] = user_service.CanPin(ctx, ctx.Doer, ctx.Repo.Repository) if ctx.Repo.Repository.Owner.IsOrganization() { org := organization.OrgFromUser(ctx.Repo.Repository.Owner) @@ -1199,7 +1199,7 @@ func loadPinData(ctx *context.Context) error { if isAdmin { ctx.Data["CanUserPinToOrg"] = true ctx.Data["IsOrgPinningRepo"] = repo_model.IsPinned(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID) - ctx.Data["CanOrgPinRepo"] = user_services.CanPin(ctx, ctx.Repo.Repository.Owner, ctx.Repo.Repository) + ctx.Data["CanOrgPinRepo"] = user_service.CanPin(ctx, ctx.Repo.Repository.Owner, ctx.Repo.Repository) } } From 088727770ea68abf29ea798db6942f04cc5a14bc Mon Sep 17 00:00:00 2001 From: Carlos Felgueiras Date: Wed, 15 May 2024 14:28:22 +0200 Subject: [PATCH 07/16] Update templates/repo/pin_unpin.tmpl Co-authored-by: silverwind --- templates/repo/pin_unpin.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/repo/pin_unpin.tmpl b/templates/repo/pin_unpin.tmpl index f06f898fda338..095861d9b910c 100644 --- a/templates/repo/pin_unpin.tmpl +++ b/templates/repo/pin_unpin.tmpl @@ -12,7 +12,7 @@ {{if .CanUserPinToOrg}}