diff --git a/cmd/hook.go b/cmd/hook.go index 11d0d072c97..578380ab40d 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -591,8 +591,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) @@ -617,8 +618,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) @@ -629,11 +628,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)) } } diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml index 8a22db0445c..f6b6252da1f 100644 --- a/models/fixtures/repo_unit.yml +++ b/models/fixtures/repo_unit.yml @@ -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 diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index e141593f415..b7970cb7c82 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -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 diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml index 8504d88ce59..c0296deec55 100644 --- a/models/fixtures/user.yml +++ b/models/fixtures/user.yml @@ -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 diff --git a/models/repo/repo_list_test.go b/models/repo/repo_list_test.go index 88cfcde6208..ca6007f6c78 100644 --- a/models/repo/repo_list_test.go +++ b/models/repo/repo_list_test.go @@ -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", diff --git a/models/user/user.go b/models/user/user.go index d5c4833cdef..c1cb988e43d 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -565,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"} ) diff --git a/models/user/user_test.go b/models/user/user_test.go index 67efb3859fd..bc1abc64512 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -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 diff --git a/modules/git/git.go b/modules/git/git.go index 05ca2608555..a19dd7771ba 100644 --- a/modules/git/git.go +++ b/modules/git/git.go @@ -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) diff --git a/modules/indexer/code/bleve/bleve.go b/modules/indexer/code/bleve/bleve.go index c17f56d3cff..90e5e62bcb4 100644 --- a/modules/indexer/code/bleve/bleve.go +++ b/modules/indexer/code/bleve/bleve.go @@ -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 { diff --git a/modules/indexer/code/bleve/token/path/path.go b/modules/indexer/code/bleve/token/path/path.go new file mode 100644 index 00000000000..107e0da1090 --- /dev/null +++ b/modules/indexer/code/bleve/token/path/path.go @@ -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) +} diff --git a/modules/indexer/code/bleve/token/path/path_test.go b/modules/indexer/code/bleve/token/path/path_test.go new file mode 100644 index 00000000000..cc52021ef7f --- /dev/null +++ b/modules/indexer/code/bleve/token/path/path_test.go @@ -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)) +} diff --git a/modules/indexer/code/elasticsearch/elasticsearch.go b/modules/indexer/code/elasticsearch/elasticsearch.go index 5c01034450b..669a1bafcc9 100644 --- a/modules/indexer/code/elasticsearch/elasticsearch.go +++ b/modules/indexer/code/elasticsearch/elasticsearch.go @@ -30,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" @@ -57,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", @@ -136,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), @@ -231,11 +270,11 @@ func (b *Indexer) doDelete(ctx context.Context, repoID int64) error { 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 @@ -244,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 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 and for the keywords, // now we should find the positions. But how to avoid html content which contains the // and tags? If elastic search has handled that? - startIndex, endIndex = indexPos(c[0], "", "") + startIndex, endIndex = contentMatchIndexPos(c[0], "", "") if startIndex == -1 { panic(fmt.Sprintf("1===%s,,,%#v,,,%s", kw, hit.Highlight, c[0])) } @@ -267,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{ @@ -283,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 since we give Content the original data + EndIndex: endIndex, Color: enry.GetColor(language), }) } @@ -315,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 { @@ -341,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"), ). @@ -373,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"), ). diff --git a/modules/indexer/code/elasticsearch/elasticsearch_test.go b/modules/indexer/code/elasticsearch/elasticsearch_test.go index c6ba93e76d4..a6d2af92b2b 100644 --- a/modules/indexer/code/elasticsearch/elasticsearch_test.go +++ b/modules/indexer/code/elasticsearch/elasticsearch_test.go @@ -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) } diff --git a/modules/indexer/code/indexer_test.go b/modules/indexer/code/indexer_test.go index 8975c5ce408..5b33528dcde 100644 --- a/modules/indexer/code/indexer_test.go +++ b/modules/indexer/code/indexer_test.go @@ -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} +} diff --git a/modules/indexer/code/internal/util.go b/modules/indexer/code/internal/util.go index 689c4f4584b..5b95783d9fc 100644 --- a/modules/indexer/code/internal/util.go +++ b/modules/indexer/code/internal/util.go @@ -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) +} diff --git a/modules/indexer/internal/bleve/util.go b/modules/indexer/internal/bleve/util.go index a2265f86e6b..b426b39bc20 100644 --- a/modules/indexer/internal/bleve/util.go +++ b/modules/indexer/internal/bleve/util.go @@ -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) } diff --git a/modules/indexer/internal/bleve/util_test.go b/modules/indexer/internal/bleve/util_test.go new file mode 100644 index 00000000000..ae0b12c08d4 --- /dev/null +++ b/modules/indexer/internal/bleve/util_test.go @@ -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)) + }) + } +} diff --git a/modules/private/hook.go b/modules/private/hook.go index 49d92987441..745c200619f 100644 --- a/modules/private/hook.go +++ b/modules/private/hook.go @@ -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 diff --git a/modules/private/pushoptions.go b/modules/private/pushoptions.go new file mode 100644 index 00000000000..7616e6b9417 --- /dev/null +++ b/modules/private/pushoptions.go @@ -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]] = "" + } +} diff --git a/modules/private/pushoptions_test.go b/modules/private/pushoptions_test.go new file mode 100644 index 00000000000..98b15522127 --- /dev/null +++ b/modules/private/pushoptions_test.go @@ -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()) +} diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index e64c85b7a4e..f58ce74564a 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -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 d’utilisateur 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. @@ -1039,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 choisir une licence. +multiple_licenses=Licences multiples object_format=Format d'objet object_format_helper=Format d’objet pour ce dépôt. Ne peut être modifié plus tard. SHA1 est le plus compatible. readme=LISEZMOI @@ -1834,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 n’est pas suffisamment approuvée. %d approbations obtenues sur %d. +pulls.blocked_by_approvals=Cette demande d’ajout n’est pas suffisamment approuvée. %d approbations obtenues sur %d. pulls.blocked_by_approvals_whitelisted=Cette demande d’ajout n’a pas encore assez d’approbations. %d sur %d approbations de la part des utilisateurs ou équipes sur la liste autorisée. pulls.blocked_by_rejection=Cette demande d’ajout nécessite des corrections sollicitées par un évaluateur officiel. pulls.blocked_by_official_review_requests=Cette demande d’ajout a des sollicitations officielles d’évaluation. @@ -2940,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 l’indexeur 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 diff --git a/options/locale/locale_ga-IE.ini b/options/locale/locale_ga-IE.ini index 82209b1b118..0fae28daeae 100644 --- a/options/locale/locale_ga-IE.ini +++ b/options/locale/locale_ga-IE.ini @@ -2890,17 +2890,167 @@ dashboard.delete_generated_repository_avatars=Scrios abhatáranna stórtha ginte dashboard.sync_repo_branches=Sync brainsí caillte ó shonraí git go bunachair sonraí dashboard.sync_repo_tags=Clibeanna sioncraigh ó shonraí git go bunachar sonraí dashboard.update_mirrors=Scátháin a nuashonrú +dashboard.repo_health_check=Seiceáil sláinte gach stóras +dashboard.check_repo_stats=Seiceáil gach staitisticí stórais +dashboard.archive_cleanup=Scrios sean-chartlanna stórais +dashboard.deleted_branches_cleanup=Brainsí scriosta a ghlanadh +dashboard.update_migration_poster_id=Nuashonraigh ID póstaer imir +dashboard.git_gc_repos=Bailíonn truflais gach stórais +dashboard.resync_all_sshkeys=Nuashonraigh an comhad '.ssh/authorized_keys' le heochracha Gitea SSH. +dashboard.resync_all_sshprincipals=Nuashonraigh an comhad '.ssh/authorized_principals' le príomhphrionsabail Gitea SSH. +dashboard.resync_all_hooks=Athshioncrónaigh crúcaí réamhfhála, nuashonraithe agus iar-fhála na stórtha go léir. +dashboard.reinit_missing_repos=Aththosaigh gach stórais Git atá in easnamh a bhfuil taifid ann dóibh +dashboard.sync_external_users=Sioncrónaigh sonraí úsáideoirí seachtracha +dashboard.cleanup_hook_task_table=Tábla hook_task glantacháin +dashboard.cleanup_packages=Pacáistí glanta in éag +dashboard.cleanup_actions=Gníomhaíochtaí glanta in éag acmhainní +dashboard.server_uptime=Aga fónaimh Freastalaí +dashboard.current_goroutine=Goroutines Reatha +dashboard.current_memory_usage=Úsáid Cuimhne Reatha +dashboard.total_memory_allocated=Cuimhne Iomlán Leithdháilte +dashboard.memory_obtained=Cuimhne Faighte +dashboard.pointer_lookup_times=Amanna Cuardaigh Pointeora +dashboard.memory_allocate_times=Leithdháiltí Cuimhne +dashboard.memory_free_times=Saorálann Cuimhne +dashboard.current_heap_usage=Úsáid Charn Reatha +dashboard.heap_memory_obtained=Cuimhne Charn Faighte +dashboard.heap_memory_idle=Díomhaoin Cuimhne Carn +dashboard.heap_memory_in_use=Cuimhne Carm In Úsáid +dashboard.heap_memory_released=Cuimhne Carn Eisithe +dashboard.heap_objects=Cuspóirí Carn +dashboard.bootstrap_stack_usage=Úsáid Staca Bootstrap +dashboard.stack_memory_obtained=Cuimhne Staca Faighte +dashboard.mspan_structures_usage=Úsáid Struchtúir MSpan +dashboard.mspan_structures_obtained=Struchtúir MSpan a Faightear +dashboard.mcache_structures_usage=Úsáid Struchtúir MCache +dashboard.mcache_structures_obtained=Struchtúir MCache a Faightear +dashboard.profiling_bucket_hash_table_obtained=Tábla Hash Buicéad Próifílithe a Faightear +dashboard.gc_metadata_obtained=Meiteashonraí GC faighte +dashboard.other_system_allocation_obtained=Leithdháileadh Córais Eile a Fuarthas +dashboard.next_gc_recycle=Athchúrsáil GC Eile +dashboard.last_gc_time=Ó Am Deiridh GC +dashboard.total_gc_time=Sos Iomlán GC +dashboard.total_gc_pause=Sos Iomlán GC +dashboard.last_gc_pause=Sos GC Deireanach +dashboard.gc_times=Amanna GC +dashboard.delete_old_actions=Scrios gach sean-ghníomhaíocht ón mbunachar +dashboard.delete_old_actions.started=Scrios na sean-ghníomhaíocht go léir ón mbunachar sonraí tosaithe. +dashboard.update_checker=Seiceoir nuashonraithe +dashboard.delete_old_system_notices=Scrios gach seanfhógra córais ón mbunachar sonraí +dashboard.gc_lfs=Bailigh truflais meta rudaí LFS +dashboard.stop_zombie_tasks=Stad gníomhartha tascanna zombie +dashboard.stop_endless_tasks=Stad gníomhartha tascanna gan deireadh +dashboard.cancel_abandoned_jobs=Cealaigh gníomhartha poist tréigthe +dashboard.start_schedule_tasks=Tosaigh tascanna sceideal gníom +dashboard.sync_branch.started=Thosaigh Brainsí Sioncronú +dashboard.sync_tag.started=Clibeanna Thosaigh Sioncronú +dashboard.rebuild_issue_indexer=Atógáil innéacsóir eisiúna dashboard.sync_repo_licenses=Sioncronaigh ceadúnais repo +users.user_manage_panel=Bainistíocht Cuntas Úsáideora +users.new_account=Cruthaigh cuntas Úsáideora +users.name=Ainm úsáideora users.full_name=Ainm Iomlán +users.activated=Gníomhachtaithe +users.admin=Riarachán +users.restricted=Srianta +users.reserved=In áirithe +users.bot=Bota +users.remote=Iargúlta +users.2fa=2FA +users.repos=Stórais +users.created=Cruthaithe +users.last_login=Sínigh Isteach Deiridh +users.never_login=Ná Sínigh Isteach riamh +users.send_register_notify=Seol Fógra um Chlárú Úsáideora +users.new_success=Tá an cuntas úsáideora "%s" cruthaithe. +users.edit=Eagar +users.auth_source=Foinse Fíordheimhnithe +users.local=Áitiúil +users.auth_login_name=Ainm Síniú Isteach Fíordheimhnithe +users.password_helper=Fág an pasfhocal folamh chun é a choinneáil gan athrú. +users.update_profile_success=Nuashonraíodh an cuntas úsáideora. +users.edit_account=Cuir Cuntas Úsáideora in Eagar +users.max_repo_creation=Uasmhéid Stóras +users.max_repo_creation_desc=(Cuir isteach -1 chun an teorainn réamhshocraithe domhanda a úsáid.) +users.is_activated=Gníomhachtaítear Cuntas Úsáideora +users.prohibit_login=Díchumasaigh Síniú Isteach +users.is_admin=Is Riarthóir +users.is_restricted=Is Srianta +users.allow_git_hook=Féadfaidh Git Hooks a Chruthú +users.allow_git_hook_tooltip=Déantar Git Hooks a fhorghníomhú mar úsáideoir OS a ritheann Gitea agus beidh an leibhéal céanna rochtana óstaigh aige. Mar thoradh air sin, is féidir le húsáideoirí a bhfuil an phribhléid speisialta Git Hook seo acu rochtain a fháil ar gach stór Gitea agus iad a mhodhnú chomh maith leis an mbunachar sonraí a úsáideann Gitea. Dá bharr sin tá siad in ann pribhléidí riarthóra Gitea a fháil freisin. +users.allow_import_local=Is féidir Stórais Áitiúla a Allmhairiú +users.allow_create_organization=Is féidir Eagraíochtaí a Chruthú +users.update_profile=Nuashonraigh Cuntas Úsáideora +users.delete_account=Scrios Cuntas Úsáide +users.cannot_delete_self=Ní féidir leat tú féin a scriosadh +users.still_own_repo=Tá stórais amháin nó níos mó fós ag an úsáideoir seo. Scrios nó aistrigh na stórais seo ar dtús. +users.still_has_org=Is ball d'eagraíocht é an t-úsáideoir seo. Bain an t-úsáideoir ó aon eagraíochtaí ar dtús. +users.purge=Úsáideoir a Ghlanadh +users.purge_help=Scrios go héigeantach úsáideoir agus aon stórais, eagraíochtaí agus pacáistí atá faoi úinéireacht an úsáideora. Scriosfar gach trácht freisin. +users.still_own_packages=Tá pacáiste amháin nó níos mó fós ag an úsáideoir seo, scrios na pacáistí seo ar dtús. +users.deletion_success=Scriosadh an cuntas úsáideora. +users.reset_2fa=Athshocraigh 2FA +users.list_status_filter.menu_text=Scagaire +users.list_status_filter.reset=Athshocraigh users.list_status_filter.is_active=Gníomhach +users.list_status_filter.not_active=Neamhghníomhach +users.list_status_filter.is_admin=Riarachán +users.list_status_filter.not_admin=Ní Riarachán +users.list_status_filter.is_restricted=Srianta +users.list_status_filter.not_restricted=Gan Srian +users.list_status_filter.is_prohibit_login=Cosc ar Logáil Isteach +users.list_status_filter.not_prohibit_login=Ceadaigh Logáil isteach +users.list_status_filter.is_2fa_enabled=2FA Cumasaithe +users.list_status_filter.not_2fa_enabled=2FA faoi mhíchumas +users.details=Sonraí Úsáideora +emails.email_manage_panel=Bainistíocht Ríomhphost Úsáideoir +emails.primary=Bunscoile +emails.activated=Gníomhachtaithe +emails.filter_sort.email=Ríomhphost +emails.filter_sort.email_reverse=Ríomhphost (droim ar ais) +emails.filter_sort.name=Ainm Úsáideora +emails.filter_sort.name_reverse=Ainm Úsáideora (droim ar ais) +emails.updated=Nuashonraíodh an ríomhphost +emails.not_updated=Theip ar an seoladh ríomhphoist iarrtha a nuashonrú: %v +emails.duplicate_active=Tá an seoladh ríomhphoist seo gníomhach cheana féin d'úsáideoir difriúil. +emails.change_email_header=Nuashonraigh Airíonna Ríomhphoist +emails.change_email_text=An bhfuil tú cinnte gur mhaith leat an seoladh ríomhphoist seo a nuashonrú? +emails.delete=Scrios Ríomhphost +emails.delete_desc=An bhfuil tú cinnte gur mhaith leat an seoladh ríomhphoist seo a scriosadh? +emails.deletion_success=Tá an seoladh ríomhphoist scriosta. +emails.delete_primary_email_error=Ní féidir leat an ríomhphost príomhúil a scriosadh. +orgs.org_manage_panel=Bainistíocht Eagraíochta +orgs.name=Ainm orgs.teams=Foirne +orgs.members=Comhaltaí +orgs.new_orga=Eagraíocht Nua +repos.repo_manage_panel=Bainistíocht Stórais +repos.unadopted=Stórais Neamhghlactha +repos.unadopted.no_more=Níor aimsíodh níos mó stórais neamhghlactha repos.owner=Úinéir +repos.name=Ainm +repos.private=Príobháideach +repos.issues=Saincheisteanna +repos.size=Méid +repos.lfs_size=Méid LFS +packages.package_manage_panel=Bainistíocht Pacáiste +packages.total_size=Méid Iomlán: %s +packages.unreferenced_size=Méid gan tagairt: %s +packages.cleanup=Glan suas sonraí in éag +packages.cleanup.success=Glanadh suas sonraí in éag go rathúil packages.owner=Úinéir +packages.creator=Cruthaitheoir +packages.name=Ainm +packages.version=Leagan +packages.type=Cineál +packages.repository=Stóráil +packages.size=Méid +packages.published=Foilsithe defaulthooks=Réamhshocraithe Crúcaí Gréasán defaulthooks.desc=Déanann Crúcaí Gréasán iarratais HTTP POST go huathoibríoch chuig freastalaí nuair a chuireann imeachtaí áirithe Gitea tús. Is mainneachtainí iad na cuacha gréasáin a shainítear anseo agus déanfar iad a chóipeáil isteach i ngach stórais nua. Léigh tuilleadh sa treoir chúca Crúcaí Gréasán. @@ -2912,60 +3062,636 @@ systemhooks.desc=Déanann Crúcaí Gréasán iarratais HTTP POST go huathoibrío systemhooks.add_webhook=Cuir Crúca Gréasán Córas leis systemhooks.update_webhook=Nuashonraigh Córas Crúca Gréasán +auths.auth_manage_panel=Bainistiú Foinse Fíordheimhnithe +auths.new=Cuir Foinse Fíordheimhni +auths.name=Ainm +auths.type=Cineál +auths.enabled=Cumasaithe +auths.syncenabled=Cumasaigh Sioncrónú Úsáideora auths.updated=Nuashonraithe +auths.auth_type=Cineál Fíordheimhnithe +auths.auth_name=Ainm Fíordheimhnithe +auths.security_protocol=Prótacal Slándála auths.domain=Fearann +auths.host=Óstach +auths.port=Calafort +auths.bind_dn=Ceangail DN +auths.bind_password=Ceangail Pasfhocal +auths.user_base=Bonn Cuardaigh Úsáideora +auths.user_dn=Úsáideoir DN +auths.attribute_username=Tréith Ainm Úsáideora +auths.attribute_username_placeholder=Fág folamh chun an t-ainm úsáideora a iontráiltear i Gitea a úsáid. +auths.attribute_name=Tréith Céad Ainm +auths.attribute_surname=Tréith Sloinne +auths.attribute_mail=Tréith ríomhphoist +auths.attribute_ssh_public_key=Tréith Eochair SSH Phoiblí +auths.attribute_avatar=Tréith Avatar +auths.attributes_in_bind=Faigh tréithe i gComhthéacs Bind DN +auths.allow_deactivate_all=Lig do thoradh cuardaigh folamh gach úsáideoir a dhíghníomhachtú +auths.use_paged_search=Úsáid Cuardach Leathanaigh +auths.search_page_size=Méid an Leathanaigh +auths.filter=Scagaire Úsáideora +auths.admin_filter=Scagaire Riaracháin +auths.restricted_filter=Scagaire Srianta +auths.restricted_filter_helper=Fág folamh chun aon úsáideoirí a shocrú mar theoranta. Úsáid réiltín ('*') chun gach úsáideoir nach meaitseálann Scagaire Riaracháin a shocrú mar theoranta. +auths.verify_group_membership=Fíoraigh ballraíocht ghrúpa i LDAP (fág an scagaire folamh le scipeáil) +auths.group_search_base=Bonn Cuardaigh Grúpa DN +auths.group_attribute_list_users=Tréith Grúpa ina bhfuil Liosta Úsáideoirí +auths.user_attribute_in_group=Tréith Úsáideora atá Liostaithe i nGrúpa +auths.map_group_to_team=Léarscáil grúpaí LDAP chuig foirne na hEagraíochta (fág an réimse folamh le scipeáil) +auths.map_group_to_team_removal=Bain úsáideoirí ó fhoirne sioncronaithe mura mbaineann an t-úsáideoir leis an ngrúpa comhfhreagrach LDAP +auths.enable_ldap_groups=Cumasaigh grúpaí LDAP +auths.ms_ad_sa=MS AD Tréithe Cuardaigh +auths.smtp_auth=Cineál Fíordheimhnithe SMTP +auths.smtphost=Óstach SMTP +auths.smtpport=SMTP Calafort +auths.allowed_domains=Fearainn Ceadaithe +auths.allowed_domains_helper=Fág folamh chun gach fearann a cheadú. Déan ilfhearann a scaradh le camóg (','). +auths.skip_tls_verify=Scipeáil Fíorú TLS +auths.force_smtps=Fórsa SMTPS +auths.force_smtps_helper=Úsáidtear SMTPS i gcónaí ar chalafort 465. Socraigh é seo chun SMTPS a chur i bhfeidhm ar chalafoirt eile. (Seachas sin úsáidfear STARTTLS ar chalafoirt eile má thacaíonn an t-óstach leis.) +auths.helo_hostname=Ainm Óstach HELO +auths.helo_hostname_helper=Ainm óstach a sheoltar le HELO. Fág bán chun an t-ainm óstach reatha a sheoladh. +auths.disable_helo=Díchumasaigh HELO +auths.pam_service_name=Ainm Seirbhíse PAM +auths.pam_email_domain=Fearann Ríomhphoist PAM (roghnach) +auths.oauth2_provider=Soláthraí OAuth2 +auths.oauth2_icon_url=URL deilbhín +auths.oauth2_clientID=Aitheantas Cliant (Eochair) +auths.oauth2_clientSecret=Rúnda Cliant +auths.openIdConnectAutoDiscoveryURL=URL Fionnachtana Uathoibríoch OpenID Connect +auths.oauth2_use_custom_url=Úsáid URLanna Saincheaptha in ionad URLanna Réamhshocraithe +auths.oauth2_tokenURL=URL Comhartha +auths.oauth2_authURL=Údaraigh URL +auths.oauth2_profileURL=URL Próifíl +auths.oauth2_emailURL=URL ríomhphoist +auths.skip_local_two_fa=Scipeáil 2FA áitiúil +auths.skip_local_two_fa_helper=Ciallaíonn fágáil gan socrú go mbeidh ar úsáideoirí áitiúla a bhfuil tacar 2FA acu 2FA a rith fós chun logáil isteach +auths.oauth2_tenant=Tionónta +auths.oauth2_scopes=Scóipeanna Breise +auths.oauth2_required_claim_name=Ainm Éilimh Riachtanach +auths.oauth2_required_claim_name_helper=Socraigh an t-ainm seo chun logáil isteach ón bhfoinse seo a shrianadh d'úsáideoirí a bhfuil éileamh acu leis an ainm seo +auths.oauth2_required_claim_value=Luach Éilimh Riachtanach +auths.oauth2_required_claim_value_helper=Socraigh an luach seo chun logáil isteach ón bhfoinse seo a shrianadh chuig úsáideoirí a bhfuil éileamh acu leis an ainm agus an luach seo +auths.oauth2_group_claim_name=Ainm éileamh ag soláthar ainmneacha grúpa don fhoinse seo (Roghnach) +auths.oauth2_admin_group=Luach Éilimh Grúpa d'úsáideoirí riarthóra. (Roghnach - teastaíonn ainm éilimh thuas) +auths.oauth2_restricted_group=Luach Éilimh Grúpa d'úsáideoirí srianta. (Roghnach - teastaíonn ainm éilimh thuas) +auths.oauth2_map_group_to_team=Map mhaígh grúpaí chuig foirne Eagraíochta. (Roghnach - éilíonn ainm an éilimh thuas) +auths.oauth2_map_group_to_team_removal=Bain úsáideoirí ó fhoirne sioncronaithe mura mbaineann an t-úsáideoir leis an ngrúpa comhfhreagrach. +auths.enable_auto_register=Cumasaigh Clárú Auto +auths.sspi_auto_create_users=Cruthaigh úsáideoirí go huathoibríoch +auths.sspi_auto_create_users_helper=Lig do mhodh auth SSPI cuntais nua a chruthú go huathoibríoch d'úsáideoirí a logálann isteach den chéad uair +auths.sspi_auto_activate_users=Gníomhachtaigh úsáideoirí go huathoibríoch +auths.sspi_auto_activate_users_helper=Lig modh auth SSPI úsáideoirí nua a ghníomhachtú go huathoibríoch +auths.sspi_strip_domain_names=Bain ainmneacha fearann ó ainm úsáideora +auths.sspi_strip_domain_names_helper=Má dhéantar iad a sheiceáil, bainfear ainmneacha fearainn ó ainmneacha logála isteach (m.sh. Beidh “DOMAIN\ user” agus "user@example.org" araon ní bheidh ach “úsáideoir”). +auths.sspi_separator_replacement=Deighilteoir le húsáid in ionad\,/agus @ +auths.sspi_separator_replacement_helper=An carachtar a úsáidfear chun na deighilteoirí a chur in ionad na n-ainmneacha logála síos-leibhéil (m.sh. an \ i "DOMAIN\úsáideoir") agus ainmneacha príomhoidí úsáideora (m.sh. an @ in "user@example.org"). +auths.sspi_default_language=Teanga úsáideora réamhshocraithe +auths.sspi_default_language_helper=Teanga réamhshocraithe d'úsáideoirí cruthaithe go huathoibríoch ag modh auth SSPI. Fág folamh más fearr leat teanga a bhrath go huathoibríoch. +auths.tips=Leideanna +auths.tips.oauth2.general=OAuth2 Fíordheimhniú +auths.tips.oauth2.general.tip=Agus fíordheimhniú OAuth2 nua á chlárú agat, ba chóir go mbeadh an URL glaonna ais/atreoraithe: +auths.tip.oauth2_provider=Soláthraí OAuth2 +auths.tip.bitbucket=Cláraigh tomhaltóir OAuth nua ar %s agus cuir an cead 'Cuntas' - 'Léigh' leis +auths.tip.nextcloud=`Cláraigh tomhaltóir OAuth nua ar do chás ag baint úsáide as an roghchlár seo a leanas "Socruithe -> Slándáil -> cliant OAuth 2.0"` +auths.tip.dropbox=Cruthaigh feidhmchlár nua ag %s +auths.tip.facebook=Cláraigh feidhmchlár nua ag %s agus cuir an táirge "Facebook Login" leis +auths.tip.github=Cláraigh feidhmchlár OAuth nua ar %s +auths.tip.gitlab_new=Cláraigh feidhmchlár nua ar %s +auths.tip.google_plus=Faigh dintiúir chliaint OAuth2 ó chonsól API Google ag %s +auths.tip.openid_connect=Úsáid URL Fionnachtana OpenID Connect "https://{server}/.well-known/openid-configuration" chun na críochphointí a shonrú +auths.tip.twitter=Téigh go %s, cruthaigh feidhmchlár agus cinntigh go bhfuil an rogha "Ceadaigh úsáid a bhaint as an bhfeidhmchlár seo chun logáil isteach le Twitter" cumasaithe +auths.tip.discord=Cláraigh feidhmchlár nua ar %s +auths.tip.gitea=Cláraigh feidhmchlár OAuth2 nua. Tá treoir le fáil ag %s +auths.tip.yandex=`Cruthaigh feidhmchlár nua ag %s. Roghnaigh na ceadanna seo a leanas ón rannán "Yandex.Passport API": "Rochtain ar sheoladh ríomhphoist", "Rochtain ar avatar úsáideora" agus "Rochtain ar ainm úsáideora, céad ainm agus sloinne, inscne"` +auths.tip.mastodon=Ionchur URL sampla saincheaptha don shampla mastodon is mian leat a fhíordheimhniú leis (nó bain úsáid as an gceann réamhshocraithe) +auths.edit=Cuir Foinse Fíordheimhnithe in Eagar +auths.activated=Tá an Foinse Fíordheimhnithe seo gníomhachtaithe +auths.new_success=Tá an fíordheimhniú "%s" curtha leis. +auths.update_success=Nuashonraíodh an fhoinse fíordheimhnithe. +auths.update=Nuashonraigh Foinse Fíordheimhnithe +auths.delete=Scrios Foinse Fíordheimhnithe +auths.delete_auth_title=Scrios Foinse Fíordheimhnithe +auths.delete_auth_desc=Má scriosann tú foinse fíordheimhnithe cuirtear cosc ​​ar úsáideoirí í a úsáid chun síniú isteach. Lean ort? +auths.still_in_used=Tá an fhoinse fíordheimhnithe fós in úsáid. Tiontaigh nó scrios aon úsáideoir a úsáideann an fhoinse fíordheimhnithe seo ar dtús. +auths.deletion_success=Tá an fhoinse fíordheimhnithe scriosta. +auths.login_source_exist=Tá an fhoinse fíordheimhnithe "%s" ann cheana. +auths.login_source_of_type_exist=Tá foinse fíordheimhnithe den chineál seo ann cheana féin. +auths.unable_to_initialize_openid=Ní féidir Soláthraí Ceangail OpenID a thionscnamh: %s +auths.invalid_openIdConnectAutoDiscoveryURL=URL Neamhbhailí Fionnachtana Uathoibríoch (ní mór gur URL bailí é seo ag tosú le http:// nó https://) +config.server_config=Cumraíocht Freastalaí +config.app_name=Teideal an Láithreáin +config.app_ver=Leagan Gitea +config.app_url=URL Bonn Gitea +config.custom_conf=Cosán Comhad Cumraíochta +config.custom_file_root_path=Cosán Fréamh Comhad Saincheaptha +config.domain=Fearann ​​Freastalaí +config.offline_mode=Mód Áitiúil +config.disable_router_log=Díchumasaigh Loga an Ródaire +config.run_user=Rith Mar Ainm úsáideora +config.run_mode=Mód Rith +config.git_version=Leagan Git +config.app_data_path=Cosán Sonraí Aip +config.repo_root_path=Cosán Fréimhe Stórála +config.lfs_root_path=Cosán Fréamh LFS +config.log_file_root_path=Cosán Logála +config.script_type=Cineál Script +config.reverse_auth_user=Úsáideoir Fíordheimhnithe Droim ar Ais +config.ssh_config=Cumraíocht SSH +config.ssh_enabled=Cumasaithe +config.ssh_start_builtin_server=Úsáid Freastalaí Ionsuite +config.ssh_domain=Fearainn Freastalaí SSH +config.ssh_port=Calafort +config.ssh_listen_port=Éist Calafort +config.ssh_root_path=Cosán Fréimhe +config.ssh_key_test_path=Cosán Tástáil Eochair +config.ssh_keygen_path=Keygen ('ssh-keygen') Cosán +config.ssh_minimum_key_size_check=Seiceáil Íosta Méid Eochair +config.ssh_minimum_key_sizes=Méideanna Íosta Eochrach +config.lfs_config=Cumraíocht LFS +config.lfs_enabled=Cumasaithe +config.lfs_content_path=Cosán Ábhar LFS +config.lfs_http_auth_expiry=Éag Auth LFS HTTP +config.db_config=Cumraíocht Bunachar Sonraí +config.db_type=Cineál +config.db_host=Óstach +config.db_name=Ainm +config.db_user=Ainm úsáideora +config.db_schema=Scéim +config.db_ssl_mode=SSL +config.db_path=Cosán +config.service_config=Cumraíocht Seirbhíse +config.register_email_confirm=Deimhniú Ríomhphost a éileamh chun Clárú +config.disable_register=Díchumasaigh Féin-Chlárú +config.allow_only_internal_registration=Ceadaigh Clárú Amháin Trí Gitea féin +config.allow_only_external_registration=Ceadaigh Clárú Trí Sheirbhísí Seachtracha amháin +config.enable_openid_signup=Cumasaigh Féinchlárú OpenID +config.enable_openid_signin=Cumasaigh Síniú isteach OpenID +config.show_registration_button=Taispeáin Cnaipe Cláraithe +config.require_sign_in_view=Teastaíonn Sínigh isteach chun Leathanaigh Amharc +config.mail_notify=Cumasaigh Fógraí Ríomhphoist +config.enable_captcha=Cumasaigh CAPTCHA +config.active_code_lives=Saol Gníomhach ag an gCód +config.reset_password_code_lives=Am Éaga Chóid Aisghabhála Cuntais +config.default_keep_email_private=Folaigh Seoltaí Ríomhphoist de réir Réamhshocrú +config.default_allow_create_organization=Ceadaigh Cruthú Eagraíochtaí de réir Réamhshocrú +config.enable_timetracking=Cumasaigh Rianú Ama +config.default_enable_timetracking=Cumasaigh Rianú Ama de réir Réamhshocrú +config.default_allow_only_contributors_to_track_time=Lig do Rannpháirtithe Amháin Rianú Am +config.no_reply_address=Fearann Ríomhphoist Folaithe +config.default_visibility_organization=Infheictheacht réamhshocraithe d'Eagraíochtaí nua +config.default_enable_dependencies=Cumasaigh Spleáchais Eisithe de réir Réamhshocrú config.webhook_config=Cumraíocht Crúca Gréasán +config.queue_length=Fad scuaine +config.deliver_timeout=Teorainn Ama Seachadta +config.skip_tls_verify=Scipeáil Fíorú TLS +config.mailer_config=Cumraíocht Seoltóra +config.mailer_enabled=Cumasaithe +config.mailer_enable_helo=Cumasaigh HELO +config.mailer_name=Ainm +config.mailer_protocol=Prótacal +config.mailer_smtp_addr=Seoladh SMTP +config.mailer_smtp_port=Calafort SMTP +config.mailer_user=Úsáideoir +config.mailer_use_sendmail=Úsáid Sendmail +config.mailer_sendmail_path=Cosán Sendmail +config.mailer_sendmail_args=Argóintí Breise chuig Sendmail +config.mailer_sendmail_timeout=Teorainn Ama Sendmail +config.mailer_use_dummy=Caochadán +config.test_email_placeholder=Ríomhphost (m.sh. test@example.com) +config.send_test_mail=Seol Ríomhphost Tástála +config.send_test_mail_submit=Seol +config.test_mail_failed=Theip ar ríomhphost tástála a sheoladh chuig "%s": %v +config.test_mail_sent=Tá ríomhphost tástála seolta chuig "%s". +config.oauth_config=Cumraíocht OAuth +config.oauth_enabled=Cumasaithe +config.cache_config=Cumraíocht taisce +config.cache_adapter=Cuibheoir taisce +config.cache_interval=Eatramh Taisce +config.cache_conn=Ceangal Taisce +config.cache_item_ttl=Mír Taisce TTL +config.cache_test=Taisce Tástáil +config.cache_test_failed=Theip ar an taisce a thaiscéaladh: %v. +config.cache_test_slow=D'éirigh leis an tástáil taisce, ach tá an freagra mall: %s. +config.cache_test_succeeded=D'éirigh leis an tástáil taisce, fuair sé freagra i %s. +config.session_config=Cumraíocht Seisiúin +config.session_provider=Soláthraí Seisiúin +config.provider_config=Cumraíocht Soláthraí +config.cookie_name=Ainm Fianán +config.gc_interval_time=Am Eatramh GC +config.session_life_time=Am Saoil na Seisiúin +config.https_only=HTTPS Amháin +config.cookie_life_time=Am Saoil Fianán +config.picture_config=Cumraíocht Pictiúr agus Avatar +config.picture_service=Seirbhís Pictiúr +config.disable_gravatar=Díchumasaigh Gravatar +config.enable_federated_avatar=Cumasaigh Avatars Cónaidhme +config.open_with_editor_app_help=Na heagarthóirí "Oscailte le" don roghchlár Clón. Má fhágtar folamh é, úsáidfear an réamhshocrú. Leathnaigh chun an réamhshocrú a fheiceáil. +config.git_config=Cumraíocht Git +config.git_disable_diff_highlight=Díchumasaigh Aibhsiú Comhréire Diff +config.git_max_diff_lines=Max Diff Lines (do chomhad amháin) +config.git_max_diff_line_characters=Carachtair Max Diff (le haghaidh líne amháin) +config.git_max_diff_files=Comhaid Max Diff (le taispeáint) +config.git_gc_args=Argóintí GC +config.git_migrate_timeout=Teorainn Ama Imirce +config.git_mirror_timeout=Teorainn Ama Nuashonraithe Scátháin +config.git_clone_timeout=Teorainn Ama Oibríochta Clón +config.git_pull_timeout=Tarraing Am Oibríochta +config.git_gc_timeout=Teorainn Ama Oibriúcháin GC +config.log_config=Cumraíocht Logáil +config.logger_name_fmt=Logálaí: %s +config.disabled_logger=Díchumasaithe +config.access_log_mode=Mód Logáil Rochtana +config.access_log_template=Teimpléad Logáil Rochtana +config.xorm_log_sql=Logáil SQL +config.set_setting_failed=Theip ar shocrú %s a shocrú +monitor.stats=Staitisticí +monitor.cron=Tascanna Cron +monitor.name=Ainm +monitor.schedule=Sceideal +monitor.next=An chéad uair eile +monitor.previous=Am Roimhe Seo +monitor.execute_times=Forghníomhaíochtaí +monitor.process=Próisis reatha +monitor.stacktrace=Rian cruachta +monitor.processes_count=Próisis %d +monitor.download_diagnosis_report=Íoslódáil tuairisc diagnóis monitor.desc=Cur síos +monitor.start=Am Tosaigh +monitor.execute_time=Am Forghníomhaithe +monitor.last_execution_result=Toradh +monitor.process.cancel=Cealaigh próiseas +monitor.process.cancel_desc=Má chuirtear próiseas ar ceal d'fhéadfadh go gcaillfí sonraí +monitor.process.cancel_notices=Cealaigh: %s? +monitor.process.children=Leanaí +monitor.queues=Scuaineanna +monitor.queue=Scuaine: %s +monitor.queue.name=Ainm +monitor.queue.type=Cineál +monitor.queue.exemplar=Cineál Eiseamláire +monitor.queue.numberworkers=Líon na nOibrithe +monitor.queue.activeworkers=Oibrithe Gníomhacha +monitor.queue.maxnumberworkers=Líon Uasta na nOibrithe +monitor.queue.numberinqueue=Uimhir i scuaine +monitor.queue.review_add=Athbhreithniú / Cuir Oibrithe leis +monitor.queue.settings.title=Socruithe Linn +monitor.queue.settings.desc=Fásann linnte go dinimiciúil mar fhreagra ar a gcuid scuaine oibrithe a bhlocáil. +monitor.queue.settings.maxnumberworkers=Uaslíon na n-oibrithe +monitor.queue.settings.maxnumberworkers.placeholder=Faoi láthair %[1]d +monitor.queue.settings.maxnumberworkers.error=Caithfidh uaslíon na n-oibrithe a bheith ina uimhir monitor.queue.settings.submit=Nuashonrú Socruithe +monitor.queue.settings.changed=Socruithe Nuashonraithe +monitor.queue.settings.remove_all_items=Bain gach +monitor.queue.settings.remove_all_items_done=Baineadh na míreanna go léir sa scuaine. notices.system_notice_list=Fógraí Córais +notices.view_detail_header=Féach ar Sonraí Fógra notices.operations=Oibríochtaí +notices.select_all=Roghnaigh Gach +notices.deselect_all=Díroghnaigh Gach +notices.inverse_selection=Roghnú Inbhéartha +notices.delete_selected=Scrios Roghnaithe +notices.delete_all=Scrios Gach Fógra +notices.type=Cineál +notices.type_1=Stóras +notices.type_2=Tasc notices.desc=Cur síos +notices.op=Oibríocht. +notices.delete_success=Scriosadh na fógraí córais. +self_check.no_problem_found=Níor aimsíodh aon fhadhb fós. +self_check.startup_warnings=Rabhadh tosaithe: +self_check.database_collation_mismatch=Bí ag súil le comhthiomsú a úsáid sa bhunachar sonraí: %s +self_check.database_collation_case_insensitive=Tá bunachar sonraí ag baint úsáide as comparáid %s, arb é comhdhlúthú neamhíogair. Cé go bhféadfadh Gitea oibriú leis, d'fhéadfadh go mbeadh roinnt cásanna annamh ann nach n-oibríonn mar a bhíothas ag súil leis. +self_check.database_inconsistent_collation_columns=Tá comhthiomsú %s in úsáid ag an mbunachar sonraí, ach tá comhthiomsuithe mímheaitseála á n-úsáid ag na colúin seo. D'fhéadfadh sé a bheith ina chúis le roinnt fadhbanna gan choinne. +self_check.database_fix_mysql=D'úsáideoirí MySQL/MariaDB, d'fhéadfá an t-ordú "gitea doctor convert" a úsáid chun na fadhbanna comhthiomsaithe a réiteach, nó d'fhéadfá an fhadhb a réiteach trí "ALTER ... COLLATE ..." SQLs de láimh freisin. +self_check.database_fix_mssql=I gcás úsáideoirí MSSQL, ní fhéadfá an fhadhb a réiteach ach trí "ALTER ... COLLATE ..." SQLs de láimh faoi láthair. +self_check.location_origin_mismatch=Ní mheaitseálann an URL reatha (%[1]s) an URL atá le feiceáil ag Gitea (%[2]s). Má tá seachfhreastalaí droim ar ais á úsáid agat, cinntigh le do thoil go bhfuil na ceanntásca "Óstríomhaire" agus "X-Forwarded-Proto" socraithe i gceart. [action] +create_repo=stóras cruthaithe %s +rename_repo=stóras athainmnithe ó %[1]s go %[3]s +commit_repo=brú chuig %[3]s ag %[4]s +create_issue=`osclaíodh ceist %[3]s#%[2]s` +close_issue=`eagrán dúnta %[3]s#%[2]s` +reopen_issue=`athoscailt an cheist %[3]s#%[2]s` +create_pull_request=`iarratas tarraingthe cruthaithe %[3]s#%[2]s` +close_pull_request=`iarratas tarraingthe dúnta %[3]s#%[2]s` +reopen_pull_request=`iarratas tarraingthe athoscailte %[3]s#%[2]s` +comment_issue=`trácht ar cheist %[3]s#%[2]s` +comment_pull=`déan trácht ar iarratas tarraingthe %[3]s#%[2]s` +merge_pull_request=`iarratas tarraingthe cumaisc %[3]s#%[2]s` +auto_merge_pull_request=`iarratas tarraingthe cumasctha go huathoibríoch %[3]s#%[2]s` +transfer_repo=aistrithe stóras %s go %s +push_tag=brú %[3]s go %[4]s +delete_tag=scriosta clib %[2]s ó %[3]s +delete_branch=brainse scriosta %[2]s ó %[3]s +compare_branch=Déan comparáid +compare_commits=Déan comparáid idir tiomáintí %d +compare_commits_general=Déan comparáid idir tiomáintí +mirror_sync_push=geallann synced do %[3]s ag %[4]s ón scáthán +mirror_sync_create=sioncronaigh tagairt nua %[3]s do %[4]s ón scáthán +mirror_sync_delete=sioncronaithe agus scriosta an tagairt %[2]s ag %[3]s ón scáthán +approve_pull_request=`ceadaithe %[3]s#%[2]s` +reject_pull_request=`athruithe molta le haghaidh %[3]s#%[2]s` +publish_release=`scaoileadh %[4]s ag %[3]s` +review_dismissed=`léirmheas ó %[4]s le haghaidh %[3]s#%[2]s` +review_dismissed_reason=Cúis: +create_branch=brainse cruthaithe %[3]s i %[4]s +starred_repo=le %[2]s le réalta +watched_repo=thosaigh sé ag breathnú ar %[2]s [tool] +now=anois +future=todhchaí +1s=1 soicind +1m=1 nóiméad +1h=1 uair an chloig +1d=1 lá +1w=1 seachtain +1mon=1 mhí +1y=1 bhliain +seconds=%d soicind +minutes=%d nóiméad +hours=%d uair an chloig +days=%d laethanta +weeks=%d seachtain +months=%d míonna +years=%d bliain +raw_seconds=soicind +raw_minutes=nóiméad [dropzone] +default_message=Scaoil comhaid nó cliceáil anseo chun iad a uaslódáil. +invalid_input_type=Ní féidir leat comhaid den chineál seo a uaslódáil. +file_too_big=Sáraíonn méid comhaid ({{filesize}} MB) an t-uasmhéid de ({{maxFilesize}} MB). +remove_file=Bain an comhad [notification] +notifications=Fógraí +unread=Gan léamh +read=Léigh +no_unread=Gan aon fhógraí neamh-léite. +no_read=Gan aon fhógraí léite. +pin=Fógra bioráin +mark_as_read=Marcáil mar léite +mark_as_unread=Marcáil mar neamh-léite +mark_all_as_read=Marcáil gach ceann mar léite +subscriptions=Síntiúis +watching=Ag féachaint +no_subscriptions=Gan síntiúis [gpg] +default_key=Sínithe leis an eochair réamhshocraithe +error.extract_sign=Theip ar an síniú a bhaint +error.generate_hash=Theip ar hash gealltanas a ghiniúint +error.no_committer_account=Níl aon chuntas nasctha le seoladh ríomhphoist an tiomnóra +error.no_gpg_keys_found=Níor aimsíodh aon eochair aithne don síniú seo sa bhunachar +error.not_signed_commit=Ní tiomantas sínithe +error.failed_retrieval_gpg_keys=Theip ar aisghabháil eochair ar bith a bhí ceangailte le cuntas an tiomnóra +error.probable_bad_signature=RABHADH! Cé go bhfuil eochair leis an ID seo sa bhunachar sonraí ní fhíoraíonn sé an tiomantas seo! Tá an tiomantas seo AMHRASACH. +error.probable_bad_default_signature=RABHADH! Cé go bhfuil an t-aitheantas seo ag an eochair réamhshocraithe ní fíoraíonn sé an tiomantas seo! Tá an tiomantas seo AMHRASACH. [units] +unit=Aonad +error.no_unit_allowed_repo=Níl cead agat rochtain a fháil ar aon chuid den tiomantas seo. +error.unit_not_allowed=Níl cead agat an rannán stóras seo a rochtain. [packages] +title=Pacáistí +desc=Bainistigh pacáistí stórais. +empty=Níl aon phacáistí ann fós. +no_metadata=Gan aon mheiteashonraí. +empty.documentation=Le haghaidh tuilleadh eolais ar chlárlann na bpacáistí, féach ar na doiciméid. +empty.repo=An ndearna tú uaslódáil ar phacáiste, ach nach bhfuil sé léirithe anseo? Téigh go socruithe pacáiste agus nasc leis an stóras seo é. +registry.documentation=Le haghaidh tuilleadh eolais ar chlárlann %s, féach ar na doiciméid. +filter.type=Cineál +filter.type.all=Gach +filter.no_result=Níor thug do scagaire aon torthaí. +filter.container.tagged=Clibeáilte +filter.container.untagged=Gan chlib +published_by=Foilsithe %[1]s ag %[3]s +published_by_in=Foilsithe ag %[1]s ag %[3]s in %[5]s +installation=Suiteáil +about=Maidir leis an bpacáiste seo +requirements=Riachtanais +dependencies=Spleithiúlachtaí +keywords=Eochairfhocail +details=Sonraí +details.author=Údar +details.project_site=Suíomh an Tionscadail +details.repository_site=Suíomh Stóras +details.documentation_site=Suíomh Doiciméadaithe +details.license=Ceadúnas +assets=Sócmhainní +versions=Leaganacha +versions.view_all=Féach ar gach +dependency.id=ID +dependency.version=Leagan +alpine.registry=Socraigh an chlár seo tríd an url a chur i do chomhad /etc/apk/repositories: +alpine.registry.key=Íoslódáil eochair RSA poiblí na clárlainne isteach san fhillteán /etc/apk/keys/ chun an síniú innéacs a fhíorú: +alpine.registry.info=Roghnaigh $branch agus $repository ón liosta thíos. +alpine.install=Chun an pacáiste a shuiteáil, rith an t-ordú seo a leanas: +alpine.repository=Eolas Stórais alpine.repository.branches=Brainsí alpine.repository.repositories=Stórais +alpine.repository.architectures=Ailtireachtaí +cargo.registry=Socraigh an clárlann seo sa chomhad cumraíochta lasta (mar shampla ~/.cargo/config.toml): +cargo.install=Chun an pacáiste a shuiteáil ag baint úsáide as Cargo, reáchtáil an t-ordú seo a leanas: +chef.registry=Socraigh an clárlann seo i do chomhad ~/.chef/config.rb: +chef.install=Chun an pacáiste a shuiteáil, rith an t-ordú seo a leanas: +composer.registry=Socraigh an chlár seo i do chomhad ~/.composer/config.json: +composer.install=Chun an pacáiste a shuiteáil ag baint úsáide as Cumadóir, reáchtáil an t-ordú seo a leanas: +composer.dependencies=Spleithiúlachtaí +composer.dependencies.development=Spleithiúlachtaí Forbartha +conan.details.repository=Stóras +conan.registry=Socraigh an clárlann seo ón líne ordaithe: +conan.install=Chun an pacáiste a shuiteáil ag úsáid Conan, reáchtáil an t-ordú seo a leanas: +conda.registry=Socraigh an chlár seo mar stóras Conda i do chomhad .condarc: +conda.install=Chun an pacáiste a shuiteáil ag úsáid Conda, reáchtáil an t-ordú seo a leanas: +container.details.type=Cineál Íomhá +container.details.platform=Ardán +container.pull=Tarraing an íomhá ón líne ordaithe: +container.digest=Díleáigh: +container.multi_arch=Córas Oibriúcháin / Ailtireacht +container.layers=Sraitheanna Íomhá +container.labels=Lipéid +container.labels.key=Eochair +container.labels.value=Luach +cran.registry=Cumraigh an chlárlann seo i do chomhad Rprofile.site: +cran.install=Chun an pacáiste a shuiteáil, rith an t-ordú seo a leanas: +debian.registry=Socraigh an clárlann seo ón líne ordaithe: +debian.registry.info=Roghnaigh $distribution agus $component ón liosta thíos. +debian.install=Chun an pacáiste a shuiteáil, rith an t-ordú seo a leanas: +debian.repository=Eolas Stóras +debian.repository.distributions=Dáiltí +debian.repository.components=Comhpháirteanna +debian.repository.architectures=Ailtireachtaí +generic.download=Íoslódáil pacáiste ón líne ordaithe: +go.install=Suiteáil an pacáiste ón líne ordaithe: +helm.registry=Socraigh an clárlann seo ón líne ordaithe: +helm.install=Chun an pacáiste a shuiteáil, rith an t-ordú seo a leanas: +maven.registry=Socraigh an clárlann seo i do chomhad pom.xml tionscadail: +maven.install=Chun an pacáiste a úsáid cuir na nithe seo a leanas sa bhloc spleáchais sa chomhad pom.xml: +maven.install2=Rith tríd an líne ordaithe: +maven.download=Chun an spleáchas a íoslódáil, rith tríd an líne ordaithe: +nuget.registry=Socraigh an clárlann seo ón líne ordaithe: +nuget.install=Chun an pacáiste a shuiteáil ag úsáid NuGet, reáchtáil an t-ordú seo a leanas: +nuget.dependency.framework=Spriocchreat +npm.registry=Socraigh an chlárlann seo i do chomhad .npmrc do thionscadail: +npm.install=Chun an pacáiste a shuiteáil ag úsáid npm, reáchtáil an t-ordú seo a leanas: +npm.install2=nó cuir leis an gcomhad package.json é: +npm.dependencies=Spleithiúlachtaí +npm.dependencies.development=Spleithiúlachtaí Forbartha +npm.dependencies.bundle=Spleáchais Chuachta +npm.dependencies.peer=Spleithiúlachtaí Piaraí +npm.dependencies.optional=Spleáchais Roghnacha +npm.details.tag=Clib +pub.install=Chun an pacáiste a shuiteáil ag úsáid Dart, reáchtáil an t-ordú seo a leanas: +pypi.requires=Teastaíonn Python +pypi.install=Chun an pacáiste a shuiteáil ag úsáid pip, reáchtáil an t-ordú seo a leanas: +rpm.registry=Socraigh an clárlann seo ón líne ordaithe: +rpm.distros.redhat=ar dháileadh bunaithe ar RedHat +rpm.distros.suse=ar dháileadh bunaithe ar SUSE +rpm.install=Chun an pacáiste a shuiteáil, rith an t-ordú seo a leanas: +rpm.repository=Eolas Stóras +rpm.repository.architectures=Ailtireachtaí +rpm.repository.multiple_groups=Tá an pacáiste seo ar fáil i ngrúpaí éagsúla. +rubygems.install=Chun an pacáiste a shuiteáil ag baint úsáide as gem, reáchtáil an t-ordú seo a leanas: +rubygems.install2=nó cuir leis an Gemfile é: +rubygems.dependencies.runtime=Spleáchais Rith-Ama +rubygems.dependencies.development=Spleáchais Forbartha +rubygems.required.ruby=Éilíonn leagan Ruby +rubygems.required.rubygems=Éilíonn leagan RubyGem +swift.registry=Socraigh an clárlann seo ón líne ordaithe: +swift.install=Cuir an pacáiste i do chomhad Package.swift: +swift.install2=agus reáchtáil an t-ordú seo a leanas: +vagrant.install=Chun bosca Vagrant a chur leis, reáchtáil an t-ordú seo a leanas: +settings.link=Nasc an pacáiste seo le stóras +settings.link.description=Má nascann tú pacáiste le stóras, liostaítear an pacáiste i liosta pacáistí an stórais. +settings.link.select=Roghnaigh Stóras +settings.link.button=Nuashonraigh Nasc Stórais +settings.link.success=D'éirigh le nasc an stórais a nuashonrú. +settings.link.error=Theip ar an nasc stóras a nuashonrú. +settings.delete=Scrios pacáiste +settings.delete.description=Tá pacáiste a scriosadh buan agus ní féidir é a chur ar ais. +settings.delete.notice=Tá tú ar tí %s (%s) a scriosadh. Tá an oibríocht seo dochúlaithe, an bhfuil tú cinnte? +settings.delete.success=Tá an pacáiste scriosta. +settings.delete.error=Theip ar an pacáiste a scriosadh. +owner.settings.cargo.title=Innéacs Clárlann Lasta +owner.settings.cargo.initialize=Innéacs a chur i dtosach +owner.settings.cargo.initialize.description=Tá gá le stóras innéacs speisialta Git chun an clárlann Cargo a úsáid. Tríd an rogha seo, cruthófar an stóras (nó athchruthófar é) agus cumrófar é go huathoibríoch. +owner.settings.cargo.initialize.error=Níorbh fhéidir an t-innéacs Cargo a thúsú: %v +owner.settings.cargo.initialize.success=Cruthaíodh an t-innéacs Cargo go rathúil. +owner.settings.cargo.rebuild=Innéacs Atógáil +owner.settings.cargo.rebuild.description=Is féidir atógáil a bheith úsáideach mura bhfuil an t-innéacs sioncronaithe leis na pacáistí Cargo stóráilte. +owner.settings.cargo.rebuild.error=Níorbh fhéidir an t-innéacs Cargo a atógáil: %v +owner.settings.cargo.rebuild.success=D'éirigh leis an innéacs Cargo a atógáil. +owner.settings.cleanuprules.title=Bainistigh Rialacha Glanta +owner.settings.cleanuprules.add=Cuir Riail Glantacháin leis +owner.settings.cleanuprules.edit=Cuir Riail Glantacháin in eagar +owner.settings.cleanuprules.none=Níl aon rialacha glanta ar fáil. Féach ar na doiciméid le do thoil. +owner.settings.cleanuprules.preview=Réamhamharc Riail Glantacháin +owner.settings.cleanuprules.preview.overview=Tá pacáistí %d beartaithe a bhaint. +owner.settings.cleanuprules.preview.none=Ní hionann riail glantacháin agus pacáistí ar bith. +owner.settings.cleanuprules.enabled=Cumasaithe +owner.settings.cleanuprules.pattern_full_match=Cuir patrún i bhfeidhm ar ainm an phacáiste iomlán +owner.settings.cleanuprules.keep.title=Coinnítear leaganacha a mheaitseálann leis na rialacha seo, fiú má mheaitseálann siad riail bhaint thíos. +owner.settings.cleanuprules.keep.count=Coinnigh an ceann is déanaí +owner.settings.cleanuprules.keep.count.1=1 leagan in aghaidh an phacáiste +owner.settings.cleanuprules.keep.count.n=Leaganacha %d in aghaidh an phacáiste +owner.settings.cleanuprules.keep.pattern=Coinnigh leaganacha meaitseála +owner.settings.cleanuprules.keep.pattern.container=Coinnítear an leagan is déanaí le haghaidh pacáistí Coimeádán i gcónaí. +owner.settings.cleanuprules.remove.title=Baintear leaganacha a mheaitseálann leis na rialacha seo, mura deir riail thuas iad a choinneáil. +owner.settings.cleanuprules.remove.days=Bain leaganacha níos sine ná +owner.settings.cleanuprules.remove.pattern=Bain leaganacha meaitseála +owner.settings.cleanuprules.success.update=Nuashonraíodh an riail ghlantacháin. +owner.settings.cleanuprules.success.delete=Scriosadh an riail glantacháin. +owner.settings.chef.title=Clárlann Chef +owner.settings.chef.keypair=Gin péire eochair +owner.settings.chef.keypair.description=Tá eochairphéire riachtanach le fíordheimhniú a dhéanamh ar chlárlann an Chef. Má tá péire eochrach ginte agat roimhe seo, má ghinfidh tú eochairphéire nua, scriosfar an seanphéire eochair. [secrets] +secrets=Rúin +description=Cuirfear rúin ar aghaidh chuig gníomhartha áirithe agus ní féidir iad a léamh ar mhalairt. +none=Níl aon rúin ann fós. +creation=Cuir Rúnda leis +creation.name_placeholder=carachtair alfanumair nó íoslaghda amháin nach féidir a thosú le GITEA_ nó GITHUB_ +creation.value_placeholder=Ionchur ábhar ar bith. Fágfar spás bán ag tús agus ag deireadh ar lár. +creation.success=Tá an rún "%s" curtha leis. +creation.failed=Theip ar an rún a chur leis. +deletion=Bain rún +deletion.description=Is buan rún a bhaint agus ní féidir é a chealú. Lean ort? +deletion.success=Tá an rún bainte. +deletion.failed=Theip ar rún a bhaint. +management=Bainistíocht Rúin [actions] +actions=Gníomhartha +unit.desc=Bainistigh gníomhartha +status.unknown=Anaithnid +status.waiting=Ag fanacht +status.running=Ag rith +status.success=Rath +status.failure=Teip +status.cancelled=Cealaíodh +status.skipped=Scipeáilte +status.blocked=Blocáilte +runners=Reathaitheoirí +runners.runner_manage_panel=Bainistíocht reathaithe +runners.new=Cruthaigh reathaí nua +runners.new_notice=Conas reathaí a thosú +runners.status=Stádas +runners.id=ID +runners.name=Ainm +runners.owner_type=Cineál runners.description=Cur síos +runners.labels=Lipéid +runners.last_online=Am Ar Líne Deiridh +runners.runner_title=Reathaí +runners.task_list=Tascanna le déanaí ar an reathaí seo +runners.task_list.no_tasks=Níl aon tasc ann fós. runners.task_list.run=Rith +runners.task_list.status=Stádas +runners.task_list.repository=Stóras runners.task_list.commit=Tiomantas +runners.task_list.done_at=Déanta ag +runners.edit_runner=Cuir Reathaí in Eagar +runners.update_runner=Nuashonrú Athruithe +runners.update_runner_success=Nuashonraíodh an Reathaí +runners.update_runner_failed=Theip ar an reathaí a nuashonrú +runners.delete_runner=Scrios an reathaí seo +runners.delete_runner_success=Scriosadh an reathaí go rathúil +runners.delete_runner_failed=Theip ar an reathaí a scriosadh +runners.delete_runner_header=Deimhnigh an reathaí seo a scriosadh +runners.delete_runner_notice=Má tá tasc ar siúl ar an reathaí seo, cuirfear deireadh leis agus marcáil mar theip. Féadfaidh sé sreabhadh oibre tógála a bhriseadh. +runners.none=Níl aon reathaí ar fáil +runners.status.unspecified=Anaithnid +runners.status.idle=Díomhaoin runners.status.active=Gníomhach +runners.status.offline=As líne +runners.version=Leagan runners.reset_registration_token=Athshocraigh comhartha clár runners.reset_registration_token_success=D'éirigh le hathshocrú comhartha clárúcháin an dara háit @@ -2974,11 +3700,54 @@ runs.commit=Tiomantas runs.scheduled=Sceidealaithe runs.pushed_by=bhrú ag runs.invalid_workflow_helper=Tá comhad cumraíochta sreabhadh oibre nebhailí. Seiceáil do chomhad cumraithe le do thoil: %s +runs.no_matching_online_runner_helper=Gan aon reathaí ar líne a mheaitseáil le lipéad: %s +runs.no_job_without_needs=Caithfidh post amháin ar a laghad a bheith sa sreabhadh oibre gan spleáchas. +runs.no_job=Caithfidh post amháin ar a laghad a bheith sa sreabhadh oibre +runs.actor=Aisteoir +runs.status=Stádas +runs.actors_no_select=Gach aisteoir +runs.status_no_select=Gach stádas +runs.no_results=Níor mheaitseáil aon torthaí. +runs.no_workflows=Níl aon sreafaí oibre ann fós. +runs.no_workflows.quick_start=Níl a fhios agam conas tosú le Gitea Actions? Féach an treoirleabhar mear tosaithe. +runs.no_workflows.documentation=Le haghaidh tuilleadh eolais ar Gitea Actions, féach ar na doiciméid. +runs.no_runs=Níl aon rith ag an sreabhadh oibre fós. +runs.empty_commit_message=(teachtaireacht tiomantas folamh) +runs.expire_log_message=Glanadh logaí toisc go raibh siad ró-sean. +workflow.disable=Díchumasaigh sreabhadh oibre +workflow.disable_success=D'éirigh le sreabhadh oibre '%s' a dhíchumasú. +workflow.enable=Cumasaigh sreabhadh oibre +workflow.enable_success=Cumasaíodh sreabhadh oibre '%s' go rathúil. +workflow.disabled=Tá sreabhadh oibre díchumasaithe +workflow.run=Rith Sreabhadh Oibre +workflow.not_found=Níor aimsíodh sreabhadh oibre '%s'. +workflow.run_success=Ritheann sreabhadh oibre '%s' go rathúil. +workflow.from_ref=Úsáid sreabhadh oibre ó +workflow.has_workflow_dispatch=Tá comhoibriú ag an gcur i bhfeidhm seo le himeacht workflow_dispatch. +need_approval_desc=Teastaíonn faomhadh chun sreafaí oibre a rith le haghaidh iarratas tarraingt forc. +variables=Athróga +variables.management=Bainistíocht Athróg +variables.creation=Cuir Athróg leis +variables.none=Níl aon athróga ann fós. +variables.deletion=Bain athróg +variables.deletion.description=Tá athróg a bhaint buan agus ní féidir é a chur ar ais. Lean ar aghaidh? +variables.description=Cuirfear athróga chuig gníomhartha áirithe agus ní féidir iad a léamh ar mhalairt eile. +variables.id_not_exist=Níl athróg le ID %d ann. +variables.edit=Cuir Athróg in Eagar +variables.deletion.failed=Theip ar athróg a bhaint. +variables.deletion.success=Tá an athróg bainte. +variables.creation.failed=Theip ar athróg a chur leis. +variables.creation.success=Tá an athróg "%s" curtha leis. +variables.update.failed=Theip ar athróg a chur in eagar. +variables.update.success=Tá an t-athróg curtha in eagar. [projects] +deleted.display_name=Tionscadal scriosta +type-1.display_name=Tionscadal Aonair +type-2.display_name=Tionscadal Stórais type-3.display_name=Tionscadal Eagrúcháin [git.filemode] diff --git a/routers/api/packages/container/blob.go b/routers/api/packages/container/blob.go index 9e3a47076c2..4595b9a33de 100644 --- a/routers/api/packages/container/blob.go +++ b/routers/api/packages/container/blob.go @@ -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, diff --git a/routers/api/v1/admin/hooks.go b/routers/api/v1/admin/hooks.go index fa60836b7e7..db481fbf594 100644 --- a/routers/api/v1/admin/hooks.go +++ b/routers/api/v1/admin/hooks.go @@ -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 diff --git a/routers/api/v1/utils/hook.go b/routers/api/v1/utils/hook.go index f1abd49a7d6..4328878e196 100644 --- a/routers/api/v1/utils/hook.go +++ b/routers/api/v1/utils/hook.go @@ -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 diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go index 5c01216356d..8d12b7a9536 100644 --- a/routers/private/hook_post_receive.go +++ b/routers/private/hook_post_receive.go @@ -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() diff --git a/routers/web/admin/admin.go b/routers/web/admin/admin.go index 6fc97c949e4..37c54b5362f 100644 --- a/routers/web/admin/admin.go +++ b/routers/web/admin/admin.go @@ -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") } } diff --git a/routers/web/admin/applications.go b/routers/web/admin/applications.go index 85833980746..9b48f21eca9 100644 --- a/routers/web/admin/applications.go +++ b/routers/web/admin/applications.go @@ -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, } } diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go index 3b89be0f8fc..60e2b7c86fc 100644 --- a/routers/web/admin/auths.go +++ b/routers/web/admin/auths.go @@ -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") } diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go index 2ae93e9cacc..d067250a5b6 100644 --- a/routers/web/admin/config.go +++ b/routers/web/admin/config.go @@ -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 { diff --git a/routers/web/admin/emails.go b/routers/web/admin/emails.go index f0d85550706..49338fbd7c4 100644 --- a/routers/web/admin/emails.go +++ b/routers/web/admin/emails.go @@ -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) diff --git a/routers/web/admin/hooks.go b/routers/web/admin/hooks.go index e40580b6e77..91ca6e3fa7b 100644 --- a/routers/web/admin/hooks.go +++ b/routers/web/admin/hooks.go @@ -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") } diff --git a/routers/web/admin/notice.go b/routers/web/admin/notice.go index 36303cbc06e..5f7432e6290 100644 --- a/routers/web/admin/notice.go +++ b/routers/web/admin/notice.go @@ -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") } diff --git a/routers/web/admin/packages.go b/routers/web/admin/packages.go index 39f064a1be6..2b9edc622d6 100644 --- a/routers/web/admin/packages.go +++ b/routers/web/admin/packages.go @@ -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") } diff --git a/routers/web/admin/queue.go b/routers/web/admin/queue.go index dce8f8077f5..59b17f88e69 100644 --- a/routers/web/admin/queue.go +++ b/routers/web/admin/queue.go @@ -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)) } diff --git a/routers/web/admin/repos.go b/routers/web/admin/repos.go index e7c27145dce..75e5ee5d86f 100644 --- a/routers/web/admin/repos.go +++ b/routers/web/admin/repos.go @@ -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)) } diff --git a/routers/web/admin/runners.go b/routers/web/admin/runners.go index d73290a8dba..4b89237364e 100644 --- a/routers/web/admin/runners.go +++ b/routers/web/admin/runners.go @@ -9,5 +9,5 @@ import ( ) func RedirectToDefaultSetting(ctx *context.Context) { - ctx.Redirect(setting.AppSubURL + "/admin/actions/runners") + ctx.Redirect(setting.AppSubURL + "/-/admin/actions/runners") } diff --git a/routers/web/admin/stacktrace.go b/routers/web/admin/stacktrace.go index b3b635af5b6..ff751be6217 100644 --- a/routers/web/admin/stacktrace.go +++ b/routers/web/admin/stacktrace.go @@ -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") } diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index 48ff8ea04b0..a6b0b5c78bb 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -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)) } diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 5cbe2f5388c..c9ef9193f12 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -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 diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index ccbb3bebf1f..730d68051be 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -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) diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index ced0bbc15a0..0efe1be76a3 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -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 { diff --git a/routers/web/repo/setting/runners.go b/routers/web/repo/setting/runners.go index 93e6f518b0b..3141d8f42ad 100644 --- a/routers/web/repo/setting/runners.go +++ b/routers/web/repo/setting/runners.go @@ -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 } diff --git a/routers/web/repo/setting/variables.go b/routers/web/repo/setting/variables.go index 45b6c0f39a6..cc2e619f662 100644 --- a/routers/web/repo/setting/variables.go +++ b/routers/web/repo/setting/variables.go @@ -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 } diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go index 76615997296..8d548c4e3d1 100644 --- a/routers/web/repo/setting/webhook.go +++ b/routers/web/repo/setting/webhook.go @@ -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 } diff --git a/routers/web/web.go b/routers/web/web.go index 0be8d68a5c1..5b5a055f836 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -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) diff --git a/services/agit/agit.go b/services/agit/agit.go index 52a70469e0c..82aa2791aa7 100644 --- a/services/agit/agit.go +++ b/services/agit/agit.go @@ -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// 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()}) diff --git a/services/auth/auth.go b/services/auth/auth.go index a2523a2452e..43ff95f0530 100644 --- a/services/auth/auth.go +++ b/services/auth/auth.go @@ -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) } } diff --git a/services/context/csrf.go b/services/context/csrf.go index 9b66d613e3b..7b475a8fd85 100644 --- a/services/context/csrf.go +++ b/services/context/csrf.go @@ -129,10 +129,8 @@ func (c *csrfProtector) PrepareForSessionUser(ctx *Context) { } if needsNew { - // FIXME: actionId. c.token = GenerateCsrfToken(c.opt.Secret, c.id, "POST", time.Now()) - cookie := newCsrfCookie(&c.opt, c.token) - ctx.Resp.Header().Add("Set-Cookie", cookie.String()) + ctx.Resp.Header().Add("Set-Cookie", newCsrfCookie(&c.opt, c.token).String()) } ctx.Data["CsrfToken"] = c.token diff --git a/services/context/permission.go b/services/context/permission.go index 14a9801dccb..9338587257c 100644 --- a/services/context/permission.go +++ b/services/context/permission.go @@ -58,6 +58,9 @@ func RequireRepoWriterOr(unitTypes ...unit.Type) func(ctx *Context) { func RequireRepoReader(unitType unit.Type) func(ctx *Context) { return func(ctx *Context) { if !ctx.Repo.CanRead(unitType) { + if unitType == unit.TypeCode && canWriteAsMaintainer(ctx) { + return + } if log.IsTrace() { if ctx.IsSigned { log.Trace("Permission Denied: User %-v cannot read %-v in Repo %-v\n"+ diff --git a/services/context/repo.go b/services/context/repo.go index 0072b63b7c9..2df2b7ea403 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -374,7 +374,7 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) { return } - if !ctx.Repo.Permission.HasAnyUnitAccessOrEveryoneAccess() { + if !ctx.Repo.Permission.HasAnyUnitAccessOrEveryoneAccess() && !canWriteAsMaintainer(ctx) { if ctx.FormString("go-get") == "1" { EarlyResponseForGoGetMeta(ctx) return @@ -1058,3 +1058,11 @@ func GitHookService() func(ctx *Context) { } } } + +// canWriteAsMaintainer check if the doer can write to a branch as a maintainer +func canWriteAsMaintainer(ctx *Context) bool { + branchName := getRefNameFromPath(ctx.Repo, ctx.PathParam("*"), func(branchName string) bool { + return issues_model.CanMaintainerWriteToBranch(ctx, ctx.Repo.Permission, branchName, ctx.Doer) + }) + return len(branchName) > 0 +} diff --git a/services/user/user_test.go b/services/user/user_test.go index cd0f5975015..efcbc669c8a 100644 --- a/services/user/user_test.go +++ b/services/user/user_test.go @@ -114,12 +114,10 @@ func TestRenameUser(t *testing.T) { }) t.Run("Non usable username", func(t *testing.T) { - usernames := []string{"--diff", "aa.png", ".well-known", "search", "aaa.atom"} + usernames := []string{"--diff", ".well-known", "gitea-actions", "aaa.atom", "aa.png"} for _, username := range usernames { - t.Run(username, func(t *testing.T) { - assert.Error(t, user_model.IsUsableUsername(username)) - assert.Error(t, RenameUser(db.DefaultContext, user, username)) - }) + assert.Error(t, user_model.IsUsableUsername(username), "non-usable username: %s", username) + assert.Error(t, RenameUser(db.DefaultContext, user, username), "non-usable username: %s", username) } }) diff --git a/templates/admin/auth/list.tmpl b/templates/admin/auth/list.tmpl index 174dda1e2af..70571698957 100644 --- a/templates/admin/auth/list.tmpl +++ b/templates/admin/auth/list.tmpl @@ -3,7 +3,7 @@

{{ctx.Locale.Tr "admin.auths.auth_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .Total}})

@@ -23,12 +23,12 @@ {{range .Sources}} {{.ID}} - {{.Name}} + {{.Name}} {{.TypeName}} {{svg (Iif .IsActive "octicon-check" "octicon-x")}} {{DateTime "short" .UpdatedUnix}} {{DateTime "short" .CreatedUnix}} - {{svg "octicon-pencil"}} + {{svg "octicon-pencil"}} {{end}} diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index 87f18192a6f..29a5e1b473d 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -231,7 +231,7 @@
{{ctx.Locale.Tr "admin.config.send_test_mail"}}
-
+ {{.CsrfTokenHtml}}
@@ -263,7 +263,7 @@
{{ctx.Locale.Tr "admin.config.cache_test"}}
- + {{.CsrfTokenHtml}}
diff --git a/templates/admin/config_settings.tmpl b/templates/admin/config_settings.tmpl index 02ab5fd0fbe..6b9bb8275cc 100644 --- a/templates/admin/config_settings.tmpl +++ b/templates/admin/config_settings.tmpl @@ -24,7 +24,7 @@ {{ctx.Locale.Tr "repository"}}
-
+
{{ctx.Locale.Tr "admin.config.open_with_editor_app_help"}} diff --git a/templates/admin/cron.tmpl b/templates/admin/cron.tmpl index bb412ef146b..1c16ed00ae2 100644 --- a/templates/admin/cron.tmpl +++ b/templates/admin/cron.tmpl @@ -4,7 +4,7 @@ {{ctx.Locale.Tr "admin.monitor.cron"}}
- + diff --git a/templates/admin/dashboard.tmpl b/templates/admin/dashboard.tmpl index b82922df0c7..af2349d2889 100644 --- a/templates/admin/dashboard.tmpl +++ b/templates/admin/dashboard.tmpl @@ -9,7 +9,7 @@ {{ctx.Locale.Tr "admin.dashboard.maintenance_operations"}}
- + {{.CsrfTokenHtml}}
diff --git a/templates/admin/emails/list.tmpl b/templates/admin/emails/list.tmpl index 93fbb9dfc23..835b77ea176 100644 --- a/templates/admin/emails/list.tmpl +++ b/templates/admin/emails/list.tmpl @@ -80,7 +80,7 @@

{{ctx.Locale.Tr "admin.emails.change_email_text"}}

- + {{$.CsrfTokenHtml}} diff --git a/templates/admin/navbar.tmpl b/templates/admin/navbar.tmpl index a09c9b83650..2d8d20691b0 100644 --- a/templates/admin/navbar.tmpl +++ b/templates/admin/navbar.tmpl @@ -5,10 +5,10 @@
{{ctx.Locale.Tr "admin.maintenance"}} @@ -16,19 +16,19 @@
{{ctx.Locale.Tr "admin.identity_access"}} @@ -37,11 +37,11 @@ {{ctx.Locale.Tr "admin.assets"}} @@ -51,22 +51,22 @@
{{ctx.Locale.Tr "admin.integrations"}}
{{else}} {{if not DisableWebhooks}} - + {{ctx.Locale.Tr "admin.hooks"}} {{end}} {{if .EnableOAuth2}} - + {{ctx.Locale.Tr "settings.applications"}} {{end}} @@ -75,10 +75,10 @@
{{ctx.Locale.Tr "actions.actions"}} @@ -87,30 +87,30 @@
{{ctx.Locale.Tr "admin.config"}}
- + {{ctx.Locale.Tr "admin.notices"}}
{{ctx.Locale.Tr "admin.monitor"}} diff --git a/templates/admin/notice.tmpl b/templates/admin/notice.tmpl index 68703cc8844..6e7eed76785 100644 --- a/templates/admin/notice.tmpl +++ b/templates/admin/notice.tmpl @@ -31,7 +31,7 @@
- + {{.CsrfTokenHtml}} diff --git a/templates/admin/packages/list.tmpl b/templates/admin/packages/list.tmpl index d1d77b62201..a5ad93b89c0 100644 --- a/templates/admin/packages/list.tmpl +++ b/templates/admin/packages/list.tmpl @@ -5,7 +5,7 @@ {{ctx.Locale.Tr "admin.packages.total_size" (FileSize .TotalBlobSize)}}, {{ctx.Locale.Tr "admin.packages.unreferenced_size" (FileSize .TotalUnreferencedBlobSize)}})
-
+ {{.CsrfTokenHtml}}
diff --git a/templates/admin/repo/list.tmpl b/templates/admin/repo/list.tmpl index 69031e42ebe..77a275427ae 100644 --- a/templates/admin/repo/list.tmpl +++ b/templates/admin/repo/list.tmpl @@ -3,7 +3,7 @@

{{ctx.Locale.Tr "admin.repos.repo_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .Total}})

diff --git a/templates/admin/repo/unadopted.tmpl b/templates/admin/repo/unadopted.tmpl index a95f6b5120e..6f26fa52915 100644 --- a/templates/admin/repo/unadopted.tmpl +++ b/templates/admin/repo/unadopted.tmpl @@ -3,7 +3,7 @@

{{ctx.Locale.Tr "admin.repos.unadopted"}}

@@ -31,7 +31,7 @@

{{ctx.Locale.Tr "repo.adopt_preexisting_content" $dir}}

-
+ {{$.CsrfTokenHtml}} @@ -48,7 +48,7 @@

{{ctx.Locale.Tr "repo.delete_preexisting_content" $dir}}

- + {{$.CsrfTokenHtml}} diff --git a/templates/admin/stacktrace.tmpl b/templates/admin/stacktrace.tmpl index e324570c965..ce03d805557 100644 --- a/templates/admin/stacktrace.tmpl +++ b/templates/admin/stacktrace.tmpl @@ -8,7 +8,7 @@ {{ctx.Locale.Tr "admin.monitor.stacktrace"}}
- +
{{ctx.Locale.Tr "tool.raw_seconds"}} diff --git a/templates/admin/user/list.tmpl b/templates/admin/user/list.tmpl index bc54d33431b..bc3d83fc5c3 100644 --- a/templates/admin/user/list.tmpl +++ b/templates/admin/user/list.tmpl @@ -3,7 +3,7 @@

{{ctx.Locale.Tr "admin.users.user_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .Total}})

diff --git a/templates/base/footer_content.tmpl b/templates/base/footer_content.tmpl index 8d0d8e669cc..4b9d9f5bbec 100644 --- a/templates/base/footer_content.tmpl +++ b/templates/base/footer_content.tmpl @@ -6,7 +6,7 @@ {{if (or .ShowFooterVersion .PageIsAdmin)}} {{ctx.Locale.Tr "version"}}: {{if .IsAdmin}} - {{AppVer}} + {{AppVer}} {{else}} {{AppVer}} {{end}} diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl index 7be2d96d744..951ee590d1c 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -158,7 +158,7 @@ {{if .IsAdmin}}
- + {{svg "octicon-server"}} {{ctx.Locale.Tr "admin_panel"}} diff --git a/templates/repo/empty.tmpl b/templates/repo/empty.tmpl index cb2a5ba1e98..76136433515 100644 --- a/templates/repo/empty.tmpl +++ b/templates/repo/empty.tmpl @@ -24,7 +24,7 @@
-

{{ctx.Locale.Tr "repo.clone_this_repo"}} {{ctx.Locale.Tr "repo.clone_helper" "http://git-scm.com/book/en/Git-Basics-Getting-a-Git-Repository"}}

+

{{ctx.Locale.Tr "repo.clone_this_repo"}} {{ctx.Locale.Tr "repo.clone_helper" "http://git-scm.com/book/en/v2/Git-Basics-Getting-a-Git-Repository"}}

{{if and .CanWriteCode (not .Repository.IsArchived)}} diff --git a/templates/shared/user/profile_big_avatar.tmpl b/templates/shared/user/profile_big_avatar.tmpl index 1069209495a..50d707176d5 100644 --- a/templates/shared/user/profile_big_avatar.tmpl +++ b/templates/shared/user/profile_big_avatar.tmpl @@ -14,7 +14,7 @@
{{if .ContextUser.FullName}}{{.ContextUser.FullName}}{{end}} {{.ContextUser.Name}} {{if .IsAdmin}} - + {{svg "octicon-gear" 18}} {{end}} diff --git a/tests/gitea-repositories-meta/org42/search-by-path.git/GIT_COLA_MSG b/tests/gitea-repositories-meta/org42/search-by-path.git/GIT_COLA_MSG new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/tests/gitea-repositories-meta/org42/search-by-path.git/GIT_COLA_MSG @@ -0,0 +1 @@ + diff --git a/tests/gitea-repositories-meta/org42/search-by-path.git/HEAD b/tests/gitea-repositories-meta/org42/search-by-path.git/HEAD new file mode 100644 index 00000000000..cb089cd89a7 --- /dev/null +++ b/tests/gitea-repositories-meta/org42/search-by-path.git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/tests/gitea-repositories-meta/org42/search-by-path.git/config b/tests/gitea-repositories-meta/org42/search-by-path.git/config new file mode 100644 index 00000000000..07d359d07cf --- /dev/null +++ b/tests/gitea-repositories-meta/org42/search-by-path.git/config @@ -0,0 +1,4 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = true diff --git a/tests/gitea-repositories-meta/org42/search-by-path.git/description b/tests/gitea-repositories-meta/org42/search-by-path.git/description new file mode 100644 index 00000000000..382e2d7f101 --- /dev/null +++ b/tests/gitea-repositories-meta/org42/search-by-path.git/description @@ -0,0 +1,8 @@ +This repository will be used to test code search. The snippet below shows its directory structure + +. +├── avocado.md +├── cucumber.md +├── ham.md +└── potato + └── ham.md diff --git a/tests/gitea-repositories-meta/org42/search-by-path.git/hooks/post-receive b/tests/gitea-repositories-meta/org42/search-by-path.git/hooks/post-receive new file mode 100755 index 00000000000..4b3d452abcc --- /dev/null +++ b/tests/gitea-repositories-meta/org42/search-by-path.git/hooks/post-receive @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +ORI_DIR=`pwd` +SHELL_FOLDER=$(cd "$(dirname "$0")";pwd) +cd "$ORI_DIR" +for i in `ls "$SHELL_FOLDER/post-receive.d"`; do + sh "$SHELL_FOLDER/post-receive.d/$i" +done \ No newline at end of file diff --git a/tests/gitea-repositories-meta/org42/search-by-path.git/hooks/post-receive.d/gitea b/tests/gitea-repositories-meta/org42/search-by-path.git/hooks/post-receive.d/gitea new file mode 100755 index 00000000000..43a948da3a9 --- /dev/null +++ b/tests/gitea-repositories-meta/org42/search-by-path.git/hooks/post-receive.d/gitea @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" post-receive diff --git a/tests/gitea-repositories-meta/org42/search-by-path.git/hooks/pre-receive b/tests/gitea-repositories-meta/org42/search-by-path.git/hooks/pre-receive new file mode 100755 index 00000000000..41270130536 --- /dev/null +++ b/tests/gitea-repositories-meta/org42/search-by-path.git/hooks/pre-receive @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +ORI_DIR=`pwd` +SHELL_FOLDER=$(cd "$(dirname "$0")";pwd) +cd "$ORI_DIR" +for i in `ls "$SHELL_FOLDER/pre-receive.d"`; do + sh "$SHELL_FOLDER/pre-receive.d/$i" +done \ No newline at end of file diff --git a/tests/gitea-repositories-meta/org42/search-by-path.git/hooks/pre-receive.d/gitea b/tests/gitea-repositories-meta/org42/search-by-path.git/hooks/pre-receive.d/gitea new file mode 100755 index 00000000000..49d09406364 --- /dev/null +++ b/tests/gitea-repositories-meta/org42/search-by-path.git/hooks/pre-receive.d/gitea @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" pre-receive diff --git a/tests/gitea-repositories-meta/org42/search-by-path.git/hooks/proc-receive b/tests/gitea-repositories-meta/org42/search-by-path.git/hooks/proc-receive new file mode 100755 index 00000000000..af2808b0370 --- /dev/null +++ b/tests/gitea-repositories-meta/org42/search-by-path.git/hooks/proc-receive @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +ORI_DIR=`pwd` +SHELL_FOLDER=$(cd "$(dirname "$0")";pwd) +cd "$ORI_DIR" +for i in `ls "$SHELL_FOLDER/proc-receive.d"`; do + sh "$SHELL_FOLDER/proc-receive.d/$i" +done diff --git a/tests/gitea-repositories-meta/org42/search-by-path.git/hooks/proc-receive.d/gitea b/tests/gitea-repositories-meta/org42/search-by-path.git/hooks/proc-receive.d/gitea new file mode 100755 index 00000000000..97521c62115 --- /dev/null +++ b/tests/gitea-repositories-meta/org42/search-by-path.git/hooks/proc-receive.d/gitea @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" proc-receive diff --git a/tests/gitea-repositories-meta/org42/search-by-path.git/hooks/update b/tests/gitea-repositories-meta/org42/search-by-path.git/hooks/update new file mode 100755 index 00000000000..c186fe4a18b --- /dev/null +++ b/tests/gitea-repositories-meta/org42/search-by-path.git/hooks/update @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +ORI_DIR=`pwd` +SHELL_FOLDER=$(cd "$(dirname "$0")";pwd) +cd "$ORI_DIR" +for i in `ls "$SHELL_FOLDER/update.d"`; do + sh "$SHELL_FOLDER/update.d/$i" $1 $2 $3 +done \ No newline at end of file diff --git a/tests/gitea-repositories-meta/org42/search-by-path.git/hooks/update.d/gitea b/tests/gitea-repositories-meta/org42/search-by-path.git/hooks/update.d/gitea new file mode 100755 index 00000000000..38101c24266 --- /dev/null +++ b/tests/gitea-repositories-meta/org42/search-by-path.git/hooks/update.d/gitea @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" update $1 $2 $3 diff --git a/tests/gitea-repositories-meta/org42/search-by-path.git/info/exclude b/tests/gitea-repositories-meta/org42/search-by-path.git/info/exclude new file mode 100644 index 00000000000..a5196d1be8f --- /dev/null +++ b/tests/gitea-repositories-meta/org42/search-by-path.git/info/exclude @@ -0,0 +1,6 @@ +# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ diff --git a/tests/gitea-repositories-meta/org42/search-by-path.git/info/refs b/tests/gitea-repositories-meta/org42/search-by-path.git/info/refs new file mode 100644 index 00000000000..6b948c96a83 --- /dev/null +++ b/tests/gitea-repositories-meta/org42/search-by-path.git/info/refs @@ -0,0 +1,13 @@ +90c1019714259b24fb81711d4416ac0f18667dfa refs/heads/DefaultBranch +985f0301dba5e7b34be866819cd15ad3d8f508ee refs/heads/branch2 +65f1bf27bc3bf70f64657658635e66094edbcb4d refs/heads/develop +65f1bf27bc3bf70f64657658635e66094edbcb4d refs/heads/feature/1 +78fb907e3a3309eae4fe8fef030874cebbf1cd5e refs/heads/home-md-img-check +3731fe53b763859aaf83e703ee731f6b9447ff1e refs/heads/master +62fb502a7172d4453f0322a2cc85bddffa57f07a refs/heads/pr-to-update +4649299398e4d39a5c09eb4f534df6f1e1eb87cc refs/heads/sub-home-md-img-check +3fa2f829675543ecfc16b2891aebe8bf0608a8f4 refs/notes/commits +4a357436d925b5c974181ff12a994538ddc5a269 refs/pull/2/head +5f22f7d0d95d614d25a5b68592adb345a4b5c7fd refs/pull/3/head +62fb502a7172d4453f0322a2cc85bddffa57f07a refs/pull/5/head +65f1bf27bc3bf70f64657658635e66094edbcb4d refs/tags/v1.1 diff --git a/tests/gitea-repositories-meta/org42/search-by-path.git/logs/refs/heads/master b/tests/gitea-repositories-meta/org42/search-by-path.git/logs/refs/heads/master new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/gitea-repositories-meta/org42/search-by-path.git/objects/info/commit-graph b/tests/gitea-repositories-meta/org42/search-by-path.git/objects/info/commit-graph new file mode 100644 index 00000000000..b38715bb92b Binary files /dev/null and b/tests/gitea-repositories-meta/org42/search-by-path.git/objects/info/commit-graph differ diff --git a/tests/gitea-repositories-meta/org42/search-by-path.git/objects/info/packs b/tests/gitea-repositories-meta/org42/search-by-path.git/objects/info/packs new file mode 100644 index 00000000000..b2af8c8378a --- /dev/null +++ b/tests/gitea-repositories-meta/org42/search-by-path.git/objects/info/packs @@ -0,0 +1,2 @@ +P pack-393dc29256bc27cb2ec73898507df710be7a3cf5.pack + diff --git a/tests/gitea-repositories-meta/org42/search-by-path.git/objects/pack/pack-393dc29256bc27cb2ec73898507df710be7a3cf5.bitmap b/tests/gitea-repositories-meta/org42/search-by-path.git/objects/pack/pack-393dc29256bc27cb2ec73898507df710be7a3cf5.bitmap new file mode 100644 index 00000000000..1fdef225e83 Binary files /dev/null and b/tests/gitea-repositories-meta/org42/search-by-path.git/objects/pack/pack-393dc29256bc27cb2ec73898507df710be7a3cf5.bitmap differ diff --git a/tests/gitea-repositories-meta/org42/search-by-path.git/objects/pack/pack-393dc29256bc27cb2ec73898507df710be7a3cf5.idx b/tests/gitea-repositories-meta/org42/search-by-path.git/objects/pack/pack-393dc29256bc27cb2ec73898507df710be7a3cf5.idx new file mode 100644 index 00000000000..0d930e7499f Binary files /dev/null and b/tests/gitea-repositories-meta/org42/search-by-path.git/objects/pack/pack-393dc29256bc27cb2ec73898507df710be7a3cf5.idx differ diff --git a/tests/gitea-repositories-meta/org42/search-by-path.git/objects/pack/pack-393dc29256bc27cb2ec73898507df710be7a3cf5.pack b/tests/gitea-repositories-meta/org42/search-by-path.git/objects/pack/pack-393dc29256bc27cb2ec73898507df710be7a3cf5.pack new file mode 100644 index 00000000000..f1aac1e7404 Binary files /dev/null and b/tests/gitea-repositories-meta/org42/search-by-path.git/objects/pack/pack-393dc29256bc27cb2ec73898507df710be7a3cf5.pack differ diff --git a/tests/gitea-repositories-meta/org42/search-by-path.git/objects/pack/pack-393dc29256bc27cb2ec73898507df710be7a3cf5.rev b/tests/gitea-repositories-meta/org42/search-by-path.git/objects/pack/pack-393dc29256bc27cb2ec73898507df710be7a3cf5.rev new file mode 100644 index 00000000000..869860ba611 Binary files /dev/null and b/tests/gitea-repositories-meta/org42/search-by-path.git/objects/pack/pack-393dc29256bc27cb2ec73898507df710be7a3cf5.rev differ diff --git a/tests/gitea-repositories-meta/org42/search-by-path.git/packed-refs b/tests/gitea-repositories-meta/org42/search-by-path.git/packed-refs new file mode 100644 index 00000000000..70e69af1e10 --- /dev/null +++ b/tests/gitea-repositories-meta/org42/search-by-path.git/packed-refs @@ -0,0 +1,14 @@ +# pack-refs with: peeled fully-peeled sorted +90c1019714259b24fb81711d4416ac0f18667dfa refs/heads/DefaultBranch +985f0301dba5e7b34be866819cd15ad3d8f508ee refs/heads/branch2 +65f1bf27bc3bf70f64657658635e66094edbcb4d refs/heads/develop +65f1bf27bc3bf70f64657658635e66094edbcb4d refs/heads/feature/1 +78fb907e3a3309eae4fe8fef030874cebbf1cd5e refs/heads/home-md-img-check +3731fe53b763859aaf83e703ee731f6b9447ff1e refs/heads/master +62fb502a7172d4453f0322a2cc85bddffa57f07a refs/heads/pr-to-update +4649299398e4d39a5c09eb4f534df6f1e1eb87cc refs/heads/sub-home-md-img-check +3fa2f829675543ecfc16b2891aebe8bf0608a8f4 refs/notes/commits +4a357436d925b5c974181ff12a994538ddc5a269 refs/pull/2/head +5f22f7d0d95d614d25a5b68592adb345a4b5c7fd refs/pull/3/head +62fb502a7172d4453f0322a2cc85bddffa57f07a refs/pull/5/head +65f1bf27bc3bf70f64657658635e66094edbcb4d refs/tags/v1.1 diff --git a/tests/integration/admin_config_test.go b/tests/integration/admin_config_test.go index 860a92d6a32..eec7e75fd91 100644 --- a/tests/integration/admin_config_test.go +++ b/tests/integration/admin_config_test.go @@ -17,7 +17,7 @@ func TestAdminConfig(t *testing.T) { defer tests.PrepareTestEnv(t)() session := loginUser(t, "user1") - req := NewRequest(t, "GET", "/admin/config") + req := NewRequest(t, "GET", "/-/admin/config") resp := session.MakeRequest(t, req, http.StatusOK) assert.True(t, test.IsNormalPageCompleted(resp.Body.String())) } diff --git a/tests/integration/admin_user_test.go b/tests/integration/admin_user_test.go index 669060c787d..d5d7e70bc7e 100644 --- a/tests/integration/admin_user_test.go +++ b/tests/integration/admin_user_test.go @@ -19,11 +19,11 @@ func TestAdminViewUsers(t *testing.T) { defer tests.PrepareTestEnv(t)() session := loginUser(t, "user1") - req := NewRequest(t, "GET", "/admin/users") + req := NewRequest(t, "GET", "/-/admin/users") session.MakeRequest(t, req, http.StatusOK) session = loginUser(t, "user2") - req = NewRequest(t, "GET", "/admin/users") + req = NewRequest(t, "GET", "/-/admin/users") session.MakeRequest(t, req, http.StatusForbidden) } @@ -31,11 +31,11 @@ func TestAdminViewUser(t *testing.T) { defer tests.PrepareTestEnv(t)() session := loginUser(t, "user1") - req := NewRequest(t, "GET", "/admin/users/1") + req := NewRequest(t, "GET", "/-/admin/users/1") session.MakeRequest(t, req, http.StatusOK) session = loginUser(t, "user2") - req = NewRequest(t, "GET", "/admin/users/1") + req = NewRequest(t, "GET", "/-/admin/users/1") session.MakeRequest(t, req, http.StatusForbidden) } @@ -51,8 +51,8 @@ func testSuccessfullEdit(t *testing.T, formData user_model.User) { func makeRequest(t *testing.T, formData user_model.User, headerCode int) { session := loginUser(t, "user1") - csrf := GetCSRF(t, session, "/admin/users/"+strconv.Itoa(int(formData.ID))+"/edit") - req := NewRequestWithValues(t, "POST", "/admin/users/"+strconv.Itoa(int(formData.ID))+"/edit", map[string]string{ + csrf := GetUserCSRFToken(t, session) + req := NewRequestWithValues(t, "POST", "/-/admin/users/"+strconv.Itoa(int(formData.ID))+"/edit", map[string]string{ "_csrf": csrf, "user_name": formData.Name, "login_name": formData.LoginName, @@ -72,8 +72,8 @@ func TestAdminDeleteUser(t *testing.T) { session := loginUser(t, "user1") - csrf := GetCSRF(t, session, "/admin/users/8/edit") - req := NewRequestWithValues(t, "POST", "/admin/users/8/delete", map[string]string{ + csrf := GetUserCSRFToken(t, session) + req := NewRequestWithValues(t, "POST", "/-/admin/users/8/delete", map[string]string{ "_csrf": csrf, }) session.MakeRequest(t, req, http.StatusSeeOther) diff --git a/tests/integration/api_httpsig_test.go b/tests/integration/api_httpsig_test.go index cca477f5e13..b9dc508ad09 100644 --- a/tests/integration/api_httpsig_test.go +++ b/tests/integration/api_httpsig_test.go @@ -95,7 +95,7 @@ func TestHTTPSigCert(t *testing.T) { defer tests.PrepareTestEnv(t)() session := loginUser(t, "user1") - csrf := GetCSRF(t, session, "/user/settings/keys") + csrf := GetUserCSRFToken(t, session) req := NewRequestWithValues(t, "POST", "/user/settings/keys", map[string]string{ "_csrf": csrf, "content": "user1", diff --git a/tests/integration/api_org_test.go b/tests/integration/api_org_test.go index 70d3a446f76..fff121490c9 100644 --- a/tests/integration/api_org_test.go +++ b/tests/integration/api_org_test.go @@ -177,7 +177,7 @@ func TestAPIGetAll(t *testing.T) { var apiOrgList []*api.Organization DecodeJSON(t, resp, &apiOrgList) - assert.Len(t, apiOrgList, 12) + assert.Len(t, apiOrgList, 13) assert.Equal(t, "Limited Org 36", apiOrgList[1].FullName) assert.Equal(t, "limited", apiOrgList[1].Visibility) @@ -186,7 +186,7 @@ func TestAPIGetAll(t *testing.T) { resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiOrgList) - assert.Len(t, apiOrgList, 8) + assert.Len(t, apiOrgList, 9) assert.Equal(t, "org 17", apiOrgList[0].FullName) assert.Equal(t, "public", apiOrgList[0].Visibility) } diff --git a/tests/integration/api_packages_container_test.go b/tests/integration/api_packages_container_test.go index 409e7513a6e..3905ad1b703 100644 --- a/tests/integration/api_packages_container_test.go +++ b/tests/integration/api_packages_container_test.go @@ -784,7 +784,7 @@ func TestPackageContainer(t *testing.T) { newOwnerName := "newUsername" req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{ - "_csrf": GetCSRF(t, session, "/user/settings"), + "_csrf": GetUserCSRFToken(t, session), "name": newOwnerName, "email": "user2@example.com", "language": "en-US", @@ -794,7 +794,7 @@ func TestPackageContainer(t *testing.T) { t.Run(fmt.Sprintf("Catalog[%s]", newOwnerName), checkCatalog(newOwnerName)) req = NewRequestWithValues(t, "POST", "/user/settings", map[string]string{ - "_csrf": GetCSRF(t, session, "/user/settings"), + "_csrf": GetUserCSRFToken(t, session), "name": user.Name, "email": "user2@example.com", "language": "en-US", diff --git a/tests/integration/api_repo_test.go b/tests/integration/api_repo_test.go index 716da762e54..93c9ca0920d 100644 --- a/tests/integration/api_repo_test.go +++ b/tests/integration/api_repo_test.go @@ -94,9 +94,9 @@ func TestAPISearchRepo(t *testing.T) { }{ { name: "RepositoriesMax50", requestURL: "/api/v1/repos/search?limit=50&private=false", expectedResults: expectedResults{ - nil: {count: 35}, - user: {count: 35}, - user2: {count: 35}, + nil: {count: 36}, + user: {count: 36}, + user2: {count: 36}, }, }, { diff --git a/tests/integration/attachment_test.go b/tests/integration/attachment_test.go index 11aa03bb7e7..30c394e9b02 100644 --- a/tests/integration/attachment_test.go +++ b/tests/integration/attachment_test.go @@ -57,14 +57,14 @@ func createAttachment(t *testing.T, session *TestSession, csrf, repoURL, filenam func TestCreateAnonymousAttachment(t *testing.T) { defer tests.PrepareTestEnv(t)() session := emptyTestSession(t) - createAttachment(t, session, GetCSRF(t, session, "/user/login"), "user2/repo1", "image.png", generateImg(), http.StatusSeeOther) + createAttachment(t, session, GetAnonymousCSRFToken(t, session), "user2/repo1", "image.png", generateImg(), http.StatusSeeOther) } func TestCreateIssueAttachment(t *testing.T) { defer tests.PrepareTestEnv(t)() const repoURL = "user2/repo1" session := loginUser(t, "user2") - uuid := createAttachment(t, session, GetCSRF(t, session, repoURL), repoURL, "image.png", generateImg(), http.StatusOK) + uuid := createAttachment(t, session, GetUserCSRFToken(t, session), repoURL, "image.png", generateImg(), http.StatusOK) req := NewRequest(t, "GET", repoURL+"/issues/new") resp := session.MakeRequest(t, req, http.StatusOK) diff --git a/tests/integration/auth_ldap_test.go b/tests/integration/auth_ldap_test.go index 317787f4031..8c8b6b02d14 100644 --- a/tests/integration/auth_ldap_test.go +++ b/tests/integration/auth_ldap_test.go @@ -156,8 +156,8 @@ func addAuthSourceLDAP(t *testing.T, sshKeyAttribute, groupFilter string, groupM groupTeamMap = groupMapParams[1] } session := loginUser(t, "user1") - csrf := GetCSRF(t, session, "/admin/auths/new") - req := NewRequestWithValues(t, "POST", "/admin/auths/new", buildAuthSourceLDAPPayload(csrf, sshKeyAttribute, groupFilter, groupTeamMap, groupTeamMapRemoval)) + csrf := GetUserCSRFToken(t, session) + req := NewRequestWithValues(t, "POST", "/-/admin/auths/new", buildAuthSourceLDAPPayload(csrf, sshKeyAttribute, groupFilter, groupTeamMap, groupTeamMapRemoval)) session.MakeRequest(t, req, http.StatusSeeOther) } @@ -187,7 +187,7 @@ func TestLDAPAuthChange(t *testing.T) { addAuthSourceLDAP(t, "", "") session := loginUser(t, "user1") - req := NewRequest(t, "GET", "/admin/auths") + req := NewRequest(t, "GET", "/-/admin/auths") resp := session.MakeRequest(t, req, http.StatusOK) doc := NewHTMLParser(t, resp.Body) href, exists := doc.Find("table.table td a").Attr("href") @@ -252,14 +252,14 @@ func TestLDAPUserSyncWithEmptyUsernameAttribute(t *testing.T) { defer tests.PrepareTestEnv(t)() session := loginUser(t, "user1") - csrf := GetCSRF(t, session, "/admin/auths/new") + csrf := GetUserCSRFToken(t, session) payload := buildAuthSourceLDAPPayload(csrf, "", "", "", "") payload["attribute_username"] = "" - req := NewRequestWithValues(t, "POST", "/admin/auths/new", payload) + req := NewRequestWithValues(t, "POST", "/-/admin/auths/new", payload) session.MakeRequest(t, req, http.StatusSeeOther) for _, u := range gitLDAPUsers { - req := NewRequest(t, "GET", "/admin/users?q="+u.UserName) + req := NewRequest(t, "GET", "/-/admin/users?q="+u.UserName) resp := session.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) @@ -487,7 +487,7 @@ func TestLDAPPreventInvalidGroupTeamMap(t *testing.T) { defer tests.PrepareTestEnv(t)() session := loginUser(t, "user1") - csrf := GetCSRF(t, session, "/admin/auths/new") - req := NewRequestWithValues(t, "POST", "/admin/auths/new", buildAuthSourceLDAPPayload(csrf, "", "", `{"NOT_A_VALID_JSON"["MISSING_DOUBLE_POINT"]}`, "off")) + csrf := GetUserCSRFToken(t, session) + req := NewRequestWithValues(t, "POST", "/-/admin/auths/new", buildAuthSourceLDAPPayload(csrf, "", "", `{"NOT_A_VALID_JSON"["MISSING_DOUBLE_POINT"]}`, "off")) session.MakeRequest(t, req, http.StatusOK) // StatusOK = failed, StatusSeeOther = ok } diff --git a/tests/integration/change_default_branch_test.go b/tests/integration/change_default_branch_test.go index 703834b7129..729eb1e4ce6 100644 --- a/tests/integration/change_default_branch_test.go +++ b/tests/integration/change_default_branch_test.go @@ -22,7 +22,7 @@ func TestChangeDefaultBranch(t *testing.T) { session := loginUser(t, owner.Name) branchesURL := fmt.Sprintf("/%s/%s/settings/branches", owner.Name, repo.Name) - csrf := GetCSRF(t, session, branchesURL) + csrf := GetUserCSRFToken(t, session) req := NewRequestWithValues(t, "POST", branchesURL, map[string]string{ "_csrf": csrf, "action": "default_branch", @@ -30,7 +30,7 @@ func TestChangeDefaultBranch(t *testing.T) { }) session.MakeRequest(t, req, http.StatusSeeOther) - csrf = GetCSRF(t, session, branchesURL) + csrf = GetUserCSRFToken(t, session) req = NewRequestWithValues(t, "POST", branchesURL, map[string]string{ "_csrf": csrf, "action": "default_branch", diff --git a/tests/integration/delete_user_test.go b/tests/integration/delete_user_test.go index 806b87dc4ce..ad3c8828820 100644 --- a/tests/integration/delete_user_test.go +++ b/tests/integration/delete_user_test.go @@ -33,7 +33,7 @@ func TestUserDeleteAccount(t *testing.T) { defer tests.PrepareTestEnv(t)() session := loginUser(t, "user8") - csrf := GetCSRF(t, session, "/user/settings/account") + csrf := GetUserCSRFToken(t, session) urlStr := fmt.Sprintf("/user/settings/account/delete?password=%s", userPassword) req := NewRequestWithValues(t, "POST", urlStr, map[string]string{ "_csrf": csrf, @@ -48,7 +48,7 @@ func TestUserDeleteAccountStillOwnRepos(t *testing.T) { defer tests.PrepareTestEnv(t)() session := loginUser(t, "user2") - csrf := GetCSRF(t, session, "/user/settings/account") + csrf := GetUserCSRFToken(t, session) urlStr := fmt.Sprintf("/user/settings/account/delete?password=%s", userPassword) req := NewRequestWithValues(t, "POST", urlStr, map[string]string{ "_csrf": csrf, diff --git a/tests/integration/editor_test.go b/tests/integration/editor_test.go index f510c79bc6b..f0f71b80d1b 100644 --- a/tests/integration/editor_test.go +++ b/tests/integration/editor_test.go @@ -49,7 +49,7 @@ func TestCreateFileOnProtectedBranch(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user2") - csrf := GetCSRF(t, session, "/user2/repo1/settings/branches") + csrf := GetUserCSRFToken(t, session) // Change master branch to protected req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{ "_csrf": csrf, @@ -84,7 +84,7 @@ func TestCreateFileOnProtectedBranch(t *testing.T) { assert.Contains(t, resp.Body.String(), "Cannot commit to protected branch "master".") // remove the protected branch - csrf = GetCSRF(t, session, "/user2/repo1/settings/branches") + csrf = GetUserCSRFToken(t, session) // Change master branch to protected req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/1/delete", map[string]string{ diff --git a/tests/integration/empty_repo_test.go b/tests/integration/empty_repo_test.go index 002aa5600e0..630a3c03af8 100644 --- a/tests/integration/empty_repo_test.go +++ b/tests/integration/empty_repo_test.go @@ -29,7 +29,7 @@ import ( func testAPINewFile(t *testing.T, session *TestSession, user, repo, branch, treePath, content string) *httptest.ResponseRecorder { url := fmt.Sprintf("/%s/%s/_new/%s", user, repo, branch) req := NewRequestWithValues(t, "POST", url, map[string]string{ - "_csrf": GetCSRF(t, session, "/user/settings"), + "_csrf": GetUserCSRFToken(t, session), "commit_choice": "direct", "tree_path": treePath, "content": content, @@ -63,7 +63,7 @@ func TestEmptyRepoAddFile(t *testing.T) { doc := NewHTMLParser(t, resp.Body).Find(`input[name="commit_choice"]`) assert.Empty(t, doc.AttrOr("checked", "_no_")) req = NewRequestWithValues(t, "POST", "/user30/empty/_new/"+setting.Repository.DefaultBranch, map[string]string{ - "_csrf": GetCSRF(t, session, "/user/settings"), + "_csrf": GetUserCSRFToken(t, session), "commit_choice": "direct", "tree_path": "test-file.md", "content": "newly-added-test-file", @@ -89,7 +89,7 @@ func TestEmptyRepoUploadFile(t *testing.T) { body := &bytes.Buffer{} mpForm := multipart.NewWriter(body) - _ = mpForm.WriteField("_csrf", GetCSRF(t, session, "/user/settings")) + _ = mpForm.WriteField("_csrf", GetUserCSRFToken(t, session)) file, _ := mpForm.CreateFormFile("file", "uploaded-file.txt") _, _ = io.Copy(file, bytes.NewBufferString("newly-uploaded-test-file")) _ = mpForm.Close() @@ -101,7 +101,7 @@ func TestEmptyRepoUploadFile(t *testing.T) { assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), &respMap)) req = NewRequestWithValues(t, "POST", "/user30/empty/_upload/"+setting.Repository.DefaultBranch, map[string]string{ - "_csrf": GetCSRF(t, session, "/user/settings"), + "_csrf": GetUserCSRFToken(t, session), "commit_choice": "direct", "files": respMap["uuid"], "tree_path": "", diff --git a/tests/integration/git_test.go b/tests/integration/git_test.go index ac56cffe5e6..76db3c69324 100644 --- a/tests/integration/git_test.go +++ b/tests/integration/git_test.go @@ -5,6 +5,7 @@ package integration import ( "bytes" + "context" "crypto/rand" "encoding/hex" "fmt" @@ -462,7 +463,7 @@ func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *tes func doProtectBranch(ctx APITestContext, branch, userToWhitelistPush, userToWhitelistForcePush, unprotectedFilePatterns string) func(t *testing.T) { // We are going to just use the owner to set the protection. return func(t *testing.T) { - csrf := GetCSRF(t, ctx.Session, fmt.Sprintf("/%s/%s/settings/branches", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame))) + csrf := GetUserCSRFToken(t, ctx.Session) formData := map[string]string{ "_csrf": csrf, @@ -644,7 +645,7 @@ func doPushCreate(ctx APITestContext, u *url.URL) func(t *testing.T) { func doBranchDelete(ctx APITestContext, owner, repo, branch string) func(*testing.T) { return func(t *testing.T) { - csrf := GetCSRF(t, ctx.Session, fmt.Sprintf("/%s/%s/branches", url.PathEscape(owner), url.PathEscape(repo))) + csrf := GetUserCSRFToken(t, ctx.Session) req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/branches/delete?name=%s", url.PathEscape(owner), url.PathEscape(repo), url.QueryEscape(branch)), map[string]string{ "_csrf": csrf, @@ -943,3 +944,59 @@ func TestDataAsync_Issue29101(t *testing.T) { defer r2.Close() }) } + +func TestAgitPullPush(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + baseAPITestContext := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + u.Path = baseAPITestContext.GitPath() + u.User = url.UserPassword("user2", userPassword) + + dstPath := t.TempDir() + doGitClone(dstPath, u)(t) + + gitRepo, err := git.OpenRepository(context.Background(), dstPath) + assert.NoError(t, err) + defer gitRepo.Close() + + doGitCreateBranch(dstPath, "test-agit-push") + + // commit 1 + _, err = generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-") + assert.NoError(t, err) + + // push to create an agit pull request + err = git.NewCommand(git.DefaultContext, "push", "origin", + "-o", "title=test-title", "-o", "description=test-description", + "HEAD:refs/for/master/test-agit-push", + ).Run(&git.RunOpts{Dir: dstPath}) + assert.NoError(t, err) + + // check pull request exist + pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: 1, Flow: issues_model.PullRequestFlowAGit, HeadBranch: "user2/test-agit-push"}) + assert.NoError(t, pr.LoadIssue(db.DefaultContext)) + assert.Equal(t, "test-title", pr.Issue.Title) + assert.Equal(t, "test-description", pr.Issue.Content) + + // commit 2 + _, err = generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-2-") + assert.NoError(t, err) + + // push 2 + err = git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master/test-agit-push").Run(&git.RunOpts{Dir: dstPath}) + assert.NoError(t, err) + + // reset to first commit + err = git.NewCommand(git.DefaultContext, "reset", "--hard", "HEAD~1").Run(&git.RunOpts{Dir: dstPath}) + assert.NoError(t, err) + + // test force push without confirm + _, stderr, err := git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master/test-agit-push").RunStdString(&git.RunOpts{Dir: dstPath}) + assert.Error(t, err) + assert.Contains(t, stderr, "[remote rejected] HEAD -> refs/for/master/test-agit-push (request `force-push` push option)") + + // test force push with confirm + err = git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master/test-agit-push", "-o", "force-push").Run(&git.RunOpts{Dir: dstPath}) + assert.NoError(t, err) + }) +} diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index 1f12430fcfb..f72ac5f51c3 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -486,23 +486,19 @@ func VerifyJSONSchema(t testing.TB, resp *httptest.ResponseRecorder, schemaFile assert.True(t, result.Valid()) } -// GetCSRF returns CSRF token from body -// If it fails, it means the CSRF token is not found in the response body returned by the url with the given session. -// In this case, you should find a better url to get it. -func GetCSRF(t testing.TB, session *TestSession, urlStr string) string { +// GetUserCSRFToken returns CSRF token for current user +func GetUserCSRFToken(t testing.TB, session *TestSession) string { t.Helper() - req := NewRequest(t, "GET", urlStr) - resp := session.MakeRequest(t, req, http.StatusOK) - doc := NewHTMLParser(t, resp.Body) - csrf := doc.GetCSRF() - require.NotEmpty(t, csrf) - return csrf + cookie := session.GetCookie("_csrf") + require.NotEmpty(t, cookie) + return cookie.Value } -// GetCSRFFrom returns CSRF token from body -func GetCSRFFromCookie(t testing.TB, session *TestSession, urlStr string) string { +// GetUserCSRFToken returns CSRF token for anonymous user (not logged in) +func GetAnonymousCSRFToken(t testing.TB, session *TestSession) string { t.Helper() - req := NewRequest(t, "GET", urlStr) - session.MakeRequest(t, req, http.StatusOK) - return session.GetCookie("_csrf").Value + resp := session.MakeRequest(t, NewRequest(t, "GET", "/user/login"), http.StatusOK) + csrfToken := NewHTMLParser(t, resp.Body).GetCSRF() + require.NotEmpty(t, csrfToken) + return csrfToken } diff --git a/tests/integration/issue_test.go b/tests/integration/issue_test.go index 308b82d4b95..df45da84a55 100644 --- a/tests/integration/issue_test.go +++ b/tests/integration/issue_test.go @@ -197,21 +197,21 @@ func TestEditIssue(t *testing.T) { issueURL := testNewIssue(t, session, "user2", "repo1", "Title", "Description") req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/content", issueURL), map[string]string{ - "_csrf": GetCSRF(t, session, issueURL), + "_csrf": GetUserCSRFToken(t, session), "content": "modified content", "context": fmt.Sprintf("/%s/%s", "user2", "repo1"), }) session.MakeRequest(t, req, http.StatusOK) req = NewRequestWithValues(t, "POST", fmt.Sprintf("%s/content", issueURL), map[string]string{ - "_csrf": GetCSRF(t, session, issueURL), + "_csrf": GetUserCSRFToken(t, session), "content": "modified content", "context": fmt.Sprintf("/%s/%s", "user2", "repo1"), }) session.MakeRequest(t, req, http.StatusBadRequest) req = NewRequestWithValues(t, "POST", fmt.Sprintf("%s/content", issueURL), map[string]string{ - "_csrf": GetCSRF(t, session, issueURL), + "_csrf": GetUserCSRFToken(t, session), "content": "modified content", "content_version": "1", "context": fmt.Sprintf("/%s/%s", "user2", "repo1"), @@ -246,11 +246,11 @@ func TestIssueCommentDelete(t *testing.T) { // Using the ID of a comment that does not belong to the repository must fail req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d/delete", "user5", "repo4", commentID), map[string]string{ - "_csrf": GetCSRF(t, session, issueURL), + "_csrf": GetUserCSRFToken(t, session), }) session.MakeRequest(t, req, http.StatusNotFound) req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d/delete", "user2", "repo1", commentID), map[string]string{ - "_csrf": GetCSRF(t, session, issueURL), + "_csrf": GetUserCSRFToken(t, session), }) session.MakeRequest(t, req, http.StatusOK) unittest.AssertNotExistsBean(t, &issues_model.Comment{ID: commentID}) @@ -270,13 +270,13 @@ func TestIssueCommentUpdate(t *testing.T) { // Using the ID of a comment that does not belong to the repository must fail req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user5", "repo4", commentID), map[string]string{ - "_csrf": GetCSRF(t, session, issueURL), + "_csrf": GetUserCSRFToken(t, session), "content": modifiedContent, }) session.MakeRequest(t, req, http.StatusNotFound) req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{ - "_csrf": GetCSRF(t, session, issueURL), + "_csrf": GetUserCSRFToken(t, session), "content": modifiedContent, }) session.MakeRequest(t, req, http.StatusOK) @@ -298,7 +298,7 @@ func TestIssueCommentUpdateSimultaneously(t *testing.T) { modifiedContent := comment.Content + "MODIFIED" req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{ - "_csrf": GetCSRF(t, session, issueURL), + "_csrf": GetUserCSRFToken(t, session), "content": modifiedContent, }) session.MakeRequest(t, req, http.StatusOK) @@ -306,13 +306,13 @@ func TestIssueCommentUpdateSimultaneously(t *testing.T) { modifiedContent = comment.Content + "2" req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{ - "_csrf": GetCSRF(t, session, issueURL), + "_csrf": GetUserCSRFToken(t, session), "content": modifiedContent, }) session.MakeRequest(t, req, http.StatusBadRequest) req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/comments/%d", "user2", "repo1", commentID), map[string]string{ - "_csrf": GetCSRF(t, session, issueURL), + "_csrf": GetUserCSRFToken(t, session), "content": modifiedContent, "content_version": "1", }) diff --git a/tests/integration/mirror_push_test.go b/tests/integration/mirror_push_test.go index 1c262b33496..6b1c808cf46 100644 --- a/tests/integration/mirror_push_test.go +++ b/tests/integration/mirror_push_test.go @@ -81,7 +81,7 @@ func testMirrorPush(t *testing.T, u *url.URL) { func doCreatePushMirror(ctx APITestContext, address, username, password string) func(t *testing.T) { return func(t *testing.T) { - csrf := GetCSRF(t, ctx.Session, fmt.Sprintf("/%s/%s/settings", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame))) + csrf := GetUserCSRFToken(t, ctx.Session) req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)), map[string]string{ "_csrf": csrf, @@ -101,7 +101,7 @@ func doCreatePushMirror(ctx APITestContext, address, username, password string) func doRemovePushMirror(ctx APITestContext, address, username, password string, pushMirrorID int) func(t *testing.T) { return func(t *testing.T) { - csrf := GetCSRF(t, ctx.Session, fmt.Sprintf("/%s/%s/settings", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame))) + csrf := GetUserCSRFToken(t, ctx.Session) req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)), map[string]string{ "_csrf": csrf, diff --git a/tests/integration/nonascii_branches_test.go b/tests/integration/nonascii_branches_test.go index a189273eacd..e5934a148d8 100644 --- a/tests/integration/nonascii_branches_test.go +++ b/tests/integration/nonascii_branches_test.go @@ -17,7 +17,7 @@ import ( func setDefaultBranch(t *testing.T, session *TestSession, user, repo, branch string) { location := path.Join("/", user, repo, "settings/branches") - csrf := GetCSRF(t, session, location) + csrf := GetUserCSRFToken(t, session) req := NewRequestWithValues(t, "POST", location, map[string]string{ "_csrf": csrf, "action": "default_branch", diff --git a/tests/integration/org_project_test.go b/tests/integration/org_project_test.go index 31d10f16ff1..c3894fd7afd 100644 --- a/tests/integration/org_project_test.go +++ b/tests/integration/org_project_test.go @@ -34,7 +34,7 @@ func TestOrgProjectAccess(t *testing.T) { // change the org's visibility to private session := loginUser(t, "user2") req = NewRequestWithValues(t, "POST", "/org/org3/settings", map[string]string{ - "_csrf": GetCSRF(t, session, "/org3/-/projects"), + "_csrf": GetUserCSRFToken(t, session), "name": "org3", "visibility": "2", }) @@ -48,7 +48,7 @@ func TestOrgProjectAccess(t *testing.T) { // disable team1's project unit session = loginUser(t, "user2") req = NewRequestWithValues(t, "POST", "/org/org3/teams/team1/edit", map[string]string{ - "_csrf": GetCSRF(t, session, "/org3/-/projects"), + "_csrf": GetUserCSRFToken(t, session), "team_name": "team1", "repo_access": "specific", "permission": "read", diff --git a/tests/integration/org_team_invite_test.go b/tests/integration/org_team_invite_test.go index 919769a61a2..274fde40850 100644 --- a/tests/integration/org_team_invite_test.go +++ b/tests/integration/org_team_invite_test.go @@ -40,7 +40,7 @@ func TestOrgTeamEmailInvite(t *testing.T) { session := loginUser(t, "user1") teamURL := fmt.Sprintf("/org/%s/teams/%s", org.Name, team.Name) - csrf := GetCSRF(t, session, teamURL) + csrf := GetUserCSRFToken(t, session) req := NewRequestWithValues(t, "POST", teamURL+"/action/add", map[string]string{ "_csrf": csrf, "uid": "1", @@ -59,7 +59,7 @@ func TestOrgTeamEmailInvite(t *testing.T) { // join the team inviteURL := fmt.Sprintf("/org/invite/%s", invites[0].Token) - csrf = GetCSRF(t, session, inviteURL) + csrf = GetUserCSRFToken(t, session) req = NewRequestWithValues(t, "POST", inviteURL, map[string]string{ "_csrf": csrf, }) @@ -94,7 +94,7 @@ func TestOrgTeamEmailInviteRedirectsExistingUser(t *testing.T) { teamURL := fmt.Sprintf("/org/%s/teams/%s", org.Name, team.Name) req := NewRequestWithValues(t, "POST", teamURL+"/action/add", map[string]string{ - "_csrf": GetCSRF(t, session, teamURL), + "_csrf": GetUserCSRFToken(t, session), "uid": "1", "uname": user.Email, }) @@ -137,7 +137,7 @@ func TestOrgTeamEmailInviteRedirectsExistingUser(t *testing.T) { // make the request req = NewRequestWithValues(t, "POST", test.RedirectURL(resp), map[string]string{ - "_csrf": GetCSRF(t, session, test.RedirectURL(resp)), + "_csrf": GetUserCSRFToken(t, session), }) resp = session.MakeRequest(t, req, http.StatusSeeOther) req = NewRequest(t, "GET", test.RedirectURL(resp)) @@ -165,7 +165,7 @@ func TestOrgTeamEmailInviteRedirectsNewUser(t *testing.T) { teamURL := fmt.Sprintf("/org/%s/teams/%s", org.Name, team.Name) req := NewRequestWithValues(t, "POST", teamURL+"/action/add", map[string]string{ - "_csrf": GetCSRF(t, session, teamURL), + "_csrf": GetUserCSRFToken(t, session), "uid": "1", "uname": "doesnotexist@example.com", }) @@ -210,7 +210,7 @@ func TestOrgTeamEmailInviteRedirectsNewUser(t *testing.T) { // make the redirected request req = NewRequestWithValues(t, "POST", test.RedirectURL(resp), map[string]string{ - "_csrf": GetCSRF(t, session, test.RedirectURL(resp)), + "_csrf": GetUserCSRFToken(t, session), }) resp = session.MakeRequest(t, req, http.StatusSeeOther) req = NewRequest(t, "GET", test.RedirectURL(resp)) @@ -233,22 +233,18 @@ func TestOrgTeamEmailInviteRedirectsNewUserWithActivation(t *testing.T) { } // enable email confirmation temporarily - defer func(prevVal bool) { - setting.Service.RegisterEmailConfirm = prevVal - }(setting.Service.RegisterEmailConfirm) - setting.Service.RegisterEmailConfirm = true - + defer test.MockVariableValue(&setting.Service.RegisterEmailConfirm, true)() defer tests.PrepareTestEnv(t)() org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2}) - // create the invite + // user1: create the invite session := loginUser(t, "user1") teamURL := fmt.Sprintf("/org/%s/teams/%s", org.Name, team.Name) req := NewRequestWithValues(t, "POST", teamURL+"/action/add", map[string]string{ - "_csrf": GetCSRF(t, session, teamURL), + "_csrf": GetUserCSRFToken(t, session), "uid": "1", "uname": "doesnotexist@example.com", }) @@ -261,53 +257,34 @@ func TestOrgTeamEmailInviteRedirectsNewUserWithActivation(t *testing.T) { assert.NoError(t, err) assert.Len(t, invites, 1) - // accept the invite + // new user: accept the invite + session = emptyTestSession(t) + inviteURL := fmt.Sprintf("/org/invite/%s", invites[0].Token) req = NewRequest(t, "GET", fmt.Sprintf("/user/sign_up?redirect_to=%s", url.QueryEscape(inviteURL))) - inviteResp := MakeRequest(t, req, http.StatusOK) - - doc := NewHTMLParser(t, resp.Body) + session.MakeRequest(t, req, http.StatusOK) req = NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{ - "_csrf": doc.GetCSRF(), "user_name": "doesnotexist", "email": "doesnotexist@example.com", "password": "examplePassword!1", "retype": "examplePassword!1", }) - for _, c := range inviteResp.Result().Cookies() { - req.AddCookie(c) - } - - resp = MakeRequest(t, req, http.StatusOK) + session.MakeRequest(t, req, http.StatusOK) user, err := user_model.GetUserByName(db.DefaultContext, "doesnotexist") assert.NoError(t, err) - ch := http.Header{} - ch.Add("Cookie", strings.Join(resp.Header()["Set-Cookie"], ";")) - cr := http.Request{Header: ch} - - session = emptyTestSession(t) - baseURL, err := url.Parse(setting.AppURL) - assert.NoError(t, err) - session.jar.SetCookies(baseURL, cr.Cookies()) - activateURL := fmt.Sprintf("/user/activate?code=%s", user.GenerateEmailActivateCode("doesnotexist@example.com")) req = NewRequestWithValues(t, "POST", activateURL, map[string]string{ "password": "examplePassword!1", }) - // use the cookies set by the signup request - for _, c := range inviteResp.Result().Cookies() { - req.AddCookie(c) - } - resp = session.MakeRequest(t, req, http.StatusSeeOther) // should be redirected to accept the invite assert.Equal(t, inviteURL, test.RedirectURL(resp)) req = NewRequestWithValues(t, "POST", test.RedirectURL(resp), map[string]string{ - "_csrf": GetCSRF(t, session, test.RedirectURL(resp)), + "_csrf": GetUserCSRFToken(t, session), }) resp = session.MakeRequest(t, req, http.StatusSeeOther) req = NewRequest(t, "GET", test.RedirectURL(resp)) @@ -342,7 +319,7 @@ func TestOrgTeamEmailInviteRedirectsExistingUserWithLogin(t *testing.T) { teamURL := fmt.Sprintf("/org/%s/teams/%s", org.Name, team.Name) req := NewRequestWithValues(t, "POST", teamURL+"/action/add", map[string]string{ - "_csrf": GetCSRF(t, session, teamURL), + "_csrf": GetUserCSRFToken(t, session), "uid": "1", "uname": user.Email, }) @@ -366,7 +343,7 @@ func TestOrgTeamEmailInviteRedirectsExistingUserWithLogin(t *testing.T) { // make the request req = NewRequestWithValues(t, "POST", test.RedirectURL(resp), map[string]string{ - "_csrf": GetCSRF(t, session, test.RedirectURL(resp)), + "_csrf": GetUserCSRFToken(t, session), }) resp = session.MakeRequest(t, req, http.StatusSeeOther) req = NewRequest(t, "GET", test.RedirectURL(resp)) diff --git a/tests/integration/privateactivity_test.go b/tests/integration/privateactivity_test.go index 5362462f7df..a1fbadec99e 100644 --- a/tests/integration/privateactivity_test.go +++ b/tests/integration/privateactivity_test.go @@ -48,7 +48,7 @@ func testPrivateActivityDoSomethingForActionEntries(t *testing.T) { func testPrivateActivityHelperEnablePrivateActivity(t *testing.T) { session := loginUser(t, privateActivityTestUser) req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{ - "_csrf": GetCSRF(t, session, "/user/settings"), + "_csrf": GetUserCSRFToken(t, session), "name": privateActivityTestUser, "email": privateActivityTestUser + "@example.com", "language": "en-US", diff --git a/tests/integration/pull_compare_test.go b/tests/integration/pull_compare_test.go index aed699fd200..def6506253f 100644 --- a/tests/integration/pull_compare_test.go +++ b/tests/integration/pull_compare_test.go @@ -14,6 +14,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/test" repo_service "code.gitea.io/gitea/services/repository" "code.gitea.io/gitea/tests" @@ -73,3 +74,80 @@ func TestPullCompare(t *testing.T) { assert.EqualValues(t, editButtonCount, 0, "Expected not to find a button to edit a file in the PR diff view because head repository has been deleted") }) } + +func TestPullCompare_EnableAllowEditsFromMaintainer(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + // repo3 is private + repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) + assert.True(t, repo3.IsPrivate) + + // user4 forks repo3 + user4Session := loginUser(t, "user4") + forkedRepoName := "user4-forked-repo3" + testRepoFork(t, user4Session, repo3.OwnerName, repo3.Name, "user4", forkedRepoName, "") + forkedRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user4", Name: forkedRepoName}) + assert.True(t, forkedRepo.IsPrivate) + + // user4 creates a new branch and a PR + testEditFileToNewBranch(t, user4Session, "user4", forkedRepoName, "master", "user4/update-readme", "README.md", "Hello, World\n(Edited by user4)\n") + resp := testPullCreateDirectly(t, user4Session, repo3.OwnerName, repo3.Name, "master", "user4", forkedRepoName, "user4/update-readme", "PR for user4 forked repo3") + prURL := test.RedirectURL(resp) + + // user2 (admin of repo3) goes to the PR files page + user2Session := loginUser(t, "user2") + resp = user2Session.MakeRequest(t, NewRequest(t, "GET", fmt.Sprintf("%s/files", prURL)), http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + nodes := htmlDoc.doc.Find(".diff-file-box[data-new-filename=\"README.md\"] .diff-file-header-actions .dropdown .menu a") + if assert.Equal(t, 1, nodes.Length()) { + // there is only "View File" button, no "Edit File" button + assert.Equal(t, "View File", nodes.First().Text()) + viewFileLink, exists := nodes.First().Attr("href") + if assert.True(t, exists) { + user2Session.MakeRequest(t, NewRequest(t, "GET", viewFileLink), http.StatusOK) + } + } + + // user4 goes to the PR page and enable "Allow maintainers to edit" + resp = user4Session.MakeRequest(t, NewRequest(t, "GET", prURL), http.StatusOK) + htmlDoc = NewHTMLParser(t, resp.Body) + dataURL, exists := htmlDoc.doc.Find("#allow-edits-from-maintainers").Attr("data-url") + assert.True(t, exists) + req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/set_allow_maintainer_edit", dataURL), map[string]string{ + "_csrf": htmlDoc.GetCSRF(), + "allow_maintainer_edit": "true", + }) + user4Session.MakeRequest(t, req, http.StatusOK) + + // user2 (admin of repo3) goes to the PR files page again + resp = user2Session.MakeRequest(t, NewRequest(t, "GET", fmt.Sprintf("%s/files", prURL)), http.StatusOK) + htmlDoc = NewHTMLParser(t, resp.Body) + nodes = htmlDoc.doc.Find(".diff-file-box[data-new-filename=\"README.md\"] .diff-file-header-actions .dropdown .menu a") + if assert.Equal(t, 2, nodes.Length()) { + // there are "View File" button and "Edit File" button + assert.Equal(t, "View File", nodes.First().Text()) + viewFileLink, exists := nodes.First().Attr("href") + if assert.True(t, exists) { + user2Session.MakeRequest(t, NewRequest(t, "GET", viewFileLink), http.StatusOK) + } + + assert.Equal(t, "Edit File", nodes.Last().Text()) + editFileLink, exists := nodes.Last().Attr("href") + if assert.True(t, exists) { + // edit the file + resp := user2Session.MakeRequest(t, NewRequest(t, "GET", editFileLink), http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + lastCommit := htmlDoc.GetInputValueByName("last_commit") + assert.NotEmpty(t, lastCommit) + req := NewRequestWithValues(t, "POST", editFileLink, map[string]string{ + "_csrf": htmlDoc.GetCSRF(), + "last_commit": lastCommit, + "tree_path": "README.md", + "content": "File is edited by the maintainer user2", + "commit_summary": "user2 updated the file", + "commit_choice": "direct", + }) + user2Session.MakeRequest(t, req, http.StatusSeeOther) + } + } + }) +} diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index 9a412329a1c..c1c8a8bf4e8 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -694,7 +694,7 @@ func TestPullAutoMergeAfterCommitStatusSucceed(t *testing.T) { }) // add protected branch for commit status - csrf := GetCSRF(t, session, "/user2/repo1/settings/branches") + csrf := GetUserCSRFToken(t, session) // Change master branch to protected req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{ "_csrf": csrf, @@ -777,7 +777,7 @@ func TestPullAutoMergeAfterCommitStatusSucceedAndApproval(t *testing.T) { }) // add protected branch for commit status - csrf := GetCSRF(t, session, "/user2/repo1/settings/branches") + csrf := GetUserCSRFToken(t, session) // Change master branch to protected req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{ "_csrf": csrf, @@ -905,7 +905,7 @@ func TestPullAutoMergeAfterCommitStatusSucceedAndApprovalForAgitFlow(t *testing. session := loginUser(t, "user1") // add protected branch for commit status - csrf := GetCSRF(t, session, "/user2/repo1/settings/branches") + csrf := GetUserCSRFToken(t, session) // Change master branch to protected req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{ "_csrf": csrf, diff --git a/tests/integration/pull_status_test.go b/tests/integration/pull_status_test.go index 26e1baeb113..ac9036ca962 100644 --- a/tests/integration/pull_status_test.go +++ b/tests/integration/pull_status_test.go @@ -29,7 +29,7 @@ func TestPullCreate_CommitStatus(t *testing.T) { url := path.Join("user1", "repo1", "compare", "master...status1") req := NewRequestWithValues(t, "POST", url, map[string]string{ - "_csrf": GetCSRF(t, session, url), + "_csrf": GetUserCSRFToken(t, session), "title": "pull request from status1", }, ) @@ -129,7 +129,7 @@ func TestPullCreate_EmptyChangesWithDifferentCommits(t *testing.T) { url := path.Join("user1", "repo1", "compare", "master...status1") req := NewRequestWithValues(t, "POST", url, map[string]string{ - "_csrf": GetCSRF(t, session, url), + "_csrf": GetUserCSRFToken(t, session), "title": "pull request from status1", }, ) @@ -152,7 +152,7 @@ func TestPullCreate_EmptyChangesWithSameCommits(t *testing.T) { url := path.Join("user1", "repo1", "compare", "master...status1") req := NewRequestWithValues(t, "POST", url, map[string]string{ - "_csrf": GetCSRF(t, session, url), + "_csrf": GetUserCSRFToken(t, session), "title": "pull request from status1", }, ) diff --git a/tests/integration/rename_branch_test.go b/tests/integration/rename_branch_test.go index 71bfb6b6cb2..576264ba951 100644 --- a/tests/integration/rename_branch_test.go +++ b/tests/integration/rename_branch_test.go @@ -54,7 +54,7 @@ func testRenameBranch(t *testing.T, u *url.URL) { assert.Equal(t, "main", repo1.DefaultBranch) // create branch1 - csrf := GetCSRF(t, session, "/user2/repo1/src/branch/main") + csrf := GetUserCSRFToken(t, session) req = NewRequestWithValues(t, "POST", "/user2/repo1/branches/_new/branch/main", map[string]string{ "_csrf": csrf, diff --git a/tests/integration/repo_branch_test.go b/tests/integration/repo_branch_test.go index f5217374b00..6d1cc8afcf1 100644 --- a/tests/integration/repo_branch_test.go +++ b/tests/integration/repo_branch_test.go @@ -27,14 +27,7 @@ import ( ) func testCreateBranch(t testing.TB, session *TestSession, user, repo, oldRefSubURL, newBranchName string, expectedStatus int) string { - var csrf string - if expectedStatus == http.StatusNotFound { - // src/branch/branch_name may not container "_csrf" input, - // so we need to get it from cookies not from body - csrf = GetCSRFFromCookie(t, session, path.Join(user, repo, "src/branch/master")) - } else { - csrf = GetCSRFFromCookie(t, session, path.Join(user, repo, "src", oldRefSubURL)) - } + csrf := GetUserCSRFToken(t, session) req := NewRequestWithValues(t, "POST", path.Join(user, repo, "branches/_new", oldRefSubURL), map[string]string{ "_csrf": csrf, "new_branch_name": newBranchName, diff --git a/tests/integration/signin_test.go b/tests/integration/signin_test.go index 77e19bba963..886d4a82593 100644 --- a/tests/integration/signin_test.go +++ b/tests/integration/signin_test.go @@ -21,7 +21,6 @@ import ( func testLoginFailed(t *testing.T, username, password, message string) { session := emptyTestSession(t) req := NewRequestWithValues(t, "POST", "/user/login", map[string]string{ - "_csrf": GetCSRF(t, session, "/user/login"), "user_name": username, "password": password, }) @@ -68,7 +67,6 @@ func TestSigninWithRememberMe(t *testing.T) { session := emptyTestSession(t) req := NewRequestWithValues(t, "POST", "/user/login", map[string]string{ - "_csrf": GetCSRF(t, session, "/user/login"), "user_name": user.Name, "password": userPassword, "remember": "on", diff --git a/tests/integration/user_avatar_test.go b/tests/integration/user_avatar_test.go index ec5813df0d5..caca9a3e560 100644 --- a/tests/integration/user_avatar_test.go +++ b/tests/integration/user_avatar_test.go @@ -37,7 +37,7 @@ func TestUserAvatar(t *testing.T) { } session := loginUser(t, "user2") - csrf := GetCSRF(t, session, "/user/settings") + csrf := GetUserCSRFToken(t, session) imgData := &bytes.Buffer{} diff --git a/tests/integration/user_test.go b/tests/integration/user_test.go index c4544f37aa3..99e413c6d95 100644 --- a/tests/integration/user_test.go +++ b/tests/integration/user_test.go @@ -5,6 +5,7 @@ package integration import ( "net/http" + "strings" "testing" auth_model "code.gitea.io/gitea/models/auth" @@ -33,7 +34,7 @@ func TestRenameUsername(t *testing.T) { session := loginUser(t, "user2") req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{ - "_csrf": GetCSRF(t, session, "/user/settings"), + "_csrf": GetUserCSRFToken(t, session), "name": "newUsername", "email": "user2@example.com", "language": "en-US", @@ -77,7 +78,7 @@ func TestRenameInvalidUsername(t *testing.T) { t.Logf("Testing username %s", invalidUsername) req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{ - "_csrf": GetCSRF(t, session, "/user/settings"), + "_csrf": GetUserCSRFToken(t, session), "name": invalidUsername, "email": "user2@example.com", }) @@ -97,45 +98,15 @@ func TestRenameReservedUsername(t *testing.T) { reservedUsernames := []string{ // ".", "..", ".well-known", // The names are not only reserved but also invalid - "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", - "repo-avatars", - "robots.txt", - "search", - "serviceworker.js", - "ssh_info", - "swagger.v1.json", - "user", - "v2", + "name.keys", } session := loginUser(t, "user2") + locale := translation.NewLocale("en-US") for _, reservedUsername := range reservedUsernames { - t.Logf("Testing username %s", reservedUsername) req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{ - "_csrf": GetCSRF(t, session, "/user/settings"), + "_csrf": GetUserCSRFToken(t, session), "name": reservedUsername, "email": "user2@example.com", "language": "en-US", @@ -145,11 +116,12 @@ func TestRenameReservedUsername(t *testing.T) { req = NewRequest(t, "GET", test.RedirectURL(resp)) resp = session.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) - assert.Contains(t, - htmlDoc.doc.Find(".ui.negative.message").Text(), - translation.NewLocale("en-US").TrString("user.form.name_reserved", reservedUsername), - ) - + actualMsg := strings.TrimSpace(htmlDoc.doc.Find(".ui.negative.message").Text()) + expectedMsg := locale.TrString("user.form.name_reserved", reservedUsername) + if strings.Contains(reservedUsername, ".") { + expectedMsg = locale.TrString("user.form.name_pattern_not_allowed", reservedUsername) + } + assert.Equal(t, expectedMsg, actualMsg) unittest.AssertNotExistsBean(t, &user_model.User{Name: reservedUsername}) } } @@ -293,7 +265,7 @@ func TestUserLocationMapLink(t *testing.T) { session := loginUser(t, "user2") req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{ - "_csrf": GetCSRF(t, session, "/user/settings"), + "_csrf": GetUserCSRFToken(t, session), "name": "user2", "email": "user@example.com", "language": "en-US", diff --git a/tests/integration/xss_test.go b/tests/integration/xss_test.go index e575ed3990c..a8eaa5fc624 100644 --- a/tests/integration/xss_test.go +++ b/tests/integration/xss_test.go @@ -21,7 +21,7 @@ func TestXSSUserFullName(t *testing.T) { session := loginUser(t, user.Name) req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{ - "_csrf": GetCSRF(t, session, "/user/settings"), + "_csrf": GetUserCSRFToken(t, session), "name": user.Name, "full_name": fullName, "email": user.Email, diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue index ce165b1b3df..986fcc11814 100644 --- a/web_src/js/components/DashboardRepoList.vue +++ b/web_src/js/components/DashboardRepoList.vue @@ -362,9 +362,9 @@ export default sfc; // activate the IDE's Vue plugin