diff --git a/modules/markup/html.go b/modules/markup/html.go index 56f63b8a76..f73221a37f 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -54,7 +54,7 @@ var ( shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`) // anySHA1Pattern splits url containing SHA into parts - anyHashPattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{40,64})(/[-+~_%.a-zA-Z0-9/]+)?(#[-+~_%.a-zA-Z0-9]+)?`) + anyHashPattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{40,64})(/[-+~_%.a-zA-Z0-9/]+)?(\?[-+~_%\.a-zA-Z0-9=&]+)?(#[-+~_%.a-zA-Z0-9]+)?`) // comparePattern matches "http://domain/org/repo/compare/COMMIT1...COMMIT2#hash" comparePattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{7,64})(\.\.\.?)([0-9a-f]{7,64})?(#[-+~_%.a-zA-Z0-9]+)?`) @@ -969,10 +969,10 @@ func fullHashPatternProcessor(ctx *RenderContext, node *html.Node) { subpath = node.Data[m[4]:m[5]] } - // 4th capture group matches a optional url hash + // 5th capture group matches a optional url hash hash := "" - if m[7] > 0 { - hash = node.Data[m[6]:m[7]][1:] + if m[9] > 0 { + hash = node.Data[m[8]:m[9]][1:] } start := m[0] diff --git a/modules/markup/html_internal_test.go b/modules/markup/html_internal_test.go index e313be7040..917f280c73 100644 --- a/modules/markup/html_internal_test.go +++ b/modules/markup/html_internal_test.go @@ -403,28 +403,39 @@ func TestRegExp_anySHA1Pattern(t *testing.T) { "https://github.com/jquery/jquery/blob/a644101ed04d0beacea864ce805e0c4f86ba1cd1/test/unit/event.js#L2703": { "a644101ed04d0beacea864ce805e0c4f86ba1cd1", "/test/unit/event.js", + "", "#L2703", }, "https://github.com/jquery/jquery/blob/a644101ed04d0beacea864ce805e0c4f86ba1cd1/test/unit/event.js": { "a644101ed04d0beacea864ce805e0c4f86ba1cd1", "/test/unit/event.js", "", + "", }, "https://github.com/jquery/jquery/commit/0705be475092aede1eddae01319ec931fb9c65fc": { "0705be475092aede1eddae01319ec931fb9c65fc", "", "", + "", }, "https://github.com/jquery/jquery/tree/0705be475092aede1eddae01319ec931fb9c65fc/src": { "0705be475092aede1eddae01319ec931fb9c65fc", "/src", "", + "", }, "https://try.gogs.io/gogs/gogs/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2": { "d8a994ef243349f321568f9e36d5c3f444b99cae", "", + "", "#diff-2", }, + "https://codeberg.org/forgejo/forgejo/src/commit/949ab9a5c4cac742f84ae5a9fa186f8d6eb2cdc0/RELEASE-NOTES.md?display=source&w=1#L7-L9": { + "949ab9a5c4cac742f84ae5a9fa186f8d6eb2cdc0", + "/RELEASE-NOTES.md", + "?display=source&w=1", + "#L7-L9", + }, } for k, v := range testCases { diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index a1a99c1a7f..cfd1a66a18 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -127,6 +127,12 @@ func TestRender_CrossReferences(t *testing.T) { test( util.URLJoin(markup.TestAppURL, "gogitea", "some-repo-name", "issues", "12345"), `

gogitea/some-repo-name#12345

`) + + sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d" + urlWithQuery := util.URLJoin(markup.TestAppURL, "forgejo", "some-repo-name", "commit", sha, "README.md") + "?display=source#L1-L5" + test( + urlWithQuery, + `

`+sha[:10]+`/README.md (L1-L5)

`) } func TestMisc_IsSameDomain(t *testing.T) { diff --git a/package-lock.json b/package-lock.json index 8988034ad6..5781507561 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "chart.js": "4.4.2", "chartjs-adapter-dayjs-4": "1.0.4", "chartjs-plugin-zoom": "2.0.1", - "clippie": "4.0.7", + "clippie": "4.1.1", "css-loader": "7.0.0", "dayjs": "1.11.11", "dropzone": "6.0.0-beta.2", @@ -67,7 +67,7 @@ "@eslint-community/eslint-plugin-eslint-comments": "4.3.0", "@playwright/test": "1.43.0", "@stoplight/spectral-cli": "6.11.1", - "@stylistic/eslint-plugin-js": "1.7.2", + "@stylistic/eslint-plugin-js": "1.8.0", "@stylistic/stylelint-plugin": "2.1.2", "@vitejs/plugin-vue": "5.0.4", "@vue/test-utils": "2.4.5", @@ -87,7 +87,7 @@ "eslint-plugin-vue-scoped-css": "2.8.0", "eslint-plugin-wc": "2.1.0", "happy-dom": "14.7.1", - "markdownlint-cli": "0.39.0", + "markdownlint-cli": "0.40.0", "postcss-html": "1.6.0", "stylelint": "16.4.0", "stylelint-declaration-block-no-ignored-properties": "2.8.0", @@ -95,7 +95,7 @@ "stylelint-value-no-unknown-custom-properties": "6.0.1", "svgo": "3.2.0", "updates": "16.0.1", - "vite-string-plugin": "1.2.0", + "vite-string-plugin": "1.3.1", "vitest": "1.5.3" }, "engines": { @@ -2126,12 +2126,12 @@ } }, "node_modules/@stylistic/eslint-plugin-js": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-1.7.2.tgz", - "integrity": "sha512-ZYX7C5p7zlHbACwFLU+lISVh6tdcRP/++PWegh2Sy0UgMT5kU0XkPa2tKWEtJYzZmPhJxu9LxbnWcnE/tTwSDQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-1.8.0.tgz", + "integrity": "sha512-jdvnzt+pZPg8TfclZlTZPiUbbima93ylvQ+wNgHLNmup3obY6heQvgewSu9i2CfS61BnRByv+F9fxQLPoNeHag==", "dev": true, "dependencies": { - "@types/eslint": "^8.56.8", + "@types/eslint": "^8.56.10", "acorn": "^8.11.3", "escape-string-regexp": "^4.0.0", "eslint-visitor-keys": "^3.4.3", @@ -2224,9 +2224,9 @@ } }, "node_modules/@types/eslint": { - "version": "8.56.9", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.9.tgz", - "integrity": "sha512-W4W3KcqzjJ0sHg2vAq9vfml6OhsJ53TcUjUqfzzZf/EChUtwspszj/S0pzMxnfRcO55/iGq47dscXw71Fxc4Zg==", + "version": "8.56.10", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", + "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -3779,9 +3779,9 @@ } }, "node_modules/clippie": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/clippie/-/clippie-4.0.7.tgz", - "integrity": "sha512-xmIARCRFQUoCR0kNNu4uIv5f/IFqM1fUts0vQwt1hQEdCPEqs3/dTaG38WenlWOgs3Fcn73PBYXbPIVSlOgFRw==" + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/clippie/-/clippie-4.1.1.tgz", + "integrity": "sha512-D9OOW77Kkj9YEiDXTQjZJZLvTjJPEmK2IBx8JbGJIZaqVd8RvSvxwIN4KVSEFQfu9Jh0z5FL6Pdc4SIknllFFA==" }, "node_modules/cliui": { "version": "7.0.4", @@ -8212,9 +8212,9 @@ } }, "node_modules/markdown-it": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.0.0.tgz", - "integrity": "sha512-seFjF0FIcPt4P9U39Bq1JYblX0KZCjDLFFQPHpL5AzHpqPEKtosxmdq/LTVZnjfH7tjt9BxStm+wXcDBNuYmzw==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", "dev": true, "dependencies": { "argparse": "^2.0.1", @@ -8222,20 +8222,20 @@ "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", - "uc.micro": "^2.0.0" + "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "node_modules/markdownlint": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.33.0.tgz", - "integrity": "sha512-4lbtT14A3m0LPX1WS/3d1m7Blg+ZwiLq36WvjQqFGsX3Gik99NV+VXp/PW3n+Q62xyPdbvGOCfjPqjW+/SKMig==", + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.34.0.tgz", + "integrity": "sha512-qwGyuyKwjkEMOJ10XN6OTKNOVYvOIi35RNvDLNxTof5s8UmyGHlCdpngRHoRGNvQVGuxO3BJ7uNSgdeX166WXw==", "dev": true, "dependencies": { - "markdown-it": "14.0.0", - "markdownlint-micromark": "0.1.8" + "markdown-it": "14.1.0", + "markdownlint-micromark": "0.1.9" }, "engines": { "node": ">=18" @@ -8245,20 +8245,22 @@ } }, "node_modules/markdownlint-cli": { - "version": "0.39.0", - "resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.39.0.tgz", - "integrity": "sha512-ZuFN7Xpsbn1Nbp0YYkeLOfXOMOfLQBik2lKRy8pVI/llmKQ2uW7x+8k5OMgF6o7XCsTDSYC/OOmeJ+3qplvnJQ==", + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.40.0.tgz", + "integrity": "sha512-JXhI3dRQcaqwiFYpPz6VJ7aKYheD53GmTz9y4D/d0F1MbZDGOp9pqKlbOfUX/pHP/iAoeiE4wYRmk8/kjLakxA==", "dev": true, "dependencies": { - "commander": "~11.1.0", + "commander": "~12.0.0", "get-stdin": "~9.0.0", - "glob": "~10.3.10", - "ignore": "~5.3.0", + "glob": "~10.3.12", + "ignore": "~5.3.1", "js-yaml": "^4.1.0", "jsonc-parser": "~3.2.1", - "markdownlint": "~0.33.0", - "minimatch": "~9.0.3", - "run-con": "~1.3.2" + "jsonpointer": "5.0.1", + "markdownlint": "~0.34.0", + "minimatch": "~9.0.4", + "run-con": "~1.3.2", + "toml": "~3.0.0" }, "bin": { "markdownlint": "markdownlint.js" @@ -8268,12 +8270,12 @@ } }, "node_modules/markdownlint-cli/node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz", + "integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==", "dev": true, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/markdownlint-cli/node_modules/glob": { @@ -8305,12 +8307,12 @@ "dev": true }, "node_modules/markdownlint-micromark": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/markdownlint-micromark/-/markdownlint-micromark-0.1.8.tgz", - "integrity": "sha512-1ouYkMRo9/6gou9gObuMDnvZM8jC/ly3QCFQyoSPCS2XV1ZClU0xpKbL1Ar3bWWRT1RnBZkWUEiNKrI2CwiBQA==", + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/markdownlint-micromark/-/markdownlint-micromark-0.1.9.tgz", + "integrity": "sha512-5hVs/DzAFa8XqYosbEAEg6ok6MF2smDj89ztn9pKkCtdKHVdPQuGMH7frFfYL9mLkvfFe4pTyAMffLbjf3/EyA==", "dev": true, "engines": { - "node": ">=16" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/DavidAnson" @@ -11831,6 +11833,12 @@ "resolved": "https://registry.npmjs.org/toastify-js/-/toastify-js-1.12.0.tgz", "integrity": "sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ==" }, + "node_modules/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", + "dev": true + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -12279,9 +12287,9 @@ } }, "node_modules/vite-string-plugin": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/vite-string-plugin/-/vite-string-plugin-1.2.0.tgz", - "integrity": "sha512-IijlLgTxUDUwOpLoBLZCZO2us4fZWPRpj8XWoD9OAYjjUEge8enV4gaDTOs7uEsC8EJ9+NmusdLwmgWajFO45Q==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/vite-string-plugin/-/vite-string-plugin-1.3.1.tgz", + "integrity": "sha512-0Wu9yNw4QlSVM4SlwozzxR0geMoKFrAIpMldgPuzDvV8lWT1v+0pFXYt+t48qocYXBaxiuVRE3qcsEwFDHBAmA==", "dev": true }, "node_modules/vite/node_modules/@types/estree": { diff --git a/package.json b/package.json index 2eb9e2da6d..b380a33568 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "chart.js": "4.4.2", "chartjs-adapter-dayjs-4": "1.0.4", "chartjs-plugin-zoom": "2.0.1", - "clippie": "4.0.7", + "clippie": "4.1.1", "css-loader": "7.0.0", "dayjs": "1.11.11", "dropzone": "6.0.0-beta.2", @@ -66,7 +66,7 @@ "@eslint-community/eslint-plugin-eslint-comments": "4.3.0", "@playwright/test": "1.43.0", "@stoplight/spectral-cli": "6.11.1", - "@stylistic/eslint-plugin-js": "1.7.2", + "@stylistic/eslint-plugin-js": "1.8.0", "@stylistic/stylelint-plugin": "2.1.2", "@vitejs/plugin-vue": "5.0.4", "@vue/test-utils": "2.4.5", @@ -86,7 +86,7 @@ "eslint-plugin-vue-scoped-css": "2.8.0", "eslint-plugin-wc": "2.1.0", "happy-dom": "14.7.1", - "markdownlint-cli": "0.39.0", + "markdownlint-cli": "0.40.0", "postcss-html": "1.6.0", "stylelint": "16.4.0", "stylelint-declaration-block-no-ignored-properties": "2.8.0", @@ -94,7 +94,7 @@ "stylelint-value-no-unknown-custom-properties": "6.0.1", "svgo": "3.2.0", "updates": "16.0.1", - "vite-string-plugin": "1.2.0", + "vite-string-plugin": "1.3.1", "vitest": "1.5.3" }, "browserslist": ["defaults"] diff --git a/release-notes/8.0.0/3608.md b/release-notes/8.0.0/3608.md new file mode 100644 index 0000000000..1c3072422a --- /dev/null +++ b/release-notes/8.0.0/3608.md @@ -0,0 +1 @@ +Fix text selection color diff --git a/release-notes/8.0.0/feat/3615.md b/release-notes/8.0.0/feat/3615.md new file mode 100644 index 0000000000..f2dd891c95 --- /dev/null +++ b/release-notes/8.0.0/feat/3615.md @@ -0,0 +1,5 @@ +Support the [Nix tarball fetcher immutable link protocol](https://github.com/nixos/nix/blob/56763ff918eb308db23080e560ed2ea3e00c80a7/doc/manual/src/protocols/tarball-fetcher.md) on archive URLs, so Forgejo-generated tarballs for branches will go into Nix's `flake.lock` as their respective commit URLs and `nix flake update` will just work. This allows natively fetching Forgejo repositories for Nix flake inputs as tarballs rather than as Git repositories, significantly improving fetch times and avoiding depending on Git at runtime. + +Concretely, Forgejo now returns a header of the following format from its archive URLs: `Link: rel="immutable"`. + +Example usage: `inputs.meow.url = "https://my-forgejo/someuser/somerepo/archive/main.tar.gz";` in `flake.nix`. For a private repository, configure `netrc-file` in `nix.conf` and use `https://my-forgejo/api/v1/repos/someuser/somerepo/archive/main.tar.gz` as a URL instead, since the normal archive endpoint doesn't support tokens. diff --git a/renovate.json b/renovate.json index 89025178c3..0a6bf6f700 100644 --- a/renovate.json +++ b/renovate.json @@ -60,7 +60,9 @@ { "description": "Group nodejs packages", "matchDepNames": ["node", "docker.io/node", "docker.io/library/node"], - "groupName": "nodejs packages" + "groupName": "nodejs packages", + "versionCompatibility": "^(?[^-]+)(?-.*)?$", + "versioning": "node" }, { "description": "Automerge renovate updates", @@ -71,20 +73,12 @@ }, { "description": "Split minor and patch updates", - "matchDepNames": [ - "vue", - "github.com/urfave/cli/v2", - "swagger-ui-dist" - ], + "matchDepNames": ["vue", "github.com/urfave/cli/v2", "swagger-ui-dist"], "separateMinorPatch": true }, { "description": "Automerge patch updates", - "matchDepNames": [ - "vue", - "github.com/urfave/cli/v2", - "swagger-ui-dist" - ], + "matchDepNames": ["vue", "github.com/urfave/cli/v2", "swagger-ui-dist"], "matchUpdateTypes": ["patch"], "automerge": true }, @@ -103,7 +97,7 @@ { "description": "Automerge some packages when ci succeeds", "extends": ["packages:linters"], - "matchDepNames": ["vitest"], + "matchDepNames": ["vitest", "vite-string-plugin"], "automerge": true }, { diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index 70eb3fbc25..34ccc929a5 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -326,6 +326,12 @@ func archiveDownload(ctx *context.APIContext) { func download(ctx *context.APIContext, archiveName string, archiver *repo_model.RepoArchiver) { downloadName := ctx.Repo.Repository.Name + "-" + archiveName + // Add nix format link header so tarballs lock correctly: + // https://github.com/nixos/nix/blob/56763ff918eb308db23080e560ed2ea3e00c80a7/doc/manual/src/protocols/tarball-fetcher.md + ctx.Resp.Header().Add("Link", fmt.Sprintf("<%s/archive/%s.tar.gz?rev=%s>; rel=\"immutable\"", + ctx.Repo.Repository.APIURL(), + archiver.CommitID, archiver.CommitID)) + rPath := archiver.RelativePath() if setting.RepoArchive.Storage.MinioConfig.ServeDirect { // If we have a signed url (S3, object storage), redirect to this directly. diff --git a/routers/web/org/setting/blocked_users.go b/routers/web/org/setting/blocked_users.go index b23f5ba596..0c7f245c13 100644 --- a/routers/web/org/setting/blocked_users.go +++ b/routers/web/org/setting/blocked_users.go @@ -10,6 +10,7 @@ import ( "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" + shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" user_service "code.gitea.io/gitea/services/user" ) @@ -27,6 +28,12 @@ func BlockedUsers(ctx *context.Context) { return } + err = shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + ctx.Data["BlockedUsers"] = blockedUsers ctx.HTML(http.StatusOK, tplBlockedUsers) diff --git a/routers/web/repo/issue_dependency.go b/routers/web/repo/issue_dependency.go index e3b85ee638..66b38688ec 100644 --- a/routers/web/repo/issue_dependency.go +++ b/routers/web/repo/issue_dependency.go @@ -109,7 +109,7 @@ func RemoveDependency(ctx *context.Context) { } // Dependency Type - depTypeStr := ctx.Req.PostForm.Get("dependencyType") + depTypeStr := ctx.Req.PostFormValue("dependencyType") var depType issues_model.DependencyType diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index a5f6c02c7a..abd68630a4 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -480,6 +480,12 @@ func Download(ctx *context.Context) { func download(ctx *context.Context, archiveName string, archiver *repo_model.RepoArchiver) { downloadName := ctx.Repo.Repository.Name + "-" + archiveName + // Add nix format link header so tarballs lock correctly: + // https://github.com/nixos/nix/blob/56763ff918eb308db23080e560ed2ea3e00c80a7/doc/manual/src/protocols/tarball-fetcher.md + ctx.Resp.Header().Add("Link", fmt.Sprintf("<%s/archive/%s.tar.gz?rev=%s>; rel=\"immutable\"", + ctx.Repo.Repository.APIURL(), + archiver.CommitID, archiver.CommitID)) + rPath := archiver.RelativePath() if setting.RepoArchive.Storage.MinioConfig.ServeDirect { // If we have a signed url (S3, object storage), redirect to this directly. diff --git a/templates/org/menu.tmpl b/templates/org/menu.tmpl index 1860a3765a..212154995d 100644 --- a/templates/org/menu.tmpl +++ b/templates/org/menu.tmpl @@ -6,6 +6,7 @@ {{if .RepoCount}}
{{.RepoCount}}
{{end}} + {{if .CanReadProjects}} @@ -13,6 +14,7 @@ {{if .ProjectCount}}
{{.ProjectCount}}
{{end}} +
{{end}} {{if and .IsPackageEnabled .CanReadPackages}} @@ -31,6 +33,7 @@
{{.NumMembers}}
{{end}} + {{if .IsOrganizationMember}} {{svg "octicon-people"}} {{ctx.Locale.Tr "org.teams"}} @@ -39,6 +42,7 @@ {{end}} {{end}} + {{if .IsOrganizationOwner}} {{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}} diff --git a/templates/user/overview/header.tmpl b/templates/user/overview/header.tmpl index 275c4e295e..1ec83042a3 100644 --- a/templates/user/overview/header.tmpl +++ b/templates/user/overview/header.tmpl @@ -10,6 +10,7 @@ {{if .RepoCount}}
{{.RepoCount}}
{{end}} +
{{if or .ContextUser.IsIndividual .CanReadProjects}} @@ -17,6 +18,7 @@ {{if .ProjectCount}}
{{.ProjectCount}}
{{end}} +
{{end}} {{if and .IsPackageEnabled (or .ContextUser.IsIndividual .CanReadPackages)}} diff --git a/tests/integration/api_repo_archive_test.go b/tests/integration/api_repo_archive_test.go index c574d49450..a4ae599615 100644 --- a/tests/integration/api_repo_archive_test.go +++ b/tests/integration/api_repo_archive_test.go @@ -8,6 +8,7 @@ import ( "io" "net/http" "net/url" + "regexp" "testing" auth_model "code.gitea.io/gitea/models/auth" @@ -41,6 +42,16 @@ func TestAPIDownloadArchive(t *testing.T) { assert.Len(t, bs, 266) assert.EqualValues(t, "application/gzip", resp.Header().Get("Content-Type")) + // Must return a link to a commit ID as the "immutable" archive link + linkHeaderRe := regexp.MustCompile(`<(?Phttps?://.*/api/v1/repos/user2/repo1/archive/[a-f0-9]+\.tar\.gz.*)>; rel="immutable"`) + m := linkHeaderRe.FindStringSubmatch(resp.Header().Get("Link")) + assert.NotEmpty(t, m[1]) + resp = MakeRequest(t, NewRequest(t, "GET", m[1]).AddTokenAuth(token), http.StatusOK) + bs2, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + // The locked URL should give the same bytes as the non-locked one + assert.EqualValues(t, bs, bs2) + link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/archive/master.bundle", user2.Name, repo.Name)) resp = MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusOK) bs, err = io.ReadAll(resp.Body) diff --git a/tests/integration/issue_test.go b/tests/integration/issue_test.go index e09656c4ee..b63127be82 100644 --- a/tests/integration/issue_test.go +++ b/tests/integration/issue_test.go @@ -15,6 +15,7 @@ import ( "testing" "time" + auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" @@ -194,6 +195,93 @@ func TestNewIssue(t *testing.T) { testNewIssue(t, session, "user2", "repo1", "Title", "Description") } +func TestIssueDependencies(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + session := loginUser(t, owner.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue) + + repo, _, f := CreateDeclarativeRepoWithOptions(t, owner, DeclarativeRepoOptions{}) + defer f() + + createIssue := func(t *testing.T, title string) api.Issue { + t.Helper() + + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner.Name, repo.Name) + req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueOption{ + Body: "", + Title: title, + }).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusCreated) + + var apiIssue api.Issue + DecodeJSON(t, resp, &apiIssue) + + return apiIssue + } + addDependency := func(t *testing.T, issue, dependency api.Issue) { + t.Helper() + + urlStr := fmt.Sprintf("/%s/%s/issues/%d/dependency/add", owner.Name, repo.Name, issue.Index) + req := NewRequestWithValues(t, "POST", urlStr, map[string]string{ + "_csrf": GetCSRF(t, session, fmt.Sprintf("/%s/%s/issues/%d", owner.Name, repo.Name, issue.Index)), + "newDependency": fmt.Sprintf("%d", dependency.Index), + }) + session.MakeRequest(t, req, http.StatusSeeOther) + } + removeDependency := func(t *testing.T, issue, dependency api.Issue) { + t.Helper() + + urlStr := fmt.Sprintf("/%s/%s/issues/%d/dependency/delete", owner.Name, repo.Name, issue.Index) + req := NewRequestWithValues(t, "POST", urlStr, map[string]string{ + "_csrf": GetCSRF(t, session, fmt.Sprintf("/%s/%s/issues/%d", owner.Name, repo.Name, issue.Index)), + "removeDependencyID": fmt.Sprintf("%d", dependency.Index), + "dependencyType": "blockedBy", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + } + + assertHasDependency := func(t *testing.T, issueID, dependencyID int64, hasDependency bool) { + t.Helper() + + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/dependencies", owner.Name, repo.Name, issueID) + req := NewRequest(t, "GET", urlStr) + resp := MakeRequest(t, req, http.StatusOK) + + var issues []api.Issue + DecodeJSON(t, resp, &issues) + + if hasDependency { + assert.NotEmpty(t, issues) + assert.EqualValues(t, issues[0].Index, dependencyID) + } else { + assert.Empty(t, issues) + } + } + + t.Run("Add dependency", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + issue1 := createIssue(t, "issue #1") + issue2 := createIssue(t, "issue #2") + addDependency(t, issue1, issue2) + + assertHasDependency(t, issue1.Index, issue2.Index, true) + }) + + t.Run("Remove dependency", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + issue1 := createIssue(t, "issue #1") + issue2 := createIssue(t, "issue #2") + addDependency(t, issue1, issue2) + removeDependency(t, issue1, issue2) + + assertHasDependency(t, issue1.Index, issue2.Index, false) + }) +} + func TestIssueCommentClose(t *testing.T) { defer tests.PrepareTestEnv(t)() session := loginUser(t, "user2") diff --git a/tests/integration/user_count_test.go b/tests/integration/user_count_test.go new file mode 100644 index 0000000000..c0837d57fd --- /dev/null +++ b/tests/integration/user_count_test.go @@ -0,0 +1,167 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "strconv" + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/organization" + project_model "code.gitea.io/gitea/models/project" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/tests" + + "github.com/PuerkitoBio/goquery" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type userCountTest struct { + doer *user_model.User + user *user_model.User + session *TestSession + repoCount int64 + projectCount int64 + memberCount int64 + teamCount int64 +} + +func (countTest *userCountTest) Init(t *testing.T, doerID, userID int64) { + countTest.doer = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: doerID}) + countTest.user = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID}) + countTest.session = loginUser(t, countTest.doer.Name) + + var err error + + countTest.repoCount, err = repo_model.CountRepository(db.DefaultContext, &repo_model.SearchRepoOptions{ + Actor: countTest.doer, + OwnerID: countTest.user.ID, + Private: true, + Collaborate: optional.Some(false), + }) + require.NoError(t, err) + + var projectType project_model.Type + if countTest.user.IsOrganization() { + projectType = project_model.TypeOrganization + } else { + projectType = project_model.TypeIndividual + } + countTest.projectCount, err = db.Count[project_model.Project](db.DefaultContext, project_model.SearchOptions{ + OwnerID: countTest.user.ID, + IsClosed: optional.Some(false), + Type: projectType, + }) + require.NoError(t, err) + + if !countTest.user.IsOrganization() { + return + } + + org := (*organization.Organization)(countTest.user) + + isMember, err := org.IsOrgMember(db.DefaultContext, countTest.doer.ID) + require.NoError(t, err) + + countTest.memberCount, err = organization.CountOrgMembers(db.DefaultContext, &organization.FindOrgMembersOpts{ + OrgID: org.ID, + PublicOnly: !isMember, + }) + require.NoError(t, err) + + teams, err := org.LoadTeams(db.DefaultContext) + require.NoError(t, err) + + countTest.teamCount = int64(len(teams)) +} + +func (countTest *userCountTest) getCount(doc *goquery.Document, name string) (int64, error) { + selection := doc.Find(fmt.Sprintf("[test-name=\"%s\"]", name)) + + if selection.Length() != 1 { + return 0, fmt.Errorf("%s was not found", name) + } + + return strconv.ParseInt(selection.Text(), 10, 64) +} + +func (countTest *userCountTest) TestPage(t *testing.T, page string, orgLink bool) { + t.Run(page, func(t *testing.T) { + var userLink string + + if orgLink { + userLink = countTest.user.OrganisationLink() + } else { + userLink = countTest.user.HomeLink() + } + + req := NewRequestf(t, "GET", "%s/%s", userLink, page) + resp := countTest.session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + repoCount, err := countTest.getCount(htmlDoc.doc, "repository-count") + require.NoError(t, err) + assert.Equal(t, countTest.repoCount, repoCount) + + projectCount, err := countTest.getCount(htmlDoc.doc, "project-count") + require.NoError(t, err) + assert.Equal(t, countTest.projectCount, projectCount) + + if !countTest.user.IsOrganization() { + return + } + + memberCount, err := countTest.getCount(htmlDoc.doc, "member-count") + require.NoError(t, err) + assert.Equal(t, countTest.memberCount, memberCount) + + teamCount, err := countTest.getCount(htmlDoc.doc, "team-count") + require.NoError(t, err) + assert.Equal(t, countTest.teamCount, teamCount) + }) +} + +func TestFrontendHeaderCountUser(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + countTest := new(userCountTest) + countTest.Init(t, 2, 2) + + countTest.TestPage(t, "", false) + countTest.TestPage(t, "?tab=repositories", false) + countTest.TestPage(t, "-/projects", false) + countTest.TestPage(t, "-/packages", false) + countTest.TestPage(t, "?tab=activity", false) + countTest.TestPage(t, "?tab=stars", false) +} + +func TestFrontendHeaderCountOrg(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + countTest := new(userCountTest) + countTest.Init(t, 15, 17) + + countTest.TestPage(t, "", false) + countTest.TestPage(t, "-/projects", false) + countTest.TestPage(t, "-/packages", false) + countTest.TestPage(t, "members", true) + countTest.TestPage(t, "teams", true) + + countTest.TestPage(t, "settings", true) + countTest.TestPage(t, "settings/hooks", true) + countTest.TestPage(t, "settings/labels", true) + countTest.TestPage(t, "settings/applications", true) + countTest.TestPage(t, "settings/packages", true) + countTest.TestPage(t, "settings/actions/runners", true) + countTest.TestPage(t, "settings/actions/secrets", true) + countTest.TestPage(t, "settings/actions/variables", true) + countTest.TestPage(t, "settings/blocked_users", true) + countTest.TestPage(t, "settings/delete", true) +} diff --git a/web_src/css/themes/theme-forgejo-dark.css b/web_src/css/themes/theme-forgejo-dark.css index 42495c854b..95d35ddec0 100644 --- a/web_src/css/themes/theme-forgejo-dark.css +++ b/web_src/css/themes/theme-forgejo-dark.css @@ -141,6 +141,7 @@ /* other colors */ --color-gold: #b1983b; --color-white: #ffffff; + --color-pure-black: #000000; --color-diff-removed-word-bg: #783030; --color-diff-added-word-bg: #255c39; --color-diff-removed-row-bg: #432121; @@ -304,7 +305,7 @@ i.grey.icon.icon.icon.icon { } ::selection { background: var(--steel-100) !important; - color: var(--color-white) !important; + color: var(--color-pure-black) !important; } strong.attention-important, svg.attention-important { color: var(--color-violet-light);