Merge branch 'main' of https://github.com/go-gitea/gitea into workflow-webhook-api

This commit is contained in:
Christopher Homberger
2025-03-21 13:19:13 +01:00
80 changed files with 570 additions and 284 deletions
+49
View File
@@ -127,6 +127,34 @@ var (
&cli.UintFlag{ &cli.UintFlag{
Name: "page-size", Name: "page-size",
Usage: "Search page size.", Usage: "Search page size.",
},
&cli.BoolFlag{
Name: "enable-groups",
Usage: "Enable LDAP groups",
},
&cli.StringFlag{
Name: "group-search-base-dn",
Usage: "The LDAP base DN at which group accounts will be searched for",
},
&cli.StringFlag{
Name: "group-member-attribute",
Usage: "Group attribute containing list of users",
},
&cli.StringFlag{
Name: "group-user-attribute",
Usage: "User attribute listed in group",
},
&cli.StringFlag{
Name: "group-filter",
Usage: "Verify group membership in LDAP",
},
&cli.StringFlag{
Name: "group-team-map",
Usage: "Map LDAP groups to Organization teams",
},
&cli.BoolFlag{
Name: "group-team-map-removal",
Usage: "Remove users from synchronized teams if user does not belong to corresponding LDAP group",
}) })
ldapSimpleAuthCLIFlags = append(commonLdapCLIFlags, ldapSimpleAuthCLIFlags = append(commonLdapCLIFlags,
@@ -273,6 +301,27 @@ func parseLdapConfig(c *cli.Context, config *ldap.Source) error {
if c.IsSet("skip-local-2fa") { if c.IsSet("skip-local-2fa") {
config.SkipLocalTwoFA = c.Bool("skip-local-2fa") config.SkipLocalTwoFA = c.Bool("skip-local-2fa")
} }
if c.IsSet("enable-groups") {
config.GroupsEnabled = c.Bool("enable-groups")
}
if c.IsSet("group-search-base-dn") {
config.GroupDN = c.String("group-search-base-dn")
}
if c.IsSet("group-member-attribute") {
config.GroupMemberUID = c.String("group-member-attribute")
}
if c.IsSet("group-user-attribute") {
config.UserUID = c.String("group-user-attribute")
}
if c.IsSet("group-filter") {
config.GroupFilter = c.String("group-filter")
}
if c.IsSet("group-team-map") {
config.GroupTeamMap = c.String("group-team-map")
}
if c.IsSet("group-team-map-removal") {
config.GroupTeamMapRemoval = c.Bool("group-team-map-removal")
}
return nil return nil
} }
+28
View File
@@ -51,6 +51,13 @@ func TestAddLdapBindDn(t *testing.T) {
"--attributes-in-bind", "--attributes-in-bind",
"--synchronize-users", "--synchronize-users",
"--page-size", "99", "--page-size", "99",
"--enable-groups",
"--group-search-base-dn", "ou=group,dc=full-domain-bind,dc=org",
"--group-member-attribute", "memberUid",
"--group-user-attribute", "uid",
"--group-filter", "(|(cn=gitea_users)(cn=admins))",
"--group-team-map", `{"cn=my-group,cn=groups,dc=example,dc=org": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2"]}}`,
"--group-team-map-removal",
}, },
source: &auth.Source{ source: &auth.Source{
Type: auth.LDAP, Type: auth.LDAP,
@@ -78,6 +85,13 @@ func TestAddLdapBindDn(t *testing.T) {
AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=full-domain-bind,dc=org)", AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=full-domain-bind,dc=org)",
RestrictedFilter: "(memberOf=cn=restricted-group,ou=example,dc=full-domain-bind,dc=org)", RestrictedFilter: "(memberOf=cn=restricted-group,ou=example,dc=full-domain-bind,dc=org)",
Enabled: true, Enabled: true,
GroupsEnabled: true,
GroupDN: "ou=group,dc=full-domain-bind,dc=org",
GroupMemberUID: "memberUid",
UserUID: "uid",
GroupFilter: "(|(cn=gitea_users)(cn=admins))",
GroupTeamMap: `{"cn=my-group,cn=groups,dc=example,dc=org": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2"]}}`,
GroupTeamMapRemoval: true,
}, },
}, },
}, },
@@ -510,6 +524,13 @@ func TestUpdateLdapBindDn(t *testing.T) {
"--bind-password", "secret-bind-full", "--bind-password", "secret-bind-full",
"--synchronize-users", "--synchronize-users",
"--page-size", "99", "--page-size", "99",
"--enable-groups",
"--group-search-base-dn", "ou=group,dc=full-domain-bind,dc=org",
"--group-member-attribute", "memberUid",
"--group-user-attribute", "uid",
"--group-filter", "(|(cn=gitea_users)(cn=admins))",
"--group-team-map", `{"cn=my-group,cn=groups,dc=example,dc=org": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2"]}}`,
"--group-team-map-removal",
}, },
id: 23, id: 23,
existingAuthSource: &auth.Source{ existingAuthSource: &auth.Source{
@@ -545,6 +566,13 @@ func TestUpdateLdapBindDn(t *testing.T) {
AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=full-domain-bind,dc=org)", AdminFilter: "(memberOf=cn=admin-group,ou=example,dc=full-domain-bind,dc=org)",
RestrictedFilter: "(memberOf=cn=restricted-group,ou=example,dc=full-domain-bind,dc=org)", RestrictedFilter: "(memberOf=cn=restricted-group,ou=example,dc=full-domain-bind,dc=org)",
Enabled: true, Enabled: true,
GroupsEnabled: true,
GroupDN: "ou=group,dc=full-domain-bind,dc=org",
GroupMemberUID: "memberUid",
UserUID: "uid",
GroupFilter: "(|(cn=gitea_users)(cn=admins))",
GroupTeamMap: `{"cn=my-group,cn=groups,dc=example,dc=org": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2"]}}`,
GroupTeamMapRemoval: true,
}, },
}, },
}, },
+5 -1
View File
@@ -172,7 +172,10 @@ func (a *Action) TableIndices() []*schemas.Index {
cuIndex := schemas.NewIndex("c_u", schemas.IndexType) cuIndex := schemas.NewIndex("c_u", schemas.IndexType)
cuIndex.AddColumn("user_id", "is_deleted") cuIndex.AddColumn("user_id", "is_deleted")
indices := []*schemas.Index{actUserIndex, repoIndex, cudIndex, cuIndex} actUserUserIndex := schemas.NewIndex("au_c_u", schemas.IndexType)
actUserUserIndex.AddColumn("act_user_id", "created_unix", "user_id")
indices := []*schemas.Index{actUserIndex, repoIndex, cudIndex, cuIndex, actUserUserIndex}
return indices return indices
} }
@@ -442,6 +445,7 @@ type GetFeedsOptions struct {
OnlyPerformedBy bool // only actions performed by requested user OnlyPerformedBy bool // only actions performed by requested user
IncludeDeleted bool // include deleted actions IncludeDeleted bool // include deleted actions
Date string // the day we want activity for: YYYY-MM-DD Date string // the day we want activity for: YYYY-MM-DD
DontCount bool // do counting in GetFeeds
} }
// ActivityReadable return whether doer can read activities of user // ActivityReadable return whether doer can read activities of user
+18 -6
View File
@@ -243,7 +243,11 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err
sess := db.GetEngine(ctx).Where(cond) sess := db.GetEngine(ctx).Where(cond)
sess = db.SetSessionPagination(sess, &opts) sess = db.SetSessionPagination(sess, &opts)
count, err = sess.Desc("`action`.created_unix").FindAndCount(&actions) if opts.DontCount {
err = sess.Desc("`action`.created_unix").Find(&actions)
} else {
count, err = sess.Desc("`action`.created_unix").FindAndCount(&actions)
}
if err != nil { if err != nil {
return nil, 0, fmt.Errorf("FindAndCount: %w", err) return nil, 0, fmt.Errorf("FindAndCount: %w", err)
} }
@@ -257,11 +261,13 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err
return nil, 0, fmt.Errorf("Find(actionsIDs): %w", err) return nil, 0, fmt.Errorf("Find(actionsIDs): %w", err)
} }
count, err = db.GetEngine(ctx).Where(cond). if !opts.DontCount {
Table("action"). count, err = db.GetEngine(ctx).Where(cond).
Cols("`action`.id").Count() Table("action").
if err != nil { Cols("`action`.id").Count()
return nil, 0, fmt.Errorf("Count: %w", err) if err != nil {
return nil, 0, fmt.Errorf("Count: %w", err)
}
} }
if err := db.GetEngine(ctx).In("`action`.id", actionIDs).Desc("`action`.created_unix").Find(&actions); err != nil { if err := db.GetEngine(ctx).In("`action`.id", actionIDs).Desc("`action`.created_unix").Find(&actions); err != nil {
@@ -275,3 +281,9 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err
return actions, count, nil return actions, count, nil
} }
func CountUserFeeds(ctx context.Context, userID int64) (int64, error) {
return db.GetEngine(ctx).Where("user_id = ?", userID).
And("is_deleted = ?", false).
Count(&Action{})
}
-4
View File
@@ -29,7 +29,3 @@ const (
// NoConditionID means a condition to filter the records which don't match any id. // NoConditionID means a condition to filter the records which don't match any id.
// eg: "milestone_id=-1" means "find the items without any milestone. // eg: "milestone_id=-1" means "find the items without any milestone.
const NoConditionID int64 = -1 const NoConditionID int64 = -1
// NonExistingID means a condition to match no result (eg: a non-existing user)
// It doesn't use -1 or -2 because they are used as builtin users.
const NonExistingID int64 = -1000000
+15 -16
View File
@@ -27,8 +27,8 @@ type IssuesOptions struct { //nolint
RepoIDs []int64 // overwrites RepoCond if the length is not 0 RepoIDs []int64 // overwrites RepoCond if the length is not 0
AllPublic bool // include also all public repositories AllPublic bool // include also all public repositories
RepoCond builder.Cond RepoCond builder.Cond
AssigneeID optional.Option[int64] AssigneeID string // "(none)" or "(any)" or a user ID
PosterID optional.Option[int64] PosterID string // "(none)" or "(any)" or a user ID
MentionedID int64 MentionedID int64
ReviewRequestedID int64 ReviewRequestedID int64
ReviewedID int64 ReviewedID int64
@@ -356,26 +356,25 @@ func issuePullAccessibleRepoCond(repoIDstr string, userID int64, owner *user_mod
return cond return cond
} }
func applyAssigneeCondition(sess *xorm.Session, assigneeID optional.Option[int64]) { func applyAssigneeCondition(sess *xorm.Session, assigneeID string) {
// old logic: 0 is also treated as "not filtering assignee", because the "assignee" was read as FormInt64 // old logic: 0 is also treated as "not filtering assignee", because the "assignee" was read as FormInt64
if !assigneeID.Has() || assigneeID.Value() == 0 { if assigneeID == "(none)" {
return
}
if assigneeID.Value() == db.NoConditionID {
sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)") sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)")
} else { } else if assigneeID == "(any)" {
sess.Where("issue.id IN (SELECT issue_id FROM issue_assignees)")
} else if assigneeIDInt64, _ := strconv.ParseInt(assigneeID, 10, 64); assigneeIDInt64 > 0 {
sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id"). sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
And("issue_assignees.assignee_id = ?", assigneeID.Value()) And("issue_assignees.assignee_id = ?", assigneeIDInt64)
} }
} }
func applyPosterCondition(sess *xorm.Session, posterID optional.Option[int64]) { func applyPosterCondition(sess *xorm.Session, posterID string) {
if !posterID.Has() { // Actually every issue has a poster.
return // The "(none)" is for internal usage only: when doer tries to search non-existing user as poster, use "(none)" to return empty result.
} if posterID == "(none)" {
// poster doesn't need to support db.NoConditionID(-1), so just use the value as-is sess.And("issue.poster_id=0")
if posterID.Has() { } else if posterIDInt64, _ := strconv.ParseInt(posterID, 10, 64); posterIDInt64 > 0 {
sess.And("issue.poster_id=?", posterID.Value()) sess.And("issue.poster_id=?", posterIDInt64)
} }
} }
+1 -2
View File
@@ -15,7 +15,6 @@ import (
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -155,7 +154,7 @@ func TestIssues(t *testing.T) {
}{ }{
{ {
issues_model.IssuesOptions{ issues_model.IssuesOptions{
AssigneeID: optional.Some(int64(1)), AssigneeID: "1",
SortType: "oldest", SortType: "oldest",
}, },
[]int64{1, 6}, []int64{1, 6},
+1
View File
@@ -377,6 +377,7 @@ func prepareMigrationTasks() []*migration {
newMigration(314, "Update OwnerID as zero for repository level action tables", v1_24.UpdateOwnerIDOfRepoLevelActionsTables), newMigration(314, "Update OwnerID as zero for repository level action tables", v1_24.UpdateOwnerIDOfRepoLevelActionsTables),
newMigration(315, "Add Ephemeral to ActionRunner", v1_24.AddEphemeralToActionRunner), newMigration(315, "Add Ephemeral to ActionRunner", v1_24.AddEphemeralToActionRunner),
newMigration(316, "Add description for secrets and variables", v1_24.AddDescriptionForSecretsAndVariables), newMigration(316, "Add description for secrets and variables", v1_24.AddDescriptionForSecretsAndVariables),
newMigration(317, "Add new index for action for heatmap", v1_24.AddNewIndexForUserDashboard),
} }
return preparedMigrations return preparedMigrations
} }
+56
View File
@@ -0,0 +1,56 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_24 //nolint
import (
"code.gitea.io/gitea/modules/timeutil"
"xorm.io/xorm"
"xorm.io/xorm/schemas"
)
type improveActionTableIndicesAction struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"INDEX"` // Receiver user id.
OpType int
ActUserID int64 // Action user id.
RepoID int64
CommentID int64 `xorm:"INDEX"`
IsDeleted bool `xorm:"NOT NULL DEFAULT false"`
RefName string
IsPrivate bool `xorm:"NOT NULL DEFAULT false"`
Content string `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
}
// TableName sets the name of this table
func (*improveActionTableIndicesAction) TableName() string {
return "action"
}
// TableIndices implements xorm's TableIndices interface
func (a *improveActionTableIndicesAction) TableIndices() []*schemas.Index {
repoIndex := schemas.NewIndex("r_u_d", schemas.IndexType)
repoIndex.AddColumn("repo_id", "user_id", "is_deleted")
actUserIndex := schemas.NewIndex("au_r_c_u_d", schemas.IndexType)
actUserIndex.AddColumn("act_user_id", "repo_id", "created_unix", "user_id", "is_deleted")
cudIndex := schemas.NewIndex("c_u_d", schemas.IndexType)
cudIndex.AddColumn("created_unix", "user_id", "is_deleted")
cuIndex := schemas.NewIndex("c_u", schemas.IndexType)
cuIndex.AddColumn("user_id", "is_deleted")
actUserUserIndex := schemas.NewIndex("au_c_u", schemas.IndexType)
actUserUserIndex.AddColumn("act_user_id", "created_unix", "user_id")
indices := []*schemas.Index{actUserIndex, repoIndex, cudIndex, cuIndex, actUserUserIndex}
return indices
}
func AddNewIndexForUserDashboard(x *xorm.Engine) error {
return x.Sync(new(improveActionTableIndicesAction))
}
+16 -4
View File
@@ -215,12 +215,24 @@ func init() {
db.RegisterModel(new(Repository)) db.RegisterModel(new(Repository))
} }
func (repo *Repository) GetName() string { func RelativePath(ownerName, repoName string) string {
return repo.Name return strings.ToLower(ownerName) + "/" + strings.ToLower(repoName) + ".git"
} }
func (repo *Repository) GetOwnerName() string { // RelativePath should be an unix style path like username/reponame.git
return repo.OwnerName func (repo *Repository) RelativePath() string {
return RelativePath(repo.OwnerName, repo.Name)
}
type StorageRepo string
// RelativePath should be an unix style path like username/reponame.git
func (sr StorageRepo) RelativePath() string {
return string(sr)
}
func (repo *Repository) WikiStorageRepo() StorageRepo {
return StorageRepo(strings.ToLower(repo.OwnerName) + "/" + strings.ToLower(repo.Name) + ".wiki.git")
} }
// SanitizedOriginalURL returns a sanitized OriginalURL // SanitizedOriginalURL returns a sanitized OriginalURL
-12
View File
@@ -44,24 +44,12 @@ func GetDefaultBranch(ctx context.Context, repo Repository) (string, error) {
return git.GetDefaultBranch(ctx, repoPath(repo)) return git.GetDefaultBranch(ctx, repoPath(repo))
} }
func GetWikiDefaultBranch(ctx context.Context, repo Repository) (string, error) {
return git.GetDefaultBranch(ctx, wikiPath(repo))
}
// IsReferenceExist returns true if given reference exists in the repository. // IsReferenceExist returns true if given reference exists in the repository.
func IsReferenceExist(ctx context.Context, repo Repository, name string) bool { func IsReferenceExist(ctx context.Context, repo Repository, name string) bool {
return git.IsReferenceExist(ctx, repoPath(repo), name) return git.IsReferenceExist(ctx, repoPath(repo), name)
} }
func IsWikiReferenceExist(ctx context.Context, repo Repository, name string) bool {
return git.IsReferenceExist(ctx, wikiPath(repo), name)
}
// IsBranchExist returns true if given branch exists in the repository. // IsBranchExist returns true if given branch exists in the repository.
func IsBranchExist(ctx context.Context, repo Repository, name string) bool { func IsBranchExist(ctx context.Context, repo Repository, name string) bool {
return IsReferenceExist(ctx, repo, git.BranchPrefix+name) return IsReferenceExist(ctx, repo, git.BranchPrefix+name)
} }
func IsWikiBranchExist(ctx context.Context, repo Repository, name string) bool {
return IsWikiReferenceExist(ctx, repo, git.BranchPrefix+name)
}
+7 -19
View File
@@ -8,7 +8,6 @@ import (
"fmt" "fmt"
"io" "io"
"path/filepath" "path/filepath"
"strings"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/reqctx"
@@ -16,21 +15,15 @@ import (
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
) )
// Repository represents a git repository which stored in a disk
type Repository interface { type Repository interface {
GetName() string RelativePath() string // We don't assume how the directory structure of the repository is, so we only need the relative path
GetOwnerName() string
}
func absPath(owner, name string) string {
return filepath.Join(setting.RepoRootPath, strings.ToLower(owner), strings.ToLower(name)+".git")
} }
// RelativePath should be an unix style path like username/reponame.git
// This method should change it according to the current OS.
func repoPath(repo Repository) string { func repoPath(repo Repository) string {
return absPath(repo.GetOwnerName(), repo.GetName()) return filepath.Join(setting.RepoRootPath, filepath.FromSlash(repo.RelativePath()))
}
func wikiPath(repo Repository) string {
return filepath.Join(setting.RepoRootPath, strings.ToLower(repo.GetOwnerName()), strings.ToLower(repo.GetName())+".wiki.git")
} }
// OpenRepository opens the repository at the given relative path with the provided context. // OpenRepository opens the repository at the given relative path with the provided context.
@@ -38,10 +31,6 @@ func OpenRepository(ctx context.Context, repo Repository) (*git.Repository, erro
return git.OpenRepository(ctx, repoPath(repo)) return git.OpenRepository(ctx, repoPath(repo))
} }
func OpenWikiRepository(ctx context.Context, repo Repository) (*git.Repository, error) {
return git.OpenRepository(ctx, wikiPath(repo))
}
// contextKey is a value for use with context.WithValue. // contextKey is a value for use with context.WithValue.
type contextKey struct { type contextKey struct {
repoPath string repoPath string
@@ -86,9 +75,8 @@ func DeleteRepository(ctx context.Context, repo Repository) error {
} }
// RenameRepository renames a repository's name on disk // RenameRepository renames a repository's name on disk
func RenameRepository(ctx context.Context, repo Repository, newName string) error { func RenameRepository(ctx context.Context, repo, newRepo Repository) error {
newRepoPath := absPath(repo.GetOwnerName(), newName) if err := util.Rename(repoPath(repo), repoPath(newRepo)); err != nil {
if err := util.Rename(repoPath(repo), newRepoPath); err != nil {
return fmt.Errorf("rename repository directory: %w", err) return fmt.Errorf("rename repository directory: %w", err)
} }
return nil return nil
+4 -14
View File
@@ -106,16 +106,11 @@ done
return hookNames, hookTpls, giteaHookTpls return hookNames, hookTpls, giteaHookTpls
} }
// CreateDelegateHooksForRepo creates all the hooks scripts for the repo // CreateDelegateHooks creates all the hooks scripts for the repo
func CreateDelegateHooksForRepo(_ context.Context, repo Repository) (err error) { func CreateDelegateHooks(_ context.Context, repo Repository) (err error) {
return createDelegateHooks(filepath.Join(repoPath(repo), "hooks")) return createDelegateHooks(filepath.Join(repoPath(repo), "hooks"))
} }
// CreateDelegateHooksForWiki creates all the hooks scripts for the wiki repo
func CreateDelegateHooksForWiki(_ context.Context, repo Repository) (err error) {
return createDelegateHooks(filepath.Join(wikiPath(repo), "hooks"))
}
func createDelegateHooks(hookDir string) (err error) { func createDelegateHooks(hookDir string) (err error) {
hookNames, hookTpls, giteaHookTpls := getHookTemplates() hookNames, hookTpls, giteaHookTpls := getHookTemplates()
@@ -178,16 +173,11 @@ func ensureExecutable(filename string) error {
return os.Chmod(filename, mode) return os.Chmod(filename, mode)
} }
// CheckDelegateHooksForRepo checks the hooks scripts for the repo // CheckDelegateHooks checks the hooks scripts for the repo
func CheckDelegateHooksForRepo(_ context.Context, repo Repository) ([]string, error) { func CheckDelegateHooks(_ context.Context, repo Repository) ([]string, error) {
return checkDelegateHooks(filepath.Join(repoPath(repo), "hooks")) return checkDelegateHooks(filepath.Join(repoPath(repo), "hooks"))
} }
// CheckDelegateHooksForWiki checks the hooks scripts for the repo
func CheckDelegateHooksForWiki(_ context.Context, repo Repository) ([]string, error) {
return checkDelegateHooks(filepath.Join(wikiPath(repo), "hooks"))
}
func checkDelegateHooks(hookDir string) ([]string, error) { func checkDelegateHooks(hookDir string) ([]string, error) {
hookNames, hookTpls, giteaHookTpls := getHookTemplates() hookNames, hookTpls, giteaHookTpls := getHookTemplates()
+14 -4
View File
@@ -5,11 +5,13 @@ package bleve
import ( import (
"context" "context"
"strconv"
"code.gitea.io/gitea/modules/indexer" "code.gitea.io/gitea/modules/indexer"
indexer_internal "code.gitea.io/gitea/modules/indexer/internal" indexer_internal "code.gitea.io/gitea/modules/indexer/internal"
inner_bleve "code.gitea.io/gitea/modules/indexer/internal/bleve" inner_bleve "code.gitea.io/gitea/modules/indexer/internal/bleve"
"code.gitea.io/gitea/modules/indexer/issues/internal" "code.gitea.io/gitea/modules/indexer/issues/internal"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"github.com/blevesearch/bleve/v2" "github.com/blevesearch/bleve/v2"
@@ -246,12 +248,20 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectColumnID.Value(), "project_board_id")) queries = append(queries, inner_bleve.NumericEqualityQuery(options.ProjectColumnID.Value(), "project_board_id"))
} }
if options.PosterID.Has() { if options.PosterID != "" {
queries = append(queries, inner_bleve.NumericEqualityQuery(options.PosterID.Value(), "poster_id")) // "(none)" becomes 0, it means no poster
posterIDInt64, _ := strconv.ParseInt(options.PosterID, 10, 64)
queries = append(queries, inner_bleve.NumericEqualityQuery(posterIDInt64, "poster_id"))
} }
if options.AssigneeID.Has() { if options.AssigneeID != "" {
queries = append(queries, inner_bleve.NumericEqualityQuery(options.AssigneeID.Value(), "assignee_id")) if options.AssigneeID == "(any)" {
queries = append(queries, inner_bleve.NumericRangeInclusiveQuery(optional.Some[int64](1), optional.None[int64](), "assignee_id"))
} else {
// "(none)" becomes 0, it means no assignee
assigneeIDInt64, _ := strconv.ParseInt(options.AssigneeID, 10, 64)
queries = append(queries, inner_bleve.NumericEqualityQuery(assigneeIDInt64, "assignee_id"))
}
} }
if options.MentionID.Has() { if options.MentionID.Has() {
+1 -1
View File
@@ -54,7 +54,7 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
RepoIDs: options.RepoIDs, RepoIDs: options.RepoIDs,
AllPublic: options.AllPublic, AllPublic: options.AllPublic,
RepoCond: nil, RepoCond: nil,
AssigneeID: optional.Some(convertID(options.AssigneeID)), AssigneeID: options.AssigneeID,
PosterID: options.PosterID, PosterID: options.PosterID,
MentionedID: convertID(options.MentionID), MentionedID: convertID(options.MentionID),
ReviewRequestedID: convertID(options.ReviewRequestedID), ReviewRequestedID: convertID(options.ReviewRequestedID),
+1 -5
View File
@@ -45,11 +45,7 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
searchOpt.ProjectID = optional.Some[int64](0) // Those issues with no project(projectid==0) searchOpt.ProjectID = optional.Some[int64](0) // Those issues with no project(projectid==0)
} }
if opts.AssigneeID.Value() == db.NoConditionID { searchOpt.AssigneeID = opts.AssigneeID
searchOpt.AssigneeID = optional.Some[int64](0) // FIXME: this is inconsistent from other places, 0 means "no assignee"
} else if opts.AssigneeID.Value() != 0 {
searchOpt.AssigneeID = opts.AssigneeID
}
// See the comment of issues_model.SearchOptions for the reason why we need to convert // See the comment of issues_model.SearchOptions for the reason why we need to convert
convertID := func(id int64) optional.Option[int64] { convertID := func(id int64) optional.Option[int64] {
@@ -212,12 +212,22 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
query.Must(elastic.NewTermQuery("project_board_id", options.ProjectColumnID.Value())) query.Must(elastic.NewTermQuery("project_board_id", options.ProjectColumnID.Value()))
} }
if options.PosterID.Has() { if options.PosterID != "" {
query.Must(elastic.NewTermQuery("poster_id", options.PosterID.Value())) // "(none)" becomes 0, it means no poster
posterIDInt64, _ := strconv.ParseInt(options.PosterID, 10, 64)
query.Must(elastic.NewTermQuery("poster_id", posterIDInt64))
} }
if options.AssigneeID.Has() { if options.AssigneeID != "" {
query.Must(elastic.NewTermQuery("assignee_id", options.AssigneeID.Value())) if options.AssigneeID == "(any)" {
q := elastic.NewRangeQuery("assignee_id")
q.Gte(1)
query.Must(q)
} else {
// "(none)" becomes 0, it means no assignee
assigneeIDInt64, _ := strconv.ParseInt(options.AssigneeID, 10, 64)
query.Must(elastic.NewTermQuery("assignee_id", assigneeIDInt64))
}
} }
if options.MentionID.Has() { if options.MentionID.Has() {
+27 -4
View File
@@ -44,6 +44,7 @@ func TestDBSearchIssues(t *testing.T) {
t.Run("search issues with order", searchIssueWithOrder) t.Run("search issues with order", searchIssueWithOrder)
t.Run("search issues in project", searchIssueInProject) t.Run("search issues in project", searchIssueInProject)
t.Run("search issues with paginator", searchIssueWithPaginator) t.Run("search issues with paginator", searchIssueWithPaginator)
t.Run("search issues with any assignee", searchIssueWithAnyAssignee)
} }
func searchIssueWithKeyword(t *testing.T) { func searchIssueWithKeyword(t *testing.T) {
@@ -176,19 +177,19 @@ func searchIssueByID(t *testing.T) {
}{ }{
{ {
opts: SearchOptions{ opts: SearchOptions{
PosterID: optional.Some(int64(1)), PosterID: "1",
}, },
expectedIDs: []int64{11, 6, 3, 2, 1}, expectedIDs: []int64{11, 6, 3, 2, 1},
}, },
{ {
opts: SearchOptions{ opts: SearchOptions{
AssigneeID: optional.Some(int64(1)), AssigneeID: "1",
}, },
expectedIDs: []int64{6, 1}, expectedIDs: []int64{6, 1},
}, },
{ {
// NOTE: This tests no assignees filtering and also ToSearchOptions() to ensure it will set AssigneeID to 0 when it is passed as -1. // NOTE: This tests no assignees filtering and also ToSearchOptions() to ensure it handles the filter correctly
opts: *ToSearchOptions("", &issues.IssuesOptions{AssigneeID: optional.Some(db.NoConditionID)}), opts: *ToSearchOptions("", &issues.IssuesOptions{AssigneeID: "(none)"}),
expectedIDs: []int64{22, 21, 16, 15, 14, 13, 12, 11, 20, 5, 19, 18, 10, 7, 4, 9, 8, 3, 2}, expectedIDs: []int64{22, 21, 16, 15, 14, 13, 12, 11, 20, 5, 19, 18, 10, 7, 4, 9, 8, 3, 2},
}, },
{ {
@@ -462,3 +463,25 @@ func searchIssueWithPaginator(t *testing.T) {
assert.Equal(t, test.expectedTotal, total) assert.Equal(t, test.expectedTotal, total)
} }
} }
func searchIssueWithAnyAssignee(t *testing.T) {
tests := []struct {
opts SearchOptions
expectedIDs []int64
expectedTotal int64
}{
{
SearchOptions{
AssigneeID: "(any)",
},
[]int64{17, 6, 1},
3,
},
}
for _, test := range tests {
issueIDs, total, err := SearchIssues(t.Context(), &test.opts)
require.NoError(t, err)
assert.Equal(t, test.expectedIDs, issueIDs)
assert.Equal(t, test.expectedTotal, total)
}
}
+2 -3
View File
@@ -97,9 +97,8 @@ type SearchOptions struct {
ProjectID optional.Option[int64] // project the issues belong to ProjectID optional.Option[int64] // project the issues belong to
ProjectColumnID optional.Option[int64] // project column the issues belong to ProjectColumnID optional.Option[int64] // project column the issues belong to
PosterID optional.Option[int64] // poster of the issues PosterID string // poster of the issues, "(none)" or "(any)" or a user ID
AssigneeID string // assignee of the issues, "(none)" or "(any)" or a user ID
AssigneeID optional.Option[int64] // assignee of the issues, zero means no assignee
MentionID optional.Option[int64] // mentioned user of the issues MentionID optional.Option[int64] // mentioned user of the issues
+18 -3
View File
@@ -379,7 +379,7 @@ var cases = []*testIndexerCase{
Paginator: &db.ListOptions{ Paginator: &db.ListOptions{
PageSize: 5, PageSize: 5,
}, },
PosterID: optional.Some(int64(1)), PosterID: "1",
}, },
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
assert.Len(t, result.Hits, 5) assert.Len(t, result.Hits, 5)
@@ -397,7 +397,7 @@ var cases = []*testIndexerCase{
Paginator: &db.ListOptions{ Paginator: &db.ListOptions{
PageSize: 5, PageSize: 5,
}, },
AssigneeID: optional.Some(int64(1)), AssigneeID: "1",
}, },
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
assert.Len(t, result.Hits, 5) assert.Len(t, result.Hits, 5)
@@ -415,7 +415,7 @@ var cases = []*testIndexerCase{
Paginator: &db.ListOptions{ Paginator: &db.ListOptions{
PageSize: 5, PageSize: 5,
}, },
AssigneeID: optional.Some(int64(0)), AssigneeID: "(none)",
}, },
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
assert.Len(t, result.Hits, 5) assert.Len(t, result.Hits, 5)
@@ -647,6 +647,21 @@ var cases = []*testIndexerCase{
} }
}, },
}, },
{
Name: "SearchAnyAssignee",
SearchOptions: &internal.SearchOptions{
AssigneeID: "(any)",
},
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
assert.Len(t, result.Hits, 180)
for _, v := range result.Hits {
assert.GreaterOrEqual(t, data[v.ID].AssigneeID, int64(1))
}
assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool {
return v.AssigneeID >= 1
}), result.Total)
},
},
} }
type testIndexerCase struct { type testIndexerCase struct {
@@ -187,12 +187,20 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
query.And(inner_meilisearch.NewFilterEq("project_board_id", options.ProjectColumnID.Value())) query.And(inner_meilisearch.NewFilterEq("project_board_id", options.ProjectColumnID.Value()))
} }
if options.PosterID.Has() { if options.PosterID != "" {
query.And(inner_meilisearch.NewFilterEq("poster_id", options.PosterID.Value())) // "(none)" becomes 0, it means no poster
posterIDInt64, _ := strconv.ParseInt(options.PosterID, 10, 64)
query.And(inner_meilisearch.NewFilterEq("poster_id", posterIDInt64))
} }
if options.AssigneeID.Has() { if options.AssigneeID != "" {
query.And(inner_meilisearch.NewFilterEq("assignee_id", options.AssigneeID.Value())) if options.AssigneeID == "(any)" {
query.And(inner_meilisearch.NewFilterGte("assignee_id", 1))
} else {
// "(none)" becomes 0, it means no assignee
assigneeIDInt64, _ := strconv.ParseInt(options.AssigneeID, 10, 64)
query.And(inner_meilisearch.NewFilterEq("assignee_id", assigneeIDInt64))
}
} }
if options.MentionID.Has() { if options.MentionID.Has() {
+1 -1
View File
@@ -138,7 +138,7 @@ func CheckInitRepository(ctx context.Context, repo *repo_model.Repository) (err
// Init git bare new repository. // Init git bare new repository.
if err = git.InitRepository(ctx, repo.RepoPath(), true, repo.ObjectFormatName); err != nil { if err = git.InitRepository(ctx, repo.RepoPath(), true, repo.ObjectFormatName); err != nil {
return fmt.Errorf("git.InitRepository: %w", err) return fmt.Errorf("git.InitRepository: %w", err)
} else if err = gitrepo.CreateDelegateHooksForRepo(ctx, repo); err != nil { } else if err = gitrepo.CreateDelegateHooks(ctx, repo); err != nil {
return fmt.Errorf("createDelegateHooks: %w", err) return fmt.Errorf("createDelegateHooks: %w", err)
} }
return nil return nil
+1
View File
@@ -3679,6 +3679,7 @@ secrets=Tajné klíče
description=Tejné klíče budou předány určitým akcím a nelze je přečíst jinak. description=Tejné klíče budou předány určitým akcím a nelze je přečíst jinak.
none=Zatím zde nejsou žádné tajné klíče. none=Zatím zde nejsou žádné tajné klíče.
creation=Přidat tajný klíč creation=Přidat tajný klíč
creation.description=Popis
creation.name_placeholder=nerozlišovat velká a malá písmena, pouze alfanumerické znaky nebo podtržítka, nemohou začínat na GITEA_ nebo GITHUB_ creation.name_placeholder=nerozlišovat velká a malá písmena, pouze alfanumerické znaky nebo podtržítka, nemohou začínat na GITEA_ nebo GITHUB_
creation.value_placeholder=Vložte jakýkoliv obsah. Mezery na začátku a konci budou vynechány. creation.value_placeholder=Vložte jakýkoliv obsah. Mezery na začátku a konci budou vynechány.
creation.success=Tajný klíč „%s“ byl přidán. creation.success=Tajný klíč „%s“ byl přidán.
+1
View File
@@ -3670,6 +3670,7 @@ secrets=Secrets
description=Secrets werden an bestimmte Aktionen weitergegeben und können nicht anderweitig ausgelesen werden. description=Secrets werden an bestimmte Aktionen weitergegeben und können nicht anderweitig ausgelesen werden.
none=Noch keine Secrets vorhanden. none=Noch keine Secrets vorhanden.
creation=Secret hinzufügen creation=Secret hinzufügen
creation.description=Beschreibung
creation.name_placeholder=Groß-/Kleinschreibung wird ignoriert, nur alphanumerische Zeichen oder Unterstriche, darf nicht mit GITEA_ oder GITHUB_ beginnen creation.name_placeholder=Groß-/Kleinschreibung wird ignoriert, nur alphanumerische Zeichen oder Unterstriche, darf nicht mit GITEA_ oder GITHUB_ beginnen
creation.value_placeholder=Beliebigen Inhalt eingeben. Leerzeichen am Anfang und Ende werden weggelassen. creation.value_placeholder=Beliebigen Inhalt eingeben. Leerzeichen am Anfang und Ende werden weggelassen.
creation.success=Das Secret "%s" wurde hinzugefügt. creation.success=Das Secret "%s" wurde hinzugefügt.
+1
View File
@@ -3338,6 +3338,7 @@ secrets=Μυστικά
description=Τα μυστικά θα περάσουν σε ορισμένες δράσεις και δεν μπορούν να αναγνωστούν αλλού. description=Τα μυστικά θα περάσουν σε ορισμένες δράσεις και δεν μπορούν να αναγνωστούν αλλού.
none=Δεν υπάρχουν ακόμα μυστικά. none=Δεν υπάρχουν ακόμα μυστικά.
creation=Προσθήκη Μυστικού creation=Προσθήκη Μυστικού
creation.description=Περιγραφή
creation.name_placeholder=αλφαριθμητικοί χαρακτήρες ή κάτω παύλες μόνο, δεν μπορούν να ξεκινούν με GITEA_ ή GITHUB_ creation.name_placeholder=αλφαριθμητικοί χαρακτήρες ή κάτω παύλες μόνο, δεν μπορούν να ξεκινούν με GITEA_ ή GITHUB_
creation.value_placeholder=Εισάγετε οποιοδήποτε περιεχόμενο. Τα κενά στην αρχή παραλείπονται. creation.value_placeholder=Εισάγετε οποιοδήποτε περιεχόμενο. Τα κενά στην αρχή παραλείπονται.
creation.success=Το μυστικό "%s" προστέθηκε. creation.success=Το μυστικό "%s" προστέθηκε.
+2 -2
View File
@@ -1547,8 +1547,8 @@ issues.filter_project = Project
issues.filter_project_all = All projects issues.filter_project_all = All projects
issues.filter_project_none = No project issues.filter_project_none = No project
issues.filter_assignee = Assignee issues.filter_assignee = Assignee
issues.filter_assginee_no_select = All assignees issues.filter_assginee_no_assignee = Assigned to nobody
issues.filter_assginee_no_assignee = No assignee issues.filter_assignee_any_assignee = Assigned to anybody
issues.filter_poster = Author issues.filter_poster = Author
issues.filter_user_placeholder = Search users issues.filter_user_placeholder = Search users
issues.filter_user_no_select = All users issues.filter_user_no_select = All users
+1
View File
@@ -3318,6 +3318,7 @@ secrets=Secretos
description=Los secretos pasarán a ciertas acciones y no se podrán leer de otro modo. description=Los secretos pasarán a ciertas acciones y no se podrán leer de otro modo.
none=Todavía no hay secretos. none=Todavía no hay secretos.
creation=Añadir secreto creation=Añadir secreto
creation.description=Descripción
creation.name_placeholder=sin distinción de mayúsculas, solo carácteres alfanuméricos o guiones bajos, no puede empezar por GITEA_ o GITHUB_ creation.name_placeholder=sin distinción de mayúsculas, solo carácteres alfanuméricos o guiones bajos, no puede empezar por GITEA_ o GITHUB_
creation.value_placeholder=Introduce cualquier contenido. Se omitirá el espacio en blanco en el inicio y el final. creation.value_placeholder=Introduce cualquier contenido. Se omitirá el espacio en blanco en el inicio y el final.
creation.success=El secreto "%s" ha sido añadido. creation.success=El secreto "%s" ha sido añadido.
+1
View File
@@ -2513,6 +2513,7 @@ conan.details.repository=مخزن
owner.settings.cleanuprules.enabled=فعال شده owner.settings.cleanuprules.enabled=فعال شده
[secrets] [secrets]
creation.description=شرح
[actions] [actions]
+1
View File
@@ -1694,6 +1694,7 @@ conan.details.repository=Repo
owner.settings.cleanuprules.enabled=Käytössä owner.settings.cleanuprules.enabled=Käytössä
[secrets] [secrets]
creation.description=Kuvaus
[actions] [actions]
+1
View File
@@ -3705,6 +3705,7 @@ secrets=Secrets
description=Les secrets seront transmis à certaines actions et ne pourront pas être lus autrement. description=Les secrets seront transmis à certaines actions et ne pourront pas être lus autrement.
none=Il n'y a pas encore de secrets. none=Il n'y a pas encore de secrets.
creation=Ajouter un secret creation=Ajouter un secret
creation.description=Description
creation.name_placeholder=Caractères alphanumériques ou tirets bas uniquement, insensibles à la casse, ne peut commencer par GITEA_ ou GITHUB_. creation.name_placeholder=Caractères alphanumériques ou tirets bas uniquement, insensibles à la casse, ne peut commencer par GITEA_ ou GITHUB_.
creation.value_placeholder=Entrez nimporte quoi. Les blancs cernant seront taillés. creation.value_placeholder=Entrez nimporte quoi. Les blancs cernant seront taillés.
creation.success=Le secret "%s" a été ajouté. creation.success=Le secret "%s" a été ajouté.
+1
View File
@@ -3705,6 +3705,7 @@ secrets=Rúin
description=Cuirfear rúin ar aghaidh chuig gníomhartha áirithe agus ní féidir iad a léamh ar mhalairt. description=Cuirfear rúin ar aghaidh chuig gníomhartha áirithe agus ní féidir iad a léamh ar mhalairt.
none=Níl aon rúin ann fós. none=Níl aon rúin ann fós.
creation=Cuir Rúnda leis creation=Cuir Rúnda leis
creation.description=Cur síos
creation.name_placeholder=carachtair alfanumair nó íoslaghda amháin nach féidir a thosú le GITEA_ nó GITHUB_ creation.name_placeholder=carachtair alfanumair nó íoslaghda amháin nach féidir a thosú le GITEA_ nó GITHUB_
creation.value_placeholder=Ionchur ábhar ar bith. Fágfar spás bán ag tús agus ag deireadh ar lár. creation.value_placeholder=Ionchur ábhar ar bith. Fágfar spás bán ag tús agus ag deireadh ar lár.
creation.success=Tá an rún "%s" curtha leis. creation.success=Tá an rún "%s" curtha leis.
+1
View File
@@ -1598,6 +1598,7 @@ conan.details.repository=Tároló
owner.settings.cleanuprules.enabled=Engedélyezett owner.settings.cleanuprules.enabled=Engedélyezett
[secrets] [secrets]
creation.description=Leírás
[actions] [actions]
+1
View File
@@ -1398,6 +1398,7 @@ conan.details.repository=Repositori
owner.settings.cleanuprules.enabled=Aktif owner.settings.cleanuprules.enabled=Aktif
[secrets] [secrets]
creation.description=Deskripsi
[actions] [actions]
+1
View File
@@ -1326,6 +1326,7 @@ npm.details.tag=Merki
pypi.requires=Þarfnast Python pypi.requires=Þarfnast Python
[secrets] [secrets]
creation.description=Lýsing
[actions] [actions]
+1
View File
@@ -2790,6 +2790,7 @@ settings.delete.error=Impossibile eliminare il pacchetto.
owner.settings.cleanuprules.enabled=Attivo owner.settings.cleanuprules.enabled=Attivo
[secrets] [secrets]
creation.description=Descrizione
[actions] [actions]
+1
View File
@@ -3699,6 +3699,7 @@ secrets=シークレット
description=シークレットは特定のActionsに渡されます。 それ以外で読み出されることはありません。 description=シークレットは特定のActionsに渡されます。 それ以外で読み出されることはありません。
none=シークレットはまだありません。 none=シークレットはまだありません。
creation=シークレットを追加 creation=シークレットを追加
creation.description=説明
creation.name_placeholder=大文字小文字の区別なし、英数字とアンダースコアのみ、GITEA_ や GITHUB_ で始まるものは不可 creation.name_placeholder=大文字小文字の区別なし、英数字とアンダースコアのみ、GITEA_ や GITHUB_ で始まるものは不可
creation.value_placeholder=内容を入力してください。前後の空白は除去されます。 creation.value_placeholder=内容を入力してください。前後の空白は除去されます。
creation.success=シークレット "%s" を追加しました。 creation.success=シークレット "%s" を追加しました。
+1
View File
@@ -1547,6 +1547,7 @@ conan.details.repository=저장소
owner.settings.cleanuprules.enabled=활성화됨 owner.settings.cleanuprules.enabled=활성화됨
[secrets] [secrets]
creation.description=설명
[actions] [actions]
+1
View File
@@ -3341,6 +3341,7 @@ secrets=Noslēpumi
description=Noslēpumi tiks padoti atsevišķām darbībām un citādi nevar tikt nolasīti. description=Noslēpumi tiks padoti atsevišķām darbībām un citādi nevar tikt nolasīti.
none=Pagaidām nav neviena noslēpuma. none=Pagaidām nav neviena noslēpuma.
creation=Pievienot noslēpumu creation=Pievienot noslēpumu
creation.description=Apraksts
creation.name_placeholder=reģistr-nejūtīgs, tikai burti, cipari un apakšsvītras, nevar sākties ar GITEA_ vai GITHUB_ creation.name_placeholder=reģistr-nejūtīgs, tikai burti, cipari un apakšsvītras, nevar sākties ar GITEA_ vai GITHUB_
creation.value_placeholder=Ievadiet jebkādu saturu. Atstarpes sākumā un beigā tiks noņemtas. creation.value_placeholder=Ievadiet jebkādu saturu. Atstarpes sākumā un beigā tiks noņemtas.
creation.success=Noslēpums "%s" tika pievienots. creation.success=Noslēpums "%s" tika pievienots.
+1
View File
@@ -2523,6 +2523,7 @@ settings.link.button=Repository link bijwerken
owner.settings.cleanuprules.enabled=Ingeschakeld owner.settings.cleanuprules.enabled=Ingeschakeld
[secrets] [secrets]
creation.description=Omschrijving
[actions] [actions]
+1
View File
@@ -2412,6 +2412,7 @@ conan.details.repository=Repozytorium
owner.settings.cleanuprules.enabled=Włączone owner.settings.cleanuprules.enabled=Włączone
[secrets] [secrets]
creation.description=Opis
[actions] [actions]
+2 -1
View File
@@ -2194,7 +2194,7 @@ settings.protect_check_status_contexts_list=Verificações de status encontradas
settings.protect_required_approvals=Aprovações necessárias: settings.protect_required_approvals=Aprovações necessárias:
settings.dismiss_stale_approvals=Descartar aprovações obsoletas settings.dismiss_stale_approvals=Descartar aprovações obsoletas
settings.dismiss_stale_approvals_desc=Quando novos commits que mudam o conteúdo do pull request são enviados para o branch, as antigas aprovações serão descartadas. settings.dismiss_stale_approvals_desc=Quando novos commits que mudam o conteúdo do pull request são enviados para o branch, as antigas aprovações serão descartadas.
settings.require_signed_commits=Exibir commits assinados settings.require_signed_commits=Exigir commits assinados
settings.require_signed_commits_desc=Rejeitar pushes para este branch se não estiverem assinados ou não forem validáveis. settings.require_signed_commits_desc=Rejeitar pushes para este branch se não estiverem assinados ou não forem validáveis.
settings.protect_branch_name_pattern=Padrão de Nome de Branch Protegida settings.protect_branch_name_pattern=Padrão de Nome de Branch Protegida
settings.protect_patterns=Padrões settings.protect_patterns=Padrões
@@ -3278,6 +3278,7 @@ secrets=Segredos
description=Os segredos serão passados a certas ações e não poderão ser lidos de outra forma. description=Os segredos serão passados a certas ações e não poderão ser lidos de outra forma.
none=Não há segredos ainda. none=Não há segredos ainda.
creation=Adicionar Segredo creation=Adicionar Segredo
creation.description=Descrição
creation.name_placeholder=apenas caracteres alfanuméricos ou underline (_), não pode começar com GITEA_ ou GITHUB_ creation.name_placeholder=apenas caracteres alfanuméricos ou underline (_), não pode começar com GITEA_ ou GITHUB_
creation.value_placeholder=Insira qualquer conteúdo. Espaços em branco no início e no fim serão omitidos. creation.value_placeholder=Insira qualquer conteúdo. Espaços em branco no início e no fim serão omitidos.
creation.success=O segredo "%s" foi adicionado. creation.success=O segredo "%s" foi adicionado.
+2
View File
@@ -3711,8 +3711,10 @@ secrets=Segredos
description=Os segredos serão transmitidos a certas operações e não poderão ser lidos de outra forma. description=Os segredos serão transmitidos a certas operações e não poderão ser lidos de outra forma.
none=Ainda não há segredos. none=Ainda não há segredos.
creation=Adicionar segredo creation=Adicionar segredo
creation.description=Descrição
creation.name_placeholder=Só sublinhados ou alfanuméricos sem distinguir maiúsculas, sem começar com GITEA_ nem GITHUB_ creation.name_placeholder=Só sublinhados ou alfanuméricos sem distinguir maiúsculas, sem começar com GITEA_ nem GITHUB_
creation.value_placeholder=Insira um conteúdo qualquer. Espaços em branco no início ou no fim serão omitidos. creation.value_placeholder=Insira um conteúdo qualquer. Espaços em branco no início ou no fim serão omitidos.
creation.description_placeholder=Escreva uma descrição curta (opcional).
creation.success=O segredo "%s" foi adicionado. creation.success=O segredo "%s" foi adicionado.
creation.failed=Falhou ao adicionar o segredo. creation.failed=Falhou ao adicionar o segredo.
deletion=Remover segredo deletion=Remover segredo
+1
View File
@@ -3275,6 +3275,7 @@ secrets=Секреты
description=Секреты будут передаваться определенным действиям и не могут быть прочитаны иначе. description=Секреты будут передаваться определенным действиям и не могут быть прочитаны иначе.
none=Секретов пока нет. none=Секретов пока нет.
creation=Добавить секрет creation=Добавить секрет
creation.description=Описание
creation.name_placeholder=регистр не важен, только алфавитно-цифровые символы и подчёркивания, не может начинаться с GITEA_ или GITHUB_ creation.name_placeholder=регистр не важен, только алфавитно-цифровые символы и подчёркивания, не может начинаться с GITEA_ или GITHUB_
creation.value_placeholder=Введите любое содержимое. Пробельные символы в начале и конце будут опущены. creation.value_placeholder=Введите любое содержимое. Пробельные символы в начале и конце будут опущены.
creation.success=Секрет «%s» добавлен. creation.success=Секрет «%s» добавлен.
+1
View File
@@ -2454,6 +2454,7 @@ conan.details.repository=කෝෂ්ඨය
owner.settings.cleanuprules.enabled=සබල කර ඇත owner.settings.cleanuprules.enabled=සබල කර ඇත
[secrets] [secrets]
creation.description=සවිස්තරය
[actions] [actions]
+1
View File
@@ -1989,6 +1989,7 @@ conan.details.repository=Utvecklingskatalog
owner.settings.cleanuprules.enabled=Aktiv owner.settings.cleanuprules.enabled=Aktiv
[secrets] [secrets]
creation.description=Beskrivning
[actions] [actions]
+1
View File
@@ -3534,6 +3534,7 @@ secrets=Gizlilikler
description=Gizlilikler belirli işlemlere aktarılacaktır, bunun dışında okunamaz. description=Gizlilikler belirli işlemlere aktarılacaktır, bunun dışında okunamaz.
none=Henüz gizlilik yok. none=Henüz gizlilik yok.
creation=Gizlilik Ekle creation=Gizlilik Ekle
creation.description=Açıklama
creation.name_placeholder=küçük-büyük harfe duyarlı değil, alfanümerik karakterler veya sadece alt tire, GITEA_ veya GITHUB_ ile başlayamaz creation.name_placeholder=küçük-büyük harfe duyarlı değil, alfanümerik karakterler veya sadece alt tire, GITEA_ veya GITHUB_ ile başlayamaz
creation.value_placeholder=Herhangi bir içerik girin. Baştaki ve sondaki boşluklar ihmal edilecektir. creation.value_placeholder=Herhangi bir içerik girin. Baştaki ve sondaki boşluklar ihmal edilecektir.
creation.success=Gizlilik "%s" eklendi. creation.success=Gizlilik "%s" eklendi.
+1
View File
@@ -2524,6 +2524,7 @@ conan.details.repository=Репозиторій
owner.settings.cleanuprules.enabled=Увімкнено owner.settings.cleanuprules.enabled=Увімкнено
[secrets] [secrets]
creation.description=Опис
[actions] [actions]
+1
View File
@@ -3658,6 +3658,7 @@ secrets=密钥
description=Secrets 将被传给特定的 Actions,其它情况将不能读取 description=Secrets 将被传给特定的 Actions,其它情况将不能读取
none=还没有密钥。 none=还没有密钥。
creation=添加密钥 creation=添加密钥
creation.description=组织描述
creation.name_placeholder=不区分大小写,字母数字或下划线不能以GITEA_ 或 GITHUB_ 开头。 creation.name_placeholder=不区分大小写,字母数字或下划线不能以GITEA_ 或 GITHUB_ 开头。
creation.value_placeholder=输入任何内容,开头和结尾的空白都会被省略 creation.value_placeholder=输入任何内容,开头和结尾的空白都会被省略
creation.success=您的密钥 '%s' 添加成功。 creation.success=您的密钥 '%s' 添加成功。
+1
View File
@@ -963,6 +963,7 @@ conan.details.repository=儲存庫
owner.settings.cleanuprules.enabled=已啟用 owner.settings.cleanuprules.enabled=已啟用
[secrets] [secrets]
creation.description=組織描述
[actions] [actions]
+1
View File
@@ -3646,6 +3646,7 @@ secrets=Secret
description=Secret 會被傳給特定的 Action,其他情況無法讀取。 description=Secret 會被傳給特定的 Action,其他情況無法讀取。
none=還沒有 Secret。 none=還沒有 Secret。
creation=加入 Secret creation=加入 Secret
creation.description=描述
creation.name_placeholder=不區分大小寫,只能包含英文字母、數字、底線 ('_'),不能以 GITEA_ 或 GITHUB_ 開頭。 creation.name_placeholder=不區分大小寫,只能包含英文字母、數字、底線 ('_'),不能以 GITEA_ 或 GITHUB_ 開頭。
creation.value_placeholder=輸入任何內容,頭尾的空白都會被忽略。 creation.value_placeholder=輸入任何內容,頭尾的空白都會被忽略。
creation.success=已新增 Secret「%s」。 creation.success=已新增 Secret「%s」。
+4 -4
View File
@@ -290,10 +290,10 @@ func SearchIssues(ctx *context.APIContext) {
if ctx.IsSigned { if ctx.IsSigned {
ctxUserID := ctx.Doer.ID ctxUserID := ctx.Doer.ID
if ctx.FormBool("created") { if ctx.FormBool("created") {
searchOpt.PosterID = optional.Some(ctxUserID) searchOpt.PosterID = strconv.FormatInt(ctxUserID, 10)
} }
if ctx.FormBool("assigned") { if ctx.FormBool("assigned") {
searchOpt.AssigneeID = optional.Some(ctxUserID) searchOpt.AssigneeID = strconv.FormatInt(ctxUserID, 10)
} }
if ctx.FormBool("mentioned") { if ctx.FormBool("mentioned") {
searchOpt.MentionID = optional.Some(ctxUserID) searchOpt.MentionID = optional.Some(ctxUserID)
@@ -538,10 +538,10 @@ func ListIssues(ctx *context.APIContext) {
} }
if createdByID > 0 { if createdByID > 0 {
searchOpt.PosterID = optional.Some(createdByID) searchOpt.PosterID = strconv.FormatInt(createdByID, 10)
} }
if assignedByID > 0 { if assignedByID > 0 {
searchOpt.AssigneeID = optional.Some(assignedByID) searchOpt.AssigneeID = strconv.FormatInt(assignedByID, 10)
} }
if mentionedByID > 0 { if mentionedByID > 0 {
searchOpt.MentionID = optional.Some(mentionedByID) searchOpt.MentionID = optional.Some(mentionedByID)
+1 -1
View File
@@ -476,7 +476,7 @@ func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error)
// findWikiRepoCommit opens the wiki repo and returns the latest commit, writing to context on error. // findWikiRepoCommit opens the wiki repo and returns the latest commit, writing to context on error.
// The caller is responsible for closing the returned repo again // The caller is responsible for closing the returned repo again
func findWikiRepoCommit(ctx *context.APIContext) (*git.Repository, *git.Commit) { func findWikiRepoCommit(ctx *context.APIContext) (*git.Repository, *git.Commit) {
wikiRepo, err := gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository) wikiRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo())
if err != nil { if err != nil {
if git.IsErrNotExist(err) || err.Error() == "no such file or directory" { if git.IsErrNotExist(err) || err.Error() == "no such file or directory" {
ctx.APIErrorNotFound(err) ctx.APIErrorNotFound(err)
+2 -2
View File
@@ -347,11 +347,11 @@ func ViewProject(ctx *context.Context) {
if ctx.Written() { if ctx.Written() {
return return
} }
assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future assigneeID := ctx.FormString("assignee")
opts := issues_model.IssuesOptions{ opts := issues_model.IssuesOptions{
LabelIDs: labelIDs, LabelIDs: labelIDs,
AssigneeID: optional.Some(assigneeID), AssigneeID: assigneeID,
Owner: project.Owner, Owner: project.Owner,
Doer: ctx.Doer, Doer: ctx.Doer,
} }
+2 -2
View File
@@ -284,7 +284,7 @@ func Diff(ctx *context.Context) {
) )
if ctx.Data["PageIsWiki"] != nil { if ctx.Data["PageIsWiki"] != nil {
gitRepo, err = gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository) gitRepo, err = gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo())
if err != nil { if err != nil {
ctx.ServerError("Repo.GitRepo.GetCommit", err) ctx.ServerError("Repo.GitRepo.GetCommit", err)
return return
@@ -417,7 +417,7 @@ func Diff(ctx *context.Context) {
func RawDiff(ctx *context.Context) { func RawDiff(ctx *context.Context) {
var gitRepo *git.Repository var gitRepo *git.Repository
if ctx.Data["PageIsWiki"] != nil { if ctx.Data["PageIsWiki"] != nil {
wikiRepo, err := gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository) wikiRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo())
if err != nil { if err != nil {
ctx.ServerError("OpenRepository", err) ctx.ServerError("OpenRepository", err)
return return
+1 -1
View File
@@ -885,7 +885,7 @@ func ExcerptBlob(ctx *context.Context) {
gitRepo := ctx.Repo.GitRepo gitRepo := ctx.Repo.GitRepo
if ctx.Data["PageIsWiki"] == true { if ctx.Data["PageIsWiki"] == true {
var err error var err error
gitRepo, err = gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository) gitRepo, err = gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo())
if err != nil { if err != nil {
ctx.ServerError("OpenRepository", err) ctx.ServerError("OpenRepository", err)
return return
+9 -9
View File
@@ -208,10 +208,10 @@ func SearchIssues(ctx *context.Context) {
if ctx.IsSigned { if ctx.IsSigned {
ctxUserID := ctx.Doer.ID ctxUserID := ctx.Doer.ID
if ctx.FormBool("created") { if ctx.FormBool("created") {
searchOpt.PosterID = optional.Some(ctxUserID) searchOpt.PosterID = strconv.FormatInt(ctxUserID, 10)
} }
if ctx.FormBool("assigned") { if ctx.FormBool("assigned") {
searchOpt.AssigneeID = optional.Some(ctxUserID) searchOpt.AssigneeID = strconv.FormatInt(ctxUserID, 10)
} }
if ctx.FormBool("mentioned") { if ctx.FormBool("mentioned") {
searchOpt.MentionID = optional.Some(ctxUserID) searchOpt.MentionID = optional.Some(ctxUserID)
@@ -373,10 +373,10 @@ func SearchRepoIssuesJSON(ctx *context.Context) {
} }
if createdByID > 0 { if createdByID > 0 {
searchOpt.PosterID = optional.Some(createdByID) searchOpt.PosterID = strconv.FormatInt(createdByID, 10)
} }
if assignedByID > 0 { if assignedByID > 0 {
searchOpt.AssigneeID = optional.Some(assignedByID) searchOpt.AssigneeID = strconv.FormatInt(assignedByID, 10)
} }
if mentionedByID > 0 { if mentionedByID > 0 {
searchOpt.MentionID = optional.Some(mentionedByID) searchOpt.MentionID = optional.Some(mentionedByID)
@@ -490,7 +490,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
viewType = "all" viewType = "all"
} }
assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future assigneeID := ctx.FormString("assignee")
posterUsername := ctx.FormString("poster") posterUsername := ctx.FormString("poster")
posterUserID := shared_user.GetFilterUserIDByName(ctx, posterUsername) posterUserID := shared_user.GetFilterUserIDByName(ctx, posterUsername)
var mentionedID, reviewRequestedID, reviewedID int64 var mentionedID, reviewRequestedID, reviewedID int64
@@ -498,11 +498,11 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
if ctx.IsSigned { if ctx.IsSigned {
switch viewType { switch viewType {
case "created_by": case "created_by":
posterUserID = optional.Some(ctx.Doer.ID) posterUserID = strconv.FormatInt(ctx.Doer.ID, 10)
case "mentioned": case "mentioned":
mentionedID = ctx.Doer.ID mentionedID = ctx.Doer.ID
case "assigned": case "assigned":
assigneeID = ctx.Doer.ID assigneeID = fmt.Sprint(ctx.Doer.ID)
case "review_requested": case "review_requested":
reviewRequestedID = ctx.Doer.ID reviewRequestedID = ctx.Doer.ID
case "reviewed_by": case "reviewed_by":
@@ -532,7 +532,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
LabelIDs: labelIDs, LabelIDs: labelIDs,
MilestoneIDs: mileIDs, MilestoneIDs: mileIDs,
ProjectID: projectID, ProjectID: projectID,
AssigneeID: optional.Some(assigneeID), AssigneeID: assigneeID,
MentionedID: mentionedID, MentionedID: mentionedID,
PosterID: posterUserID, PosterID: posterUserID,
ReviewRequestedID: reviewRequestedID, ReviewRequestedID: reviewRequestedID,
@@ -613,7 +613,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
PageSize: setting.UI.IssuePagingNum, PageSize: setting.UI.IssuePagingNum,
}, },
RepoIDs: []int64{repo.ID}, RepoIDs: []int64{repo.ID},
AssigneeID: optional.Some(assigneeID), AssigneeID: assigneeID,
PosterID: posterUserID, PosterID: posterUserID,
MentionedID: mentionedID, MentionedID: mentionedID,
ReviewRequestedID: reviewRequestedID, ReviewRequestedID: reviewRequestedID,
+2 -2
View File
@@ -315,12 +315,12 @@ func ViewProject(ctx *context.Context) {
labelIDs := issue.PrepareFilterIssueLabels(ctx, ctx.Repo.Repository.ID, ctx.Repo.Owner) labelIDs := issue.PrepareFilterIssueLabels(ctx, ctx.Repo.Repository.ID, ctx.Repo.Owner)
assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future assigneeID := ctx.FormString("assignee")
issuesMap, err := project_service.LoadIssuesFromProject(ctx, project, &issues_model.IssuesOptions{ issuesMap, err := project_service.LoadIssuesFromProject(ctx, project, &issues_model.IssuesOptions{
RepoIDs: []int64{ctx.Repo.Repository.ID}, RepoIDs: []int64{ctx.Repo.Repository.ID},
LabelIDs: labelIDs, LabelIDs: labelIDs,
AssigneeID: optional.Some(assigneeID), AssigneeID: assigneeID,
}) })
if err != nil { if err != nil {
ctx.ServerError("LoadIssuesOfColumns", err) ctx.ServerError("LoadIssuesOfColumns", err)
+2 -2
View File
@@ -96,7 +96,7 @@ func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error)
} }
func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, error) { func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, error) {
wikiGitRepo, errGitRepo := gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository) wikiGitRepo, errGitRepo := gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo())
if errGitRepo != nil { if errGitRepo != nil {
ctx.ServerError("OpenRepository", errGitRepo) ctx.ServerError("OpenRepository", errGitRepo)
return nil, nil, errGitRepo return nil, nil, errGitRepo
@@ -105,7 +105,7 @@ func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, err
commit, errCommit := wikiGitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultWikiBranch) commit, errCommit := wikiGitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultWikiBranch)
if git.IsErrNotExist(errCommit) { if git.IsErrNotExist(errCommit) {
// if the default branch recorded in database is out of sync, then re-sync it // if the default branch recorded in database is out of sync, then re-sync it
gitRepoDefaultBranch, errBranch := gitrepo.GetWikiDefaultBranch(ctx, ctx.Repo.Repository) gitRepoDefaultBranch, errBranch := gitrepo.GetDefaultBranch(ctx, ctx.Repo.Repository.WikiStorageRepo())
if errBranch != nil { if errBranch != nil {
return wikiGitRepo, nil, errBranch return wikiGitRepo, nil, errBranch
} }
+1 -1
View File
@@ -29,7 +29,7 @@ const (
) )
func wikiEntry(t *testing.T, repo *repo_model.Repository, wikiName wiki_service.WebPath) *git.TreeEntry { func wikiEntry(t *testing.T, repo *repo_model.Repository, wikiName wiki_service.WebPath) *git.TreeEntry {
wikiRepo, err := gitrepo.OpenWikiRepository(git.DefaultContext, repo) wikiRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo.WikiStorageRepo())
assert.NoError(t, err) assert.NoError(t, err)
defer wikiRepo.Close() defer wikiRepo.Close()
commit, err := wikiRepo.GetBranchCommit("master") commit, err := wikiRepo.GetBranchCommit("master")
+9 -10
View File
@@ -8,9 +8,7 @@ import (
"slices" "slices"
"strconv" "strconv"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/user" "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/optional"
) )
func MakeSelfOnTop(doer *user.User, users []*user.User) []*user.User { func MakeSelfOnTop(doer *user.User, users []*user.User) []*user.User {
@@ -34,19 +32,20 @@ func MakeSelfOnTop(doer *user.User, users []*user.User) []*user.User {
// So it's better to make it work like GitHub: users could input username directly. // So it's better to make it work like GitHub: users could input username directly.
// Since it only converts the username to ID directly and is only used internally (to search issues), so no permission check is needed. // Since it only converts the username to ID directly and is only used internally (to search issues), so no permission check is needed.
// Return values: // Return values:
// * nil: no filter // * "": no filter
// * some(id): match the id, the id could be -1 to match the issues without assignee // * "{the-id}": match the id
// * some(NonExistingID): match no issue (due to the user doesn't exist) // * "(none)": match no issue (due to the user doesn't exist)
func GetFilterUserIDByName(ctx context.Context, name string) optional.Option[int64] { func GetFilterUserIDByName(ctx context.Context, name string) string {
if name == "" { if name == "" {
return optional.None[int64]() return ""
} }
u, err := user.GetUserByName(ctx, name) u, err := user.GetUserByName(ctx, name)
if err != nil { if err != nil {
if id, err := strconv.ParseInt(name, 10, 64); err == nil { if id, err := strconv.ParseInt(name, 10, 64); err == nil {
return optional.Some(id) return strconv.FormatInt(id, 10)
} }
return optional.Some(db.NonExistingID) // The "(none)" is for internal usage only: when doer tries to search non-existing user, use "(none)" to return empty result.
return "(none)"
} }
return optional.Some(u.ID) return strconv.FormatInt(u.ID, 10)
} }
+9 -9
View File
@@ -119,7 +119,7 @@ func Dashboard(ctx *context.Context) {
ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data) ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data)
} }
feeds, count, err := feed_service.GetFeeds(ctx, activities_model.GetFeedsOptions{ feeds, count, err := feed_service.GetFeedsForDashboard(ctx, activities_model.GetFeedsOptions{
RequestedUser: ctxUser, RequestedUser: ctxUser,
RequestedTeam: ctx.Org.Team, RequestedTeam: ctx.Org.Team,
Actor: ctx.Doer, Actor: ctx.Doer,
@@ -501,9 +501,9 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
case issues_model.FilterModeAll: case issues_model.FilterModeAll:
case issues_model.FilterModeYourRepositories: case issues_model.FilterModeYourRepositories:
case issues_model.FilterModeAssign: case issues_model.FilterModeAssign:
opts.AssigneeID = optional.Some(ctx.Doer.ID) opts.AssigneeID = strconv.FormatInt(ctx.Doer.ID, 10)
case issues_model.FilterModeCreate: case issues_model.FilterModeCreate:
opts.PosterID = optional.Some(ctx.Doer.ID) opts.PosterID = strconv.FormatInt(ctx.Doer.ID, 10)
case issues_model.FilterModeMention: case issues_model.FilterModeMention:
opts.MentionedID = ctx.Doer.ID opts.MentionedID = ctx.Doer.ID
case issues_model.FilterModeReviewRequested: case issues_model.FilterModeReviewRequested:
@@ -792,9 +792,9 @@ func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMod
case issues_model.FilterModeYourRepositories: case issues_model.FilterModeYourRepositories:
openClosedOpts.AllPublic = false openClosedOpts.AllPublic = false
case issues_model.FilterModeAssign: case issues_model.FilterModeAssign:
openClosedOpts.AssigneeID = optional.Some(doerID) openClosedOpts.AssigneeID = strconv.FormatInt(doerID, 10)
case issues_model.FilterModeCreate: case issues_model.FilterModeCreate:
openClosedOpts.PosterID = optional.Some(doerID) openClosedOpts.PosterID = strconv.FormatInt(doerID, 10)
case issues_model.FilterModeMention: case issues_model.FilterModeMention:
openClosedOpts.MentionID = optional.Some(doerID) openClosedOpts.MentionID = optional.Some(doerID)
case issues_model.FilterModeReviewRequested: case issues_model.FilterModeReviewRequested:
@@ -816,8 +816,8 @@ func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMod
// Below stats are for the left sidebar // Below stats are for the left sidebar
opts = opts.Copy(func(o *issue_indexer.SearchOptions) { opts = opts.Copy(func(o *issue_indexer.SearchOptions) {
o.AssigneeID = nil o.AssigneeID = ""
o.PosterID = nil o.PosterID = ""
o.MentionID = nil o.MentionID = nil
o.ReviewRequestedID = nil o.ReviewRequestedID = nil
o.ReviewedID = nil o.ReviewedID = nil
@@ -827,11 +827,11 @@ func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMod
if err != nil { if err != nil {
return nil, err return nil, err
} }
ret.AssignCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.AssigneeID = optional.Some(doerID) })) ret.AssignCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.AssigneeID = strconv.FormatInt(doerID, 10) }))
if err != nil { if err != nil {
return nil, err return nil, err
} }
ret.CreateCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.PosterID = optional.Some(doerID) })) ret.CreateCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.PosterID = strconv.FormatInt(doerID, 10) }))
if err != nil { if err != nil {
return nil, err return nil, err
} }
+1 -1
View File
@@ -204,7 +204,7 @@ Loop:
return false, "", nil, &ErrWontSign{twofa} return false, "", nil, &ErrWontSign{twofa}
} }
case parentSigned: case parentSigned:
gitRepo, err := gitrepo.OpenWikiRepository(ctx, repo) gitRepo, err := gitrepo.OpenRepository(ctx, repo.WikiStorageRepo())
if err != nil { if err != nil {
return false, "", nil, err return false, "", nil, err
} }
+2 -2
View File
@@ -49,14 +49,14 @@ func checkScriptType(ctx context.Context, logger log.Logger, autofix bool) error
func checkHooks(ctx context.Context, logger log.Logger, autofix bool) error { func checkHooks(ctx context.Context, logger log.Logger, autofix bool) error {
if err := iterateRepositories(ctx, func(repo *repo_model.Repository) error { if err := iterateRepositories(ctx, func(repo *repo_model.Repository) error {
results, err := gitrepo.CheckDelegateHooksForRepo(ctx, repo) results, err := gitrepo.CheckDelegateHooks(ctx, repo)
if err != nil { if err != nil {
logger.Critical("Unable to check delegate hooks for repo %-v. ERROR: %v", repo, err) logger.Critical("Unable to check delegate hooks for repo %-v. ERROR: %v", repo, err)
return fmt.Errorf("Unable to check delegate hooks for repo %-v. ERROR: %w", repo, err) return fmt.Errorf("Unable to check delegate hooks for repo %-v. ERROR: %w", repo, err)
} }
if len(results) > 0 && autofix { if len(results) > 0 && autofix {
logger.Warn("Regenerated hooks for %s", repo.FullName()) logger.Warn("Regenerated hooks for %s", repo.FullName())
if err := gitrepo.CreateDelegateHooksForRepo(ctx, repo); err != nil { if err := gitrepo.CreateDelegateHooks(ctx, repo); err != nil {
logger.Critical("Unable to recreate delegate hooks for %-v. ERROR: %v", repo, err) logger.Critical("Unable to recreate delegate hooks for %-v. ERROR: %v", repo, err)
return fmt.Errorf("Unable to recreate delegate hooks for %-v. ERROR: %w", repo, err) return fmt.Errorf("Unable to recreate delegate hooks for %-v. ERROR: %w", repo, err)
} }
+26
View File
@@ -13,9 +13,28 @@ import (
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
) )
func userFeedCacheKey(userID int64) string {
return fmt.Sprintf("user_feed_%d", userID)
}
func GetFeedsForDashboard(ctx context.Context, opts activities_model.GetFeedsOptions) (activities_model.ActionList, int64, error) {
opts.DontCount = opts.RequestedTeam == nil && opts.Date == ""
results, cnt, err := activities_model.GetFeeds(ctx, opts)
if err != nil {
return nil, 0, err
}
if opts.DontCount {
cnt, err = cache.GetInt64(userFeedCacheKey(opts.Actor.ID), func() (int64, error) {
return activities_model.CountUserFeeds(ctx, opts.Actor.ID)
})
}
return results, cnt, err
}
// GetFeeds returns actions according to the provided options // GetFeeds returns actions according to the provided options
func GetFeeds(ctx context.Context, opts activities_model.GetFeedsOptions) (activities_model.ActionList, int64, error) { func GetFeeds(ctx context.Context, opts activities_model.GetFeedsOptions) (activities_model.ActionList, int64, error) {
return activities_model.GetFeeds(ctx, opts) return activities_model.GetFeeds(ctx, opts)
@@ -68,6 +87,13 @@ func notifyWatchers(ctx context.Context, act *activities_model.Action, watchers
if err := db.Insert(ctx, act); err != nil { if err := db.Insert(ctx, act); err != nil {
return fmt.Errorf("insert new action: %w", err) return fmt.Errorf("insert new action: %w", err)
} }
total, err := activities_model.CountUserFeeds(ctx, act.UserID)
if err != nil {
return fmt.Errorf("count user feeds: %w", err)
}
_ = cache.GetCache().Put(userFeedCacheKey(act.UserID), fmt.Sprintf("%d", total), setting.CacheService.TTLSeconds())
} }
return nil return nil
+1 -1
View File
@@ -143,7 +143,7 @@ func runPushSync(ctx context.Context, m *repo_model.PushMirror) error {
var gitRepo *git.Repository var gitRepo *git.Repository
if isWiki { if isWiki {
gitRepo, err = gitrepo.OpenWikiRepository(ctx, repo) gitRepo, err = gitrepo.OpenRepository(ctx, repo.WikiStorageRepo())
} else { } else {
gitRepo, err = gitrepo.OpenRepository(ctx, repo) gitRepo, err = gitrepo.OpenRepository(ctx, repo)
} }
+1 -1
View File
@@ -115,7 +115,7 @@ func adoptRepository(ctx context.Context, repo *repo_model.Repository, defaultBr
return fmt.Errorf("adoptRepository: path does not already exist: %s", repo.FullName()) return fmt.Errorf("adoptRepository: path does not already exist: %s", repo.FullName())
} }
if err := gitrepo.CreateDelegateHooksForRepo(ctx, repo); err != nil { if err := gitrepo.CreateDelegateHooks(ctx, repo); err != nil {
return fmt.Errorf("createDelegateHooks: %w", err) return fmt.Errorf("createDelegateHooks: %w", err)
} }
+1 -1
View File
@@ -170,7 +170,7 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork
return fmt.Errorf("git update-server-info: %w", err) return fmt.Errorf("git update-server-info: %w", err)
} }
if err = gitrepo.CreateDelegateHooksForRepo(ctx, repo); err != nil { if err = gitrepo.CreateDelegateHooks(ctx, repo); err != nil {
return fmt.Errorf("createDelegateHooks: %w", err) return fmt.Errorf("createDelegateHooks: %w", err)
} }
+2 -2
View File
@@ -31,11 +31,11 @@ func SyncRepositoryHooks(ctx context.Context) error {
default: default:
} }
if err := gitrepo.CreateDelegateHooksForRepo(ctx, repo); err != nil { if err := gitrepo.CreateDelegateHooks(ctx, repo); err != nil {
return fmt.Errorf("SyncRepositoryHook: %w", err) return fmt.Errorf("SyncRepositoryHook: %w", err)
} }
if repo.HasWiki() { if repo.HasWiki() {
if err := gitrepo.CreateDelegateHooksForWiki(ctx, repo); err != nil { if err := gitrepo.CreateDelegateHooks(ctx, repo.WikiStorageRepo()); err != nil {
return fmt.Errorf("SyncRepositoryHook: %w", err) return fmt.Errorf("SyncRepositoryHook: %w", err)
} }
} }
+2 -2
View File
@@ -265,11 +265,11 @@ func cleanUpMigrateGitConfig(ctx context.Context, repoPath string) error {
// CleanUpMigrateInfo finishes migrating repository and/or wiki with things that don't need to be done for mirrors. // CleanUpMigrateInfo finishes migrating repository and/or wiki with things that don't need to be done for mirrors.
func CleanUpMigrateInfo(ctx context.Context, repo *repo_model.Repository) (*repo_model.Repository, error) { func CleanUpMigrateInfo(ctx context.Context, repo *repo_model.Repository) (*repo_model.Repository, error) {
if err := gitrepo.CreateDelegateHooksForRepo(ctx, repo); err != nil { if err := gitrepo.CreateDelegateHooks(ctx, repo); err != nil {
return repo, fmt.Errorf("createDelegateHooks: %w", err) return repo, fmt.Errorf("createDelegateHooks: %w", err)
} }
if repo.HasWiki() { if repo.HasWiki() {
if err := gitrepo.CreateDelegateHooksForWiki(ctx, repo); err != nil { if err := gitrepo.CreateDelegateHooks(ctx, repo.WikiStorageRepo()); err != nil {
return repo, fmt.Errorf("createDelegateHooks.(wiki): %w", err) return repo, fmt.Errorf("createDelegateHooks.(wiki): %w", err)
} }
} }
+111 -86
View File
@@ -167,8 +167,9 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
} }
} }
branch := opts.RefFullName.BranchName()
if !opts.IsDelRef() { if !opts.IsDelRef() {
branch := opts.RefFullName.BranchName()
log.Trace("TriggerTask '%s/%s' by %s", repo.Name, branch, pusher.Name) log.Trace("TriggerTask '%s/%s' by %s", repo.Name, branch, pusher.Name)
newCommit, err := gitRepo.GetCommit(opts.NewCommitID) newCommit, err := gitRepo.GetCommit(opts.NewCommitID)
@@ -176,60 +177,15 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
return fmt.Errorf("gitRepo.GetCommit(%s) in %s/%s[%d]: %w", opts.NewCommitID, repo.OwnerName, repo.Name, repo.ID, err) return fmt.Errorf("gitRepo.GetCommit(%s) in %s/%s[%d]: %w", opts.NewCommitID, repo.OwnerName, repo.Name, repo.ID, err)
} }
refName := opts.RefName()
// Push new branch. // Push new branch.
var l []*git.Commit var l []*git.Commit
if opts.IsNewRef() { if opts.IsNewRef() {
if repo.IsEmpty { // Change default branch and empty status only if pushed ref is non-empty branch. l, err = pushNewBranch(ctx, repo, pusher, opts, newCommit)
repo.DefaultBranch = refName
repo.IsEmpty = false
if repo.DefaultBranch != setting.Repository.DefaultBranch {
if err := gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil {
return err
}
}
// Update the is empty and default_branch columns
if err := repo_model.UpdateRepositoryCols(ctx, repo, "default_branch", "is_empty"); err != nil {
return fmt.Errorf("UpdateRepositoryCols: %w", err)
}
}
l, err = newCommit.CommitsBeforeLimit(10)
if err != nil {
return fmt.Errorf("newCommit.CommitsBeforeLimit: %w", err)
}
notify_service.CreateRef(ctx, pusher, repo, opts.RefFullName, opts.NewCommitID)
} else { } else {
l, err = newCommit.CommitsBeforeUntil(opts.OldCommitID) l, err = pushUpdateBranch(ctx, repo, pusher, opts, newCommit)
if err != nil { }
return fmt.Errorf("newCommit.CommitsBeforeUntil: %w", err) if err != nil {
} return err
isForcePush, err := newCommit.IsForcePush(opts.OldCommitID)
if err != nil {
log.Error("IsForcePush %s:%s failed: %v", repo.FullName(), branch, err)
}
// only update branch can trigger pull request task because the pull request hasn't been created yet when creaing a branch
go pull_service.AddTestPullRequestTask(pull_service.TestPullRequestOptions{
RepoID: repo.ID,
Doer: pusher,
Branch: branch,
IsSync: true,
IsForcePush: isForcePush,
OldCommitID: opts.OldCommitID,
NewCommitID: opts.NewCommitID,
})
if isForcePush {
log.Trace("Push %s is a force push", opts.NewCommitID)
cache.Remove(repo.GetCommitsCountCacheKey(opts.RefName(), true))
} else {
// TODO: increment update the commit count cache but not remove
cache.Remove(repo.GetCommitsCountCacheKey(opts.RefName(), true))
}
} }
// delete cache for divergence // delete cache for divergence
@@ -246,36 +202,11 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
commits := repo_module.GitToPushCommits(l) commits := repo_module.GitToPushCommits(l)
commits.HeadCommit = repo_module.CommitToPushCommit(newCommit) commits.HeadCommit = repo_module.CommitToPushCommit(newCommit)
if err := issue_service.UpdateIssuesCommit(ctx, pusher, repo, commits.Commits, refName); err != nil { if err := issue_service.UpdateIssuesCommit(ctx, pusher, repo, commits.Commits, opts.RefName()); err != nil {
log.Error("updateIssuesCommit: %v", err) log.Error("updateIssuesCommit: %v", err)
} }
oldCommitID := opts.OldCommitID commits.CompareURL = getCompareURL(repo, gitRepo, objectFormat, commits.Commits, opts)
if oldCommitID == objectFormat.EmptyObjectID().String() && len(commits.Commits) > 0 {
oldCommit, err := gitRepo.GetCommit(commits.Commits[len(commits.Commits)-1].Sha1)
if err != nil && !git.IsErrNotExist(err) {
log.Error("unable to GetCommit %s from %-v: %v", oldCommitID, repo, err)
}
if oldCommit != nil {
for i := 0; i < oldCommit.ParentCount(); i++ {
commitID, _ := oldCommit.ParentID(i)
if !commitID.IsZero() {
oldCommitID = commitID.String()
break
}
}
}
}
if oldCommitID == objectFormat.EmptyObjectID().String() && repo.DefaultBranch != branch {
oldCommitID = repo.DefaultBranch
}
if oldCommitID != objectFormat.EmptyObjectID().String() {
commits.CompareURL = repo.ComposeCompareURL(oldCommitID, opts.NewCommitID)
} else {
commits.CompareURL = ""
}
if len(commits.Commits) > setting.UI.FeedMaxCommitNum { if len(commits.Commits) > setting.UI.FeedMaxCommitNum {
commits.Commits = commits.Commits[:setting.UI.FeedMaxCommitNum] commits.Commits = commits.Commits[:setting.UI.FeedMaxCommitNum]
@@ -288,12 +219,7 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
log.Error("repo_module.CacheRef %s/%s failed: %v", repo.ID, branch, err) log.Error("repo_module.CacheRef %s/%s failed: %v", repo.ID, branch, err)
} }
} else { } else {
notify_service.DeleteRef(ctx, pusher, repo, opts.RefFullName) pushDeleteBranch(ctx, repo, pusher, opts)
if err := pull_service.AdjustPullsCausedByBranchDeleted(ctx, pusher, repo, branch); err != nil {
// close all related pulls
log.Error("close related pull request failed: %v", err)
}
} }
// Even if user delete a branch on a repository which he didn't watch, he will be watch that. // Even if user delete a branch on a repository which he didn't watch, he will be watch that.
@@ -304,8 +230,11 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
log.Trace("Non-tag and non-branch commits pushed.") log.Trace("Non-tag and non-branch commits pushed.")
} }
} }
if err := PushUpdateAddDeleteTags(ctx, repo, gitRepo, addTags, delTags); err != nil {
return fmt.Errorf("PushUpdateAddDeleteTags: %w", err) if len(addTags)+len(delTags) > 0 {
if err := PushUpdateAddDeleteTags(ctx, repo, gitRepo, addTags, delTags); err != nil {
return fmt.Errorf("PushUpdateAddDeleteTags: %w", err)
}
} }
// Change repository last updated time. // Change repository last updated time.
@@ -316,6 +245,102 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
return nil return nil
} }
func getCompareURL(repo *repo_model.Repository, gitRepo *git.Repository, objectFormat git.ObjectFormat, commits []*repo_module.PushCommit, opts *repo_module.PushUpdateOptions) string {
oldCommitID := opts.OldCommitID
if oldCommitID == objectFormat.EmptyObjectID().String() && len(commits) > 0 {
oldCommit, err := gitRepo.GetCommit(commits[len(commits)-1].Sha1)
if err != nil && !git.IsErrNotExist(err) {
log.Error("unable to GetCommit %s from %-v: %v", oldCommitID, repo, err)
}
if oldCommit != nil {
for i := 0; i < oldCommit.ParentCount(); i++ {
commitID, _ := oldCommit.ParentID(i)
if !commitID.IsZero() {
oldCommitID = commitID.String()
break
}
}
}
}
if oldCommitID == objectFormat.EmptyObjectID().String() && repo.DefaultBranch != opts.RefFullName.BranchName() {
oldCommitID = repo.DefaultBranch
}
if oldCommitID != objectFormat.EmptyObjectID().String() {
return repo.ComposeCompareURL(oldCommitID, opts.NewCommitID)
}
return ""
}
func pushNewBranch(ctx context.Context, repo *repo_model.Repository, pusher *user_model.User, opts *repo_module.PushUpdateOptions, newCommit *git.Commit) ([]*git.Commit, error) {
if repo.IsEmpty { // Change default branch and empty status only if pushed ref is non-empty branch.
repo.DefaultBranch = opts.RefName()
repo.IsEmpty = false
if repo.DefaultBranch != setting.Repository.DefaultBranch {
if err := gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil {
return nil, err
}
}
// Update the is empty and default_branch columns
if err := repo_model.UpdateRepositoryCols(ctx, repo, "default_branch", "is_empty"); err != nil {
return nil, fmt.Errorf("UpdateRepositoryCols: %w", err)
}
}
l, err := newCommit.CommitsBeforeLimit(10)
if err != nil {
return nil, fmt.Errorf("newCommit.CommitsBeforeLimit: %w", err)
}
notify_service.CreateRef(ctx, pusher, repo, opts.RefFullName, opts.NewCommitID)
return l, nil
}
func pushUpdateBranch(_ context.Context, repo *repo_model.Repository, pusher *user_model.User, opts *repo_module.PushUpdateOptions, newCommit *git.Commit) ([]*git.Commit, error) {
l, err := newCommit.CommitsBeforeUntil(opts.OldCommitID)
if err != nil {
return nil, fmt.Errorf("newCommit.CommitsBeforeUntil: %w", err)
}
branch := opts.RefFullName.BranchName()
isForcePush, err := newCommit.IsForcePush(opts.OldCommitID)
if err != nil {
log.Error("IsForcePush %s:%s failed: %v", repo.FullName(), branch, err)
}
// only update branch can trigger pull request task because the pull request hasn't been created yet when creating a branch
go pull_service.AddTestPullRequestTask(pull_service.TestPullRequestOptions{
RepoID: repo.ID,
Doer: pusher,
Branch: branch,
IsSync: true,
IsForcePush: isForcePush,
OldCommitID: opts.OldCommitID,
NewCommitID: opts.NewCommitID,
})
if isForcePush {
log.Trace("Push %s is a force push", opts.NewCommitID)
cache.Remove(repo.GetCommitsCountCacheKey(opts.RefName(), true))
} else {
// TODO: increment update the commit count cache but not remove
cache.Remove(repo.GetCommitsCountCacheKey(opts.RefName(), true))
}
return l, nil
}
func pushDeleteBranch(ctx context.Context, repo *repo_model.Repository, pusher *user_model.User, opts *repo_module.PushUpdateOptions) {
notify_service.DeleteRef(ctx, pusher, repo, opts.RefFullName)
if err := pull_service.AdjustPullsCausedByBranchDeleted(ctx, pusher, repo, opts.RefFullName.BranchName()); err != nil {
// close all related pulls
log.Error("close related pull request failed: %v", err)
}
}
// PushUpdateAddDeleteTags updates a number of added and delete tags // PushUpdateAddDeleteTags updates a number of added and delete tags
func PushUpdateAddDeleteTags(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, addTags, delTags []string) error { func PushUpdateAddDeleteTags(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, addTags, delTags []string) error {
return db.WithTx(ctx, func(ctx context.Context) error { return db.WithTx(ctx, func(ctx context.Context) error {
+3 -2
View File
@@ -331,12 +331,13 @@ func changeRepositoryName(ctx context.Context, repo *repo_model.Repository, newR
return fmt.Errorf("IsRepositoryExist: %w", err) return fmt.Errorf("IsRepositoryExist: %w", err)
} else if has { } else if has {
return repo_model.ErrRepoAlreadyExist{ return repo_model.ErrRepoAlreadyExist{
Uname: repo.Owner.Name, Uname: repo.OwnerName,
Name: newRepoName, Name: newRepoName,
} }
} }
if err = gitrepo.RenameRepository(ctx, repo, newRepoName); err != nil { if err = gitrepo.RenameRepository(ctx, repo,
repo_model.StorageRepo(repo_model.RelativePath(repo.OwnerName, newRepoName))); err != nil {
return fmt.Errorf("rename repository directory: %w", err) return fmt.Errorf("rename repository directory: %w", err)
} }
+4 -4
View File
@@ -41,7 +41,7 @@ func InitWiki(ctx context.Context, repo *repo_model.Repository) error {
if err := git.InitRepository(ctx, repo.WikiPath(), true, repo.ObjectFormatName); err != nil { if err := git.InitRepository(ctx, repo.WikiPath(), true, repo.ObjectFormatName); err != nil {
return fmt.Errorf("InitRepository: %w", err) return fmt.Errorf("InitRepository: %w", err)
} else if err = gitrepo.CreateDelegateHooksForWiki(ctx, repo); err != nil { } else if err = gitrepo.CreateDelegateHooks(ctx, repo.WikiStorageRepo()); err != nil {
return fmt.Errorf("createDelegateHooks: %w", err) return fmt.Errorf("createDelegateHooks: %w", err)
} else if _, _, err = git.NewCommand("symbolic-ref", "HEAD").AddDynamicArguments(git.BranchPrefix+repo.DefaultWikiBranch).RunStdString(ctx, &git.RunOpts{Dir: repo.WikiPath()}); err != nil { } else if _, _, err = git.NewCommand("symbolic-ref", "HEAD").AddDynamicArguments(git.BranchPrefix+repo.DefaultWikiBranch).RunStdString(ctx, &git.RunOpts{Dir: repo.WikiPath()}); err != nil {
return fmt.Errorf("unable to set default wiki branch to %q: %w", repo.DefaultWikiBranch, err) return fmt.Errorf("unable to set default wiki branch to %q: %w", repo.DefaultWikiBranch, err)
@@ -100,7 +100,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
return fmt.Errorf("InitWiki: %w", err) return fmt.Errorf("InitWiki: %w", err)
} }
hasDefaultBranch := gitrepo.IsWikiBranchExist(ctx, repo, repo.DefaultWikiBranch) hasDefaultBranch := gitrepo.IsBranchExist(ctx, repo.WikiStorageRepo(), repo.DefaultWikiBranch)
basePath, err := repo_module.CreateTemporaryPath("update-wiki") basePath, err := repo_module.CreateTemporaryPath("update-wiki")
if err != nil { if err != nil {
@@ -381,7 +381,7 @@ func ChangeDefaultWikiBranch(ctx context.Context, repo *repo_model.Repository, n
return nil return nil
} }
oldDefBranch, err := gitrepo.GetWikiDefaultBranch(ctx, repo) oldDefBranch, err := gitrepo.GetDefaultBranch(ctx, repo.WikiStorageRepo())
if err != nil { if err != nil {
return fmt.Errorf("unable to get default branch: %w", err) return fmt.Errorf("unable to get default branch: %w", err)
} }
@@ -389,7 +389,7 @@ func ChangeDefaultWikiBranch(ctx context.Context, repo *repo_model.Repository, n
return nil return nil
} }
gitRepo, err := gitrepo.OpenWikiRepository(ctx, repo) gitRepo, err := gitrepo.OpenRepository(ctx, repo.WikiStorageRepo())
if errors.Is(err, util.ErrNotExist) { if errors.Is(err, util.ErrNotExist) {
return nil // no git repo on storage, no need to do anything else return nil // no git repo on storage, no need to do anything else
} else if err != nil { } else if err != nil {
+4 -4
View File
@@ -166,7 +166,7 @@ func TestRepository_AddWikiPage(t *testing.T) {
webPath := UserTitleToWebPath("", userTitle) webPath := UserTitleToWebPath("", userTitle)
assert.NoError(t, AddWikiPage(git.DefaultContext, doer, repo, webPath, wikiContent, commitMsg)) assert.NoError(t, AddWikiPage(git.DefaultContext, doer, repo, webPath, wikiContent, commitMsg))
// Now need to show that the page has been added: // Now need to show that the page has been added:
gitRepo, err := gitrepo.OpenWikiRepository(git.DefaultContext, repo) gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo.WikiStorageRepo())
require.NoError(t, err) require.NoError(t, err)
defer gitRepo.Close() defer gitRepo.Close()
@@ -213,7 +213,7 @@ func TestRepository_EditWikiPage(t *testing.T) {
assert.NoError(t, EditWikiPage(git.DefaultContext, doer, repo, "Home", webPath, newWikiContent, commitMsg)) assert.NoError(t, EditWikiPage(git.DefaultContext, doer, repo, "Home", webPath, newWikiContent, commitMsg))
// Now need to show that the page has been added: // Now need to show that the page has been added:
gitRepo, err := gitrepo.OpenWikiRepository(git.DefaultContext, repo) gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo.WikiStorageRepo())
assert.NoError(t, err) assert.NoError(t, err)
masterTree, err := gitRepo.GetTree(repo.DefaultWikiBranch) masterTree, err := gitRepo.GetTree(repo.DefaultWikiBranch)
assert.NoError(t, err) assert.NoError(t, err)
@@ -237,7 +237,7 @@ func TestRepository_DeleteWikiPage(t *testing.T) {
assert.NoError(t, DeleteWikiPage(git.DefaultContext, doer, repo, "Home")) assert.NoError(t, DeleteWikiPage(git.DefaultContext, doer, repo, "Home"))
// Now need to show that the page has been added: // Now need to show that the page has been added:
gitRepo, err := gitrepo.OpenWikiRepository(git.DefaultContext, repo) gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo.WikiStorageRepo())
require.NoError(t, err) require.NoError(t, err)
defer gitRepo.Close() defer gitRepo.Close()
@@ -251,7 +251,7 @@ func TestRepository_DeleteWikiPage(t *testing.T) {
func TestPrepareWikiFileName(t *testing.T) { func TestPrepareWikiFileName(t *testing.T) {
unittest.PrepareTestEnv(t) unittest.PrepareTestEnv(t)
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
gitRepo, err := gitrepo.OpenWikiRepository(git.DefaultContext, repo) gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo.WikiStorageRepo())
require.NoError(t, err) require.NoError(t, err)
defer gitRepo.Close() defer gitRepo.Close()
+2 -2
View File
@@ -15,8 +15,8 @@
"UserSearchList" $.Assignees "UserSearchList" $.Assignees
"SelectedUserId" $.AssigneeID "SelectedUserId" $.AssigneeID
"TextFilterTitle" (ctx.Locale.Tr "repo.issues.filter_assignee") "TextFilterTitle" (ctx.Locale.Tr "repo.issues.filter_assignee")
"TextZeroValue" (ctx.Locale.Tr "repo.issues.filter_assginee_no_select") "TextFilterMatchNone" (ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee")
"TextNegativeOne" (ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee") "TextFilterMatchAny" (ctx.Locale.Tr "repo.issues.filter_assignee_any_assignee")
}} }}
</div> </div>
</div> </div>
@@ -4,8 +4,8 @@
* UserSearchList * UserSearchList
* SelectedUserId: 0 or empty means default, -1 means "no user is set" * SelectedUserId: 0 or empty means default, -1 means "no user is set"
* TextFilterTitle * TextFilterTitle
* TextZeroValue: the text for "all issues" * TextFilterMatchNone: the text for "issues with no assignee"
* TextNegativeOne: the text for "issues with no assignee" * TextFilterMatchAny: the text for "issues with any assignee"
*/}} */}}
{{$queryLink := .QueryLink}} {{$queryLink := .QueryLink}}
<div class="item ui dropdown jump {{if not .UserSearchList}}disabled{{end}}"> <div class="item ui dropdown jump {{if not .UserSearchList}}disabled{{end}}">
@@ -15,16 +15,24 @@
<i class="icon">{{svg "octicon-search" 16}}</i> <i class="icon">{{svg "octicon-search" 16}}</i>
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_user_placeholder"}}"> <input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_user_placeholder"}}">
</div> </div>
{{if $.TextZeroValue}} {{if $.TextFilterMatchNone}}
<a class="item {{if not .SelectedUserId}}selected{{end}}" href="{{QueryBuild $queryLink $.QueryParamKey NIL}}">{{$.TextZeroValue}}</a> {{$isSelected := eq .SelectedUserId "(none)"}}
<a class="item" href="{{QueryBuild $queryLink $.QueryParamKey (Iif $isSelected NIL "(none)")}}">
{{svg "octicon-check" 14 (Iif $isSelected "" "tw-invisible")}} {{$.TextFilterMatchNone}}
</a>
{{end}} {{end}}
{{if $.TextNegativeOne}} {{if $.TextFilterMatchAny}}
<a class="item {{if eq .SelectedUserId -1}}selected{{end}}" href="{{QueryBuild $queryLink $.QueryParamKey -1}}">{{$.TextNegativeOne}}</a> {{$isSelected := eq .SelectedUserId "(any)"}}
<a class="item" href="{{QueryBuild $queryLink $.QueryParamKey (Iif $isSelected NIL "(any)")}}">
{{svg "octicon-check" 14 (Iif $isSelected "" "tw-invisible")}} {{$.TextFilterMatchAny}}
</a>
{{end}} {{end}}
<div class="divider"></div> <div class="divider"></div>
{{range .UserSearchList}} {{range $user := .UserSearchList}}
<a class="item {{if eq $.SelectedUserId .ID}}selected{{end}}" href="{{QueryBuild $queryLink $.QueryParamKey .ID}}"> {{$isSelected := eq $.SelectedUserId (print $user.ID)}}
{{ctx.AvatarUtils.Avatar . 20}}{{template "repo/search_name" .}} <a class="item" href="{{QueryBuild $queryLink $.QueryParamKey (Iif $isSelected NIL $user.ID)}}">
{{svg "octicon-check" 14 (Iif $isSelected "" "tw-invisible")}}
{{ctx.AvatarUtils.Avatar $user 20}}{{template "repo/search_name" .}}
</a> </a>
{{end}} {{end}}
</div> </div>
+2 -2
View File
@@ -94,8 +94,8 @@
"UserSearchList" $.Assignees "UserSearchList" $.Assignees
"SelectedUserId" $.AssigneeID "SelectedUserId" $.AssigneeID
"TextFilterTitle" (ctx.Locale.Tr "repo.issues.filter_assignee") "TextFilterTitle" (ctx.Locale.Tr "repo.issues.filter_assignee")
"TextZeroValue" (ctx.Locale.Tr "repo.issues.filter_assginee_no_select") "TextFilterMatchNone" (ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee")
"TextNegativeOne" (ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee") "TextFilterMatchAny" (ctx.Locale.Tr "repo.issues.filter_assignee_any_assignee")
}} }}
{{if .IsSigned}} {{if .IsSigned}}
+7 -5
View File
@@ -14,19 +14,21 @@
{{$entry := $item.Entry}} {{$entry := $item.Entry}}
{{$commit := $item.Commit}} {{$commit := $item.Commit}}
{{$submoduleFile := $item.SubmoduleFile}} {{$submoduleFile := $item.SubmoduleFile}}
<div class="repo-file-cell name {{if not $commit}}notready{{end}}"> <div class="repo-file-cell name muted-links {{if not $commit}}notready{{end}}">
{{ctx.RenderUtils.RenderFileIcon $entry}} {{ctx.RenderUtils.RenderFileIcon $entry}}
{{if $entry.IsSubModule}} {{if $entry.IsSubModule}}
{{$submoduleLink := $submoduleFile.SubmoduleWebLink ctx}} {{$submoduleLink := $submoduleFile.SubmoduleWebLink ctx}}
{{if $submoduleLink}} {{if $submoduleLink}}
<a class="muted" href="{{$submoduleLink.RepoWebLink}}">{{$entry.Name}}</a> <span class="at">@</span> <a href="{{$submoduleLink.CommitWebLink}}">{{ShortSha $submoduleFile.RefID}}</a> <a class="entry-name" href="{{$submoduleLink.RepoWebLink}}" title="{{$entry.Name}}">{{$entry.Name}}</a>
@ <a class="text primary" href="{{$submoduleLink.CommitWebLink}}">{{ShortSha $submoduleFile.RefID}}</a>
{{else}} {{else}}
{{$entry.Name}} <span class="at">@</span> {{ShortSha $submoduleFile.RefID}} <span class="entry-name" title="{{$entry.Name}}">{{$entry.Name}}</span>
@ {{ShortSha $submoduleFile.RefID}}
{{end}} {{end}}
{{else}} {{else}}
{{if $entry.IsDir}} {{if $entry.IsDir}}
{{$subJumpablePathName := $entry.GetSubJumpablePathName}} {{$subJumpablePathName := $entry.GetSubJumpablePathName}}
<a class="muted" href="{{$.TreeLink}}/{{PathEscapeSegments $subJumpablePathName}}" title="{{$subJumpablePathName}}"> <a class="entry-name" href="{{$.TreeLink}}/{{PathEscapeSegments $subJumpablePathName}}" title="{{$subJumpablePathName}}">
{{$subJumpablePathFields := StringUtils.Split $subJumpablePathName "/"}} {{$subJumpablePathFields := StringUtils.Split $subJumpablePathName "/"}}
{{$subJumpablePathFieldLast := (Eval (len $subJumpablePathFields) "-" 1)}} {{$subJumpablePathFieldLast := (Eval (len $subJumpablePathFields) "-" 1)}}
{{if eq $subJumpablePathFieldLast 0}} {{if eq $subJumpablePathFieldLast 0}}
@@ -37,7 +39,7 @@
{{end}} {{end}}
</a> </a>
{{else}} {{else}}
<a class="muted" href="{{$.TreeLink}}/{{PathEscapeSegments $entry.Name}}" title="{{$entry.Name}}">{{$entry.Name}}</a> <a class="entry-name" href="{{$.TreeLink}}/{{PathEscapeSegments $entry.Name}}" title="{{$entry.Name}}">{{$entry.Name}}</a>
{{end}} {{end}}
{{end}} {{end}}
</div> </div>
+14 -4
View File
@@ -14,10 +14,6 @@
} }
} }
#repo-files-table .repo-file-cell.name .svg {
margin-right: 2px;
}
#repo-files-table .svg.octicon-file-directory-fill, #repo-files-table .svg.octicon-file-directory-fill,
#repo-files-table .svg.octicon-file-submodule { #repo-files-table .svg.octicon-file-submodule {
color: var(--color-primary); color: var(--color-primary);
@@ -70,11 +66,25 @@
} }
#repo-files-table .repo-file-cell.name { #repo-files-table .repo-file-cell.name {
display: flex;
align-items: center;
gap: 0.5em;
overflow: hidden;
}
#repo-files-table .repo-file-cell.name > a,
#repo-files-table .repo-file-cell.name > span {
flex-shrink: 0;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
#repo-files-table .repo-file-cell.name .entry-name {
flex-shrink: 1;
min-width: 3em;
}
@media (max-width: 767.98px) { @media (max-width: 767.98px) {
#repo-files-table .repo-file-cell.name { #repo-files-table .repo-file-cell.name {
max-width: 35vw; max-width: 35vw;
@@ -1130,7 +1130,11 @@ $.fn.dropdown = function(parameters) {
icon: { icon: {
click: function(event) { click: function(event) {
iconClicked=true; iconClicked=true;
if(module.has.search()) { // GITEA-PATCH: official dropdown doesn't support the search input in menu
// so we need to make the menu could be shown when the search input is in menu and user clicks the icon
const searchInputInMenu = Boolean($menu.find('.search > input').length);
if(module.has.search() && !searchInputInMenu) {
// the search input is in the dropdown element (but not in the popup menu), try to focus it
if(!module.is.active()) { if(!module.is.active()) {
if(settings.showOnFocus){ if(settings.showOnFocus){
module.focusSearch(); module.focusSearch();
@@ -20,7 +20,7 @@ test('toAbsoluteLocaleDate', () => {
// test different timezone // test different timezone
const oldTZ = process.env.TZ; const oldTZ = process.env.TZ;
process.env.TZ = 'America/New_York'; process.env.TZ = 'America/New_York';
expect(new Date('2024-03-15').toLocaleString()).toEqual('3/14/2024, 8:00:00 PM'); expect(new Date('2024-03-15').toLocaleString('en-US')).toEqual('3/14/2024, 8:00:00 PM');
expect(toAbsoluteLocaleDate('2024-03-15')).toEqual('3/15/2024, 12:00:00 AM'); expect(toAbsoluteLocaleDate('2024-03-15', 'en-US')).toEqual('3/15/2024, 12:00:00 AM');
process.env.TZ = oldTZ; process.env.TZ = oldTZ;
}); });