From 7690de56f7bdcc5065af2c9478e8572d318a84f3 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Thu, 21 Jul 2022 12:41:50 +0200 Subject: [PATCH 01/16] Simplify visibility checks (#20406) Was looking into the visibility checks because I need them for something different and noticed the checks are more complicated than they have to be. The rule is just: user/org is visible if - The doer is a member of the org, regardless of the org visibility - The doer is not restricted and the user/org is public or limited --- models/user/search.go | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/models/user/search.go b/models/user/search.go index 1b65dcb12d46..76ff55ea2664 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 From 0a97480934fc2082c9cff18a5b66cedc12575919 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Thu, 21 Jul 2022 21:18:41 +0200 Subject: [PATCH 02/16] Add "X-Gitea-Object-Type" header for GET `/raw/` & `/media/` API (#20438) --- integrations/api_repo_raw_test.go | 8 ++++++-- routers/api/v1/repo/file.go | 14 ++++++++++---- services/repository/files/content.go | 16 ++++++++++++++++ 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/integrations/api_repo_raw_test.go b/integrations/api_repo_raw_test.go index 2a77d1ba630d..258b409befb8 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/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index 1ac108883923..ba8a938b8308 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -33,6 +33,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 +74,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 +123,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 +224,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) { @@ -251,7 +257,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/services/repository/files/content.go b/services/repository/files/content.go index 2237671a60cb..c2069092899e 100644 --- a/services/repository/files/content.go +++ b/services/repository/files/content.go @@ -101,6 +101,22 @@ func GetContentsOrList(ctx context.Context, repo *repo_model.Repository, treePat return fileList, nil } +// GetObjectTypeFromTreeEntry check what content is behind it +func GetObjectTypeFromTreeEntry(entry *git.TreeEntry) ContentType { + switch { + case entry.IsDir(): + return ContentTypeDir + case entry.IsSubModule(): + return ContentTypeSubmodule + case entry.IsExecutable(), entry.IsRegular(): + return ContentTypeRegular + case entry.IsLink(): + return ContentTypeLink + default: + return "" + } +} + // GetContents gets the meta data on a file's contents. Ref can be a branch, commit or tag func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref string, forList bool) (*api.ContentsResponse, error) { if ref == "" { From 3df33799c1b8775b4110ed11171a37f6c5ef4ae2 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Fri, 22 Jul 2022 03:01:22 +0530 Subject: [PATCH 03/16] Fix: Actor is required to get user repositories (#20443) Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: Andrew Thornton --- models/repo/repo_list.go | 4 ++++ services/user/user.go | 1 + services/user/user_test.go | 20 ++++++++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go index a70fc8efd409..9de76fa5ffa1 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/services/user/user.go b/services/user/user.go index 448b7c2daf8d..1edd9294cbfb 100644 --- a/services/user/user.go +++ b/services/user/user.go @@ -75,6 +75,7 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error { }, Private: true, OwnerID: u.ID, + Actor: u, }) if err != nil { return fmt.Errorf("SearchRepositoryByName: %v", err) diff --git a/services/user/user_test.go b/services/user/user_test.go index aefbcd9ecb49..d8673593df22 100644 --- a/services/user/user_test.go +++ b/services/user/user_test.go @@ -60,6 +60,26 @@ func TestDeleteUser(t *testing.T) { assert.Error(t, DeleteUser(db.DefaultContext, org, false)) } +func TestPurgeUser(t *testing.T) { + test := func(userID int64) { + assert.NoError(t, unittest.PrepareTestDatabase()) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID}).(*user_model.User) + + err := DeleteUser(db.DefaultContext, user, true) + assert.NoError(t, err) + + unittest.AssertNotExistsBean(t, &user_model.User{ID: userID}) + unittest.CheckConsistencyFor(t, &user_model.User{}, &repo_model.Repository{}) + } + test(2) + test(4) + test(8) + test(11) + + org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}).(*user_model.User) + assert.Error(t, DeleteUser(db.DefaultContext, org, false)) +} + func TestCreateUser(t *testing.T) { user := &user_model.User{ Name: "GiteaBot", From bc17cba83587f2b4015f0df25925817abef5c082 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 22 Jul 2022 03:10:22 +0200 Subject: [PATCH 04/16] Add eslint-plugin-sonarjs (#20431) We had this plugin before but it was removed as it became outdated, now it was updated again, so it's compatible again. Co-authored-by: wxiaoguang Co-authored-by: 6543 <6543@obermui.de> --- .eslintrc.yaml | 33 ++++++++++++++++++++++++++++++++ package-lock.json | 20 +++++++++++++++++++ package.json | 1 + web_src/js/features/stopwatch.js | 6 +++--- web_src/js/utils.js | 2 +- 5 files changed, 58 insertions(+), 4 deletions(-) diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 90bb3c9c5eba..86f886b4b4d5 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/package-lock.json b/package-lock.json index d2aa3dbe1b2e..cfd2a6ad2a01 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 c1ecd163432a..f4752aeec95f 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/web_src/js/features/stopwatch.js b/web_src/js/features/stopwatch.js index c47ba221241b..d63da4155af2 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 f01f2d3b2244..e9cd39032df8 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(''); From 339007bff07a4b8566a5b5d9dad3cad908a02f7c Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 22 Jul 2022 04:22:44 +0200 Subject: [PATCH 05/16] Downgrade golangci-lint to 1.47.0 (#20445) This should fix some recently seen linter performance issues. There is some log spam, but it's definitely faster. Ref: https://github.com/golangci/golangci-lint/issues/2997 Co-authored-by: Lunny Xiao --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index d4cc71700df1..5cd9bc25b5c1 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 From 1a70fc9bc6668e07a19dd6715fbfe0f24b5b6db1 Mon Sep 17 00:00:00 2001 From: Andrew Imeson Date: Fri, 22 Jul 2022 06:12:27 -0400 Subject: [PATCH 06/16] Correct code block in installation docs for Snap (#20440) Without this, it was rendering on the site like: "sh snap install gitea", instead of: "snap install gitea" Co-authored-by: wxiaoguang --- docs/content/doc/installation/from-package.en-us.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/doc/installation/from-package.en-us.md b/docs/content/doc/installation/from-package.en-us.md index e4081024bd0e..56ca97a8a5f7 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 From 599ae09a94e40f9ef567aa2ec7486f68a64a3384 Mon Sep 17 00:00:00 2001 From: Lucas Azevedo Date: Fri, 22 Jul 2022 07:49:24 -0300 Subject: [PATCH 07/16] Use body text color in repository files table links (#20386) Use body text color in for links in the repository files table Issue/PR links (`.ref-issue`) will not be affected, as seen in other git services. Co-authored-by: silverwind Co-authored-by: wxiaoguang Co-authored-by: Lauris BH --- modules/markup/html.go | 2 +- templates/repo/view_list.tmpl | 12 ++++++------ web_src/less/_base.less | 6 ++++++ web_src/less/_repository.less | 7 +++---- web_src/less/themes/theme-arc-green.less | 1 + 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/modules/markup/html.go b/modules/markup/html.go index 6071180501c4..a5606dbb516a 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/templates/repo/view_list.tmpl b/templates/repo/view_list.tmpl index 664dfaf9b9a2..43441b56c128 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/web_src/less/_base.less b/web_src/less/_base.less index f6035b1a8e35..eb55ca8da8d6 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 d5942bdfcb58..5aed4dcf7235 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/themes/theme-arc-green.less b/web_src/less/themes/theme-arc-green.less index e510866a9090..cf63580911f1 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; From 4d22bda4db4c29e61ff4ac7a53ab25e7ff8a55a3 Mon Sep 17 00:00:00 2001 From: Gergely Nagy Date: Fri, 22 Jul 2022 23:54:02 +0200 Subject: [PATCH 08/16] Allow non-semver packages in the Conan package registry (#20412) A lot of existing packages do not conform to SemVer, yet, they should be allowed in the Conan package registry as-is. To achieve this, remove the SemVer check from `NewRecipeReference`, and replace it with a simple empty string check. A unit test with a non-semver version is also included. Fixes #20405. Signed-off-by: Gergely Nagy Co-authored-by: KN4CK3R --- modules/packages/conan/reference.go | 9 +++++---- modules/packages/conan/reference_test.go | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/modules/packages/conan/reference.go b/modules/packages/conan/reference.go index c43446e6e5bd..49236981b67f 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 29ba3a543bf2..98eb2c847843 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}, } From d9608c4e76401ffdd5958e1944ba0a17ded825b9 Mon Sep 17 00:00:00 2001 From: Gergely Nagy Date: Sat, 23 Jul 2022 00:20:56 +0000 Subject: [PATCH 09/16] [skip ci] Updated translations via Crowdin --- options/locale/locale_el-GR.ini | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini index 963484a7f9c7..51bd62d91f32 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,6 +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=τροποποίησε την ημερομηνία παράδοσης από %[2]s σε %[1]s %[3]s issues.due_date_remove=αφαίρεσε την ημερομηνία παράδοσης %s %s issues.due_date_overdue=Εκπρόθεσμο issues.due_date_invalid=Η ημερομηνία παράδοσης δεν είναι έγκυρη ή εκτός εύρους. Παρακαλούμε χρησιμοποιήστε τη μορφή 'εεεε-μμ-ηη'. From 14178c56bb0fbc8b0b5820539e7681a7defeff47 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 23 Jul 2022 08:38:03 +0200 Subject: [PATCH 10/16] Add Cache-Control header to html and api responses, add no-transform (#20432) `no-transform` allegedly disables CloudFlare auto-minify and we did not set caching headers on html or api requests, which seems good to have regardless. Transformation is still allowed for asset requests. Signed-off-by: Andrew Thornton Co-authored-by: wxiaoguang Co-authored-by: Andrew Thornton --- modules/context/api.go | 2 ++ modules/context/context.go | 2 ++ modules/httpcache/httpcache.go | 17 ++++++++++++----- routers/install/routes.go | 2 ++ routers/web/base.go | 1 + 5 files changed, 19 insertions(+), 5 deletions(-) diff --git a/modules/context/api.go b/modules/context/api.go index 558a9f51ee34..b9d130e2a8ac 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 68f8a1b408c1..882491161992 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/httpcache/httpcache.go b/modules/httpcache/httpcache.go index 5797e981cf80..750233d4a71c 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/routers/install/routes.go b/routers/install/routes.go index 32829ede9e26..fdabcb9dc22c 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/base.go b/routers/web/base.go index c7ade55a61f6..30a24a127543 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 { From 3310dd1d197495fbfa2d7ee490e19e6d08e1d30f Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 23 Jul 2022 19:28:02 +0800 Subject: [PATCH 11/16] Improve code diff highlight, fix incorrect rendered diff result (#19958) Use Unicode placeholders to replace HTML tags and HTML entities first, then do diff, then recover the HTML tags and HTML entities. Now the code diff with highlight has stable behavior, and won't emit broken tags. --- modules/highlight/highlight.go | 8 +- services/gitdiff/gitdiff.go | 312 ++----------------------- services/gitdiff/gitdiff_test.go | 88 +------ services/gitdiff/highlightdiff.go | 223 ++++++++++++++++++ services/gitdiff/highlightdiff_test.go | 126 ++++++++++ 5 files changed, 379 insertions(+), 378 deletions(-) create mode 100644 services/gitdiff/highlightdiff.go create mode 100644 services/gitdiff/highlightdiff_test.go diff --git a/modules/highlight/highlight.go b/modules/highlight/highlight.go index acd3bebb9f40..6832207c0fc7 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/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index 6e8c149dabd9..6dd237bbc8d4 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 caca0e91d8f1..e88d831759b7 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 000000000000..4ceada4d7ec9 --- /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 000000000000..1cd78bc94272 --- /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, " Date: Sun, 24 Jul 2022 01:33:55 +0800 Subject: [PATCH 12/16] Improve pprof doc (#20463) --- cmd/web.go | 3 ++- docs/content/doc/advanced/config-cheat-sheet.en-us.md | 2 +- docs/content/doc/help/seek-help.en-us.md | 7 ++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/cmd/web.go b/cmd/web.go index 43bb0ada911e..3bc61b04433f 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 a0e6fb8f13b1..4df104419af3 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/seek-help.en-us.md b/docs/content/doc/help/seek-help.en-us.md index 3ee160f4316f..f1a93eacea80 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 From 9cf0352f149b70091515d33614dca28db3113a98 Mon Sep 17 00:00:00 2001 From: Gusted Date: Sun, 24 Jul 2022 05:45:33 +0200 Subject: [PATCH 13/16] Prepend commit message to template content (#20429) - When a repository has a pull request template, it will always override the current content. With this PR it will prepend content to the template content when appropriate. This is similar how GitHub(and GitLab I presume) does it and it saves developers time to not go open their commit and copy paste their will written commit message. --- routers/web/repo/compare.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 5c46882f3d26..8ed794b45c7d 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") From 16edee85bd36c12967a18072a03539577363eeaf Mon Sep 17 00:00:00 2001 From: Tyrone Yeh Date: Mon, 25 Jul 2022 00:53:40 +0800 Subject: [PATCH 14/16] Add repository condition for issue count (#20454) * Add repository condition for issue count * Update routers/web/user/home.go Co-authored-by: Gusted Co-authored-by: Gusted Co-authored-by: Lunny Xiao --- routers/web/user/home.go | 1 + 1 file changed, 1 insertion(+) diff --git a/routers/web/user/home.go b/routers/web/user/home.go index 698117e9571f..648269980472 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) From 7205f6b6a342b1fcca99196df60f3f82f4e06afb Mon Sep 17 00:00:00 2001 From: Tyrone Yeh Date: Mon, 25 Jul 2022 00:21:14 +0000 Subject: [PATCH 15/16] [skip ci] Updated translations via Crowdin --- options/locale/locale_ja-JP.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index 065c828afa99..416f6add0293 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -1420,6 +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=が期日を %[2]s から %[1]s に変更 %[3]s issues.due_date_remove=が期日 %s を削除 %s issues.due_date_overdue=期日は過ぎています issues.due_date_invalid=期日が正しくないか範囲を超えています。 'yyyy-mm-dd' の形式で入力してください。 From 690272d2e24846390d785a1f053af6c7ba5963a3 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Mon, 25 Jul 2022 02:52:14 +0200 Subject: [PATCH 16/16] Fix Ruby package parsing by removed unused email field (#20470) --- modules/packages/rubygems/metadata.go | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/packages/rubygems/metadata.go b/modules/packages/rubygems/metadata.go index 942f205fc315..05c1a8a719cb 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"`