mirror of https://github.com/go-gitea/gitea.git
Merge 805dbc7ed4
into 58a03e9fad
This commit is contained in:
commit
fe403ce815
|
@ -0,0 +1,61 @@
|
|||
// 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 "repository_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 {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
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 nil
|
||||
})
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
// Copyright 2024 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))
|
||||
}
|
|
@ -54,6 +54,41 @@ 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{
|
||||
"repository_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", "repository_pin", "`repository`.id=`repository_pin`.repo_id")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func GetPinnedRepos(ctx context.Context, opts *PinnedReposOptions) (RepositoryList, error) {
|
||||
return db.Find[Repository](ctx, opts)
|
||||
}
|
||||
|
||||
func CountPinnedRepos(ctx context.Context, opts *PinnedReposOptions) (int64, error) {
|
||||
return db.Count[Repository](ctx, opts)
|
||||
}
|
||||
|
||||
type WatchedReposOptions struct {
|
||||
db.ListOptions
|
||||
WatcherID int64
|
||||
|
|
|
@ -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
|
||||
|
@ -1187,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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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_service "code.gitea.io/gitea/services/user"
|
||||
|
||||
"github.com/nektos/act/pkg/model"
|
||||
|
||||
|
@ -791,6 +793,14 @@ func Home(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
if ctx.IsSigned {
|
||||
err := loadPinData(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("loadPinData", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
renderHomeCode(ctx)
|
||||
}
|
||||
|
||||
|
@ -1168,3 +1178,37 @@ 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_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"], err = user_service.CanPin(ctx, ctx.Doer, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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"], err = user_service.CanPin(ctx, ctx.Repo.Repository.Owner, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -0,0 +1,169 @@
|
|||
// 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"
|
||||
)
|
||||
|
||||
const maxPins = 6
|
||||
|
||||
// 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, error) {
|
||||
count, err := repo_model.CountPinnedRepos(*ctx, &repo_model.PinnedReposOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
ListAll: true,
|
||||
},
|
||||
PinnerID: u.ID,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("CountPinnedRepos", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
if count >= maxPins {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return HasPermsToPin(ctx, u, r), nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
canPin, err := CanPin(ctx, targetUser, repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !canPin {
|
||||
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
|
||||
}
|
|
@ -8,6 +8,9 @@
|
|||
{{if .ProfileReadme}}
|
||||
<div id="readme_profile" class="markup">{{.ProfileReadme}}</div>
|
||||
{{end}}
|
||||
{{if .PinnedRepos}}
|
||||
{{template "shared/pinned_repo_cards" .}}
|
||||
{{end}}
|
||||
{{template "shared/repo_search" .}}
|
||||
{{template "explore/repo_list" .}}
|
||||
{{template "base/paginate" .}}
|
||||
|
|
|
@ -60,6 +60,7 @@
|
|||
{{svg "octicon-rss" 16}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{template "repo/pin_unpin" $}}
|
||||
{{template "repo/watch_unwatch" $}}
|
||||
{{if not $.DisableStars}}
|
||||
{{template "repo/star_unstar" $}}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
<div class="ui buttons">
|
||||
<form hx-boost="true" hx-target="this" method="post" action="{{$.RepoLink}}/action/{{if .IsPinningRepo}}unpin{{else}}pin{{end}}">
|
||||
<div class="ui labeled item" {{if not $.IsSigned}}data-tooltip-content="{{ctx.Locale.Tr "repo.pin_guest_user"}}"{{end}}>
|
||||
{{$buttonText := ctx.Locale.Tr "repo.pin"}}
|
||||
{{if $.IsPinningRepo}}{{$buttonText = ctx.Locale.Tr "repo.unpin"}}{{end}}
|
||||
<button type="submit" class="ui compact small basic button"{{if or (not $.IsSigned) (and (not $.IsPinningRepo) (not .CanPinRepo))}} disabled{{end}} aria-label="{{$buttonText}}">
|
||||
{{if $.IsPinningRepo}}{{svg "octicon-pin-slash"}}{{else}}{{svg "octicon-pin"}}{{end}}
|
||||
<span class="not-mobile" aria-hidden="true">{{$buttonText}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{{if .CanUserPinToOrg}}
|
||||
<div class="ui floating dropdown icon button">
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="compact menu">
|
||||
<form hx-boost="true" hx-target="this" method="post" action="{{$.RepoLink}}/action/{{if .IsOrgPinningRepo}}unpin{{else}}pin{{end}}-org">
|
||||
<div class="ui labeled button item" {{if not $.IsSigned}}data-tooltip-content="{{ctx.Locale.Tr "repo.pin_guest_user"}}"{{end}}>
|
||||
{{$buttonText = ctx.Locale.Tr "repo.pin-org" .Owner.Name}}
|
||||
{{if $.IsOrgPinningRepo}}{{$buttonText = ctx.Locale.Tr "repo.unpin-org" .Owner.Name}}{{end}}
|
||||
<button type="submit" class="ui compact small basic button"{{if or (not $.IsSigned) (and (not $.IsOrgPinningRepo) (not .CanOrgPinRepo))}} disabled{{end}} aria-label="{{$buttonText}}">
|
||||
{{if $.IsOrgPinningRepo}}{{svg "octicon-pin-slash"}}{{else}}{{svg "octicon-pin"}}{{end}}
|
||||
<span class="not-mobile" aria-hidden="true">{{$buttonText}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
|
@ -0,0 +1,62 @@
|
|||
<div class="ui three stackable cards">
|
||||
{{range .PinnedRepos}}
|
||||
<div class="ui card pin-repo-item">
|
||||
<div class="content">
|
||||
<div class="header">
|
||||
<div class="flex-item tw-items-center">
|
||||
<div class="flex-item-main">
|
||||
<a class="name" href="{{.Link}}">
|
||||
{{if or $.PageIsExplore $.PageIsProfileStarList}}{{if .Owner}}{{.Owner.Name}} / {{end}}{{end}}{{.Name}}
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex-item-trailing">
|
||||
{{if .IsArchived}}
|
||||
<span class="ui basic label">{{ctx.Locale.Tr "repo.desc.archived"}}</span>
|
||||
{{end}}
|
||||
{{if .IsTemplate}}
|
||||
{{if .IsPrivate}}
|
||||
<span class="ui basic label">{{ctx.Locale.Tr "repo.desc.private_template"}}</span>
|
||||
{{end}}
|
||||
{{else}}
|
||||
{{if .IsPrivate}}
|
||||
<span class="ui basic label">{{ctx.Locale.Tr "repo.desc.private"}}</span>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{if .IsFork}}
|
||||
{{svg "octicon-repo-forked"}}
|
||||
{{else if .IsMirror}}
|
||||
{{svg "octicon-mirror"}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="extra content">
|
||||
<div class="metas df ac">
|
||||
{{if .PrimaryLanguage}}
|
||||
<span class="text grey df ac mr-3"><i class="color-icon mr-3" style="background-color: {{.PrimaryLanguage.Color}}"></i>{{.PrimaryLanguage.Language}}</span>
|
||||
{{end}}
|
||||
{{if not $.DisableStars}}
|
||||
<span class="text grey df ac mr-3">{{svg "octicon-star" 16 "mr-3"}}{{.NumStars}}</span>
|
||||
{{end}}
|
||||
<span class="text grey df ac mr-3">{{svg "octicon-git-branch" 16 "mr-3"}}{{.NumForks}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="description">
|
||||
{{$description := .DescriptionHTML $.Context}}
|
||||
{{if $description}}<p>{{$description}}</p>{{end}}
|
||||
{{if .Topics}}
|
||||
<div class="ui tags">
|
||||
{{range .Topics}}
|
||||
{{if ne . ""}}
|
||||
<a href="{{AppSubUrl}}/explore/repos?q={{.}}&topic=1">
|
||||
<div class="ui small label topic">{{.}}</div>
|
||||
</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
|
@ -28,6 +28,9 @@
|
|||
{{else if eq .TabName "overview"}}
|
||||
<div id="readme_profile" class="markup">{{.ProfileReadme}}</div>
|
||||
{{else}}
|
||||
{{if .PinnedRepos}}
|
||||
{{template "shared/pinned_repo_cards" .}}
|
||||
{{end}}
|
||||
{{template "shared/repo_search" .}}
|
||||
{{template "explore/repo_list" .}}
|
||||
{{template "base/paginate" .}}
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
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
|
||||
}
|
|
@ -138,3 +138,8 @@
|
|||
.notifications-item:hover .notifications-updated {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pin-repo-item {
|
||||
width: calc(33.33333333333333% - 1em) !important;
|
||||
margin: 0.4375em 0.5em !important;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue