From 725a3ed9ad110f8354303140f527326c64f42dc8 Mon Sep 17 00:00:00 2001 From: Zettat123 Date: Mon, 26 Aug 2024 01:18:19 +0800 Subject: [PATCH 1/9] Handle "close" actionable references for manual merges (#31879) Fix #31743 --- services/pull/merge.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/services/pull/merge.go b/services/pull/merge.go index e19292c31c..eb67e06946 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -219,6 +219,10 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U // Reset cached commit count cache.Remove(pr.Issue.Repo.GetCommitsCountCacheKey(pr.BaseBranch, true)) + return handleCloseCrossReferences(ctx, pr, doer) +} + +func handleCloseCrossReferences(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User) error { // Resolve cross references refs, err := pr.ResolveCrossReferences(ctx) if err != nil { @@ -542,5 +546,6 @@ func MergedManually(ctx context.Context, pr *issues_model.PullRequest, doer *use notify_service.MergePullRequest(baseGitRepo.Ctx, doer, pr) log.Info("manuallyMerged[%d]: Marked as manually merged into %s/%s by commit id: %s", pr.ID, pr.BaseRepo.Name, pr.BaseBranch, commitID) - return nil + + return handleCloseCrossReferences(ctx, pr, doer) } From 40395ce582c681a9f55263dcaa5e8dc80e1c2c1b Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 25 Aug 2024 19:23:13 +0200 Subject: [PATCH 2/9] Update mermaid to v11 (#31913) Update mermaid to [v11](https://github.com/mermaid-js/mermaid/releases/tag/v11.0.0) and enable the new [`suppressErrorRendering` option](https://github.com/mermaid-js/mermaid/pull/4359) to ensure mermaid never renders error elements into the DOM (we have per-chart error rendering, so don't need it). Tested various chart types. BTW, I was unable to reproduce that error rendering from mermaid with `suppressErrorRendering: false` and I thought we had some CSS to hide the error element, but I could not find it, not even in git history. --- package-lock.json | 868 +++++++++-------------------------- package.json | 2 +- web_src/js/markup/mermaid.ts | 1 + 3 files changed, 211 insertions(+), 660 deletions(-) diff --git a/package-lock.json b/package-lock.json index eb3ba5fb48..d469cc924c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,7 @@ "jquery": "3.7.1", "katex": "0.16.11", "license-checker-webpack-plugin": "0.2.1", - "mermaid": "10.9.1", + "mermaid": "11.0.2", "mini-css-extract-plugin": "2.9.0", "minimatch": "10.0.1", "monaco-editor": "0.50.0", @@ -318,11 +318,50 @@ } }, "node_modules/@braintree/sanitize-url": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz", - "integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.0.tgz", + "integrity": "sha512-o+UlMLt49RvtCASlOMW0AkHnabN9wR9rwCCherxO0yG4Npy34GkvrAqdXQvrhNs+jh+gkK8gB8Lf05qL/O7KWg==", "license": "MIT" }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", + "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "11.0.3", + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/gast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", + "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", + "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/types": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", + "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", + "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", + "license": "Apache-2.0" + }, "node_modules/@citation-js/core": { "version": "0.7.14", "resolved": "https://registry.npmjs.org/@citation-js/core/-/core-0.7.14.tgz", @@ -1464,6 +1503,15 @@ "@mcaptcha/core-glue": "^0.1.0-alpha-5" } }, + "node_modules/@mermaid-js/parser": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.2.0.tgz", + "integrity": "sha512-33dyFdhwsX9n4+E8SRj1ulxwAgwCj9RyCMtoqXD5cDfS9F6y9xmvmjFjHoPaViH4H7I7BXD8yP/XEWig5XrHSQ==", + "license": "MIT", + "dependencies": { + "langium": "3.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2445,36 +2493,6 @@ "@types/tern": "*" } }, - "node_modules/@types/d3-scale": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", - "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", - "license": "MIT", - "dependencies": { - "@types/d3-time": "*" - } - }, - "node_modules/@types/d3-scale-chromatic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz", - "integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==", - "license": "MIT" - }, - "node_modules/@types/d3-time": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", - "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==", - "license": "MIT" - }, - "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, "node_modules/@types/dropzone": { "version": "5.7.8", "resolved": "https://registry.npmjs.org/@types/dropzone/-/dropzone-5.7.8.tgz", @@ -2567,21 +2585,6 @@ "integrity": "sha512-a79Yc3TOk6dGdituy8hmTTJXjOkZ7zsFYV10L337ttq/rec8lRMDBpV7fL3uLx6TgbFCa5DU/h8FmIBQPSbU0w==", "license": "MIT" }, - "node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/@types/ms": { - "version": "0.7.34", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", - "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", - "license": "MIT" - }, "node_modules/@types/node": { "version": "22.1.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.1.0.tgz", @@ -2687,12 +2690,6 @@ "source-map": "^0.6.1" } }, - "node_modules/@types/unist": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", - "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", - "license": "MIT" - }, "node_modules/@types/urijs": { "version": "1.19.25", "resolved": "https://registry.npmjs.org/@types/urijs/-/urijs-1.19.25.tgz", @@ -4028,16 +4025,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/character-entities": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", - "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/chart.js": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.3.tgz", @@ -4085,6 +4072,32 @@ "node": ">= 16" } }, + "node_modules/chevrotain": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", + "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "11.0.3", + "@chevrotain/gast": "11.0.3", + "@chevrotain/regexp-to-ast": "11.0.3", + "@chevrotain/types": "11.0.3", + "@chevrotain/utils": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/chevrotain-allstar": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz", + "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==", + "license": "MIT", + "dependencies": { + "lodash-es": "^4.17.21" + }, + "peerDependencies": { + "chevrotain": "^11.0.0" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -5038,6 +5051,7 @@ "version": "4.3.6", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, "license": "MIT", "dependencies": { "ms": "2.1.2" @@ -5051,19 +5065,6 @@ } } }, - "node_modules/decode-named-character-reference": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", - "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", - "license": "MIT", - "dependencies": { - "character-entities": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/decode-uri-component": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", @@ -5138,6 +5139,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5149,15 +5151,6 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "license": "Apache-2.0" }, - "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -5299,12 +5292,6 @@ "integrity": "sha512-orzA81VqLyIGUEA77YkVA1D+N+nNfl2isJVjjmOyrlxuooZ19ynb+dOlaDTqd/idKRS9lDCSBmtzM+kyCsMnkA==", "license": "ISC" }, - "node_modules/elkjs": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz", - "integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==", - "license": "EPL-2.0" - }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -7493,6 +7480,12 @@ "dev": true, "license": "MIT" }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, "node_modules/hammerjs": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz", @@ -8248,15 +8241,6 @@ "node": ">=0.10.0" } }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/known-css-properties": { "version": "0.34.0", "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.34.0.tgz", @@ -8264,6 +8248,22 @@ "dev": true, "license": "MIT" }, + "node_modules/langium": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/langium/-/langium-3.0.0.tgz", + "integrity": "sha512-+Ez9EoiByeoTu/2BXmEaZ06iPNXM6thWJp02KfBO/raSMyCJ4jw7AkWWa+zBCTm0+Tw1Fj9FOxdqSskyN5nAwg==", + "license": "MIT", + "dependencies": { + "chevrotain": "~11.0.3", + "chevrotain-allstar": "~0.3.0", + "vscode-languageserver": "~9.0.1", + "vscode-languageserver-textdocument": "~1.0.11", + "vscode-uri": "~3.0.8" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -8724,43 +8724,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/mdast-util-from-markdown": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", - "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "mdast-util-to-string": "^3.1.0", - "micromark": "^3.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-decode-string": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "unist-util-stringify-position": "^3.0.0", - "uvu": "^0.5.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", - "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", @@ -8804,475 +8767,42 @@ } }, "node_modules/mermaid": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.9.1.tgz", - "integrity": "sha512-Mx45Obds5W1UkW1nv/7dHRsbfMM1aOKA2+Pxs/IGHNonygDHwmng8xTHyS9z4KWVi0rbko8gjiBmuwwXQ7tiNA==", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.0.2.tgz", + "integrity": "sha512-KFM1o560odBHvXTTSx47ne/SE4aJKb2GbysHAVdQafIJtB6O3c0K4F+v3nC+zqS6CJhk7sXaagectNrTG+ARDw==", "license": "MIT", "dependencies": { - "@braintree/sanitize-url": "^6.0.1", - "@types/d3-scale": "^4.0.3", - "@types/d3-scale-chromatic": "^3.0.0", - "cytoscape": "^3.28.1", + "@braintree/sanitize-url": "^7.0.1", + "@mermaid-js/parser": "^0.2.0", + "cytoscape": "^3.29.2", "cytoscape-cose-bilkent": "^4.1.0", - "d3": "^7.4.0", + "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.10", - "dayjs": "^1.11.7", - "dompurify": "^3.0.5", - "elkjs": "^0.9.0", + "dayjs": "^1.11.10", + "dompurify": "^3.0.11", "katex": "^0.16.9", - "khroma": "^2.0.0", + "khroma": "^2.1.0", "lodash-es": "^4.17.21", - "mdast-util-from-markdown": "^1.3.0", - "non-layered-tidy-tree-layout": "^2.0.2", - "stylis": "^4.1.3", + "marked": "^13.0.2", + "roughjs": "^4.6.6", + "stylis": "^4.3.1", "ts-dedent": "^2.2.0", - "uuid": "^9.0.0", - "web-worker": "^1.2.0" + "uuid": "^9.0.1" } }, - "node_modules/micromark": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", - "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/mermaid/node_modules/marked": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-13.0.3.tgz", + "integrity": "sha512-rqRix3/TWzE9rIoFGIn8JmsVfhiuC8VIQ8IdX5TfzmeBucdY05/0UlzKaw0eVtpcN/OdVFpBk7CjKGo9iHJ/zA==", "license": "MIT", - "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "micromark-core-commonmark": "^1.0.1", - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-combine-extensions": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-sanitize-uri": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "uvu": "^0.5.0" + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" } }, - "node_modules/micromark-core-commonmark": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", - "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-factory-destination": "^1.0.0", - "micromark-factory-label": "^1.0.0", - "micromark-factory-space": "^1.0.0", - "micromark-factory-title": "^1.0.0", - "micromark-factory-whitespace": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-classify-character": "^1.0.0", - "micromark-util-html-tag-name": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "uvu": "^0.5.0" - } - }, - "node_modules/micromark-factory-destination": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", - "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-factory-label": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", - "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - } - }, - "node_modules/micromark-factory-space": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", - "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-factory-title": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", - "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-factory-whitespace": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", - "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-character": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", - "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-chunked": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", - "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-util-classify-character": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", - "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-combine-extensions": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", - "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", - "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-util-decode-string": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", - "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-util-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", - "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-html-tag-name": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", - "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-normalize-identifier": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", - "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-util-resolve-all": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", - "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", - "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-util-subtokenize": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", - "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - } - }, - "node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, "node_modules/micromatch": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", @@ -9409,19 +8939,11 @@ "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==", "license": "BSD-3-Clause" }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, "license": "MIT" }, "node_modules/mz": { @@ -9550,12 +9072,6 @@ "node": ">=12.4.0" } }, - "node_modules/non-layered-tidy-tree-layout": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz", - "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==", - "license": "MIT" - }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -9841,6 +9357,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -10065,6 +9587,22 @@ "node": ">=4" } }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, "node_modules/pony-cause": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pony-cause/-/pony-cause-1.1.1.tgz", @@ -10879,6 +10417,18 @@ "fsevents": "~2.3.2" } }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, "node_modules/run-con": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/run-con/-/run-con-1.3.2.tgz", @@ -10924,18 +10474,6 @@ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", "license": "BSD-3-Clause" }, - "node_modules/sade": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", - "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "license": "MIT", - "dependencies": { - "mri": "^1.1.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -12349,19 +11887,6 @@ "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==", "license": "MIT" }, - "node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -12460,24 +11985,6 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/uvu": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", - "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.0", - "diff": "^5.0.0", - "kleur": "^4.0.3", - "sade": "^1.7.3" - }, - "bin": { - "uvu": "bin.js" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -12724,6 +12231,55 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "license": "MIT" + }, "node_modules/vue": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.35.tgz", @@ -12855,12 +12411,6 @@ "node": ">=10.13.0" } }, - "node_modules/web-worker": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.3.0.tgz", - "integrity": "sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA==", - "license": "Apache-2.0" - }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", diff --git a/package.json b/package.json index 7d9e857e90..9d3556475e 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "jquery": "3.7.1", "katex": "0.16.11", "license-checker-webpack-plugin": "0.2.1", - "mermaid": "10.9.1", + "mermaid": "11.0.2", "mini-css-extract-plugin": "2.9.0", "minimatch": "10.0.1", "monaco-editor": "0.50.0", diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index da07161ed1..f2754e659b 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -20,6 +20,7 @@ export async function renderMermaid() { startOnLoad: false, theme: isDarkTheme() ? 'dark' : 'neutral', securityLevel: 'strict', + suppressErrorRendering: true, }); for (const el of els) { From d477dd5e8897eea642100f247ddab5654012b62b Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Mon, 26 Aug 2024 00:28:33 +0000 Subject: [PATCH 3/9] [skip ci] Updated licenses and gitignores --- options/license/DocBook-Schema | 22 ++++++++ options/license/DocBook-XML | 48 +++++++++++++++++ options/license/Ubuntu-font-1.0 | 96 +++++++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+) create mode 100644 options/license/DocBook-Schema create mode 100644 options/license/DocBook-XML create mode 100644 options/license/Ubuntu-font-1.0 diff --git a/options/license/DocBook-Schema b/options/license/DocBook-Schema new file mode 100644 index 0000000000..56203a0878 --- /dev/null +++ b/options/license/DocBook-Schema @@ -0,0 +1,22 @@ +Copyright 1992-2011 HaL Computer Systems, Inc., +O'Reilly & Associates, Inc., ArborText, Inc., Fujitsu Software +Corporation, Norman Walsh, Sun Microsystems, Inc., and the +Organization for the Advancement of Structured Information +Standards (OASIS). + +Permission to use, copy, modify and distribute the DocBook schema +and its accompanying documentation for any purpose and without fee +is hereby granted in perpetuity, provided that the above copyright +notice and this paragraph appear in all copies. The copyright +holders make no representation about the suitability of the schema +for any purpose. It is provided "as is" without expressed or implied +warranty. + +If you modify the DocBook schema in any way, label your schema as a +variant of DocBook. See the reference documentation +(http://docbook.org/tdg5/en/html/ch05.html#s-notdocbook) +for more information. + +Please direct all questions, bug reports, or suggestions for changes +to the docbook@lists.oasis-open.org mailing list. For more +information, see http://www.oasis-open.org/docbook/. diff --git a/options/license/DocBook-XML b/options/license/DocBook-XML new file mode 100644 index 0000000000..9553feee6b --- /dev/null +++ b/options/license/DocBook-XML @@ -0,0 +1,48 @@ +Copyright +--------- +Copyright (C) 1999-2007 Norman Walsh +Copyright (C) 2003 Jiří Kosek +Copyright (C) 2004-2007 Steve Ball +Copyright (C) 2005-2014 The DocBook Project +Copyright (C) 2011-2012 O'Reilly Media + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the ``Software''), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +Except as contained in this notice, the names of individuals +credited with contribution to this software shall not be used in +advertising or otherwise to promote the sale, use or other +dealings in this Software without prior written authorization +from the individuals in question. + +Any stylesheet derived from this Software that is publically +distributed will be identified with a different name and the +version strings in any derived Software will be changed so that +no possibility of confusion between the derived package and this +Software will exist. + +Warranty +-------- +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL NORMAN WALSH OR ANY OTHER +CONTRIBUTOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +Contacting the Author +--------------------- +The DocBook XSL stylesheets are maintained by Norman Walsh, +, and members of the DocBook Project, + diff --git a/options/license/Ubuntu-font-1.0 b/options/license/Ubuntu-font-1.0 new file mode 100644 index 0000000000..ae78a8f94e --- /dev/null +++ b/options/license/Ubuntu-font-1.0 @@ -0,0 +1,96 @@ +------------------------------- +UBUNTU FONT LICENCE Version 1.0 +------------------------------- + +PREAMBLE +This licence allows the licensed fonts to be used, studied, modified and +redistributed freely. The fonts, including any derivative works, can be +bundled, embedded, and redistributed provided the terms of this licence +are met. The fonts and derivatives, however, cannot be released under +any other licence. The requirement for fonts to remain under this +licence does not require any document created using the fonts or their +derivatives to be published under this licence, as long as the primary +purpose of the document is not to be a vehicle for the distribution of +the fonts. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this licence and clearly marked as such. This may +include source files, build scripts and documentation. + +"Original Version" refers to the collection of Font Software components +as received under this licence. + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to +a new environment. + +"Copyright Holder(s)" refers to all individuals and companies who have a +copyright ownership of the Font Software. + +"Substantially Changed" refers to Modified Versions which can be easily +identified as dissimilar to the Font Software by users of the Font +Software comparing the Original Version with the Modified Version. + +To "Propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification and with or without charging +a redistribution fee), making available to the public, and in some +countries other activities as well. + +PERMISSION & CONDITIONS +This licence does not grant any rights under trademark law and all such +rights are reserved. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of the Font Software, to propagate the Font Software, subject to +the below conditions: + +1) Each copy of the Font Software must contain the above copyright +notice and this licence. These can be included either as stand-alone +text files, human-readable headers or in the appropriate machine- +readable metadata fields within text or binary files as long as those +fields can be easily viewed by the user. + +2) The font name complies with the following: +(a) The Original Version must retain its name, unmodified. +(b) Modified Versions which are Substantially Changed must be renamed to +avoid use of the name of the Original Version or similar names entirely. +(c) Modified Versions which are not Substantially Changed must be +renamed to both (i) retain the name of the Original Version and (ii) add +additional naming elements to distinguish the Modified Version from the +Original Version. The name of such Modified Versions must be the name of +the Original Version, with "derivative X" where X represents the name of +the new work, appended to that name. + +3) The name(s) of the Copyright Holder(s) and any contributor to the +Font Software shall not be used to promote, endorse or advertise any +Modified Version, except (i) as required by this licence, (ii) to +acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with +their explicit written permission. + +4) The Font Software, modified or unmodified, in part or in whole, must +be distributed entirely under this licence, and must not be distributed +under any other licence. The requirement for fonts to remain under this +licence does not affect any document created using the Font Software, +except any version of the Font Software extracted from a document +created using the Font Software may only be distributed under this +licence. + +TERMINATION +This licence becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER +DEALINGS IN THE FONT SOFTWARE. From 1e4be0945b466a17cd98b5aed19faf6caad12fb4 Mon Sep 17 00:00:00 2001 From: Jason Song Date: Mon, 26 Aug 2024 22:27:57 +0800 Subject: [PATCH 4/9] Introduce globallock as distributed locks (#31908) To help #31813, but do not replace it, since this PR just introduces the new module but misses some work: - New option in settings. `#31813` has done it. - Use the locks in business logic. `#31813` has done it. So I think the most efficient way is to merge this PR first (if it's acceptable) and then finish #31813. ## Design principles ### Use spinlock even in memory implementation In actual use cases, users may cancel requests. `sync.Mutex` will block the goroutine until the lock is acquired even if the request is canceled. And the spinlock is more suitable for this scenario since it's possible to give up the lock acquisition. Although the spinlock consumes more CPU resources, I think it's acceptable in most cases. ### Do not expose the mutex to callers If we expose the mutex to callers, it's possible for callers to reuse the mutex, which causes more complexity. For example: ```go lock := GetLocker(key) lock.Lock() // ... // even if the lock is unlocked, we cannot GC the lock, // since the caller may still use it again. lock.Unlock() lock.Lock() // ... lock.Unlock() // callers have to GC the lock manually. RemoveLocker(key) ``` That's why https://github.com/go-gitea/gitea/pull/31813#discussion_r1721200549 In this PR, we only expose `ReleaseFunc` to callers. So callers just need to call `ReleaseFunc` to release the lock, and do not need to care about the lock's lifecycle. ```go _, release, err := locker.Lock(ctx, key) if err != nil { return err } // ... release() // if callers want to lock again, they have to re-acquire the lock. _, release, err := locker.Lock(ctx, key) // ... ``` In this way, it's also much easier for redis implementation to extend the mutex automatically, so that callers do not need to care about the lock's lifecycle. See also https://github.com/go-gitea/gitea/pull/31813#discussion_r1722659743 ### Use "release" instead of "unlock" For "unlock", it has the meaning of "unlock an acquired lock". So it's not acceptable to call "unlock" when failed to acquire the lock, or call "unlock" multiple times. It causes more complexity for callers to decide whether to call "unlock" or not. So we use "release" instead of "unlock" to make it clear. Whether the lock is acquired or not, callers can always call "release", and it's also safe to call "release" multiple times. But the code DO NOT expect callers to not call "release" after acquiring the lock. If callers forget to call "release", it will cause resource leak. That's why it's always safe to call "release" without extra checks: to avoid callers to forget to call it. ### Acquired locks could be lost Unlike `sync.Mutex` which will be locked forever once acquired until calling `Unlock`, in the new module, the acquired lock could be lost. For example, the caller has acquired the lock, and it holds the lock for a long time since auto-extending is working for redis. However, it lost the connection to the redis server, and it's impossible to extend the lock anymore. If the caller don't stop what it's doing, another instance which can connect to the redis server could acquire the lock, and do the same thing, which could cause data inconsistency. So the caller should know what happened, the solution is to return a new context which will be canceled if the lock is lost or released: ```go ctx, release, err := locker.Lock(ctx, key) if err != nil { return err } defer release() // ... DoSomething(ctx) // the lock is lost now, then ctx has been canceled. // Failed, since ctx has been canceled. DoSomethingElse(ctx) ``` ### Multiple ways to use the lock 1. Regular way ```go ctx, release, err := Lock(ctx, key) if err != nil { return err } defer release() // ... ``` 2. Early release ```go ctx, release, err := Lock(ctx, key) if err != nil { return err } defer release() // ... // release the lock earlier and reset the context back ctx = release() // continue to do something else // ... ``` 3. Functional way ```go if err := LockAndDo(ctx, key, func(ctx context.Context) error { // ... return nil }); err != nil { return err } ``` --- go.mod | 3 + go.sum | 19 +++ modules/globallock/globallock.go | 66 ++++++++ modules/globallock/globallock_test.go | 96 ++++++++++++ modules/globallock/locker.go | 60 ++++++++ modules/globallock/locker_test.go | 211 ++++++++++++++++++++++++++ modules/globallock/memory_locker.go | 80 ++++++++++ modules/globallock/redis_locker.go | 154 +++++++++++++++++++ 8 files changed, 689 insertions(+) create mode 100644 modules/globallock/globallock.go create mode 100644 modules/globallock/globallock_test.go create mode 100644 modules/globallock/locker.go create mode 100644 modules/globallock/locker_test.go create mode 100644 modules/globallock/memory_locker.go create mode 100644 modules/globallock/redis_locker.go diff --git a/go.mod b/go.mod index f5c189893f..69695fa178 100644 --- a/go.mod +++ b/go.mod @@ -49,6 +49,7 @@ require ( github.com/go-git/go-billy/v5 v5.5.0 github.com/go-git/go-git/v5 v5.12.0 github.com/go-ldap/ldap/v3 v3.4.6 + github.com/go-redsync/redsync/v4 v4.13.0 github.com/go-sql-driver/mysql v1.8.1 github.com/go-swagger/go-swagger v0.31.0 github.com/go-testfixtures/testfixtures/v3 v3.11.0 @@ -218,7 +219,9 @@ require ( github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/imdario/mergo v0.3.16 // indirect diff --git a/go.sum b/go.sum index f1780fada7..510ef8479d 100644 --- a/go.sum +++ b/go.sum @@ -342,6 +342,14 @@ github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= +github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOrTI= +github.com/go-redis/redis/v7 v7.4.1/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= +github.com/go-redsync/redsync/v4 v4.13.0 h1:49X6GJfnbLGaIpBBREM/zA4uIMDXKAh1NDkvQ1EkZKA= +github.com/go-redsync/redsync/v4 v4.13.0/go.mod h1:HMW4Q224GZQz6x1Xc7040Yfgacukdzu7ifTDAKiyErQ= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= @@ -397,6 +405,8 @@ github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= +github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -449,10 +459,15 @@ github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE= github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= @@ -674,6 +689,8 @@ github.com/quasoft/websspi v1.1.2/go.mod h1:HmVdl939dQ0WIXZhyik+ARdI03M6bQzaSEKc github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/redis/go-redis/v9 v9.6.0 h1:NLck+Rab3AOTHw21CGRpvQpgTrAU4sgdCswqGtlhGRA= github.com/redis/go-redis/v9 v9.6.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/redis/rueidis v1.0.19 h1:s65oWtotzlIFN8eMPhyYwxlwLR1lUdhza2KtWprKYSo= +github.com/redis/rueidis v1.0.19/go.mod h1:8B+r5wdnjwK3lTFml5VtxjzGOQAC+5UmujoD12pDrEo= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rhysd/actionlint v1.7.1 h1:WJaDzyT1StBWVKGSsZPYnbV0HF9Y9/vD6KFdZQL42qE= @@ -765,6 +782,8 @@ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM= +github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= diff --git a/modules/globallock/globallock.go b/modules/globallock/globallock.go new file mode 100644 index 0000000000..707d169f05 --- /dev/null +++ b/modules/globallock/globallock.go @@ -0,0 +1,66 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package globallock + +import ( + "context" + "sync" +) + +var ( + defaultLocker Locker + initOnce sync.Once + initFunc = func() { + // TODO: read the setting and initialize the default locker. + // Before implementing this, don't use it. + } // define initFunc as a variable to make it possible to change it in tests +) + +// DefaultLocker returns the default locker. +func DefaultLocker() Locker { + initOnce.Do(func() { + initFunc() + }) + return defaultLocker +} + +// Lock tries to acquire a lock for the given key, it uses the default locker. +// Read the documentation of Locker.Lock for more information about the behavior. +func Lock(ctx context.Context, key string) (context.Context, ReleaseFunc, error) { + return DefaultLocker().Lock(ctx, key) +} + +// TryLock tries to acquire a lock for the given key, it uses the default locker. +// Read the documentation of Locker.TryLock for more information about the behavior. +func TryLock(ctx context.Context, key string) (bool, context.Context, ReleaseFunc, error) { + return DefaultLocker().TryLock(ctx, key) +} + +// LockAndDo tries to acquire a lock for the given key and then calls the given function. +// It uses the default locker, and it will return an error if failed to acquire the lock. +func LockAndDo(ctx context.Context, key string, f func(context.Context) error) error { + ctx, release, err := Lock(ctx, key) + if err != nil { + return err + } + defer release() + + return f(ctx) +} + +// TryLockAndDo tries to acquire a lock for the given key and then calls the given function. +// It uses the default locker, and it will return false if failed to acquire the lock. +func TryLockAndDo(ctx context.Context, key string, f func(context.Context) error) (bool, error) { + ok, ctx, release, err := TryLock(ctx, key) + if err != nil { + return false, err + } + defer release() + + if !ok { + return false, nil + } + + return true, f(ctx) +} diff --git a/modules/globallock/globallock_test.go b/modules/globallock/globallock_test.go new file mode 100644 index 0000000000..88a555c86f --- /dev/null +++ b/modules/globallock/globallock_test.go @@ -0,0 +1,96 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package globallock + +import ( + "context" + "os" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLockAndDo(t *testing.T) { + t.Run("redis", func(t *testing.T) { + url := "redis://127.0.0.1:6379/0" + if os.Getenv("CI") == "" { + // Make it possible to run tests against a local redis instance + url = os.Getenv("TEST_REDIS_URL") + if url == "" { + t.Skip("TEST_REDIS_URL not set and not running in CI") + return + } + } + + oldDefaultLocker := defaultLocker + oldInitFunc := initFunc + defer func() { + defaultLocker = oldDefaultLocker + initFunc = oldInitFunc + if defaultLocker == nil { + initOnce = sync.Once{} + } + }() + + initOnce = sync.Once{} + initFunc = func() { + defaultLocker = NewRedisLocker(url) + } + + testLockAndDo(t) + require.NoError(t, defaultLocker.(*redisLocker).Close()) + }) + t.Run("memory", func(t *testing.T) { + oldDefaultLocker := defaultLocker + oldInitFunc := initFunc + defer func() { + defaultLocker = oldDefaultLocker + initFunc = oldInitFunc + if defaultLocker == nil { + initOnce = sync.Once{} + } + }() + + initOnce = sync.Once{} + initFunc = func() { + defaultLocker = NewMemoryLocker() + } + + testLockAndDo(t) + }) +} + +func testLockAndDo(t *testing.T) { + const concurrency = 1000 + + ctx := context.Background() + count := 0 + wg := sync.WaitGroup{} + wg.Add(concurrency) + for i := 0; i < concurrency; i++ { + go func() { + defer wg.Done() + err := LockAndDo(ctx, "test", func(ctx context.Context) error { + count++ + + // It's impossible to acquire the lock inner the function + ok, err := TryLockAndDo(ctx, "test", func(ctx context.Context) error { + assert.Fail(t, "should not acquire the lock") + return nil + }) + assert.False(t, ok) + assert.NoError(t, err) + + return nil + }) + require.NoError(t, err) + }() + } + + wg.Wait() + + assert.Equal(t, concurrency, count) +} diff --git a/modules/globallock/locker.go b/modules/globallock/locker.go new file mode 100644 index 0000000000..b0764cd71c --- /dev/null +++ b/modules/globallock/locker.go @@ -0,0 +1,60 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package globallock + +import ( + "context" + "fmt" +) + +type Locker interface { + // Lock tries to acquire a lock for the given key, it blocks until the lock is acquired or the context is canceled. + // + // Lock returns a new context which should be used in the following code. + // The new context will be canceled when the lock is released or lost - yes, it's possible to lose a lock. + // For example, it lost the connection to the redis server while holding the lock. + // If it fails to acquire the lock, the returned context will be the same as the input context. + // + // Lock returns a ReleaseFunc to release the lock, it cannot be nil. + // It's always safe to call this function even if it fails to acquire the lock, and it will do nothing in that case. + // And it's also safe to call it multiple times, but it will only release the lock once. + // That's why it's called ReleaseFunc, not UnlockFunc. + // But be aware that it's not safe to not call it at all; it could lead to a memory leak. + // So a recommended pattern is to use defer to call it: + // ctx, release, err := locker.Lock(ctx, "key") + // if err != nil { + // return err + // } + // defer release() + // The ReleaseFunc will return the original context which was used to acquire the lock. + // It's useful when you want to continue to do something after releasing the lock. + // At that time, the ctx will be canceled, and you can use the returned context by the ReleaseFunc to continue: + // ctx, release, err := locker.Lock(ctx, "key") + // if err != nil { + // return err + // } + // defer release() + // doSomething(ctx) + // ctx = release() + // doSomethingElse(ctx) + // Please ignore it and use `defer release()` instead if you don't need this, to avoid forgetting to release the lock. + // + // Lock returns an error if failed to acquire the lock. + // Be aware that even the context is not canceled, it's still possible to fail to acquire the lock. + // For example, redis is down, or it reached the maximum number of tries. + Lock(ctx context.Context, key string) (context.Context, ReleaseFunc, error) + + // TryLock tries to acquire a lock for the given key, it returns immediately. + // It follows the same pattern as Lock, but it doesn't block. + // And if it fails to acquire the lock because it's already locked, not other reasons like redis is down, + // it will return false without any error. + TryLock(ctx context.Context, key string) (bool, context.Context, ReleaseFunc, error) +} + +// ReleaseFunc is a function that releases a lock. +// It returns the original context which was used to acquire the lock. +type ReleaseFunc func() context.Context + +// ErrLockReleased is used as context cause when a lock is released +var ErrLockReleased = fmt.Errorf("lock released") diff --git a/modules/globallock/locker_test.go b/modules/globallock/locker_test.go new file mode 100644 index 0000000000..15a3c65bb0 --- /dev/null +++ b/modules/globallock/locker_test.go @@ -0,0 +1,211 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package globallock + +import ( + "context" + "os" + "sync" + "testing" + "time" + + "github.com/go-redsync/redsync/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLocker(t *testing.T) { + t.Run("redis", func(t *testing.T) { + url := "redis://127.0.0.1:6379/0" + if os.Getenv("CI") == "" { + // Make it possible to run tests against a local redis instance + url = os.Getenv("TEST_REDIS_URL") + if url == "" { + t.Skip("TEST_REDIS_URL not set and not running in CI") + return + } + } + oldExpiry := redisLockExpiry + redisLockExpiry = 5 * time.Second // make it shorter for testing + defer func() { + redisLockExpiry = oldExpiry + }() + + locker := NewRedisLocker(url) + testLocker(t, locker) + testRedisLocker(t, locker.(*redisLocker)) + require.NoError(t, locker.(*redisLocker).Close()) + }) + t.Run("memory", func(t *testing.T) { + locker := NewMemoryLocker() + testLocker(t, locker) + testMemoryLocker(t, locker.(*memoryLocker)) + }) +} + +func testLocker(t *testing.T, locker Locker) { + t.Run("lock", func(t *testing.T) { + parentCtx := context.Background() + ctx, release, err := locker.Lock(parentCtx, "test") + defer release() + + assert.NotEqual(t, parentCtx, ctx) // new context should be returned + assert.NoError(t, err) + + func() { + parentCtx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + ctx, release, err := locker.Lock(parentCtx, "test") + defer release() + + assert.Error(t, err) + assert.Equal(t, parentCtx, ctx) // should return the same context + }() + + release() + assert.Error(t, ctx.Err()) + + func() { + _, release, err := locker.Lock(context.Background(), "test") + defer release() + + assert.NoError(t, err) + }() + }) + + t.Run("try lock", func(t *testing.T) { + parentCtx := context.Background() + ok, ctx, release, err := locker.TryLock(parentCtx, "test") + defer release() + + assert.True(t, ok) + assert.NotEqual(t, parentCtx, ctx) // new context should be returned + assert.NoError(t, err) + + func() { + parentCtx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + ok, ctx, release, err := locker.TryLock(parentCtx, "test") + defer release() + + assert.False(t, ok) + assert.NoError(t, err) + assert.Equal(t, parentCtx, ctx) // should return the same context + }() + + release() + assert.Error(t, ctx.Err()) + + func() { + ok, _, release, _ := locker.TryLock(context.Background(), "test") + defer release() + + assert.True(t, ok) + }() + }) + + t.Run("wait and acquired", func(t *testing.T) { + ctx := context.Background() + _, release, err := locker.Lock(ctx, "test") + require.NoError(t, err) + + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + started := time.Now() + _, release, err := locker.Lock(context.Background(), "test") // should be blocked for seconds + defer release() + assert.Greater(t, time.Since(started), time.Second) + assert.NoError(t, err) + }() + + time.Sleep(2 * time.Second) + release() + + wg.Wait() + }) + + t.Run("continue after release", func(t *testing.T) { + ctx := context.Background() + + ctxBeforeLock := ctx + ctx, release, err := locker.Lock(ctx, "test") + + require.NoError(t, err) + assert.NoError(t, ctx.Err()) + assert.NotEqual(t, ctxBeforeLock, ctx) + + ctxBeforeRelease := ctx + ctx = release() + + assert.NoError(t, ctx.Err()) + assert.Error(t, ctxBeforeRelease.Err()) + + // so it can continue with ctx to do more work + }) + + t.Run("multiple release", func(t *testing.T) { + ctx := context.Background() + + _, release1, err := locker.Lock(ctx, "test") + require.NoError(t, err) + + release1() + + _, release2, err := locker.Lock(ctx, "test") + defer release2() + require.NoError(t, err) + + // Call release1 again, + // it should not panic or block, + // and it shouldn't affect the other lock + release1() + + ok, _, release3, err := locker.TryLock(ctx, "test") + defer release3() + require.NoError(t, err) + // It should be able to acquire the lock; + // otherwise, it means the lock has been released by release1 + assert.False(t, ok) + }) +} + +// testMemoryLocker does specific tests for memoryLocker +func testMemoryLocker(t *testing.T, locker *memoryLocker) { + // nothing to do +} + +// testRedisLocker does specific tests for redisLocker +func testRedisLocker(t *testing.T, locker *redisLocker) { + defer func() { + // This case should be tested at the end. + // Otherwise, it will affect other tests. + t.Run("close", func(t *testing.T) { + assert.NoError(t, locker.Close()) + _, _, err := locker.Lock(context.Background(), "test") + assert.Error(t, err) + }) + }() + + t.Run("failed extend", func(t *testing.T) { + ctx, release, err := locker.Lock(context.Background(), "test") + defer release() + require.NoError(t, err) + + // It simulates that there are some problems with extending like network issues or redis server down. + v, ok := locker.mutexM.Load("test") + require.True(t, ok) + m := v.(*redisMutex) + _, _ = m.mutex.Unlock() // release it to make it impossible to extend + + select { + case <-time.After(redisLockExpiry + time.Second): + t.Errorf("lock should be expired") + case <-ctx.Done(): + var errTaken *redsync.ErrTaken + assert.ErrorAs(t, context.Cause(ctx), &errTaken) + } + }) +} diff --git a/modules/globallock/memory_locker.go b/modules/globallock/memory_locker.go new file mode 100644 index 0000000000..fb1fc79bd0 --- /dev/null +++ b/modules/globallock/memory_locker.go @@ -0,0 +1,80 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package globallock + +import ( + "context" + "sync" + "time" +) + +type memoryLocker struct { + locks sync.Map +} + +var _ Locker = &memoryLocker{} + +func NewMemoryLocker() Locker { + return &memoryLocker{} +} + +func (l *memoryLocker) Lock(ctx context.Context, key string) (context.Context, ReleaseFunc, error) { + originalCtx := ctx + + if l.tryLock(key) { + ctx, cancel := context.WithCancelCause(ctx) + releaseOnce := sync.Once{} + return ctx, func() context.Context { + releaseOnce.Do(func() { + l.locks.Delete(key) + cancel(ErrLockReleased) + }) + return originalCtx + }, nil + } + + ticker := time.NewTicker(time.Millisecond * 100) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return ctx, func() context.Context { return originalCtx }, ctx.Err() + case <-ticker.C: + if l.tryLock(key) { + ctx, cancel := context.WithCancelCause(ctx) + releaseOnce := sync.Once{} + return ctx, func() context.Context { + releaseOnce.Do(func() { + l.locks.Delete(key) + cancel(ErrLockReleased) + }) + return originalCtx + }, nil + } + } + } +} + +func (l *memoryLocker) TryLock(ctx context.Context, key string) (bool, context.Context, ReleaseFunc, error) { + originalCtx := ctx + + if l.tryLock(key) { + ctx, cancel := context.WithCancelCause(ctx) + releaseOnce := sync.Once{} + return true, ctx, func() context.Context { + releaseOnce.Do(func() { + cancel(ErrLockReleased) + l.locks.Delete(key) + }) + return originalCtx + }, nil + } + + return false, ctx, func() context.Context { return originalCtx }, nil +} + +func (l *memoryLocker) tryLock(key string) bool { + _, loaded := l.locks.LoadOrStore(key, struct{}{}) + return !loaded +} diff --git a/modules/globallock/redis_locker.go b/modules/globallock/redis_locker.go new file mode 100644 index 0000000000..34b2fabfb3 --- /dev/null +++ b/modules/globallock/redis_locker.go @@ -0,0 +1,154 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package globallock + +import ( + "context" + "errors" + "fmt" + "sync" + "sync/atomic" + "time" + + "code.gitea.io/gitea/modules/nosql" + + "github.com/go-redsync/redsync/v4" + "github.com/go-redsync/redsync/v4/redis/goredis/v9" +) + +const redisLockKeyPrefix = "gitea:globallock:" + +// redisLockExpiry is the default expiry time for a lock. +// Define it as a variable to make it possible to change it in tests. +var redisLockExpiry = 30 * time.Second + +type redisLocker struct { + rs *redsync.Redsync + + mutexM sync.Map + closed atomic.Bool + extendWg sync.WaitGroup +} + +var _ Locker = &redisLocker{} + +func NewRedisLocker(connection string) Locker { + l := &redisLocker{ + rs: redsync.New( + goredis.NewPool( + nosql.GetManager().GetRedisClient(connection), + ), + ), + } + + l.extendWg.Add(1) + l.startExtend() + + return l +} + +func (l *redisLocker) Lock(ctx context.Context, key string) (context.Context, ReleaseFunc, error) { + return l.lock(ctx, key, 0) +} + +func (l *redisLocker) TryLock(ctx context.Context, key string) (bool, context.Context, ReleaseFunc, error) { + ctx, f, err := l.lock(ctx, key, 1) + + var ( + errTaken *redsync.ErrTaken + errNodeTaken *redsync.ErrNodeTaken + ) + if errors.As(err, &errTaken) || errors.As(err, &errNodeTaken) { + return false, ctx, f, nil + } + return err == nil, ctx, f, err +} + +// Close closes the locker. +// It will stop extending the locks and refuse to acquire new locks. +// In actual use, it is not necessary to call this function. +// But it's useful in tests to release resources. +// It could take some time since it waits for the extending goroutine to finish. +func (l *redisLocker) Close() error { + l.closed.Store(true) + l.extendWg.Wait() + return nil +} + +type redisMutex struct { + mutex *redsync.Mutex + cancel context.CancelCauseFunc +} + +func (l *redisLocker) lock(ctx context.Context, key string, tries int) (context.Context, ReleaseFunc, error) { + if l.closed.Load() { + return ctx, func() context.Context { return ctx }, fmt.Errorf("locker is closed") + } + + originalCtx := ctx + + options := []redsync.Option{ + redsync.WithExpiry(redisLockExpiry), + } + if tries > 0 { + options = append(options, redsync.WithTries(tries)) + } + mutex := l.rs.NewMutex(redisLockKeyPrefix+key, options...) + if err := mutex.LockContext(ctx); err != nil { + return ctx, func() context.Context { return originalCtx }, err + } + + ctx, cancel := context.WithCancelCause(ctx) + + l.mutexM.Store(key, &redisMutex{ + mutex: mutex, + cancel: cancel, + }) + + releaseOnce := sync.Once{} + return ctx, func() context.Context { + releaseOnce.Do(func() { + l.mutexM.Delete(key) + + // It's safe to ignore the error here, + // if it failed to unlock, it will be released automatically after the lock expires. + // Do not call mutex.UnlockContext(ctx) here, or it will fail to release when ctx has timed out. + _, _ = mutex.Unlock() + + cancel(ErrLockReleased) + }) + return originalCtx + }, nil +} + +func (l *redisLocker) startExtend() { + if l.closed.Load() { + l.extendWg.Done() + return + } + + toExtend := make([]*redisMutex, 0) + l.mutexM.Range(func(_, value any) bool { + m := value.(*redisMutex) + + // Extend the lock if it is not expired. + // Although the mutex will be removed from the map before it is released, + // it still can be expired because of a failed extension. + // If it happens, the cancel function should have been called, + // so it does not need to be extended anymore. + if time.Now().After(m.mutex.Until()) { + return true + } + + toExtend = append(toExtend, m) + return true + }) + for _, v := range toExtend { + if ok, err := v.mutex.Extend(); !ok { + v.cancel(err) + } + } + + time.AfterFunc(redisLockExpiry/2, l.startExtend) +} From 521d91944ed10add89f435efd2e3df17d8af2d51 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Tue, 27 Aug 2024 00:28:30 +0000 Subject: [PATCH 5/9] [skip ci] Updated translations via Crowdin --- options/locale/locale_zh-CN.ini | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 3a6dadf9f8..d8897735c2 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -206,7 +206,7 @@ buttons.list.unordered.tooltip=添加待办清单 buttons.list.ordered.tooltip=添加编号列表 buttons.list.task.tooltip=添加任务列表 buttons.mention.tooltip=提及用户或团队 -buttons.ref.tooltip=引用一个问题或拉取请求 +buttons.ref.tooltip=引用一个问题或合并请求 buttons.switch_to_legacy.tooltip=使用旧版编辑器 buttons.enable_monospace_font=启用等宽字体 buttons.disable_monospace_font=禁用等宽字体 @@ -1752,8 +1752,9 @@ compare.compare_head=比较 pulls.desc=启用合并请求和代码评审。 pulls.new=创建合并请求 pulls.new.blocked_user=无法创建合并请求,因为您已被仓库所有者屏蔽。 +pulls.new.must_collaborator=您必须是仓库的协作者才能创建合并请求。 pulls.edit.already_changed=无法保存对合并请求的更改。其内容似乎已被其他用户更改。 请刷新页面并重新编辑以避免覆盖他们的更改 -pulls.view=查看拉取请求 +pulls.view=查看合并请求 pulls.compare_changes=创建合并请求 pulls.allow_edits_from_maintainers=允许维护者编辑 pulls.allow_edits_from_maintainers_desc=对基础分支有写入权限的用户也可以推送到此分支 @@ -1830,8 +1831,8 @@ pulls.wrong_commit_id=提交 id 必须在目标分支 上 pulls.no_merge_desc=由于未启用合并选项,此合并请求无法被合并。 pulls.no_merge_helper=在仓库设置中启用合并选项或者手工合并请求。 pulls.no_merge_wip=这个合并请求无法合并,因为被标记为尚未完成的工作。 -pulls.no_merge_not_ready=此拉取请求尚未准备好合并,请检查审核状态和状态检查。 -pulls.no_merge_access=您无权合并此拉取请求。 +pulls.no_merge_not_ready=此合并请求尚未准备好合并,请检查审核状态和状态检查。 +pulls.no_merge_access=您无权合并此合并请求。 pulls.merge_pull_request=创建合并提交 pulls.rebase_merge_pull_request=变基后快进 pulls.rebase_merge_commit_pull_request=变基后创建合并提交 @@ -1876,6 +1877,7 @@ pulls.cmd_instruction_checkout_title=检出 pulls.cmd_instruction_checkout_desc=从你的仓库中检出一个新的分支并测试变更。 pulls.cmd_instruction_merge_title=合并 pulls.cmd_instruction_merge_desc=合并变更并更新到 Gitea 上 +pulls.cmd_instruction_merge_warning=警告:此操作不能合并该合并请求,因为“自动检测手动合并”未启用 pulls.clear_merge_message=清除合并信息 pulls.clear_merge_message_hint=清除合并消息只会删除提交消息内容,并保留生成的 git 附加内容,如“Co-Authored-By …”。 @@ -1888,11 +1890,11 @@ pulls.auto_merge_cancel_schedule=取消自动合并 pulls.auto_merge_not_scheduled=此合并请求没有计划自动合并。 pulls.auto_merge_canceled_schedule=此合并请求的自动合并已取消。 -pulls.auto_merge_newly_scheduled_comment=`已于 %[1]s 设置此拉取请求在所有检查成功后自动合并` +pulls.auto_merge_newly_scheduled_comment=`已于 %[1]s 设置此合并请求在所有检查成功后自动合并` pulls.auto_merge_canceled_schedule_comment=`已于 %[1]s 取消了自动合并设置 ` -pulls.delete.title=删除此拉取请求? -pulls.delete.text=你真的要删除这个拉取请求吗? (这将永久删除所有内容。如果你打算将内容存档,请考虑关闭它) +pulls.delete.title=删除此合并请求? +pulls.delete.text=你真的要删除这个合并请求吗? (这将永久删除所有内容。如果你打算将内容存档,请考虑关闭它) pulls.recently_pushed_new_branches=您已经于%[2]s推送了分支 %[1]s @@ -2125,7 +2127,7 @@ settings.allow_only_contributors_to_track_time=仅允许成员跟踪时间 settings.pulls_desc=启用合并请求 settings.pulls.ignore_whitespace=忽略空白冲突 settings.pulls.enable_autodetect_manual_merge=启用自动检测手动合并 (注意:在某些特殊情况下可能发生错误判断) -settings.pulls.allow_rebase_update=允许通过变基更新拉取请求分支 +settings.pulls.allow_rebase_update=允许通过变基更新合并请求分支 settings.pulls.default_delete_branch_after_merge=默认合并后删除合并请求分支 settings.pulls.default_allow_edits_from_maintainers=默认开启允许维护者编辑 settings.releases_desc=启用发布 @@ -2375,7 +2377,7 @@ settings.protect_status_check_matched=匹配 settings.protect_invalid_status_check_pattern=无效的状态检查规则:“%s”。 settings.protect_no_valid_status_check_patterns=没有有效的状态检查规则。 settings.protect_required_approvals=所需的批准: -settings.protect_required_approvals_desc=只允许合并有足够审核人数的拉取请求。 +settings.protect_required_approvals_desc=只允许合并有足够审核人数的合并请求。 settings.dismiss_stale_approvals=取消过时的批准 settings.dismiss_stale_approvals_desc=当新的提交更改合并请求内容被推送到分支时,旧的批准将被撤销。 settings.ignore_stale_approvals=忽略过期批准 @@ -2400,7 +2402,7 @@ settings.block_rejected_reviews=拒绝审核阻止了此合并 settings.block_rejected_reviews_desc=如果官方审查人员要求作出改动,即使有足够的批准,合并也不允许。 settings.block_on_official_review_requests=有官方审核阻止了代码合并 settings.block_on_official_review_requests_desc=处于评审状态时,即使有足够的批准,也不能合并。 -settings.block_outdated_branch=如果拉取请求已经过时,阻止合并 +settings.block_outdated_branch=如果合并请求已经过时,阻止合并 settings.block_outdated_branch_desc=当头部分支落后基础分支时,不能合并。 settings.default_branch_desc=请选择一个默认的分支用于合并请求和提交: settings.merge_style_desc=合并方式 From 39d2fdefaf0dd42aa5e3ee8d3ea0a84b40c005f5 Mon Sep 17 00:00:00 2001 From: a1012112796 <1012112796@qq.com> Date: Tue, 27 Aug 2024 10:54:12 +0800 Subject: [PATCH 6/9] Split org Propfile README to a new tab `overview` (#31373) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit like user profile, add a new overviw tab to show profile READEME when it is exist. replace #31349 (another solution option) example view: ![屏幕截图 2024-06-14 094116](https://github.com/go-gitea/gitea/assets/25342410/3303a1f2-ae02-48e0-9519-7fa11e65657f) ![屏幕截图 2024-06-14 094101](https://github.com/go-gitea/gitea/assets/25342410/7a4a5a48-dc2b-4ad4-b2a2-9ea4ab5d5808) --------- Signed-off-by: a1012112796 <1012112796@qq.com> --- routers/web/org/home.go | 75 ++++++++++++++++++------------- routers/web/org/members.go | 4 +- routers/web/org/teams.go | 4 +- routers/web/shared/user/header.go | 12 +++++ routers/web/web.go | 2 + templates/org/menu.tmpl | 7 ++- 6 files changed, 67 insertions(+), 37 deletions(-) diff --git a/routers/web/org/home.go b/routers/web/org/home.go index 77d49f5b78..366a7b20de 100644 --- a/routers/web/org/home.go +++ b/routers/web/org/home.go @@ -13,7 +13,6 @@ import ( "code.gitea.io/gitea/models/organization" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/base" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" @@ -42,6 +41,14 @@ func Home(ctx *context.Context) { return } + home(ctx, false) +} + +func Repositories(ctx *context.Context) { + home(ctx, true) +} + +func home(ctx *context.Context, viewRepositories bool) { org := ctx.Org.Organization ctx.Data["PageIsUserProfile"] = true @@ -101,10 +108,34 @@ func Home(ctx *context.Context) { private := ctx.FormOptionalBool("private") ctx.Data["IsPrivate"] = private + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + + opts := &organization.FindOrgMembersOpts{ + OrgID: org.ID, + PublicOnly: ctx.Org.PublicMemberOnly, + ListOptions: db.ListOptions{Page: 1, PageSize: 25}, + } + members, _, err := organization.FindOrgMembers(ctx, opts) + if err != nil { + ctx.ServerError("FindOrgMembers", err) + return + } + ctx.Data["Members"] = members + ctx.Data["Teams"] = ctx.Org.Teams + ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull + ctx.Data["ShowMemberAndTeamTab"] = ctx.Org.IsMember || len(members) > 0 + + if !prepareOrgProfileReadme(ctx, viewRepositories) { + ctx.Data["PageIsViewRepositories"] = true + } + var ( repos []*repo_model.Repository count int64 - err error ) repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ @@ -129,29 +160,8 @@ func Home(ctx *context.Context) { return } - opts := &organization.FindOrgMembersOpts{ - OrgID: org.ID, - PublicOnly: ctx.Org.PublicMemberOnly, - ListOptions: db.ListOptions{Page: 1, PageSize: 25}, - } - members, _, err := organization.FindOrgMembers(ctx, opts) - if err != nil { - ctx.ServerError("FindOrgMembers", err) - return - } - ctx.Data["Repos"] = repos ctx.Data["Total"] = count - ctx.Data["Members"] = members - ctx.Data["Teams"] = ctx.Org.Teams - ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull - ctx.Data["PageIsViewRepositories"] = true - - err = shared_user.LoadHeaderCount(ctx) - if err != nil { - ctx.ServerError("LoadHeaderCount", err) - return - } pager := context.NewPagination(int(count), setting.UI.User.RepoPagingNum, page, 5) pager.SetDefaultParams(ctx) @@ -173,18 +183,16 @@ func Home(ctx *context.Context) { } ctx.Data["Page"] = pager - ctx.Data["ShowMemberAndTeamTab"] = ctx.Org.IsMember || len(members) > 0 - - profileDbRepo, profileGitRepo, profileReadmeBlob, profileClose := shared_user.FindUserProfileReadme(ctx, ctx.Doer) - defer profileClose() - prepareOrgProfileReadme(ctx, profileGitRepo, profileDbRepo, profileReadmeBlob) - ctx.HTML(http.StatusOK, tplOrgHome) } -func prepareOrgProfileReadme(ctx *context.Context, profileGitRepo *git.Repository, profileDbRepo *repo_model.Repository, profileReadme *git.Blob) { - if profileGitRepo == nil || profileReadme == nil { - return +func prepareOrgProfileReadme(ctx *context.Context, viewRepositories bool) bool { + profileDbRepo, profileGitRepo, profileReadme, profileClose := shared_user.FindUserProfileReadme(ctx, ctx.Doer) + defer profileClose() + ctx.Data["HasProfileReadme"] = profileReadme != nil + + if profileGitRepo == nil || profileReadme == nil || viewRepositories { + return false } if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil { @@ -206,4 +214,7 @@ func prepareOrgProfileReadme(ctx *context.Context, profileGitRepo *git.Repositor ctx.Data["ProfileReadme"] = profileContent } } + + ctx.Data["PageIsViewOverview"] = true + return true } diff --git a/routers/web/org/members.go b/routers/web/org/members.go index 58d0b9b8c4..8ff75b0651 100644 --- a/routers/web/org/members.go +++ b/routers/web/org/members.go @@ -54,9 +54,9 @@ func Members(ctx *context.Context) { return } - err = shared_user.LoadHeaderCount(ctx) + err = shared_user.RenderOrgHeader(ctx) if err != nil { - ctx.ServerError("LoadHeaderCount", err) + ctx.ServerError("RenderOrgHeader", err) return } diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go index aaac1177ae..31b9601ce7 100644 --- a/routers/web/org/teams.go +++ b/routers/web/org/teams.go @@ -59,9 +59,9 @@ func Teams(ctx *context.Context) { } ctx.Data["Teams"] = ctx.Org.Teams - err := shared_user.LoadHeaderCount(ctx) + err := shared_user.RenderOrgHeader(ctx) if err != nil { - ctx.ServerError("LoadHeaderCount", err) + ctx.ServerError("RenderOrgHeader", err) return } diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go index 7531e1ba26..ef111cff80 100644 --- a/routers/web/shared/user/header.go +++ b/routers/web/shared/user/header.go @@ -162,3 +162,15 @@ func LoadHeaderCount(ctx *context.Context) error { return nil } + +func RenderOrgHeader(ctx *context.Context) error { + if err := LoadHeaderCount(ctx); err != nil { + return err + } + + _, _, profileReadmeBlob, profileClose := FindUserProfileReadme(ctx, ctx.Doer) + defer profileClose() + ctx.Data["HasProfileReadme"] = profileReadmeBlob != nil + + return nil +} diff --git a/routers/web/web.go b/routers/web/web.go index 4e917b5ede..98b4252cb9 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -995,6 +995,8 @@ func registerRoutes(m *web.Router) { }, context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead)) } + m.Get("/repositories", org.Repositories) + m.Group("/projects", func() { m.Group("", func() { m.Get("", org.Projects) diff --git a/templates/org/menu.tmpl b/templates/org/menu.tmpl index 698a9559c5..29238f8d6b 100644 --- a/templates/org/menu.tmpl +++ b/templates/org/menu.tmpl @@ -1,7 +1,12 @@
- + {{if .HasProfileReadme}} + + {{svg "octicon-info"}} {{ctx.Locale.Tr "user.overview"}} + + {{end}} + {{svg "octicon-repo"}} {{ctx.Locale.Tr "user.repositories"}} {{if .RepoCount}}
{{.RepoCount}}
From 7207d93f01f0cd796e1b9e0156798990846d894b Mon Sep 17 00:00:00 2001 From: silverwind Date: Wed, 28 Aug 2024 18:32:38 +0200 Subject: [PATCH 7/9] Fix a number of Typescript issues (#31877) Typescript error count is reduced from 633 to 540 with this. No runtime changes except in test code. --- types.d.ts | 52 +++++++++++++++++++++++++------- web_src/js/htmx.ts | 9 +++--- web_src/js/index.ts | 4 +-- web_src/js/render/ansi.ts | 4 +-- web_src/js/standalone/swagger.ts | 2 +- web_src/js/svg.test.ts | 2 +- web_src/js/types.ts | 7 +++++ web_src/js/utils.test.ts | 15 ++++----- web_src/js/utils.ts | 47 +++++++++++++++-------------- web_src/js/utils/color.ts | 8 ++--- web_src/js/utils/dom.ts | 4 +-- web_src/js/utils/image.ts | 16 ++++++++-- web_src/js/utils/match.ts | 8 ++--- web_src/js/utils/time.ts | 24 ++++++++++----- web_src/js/utils/url.ts | 6 ++-- web_src/js/vitest.setup.ts | 12 ++++++-- 16 files changed, 141 insertions(+), 79 deletions(-) diff --git a/types.d.ts b/types.d.ts index a8dc09e064..68081af606 100644 --- a/types.d.ts +++ b/types.d.ts @@ -10,22 +10,52 @@ declare module '*.css' { declare let __webpack_public_path__: string; -interface Window { - config: import('./web_src/js/types.ts').Config; - $: typeof import('@types/jquery'), - jQuery: typeof import('@types/jquery'), - htmx: typeof import('htmx.org'), - _globalHandlerErrors: Array & { - _inited: boolean, - push: (e: ErrorEvent & PromiseRejectionEvent) => void | number, - }, -} - declare module 'htmx.org/dist/htmx.esm.js' { const value = await import('htmx.org'); export default value; } +declare module 'uint8-to-base64' { + export function encode(arrayBuffer: ArrayBuffer): string; + export function decode(base64str: string): ArrayBuffer; +} + +declare module 'swagger-ui-dist/swagger-ui-es-bundle.js' { + const value = await import('swagger-ui-dist'); + export default value.SwaggerUIBundle; +} + +interface JQuery { + api: any, // fomantic + areYouSure: any, // jquery.are-you-sure + dimmer: any, // fomantic + dropdown: any; // fomantic + modal: any; // fomantic + tab: any; // fomantic + transition: any, // fomantic +} + +interface JQueryStatic { + api: any, // fomantic +} + interface Element { _tippy: import('tippy.js').Instance; } + +type Writable = { -readonly [K in keyof T]: T[K] }; + +interface Window { + config: import('./web_src/js/types.ts').Config; + $: typeof import('@types/jquery'), + jQuery: typeof import('@types/jquery'), + htmx: Omit & { + config?: Writable, + }, + ui?: any, + _globalHandlerErrors: Array & { + _inited: boolean, + push: (e: ErrorEvent & PromiseRejectionEvent) => void | number, + }, + __webpack_public_path__: string; +} diff --git a/web_src/js/htmx.ts b/web_src/js/htmx.ts index bfc2147736..d4f317ee5a 100644 --- a/web_src/js/htmx.ts +++ b/web_src/js/htmx.ts @@ -1,20 +1,21 @@ import {showErrorToast} from './modules/toast.ts'; +import 'idiomorph/dist/idiomorph-ext.js'; // https://github.com/bigskysoftware/idiomorph#htmx +import type {HtmxResponseInfo} from 'htmx.org'; -// https://github.com/bigskysoftware/idiomorph#htmx -import 'idiomorph/dist/idiomorph-ext.js'; +type HtmxEvent = Event & {detail: HtmxResponseInfo}; // https://htmx.org/reference/#config window.htmx.config.requestClass = 'is-loading'; window.htmx.config.scrollIntoViewOnBoost = false; // https://htmx.org/events/#htmx:sendError -document.body.addEventListener('htmx:sendError', (event) => { +document.body.addEventListener('htmx:sendError', (event: HtmxEvent) => { // TODO: add translations showErrorToast(`Network error when calling ${event.detail.requestConfig.path}`); }); // https://htmx.org/events/#htmx:responseError -document.body.addEventListener('htmx:responseError', (event) => { +document.body.addEventListener('htmx:responseError', (event: HtmxEvent) => { // TODO: add translations showErrorToast(`Error ${event.detail.xhr.status} when calling ${event.detail.requestConfig.path}`); }); diff --git a/web_src/js/index.ts b/web_src/js/index.ts index 2bdc8655fe..db678a25ba 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -98,12 +98,12 @@ initGiteaFomantic(); initDirAuto(); initSubmitEventPolyfill(); -function callInitFunctions(functions) { +function callInitFunctions(functions: (() => any)[]) { // Start performance trace by accessing a URL by "https://localhost/?_ui_performance_trace=1" or "https://localhost/?key=value&_ui_performance_trace=1" // It is a quick check, no side effect so no need to do slow URL parsing. const initStart = performance.now(); if (window.location.search.includes('_ui_performance_trace=1')) { - let results = []; + let results: {name: string, dur: number}[] = []; for (const func of functions) { const start = performance.now(); func(); diff --git a/web_src/js/render/ansi.ts b/web_src/js/render/ansi.ts index bb622dd1eb..685e916c9a 100644 --- a/web_src/js/render/ansi.ts +++ b/web_src/js/render/ansi.ts @@ -1,12 +1,12 @@ import {AnsiUp} from 'ansi_up'; -const replacements = [ +const replacements: Array<[RegExp, string]> = [ [/\x1b\[\d+[A-H]/g, ''], // Move cursor, treat them as no-op [/\x1b\[\d?[JK]/g, '\r'], // Erase display/line, treat them as a Carriage Return ]; // render ANSI to HTML -export function renderAnsi(line) { +export function renderAnsi(line: string): string { // create a fresh ansi_up instance because otherwise previous renders can influence // the output of future renders, because ansi_up is stateful and remembers things like // unclosed opening tags for colors. diff --git a/web_src/js/standalone/swagger.ts b/web_src/js/standalone/swagger.ts index 2928813167..63b676b2ea 100644 --- a/web_src/js/standalone/swagger.ts +++ b/web_src/js/standalone/swagger.ts @@ -8,7 +8,7 @@ window.addEventListener('load', async () => { // Make the page's protocol be at the top of the schemes list const proto = window.location.protocol.slice(0, -1); - spec.schemes.sort((a, b) => { + spec.schemes.sort((a: string, b: string) => { if (a === proto) return -1; if (b === proto) return 1; return 0; diff --git a/web_src/js/svg.test.ts b/web_src/js/svg.test.ts index 015758a271..7f3e0496ec 100644 --- a/web_src/js/svg.test.ts +++ b/web_src/js/svg.test.ts @@ -17,7 +17,7 @@ test('svgParseOuterInner', () => { test('SvgIcon', () => { const root = document.createElement('div'); createApp({render: () => h(SvgIcon, {name: 'octicon-link', size: 24, class: 'base', className: 'extra'})}).mount(root); - const node = root.firstChild; + const node = root.firstChild as Element; expect(node.nodeName).toEqual('svg'); expect(node.getAttribute('width')).toEqual('24'); expect(node.getAttribute('height')).toEqual('24'); diff --git a/web_src/js/types.ts b/web_src/js/types.ts index 3bd1c072a8..f3ac305162 100644 --- a/web_src/js/types.ts +++ b/web_src/js/types.ts @@ -29,3 +29,10 @@ export type RequestData = string | FormData | URLSearchParams; export type RequestOpts = { data?: RequestData, } & RequestInit; + +export type IssueData = { + owner: string, + repo: string, + type: string, + index: string, +} diff --git a/web_src/js/utils.test.ts b/web_src/js/utils.test.ts index 4c09f49ba8..55896706ff 100644 --- a/web_src/js/utils.test.ts +++ b/web_src/js/utils.test.ts @@ -95,23 +95,20 @@ test('toAbsoluteUrl', () => { }); test('encodeURLEncodedBase64, decodeURLEncodedBase64', () => { - // TextEncoder is Node.js API while Uint8Array is jsdom API and their outputs are not - // structurally comparable, so we convert to array to compare. The conversion can be - // removed once https://github.com/jsdom/jsdom/issues/2524 is resolved. const encoder = new TextEncoder(); const uint8array = encoder.encode.bind(encoder); expect(encodeURLEncodedBase64(uint8array('AA?'))).toEqual('QUE_'); // standard base64: "QUE/" expect(encodeURLEncodedBase64(uint8array('AA~'))).toEqual('QUF-'); // standard base64: "QUF+" - expect(Array.from(decodeURLEncodedBase64('QUE/'))).toEqual(Array.from(uint8array('AA?'))); - expect(Array.from(decodeURLEncodedBase64('QUF+'))).toEqual(Array.from(uint8array('AA~'))); - expect(Array.from(decodeURLEncodedBase64('QUE_'))).toEqual(Array.from(uint8array('AA?'))); - expect(Array.from(decodeURLEncodedBase64('QUF-'))).toEqual(Array.from(uint8array('AA~'))); + expect(new Uint8Array(decodeURLEncodedBase64('QUE/'))).toEqual(uint8array('AA?')); + expect(new Uint8Array(decodeURLEncodedBase64('QUF+'))).toEqual(uint8array('AA~')); + expect(new Uint8Array(decodeURLEncodedBase64('QUE_'))).toEqual(uint8array('AA?')); + expect(new Uint8Array(decodeURLEncodedBase64('QUF-'))).toEqual(uint8array('AA~')); expect(encodeURLEncodedBase64(uint8array('a'))).toEqual('YQ'); // standard base64: "YQ==" - expect(Array.from(decodeURLEncodedBase64('YQ'))).toEqual(Array.from(uint8array('a'))); - expect(Array.from(decodeURLEncodedBase64('YQ=='))).toEqual(Array.from(uint8array('a'))); + expect(new Uint8Array(decodeURLEncodedBase64('YQ'))).toEqual(uint8array('a')); + expect(new Uint8Array(decodeURLEncodedBase64('YQ=='))).toEqual(uint8array('a')); }); test('file detection', () => { diff --git a/web_src/js/utils.ts b/web_src/js/utils.ts index 2d40fa20a8..c52bf500d4 100644 --- a/web_src/js/utils.ts +++ b/web_src/js/utils.ts @@ -1,13 +1,14 @@ import {encode, decode} from 'uint8-to-base64'; +import type {IssueData} from './types.ts'; // transform /path/to/file.ext to file.ext -export function basename(path) { +export function basename(path: string): string { const lastSlashIndex = path.lastIndexOf('/'); return lastSlashIndex < 0 ? path : path.substring(lastSlashIndex + 1); } // transform /path/to/file.ext to .ext -export function extname(path) { +export function extname(path: string): string { const lastSlashIndex = path.lastIndexOf('/'); const lastPointIndex = path.lastIndexOf('.'); if (lastSlashIndex > lastPointIndex) return ''; @@ -15,54 +16,54 @@ export function extname(path) { } // test whether a variable is an object -export function isObject(obj) { +export function isObject(obj: any): boolean { return Object.prototype.toString.call(obj) === '[object Object]'; } // returns whether a dark theme is enabled -export function isDarkTheme() { +export function isDarkTheme(): boolean { const style = window.getComputedStyle(document.documentElement); return style.getPropertyValue('--is-dark-theme').trim().toLowerCase() === 'true'; } // strip from a string -export function stripTags(text) { +export function stripTags(text: string): string { return text.replace(/<[^>]*>?/g, ''); } -export function parseIssueHref(href) { +export function parseIssueHref(href: string): IssueData { const path = (href || '').replace(/[#?].*$/, ''); const [_, owner, repo, type, index] = /([^/]+)\/([^/]+)\/(issues|pulls)\/([0-9]+)/.exec(path) || []; return {owner, repo, type, index}; } // parse a URL, either relative '/path' or absolute 'https://localhost/path' -export function parseUrl(str) { +export function parseUrl(str: string): URL { return new URL(str, str.startsWith('http') ? undefined : window.location.origin); } // return current locale chosen by user -export function getCurrentLocale() { +export function getCurrentLocale(): string { return document.documentElement.lang; } // given a month (0-11), returns it in the documents language -export function translateMonth(month) { +export function translateMonth(month: number) { return new Date(Date.UTC(2022, month, 12)).toLocaleString(getCurrentLocale(), {month: 'short', timeZone: 'UTC'}); } // given a weekday (0-6, Sunday to Saturday), returns it in the documents language -export function translateDay(day) { +export function translateDay(day: number) { return new Date(Date.UTC(2022, 7, day)).toLocaleString(getCurrentLocale(), {weekday: 'short', timeZone: 'UTC'}); } // convert a Blob to a DataURI -export function blobToDataURI(blob) { +export function blobToDataURI(blob: Blob): Promise { return new Promise((resolve, reject) => { try { const reader = new FileReader(); reader.addEventListener('load', (e) => { - resolve(e.target.result); + resolve(e.target.result as string); }); reader.addEventListener('error', () => { reject(new Error('FileReader failed')); @@ -75,7 +76,7 @@ export function blobToDataURI(blob) { } // convert image Blob to another mime-type format. -export function convertImage(blob, mime) { +export function convertImage(blob: Blob, mime: string): Promise { return new Promise(async (resolve, reject) => { try { const img = new Image(); @@ -104,7 +105,7 @@ export function convertImage(blob, mime) { }); } -export function toAbsoluteUrl(url) { +export function toAbsoluteUrl(url: string): string { if (url.startsWith('http://') || url.startsWith('https://')) { return url; } @@ -118,15 +119,15 @@ export function toAbsoluteUrl(url) { } // Encode an ArrayBuffer into a URLEncoded base64 string. -export function encodeURLEncodedBase64(arrayBuffer) { +export function encodeURLEncodedBase64(arrayBuffer: ArrayBuffer): string { return encode(arrayBuffer) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); } -// Decode a URLEncoded base64 to an ArrayBuffer string. -export function decodeURLEncodedBase64(base64url) { +// Decode a URLEncoded base64 to an ArrayBuffer. +export function decodeURLEncodedBase64(base64url: string): ArrayBuffer { return decode(base64url .replace(/_/g, '/') .replace(/-/g, '+')); @@ -135,20 +136,22 @@ export function decodeURLEncodedBase64(base64url) { const domParser = new DOMParser(); const xmlSerializer = new XMLSerializer(); -export function parseDom(text, contentType) { +export function parseDom(text: string, contentType: DOMParserSupportedType): Document { return domParser.parseFromString(text, contentType); } -export function serializeXml(node) { +export function serializeXml(node: Element | Node): string { return xmlSerializer.serializeToString(node); } -export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); +export function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} -export function isImageFile({name, type}) { +export function isImageFile({name, type}: {name: string, type?: string}): boolean { return /\.(jpe?g|png|gif|webp|svg|heic)$/i.test(name || '') || type?.startsWith('image/'); } -export function isVideoFile({name, type}) { +export function isVideoFile({name, type}: {name: string, type?: string}): boolean { return /\.(mpe?g|mp4|mkv|webm)$/i.test(name || '') || type?.startsWith('video/'); } diff --git a/web_src/js/utils/color.ts b/web_src/js/utils/color.ts index 3ee32395fb..a0409353d2 100644 --- a/web_src/js/utils/color.ts +++ b/web_src/js/utils/color.ts @@ -3,23 +3,23 @@ import type {ColorInput} from 'tinycolor2'; // Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance // Keep this in sync with modules/util/color.go -function getRelativeLuminance(color: ColorInput) { +function getRelativeLuminance(color: ColorInput): number { const {r, g, b} = tinycolor(color).toRgb(); return (0.2126729 * r + 0.7151522 * g + 0.072175 * b) / 255; } -function useLightText(backgroundColor: ColorInput) { +function useLightText(backgroundColor: ColorInput): boolean { return getRelativeLuminance(backgroundColor) < 0.453; } // Given a background color, returns a black or white foreground color that the highest // contrast ratio. In the future, the APCA contrast function, or CSS `contrast-color` will be better. // https://github.com/color-js/color.js/blob/eb7b53f7a13bb716ec8b28c7a56f052cd599acd9/src/contrast/APCA.js#L42 -export function contrastColor(backgroundColor: ColorInput) { +export function contrastColor(backgroundColor: ColorInput): string { return useLightText(backgroundColor) ? '#fff' : '#000'; } -function resolveColors(obj: Record) { +function resolveColors(obj: Record): Record { const styles = window.getComputedStyle(document.documentElement); const getColor = (name: string) => styles.getPropertyValue(name).trim(); return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, getColor(value)])); diff --git a/web_src/js/utils/dom.ts b/web_src/js/utils/dom.ts index 5fc2183194..7dd63ecbbf 100644 --- a/web_src/js/utils/dom.ts +++ b/web_src/js/utils/dom.ts @@ -266,10 +266,8 @@ export function initSubmitEventPolyfill() { /** * Check if an element is visible, equivalent to jQuery's `:visible` pseudo. * Note: This function doesn't account for all possible visibility scenarios. - * @param {HTMLElement} element The element to check. - * @returns {boolean} True if the element is visible. */ -export function isElemVisible(element: HTMLElement) { +export function isElemVisible(element: HTMLElement): boolean { if (!element) return false; return Boolean(element.offsetWidth || element.offsetHeight || element.getClientRects().length); diff --git a/web_src/js/utils/image.ts b/web_src/js/utils/image.ts index c71d715941..558a63f22e 100644 --- a/web_src/js/utils/image.ts +++ b/web_src/js/utils/image.ts @@ -1,6 +1,11 @@ -export async function pngChunks(blob) { +type PngChunk = { + name: string, + data: Uint8Array, +} + +export async function pngChunks(blob: Blob): Promise { const uint8arr = new Uint8Array(await blob.arrayBuffer()); - const chunks = []; + const chunks: PngChunk[] = []; if (uint8arr.length < 12) return chunks; const view = new DataView(uint8arr.buffer); if (view.getBigUint64(0) !== 9894494448401390090n) return chunks; @@ -19,9 +24,14 @@ export async function pngChunks(blob) { return chunks; } +type ImageInfo = { + width?: number, + dppx?: number, +} + // decode a image and try to obtain width and dppx. It will never throw but instead // return default values. -export async function imageInfo(blob) { +export async function imageInfo(blob: Blob): Promise { let width = 0, dppx = 1; // dppx: 1 dot per pixel for non-HiDPI screens if (blob.type === 'image/png') { // only png is supported currently diff --git a/web_src/js/utils/match.ts b/web_src/js/utils/match.ts index 17fdfed113..0ce7e2b1a2 100644 --- a/web_src/js/utils/match.ts +++ b/web_src/js/utils/match.ts @@ -2,17 +2,17 @@ import emojis from '../../../assets/emoji.json'; const maxMatches = 6; -function sortAndReduce(map) { +function sortAndReduce(map: Map) { const sortedMap = new Map(Array.from(map.entries()).sort((a, b) => a[1] - b[1])); return Array.from(sortedMap.keys()).slice(0, maxMatches); } -export function matchEmoji(queryText) { +export function matchEmoji(queryText: string): string[] { const query = queryText.toLowerCase().replaceAll('_', ' '); if (!query) return emojis.slice(0, maxMatches).map((e) => e.aliases[0]); // results is a map of weights, lower is better - const results = new Map(); + const results = new Map(); for (const {aliases} of emojis) { const mainAlias = aliases[0]; for (const [aliasIndex, alias] of aliases.entries()) { @@ -27,7 +27,7 @@ export function matchEmoji(queryText) { return sortAndReduce(results); } -export function matchMention(queryText) { +export function matchMention(queryText: string): string[] { const query = queryText.toLowerCase(); // results is a map of weights, lower is better diff --git a/web_src/js/utils/time.ts b/web_src/js/utils/time.ts index d3a986e736..5251386230 100644 --- a/web_src/js/utils/time.ts +++ b/web_src/js/utils/time.ts @@ -1,16 +1,17 @@ import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc.js'; import {getCurrentLocale} from '../utils.ts'; +import type {ConfigType} from 'dayjs'; dayjs.extend(utc); /** * Returns an array of millisecond-timestamps of start-of-week days (Sundays) * - * @param startConfig The start date. Can take any type that `Date` accepts. - * @param endConfig The end date. Can take any type that `Date` accepts. + * @param startDate The start date. Can take any type that dayjs accepts. + * @param endDate The end date. Can take any type that dayjs accepts. */ -export function startDaysBetween(startDate, endDate) { +export function startDaysBetween(startDate: ConfigType, endDate: ConfigType): number[] { const start = dayjs.utc(startDate); const end = dayjs.utc(endDate); @@ -21,7 +22,7 @@ export function startDaysBetween(startDate, endDate) { current = current.add(1, 'day'); } - const startDays = []; + const startDays: number[] = []; while (current.isBefore(end)) { startDays.push(current.valueOf()); current = current.add(1, 'week'); @@ -30,7 +31,7 @@ export function startDaysBetween(startDate, endDate) { return startDays; } -export function firstStartDateAfterDate(inputDate) { +export function firstStartDateAfterDate(inputDate: Date): number { if (!(inputDate instanceof Date)) { throw new Error('Invalid date'); } @@ -41,7 +42,14 @@ export function firstStartDateAfterDate(inputDate) { return resultDate.valueOf(); } -export function fillEmptyStartDaysWithZeroes(startDays, data) { +type DayData = { + week: number, + additions: number, + deletions: number, + commits: number, +} + +export function fillEmptyStartDaysWithZeroes(startDays: number[], data: DayData): DayData[] { const result = {}; for (const startDay of startDays) { @@ -51,11 +59,11 @@ export function fillEmptyStartDaysWithZeroes(startDays, data) { return Object.values(result); } -let dateFormat; +let dateFormat: Intl.DateTimeFormat; // format a Date object to document's locale, but with 24h format from user's current locale because this // option is a personal preference of the user, not something that the document's locale should dictate. -export function formatDatetime(date) { +export function formatDatetime(date: Date | number): string { if (!dateFormat) { // TODO: replace `hour12` with `Intl.Locale.prototype.getHourCycles` once there is broad browser support dateFormat = new Intl.DateTimeFormat(getCurrentLocale(), { diff --git a/web_src/js/utils/url.ts b/web_src/js/utils/url.ts index 470ece31b0..c5a28774a9 100644 --- a/web_src/js/utils/url.ts +++ b/web_src/js/utils/url.ts @@ -1,12 +1,12 @@ -export function pathEscapeSegments(s) { +export function pathEscapeSegments(s: string): string { return s.split('/').map(encodeURIComponent).join('/'); } -function stripSlash(url) { +function stripSlash(url: string): string { return url.endsWith('/') ? url.slice(0, -1) : url; } -export function isUrl(url) { +export function isUrl(url: string): boolean { try { return stripSlash((new URL(url).href)).trim() === stripSlash(url).trim(); } catch { diff --git a/web_src/js/vitest.setup.ts b/web_src/js/vitest.setup.ts index 6fb0f5dc8f..68e300f551 100644 --- a/web_src/js/vitest.setup.ts +++ b/web_src/js/vitest.setup.ts @@ -1,10 +1,16 @@ window.__webpack_public_path__ = ''; window.config = { + appUrl: 'http://localhost:3000/', + appSubUrl: '', + assetVersionEncoded: '', + assetUrlPrefix: '', + runModeIsProd: true, + customEmojis: {}, csrfToken: 'test-csrf-token-123456', pageData: {}, - i18n: {}, - appSubUrl: '', + notificationSettings: {}, + enableTimeTracking: true, mentionValues: [ {key: 'user1 User 1', value: 'user1', name: 'user1', fullname: 'User 1', avatar: 'https://avatar1.com'}, {key: 'user2 User 2', value: 'user2', name: 'user2', fullname: 'User 2', avatar: 'https://avatar2.com'}, @@ -14,4 +20,6 @@ window.config = { {key: 'org6 User 6', value: 'org6', name: 'org6', fullname: 'User 6', avatar: 'https://avatar6.com'}, {key: 'org7 User 7', value: 'org7', name: 'org7', fullname: 'User 7', avatar: 'https://avatar7.com'}, ], + mermaidMaxSourceCharacters: 5000, + i18n: {}, }; From bc0977f1c99fb2a61b885d6692179395d6cc12c2 Mon Sep 17 00:00:00 2001 From: Jason Song Date: Thu, 29 Aug 2024 11:48:21 +0800 Subject: [PATCH 8/9] Refactor globallock (#31933) Follow #31908. The main refactor is that it has removed the returned context of `Lock`. The returned context of `Lock` in old code is to provide a way to let callers know that they have lost the lock. But in most cases, callers shouldn't cancel what they are doing even it has lost the lock. And the design would confuse developers and make them use it incorrectly. See the discussion history: https://github.com/go-gitea/gitea/pull/31813#discussion_r1732041513 and https://github.com/go-gitea/gitea/pull/31813#discussion_r1734078998 It's a breaking change, but since the new module hasn't been used yet, I think it's OK to not add the `pr/breaking` label. ## Design principles It's almost copied from #31908, but with some changes. ### Use spinlock even in memory implementation (unchanged) In actual use cases, users may cancel requests. `sync.Mutex` will block the goroutine until the lock is acquired even if the request is canceled. And the spinlock is more suitable for this scenario since it's possible to give up the lock acquisition. Although the spinlock consumes more CPU resources, I think it's acceptable in most cases. ### Do not expose the mutex to callers (unchanged) If we expose the mutex to callers, it's possible for callers to reuse the mutex, which causes more complexity. For example: ```go lock := GetLocker(key) lock.Lock() // ... // even if the lock is unlocked, we cannot GC the lock, // since the caller may still use it again. lock.Unlock() lock.Lock() // ... lock.Unlock() // callers have to GC the lock manually. RemoveLocker(key) ``` That's why https://github.com/go-gitea/gitea/pull/31813#discussion_r1721200549 In this PR, we only expose `ReleaseFunc` to callers. So callers just need to call `ReleaseFunc` to release the lock, and do not need to care about the lock's lifecycle. ```go release, err := locker.Lock(ctx, key) if err != nil { return err } // ... release() // if callers want to lock again, they have to re-acquire the lock. release, err := locker.Lock(ctx, key) // ... ``` In this way, it's also much easier for redis implementation to extend the mutex automatically, so that callers do not need to care about the lock's lifecycle. See also https://github.com/go-gitea/gitea/pull/31813#discussion_r1722659743 ### Use "release" instead of "unlock" (unchanged) For "unlock", it has the meaning of "unlock an acquired lock". So it's not acceptable to call "unlock" when failed to acquire the lock, or call "unlock" multiple times. It causes more complexity for callers to decide whether to call "unlock" or not. So we use "release" instead of "unlock" to make it clear. Whether the lock is acquired or not, callers can always call "release", and it's also safe to call "release" multiple times. But the code DO NOT expect callers to not call "release" after acquiring the lock. If callers forget to call "release", it will cause resource leak. That's why it's always safe to call "release" without extra checks: to avoid callers to forget to call it. ### Acquired locks could be lost, but the callers shouldn't stop Unlike `sync.Mutex` which will be locked forever once acquired until calling `Unlock`, for distributed lock, the acquired lock could be lost. For example, the caller has acquired the lock, and it holds the lock for a long time since auto-extending is working for redis. However, it lost the connection to the redis server, and it's impossible to extend the lock anymore. In #31908, it will cancel the context to make the operation stop, but it's not safe. Many operations are not revert-able. If they have been interrupted, then the instance goes corrupted. So `Lock` won't return `ctx` anymore in this PR. ### Multiple ways to use the lock 1. Regular way ```go release, err := Lock(ctx, key) if err != nil { return err } defer release() // ... ``` 2. Early release ```go release, err := Lock(ctx, key) if err != nil { return err } defer release() // ... // release the lock earlier release() // continue to do something else // ... ``` 3. Functional way ```go if err := LockAndDo(ctx, key, func(ctx context.Context) error { // ... return nil }); err != nil { return err } ``` --- modules/globallock/globallock.go | 8 ++-- modules/globallock/locker.go | 30 ++----------- modules/globallock/locker_test.go | 68 ++++++++--------------------- modules/globallock/memory_locker.go | 27 +++--------- modules/globallock/redis_locker.go | 49 +++++++-------------- 5 files changed, 50 insertions(+), 132 deletions(-) diff --git a/modules/globallock/globallock.go b/modules/globallock/globallock.go index 707d169f05..aa53557729 100644 --- a/modules/globallock/globallock.go +++ b/modules/globallock/globallock.go @@ -27,20 +27,20 @@ func DefaultLocker() Locker { // Lock tries to acquire a lock for the given key, it uses the default locker. // Read the documentation of Locker.Lock for more information about the behavior. -func Lock(ctx context.Context, key string) (context.Context, ReleaseFunc, error) { +func Lock(ctx context.Context, key string) (ReleaseFunc, error) { return DefaultLocker().Lock(ctx, key) } // TryLock tries to acquire a lock for the given key, it uses the default locker. // Read the documentation of Locker.TryLock for more information about the behavior. -func TryLock(ctx context.Context, key string) (bool, context.Context, ReleaseFunc, error) { +func TryLock(ctx context.Context, key string) (bool, ReleaseFunc, error) { return DefaultLocker().TryLock(ctx, key) } // LockAndDo tries to acquire a lock for the given key and then calls the given function. // It uses the default locker, and it will return an error if failed to acquire the lock. func LockAndDo(ctx context.Context, key string, f func(context.Context) error) error { - ctx, release, err := Lock(ctx, key) + release, err := Lock(ctx, key) if err != nil { return err } @@ -52,7 +52,7 @@ func LockAndDo(ctx context.Context, key string, f func(context.Context) error) e // TryLockAndDo tries to acquire a lock for the given key and then calls the given function. // It uses the default locker, and it will return false if failed to acquire the lock. func TryLockAndDo(ctx context.Context, key string, f func(context.Context) error) (bool, error) { - ok, ctx, release, err := TryLock(ctx, key) + ok, release, err := TryLock(ctx, key) if err != nil { return false, err } diff --git a/modules/globallock/locker.go b/modules/globallock/locker.go index b0764cd71c..682e24d052 100644 --- a/modules/globallock/locker.go +++ b/modules/globallock/locker.go @@ -5,56 +5,34 @@ package globallock import ( "context" - "fmt" ) type Locker interface { // Lock tries to acquire a lock for the given key, it blocks until the lock is acquired or the context is canceled. // - // Lock returns a new context which should be used in the following code. - // The new context will be canceled when the lock is released or lost - yes, it's possible to lose a lock. - // For example, it lost the connection to the redis server while holding the lock. - // If it fails to acquire the lock, the returned context will be the same as the input context. - // // Lock returns a ReleaseFunc to release the lock, it cannot be nil. // It's always safe to call this function even if it fails to acquire the lock, and it will do nothing in that case. // And it's also safe to call it multiple times, but it will only release the lock once. // That's why it's called ReleaseFunc, not UnlockFunc. // But be aware that it's not safe to not call it at all; it could lead to a memory leak. // So a recommended pattern is to use defer to call it: - // ctx, release, err := locker.Lock(ctx, "key") + // release, err := locker.Lock(ctx, "key") // if err != nil { // return err // } // defer release() - // The ReleaseFunc will return the original context which was used to acquire the lock. - // It's useful when you want to continue to do something after releasing the lock. - // At that time, the ctx will be canceled, and you can use the returned context by the ReleaseFunc to continue: - // ctx, release, err := locker.Lock(ctx, "key") - // if err != nil { - // return err - // } - // defer release() - // doSomething(ctx) - // ctx = release() - // doSomethingElse(ctx) - // Please ignore it and use `defer release()` instead if you don't need this, to avoid forgetting to release the lock. // // Lock returns an error if failed to acquire the lock. // Be aware that even the context is not canceled, it's still possible to fail to acquire the lock. // For example, redis is down, or it reached the maximum number of tries. - Lock(ctx context.Context, key string) (context.Context, ReleaseFunc, error) + Lock(ctx context.Context, key string) (ReleaseFunc, error) // TryLock tries to acquire a lock for the given key, it returns immediately. // It follows the same pattern as Lock, but it doesn't block. // And if it fails to acquire the lock because it's already locked, not other reasons like redis is down, // it will return false without any error. - TryLock(ctx context.Context, key string) (bool, context.Context, ReleaseFunc, error) + TryLock(ctx context.Context, key string) (bool, ReleaseFunc, error) } // ReleaseFunc is a function that releases a lock. -// It returns the original context which was used to acquire the lock. -type ReleaseFunc func() context.Context - -// ErrLockReleased is used as context cause when a lock is released -var ErrLockReleased = fmt.Errorf("lock released") +type ReleaseFunc func() diff --git a/modules/globallock/locker_test.go b/modules/globallock/locker_test.go index 15a3c65bb0..bee4d34b34 100644 --- a/modules/globallock/locker_test.go +++ b/modules/globallock/locker_test.go @@ -47,27 +47,24 @@ func TestLocker(t *testing.T) { func testLocker(t *testing.T, locker Locker) { t.Run("lock", func(t *testing.T) { parentCtx := context.Background() - ctx, release, err := locker.Lock(parentCtx, "test") + release, err := locker.Lock(parentCtx, "test") defer release() - assert.NotEqual(t, parentCtx, ctx) // new context should be returned assert.NoError(t, err) func() { - parentCtx, cancel := context.WithTimeout(context.Background(), time.Second) + ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() - ctx, release, err := locker.Lock(parentCtx, "test") + release, err := locker.Lock(ctx, "test") defer release() assert.Error(t, err) - assert.Equal(t, parentCtx, ctx) // should return the same context }() release() - assert.Error(t, ctx.Err()) func() { - _, release, err := locker.Lock(context.Background(), "test") + release, err := locker.Lock(context.Background(), "test") defer release() assert.NoError(t, err) @@ -76,29 +73,26 @@ func testLocker(t *testing.T, locker Locker) { t.Run("try lock", func(t *testing.T) { parentCtx := context.Background() - ok, ctx, release, err := locker.TryLock(parentCtx, "test") + ok, release, err := locker.TryLock(parentCtx, "test") defer release() assert.True(t, ok) - assert.NotEqual(t, parentCtx, ctx) // new context should be returned assert.NoError(t, err) func() { - parentCtx, cancel := context.WithTimeout(context.Background(), time.Second) + ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() - ok, ctx, release, err := locker.TryLock(parentCtx, "test") + ok, release, err := locker.TryLock(ctx, "test") defer release() assert.False(t, ok) assert.NoError(t, err) - assert.Equal(t, parentCtx, ctx) // should return the same context }() release() - assert.Error(t, ctx.Err()) func() { - ok, _, release, _ := locker.TryLock(context.Background(), "test") + ok, release, _ := locker.TryLock(context.Background(), "test") defer release() assert.True(t, ok) @@ -107,7 +101,7 @@ func testLocker(t *testing.T, locker Locker) { t.Run("wait and acquired", func(t *testing.T) { ctx := context.Background() - _, release, err := locker.Lock(ctx, "test") + release, err := locker.Lock(ctx, "test") require.NoError(t, err) wg := &sync.WaitGroup{} @@ -115,7 +109,7 @@ func testLocker(t *testing.T, locker Locker) { go func() { defer wg.Done() started := time.Now() - _, release, err := locker.Lock(context.Background(), "test") // should be blocked for seconds + release, err := locker.Lock(context.Background(), "test") // should be blocked for seconds defer release() assert.Greater(t, time.Since(started), time.Second) assert.NoError(t, err) @@ -127,34 +121,15 @@ func testLocker(t *testing.T, locker Locker) { wg.Wait() }) - t.Run("continue after release", func(t *testing.T) { - ctx := context.Background() - - ctxBeforeLock := ctx - ctx, release, err := locker.Lock(ctx, "test") - - require.NoError(t, err) - assert.NoError(t, ctx.Err()) - assert.NotEqual(t, ctxBeforeLock, ctx) - - ctxBeforeRelease := ctx - ctx = release() - - assert.NoError(t, ctx.Err()) - assert.Error(t, ctxBeforeRelease.Err()) - - // so it can continue with ctx to do more work - }) - t.Run("multiple release", func(t *testing.T) { ctx := context.Background() - _, release1, err := locker.Lock(ctx, "test") + release1, err := locker.Lock(ctx, "test") require.NoError(t, err) release1() - _, release2, err := locker.Lock(ctx, "test") + release2, err := locker.Lock(ctx, "test") defer release2() require.NoError(t, err) @@ -163,7 +138,7 @@ func testLocker(t *testing.T, locker Locker) { // and it shouldn't affect the other lock release1() - ok, _, release3, err := locker.TryLock(ctx, "test") + ok, release3, err := locker.TryLock(ctx, "test") defer release3() require.NoError(t, err) // It should be able to acquire the lock; @@ -184,28 +159,23 @@ func testRedisLocker(t *testing.T, locker *redisLocker) { // Otherwise, it will affect other tests. t.Run("close", func(t *testing.T) { assert.NoError(t, locker.Close()) - _, _, err := locker.Lock(context.Background(), "test") + _, err := locker.Lock(context.Background(), "test") assert.Error(t, err) }) }() t.Run("failed extend", func(t *testing.T) { - ctx, release, err := locker.Lock(context.Background(), "test") + release, err := locker.Lock(context.Background(), "test") defer release() require.NoError(t, err) // It simulates that there are some problems with extending like network issues or redis server down. v, ok := locker.mutexM.Load("test") require.True(t, ok) - m := v.(*redisMutex) - _, _ = m.mutex.Unlock() // release it to make it impossible to extend + m := v.(*redsync.Mutex) + _, _ = m.Unlock() // release it to make it impossible to extend - select { - case <-time.After(redisLockExpiry + time.Second): - t.Errorf("lock should be expired") - case <-ctx.Done(): - var errTaken *redsync.ErrTaken - assert.ErrorAs(t, context.Cause(ctx), &errTaken) - } + // In current design, callers can't know the lock can't be extended. + // Just keep this case to improve the test coverage. }) } diff --git a/modules/globallock/memory_locker.go b/modules/globallock/memory_locker.go index fb1fc79bd0..3f818d8d43 100644 --- a/modules/globallock/memory_locker.go +++ b/modules/globallock/memory_locker.go @@ -19,18 +19,13 @@ func NewMemoryLocker() Locker { return &memoryLocker{} } -func (l *memoryLocker) Lock(ctx context.Context, key string) (context.Context, ReleaseFunc, error) { - originalCtx := ctx - +func (l *memoryLocker) Lock(ctx context.Context, key string) (ReleaseFunc, error) { if l.tryLock(key) { - ctx, cancel := context.WithCancelCause(ctx) releaseOnce := sync.Once{} - return ctx, func() context.Context { + return func() { releaseOnce.Do(func() { l.locks.Delete(key) - cancel(ErrLockReleased) }) - return originalCtx }, nil } @@ -39,39 +34,31 @@ func (l *memoryLocker) Lock(ctx context.Context, key string) (context.Context, R for { select { case <-ctx.Done(): - return ctx, func() context.Context { return originalCtx }, ctx.Err() + return func() {}, ctx.Err() case <-ticker.C: if l.tryLock(key) { - ctx, cancel := context.WithCancelCause(ctx) releaseOnce := sync.Once{} - return ctx, func() context.Context { + return func() { releaseOnce.Do(func() { l.locks.Delete(key) - cancel(ErrLockReleased) }) - return originalCtx }, nil } } } } -func (l *memoryLocker) TryLock(ctx context.Context, key string) (bool, context.Context, ReleaseFunc, error) { - originalCtx := ctx - +func (l *memoryLocker) TryLock(_ context.Context, key string) (bool, ReleaseFunc, error) { if l.tryLock(key) { - ctx, cancel := context.WithCancelCause(ctx) releaseOnce := sync.Once{} - return true, ctx, func() context.Context { + return true, func() { releaseOnce.Do(func() { - cancel(ErrLockReleased) l.locks.Delete(key) }) - return originalCtx }, nil } - return false, ctx, func() context.Context { return originalCtx }, nil + return false, func() {}, nil } func (l *memoryLocker) tryLock(key string) bool { diff --git a/modules/globallock/redis_locker.go b/modules/globallock/redis_locker.go index 34b2fabfb3..34ed9e389b 100644 --- a/modules/globallock/redis_locker.go +++ b/modules/globallock/redis_locker.go @@ -48,21 +48,21 @@ func NewRedisLocker(connection string) Locker { return l } -func (l *redisLocker) Lock(ctx context.Context, key string) (context.Context, ReleaseFunc, error) { +func (l *redisLocker) Lock(ctx context.Context, key string) (ReleaseFunc, error) { return l.lock(ctx, key, 0) } -func (l *redisLocker) TryLock(ctx context.Context, key string) (bool, context.Context, ReleaseFunc, error) { - ctx, f, err := l.lock(ctx, key, 1) +func (l *redisLocker) TryLock(ctx context.Context, key string) (bool, ReleaseFunc, error) { + f, err := l.lock(ctx, key, 1) var ( errTaken *redsync.ErrTaken errNodeTaken *redsync.ErrNodeTaken ) if errors.As(err, &errTaken) || errors.As(err, &errNodeTaken) { - return false, ctx, f, nil + return false, f, nil } - return err == nil, ctx, f, err + return err == nil, f, err } // Close closes the locker. @@ -76,18 +76,11 @@ func (l *redisLocker) Close() error { return nil } -type redisMutex struct { - mutex *redsync.Mutex - cancel context.CancelCauseFunc -} - -func (l *redisLocker) lock(ctx context.Context, key string, tries int) (context.Context, ReleaseFunc, error) { +func (l *redisLocker) lock(ctx context.Context, key string, tries int) (ReleaseFunc, error) { if l.closed.Load() { - return ctx, func() context.Context { return ctx }, fmt.Errorf("locker is closed") + return func() {}, fmt.Errorf("locker is closed") } - originalCtx := ctx - options := []redsync.Option{ redsync.WithExpiry(redisLockExpiry), } @@ -96,18 +89,13 @@ func (l *redisLocker) lock(ctx context.Context, key string, tries int) (context. } mutex := l.rs.NewMutex(redisLockKeyPrefix+key, options...) if err := mutex.LockContext(ctx); err != nil { - return ctx, func() context.Context { return originalCtx }, err + return func() {}, err } - ctx, cancel := context.WithCancelCause(ctx) - - l.mutexM.Store(key, &redisMutex{ - mutex: mutex, - cancel: cancel, - }) + l.mutexM.Store(key, mutex) releaseOnce := sync.Once{} - return ctx, func() context.Context { + return func() { releaseOnce.Do(func() { l.mutexM.Delete(key) @@ -115,10 +103,7 @@ func (l *redisLocker) lock(ctx context.Context, key string, tries int) (context. // if it failed to unlock, it will be released automatically after the lock expires. // Do not call mutex.UnlockContext(ctx) here, or it will fail to release when ctx has timed out. _, _ = mutex.Unlock() - - cancel(ErrLockReleased) }) - return originalCtx }, nil } @@ -128,16 +113,15 @@ func (l *redisLocker) startExtend() { return } - toExtend := make([]*redisMutex, 0) + toExtend := make([]*redsync.Mutex, 0) l.mutexM.Range(func(_, value any) bool { - m := value.(*redisMutex) + m := value.(*redsync.Mutex) // Extend the lock if it is not expired. // Although the mutex will be removed from the map before it is released, // it still can be expired because of a failed extension. - // If it happens, the cancel function should have been called, - // so it does not need to be extended anymore. - if time.Now().After(m.mutex.Until()) { + // If it happens, it does not need to be extended anymore. + if time.Now().After(m.Until()) { return true } @@ -145,9 +129,8 @@ func (l *redisLocker) startExtend() { return true }) for _, v := range toExtend { - if ok, err := v.mutex.Extend(); !ok { - v.cancel(err) - } + // If it failed to extend, it will be released automatically after the lock expires. + _, _ = v.Extend() } time.AfterFunc(redisLockExpiry/2, l.startExtend) From fcac28c878b0e9539e4d68f3a496e9d5b26bd285 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 29 Aug 2024 12:54:38 +0800 Subject: [PATCH 9/9] Upgrade micromatch to 4.0.8 (#31939) --- package-lock.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index d469cc924c..737dbd12b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8804,10 +8804,9 @@ } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", - "license": "MIT", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1"