1
0
mirror of https://github.com/go-gitea/gitea.git synced 2024-11-04 08:17:24 -05:00

Merge branch 'main' into lunny/fix_bot_automerge

This commit is contained in:
Lunny Xiao 2024-10-21 12:38:02 -07:00
commit adbda0636d
198 changed files with 5771 additions and 677 deletions

View File

@ -179,6 +179,7 @@ TEST_PGSQL_DBNAME ?= testgitea
TEST_PGSQL_USERNAME ?= postgres
TEST_PGSQL_PASSWORD ?= postgres
TEST_PGSQL_SCHEMA ?= gtestschema
TEST_MINIO_ENDPOINT ?= minio:9000
TEST_MSSQL_HOST ?= mssql:1433
TEST_MSSQL_DBNAME ?= gitea
TEST_MSSQL_USERNAME ?= sa
@ -574,6 +575,7 @@ generate-ini-pgsql:
-e 's|{{TEST_PGSQL_USERNAME}}|${TEST_PGSQL_USERNAME}|g' \
-e 's|{{TEST_PGSQL_PASSWORD}}|${TEST_PGSQL_PASSWORD}|g' \
-e 's|{{TEST_PGSQL_SCHEMA}}|${TEST_PGSQL_SCHEMA}|g' \
-e 's|{{TEST_MINIO_ENDPOINT}}|${TEST_MINIO_ENDPOINT}|g' \
-e 's|{{REPO_TEST_DIR}}|${REPO_TEST_DIR}|g' \
-e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
-e 's|{{TEST_TYPE}}|$(or $(TEST_TYPE),integration)|g' \

View File

@ -592,8 +592,9 @@ Gitea or set your environment appropriately.`, "")
// S: ... ...
// S: flush-pkt
hookOptions := private.HookOptions{
UserName: pusherName,
UserID: pusherID,
UserName: pusherName,
UserID: pusherID,
GitPushOptions: make(map[string]string),
}
hookOptions.OldCommitIDs = make([]string, 0, hookBatchSize)
hookOptions.NewCommitIDs = make([]string, 0, hookBatchSize)
@ -618,8 +619,6 @@ Gitea or set your environment appropriately.`, "")
hookOptions.RefFullNames = append(hookOptions.RefFullNames, git.RefName(t[2]))
}
hookOptions.GitPushOptions = make(map[string]string)
if hasPushOptions {
for {
rs, err = readPktLine(ctx, reader, pktLineTypeUnknow)
@ -630,11 +629,7 @@ Gitea or set your environment appropriately.`, "")
if rs.Type == pktLineTypeFlush {
break
}
kv := strings.SplitN(string(rs.Data), "=", 2)
if len(kv) == 2 {
hookOptions.GitPushOptions[kv[0]] = kv[1]
}
hookOptions.GitPushOptions.AddFromKeyValue(string(rs.Data))
}
}

View File

@ -64,6 +64,11 @@ func main() {
Value: "",
Usage: "Forked user name on Github",
},
&cli.StringFlag{
Name: "gh-access-token",
Value: "",
Usage: "Access token for GitHub api request",
},
&cli.BoolFlag{
Name: "no-fetch",
Usage: "Set this flag to prevent fetch of remote branches",
@ -169,9 +174,10 @@ func runBackport(c *cli.Context) error {
fmt.Printf("* Backporting %s to %s as %s\n", pr, localReleaseBranch, backportBranch)
sha := c.String("cherry-pick")
accessToken := c.String("gh-access-token")
if sha == "" {
var err error
sha, err = determineSHAforPR(ctx, pr)
sha, err = determineSHAforPR(ctx, pr, accessToken)
if err != nil {
return err
}
@ -427,13 +433,16 @@ func readVersion() string {
return strings.Join(split[:2], ".")
}
func determineSHAforPR(ctx context.Context, prStr string) (string, error) {
func determineSHAforPR(ctx context.Context, prStr, accessToken string) (string, error) {
prNum, err := strconv.Atoi(prStr)
if err != nil {
return "", err
}
client := github.NewClient(http.DefaultClient)
if accessToken != "" {
client = client.WithAuthToken(accessToken)
}
pr, _, err := client.PullRequests.Get(ctx, "go-gitea", "gitea", prNum)
if err != nil {

View File

@ -1507,6 +1507,8 @@ LEVEL = Info
;; - manage_gpg_keys: a user cannot configure gpg keys
;; - manage_mfa: a user cannot configure mfa devices
;; - manage_credentials: a user cannot configure emails, passwords, or openid
;; - change_username: a user cannot change their username
;; - change_full_name: a user cannot change their full name
;;EXTERNAL_USER_DISABLE_FEATURES =
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

4
go.mod
View File

@ -54,7 +54,7 @@ require (
github.com/go-chi/chi/v5 v5.0.13
github.com/go-chi/cors v1.2.1
github.com/go-co-op/gocron v1.37.0
github.com/go-enry/go-enry/v2 v2.8.8
github.com/go-enry/go-enry/v2 v2.9.1
github.com/go-git/go-billy/v5 v5.5.0
github.com/go-git/go-git/v5 v5.12.0
github.com/go-ldap/ldap/v3 v3.4.6
@ -331,7 +331,7 @@ replace github.com/hashicorp/go-version => github.com/6543/go-version v1.3.1
replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0
replace github.com/nektos/act => gitea.com/gitea/act v0.259.1
replace github.com/nektos/act => gitea.com/gitea/act v0.261.3
replace github.com/charmbracelet/git-lfs-transfer => gitea.com/gitea/git-lfs-transfer v0.2.0

8
go.sum
View File

@ -16,8 +16,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:cliQ4HHsCo6xi2oWZYKWW4bly/Ory9FuTpFPRxj/mAg=
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs=
gitea.com/gitea/act v0.259.1 h1:8GG1o/xtUHl3qjn5f0h/2FXrT5ubBn05TJOM5ry+FBw=
gitea.com/gitea/act v0.259.1/go.mod h1:UxZWRYqQG2Yj4+4OqfGWW5a3HELwejyWFQyU7F1jUD8=
gitea.com/gitea/act v0.261.3 h1:BhiYpGJQKGq0XMYYICCYAN4KnsEWHyLbA6dxhZwFcV4=
gitea.com/gitea/act v0.261.3/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok=
gitea.com/gitea/git-lfs-transfer v0.2.0 h1:baHaNoBSRaeq/xKayEXwiDQtlIjps4Ac/Ll4KqLMB40=
gitea.com/gitea/git-lfs-transfer v0.2.0/go.mod h1:UrXUCm3xLQkq15fu7qlXHUMlrhdlXHoi13KH2Dfiits=
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed h1:EZZBtilMLSZNWtHHcgq2mt6NSGhJSZBuduAlinMEmso=
@ -315,8 +315,8 @@ github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0=
github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY=
github.com/go-enry/go-enry/v2 v2.8.8 h1:EhfxWpw4DQ3WEFB1Y77X8vKqZL0D0EDUUWYDUAIv9/4=
github.com/go-enry/go-enry/v2 v2.8.8/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8=
github.com/go-enry/go-enry/v2 v2.9.1 h1:G9iDteJ/Mc0F4Di5NeQknf83R2OkRbwY9cAYmcqVG6U=
github.com/go-enry/go-enry/v2 v2.9.1/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8=
github.com/go-enry/go-oniguruma v1.2.1 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo=
github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4=
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=

View File

@ -69,7 +69,7 @@ func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPa
OwnerID: t.OwnerID,
CommitSHA: t.CommitSHA,
Status: int64(ArtifactStatusUploadPending),
ExpiredUnix: timeutil.TimeStamp(time.Now().Unix() + 3600*24*expiredDays),
ExpiredUnix: timeutil.TimeStamp(time.Now().Unix() + timeutil.Day*expiredDays),
}
if _, err := db.GetEngine(ctx).Insert(artifact); err != nil {
return nil, err
@ -78,6 +78,13 @@ func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPa
} else if err != nil {
return nil, err
}
if _, err := db.GetEngine(ctx).ID(artifact.ID).Cols("expired_unix").Update(&ActionArtifact{
ExpiredUnix: timeutil.TimeStamp(time.Now().Unix() + timeutil.Day*expiredDays),
}); err != nil {
return nil, err
}
return artifact, nil
}

View File

@ -712,3 +712,24 @@
type: 3
config: "{\"IgnoreWhitespaceConflicts\":false,\"AllowMerge\":true,\"AllowRebase\":true,\"AllowRebaseMerge\":true,\"AllowSquash\":true}"
created_unix: 946684810
-
id: 108
repo_id: 62
type: 1
config: "{}"
created_unix: 946684810
-
id: 109
repo_id: 62
type: 2
config: "{\"EnableTimetracker\":true,\"AllowOnlyContributorsToTrackTime\":true}"
created_unix: 946684810
-
id: 110
repo_id: 62
type: 3
config: "{\"IgnoreWhitespaceConflicts\":false,\"AllowMerge\":true,\"AllowRebase\":true,\"AllowRebaseMerge\":true,\"AllowSquash\":true}"
created_unix: 946684810

View File

@ -1768,3 +1768,34 @@
size: 0
is_fsck_enabled: true
close_issues_via_commit_in_any_branch: false
-
id: 62
owner_id: 42
owner_name: org42
lower_name: search-by-path
name: search-by-path
default_branch: master
num_watches: 0
num_stars: 0
num_forks: 0
num_issues: 0
num_closed_issues: 0
num_pulls: 0
num_closed_pulls: 0
num_milestones: 0
num_closed_milestones: 0
num_projects: 0
num_closed_projects: 0
is_private: false
is_empty: false
is_archived: false
is_mirror: false
status: 0
is_fork: false
fork_id: 0
is_template: false
template_id: 0
size: 0
is_fsck_enabled: true
close_issues_via_commit_in_any_branch: false

View File

@ -1517,3 +1517,40 @@
repo_admin_change_team_access: false
theme: ""
keep_activity_private: false
-
id: 42
lower_name: org42
name: org42
full_name: Org42
email: org42@example.com
keep_email_private: false
email_notifications_preference: onmention
passwd: ZogKvWdyEx:password
passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: org42
type: 1
salt: ZogKvWdyEx
max_repo_creation: -1
is_active: false
is_admin: false
is_restricted: false
allow_git_hook: false
allow_import_local: false
allow_create_organization: true
prohibit_login: false
avatar: avatar42
avatar_email: org42@example.com
use_custom_avatar: false
num_followers: 0
num_following: 0
num_stars: 0
num_repos: 1
num_teams: 0
num_members: 0
visibility: 0
repo_admin_change_team_access: false
theme: ""
keep_activity_private: false

View File

@ -26,6 +26,7 @@ type PullRequestsOptions struct {
SortType string
Labels []int64
MilestoneID int64
PosterID int64
}
func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullRequestsOptions) *xorm.Session {
@ -46,6 +47,10 @@ func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullR
sess.And("issue.milestone_id=?", opts.MilestoneID)
}
if opts.PosterID > 0 {
sess.And("issue.poster_id=?", opts.PosterID)
}
return sess
}

View File

@ -138,12 +138,12 @@ func getTestCases() []struct {
{
name: "AllPublic/PublicRepositoriesOfUserIncludingCollaborative",
opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, AllPublic: true, Template: optional.Some(false)},
count: 33,
count: 34,
},
{
name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative",
opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true, AllLimited: true, Template: optional.Some(false)},
count: 38,
count: 39,
},
{
name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborativeByName",
@ -158,7 +158,7 @@ func getTestCases() []struct {
{
name: "AllPublic/PublicRepositoriesOfOrganization",
opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, AllPublic: true, Collaborate: optional.Some(false), Template: optional.Some(false)},
count: 33,
count: 34,
},
{
name: "AllTemplates",

View File

@ -65,7 +65,19 @@ func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Sess
builder.Like{"LOWER(full_name)", lowerKeyword},
)
if opts.SearchByEmail {
keywordCond = keywordCond.Or(builder.Like{"LOWER(email)", lowerKeyword})
var emailCond builder.Cond
emailCond = builder.Like{"LOWER(email)", lowerKeyword}
if opts.Actor == nil {
emailCond = emailCond.And(builder.Eq{"keep_email_private": false})
} else if !opts.Actor.IsAdmin {
emailCond = emailCond.And(
builder.Or(
builder.Eq{"keep_email_private": false},
builder.Eq{"id": opts.Actor.ID},
),
)
}
keywordCond = keywordCond.Or(emailCond)
}
cond = cond.And(keywordCond)

View File

@ -408,6 +408,10 @@ func (u *User) IsIndividual() bool {
return u.Type == UserTypeIndividual
}
func (u *User) IsUser() bool {
return u.Type == UserTypeIndividual || u.Type == UserTypeBot
}
// IsBot returns whether or not the user is of type bot
func (u *User) IsBot() bool {
return u.Type == UserTypeBot
@ -561,42 +565,43 @@ var (
".",
"..",
".well-known",
"admin",
"api",
"assets",
"attachments",
"avatar",
"avatars",
"captcha",
"commits",
"debug",
"error",
"explore",
"favicon.ico",
"ghost",
"issues",
"login",
"manifest.json",
"metrics",
"milestones",
"new",
"notifications",
"org",
"pulls",
"raw",
"repo",
"api", // gitea api
"metrics", // prometheus metrics api
"v2", // container registry api
"assets", // static asset files
"attachments", // issue attachments
"avatar", // avatar by email hash
"avatars", // user avatars by file name
"repo-avatars",
"robots.txt",
"search",
"serviceworker.js",
"ssh_info",
"captcha",
"login", // oauth2 login
"org", // org create/manage, or "/org/{org}", BUT if an org is named as "invite" then it goes wrong
"repo", // repo create/migrate, etc
"user", // user login/activate/settings, etc
"explore",
"issues",
"pulls",
"milestones",
"notifications",
"favicon.ico",
"manifest.json", // web app manifests
"robots.txt", // search engine robots
"sitemap.xml", // search engine sitemap
"ssh_info", // agit info
"swagger.v1.json",
"user",
"v2",
"gitea-actions",
"ghost", // reserved name for deleted users (id: -1)
"gitea-actions", // gitea builtin user (id: -2)
}
// DON'T ADD ANY NEW STUFF, WE SOLVE THIS WITH `/user/{obj}` PATHS!
// These names are reserved for user accounts: user's keys, user's rss feed, user's avatar, etc.
// DO NOT add any new stuff! The paths with these names are processed by `/{username}` handler (UsernameSubRoute) manually.
reservedUserPatterns = []string{"*.keys", "*.gpg", "*.rss", "*.atom", "*.png"}
)

View File

@ -92,7 +92,10 @@ func TestSearchUsers(t *testing.T) {
testOrgSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 4, PageSize: 2}},
[]int64{26, 41})
testOrgSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 5, PageSize: 2}},
testOrgSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 5, PageSize: 2}},
[]int64{42})
testOrgSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 6, PageSize: 2}},
[]int64{})
// test users

View File

@ -111,12 +111,12 @@ func SetExecutablePath(path string) error {
func ensureGitVersion() error {
if !DefaultFeatures().CheckVersionAtLeast(RequiredVersion) {
moreHint := "get git: https://git-scm.com/download/"
moreHint := "get git: https://git-scm.com/downloads"
if runtime.GOOS == "linux" {
// there are a lot of CentOS/RHEL users using old git, so we add a special hint for them
if _, err := os.Stat("/etc/redhat-release"); err == nil {
// ius.io is the recommended official(git-scm.com) method to install git
moreHint = "get git: https://git-scm.com/download/linux and https://ius.io"
moreHint = "get git: https://git-scm.com/downloads/linux and https://ius.io"
}
}
return fmt.Errorf("installed git version %q is not supported, Gitea requires git version >= %q, %s", DefaultFeatures().gitVersion.Original(), RequiredVersion, moreHint)

View File

@ -17,6 +17,7 @@ import (
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
path_filter "code.gitea.io/gitea/modules/indexer/code/bleve/token/path"
"code.gitea.io/gitea/modules/indexer/code/internal"
indexer_internal "code.gitea.io/gitea/modules/indexer/internal"
inner_bleve "code.gitea.io/gitea/modules/indexer/internal/bleve"
@ -53,6 +54,7 @@ type RepoIndexerData struct {
RepoID int64
CommitID string
Content string
Filename string
Language string
UpdatedAt time.Time
}
@ -64,8 +66,10 @@ func (d *RepoIndexerData) Type() string {
const (
repoIndexerAnalyzer = "repoIndexerAnalyzer"
filenameIndexerAnalyzer = "filenameIndexerAnalyzer"
filenameIndexerTokenizer = "filenameIndexerTokenizer"
repoIndexerDocType = "repoIndexerDocType"
repoIndexerLatestVersion = 6
repoIndexerLatestVersion = 7
)
// generateBleveIndexMapping generates a bleve index mapping for the repo indexer
@ -79,6 +83,11 @@ func generateBleveIndexMapping() (mapping.IndexMapping, error) {
textFieldMapping.IncludeInAll = false
docMapping.AddFieldMappingsAt("Content", textFieldMapping)
fileNamedMapping := bleve.NewTextFieldMapping()
fileNamedMapping.IncludeInAll = false
fileNamedMapping.Analyzer = filenameIndexerAnalyzer
docMapping.AddFieldMappingsAt("Filename", fileNamedMapping)
termFieldMapping := bleve.NewTextFieldMapping()
termFieldMapping.IncludeInAll = false
termFieldMapping.Analyzer = analyzer_keyword.Name
@ -90,6 +99,7 @@ func generateBleveIndexMapping() (mapping.IndexMapping, error) {
docMapping.AddFieldMappingsAt("UpdatedAt", timeFieldMapping)
mapping := bleve.NewIndexMapping()
if err := addUnicodeNormalizeTokenFilter(mapping); err != nil {
return nil, err
} else if err := mapping.AddCustomAnalyzer(repoIndexerAnalyzer, map[string]any{
@ -100,6 +110,16 @@ func generateBleveIndexMapping() (mapping.IndexMapping, error) {
}); err != nil {
return nil, err
}
if err := mapping.AddCustomAnalyzer(filenameIndexerAnalyzer, map[string]any{
"type": analyzer_custom.Name,
"char_filters": []string{},
"tokenizer": unicode.Name,
"token_filters": []string{unicodeNormalizeName, path_filter.Name, lowercase.Name},
}); err != nil {
return nil, err
}
mapping.DefaultAnalyzer = repoIndexerAnalyzer
mapping.AddDocumentMapping(repoIndexerDocType, docMapping)
mapping.AddDocumentMapping("_all", bleve.NewDocumentDisabledMapping())
@ -174,6 +194,7 @@ func (b *Indexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserErro
return batch.Index(id, &RepoIndexerData{
RepoID: repo.ID,
CommitID: commitSha,
Filename: update.Filename,
Content: string(charset.ToUTF8DropErrors(fileContents, charset.ConvertOpts{})),
Language: analyze.GetCodeLanguage(update.Filename, fileContents),
UpdatedAt: time.Now().UTC(),
@ -240,14 +261,19 @@ func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int
keywordQuery query.Query
)
phraseQuery := bleve.NewMatchPhraseQuery(opts.Keyword)
phraseQuery.FieldVal = "Content"
phraseQuery.Analyzer = repoIndexerAnalyzer
keywordQuery = phraseQuery
pathQuery := bleve.NewPrefixQuery(strings.ToLower(opts.Keyword))
pathQuery.FieldVal = "Filename"
pathQuery.SetBoost(10)
contentQuery := bleve.NewMatchQuery(opts.Keyword)
contentQuery.FieldVal = "Content"
if opts.IsKeywordFuzzy {
phraseQuery.Fuzziness = inner_bleve.GuessFuzzinessByKeyword(opts.Keyword)
contentQuery.Fuzziness = inner_bleve.GuessFuzzinessByKeyword(opts.Keyword)
}
keywordQuery = bleve.NewDisjunctionQuery(contentQuery, pathQuery)
if len(opts.RepoIDs) > 0 {
repoQueries := make([]query.Query, 0, len(opts.RepoIDs))
for _, repoID := range opts.RepoIDs {
@ -277,7 +303,7 @@ func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int
from, pageSize := opts.GetSkipTake()
searchRequest := bleve.NewSearchRequestOptions(indexerQuery, pageSize, from, false)
searchRequest.Fields = []string{"Content", "RepoID", "Language", "CommitID", "UpdatedAt"}
searchRequest.Fields = []string{"Content", "Filename", "RepoID", "Language", "CommitID", "UpdatedAt"}
searchRequest.IncludeLocations = true
if len(opts.Language) == 0 {
@ -307,6 +333,10 @@ func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int
endIndex = locationEnd
}
}
if len(hit.Locations["Filename"]) > 0 {
startIndex, endIndex = internal.FilenameMatchIndexPos(hit.Fields["Content"].(string))
}
language := hit.Fields["Language"].(string)
var updatedUnix timeutil.TimeStamp
if t, err := time.Parse(time.RFC3339, hit.Fields["UpdatedAt"].(string)); err == nil {

View File

@ -0,0 +1,101 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package path
import (
"slices"
"strings"
"github.com/blevesearch/bleve/v2/analysis"
"github.com/blevesearch/bleve/v2/registry"
)
const (
Name = "gitea/path"
)
type TokenFilter struct{}
func NewTokenFilter() *TokenFilter {
return &TokenFilter{}
}
func TokenFilterConstructor(config map[string]any, cache *registry.Cache) (analysis.TokenFilter, error) {
return NewTokenFilter(), nil
}
func (s *TokenFilter) Filter(input analysis.TokenStream) analysis.TokenStream {
if len(input) == 1 {
// if there is only one token, we dont need to generate the reversed chain
return generatePathTokens(input, false)
}
normal := generatePathTokens(input, false)
reversed := generatePathTokens(input, true)
return append(normal, reversed...)
}
// Generates path tokens from the input tokens.
// This mimics the behavior of the path hierarchy tokenizer in ES. It takes the input tokens and combine them, generating a term for each component
// in tree (e.g., foo/bar/baz.md will generate foo, foo/bar, and foo/bar/baz.md).
//
// If the reverse flag is set, the order of the tokens is reversed (the same input will generate baz.md, baz.md/bar, baz.md/bar/foo). This is useful
// to efficiently search for filenames without supplying the fullpath.
func generatePathTokens(input analysis.TokenStream, reversed bool) analysis.TokenStream {
terms := make([]string, 0, len(input))
longestTerm := 0
if reversed {
slices.Reverse(input)
}
for i := 0; i < len(input); i++ {
var sb strings.Builder
sb.WriteString(string(input[0].Term))
for j := 1; j < i; j++ {
sb.WriteString("/")
sb.WriteString(string(input[j].Term))
}
term := sb.String()
if longestTerm < len(term) {
longestTerm = len(term)
}
terms = append(terms, term)
}
output := make(analysis.TokenStream, 0, len(terms))
for _, term := range terms {
var start, end int
if reversed {
start = 0
end = len(term)
} else {
start = longestTerm - len(term)
end = longestTerm
}
token := analysis.Token{
Position: 1,
Start: start,
End: end,
Type: analysis.AlphaNumeric,
Term: []byte(term),
}
output = append(output, &token)
}
return output
}
func init() {
registry.RegisterTokenFilter(Name, TokenFilterConstructor)
}

View File

@ -0,0 +1,76 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package path
import (
"fmt"
"testing"
"github.com/blevesearch/bleve/v2/analysis"
"github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode"
"github.com/stretchr/testify/assert"
)
type Scenario struct {
Input string
Tokens []string
}
func TestTokenFilter(t *testing.T) {
scenarios := []struct {
Input string
Terms []string
}{
{
Input: "Dockerfile",
Terms: []string{"Dockerfile"},
},
{
Input: "Dockerfile.rootless",
Terms: []string{"Dockerfile.rootless"},
},
{
Input: "a/b/c/Dockerfile.rootless",
Terms: []string{"a", "a/b", "a/b/c", "a/b/c/Dockerfile.rootless", "Dockerfile.rootless", "Dockerfile.rootless/c", "Dockerfile.rootless/c/b", "Dockerfile.rootless/c/b/a"},
},
{
Input: "",
Terms: []string{},
},
}
for _, scenario := range scenarios {
t.Run(fmt.Sprintf("ensure terms of '%s'", scenario.Input), func(t *testing.T) {
terms := extractTerms(scenario.Input)
assert.Len(t, terms, len(scenario.Terms))
for _, term := range terms {
assert.Contains(t, scenario.Terms, term)
}
})
}
}
func extractTerms(input string) []string {
tokens := tokenize(input)
filteredTokens := filter(tokens)
terms := make([]string, 0, len(filteredTokens))
for _, token := range filteredTokens {
terms = append(terms, string(token.Term))
}
return terms
}
func filter(input analysis.TokenStream) analysis.TokenStream {
filter := NewTokenFilter()
return filter.Filter(input)
}
func tokenize(input string) analysis.TokenStream {
tokenizer := unicode.NewUnicodeTokenizer()
return tokenizer.Tokenize([]byte(input))
}

View File

@ -20,6 +20,7 @@ import (
indexer_internal "code.gitea.io/gitea/modules/indexer/internal"
inner_elasticsearch "code.gitea.io/gitea/modules/indexer/internal/elasticsearch"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/typesniffer"
@ -29,7 +30,7 @@ import (
)
const (
esRepoIndexerLatestVersion = 1
esRepoIndexerLatestVersion = 2
// multi-match-types, currently only 2 types are used
// Reference: https://www.elastic.co/guide/en/elasticsearch/reference/7.0/query-dsl-multi-match-query.html#multi-match-types
esMultiMatchTypeBestFields = "best_fields"
@ -56,12 +57,50 @@ func NewIndexer(url, indexerName string) *Indexer {
const (
defaultMapping = `{
"settings": {
"analysis": {
"analyzer": {
"filename_path_analyzer": {
"tokenizer": "path_tokenizer"
},
"reversed_filename_path_analyzer": {
"tokenizer": "reversed_path_tokenizer"
}
},
"tokenizer": {
"path_tokenizer": {
"type": "path_hierarchy",
"delimiter": "/"
},
"reversed_path_tokenizer": {
"type": "path_hierarchy",
"delimiter": "/",
"reverse": true
}
}
}
},
"mappings": {
"properties": {
"repo_id": {
"type": "long",
"index": true
},
"filename": {
"type": "text",
"term_vector": "with_positions_offsets",
"index": true,
"fields": {
"path": {
"type": "text",
"analyzer": "reversed_filename_path_analyzer"
},
"path_reversed": {
"type": "text",
"analyzer": "filename_path_analyzer"
}
}
},
"content": {
"type": "text",
"term_vector": "with_positions_offsets",
@ -135,6 +174,7 @@ func (b *Indexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserErro
Id(id).
Doc(map[string]any{
"repo_id": repo.ID,
"filename": update.Filename,
"content": string(charset.ToUTF8DropErrors(fileContents, charset.ConvertOpts{})),
"commit_id": sha,
"language": analyze.GetCodeLanguage(update.Filename, fileContents),
@ -197,19 +237,44 @@ func (b *Indexer) Index(ctx context.Context, repo *repo_model.Repository, sha st
return nil
}
// Delete deletes indexes by ids
// Delete entries by repoId
func (b *Indexer) Delete(ctx context.Context, repoID int64) error {
if err := b.doDelete(ctx, repoID); err != nil {
// Maybe there is a conflict during the delete operation, so we should retry after a refresh
log.Warn("Deletion of entries of repo %v within index %v was erroneus. Trying to refresh index before trying again", repoID, b.inner.VersionedIndexName(), err)
if err := b.refreshIndex(ctx); err != nil {
return err
}
if err := b.doDelete(ctx, repoID); err != nil {
log.Error("Could not delete entries of repo %v within index %v", repoID, b.inner.VersionedIndexName())
return err
}
}
return nil
}
func (b *Indexer) refreshIndex(ctx context.Context) error {
if _, err := b.inner.Client.Refresh(b.inner.VersionedIndexName()).Do(ctx); err != nil {
log.Error("Error while trying to refresh index %v", b.inner.VersionedIndexName(), err)
return err
}
return nil
}
// Delete entries by repoId
func (b *Indexer) doDelete(ctx context.Context, repoID int64) error {
_, err := b.inner.Client.DeleteByQuery(b.inner.VersionedIndexName()).
Query(elastic.NewTermsQuery("repo_id", repoID)).
Do(ctx)
return err
}
// indexPos find words positions for start and the following end on content. It will
// contentMatchIndexPos find words positions for start and the following end on content. It will
// return the beginning position of the first start and the ending position of the
// first end following the start string.
// If not found any of the positions, it will return -1, -1.
func indexPos(content, start, end string) (int, int) {
func contentMatchIndexPos(content, start, end string) (int, int) {
startIdx := strings.Index(content, start)
if startIdx < 0 {
return -1, -1
@ -218,22 +283,29 @@ func indexPos(content, start, end string) (int, int) {
if endIdx < 0 {
return -1, -1
}
return startIdx, startIdx + len(start) + endIdx + len(end)
return startIdx, (startIdx + len(start) + endIdx + len(end)) - 9 // remove the length <em></em> since we give Content the original data
}
func convertResult(searchResult *elastic.SearchResult, kw string, pageSize int) (int64, []*internal.SearchResult, []*internal.SearchResultLanguages, error) {
hits := make([]*internal.SearchResult, 0, pageSize)
for _, hit := range searchResult.Hits.Hits {
repoID, fileName := internal.ParseIndexerID(hit.Id)
res := make(map[string]any)
if err := json.Unmarshal(hit.Source, &res); err != nil {
return 0, nil, nil, err
}
// FIXME: There is no way to get the position the keyword on the content currently on the same request.
// So we get it from content, this may made the query slower. See
// https://discuss.elastic.co/t/fetching-position-of-keyword-in-matched-document/94291
var startIndex, endIndex int
c, ok := hit.Highlight["content"]
if ok && len(c) > 0 {
if c, ok := hit.Highlight["filename"]; ok && len(c) > 0 {
startIndex, endIndex = internal.FilenameMatchIndexPos(res["content"].(string))
} else if c, ok := hit.Highlight["content"]; ok && len(c) > 0 {
// FIXME: Since the highlighting content will include <em> and </em> for the keywords,
// now we should find the positions. But how to avoid html content which contains the
// <em> and </em> tags? If elastic search has handled that?
startIndex, endIndex = indexPos(c[0], "<em>", "</em>")
startIndex, endIndex = contentMatchIndexPos(c[0], "<em>", "</em>")
if startIndex == -1 {
panic(fmt.Sprintf("1===%s,,,%#v,,,%s", kw, hit.Highlight, c[0]))
}
@ -241,12 +313,6 @@ func convertResult(searchResult *elastic.SearchResult, kw string, pageSize int)
panic(fmt.Sprintf("2===%#v", hit.Highlight))
}
repoID, fileName := internal.ParseIndexerID(hit.Id)
res := make(map[string]any)
if err := json.Unmarshal(hit.Source, &res); err != nil {
return 0, nil, nil, err
}
language := res["language"].(string)
hits = append(hits, &internal.SearchResult{
@ -257,7 +323,7 @@ func convertResult(searchResult *elastic.SearchResult, kw string, pageSize int)
UpdatedUnix: timeutil.TimeStamp(res["updated_at"].(float64)),
Language: language,
StartIndex: startIndex,
EndIndex: endIndex - 9, // remove the length <em></em> since we give Content the original data
EndIndex: endIndex,
Color: enry.GetColor(language),
})
}
@ -289,7 +355,10 @@ func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int
searchType = esMultiMatchTypeBestFields
}
kwQuery := elastic.NewMultiMatchQuery(opts.Keyword, "content").Type(searchType)
kwQuery := elastic.NewBoolQuery().Should(
elastic.NewMultiMatchQuery(opts.Keyword, "content").Type(searchType),
elastic.NewMultiMatchQuery(opts.Keyword, "filename^10").Type(esMultiMatchTypePhrasePrefix),
)
query := elastic.NewBoolQuery()
query = query.Must(kwQuery)
if len(opts.RepoIDs) > 0 {
@ -315,6 +384,7 @@ func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int
Highlight(
elastic.NewHighlight().
Field("content").
Field("filename").
NumOfFragments(0). // return all highting content on fragments
HighlighterType("fvh"),
).
@ -347,6 +417,7 @@ func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int
Highlight(
elastic.NewHighlight().
Field("content").
Field("filename").
NumOfFragments(0). // return all highting content on fragments
HighlighterType("fvh"),
).

View File

@ -10,7 +10,7 @@ import (
)
func TestIndexPos(t *testing.T) {
startIdx, endIdx := indexPos("test index start and end", "start", "end")
startIdx, endIdx := contentMatchIndexPos("test index start and end", "start", "end")
assert.EqualValues(t, 11, startIdx)
assert.EqualValues(t, 24, endIdx)
assert.EqualValues(t, 15, endIdx)
}

View File

@ -6,6 +6,7 @@ package code
import (
"context"
"os"
"slices"
"testing"
"code.gitea.io/gitea/models/db"
@ -20,53 +21,166 @@ import (
_ "code.gitea.io/gitea/models/activities"
"github.com/stretchr/testify/assert"
_ "github.com/mattn/go-sqlite3"
)
type codeSearchResult struct {
Filename string
Content string
}
func TestMain(m *testing.M) {
unittest.MainTest(m)
}
func testIndexer(name string, t *testing.T, indexer internal.Indexer) {
t.Run(name, func(t *testing.T) {
var repoID int64 = 1
err := index(git.DefaultContext, indexer, repoID)
assert.NoError(t, err)
assert.NoError(t, setupRepositoryIndexes(git.DefaultContext, indexer))
keywords := []struct {
RepoIDs []int64
Keyword string
IDs []int64
Langs int
Results []codeSearchResult
}{
// Search for an exact match on the contents of a file
// This scenario yields a single result (the file README.md on the repo '1')
{
RepoIDs: nil,
Keyword: "Description",
IDs: []int64{repoID},
Langs: 1,
Results: []codeSearchResult{
{
Filename: "README.md",
Content: "# repo1\n\nDescription for repo1",
},
},
},
// Search for an exact match on the contents of a file within the repo '2'.
// This scenario yields no results
{
RepoIDs: []int64{2},
Keyword: "Description",
IDs: []int64{},
Langs: 0,
},
// Search for an exact match on the contents of a file
// This scenario yields a single result (the file README.md on the repo '1')
{
RepoIDs: nil,
Keyword: "repo1",
IDs: []int64{repoID},
Langs: 1,
Results: []codeSearchResult{
{
Filename: "README.md",
Content: "# repo1\n\nDescription for repo1",
},
},
},
// Search for an exact match on the contents of a file within the repo '2'.
// This scenario yields no results
{
RepoIDs: []int64{2},
Keyword: "repo1",
IDs: []int64{},
Langs: 0,
},
// Search for a non-existing term.
// This scenario yields no results
{
RepoIDs: nil,
Keyword: "non-exist",
IDs: []int64{},
Langs: 0,
},
// Search for an exact match on the contents of a file within the repo '62'.
// This scenario yields a single result (the file avocado.md on the repo '62')
{
RepoIDs: []int64{62},
Keyword: "pineaple",
Langs: 1,
Results: []codeSearchResult{
{
Filename: "avocado.md",
Content: "# repo1\n\npineaple pie of cucumber juice",
},
},
},
// Search for an exact match on the filename within the repo '62'.
// This scenario yields a single result (the file avocado.md on the repo '62')
{
RepoIDs: []int64{62},
Keyword: "avocado.md",
Langs: 1,
Results: []codeSearchResult{
{
Filename: "avocado.md",
Content: "# repo1\n\npineaple pie of cucumber juice",
},
},
},
// Search for an partial match on the filename within the repo '62'.
// This scenario yields a single result (the file avocado.md on the repo '62')
{
RepoIDs: []int64{62},
Keyword: "avo",
Langs: 1,
Results: []codeSearchResult{
{
Filename: "avocado.md",
Content: "# repo1\n\npineaple pie of cucumber juice",
},
},
},
// Search for matches on both the contents and the filenames within the repo '62'.
// This scenario yields two results: the first result is baed on the file (cucumber.md) while the second is based on the contents
{
RepoIDs: []int64{62},
Keyword: "cucumber",
Langs: 1,
Results: []codeSearchResult{
{
Filename: "cucumber.md",
Content: "Salad is good for your health",
},
{
Filename: "avocado.md",
Content: "# repo1\n\npineaple pie of cucumber juice",
},
},
},
// Search for matches on the filenames within the repo '62'.
// This scenario yields two results (both are based on filename, the first one is an exact match)
{
RepoIDs: []int64{62},
Keyword: "ham",
Langs: 1,
Results: []codeSearchResult{
{
Filename: "ham.md",
Content: "This is also not cheese",
},
{
Filename: "potato/ham.md",
Content: "This is not cheese",
},
},
},
// Search for matches on the contents of files within the repo '62'.
// This scenario yields two results (both are based on contents, the first one is an exact match where as the second is a 'fuzzy' one)
{
RepoIDs: []int64{62},
Keyword: "This is not cheese",
Langs: 1,
Results: []codeSearchResult{
{
Filename: "potato/ham.md",
Content: "This is not cheese",
},
{
Filename: "ham.md",
Content: "This is also not cheese",
},
},
},
}
for _, kw := range keywords {
@ -81,19 +195,37 @@ func testIndexer(name string, t *testing.T, indexer internal.Indexer) {
IsKeywordFuzzy: true,
})
assert.NoError(t, err)
assert.Len(t, kw.IDs, int(total))
assert.Len(t, langs, kw.Langs)
ids := make([]int64, 0, len(res))
for _, hit := range res {
ids = append(ids, hit.RepoID)
assert.EqualValues(t, "# repo1\n\nDescription for repo1", hit.Content)
hits := make([]codeSearchResult, 0, len(res))
if total > 0 {
assert.NotEmpty(t, kw.Results, "The given scenario does not provide any expected results")
}
for _, hit := range res {
hits = append(hits, codeSearchResult{
Filename: hit.Filename,
Content: hit.Content,
})
}
lastIndex := -1
for _, expected := range kw.Results {
index := slices.Index(hits, expected)
if index == -1 {
assert.Failf(t, "Result not found", "Expected %v in %v", expected, hits)
} else if lastIndex > index {
assert.Failf(t, "Result is out of order", "The order of %v within %v is wrong", expected, hits)
} else {
lastIndex = index
}
}
assert.EqualValues(t, kw.IDs, ids)
})
}
assert.NoError(t, indexer.Delete(context.Background(), repoID))
assert.NoError(t, tearDownRepositoryIndexes(indexer))
})
}
@ -136,3 +268,25 @@ func TestESIndexAndSearch(t *testing.T) {
testIndexer("elastic_search", t, indexer)
}
func setupRepositoryIndexes(ctx context.Context, indexer internal.Indexer) error {
for _, repoID := range repositoriesToSearch() {
if err := index(ctx, indexer, repoID); err != nil {
return err
}
}
return nil
}
func tearDownRepositoryIndexes(indexer internal.Indexer) error {
for _, repoID := range repositoriesToSearch() {
if err := indexer.Delete(context.Background(), repoID); err != nil {
return err
}
}
return nil
}
func repositoriesToSearch() []int64 {
return []int64{1, 62}
}

View File

@ -10,6 +10,10 @@ import (
"code.gitea.io/gitea/modules/log"
)
const (
filenameMatchNumberOfLines = 7 // Copied from github search
)
func FilenameIndexerID(repoID int64, filename string) string {
return internal.Base36(repoID) + "_" + filename
}
@ -30,3 +34,17 @@ func FilenameOfIndexerID(indexerID string) string {
}
return indexerID[index+1:]
}
// Given the contents of file, returns the boundaries of its first seven lines.
func FilenameMatchIndexPos(content string) (int, int) {
count := 1
for i, c := range content {
if c == '\n' {
count++
if count == filenameMatchNumberOfLines {
return 0, i
}
}
}
return 0, len(content)
}

View File

@ -11,10 +11,15 @@ import (
"code.gitea.io/gitea/modules/util"
"github.com/blevesearch/bleve/v2"
"github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode"
"github.com/blevesearch/bleve/v2/index/upsidedown"
"github.com/ethantkoenig/rupture"
)
const (
maxFuzziness = 2
)
// openIndexer open the index at the specified path, checking for metadata
// updates and bleve version updates. If index needs to be created (or
// re-created), returns (nil, nil)
@ -48,7 +53,27 @@ func openIndexer(path string, latestVersion int) (bleve.Index, int, error) {
return index, 0, nil
}
// This method test the GuessFuzzinessByKeyword method. The fuzziness is based on the levenshtein distance and determines how many chars
// may be different on two string and they still be considered equivalent.
// Given a phrasse, its shortest word determines its fuzziness. If a phrase uses CJK (eg: `갃갃갃` `啊啊啊`), the fuzziness is zero.
func GuessFuzzinessByKeyword(s string) int {
tokenizer := unicode.NewUnicodeTokenizer()
tokens := tokenizer.Tokenize([]byte(s))
if len(tokens) > 0 {
fuzziness := maxFuzziness
for _, token := range tokens {
fuzziness = min(fuzziness, guessFuzzinessByKeyword(string(token.Term)))
}
return fuzziness
}
return 0
}
func guessFuzzinessByKeyword(s string) int {
// according to https://github.com/blevesearch/bleve/issues/1563, the supported max fuzziness is 2
// magic number 4 was chosen to determine the levenshtein distance per each character of a keyword
// BUT, when using CJK (eg: `갃갃갃` `啊啊啊`), it mismatches a lot.
@ -57,5 +82,5 @@ func GuessFuzzinessByKeyword(s string) int {
return 0
}
}
return min(2, len(s)/4)
return min(maxFuzziness, len(s)/4)
}

View File

@ -0,0 +1,45 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package bleve
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestBleveGuessFuzzinessByKeyword(t *testing.T) {
scenarios := []struct {
Input string
Fuzziness int // See util.go for the definition of fuzziness in this particular context
}{
{
Input: "",
Fuzziness: 0,
},
{
Input: "Avocado",
Fuzziness: 1,
},
{
Input: "Geschwindigkeit",
Fuzziness: 2,
},
{
Input: "non-exist",
Fuzziness: 0,
},
{
Input: "갃갃갃",
Fuzziness: 0,
},
}
for _, scenario := range scenarios {
t.Run(fmt.Sprintf("ensure fuzziness of '%s' is '%d'", scenario.Input, scenario.Fuzziness), func(t *testing.T) {
assert.Equal(t, scenario.Fuzziness, GuessFuzzinessByKeyword(scenario.Input))
})
}
}

View File

@ -37,6 +37,7 @@ type PullRequest struct {
ForeignIndex int64
Context DownloaderContext `yaml:"-"`
EnsuredSafe bool `yaml:"ensured_safe"`
IsDraft bool `yaml:"is_draft"`
}
func (p *PullRequest) GetLocalIndex() int64 { return p.Number }

View File

@ -7,11 +7,9 @@ import (
"context"
"fmt"
"net/url"
"strconv"
"time"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
)
@ -24,25 +22,6 @@ const (
GitPushOptionCount = "GIT_PUSH_OPTION_COUNT"
)
// GitPushOptions is a wrapper around a map[string]string
type GitPushOptions map[string]string
// GitPushOptions keys
const (
GitPushOptionRepoPrivate = "repo.private"
GitPushOptionRepoTemplate = "repo.template"
)
// Bool checks for a key in the map and parses as a boolean
func (g GitPushOptions) Bool(key string) optional.Option[bool] {
if val, ok := g[key]; ok {
if b, err := strconv.ParseBool(val); err == nil {
return optional.Some(b)
}
}
return optional.None[bool]()
}
// HookOptions represents the options for the Hook calls
type HookOptions struct {
OldCommitIDs []string

View File

@ -0,0 +1,45 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package private
import (
"strconv"
"strings"
"code.gitea.io/gitea/modules/optional"
)
// GitPushOptions is a wrapper around a map[string]string
type GitPushOptions map[string]string
// GitPushOptions keys
const (
GitPushOptionRepoPrivate = "repo.private"
GitPushOptionRepoTemplate = "repo.template"
GitPushOptionForcePush = "force-push"
)
// Bool checks for a key in the map and parses as a boolean
// An option without value is considered true, eg: "-o force-push" or "-o repo.private"
func (g GitPushOptions) Bool(key string) optional.Option[bool] {
if val, ok := g[key]; ok {
if val == "" {
return optional.Some(true)
}
if b, err := strconv.ParseBool(val); err == nil {
return optional.Some(b)
}
}
return optional.None[bool]()
}
// AddFromKeyValue adds a key value pair to the map by "key=value" format or "key" for empty value
func (g GitPushOptions) AddFromKeyValue(line string) {
kv := strings.SplitN(line, "=", 2)
if len(kv) == 2 {
g[kv[0]] = kv[1]
} else {
g[kv[0]] = ""
}
}

View File

@ -0,0 +1,30 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package private
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGitPushOptions(t *testing.T) {
o := GitPushOptions{}
v := o.Bool("no-such")
assert.False(t, v.Has())
assert.False(t, v.Value())
o.AddFromKeyValue("opt1=a=b")
o.AddFromKeyValue("opt2=false")
o.AddFromKeyValue("opt3=true")
o.AddFromKeyValue("opt4")
assert.Equal(t, "a=b", o["opt1"])
assert.False(t, o.Bool("opt1").Value())
assert.True(t, o.Bool("opt2").Has())
assert.False(t, o.Bool("opt2").Value())
assert.True(t, o.Bool("opt3").Value())
assert.True(t, o.Bool("opt4").Value())
}

View File

@ -29,4 +29,6 @@ const (
UserFeatureManageGPGKeys = "manage_gpg_keys"
UserFeatureManageMFA = "manage_mfa"
UserFeatureManageCredentials = "manage_credentials"
UserFeatureChangeUsername = "change_username"
UserFeatureChangeFullName = "change_full_name"
)

View File

@ -217,13 +217,14 @@ const (
// IssueCommentPayload represents a payload information of issue comment event.
type IssueCommentPayload struct {
Action HookIssueCommentAction `json:"action"`
Issue *Issue `json:"issue"`
Comment *Comment `json:"comment"`
Changes *ChangesPayload `json:"changes,omitempty"`
Repository *Repository `json:"repository"`
Sender *User `json:"sender"`
IsPull bool `json:"is_pull"`
Action HookIssueCommentAction `json:"action"`
Issue *Issue `json:"issue"`
PullRequest *PullRequest `json:"pull_request,omitempty"`
Comment *Comment `json:"comment"`
Changes *ChangesPayload `json:"changes,omitempty"`
Repository *Repository `json:"repository"`
Sender *User `json:"sender"`
IsPull bool `json:"is_pull"`
}
// JSONPayload implements Payload

View File

@ -16,6 +16,8 @@ _autosave-*
*-save.pro
*-save.kicad_pcb
fp-info-cache
~*.lck
\#auto_saved_files#
# Netlist files (exported from Eeschema)
*.net

View File

@ -0,0 +1,75 @@
SENDMAIL OPEN SOURCE LICENSE
The following license terms and conditions apply to this open source
software ("Software"), unless a different license is obtained directly
from Sendmail, Inc. ("Sendmail") located at 6475 Christie Ave, Suite 350,
Emeryville, CA 94608, USA.
Use, modification and redistribution (including distribution of any
modified or derived work) of the Software in source and binary forms is
permitted only if each of the following conditions of 1-6 are met:
1. Redistributions of the Software qualify as "freeware" or "open
source software" under one of the following terms:
(a) Redistributions are made at no charge beyond the reasonable
cost of materials and delivery; or
(b) Redistributions are accompanied by a copy of the modified
Source Code (on an acceptable machine-readable medium) or by an
irrevocable offer to provide a copy of the modified Source Code
(on an acceptable machine-readable medium) for up to three years
at the cost of materials and delivery. Such redistributions must
allow further use, modification, and redistribution of the Source
Code under substantially the same terms as this license. For
the purposes of redistribution "Source Code" means the complete
human-readable, compilable, linkable, and operational source
code of the redistributed module(s) including all modifications.
2. Redistributions of the Software Source Code must retain the
copyright notices as they appear in each Source Code file, these
license terms and conditions, and the disclaimer/limitation of
liability set forth in paragraph 6 below. Redistributions of the
Software Source Code must also comply with the copyright notices
and/or license terms and conditions imposed by contributors on
embedded code. The contributors' license terms and conditions
and/or copyright notices are contained in the Source Code
distribution.
3. Redistributions of the Software in binary form must reproduce the
Copyright Notice described below, these license terms and conditions,
and the disclaimer/limitation of liability set forth in paragraph
6 below, in the documentation and/or other materials provided with
the binary distribution. For the purposes of binary distribution,
"Copyright Notice" refers to the following language: "Copyright (c)
1998-2009 Sendmail, Inc. All rights reserved."
4. Neither the name, trademark or logo of Sendmail, Inc. (including
without limitation its subsidiaries or affiliates) or its contributors
may be used to endorse or promote products, or software or services
derived from this Software without specific prior written permission.
The name "sendmail" is a registered trademark and service mark of
Sendmail, Inc.
5. We reserve the right to cancel this license if you do not comply with
the terms. This license is governed by California law and both of us
agree that for any dispute arising out of or relating to this Software,
that jurisdiction and venue is proper in San Francisco or Alameda
counties. These license terms and conditions reflect the complete
agreement for the license of the Software (which means this supercedes
prior or contemporaneous agreements or representations). If any term
or condition under this license is found to be invalid, the remaining
terms and conditions still apply.
6. Disclaimer/Limitation of Liability: THIS SOFTWARE IS PROVIDED BY
SENDMAIL AND ITS CONTRIBUTORS "AS IS" WITHOUT WARRANTY OF ANY KIND
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY, NON-INFRINGEMENT AND FITNESS FOR A
PARTICULAR PURPOSE ARE EXPRESSLY DISCLAIMED. IN NO EVENT SHALL SENDMAIL
OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
WITHOUT LIMITATION NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.

View File

@ -0,0 +1,23 @@
As a special exception, the Harbour Project gives permission for
additional uses of the text contained in its release of Harbour.
The exception is that, if you link the Harbour libraries with other
files to produce an executable, this does not by itself cause the
resulting executable to be covered by the GNU General Public License.
Your use of that executable is in no way restricted on account of
linking the Harbour library code into it.
This exception does not however invalidate any other reasons why
the executable file might be covered by the GNU General Public License.
This exception applies only to the code released by the Harbour
Project under the name Harbour. If you copy code from other
Harbour Project or Free Software Foundation releases into a copy of
Harbour, as the General Public License permits, the exception does
not apply to the code that you add in this way. To avoid misleading
anyone as to the status of such modified files, you must delete
this exception notice from them.
If you write modifications of your own for Harbour, it is your choice
whether to permit this exception to apply to your modifications.
If you do not wish that, delete this exception notice.

View File

@ -689,7 +689,6 @@ public_profile=Veřejný profil
biography_placeholder=Řekněte nám něco o sobě! (Můžete použít Markdown)
location_placeholder=Sdílejte svou přibližnou polohu s ostatními
profile_desc=Nastavte, jak bude váš profil zobrazen ostatním uživatelům. Vaše hlavní e-mailová adresa bude použita pro oznámení, obnovení hesla a operace Git.
password_username_disabled=Externí uživatelé nemohou měnit svoje uživatelské jméno. Kontaktujte prosím svého administrátora pro více detailů.
full_name=Celé jméno
website=Web
location=Místo

View File

@ -683,7 +683,6 @@ public_profile=Öffentliches Profil
biography_placeholder=Erzähle uns ein wenig über Dich selbst! (Du kannst Markdown verwenden)
location_placeholder=Teile Deinen ungefähren Standort mit anderen
profile_desc=Lege fest, wie dein Profil anderen Benutzern angezeigt wird. Deine primäre E-Mail-Adresse wird für Benachrichtigungen, Passwort-Wiederherstellung und webbasierte Git-Operationen verwendet.
password_username_disabled=Benutzer, die nicht von Gitea verwaltet werden können ihren Benutzernamen nicht ändern. Bitte kontaktiere deinen Administrator für mehr Details.
full_name=Vollständiger Name
website=Webseite
location=Standort

View File

@ -620,7 +620,6 @@ public_profile=Δημόσιο Προφίλ
biography_placeholder=Πείτε μας λίγο για τον εαυτό σας! (Μπορείτε να γράψετε με Markdown)
location_placeholder=Μοιραστείτε την κατά προσέγγιση τοποθεσία σας με άλλους
profile_desc=Ελέγξτε πώς εμφανίζεται το προφίλ σας σε άλλους χρήστες. Η κύρια διεύθυνση email σας θα χρησιμοποιηθεί για ειδοποιήσεις, ανάκτηση κωδικού πρόσβασης και λειτουργίες Git που βασίζονται στο web.
password_username_disabled=Οι μη τοπικοί χρήστες δεν επιτρέπεται να αλλάξουν το όνομα χρήστη τους. Επικοινωνήστε με το διαχειριστή σας για περισσότερες λεπτομέρειες.
full_name=Πλήρες Όνομα
website=Ιστοσελίδα
location=Τοποθεσία

View File

@ -580,6 +580,8 @@ lang_select_error = Select a language from the list.
username_been_taken = The username is already taken.
username_change_not_local_user = Non-local users are not allowed to change their username.
change_username_disabled = Changing username is disabled.
change_full_name_disabled = Changing full name is disabled.
username_has_not_been_changed = Username has not been changed
repo_name_been_taken = The repository name is already used.
repository_force_private = Force Private is enabled: private repositories cannot be made public.
@ -705,7 +707,8 @@ public_profile = Public Profile
biography_placeholder = Tell us a little bit about yourself! (You can use Markdown)
location_placeholder = Share your approximate location with others
profile_desc = Control how your profile is show to other users. Your primary email address will be used for notifications, password recovery and web-based Git operations.
password_username_disabled = Non-local users are not allowed to change their username. Please contact your site administrator for more details.
password_username_disabled = You are not allowed to change their username. Please contact your site administrator for more details.
password_full_name_disabled = You are not allowed to change their full name. Please contact your site administrator for more details.
full_name = Full Name
website = Website
location = Location

View File

@ -617,7 +617,6 @@ public_profile=Perfil público
biography_placeholder=¡Cuéntanos un poco sobre ti mismo! (Puedes usar Markdown)
location_placeholder=Comparte tu ubicación aproximada con otros
profile_desc=Controla cómo se muestra su perfil a otros usuarios. Tu dirección de correo electrónico principal se utilizará para notificaciones, recuperación de contraseña y operaciones de Git basadas en la web.
password_username_disabled=Usuarios no locales no tienen permitido cambiar su nombre de usuario. Por favor, contacta con el administrador del sistema para más detalles.
full_name=Nombre completo
website=Página web
location=Localización

View File

@ -491,7 +491,6 @@ account_link=حساب‌های مرتبط
organization=سازمان ها
public_profile=نمایه عمومی
password_username_disabled=حساب‌های غیر محلی مجاز به تغییر نام کاربری نیستند. لطفا با مدیر سایت در ارتباط باشید.
full_name=نام کامل
website=تارنما
location=موقعیت مکانی

View File

@ -451,7 +451,6 @@ account_link=Linkitetyt tilit
organization=Organisaatiot
public_profile=Julkinen profiili
password_username_disabled=Ei-paikalliset käyttäjät eivät voi muuttaa käyttäjätunnustaan. Ole hyvä ja ota yhteyttä sivuston ylläpitäjään saadaksesi lisätietoa.
full_name=Kokonimi
website=Nettisivut
location=Sijainti

View File

@ -580,6 +580,8 @@ lang_select_error=Sélectionnez une langue dans la liste.
username_been_taken=Le nom d'utilisateur est déjà pris.
username_change_not_local_user=Les utilisateurs non-locaux n'ont pas le droit de modifier leur nom d'utilisateur.
change_username_disabled=Le changement de nom dutilisateur est désactivé.
change_full_name_disabled=Le changement de nom complet est désactivé.
username_has_not_been_changed=Le nom d'utilisateur n'a pas été modifié
repo_name_been_taken=Ce nom de dépôt est déjà utilisé.
repository_force_private=Force Private est activé : les dépôts privés ne peuvent pas être rendus publics.
@ -705,7 +707,6 @@ public_profile=Profil public
biography_placeholder=Parlez-nous un peu de vous! (Vous pouvez utiliser Markdown)
location_placeholder=Partagez votre position approximative avec d'autres personnes
profile_desc=Contrôlez comment votre profil est affiché aux autres utilisateurs. Votre adresse courriel principale sera utilisée pour les notifications, la récupération de mot de passe et les opérations Git basées sur le Web.
password_username_disabled=Les utilisateurs externes ne sont pas autorisés à modifier leur nom d'utilisateur. Veuillez contacter l'administrateur de votre site pour plus de détails.
full_name=Nom complet
website=Site Web
location=Localisation
@ -1040,6 +1041,7 @@ issue_labels_helper=Sélectionner un jeu de label.
license=Licence
license_helper=Sélectionner une licence
license_helper_desc=Une licence réglemente ce que les autres peuvent ou ne peuvent pas faire avec votre code. Vous ne savez pas laquelle est la bonne pour votre projet ? Comment <a target="_blank" rel="noopener noreferrer" href="%s">choisir une licence</a>.
multiple_licenses=Licences multiples
object_format=Format d'objet
object_format_helper=Format dobjet pour ce dépôt. Ne peut être modifié plus tard. SHA1 est le plus compatible.
readme=LISEZMOI
@ -1835,7 +1837,7 @@ pulls.is_empty=Les changements sur cette branche sont déjà sur la branche cibl
pulls.required_status_check_failed=Certains contrôles requis n'ont pas réussi.
pulls.required_status_check_missing=Certains contrôles requis sont manquants.
pulls.required_status_check_administrator=En tant qu'administrateur, vous pouvez toujours fusionner cette requête de pull.
pulls.blocked_by_approvals=Cette demande d'ajout nest pas suffisamment approuvée. %d approbations obtenues sur %d.
pulls.blocked_by_approvals=Cette demande dajout nest pas suffisamment approuvée. %d approbations obtenues sur %d.
pulls.blocked_by_approvals_whitelisted=Cette demande dajout na pas encore assez dapprobations. %d sur %d approbations de la part des utilisateurs ou équipes sur la liste autorisée.
pulls.blocked_by_rejection=Cette demande dajout nécessite des corrections sollicitées par un évaluateur officiel.
pulls.blocked_by_official_review_requests=Cette demande dajout a des sollicitations officielles dévaluation.
@ -2941,6 +2943,7 @@ dashboard.start_schedule_tasks=Démarrer les tâches planifiées
dashboard.sync_branch.started=Début de la synchronisation des branches
dashboard.sync_tag.started=Synchronisation des étiquettes
dashboard.rebuild_issue_indexer=Reconstruire lindexeur des tickets
dashboard.sync_repo_licenses=Synchroniser les licences du dépôt
users.user_manage_panel=Gestion du compte utilisateur
users.new_account=Créer un compte

File diff suppressed because it is too large Load Diff

View File

@ -397,7 +397,6 @@ account_link=Kapcsolt fiókok
organization=Szervezetek
public_profile=Nyilvános profil
password_username_disabled=A nem helyi felhasználóknak nem engedélyezett, hogy megváltoztassák a felhasználói nevüket. Kérjük lépjen kapcsolatba a helyi rendszergazdájával további információkért.
full_name=Teljes név
website=Webhely
location=Hely

View File

@ -317,7 +317,6 @@ account_link=Akun Tertaut
organization=Organisasi
public_profile=Profil Publik
password_username_disabled=Pengguna non-lokal tidak diizinkan untuk mengubah nama pengguna mereka. Silakan hubungi administrator sistem anda untuk lebih lanjut.
full_name=Nama Lengkap
website=Situs Web
location=Lokasi

View File

@ -428,7 +428,6 @@ account_link=Tengdir Reikningar
organization=Stofnanir
public_profile=Opinber Notandasíða
password_username_disabled=Notendum utan staðarins er ekki heimilt að breyta notendanafni sínu. Vinsamlegast hafðu samband við síðustjórann þinn til að fá frekari upplýsingar.
full_name=Fullt Nafn
website=Vefsíða
location=Staðsetning

View File

@ -516,7 +516,6 @@ account_link=Account collegati
organization=Organizzazioni
public_profile=Profilo pubblico
password_username_disabled=Gli utenti non locali non hanno il permesso di cambiare il proprio nome utente. per maggiori dettagli si prega di contattare l'amministratore del sito.
full_name=Nome Completo
website=Sito web
location=Posizione

View File

@ -705,7 +705,6 @@ public_profile=公開プロフィール
biography_placeholder=自己紹介してください!(Markdownを使うことができます)
location_placeholder=おおよその場所を他の人と共有
profile_desc=あなたのプロフィールが他のユーザーにどのように表示されるかを制御します。あなたのプライマリメールアドレスは、通知、パスワードの回復、WebベースのGit操作に使用されます。
password_username_disabled=非ローカルユーザーのユーザー名は変更できません。詳細はサイト管理者にお問い合わせください。
full_name=フルネーム
website=Webサイト
location=場所
@ -1040,6 +1039,7 @@ issue_labels_helper=イシューのラベルセットを選択
license=ライセンス
license_helper=ライセンス ファイルを選択してください。
license_helper_desc=ライセンスにより、他人があなたのコードに対して何ができて何ができないのかを規定します。 どれがプロジェクトにふさわしいか迷っていますか? <a target="_blank" rel="noopener noreferrer" href="%s">ライセンス選択サイト</a> も確認してみてください。
multiple_licenses=複数のライセンス
object_format=オブジェクトのフォーマット
object_format_helper=リポジトリのオブジェクトフォーマット。後で変更することはできません。SHA1 は最も互換性があります。
readme=README
@ -1926,6 +1926,7 @@ pulls.delete.text=本当にこのプルリクエストを削除しますか? (
pulls.recently_pushed_new_branches=%[2]s 、あなたはブランチ <strong>%[1]s</strong> にプッシュしました
pull.deleted_branch=(削除済み):%s
pull.agit_documentation=AGitに関するドキュメントを確認する
comments.edit.already_changed=コメントの変更を保存できません。 他のユーザーによって内容がすでに変更されているようです。 変更を上書きしないようにするため、ページを更新してからもう一度編集してください
@ -2940,6 +2941,7 @@ dashboard.start_schedule_tasks=Actionsスケジュールタスクを開始
dashboard.sync_branch.started=ブランチの同期を開始しました
dashboard.sync_tag.started=タグの同期を開始しました
dashboard.rebuild_issue_indexer=イシューインデクサーの再構築
dashboard.sync_repo_licenses=リポジトリライセンスの同期
users.user_manage_panel=ユーザーアカウント管理
users.new_account=ユーザーアカウントを作成

View File

@ -375,7 +375,6 @@ account_link=연결된 계정
organization=조직
public_profile=공개 프로필
password_username_disabled=로컬 사용자가 아닌 경우 사용자 이름 변경을 할 수 없습니다. 자세한 내용은 관리자에게 문의해주세요.
full_name=성명
website=웹 사이트
location=위치

View File

@ -623,7 +623,6 @@ public_profile=Publiskais profils
biography_placeholder=Pastāsti mums mazliet par sevi! (Var izmantot Markdown)
location_placeholder=Kopīgot savu aptuveno atrašanās vietu ar citiem
profile_desc=Norādīt, kā profils tiek attēlots citiem lietotājiem. Primārā e-pasta adrese tiks izmantota paziņojumiem, paroles atjaunošanai un Git tīmekļa darbībām.
password_username_disabled=Ne-lokāliem lietotājiem nav atļauts mainīt savu lietotāja vārdu. Sazinieties ar sistēmas administratoru, lai uzzinātu sīkāk.
full_name=Pilns vārds
website=Mājas lapa
location=Atrašanās vieta

View File

@ -515,7 +515,6 @@ account_link=Gekoppelde Accounts
organization=Organisaties
public_profile=Openbaar profiel
password_username_disabled=Niet-lokale gebruikers kunnen hun gebruikersnaam niet veranderen. Neem contact op met de sitebeheerder voor meer details.
full_name=Volledige naam
website=Website
location=Locatie

View File

@ -500,7 +500,6 @@ account_link=Powiązane Konta
organization=Organizacje
public_profile=Profil publiczny
password_username_disabled=Użytkownicy nielokalni nie mogą zmieniać swoich nazw. Aby uzyskać więcej informacji, skontaktuj się z administratorem strony.
full_name=Imię i nazwisko
website=Strona
location=Lokalizacja

View File

@ -622,7 +622,6 @@ public_profile=Perfil público
biography_placeholder=Conte-nos um pouco sobre você! (Você pode usar Markdown)
location_placeholder=Compartilhe sua localização aproximada com outras pessoas
profile_desc=Controle como o seu perfil é exibido para outros usuários. Seu endereço de e-mail principal será usado para notificações, recuperação de senha e operações do Git baseadas na Web.
password_username_disabled=Usuários não-locais não podem alterar seus nomes de usuário. Por favor contate o administrador do site para mais informações.
full_name=Nome completo
website=Site
location=Localização

View File

@ -580,6 +580,8 @@ lang_select_error=Escolha um idioma da lista.
username_been_taken=O nome de utilizador já foi tomado.
username_change_not_local_user=Utilizadores que não são locais não têm permissão para mudar o nome de utilizador.
change_username_disabled=Alterar o nome de utilizador está desabilitado.
change_full_name_disabled=Alterar o nome completo está desabilitado.
username_has_not_been_changed=O nome de utilizador não foi modificado
repo_name_been_taken=O nome do repositório já foi usado.
repository_force_private=Forçar Privado está habilitado: repositórios privados não podem ser tornados públicos.
@ -705,7 +707,8 @@ public_profile=Perfil público
biography_placeholder=Conte-nos um pouco sobre si! (Pode usar Markdown)
location_placeholder=Partilhe a sua localização aproximada com outros
profile_desc=Controle como o seu perfil é apresentado aos outros utilizadores. O seu endereço de email principal será usado para notificações, recuperação de senha e operações Git baseadas na web.
password_username_disabled=Utilizadores não-locais não podem mudar os seus nomes de utilizador. Entre em contacto com o administrador do sítio saber para mais detalhes.
password_username_disabled=Não tem permissão para alterar os nomes de utilizador deles/delas. Entre em contacto com o administrador para saber mais detalhes.
password_full_name_disabled=Não tem permissão para alterar o nome completo deles/delas. Entre em contacto com o administrador para saber mais detalhes.
full_name=Nome completo
website=Sítio web
location=Localização
@ -1040,6 +1043,7 @@ issue_labels_helper=Escolha um conjunto de rótulos para as questões.
license=Licença
license_helper=Escolha um ficheiro de licença.
license_helper_desc=Uma licença rege o que os outros podem, ou não, fazer com o seu código fonte. Não tem a certeza sobre qual a mais indicada para o seu trabalho? Veja: <a target="_blank" rel="noopener noreferrer" href="%s">Escolher uma licença.</a>
multiple_licenses=Múltiplas licenças
object_format=Formato dos elementos
object_format_helper=Formato dos elementos do repositório. Não poderá ser alterado mais tarde. SHA1 é o mais compatível.
readme=README
@ -2941,6 +2945,7 @@ dashboard.start_schedule_tasks=Iniciar tarefas de agendamento das operações
dashboard.sync_branch.started=Sincronização de ramos iniciada
dashboard.sync_tag.started=Sincronização de etiquetas iniciada
dashboard.rebuild_issue_indexer=Reconstruir indexador de questões
dashboard.sync_repo_licenses=Sincronizar licenças do repositório
users.user_manage_panel=Gestão das contas de utilizadores
users.new_account=Criar conta de utilizador

View File

@ -618,7 +618,6 @@ public_profile=Открытый профиль
biography_placeholder=Расскажите немного о себе! (Можно использовать Markdown)
location_placeholder=Поделитесь своим приблизительным местоположением с другими
profile_desc=Контролируйте, как ваш профиль будет отображаться другим пользователям. Ваш основной адрес электронной почты будет использоваться для уведомлений, восстановления пароля и веб-операций Git.
password_username_disabled=Нелокальным пользователям запрещено изменение их имени пользователя. Для получения более подробной информации обратитесь к администратору сайта.
full_name=Имя и фамилия
website=Веб-сайт
location=Местоположение

View File

@ -480,7 +480,6 @@ account_link=සම්බන්ධිත ගිණුම්
organization=සංවිධාන
public_profile=ප්‍රසිද්ධ පැතිකඩ
password_username_disabled=දේශීය නොවන පරිශීලකයින්ට ඔවුන්ගේ පරිශීලක නාමය වෙනස් කිරීමට අවසර නැත. වැඩි විස්තර සඳහා කරුණාකර ඔබේ වෙබ් අඩවිය පරිපාලක අමතන්න.
full_name=සම්පූර්ණ නම
website=වියමන අඩවිය
location=ස්ථානය

View File

@ -583,7 +583,6 @@ account_link=Prepojené účty
organization=Organizácie
public_profile=Verejný profil
password_username_disabled=Externí používatelia nemôžu meniť svoje používateľské meno. Kontaktujte, prosím, svojho administrátora kvôli detailom.
full_name=Celé meno
website=Webová stránka
location=Miesto

View File

@ -418,7 +418,6 @@ account_link=Länkade Konton
organization=Organisationer
public_profile=Offentlig profil
password_username_disabled=Externa användare kan inte ändra sitt användarnamn. Kontakta din webbadministratör för mera information.
full_name=Fullständigt namn
website=Webbplats
location=Plats

View File

@ -694,7 +694,6 @@ public_profile=Herkese Açık Profil
biography_placeholder=Bize kendiniz hakkında birşeyler söyleyin! (Markdown kullanabilirsiniz)
location_placeholder=Yaklaşık konumunuzu başkalarıyla paylaşın
profile_desc=Profilinizin başkalarına nasıl gösterildiğini yönetin. Ana e-posta adresiniz bildirimler, parola kurtarma ve web tabanlı Git işlemleri için kullanılacaktır.
password_username_disabled=Yerel olmayan kullanıcılara kullanıcı adlarını değiştirme izni verilmemiştir. Daha fazla bilgi edinmek için lütfen site yöneticisi ile iletişime geçiniz.
full_name=Ad Soyad
website=Web Sitesi
location=Konum

View File

@ -494,7 +494,6 @@ account_link=Прив'язані облікові записи
organization=Організації
public_profile=Загальнодоступний профіль
password_username_disabled=Нелокальним користувачам заборонено змінювати ім'я користувача. Щоб отримати докладнішу інформацію, зв'яжіться з адміністратором сайту.
full_name=Повне ім'я
website=Веб-сайт
location=Місцезнаходження

View File

@ -686,7 +686,6 @@ public_profile=公开信息
biography_placeholder=告诉我们一点您自己! (您可以使用Markdown)
location_placeholder=与他人分享你的大概位置
profile_desc=控制您的个人资料对其他用户的显示方式。您的主要电子邮件地址将用于通知、密码恢复和基于网页界面的 Git 操作
password_username_disabled=不允许非本地用户更改他们的用户名。更多详情请联系您的系统管理员。
full_name=自定义名称
website=个人网站
location=所在地区

View File

@ -583,7 +583,6 @@ account_link=已連結帳號
organization=組織
public_profile=公開的個人資料
password_username_disabled=非本地使用者不允許更改他們的帳號。詳細資訊請聯絡您的系統管理員。
full_name=全名
website=個人網站
location=所在地區

9
package-lock.json generated
View File

@ -29,7 +29,7 @@
"esbuild-loader": "4.2.2",
"escape-goat": "4.0.0",
"fast-glob": "3.3.2",
"htmx.org": "2.0.2",
"htmx.org": "2.0.3",
"idiomorph": "0.3.0",
"jquery": "3.7.1",
"katex": "0.16.11",
@ -10548,10 +10548,9 @@
}
},
"node_modules/htmx.org": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.2.tgz",
"integrity": "sha512-eUPIpQaWKKstX393XNCRCMJTrqPzikh36Y9RceqsUZLTtlFjFaVDgwZLUsrFk8J2uzZxkkfiy0TE359j2eN6hA==",
"license": "0BSD"
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.3.tgz",
"integrity": "sha512-AeoJUAjkCVVajbfKX+3sVQBTCt8Ct4lif1T+z/tptTXo8+8yyq3QIMQQe/IT+R8ssfrO1I0DeX4CAronzCL6oA=="
},
"node_modules/human-signals": {
"version": "5.0.0",

View File

@ -28,7 +28,7 @@
"esbuild-loader": "4.2.2",
"escape-goat": "4.0.0",
"fast-glob": "3.3.2",
"htmx.org": "2.0.2",
"htmx.org": "2.0.3",
"idiomorph": "0.3.0",
"jquery": "3.7.1",
"katex": "0.16.11",

View File

@ -63,6 +63,20 @@ func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) {
ctx.Error(http.StatusUnauthorized, "reqPackageAccess", "user should have specific permission or be a site admin")
return
}
// check if scope only applies to public resources
publicOnly, err := scope.PublicOnly()
if err != nil {
ctx.Error(http.StatusForbidden, "tokenRequiresScope", "parsing public resource scope failed: "+err.Error())
return
}
if publicOnly {
if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() {
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public packages")
return
}
}
}
}

View File

@ -10,11 +10,11 @@ import (
"fmt"
"os"
"strings"
"sync"
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
container_model "code.gitea.io/gitea/models/packages/container"
"code.gitea.io/gitea/modules/globallock"
"code.gitea.io/gitea/modules/log"
packages_module "code.gitea.io/gitea/modules/packages"
container_module "code.gitea.io/gitea/modules/packages/container"
@ -22,8 +22,6 @@ import (
packages_service "code.gitea.io/gitea/services/packages"
)
var uploadVersionMutex sync.Mutex
// saveAsPackageBlob creates a package blob from an upload
// The uploaded blob gets stored in a special upload version to link them to the package/image
func saveAsPackageBlob(ctx context.Context, hsr packages_module.HashedSizeReader, pci *packages_service.PackageCreationInfo) (*packages_model.PackageBlob, error) { //nolint:unparam
@ -90,13 +88,20 @@ func mountBlob(ctx context.Context, pi *packages_service.PackageInfo, pb *packag
})
}
func containerPkgName(piOwnerID int64, piName string) string {
return fmt.Sprintf("pkg_%d_container_%s", piOwnerID, strings.ToLower(piName))
}
func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageInfo) (*packages_model.PackageVersion, error) {
var uploadVersion *packages_model.PackageVersion
// FIXME: Replace usage of mutex with database transaction
// https://github.com/go-gitea/gitea/pull/21862
uploadVersionMutex.Lock()
err := db.WithTx(ctx, func(ctx context.Context) error {
releaser, err := globallock.Lock(ctx, containerPkgName(pi.Owner.ID, pi.Name))
if err != nil {
return nil, err
}
defer releaser()
err = db.WithTx(ctx, func(ctx context.Context) error {
created := true
p := &packages_model.Package{
OwnerID: pi.Owner.ID,
@ -140,7 +145,6 @@ func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageI
return nil
})
uploadVersionMutex.Unlock()
return uploadVersion, err
}
@ -173,6 +177,12 @@ func createFileForBlob(ctx context.Context, pv *packages_model.PackageVersion, p
}
func deleteBlob(ctx context.Context, ownerID int64, image, digest string) error {
releaser, err := globallock.Lock(ctx, containerPkgName(ownerID, image))
if err != nil {
return err
}
defer releaser()
return db.WithTx(ctx, func(ctx context.Context) error {
pfds, err := container_model.GetContainerBlobs(ctx, &container_model.BlobSearchOptions{
OwnerID: ownerID,

View File

@ -45,7 +45,7 @@ func ListHooks(ctx *context.APIContext) {
}
hooks := make([]*api.Hook, len(sysHooks))
for i, hook := range sysHooks {
h, err := webhook_service.ToHook(setting.AppURL+"/admin", hook)
h, err := webhook_service.ToHook(setting.AppURL+"/-/admin", hook)
if err != nil {
ctx.Error(http.StatusInternalServerError, "convert.ToHook", err)
return
@ -83,7 +83,7 @@ func GetHook(ctx *context.APIContext) {
}
return
}
h, err := webhook_service.ToHook("/admin/", hook)
h, err := webhook_service.ToHook("/-/admin/", hook)
if err != nil {
ctx.Error(http.StatusInternalServerError, "convert.ToHook", err)
return

View File

@ -235,6 +235,62 @@ func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.APIContext)
}
}
func checkTokenPublicOnly() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if !ctx.PublicOnly {
return
}
requiredScopeCategories, ok := ctx.Data["requiredScopeCategories"].([]auth_model.AccessTokenScopeCategory)
if !ok || len(requiredScopeCategories) == 0 {
return
}
// public Only permission check
switch {
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryRepository):
if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public repos")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryIssue):
if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public issues")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryOrganization):
if ctx.Org.Organization != nil && ctx.Org.Organization.Visibility != api.VisibleTypePublic {
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public orgs")
return
}
if ctx.ContextUser != nil && ctx.ContextUser.IsOrganization() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public orgs")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryUser):
if ctx.ContextUser != nil && ctx.ContextUser.IsUser() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public users")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryActivityPub):
if ctx.ContextUser != nil && ctx.ContextUser.IsUser() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public activitypub")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryNotification):
if ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public notifications")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryPackage):
if ctx.Package != nil && ctx.Package.Owner.Visibility.IsPrivate() {
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public packages")
return
}
}
}
}
// if a token is being used for auth, we check that it contains the required scope
// if a token is not being used, reqToken will enforce other sign in methods
func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeCategory) func(ctx *context.APIContext) {
@ -250,9 +306,6 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC
return
}
ctx.Data["ApiTokenScopePublicRepoOnly"] = false
ctx.Data["ApiTokenScopePublicOrgOnly"] = false
// use the http method to determine the access level
requiredScopeLevel := auth_model.Read
if ctx.Req.Method == "POST" || ctx.Req.Method == "PUT" || ctx.Req.Method == "PATCH" || ctx.Req.Method == "DELETE" {
@ -261,6 +314,18 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC
// get the required scope for the given access level and category
requiredScopes := auth_model.GetRequiredScopes(requiredScopeLevel, requiredScopeCategories...)
allow, err := scope.HasScope(requiredScopes...)
if err != nil {
ctx.Error(http.StatusForbidden, "tokenRequiresScope", "checking scope failed: "+err.Error())
return
}
if !allow {
ctx.Error(http.StatusForbidden, "tokenRequiresScope", fmt.Sprintf("token does not have at least one of required scope(s): %v", requiredScopes))
return
}
ctx.Data["requiredScopeCategories"] = requiredScopeCategories
// check if scope only applies to public resources
publicOnly, err := scope.PublicOnly()
@ -269,21 +334,8 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC
return
}
// this context is used by the middleware in the specific route
ctx.Data["ApiTokenScopePublicRepoOnly"] = publicOnly && auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryRepository)
ctx.Data["ApiTokenScopePublicOrgOnly"] = publicOnly && auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryOrganization)
allow, err := scope.HasScope(requiredScopes...)
if err != nil {
ctx.Error(http.StatusForbidden, "tokenRequiresScope", "checking scope failed: "+err.Error())
return
}
if allow {
return
}
ctx.Error(http.StatusForbidden, "tokenRequiresScope", fmt.Sprintf("token does not have at least one of required scope(s): %v", requiredScopes))
// assign to true so that those searching should only filter public repositories/users/organizations
ctx.PublicOnly = publicOnly
}
}
@ -295,25 +347,6 @@ func reqToken() func(ctx *context.APIContext) {
return
}
if true == ctx.Data["IsApiToken"] {
publicRepo, pubRepoExists := ctx.Data["ApiTokenScopePublicRepoOnly"]
publicOrg, pubOrgExists := ctx.Data["ApiTokenScopePublicOrgOnly"]
if pubRepoExists && publicRepo.(bool) &&
ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public repos")
return
}
if pubOrgExists && publicOrg.(bool) &&
ctx.Org.Organization != nil && ctx.Org.Organization.Visibility != api.VisibleTypePublic {
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public orgs")
return
}
return
}
if ctx.IsSigned {
return
}
@ -879,11 +912,11 @@ func Routes() *web.Router {
m.Group("/user/{username}", func() {
m.Get("", activitypub.Person)
m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
}, context.UserAssignmentAPI())
}, context.UserAssignmentAPI(), checkTokenPublicOnly())
m.Group("/user-id/{user-id}", func() {
m.Get("", activitypub.Person)
m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
}, context.UserIDAssignmentAPI())
}, context.UserIDAssignmentAPI(), checkTokenPublicOnly())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryActivityPub))
}
@ -939,7 +972,7 @@ func Routes() *web.Router {
}, reqSelfOrAdmin(), reqBasicOrRevProxyAuth())
m.Get("/activities/feeds", user.ListUserActivityFeeds)
}, context.UserAssignmentAPI(), individualPermsChecker)
}, context.UserAssignmentAPI(), checkTokenPublicOnly(), individualPermsChecker)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser))
// Users (requires user scope)
@ -957,7 +990,7 @@ func Routes() *web.Router {
m.Get("/starred", user.GetStarredRepos)
m.Get("/subscriptions", user.GetWatchedRepos)
}, context.UserAssignmentAPI())
}, context.UserAssignmentAPI(), checkTokenPublicOnly())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
// Users (requires user scope)
@ -1044,7 +1077,7 @@ func Routes() *web.Router {
m.Get("", user.IsStarring)
m.Put("", user.Star)
m.Delete("", user.Unstar)
}, repoAssignment())
}, repoAssignment(), checkTokenPublicOnly())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
m.Get("/times", repo.ListMyTrackedTimes)
m.Get("/stopwatches", repo.GetStopwatches)
@ -1069,18 +1102,20 @@ func Routes() *web.Router {
m.Get("", user.CheckUserBlock)
m.Put("", user.BlockUser)
m.Delete("", user.UnblockUser)
}, context.UserAssignmentAPI())
}, context.UserAssignmentAPI(), checkTokenPublicOnly())
})
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
// Repositories (requires repo scope, org scope)
m.Post("/org/{org}/repos",
// FIXME: we need org in context
tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization, auth_model.AccessTokenScopeCategoryRepository),
reqToken(),
bind(api.CreateRepoOption{}),
repo.CreateOrgRepoDeprecated)
// requires repo scope
// FIXME: Don't expose repository id outside of the system
m.Combo("/repositories/{id}", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)).Get(repo.GetByID)
// Repos (requires repo scope)
@ -1334,7 +1369,7 @@ func Routes() *web.Router {
m.Post("", bind(api.UpdateRepoAvatarOption{}), repo.UpdateAvatar)
m.Delete("", repo.DeleteAvatar)
}, reqAdmin(), reqToken())
}, repoAssignment())
}, repoAssignment(), checkTokenPublicOnly())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
// Notifications (requires notifications scope)
@ -1343,7 +1378,7 @@ func Routes() *web.Router {
m.Combo("/notifications", reqToken()).
Get(notify.ListRepoNotifications).
Put(notify.ReadRepoNotifications)
}, repoAssignment())
}, repoAssignment(), checkTokenPublicOnly())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryNotification))
// Issue (requires issue scope)
@ -1457,7 +1492,7 @@ func Routes() *web.Router {
Patch(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditMilestoneOption{}), repo.EditMilestone).
Delete(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteMilestone)
})
}, repoAssignment())
}, repoAssignment(), checkTokenPublicOnly())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue))
// NOTE: these are Gitea package management API - see packages.CommonRoutes and packages.DockerContainerRoutes for endpoints that implement package manager APIs
@ -1468,14 +1503,14 @@ func Routes() *web.Router {
m.Get("/files", reqToken(), packages.ListPackageFiles)
})
m.Get("/", reqToken(), packages.ListPackages)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead))
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead), checkTokenPublicOnly())
// Organizations
m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), org.ListMyOrgs)
m.Group("/users/{username}/orgs", func() {
m.Get("", reqToken(), org.ListUserOrgs)
m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context.UserAssignmentAPI())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context.UserAssignmentAPI(), checkTokenPublicOnly())
m.Post("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), reqToken(), bind(api.CreateOrgOption{}), org.Create)
m.Get("/orgs", org.GetAll, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization))
m.Group("/orgs/{org}", func() {
@ -1533,7 +1568,7 @@ func Routes() *web.Router {
m.Delete("", org.UnblockUser)
})
}, reqToken(), reqOrgOwnership())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true))
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true), checkTokenPublicOnly())
m.Group("/teams/{teamid}", func() {
m.Combo("").Get(reqToken(), org.GetTeam).
Patch(reqToken(), reqOrgOwnership(), bind(api.EditTeamOption{}), org.EditTeam).
@ -1553,7 +1588,7 @@ func Routes() *web.Router {
Get(reqToken(), org.GetTeamRepo)
})
m.Get("/activities/feeds", org.ListTeamActivityFeeds)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(false, true), reqToken(), reqTeamMembership())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(false, true), reqToken(), reqTeamMembership(), checkTokenPublicOnly())
m.Group("/admin", func() {
m.Group("/cron", func() {

View File

@ -191,7 +191,7 @@ func GetAll(ctx *context.APIContext) {
// "$ref": "#/responses/OrganizationList"
vMode := []api.VisibleType{api.VisibleTypePublic}
if ctx.IsSigned {
if ctx.IsSigned && !ctx.PublicOnly {
vMode = append(vMode, api.VisibleTypeLimited)
if ctx.Doer.IsAdmin {
vMode = append(vMode, api.VisibleTypePrivate)

View File

@ -41,80 +41,93 @@ func SearchIssues(ctx *context.APIContext) {
// parameters:
// - name: state
// in: query
// description: whether issue is open or closed
// description: State of the issue
// type: string
// enum: [open, closed, all]
// default: open
// - name: labels
// in: query
// description: comma separated list of labels. Fetch only issues that have any of this labels. Non existent labels are discarded
// description: Comma-separated list of label names. Fetch only issues that have any of these labels. Non existent labels are discarded.
// type: string
// - name: milestones
// in: query
// description: comma separated list of milestone names. Fetch only issues that have any of this milestones. Non existent are discarded
// description: Comma-separated list of milestone names. Fetch only issues that have any of these milestones. Non existent milestones are discarded.
// type: string
// - name: q
// in: query
// description: search string
// description: Search string
// type: string
// - name: priority_repo_id
// in: query
// description: repository to prioritize in the results
// description: Repository ID to prioritize in the results
// type: integer
// format: int64
// - name: type
// in: query
// description: filter by type (issues / pulls) if set
// description: Filter by issue type
// type: string
// enum: [issues, pulls]
// - name: since
// in: query
// description: Only show notifications updated after the given time. This is a timestamp in RFC 3339 format
// description: Only show issues updated after the given time (RFC 3339 format)
// type: string
// format: date-time
// required: false
// - name: before
// in: query
// description: Only show notifications updated before the given time. This is a timestamp in RFC 3339 format
// description: Only show issues updated before the given time (RFC 3339 format)
// type: string
// format: date-time
// required: false
// - name: assigned
// in: query
// description: filter (issues / pulls) assigned to you, default is false
// description: Filter issues or pulls assigned to the authenticated user
// type: boolean
// default: false
// - name: created
// in: query
// description: filter (issues / pulls) created by you, default is false
// description: Filter issues or pulls created by the authenticated user
// type: boolean
// default: false
// - name: mentioned
// in: query
// description: filter (issues / pulls) mentioning you, default is false
// description: Filter issues or pulls mentioning the authenticated user
// type: boolean
// default: false
// - name: review_requested
// in: query
// description: filter pulls requesting your review, default is false
// description: Filter pull requests where the authenticated user's review was requested
// type: boolean
// default: false
// - name: reviewed
// in: query
// description: filter pulls reviewed by you, default is false
// description: Filter pull requests reviewed by the authenticated user
// type: boolean
// default: false
// - name: owner
// in: query
// description: filter by owner
// description: Filter by repository owner
// type: string
// - name: team
// in: query
// description: filter by team (requires organization owner parameter to be provided)
// description: Filter by team (requires organization owner parameter)
// type: string
// - name: page
// in: query
// description: page number of results to return (1-based)
// description: Page number of results to return (1-based)
// type: integer
// minimum: 1
// default: 1
// - name: limit
// in: query
// description: page size of results
// description: Number of items per page
// type: integer
// minimum: 0
// responses:
// "200":
// "$ref": "#/responses/IssueList"
// "400":
// "$ref": "#/responses/error"
// "422":
// "$ref": "#/responses/validationError"
before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
@ -149,7 +162,7 @@ func SearchIssues(ctx *context.APIContext) {
Actor: ctx.Doer,
}
if ctx.IsSigned {
opts.Private = true
opts.Private = !ctx.PublicOnly
opts.AllLimited = true
}
if ctx.FormString("owner") != "" {

View File

@ -52,56 +52,79 @@ func ListPullRequests(ctx *context.APIContext) {
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// description: Owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// description: Name of the repo
// type: string
// required: true
// - name: state
// in: query
// description: "State of pull request: open or closed (optional)"
// description: State of pull request
// type: string
// enum: [closed, open, all]
// enum: [open, closed, all]
// default: open
// - name: sort
// in: query
// description: "Type of sort"
// description: Type of sort
// type: string
// enum: [oldest, recentupdate, leastupdate, mostcomment, leastcomment, priority]
// - name: milestone
// in: query
// description: "ID of the milestone"
// description: ID of the milestone
// type: integer
// format: int64
// - name: labels
// in: query
// description: "Label IDs"
// description: Label IDs
// type: array
// collectionFormat: multi
// items:
// type: integer
// format: int64
// - name: poster
// in: query
// description: Filter by pull request author
// type: string
// - name: page
// in: query
// description: page number of results to return (1-based)
// description: Page number of results to return (1-based)
// type: integer
// minimum: 1
// default: 1
// - name: limit
// in: query
// description: page size of results
// description: Page size of results
// type: integer
// minimum: 0
// responses:
// "200":
// "$ref": "#/responses/PullRequestList"
// "404":
// "$ref": "#/responses/notFound"
// "500":
// "$ref": "#/responses/error"
labelIDs, err := base.StringsToInt64s(ctx.FormStrings("labels"))
if err != nil {
ctx.Error(http.StatusInternalServerError, "PullRequests", err)
return
}
var posterID int64
if posterStr := ctx.FormString("poster"); posterStr != "" {
poster, err := user_model.GetUserByName(ctx, posterStr)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Error(http.StatusBadRequest, "Poster not found", err)
} else {
ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
}
return
}
posterID = poster.ID
}
listOptions := utils.GetListOptions(ctx)
prs, maxResults, err := issues_model.PullRequests(ctx, ctx.Repo.Repository.ID, &issues_model.PullRequestsOptions{
ListOptions: listOptions,
@ -109,6 +132,7 @@ func ListPullRequests(ctx *context.APIContext) {
SortType: ctx.FormTrim("sort"),
Labels: labelIDs,
MilestoneID: ctx.FormInt64("milestone"),
PosterID: posterID,
})
if err != nil {
ctx.Error(http.StatusInternalServerError, "PullRequests", err)
@ -1124,9 +1148,20 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
// Check if current user has fork of repository or in the same repository.
headRepo := repo_model.GetForkedRepo(ctx, headUser.ID, baseRepo.ID)
if headRepo == nil && !isSameRepo {
log.Trace("parseCompareInfo[%d]: does not have fork or in same repository", baseRepo.ID)
ctx.NotFound("GetForkedRepo")
return nil, nil, nil, "", ""
err := baseRepo.GetBaseRepo(ctx)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetBaseRepo", err)
return nil, nil, nil, "", ""
}
// Check if baseRepo's base repository is the same as headUser's repository.
if baseRepo.BaseRepo == nil || baseRepo.BaseRepo.OwnerID != headUser.ID {
log.Trace("parseCompareInfo[%d]: does not have fork or in same repository", baseRepo.ID)
ctx.NotFound("GetBaseRepo")
return nil, nil, nil, "", ""
}
// Assign headRepo so it can be used below.
headRepo = baseRepo.BaseRepo
}
var headGitRepo *git.Repository

View File

@ -129,6 +129,11 @@ func Search(ctx *context.APIContext) {
// "422":
// "$ref": "#/responses/validationError"
private := ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private"))
if ctx.PublicOnly {
private = false
}
opts := &repo_model.SearchRepoOptions{
ListOptions: utils.GetListOptions(ctx),
Actor: ctx.Doer,
@ -138,7 +143,7 @@ func Search(ctx *context.APIContext) {
TeamID: ctx.FormInt64("team_id"),
TopicOnly: ctx.FormBool("topic"),
Collaborate: optional.None[bool](),
Private: ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private")),
Private: private,
Template: optional.None[bool](),
StarredByID: ctx.FormInt64("starredBy"),
IncludeDescription: ctx.FormBool("includeDesc"),

View File

@ -9,6 +9,7 @@ import (
activities_model "code.gitea.io/gitea/models/activities"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
@ -67,12 +68,18 @@ func Search(ctx *context.APIContext) {
maxResults = 1
users = []*user_model.User{user_model.NewActionsUser()}
default:
var visible []structs.VisibleType
if ctx.PublicOnly {
visible = []structs.VisibleType{structs.VisibleTypePublic}
}
users, maxResults, err = user_model.SearchUsers(ctx, &user_model.SearchUserOptions{
Actor: ctx.Doer,
Keyword: ctx.FormTrim("q"),
UID: uid,
Type: user_model.UserTypeIndividual,
ListOptions: listOptions,
Actor: ctx.Doer,
Keyword: ctx.FormTrim("q"),
UID: uid,
Type: user_model.UserTypeIndividual,
SearchByEmail: true,
Visible: visible,
ListOptions: listOptions,
})
if err != nil {
ctx.JSON(http.StatusInternalServerError, map[string]any{

View File

@ -100,7 +100,7 @@ func checkCreateHookOption(ctx *context.APIContext, form *api.CreateHookOption)
func AddSystemHook(ctx *context.APIContext, form *api.CreateHookOption) {
hook, ok := addHook(ctx, form, 0, 0)
if ok {
h, err := webhook_service.ToHook(setting.AppSubURL+"/admin", hook)
h, err := webhook_service.ToHook(setting.AppSubURL+"/-/admin", hook)
if err != nil {
ctx.Error(http.StatusInternalServerError, "convert.ToHook", err)
return
@ -268,7 +268,7 @@ func EditSystemHook(ctx *context.APIContext, form *api.EditHookOption, hookID in
ctx.Error(http.StatusInternalServerError, "GetSystemOrDefaultWebhook", err)
return
}
h, err := webhook_service.ToHook(setting.AppURL+"/admin", updated)
h, err := webhook_service.ToHook(setting.AppURL+"/-/admin", updated)
if err != nil {
ctx.Error(http.StatusInternalServerError, "convert.ToHook", err)
return

View File

@ -208,7 +208,7 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
return
}
cols := make([]string, 0, len(opts.GitPushOptions))
cols := make([]string, 0, 2)
if isPrivate.Has() {
repo.IsPrivate = isPrivate.Value()

View File

@ -185,9 +185,9 @@ func DashboardPost(ctx *context.Context) {
}
}
if form.From == "monitor" {
ctx.Redirect(setting.AppSubURL + "/admin/monitor/cron")
ctx.Redirect(setting.AppSubURL + "/-/admin/monitor/cron")
} else {
ctx.Redirect(setting.AppSubURL + "/admin")
ctx.Redirect(setting.AppSubURL + "/-/admin")
}
}

View File

@ -23,8 +23,8 @@ var (
func newOAuth2CommonHandlers() *user_setting.OAuth2CommonHandlers {
return &user_setting.OAuth2CommonHandlers{
OwnerID: 0,
BasePathList: fmt.Sprintf("%s/admin/applications", setting.AppSubURL),
BasePathEditPrefix: fmt.Sprintf("%s/admin/applications/oauth2", setting.AppSubURL),
BasePathList: fmt.Sprintf("%s/-/admin/applications", setting.AppSubURL),
BasePathEditPrefix: fmt.Sprintf("%s/-/admin/applications/oauth2", setting.AppSubURL),
TplAppEdit: tplSettingsOauth2ApplicationEdit,
}
}

View File

@ -324,7 +324,7 @@ func NewAuthSourcePost(ctx *context.Context) {
log.Trace("Authentication created by admin(%s): %s", ctx.Doer.Name, form.Name)
ctx.Flash.Success(ctx.Tr("admin.auths.new_success", form.Name))
ctx.Redirect(setting.AppSubURL + "/admin/auths")
ctx.Redirect(setting.AppSubURL + "/-/admin/auths")
}
// EditAuthSource render editing auth source page
@ -437,7 +437,7 @@ func EditAuthSourcePost(ctx *context.Context) {
log.Trace("Authentication changed by admin(%s): %d", ctx.Doer.Name, source.ID)
ctx.Flash.Success(ctx.Tr("admin.auths.update_success"))
ctx.Redirect(setting.AppSubURL + "/admin/auths/" + strconv.FormatInt(form.ID, 10))
ctx.Redirect(setting.AppSubURL + "/-/admin/auths/" + strconv.FormatInt(form.ID, 10))
}
// DeleteAuthSource response for deleting an auth source
@ -454,11 +454,11 @@ func DeleteAuthSource(ctx *context.Context) {
} else {
ctx.Flash.Error(fmt.Sprintf("auth_service.DeleteSource: %v", err))
}
ctx.JSONRedirect(setting.AppSubURL + "/admin/auths/" + url.PathEscape(ctx.PathParam(":authid")))
ctx.JSONRedirect(setting.AppSubURL + "/-/admin/auths/" + url.PathEscape(ctx.PathParam(":authid")))
return
}
log.Trace("Authentication deleted by admin(%s): %d", ctx.Doer.Name, source.ID)
ctx.Flash.Success(ctx.Tr("admin.auths.deletion_success"))
ctx.JSONRedirect(setting.AppSubURL + "/admin/auths")
ctx.JSONRedirect(setting.AppSubURL + "/-/admin/auths")
}

View File

@ -40,7 +40,7 @@ func SendTestMail(ctx *context.Context) {
ctx.Flash.Info(ctx.Tr("admin.config.test_mail_sent", email))
}
ctx.Redirect(setting.AppSubURL + "/admin/config")
ctx.Redirect(setting.AppSubURL + "/-/admin/config")
}
// TestCache test the cache settings
@ -56,7 +56,7 @@ func TestCache(ctx *context.Context) {
}
}
ctx.Redirect(setting.AppSubURL + "/admin/config")
ctx.Redirect(setting.AppSubURL + "/-/admin/config")
}
func shadowPasswordKV(cfgItem, splitter string) string {

View File

@ -134,7 +134,7 @@ func ActivateEmail(ctx *context.Context) {
ctx.Flash.Info(ctx.Tr("admin.emails.updated"))
}
redirect, _ := url.Parse(setting.AppSubURL + "/admin/emails")
redirect, _ := url.Parse(setting.AppSubURL + "/-/admin/emails")
q := url.Values{}
if val := ctx.FormTrim("q"); len(val) > 0 {
q.Set("q", val)

View File

@ -36,8 +36,8 @@ func DefaultOrSystemWebhooks(ctx *context.Context) {
sys["Title"] = ctx.Tr("admin.systemhooks")
sys["Description"] = ctx.Tr("admin.systemhooks.desc", "https://docs.gitea.com/usage/webhooks")
sys["Webhooks"], err = webhook.GetSystemWebhooks(ctx, optional.None[bool]())
sys["BaseLink"] = setting.AppSubURL + "/admin/hooks"
sys["BaseLinkNew"] = setting.AppSubURL + "/admin/system-hooks"
sys["BaseLink"] = setting.AppSubURL + "/-/admin/hooks"
sys["BaseLinkNew"] = setting.AppSubURL + "/-/admin/system-hooks"
if err != nil {
ctx.ServerError("GetWebhooksAdmin", err)
return
@ -46,8 +46,8 @@ func DefaultOrSystemWebhooks(ctx *context.Context) {
def["Title"] = ctx.Tr("admin.defaulthooks")
def["Description"] = ctx.Tr("admin.defaulthooks.desc", "https://docs.gitea.com/usage/webhooks")
def["Webhooks"], err = webhook.GetDefaultWebhooks(ctx)
def["BaseLink"] = setting.AppSubURL + "/admin/hooks"
def["BaseLinkNew"] = setting.AppSubURL + "/admin/default-hooks"
def["BaseLink"] = setting.AppSubURL + "/-/admin/hooks"
def["BaseLinkNew"] = setting.AppSubURL + "/-/admin/default-hooks"
if err != nil {
ctx.ServerError("GetWebhooksAdmin", err)
return
@ -67,5 +67,5 @@ func DeleteDefaultOrSystemWebhook(ctx *context.Context) {
ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success"))
}
ctx.JSONRedirect(setting.AppSubURL + "/admin/hooks")
ctx.JSONRedirect(setting.AppSubURL + "/-/admin/hooks")
}

View File

@ -74,5 +74,5 @@ func EmptyNotices(ctx *context.Context) {
log.Trace("System notices deleted by admin (%s): [start: %d]", ctx.Doer.Name, 0)
ctx.Flash.Success(ctx.Tr("admin.notices.delete_success"))
ctx.Redirect(setting.AppSubURL + "/admin/notices")
ctx.Redirect(setting.AppSubURL + "/-/admin/notices")
}

View File

@ -99,7 +99,7 @@ func DeletePackageVersion(ctx *context.Context) {
}
ctx.Flash.Success(ctx.Tr("packages.settings.delete.success"))
ctx.JSONRedirect(setting.AppSubURL + "/admin/packages?page=" + url.QueryEscape(ctx.FormString("page")) + "&q=" + url.QueryEscape(ctx.FormString("q")) + "&type=" + url.QueryEscape(ctx.FormString("type")))
ctx.JSONRedirect(setting.AppSubURL + "/-/admin/packages?page=" + url.QueryEscape(ctx.FormString("page")) + "&q=" + url.QueryEscape(ctx.FormString("q")) + "&type=" + url.QueryEscape(ctx.FormString("type")))
}
func CleanupExpiredData(ctx *context.Context) {
@ -109,5 +109,5 @@ func CleanupExpiredData(ctx *context.Context) {
}
ctx.Flash.Success(ctx.Tr("admin.packages.cleanup.success"))
ctx.Redirect(setting.AppSubURL + "/admin/packages")
ctx.Redirect(setting.AppSubURL + "/-/admin/packages")
}

View File

@ -53,7 +53,7 @@ func QueueSet(ctx *context.Context) {
maxNumber, err = strconv.Atoi(maxNumberStr)
if err != nil {
ctx.Flash.Error(ctx.Tr("admin.monitor.queue.settings.maxnumberworkers.error"))
ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
ctx.Redirect(setting.AppSubURL + "/-/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
return
}
if maxNumber < -1 {
@ -65,7 +65,7 @@ func QueueSet(ctx *context.Context) {
mq.SetWorkerMaxNumber(maxNumber)
ctx.Flash.Success(ctx.Tr("admin.monitor.queue.settings.changed"))
ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
ctx.Redirect(setting.AppSubURL + "/-/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
}
func QueueRemoveAllItems(ctx *context.Context) {
@ -85,5 +85,5 @@ func QueueRemoveAllItems(ctx *context.Context) {
}
ctx.Flash.Success(ctx.Tr("admin.monitor.queue.settings.remove_all_items_done"))
ctx.Redirect(setting.AppSubURL + "/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
ctx.Redirect(setting.AppSubURL + "/-/admin/monitor/queue/" + strconv.FormatInt(qid, 10))
}

View File

@ -58,7 +58,7 @@ func DeleteRepo(ctx *context.Context) {
log.Trace("Repository deleted: %s", repo.FullName())
ctx.Flash.Success(ctx.Tr("repo.settings.deletion_success"))
ctx.JSONRedirect(setting.AppSubURL + "/admin/repos?page=" + url.QueryEscape(ctx.FormString("page")) + "&sort=" + url.QueryEscape(ctx.FormString("sort")))
ctx.JSONRedirect(setting.AppSubURL + "/-/admin/repos?page=" + url.QueryEscape(ctx.FormString("page")) + "&sort=" + url.QueryEscape(ctx.FormString("sort")))
}
// UnadoptedRepos lists the unadopted repositories
@ -114,7 +114,7 @@ func AdoptOrDeleteRepository(ctx *context.Context) {
dirSplit := strings.SplitN(dir, "/", 2)
if len(dirSplit) != 2 {
ctx.Redirect(setting.AppSubURL + "/admin/repos")
ctx.Redirect(setting.AppSubURL + "/-/admin/repos")
return
}
@ -122,7 +122,7 @@ func AdoptOrDeleteRepository(ctx *context.Context) {
if err != nil {
if user_model.IsErrUserNotExist(err) {
log.Debug("User does not exist: %s", dirSplit[0])
ctx.Redirect(setting.AppSubURL + "/admin/repos")
ctx.Redirect(setting.AppSubURL + "/-/admin/repos")
return
}
ctx.ServerError("GetUserByName", err)
@ -160,5 +160,5 @@ func AdoptOrDeleteRepository(ctx *context.Context) {
}
ctx.Flash.Success(ctx.Tr("repo.delete_preexisting_success", dir))
}
ctx.Redirect(setting.AppSubURL + "/admin/repos/unadopted?search=true&q=" + url.QueryEscape(q) + "&page=" + url.QueryEscape(page))
ctx.Redirect(setting.AppSubURL + "/-/admin/repos/unadopted?search=true&q=" + url.QueryEscape(q) + "&page=" + url.QueryEscape(page))
}

View File

@ -9,5 +9,5 @@ import (
)
func RedirectToDefaultSetting(ctx *context.Context) {
ctx.Redirect(setting.AppSubURL + "/admin/actions/runners")
ctx.Redirect(setting.AppSubURL + "/-/admin/actions/runners")
}

View File

@ -42,5 +42,5 @@ func Stacktrace(ctx *context.Context) {
func StacktraceCancel(ctx *context.Context) {
pid := ctx.PathParam("pid")
process.GetManager().Cancel(process.IDType(pid))
ctx.JSONRedirect(setting.AppSubURL + "/admin/monitor/stacktrace")
ctx.JSONRedirect(setting.AppSubURL + "/-/admin/monitor/stacktrace")
}

View File

@ -215,14 +215,14 @@ func NewUserPost(ctx *context.Context) {
}
ctx.Flash.Success(ctx.Tr("admin.users.new_success", u.Name))
ctx.Redirect(setting.AppSubURL + "/admin/users/" + strconv.FormatInt(u.ID, 10))
ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + strconv.FormatInt(u.ID, 10))
}
func prepareUserInfo(ctx *context.Context) *user_model.User {
u, err := user_model.GetUserByID(ctx, ctx.PathParamInt64(":userid"))
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Redirect(setting.AppSubURL + "/admin/users")
ctx.Redirect(setting.AppSubURL + "/-/admin/users")
} else {
ctx.ServerError("GetUserByID", err)
}
@ -481,7 +481,7 @@ func EditUserPost(ctx *context.Context) {
}
ctx.Flash.Success(ctx.Tr("admin.users.update_profile_success"))
ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.PathParam(":userid")))
ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam(":userid")))
}
// DeleteUser response for deleting a user
@ -495,7 +495,7 @@ func DeleteUser(ctx *context.Context) {
// admin should not delete themself
if u.ID == ctx.Doer.ID {
ctx.Flash.Error(ctx.Tr("admin.users.cannot_delete_self"))
ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.PathParam(":userid")))
ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam(":userid")))
return
}
@ -503,16 +503,16 @@ func DeleteUser(ctx *context.Context) {
switch {
case models.IsErrUserOwnRepos(err):
ctx.Flash.Error(ctx.Tr("admin.users.still_own_repo"))
ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.PathParam(":userid")))
ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam(":userid")))
case models.IsErrUserHasOrgs(err):
ctx.Flash.Error(ctx.Tr("admin.users.still_has_org"))
ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.PathParam(":userid")))
ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam(":userid")))
case models.IsErrUserOwnPackages(err):
ctx.Flash.Error(ctx.Tr("admin.users.still_own_packages"))
ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.PathParam(":userid")))
ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam(":userid")))
case models.IsErrDeleteLastAdminUser(err):
ctx.Flash.Error(ctx.Tr("auth.last_admin"))
ctx.Redirect(setting.AppSubURL + "/admin/users/" + url.PathEscape(ctx.PathParam(":userid")))
ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + url.PathEscape(ctx.PathParam(":userid")))
default:
ctx.ServerError("DeleteUser", err)
}
@ -521,7 +521,7 @@ func DeleteUser(ctx *context.Context) {
log.Trace("Account deleted by admin (%s): %s", ctx.Doer.Name, u.Name)
ctx.Flash.Success(ctx.Tr("admin.users.deletion_success"))
ctx.Redirect(setting.AppSubURL + "/admin/users")
ctx.Redirect(setting.AppSubURL + "/-/admin/users")
}
// AvatarPost response for change user's avatar request
@ -538,7 +538,7 @@ func AvatarPost(ctx *context.Context) {
ctx.Flash.Success(ctx.Tr("settings.update_user_avatar_success"))
}
ctx.Redirect(setting.AppSubURL + "/admin/users/" + strconv.FormatInt(u.ID, 10))
ctx.Redirect(setting.AppSubURL + "/-/admin/users/" + strconv.FormatInt(u.ID, 10))
}
// DeleteAvatar render delete avatar page
@ -552,5 +552,5 @@ func DeleteAvatar(ctx *context.Context) {
ctx.Flash.Error(err.Error())
}
ctx.JSONRedirect(setting.AppSubURL + "/admin/users/" + strconv.FormatInt(u.ID, 10))
ctx.JSONRedirect(setting.AppSubURL + "/-/admin/users/" + strconv.FormatInt(u.ID, 10))
}

View File

@ -98,7 +98,7 @@ func autoSignIn(ctx *context.Context) (bool, error) {
return false, err
}
ctx.Csrf.DeleteCookie(ctx)
ctx.Csrf.PrepareForSessionUser(ctx)
return true, nil
}
@ -359,8 +359,8 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req)
}
// Clear whatever CSRF cookie has right now, force to generate a new one
ctx.Csrf.DeleteCookie(ctx)
// force to generate a new CSRF token
ctx.Csrf.PrepareForSessionUser(ctx)
// Register last login
if err := user_service.UpdateUser(ctx, u, &user_service.UpdateOptions{SetLastLogin: true}); err != nil {
@ -804,6 +804,8 @@ func handleAccountActivation(ctx *context.Context, user *user_model.User) {
return
}
ctx.Csrf.PrepareForSessionUser(ctx)
if err := resetLocale(ctx, user); err != nil {
ctx.ServerError("resetLocale", err)
return

View File

@ -358,8 +358,8 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
return
}
// Clear whatever CSRF cookie has right now, force to generate a new one
ctx.Csrf.DeleteCookie(ctx)
// force to generate a new CSRF token
ctx.Csrf.PrepareForSessionUser(ctx)
if err := resetLocale(ctx, u); err != nil {
ctx.ServerError("resetLocale", err)

View File

@ -166,7 +166,7 @@ func setMergeTarget(ctx *context.Context, pull *issues_model.PullRequest) {
ctx.Data["BaseTarget"] = pull.BaseBranch
headBranchLink := ""
if pull.Flow == issues_model.PullRequestFlowGithub {
b, err := git_model.GetBranch(ctx, ctx.Repo.Repository.ID, pull.HeadBranch)
b, err := git_model.GetBranch(ctx, pull.HeadRepoID, pull.HeadBranch)
switch {
case err == nil:
if !b.IsDeleted {
@ -887,8 +887,6 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
}
if pull.HeadRepo != nil {
ctx.Data["SourcePath"] = pull.HeadRepo.Link() + "/src/commit/" + endCommitID
if !pull.HasMerged && ctx.Doer != nil {
perm, err := access_model.GetUserRepoPermission(ctx, pull.HeadRepo, ctx.Doer)
if err != nil {

View File

@ -76,7 +76,7 @@ func getRunnersCtx(ctx *context.Context) (*runnersCtx, error) {
IsAdmin: true,
RunnersTemplate: tplAdminRunners,
RunnerEditTemplate: tplAdminRunnerEdit,
RedirectLink: setting.AppSubURL + "/admin/actions/runners/",
RedirectLink: setting.AppSubURL + "/-/admin/actions/runners/",
}, nil
}

View File

@ -74,7 +74,7 @@ func getVariablesCtx(ctx *context.Context) (*variablesCtx, error) {
RepoID: 0,
IsGlobal: true,
VariablesTemplate: tplAdminVariables,
RedirectLink: setting.AppSubURL + "/admin/actions/variables",
RedirectLink: setting.AppSubURL + "/-/admin/actions/variables",
}, nil
}

View File

@ -100,8 +100,8 @@ func getOwnerRepoCtx(ctx *context.Context) (*ownerRepoCtx, error) {
return &ownerRepoCtx{
IsAdmin: true,
IsSystemWebhook: ctx.PathParam(":configType") == "system-hooks",
Link: path.Join(setting.AppSubURL, "/admin/hooks"),
LinkNew: path.Join(setting.AppSubURL, "/admin/", ctx.PathParam(":configType")),
Link: path.Join(setting.AppSubURL, "/-/admin/hooks"),
LinkNew: path.Join(setting.AppSubURL, "/-/admin/", ctx.PathParam(":configType")),
NewTemplate: tplAdminHookNew,
}, nil
}

View File

@ -69,6 +69,11 @@ func ProfilePost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.UpdateProfileForm)
if form.Name != "" {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureChangeUsername) {
ctx.Flash.Error(ctx.Tr("user.form.change_username_disabled"))
ctx.Redirect(setting.AppSubURL + "/user/settings")
return
}
if err := user_service.RenameUser(ctx, ctx.Doer, form.Name); err != nil {
switch {
case user_model.IsErrUserIsNotLocal(err):
@ -91,7 +96,6 @@ func ProfilePost(ctx *context.Context) {
}
opts := &user_service.UpdateOptions{
FullName: optional.Some(form.FullName),
KeepEmailPrivate: optional.Some(form.KeepEmailPrivate),
Description: optional.Some(form.Description),
Website: optional.Some(form.Website),
@ -99,6 +103,16 @@ func ProfilePost(ctx *context.Context) {
Visibility: optional.Some(form.Visibility),
KeepActivityPrivate: optional.Some(form.KeepActivityPrivate),
}
if form.FullName != "" {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureChangeFullName) {
ctx.Flash.Error(ctx.Tr("user.form.change_full_name_disabled"))
ctx.Redirect(setting.AppSubURL + "/user/settings")
return
}
opts.FullName = optional.Some(form.FullName)
}
if err := user_service.UpdateUser(ctx, ctx.Doer, opts); err != nil {
ctx.ServerError("UpdateUser", err)
return

View File

@ -683,7 +683,7 @@ func registerRoutes(m *web.Router) {
adminReq := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: true, AdminRequired: true})
// ***** START: Admin *****
m.Group("/admin", func() {
m.Group("/-/admin", func() {
m.Get("", admin.Dashboard)
m.Get("/system_status", admin.SystemStatus)
m.Post("", web.Bind(forms.AdminDashboardForm{}), admin.DashboardPost)
@ -1461,6 +1461,35 @@ func registerRoutes(m *web.Router) {
)
// end "/{username}/{reponame}/activity"
m.Group("/{username}/{reponame}", func() {
m.Group("/pulls/{index}", func() {
m.Get("", repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewIssue)
m.Get(".diff", repo.DownloadPullDiff)
m.Get(".patch", repo.DownloadPullPatch)
m.Group("/commits", func() {
m.Get("", context.RepoRef(), repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewPullCommits)
m.Get("/list", context.RepoRef(), repo.GetPullCommits)
m.Get("/{sha:[a-f0-9]{7,40}}", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForSingleCommit)
})
m.Post("/merge", context.RepoMustNotBeArchived(), web.Bind(forms.MergePullRequestForm{}), repo.MergePullRequest)
m.Post("/cancel_auto_merge", context.RepoMustNotBeArchived(), repo.CancelAutoMergePullRequest)
m.Post("/update", repo.UpdatePullRequest)
m.Post("/set_allow_maintainer_edit", web.Bind(forms.UpdateAllowEditsForm{}), repo.SetAllowEdits)
m.Post("/cleanup", context.RepoMustNotBeArchived(), context.RepoRef(), repo.CleanUpPullRequest)
m.Group("/files", func() {
m.Get("", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForAllCommitsOfPr)
m.Get("/{sha:[a-f0-9]{7,40}}", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesStartingFromCommit)
m.Get("/{shaFrom:[a-f0-9]{7,40}}..{shaTo:[a-f0-9]{7,40}}", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForRange)
m.Group("/reviews", func() {
m.Get("/new_comment", repo.RenderNewCodeCommentForm)
m.Post("/comments", web.Bind(forms.CodeCommentForm{}), repo.SetShowOutdatedComments, repo.CreateCodeComment)
m.Post("/submit", web.Bind(forms.SubmitReviewForm{}), repo.SubmitReview)
}, context.RepoMustNotBeArchived())
})
})
}, ignSignIn, context.RepoAssignment, repo.MustAllowPulls, reqRepoPullsReader)
// end "/{username}/{reponame}/pulls/{index}": repo pull request
m.Group("/{username}/{reponame}", func() {
m.Group("/activity_author_data", func() {
m.Get("", repo.ActivityAuthors)
@ -1499,32 +1528,6 @@ func registerRoutes(m *web.Router) {
return cancel
})
m.Group("/pulls/{index}", func() {
m.Get("", repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewIssue)
m.Get(".diff", repo.DownloadPullDiff)
m.Get(".patch", repo.DownloadPullPatch)
m.Group("/commits", func() {
m.Get("", context.RepoRef(), repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewPullCommits)
m.Get("/list", context.RepoRef(), repo.GetPullCommits)
m.Get("/{sha:[a-f0-9]{7,40}}", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForSingleCommit)
})
m.Post("/merge", context.RepoMustNotBeArchived(), web.Bind(forms.MergePullRequestForm{}), repo.MergePullRequest)
m.Post("/cancel_auto_merge", context.RepoMustNotBeArchived(), repo.CancelAutoMergePullRequest)
m.Post("/update", repo.UpdatePullRequest)
m.Post("/set_allow_maintainer_edit", web.Bind(forms.UpdateAllowEditsForm{}), repo.SetAllowEdits)
m.Post("/cleanup", context.RepoMustNotBeArchived(), context.RepoRef(), repo.CleanUpPullRequest)
m.Group("/files", func() {
m.Get("", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForAllCommitsOfPr)
m.Get("/{sha:[a-f0-9]{7,40}}", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesStartingFromCommit)
m.Get("/{shaFrom:[a-f0-9]{7,40}}..{shaTo:[a-f0-9]{7,40}}", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForRange)
m.Group("/reviews", func() {
m.Get("/new_comment", repo.RenderNewCodeCommentForm)
m.Post("/comments", web.Bind(forms.CodeCommentForm{}), repo.SetShowOutdatedComments, repo.CreateCodeComment)
m.Post("/submit", web.Bind(forms.SubmitReviewForm{}), repo.SubmitReview)
}, context.RepoMustNotBeArchived())
})
}, repo.MustAllowPulls)
m.Group("/media", func() {
m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.SingleDownloadOrLFS)
m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.SingleDownloadOrLFS)

View File

@ -116,11 +116,20 @@ func (input *notifyInput) Notify(ctx context.Context) {
}
func notify(ctx context.Context, input *notifyInput) error {
shouldDetectSchedules := input.Event == webhook_module.HookEventPush && input.Ref.BranchName() == input.Repo.DefaultBranch
if input.Doer.IsActions() {
// avoiding triggering cyclically, for example:
// a comment of an issue will trigger the runner to add a new comment as reply,
// and the new comment will trigger the runner again.
log.Debug("ignore executing %v for event %v whose doer is %v", getMethod(ctx), input.Event, input.Doer.Name)
// we should update schedule tasks in this case, because
// 1. schedule tasks cannot be triggered by other events, so cyclic triggering will not occur
// 2. some schedule tasks may update the repo periodically, so the refs of schedule tasks need to be updated
if shouldDetectSchedules {
return DetectAndHandleSchedules(ctx, input.Repo)
}
return nil
}
if input.Repo.IsEmpty || input.Repo.IsArchived {
@ -174,7 +183,6 @@ func notify(ctx context.Context, input *notifyInput) error {
var detectedWorkflows []*actions_module.DetectedWorkflow
actionsConfig := input.Repo.MustGetUnit(ctx, unit_model.TypeActions).ActionsConfig()
shouldDetectSchedules := input.Event == webhook_module.HookEventPush && input.Ref.BranchName() == input.Repo.DefaultBranch
workflows, schedules, err := actions_module.DetectWorkflows(gitRepo, commit,
input.Event,
input.Payload,

View File

@ -7,7 +7,6 @@ import (
"context"
"fmt"
"os"
"strconv"
"strings"
issues_model "code.gitea.io/gitea/models/issues"
@ -24,10 +23,10 @@ import (
// ProcReceive handle proc receive work
func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, opts *private.HookOptions) ([]private.HookProcReceiveRefResult, error) {
results := make([]private.HookProcReceiveRefResult, 0, len(opts.OldCommitIDs))
forcePush := opts.GitPushOptions.Bool(private.GitPushOptionForcePush)
topicBranch := opts.GitPushOptions["topic"]
forcePush, _ := strconv.ParseBool(opts.GitPushOptions["force-push"])
title := strings.TrimSpace(opts.GitPushOptions["title"])
description := strings.TrimSpace(opts.GitPushOptions["description"]) // TODO: Add more options?
description := strings.TrimSpace(opts.GitPushOptions["description"])
objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
userName := strings.ToLower(opts.UserName)
@ -56,19 +55,19 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
}
baseBranchName := opts.RefFullNames[i].ForBranchName()
curentTopicBranch := ""
currentTopicBranch := ""
if !gitRepo.IsBranchExist(baseBranchName) {
// try match refs/for/<target-branch>/<topic-branch>
for p, v := range baseBranchName {
if v == '/' && gitRepo.IsBranchExist(baseBranchName[:p]) && p != len(baseBranchName)-1 {
curentTopicBranch = baseBranchName[p+1:]
currentTopicBranch = baseBranchName[p+1:]
baseBranchName = baseBranchName[:p]
break
}
}
}
if len(topicBranch) == 0 && len(curentTopicBranch) == 0 {
if len(topicBranch) == 0 && len(currentTopicBranch) == 0 {
results = append(results, private.HookProcReceiveRefResult{
OriginalRef: opts.RefFullNames[i],
OldOID: opts.OldCommitIDs[i],
@ -78,18 +77,18 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
continue
}
if len(curentTopicBranch) == 0 {
curentTopicBranch = topicBranch
if len(currentTopicBranch) == 0 {
currentTopicBranch = topicBranch
}
// because different user maybe want to use same topic,
// So it's better to make sure the topic branch name
// has user name prefix
// has username prefix
var headBranch string
if !strings.HasPrefix(curentTopicBranch, userName+"/") {
headBranch = userName + "/" + curentTopicBranch
if !strings.HasPrefix(currentTopicBranch, userName+"/") {
headBranch = userName + "/" + currentTopicBranch
} else {
headBranch = curentTopicBranch
headBranch = currentTopicBranch
}
pr, err := issues_model.GetUnmergedPullRequest(ctx, repo.ID, repo.ID, headBranch, baseBranchName, issues_model.PullRequestFlowAGit)
@ -178,7 +177,7 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
continue
}
if !forcePush {
if !forcePush.Value() {
output, _, err := git.NewCommand(ctx, "rev-list", "--max-count=1").
AddDynamicArguments(oldCommitID, "^"+opts.NewCommitIDs[i]).
RunStdString(&git.RunOpts{Dir: repo.RepoPath(), Env: os.Environ()})

View File

@ -103,8 +103,8 @@ func handleSignIn(resp http.ResponseWriter, req *http.Request, sess SessionStore
middleware.SetLocaleCookie(resp, user.Language, 0)
// Clear whatever CSRF has right now, force to generate a new one
// force to generate a new CSRF token
if ctx := gitea_context.GetWebContext(req); ctx != nil {
ctx.Csrf.DeleteCookie(ctx)
ctx.Csrf.PrepareForSessionUser(ctx)
}
}

View File

@ -35,9 +35,10 @@ type APIContext struct {
ContextUser *user_model.User // the user which is being visited, in most cases it differs from Doer
Repo *Repository
Org *APIOrganization
Package *Package
Repo *Repository
Org *APIOrganization
Package *Package
PublicOnly bool // Whether the request is for a public endpoint
}
func init() {

Some files were not shown because too many files have changed in this diff Show More