diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 90bb3c9c5e..86f886b4b4 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -12,6 +12,7 @@ plugins: - eslint-plugin-unicorn - eslint-plugin-import - eslint-plugin-jquery + - eslint-plugin-sonarjs env: es2022: true @@ -369,6 +370,38 @@ rules: semi-spacing: [2, {before: false, after: true}] semi-style: [2, last] semi: [2, always, {omitLastInOneLineBlock: true}] + sonarjs/cognitive-complexity: [0] + sonarjs/elseif-without-else: [0] + sonarjs/max-switch-cases: [0] + sonarjs/no-all-duplicated-branches: [2] + sonarjs/no-collapsible-if: [0] + sonarjs/no-collection-size-mischeck: [2] + sonarjs/no-duplicate-string: [0] + sonarjs/no-duplicated-branches: [0] + sonarjs/no-element-overwrite: [2] + sonarjs/no-empty-collection: [2] + sonarjs/no-extra-arguments: [0] + sonarjs/no-gratuitous-expressions: [2] + sonarjs/no-identical-conditions: [2] + sonarjs/no-identical-expressions: [0] + sonarjs/no-identical-functions: [0] + sonarjs/no-ignored-return: [2] + sonarjs/no-inverted-boolean-check: [2] + sonarjs/no-nested-switch: [0] + sonarjs/no-nested-template-literals: [0] + sonarjs/no-one-iteration-loop: [2] + sonarjs/no-redundant-boolean: [2] + sonarjs/no-redundant-jump: [0] + sonarjs/no-same-line-conditional: [2] + sonarjs/no-small-switch: [0] + sonarjs/no-unused-collection: [2] + sonarjs/no-use-of-empty-return-value: [2] + sonarjs/no-useless-catch: [0] + sonarjs/non-existent-operator: [2] + sonarjs/prefer-immediate-return: [0] + sonarjs/prefer-object-literal: [0] + sonarjs/prefer-single-boolean-return: [0] + sonarjs/prefer-while: [2] sort-imports: [0] sort-keys: [0] sort-vars: [0] diff --git a/Makefile b/Makefile index d4cc71700d..5cd9bc25b5 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ AIR_PACKAGE ?= github.com/cosmtrek/air@v1.40.4 EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/cmd/editorconfig-checker@2.5.0 ERRCHECK_PACKAGE ?= github.com/kisielk/errcheck@v1.6.1 GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.3.1 -GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.47.1 +GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.47.0 GXZ_PAGAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.10 MISSPELL_PACKAGE ?= github.com/client9/misspell/cmd/misspell@v0.3.4 SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.29.0 diff --git a/cmd/dump.go b/cmd/dump.go index d807cb0587..73c2251b92 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -92,7 +92,7 @@ func (o outputType) String() string { } var outputTypeEnum = &outputType{ - Enum: []string{"zip", "tar", "tar.sz", "tar.gz", "tar.xz", "tar.bz2", "tar.br", "tar.lz4"}, + Enum: []string{"zip", "tar", "tar.sz", "tar.gz", "tar.xz", "tar.bz2", "tar.br", "tar.lz4", "tar.zst"}, Default: "zip", } diff --git a/cmd/web.go b/cmd/web.go index 43bb0ada91..3bc61b0443 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -148,8 +148,9 @@ func runWeb(ctx *cli.Context) error { go func() { http.DefaultServeMux.Handle("/debug/fgprof", fgprof.Handler()) _, _, finished := process.GetManager().AddTypedContext(context.Background(), "Web: PProf Server", process.SystemProcessType, true) + // The pprof server is for debug purpose only, it shouldn't be exposed on public network. At the moment it's not worth to introduce a configurable option for it. log.Info("Starting pprof server on localhost:6060") - log.Info("%v", http.ListenAndServe("localhost:6060", nil)) + log.Info("Stopped pprof server: %v", http.ListenAndServe("localhost:6060", nil)) finished() }() } diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index a0e6fb8f13..4df104419a 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -300,7 +300,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a - `APP_DATA_PATH`: **data** (**/data/gitea** on docker): Default path for application data. - `STATIC_CACHE_TIME`: **6h**: Web browser cache time for static resources on `custom/`, `public/` and all uploaded avatars. Note that this cache is disabled when `RUN_MODE` is "dev". - `ENABLE_GZIP`: **false**: Enable gzip compression for runtime-generated content, static resources excluded. -- `ENABLE_PPROF`: **false**: Application profiling (memory and cpu). For "web" command it listens on localhost:6060. For "serv" command it dumps to disk at `PPROF_DATA_PATH` as `(cpuprofile|memprofile)__` +- `ENABLE_PPROF`: **false**: Application profiling (memory and cpu). For "web" command it listens on `localhost:6060`. For "serv" command it dumps to disk at `PPROF_DATA_PATH` as `(cpuprofile|memprofile)__` - `PPROF_DATA_PATH`: **data/tmp/pprof**: `PPROF_DATA_PATH`, use an absolute path when you start Gitea as service - `LANDING_PAGE`: **home**: Landing page for unauthenticated users \[home, explore, organizations, login, **custom**\]. Where custom would instead be any URL such as "/org/repo" or even `https://anotherwebsite.com` - `LFS_START_SERVER`: **false**: Enables Git LFS support. diff --git a/docs/content/doc/help/faq.en-us.md b/docs/content/doc/help/faq.en-us.md index a64cccfa66..5783169fa9 100644 --- a/docs/content/doc/help/faq.en-us.md +++ b/docs/content/doc/help/faq.en-us.md @@ -403,3 +403,9 @@ gitea doctor recreate-table ``` It is highly recommended to back-up your database before running these commands. + + +## Why are tabs/indents wrong when viewing files + +If you are using Cloudflare, turn off the auto-minify option in the dashboard. +`Speed` -> `Optimization` -> Uncheck `HTML` within the `Auto-Minify` settings. diff --git a/docs/content/doc/help/seek-help.en-us.md b/docs/content/doc/help/seek-help.en-us.md index 3ee160f431..f1a93eacea 100644 --- a/docs/content/doc/help/seek-help.en-us.md +++ b/docs/content/doc/help/seek-help.en-us.md @@ -44,12 +44,13 @@ menu: * This will greatly improve the chance that the root of the issue can be quickly discovered and resolved. 5. If you meet slow/hanging/deadlock problems, please report the stack trace when the problem occurs: 1. Enable pprof in `app.ini` and restart Gitea - ``` + ```ini [server] ENABLE_PPROF = true ``` - 2. Trigger the bug, when Gitea gets stuck, use curl or browser to visit: `http://127.0.0.1:6060/debug/pprof/goroutine?debug=1` (IP is `127.0.0.1` and port is `6060`) - 3. Report the output (the stack trace doesn't contain sensitive data) + 2. Trigger the bug, when Gitea gets stuck, use curl or browser to visit: `http://127.0.0.1:6060/debug/pprof/goroutine?debug=1` (IP must be `127.0.0.1` and port must be `6060`). + 3. If you are using Docker, please use `docker exec -it curl "http://127.0.0.1:6060/debug/pprof/goroutine?debug=1"`. + 4. Report the output (the stack trace doesn't contain sensitive data) ## Bugs diff --git a/docs/content/doc/installation/from-package.en-us.md b/docs/content/doc/installation/from-package.en-us.md index e4081024bd..56ca97a8a5 100644 --- a/docs/content/doc/installation/from-package.en-us.md +++ b/docs/content/doc/installation/from-package.en-us.md @@ -47,9 +47,9 @@ pacman -S gitea There is a [Gitea Snap](https://snapcraft.io/gitea) package which follows the latest stable version. -``sh +```sh snap install gitea -`` +``` ## SUSE and openSUSE diff --git a/docs/content/doc/installation/with-docker.en-us.md b/docs/content/doc/installation/with-docker.en-us.md index c2e7a817c9..940b38aa75 100644 --- a/docs/content/doc/installation/with-docker.en-us.md +++ b/docs/content/doc/installation/with-docker.en-us.md @@ -309,6 +309,8 @@ To set required TOKEN and SECRET values, consider using Gitea's built-in [genera Since SSH is running inside the container, SSH needs to be passed through from the host to the container if SSH support is desired. One option would be to run the container SSH on a non-standard port (or moving the host port to a non-standard port). Another option which might be more straightforward is for Gitea users to ssh to a Gitea user on the host which will then relay those connections to the docker. +### Understanding SSH access to Gitea (without passthrough) + To understand what needs to happen, you first need to understand what happens without passthrough. So we will try to explain this: 1. The client adds their SSH public key to Gitea using the webpage. diff --git a/integrations/api_packages_container_test.go b/integrations/api_packages_container_test.go index 1ed80dfd02..bdb8e2e90e 100644 --- a/integrations/api_packages_container_test.go +++ b/integrations/api_packages_container_test.go @@ -20,6 +20,7 @@ import ( container_module "code.gitea.io/gitea/modules/packages/container" "code.gitea.io/gitea/modules/packages/container/oci" "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" "github.com/stretchr/testify/assert" ) @@ -487,6 +488,13 @@ func TestPackageContainer(t *testing.T) { assert.Equal(t, c.ExpectedTags, tagList.Tags) assert.Equal(t, c.ExpectedLink, resp.Header().Get("Link")) } + + req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s?type=container&q=%s", user.Name, image)) + resp := MakeRequest(t, req, http.StatusOK) + + var apiPackages []*api.Package + DecodeJSON(t, resp, &apiPackages) + assert.Len(t, apiPackages, 4) // "latest", "main", "multi", "sha256:..." }) t.Run("Delete", func(t *testing.T) { diff --git a/integrations/api_repo_raw_test.go b/integrations/api_repo_raw_test.go index 2a77d1ba63..258b409bef 100644 --- a/integrations/api_repo_raw_test.go +++ b/integrations/api_repo_raw_test.go @@ -10,6 +10,8 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + + "github.com/stretchr/testify/assert" ) func TestAPIReposRaw(t *testing.T) { @@ -25,9 +27,11 @@ func TestAPIReposRaw(t *testing.T) { "65f1bf27bc3bf70f64657658635e66094edbcb4d", // Commit } { req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/raw/%s/README.md?token="+token, user.Name, ref) - session.MakeRequest(t, req, http.StatusOK) + resp := session.MakeRequest(t, req, http.StatusOK) + assert.EqualValues(t, "file", resp.Header().Get("x-gitea-object-type")) } // Test default branch req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/raw/README.md?token="+token, user.Name) - session.MakeRequest(t, req, http.StatusOK) + resp := session.MakeRequest(t, req, http.StatusOK) + assert.EqualValues(t, "file", resp.Header().Get("x-gitea-object-type")) } diff --git a/integrations/org_test.go b/integrations/org_test.go index d755385726..d787e6f791 100644 --- a/integrations/org_test.go +++ b/integrations/org_test.go @@ -116,6 +116,24 @@ func TestPrivateOrg(t *testing.T) { session.MakeRequest(t, req, http.StatusOK) } +func TestOrgMembers(t *testing.T) { + defer prepareTestEnv(t)() + + // not logged in user + req := NewRequest(t, "GET", "/org/org25/members") + MakeRequest(t, req, http.StatusOK) + + // org member + session := loginUser(t, "user24") + req = NewRequest(t, "GET", "/org/org25/members") + session.MakeRequest(t, req, http.StatusOK) + + // site admin + session = loginUser(t, "user1") + req = NewRequest(t, "GET", "/org/org25/members") + session.MakeRequest(t, req, http.StatusOK) +} + func TestOrgRestrictedUser(t *testing.T) { defer prepareTestEnv(t)() diff --git a/models/issues/issue_list.go b/models/issues/issue_list.go index e311e80b1d..874f2a6368 100644 --- a/models/issues/issue_list.go +++ b/models/issues/issue_list.go @@ -9,6 +9,7 @@ import ( "fmt" "code.gitea.io/gitea/models/db" + project_model "code.gitea.io/gitea/models/project" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" @@ -222,6 +223,46 @@ func (issues IssueList) loadMilestones(ctx context.Context) error { return nil } +func (issues IssueList) getProjectIDs() []int64 { + ids := make(map[int64]struct{}, len(issues)) + for _, issue := range issues { + projectID := issue.ProjectID() + if _, ok := ids[projectID]; !ok { + ids[projectID] = struct{}{} + } + } + return container.KeysInt64(ids) +} + +func (issues IssueList) loadProjects(ctx context.Context) error { + projectIDs := issues.getProjectIDs() + if len(projectIDs) == 0 { + return nil + } + + projectMaps := make(map[int64]*project_model.Project, len(projectIDs)) + left := len(projectIDs) + for left > 0 { + limit := db.DefaultMaxInSize + if left < limit { + limit = left + } + err := db.GetEngine(ctx). + In("id", projectIDs[:limit]). + Find(&projectMaps) + if err != nil { + return err + } + left -= limit + projectIDs = projectIDs[limit:] + } + + for _, issue := range issues { + issue.Project = projectMaps[issue.ProjectID()] + } + return nil +} + func (issues IssueList) loadAssignees(ctx context.Context) error { if len(issues) == 0 { return nil @@ -495,6 +536,10 @@ func (issues IssueList) loadAttributes(ctx context.Context) error { return fmt.Errorf("issue.loadAttributes: loadMilestones: %v", err) } + if err := issues.loadProjects(ctx); err != nil { + return fmt.Errorf("issue.loadAttributes: loadProjects: %v", err) + } + if err := issues.loadAssignees(ctx); err != nil { return fmt.Errorf("issue.loadAttributes: loadAssignees: %v", err) } diff --git a/models/packages/package_version.go b/models/packages/package_version.go index 83c2fdb674..5479bae1c2 100644 --- a/models/packages/package_version.go +++ b/models/packages/package_version.go @@ -122,8 +122,9 @@ func getVersionByNameAndVersion(ctx context.Context, ownerID int64, packageType // GetVersionsByPackageType gets all versions of a specific type func GetVersionsByPackageType(ctx context.Context, ownerID int64, packageType Type) ([]*PackageVersion, error) { pvs, _, err := SearchVersions(ctx, &PackageSearchOptions{ - OwnerID: ownerID, - Type: packageType, + OwnerID: ownerID, + Type: packageType, + IsInternal: util.OptionalBoolFalse, }) return pvs, err } @@ -137,6 +138,7 @@ func GetVersionsByPackageName(ctx context.Context, ownerID int64, packageType Ty ExactMatch: true, Value: name, }, + IsInternal: util.OptionalBoolFalse, }) return pvs, err } diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go index a70fc8efd4..9de76fa5ff 100644 --- a/models/repo/repo_list.go +++ b/models/repo/repo_list.go @@ -6,6 +6,7 @@ package repo import ( "context" + "errors" "fmt" "strings" @@ -695,6 +696,9 @@ func GetUserRepositories(opts *SearchRepoOptions) (RepositoryList, int64, error) } cond := builder.NewCond() + if opts.Actor == nil { + return nil, 0, errors.New("GetUserRepositories: Actor is needed but not given") + } cond = cond.And(builder.Eq{"owner_id": opts.Actor.ID}) if !opts.Private { cond = cond.And(builder.Eq{"is_private": false}) diff --git a/models/user/search.go b/models/user/search.go index 1b65dcb12d..76ff55ea26 100644 --- a/models/user/search.go +++ b/models/user/search.go @@ -59,25 +59,18 @@ func (opts *SearchUserOptions) toSearchQueryBase() *xorm.Session { } if opts.Actor != nil { - exprCond := builder.Expr("org_user.org_id = `user`.id") - // If Admin - they see all users! if !opts.Actor.IsAdmin { - // Force visibility for privacy - var accessCond builder.Cond + // Users can see an organization they are a member of + accessCond := builder.In("id", builder.Select("org_id").From("org_user").Where(builder.Eq{"uid": opts.Actor.ID})) if !opts.Actor.IsRestricted { - accessCond = builder.Or( - builder.In("id", builder.Select("org_id").From("org_user").LeftJoin("`user`", exprCond).Where(builder.And(builder.Eq{"uid": opts.Actor.ID}, builder.Eq{"visibility": structs.VisibleTypePrivate}))), - builder.In("visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited)) - } else { - // restricted users only see orgs they are a member of - accessCond = builder.In("id", builder.Select("org_id").From("org_user").LeftJoin("`user`", exprCond).Where(builder.And(builder.Eq{"uid": opts.Actor.ID}))) + // Not-Restricted users can see public and limited users/organizations + accessCond = accessCond.Or(builder.In("visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited)) } // Don't forget about self accessCond = accessCond.Or(builder.Eq{"id": opts.Actor.ID}) cond = cond.And(accessCond) } - } else { // Force visibility for privacy // Not logged in - only public users diff --git a/modules/context/api.go b/modules/context/api.go index 558a9f51ee..b9d130e2a8 100644 --- a/modules/context/api.go +++ b/modules/context/api.go @@ -16,6 +16,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/httpcache" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web/middleware" @@ -268,6 +269,7 @@ func APIContexter() func(http.Handler) http.Handler { } } + httpcache.AddCacheControlToHeader(ctx.Resp.Header(), 0, "no-transform") ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions) ctx.Data["Context"] = &ctx diff --git a/modules/context/context.go b/modules/context/context.go index 68f8a1b408..8824911619 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -28,6 +28,7 @@ import ( "code.gitea.io/gitea/modules/base" mc "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/httpcache" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -767,6 +768,7 @@ func Contexter() func(next http.Handler) http.Handler { } } + httpcache.AddCacheControlToHeader(ctx.Resp.Header(), 0, "no-transform") ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions) ctx.Data["CsrfToken"] = ctx.csrf.GetToken() diff --git a/modules/context/repo.go b/modules/context/repo.go index 1836373918..1d9f98158e 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -1001,6 +1001,8 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context return } ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount + ctx.Repo.GitRepo.LastCommitCache = git.NewLastCommitCache(ctx.Repo.CommitsCount, ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, cache.GetCache()) + return cancel } } diff --git a/modules/git/commit.go b/modules/git/commit.go index 82712dd1ef..32589f5349 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -80,6 +80,9 @@ func (c *Commit) ParentCount() int { // GetCommitByPath return the commit of relative path object. func (c *Commit) GetCommitByPath(relpath string) (*Commit, error) { + if c.repo.LastCommitCache != nil { + return c.repo.LastCommitCache.GetCommitByPath(c.ID.String(), relpath) + } return c.repo.getCommitByPathWithID(c.ID, relpath) } diff --git a/modules/git/commit_info_gogit.go b/modules/git/commit_info_gogit.go index 91a1804db5..341698ab34 100644 --- a/modules/git/commit_info_gogit.go +++ b/modules/git/commit_info_gogit.go @@ -17,7 +17,7 @@ import ( ) // GetCommitsInfo gets information of all commits that are corresponding to these entries -func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath string, cache *LastCommitCache) ([]CommitInfo, *Commit, error) { +func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) { entryPaths := make([]string, len(tes)+1) // Get the commit for the treePath itself entryPaths[0] = "" @@ -35,15 +35,15 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath return nil, nil, err } - var revs map[string]*object.Commit - if cache != nil { + var revs map[string]*Commit + if commit.repo.LastCommitCache != nil { var unHitPaths []string - revs, unHitPaths, err = getLastCommitForPathsByCache(commit.ID.String(), treePath, entryPaths, cache) + revs, unHitPaths, err = getLastCommitForPathsByCache(commit.ID.String(), treePath, entryPaths, commit.repo.LastCommitCache) if err != nil { return nil, nil, err } if len(unHitPaths) > 0 { - revs2, err := GetLastCommitForPaths(ctx, cache, c, treePath, unHitPaths) + revs2, err := GetLastCommitForPaths(ctx, commit.repo.LastCommitCache, c, treePath, unHitPaths) if err != nil { return nil, nil, err } @@ -68,8 +68,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath } // Check if we have found a commit for this entry in time - if rev, ok := revs[entry.Name()]; ok { - entryCommit := convertCommit(rev) + if entryCommit, ok := revs[entry.Name()]; ok { commitsInfo[i].Commit = entryCommit } @@ -96,10 +95,10 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath // get it for free during the tree traversal and it's used for listing // pages to display information about newest commit for a given path. var treeCommit *Commit + var ok bool if treePath == "" { treeCommit = commit - } else if rev, ok := revs[""]; ok { - treeCommit = convertCommit(rev) + } else if treeCommit, ok = revs[""]; ok { treeCommit.repo = commit.repo } return commitsInfo, treeCommit, nil @@ -155,16 +154,16 @@ func getFileHashes(c cgobject.CommitNode, treePath string, paths []string) (map[ return hashes, nil } -func getLastCommitForPathsByCache(commitID, treePath string, paths []string, cache *LastCommitCache) (map[string]*object.Commit, []string, error) { +func getLastCommitForPathsByCache(commitID, treePath string, paths []string, cache *LastCommitCache) (map[string]*Commit, []string, error) { var unHitEntryPaths []string - results := make(map[string]*object.Commit) + results := make(map[string]*Commit) for _, p := range paths { lastCommit, err := cache.Get(commitID, path.Join(treePath, p)) if err != nil { return nil, nil, err } if lastCommit != nil { - results[p] = lastCommit.(*object.Commit) + results[p] = lastCommit continue } @@ -175,7 +174,7 @@ func getLastCommitForPathsByCache(commitID, treePath string, paths []string, cac } // GetLastCommitForPaths returns last commit information -func GetLastCommitForPaths(ctx context.Context, cache *LastCommitCache, c cgobject.CommitNode, treePath string, paths []string) (map[string]*object.Commit, error) { +func GetLastCommitForPaths(ctx context.Context, cache *LastCommitCache, c cgobject.CommitNode, treePath string, paths []string) (map[string]*Commit, error) { refSha := c.ID().String() // We do a tree traversal with nodes sorted by commit time @@ -293,13 +292,13 @@ heaploop: } // Post-processing - result := make(map[string]*object.Commit) + result := make(map[string]*Commit) for path, commitNode := range resultNodes { - var err error - result[path], err = commitNode.Commit() + commit, err := commitNode.Commit() if err != nil { return nil, err } + result[path] = convertCommit(commit) } return result, nil diff --git a/modules/git/commit_info_nogogit.go b/modules/git/commit_info_nogogit.go index ceab11adbb..d7bca3b948 100644 --- a/modules/git/commit_info_nogogit.go +++ b/modules/git/commit_info_nogogit.go @@ -17,7 +17,7 @@ import ( ) // GetCommitsInfo gets information of all commits that are corresponding to these entries -func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath string, cache *LastCommitCache) ([]CommitInfo, *Commit, error) { +func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) { entryPaths := make([]string, len(tes)+1) // Get the commit for the treePath itself entryPaths[0] = "" @@ -28,15 +28,15 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath var err error var revs map[string]*Commit - if cache != nil { + if commit.repo.LastCommitCache != nil { var unHitPaths []string - revs, unHitPaths, err = getLastCommitForPathsByCache(ctx, commit.ID.String(), treePath, entryPaths, cache) + revs, unHitPaths, err = getLastCommitForPathsByCache(ctx, commit.ID.String(), treePath, entryPaths, commit.repo.LastCommitCache) if err != nil { return nil, nil, err } if len(unHitPaths) > 0 { sort.Strings(unHitPaths) - commits, err := GetLastCommitForPaths(ctx, cache, commit, treePath, unHitPaths) + commits, err := GetLastCommitForPaths(ctx, commit, treePath, unHitPaths) if err != nil { return nil, nil, err } @@ -47,7 +47,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath } } else { sort.Strings(entryPaths) - revs, err = GetLastCommitForPaths(ctx, nil, commit, treePath, entryPaths) + revs, err = GetLastCommitForPaths(ctx, commit, treePath, entryPaths) } if err != nil { return nil, nil, err @@ -99,18 +99,15 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath } func getLastCommitForPathsByCache(ctx context.Context, commitID, treePath string, paths []string, cache *LastCommitCache) (map[string]*Commit, []string, error) { - wr, rd, cancel := cache.repo.CatFileBatch(ctx) - defer cancel() - var unHitEntryPaths []string results := make(map[string]*Commit) for _, p := range paths { - lastCommit, err := cache.Get(commitID, path.Join(treePath, p), wr, rd) + lastCommit, err := cache.Get(commitID, path.Join(treePath, p)) if err != nil { return nil, nil, err } if lastCommit != nil { - results[p] = lastCommit.(*Commit) + results[p] = lastCommit continue } @@ -121,9 +118,9 @@ func getLastCommitForPathsByCache(ctx context.Context, commitID, treePath string } // GetLastCommitForPaths returns last commit information -func GetLastCommitForPaths(ctx context.Context, cache *LastCommitCache, commit *Commit, treePath string, paths []string) (map[string]*Commit, error) { +func GetLastCommitForPaths(ctx context.Context, commit *Commit, treePath string, paths []string) (map[string]*Commit, error) { // We read backwards from the commit to obtain all of the commits - revs, err := WalkGitLog(ctx, cache, commit.repo, commit, treePath, paths...) + revs, err := WalkGitLog(ctx, commit.repo, commit, treePath, paths...) if err != nil { return nil, err } diff --git a/modules/git/commit_info_test.go b/modules/git/commit_info_test.go index 49845522a9..a12452c404 100644 --- a/modules/git/commit_info_test.go +++ b/modules/git/commit_info_test.go @@ -91,7 +91,7 @@ func testGetCommitsInfo(t *testing.T, repo1 *Repository) { } // FIXME: Context.TODO() - if graceful has started we should use its Shutdown context otherwise use install signals in TestMain. - commitsInfo, treeCommit, err := entries.GetCommitsInfo(context.TODO(), commit, testCase.Path, nil) + commitsInfo, treeCommit, err := entries.GetCommitsInfo(context.TODO(), commit, testCase.Path) assert.NoError(t, err, "Unable to get commit information for entries of subtree: %s in commit: %s from testcase due to error: %v", testCase.Path, testCase.CommitID, err) if err != nil { t.FailNow() @@ -170,7 +170,7 @@ func BenchmarkEntries_GetCommitsInfo(b *testing.B) { b.ResetTimer() b.Run(benchmark.name, func(b *testing.B) { for i := 0; i < b.N; i++ { - _, _, err := entries.GetCommitsInfo(context.Background(), commit, "", nil) + _, _, err := entries.GetCommitsInfo(context.Background(), commit, "") if err != nil { b.Fatal(err) } diff --git a/modules/git/last_commit_cache.go b/modules/git/last_commit_cache.go index d4ec517b51..2b51d59720 100644 --- a/modules/git/last_commit_cache.go +++ b/modules/git/last_commit_cache.go @@ -9,6 +9,7 @@ import ( "fmt" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" ) // Cache represents a caching interface @@ -19,16 +20,96 @@ type Cache interface { Get(key string) interface{} } -func (c *LastCommitCache) getCacheKey(repoPath, ref, entryPath string) string { - hashBytes := sha256.Sum256([]byte(fmt.Sprintf("%s:%s:%s", repoPath, ref, entryPath))) +func getCacheKey(repoPath, commitID, entryPath string) string { + hashBytes := sha256.Sum256([]byte(fmt.Sprintf("%s:%s:%s", repoPath, commitID, entryPath))) return fmt.Sprintf("last_commit:%x", hashBytes) } +// LastCommitCache represents a cache to store last commit +type LastCommitCache struct { + repoPath string + ttl func() int64 + repo *Repository + commitCache map[string]*Commit + cache Cache +} + +// NewLastCommitCache creates a new last commit cache for repo +func NewLastCommitCache(count int64, repoPath string, gitRepo *Repository, cache Cache) *LastCommitCache { + if cache == nil { + return nil + } + if !setting.CacheService.LastCommit.Enabled || count < setting.CacheService.LastCommit.CommitsCount { + return nil + } + + return &LastCommitCache{ + repoPath: repoPath, + repo: gitRepo, + ttl: setting.LastCommitCacheTTLSeconds, + cache: cache, + } +} + // Put put the last commit id with commit and entry path func (c *LastCommitCache) Put(ref, entryPath, commitID string) error { if c == nil || c.cache == nil { return nil } log.Debug("LastCommitCache save: [%s:%s:%s]", ref, entryPath, commitID) - return c.cache.Put(c.getCacheKey(c.repoPath, ref, entryPath), commitID, c.ttl()) + return c.cache.Put(getCacheKey(c.repoPath, ref, entryPath), commitID, c.ttl()) +} + +// Get gets the last commit information by commit id and entry path +func (c *LastCommitCache) Get(ref, entryPath string) (*Commit, error) { + if c == nil || c.cache == nil { + return nil, nil + } + + commitID, ok := c.cache.Get(getCacheKey(c.repoPath, ref, entryPath)).(string) + if !ok || commitID == "" { + return nil, nil + } + + log.Debug("LastCommitCache hit level 1: [%s:%s:%s]", ref, entryPath, commitID) + if c.commitCache != nil { + if commit, ok := c.commitCache[commitID]; ok { + log.Debug("LastCommitCache hit level 2: [%s:%s:%s]", ref, entryPath, commitID) + return commit, nil + } + } + + commit, err := c.repo.GetCommit(commitID) + if err != nil { + return nil, err + } + if c.commitCache == nil { + c.commitCache = make(map[string]*Commit) + } + c.commitCache[commitID] = commit + return commit, nil +} + +// GetCommitByPath gets the last commit for the entry in the provided commit +func (c *LastCommitCache) GetCommitByPath(commitID, entryPath string) (*Commit, error) { + sha1, err := NewIDFromString(commitID) + if err != nil { + return nil, err + } + + lastCommit, err := c.Get(sha1.String(), entryPath) + if err != nil || lastCommit != nil { + return lastCommit, err + } + + lastCommit, err = c.repo.getCommitByPathWithID(sha1, entryPath) + if err != nil { + return nil, err + } + + if err := c.Put(commitID, entryPath, lastCommit.ID.String()); err != nil { + log.Error("Unable to cache %s as the last commit for %q in %s %s. Error %v", lastCommit.ID.String(), entryPath, commitID, c.repoPath, err) + } + + return lastCommit, nil } diff --git a/modules/git/last_commit_cache_gogit.go b/modules/git/last_commit_cache_gogit.go index 8897000350..82c76bad20 100644 --- a/modules/git/last_commit_cache_gogit.go +++ b/modules/git/last_commit_cache_gogit.go @@ -9,71 +9,25 @@ package git import ( "context" - "code.gitea.io/gitea/modules/log" - - "github.com/go-git/go-git/v5/plumbing/object" cgobject "github.com/go-git/go-git/v5/plumbing/object/commitgraph" ) -// LastCommitCache represents a cache to store last commit -type LastCommitCache struct { - repoPath string - ttl func() int64 - repo *Repository - commitCache map[string]*object.Commit - cache Cache -} - -// NewLastCommitCache creates a new last commit cache for repo -func NewLastCommitCache(repoPath string, gitRepo *Repository, ttl func() int64, cache Cache) *LastCommitCache { - if cache == nil { +// CacheCommit will cache the commit from the gitRepository +func (c *Commit) CacheCommit(ctx context.Context) error { + if c.repo.LastCommitCache == nil { return nil } - return &LastCommitCache{ - repoPath: repoPath, - repo: gitRepo, - commitCache: make(map[string]*object.Commit), - ttl: ttl, - cache: cache, - } -} + commitNodeIndex, _ := c.repo.CommitNodeIndex() -// Get get the last commit information by commit id and entry path -func (c *LastCommitCache) Get(ref, entryPath string) (interface{}, error) { - v := c.cache.Get(c.getCacheKey(c.repoPath, ref, entryPath)) - if vs, ok := v.(string); ok { - log.Debug("LastCommitCache hit level 1: [%s:%s:%s]", ref, entryPath, vs) - if commit, ok := c.commitCache[vs]; ok { - log.Debug("LastCommitCache hit level 2: [%s:%s:%s]", ref, entryPath, vs) - return commit, nil - } - id, err := c.repo.ConvertToSHA1(vs) - if err != nil { - return nil, err - } - commit, err := c.repo.GoGitRepo().CommitObject(id) - if err != nil { - return nil, err - } - c.commitCache[vs] = commit - return commit, nil - } - return nil, nil -} - -// CacheCommit will cache the commit from the gitRepository -func (c *LastCommitCache) CacheCommit(ctx context.Context, commit *Commit) error { - commitNodeIndex, _ := commit.repo.CommitNodeIndex() - - index, err := commitNodeIndex.Get(commit.ID) + index, err := commitNodeIndex.Get(c.ID) if err != nil { return err } - return c.recursiveCache(ctx, index, &commit.Tree, "", 1) + return c.recursiveCache(ctx, index, &c.Tree, "", 1) } -func (c *LastCommitCache) recursiveCache(ctx context.Context, index cgobject.CommitNode, tree *Tree, treePath string, level int) error { +func (c *Commit) recursiveCache(ctx context.Context, index cgobject.CommitNode, tree *Tree, treePath string, level int) error { if level == 0 { return nil } @@ -90,7 +44,7 @@ func (c *LastCommitCache) recursiveCache(ctx context.Context, index cgobject.Com entryMap[entry.Name()] = entry } - commits, err := GetLastCommitForPaths(ctx, c, index, treePath, entryPaths) + commits, err := GetLastCommitForPaths(ctx, c.repo.LastCommitCache, index, treePath, entryPaths) if err != nil { return err } diff --git a/modules/git/last_commit_cache_nogogit.go b/modules/git/last_commit_cache_nogogit.go index 030d5486b6..1f4d693a26 100644 --- a/modules/git/last_commit_cache_nogogit.go +++ b/modules/git/last_commit_cache_nogogit.go @@ -7,67 +7,18 @@ package git import ( - "bufio" "context" - - "code.gitea.io/gitea/modules/log" ) -// LastCommitCache represents a cache to store last commit -type LastCommitCache struct { - repoPath string - ttl func() int64 - repo *Repository - commitCache map[string]*Commit - cache Cache -} - -// NewLastCommitCache creates a new last commit cache for repo -func NewLastCommitCache(repoPath string, gitRepo *Repository, ttl func() int64, cache Cache) *LastCommitCache { - if cache == nil { +// CacheCommit will cache the commit from the gitRepository +func (c *Commit) CacheCommit(ctx context.Context) error { + if c.repo.LastCommitCache == nil { return nil } - return &LastCommitCache{ - repoPath: repoPath, - repo: gitRepo, - commitCache: make(map[string]*Commit), - ttl: ttl, - cache: cache, - } + return c.recursiveCache(ctx, &c.Tree, "", 1) } -// Get get the last commit information by commit id and entry path -func (c *LastCommitCache) Get(ref, entryPath string, wr WriteCloserError, rd *bufio.Reader) (interface{}, error) { - v := c.cache.Get(c.getCacheKey(c.repoPath, ref, entryPath)) - if vs, ok := v.(string); ok { - log.Debug("LastCommitCache hit level 1: [%s:%s:%s]", ref, entryPath, vs) - if commit, ok := c.commitCache[vs]; ok { - log.Debug("LastCommitCache hit level 2: [%s:%s:%s]", ref, entryPath, vs) - return commit, nil - } - id, err := c.repo.ConvertToSHA1(vs) - if err != nil { - return nil, err - } - if _, err := wr.Write([]byte(vs + "\n")); err != nil { - return nil, err - } - commit, err := c.repo.getCommitFromBatchReader(rd, id) - if err != nil { - return nil, err - } - c.commitCache[vs] = commit - return commit, nil - } - return nil, nil -} - -// CacheCommit will cache the commit from the gitRepository -func (c *LastCommitCache) CacheCommit(ctx context.Context, commit *Commit) error { - return c.recursiveCache(ctx, commit, &commit.Tree, "", 1) -} - -func (c *LastCommitCache) recursiveCache(ctx context.Context, commit *Commit, tree *Tree, treePath string, level int) error { +func (c *Commit) recursiveCache(ctx context.Context, tree *Tree, treePath string, level int) error { if level == 0 { return nil } @@ -82,7 +33,7 @@ func (c *LastCommitCache) recursiveCache(ctx context.Context, commit *Commit, tr entryPaths[i] = entry.Name() } - _, err = WalkGitLog(ctx, c, commit.repo, commit, treePath, entryPaths...) + _, err = WalkGitLog(ctx, c.repo, c, treePath, entryPaths...) if err != nil { return err } @@ -94,7 +45,7 @@ func (c *LastCommitCache) recursiveCache(ctx context.Context, commit *Commit, tr if err != nil { return err } - if err := c.recursiveCache(ctx, commit, subTree, treeEntry.Name(), level-1); err != nil { + if err := c.recursiveCache(ctx, subTree, treeEntry.Name(), level-1); err != nil { return err } } diff --git a/modules/git/log_name_status.go b/modules/git/log_name_status.go index e1e117ff4b..80f1602708 100644 --- a/modules/git/log_name_status.go +++ b/modules/git/log_name_status.go @@ -281,7 +281,7 @@ func (g *LogNameStatusRepoParser) Close() { } // WalkGitLog walks the git log --name-status for the head commit in the provided treepath and files -func WalkGitLog(ctx context.Context, cache *LastCommitCache, repo *Repository, head *Commit, treepath string, paths ...string) (map[string]string, error) { +func WalkGitLog(ctx context.Context, repo *Repository, head *Commit, treepath string, paths ...string) (map[string]string, error) { headRef := head.ID.String() tree, err := head.SubTree(treepath) @@ -374,14 +374,14 @@ heaploop: changed[i] = false if results[i] == "" { results[i] = current.CommitID - if err := cache.Put(headRef, path.Join(treepath, paths[i]), current.CommitID); err != nil { + if err := repo.LastCommitCache.Put(headRef, path.Join(treepath, paths[i]), current.CommitID); err != nil { return nil, err } delete(path2idx, paths[i]) remaining-- if results[0] == "" { results[0] = current.CommitID - if err := cache.Put(headRef, treepath, current.CommitID); err != nil { + if err := repo.LastCommitCache.Put(headRef, treepath, current.CommitID); err != nil { return nil, err } delete(path2idx, "") diff --git a/modules/git/notes_gogit.go b/modules/git/notes_gogit.go index 76bc828957..fe6d1f1e58 100644 --- a/modules/git/notes_gogit.go +++ b/modules/git/notes_gogit.go @@ -83,7 +83,7 @@ func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note) log.Error("Unable to get the commit for the path %q. Error: %v", path, err) return err } - note.Commit = convertCommit(lastCommits[path]) + note.Commit = lastCommits[path] return nil } diff --git a/modules/git/notes_nogogit.go b/modules/git/notes_nogogit.go index 1476805dcd..ba216ce3e4 100644 --- a/modules/git/notes_nogogit.go +++ b/modules/git/notes_nogogit.go @@ -81,7 +81,7 @@ func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note) path = path[idx+1:] } - lastCommits, err := GetLastCommitForPaths(ctx, nil, notes, treePath, []string{path}) + lastCommits, err := GetLastCommitForPaths(ctx, notes, treePath, []string{path}) if err != nil { log.Error("Unable to get the commit for the path %q. Error: %v", treePath, err) return err diff --git a/modules/git/repo_base_gogit.go b/modules/git/repo_base_gogit.go index cd2ca25dfb..8fe9c404c3 100644 --- a/modules/git/repo_base_gogit.go +++ b/modules/git/repo_base_gogit.go @@ -31,7 +31,8 @@ type Repository struct { gogitStorage *filesystem.Storage gpgSettings *GPGSettings - Ctx context.Context + Ctx context.Context + LastCommitCache *LastCommitCache } // openRepositoryWithDefaultContext opens the repository at the given path with DefaultContext. @@ -79,6 +80,8 @@ func (repo *Repository) Close() (err error) { if err := repo.gogitStorage.Close(); err != nil { gitealog.Error("Error closing storage: %v", err) } + repo.LastCommitCache = nil + repo.tagCache = nil return } diff --git a/modules/git/repo_base_nogogit.go b/modules/git/repo_base_nogogit.go index 63c278c261..56af2c640f 100644 --- a/modules/git/repo_base_nogogit.go +++ b/modules/git/repo_base_nogogit.go @@ -32,7 +32,8 @@ type Repository struct { checkReader *bufio.Reader checkWriter WriteCloserError - Ctx context.Context + Ctx context.Context + LastCommitCache *LastCommitCache } // openRepositoryWithDefaultContext opens the repository at the given path with DefaultContext. @@ -101,5 +102,7 @@ func (repo *Repository) Close() (err error) { repo.checkReader = nil repo.checkWriter = nil } + repo.LastCommitCache = nil + repo.tagCache = nil return err } diff --git a/modules/highlight/highlight.go b/modules/highlight/highlight.go index acd3bebb9f..6832207c0f 100644 --- a/modules/highlight/highlight.go +++ b/modules/highlight/highlight.go @@ -40,9 +40,11 @@ var ( // NewContext loads custom highlight map from local config func NewContext() { once.Do(func() { - keys := setting.Cfg.Section("highlight.mapping").Keys() - for i := range keys { - highlightMapping[keys[i].Name()] = keys[i].Value() + if setting.Cfg != nil { + keys := setting.Cfg.Section("highlight.mapping").Keys() + for i := range keys { + highlightMapping[keys[i].Name()] = keys[i].Value() + } } // The size 512 is simply a conservative rule of thumb diff --git a/modules/httpcache/httpcache.go b/modules/httpcache/httpcache.go index 5797e981cf..750233d4a7 100644 --- a/modules/httpcache/httpcache.go +++ b/modules/httpcache/httpcache.go @@ -17,16 +17,23 @@ import ( ) // AddCacheControlToHeader adds suitable cache-control headers to response -func AddCacheControlToHeader(h http.Header, d time.Duration) { +func AddCacheControlToHeader(h http.Header, maxAge time.Duration, additionalDirectives ...string) { + directives := make([]string, 0, 2+len(additionalDirectives)) + if setting.IsProd { - h.Set("Cache-Control", "private, max-age="+strconv.Itoa(int(d.Seconds()))) + if maxAge == 0 { + directives = append(directives, "no-store") + } else { + directives = append(directives, "private", "max-age="+strconv.Itoa(int(maxAge.Seconds()))) + } } else { - h.Set("Cache-Control", "no-store") + directives = append(directives, "no-store") + // to remind users they are using non-prod setting. - // some users may be confused by "Cache-Control: no-store" in their setup if they did wrong to `RUN_MODE` in `app.ini`. h.Add("X-Gitea-Debug", "RUN_MODE="+setting.RunMode) - h.Add("X-Gitea-Debug", "CacheControl=no-store") } + + h.Set("Cache-Control", strings.Join(append(directives, additionalDirectives...), ", ")) } // generateETag generates an ETag based on size, filename and file modification time diff --git a/modules/markup/html.go b/modules/markup/html.go index 6071180501..a5606dbb51 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -1176,7 +1176,7 @@ func genDefaultLinkProcessor(defaultLink string) processor { node.DataAtom = atom.A node.Attr = []html.Attribute{ {Key: "href", Val: defaultLink}, - {Key: "class", Val: "default-link"}, + {Key: "class", Val: "default-link muted"}, } node.FirstChild, node.LastChild = ch, ch } diff --git a/modules/packages/conan/reference.go b/modules/packages/conan/reference.go index c43446e6e5..49236981b6 100644 --- a/modules/packages/conan/reference.go +++ b/modules/packages/conan/reference.go @@ -8,10 +8,9 @@ import ( "errors" "fmt" "regexp" + "strings" "code.gitea.io/gitea/modules/log" - - goversion "github.com/hashicorp/go-version" ) const ( @@ -56,7 +55,9 @@ func NewRecipeReference(name, version, user, channel, revision string) (*RecipeR if !namePattern.MatchString(name) { return nil, ErrValidation } - if _, err := goversion.NewSemver(version); err != nil { + + v := strings.TrimSpace(version) + if v == "" { return nil, ErrValidation } if user != "" && !namePattern.MatchString(user) { @@ -69,7 +70,7 @@ func NewRecipeReference(name, version, user, channel, revision string) (*RecipeR return nil, ErrValidation } - return &RecipeReference{name, version, user, channel, revision}, nil + return &RecipeReference{name, v, user, channel, revision}, nil } func (r *RecipeReference) RevisionOrDefault() string { diff --git a/modules/packages/conan/reference_test.go b/modules/packages/conan/reference_test.go index 29ba3a543b..98eb2c8478 100644 --- a/modules/packages/conan/reference_test.go +++ b/modules/packages/conan/reference_test.go @@ -34,6 +34,7 @@ func TestNewRecipeReference(t *testing.T) { {"name", "1.0", "_", "_", "", true}, {"name", "1.0", "_", "_", "0", true}, {"name", "1.0", "", "", "0", true}, + {"name", "1.0.0q", "", "", "0", true}, {"name", "1.0", "", "", "000000000000000000000000000000000000000000000000000000000000", false}, } diff --git a/modules/packages/rubygems/metadata.go b/modules/packages/rubygems/metadata.go index 942f205fc3..05c1a8a719 100644 --- a/modules/packages/rubygems/metadata.go +++ b/modules/packages/rubygems/metadata.go @@ -80,7 +80,6 @@ type gemspec struct { VersionRequirements requirement `yaml:"version_requirements"` } `yaml:"dependencies"` Description string `yaml:"description"` - Email string `yaml:"email"` Executables []string `yaml:"executables"` Extensions []interface{} `yaml:"extensions"` ExtraRdocFiles []string `yaml:"extra_rdoc_files"` diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 06f62cf50c..cc449cae9f 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -1356,7 +1356,6 @@ issues.due_date_form_remove=Odstranit issues.due_date_not_writer=Potřebujete práva na zápis do repozitáře pro úpravy termínu dokončení úkolu. issues.due_date_not_set=Žádný termín dokončení. issues.due_date_added=přidal/a termín dokončení %s %s -issues.due_date_modified=upravil/a termín dokončení z %s na %s %s issues.due_date_remove=odstranil/a termín dokončení %s %s issues.due_date_overdue=Zpožděné issues.due_date_invalid=Termín dokončení není platný nebo je mimo rozsah. Použijte prosím formát „rrrr-mm-dd“. diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 1842b5c18c..796b4b835b 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -1417,7 +1417,6 @@ issues.due_date_form_remove=Entfernen issues.due_date_not_writer=Du musst Schreibrechte in diesem Repository haben, um das Fälligkeitsdatum zu ändern. issues.due_date_not_set=Kein Fälligkeitsdatum gesetzt. issues.due_date_added=hat %[2]s das Fälligkeitsdatum %[1]s hinzugefügt -issues.due_date_modified=hat %[3]s das Fälligkeitsdatum von %[2]s zu %[1]s geändert issues.due_date_remove=hat %[2]s das Fälligkeitsdatum %[1]s entfernt issues.due_date_overdue=Überfällig issues.due_date_invalid=Das Fälligkeitsdatum ist ungültig oder außerhalb des zulässigen Bereichs. Bitte verwende das Format „jjjj-mm-tt“. diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini index 19bed8d21b..51bd62d91f 100644 --- a/options/locale/locale_el-GR.ini +++ b/options/locale/locale_el-GR.ini @@ -1177,7 +1177,7 @@ projects.type.basic_kanban=Βασικό Kanban projects.type.bug_triage=Διαλογή Σφαλμάτων projects.template.desc=Πρότυπο έργου projects.template.desc_helper=Επιλέξτε ένα πρότυπο έργου για να ξεκινήσετε -projects.type.uncategorized=Αταξινόμητο +projects.type.uncategorized=Χωρίς Κατηγορία projects.board.edit=Επεξεργασία πίνακα projects.board.edit_title=Νέο Όνομα Πίνακα projects.board.new_title=Νέο Όνομα Πίνακα @@ -1186,7 +1186,7 @@ projects.board.new=Νέος Πίνακας projects.board.set_default=Ορισμός Προεπιλογής projects.board.set_default_desc=Ορίστε αυτόν τον πίνακα ως προεπιλογή για μη κατηγοριοποιημένα ζητήματα και pull requests projects.board.delete=Διαγραφή Πίνακα -projects.board.deletion_desc=Η διαγραφή ενός πίνακα έργου μετακινεί όλα τα σχετιζόμενα ζητήματα σε 'Αταξινόμητα'. Συνέχεια; +projects.board.deletion_desc=Η διαγραφή ενός πίνακα έργου μετακινεί όλα τα σχετιζόμενα ζητήματα σε 'Χωρίς Κατηγορία'. Συνέχεια; projects.board.color=Χρώμα projects.open=Άνοιγμα projects.close=Κλείσιμο @@ -1420,7 +1420,7 @@ issues.due_date_form_remove=Διαγραφή issues.due_date_not_writer=Χρειάζεστε πρόσβαση εγγραφής στο αποθετήριο για να ενημερώσετε την ημερομηνία λήξης ενός ζητήματος. issues.due_date_not_set=Δεν ορίστηκε ημερομηνία παράδοσης. issues.due_date_added=πρόσθεσε την ημερομηνία παράδοσης %s %s -issues.due_date_modified=τροποποίησε την ημερομηνία παράδοσης σε %s από %s %s +issues.due_date_modified=τροποποίησε την ημερομηνία παράδοσης από %[2]s σε %[1]s %[3]s issues.due_date_remove=αφαίρεσε την ημερομηνία παράδοσης %s %s issues.due_date_overdue=Εκπρόθεσμο issues.due_date_invalid=Η ημερομηνία παράδοσης δεν είναι έγκυρη ή εκτός εύρους. Παρακαλούμε χρησιμοποιήστε τη μορφή 'εεεε-μμ-ηη'. diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 566a7bd167..a97e2e2b3b 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3044,6 +3044,7 @@ title = Packages desc = Manage repository packages. empty = There are no packages yet. empty.documentation = For more information on the package registry, see the documentation. +empty.repo = Did you upload a package, but it's not shown here? Go to package settings and link it to this repo. filter.type = Type filter.type.all = All filter.no_result = Your filter produced no results. diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index bacf2a427e..d2f5695ec2 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -1418,7 +1418,6 @@ issues.due_date_form_remove=Eliminar issues.due_date_not_writer=Necesita acceso de escritura al repositorio para actualizar la fecha de vencimiento de un issue. issues.due_date_not_set=Sin fecha de vencimiento. issues.due_date_added=añadió la fecha de vencimiento %s %s -issues.due_date_modified=modificó la fecha de vencimiento a %s de %s %s issues.due_date_remove=eliminó la fecha de vencimiento %s %s issues.due_date_overdue=Vencido issues.due_date_invalid=La fecha de vencimiento es inválida o está fuera de rango. Por favor utilice el formato 'aaaa-mm-dd'. diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini index ae62a8c8fd..d97d3eead1 100644 --- a/options/locale/locale_fa-IR.ini +++ b/options/locale/locale_fa-IR.ini @@ -1309,7 +1309,6 @@ issues.due_date_form_remove=حذف/ساقط کردن issues.due_date_not_writer=شما نیازمند دسترسی نوشتن به این مخزن را برای تغییر موعد مقرر این مسئله را دارید. issues.due_date_not_set=هیچ موعد مقرری ثبت نشده. issues.due_date_added=موعد مقرر اضافه شد %s %s -issues.due_date_modified=موعد مقرر از %s به %s %s تغییر کرد. issues.due_date_remove=موعد مقرر %s %s حذف شد issues.due_date_overdue=تاریخ گذشته issues.due_date_invalid=موعد مقرر نامعتبر است یا خارج از محدوده. لطفاً از قالب 'yyy-mm-dd' استفاده کنید. diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index 9fbdec862c..c1f7f6e0b2 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -1306,7 +1306,6 @@ issues.due_date_form_remove=Supprimer issues.due_date_not_writer=Vous devez avoir accès au dépôt en écriture pour mettre à jour l'échéance d'un ticket. issues.due_date_not_set=Aucune échéance n'a été définie. issues.due_date_added=a ajouté l'échéance %s %s -issues.due_date_modified=a modifié l'échéance de %[2]s vers %[1]s %[3]s issues.due_date_remove=a supprimé l'échéance %s %s issues.due_date_overdue=En retard issues.due_date_invalid=La date d’échéance est invalide ou hors plage. Veuillez utiliser le format 'aaaa-mm-dd'. diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini index ed99432dff..43bae7e10c 100644 --- a/options/locale/locale_hu-HU.ini +++ b/options/locale/locale_hu-HU.ini @@ -922,7 +922,6 @@ issues.due_date_form_add=Határidő hozzáadása issues.due_date_form_edit=Szerkesztés issues.due_date_form_remove=Eltávolítás issues.due_date_not_set=Nincs beállítva határidő. -issues.due_date_modified=határidő módosítva %s-ről %s %s-re issues.due_date_remove=%s %s-es határidő eltávolítva issues.due_date_overdue=Lejárt issues.dependency.title=Függőségek diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini index 7087416f91..5b397773f9 100644 --- a/options/locale/locale_it-IT.ini +++ b/options/locale/locale_it-IT.ini @@ -1124,7 +1124,6 @@ issues.due_date_form_remove=Rimuovi issues.due_date_not_writer=E' necessario l'accesso di scrittura del repository per aggiornare la data di una sua issue. issues.due_date_not_set=Nessuna data di scadenza impostata. issues.due_date_added=la data di scadenza %s è stata aggiunta %s -issues.due_date_modified=data di scadenza modificata da %s a %s %s issues.due_date_remove=rimossa la data di scadenza %s %s issues.due_date_overdue=Scaduto issues.due_date_invalid=La data di scadenza non è valida o fuori intervallo. Si prega di utilizzare il formato 'aaaa-mm-dd'. diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini index 7c8754080d..6049c44313 100644 --- a/options/locale/locale_ko-KR.ini +++ b/options/locale/locale_ko-KR.ini @@ -807,7 +807,6 @@ issues.due_date_form_remove=삭제 issues.due_date_not_writer=이슈의 마감일을 갱신하려면 저장소 쓰기 권한이 필요합니다. issues.due_date_not_set=마감일이 설정되지 않았습니다. issues.due_date_added=마감일 %s 를 추가 %s -issues.due_date_modified=%s 마감일이 %s %s 로 변경되었습니다 issues.due_date_remove=%s %s 마감일이 삭제되었습니다. issues.due_date_overdue=기한 초과 issues.due_date_invalid=기한이 올바르지 않거나 범위를 벗어났습니다. 'yyyy-mm-dd'형식을 사용해주십시오. diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index 0b09167a0c..5a43101b1e 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -1416,7 +1416,6 @@ issues.due_date_form_remove=Noņemt issues.due_date_not_writer=Jums ir nepieciešamas rakstīšanas tiesības uz šo repozitoriju, lai mainītu izpildes termiņu. issues.due_date_not_set=Izpildes termiņš nav uzstādīts. issues.due_date_added=pievienoja izpildes termiņu %s %s -issues.due_date_modified=mainīja izpildes termiņu uz %s no %s %s issues.due_date_remove=noņēma izpildes termiņu %s %s issues.due_date_overdue=Nokavēts issues.due_date_invalid=Datums līdz nav korekts. Izmantojiet formātu 'gggg-mm-dd'. diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini index ccdf978800..3e54ef3fd7 100644 --- a/options/locale/locale_nl-NL.ini +++ b/options/locale/locale_nl-NL.ini @@ -1141,7 +1141,6 @@ issues.due_date_form_remove=Verwijder issues.due_date_not_writer=Je hebt schrijftoegang in deze repository nodig om de deadline van een kwestie aan te passen. issues.due_date_not_set=Geen vervaldatum ingesteld. issues.due_date_added=heeft %[2]s de deadline %[1]s toegevoegd -issues.due_date_modified=heeft %[3]s de deadline aangepast van %[1]s naar %[2]s issues.due_date_remove=heeft %[2]s de deadline %[1]s verwijderd issues.due_date_overdue=Over tijd issues.due_date_invalid=De deadline is ongeldig of buiten bereik. Gebruik het formaat 'jjjj-mm-dd'. diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini index 8843dd4c26..5e6c078b2d 100644 --- a/options/locale/locale_pl-PL.ini +++ b/options/locale/locale_pl-PL.ini @@ -1299,7 +1299,6 @@ issues.due_date_form_remove=Usuń issues.due_date_not_writer=Potrzebujesz uprawnień zapisu w tym repozytorium, aby zaktualizować termin realizacji zgłoszenia. issues.due_date_not_set=Brak ustawionego terminu realizacji. issues.due_date_added=dodaje termin realizacji %s %s -issues.due_date_modified=zmienia termin realizacji na %s z %s %s issues.due_date_remove=usuwa termin realizacji %s %s issues.due_date_overdue=Zaległe issues.due_date_invalid=Data realizacji jest niewłaściwa lub spoza zakresu. Użyj formatu 'yyyy-mm-dd'. diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index 660b0a240a..72262c6086 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -1419,7 +1419,6 @@ issues.due_date_form_remove=Remover issues.due_date_not_writer=Você deve ter permissão de escrita no repositório para atualizar a data limite de uma issue. issues.due_date_not_set=Data limite não informada. issues.due_date_added=adicionou a data limite %s %s -issues.due_date_modified=modificou a data limite para %s ao invés de %s %s issues.due_date_remove=removeu a data limite %s %s issues.due_date_overdue=Em atraso issues.due_date_invalid=A data limite é inválida ou está fora do intervalo. Por favor, use o formato 'dd/mm/aaaa'. @@ -1800,6 +1799,7 @@ settings.tracker_url_format_error=O formato da URL do issue tracker externo não settings.tracker_issue_style=Formato de número do issue tracker externo settings.tracker_issue_style.numeric=Numérico settings.tracker_issue_style.alphanumeric=Alfanumérico +settings.tracker_issue_style.regexp=Expressão Regular settings.tracker_url_format_desc=Use os espaços reservados {user}, {repo} e {index} para o nome de usuário, nome do repositório e o índice de problemas. settings.enable_timetracker=Habilitar Cronômetro settings.allow_only_contributors_to_track_time=Permitir que apenas os colaboradores acompanhem o contador de tempo diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index 135eed89bd..e95ceb2453 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -1376,7 +1376,6 @@ issues.due_date_form_remove=Удалить issues.due_date_not_writer=Для обновления срока выполнения необходим доступ на запись в репозиторий. issues.due_date_not_set=Срок выполнения не установлен. issues.due_date_added=добавлено в срок выполнения %s %s -issues.due_date_modified=срок выполнения изменён на %s с %s %s issues.due_date_remove=удалён срок выполнения %s %s issues.due_date_overdue=Просроченные issues.due_date_invalid=Срок действия недействителен или находится за пределами допустимого диапазона. Пожалуйста, используйте формат 'гггг-мм-дд'. diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini index 426cbf73dc..a07ff03788 100644 --- a/options/locale/locale_si-LK.ini +++ b/options/locale/locale_si-LK.ini @@ -1254,7 +1254,6 @@ issues.due_date_form_remove=ඉවත් කරන්න issues.due_date_not_writer=ඔබ නිකුත් ගේ නියමිත දිනය යාවත්කාලීන කිරීමට ගබඩාවක් ලිවීමට ප්රවේශය අවශ්ය. issues.due_date_not_set=නියමිත දිනය නියම කර නැත. issues.due_date_added=නියමිත දිනය එකතු %s %s -issues.due_date_modified=නියමිත දිනය %s සිට %s %sදක්වා වෙනස් කරන ලදි issues.due_date_remove=නියමිත දිනය ඉවත් කරන ලදි %s %s issues.due_date_overdue=කල් ඉකුත්වීම issues.due_date_invalid=නියමිත දිනය අවලංගු හෝ පරාසයෙන් බැහැර වේ. කරුණාකර 'yyyy-mm-dd' ආකෘතිය භාවිතා කරන්න. diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini index e8458136b5..c2ea1b327b 100644 --- a/options/locale/locale_sv-SE.ini +++ b/options/locale/locale_sv-SE.ini @@ -1068,7 +1068,6 @@ issues.due_date_form_remove=Ta bort issues.due_date_not_writer=Du måste ha skrivrättigheter för att ändra ett ärendes förfallodatum. issues.due_date_not_set=Inget förfallodatum satt. issues.due_date_added=lade till förfallodatumet %s %s -issues.due_date_modified=ändrade förfallodatumet från %s till %s %s issues.due_date_remove=tog bort förfallodatumet %s %s issues.due_date_overdue=Försenad issues.due_date_invalid=Förfallodatumet är ogiltigt eller utanför gränserna. Använd formatet 'åååå-mm-dd'. diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index b302209121..d2a19bc8f5 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -1275,7 +1275,6 @@ issues.due_date_form_remove=Kaldır issues.due_date_not_writer=Bir konunun bitiş tarihini değiştirmek için depoda yazma hakkınız olmalıdır. issues.due_date_not_set=Bitiş tarihi atanmadı. issues.due_date_added=%[2]s %[1]s bitiş tarihini ekledi -issues.due_date_modified=%s bitiş tarihini %s iken %s olarak değiştirildi issues.due_date_remove=%[2]s %[1]s bitiş tarihini kaldırdı issues.due_date_overdue=Süresi Geçmiş issues.due_date_invalid=Bitiş tarihi geçersiz veya aralık dışında. Lütfen 'yyyy-aa-gg' biçimini kullanın. diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini index f9e47f4a80..7fbb0b62d1 100644 --- a/options/locale/locale_uk-UA.ini +++ b/options/locale/locale_uk-UA.ini @@ -1317,7 +1317,6 @@ issues.due_date_form_remove=Видалити issues.due_date_not_writer=Вам потрібен доступ до запису в репозиторії, щоб оновити дату завершення задач. issues.due_date_not_set=Термін виконання не встановлений. issues.due_date_added=додав(ла) дату завершення %s %s -issues.due_date_modified=термін змінено з %s %s на %s issues.due_date_remove=видалив(ла) дату завершення %s %s issues.due_date_overdue=Прострочено issues.due_date_invalid=Термін дії не дійсний або знаходиться за межами допустимого діапазону. Будь ласка використовуйте формат 'yyyy-mm-dd'. diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index bccf5d7e37..2823b9a2eb 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -1419,7 +1419,6 @@ issues.due_date_form_remove=删除 issues.due_date_not_writer=你需要仓库写入权限来修改工单到期时间。 issues.due_date_not_set=未设置到期时间。 issues.due_date_added=于 %[2]s 设置到期时间为 %[1]s -issues.due_date_modified=于 %[3]s 将到期时间从 %[2]s 修改为 %[1]s issues.due_date_remove=于 %[2]s 删除了到期时间 %[1]s issues.due_date_overdue=过期 issues.due_date_invalid=到期日期无效或超出范围。请使用 'yyyy-mm-dd' 格式。 diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index 10e6418be0..f795879e11 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -1419,7 +1419,6 @@ issues.due_date_form_remove=移除 issues.due_date_not_writer=您需要儲存庫寫入權限來更改問題的截止日。 issues.due_date_not_set=未設定截止日期。 issues.due_date_added=新增了截止日期 %s %s -issues.due_date_modified=將截止日期修改為 %s ,原截止日期: %s %s issues.due_date_remove=移除了截止日期 %s %s issues.due_date_overdue=逾期 issues.due_date_invalid=截止日期無效或超出範圍,請使用「yyyy-mm-dd」的格式。 diff --git a/package-lock.json b/package-lock.json index d2aa3dbe1b..cfd2a6ad2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,6 +50,7 @@ "eslint": "8.20.0", "eslint-plugin-import": "2.26.0", "eslint-plugin-jquery": "1.5.1", + "eslint-plugin-sonarjs": "0.13.0", "eslint-plugin-unicorn": "43.0.2", "eslint-plugin-vue": "9.2.0", "jest": "28.1.3", @@ -5492,6 +5493,18 @@ "eslint": ">=5.4.0" } }, + "node_modules/eslint-plugin-sonarjs": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.13.0.tgz", + "integrity": "sha512-t3m7ta0EspzDxSOZh3cEOJIJVZgN/TlJYaBGnQlK6W/PZNbWep8q4RQskkJkA7/zwNpX0BaoEOSUUrqaADVoqA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/eslint-plugin-unicorn": { "version": "43.0.2", "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-43.0.2.tgz", @@ -16787,6 +16800,13 @@ "dev": true, "requires": {} }, + "eslint-plugin-sonarjs": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.13.0.tgz", + "integrity": "sha512-t3m7ta0EspzDxSOZh3cEOJIJVZgN/TlJYaBGnQlK6W/PZNbWep8q4RQskkJkA7/zwNpX0BaoEOSUUrqaADVoqA==", + "dev": true, + "requires": {} + }, "eslint-plugin-unicorn": { "version": "43.0.2", "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-43.0.2.tgz", diff --git a/package.json b/package.json index c1ecd16343..f4752aeec9 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "eslint": "8.20.0", "eslint-plugin-import": "2.26.0", "eslint-plugin-jquery": "1.5.1", + "eslint-plugin-sonarjs": "0.13.0", "eslint-plugin-unicorn": "43.0.2", "eslint-plugin-vue": "9.2.0", "jest": "28.1.3", diff --git a/routers/api/packages/composer/composer.go b/routers/api/packages/composer/composer.go index 23de28c7f9..b7c1f140dc 100644 --- a/routers/api/packages/composer/composer.go +++ b/routers/api/packages/composer/composer.go @@ -19,6 +19,7 @@ import ( packages_module "code.gitea.io/gitea/modules/packages" composer_module "code.gitea.io/gitea/modules/packages/composer" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/api/packages/helper" packages_service "code.gitea.io/gitea/services/packages" @@ -62,10 +63,11 @@ func SearchPackages(ctx *context.Context) { } opts := &packages_model.PackageSearchOptions{ - OwnerID: ctx.Package.Owner.ID, - Type: packages_model.TypeComposer, - Name: packages_model.SearchValue{Value: ctx.FormTrim("q")}, - Paginator: &paginator, + OwnerID: ctx.Package.Owner.ID, + Type: packages_model.TypeComposer, + Name: packages_model.SearchValue{Value: ctx.FormTrim("q")}, + IsInternal: util.OptionalBoolFalse, + Paginator: &paginator, } if ctx.FormTrim("type") != "" { opts.Properties = map[string]string{ diff --git a/routers/api/packages/helm/helm.go b/routers/api/packages/helm/helm.go index ae0643a35a..f59cfc7c7b 100644 --- a/routers/api/packages/helm/helm.go +++ b/routers/api/packages/helm/helm.go @@ -19,6 +19,7 @@ import ( packages_module "code.gitea.io/gitea/modules/packages" helm_module "code.gitea.io/gitea/modules/packages/helm" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/api/packages/helper" packages_service "code.gitea.io/gitea/services/packages" @@ -39,8 +40,9 @@ func apiError(ctx *context.Context, status int, obj interface{}) { // Index generates the Helm charts index func Index(ctx *context.Context) { pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ - OwnerID: ctx.Package.Owner.ID, - Type: packages_model.TypeHelm, + OwnerID: ctx.Package.Owner.ID, + Type: packages_model.TypeHelm, + IsInternal: util.OptionalBoolFalse, }) if err != nil { apiError(ctx, http.StatusInternalServerError, err) @@ -108,6 +110,7 @@ func DownloadPackageFile(ctx *context.Context) { Value: ctx.Params("package"), }, HasFileWithName: filename, + IsInternal: util.OptionalBoolFalse, }) if err != nil { apiError(ctx, http.StatusInternalServerError, err) diff --git a/routers/api/packages/npm/npm.go b/routers/api/packages/npm/npm.go index d127134d44..152edc681a 100644 --- a/routers/api/packages/npm/npm.go +++ b/routers/api/packages/npm/npm.go @@ -18,6 +18,7 @@ import ( packages_module "code.gitea.io/gitea/modules/packages" npm_module "code.gitea.io/gitea/modules/packages/npm" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/api/packages/helper" packages_service "code.gitea.io/gitea/services/packages" @@ -261,6 +262,7 @@ func setPackageTag(tag string, pv *packages_model.PackageVersion, deleteOnly boo Properties: map[string]string{ npm_module.TagProperty: tag, }, + IsInternal: util.OptionalBoolFalse, }) if err != nil { return err diff --git a/routers/api/packages/nuget/nuget.go b/routers/api/packages/nuget/nuget.go index 013c0c1e33..b7667a3222 100644 --- a/routers/api/packages/nuget/nuget.go +++ b/routers/api/packages/nuget/nuget.go @@ -17,6 +17,7 @@ import ( packages_module "code.gitea.io/gitea/modules/packages" nuget_module "code.gitea.io/gitea/modules/packages/nuget" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/api/packages/helper" packages_service "code.gitea.io/gitea/services/packages" ) @@ -39,9 +40,10 @@ func ServiceIndex(ctx *context.Context) { // SearchService https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-for-packages func SearchService(ctx *context.Context) { pvs, count, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ - OwnerID: ctx.Package.Owner.ID, - Type: packages_model.TypeNuGet, - Name: packages_model.SearchValue{Value: ctx.FormTrim("q")}, + OwnerID: ctx.Package.Owner.ID, + Type: packages_model.TypeNuGet, + Name: packages_model.SearchValue{Value: ctx.FormTrim("q")}, + IsInternal: util.OptionalBoolFalse, Paginator: db.NewAbsoluteListOptions( ctx.FormInt("skip"), ctx.FormInt("take"), diff --git a/routers/api/packages/rubygems/rubygems.go b/routers/api/packages/rubygems/rubygems.go index b3815a914e..4f066a8303 100644 --- a/routers/api/packages/rubygems/rubygems.go +++ b/routers/api/packages/rubygems/rubygems.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/context" packages_module "code.gitea.io/gitea/modules/packages" rubygems_module "code.gitea.io/gitea/modules/packages/rubygems" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/api/packages/helper" packages_service "code.gitea.io/gitea/services/packages" ) @@ -40,8 +41,9 @@ func EnumeratePackages(ctx *context.Context) { // EnumeratePackagesLatest serves the list of the latest version of every package func EnumeratePackagesLatest(ctx *context.Context) { pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{ - OwnerID: ctx.Package.Owner.ID, - Type: packages_model.TypeRubyGems, + OwnerID: ctx.Package.Owner.ID, + Type: packages_model.TypeRubyGems, + IsInternal: util.OptionalBoolFalse, }) if err != nil { apiError(ctx, http.StatusInternalServerError, err) @@ -289,6 +291,7 @@ func getVersionsByFilename(ctx *context.Context, filename string) ([]*packages_m OwnerID: ctx.Package.Owner.ID, Type: packages_model.TypeRubyGems, HasFileWithName: filename, + IsInternal: util.OptionalBoolFalse, }) return pvs, err } diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go index 038924737a..5a9c93b3ca 100644 --- a/routers/api/v1/packages/package.go +++ b/routers/api/v1/packages/package.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/convert" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/api/v1/utils" packages_service "code.gitea.io/gitea/services/packages" ) @@ -55,10 +56,11 @@ func ListPackages(ctx *context.APIContext) { query := ctx.FormTrim("q") pvs, count, err := packages.SearchVersions(ctx, &packages.PackageSearchOptions{ - OwnerID: ctx.Package.Owner.ID, - Type: packages.Type(packageType), - Name: packages.SearchValue{Value: query}, - Paginator: &listOptions, + OwnerID: ctx.Package.Owner.ID, + Type: packages.Type(packageType), + Name: packages.SearchValue{Value: query}, + IsInternal: util.OptionalBoolFalse, + Paginator: &listOptions, }) if err != nil { ctx.Error(http.StatusInternalServerError, "SearchVersions", err) diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index 1ac1088839..8353a4e501 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -18,7 +18,6 @@ import ( git_model "code.gitea.io/gitea/models/git" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" - "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/httpcache" @@ -33,6 +32,8 @@ import ( files_service "code.gitea.io/gitea/services/repository/files" ) +const giteaObjectTypeHeader = "X-Gitea-Object-Type" + // GetRawFile get a file by path on a repository func GetRawFile(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/raw/{filepath} repository repoGetRawFile @@ -72,11 +73,13 @@ func GetRawFile(ctx *context.APIContext) { return } - blob, lastModified := getBlobForEntry(ctx) + blob, entry, lastModified := getBlobForEntry(ctx) if ctx.Written() { return } + ctx.RespHeader().Set(giteaObjectTypeHeader, string(files_service.GetObjectTypeFromTreeEntry(entry))) + if err := common.ServeBlob(ctx.Context, blob, lastModified); err != nil { ctx.Error(http.StatusInternalServerError, "ServeBlob", err) } @@ -119,11 +122,13 @@ func GetRawFileOrLFS(ctx *context.APIContext) { return } - blob, lastModified := getBlobForEntry(ctx) + blob, entry, lastModified := getBlobForEntry(ctx) if ctx.Written() { return } + ctx.RespHeader().Set(giteaObjectTypeHeader, string(files_service.GetObjectTypeFromTreeEntry(entry))) + // LFS Pointer files are at most 1024 bytes - so any blob greater than 1024 bytes cannot be an LFS file if blob.Size() > 1024 { // First handle caching for the blob @@ -218,7 +223,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) { } } -func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, lastModified time.Time) { +func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEntry, lastModified time.Time) { entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) if err != nil { if git.IsErrNotExist(err) { @@ -234,12 +239,7 @@ func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, lastModified time return } - var c *git.LastCommitCache - if setting.CacheService.LastCommit.Enabled && ctx.Repo.CommitsCount >= setting.CacheService.LastCommit.CommitsCount { - c = git.NewLastCommitCache(ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, setting.LastCommitCacheTTLSeconds, cache.GetCache()) - } - - info, _, err := git.Entries([]*git.TreeEntry{entry}).GetCommitsInfo(ctx, ctx.Repo.Commit, path.Dir("/" + ctx.Repo.TreePath)[1:], c) + info, _, err := git.Entries([]*git.TreeEntry{entry}).GetCommitsInfo(ctx, ctx.Repo.Commit, path.Dir("/" + ctx.Repo.TreePath)[1:]) if err != nil { ctx.Error(http.StatusInternalServerError, "GetCommitsInfo", err) return @@ -251,7 +251,7 @@ func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, lastModified time } blob = entry.Blob() - return blob, lastModified + return blob, entry, lastModified } // GetArchive get archive of a repository diff --git a/routers/api/v1/utils/git.go b/routers/api/v1/utils/git.go index ac64d5b87b..f18442d046 100644 --- a/routers/api/v1/utils/git.go +++ b/routers/api/v1/utils/git.go @@ -8,8 +8,10 @@ import ( "fmt" "net/http" + "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" ) // ResolveRefOrSha resolve ref to sha if exist @@ -19,6 +21,7 @@ func ResolveRefOrSha(ctx *context.APIContext, ref string) string { return "" } + sha := ref // Search branches and tags for _, refType := range []string{"heads", "tags"} { refSHA, lastMethodName, err := searchRefCommitByType(ctx, refType, ref) @@ -27,10 +30,27 @@ func ResolveRefOrSha(ctx *context.APIContext, ref string) string { return "" } if refSHA != "" { - return refSHA + sha = refSHA + break } } - return ref + + if ctx.Repo.GitRepo != nil && ctx.Repo.GitRepo.LastCommitCache == nil { + commitsCount, err := cache.GetInt64(ctx.Repo.Repository.GetCommitsCountCacheKey(ref, true), func() (int64, error) { + commit, err := ctx.Repo.GitRepo.GetCommit(sha) + if err != nil { + return 0, err + } + return commit.CommitsCount() + }) + if err != nil { + log.Error("Unable to get commits count for %s in %s. Error: %v", sha, ctx.Repo.Repository.FullName(), err) + return sha + } + ctx.Repo.GitRepo.LastCommitCache = git.NewLastCommitCache(commitsCount, ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, cache.GetCache()) + } + + return sha } // GetGitRefs return git references based on filter diff --git a/routers/install/routes.go b/routers/install/routes.go index 32829ede9e..fdabcb9dc2 100644 --- a/routers/install/routes.go +++ b/routers/install/routes.go @@ -9,6 +9,7 @@ import ( "net/http" "path" + "code.gitea.io/gitea/modules/httpcache" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/public" "code.gitea.io/gitea/modules/setting" @@ -62,6 +63,7 @@ func installRecovery() func(next http.Handler) http.Handler { "SignedUserName": "", } + httpcache.AddCacheControlToHeader(w.Header(), 0, "no-transform") w.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions) if !setting.IsProd { diff --git a/routers/web/admin/packages.go b/routers/web/admin/packages.go index 79bf025dd2..5de8922e6f 100644 --- a/routers/web/admin/packages.go +++ b/routers/web/admin/packages.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" packages_service "code.gitea.io/gitea/services/packages" ) @@ -31,9 +32,10 @@ func Packages(ctx *context.Context) { sort := ctx.FormTrim("sort") pvs, total, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ - Type: packages_model.Type(packageType), - Name: packages_model.SearchValue{Value: query}, - Sort: sort, + Type: packages_model.Type(packageType), + Name: packages_model.SearchValue{Value: query}, + Sort: sort, + IsInternal: util.OptionalBoolFalse, Paginator: &db.ListOptions{ PageSize: setting.UI.PackagesPagingNum, Page: page, diff --git a/routers/web/base.go b/routers/web/base.go index c7ade55a61..30a24a1275 100644 --- a/routers/web/base.go +++ b/routers/web/base.go @@ -158,6 +158,7 @@ func Recovery() func(next http.Handler) http.Handler { store["SignedUserName"] = "" } + httpcache.AddCacheControlToHeader(w.Header(), 0, "no-transform") w.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions) if !setting.IsProd { diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 5c46882f3d..8ed794b45c 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -786,6 +786,19 @@ func CompareDiff(ctx *context.Context) { ctx.Data["IsDiffCompare"] = true ctx.Data["RequireTribute"] = true setTemplateIfExists(ctx, pullRequestTemplateKey, nil, pullRequestTemplateCandidates) + + // If a template content is set, prepend the "content". In this case that's only + // applicable if you have one commit to compare and that commit has a message. + // In that case the commit message will be prepend to the template body. + if templateContent, ok := ctx.Data[pullRequestTemplateKey].(string); ok && templateContent != "" { + if content, ok := ctx.Data["content"].(string); ok && content != "" { + // Re-use the same key as that's priortized over the "content" key. + // Add two new lines between the content to ensure there's always at least + // one empty line between them. + ctx.Data[pullRequestTemplateKey] = content + "\n\n" + templateContent + } + } + ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled upload.AddUploadContext(ctx, "comment") diff --git a/routers/web/repo/download.go b/routers/web/repo/download.go index 6755cda874..cd2d305de6 100644 --- a/routers/web/repo/download.go +++ b/routers/web/repo/download.go @@ -10,7 +10,6 @@ import ( "time" git_model "code.gitea.io/gitea/models/git" - "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/httpcache" @@ -99,12 +98,7 @@ func getBlobForEntry(ctx *context.Context) (blob *git.Blob, lastModified time.Ti return } - var c *git.LastCommitCache - if setting.CacheService.LastCommit.Enabled && ctx.Repo.CommitsCount >= setting.CacheService.LastCommit.CommitsCount { - c = git.NewLastCommitCache(ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, setting.LastCommitCacheTTLSeconds, cache.GetCache()) - } - - info, _, err := git.Entries([]*git.TreeEntry{entry}).GetCommitsInfo(ctx, ctx.Repo.Commit, path.Dir("/" + ctx.Repo.TreePath)[1:], c) + info, _, err := git.Entries([]*git.TreeEntry{entry}).GetCommitsInfo(ctx, ctx.Repo.Commit, path.Dir("/" + ctx.Repo.TreePath)[1:]) if err != nil { ctx.ServerError("GetCommitsInfo", err) return diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go index 03ea4fc5f4..57db19aa32 100644 --- a/routers/web/repo/packages.go +++ b/routers/web/repo/packages.go @@ -9,9 +9,11 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" ) const ( @@ -32,10 +34,11 @@ func Packages(ctx *context.Context) { PageSize: setting.UI.PackagesPagingNum, Page: page, }, - OwnerID: ctx.ContextUser.ID, - RepoID: ctx.Repo.Repository.ID, - Type: packages.Type(packageType), - Name: packages.SearchValue{Value: query}, + OwnerID: ctx.ContextUser.ID, + RepoID: ctx.Repo.Repository.ID, + Type: packages.Type(packageType), + Name: packages.SearchValue{Value: query}, + IsInternal: util.OptionalBoolFalse, }) if err != nil { ctx.ServerError("SearchLatestVersions", err) @@ -60,6 +63,9 @@ func Packages(ctx *context.Context) { ctx.Data["Query"] = query ctx.Data["PackageType"] = packageType ctx.Data["HasPackages"] = hasPackages + if ctx.Repo != nil { + ctx.Data["CanWritePackages"] = ctx.IsUserRepoWriter([]unit.Type{unit.TypePackages}) || ctx.IsUserSiteAdmin() + } ctx.Data["PackageDescriptors"] = pds ctx.Data["Total"] = total ctx.Data["RepositoryAccessMap"] = map[int64]bool{ctx.Repo.Repository.ID: true} // There is only the current repository diff --git a/routers/web/repo/setting.go b/routers/web/repo/setting.go index 5ded0561c6..7f6b0feafb 100644 --- a/routers/web/repo/setting.go +++ b/routers/web/repo/setting.go @@ -476,7 +476,7 @@ func SettingsPost(ctx *context.Context) { deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects) } - if form.EnablePackages && !unit_model.TypeProjects.UnitGlobalDisabled() { + if form.EnablePackages && !unit_model.TypePackages.UnitGlobalDisabled() { units = append(units, repo_model.RepoUnit{ RepoID: repo.ID, Type: unit_model.TypePackages, diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 6b6660f774..a396be8ae3 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -27,7 +27,6 @@ import ( unit_model "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" - "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" @@ -812,11 +811,6 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri defer cancel() } - var c *git.LastCommitCache - if setting.CacheService.LastCommit.Enabled && ctx.Repo.CommitsCount >= setting.CacheService.LastCommit.CommitsCount { - c = git.NewLastCommitCache(ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, setting.LastCommitCacheTTLSeconds, cache.GetCache()) - } - selected := map[string]bool{} for _, pth := range ctx.FormStrings("f[]") { selected[pth] = true @@ -833,7 +827,7 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri } var latestCommit *git.Commit - ctx.Data["Files"], latestCommit, err = entries.GetCommitsInfo(commitInfoCtx, ctx.Repo.Commit, ctx.Repo.TreePath, c) + ctx.Data["Files"], latestCommit, err = entries.GetCommitsInfo(commitInfoCtx, ctx.Repo.Commit, ctx.Repo.TreePath) if err != nil { ctx.ServerError("GetCommitsInfo", err) return nil diff --git a/routers/web/user/home.go b/routers/web/user/home.go index 698117e957..6482699804 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -591,6 +591,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { LabelIDs: opts.LabelIDs, Org: org, Team: team, + RepoCond: opts.RepoCond, } issueStats, err = issues_model.GetUserIssueStats(statsOpts) diff --git a/routers/web/user/package.go b/routers/web/user/package.go index b2b550cb73..aa379152b3 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -17,6 +17,7 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/forms" packages_service "code.gitea.io/gitea/services/packages" @@ -43,9 +44,10 @@ func ListPackages(ctx *context.Context) { PageSize: setting.UI.PackagesPagingNum, Page: page, }, - OwnerID: ctx.ContextUser.ID, - Type: packages_model.Type(packageType), - Name: packages_model.SearchValue{Value: query}, + OwnerID: ctx.ContextUser.ID, + Type: packages_model.Type(packageType), + Name: packages_model.SearchValue{Value: query}, + IsInternal: util.OptionalBoolFalse, }) if err != nil { ctx.ServerError("SearchLatestVersions", err) @@ -112,7 +114,8 @@ func RedirectToLastVersion(ctx *context.Context) { } pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{ - PackageID: p.ID, + PackageID: p.ID, + IsInternal: util.OptionalBoolFalse, }) if err != nil { ctx.ServerError("GetPackageByName", err) @@ -157,8 +160,9 @@ func ViewPackageVersion(ctx *context.Context) { }) default: pvs, total, err = packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ - Paginator: db.NewAbsoluteListOptions(0, 5), - PackageID: pd.Package.ID, + Paginator: db.NewAbsoluteListOptions(0, 5), + PackageID: pd.Package.ID, + IsInternal: util.OptionalBoolFalse, }) if err != nil { ctx.ServerError("SearchVersions", err) @@ -254,6 +258,7 @@ func ListPackageVersions(ctx *context.Context) { ExactMatch: false, Value: query, }, + IsInternal: util.OptionalBoolFalse, }) if err != nil { ctx.ServerError("SearchVersions", err) diff --git a/routers/web/web.go b/routers/web/web.go index 5a3a60ac44..b1e78d5888 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/activitypub" + "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/httpcache" @@ -1012,6 +1013,7 @@ func RegisterRoutes(m *web.Route) { return } ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount + ctx.Repo.GitRepo.LastCommitCache = git.NewLastCommitCache(ctx.Repo.CommitsCount, ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, cache.GetCache()) }) }, ignSignIn, context.RepoAssignment, context.UnitTypes(), reqRepoReleaseReader) diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index 6e8c149dab..6dd237bbc8 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -15,7 +15,6 @@ import ( "io" "net/url" "os" - "regexp" "sort" "strings" "time" @@ -40,7 +39,7 @@ import ( "golang.org/x/text/transform" ) -// DiffLineType represents the type of a DiffLine. +// DiffLineType represents the type of DiffLine. type DiffLineType uint8 // DiffLineType possible values. @@ -51,7 +50,7 @@ const ( DiffLineSection ) -// DiffFileType represents the type of a DiffFile. +// DiffFileType represents the type of DiffFile. type DiffFileType uint8 // DiffFileType possible values. @@ -100,12 +99,12 @@ type DiffLineSectionInfo struct { // BlobExcerptChunkSize represent max lines of excerpt const BlobExcerptChunkSize = 20 -// GetType returns the type of a DiffLine. +// GetType returns the type of DiffLine. func (d *DiffLine) GetType() int { return int(d.Type) } -// CanComment returns whether or not a line can get commented +// CanComment returns whether a line can get commented func (d *DiffLine) CanComment() bool { return len(d.Comments) == 0 && d.Type != DiffLineSection } @@ -191,287 +190,13 @@ var ( codeTagSuffix = []byte(``) ) -var ( - unfinishedtagRegex = regexp.MustCompile(`<[^>]*$`) - trailingSpanRegex = regexp.MustCompile(`]?$`) - entityRegex = regexp.MustCompile(`&[#]*?[0-9[:alpha:]]*$`) -) - -// shouldWriteInline represents combinations where we manually write inline changes -func shouldWriteInline(diff diffmatchpatch.Diff, lineType DiffLineType) bool { - if true && - diff.Type == diffmatchpatch.DiffEqual || - diff.Type == diffmatchpatch.DiffInsert && lineType == DiffLineAdd || - diff.Type == diffmatchpatch.DiffDelete && lineType == DiffLineDel { - return true - } - return false -} - -func fixupBrokenSpans(diffs []diffmatchpatch.Diff) []diffmatchpatch.Diff { - // Create a new array to store our fixed up blocks - fixedup := make([]diffmatchpatch.Diff, 0, len(diffs)) - - // semantically label some numbers - const insert, delete, equal = 0, 1, 2 - - // record the positions of the last type of each block in the fixedup blocks - last := []int{-1, -1, -1} - operation := []diffmatchpatch.Operation{diffmatchpatch.DiffInsert, diffmatchpatch.DiffDelete, diffmatchpatch.DiffEqual} - - // create a writer for insert and deletes - toWrite := []strings.Builder{ - {}, - {}, - } - - // make some flags for insert and delete - unfinishedTag := []bool{false, false} - unfinishedEnt := []bool{false, false} - - // store stores the provided text in the writer for the typ - store := func(text string, typ int) { - (&(toWrite[typ])).WriteString(text) - } - - // hasStored returns true if there is stored content - hasStored := func(typ int) bool { - return (&toWrite[typ]).Len() > 0 - } - - // stored will return that content - stored := func(typ int) string { - return (&toWrite[typ]).String() - } - - // empty will empty the stored content - empty := func(typ int) { - (&toWrite[typ]).Reset() - } - - // pop will remove the stored content appending to a diff block for that typ - pop := func(typ int, fixedup []diffmatchpatch.Diff) []diffmatchpatch.Diff { - if hasStored(typ) { - if last[typ] > last[equal] { - fixedup[last[typ]].Text += stored(typ) - } else { - fixedup = append(fixedup, diffmatchpatch.Diff{ - Type: operation[typ], - Text: stored(typ), - }) - } - empty(typ) - } - return fixedup - } - - // Now we walk the provided diffs and check the type of each block in turn - for _, diff := range diffs { - - typ := delete // flag for handling insert or delete typs - switch diff.Type { - case diffmatchpatch.DiffEqual: - // First check if there is anything stored - if hasStored(insert) || hasStored(delete) { - // There are two reasons for storing content: - // 1. Unfinished Entity <- Could be more efficient here by not doing this if we're looking for a tag - if unfinishedEnt[insert] || unfinishedEnt[delete] { - // we look for a ';' to finish an entity - idx := strings.IndexRune(diff.Text, ';') - if idx >= 0 { - // if we find a ';' store the preceding content to both insert and delete - store(diff.Text[:idx+1], insert) - store(diff.Text[:idx+1], delete) - - // and remove it from this block - diff.Text = diff.Text[idx+1:] - - // reset the ent flags - unfinishedEnt[insert] = false - unfinishedEnt[delete] = false - } else { - // otherwise store it all on insert and delete - store(diff.Text, insert) - store(diff.Text, delete) - // and empty this block - diff.Text = "" - } - } - // 2. Unfinished Tag - if unfinishedTag[insert] || unfinishedTag[delete] { - // we look for a '>' to finish a tag - idx := strings.IndexRune(diff.Text, '>') - if idx >= 0 { - store(diff.Text[:idx+1], insert) - store(diff.Text[:idx+1], delete) - diff.Text = diff.Text[idx+1:] - unfinishedTag[insert] = false - unfinishedTag[delete] = false - } else { - store(diff.Text, insert) - store(diff.Text, delete) - diff.Text = "" - } - } - - // If we've completed the required tag/entities - if !(unfinishedTag[insert] || unfinishedTag[delete] || unfinishedEnt[insert] || unfinishedEnt[delete]) { - // pop off the stack - fixedup = pop(insert, fixedup) - fixedup = pop(delete, fixedup) - } - - // If that has left this diff block empty then shortcut - if len(diff.Text) == 0 { - continue - } - } - - // check if this block ends in an unfinished tag? - idx := unfinishedtagRegex.FindStringIndex(diff.Text) - if idx != nil { - unfinishedTag[insert] = true - unfinishedTag[delete] = true - } else { - // otherwise does it end in an unfinished entity? - idx = entityRegex.FindStringIndex(diff.Text) - if idx != nil { - unfinishedEnt[insert] = true - unfinishedEnt[delete] = true - } - } - - // If there is an unfinished component - if idx != nil { - // Store the fragment - store(diff.Text[idx[0]:], insert) - store(diff.Text[idx[0]:], delete) - // and remove it from this block - diff.Text = diff.Text[:idx[0]] - } - - // If that hasn't left the block empty - if len(diff.Text) > 0 { - // store the position of the last equal block and store it in our diffs - last[equal] = len(fixedup) - fixedup = append(fixedup, diff) - } - continue - case diffmatchpatch.DiffInsert: - typ = insert - fallthrough - case diffmatchpatch.DiffDelete: - // First check if there is anything stored for this type - if hasStored(typ) { - // if there is prepend it to this block, empty the storage and reset our flags - diff.Text = stored(typ) + diff.Text - empty(typ) - unfinishedEnt[typ] = false - unfinishedTag[typ] = false - } - - // check if this block ends in an unfinished tag - idx := unfinishedtagRegex.FindStringIndex(diff.Text) - if idx != nil { - unfinishedTag[typ] = true - } else { - // otherwise does it end in an unfinished entity - idx = entityRegex.FindStringIndex(diff.Text) - if idx != nil { - unfinishedEnt[typ] = true - } - } - - // If there is an unfinished component - if idx != nil { - // Store the fragment - store(diff.Text[idx[0]:], typ) - // and remove it from this block - diff.Text = diff.Text[:idx[0]] - } - - // If that hasn't left the block empty - if len(diff.Text) > 0 { - // if the last block of this type was after the last equal block - if last[typ] > last[equal] { - // store this blocks content on that block - fixedup[last[typ]].Text += diff.Text - } else { - // otherwise store the position of the last block of this type and store the block - last[typ] = len(fixedup) - fixedup = append(fixedup, diff) - } - } - continue - } - } - - // pop off any remaining stored content - fixedup = pop(insert, fixedup) - fixedup = pop(delete, fixedup) - - return fixedup -} - -func diffToHTML(fileName string, diffs []diffmatchpatch.Diff, lineType DiffLineType) DiffInline { +func diffToHTML(lineWrapperTags []string, diffs []diffmatchpatch.Diff, lineType DiffLineType) string { buf := bytes.NewBuffer(nil) - match := "" - - diffs = fixupBrokenSpans(diffs) - + // restore the line wrapper tags and , if necessary + for _, tag := range lineWrapperTags { + buf.WriteString(tag) + } for _, diff := range diffs { - if shouldWriteInline(diff, lineType) { - if len(match) > 0 { - diff.Text = match + diff.Text - match = "" - } - // Chroma HTML syntax highlighting is done before diffing individual lines in order to maintain consistency. - // Since inline changes might split in the middle of a chroma span tag or HTML entity, make we manually put it back together - // before writing so we don't try insert added/removed code spans in the middle of one of those - // and create broken HTML. This is done by moving incomplete HTML forward until it no longer matches our pattern of - // a line ending with an incomplete HTML entity or partial/opening . - - // EX: - // diffs[{Type: dmp.DiffDelete, Text: "language}] - - // After first iteration - // diffs[{Type: dmp.DiffDelete, Text: "language"}, //write out - // {Type: dmp.DiffEqual, Text: ",}] - - // After second iteration - // {Type: dmp.DiffEqual, Text: ""}, // write out - // {Type: dmp.DiffDelete, Text: ",}] - - // Final - // {Type: dmp.DiffDelete, Text: ",}] - // end up writing , - // Instead of lass="p", - - m := trailingSpanRegex.FindStringSubmatchIndex(diff.Text) - if m != nil { - match = diff.Text[m[0]:m[1]] - diff.Text = strings.TrimSuffix(diff.Text, match) - } - m = entityRegex.FindStringSubmatchIndex(diff.Text) - if m != nil { - match = diff.Text[m[0]:m[1]] - diff.Text = strings.TrimSuffix(diff.Text, match) - } - // Print an existing closing span first before opening added/remove-code span so it doesn't unintentionally close it - if strings.HasPrefix(diff.Text, "") { - buf.WriteString("") - diff.Text = strings.TrimPrefix(diff.Text, "") - } - // If we weren't able to fix it then this should avoid broken HTML by not inserting more spans below - // The previous/next diff section will contain the rest of the tag that is missing here - if strings.Count(diff.Text, "<") != strings.Count(diff.Text, ">") { - buf.WriteString(diff.Text) - continue - } - } switch { case diff.Type == diffmatchpatch.DiffEqual: buf.WriteString(diff.Text) @@ -485,7 +210,10 @@ func diffToHTML(fileName string, diffs []diffmatchpatch.Diff, lineType DiffLineT buf.Write(codeTagSuffix) } } - return DiffInlineWithUnicodeEscape(template.HTML(buf.String())) + for range lineWrapperTags { + buf.WriteString("") + } + return buf.String() } // GetLine gets a specific line by type (add or del) and file line number @@ -597,10 +325,12 @@ func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine) Dif return DiffInlineWithHighlightCode(diffSection.FileName, language, diffLine.Content) } - diffRecord := diffMatchPatch.DiffMain(highlight.Code(diffSection.FileName, language, diff1[1:]), highlight.Code(diffSection.FileName, language, diff2[1:]), true) - diffRecord = diffMatchPatch.DiffCleanupEfficiency(diffRecord) - - return diffToHTML(diffSection.FileName, diffRecord, diffLine.Type) + hcd := newHighlightCodeDiff() + diffRecord := hcd.diffWithHighlight(diffSection.FileName, language, diff1[1:], diff2[1:]) + // it seems that Gitea doesn't need the line wrapper of Chroma, so do not add them back + // if the line wrappers are still needed in the future, it can be added back by "diffToHTML(hcd.lineWrapperTags. ...)" + diffHTML := diffToHTML(nil, diffRecord, diffLine.Type) + return DiffInlineWithUnicodeEscape(template.HTML(diffHTML)) } // DiffFile represents a file diff. @@ -1289,7 +1019,7 @@ func readFileName(rd *strings.Reader) (string, bool) { if char == '"' { fmt.Fscanf(rd, "%q ", &name) if len(name) == 0 { - log.Error("Reader has no file name: %v", rd) + log.Error("Reader has no file name: reader=%+v", rd) return "", true } @@ -1311,7 +1041,7 @@ func readFileName(rd *strings.Reader) (string, bool) { } } if len(name) < 2 { - log.Error("Unable to determine name from reader: %v", rd) + log.Error("Unable to determine name from reader: reader=%+v", rd) return "", true } return name[2:], ambiguity diff --git a/services/gitdiff/gitdiff_test.go b/services/gitdiff/gitdiff_test.go index caca0e91d8..e88d831759 100644 --- a/services/gitdiff/gitdiff_test.go +++ b/services/gitdiff/gitdiff_test.go @@ -7,7 +7,6 @@ package gitdiff import ( "fmt" - "html/template" "strconv" "strings" "testing" @@ -17,93 +16,27 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/highlight" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/setting" dmp "github.com/sergi/go-diff/diffmatchpatch" "github.com/stretchr/testify/assert" - "gopkg.in/ini.v1" ) -func assertEqual(t *testing.T, s1 string, s2 template.HTML) { - if s1 != string(s2) { - t.Errorf("Did not receive expected results:\nExpected: %s\nActual: %s", s1, s2) - } -} - func TestDiffToHTML(t *testing.T) { - setting.Cfg = ini.Empty() - assertEqual(t, "foo bar biz", diffToHTML("", []dmp.Diff{ + assert.Equal(t, "foo bar biz", diffToHTML(nil, []dmp.Diff{ {Type: dmp.DiffEqual, Text: "foo "}, {Type: dmp.DiffInsert, Text: "bar"}, {Type: dmp.DiffDelete, Text: " baz"}, {Type: dmp.DiffEqual, Text: " biz"}, - }, DiffLineAdd).Content) + }, DiffLineAdd)) - assertEqual(t, "foo bar biz", diffToHTML("", []dmp.Diff{ + assert.Equal(t, "foo bar biz", diffToHTML(nil, []dmp.Diff{ {Type: dmp.DiffEqual, Text: "foo "}, {Type: dmp.DiffDelete, Text: "bar"}, {Type: dmp.DiffInsert, Text: " baz"}, {Type: dmp.DiffEqual, Text: " biz"}, - }, DiffLineDel).Content) - - assertEqual(t, "if !nohl && (lexer != nil || r.GuessLanguage) {", diffToHTML("", []dmp.Diff{ - {Type: dmp.DiffEqual, Text: "if !nohl && (lexer != nil"}, - {Type: dmp.DiffInsert, Text: " || r.GuessLanguage)"}, - {Type: dmp.DiffEqual, Text: " {"}, - }, DiffLineAdd).Content) - - assertEqual(t, "tagURL := fmt.Sprintf("## [%s](%s/%s/%s/%s?q=&type=all&state=closed&milestone=%d) - %s", ge.Milestone\", ge.BaseURL, ge.Owner, ge.Repo, from, milestoneID, time.Now().Format("2006-01-02"))", diffToHTML("", []dmp.Diff{ - {Type: dmp.DiffEqual, Text: "tagURL := fmt.Sprintf("## [%s](%s/%s/%s/%s?q=&type=all&state=closed&milestone=%d) - %s", ge.Milestone\""}, - {Type: dmp.DiffInsert, Text: "f\">getGiteaTagURL(client"}, - {Type: dmp.DiffEqual, Text: ", ge.BaseURL, ge.Owner, ge.Repo, "}, - {Type: dmp.DiffDelete, Text: "from, milestoneID, time.Now().Format("2006-01-02")"}, - {Type: dmp.DiffInsert, Text: "ge.Milestone, from, milestoneID"}, - {Type: dmp.DiffEqual, Text: ")"}, - }, DiffLineDel).Content) - - assertEqual(t, "r.WrapperRenderer(w, language, true, attrs, false)", diffToHTML("", []dmp.Diff{ - {Type: dmp.DiffEqual, Text: "r.WrapperRenderer(w, "}, - {Type: dmp.DiffDelete, Text: "language, true, attrs"}, - {Type: dmp.DiffEqual, Text: ", false)"}, - }, DiffLineDel).Content) - - assertEqual(t, "language, true, attrs, false)", diffToHTML("", []dmp.Diff{ - {Type: dmp.DiffInsert, Text: "language, true, attrs"}, - {Type: dmp.DiffEqual, Text: ", false)"}, - }, DiffLineAdd).Content) - - assertEqual(t, "print("// ", sys.argv)", diffToHTML("", []dmp.Diff{ - {Type: dmp.DiffEqual, Text: "print"}, - {Type: dmp.DiffInsert, Text: "("}, - {Type: dmp.DiffEqual, Text: ""// ", sys.argv"}, - {Type: dmp.DiffInsert, Text: ")"}, - }, DiffLineAdd).Content) - - assertEqual(t, "sh 'useradd -u $(stat -c "%u" .gitignore) jenkins'", diffToHTML("", []dmp.Diff{ - {Type: dmp.DiffEqual, Text: "sh "}, - {Type: dmp.DiffDelete, Text: "4;useradd -u 111 jenkins""}, - {Type: dmp.DiffInsert, Text: "9;useradd -u $(stat -c "%u" .gitignore) jenkins'"}, - {Type: dmp.DiffEqual, Text: ";"}, - }, DiffLineAdd).Content) - - assertEqual(t, " <h4 class="release-list-title df ac">", diffToHTML("", []dmp.Diff{ - {Type: dmp.DiffEqual, Text: " <h"}, - {Type: dmp.DiffInsert, Text: "4 class=&#"}, - {Type: dmp.DiffEqual, Text: "3"}, - {Type: dmp.DiffInsert, Text: "4;release-list-title df ac""}, - {Type: dmp.DiffEqual, Text: ">"}, - }, DiffLineAdd).Content) + }, DiffLineDel)) } func TestParsePatch_skipTo(t *testing.T) { @@ -592,7 +525,6 @@ index 0000000..6bb8f39 if err != nil { t.Errorf("ParsePatch failed: %s", err) } - println(result) diff2 := `diff --git "a/A \\ B" "b/A \\ B" --- "a/A \\ B" @@ -712,18 +644,6 @@ func TestGetDiffRangeWithWhitespaceBehavior(t *testing.T) { } } -func TestDiffToHTML_14231(t *testing.T) { - setting.Cfg = ini.Empty() - diffRecord := diffMatchPatch.DiffMain(highlight.Code("main.v", "", " run()\n"), highlight.Code("main.v", "", " run(db)\n"), true) - diffRecord = diffMatchPatch.DiffCleanupEfficiency(diffRecord) - - expected := ` run(db) -` - output := diffToHTML("main.v", diffRecord, DiffLineAdd) - - assertEqual(t, expected, output.Content) -} - func TestNoCrashes(t *testing.T) { type testcase struct { gitdiff string diff --git a/services/gitdiff/highlightdiff.go b/services/gitdiff/highlightdiff.go new file mode 100644 index 0000000000..4ceada4d7e --- /dev/null +++ b/services/gitdiff/highlightdiff.go @@ -0,0 +1,223 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package gitdiff + +import ( + "strings" + + "code.gitea.io/gitea/modules/highlight" + + "github.com/sergi/go-diff/diffmatchpatch" +) + +// token is a html tag or entity, eg: "", "", "<" +func extractHTMLToken(s string) (before, token, after string, valid bool) { + for pos1 := 0; pos1 < len(s); pos1++ { + if s[pos1] == '<' { + pos2 := strings.IndexByte(s[pos1:], '>') + if pos2 == -1 { + return "", "", s, false + } + return s[:pos1], s[pos1 : pos1+pos2+1], s[pos1+pos2+1:], true + } else if s[pos1] == '&' { + pos2 := strings.IndexByte(s[pos1:], ';') + if pos2 == -1 { + return "", "", s, false + } + return s[:pos1], s[pos1 : pos1+pos2+1], s[pos1+pos2+1:], true + } + } + return "", "", s, true +} + +// highlightCodeDiff is used to do diff with highlighted HTML code. +// It totally depends on Chroma's valid HTML output and its structure, do not use these functions for other purposes. +// The HTML tags and entities will be replaced by Unicode placeholders: "{TEXT}" => "\uE000{TEXT}\uE001" +// These Unicode placeholders are friendly to the diff. +// Then after diff, the placeholders in diff result will be recovered to the HTML tags and entities. +// It's guaranteed that the tags in final diff result are paired correctly. +type highlightCodeDiff struct { + placeholderBegin rune + placeholderMaxCount int + placeholderIndex int + placeholderTokenMap map[rune]string + tokenPlaceholderMap map[string]rune + + placeholderOverflowCount int + + lineWrapperTags []string +} + +func newHighlightCodeDiff() *highlightCodeDiff { + return &highlightCodeDiff{ + placeholderBegin: rune(0x100000), // Plane 16: Supplementary Private Use Area B (U+100000..U+10FFFD) + placeholderMaxCount: 64000, + placeholderTokenMap: map[rune]string{}, + tokenPlaceholderMap: map[string]rune{}, + } +} + +// nextPlaceholder returns 0 if no more placeholder can be used +// the diff is done line by line, usually there are only a few (no more than 10) placeholders in one line +// so the placeholderMaxCount is impossible to be exhausted in real cases. +func (hcd *highlightCodeDiff) nextPlaceholder() rune { + for hcd.placeholderIndex < hcd.placeholderMaxCount { + r := hcd.placeholderBegin + rune(hcd.placeholderIndex) + hcd.placeholderIndex++ + // only use non-existing (not used by code) rune as placeholders + if _, ok := hcd.placeholderTokenMap[r]; !ok { + return r + } + } + return 0 // no more available placeholder +} + +func (hcd *highlightCodeDiff) isInPlaceholderRange(r rune) bool { + return hcd.placeholderBegin <= r && r < hcd.placeholderBegin+rune(hcd.placeholderMaxCount) +} + +func (hcd *highlightCodeDiff) collectUsedRunes(code string) { + for _, r := range code { + if hcd.isInPlaceholderRange(r) { + // put the existing rune (used by code) in map, then this rune won't be used a placeholder anymore. + hcd.placeholderTokenMap[r] = "" + } + } +} + +func (hcd *highlightCodeDiff) diffWithHighlight(filename, language, codeA, codeB string) []diffmatchpatch.Diff { + hcd.collectUsedRunes(codeA) + hcd.collectUsedRunes(codeB) + + highlightCodeA := highlight.Code(filename, language, codeA) + highlightCodeB := highlight.Code(filename, language, codeB) + + highlightCodeA = hcd.convertToPlaceholders(highlightCodeA) + highlightCodeB = hcd.convertToPlaceholders(highlightCodeB) + + diffs := diffMatchPatch.DiffMain(highlightCodeA, highlightCodeB, true) + diffs = diffMatchPatch.DiffCleanupEfficiency(diffs) + + for i := range diffs { + hcd.recoverOneDiff(&diffs[i]) + } + return diffs +} + +// convertToPlaceholders totally depends on Chroma's valid HTML output and its structure, do not use these functions for other purposes. +func (hcd *highlightCodeDiff) convertToPlaceholders(htmlCode string) string { + var tagStack []string + res := strings.Builder{} + + firstRunForLineTags := hcd.lineWrapperTags == nil + + var beforeToken, token string + var valid bool + + // the standard chroma highlight HTML is " ... " + for { + beforeToken, token, htmlCode, valid = extractHTMLToken(htmlCode) + if !valid || token == "" { + break + } + // write the content before the token into result string, and consume the token in the string + res.WriteString(beforeToken) + + // the line wrapper tags should be removed before diff + if strings.HasPrefix(token, `") + continue + } + + var tokenInMap string + if strings.HasSuffix(token, "" for "" + tokenInMap = token + "" + tagStack = tagStack[:len(tagStack)-1] + } else if token[0] == '<' { // for opening tag + tokenInMap = token + tagStack = append(tagStack, token) + } else if token[0] == '&' { // for html entity + tokenInMap = token + } // else: impossible + + // remember the placeholder and token in the map + placeholder, ok := hcd.tokenPlaceholderMap[tokenInMap] + if !ok { + placeholder = hcd.nextPlaceholder() + if placeholder != 0 { + hcd.tokenPlaceholderMap[tokenInMap] = placeholder + hcd.placeholderTokenMap[placeholder] = tokenInMap + } + } + + if placeholder != 0 { + res.WriteRune(placeholder) // use the placeholder to replace the token + } else { + // unfortunately, all private use runes has been exhausted, no more placeholder could be used, no more converting + // usually, the exhausting won't occur in real cases, the magnitude of used placeholders is not larger than that of the CSS classes outputted by chroma. + hcd.placeholderOverflowCount++ + if strings.HasPrefix(token, "&") { + // when the token is a html entity, something must be outputted even if there is no placeholder. + res.WriteRune(0xFFFD) // replacement character TODO: how to handle this case more gracefully? + res.WriteString(token[1:]) // still output the entity code part, otherwise there will be no diff result. + } + } + } + + // write the remaining string + res.WriteString(htmlCode) + return res.String() +} + +func (hcd *highlightCodeDiff) recoverOneDiff(diff *diffmatchpatch.Diff) { + sb := strings.Builder{} + var tagStack []string + + for _, r := range diff.Text { + token, ok := hcd.placeholderTokenMap[r] + if !ok || token == "" { + sb.WriteRune(r) // if the rune is not a placeholder, write it as it is + continue + } + var tokenToRecover string + if strings.HasPrefix(token, "')+1] + if len(tagStack) == 0 { + continue // if no opening tag in stack yet, skip the closing tag + } + tagStack = tagStack[:len(tagStack)-1] + } else if token[0] == '<' { // for opening tag + tokenToRecover = token + tagStack = append(tagStack, token) + } else if token[0] == '&' { // for html entity + tokenToRecover = token + } // else: impossible + sb.WriteString(tokenToRecover) + } + + if len(tagStack) > 0 { + // close all opening tags + for i := len(tagStack) - 1; i >= 0; i-- { + tagToClose := tagStack[i] + // get the closing tag "" from "" or "" + pos := strings.IndexAny(tagToClose, " >") + if pos != -1 { + sb.WriteString("") + } // else: impossible. every tag was pushed into the stack by the code above and is valid HTML opening tag + } + } + + diff.Text = sb.String() +} diff --git a/services/gitdiff/highlightdiff_test.go b/services/gitdiff/highlightdiff_test.go new file mode 100644 index 0000000000..1cd78bc942 --- /dev/null +++ b/services/gitdiff/highlightdiff_test.go @@ -0,0 +1,126 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package gitdiff + +import ( + "fmt" + "strings" + "testing" + + "github.com/sergi/go-diff/diffmatchpatch" + "github.com/stretchr/testify/assert" +) + +func TestDiffWithHighlight(t *testing.T) { + hcd := newHighlightCodeDiff() + diffs := hcd.diffWithHighlight( + "main.v", "", + " run('<>')\n", + " run(db)\n", + ) + + expected := ` run('<>')` + "\n" + output := diffToHTML(nil, diffs, DiffLineDel) + assert.Equal(t, expected, output) + + expected = ` run(db)` + "\n" + output = diffToHTML(nil, diffs, DiffLineAdd) + assert.Equal(t, expected, output) + + hcd = newHighlightCodeDiff() + hcd.placeholderTokenMap['O'] = "" + hcd.placeholderTokenMap['C'] = "" + diff := diffmatchpatch.Diff{} + + diff.Text = "OC" + hcd.recoverOneDiff(&diff) + assert.Equal(t, "", diff.Text) + + diff.Text = "O" + hcd.recoverOneDiff(&diff) + assert.Equal(t, "", diff.Text) + + diff.Text = "C" + hcd.recoverOneDiff(&diff) + assert.Equal(t, "", diff.Text) +} + +func TestDiffWithHighlightPlaceholder(t *testing.T) { + hcd := newHighlightCodeDiff() + diffs := hcd.diffWithHighlight( + "main.js", "", + "a='\U00100000'", + "a='\U0010FFFD''", + ) + assert.Equal(t, "", hcd.placeholderTokenMap[0x00100000]) + assert.Equal(t, "", hcd.placeholderTokenMap[0x0010FFFD]) + + expected := fmt.Sprintf(`a='%s'`, "\U00100000") + output := diffToHTML(hcd.lineWrapperTags, diffs, DiffLineDel) + assert.Equal(t, expected, output) + + hcd = newHighlightCodeDiff() + diffs = hcd.diffWithHighlight( + "main.js", "", + "a='\U00100000'", + "a='\U0010FFFD'", + ) + expected = fmt.Sprintf(`a='%s'`, "\U0010FFFD") + output = diffToHTML(nil, diffs, DiffLineAdd) + assert.Equal(t, expected, output) +} + +func TestDiffWithHighlightPlaceholderExhausted(t *testing.T) { + hcd := newHighlightCodeDiff() + hcd.placeholderMaxCount = 0 + diffs := hcd.diffWithHighlight( + "main.js", "", + "'", + ``, + ) + output := diffToHTML(nil, diffs, DiffLineDel) + expected := fmt.Sprintf(`%s#39;`, "\uFFFD") + assert.Equal(t, expected, output) + + hcd = newHighlightCodeDiff() + hcd.placeholderMaxCount = 0 + diffs = hcd.diffWithHighlight( + "main.js", "", + "a < b", + "a > b", + ) + output = diffToHTML(nil, diffs, DiffLineDel) + expected = fmt.Sprintf(`a %slt; b`, "\uFFFD") + assert.Equal(t, expected, output) + + output = diffToHTML(nil, diffs, DiffLineAdd) + expected = fmt.Sprintf(`a %sgt; b`, "\uFFFD") + assert.Equal(t, expected, output) +} + +func TestDiffWithHighlightTagMatch(t *testing.T) { + totalOverflow := 0 + for i := 0; i < 100; i++ { + hcd := newHighlightCodeDiff() + hcd.placeholderMaxCount = i + diffs := hcd.diffWithHighlight( + "main.js", "", + "a='1'", + "b='2'", + ) + totalOverflow += hcd.placeholderOverflowCount + + output := diffToHTML(nil, diffs, DiffLineDel) + c1 := strings.Count(output, " - + {{svg "octicon-bell"}} diff --git a/templates/org/member/members.tmpl b/templates/org/member/members.tmpl index 9a2c235aa6..b558dbe5ee 100644 --- a/templates/org/member/members.tmpl +++ b/templates/org/member/members.tmpl @@ -29,7 +29,7 @@ {{end}} - {{if not .PublicOnly}} + {{if not $.PublicOnly}}
{{$.locale.Tr "org.members.member_role"}} diff --git a/templates/package/shared/list.tmpl b/templates/package/shared/list.tmpl index 3b3a1720ea..189b75b59e 100644 --- a/templates/package/shared/list.tmpl +++ b/templates/package/shared/list.tmpl @@ -47,6 +47,10 @@
{{svg "octicon-package" 32}}

{{.locale.Tr "packages.empty"}}

+ {{if and .Repository .CanWritePackages}} + {{$packagesUrl := URLJoin .Owner.HTMLURL "-" "packages" }} +

{{.locale.Tr "packages.empty.repo" $packagesUrl | Safe}}

+ {{end}}

{{.locale.Tr "packages.empty.documentation" | Safe}}

{{else}} diff --git a/templates/repo/clone_buttons.tmpl b/templates/repo/clone_buttons.tmpl index 3d74e5dc28..fb54b27c82 100644 --- a/templates/repo/clone_buttons.tmpl +++ b/templates/repo/clone_buttons.tmpl @@ -19,6 +19,6 @@ document.getElementById('repo-clone-url').value = btn ? btn.getAttribute('data-link') : ''; })(); - diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index 852c87f2f3..e284d5aef5 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -484,7 +484,7 @@
-
+
{{if .RenderedContent}} {{.RenderedContent|Str2html}} {{else}} diff --git a/templates/repo/view_list.tmpl b/templates/repo/view_list.tmpl index 664dfaf9b9..43441b56c1 100644 --- a/templates/repo/view_list.tmpl +++ b/templates/repo/view_list.tmpl @@ -8,9 +8,9 @@ {{if .LatestCommitUser}} {{avatar .LatestCommitUser 24}} {{if .LatestCommitUser.FullName}} - {{.LatestCommitUser.FullName}} + {{.LatestCommitUser.FullName}} {{else}} - {{if .LatestCommit.Author}}{{.LatestCommit.Author.Name}}{{else}}{{.LatestCommitUser.Name}}{{end}} + {{if .LatestCommit.Author}}{{.LatestCommit.Author.Name}}{{else}}{{.LatestCommitUser.Name}}{{end}} {{end}} {{else}} {{if .LatestCommit.Author}} @@ -54,7 +54,7 @@ {{svg "octicon-file-submodule"}} {{$refURL := $subModuleFile.RefURL AppUrl $.Repository.FullName $.SSHDomain}} {{if $refURL}} - {{$entry.Name}}@{{ShortSha $subModuleFile.RefID}} + {{$entry.Name}}@{{ShortSha $subModuleFile.RefID}} {{else}} {{$entry.Name}}@{{ShortSha $subModuleFile.RefID}} {{end}} @@ -63,16 +63,16 @@ {{$subJumpablePathName := $entry.GetSubJumpablePathName}} {{$subJumpablePath := SubJumpablePath $subJumpablePathName}} {{svg "octicon-file-directory-fill"}} - + {{if eq (len $subJumpablePath) 2}} - {{index $subJumpablePath 0}}{{index $subJumpablePath 1}} + {{index $subJumpablePath 0}}{{index $subJumpablePath 1}} {{else}} {{index $subJumpablePath 0}} {{end}} {{else}} {{svg (printf "octicon-%s" (EntryIcon $entry))}} - {{$entry.Name}} + {{$entry.Name}} {{end}} {{end}} diff --git a/templates/shared/issuelist.tmpl b/templates/shared/issuelist.tmpl index 7d0abb689b..d1555c16c5 100644 --- a/templates/shared/issuelist.tmpl +++ b/templates/shared/issuelist.tmpl @@ -89,6 +89,11 @@ {{svg "octicon-milestone" 14 "mr-2"}}{{.Milestone.Name}} {{end}} + {{if .Project}} + + {{svg "octicon-project" 14 "mr-2"}}{{.Project.Title}} + + {{end}} {{if .Ref}} {{svg "octicon-git-branch" 14 "mr-2"}}{{index $.IssueRefEndNames .ID}} diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js index 419c5996dc..3ce74442a0 100644 --- a/web_src/js/features/common-global.js +++ b/web_src/js/features/common-global.js @@ -389,7 +389,8 @@ export function initGlobalButtons() { */ export function checkAppUrl() { const curUrl = window.location.href; - if (curUrl.startsWith(appUrl)) { + // some users visit "https://domain/gitea" while appUrl is "https://domain/gitea/", there should be no warning + if (curUrl.startsWith(appUrl) || `${curUrl}/` === appUrl) { return; } if (document.querySelector('.page-content.install')) { diff --git a/web_src/js/features/stopwatch.js b/web_src/js/features/stopwatch.js index c47ba22124..d63da4155a 100644 --- a/web_src/js/features/stopwatch.js +++ b/web_src/js/features/stopwatch.js @@ -140,7 +140,7 @@ function updateStopwatchData(data) { $('.stopwatch-cancel').attr('action', `${issueUrl}/times/stopwatch/cancel`); $('.stopwatch-issue').text(`${repo_owner_name}/${repo_name}#${issue_index}`); $('.stopwatch-time').text(prettyMilliseconds(seconds * 1000)); - updateStopwatchTime(seconds); + updateTimeInterval = updateStopwatchTime(seconds); btnEl.removeClass('hidden'); } @@ -149,10 +149,10 @@ function updateStopwatchData(data) { function updateStopwatchTime(seconds) { const secs = parseInt(seconds); - if (!Number.isFinite(secs)) return; + if (!Number.isFinite(secs)) return null; const start = Date.now(); - updateTimeInterval = setInterval(() => { + return setInterval(() => { const delta = Date.now() - start; const dur = prettyMilliseconds(secs * 1000 + delta, {compact: true}); $('.stopwatch-time').text(dur); diff --git a/web_src/js/utils.js b/web_src/js/utils.js index f01f2d3b22..e9cd39032d 100644 --- a/web_src/js/utils.js +++ b/web_src/js/utils.js @@ -64,7 +64,7 @@ export function parseIssueHref(href) { export function strSubMatch(full, sub) { const res = ['']; let i = 0, j = 0; - for (; i < sub.length && j < full.length;) { + while (i < sub.length && j < full.length) { while (j < full.length) { if (sub[i] === full[j]) { if (res.length % 2 !== 0) res.push(''); diff --git a/web_src/less/_base.less b/web_src/less/_base.less index f6035b1a8e..eb55ca8da8 100644 --- a/web_src/less/_base.less +++ b/web_src/less/_base.less @@ -118,6 +118,7 @@ --color-text-dark: #080808; --color-text: #212121; --color-text-light: #555555; + --color-text-light-1: #6a6a6a; --color-text-light-2: #808080; --color-text-light-3: #a0a0a0; --color-box-header: #f7f7f7; @@ -275,6 +276,7 @@ a.muted { a:hover, a.muted:hover, +a.muted:hover [class*="color-text"], .ui.breadcrumb a:hover { color: var(--color-primary); } @@ -2206,3 +2208,7 @@ table th[data-sortt-desc] { } } } + +.color-text-light-2 { + color: var(--color-text-light-2); +} diff --git a/web_src/less/_repository.less b/web_src/less/_repository.less index d5942bdfcb..5aed4dcf72 100644 --- a/web_src/less/_repository.less +++ b/web_src/less/_repository.less @@ -367,6 +367,8 @@ } &.message { + color: var(--color-text-light-1); + @media @mediaXl { max-width: 400px; } @@ -381,6 +383,7 @@ &.age { width: 120px; + color: var(--color-text-light-1); } .truncate { @@ -432,10 +435,6 @@ padding-bottom: 8px; width: calc(100% - 1.25rem); } - - .jumpable-path { - color: var(--color-text-light-2); - } } .non-diff-file-content { diff --git a/web_src/less/shared/issuelist.less b/web_src/less/shared/issuelist.less index 775fc98478..a0331a1cbe 100644 --- a/web_src/less/shared/issuelist.less +++ b/web_src/less/shared/issuelist.less @@ -101,7 +101,8 @@ padding-left: 5px; } - a.milestone { + a.milestone, + a.project { margin-left: 5px; } diff --git a/web_src/less/themes/theme-arc-green.less b/web_src/less/themes/theme-arc-green.less index e510866a90..cf63580911 100644 --- a/web_src/less/themes/theme-arc-green.less +++ b/web_src/less/themes/theme-arc-green.less @@ -98,6 +98,7 @@ --color-text-dark: #dbe0ea; --color-text: #bbc0ca; --color-text-light: #a6aab5; + --color-text-light-1: #979ba6; --color-text-light-2: #8a8e99; --color-text-light-3: #707687; --color-footer: #2e323e;