Compare commits

..

No commits in common. "forgejo" and "v13.0.0-dev" have entirely different histories.

261 changed files with 2687 additions and 4703 deletions

View file

@ -27,13 +27,15 @@ forgejo.org/models/db
TruncateBeans TruncateBeans
InTransaction InTransaction
DumpTables DumpTables
GetTableNames
forgejo.org/models/dbfs forgejo.org/models/dbfs
file.renameTo file.renameTo
Create Create
Rename Rename
forgejo.org/models/forgefed
GetFederationHost
forgejo.org/models/forgejo/semver forgejo.org/models/forgejo/semver
GetVersion GetVersion
SetVersionString SetVersionString
@ -65,6 +67,7 @@ forgejo.org/models/user
DeleteUserSetting DeleteUserSetting
GetFederatedUser GetFederatedUser
GetFederatedUserByUserID GetFederatedUserByUserID
UpdateFederatedUser
GetFollowersForUser GetFollowersForUser
AddFollower AddFollower
RemoveFollower RemoveFollower
@ -245,9 +248,6 @@ forgejo.org/routers/web/org
forgejo.org/services/context forgejo.org/services/context
GetPrivateContext GetPrivateContext
forgejo.org/services/federation
Init
forgejo.org/services/repository forgejo.org/services/repository
IsErrForkAlreadyExist IsErrForkAlreadyExist

View file

@ -6,7 +6,7 @@
"ghcr.io/devcontainers/features/node:1": { "ghcr.io/devcontainers/features/node:1": {
"version": "22" "version": "22"
}, },
"ghcr.io/devcontainers/features/git-lfs:1.2.5": {}, "ghcr.io/devcontainers/features/git-lfs:1.2.4": {},
"ghcr.io/warrenbuckley/codespace-features/sqlite:1": {} "ghcr.io/warrenbuckley/codespace-features/sqlite:1": {}
}, },
"customizations": { "customizations": {

View file

@ -1,22 +0,0 @@
#
# Install the minimal version of Git supported by Forgejo
#
runs:
using: "composite"
steps:
- name: install git and git-lfs
run: |
set -x
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get -q install -y -qq curl ca-certificates
curl -sS -o /tmp/git-man.deb http://archive.ubuntu.com/ubuntu/pool/main/g/git/git-man_2.34.1-1ubuntu1_all.deb
curl -sS -o /tmp/git.deb https://archive.ubuntu.com/ubuntu/pool/main/g/git/git_2.34.1-1ubuntu1_amd64.deb
curl -sS -o /tmp/git-lfs.deb https://archive.ubuntu.com/ubuntu/pool/universe/g/git-lfs/git-lfs_3.0.2-1_amd64.deb
apt-get -q install --allow-downgrades -y -qq /tmp/git-man.deb
apt-get -q install --allow-downgrades -y -qq /tmp/git.deb
apt-get -q install --allow-downgrades -y -qq /tmp/git-lfs.deb

View file

@ -28,7 +28,7 @@ jobs:
runs-on: docker runs-on: docker
container: container:
image: data.forgejo.org/renovate/renovate:41.17.2 image: data.forgejo.org/renovate/renovate:41.1.4
steps: steps:
- name: Load renovate repo cache - name: Load renovate repo cache

View file

@ -33,8 +33,11 @@ jobs:
steps: steps:
- uses: https://data.forgejo.org/actions/checkout@v4 - uses: https://data.forgejo.org/actions/checkout@v4
- uses: ./.forgejo/workflows-composite/setup-env - uses: ./.forgejo/workflows-composite/setup-env
- name: install git 2.34.1 and git-lfs 3.0.2 - name: install git 2.30
uses: ./.forgejo/workflows-composite/install-minimum-git-version uses: ./.forgejo/workflows-composite/apt-install-from
with:
packages: git/bullseye git-lfs/bullseye
release: bullseye
- uses: ./.forgejo/workflows-composite/build-backend - uses: ./.forgejo/workflows-composite/build-backend
- run: | - run: |
su forgejo -c 'make test-backend test-check' su forgejo -c 'make test-backend test-check'
@ -52,8 +55,11 @@ jobs:
steps: steps:
- uses: https://data.forgejo.org/actions/checkout@v4 - uses: https://data.forgejo.org/actions/checkout@v4
- uses: ./.forgejo/workflows-composite/setup-env - uses: ./.forgejo/workflows-composite/setup-env
- name: install git 2.34.1 and git-lfs 3.0.2 - name: install git 2.30
uses: ./.forgejo/workflows-composite/install-minimum-git-version uses: ./.forgejo/workflows-composite/apt-install-from
with:
packages: git/bullseye git-lfs/bullseye
release: bullseye
- uses: ./.forgejo/workflows-composite/build-backend - uses: ./.forgejo/workflows-composite/build-backend
- run: | - run: |
su forgejo -c 'make test-sqlite-migration test-sqlite' su forgejo -c 'make test-sqlite-migration test-sqlite'

View file

@ -47,7 +47,7 @@ GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1.6.0 # renovate: datasour
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 # renovate: datasource=go GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 # renovate: datasource=go
DEADCODE_PACKAGE ?= golang.org/x/tools/cmd/deadcode@v0.34.0 # renovate: datasource=go DEADCODE_PACKAGE ?= golang.org/x/tools/cmd/deadcode@v0.34.0 # renovate: datasource=go
GOMOCK_PACKAGE ?= go.uber.org/mock/mockgen@v0.5.2 # renovate: datasource=go GOMOCK_PACKAGE ?= go.uber.org/mock/mockgen@v0.5.2 # renovate: datasource=go
RENOVATE_NPM_PACKAGE ?= renovate@41.17.2 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate RENOVATE_NPM_PACKAGE ?= renovate@41.1.4 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate
# https://github.com/disposable-email-domains/disposable-email-domains/commits/main/ # https://github.com/disposable-email-domains/disposable-email-domains/commits/main/
DISPOSABLE_EMAILS_SHA ?= 0c27e671231d27cf66370034d7f6818037416989 # renovate: ... DISPOSABLE_EMAILS_SHA ?= 0c27e671231d27cf66370034d7f6818037416989 # renovate: ...

View file

@ -595,8 +595,8 @@
"licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Huan Du\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n" "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Huan Du\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
}, },
{ {
"name": "github.com/inbucket/html2text", "name": "github.com/jaytaylor/html2text",
"path": "github.com/inbucket/html2text/LICENSE", "path": "github.com/jaytaylor/html2text/LICENSE",
"licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Jay Taylor\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n" "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Jay Taylor\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
}, },
{ {
@ -779,16 +779,6 @@
"path": "github.com/nwaples/rardecode/LICENSE", "path": "github.com/nwaples/rardecode/LICENSE",
"licenseText": "Copyright (c) 2015, Nicholas Waples\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" "licenseText": "Copyright (c) 2015, Nicholas Waples\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
}, },
{
"name": "github.com/olekukonko/errors",
"path": "github.com/olekukonko/errors/LICENSE",
"licenseText": "MIT License\n\nCopyright (c) 2025 Oleku Konko\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
},
{
"name": "github.com/olekukonko/ll",
"path": "github.com/olekukonko/ll/LICENSE",
"licenseText": "MIT License\n\nCopyright (c) 2025 Oleku Konko\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
},
{ {
"name": "github.com/olekukonko/tablewriter", "name": "github.com/olekukonko/tablewriter",
"path": "github.com/olekukonko/tablewriter/LICENSE.md", "path": "github.com/olekukonko/tablewriter/LICENSE.md",

View file

@ -82,11 +82,6 @@ wiki, issues, labels, releases, release_assets, milestones, pull_requests, comme
} }
func runDumpRepository(stdCtx context.Context, ctx *cli.Command) error { func runDumpRepository(stdCtx context.Context, ctx *cli.Command) error {
setupConsoleLogger(log.INFO, log.CanColorStderr, os.Stderr)
// setting.DisableLoggerInit()
setting.LoadSettings() // cannot access skip_tls_verify settings otherwise
stdCtx, cancel := installSignals(stdCtx) stdCtx, cancel := installSignals(stdCtx)
defer cancel() defer cancel()

View file

@ -231,6 +231,8 @@ Forgejo or set your environment appropriately.`, "")
} }
} }
supportProcReceive := git.CheckGitVersionAtLeast("2.29") == nil
for scanner.Scan() { for scanner.Scan() {
// TODO: support news feeds for wiki // TODO: support news feeds for wiki
if isWiki { if isWiki {
@ -248,7 +250,10 @@ Forgejo or set your environment appropriately.`, "")
total++ total++
lastline++ lastline++
// All references should be checked because permission check was delayed. // If the ref is a branch or tag, check if it's protected
// if supportProcReceive all ref should be checked because
// permission check was delayed
if supportProcReceive || refFullName.IsBranch() || refFullName.IsTag() {
oldCommitIDs[count] = oldCommitID oldCommitIDs[count] = oldCommitID
newCommitIDs[count] = newCommitID newCommitIDs[count] = newCommitID
refFullNames[count] = refFullName refFullNames[count] = refFullName
@ -268,6 +273,9 @@ Forgejo or set your environment appropriately.`, "")
count = 0 count = 0
lastline = 0 lastline = 0
} }
} else {
fmt.Fprint(out, ".")
}
if lastline >= hookBatchSize { if lastline >= hookBatchSize {
fmt.Fprint(out, "\n") fmt.Fprint(out, "\n")
lastline = 0 lastline = 0
@ -505,6 +513,10 @@ Forgejo or set your environment appropriately.`, "")
return nil return nil
} }
if git.CheckGitVersionAtLeast("2.29") != nil {
return fail(ctx, "No proc-receive support", "current git version doesn't support proc-receive.")
}
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
repoUser := os.Getenv(repo_module.EnvRepoUsername) repoUser := os.Getenv(repo_module.EnvRepoUsername)
repoName := os.Getenv(repo_module.EnvRepoName) repoName := os.Getenv(repo_module.EnvRepoName)

View file

@ -44,11 +44,6 @@ func defaultLoggingFlags() []cli.Flag {
Aliases: []string{"e"}, Aliases: []string{"e"},
Usage: "Matching expression for the logger", Usage: "Matching expression for the logger",
}, },
&cli.StringFlag{
Name: "exclusion",
Aliases: []string{"x"},
Usage: "Exclusion for the logger",
},
&cli.StringFlag{ &cli.StringFlag{
Name: "prefix", Name: "prefix",
Aliases: []string{"p"}, Aliases: []string{"p"},
@ -291,9 +286,6 @@ func commonAddLogger(ctx context.Context, c *cli.Command, mode string, vals map[
if len(c.String("expression")) > 0 { if len(c.String("expression")) > 0 {
vals["expression"] = c.String("expression") vals["expression"] = c.String("expression")
} }
if len(c.String("exclusion")) > 0 {
vals["exclusion"] = c.String("exclusion")
}
if len(c.String("prefix")) > 0 { if len(c.String("prefix")) > 0 {
vals["prefix"] = c.String("prefix") vals["prefix"] = c.String("prefix")
} }

View file

@ -193,11 +193,13 @@ func runServ(ctx context.Context, c *cli.Command) error {
} }
if len(words) < 2 { if len(words) < 2 {
if git.CheckGitVersionAtLeast("2.29") == nil {
// for AGit Flow // for AGit Flow
if cmd == "ssh_info" { if cmd == "ssh_info" {
fmt.Print(`{"type":"agit","version":1}`) fmt.Print(`{"type":"agit","version":1}`)
return nil return nil
} }
}
return fail(ctx, "Too few arguments", "Too few arguments in cmd: %s", cmd) return fail(ctx, "Too few arguments", "Too few arguments in cmd: %s", cmd)
} }

View file

@ -631,7 +631,6 @@ LEVEL = Info
;LEVEL= ;LEVEL=
;FLAGS = stdflags or journald ;FLAGS = stdflags or journald
;EXPRESSION = ;EXPRESSION =
;EXCLUSION =
;PREFIX = ;PREFIX =
;COLORIZE = false ;COLORIZE = false
;; ;;

20
go.mod
View file

@ -24,7 +24,7 @@ require (
github.com/ProtonMail/go-crypto v1.3.0 github.com/ProtonMail/go-crypto v1.3.0
github.com/PuerkitoBio/goquery v1.10.3 github.com/PuerkitoBio/goquery v1.10.3
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.7.2 github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.7.2
github.com/alecthomas/chroma/v2 v2.19.0 github.com/alecthomas/chroma/v2 v2.18.0
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
github.com/blevesearch/bleve/v2 v2.5.2 github.com/blevesearch/bleve/v2 v2.5.2
github.com/buildkite/terminal-to-html/v3 v3.16.8 github.com/buildkite/terminal-to-html/v3 v3.16.8
@ -42,7 +42,7 @@ require (
github.com/go-ap/activitypub v0.0.0-20231114162308-e219254dc5c9 github.com/go-ap/activitypub v0.0.0-20231114162308-e219254dc5c9
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73
github.com/go-chi/chi/v5 v5.2.2 github.com/go-chi/chi/v5 v5.2.2
github.com/go-chi/cors v1.2.2 github.com/go-chi/cors v1.2.1
github.com/go-co-op/gocron v1.37.0 github.com/go-co-op/gocron v1.37.0
github.com/go-enry/go-enry/v2 v2.9.2 github.com/go-enry/go-enry/v2 v2.9.2
github.com/go-git/go-git/v5 v5.13.2 github.com/go-git/go-git/v5 v5.13.2
@ -56,15 +56,15 @@ require (
github.com/golang-jwt/jwt/v5 v5.2.2 github.com/golang-jwt/jwt/v5 v5.2.2
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/google/go-github/v64 v64.0.0 github.com/google/go-github/v64 v64.0.0
github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/feeds v1.2.0 github.com/gorilla/feeds v1.2.0
github.com/gorilla/sessions v1.4.0 github.com/gorilla/sessions v1.4.0
github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/go-version v1.7.0
github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/huandu/xstrings v1.5.0 github.com/huandu/xstrings v1.5.0
github.com/inbucket/html2text v0.9.0 github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056
github.com/jhillyerd/enmime/v2 v2.2.0 github.com/jhillyerd/enmime/v2 v2.1.0
github.com/json-iterator/go v1.1.12 github.com/json-iterator/go v1.1.12
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/klauspost/compress v1.18.0 github.com/klauspost/compress v1.18.0
@ -79,7 +79,7 @@ require (
github.com/minio/minio-go/v7 v7.0.94 github.com/minio/minio-go/v7 v7.0.94
github.com/msteinert/pam/v2 v2.1.0 github.com/msteinert/pam/v2 v2.1.0
github.com/nektos/act v0.2.52 github.com/nektos/act v0.2.52
github.com/niklasfasching/go-org v1.9.0 github.com/niklasfasching/go-org v1.8.0
github.com/olivere/elastic/v7 v7.0.32 github.com/olivere/elastic/v7 v7.0.32
github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.1 github.com/opencontainers/image-spec v1.1.1
@ -158,7 +158,7 @@ require (
github.com/dlclark/regexp2 v1.11.5 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect
github.com/emirpasic/gods v1.18.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect
github.com/fatih/color v1.18.0 // indirect github.com/fatih/color v1.16.0 // indirect
github.com/fxamacker/cbor/v2 v2.8.0 // indirect github.com/fxamacker/cbor/v2 v2.8.0 // indirect
github.com/go-ap/errors v0.0.0-20231003111023-183eef4b31b7 // indirect github.com/go-ap/errors v0.0.0-20231003111023-183eef4b31b7 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
@ -192,7 +192,7 @@ require (
github.com/libdns/libdns v1.0.0-beta.1 // indirect github.com/libdns/libdns v1.0.0-beta.1 // indirect
github.com/mailru/easyjson v0.9.0 // indirect github.com/mailru/easyjson v0.9.0 // indirect
github.com/markbates/going v1.0.3 // indirect github.com/markbates/going v1.0.3 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mholt/acmez/v3 v3.1.2 // indirect github.com/mholt/acmez/v3 v3.1.2 // indirect
github.com/miekg/dns v1.1.63 // indirect github.com/miekg/dns v1.1.63 // indirect
@ -205,9 +205,7 @@ require (
github.com/mschoch/smat v0.2.0 // indirect github.com/mschoch/smat v0.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nwaples/rardecode v1.1.3 // indirect github.com/nwaples/rardecode v1.1.3 // indirect
github.com/olekukonko/errors v1.1.0 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/olekukonko/ll v0.0.9 // indirect
github.com/olekukonko/tablewriter v1.0.7 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect github.com/onsi/ginkgo v1.16.5 // indirect
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect

43
go.sum
View file

@ -62,8 +62,8 @@ github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.7.2/go.mod h1:JitQWJ8JuV4Y
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
github.com/alecthomas/chroma/v2 v2.19.0 h1:Im+SLRgT8maArxv81mULDWN8oKxkzboH07CHesxElq4= github.com/alecthomas/chroma/v2 v2.18.0 h1:6h53Q4hW83SuF+jcsp7CVhLsMozzvQvO8HBbKQW+gn4=
github.com/alecthomas/chroma/v2 v2.19.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk= github.com/alecthomas/chroma/v2 v2.18.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
@ -192,8 +192,8 @@ github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43/go.mod h1:iL2twTe
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY= github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY=
github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
@ -215,8 +215,8 @@ github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkPro
github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0= github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0=
github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY= github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY=
github.com/go-enry/go-enry/v2 v2.9.2 h1:giOQAtCgBX08kosrX818DCQJTCNtKwoPBGu0qb6nKTY= github.com/go-enry/go-enry/v2 v2.9.2 h1:giOQAtCgBX08kosrX818DCQJTCNtKwoPBGu0qb6nKTY=
@ -307,8 +307,8 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@ -341,12 +341,12 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
github.com/inbucket/html2text v0.9.0 h1:ULJmVcBEMAcmLE+/rN815KG1Fx6+a4HhbUxiDiN+qks= github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA=
github.com/inbucket/html2text v0.9.0/go.mod h1:QDaumzl+/OzlSVbNohhmg+yAy5pKjUjzCKW2BMvztKE= github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jhillyerd/enmime/v2 v2.2.0 h1:Pe35MB96eZK5Q0XjlvPftOgWypQpd1gcbfJKAt7rsB8= github.com/jhillyerd/enmime/v2 v2.1.0 h1:c8Qwi5Xq5EdtMN6byQWoZ/8I2RMTo6OJ7Xay+s1oPO0=
github.com/jhillyerd/enmime/v2 v2.2.0/go.mod h1:SOBXlCemjhiV2DvHhAKnJiWrtJGS/Ffuw4Iy7NjBTaI= github.com/jhillyerd/enmime/v2 v2.1.0/go.mod h1:EJ74dcRbBcqHSP2TBu08XRoy6y3Yx0cevwb1YkGMEmQ=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@ -389,10 +389,12 @@ github.com/markbates/going v1.0.3 h1:mY45T5TvW+Xz5A6jY7lf4+NLg9D8+iuStIHyR7M8qsE
github.com/markbates/going v1.0.3/go.mod h1:fQiT6v6yQar9UD6bd/D4Z5Afbk9J6BBVBtLiyY4gp2o= github.com/markbates/going v1.0.3/go.mod h1:fQiT6v6yQar9UD6bd/D4Z5Afbk9J6BBVBtLiyY4gp2o=
github.com/markbates/goth v1.80.0 h1:NnvatczZDzOs1hn9Ug+dVYf2Viwwkp/ZDX5K+GLjan8= github.com/markbates/goth v1.80.0 h1:NnvatczZDzOs1hn9Ug+dVYf2Viwwkp/ZDX5K+GLjan8=
github.com/markbates/goth v1.80.0/go.mod h1:4/GYHo+W6NWisrMPZnq0Yr2Q70UntNLn7KXEFhrIdAY= github.com/markbates/goth v1.80.0/go.mod h1:4/GYHo+W6NWisrMPZnq0Yr2Q70UntNLn7KXEFhrIdAY=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
@ -426,20 +428,16 @@ github.com/msteinert/pam/v2 v2.1.0 h1:er5F9TKV5nGFuTt12ubtqPHEUdeBwReP7vd3wovidG
github.com/msteinert/pam/v2 v2.1.0/go.mod h1:KT28NNIcDFf3PcBmNI2mIGO4zZJ+9RSs/At2PB3IDVc= github.com/msteinert/pam/v2 v2.1.0/go.mod h1:KT28NNIcDFf3PcBmNI2mIGO4zZJ+9RSs/At2PB3IDVc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/niklasfasching/go-org v1.9.0 h1:4/Sr68Qx06hjC9MVDB/4etGP67JionLHGscLMOClpnk= github.com/niklasfasching/go-org v1.8.0 h1:WyGLaajLLp8JbQzkmapZ1y0MOzKuKV47HkZRloi+HGY=
github.com/niklasfasching/go-org v1.9.0/go.mod h1:ZAGFFkWvUQcpazmi/8nHqwvARpr1xpb+Es67oUGX/48= github.com/niklasfasching/go-org v1.8.0/go.mod h1:e2A9zJs7cdONrEGs3gvxCcaAEpwwPNPG7csDpXckMNg=
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc= github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc=
github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
github.com/olekukonko/tablewriter v1.0.7 h1:HCC2e3MM+2g72M81ZcJU11uciw6z/p82aEnm4/ySDGw=
github.com/olekukonko/tablewriter v1.0.7/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=
github.com/olivere/elastic/v7 v7.0.32 h1:R7CXvbu8Eq+WlsLgxmKVKPox0oOwAE/2T9Si5BnvK6E= github.com/olivere/elastic/v7 v7.0.32 h1:R7CXvbu8Eq+WlsLgxmKVKPox0oOwAE/2T9Si5BnvK6E=
github.com/olivere/elastic/v7 v7.0.32/go.mod h1:c7PVmLe3Fxq77PIfY/bZmxY/TAamBhCzZ8xDOE09a9k= github.com/olivere/elastic/v7 v7.0.32/go.mod h1:c7PVmLe3Fxq77PIfY/bZmxY/TAamBhCzZ8xDOE09a9k=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@ -647,6 +645,7 @@ golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

View file

@ -284,10 +284,16 @@ func GetLatestRun(ctx context.Context, repoID int64) (*ActionRun, error) {
return &run, nil return &run, nil
} }
func GetRunBefore(ctx context.Context, _ *ActionRun) (*ActionRun, error) { // GetRunBefore returns the last run that completed a given timestamp (not inclusive).
// TODO return the most recent run related to the run given in argument func GetRunBefore(ctx context.Context, repoID int64, timestamp timeutil.TimeStamp) (*ActionRun, error) {
// see https://codeberg.org/forgejo/user-research/issues/63 for context var run ActionRun
return nil, nil has, err := db.GetEngine(ctx).Where("repo_id=? AND stopped IS NOT NULL AND stopped<?", repoID, timestamp).OrderBy("stopped DESC").Limit(1).Get(&run)
if err != nil {
return nil, err
} else if !has {
return nil, fmt.Errorf("run before: %w", util.ErrNotExist)
}
return &run, nil
} }
func GetLatestRunForBranchAndWorkflow(ctx context.Context, repoID int64, branch, workflowFile, event string) (*ActionRun, error) { func GetLatestRunForBranchAndWorkflow(ctx context.Context, repoID int64, branch, workflowFile, event string) (*ActionRun, error) {

View file

@ -5,7 +5,92 @@ package actions
import ( import (
"testing" "testing"
"time"
"forgejo.org/models/db"
"forgejo.org/models/unittest"
"forgejo.org/modules/timeutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestGetRunBefore(t *testing.T) { func TestGetRunBefore(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
// this repo is part of the test database requiring loading "repository.yml" in main_test.go
var repoID int64 = 1
workflowID := "test_workflow"
// third completed run
time1, err := time.Parse(time.RFC3339, "2024-07-31T15:47:55+08:00")
require.NoError(t, err)
timeutil.MockSet(time1)
run1 := ActionRun{
ID: 1,
Index: 1,
RepoID: repoID,
Stopped: timeutil.TimeStampNow(),
WorkflowID: workflowID,
}
// fourth completed run
time2, err := time.Parse(time.RFC3339, "2024-08-31T15:47:55+08:00")
require.NoError(t, err)
timeutil.MockSet(time2)
run2 := ActionRun{
ID: 2,
Index: 2,
RepoID: repoID,
Stopped: timeutil.TimeStampNow(),
WorkflowID: workflowID,
}
// second completed run
time3, err := time.Parse(time.RFC3339, "2024-07-31T15:47:54+08:00")
require.NoError(t, err)
timeutil.MockSet(time3)
run3 := ActionRun{
ID: 3,
Index: 3,
RepoID: repoID,
Stopped: timeutil.TimeStampNow(),
WorkflowID: workflowID,
}
// first completed run
time4, err := time.Parse(time.RFC3339, "2024-06-30T15:47:54+08:00")
require.NoError(t, err)
timeutil.MockSet(time4)
run4 := ActionRun{
ID: 4,
Index: 4,
RepoID: repoID,
Stopped: timeutil.TimeStampNow(),
WorkflowID: workflowID,
}
require.NoError(t, db.Insert(db.DefaultContext, &run1))
runBefore, err := GetRunBefore(db.DefaultContext, repoID, run1.Stopped)
// there is no run before run1
require.Error(t, err)
require.Nil(t, runBefore)
// now there is only run3 before run1
require.NoError(t, db.Insert(db.DefaultContext, &run3))
runBefore, err = GetRunBefore(db.DefaultContext, repoID, run1.Stopped)
require.NoError(t, err)
assert.Equal(t, run3.ID, runBefore.ID)
// there still is only run3 before run1
require.NoError(t, db.Insert(db.DefaultContext, &run2))
runBefore, err = GetRunBefore(db.DefaultContext, repoID, run1.Stopped)
require.NoError(t, err)
assert.Equal(t, run3.ID, runBefore.ID)
// run4 is further away from run1
require.NoError(t, db.Insert(db.DefaultContext, &run4))
runBefore, err = GetRunBefore(db.DefaultContext, repoID, run1.Stopped)
require.NoError(t, err)
assert.Equal(t, run3.ID, runBefore.ID)
} }

View file

@ -15,6 +15,8 @@ func TestMain(m *testing.M) {
"gpg_key.yml", "gpg_key.yml",
"public_key.yml", "public_key.yml",
"TestParseCommitWithSSHSignature/public_key.yml", "TestParseCommitWithSSHSignature/public_key.yml",
"deploy_key.yml",
"gpg_key_import.yml",
"user.yml", "user.yml",
"email_address.yml", "email_address.yml",
}, },

View file

@ -15,7 +15,6 @@ import (
"strings" "strings"
"time" "time"
"forgejo.org/modules/container"
"forgejo.org/modules/log" "forgejo.org/modules/log"
"forgejo.org/modules/setting" "forgejo.org/modules/setting"
@ -439,12 +438,3 @@ func GetMasterEngine(x Engine) (*xorm.Engine, error) {
return engine, nil return engine, nil
} }
// GetTableNames returns the table name of all registered models.
func GetTableNames() container.Set[string] {
names := make(container.Set[string])
for _, table := range tables {
names.Add(x.TableName(table))
}
return names
}

View file

@ -1,40 +0,0 @@
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later
package db
import (
"slices"
"testing"
"forgejo.org/modules/test"
"github.com/stretchr/testify/assert"
)
func TestGetTableNames(t *testing.T) {
t.Run("Simple", func(t *testing.T) {
defer test.MockVariableValue(&tables, []any{new(GPGKey)})()
assert.Equal(t, []string{"gpg_key"}, GetTableNames().Values())
})
t.Run("Multiple tables", func(t *testing.T) {
defer test.MockVariableValue(&tables, []any{new(GPGKey), new(User), new(BlockedUser)})()
tableNames := GetTableNames().Values()
slices.Sort(tableNames)
assert.Equal(t, []string{"forgejo_blocked_user", "gpg_key", "user"}, tableNames)
})
}
type GPGKey struct{}
type User struct{}
type BlockedUser struct{}
func (*BlockedUser) TableName() string {
return "forgejo_blocked_user"
}

View file

@ -1,7 +0,0 @@
-
id: 1001
uid: 1001
email: AnotherTestUserWithUpperCaseEmail@otto.splvs.net
lower_email: anothertestuserwithuppercaseemail@otto.splvs.net
is_activated: false
is_primary: true

View file

@ -1,12 +0,0 @@
-
id: 1001
lower_name: user1001
name: user1001
full_name: User That loves Upper Cases
email: AnotherTestUserWithUpperCaseEmail@otto.splvs.net
passwd: ZogKvWdyEx:password
passwd_hash_algo: dummy
avatar: ''
avatar_email: anothertestuserwithuppercaseemail@otto.splvs.net
login_name: user1
created_unix: 1672578000

View file

@ -0,0 +1 @@
[] # empty

View file

@ -186,46 +186,10 @@
type: 8 # milestone type: 8 # milestone
poster_id: 1 poster_id: 1
issue_id: 1 # in repo_id 1 issue_id: 1 # in repo_id 1
milestone_id: 10 # not existing milestone milestone_id: 10 # not exsting milestone
old_milestone_id: 0 old_milestone_id: 0
created_unix: 946685080 created_unix: 946685080
-
id: 2004
type: 8 # milestone
poster_id: 1
issue_id: 1 # in repo_id 1
milestone_id: 1
old_milestone_id: 10 # not existing (ghost) milestone
created_unix: 946685085
-
id: 2005
type: 8 # milestone
poster_id: 1
issue_id: 1 # in repo_id 1
milestone_id: 10 # not existing (ghost) milestone
old_milestone_id: 1
created_unix: 946685090
-
id: 2006
type: 8 # milestone
poster_id: 1
issue_id: 1 # in repo_id 1
milestone_id: 11 # not existing (ghost) milestone
old_milestone_id: 10 # not existing (ghost) milestone
created_unix: 946685095
-
id: 2007
type: 8 # milestone
poster_id: 1
issue_id: 1 # in repo_id 1
milestone_id: 0
old_milestone_id: 11 # not existing (ghost) milestone
created_unix: 946685100
- -
id: 2010 id: 2010
type: 30 # project type: 30 # project

View file

@ -0,0 +1 @@
[] # empty

View file

@ -0,0 +1 @@
[] # empty

View file

@ -0,0 +1 @@
[] # empty

View file

@ -0,0 +1 @@
[] # empty

View file

@ -0,0 +1 @@
[] # empty

View file

@ -0,0 +1 @@
[] # empty

View file

@ -0,0 +1 @@
[] # empty

View file

@ -0,0 +1 @@
[] # empty

View file

@ -0,0 +1 @@
[] # empty

View file

@ -0,0 +1 @@
[] # empty

View file

@ -0,0 +1 @@
[] # empty

View file

@ -0,0 +1 @@
[] # empty

View file

@ -108,7 +108,7 @@ var migrations = []*Migration{
// v33 -> v34 // v33 -> v34
NewMigration("Add `notify-email` column to `action_run` table", AddNotifyEmailToActionRun), NewMigration("Add `notify-email` column to `action_run` table", AddNotifyEmailToActionRun),
// v34 -> v35 // v34 -> v35
NewMigration("Noop because of https://codeberg.org/forgejo/forgejo/issues/8373", NoopAddIndexToActionRunStopped), NewMigration("Add index to `stopped` column in `action_run` table", AddIndexToActionRunStopped),
} }
// GetCurrentDBVersion returns the current Forgejo database version. // GetCurrentDBVersion returns the current Forgejo database version.

View file

@ -4,10 +4,16 @@
package forgejo_migrations //nolint:revive package forgejo_migrations //nolint:revive
import ( import (
"forgejo.org/modules/timeutil"
"xorm.io/xorm" "xorm.io/xorm"
) )
// see https://codeberg.org/forgejo/forgejo/issues/8373 func AddIndexToActionRunStopped(x *xorm.Engine) error {
func NoopAddIndexToActionRunStopped(x *xorm.Engine) error { type ActionRun struct {
return nil ID int64
Stopped timeutil.TimeStamp `xorm:"index"`
}
return x.Sync(&ActionRun{})
} }

View file

@ -101,7 +101,7 @@ func (comments CommentList) loadMilestones(ctx context.Context) error {
return nil return nil
} }
milestones := make(map[int64]*Milestone, len(milestoneIDs)) milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs))
left := len(milestoneIDs) left := len(milestoneIDs)
for left > 0 { for left > 0 {
limit := db.DefaultMaxInSize limit := db.DefaultMaxInSize
@ -110,7 +110,7 @@ func (comments CommentList) loadMilestones(ctx context.Context) error {
} }
err := db.GetEngine(ctx). err := db.GetEngine(ctx).
In("id", milestoneIDs[:limit]). In("id", milestoneIDs[:limit]).
Find(&milestones) Find(&milestoneMaps)
if err != nil { if err != nil {
return err return err
} }
@ -118,8 +118,8 @@ func (comments CommentList) loadMilestones(ctx context.Context) error {
milestoneIDs = milestoneIDs[limit:] milestoneIDs = milestoneIDs[limit:]
} }
for _, comment := range comments { for _, issue := range comments {
comment.Milestone = milestones[comment.MilestoneID] issue.Milestone = milestoneMaps[issue.MilestoneID]
} }
return nil return nil
} }
@ -140,7 +140,7 @@ func (comments CommentList) loadOldMilestones(ctx context.Context) error {
return nil return nil
} }
milestones := make(map[int64]*Milestone, len(milestoneIDs)) milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs))
left := len(milestoneIDs) left := len(milestoneIDs)
for left > 0 { for left > 0 {
limit := db.DefaultMaxInSize limit := db.DefaultMaxInSize
@ -149,7 +149,7 @@ func (comments CommentList) loadOldMilestones(ctx context.Context) error {
} }
err := db.GetEngine(ctx). err := db.GetEngine(ctx).
In("id", milestoneIDs[:limit]). In("id", milestoneIDs[:limit]).
Find(&milestones) Find(&milestoneMaps)
if err != nil { if err != nil {
return err return err
} }
@ -157,8 +157,8 @@ func (comments CommentList) loadOldMilestones(ctx context.Context) error {
milestoneIDs = milestoneIDs[limit:] milestoneIDs = milestoneIDs[limit:]
} }
for _, comment := range comments { for _, issue := range comments {
comment.OldMilestone = milestones[comment.OldMilestoneID] issue.OldMilestone = milestoneMaps[issue.MilestoneID]
} }
return nil return nil
} }

View file

@ -48,8 +48,6 @@ type IssuesOptions struct { //nolint
UpdatedBeforeUnix int64 UpdatedBeforeUnix int64
// prioritize issues from this repo // prioritize issues from this repo
PriorityRepoID int64 PriorityRepoID int64
// if this issue index (not ID) exists and matches the filters, *and* priorityrepo sort is used, show it first
PriorityIssueIndex int64
IsArchived optional.Option[bool] IsArchived optional.Option[bool]
// If combined with AllPublic, then private as well as public issues // If combined with AllPublic, then private as well as public issues
@ -62,7 +60,7 @@ type IssuesOptions struct { //nolint
// applySorts sort an issues-related session based on the provided // applySorts sort an issues-related session based on the provided
// sortType string // sortType string
func applySorts(sess *xorm.Session, sortType string, priorityRepoID, priorityIssueIndex int64) { func applySorts(sess *xorm.Session, sortType string, priorityRepoID int64) {
switch sortType { switch sortType {
case "oldest": case "oldest":
sess.Asc("issue.created_unix").Asc("issue.id") sess.Asc("issue.created_unix").Asc("issue.id")
@ -99,11 +97,8 @@ func applySorts(sess *xorm.Session, sortType string, priorityRepoID, priorityIss
case "priorityrepo": case "priorityrepo":
sess.OrderBy("CASE "+ sess.OrderBy("CASE "+
"WHEN issue.repo_id = ? THEN 1 "+ "WHEN issue.repo_id = ? THEN 1 "+
"ELSE 2 END ASC", priorityRepoID) "ELSE 2 END ASC", priorityRepoID).
if priorityIssueIndex != 0 { Desc("issue.created_unix").
sess.OrderBy("issue.index = ? DESC", priorityIssueIndex)
}
sess.Desc("issue.created_unix").
Desc("issue.id") Desc("issue.id")
case "project-column-sorting": case "project-column-sorting":
sess.Asc("project_issue.sorting").Desc("issue.created_unix").Desc("issue.id") sess.Asc("project_issue.sorting").Desc("issue.created_unix").Desc("issue.id")
@ -475,7 +470,7 @@ func Issues(ctx context.Context, opts *IssuesOptions) (IssueList, error) {
Join("INNER", "repository", "`issue`.repo_id = `repository`.id") Join("INNER", "repository", "`issue`.repo_id = `repository`.id")
applyLimit(sess, opts) applyLimit(sess, opts)
applyConditions(sess, opts) applyConditions(sess, opts)
applySorts(sess, opts.SortType, opts.PriorityRepoID, opts.PriorityIssueIndex) applySorts(sess, opts.SortType, opts.PriorityRepoID)
issues := IssueList{} issues := IssueList{}
if err := sess.Find(&issues); err != nil { if err := sess.Find(&issues); err != nil {
@ -499,7 +494,7 @@ func IssueIDs(ctx context.Context, opts *IssuesOptions, otherConds ...builder.Co
} }
applyLimit(sess, opts) applyLimit(sess, opts)
applySorts(sess, opts.SortType, opts.PriorityRepoID, opts.PriorityIssueIndex) applySorts(sess, opts.SortType, opts.PriorityRepoID)
var res []int64 var res []int64
total, err := sess.Select("`issue`.id").Table(&Issue{}).FindAndCount(&res) total, err := sess.Select("`issue`.id").Table(&Issue{}).FindAndCount(&res)

View file

@ -149,7 +149,7 @@ func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptio
} }
findSession := listPullRequestStatement(ctx, baseRepoID, opts) findSession := listPullRequestStatement(ctx, baseRepoID, opts)
applySorts(findSession, opts.SortType, 0, 0) applySorts(findSession, opts.SortType, 0)
findSession = db.SetSessionPagination(findSession, opts) findSession = db.SetSessionPagination(findSession, opts)
prs := make([]*PullRequest, 0, opts.PageSize) prs := make([]*PullRequest, 0, opts.PageSize)
found := findSession.Find(&prs) found := findSession.Find(&prs)

View file

@ -96,7 +96,6 @@ func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, fu
if err := unittest.InitFixtures( if err := unittest.InitFixtures(
unittest.FixturesOptions{ unittest.FixturesOptions{
Dir: fixturesDir, Dir: fixturesDir,
SkipCleanRegistedModels: true,
}, x); err != nil { }, x); err != nil {
t.Errorf("error whilst initializing fixtures from %s: %v", fixturesDir, err) t.Errorf("error whilst initializing fixtures from %s: %v", fixturesDir, err)
return x, deferFn return x, deferFn

View file

@ -100,7 +100,7 @@ type AbuseReport struct {
// The abuse category selected by the reporter. // The abuse category selected by the reporter.
Category AbuseCategoryType `xorm:"INDEX NOT NULL"` Category AbuseCategoryType `xorm:"INDEX NOT NULL"`
// Remarks provided by the reporter. // Remarks provided by the reporter.
Remarks string `xorm:"VARCHAR(500)"` Remarks string
// The ID of the corresponding shadow-copied content when exists; otherwise null. // The ID of the corresponding shadow-copied content when exists; otherwise null.
ShadowCopyID sql.NullInt64 `xorm:"DEFAULT NULL"` ShadowCopyID sql.NullInt64 `xorm:"DEFAULT NULL"`
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"` CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`

View file

@ -17,7 +17,7 @@ import (
type AbuseReportShadowCopy struct { type AbuseReportShadowCopy struct {
ID int64 `xorm:"pk autoincr"` ID int64 `xorm:"pk autoincr"`
RawValue string `xorm:"LONGTEXT NOT NULL"` // A JSON with relevant fields from user, repository, issue or comment table. RawValue string `xorm:"NOT NULL"`
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"` CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
} }

View file

@ -12,8 +12,6 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"forgejo.org/modules/container"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@ -34,15 +32,13 @@ type loader struct {
fixtureFiles []*fixtureFile fixtureFiles []*fixtureFile
} }
func newFixtureLoader(db *sql.DB, dialect string, fixturePaths []string, allTableNames container.Set[string]) (*loader, error) { func newFixtureLoader(db *sql.DB, dialect string, fixturePaths []string) (*loader, error) {
l := &loader{ l := &loader{
db: db, db: db,
dialect: dialect, dialect: dialect,
fixtureFiles: []*fixtureFile{}, fixtureFiles: []*fixtureFile{},
} }
tablesWithoutFixture := allTableNames
// Load fixtures // Load fixtures
for _, fixturePath := range fixturePaths { for _, fixturePath := range fixturePaths {
stat, err := os.Stat(fixturePath) stat, err := os.Stat(fixturePath)
@ -64,7 +60,6 @@ func newFixtureLoader(db *sql.DB, dialect string, fixturePaths []string, allTabl
return nil, err return nil, err
} }
l.fixtureFiles = append(l.fixtureFiles, fixtureFile) l.fixtureFiles = append(l.fixtureFiles, fixtureFile)
tablesWithoutFixture.Remove(fixtureFile.name)
} }
} }
} else { } else {
@ -76,14 +71,6 @@ func newFixtureLoader(db *sql.DB, dialect string, fixturePaths []string, allTabl
} }
} }
// Even though these tables have no fixtures, they can still be used and ensure
// they are cleaned.
for table := range tablesWithoutFixture.Seq() {
l.fixtureFiles = append(l.fixtureFiles, &fixtureFile{
name: table,
})
}
return l, nil return l, nil
} }
@ -191,13 +178,13 @@ func (l *loader) Load() error {
}() }()
// Clean the table and re-insert the fixtures. // Clean the table and re-insert the fixtures.
tableDeleted := make(container.Set[string]) tableDeleted := map[string]struct{}{}
for _, fixture := range l.fixtureFiles { for _, fixture := range l.fixtureFiles {
if !tableDeleted.Contains(fixture.name) { if _, ok := tableDeleted[fixture.name]; !ok {
if _, err := tx.Exec(fmt.Sprintf("DELETE FROM %s", l.quoteKeyword(fixture.name))); err != nil { if _, err := tx.Exec(fmt.Sprintf("DELETE FROM %s", l.quoteKeyword(fixture.name))); err != nil {
return fmt.Errorf("cannot delete table %s: %w", fixture.name, err) return fmt.Errorf("cannot delete table %s: %w", fixture.name, err)
} }
tableDeleted.Add(fixture.name) tableDeleted[fixture.name] = struct{}{}
} }
for _, insertSQL := range fixture.insertSQLs { for _, insertSQL := range fixture.insertSQLs {

View file

@ -7,12 +7,10 @@ package unittest
import ( import (
"fmt" "fmt"
"path/filepath" "path/filepath"
"sync"
"time" "time"
"forgejo.org/models/db" "forgejo.org/models/db"
"forgejo.org/modules/auth/password/hash" "forgejo.org/modules/auth/password/hash"
"forgejo.org/modules/container"
"forgejo.org/modules/setting" "forgejo.org/modules/setting"
"xorm.io/xorm" "xorm.io/xorm"
@ -46,8 +44,6 @@ func OverrideFixtures(dir string) func() {
} }
} }
var allTableNames = sync.OnceValue(db.GetTableNames)
// InitFixtures initialize test fixtures for a test database // InitFixtures initialize test fixtures for a test database
func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) { func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) {
e, err := GetXORMEngine(engine...) e, err := GetXORMEngine(engine...)
@ -79,12 +75,7 @@ func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) {
panic("Unsupported RDBMS for test") panic("Unsupported RDBMS for test")
} }
var allTables container.Set[string] fixturesLoader, err = newFixtureLoader(e.DB().DB, dialect, fixturePaths)
if !opts.SkipCleanRegistedModels {
allTables = allTableNames().Clone()
}
fixturesLoader, err = newFixtureLoader(e.DB().DB, dialect, fixturePaths, allTables)
if err != nil { if err != nil {
return err return err
} }

View file

@ -217,10 +217,6 @@ type FixturesOptions struct {
Files []string Files []string
Dirs []string Dirs []string
Base string Base string
// By default all registered models are cleaned, even if they do not have
// fixture. Enabling this will skip that and only models with fixtures are
// considered.
SkipCleanRegistedModels bool
} }
// CreateTestEngine creates a memory database and loads the fixture data from fixturesDir // CreateTestEngine creates a memory database and loads the fixture data from fixturesDir

View file

@ -19,7 +19,7 @@ func (u *User) APActorID() string {
return fmt.Sprintf("%sapi/v1/activitypub/user-id/%s", setting.AppURL, url.PathEscape(fmt.Sprintf("%d", u.ID))) return fmt.Sprintf("%sapi/v1/activitypub/user-id/%s", setting.AppURL, url.PathEscape(fmt.Sprintf("%d", u.ID)))
} }
// KeyID returns the ID of the user's public key // APActorKeyID returns the ID of the user's public key
func (u *User) KeyID() string { func (u *User) APActorKeyID() string {
return u.APActorID() + "#main-key" return u.APActorID() + "#main-key"
} }

View file

@ -181,20 +181,3 @@ func TestDeletePrimaryEmailAddressOfUser(t *testing.T) {
assert.True(t, user_model.IsErrEmailAddressNotExist(err)) assert.True(t, user_model.IsErrEmailAddressNotExist(err))
assert.Nil(t, email) assert.Nil(t, email)
} }
func TestActivateUserEmail(t *testing.T) {
defer unittest.OverrideFixtures("models/fixtures/TestActivateUserEmail")()
require.NoError(t, unittest.PrepareTestDatabase())
t.Run("Activate email", func(t *testing.T) {
require.NoError(t, user_model.ActivateUserEmail(t.Context(), 1001, "AnotherTestUserWithUpperCaseEmail@otto.splvs.net", true))
unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{UID: 1001}, "is_activated = true")
})
t.Run("Deactivate email", func(t *testing.T) {
require.NoError(t, user_model.ActivateUserEmail(t.Context(), 1001, "AnotherTestUserWithUpperCaseEmail@otto.splvs.net", false))
unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{UID: 1001}, "is_activated = false")
})
}

View file

@ -182,11 +182,11 @@ func (u *User) BeforeUpdate() {
u.MaxRepoCreation = -1 u.MaxRepoCreation = -1
} }
// Ensure AvatarEmail is set for non-organization users, because organization // Organization does not need email
// are not required to have a email set. u.Email = strings.ToLower(u.Email)
if !u.IsOrganization() { if !u.IsOrganization() {
if len(u.AvatarEmail) == 0 { if len(u.AvatarEmail) == 0 {
u.AvatarEmail = strings.ToLower(u.Email) u.AvatarEmail = u.Email
} }
} }

View file

@ -57,6 +57,14 @@ func CreateFederatedUser(ctx context.Context, user *User, federatedUser *Federat
return committer.Commit() return committer.Commit()
} }
func (federatedUser *FederatedUser) UpdateFederatedUser(ctx context.Context) error {
if _, err := validation.IsValid(federatedUser); err != nil {
return err
}
_, err := db.GetEngine(ctx).ID(federatedUser.ID).Cols("inbox_path").Update(federatedUser)
return err
}
func FindFederatedUser(ctx context.Context, externalID string, federationHostID int64) (*User, *FederatedUser, error) { func FindFederatedUser(ctx context.Context, externalID string, federationHostID int64) (*User, *FederatedUser, error) {
federatedUser := new(FederatedUser) federatedUser := new(FederatedUser)
user := new(User) user := new(User)
@ -211,6 +219,7 @@ func RemoveFollower(ctx context.Context, followedUser *User, followingUser *Fede
return err return err
} }
// TODO: We should unify Activity-pub-following and classical following (see models/user/follow.go)
func IsFollowingAp(ctx context.Context, followedUser *User, followingUser *FederatedUser) (bool, error) { func IsFollowingAp(ctx context.Context, followedUser *User, followingUser *FederatedUser) (bool, error) {
if res, err := validation.IsValid(followedUser); !res { if res, err := validation.IsValid(followedUser); !res {
return false, err return false, err

View file

@ -150,7 +150,7 @@ func TestAPActorID_APActorID(t *testing.T) {
func TestKeyID(t *testing.T) { func TestKeyID(t *testing.T) {
user := user_model.User{ID: 1} user := user_model.User{ID: 1}
url := user.KeyID() url := user.APActorKeyID()
expected := "https://try.gitea.io/api/v1/activitypub/user-id/1#main-key" expected := "https://try.gitea.io/api/v1/activitypub/user-id/1#main-key"
assert.Equal(t, expected, url) assert.Equal(t, expected, url)
} }

View file

@ -89,7 +89,6 @@ func NewClientFactory() (c *ClientFactory, err error) {
type APClientFactory interface { type APClientFactory interface {
WithKeys(ctx context.Context, user *user_model.User, pubID string) (APClient, error) WithKeys(ctx context.Context, user *user_model.User, pubID string) (APClient, error)
WithKeysDirect(ctx context.Context, privateKey, pubID string) (APClient, error)
} }
// Client struct // Client struct
@ -104,8 +103,12 @@ type Client struct {
} }
// NewRequest function // NewRequest function
func (cf *ClientFactory) WithKeysDirect(ctx context.Context, privateKey, pubID string) (APClient, error) { func (cf *ClientFactory) WithKeys(ctx context.Context, user *user_model.User, pubID string) (APClient, error) {
privPem, _ := pem.Decode([]byte(privateKey)) priv, err := GetPrivateKey(ctx, user)
if err != nil {
return nil, err
}
privPem, _ := pem.Decode([]byte(priv))
privParsed, err := x509.ParsePKCS1PrivateKey(privPem.Bytes) privParsed, err := x509.ParsePKCS1PrivateKey(privPem.Bytes)
if err != nil { if err != nil {
return nil, err return nil, err
@ -123,14 +126,6 @@ func (cf *ClientFactory) WithKeysDirect(ctx context.Context, privateKey, pubID s
return &c, nil return &c, nil
} }
func (cf *ClientFactory) WithKeys(ctx context.Context, user *user_model.User, pubID string) (APClient, error) {
priv, err := GetPrivateKey(ctx, user)
if err != nil {
return nil, err
}
return cf.WithKeysDirect(ctx, priv, pubID)
}
// NewRequest function // NewRequest function
func (c *Client) newRequest(method string, b []byte, to string) (req *http.Request, err error) { func (c *Client) newRequest(method string, b []byte, to string) (req *http.Request, err error) {
buf := bytes.NewBuffer(b) buf := bytes.NewBuffer(b)
@ -154,7 +149,6 @@ func (c *Client) Post(b []byte, to string) (resp *http.Response, err error) {
return nil, err return nil, err
} }
if c.pubID != "" {
signer, _, err := httpsig.NewSigner(c.algs, c.digestAlg, c.postHeaders, httpsig.Signature, httpsigExpirationTime) signer, _, err := httpsig.NewSigner(c.algs, c.digestAlg, c.postHeaders, httpsig.Signature, httpsigExpirationTime)
if err != nil { if err != nil {
return nil, err return nil, err
@ -162,7 +156,6 @@ func (c *Client) Post(b []byte, to string) (resp *http.Response, err error) {
if err := signer.SignRequest(c.priv, c.pubID, req, b); err != nil { if err := signer.SignRequest(c.priv, c.pubID, req, b); err != nil {
return nil, err return nil, err
} }
}
resp, err = c.client.Do(req) resp, err = c.client.Do(req)
return resp, err return resp, err
@ -174,8 +167,6 @@ func (c *Client) Get(to string) (resp *http.Response, err error) {
if req, err = c.newRequest(http.MethodGet, nil, to); err != nil { if req, err = c.newRequest(http.MethodGet, nil, to); err != nil {
return nil, err return nil, err
} }
if c.pubID != "" {
signer, _, err := httpsig.NewSigner(c.algs, c.digestAlg, c.getHeaders, httpsig.Signature, httpsigExpirationTime) signer, _, err := httpsig.NewSigner(c.algs, c.digestAlg, c.getHeaders, httpsig.Signature, httpsigExpirationTime)
if err != nil { if err != nil {
return nil, err return nil, err
@ -183,7 +174,6 @@ func (c *Client) Get(to string) (resp *http.Response, err error) {
if err := signer.SignRequest(c.priv, c.pubID, req, nil); err != nil { if err := signer.SignRequest(c.priv, c.pubID, req, nil); err != nil {
return nil, err return nil, err
} }
}
resp, err = c.client.Do(req) resp, err = c.client.Do(req)
return resp, err return resp, err

View file

@ -74,8 +74,3 @@ func (s Set[T]) Values() []T {
func (s Set[T]) Seq() iter.Seq[T] { func (s Set[T]) Seq() iter.Seq[T] {
return maps.Keys(s) return maps.Keys(s)
} }
// Clone returns a identical shallow copy of this set.
func (s Set[T]) Clone() Set[T] {
return maps.Clone(s)
}

View file

@ -47,11 +47,4 @@ func TestSet(t *testing.T) {
assert.False(t, s.IsSubset([]string{"key1"})) assert.False(t, s.IsSubset([]string{"key1"}))
assert.True(t, s.IsSubset([]string{})) assert.True(t, s.IsSubset([]string{}))
t.Run("Clone", func(t *testing.T) {
clonedSet := s.Clone()
clonedSet.Remove("key6")
assert.False(t, clonedSet.Contains("key6"))
assert.True(t, s.Contains("key6"))
})
} }

View file

@ -132,7 +132,7 @@ func (r *BlameReader) Close() error {
// CreateBlameReader creates reader for given repository, commit and file // CreateBlameReader creates reader for given repository, commit and file
func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath string, commit *Commit, file string, bypassBlameIgnore bool) (*BlameReader, error) { func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath string, commit *Commit, file string, bypassBlameIgnore bool) (*BlameReader, error) {
var ignoreRevsFile *string var ignoreRevsFile *string
if !bypassBlameIgnore { if CheckGitVersionAtLeast("2.23") == nil && !bypassBlameIgnore {
ignoreRevsFile = tryCreateBlameIgnoreRevsFile(commit) ignoreRevsFile = tryCreateBlameIgnoreRevsFile(commit)
} }

View file

@ -8,7 +8,6 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"encoding/base64" "encoding/base64"
"fmt"
"io" "io"
"forgejo.org/modules/log" "forgejo.org/modules/log"
@ -173,43 +172,60 @@ func (b *Blob) GetBlobContent(limit int64) (string, error) {
return string(buf), err return string(buf), err
} }
type BlobTooLargeError struct { // GetBlobLineCount gets line count of the blob
Size, Limit int64 func (b *Blob) GetBlobLineCount() (int, error) {
reader, err := b.DataAsync()
if err != nil {
return 0, err
} }
defer reader.Close()
buf := make([]byte, 32*1024)
count := 1
lineSep := []byte{'\n'}
func (b BlobTooLargeError) Error() string { c, err := reader.Read(buf)
return fmt.Sprintf("blob: content larger than limit (%d > %d)", b.Size, b.Limit) if c == 0 && err == io.EOF {
return 0, nil
} }
for {
// GetContentBase64 Reads the content of the blob and returns it as base64 encoded string. count += bytes.Count(buf[:c], lineSep)
// Returns [BlobTooLargeError] if the (unencoded) content is larger than the limit. switch {
func (b *Blob) GetContentBase64(limit int64) (string, error) { case err == io.EOF:
if b.Size() > limit { return count, nil
return "", BlobTooLargeError{ case err != nil:
Size: b.Size(), return count, err
Limit: limit, }
c, err = reader.Read(buf)
} }
} }
rc, size, err := b.NewTruncatedReader(limit) // GetBlobContentBase64 Reads the content of the blob with a base64 encode and returns the encoded string
func (b *Blob) GetBlobContentBase64() (string, error) {
dataRc, err := b.DataAsync()
if err != nil { if err != nil {
return "", err return "", err
} }
defer rc.Close() defer dataRc.Close()
encoding := base64.StdEncoding pr, pw := io.Pipe()
buf := bytes.NewBuffer(make([]byte, 0, encoding.EncodedLen(int(size)))) encoder := base64.NewEncoder(base64.StdEncoding, pw)
encoder := base64.NewEncoder(encoding, buf) go func() {
_, err := io.Copy(encoder, dataRc)
_ = encoder.Close()
if _, err := io.Copy(encoder, rc); err != nil { if err != nil {
_ = pw.CloseWithError(err)
} else {
_ = pw.Close()
}
}()
out, err := io.ReadAll(pr)
if err != nil {
return "", err return "", err
} }
if err := encoder.Close(); err != nil { return string(out), nil
return "", err
}
return buf.String(), nil
} }
// GuessContentType guesses the content type of the blob. // GuessContentType guesses the content type of the blob.
@ -220,7 +236,7 @@ func (b *Blob) GuessContentType() (typesniffer.SniffedType, error) {
} }
defer r.Close() defer r.Close()
return typesniffer.DetectContentTypeFromReader(r, b.Name()) return typesniffer.DetectContentTypeFromReader(r)
} }
// GetBlob finds the blob object in the repository. // GetBlob finds the blob object in the repository.

View file

@ -63,24 +63,6 @@ func TestBlob(t *testing.T) {
require.Equal(t, "file2\n", r) require.Equal(t, "file2\n", r)
}) })
t.Run("GetContentBase64", func(t *testing.T) {
r, err := testBlob.GetContentBase64(100)
require.NoError(t, err)
require.Equal(t, "ZmlsZTIK", r)
r, err = testBlob.GetContentBase64(-1)
require.ErrorAs(t, err, &BlobTooLargeError{})
require.Empty(t, r)
r, err = testBlob.GetContentBase64(4)
require.ErrorAs(t, err, &BlobTooLargeError{})
require.Empty(t, r)
r, err = testBlob.GetContentBase64(6)
require.NoError(t, err)
require.Equal(t, "ZmlsZTIK", r)
})
t.Run("NewTruncatedReader", func(t *testing.T) { t.Run("NewTruncatedReader", func(t *testing.T) {
// read fewer than available // read fewer than available
rc, size, err := testBlob.NewTruncatedReader(100) rc, size, err := testBlob.NewTruncatedReader(100)

View file

@ -412,7 +412,11 @@ func (c *Commit) GetSubModule(entryname string) (string, error) {
// GetBranchName gets the closest branch name (as returned by 'git name-rev --name-only') // GetBranchName gets the closest branch name (as returned by 'git name-rev --name-only')
func (c *Commit) GetBranchName() (string, error) { func (c *Commit) GetBranchName() (string, error) {
cmd := NewCommand(c.repo.Ctx, "name-rev", "--exclude", "refs/tags/*", "--name-only", "--no-undefined").AddDynamicArguments(c.ID.String()) cmd := NewCommand(c.repo.Ctx, "name-rev")
if CheckGitVersionAtLeast("2.13.0") == nil {
cmd.AddArguments("--exclude", "refs/tags/*")
}
cmd.AddArguments("--name-only", "--no-undefined").AddDynamicArguments(c.ID.String())
data, _, err := cmd.RunStdString(&RunOpts{Dir: c.repo.Path}) data, _, err := cmd.RunStdString(&RunOpts{Dir: c.repo.Path})
if err != nil { if err != nil {
// handle special case where git can not describe commit // handle special case where git can not describe commit

View file

@ -23,7 +23,7 @@ import (
) )
// RequiredVersion is the minimum Git version required // RequiredVersion is the minimum Git version required
const RequiredVersion = "2.34.1" const RequiredVersion = "2.0.0"
var ( var (
// GitExecutable is the command name of git // GitExecutable is the command name of git
@ -33,6 +33,7 @@ var (
// DefaultContext is the default context to run git commands in, must be initialized by git.InitXxx // DefaultContext is the default context to run git commands in, must be initialized by git.InitXxx
DefaultContext context.Context DefaultContext context.Context
SupportProcReceive bool // >= 2.29
SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an experimental curiosity SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an experimental curiosity
InvertedGitFlushEnv bool // 2.43.1 InvertedGitFlushEnv bool // 2.43.1
SupportCheckAttrOnBare bool // >= 2.40 SupportCheckAttrOnBare bool // >= 2.40
@ -112,7 +113,7 @@ func VersionInfo() string {
format := "%s" format := "%s"
args := []any{GitVersion.Original()} args := []any{GitVersion.Original()}
// Since git wire protocol has been released from git v2.18 // Since git wire protocol has been released from git v2.18
if setting.Git.EnableAutoGitWireProtocol { if setting.Git.EnableAutoGitWireProtocol && CheckGitVersionAtLeast("2.18") == nil {
format += ", Wire Protocol %s Enabled" format += ", Wire Protocol %s Enabled"
args = append(args, "Version 2") // for focus color args = append(args, "Version 2") // for focus color
} }
@ -171,13 +172,16 @@ func InitFull(ctx context.Context) (err error) {
_ = os.Setenv("GNUPGHOME", filepath.Join(HomeDir(), ".gnupg")) _ = os.Setenv("GNUPGHOME", filepath.Join(HomeDir(), ".gnupg"))
} }
if setting.Git.EnableAutoGitWireProtocol { // Since git wire protocol has been released from git v2.18
if setting.Git.EnableAutoGitWireProtocol && CheckGitVersionAtLeast("2.18") == nil {
globalCommandArgs = append(globalCommandArgs, "-c", "protocol.version=2") globalCommandArgs = append(globalCommandArgs, "-c", "protocol.version=2")
} }
// Explicitly disable credential helper, otherwise Git credentials might leak // Explicitly disable credential helper, otherwise Git credentials might leak
if CheckGitVersionAtLeast("2.9") == nil {
globalCommandArgs = append(globalCommandArgs, "-c", "credential.helper=") globalCommandArgs = append(globalCommandArgs, "-c", "credential.helper=")
}
SupportProcReceive = CheckGitVersionAtLeast("2.29") == nil
SupportHashSha256 = CheckGitVersionAtLeast("2.42") == nil SupportHashSha256 = CheckGitVersionAtLeast("2.42") == nil
SupportCheckAttrOnBare = CheckGitVersionAtLeast("2.40") == nil SupportCheckAttrOnBare = CheckGitVersionAtLeast("2.40") == nil
if SupportHashSha256 { if SupportHashSha256 {
@ -191,6 +195,9 @@ func InitFull(ctx context.Context) (err error) {
SupportGrepMaxCount = CheckGitVersionAtLeast("2.38") == nil SupportGrepMaxCount = CheckGitVersionAtLeast("2.38") == nil
if setting.LFS.StartServer { if setting.LFS.StartServer {
if CheckGitVersionAtLeast("2.1.2") != nil {
return errors.New("LFS server support requires Git >= 2.1.2")
}
globalCommandArgs = append(globalCommandArgs, "-c", "filter.lfs.required=", "-c", "filter.lfs.smudge=", "-c", "filter.lfs.clean=") globalCommandArgs = append(globalCommandArgs, "-c", "filter.lfs.required=", "-c", "filter.lfs.smudge=", "-c", "filter.lfs.clean=")
} }
@ -227,15 +234,18 @@ func syncGitConfig() (err error) {
} }
} }
// Set git some configurations - these must be set to these values for forgejo to work correctly // Set git some configurations - these must be set to these values for gitea to work correctly
if err := configSet("core.quotePath", "false"); err != nil { if err := configSet("core.quotePath", "false"); err != nil {
return err return err
} }
if CheckGitVersionAtLeast("2.10") == nil {
if err := configSet("receive.advertisePushOptions", "true"); err != nil { if err := configSet("receive.advertisePushOptions", "true"); err != nil {
return err return err
} }
}
if CheckGitVersionAtLeast("2.18") == nil {
if err := configSet("core.commitGraph", "true"); err != nil { if err := configSet("core.commitGraph", "true"); err != nil {
return err return err
} }
@ -245,11 +255,18 @@ func syncGitConfig() (err error) {
if err := configSet("fetch.writeCommitGraph", "true"); err != nil { if err := configSet("fetch.writeCommitGraph", "true"); err != nil {
return err return err
} }
}
if SupportProcReceive {
// set support for AGit flow // set support for AGit flow
if err := configAddNonExist("receive.procReceiveRefs", "refs/for"); err != nil { if err := configAddNonExist("receive.procReceiveRefs", "refs/for"); err != nil {
return err return err
} }
} else {
if err := configUnsetAll("receive.procReceiveRefs", "refs/for"); err != nil {
return err
}
}
// Due to CVE-2022-24765, git now denies access to git directories which are not owned by current user // Due to CVE-2022-24765, git now denies access to git directories which are not owned by current user
// however, some docker users and samba users find it difficult to configure their systems so that Gitea's git repositories are owned by the Gitea user. (Possibly Windows Service users - but ownership in this case should really be set correctly on the filesystem.) // however, some docker users and samba users find it difficult to configure their systems so that Gitea's git repositories are owned by the Gitea user. (Possibly Windows Service users - but ownership in this case should really be set correctly on the filesystem.)
@ -267,6 +284,11 @@ func syncGitConfig() (err error) {
switch setting.Repository.Signing.Format { switch setting.Repository.Signing.Format {
case "ssh": case "ssh":
// First do a git version check.
if CheckGitVersionAtLeast("2.34.0") != nil {
return errors.New("ssh signing requires Git >= 2.34.0")
}
// Get the ssh-keygen binary that Git will use. // Get the ssh-keygen binary that Git will use.
// This can be overridden in app.ini in [git.config] section, so we must // This can be overridden in app.ini in [git.config] section, so we must
// query this information. // query this information.
@ -303,7 +325,8 @@ func syncGitConfig() (err error) {
} }
} }
if !setting.Git.DisablePartialClone { // By default partial clones are disabled, enable them from git v2.22
if !setting.Git.DisablePartialClone && CheckGitVersionAtLeast("2.22") == nil {
if err = configSet("uploadpack.allowfilter", "true"); err != nil { if err = configSet("uploadpack.allowfilter", "true"); err != nil {
return err return err
} }

View file

@ -14,6 +14,7 @@ import (
"forgejo.org/modules/test" "forgejo.org/modules/test"
"forgejo.org/modules/util" "forgejo.org/modules/util"
"github.com/hashicorp/go-version"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -104,6 +105,10 @@ func TestSyncConfigGPGFormat(t *testing.T) {
}) })
t.Run("SSH format", func(t *testing.T) { t.Run("SSH format", func(t *testing.T) {
if CheckGitVersionAtLeast("2.34.0") != nil {
t.SkipNow()
}
r, err := os.OpenRoot(t.TempDir()) r, err := os.OpenRoot(t.TempDir())
require.NoError(t, err) require.NoError(t, err)
f, err := r.OpenFile("ssh-keygen", os.O_CREATE|os.O_TRUNC, 0o700) f, err := r.OpenFile("ssh-keygen", os.O_CREATE|os.O_TRUNC, 0o700)
@ -116,6 +121,13 @@ func TestSyncConfigGPGFormat(t *testing.T) {
assert.True(t, gitConfigContains("[gpg]")) assert.True(t, gitConfigContains("[gpg]"))
assert.True(t, gitConfigContains("format = ssh")) assert.True(t, gitConfigContains("format = ssh"))
t.Run("Old version", func(t *testing.T) {
oldVersion, err := version.NewVersion("2.33.0")
require.NoError(t, err)
defer test.MockVariableValue(&GitVersion, oldVersion)()
require.ErrorContains(t, syncGitConfig(), "ssh signing requires Git >= 2.34.0")
})
t.Run("No ssh-keygen binary", func(t *testing.T) { t.Run("No ssh-keygen binary", func(t *testing.T) {
require.NoError(t, r.Remove("ssh-keygen")) require.NoError(t, r.Remove("ssh-keygen"))
require.ErrorContains(t, syncGitConfig(), "git signing requires a ssh-keygen binary") require.ErrorContains(t, syncGitConfig(), "git signing requires a ssh-keygen binary")

View file

@ -16,6 +16,26 @@ import (
"forgejo.org/modules/log" "forgejo.org/modules/log"
) )
// RevListAllObjects runs rev-list --objects --all and writes to a pipewriter
func RevListAllObjects(ctx context.Context, revListWriter *io.PipeWriter, wg *sync.WaitGroup, basePath string, errChan chan<- error) {
defer wg.Done()
defer revListWriter.Close()
stderr := new(bytes.Buffer)
var errbuf strings.Builder
cmd := git.NewCommand(ctx, "rev-list", "--objects", "--all")
if err := cmd.Run(&git.RunOpts{
Dir: basePath,
Stdout: revListWriter,
Stderr: stderr,
}); err != nil {
log.Error("git rev-list --objects --all [%s]: %v - %s", basePath, err, errbuf.String())
err = fmt.Errorf("git rev-list --objects --all [%s]: %w - %s", basePath, err, errbuf.String())
_ = revListWriter.CloseWithError(err)
errChan <- err
}
}
// RevListObjects run rev-list --objects from headSHA to baseSHA // RevListObjects run rev-list --objects from headSHA to baseSHA
func RevListObjects(ctx context.Context, revListWriter *io.PipeWriter, wg *sync.WaitGroup, tmpBasePath, headSHA, baseSHA string, errChan chan<- error) { func RevListObjects(ctx context.Context, revListWriter *io.PipeWriter, wg *sync.WaitGroup, tmpBasePath, headSHA, baseSHA string, errChan chan<- error) {
defer wg.Done() defer wg.Done()

View file

@ -12,7 +12,14 @@ import (
// GetRemoteAddress returns remote url of git repository in the repoPath with special remote name // GetRemoteAddress returns remote url of git repository in the repoPath with special remote name
func GetRemoteAddress(ctx context.Context, repoPath, remoteName string) (string, error) { func GetRemoteAddress(ctx context.Context, repoPath, remoteName string) (string, error) {
result, _, err := NewCommand(ctx, "remote", "get-url").AddDynamicArguments(remoteName).RunStdString(&RunOpts{Dir: repoPath}) var cmd *Command
if CheckGitVersionAtLeast("2.7") == nil {
cmd = NewCommand(ctx, "remote", "get-url").AddDynamicArguments(remoteName)
} else {
cmd = NewCommand(ctx, "config", "--get").AddDynamicArguments("remote." + remoteName + ".url")
}
result, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath})
if err != nil { if err != nil {
return "", err return "", err
} }

View file

@ -5,6 +5,7 @@ package git
import ( import (
"context" "context"
"fmt"
"io" "io"
"io/fs" "io/fs"
"os" "os"
@ -196,7 +197,7 @@ func TestGitAttributeCheckerError(t *testing.T) {
path := t.TempDir() path := t.TempDir()
// we can't use unittest.CopyDir because of an import cycle (git.Init in unittest) // we can't use unittest.CopyDir because of an import cycle (git.Init in unittest)
require.NoError(t, os.CopyFS(path, os.DirFS(filepath.Join(testReposDir, "language_stats_repo")))) require.NoError(t, CopyFS(path, os.DirFS(filepath.Join(testReposDir, "language_stats_repo"))))
gitRepo, err := openRepositoryWithDefaultContext(path) gitRepo, err := openRepositoryWithDefaultContext(path)
require.NoError(t, err) require.NoError(t, err)
@ -323,3 +324,32 @@ func TestGitAttributeCheckerError(t *testing.T) {
require.ErrorIs(t, err, fs.ErrClosed) require.ErrorIs(t, err, fs.ErrClosed)
}) })
} }
// CopyFS is adapted from https://github.com/golang/go/issues/62484
// which should be available with go1.23
func CopyFS(dir string, fsys fs.FS) error {
return fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, _ error) error {
targ := filepath.Join(dir, filepath.FromSlash(path))
if d.IsDir() {
return os.MkdirAll(targ, 0o777)
}
r, err := fsys.Open(path)
if err != nil {
return err
}
defer r.Close()
info, err := r.Stat()
if err != nil {
return err
}
w, err := os.OpenFile(targ, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o666|info.Mode()&0o777)
if err != nil {
return err
}
if _, err := io.Copy(w, r); err != nil {
w.Close()
return fmt.Errorf("copying %s: %v", path, err)
}
return w.Close()
})
}

View file

@ -443,6 +443,7 @@ func (repo *Repository) getCommitsBeforeLimit(id ObjectID, num int) ([]*Commit,
} }
func (repo *Repository) getBranches(commit *Commit, limit int) ([]string, error) { func (repo *Repository) getBranches(commit *Commit, limit int) ([]string, error) {
if CheckGitVersionAtLeast("2.7.0") == nil {
command := NewCommand(repo.Ctx, "for-each-ref", "--format=%(refname:strip=2)").AddOptionValues("--contains", commit.ID.String(), BranchPrefix) command := NewCommand(repo.Ctx, "for-each-ref", "--format=%(refname:strip=2)").AddOptionValues("--contains", commit.ID.String(), BranchPrefix)
if limit != -1 { if limit != -1 {
@ -458,6 +459,29 @@ func (repo *Repository) getBranches(commit *Commit, limit int) ([]string, error)
return branches, nil return branches, nil
} }
stdout, _, err := NewCommand(repo.Ctx, "branch").AddOptionValues("--contains", commit.ID.String()).RunStdString(&RunOpts{Dir: repo.Path})
if err != nil {
return nil, err
}
refs := strings.Split(stdout, "\n")
var max int
if len(refs) > limit {
max = limit
} else {
max = len(refs) - 1
}
branches := make([]string, max)
for i, ref := range refs[:max] {
parts := strings.Fields(ref)
branches[i] = parts[len(parts)-1]
}
return branches, nil
}
// GetCommitsFromIDs get commits from commit IDs // GetCommitsFromIDs get commits from commit IDs
func (repo *Repository) GetCommitsFromIDs(commitIDs []string) []*Commit { func (repo *Repository) GetCommitsFromIDs(commitIDs []string) []*Commit {
commits := make([]*Commit, 0, len(commitIDs)) commits := make([]*Commit, 0, len(commitIDs))

View file

@ -11,8 +11,10 @@ import (
// WriteCommitGraph write commit graph to speed up repo access // WriteCommitGraph write commit graph to speed up repo access
// this requires git v2.18 to be installed // this requires git v2.18 to be installed
func WriteCommitGraph(ctx context.Context, repoPath string) error { func WriteCommitGraph(ctx context.Context, repoPath string) error {
if CheckGitVersionAtLeast("2.18") == nil {
if _, _, err := NewCommand(ctx, "commit-graph", "write").RunStdString(&RunOpts{Dir: repoPath}); err != nil { if _, _, err := NewCommand(ctx, "commit-graph", "write").RunStdString(&RunOpts{Dir: repoPath}); err != nil {
return fmt.Errorf("unable to write commit-graph for '%s' : %w", repoPath, err) return fmt.Errorf("unable to write commit-graph for '%s' : %w", repoPath, err)
} }
}
return nil return nil
} }

View file

@ -116,37 +116,32 @@ func (te *TreeEntry) Type() string {
} }
} }
// LinkTarget returns the target of the symlink as string.
func (te *TreeEntry) LinkTarget() (string, error) {
if !te.IsLink() {
return "", ErrBadLink{te.Name(), "not a symlink"}
}
const symlinkLimit = 4096 // according to git config core.longpaths https://stackoverflow.com/a/22575737
blob := te.Blob()
if blob.Size() > symlinkLimit {
return "", ErrBadLink{te.Name(), "symlink too large"}
}
rc, size, err := blob.NewTruncatedReader(symlinkLimit)
if err != nil {
return "", err
}
defer rc.Close()
buf := make([]byte, int(size))
_, err = io.ReadFull(rc, buf)
return string(buf), err
}
// FollowLink returns the entry pointed to by a symlink // FollowLink returns the entry pointed to by a symlink
func (te *TreeEntry) FollowLink() (*TreeEntry, string, error) { func (te *TreeEntry) FollowLink() (*TreeEntry, string, error) {
if !te.IsLink() {
return nil, "", ErrBadLink{te.Name(), "not a symlink"}
}
// read the link // read the link
lnk, err := te.LinkTarget() r, err := te.Blob().DataAsync()
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
closed := false
defer func() {
if !closed {
_ = r.Close()
}
}()
buf := make([]byte, te.Size())
_, err = io.ReadFull(r, buf)
if err != nil {
return nil, "", err
}
_ = r.Close()
closed = true
lnk := string(buf)
t := te.ptree t := te.ptree
// traverse up directories // traverse up directories

View file

@ -99,7 +99,7 @@ func setServeHeadersByFile(r *http.Request, w http.ResponseWriter, filePath stri
Filename: path.Base(filePath), Filename: path.Base(filePath),
} }
sniffedType := typesniffer.DetectContentType(mineBuf, opts.Filename) sniffedType := typesniffer.DetectContentType(mineBuf)
// the "render" parameter came from year 2016: 638dd24c, it doesn't have clear meaning, so I think it could be removed later // the "render" parameter came from year 2016: 638dd24c, it doesn't have clear meaning, so I think it could be removed later
isPlain := sniffedType.IsText() || r.FormValue("render") != "" isPlain := sniffedType.IsText() || r.FormValue("render") != ""

View file

@ -177,7 +177,7 @@ func (b *Indexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserErro
fileContents, err := io.ReadAll(io.LimitReader(batchReader, size)) fileContents, err := io.ReadAll(io.LimitReader(batchReader, size))
if err != nil { if err != nil {
return err return err
} else if !typesniffer.DetectContentType(fileContents, update.Filename).IsText() { } else if !typesniffer.DetectContentType(fileContents).IsText() {
// FIXME: UTF-16 files will probably fail here // FIXME: UTF-16 files will probably fail here
// Even if the file is not recognized as a "text file", we could still put its name into the indexers to make the filename become searchable, while leave the content to empty. // Even if the file is not recognized as a "text file", we could still put its name into the indexers to make the filename become searchable, while leave the content to empty.
fileContents = nil fileContents = nil

View file

@ -144,7 +144,7 @@ func (b *Indexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserErro
fileContents, err := io.ReadAll(io.LimitReader(batchReader, size)) fileContents, err := io.ReadAll(io.LimitReader(batchReader, size))
if err != nil { if err != nil {
return nil, err return nil, err
} else if !typesniffer.DetectContentType(fileContents, update.Filename).IsText() { } else if !typesniffer.DetectContentType(fileContents).IsText() {
// FIXME: UTF-16 files will probably fail here // FIXME: UTF-16 files will probably fail here
return nil, nil return nil, nil
} }

View file

@ -156,12 +156,11 @@ func (b *Indexer) Delete(_ context.Context, ids ...int64) error {
func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (*internal.SearchResult, error) { func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (*internal.SearchResult, error) {
var queries []query.Query var queries []query.Query
if options.Keyword != "" {
tokens, err := options.Tokens() tokens, err := options.Tokens()
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(tokens) > 0 {
q := bleve.NewBooleanQuery() q := bleve.NewBooleanQuery()
for _, token := range tokens { for _, token := range tokens {
innerQ := bleve.NewDisjunctionQuery( innerQ := bleve.NewDisjunctionQuery(
@ -171,7 +170,7 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
if issueID, err := token.ParseIssueReference(); err == nil { if issueID, err := token.ParseIssueReference(); err == nil {
idQuery := inner_bleve.NumericEqualityQuery(issueID, "index") idQuery := inner_bleve.NumericEqualityQuery(issueID, "index")
idQuery.SetBoost(20.0) idQuery.SetBoost(5.0)
innerQ.AddQuery(idQuery) innerQ.AddQuery(idQuery)
} }
@ -198,15 +197,6 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
queries = append(queries, bleve.NewDisjunctionQuery(repoQueries...)) queries = append(queries, bleve.NewDisjunctionQuery(repoQueries...))
} }
if options.PriorityRepoID.Has() {
eq := inner_bleve.NumericEqualityQuery(options.PriorityRepoID.Value(), "repo_id")
eq.SetBoost(10.0)
meh := bleve.NewMatchAllQuery()
meh.SetBoost(0)
should := bleve.NewDisjunctionQuery(eq, meh)
queries = append(queries, should)
}
if options.IsPull.Has() { if options.IsPull.Has() {
queries = append(queries, inner_bleve.BoolFieldQuery(options.IsPull.Value(), "is_pull")) queries = append(queries, inner_bleve.BoolFieldQuery(options.IsPull.Value(), "is_pull"))
} }

View file

@ -53,7 +53,6 @@ func (i *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
cond := builder.NewCond() cond := builder.NewCond()
var priorityIssueIndex int64
if options.Keyword != "" { if options.Keyword != "" {
repoCond := builder.In("repo_id", options.RepoIDs) repoCond := builder.In("repo_id", options.RepoIDs)
if len(options.RepoIDs) == 1 { if len(options.RepoIDs) == 1 {
@ -83,7 +82,6 @@ func (i *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
builder.Eq{"`index`": issueID}, builder.Eq{"`index`": issueID},
cond, cond,
) )
priorityIssueIndex = issueID
} }
} }
@ -91,7 +89,6 @@ func (i *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
if err != nil { if err != nil {
return nil, err return nil, err
} }
opt.PriorityIssueIndex = priorityIssueIndex
// If pagesize == 0, return total count only. It's a special case for search count. // If pagesize == 0, return total count only. It's a special case for search count.
if options.Paginator != nil && options.Paginator.PageSize == 0 { if options.Paginator != nil && options.Paginator.PageSize == 0 {

View file

@ -78,11 +78,6 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
User: nil, User: nil,
} }
if options.PriorityRepoID.Has() {
opts.SortType = "priorityrepo"
opts.PriorityRepoID = options.PriorityRepoID.Value()
}
if len(options.MilestoneIDs) == 1 && options.MilestoneIDs[0] == 0 { if len(options.MilestoneIDs) == 1 && options.MilestoneIDs[0] == 0 {
opts.MilestoneIDs = []int64{db.NoConditionID} opts.MilestoneIDs = []int64{db.NoConditionID}
} else { } else {

View file

@ -149,13 +149,12 @@ func (b *Indexer) Delete(ctx context.Context, ids ...int64) error {
func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (*internal.SearchResult, error) { func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (*internal.SearchResult, error) {
query := elastic.NewBoolQuery() query := elastic.NewBoolQuery()
if options.Keyword != "" {
q := elastic.NewBoolQuery()
tokens, err := options.Tokens() tokens, err := options.Tokens()
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(tokens) > 0 {
q := elastic.NewBoolQuery()
for _, token := range tokens { for _, token := range tokens {
innerQ := elastic.NewMultiMatchQuery(token.Term, "content", "comments").FieldWithBoost("title", 2.0).TieBreaker(0.5) innerQ := elastic.NewMultiMatchQuery(token.Term, "content", "comments").FieldWithBoost("title", 2.0).TieBreaker(0.5)
if token.Fuzzy { if token.Fuzzy {
@ -166,7 +165,7 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
} }
var eitherQ elastic.Query = innerQ var eitherQ elastic.Query = innerQ
if issueID, err := token.ParseIssueReference(); err == nil { if issueID, err := token.ParseIssueReference(); err == nil {
indexQ := elastic.NewTermQuery("index", issueID).Boost(20) indexQ := elastic.NewTermQuery("index", issueID).Boost(15.0)
eitherQ = elastic.NewDisMaxQuery().Query(indexQ).Query(innerQ).TieBreaker(0.5) eitherQ = elastic.NewDisMaxQuery().Query(indexQ).Query(innerQ).TieBreaker(0.5)
} }
switch token.Kind { switch token.Kind {
@ -189,10 +188,6 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
} }
query.Must(q) query.Must(q)
} }
if options.PriorityRepoID.Has() {
q := elastic.NewTermQuery("repo_id", options.PriorityRepoID.Value()).Boost(10)
query.Should(q)
}
if options.IsPull.Has() { if options.IsPull.Has() {
query.Must(elastic.NewTermQuery("is_pull", options.IsPull.Value())) query.Must(elastic.NewTermQuery("is_pull", options.IsPull.Value()))

View file

@ -77,7 +77,6 @@ type SearchOptions struct {
RepoIDs []int64 // repository IDs which the issues belong to RepoIDs []int64 // repository IDs which the issues belong to
AllPublic bool // if include all public repositories AllPublic bool // if include all public repositories
PriorityRepoID optional.Option[int64] // issues from this repository will be prioritized when SortByScore
IsPull optional.Option[bool] // if the issues is a pull request IsPull optional.Option[bool] // if the issues is a pull request
IsClosed optional.Option[bool] // if the issues is closed IsClosed optional.Option[bool] // if the issues is closed

View file

@ -45,9 +45,12 @@ func (t *Tokenizer) next() (tk Token, err error) {
// skip all leading white space // skip all leading white space
for { for {
if r, _, err = t.in.ReadRune(); err != nil || r != ' ' { if r, _, err = t.in.ReadRune(); err == nil && r == ' ' {
break //nolint:staticcheck,wastedassign // SA4006 the variable is used after the loop
r, _, err = t.in.ReadRune()
continue
} }
break
} }
if err != nil { if err != nil {
return tk, err return tk, err
@ -104,18 +107,12 @@ nextEnd:
// Tokenize the keyword // Tokenize the keyword
func (o *SearchOptions) Tokens() (tokens []Token, err error) { func (o *SearchOptions) Tokens() (tokens []Token, err error) {
if o.Keyword == "" {
return nil, nil
}
in := strings.NewReader(o.Keyword) in := strings.NewReader(o.Keyword)
it := Tokenizer{in: in} it := Tokenizer{in: in}
for token, err := it.next(); err == nil; token, err = it.next() { for token, err := it.next(); err == nil; token, err = it.next() {
if token.Term != "" {
tokens = append(tokens, token) tokens = append(tokens, token)
} }
}
if err != nil && err != io.EOF { if err != nil && err != io.EOF {
return nil, err return nil, err
} }

View file

@ -41,36 +41,6 @@ var testOpts = []testIssueQueryStringOpt{
}, },
}, },
}, },
{
Keyword: "Hello World",
Results: []Token{
{
Term: "Hello",
Fuzzy: true,
Kind: BoolOptShould,
},
{
Term: "World",
Fuzzy: true,
Kind: BoolOptShould,
},
},
},
{
Keyword: " Hello World ",
Results: []Token{
{
Term: "Hello",
Fuzzy: true,
Kind: BoolOptShould,
},
{
Term: "World",
Fuzzy: true,
Kind: BoolOptShould,
},
},
},
{ {
Keyword: "+Hello +World", Keyword: "+Hello +World",
Results: []Token{ Results: []Token{
@ -186,68 +156,6 @@ var testOpts = []testIssueQueryStringOpt{
}, },
}, },
}, },
{
Keyword: "\\",
Results: nil,
},
{
Keyword: "\"",
Results: nil,
},
{
Keyword: "Hello \\",
Results: []Token{
{
Term: "Hello",
Fuzzy: true,
Kind: BoolOptShould,
},
},
},
{
Keyword: "\"\"",
Results: nil,
},
{
Keyword: "\" World \"",
Results: []Token{
{
Term: " World ",
Fuzzy: false,
Kind: BoolOptShould,
},
},
},
{
Keyword: "\"\" World \"\"",
Results: []Token{
{
Term: "World",
Fuzzy: true,
Kind: BoolOptShould,
},
},
},
{
Keyword: "Best \"Hello World\" Ever",
Results: []Token{
{
Term: "Best",
Fuzzy: true,
Kind: BoolOptShould,
},
{
Term: "Hello World",
Fuzzy: false,
Kind: BoolOptShould,
},
{
Term: "Ever",
Fuzzy: true,
Kind: BoolOptShould,
},
},
},
} }
func TestIssueQueryString(t *testing.T) { func TestIssueQueryString(t *testing.T) {

View file

@ -87,44 +87,14 @@ func TestIndexer(t *testing.T, indexer internal.Indexer) {
} }
} }
func allResults(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
assert.Len(t, result.Hits, len(data))
assert.Equal(t, len(data), int(result.Total))
}
var cases = []*testIndexerCase{ var cases = []*testIndexerCase{
{ {
Name: "default", Name: "default",
SearchOptions: &internal.SearchOptions{}, SearchOptions: &internal.SearchOptions{},
Expected: allResults, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
assert.Len(t, result.Hits, len(data))
assert.Equal(t, len(data), int(result.Total))
}, },
{
Name: "empty keyword",
SearchOptions: &internal.SearchOptions{
Keyword: "",
},
Expected: allResults,
},
{
Name: "whitespace keyword",
SearchOptions: &internal.SearchOptions{
Keyword: " ",
},
Expected: allResults,
},
{
Name: "dangling slash in keyword",
SearchOptions: &internal.SearchOptions{
Keyword: "\\",
},
Expected: allResults,
},
{
Name: "dangling quote in keyword",
SearchOptions: &internal.SearchOptions{
Keyword: "\"",
},
Expected: allResults,
}, },
{ {
Name: "empty", Name: "empty",
@ -772,25 +742,6 @@ var cases = []*testIndexerCase{
} }
}, },
}, },
{
Name: "PriorityRepoID",
SearchOptions: &internal.SearchOptions{
IsPull: optional.Some(false),
IsClosed: optional.Some(false),
PriorityRepoID: optional.Some(int64(3)),
Paginator: &db.ListOptionsAll,
SortBy: internal.SortByScore,
},
Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) {
for i, v := range result.Hits {
if i < 7 {
assert.Equal(t, int64(3), data[v.ID].RepoID)
} else {
assert.NotEqual(t, int64(3), data[v.ID].RepoID)
}
}
},
},
} }
type testIndexerCase struct { type testIndexerCase struct {

View file

@ -39,7 +39,16 @@ func SearchPointerBlobs(ctx context.Context, repo *git.Repository, pointerChan c
go pipeline.BlobsLessThan1024FromCatFileBatchCheck(catFileCheckReader, shasToBatchWriter, &wg) go pipeline.BlobsLessThan1024FromCatFileBatchCheck(catFileCheckReader, shasToBatchWriter, &wg)
// 1. Run batch-check on all objects in the repository // 1. Run batch-check on all objects in the repository
if git.CheckGitVersionAtLeast("2.6.0") != nil {
revListReader, revListWriter := io.Pipe()
shasToCheckReader, shasToCheckWriter := io.Pipe()
wg.Add(2)
go pipeline.CatFileBatchCheck(ctx, shasToCheckReader, catFileCheckWriter, &wg, basePath)
go pipeline.BlobsFromRevListObjects(revListReader, shasToCheckWriter, &wg)
go pipeline.RevListAllObjects(ctx, revListWriter, &wg, basePath, errChan)
} else {
go pipeline.CatFileBatchCheckAllObjects(ctx, catFileCheckWriter, &wg, basePath, errChan) go pipeline.CatFileBatchCheckAllObjects(ctx, catFileCheckWriter, &wg, basePath, errChan)
}
wg.Wait() wg.Wait()
close(pointerChan) close(pointerChan)

View file

@ -26,7 +26,6 @@ type WriterMode struct {
Flags Flags Flags Flags
Expression string Expression string
Exclusion string
StacktraceLevel Level StacktraceLevel Level

View file

@ -68,14 +68,6 @@ func (b *EventWriterBaseImpl) Run(ctx context.Context) {
} }
} }
var exclusionRegexp *regexp.Regexp
if b.Mode.Exclusion != "" {
var err error
if exclusionRegexp, err = regexp.Compile(b.Mode.Exclusion); err != nil {
FallbackErrorf("unable to compile exclusion %q for writer %q: %v", b.Mode.Exclusion, b.Name, err)
}
}
handlePaused := func() { handlePaused := func() {
if pause := b.GetPauseChan(); pause != nil { if pause := b.GetPauseChan(); pause != nil {
select { select {
@ -103,13 +95,6 @@ func (b *EventWriterBaseImpl) Run(ctx context.Context) {
continue continue
} }
} }
if exclusionRegexp != nil {
fileLineCaller := fmt.Sprintf("%s:%d:%s", event.Origin.Filename, event.Origin.Line, event.Origin.Caller)
matched := exclusionRegexp.MatchString(fileLineCaller) || exclusionRegexp.MatchString(event.Origin.MsgSimpleText)
if matched {
continue
}
}
var err error var err error
switch msg := event.Msg.(type) { switch msg := event.Msg.(type) {

View file

@ -31,49 +31,3 @@ func TestBufferLogger(t *testing.T) {
logger.Close() logger.Close()
assert.Contains(t, bufferWriter.Buffer.String(), expected) assert.Contains(t, bufferWriter.Buffer.String(), expected)
} }
func TestBufferLoggerWithExclusion(t *testing.T) {
prefix := "ExclusionPrefix "
level := log.INFO
message := "something"
bufferWriter := log.NewEventWriterBuffer("test-buffer", log.WriterMode{
Level: level,
Prefix: prefix,
Exclusion: message,
})
logger := log.NewLoggerWithWriters(t.Context(), "test", bufferWriter)
logger.SendLogEvent(&log.Event{
Level: log.INFO,
MsgSimpleText: message,
})
logger.Close()
assert.NotContains(t, bufferWriter.Buffer.String(), message)
}
func TestBufferLoggerWithExpressionAndExclusion(t *testing.T) {
prefix := "BothPrefix "
level := log.INFO
expression := ".*foo.*"
exclusion := ".*bar.*"
bufferWriter := log.NewEventWriterBuffer("test-buffer", log.WriterMode{
Level: level,
Prefix: prefix,
Expression: expression,
Exclusion: exclusion,
})
logger := log.NewLoggerWithWriters(t.Context(), "test", bufferWriter)
logger.SendLogEvent(&log.Event{Level: log.INFO, MsgSimpleText: "foo expression"})
logger.SendLogEvent(&log.Event{Level: log.INFO, MsgSimpleText: "bar exclusion"})
logger.SendLogEvent(&log.Event{Level: log.INFO, MsgSimpleText: "foo bar both"})
logger.SendLogEvent(&log.Event{Level: log.INFO, MsgSimpleText: "none"})
logger.Close()
assert.Contains(t, bufferWriter.Buffer.String(), "foo expression")
assert.NotContains(t, bufferWriter.Buffer.String(), "bar")
}

View file

@ -143,19 +143,3 @@ func TestLoggerExpressionFilter(t *testing.T) {
assert.Equal(t, []string{"foo\n", "foo bar\n", "by filename\n"}, w1.GetLogs()) assert.Equal(t, []string{"foo\n", "foo bar\n", "by filename\n"}, w1.GetLogs())
} }
func TestLoggerExclusionFilter(t *testing.T) {
logger := NewLoggerWithWriters(t.Context(), "test")
w1 := newDummyWriter("dummy-1", DEBUG, 0)
w1.Mode.Exclusion = "foo.*"
logger.AddWriters(w1)
logger.Info("foo")
logger.Info("bar")
logger.Info("foo bar")
logger.SendLogEvent(&Event{Level: INFO, Filename: "foo.go", MsgSimpleText: "by filename"})
logger.Close()
assert.Equal(t, []string{"bar\n"}, w1.GetLogs())
}

View file

@ -104,7 +104,7 @@ func TestRender_Images(t *testing.T) {
test( test(
"!["+title+"]("+url+")", "!["+title+"]("+url+")",
`<p><a href="`+result+`" target="_blank" rel="nofollow noopener"><img src="`+result+`" alt="`+title+`" loading="lazy"/></a></p>`) `<p><a href="`+result+`" target="_blank" rel="nofollow noopener"><img src="`+result+`" alt="`+title+`"/></a></p>`)
test( test(
"[["+title+"|"+url+"]]", "[["+title+"|"+url+"]]",
@ -115,7 +115,7 @@ func TestRender_Images(t *testing.T) {
test( test(
"!["+title+"]("+url+")", "!["+title+"]("+url+")",
`<p><a href="`+result+`" target="_blank" rel="nofollow noopener"><img src="`+result+`" alt="`+title+`" loading="lazy"/></a></p>`) `<p><a href="`+result+`" target="_blank" rel="nofollow noopener"><img src="`+result+`" alt="`+title+`"/></a></p>`)
test( test(
"[["+title+"|"+url+"]]", "[["+title+"|"+url+"]]",
@ -412,8 +412,8 @@ func TestRenderSiblingImages_Issue12925(t *testing.T) {
testcase := `![image1](/image1) testcase := `![image1](/image1)
![image2](/image2) ![image2](/image2)
` `
expected := `<p><a href="/image1" target="_blank" rel="nofollow noopener"><img src="/image1" alt="image1" loading="lazy"></a><br> expected := `<p><a href="/image1" target="_blank" rel="nofollow noopener"><img src="/image1" alt="image1"></a><br>
<a href="/image2" target="_blank" rel="nofollow noopener"><img src="/image2" alt="image2" loading="lazy"></a></p> <a href="/image2" target="_blank" rel="nofollow noopener"><img src="/image2" alt="image2"></a></p>
` `
res, err := markdown.RenderRawString(&markup.RenderContext{Ctx: git.DefaultContext}, testcase) res, err := markdown.RenderRawString(&markup.RenderContext{Ctx: git.DefaultContext}, testcase)
require.NoError(t, err) require.NoError(t, err)
@ -845,10 +845,10 @@ mail@domain.com
<a href="https://example.com" rel="nofollow">remote link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/src/file.bin" rel="nofollow">local link</a><br/> <a href="/src/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/image.jpg" target="_blank" rel="nofollow noopener"><img src="/image.jpg" alt="local image" loading="lazy"/></a><br/> <a href="/image.jpg" target="_blank" rel="nofollow noopener"><img src="/image.jpg" alt="local image"/></a><br/>
<a href="/path/file" target="_blank" rel="nofollow noopener"><img src="/path/file" alt="local image" loading="lazy"/></a><br/> <a href="/path/file" target="_blank" rel="nofollow noopener"><img src="/path/file" alt="local image"/></a><br/>
<a href="/path/file" target="_blank" rel="nofollow noopener"><img src="/path/file" alt="local image" loading="lazy"/></a><br/> <a href="/path/file" target="_blank" rel="nofollow noopener"><img src="/path/file" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image" loading="lazy"/></a><br/> <a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/image.jpg" rel="nofollow"><img src="/image.jpg" title="local image" alt=""/></a><br/> <a href="/image.jpg" rel="nofollow"><img src="/image.jpg" title="local image" alt=""/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt=""/></a><br/> <a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt=""/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/> <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
@ -872,10 +872,10 @@ space</p>
<a href="https://example.com" rel="nofollow">remote link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/wiki/file.bin" rel="nofollow">local link</a><br/> <a href="/wiki/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/wiki/raw/image.jpg" alt="local image" loading="lazy"/></a><br/> <a href="/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/wiki/raw/image.jpg" alt="local image"/></a><br/>
<a href="/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/wiki/raw/path/file" alt="local image" loading="lazy"/></a><br/> <a href="/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/wiki/raw/path/file" alt="local image"/></a><br/>
<a href="/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/wiki/raw/path/file" alt="local image" loading="lazy"/></a><br/> <a href="/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/wiki/raw/path/file" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image" loading="lazy"/></a><br/> <a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/wiki/raw/image.jpg" rel="nofollow"><img src="/wiki/raw/image.jpg" title="local image" alt=""/></a><br/> <a href="/wiki/raw/image.jpg" rel="nofollow"><img src="/wiki/raw/image.jpg" title="local image" alt=""/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt=""/></a><br/> <a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt=""/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/> <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
@ -901,10 +901,10 @@ space</p>
<a href="https://example.com" rel="nofollow">remote link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="https://gitea.io/src/file.bin" rel="nofollow">local link</a><br/> <a href="https://gitea.io/src/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="https://gitea.io/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/image.jpg" alt="local image" loading="lazy"/></a><br/> <a href="https://gitea.io/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/image.jpg" alt="local image"/></a><br/>
<a href="https://gitea.io/path/file" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/path/file" alt="local image" loading="lazy"/></a><br/> <a href="https://gitea.io/path/file" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/path/file" alt="local image"/></a><br/>
<a href="https://gitea.io/path/file" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/path/file" alt="local image" loading="lazy"/></a><br/> <a href="https://gitea.io/path/file" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/path/file" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image" loading="lazy"/></a><br/> <a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="https://gitea.io/image.jpg" rel="nofollow"><img src="https://gitea.io/image.jpg" title="local image" alt=""/></a><br/> <a href="https://gitea.io/image.jpg" rel="nofollow"><img src="https://gitea.io/image.jpg" title="local image" alt=""/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt=""/></a><br/> <a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt=""/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/> <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
@ -930,10 +930,10 @@ space</p>
<a href="https://example.com" rel="nofollow">remote link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="https://gitea.io/wiki/file.bin" rel="nofollow">local link</a><br/> <a href="https://gitea.io/wiki/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="https://gitea.io/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/wiki/raw/image.jpg" alt="local image" loading="lazy"/></a><br/> <a href="https://gitea.io/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/wiki/raw/image.jpg" alt="local image"/></a><br/>
<a href="https://gitea.io/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/wiki/raw/path/file" alt="local image" loading="lazy"/></a><br/> <a href="https://gitea.io/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/wiki/raw/path/file" alt="local image"/></a><br/>
<a href="https://gitea.io/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/wiki/raw/path/file" alt="local image" loading="lazy"/></a><br/> <a href="https://gitea.io/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/wiki/raw/path/file" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image" loading="lazy"/></a><br/> <a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="https://gitea.io/wiki/raw/image.jpg" rel="nofollow"><img src="https://gitea.io/wiki/raw/image.jpg" title="local image" alt=""/></a><br/> <a href="https://gitea.io/wiki/raw/image.jpg" rel="nofollow"><img src="https://gitea.io/wiki/raw/image.jpg" title="local image" alt=""/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt=""/></a><br/> <a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt=""/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/> <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
@ -959,10 +959,10 @@ space</p>
<a href="https://example.com" rel="nofollow">remote link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/relative/path/src/file.bin" rel="nofollow">local link</a><br/> <a href="/relative/path/src/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/relative/path/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/image.jpg" alt="local image" loading="lazy"/></a><br/> <a href="/relative/path/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/image.jpg" alt="local image"/></a><br/>
<a href="/relative/path/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/path/file" alt="local image" loading="lazy"/></a><br/> <a href="/relative/path/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/path/file" alt="local image"/></a><br/>
<a href="/relative/path/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/path/file" alt="local image" loading="lazy"/></a><br/> <a href="/relative/path/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/path/file" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image" loading="lazy"/></a><br/> <a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/relative/path/image.jpg" rel="nofollow"><img src="/relative/path/image.jpg" title="local image" alt=""/></a><br/> <a href="/relative/path/image.jpg" rel="nofollow"><img src="/relative/path/image.jpg" title="local image" alt=""/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt=""/></a><br/> <a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt=""/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/> <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
@ -988,10 +988,10 @@ space</p>
<a href="https://example.com" rel="nofollow">remote link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/> <a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image" loading="lazy"/></a><br/> <a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/>
<a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image" loading="lazy"/></a><br/> <a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image"/></a><br/>
<a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image" loading="lazy"/></a><br/> <a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image" loading="lazy"/></a><br/> <a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt=""/></a><br/> <a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt=""/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt=""/></a><br/> <a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt=""/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/> <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
@ -1018,10 +1018,10 @@ space</p>
<a href="https://example.com" rel="nofollow">remote link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/user/repo/src/branch/main/file.bin" rel="nofollow">local link</a><br/> <a href="/user/repo/src/branch/main/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/user/repo/media/branch/main/image.jpg" target="_blank" rel="nofollow noopener"><img src="/user/repo/media/branch/main/image.jpg" alt="local image" loading="lazy"/></a><br/> <a href="/user/repo/media/branch/main/image.jpg" target="_blank" rel="nofollow noopener"><img src="/user/repo/media/branch/main/image.jpg" alt="local image"/></a><br/>
<a href="/user/repo/media/branch/main/path/file" target="_blank" rel="nofollow noopener"><img src="/user/repo/media/branch/main/path/file" alt="local image" loading="lazy"/></a><br/> <a href="/user/repo/media/branch/main/path/file" target="_blank" rel="nofollow noopener"><img src="/user/repo/media/branch/main/path/file" alt="local image"/></a><br/>
<a href="/user/repo/media/branch/main/path/file" target="_blank" rel="nofollow noopener"><img src="/user/repo/media/branch/main/path/file" alt="local image" loading="lazy"/></a><br/> <a href="/user/repo/media/branch/main/path/file" target="_blank" rel="nofollow noopener"><img src="/user/repo/media/branch/main/path/file" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image" loading="lazy"/></a><br/> <a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/user/repo/media/branch/main/image.jpg" rel="nofollow"><img src="/user/repo/media/branch/main/image.jpg" title="local image" alt=""/></a><br/> <a href="/user/repo/media/branch/main/image.jpg" rel="nofollow"><img src="/user/repo/media/branch/main/image.jpg" title="local image" alt=""/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt=""/></a><br/> <a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt=""/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/> <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
@ -1048,10 +1048,10 @@ space</p>
<a href="https://example.com" rel="nofollow">remote link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/> <a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image" loading="lazy"/></a><br/> <a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/>
<a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image" loading="lazy"/></a><br/> <a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image"/></a><br/>
<a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image" loading="lazy"/></a><br/> <a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image" loading="lazy"/></a><br/> <a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt=""/></a><br/> <a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt=""/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt=""/></a><br/> <a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt=""/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/> <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
@ -1078,10 +1078,10 @@ space</p>
<a href="https://example.com" rel="nofollow">remote link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/user/repo/src/sub/folder/file.bin" rel="nofollow">local link</a><br/> <a href="/user/repo/src/sub/folder/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/user/repo/image.jpg" target="_blank" rel="nofollow noopener"><img src="/user/repo/image.jpg" alt="local image" loading="lazy"/></a><br/> <a href="/user/repo/image.jpg" target="_blank" rel="nofollow noopener"><img src="/user/repo/image.jpg" alt="local image"/></a><br/>
<a href="/user/repo/path/file" target="_blank" rel="nofollow noopener"><img src="/user/repo/path/file" alt="local image" loading="lazy"/></a><br/> <a href="/user/repo/path/file" target="_blank" rel="nofollow noopener"><img src="/user/repo/path/file" alt="local image"/></a><br/>
<a href="/user/repo/path/file" target="_blank" rel="nofollow noopener"><img src="/user/repo/path/file" alt="local image" loading="lazy"/></a><br/> <a href="/user/repo/path/file" target="_blank" rel="nofollow noopener"><img src="/user/repo/path/file" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image" loading="lazy"/></a><br/> <a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/user/repo/image.jpg" rel="nofollow"><img src="/user/repo/image.jpg" title="local image" alt=""/></a><br/> <a href="/user/repo/image.jpg" rel="nofollow"><img src="/user/repo/image.jpg" title="local image" alt=""/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt=""/></a><br/> <a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt=""/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/> <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
@ -1108,10 +1108,10 @@ space</p>
<a href="https://example.com" rel="nofollow">remote link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/> <a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image" loading="lazy"/></a><br/> <a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/>
<a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image" loading="lazy"/></a><br/> <a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image"/></a><br/>
<a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image" loading="lazy"/></a><br/> <a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image" loading="lazy"/></a><br/> <a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt=""/></a><br/> <a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt=""/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt=""/></a><br/> <a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt=""/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/> <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
@ -1139,10 +1139,10 @@ space</p>
<a href="https://example.com" rel="nofollow">remote link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/user/repo/src/branch/main/sub/folder/file.bin" rel="nofollow">local link</a><br/> <a href="/user/repo/src/branch/main/sub/folder/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/user/repo/media/branch/main/sub/folder/image.jpg" target="_blank" rel="nofollow noopener"><img src="/user/repo/media/branch/main/sub/folder/image.jpg" alt="local image" loading="lazy"/></a><br/> <a href="/user/repo/media/branch/main/sub/folder/image.jpg" target="_blank" rel="nofollow noopener"><img src="/user/repo/media/branch/main/sub/folder/image.jpg" alt="local image"/></a><br/>
<a href="/user/repo/media/branch/main/sub/folder/path/file" target="_blank" rel="nofollow noopener"><img src="/user/repo/media/branch/main/sub/folder/path/file" alt="local image" loading="lazy"/></a><br/> <a href="/user/repo/media/branch/main/sub/folder/path/file" target="_blank" rel="nofollow noopener"><img src="/user/repo/media/branch/main/sub/folder/path/file" alt="local image"/></a><br/>
<a href="/user/repo/media/branch/main/sub/folder/path/file" target="_blank" rel="nofollow noopener"><img src="/user/repo/media/branch/main/sub/folder/path/file" alt="local image" loading="lazy"/></a><br/> <a href="/user/repo/media/branch/main/sub/folder/path/file" target="_blank" rel="nofollow noopener"><img src="/user/repo/media/branch/main/sub/folder/path/file" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image" loading="lazy"/></a><br/> <a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/user/repo/media/branch/main/sub/folder/image.jpg" rel="nofollow"><img src="/user/repo/media/branch/main/sub/folder/image.jpg" title="local image" alt=""/></a><br/> <a href="/user/repo/media/branch/main/sub/folder/image.jpg" rel="nofollow"><img src="/user/repo/media/branch/main/sub/folder/image.jpg" title="local image" alt=""/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt=""/></a><br/> <a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt=""/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/> <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
@ -1170,10 +1170,10 @@ space</p>
<a href="https://example.com" rel="nofollow">remote link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/> <a href="/relative/path/wiki/file.bin" rel="nofollow">local link</a><br/>
<a href="https://example.com" rel="nofollow">remote link</a><br/> <a href="https://example.com" rel="nofollow">remote link</a><br/>
<a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image" loading="lazy"/></a><br/> <a href="/relative/path/wiki/raw/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/image.jpg" alt="local image"/></a><br/>
<a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image" loading="lazy"/></a><br/> <a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image"/></a><br/>
<a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image" loading="lazy"/></a><br/> <a href="/relative/path/wiki/raw/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/wiki/raw/path/file" alt="local image"/></a><br/>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image" loading="lazy"/></a><br/> <a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a><br/>
<a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt=""/></a><br/> <a href="/relative/path/wiki/raw/image.jpg" rel="nofollow"><img src="/relative/path/wiki/raw/image.jpg" title="local image" alt=""/></a><br/>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt=""/></a><br/> <a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt=""/></a><br/>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/> <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>

View file

@ -44,7 +44,6 @@ func (g *ASTTransformer) transformImage(ctx *markup.RenderContext, v *ast.Image)
for _, attr := range v.Attributes() { for _, attr := range v.Attributes() {
image.SetAttribute(attr.Name, attr.Value) image.SetAttribute(attr.Name, attr.Value)
} }
image.SetAttributeString("loading", []byte("lazy"))
for child := v.FirstChild(); child != nil; { for child := v.FirstChild(); child != nil; {
next := child.NextSibling() next := child.NextSibling()
image.AppendChild(image, child) image.AppendChild(image, child)

View file

@ -108,9 +108,6 @@ func createDefaultPolicy() *bluemonday.Policy {
// Allow classes for emojis // Allow classes for emojis
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^emoji$`)).OnElements("img") policy.AllowAttrs("class").Matching(regexp.MustCompile(`^emoji$`)).OnElements("img")
// Allow attributes for images
policy.AllowAttrs("loading").Matching(regexp.MustCompile(`^lazy$`)).OnElements("img")
// Allow icons, emojis, chroma syntax and keyword markup on span // Allow icons, emojis, chroma syntax and keyword markup on span
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji)|(language-math display)|(language-math inline))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span") policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji)|(language-math display)|(language-math inline))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span")
policy.AllowAttrs("data-alias").Matching(regexp.MustCompile(`^[a-zA-Z0-9-_+]+$`)).OnElements("span") policy.AllowAttrs("data-alias").Matching(regexp.MustCompile(`^[a-zA-Z0-9-_+]+$`)).OnElements("span")

View file

@ -75,10 +75,6 @@ func Test_Sanitizer(t *testing.T) {
// Emoji // Emoji
`<span class="emoji" aria-label="thumbs up" data-alias="+1">THUMBS UP</span>`, `<span class="emoji" aria-label="thumbs up" data-alias="+1">THUMBS UP</span>`, `<span class="emoji" aria-label="thumbs up" data-alias="+1">THUMBS UP</span>`, `<span class="emoji" aria-label="thumbs up" data-alias="+1">THUMBS UP</span>`,
`<span class="emoji" aria-label="thumbs up" data-alias="(+!)">THUMBS UP</span>`, `<span class="emoji" aria-label="thumbs up">THUMBS UP</span>`, `<span class="emoji" aria-label="thumbs up" data-alias="(+!)">THUMBS UP</span>`, `<span class="emoji" aria-label="thumbs up">THUMBS UP</span>`,
// Images lazy loading
`<img src="/image1" alt="image1" loading="lazy">`, `<img src="/image1" alt="image1" loading="lazy">`,
`<img src="/image1" alt="image1" loading="eager">`, `<img src="/image1" alt="image1">`,
} }
for i := 0; i < len(testCases); i += 2 { for i := 0; i < len(testCases); i += 2 {

View file

@ -133,7 +133,6 @@ func loadLogModeByName(rootCfg ConfigProvider, loggerName, modeName string) (wri
writerMode.StacktraceLevel = log.LevelFromString(ConfigInheritedKeyString(sec, "STACKTRACE_LEVEL", Log.StacktraceLogLevel.String())) writerMode.StacktraceLevel = log.LevelFromString(ConfigInheritedKeyString(sec, "STACKTRACE_LEVEL", Log.StacktraceLogLevel.String()))
writerMode.Prefix = ConfigInheritedKeyString(sec, "PREFIX") writerMode.Prefix = ConfigInheritedKeyString(sec, "PREFIX")
writerMode.Expression = ConfigInheritedKeyString(sec, "EXPRESSION") writerMode.Expression = ConfigInheritedKeyString(sec, "EXPRESSION")
writerMode.Exclusion = ConfigInheritedKeyString(sec, "EXCLUSION")
// flags are updated and set below // flags are updated and set below
switch writerType { switch writerType {

View file

@ -44,7 +44,6 @@ func TestLogConfigDefault(t *testing.T) {
"BufferLen": 10000, "BufferLen": 10000,
"Colorize": false, "Colorize": false,
"Expression": "", "Expression": "",
"Exclusion": "",
"Flags": "stdflags", "Flags": "stdflags",
"Level": "info", "Level": "info",
"Prefix": "", "Prefix": "",
@ -84,7 +83,6 @@ logger.xorm.MODE =
"BufferLen": 10000, "BufferLen": 10000,
"Colorize": false, "Colorize": false,
"Expression": "", "Expression": "",
"Exclusion": "",
"Flags": "stdflags", "Flags": "stdflags",
"Level": "info", "Level": "info",
"Prefix": "", "Prefix": "",
@ -123,7 +121,6 @@ MODE = console
"BufferLen": 10000, "BufferLen": 10000,
"Colorize": false, "Colorize": false,
"Expression": "", "Expression": "",
"Exclusion": "",
"Flags": "stdflags", "Flags": "stdflags",
"Level": "info", "Level": "info",
"Prefix": "", "Prefix": "",
@ -171,7 +168,6 @@ ACCESS = file
"BufferLen": 10000, "BufferLen": 10000,
"Colorize": false, "Colorize": false,
"Expression": "", "Expression": "",
"Exclusion": "",
"Flags": "stdflags", "Flags": "stdflags",
"Level": "info", "Level": "info",
"Prefix": "", "Prefix": "",
@ -195,7 +191,6 @@ ACCESS = file
"BufferLen": 10000, "BufferLen": 10000,
"Colorize": false, "Colorize": false,
"Expression": "", "Expression": "",
"Exclusion": "",
"Flags": "none", "Flags": "none",
"Level": "info", "Level": "info",
"Prefix": "", "Prefix": "",
@ -262,7 +257,6 @@ STDERR = true
"BufferLen": 10000, "BufferLen": 10000,
"Colorize": false, "Colorize": false,
"Expression": "", "Expression": "",
"Exclusion": "",
"Flags": "stdflags", "Flags": "stdflags",
"Level": "warn", "Level": "warn",
"Prefix": "", "Prefix": "",
@ -276,7 +270,6 @@ STDERR = true
"BufferLen": 10000, "BufferLen": 10000,
"Colorize": false, "Colorize": false,
"Expression": "", "Expression": "",
"Exclusion": "",
"Flags": "stdflags", "Flags": "stdflags",
"Level": "error", "Level": "error",
"Prefix": "", "Prefix": "",
@ -294,7 +287,6 @@ STDERR = true
"BufferLen": 10000, "BufferLen": 10000,
"Colorize": false, "Colorize": false,
"Expression": "", "Expression": "",
"Exclusion": "",
"Flags": "none", "Flags": "none",
"Level": "warn", "Level": "warn",
"Prefix": "", "Prefix": "",
@ -331,7 +323,6 @@ MODE = file
LEVEL = error LEVEL = error
STACKTRACE_LEVEL = fatal STACKTRACE_LEVEL = fatal
EXPRESSION = filter EXPRESSION = filter
EXCLUSION = not
FLAGS = medfile FLAGS = medfile
PREFIX = "[Prefix] " PREFIX = "[Prefix] "
FILE_NAME = file-xxx.log FILE_NAME = file-xxx.log
@ -350,7 +341,6 @@ COMPRESSION_LEVEL = 4
"BufferLen": 10, "BufferLen": 10,
"Colorize": false, "Colorize": false,
"Expression": "", "Expression": "",
"Exclusion": "",
"Flags": "stdflags", "Flags": "stdflags",
"Level": "info", "Level": "info",
"Prefix": "", "Prefix": "",
@ -370,7 +360,6 @@ COMPRESSION_LEVEL = 4
"BufferLen": 10, "BufferLen": 10,
"Colorize": false, "Colorize": false,
"Expression": "filter", "Expression": "filter",
"Exclusion": "not",
"Flags": "medfile", "Flags": "medfile",
"Level": "error", "Level": "error",
"Prefix": "[Prefix] ", "Prefix": "[Prefix] ",

View file

@ -1,5 +1,4 @@
// Copyright 2022 The Gitea Authors. All rights reserved. // Copyright 2022 The Gitea Authors. All rights reserved.
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
package structs package structs
@ -8,15 +7,3 @@ package structs
type ActivityPub struct { type ActivityPub struct {
Context string `json:"@context"` Context string `json:"@context"`
} }
type APRemoteFollowOption struct {
Target string `json:"target"`
}
type APPersonFollowItem struct {
ActorID string `json:"actor_id"`
Note string `json:"note"`
OriginalURL string `json:"original_url"`
OriginalItem string `json:"original_item"`
}

View file

@ -22,12 +22,6 @@ type Attachment struct {
Type string `json:"type"` Type string `json:"type"`
} }
// WebAttachment the generic attachment with mime type
type WebAttachment struct {
*Attachment
MimeType string `json:"mime_type"`
}
// EditAttachmentOptions options for editing attachments // EditAttachmentOptions options for editing attachments
// swagger:model // swagger:model
type EditAttachmentOptions struct { type EditAttachmentOptions struct {

View file

@ -54,6 +54,7 @@ type CreateHookOption struct {
AuthorizationHeader string `json:"authorization_header"` AuthorizationHeader string `json:"authorization_header"`
// default: false // default: false
Active bool `json:"active"` Active bool `json:"active"`
IsSystemWebhook bool `json:"is_system_webhook"`
} }
// EditHookOption options when modify one hook // EditHookOption options when modify one hook

View file

@ -192,8 +192,8 @@ func TestRenderMarkdownToHtml(t *testing.T) {
<a href="https://example.com" rel="nofollow">remote link</a> <a href="https://example.com" rel="nofollow">remote link</a>
<a href="/src/file.bin" rel="nofollow">local link</a> <a href="/src/file.bin" rel="nofollow">local link</a>
<a href="https://example.com" rel="nofollow">remote link</a> <a href="https://example.com" rel="nofollow">remote link</a>
<a href="/image.jpg" target="_blank" rel="nofollow noopener"><img src="/image.jpg" alt="local image" loading="lazy"/></a> <a href="/image.jpg" target="_blank" rel="nofollow noopener"><img src="/image.jpg" alt="local image"/></a>
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image" loading="lazy"/></a> <a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a>
<a href="/image.jpg" rel="nofollow"><img src="/image.jpg" title="local image" alt=""/></a> <a href="/image.jpg" rel="nofollow"><img src="/image.jpg" title="local image" alt=""/></a>
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt=""/></a> <a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt=""/></a>
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a> <a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a>

View file

@ -10,79 +10,56 @@ import (
"net/http/httptest" "net/http/httptest"
"strings" "strings"
"testing" "testing"
"forgejo.org/modules/util"
) )
type FederationServerMockPerson struct { type FederationServerMockPerson struct {
ID int64 ID int64
Name string Name string
PubKey string PubKey string
PrivKey string
} }
type FederationServerMockRepository struct { type FederationServerMockRepository struct {
ID int64 ID int64
} }
type ApActorMock struct {
PrivKey string
PubKey string
}
type FederationServerMock struct { type FederationServerMock struct {
ApActor ApActorMock
Persons []FederationServerMockPerson Persons []FederationServerMockPerson
Repositories []FederationServerMockRepository Repositories []FederationServerMockRepository
LastPost string LastPost string
} }
func NewFederationServerMockPerson(id int64, name string) FederationServerMockPerson { func NewFederationServerMockPerson(id int64, name string) FederationServerMockPerson {
priv, pub, _ := util.GenerateKeyPair(3072)
return FederationServerMockPerson{ return FederationServerMockPerson{
ID: id, ID: id,
Name: name, Name: name,
PubKey: pub, PubKey: `"-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA18H5s7N6ItZUAh9tneII\nIuZdTTa3cZlLa/9ejWAHTkcp3WLW+/zbsumlMrWYfBy2/yTm56qasWt38iY4D6ul\n` +
PrivKey: priv, `CPiwhAqX3REvVq8tM79a2CEqZn9ka6vuXoDgBg/sBf/BUWqf7orkjUXwk/U0Egjf\nk5jcurF4vqf1u+rlAHH37dvSBaDjNj6Qnj4OP12bjfaY/yvs7+jue/eNXFHjzN4E\n` +
`T2H4B/yeKTJ4UuAwTlLaNbZJul2baLlHelJPAsxiYaziVuV5P+IGWckY6RSerRaZ\nAkc4mmGGtjAyfN9aewe+lNVfwS7ElFx546PlLgdQgjmeSwLX8FWxbPE5A/PmaXCs\n` +
`nx+nou+3dD7NluULLtdd7K+2x02trObKXCAzmi5/Dc+yKTzpFqEz+hLNCz7TImP/\ncK//NV9Q+X67J9O27baH9R9ZF4zMw8rv2Pg0WLSw1z7lLXwlgIsDapeMCsrxkVO4\n` +
`LXX5AQ1xQNtlssnVoUBqBrvZsX2jUUKUocvZqMGuE4hfAgMBAAE=\n-----END PUBLIC KEY-----\n"`,
} }
} }
func (p *FederationServerMockPerson) KeyID(host string) string {
return fmt.Sprintf("%[1]v/api/v1/activitypub/user-id/%[2]v#main-key", host, p.ID)
}
func NewFederationServerMockRepository(id int64) FederationServerMockRepository { func NewFederationServerMockRepository(id int64) FederationServerMockRepository {
return FederationServerMockRepository{ return FederationServerMockRepository{
ID: id, ID: id,
} }
} }
func NewApActorMock() ApActorMock {
priv, pub, _ := util.GenerateKeyPair(1024)
return ApActorMock{
PrivKey: priv,
PubKey: pub,
}
}
func (u *ApActorMock) KeyID(host string) string {
return fmt.Sprintf("%[1]v/api/v1/activitypub/actor#main-key", host)
}
func (p FederationServerMockPerson) marshal(host string) string { func (p FederationServerMockPerson) marshal(host string) string {
return fmt.Sprintf(`{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1"],`+ return fmt.Sprintf(`{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1"],`+
`"id":"http://%[1]v/api/v1/activitypub/user-id/%[2]v",`+ `"id":"http://%[1]v/api/activitypub/user-id/%[2]v",`+
`"type":"Person",`+ `"type":"Person",`+
`"icon":{"type":"Image","mediaType":"image/png","url":"http://%[1]v/avatars/1bb05d9a5f6675ed0272af9ea193063c"},`+ `"icon":{"type":"Image","mediaType":"image/png","url":"http://%[1]v/avatars/1bb05d9a5f6675ed0272af9ea193063c"},`+
`"url":"http://%[1]v/%[2]v",`+ `"url":"http://%[1]v/%[2]v",`+
`"inbox":"http://%[1]v/api/v1/activitypub/user-id/%[2]v/inbox",`+ `"inbox":"http://%[1]v/api/activitypub/user-id/%[2]v/inbox",`+
`"outbox":"http://%[1]v/api/v1/activitypub/user-id/%[2]v/outbox",`+ `"outbox":"http://%[1]v/api/activitypub/user-id/%[2]v/outbox",`+
`"preferredUsername":"%[3]v",`+ `"preferredUsername":"%[3]v",`+
`"publicKey":{"id":"http://%[1]v/api/v1/activitypub/user-id/%[2]v#main-key",`+ `"publicKey":{"id":"http://%[1]v/api/activitypub/user-id/%[2]v#main-key",`+
`"owner":"http://%[1]v/api/v1/activitypub/user-id/%[2]v",`+ `"owner":"http://%[1]v/api/activitypub/user-id/%[2]v",`+
`"publicKeyPem":%[4]q}}`, host, p.ID, p.Name, p.PubKey) `"publicKeyPem":%[4]v}}`, host, p.ID, p.Name, p.PubKey)
} }
func NewFederationServerMock() *FederationServerMock { func NewFederationServerMock() *FederationServerMock {
return &FederationServerMock{ return &FederationServerMock{
ApActor: NewApActorMock(),
Persons: []FederationServerMockPerson{ Persons: []FederationServerMockPerson{
NewFederationServerMockPerson(15, "stargoose1"), NewFederationServerMockPerson(15, "stargoose1"),
NewFederationServerMockPerson(30, "stargoose2"), NewFederationServerMockPerson(30, "stargoose2"),
@ -94,18 +71,8 @@ func NewFederationServerMock() *FederationServerMock {
} }
} }
func (mock *FederationServerMock) recordLastPost(t *testing.T, req *http.Request) {
buf := new(strings.Builder)
_, err := io.Copy(buf, req.Body)
if err != nil {
t.Errorf("Error reading body: %q", err)
}
mock.LastPost = strings.ReplaceAll(buf.String(), req.Host, "DISTANT_FEDERATION_HOST")
}
func (mock *FederationServerMock) DistantServer(t *testing.T) *httptest.Server { func (mock *FederationServerMock) DistantServer(t *testing.T) *httptest.Server {
federatedRoutes := http.NewServeMux() federatedRoutes := http.NewServeMux()
federatedRoutes.HandleFunc("/.well-known/nodeinfo", federatedRoutes.HandleFunc("/.well-known/nodeinfo",
func(res http.ResponseWriter, req *http.Request) { func(res http.ResponseWriter, req *http.Request) {
// curl -H "Accept: application/json" https://federated-repo.prod.meissa.de/.well-known/nodeinfo // curl -H "Accept: application/json" https://federated-repo.prod.meissa.de/.well-known/nodeinfo
@ -120,28 +87,30 @@ func (mock *FederationServerMock) DistantServer(t *testing.T) *httptest.Server {
`"protocols":["activitypub"],"services":{"inbound":[],"outbound":["rss2.0"]},`+ `"protocols":["activitypub"],"services":{"inbound":[],"outbound":["rss2.0"]},`+
`"openRegistrations":true,"usage":{"users":{"total":14,"activeHalfyear":2}},"metadata":{}}`) `"openRegistrations":true,"usage":{"users":{"total":14,"activeHalfyear":2}},"metadata":{}}`)
}) })
for _, person := range mock.Persons { for _, person := range mock.Persons {
federatedRoutes.HandleFunc(fmt.Sprintf("/api/v1/activitypub/user-id/%v", person.ID), federatedRoutes.HandleFunc(fmt.Sprintf("/api/v1/activitypub/user-id/%v", person.ID),
func(res http.ResponseWriter, req *http.Request) { func(res http.ResponseWriter, req *http.Request) {
// curl -H "Accept: application/json" https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/2 // curl -H "Accept: application/json" https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/2
fmt.Fprint(res, person.marshal(req.Host)) fmt.Fprint(res, person.marshal(req.Host))
}) })
federatedRoutes.HandleFunc(fmt.Sprintf("POST /api/v1/activitypub/user-id/%v/inbox", person.ID),
func(res http.ResponseWriter, req *http.Request) {
mock.recordLastPost(t, req)
})
} }
for _, repository := range mock.Repositories { for _, repository := range mock.Repositories {
federatedRoutes.HandleFunc(fmt.Sprintf("POST /api/v1/activitypub/repository-id/%v/inbox", repository.ID), federatedRoutes.HandleFunc(fmt.Sprintf("/api/v1/activitypub/repository-id/%v/inbox", repository.ID),
func(res http.ResponseWriter, req *http.Request) { func(res http.ResponseWriter, req *http.Request) {
mock.recordLastPost(t, req) if req.Method != "POST" {
t.Errorf("POST expected at: %q", req.URL.EscapedPath())
}
buf := new(strings.Builder)
_, err := io.Copy(buf, req.Body)
if err != nil {
t.Errorf("Error reading body: %q", err)
}
mock.LastPost = buf.String()
}) })
} }
federatedRoutes.HandleFunc("/", federatedRoutes.HandleFunc("/",
func(res http.ResponseWriter, req *http.Request) { func(res http.ResponseWriter, req *http.Request) {
t.Errorf("Unhandled %v request: %q", req.Method, req.URL.EscapedPath()) t.Errorf("Unhandled request: %q", req.URL.EscapedPath())
}) })
federatedSrv := httptest.NewServer(federatedRoutes) federatedSrv := httptest.NewServer(federatedRoutes)
return federatedSrv return federatedSrv

View file

@ -124,7 +124,7 @@ func (ct SniffedType) GetMimeType() string {
} }
// DetectContentType extends http.DetectContentType with more content types. Defaults to text/unknown if input is empty. // DetectContentType extends http.DetectContentType with more content types. Defaults to text/unknown if input is empty.
func DetectContentType(data []byte, filename string) SniffedType { func DetectContentType(data []byte) SniffedType {
if len(data) == 0 { if len(data) == 0 {
return SniffedType{"text/unknown"} return SniffedType{"text/unknown"}
} }
@ -176,13 +176,6 @@ func DetectContentType(data []byte, filename string) SniffedType {
} }
} }
if ct == "application/octet-stream" &&
filename != "" &&
!strings.HasSuffix(strings.ToUpper(filename), ".LCOM") &&
bytes.Contains(data, []byte("(DEFINE-FILE-INFO ")) {
ct = "text/vnd.interlisp"
}
// GLTF is unsupported by http.DetectContentType // GLTF is unsupported by http.DetectContentType
// hexdump -n 4 -C glTF.glb // hexdump -n 4 -C glTF.glb
if bytes.HasPrefix(data, []byte("glTF")) { if bytes.HasPrefix(data, []byte("glTF")) {
@ -193,7 +186,7 @@ func DetectContentType(data []byte, filename string) SniffedType {
} }
// DetectContentTypeFromReader guesses the content type contained in the reader. // DetectContentTypeFromReader guesses the content type contained in the reader.
func DetectContentTypeFromReader(r io.Reader, filename string) (SniffedType, error) { func DetectContentTypeFromReader(r io.Reader) (SniffedType, error) {
buf := make([]byte, sniffLen) buf := make([]byte, sniffLen)
n, err := util.ReadAtMost(r, buf) n, err := util.ReadAtMost(r, buf)
if err != nil { if err != nil {
@ -201,5 +194,5 @@ func DetectContentTypeFromReader(r io.Reader, filename string) (SniffedType, err
} }
buf = buf[:n] buf = buf[:n]
return DetectContentType(buf, filename), nil return DetectContentType(buf), nil
} }

View file

@ -16,63 +16,63 @@ import (
func TestDetectContentTypeLongerThanSniffLen(t *testing.T) { func TestDetectContentTypeLongerThanSniffLen(t *testing.T) {
// Pre-condition: Shorter than sniffLen detects SVG. // Pre-condition: Shorter than sniffLen detects SVG.
assert.Equal(t, "image/svg+xml", DetectContentType([]byte(`<!-- Comment --><svg></svg>`), "").contentType) assert.Equal(t, "image/svg+xml", DetectContentType([]byte(`<!-- Comment --><svg></svg>`)).contentType)
// Longer than sniffLen detects something else. // Longer than sniffLen detects something else.
assert.NotEqual(t, "image/svg+xml", DetectContentType([]byte(`<!-- `+strings.Repeat("x", sniffLen)+` --><svg></svg>`), "").contentType) assert.NotEqual(t, "image/svg+xml", DetectContentType([]byte(`<!-- `+strings.Repeat("x", sniffLen)+` --><svg></svg>`)).contentType)
} }
func TestIsTextFile(t *testing.T) { func TestIsTextFile(t *testing.T) {
assert.True(t, DetectContentType([]byte{}, "").IsText()) assert.True(t, DetectContentType([]byte{}).IsText())
assert.True(t, DetectContentType([]byte("lorem ipsum"), "").IsText()) assert.True(t, DetectContentType([]byte("lorem ipsum")).IsText())
} }
func TestIsSvgImage(t *testing.T) { func TestIsSvgImage(t *testing.T) {
assert.True(t, DetectContentType([]byte("<svg></svg>"), "").IsSvgImage()) assert.True(t, DetectContentType([]byte("<svg></svg>")).IsSvgImage())
assert.True(t, DetectContentType([]byte(" <svg></svg>"), "").IsSvgImage()) assert.True(t, DetectContentType([]byte(" <svg></svg>")).IsSvgImage())
assert.True(t, DetectContentType([]byte(`<svg width="100"></svg>`), "").IsSvgImage()) assert.True(t, DetectContentType([]byte(`<svg width="100"></svg>`)).IsSvgImage())
assert.True(t, DetectContentType([]byte(`<?xml version="1.0" encoding="UTF-8"?><svg></svg>`), "").IsSvgImage()) assert.True(t, DetectContentType([]byte(`<?xml version="1.0" encoding="UTF-8"?><svg></svg>`)).IsSvgImage())
assert.True(t, DetectContentType([]byte(`<!-- Comment --> assert.True(t, DetectContentType([]byte(`<!-- Comment -->
<svg></svg>`), "").IsSvgImage()) <svg></svg>`)).IsSvgImage())
assert.True(t, DetectContentType([]byte(`<!-- Multiple --> assert.True(t, DetectContentType([]byte(`<!-- Multiple -->
<!-- Comments --> <!-- Comments -->
<svg></svg>`), "").IsSvgImage()) <svg></svg>`)).IsSvgImage())
assert.True(t, DetectContentType([]byte(`<!-- Multiline assert.True(t, DetectContentType([]byte(`<!-- Multiline
Comment --> Comment -->
<svg></svg>`), "").IsSvgImage()) <svg></svg>`)).IsSvgImage())
assert.True(t, DetectContentType([]byte(`<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1 Basic//EN" assert.True(t, DetectContentType([]byte(`<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1 Basic//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-basic.dtd"> "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-basic.dtd">
<svg></svg>`), "").IsSvgImage()) <svg></svg>`)).IsSvgImage())
assert.True(t, DetectContentType([]byte(`<?xml version="1.0" encoding="UTF-8"?> assert.True(t, DetectContentType([]byte(`<?xml version="1.0" encoding="UTF-8"?>
<!-- Comment --> <!-- Comment -->
<svg></svg>`), "").IsSvgImage()) <svg></svg>`)).IsSvgImage())
assert.True(t, DetectContentType([]byte(`<?xml version="1.0" encoding="UTF-8"?> assert.True(t, DetectContentType([]byte(`<?xml version="1.0" encoding="UTF-8"?>
<!-- Multiple --> <!-- Multiple -->
<!-- Comments --> <!-- Comments -->
<svg></svg>`), "").IsSvgImage()) <svg></svg>`)).IsSvgImage())
assert.True(t, DetectContentType([]byte(`<?xml version="1.0" encoding="UTF-8"?> assert.True(t, DetectContentType([]byte(`<?xml version="1.0" encoding="UTF-8"?>
<!-- Multiline <!-- Multiline
Comment --> Comment -->
<svg></svg>`), "").IsSvgImage()) <svg></svg>`)).IsSvgImage())
assert.True(t, DetectContentType([]byte(`<?xml version="1.0" encoding="UTF-8"?> assert.True(t, DetectContentType([]byte(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Multiline <!-- Multiline
Comment --> Comment -->
<svg></svg>`), "").IsSvgImage()) <svg></svg>`)).IsSvgImage())
// the DetectContentType should work for incomplete data, because only beginning bytes are used for detection // the DetectContentType should work for incomplete data, because only beginning bytes are used for detection
assert.True(t, DetectContentType([]byte(`<svg>....`), "").IsSvgImage()) assert.True(t, DetectContentType([]byte(`<svg>....`)).IsSvgImage())
assert.False(t, DetectContentType([]byte{}, "").IsSvgImage()) assert.False(t, DetectContentType([]byte{}).IsSvgImage())
assert.False(t, DetectContentType([]byte("svg"), "").IsSvgImage()) assert.False(t, DetectContentType([]byte("svg")).IsSvgImage())
assert.False(t, DetectContentType([]byte("<svgfoo></svgfoo>"), "").IsSvgImage()) assert.False(t, DetectContentType([]byte("<svgfoo></svgfoo>")).IsSvgImage())
assert.False(t, DetectContentType([]byte("text<svg></svg>"), "").IsSvgImage()) assert.False(t, DetectContentType([]byte("text<svg></svg>")).IsSvgImage())
assert.False(t, DetectContentType([]byte("<html><body><svg></svg></body></html>"), "").IsSvgImage()) assert.False(t, DetectContentType([]byte("<html><body><svg></svg></body></html>")).IsSvgImage())
assert.False(t, DetectContentType([]byte(`<script>"<svg></svg>"</script>`), "").IsSvgImage()) assert.False(t, DetectContentType([]byte(`<script>"<svg></svg>"</script>`)).IsSvgImage())
assert.False(t, DetectContentType([]byte(`<!-- <svg></svg> inside comment --> assert.False(t, DetectContentType([]byte(`<!-- <svg></svg> inside comment -->
<foo></foo>`), "").IsSvgImage()) <foo></foo>`)).IsSvgImage())
assert.False(t, DetectContentType([]byte(`<?xml version="1.0" encoding="UTF-8"?> assert.False(t, DetectContentType([]byte(`<?xml version="1.0" encoding="UTF-8"?>
<!-- <svg></svg> inside comment --> <!-- <svg></svg> inside comment -->
<foo></foo>`), "").IsSvgImage()) <foo></foo>`)).IsSvgImage())
assert.False(t, DetectContentType([]byte(` assert.False(t, DetectContentType([]byte(`
<!-- comment1 --> <!-- comment1 -->
@ -80,7 +80,7 @@ func TestIsSvgImage(t *testing.T) {
<!-- comment2 --> <!-- comment2 -->
<svg></svg> <svg></svg>
</div> </div>
`), "").IsSvgImage()) `)).IsSvgImage())
assert.False(t, DetectContentType([]byte(` assert.False(t, DetectContentType([]byte(`
<!-- comment1 <!-- comment1
@ -90,56 +90,56 @@ func TestIsSvgImage(t *testing.T) {
--> -->
<svg></svg> <svg></svg>
</div> </div>
`), "").IsSvgImage()) `)).IsSvgImage())
assert.False(t, DetectContentType([]byte(`<html><body><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg></svg></body></html>`), "").IsSvgImage()) assert.False(t, DetectContentType([]byte(`<html><body><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg></svg></body></html>`)).IsSvgImage())
assert.False(t, DetectContentType([]byte(`<html><body><?xml version="1.0" encoding="UTF-8"?><svg></svg></body></html>`), "").IsSvgImage()) assert.False(t, DetectContentType([]byte(`<html><body><?xml version="1.0" encoding="UTF-8"?><svg></svg></body></html>`)).IsSvgImage())
} }
func TestIsPDF(t *testing.T) { func TestIsPDF(t *testing.T) {
pdf, _ := base64.StdEncoding.DecodeString("JVBERi0xLjYKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURlY29kZT4+CnN0cmVhbQp4nF3NPwsCMQwF8D2f4s2CNYk1baF0EHRwOwg4iJt/NsFb/PpevUE4Mjwe") pdf, _ := base64.StdEncoding.DecodeString("JVBERi0xLjYKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURlY29kZT4+CnN0cmVhbQp4nF3NPwsCMQwF8D2f4s2CNYk1baF0EHRwOwg4iJt/NsFb/PpevUE4Mjwe")
assert.True(t, DetectContentType(pdf, "").IsPDF()) assert.True(t, DetectContentType(pdf).IsPDF())
assert.False(t, DetectContentType([]byte("plain text"), "").IsPDF()) assert.False(t, DetectContentType([]byte("plain text")).IsPDF())
} }
func TestIsVideo(t *testing.T) { func TestIsVideo(t *testing.T) {
mp4, _ := base64.StdEncoding.DecodeString("AAAAGGZ0eXBtcDQyAAAAAGlzb21tcDQyAAEI721vb3YAAABsbXZoZAAAAADaBlwX2gZcFwAAA+gA") mp4, _ := base64.StdEncoding.DecodeString("AAAAGGZ0eXBtcDQyAAAAAGlzb21tcDQyAAEI721vb3YAAABsbXZoZAAAAADaBlwX2gZcFwAAA+gA")
assert.True(t, DetectContentType(mp4, "").IsVideo()) assert.True(t, DetectContentType(mp4).IsVideo())
assert.False(t, DetectContentType([]byte("plain text"), "").IsVideo()) assert.False(t, DetectContentType([]byte("plain text")).IsVideo())
} }
func TestIsAudio(t *testing.T) { func TestIsAudio(t *testing.T) {
mp3, _ := base64.StdEncoding.DecodeString("SUQzBAAAAAABAFRYWFgAAAASAAADbWFqb3JfYnJhbmQAbXA0MgBUWFhYAAAAEQAAA21pbm9yX3Zl") mp3, _ := base64.StdEncoding.DecodeString("SUQzBAAAAAABAFRYWFgAAAASAAADbWFqb3JfYnJhbmQAbXA0MgBUWFhYAAAAEQAAA21pbm9yX3Zl")
assert.True(t, DetectContentType(mp3, "").IsAudio()) assert.True(t, DetectContentType(mp3).IsAudio())
assert.False(t, DetectContentType([]byte("plain text"), "").IsAudio()) assert.False(t, DetectContentType([]byte("plain text")).IsAudio())
assert.True(t, DetectContentType([]byte("ID3Toy\000"), "").IsAudio()) assert.True(t, DetectContentType([]byte("ID3Toy\000")).IsAudio())
assert.True(t, DetectContentType([]byte("ID3Toy\n====\t* hi 🌞, ..."), "").IsText()) // test ID3 tag for plain text assert.True(t, DetectContentType([]byte("ID3Toy\n====\t* hi 🌞, ...")).IsText()) // test ID3 tag for plain text
assert.True(t, DetectContentType([]byte("ID3Toy\n====\t* hi 🌞, ..."+"🌛"[0:2]), "").IsText()) // test ID3 tag with incomplete UTF8 char assert.True(t, DetectContentType([]byte("ID3Toy\n====\t* hi 🌞, ..."+"🌛"[0:2])).IsText()) // test ID3 tag with incomplete UTF8 char
} }
func TestIsGLB(t *testing.T) { func TestIsGLB(t *testing.T) {
glb, _ := hex.DecodeString("676c5446") glb, _ := hex.DecodeString("676c5446")
assert.True(t, DetectContentType(glb, "").IsGLB()) assert.True(t, DetectContentType(glb).IsGLB())
assert.True(t, DetectContentType(glb, "").Is3DModel()) assert.True(t, DetectContentType(glb).Is3DModel())
assert.False(t, DetectContentType([]byte("plain text"), "").IsGLB()) assert.False(t, DetectContentType([]byte("plain text")).IsGLB())
assert.False(t, DetectContentType([]byte("plain text"), "").Is3DModel()) assert.False(t, DetectContentType([]byte("plain text")).Is3DModel())
} }
func TestDetectContentTypeFromReader(t *testing.T) { func TestDetectContentTypeFromReader(t *testing.T) {
mp3, _ := base64.StdEncoding.DecodeString("SUQzBAAAAAABAFRYWFgAAAASAAADbWFqb3JfYnJhbmQAbXA0MgBUWFhYAAAAEQAAA21pbm9yX3Zl") mp3, _ := base64.StdEncoding.DecodeString("SUQzBAAAAAABAFRYWFgAAAASAAADbWFqb3JfYnJhbmQAbXA0MgBUWFhYAAAAEQAAA21pbm9yX3Zl")
st, err := DetectContentTypeFromReader(bytes.NewReader(mp3), "") st, err := DetectContentTypeFromReader(bytes.NewReader(mp3))
require.NoError(t, err) require.NoError(t, err)
assert.True(t, st.IsAudio()) assert.True(t, st.IsAudio())
} }
func TestDetectContentTypeOgg(t *testing.T) { func TestDetectContentTypeOgg(t *testing.T) {
oggAudio, _ := hex.DecodeString("4f67675300020000000000000000352f0000000000007dc39163011e01766f72626973000000000244ac0000000000000071020000000000b8014f6767530000") oggAudio, _ := hex.DecodeString("4f67675300020000000000000000352f0000000000007dc39163011e01766f72626973000000000244ac0000000000000071020000000000b8014f6767530000")
st, err := DetectContentTypeFromReader(bytes.NewReader(oggAudio), "") st, err := DetectContentTypeFromReader(bytes.NewReader(oggAudio))
require.NoError(t, err) require.NoError(t, err)
assert.True(t, st.IsAudio()) assert.True(t, st.IsAudio())
oggVideo, _ := hex.DecodeString("4f676753000200000000000000007d9747ef000000009b59daf3012a807468656f7261030201001e00110001e000010e00020000001e00000001000001000001") oggVideo, _ := hex.DecodeString("4f676753000200000000000000007d9747ef000000009b59daf3012a807468656f7261030201001e00110001e000010e00020000001e00000001000001000001")
st, err = DetectContentTypeFromReader(bytes.NewReader(oggVideo), "") st, err = DetectContentTypeFromReader(bytes.NewReader(oggVideo))
require.NoError(t, err) require.NoError(t, err)
assert.True(t, st.IsVideo()) assert.True(t, st.IsVideo())
} }
@ -148,7 +148,7 @@ func TestDetectContentTypeAvif(t *testing.T) {
avifImage, err := hex.DecodeString("000000206674797061766966") avifImage, err := hex.DecodeString("000000206674797061766966")
require.NoError(t, err) require.NoError(t, err)
st, err := DetectContentTypeFromReader(bytes.NewReader(avifImage), "") st, err := DetectContentTypeFromReader(bytes.NewReader(avifImage))
require.NoError(t, err) require.NoError(t, err)
assert.True(t, st.IsImage()) assert.True(t, st.IsImage())
@ -158,24 +158,10 @@ func TestDetectContentTypeModelGLB(t *testing.T) {
glb, err := hex.DecodeString("676c5446") glb, err := hex.DecodeString("676c5446")
require.NoError(t, err) require.NoError(t, err)
st, err := DetectContentTypeFromReader(bytes.NewReader(glb), "") st, err := DetectContentTypeFromReader(bytes.NewReader(glb))
require.NoError(t, err) require.NoError(t, err)
// print st for debugging // print st for debugging
assert.Equal(t, "model/gltf-binary", st.GetMimeType()) assert.Equal(t, "model/gltf-binary", st.GetMimeType())
assert.True(t, st.IsGLB()) assert.True(t, st.IsGLB())
} }
func TestDetectInterlisp(t *testing.T) {
interlisp, err := base64.StdEncoding.DecodeString("ICAKKERFRklORS1GSUxFLUlORk8gHlBBQ0tBR0UgIklOVEVSTElTUCIgHlJFQURUQUJMRSAiSU5URVJMSVNQIiAeQkFTRSAxMCkKCgYB")
require.NoError(t, err)
st, err := DetectContentTypeFromReader(bytes.NewReader(interlisp), "test")
require.NoError(t, err)
assert.True(t, st.IsText())
st, err = DetectContentTypeFromReader(bytes.NewReader(interlisp), "")
require.NoError(t, err)
assert.False(t, st.IsText())
st, err = DetectContentTypeFromReader(bytes.NewReader(interlisp), "test.lcom")
require.NoError(t, err)
assert.False(t, st.IsText())
}

View file

@ -699,7 +699,7 @@ issues.filter_milestone_all = كل الأهداف
issues.unlock.notice_2 = - يمكنك دوما إقفال هذه المسألة من جديد في المستقبل. issues.unlock.notice_2 = - يمكنك دوما إقفال هذه المسألة من جديد في المستقبل.
issues.num_participants_few = %d متحاور issues.num_participants_few = %d متحاور
release.title = عنوان الإصدار release.title = عنوان الإصدار
issues.closed_at = `أغلق هذه المسألة %s` issues.closed_at = `أغلق هذه المسألة <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.lock.title = إقفال التحاور في هذه المسألة. issues.lock.title = إقفال التحاور في هذه المسألة.
issues.new.no_label = بلا تصنيف issues.new.no_label = بلا تصنيف
issues.filter_sort.mostforks = الأعلى اشتقاقا issues.filter_sort.mostforks = الأعلى اشتقاقا
@ -759,7 +759,7 @@ branch.renamed = غُيّر اسم الفرع %s إلى %s.
delete_preexisting = احذف الملفات الموجودة سابقا delete_preexisting = احذف الملفات الموجودة سابقا
branch.included_desc = هذا الفرع جزء من الفرع المبدئي branch.included_desc = هذا الفرع جزء من الفرع المبدئي
trust_model_helper_collaborator_committer = مشترك+مودع: ثق بتوقيعات المشتركين التي تطابق المودع trust_model_helper_collaborator_committer = مشترك+مودع: ثق بتوقيعات المشتركين التي تطابق المودع
issues.reopened_at = `أعاد فتح هذه المسألة %s` issues.reopened_at = `أعاد فتح هذه المسألة <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.action_milestone = هدف issues.action_milestone = هدف
issues.new.assignees = المكلَّفون issues.new.assignees = المكلَّفون
release.tag_name_protected = اسم الوسم محمي. release.tag_name_protected = اسم الوسم محمي.
@ -1166,7 +1166,7 @@ pulls.status_checking = في انتظار بعض الفحوص
pulls.status_checks_failure = بعض الفحوص فشلت pulls.status_checks_failure = بعض الفحوص فشلت
pulls.status_checks_success = جميع الفحوص ناجحة pulls.status_checks_success = جميع الفحوص ناجحة
pulls.status_checks_warning = بعض الفحوص تعطي تحذيرات pulls.status_checks_warning = بعض الفحوص تعطي تحذيرات
pulls.commit_ref_at = `أشار إلى طلب الدمج من إيداع %s` pulls.commit_ref_at = `أشار إلى طلب الدمج من إيداع <a id="%[1]s" href="#%[1]s">%[2]s</a>`
pulls.cmd_instruction_hint = `أظهر شرح استخدام سطر الأوامر.` pulls.cmd_instruction_hint = `أظهر شرح استخدام سطر الأوامر.`
pulls.cmd_instruction_checkout_title = اسحب pulls.cmd_instruction_checkout_title = اسحب
pulls.cmd_instruction_checkout_desc = من مستودع مشروعك، اسحب (check out) فرعا جديدا واختبر التغييرات. pulls.cmd_instruction_checkout_desc = من مستودع مشروعك، اسحب (check out) فرعا جديدا واختبر التغييرات.
@ -1257,8 +1257,8 @@ pulls.status_checks_details = تفاصيل
pulls.status_checks_hide_all = أخفِ كل الفحوص pulls.status_checks_hide_all = أخفِ كل الفحوص
pulls.status_checks_show_all = أظهر كل الفحوص pulls.status_checks_show_all = أظهر كل الفحوص
pulls.close = أغلق طلب الدمج pulls.close = أغلق طلب الدمج
pulls.closed_at = `أغلق طلب الدمج %s` pulls.closed_at = `أغلق طلب الدمج <a id="%[1]s" href="#%[1]s">%[2]s</a>`
pulls.reopened_at = `أعاد فتح طلب الدمج %s` pulls.reopened_at = `أعاد فتح طلب الدمج <a id="%[1]s" href="#%[1]s">%[2]s</a>`
milestones.title = العنوان milestones.title = العنوان
milestones.desc = الوصف milestones.desc = الوصف
milestones.edit = عدّل الهدف milestones.edit = عدّل الهدف
@ -1302,11 +1302,11 @@ issues.closed_by_fake = من %[2]s أُغلقت %[1]s
issues.num_comments_1 = %d تعليق issues.num_comments_1 = %d تعليق
issues.num_comments = %d تعليقا issues.num_comments = %d تعليقا
issues.commented_at = `علّق <a href="#%s">%s</a>` issues.commented_at = `علّق <a href="#%s">%s</a>`
issues.commit_ref_at = `أشار إلى هذه المسألة من إيداع %s` issues.commit_ref_at = `أشار إلى هذه المسألة من إيداع <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.ref_issue_from = `<a href="%[2]s">أشار إلى هذه المسألة %[3]s</a> %[1]s` issues.ref_issue_from = `<a href="%[3]s">أشار إلى هذه المسألة %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.ref_pull_from = `<a href="%[2]s">أشار إلى هذا الطلب %[3]s</a> %[1]s` issues.ref_pull_from = `<a href="%[3]s">أشار إلى هذا الطلب %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.ref_closing_from = `<a href="%[2]s">أشار إلى طلب دمج %[3]s سيغلق هذه المسألة</a> %[1]s` issues.ref_closing_from = `<a href="%[3]s">أشار إلى طلب دمج %[4]s سيغلق هذه المسألة</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.ref_reopening_from = `<a href="%[2]s">أشار إلى طلب دمج %[3]s سيعيد فتح هذه المسألة</a> %[1]s` issues.ref_reopening_from = `<a href="%[3]s">أشار إلى طلب دمج %[4]s سيعيد فتح هذه المسألة</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.ref_closed_from = `<a href="%[3]s">أغلق هذه المسألة %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.ref_closed_from = `<a href="%[3]s">أغلق هذه المسألة %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.ref_reopened_from = `<a href="%[3]s">أعاد فتح هذه المسألة %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.ref_reopened_from = `<a href="%[3]s">أعاد فتح هذه المسألة %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.reference_issue.body = المحتوى issues.reference_issue.body = المحتوى

View file

@ -749,7 +749,7 @@ settings.admin_settings = Администраторски настройки
issues.role.owner = Притежател issues.role.owner = Притежател
settings.transfer.title = Прехвърляне на притежанието settings.transfer.title = Прехвърляне на притежанието
issues.author = Автор issues.author = Автор
issues.closed_at = `затвори тази задача %s` issues.closed_at = `затвори тази задача <a id="%[1]s" href="#%[1]s">%[2]s</a>`
settings.collaborator_deletion_desc = Премахването на сътрудник ще отнеме достъпа му до това хранилище. Продължаване? settings.collaborator_deletion_desc = Премахването на сътрудник ще отнеме достъпа му до това хранилище. Продължаване?
commits.message = Съобщение commits.message = Съобщение
issues.due_date_not_set = Няма зададен краен срок. issues.due_date_not_set = Няма зададен краен срок.
@ -773,9 +773,9 @@ issues.filter_type.all_issues = Всички задачи
issues.filter_poster_no_select = Всички автори issues.filter_poster_no_select = Всички автори
issues.opened_by = отворена %[1]s от <a href="%[2]s">%[3]s</a> issues.opened_by = отворена %[1]s от <a href="%[2]s">%[3]s</a>
issues.action_open = Отваряне issues.action_open = Отваряне
pulls.closed_at = `затвори тази заявка за сливане %s` pulls.closed_at = `затвори тази заявка за сливане <a id="%[1]s" href="#%[1]s">%[2]s</a>`
pulls.reopened_at = `отвори наново тази заявка за сливане %s` pulls.reopened_at = `отвори наново тази заявка за сливане <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.reopened_at = `отвори наново тази задача %s` issues.reopened_at = `отвори наново тази задача <a id="%[1]s" href="#%[1]s">%[2]s</a>`
projects.column.edit = Редактиране на колоната projects.column.edit = Редактиране на колоната
issues.close = Затваряне на задачата issues.close = Затваряне на задачата
issues.ref_reopened_from = `<a href="%[3]s">отвори наново тази задача %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.ref_reopened_from = `<a href="%[3]s">отвори наново тази задача %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
@ -1205,7 +1205,7 @@ issues.dependency.cancel = Отказ
issues.dependency.add_error_dep_exists = Зависимостта вече съществува. issues.dependency.add_error_dep_exists = Зависимостта вече съществува.
issues.dependency.add_error_dep_not_exist = Зависимостта не съществува. issues.dependency.add_error_dep_not_exist = Зависимостта не съществува.
issues.remove_ref_at = `премахна препратката <b>%s</b> %s` issues.remove_ref_at = `премахна препратката <b>%s</b> %s`
issues.ref_pull_from = `<a href="%[2]s">спомена тази заявка за сливане %[3]s</a> %[1]s` issues.ref_pull_from = `<a href="%[3]s">спомена тази заявка за сливане %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.dependency.pr_no_dependencies = Няма зададени зависимости. issues.dependency.pr_no_dependencies = Няма зададени зависимости.
issues.dependency.remove_info = Премахване на тази зависимост issues.dependency.remove_info = Премахване на тази зависимост
issues.dependency.removed_dependency = `премахна зависимостта %s` issues.dependency.removed_dependency = `премахна зависимостта %s`
@ -1230,11 +1230,11 @@ issues.dependency.title = Зависимости
issues.dependency.issue_no_dependencies = Няма зададени зависимости. issues.dependency.issue_no_dependencies = Няма зададени зависимости.
issues.dependency.pr_close_blocked = Трябва да затворите всички задачи, блокиращи тази заявка за сливане, преди да можете да я слеете. issues.dependency.pr_close_blocked = Трябва да затворите всички задачи, блокиращи тази заявка за сливане, преди да можете да я слеете.
issues.dependency.pr_close_blocks = Тази заявка за сливане блокира затварянето на следните задачи issues.dependency.pr_close_blocks = Тази заявка за сливане блокира затварянето на следните задачи
issues.ref_issue_from = `<a href="%[2]s">спомена тази задача %[3]s</a> %[1]s` issues.ref_issue_from = `<a href="%[3]s">спомена тази задача %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.commit_ref_at = `спомена тази задача в подаване %s` issues.commit_ref_at = `спомена тази задача в подаване <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.add_ref_at = `добави препратка <b>%s</b> %s` issues.add_ref_at = `добави препратка <b>%s</b> %s`
pulls.merged_info_text = Клонът %s вече може да бъде изтрит. pulls.merged_info_text = Клонът %s вече може да бъде изтрит.
pulls.commit_ref_at = `спомена тази заявка за сливане в подаване %s` pulls.commit_ref_at = `спомена тази заявка за сливане в подаване <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.change_ref_at = `промени препратката от <b><strike>%s</strike></b> на <b>%s</b> %s` issues.change_ref_at = `промени препратката от <b><strike>%s</strike></b> на <b>%s</b> %s`
diff.review.reject = Поискване на промени diff.review.reject = Поискване на промени
diff.bin_not_shown = Двоичният файл не е показан. diff.bin_not_shown = Двоичният файл не е показан.
@ -1299,9 +1299,9 @@ branch.create_new_branch = Създаване на клон от клон:
pulls.status_checks_show_all = Показване на всички проверки pulls.status_checks_show_all = Показване на всички проверки
size_format = %[1]s: %[2]s; %[3]s: %[4]s size_format = %[1]s: %[2]s; %[3]s: %[4]s
pulls.filter_changes_by_commit = Филтриране по подаване pulls.filter_changes_by_commit = Филтриране по подаване
issues.ref_closing_from = `<a href="%[2]s">спомена тази задача в заявка за сливане %[3]s, която ще я затвори</a>, %[1]s` issues.ref_closing_from = `<a href="%[3]s">спомена тази задача в заявка за сливане %[4]s, която ще я затвори</a>, <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.ref_from = `от %[1]s` issues.ref_from = `от %[1]s`
issues.ref_reopening_from = `<a href="%[2]s">спомена тази задача в заявка за сливане %[3]s, която ще я отвори наново </a>, %[1]s` issues.ref_reopening_from = `<a href="%[3]s">спомена тази задача в заявка за сливане %[4]s, която ще я отвори наново </a>, <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.draft_title = Чернова issues.draft_title = Чернова
pulls.reopen_to_merge = Моля, отворете наново тази заявка за сливане, за да извършите сливане. pulls.reopen_to_merge = Моля, отворете наново тази заявка за сливане, за да извършите сливане.
pulls.cant_reopen_deleted_branch = Тази заявка за сливане не може да бъде отворена наново, защото клонът е изтрит. pulls.cant_reopen_deleted_branch = Тази заявка за сливане не може да бъде отворена наново, защото клонът е изтрит.

View file

@ -1063,7 +1063,7 @@ language.localization_project = Pomozte nám s překladem Forgejo do vašeho jaz
user_block_yourself = Nemůžete zablokovat sami sebe. user_block_yourself = Nemůžete zablokovat sami sebe.
pronouns_custom_label = Vlastní zájmena pronouns_custom_label = Vlastní zájmena
change_username_redirect_prompt.with_cooldown.few = Staré uživatelské jméno bude dostupné ostatním po %[1]d dnech. Do té doby budete moci své staré uživatelské jméno znovu získat. change_username_redirect_prompt.with_cooldown.few = Staré uživatelské jméno bude dostupné ostatním po %[1]d dnech. Do té doby budete moci své staré uživatelské jméno znovu získat.
change_username_redirect_prompt.with_cooldown.one = Staré uživatelské jméno bude dostupné ostatním po %[1]d dnu. Do té doby budete moci své staré uživatelské jméno znovu získat. change_username_redirect_prompt.with_cooldown.one = Staré uživatelské jméno bude dostupné ostatním po %[1]d dni. Do té doby budete moci své staré uživatelské jméno znovu získat.
keep_pronouns_private = Zobrazovat zájmena pouze přihlášeným uživatelům keep_pronouns_private = Zobrazovat zájmena pouze přihlášeným uživatelům
keep_pronouns_private.description = Toto nastavení skryje vaše zájmena před návštěvníky, kteří nejsou přihlášeni. keep_pronouns_private.description = Toto nastavení skryje vaše zájmena před návštěvníky, kteří nejsou přihlášeni.
quota = Kvóta quota = Kvóta
@ -1579,7 +1579,7 @@ issues.remove_ref_at=`odstranil/a referenci <b>%s</b> %s`
issues.add_ref_at=`přidal/a referenci <b>%s</b> %s` issues.add_ref_at=`přidal/a referenci <b>%s</b> %s`
issues.delete_branch_at=`odstranil/a větev <b>%s</b> %s` issues.delete_branch_at=`odstranil/a větev <b>%s</b> %s`
issues.filter_label=Štítek issues.filter_label=Štítek
issues.filter_label_exclude=Chcete-li vyloučit štítky, použijte <kbd>Alt</kbd> + <kbd>kliknutí</kbd> issues.filter_label_exclude=`Chcete-li vyloučit štítky, použijte <code>alt</code> + <code>click/enter</code>`
issues.filter_label_no_select=Všechny štítky issues.filter_label_no_select=Všechny štítky
issues.filter_label_select_no_label=Bez štítku issues.filter_label_select_no_label=Bez štítku
issues.filter_milestone=Milník issues.filter_milestone=Milník
@ -1633,13 +1633,13 @@ issues.opened_by_fake=otevřeno %[1]s uživatelem %[2]s
issues.closed_by_fake=od %[2]s byl uzavřen %[1]s issues.closed_by_fake=od %[2]s byl uzavřen %[1]s
issues.previous=Předchozí issues.previous=Předchozí
issues.next=Další issues.next=Další
issues.open_title=Otevřené issues.open_title=Otevřeno
issues.closed_title=Uzavřené issues.closed_title=Uzavřeno
issues.draft_title=Koncept issues.draft_title=Koncept
issues.num_comments_1=%d komentář issues.num_comments_1=%d komentář
issues.num_comments=%d komentářů issues.num_comments=%d komentářů
issues.commented_at=`okomentoval/a <a href="#%s">%s</a>` issues.commented_at=`okomentoval/a <a href="#%s">%s</a>`
issues.delete_comment_confirm=Opravdu chcete smazat tento komentář? issues.delete_comment_confirm=Jste si jist, že chcete smazat tento komentář?
issues.context.copy_link=Kopírovat odkaz issues.context.copy_link=Kopírovat odkaz
issues.context.quote_reply=Citovat odpověď issues.context.quote_reply=Citovat odpověď
issues.context.reference_issue=Odkázat v novém problému issues.context.reference_issue=Odkázat v novém problému
@ -1653,13 +1653,13 @@ issues.close_comment_issue=Zavřít s komentářem
issues.reopen_issue=Znovu otevřít issues.reopen_issue=Znovu otevřít
issues.reopen_comment_issue=Znovu otevřít s komentářem issues.reopen_comment_issue=Znovu otevřít s komentářem
issues.create_comment=Komentovat issues.create_comment=Komentovat
issues.closed_at=`uzavřel/a tento problém %s` issues.closed_at=`uzavřel/a tento problém <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.reopened_at=`znovu otevřel/a tento problém %s` issues.reopened_at=`znovu otevřel/a tento problém <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.commit_ref_at=`odkázal/a na tento problém z revize %s` issues.commit_ref_at=`odkázal/a na tento problém z revize <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.ref_issue_from=`<a href="%[2]s">odkázal/a na tento problém %[3]s</a> %[1]s` issues.ref_issue_from=`<a href="%[3]s">odkázal/a na tento problém %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.ref_pull_from=`<a href="%[2]s">odkázal/a na tuto žádost o sloučení %[3]s</a> %[1]s` issues.ref_pull_from=`<a href="%[3]s">odkázal/a na tuto žádost o sloučení %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.ref_closing_from=`<a href="%[2]s">odkázal/a na tento problém ze žádosti o sloučení %[3]s, která jej uzavře</a>, %[1]s` issues.ref_closing_from=`<a href="%[3]s">odkazoval/a na tento problém ze žádosti o sloučení %[4]s, která jej uzavře</a>, <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.ref_reopening_from=`<a href="%[2]s">odkázal/a na tento problém ze žádosti o sloučení %[3]s, která jej znovu otevře</a>, %[1]s` issues.ref_reopening_from=`<a href="%[3]s">odkazoval/a na tento problém ze žádosti o sloučení %[4]s, která jej znovu otevře</a>, <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.ref_closed_from=`<a href="%[3]s">uzavřel/a tento problém %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.ref_closed_from=`<a href="%[3]s">uzavřel/a tento problém %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.ref_reopened_from=`<a href="%[3]s">znovu otevřel/a tento problém %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.ref_reopened_from=`<a href="%[3]s">znovu otevřel/a tento problém %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.ref_from=`z %[1]s` issues.ref_from=`z %[1]s`
@ -1966,8 +1966,8 @@ pulls.update_branch_success=Aktualizace větve byla úspěšná
pulls.update_not_allowed=Nemáte oprávnění aktualizovat větev pulls.update_not_allowed=Nemáte oprávnění aktualizovat větev
pulls.outdated_with_base_branch=Tato větev je zastaralá oproti základní větvi pulls.outdated_with_base_branch=Tato větev je zastaralá oproti základní větvi
pulls.close=Zavřít žádost o sloučení pulls.close=Zavřít žádost o sloučení
pulls.closed_at=`uzavřel/a tuto žádost o sloučení %s` pulls.closed_at=`uzavřel/a tuto žádost o sloučení <a id="%[1]s" href="#%[1]s">%[2]s</a>`
pulls.reopened_at=`znovu otevřel/a tuto žádost o sloučení %s` pulls.reopened_at=`znovu otevřel/a tuto žádost o sloučení <a id="%[1]s" href="#%[1]s">%[2]s</a>`
pulls.cmd_instruction_hint=Zobrazit instrukce příkazové řádky pulls.cmd_instruction_hint=Zobrazit instrukce příkazové řádky
pulls.cmd_instruction_checkout_desc=Z vašeho repositáře projektu se podívejte na novou větev a vyzkoušejte změny. pulls.cmd_instruction_checkout_desc=Z vašeho repositáře projektu se podívejte na novou větev a vyzkoušejte změny.
pulls.cmd_instruction_merge_title=Sloučit pulls.cmd_instruction_merge_title=Sloučit
@ -2758,7 +2758,7 @@ settings.mirror_settings.docs.disabled_push_mirror.pull_mirror_warning = Tuto ak
settings.new_owner_blocked_doer = Nový majitel vás zablokoval. settings.new_owner_blocked_doer = Nový majitel vás zablokoval.
settings.mirror_settings.pushed_repository = Odeslaný repozitář settings.mirror_settings.pushed_repository = Odeslaný repozitář
settings.add_collaborator_blocked_our = Nepodařilo se přidat spolupracovníka, jelikož byl zablokován majitelem repozitáře. settings.add_collaborator_blocked_our = Nepodařilo se přidat spolupracovníka, jelikož byl zablokován majitelem repozitáře.
pulls.commit_ref_at = `odkázal/a na tuto žádost o sloučení z revize %s` pulls.commit_ref_at = `se odkázal/a na tuto žádost o sloučení z revize <a id="%[1]s" href="#%[1]s">%[2]s</a>`
settings.wiki_rename_branch_main = Normalizovat název větve wiki settings.wiki_rename_branch_main = Normalizovat název větve wiki
settings.wiki_rename_branch_main_desc = Přejmenovat větev interně používanou pro wiki na „%s“. Tato změna je trvalá a nelze ji vrátit. settings.wiki_rename_branch_main_desc = Přejmenovat větev interně používanou pro wiki na „%s“. Tato změna je trvalá a nelze ji vrátit.
pulls.fast_forward_only_merge_pull_request = Pouze zrychlené pulls.fast_forward_only_merge_pull_request = Pouze zrychlené
@ -3058,7 +3058,7 @@ teams.invite.by=Pozvání od %s
teams.invite.description=Pro připojení k týmu klikněte na tlačítko níže. teams.invite.description=Pro připojení k týmu klikněte na tlačítko níže.
follow_blocked_user = Tuto organizaci nemůžete sledovat, protože jste v ní zablokováni. follow_blocked_user = Tuto organizaci nemůžete sledovat, protože jste v ní zablokováni.
open_dashboard = Otevřít nástěnku open_dashboard = Otevřít nástěnku
settings.change_orgname_redirect_prompt.with_cooldown.one = Starý název organizace bude dostupný ostatním po %[1]d dnu. Do té doby budete moci staré jméno znovu získat. settings.change_orgname_redirect_prompt.with_cooldown.one = Starý název organizace bude dostupný ostatním po %[1]d dni. Do té doby budete moci staré jméno znovu získat.
settings.change_orgname_redirect_prompt.with_cooldown.few = Starý název organizace bude dostupný ostatním po %[1]d dnech. Do té doby budete moci starý název znovu získat. settings.change_orgname_redirect_prompt.with_cooldown.few = Starý název organizace bude dostupný ostatním po %[1]d dnech. Do té doby budete moci starý název znovu získat.
[admin] [admin]

View file

@ -1520,15 +1520,15 @@ issues.add_labels = tilføjede %s etiketterne %s
issues.add_remove_labels = tilføjede %s og fjernede %s etiketter %s issues.add_remove_labels = tilføjede %s og fjernede %s etiketter %s
issues.add_milestone_at = `føjede dette til <b>%s</b> milepælen %s` issues.add_milestone_at = `føjede dette til <b>%s</b> milepælen %s`
issues.add_project_at = `føjede dette til <b>%s</b>- projektet %s` issues.add_project_at = `føjede dette til <b>%s</b>- projektet %s`
issues.ref_reopening_from = `<a href="%[2]s">henviste til dette problem fra en pull-anmodning %[3]s, der vil genåbne den</a>, %[1]s` issues.ref_reopening_from = `<a href="%[3]s">henviste til dette problem fra en pull-anmodning %[4]s, der vil genåbne den</a>, <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.ref_closed_from = `<a href="%[3]s">lukkede dette problem %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2 ]s</a>` issues.ref_closed_from = `<a href="%[3]s">lukkede dette problem %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2 ]s</a>`
issues.ref_reopened_from = `<a href="%[3]s">genåbnede dette problem %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2 ]s</a>` issues.ref_reopened_from = `<a href="%[3]s">genåbnede dette problem %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2 ]s</a>`
issues.ref_from = `fra %[1]s` issues.ref_from = `fra %[1]s`
issues.author = Forfatter issues.author = Forfatter
issues.commit_ref_at = `henviste til dette problem fra en commit %s` issues.commit_ref_at = `henviste til dette problem fra en commit <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.ref_issue_from = `<a href="%[2]s">henviste til dette problem %[3]s</a> <a id="%[1]s" href="#%[1]s">%[2 ]s</a>` issues.ref_issue_from = `<a href="%[3]s">henviste til dette problem %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2 ]s</a>`
issues.ref_pull_from = `<a href="%[2]s">henviste til denne pull-anmodning %[3]s</a> %[1]s` issues.ref_pull_from = `<a href="%[3]s">henviste til denne pull-anmodning %[4]s</a> <a id="%[1]s" href="#%[1]s">%[ 2]s</a>`
issues.ref_closing_from = `<a href="%[2]s">henviste til dette problem fra en pull-anmodning %[3]s, der vil lukke det</a>, %[1]s` issues.ref_closing_from = `<a href="%[3]s">henviste til dette problem fra en pull-anmodning %[4]s, der vil lukke det</a>, <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.author.tooltip.issue = Denne bruger er forfatteren til dette problem. issues.author.tooltip.issue = Denne bruger er forfatteren til dette problem.
issues.author.tooltip.pr = Denne bruger er forfatteren af denne pull-anmodning. issues.author.tooltip.pr = Denne bruger er forfatteren af denne pull-anmodning.
issues.role.owner = Ejer issues.role.owner = Ejer
@ -1564,8 +1564,8 @@ issues.reaction.alt_add = Tilføj %[1]s reaktion til kommentar.
issues.context.menu = Kommentar menu issues.context.menu = Kommentar menu
issues.reopen_comment_issue = Genåbner med kommentar issues.reopen_comment_issue = Genåbner med kommentar
issues.create_comment = Kommentar issues.create_comment = Kommentar
issues.closed_at = `lukkede dette problem %s` issues.closed_at = `lukkede dette problem <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.reopened_at = `genåbnede dette problem %s` issues.reopened_at = `genåbnede dette problem <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.remove_label = fjernede %s etiketten %s issues.remove_label = fjernede %s etiketten %s
issues.remove_labels = fjernede %s etiketterne %s issues.remove_labels = fjernede %s etiketterne %s
issues.change_project_at = `modificerede projektet fra <b>%s</b> til <b>%s</b> %s` issues.change_project_at = `modificerede projektet fra <b>%s</b> til <b>%s</b> %s`
@ -1911,10 +1911,10 @@ pulls.editable_explanation = Denne pull-anmodning tillader redigeringer fra vedl
pulls.auto_merge_button_when_succeed = (Når kontroller lykkes) pulls.auto_merge_button_when_succeed = (Når kontroller lykkes)
pulls.status_checks_requested = Påkrævet pulls.status_checks_requested = Påkrævet
pulls.close = Luk pull anmodning pulls.close = Luk pull anmodning
pulls.commit_ref_at = `henviste til denne pull-anmodning fra en commit %s` pulls.commit_ref_at = `henviste til denne pull-anmodning fra en commit <a id="%[1]s" href="#%[1]s">%[2]s</a>`
pulls.cmd_instruction_hint = Se instruktionerne på kommandolinjen pulls.cmd_instruction_hint = Se instruktionerne på kommandolinjen
pulls.reopened_at = `genåbnede denne pull-anmodning %s` pulls.reopened_at = `genåbnede denne pull-anmodning <a id="%[1]s" href="#%[1]s">%[2]s</a>`
pulls.closed_at = `lukkede denne pull-anmodning %s` pulls.closed_at = `lukkede denne pull-anmodning <a id="%[1]s" href="#%[1]s">%[2]s</a>`
pulls.cmd_instruction_checkout_desc = Fra dit projektdepot, tjek en ny gren og test ændringerne. pulls.cmd_instruction_checkout_desc = Fra dit projektdepot, tjek en ny gren og test ændringerne.
pulls.editable = Redigerbar pulls.editable = Redigerbar
pulls.made_using_agit = AGit pulls.made_using_agit = AGit

View file

@ -1577,7 +1577,7 @@ issues.remove_ref_at=`hat die Referenz <b>%s</b> %s entfernt`
issues.add_ref_at=`hat die Referenz <b>%s</b> %s hinzugefügt` issues.add_ref_at=`hat die Referenz <b>%s</b> %s hinzugefügt`
issues.delete_branch_at=`löschte den Branch <b>%s</b> %s` issues.delete_branch_at=`löschte den Branch <b>%s</b> %s`
issues.filter_label=Label issues.filter_label=Label
issues.filter_label_exclude=`Verwende <kbd>Alt</kbd> + <kbd>Klick/Enter</kbd>, um Labels auszuschließen` issues.filter_label_exclude=`<code>Alt</code> + <code>Klick/Enter</code> verwenden, um Labels auszuschließen`
issues.filter_label_no_select=Alle Labels issues.filter_label_no_select=Alle Labels
issues.filter_label_select_no_label=Kein Label issues.filter_label_select_no_label=Kein Label
issues.filter_milestone=Meilenstein issues.filter_milestone=Meilenstein
@ -1651,13 +1651,13 @@ issues.close_comment_issue=Mit Kommentar schließen
issues.reopen_issue=Wieder öffnen issues.reopen_issue=Wieder öffnen
issues.reopen_comment_issue=Mit Kommentar wieder öffnen issues.reopen_comment_issue=Mit Kommentar wieder öffnen
issues.create_comment=Kommentieren issues.create_comment=Kommentieren
issues.closed_at=`hat dieses Issue %s geschlossen` issues.closed_at=`hat diesen Issue <a id="%[1]s" href="#%[1]s">%[2]s</a> geschlossen`
issues.reopened_at=`hat dieses Issue %s wieder geöffnet` issues.reopened_at=`hat dieses Issue <a id="%[1]s" href="#%[1]s">%[2]s</a> wieder geöffnet`
issues.commit_ref_at=`hat dieses Issue %s aus einem Commit referenziert` issues.commit_ref_at=`hat dieses Issue <a id="%[1]s" href="#%[1]s">%[2]s</a> aus einem Commit referenziert`
issues.ref_issue_from=`<a href="%[2]s">hat</a> %[1]s <a href="%[2]s">auf dieses Issue verwiesen %[3]s</a>` issues.ref_issue_from=`<a href="%[3]s">hat</a> <a id="%[1]s" href="#%[1]s">%[2]s</a> <a href="%[3]s">auf dieses Issue verwiesen %[4]s</a>`
issues.ref_pull_from=`<a href="%[2]s">referenzierte diesen Pull-Request %[3]s</a> %[1]s` issues.ref_pull_from=`<a href="%[3]s">hat</a> <a id="%[1]s" href="#%[1]s">%[2]s</a> <a href="%[3]s">auf diesen Pull-Request verwiesen %[4]s</a>`
issues.ref_closing_from=`<a href="%[2]s">referenzierte dieses Issue aus einem Pull-Request %[3]s der es schließen wird</a>, %[1]s` issues.ref_closing_from=`<a href="%[3]s">hat</a> <a id="%[1]s" href="#%[1]s">%[2]s</a> <a href="%[3]s">in einem Pull-Request %[4]s auf dieses Issue verwiesen, welcher es schließen wird</a>`
issues.ref_reopening_from=`<a href="%[2]s">referenzierte dieses Issue aus einem Pull-Request %[3]s der es wieder öffnen wird</a>, %[1]s` issues.ref_reopening_from=`<a href="%[3]s">hat</a> <a id="%[1]s" href="#%[1]s">%[2]s</a> <a href="%[3]s"> in einem Pull-Request %[4]s auf dieses Issue verwiesen, welcher es erneut öffnen wird</a>`
issues.ref_closed_from=`<a href="%[3]s">hat dieses Issue %[4]s geschlossen</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.ref_closed_from=`<a href="%[3]s">hat dieses Issue %[4]s geschlossen</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.ref_reopened_from=`<a href="%[3]s">hat dieses Issue %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a> wieder geöffnet` issues.ref_reopened_from=`<a href="%[3]s">hat dieses Issue %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a> wieder geöffnet`
issues.ref_from=`von %[1]s` issues.ref_from=`von %[1]s`
@ -1962,8 +1962,8 @@ pulls.update_branch_success=Branch-Aktualisierung erfolgreich
pulls.update_not_allowed=Du hast keine Berechtigung, den Branch zu updaten pulls.update_not_allowed=Du hast keine Berechtigung, den Branch zu updaten
pulls.outdated_with_base_branch=Dieser Branch enthält nicht die neusten Commits des Basis-Branches pulls.outdated_with_base_branch=Dieser Branch enthält nicht die neusten Commits des Basis-Branches
pulls.close=Pull-Request schließen pulls.close=Pull-Request schließen
pulls.closed_at=`hat diesen Pull-Request %s geschlossen` pulls.closed_at=`hat diesen Pull-Request <a id="%[1]s" href="#%[1]s">%[2]s</a> geschlossen`
pulls.reopened_at=`hat diesen Pull-Request %s wieder geöffnet` pulls.reopened_at=`hat diesen Pull-Request <a id="%[1]s" href="#%[1]s">%[2]s</a> wieder geöffnet`
pulls.clear_merge_message=Merge-Nachricht löschen pulls.clear_merge_message=Merge-Nachricht löschen
pulls.clear_merge_message_hint=Das Löschen der Merge-Nachricht wird nur den Inhalt der Commit-Nachricht entfernen und generierte Git-Trailer wie „Co-Authored-By …“ erhalten. pulls.clear_merge_message_hint=Das Löschen der Merge-Nachricht wird nur den Inhalt der Commit-Nachricht entfernen und generierte Git-Trailer wie „Co-Authored-By …“ erhalten.
@ -2767,7 +2767,7 @@ settings.wiki_globally_editable = Allen erlauben, das Wiki zu bearbeiten
settings.protect_branch_name_pattern_desc = Geschützte Branch-Namens-Patterns. Siehe <a href="%s">die Dokumentation</a> für Pattern-Syntax. Beispiele: main, release/** settings.protect_branch_name_pattern_desc = Geschützte Branch-Namens-Patterns. Siehe <a href="%s">die Dokumentation</a> für Pattern-Syntax. Beispiele: main, release/**
settings.ignore_stale_approvals = Abgestandene Genehmigungen ignorieren settings.ignore_stale_approvals = Abgestandene Genehmigungen ignorieren
settings.ignore_stale_approvals_desc = Genehmigungen, welche für ältere Commits gemacht wurden (abgestandene Reviews), nicht in die Gesamtzahl der Genehmigung des PRs mitzählen. Irrelevant, falls abgestandene Reviews bereits verworfen werden. settings.ignore_stale_approvals_desc = Genehmigungen, welche für ältere Commits gemacht wurden (abgestandene Reviews), nicht in die Gesamtzahl der Genehmigung des PRs mitzählen. Irrelevant, falls abgestandene Reviews bereits verworfen werden.
pulls.commit_ref_at = `referenzierte diesen Pull-Request aus einem Commit %s` pulls.commit_ref_at = `hat sich auf diesen Pull-Request von einem Commit <a id="%[1]s" href="#%[1]s">%[2]s</a> bezogen`
pulls.fast_forward_only_merge_pull_request = Nur Fast-forward pulls.fast_forward_only_merge_pull_request = Nur Fast-forward
pulls.cmd_instruction_checkout_desc = Checke einen neuen Branch aus deinem Projekt-Repository aus und teste die Änderungen. pulls.cmd_instruction_checkout_desc = Checke einen neuen Branch aus deinem Projekt-Repository aus und teste die Änderungen.
pulls.cmd_instruction_merge_title = Zusammenführen pulls.cmd_instruction_merge_title = Zusammenführen
@ -3061,8 +3061,8 @@ teams.invite.by=Von %s eingeladen
teams.invite.description=Bitte klicke auf die folgende Schaltfläche, um dem Team beizutreten. teams.invite.description=Bitte klicke auf die folgende Schaltfläche, um dem Team beizutreten.
follow_blocked_user = Du kannst dieser Organisation nicht folgen, weil diese Organisation dich blockiert hat. follow_blocked_user = Du kannst dieser Organisation nicht folgen, weil diese Organisation dich blockiert hat.
open_dashboard = Übersicht öffnen open_dashboard = Übersicht öffnen
settings.change_orgname_redirect_prompt.with_cooldown.one = Der alte Organisationsname ist nach einer Schutzzeit von einem Tag wieder für alle verfügbar. Du kannst den alten Namen während dieser Schutzzeit erneut beanspruchen. settings.change_orgname_redirect_prompt.with_cooldown.one = Der alte Organisationsname ist nach einer Abkühldauer von einem Tag wieder für alle verfügbar. Du kannst den alten Namen während dieser Abkühldauer erneut beanspruchen.
settings.change_orgname_redirect_prompt.with_cooldown.few = Der alte Organisationsname ist nach einer Schutzzeit von %[1]d Tagen wieder für alle verfügbar. Du kannst den alten Namen während dieser Schutzzeit erneut beanspruchen. settings.change_orgname_redirect_prompt.with_cooldown.few = Der alte Organisationsname ist nach einer Abkühldauer von %[1]d Tagen wieder für alle verfügbar. Du kannst den alten Namen während dieser Abkühldauer erneut beanspruchen.
[admin] [admin]
dashboard=Übersicht dashboard=Übersicht

Some files were not shown because too many files have changed in this diff Show more