mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-07-03 23:47:25 +00:00
Compare commits
66 commits
v13.0.0-de
...
forgejo
Author | SHA1 | Date | |
---|---|---|---|
|
72620db8df | ||
|
b669564f39 | ||
|
aca7e8a9af | ||
|
b580c830e0 | ||
|
4935e6e1a3 | ||
|
bc2e4942fc | ||
|
8cc2086402 | ||
|
c0eeb75322 | ||
|
bcde3aea4f | ||
|
abb95c8c92 | ||
|
0ecd9d9682 | ||
|
1ed750a33a | ||
|
6f501b1fdf | ||
|
7a8ff20bf3 | ||
|
8f28942233 | ||
|
2b5123a90f | ||
|
0730e5481f | ||
|
2f0a993a33 | ||
|
0ecb25fdcb | ||
|
6e58d285c7 | ||
|
6e66380408 | ||
|
06bff3bb7e | ||
|
a943271205 | ||
|
4927d4ee3d | ||
|
c57dea336c | ||
|
31fc02332a | ||
|
878ce241a4 | ||
|
447c5789bd | ||
|
920f6d24d2 | ||
|
2160741221 | ||
|
3feceb10c7 | ||
|
7a881e2f26 | ||
|
ad1adabcbb | ||
|
33217a3633 | ||
|
84ed8aa740 | ||
|
ba37b69252 | ||
|
b6c6981c30 | ||
|
14309837d4 | ||
|
b5e608f3e2 | ||
|
66e0988a43 | ||
|
b8e66a5552 | ||
|
a300c0b9fd | ||
|
d6e4342353 | ||
|
225a0f7026 | ||
|
6b27fa66b9 | ||
|
69d374435b | ||
|
c085d6c9ac | ||
|
3fb6e17105 | ||
|
aee161ff25 | ||
|
a2e7446fe7 | ||
|
7ad20a2730 | ||
|
184e068f37 | ||
|
414199fc66 | ||
|
aee5e1fb94 | ||
|
d3c712fe2a | ||
|
4a1f4acf76 | ||
|
30bfa13308 | ||
|
507a12bf82 | ||
|
69bd7a1f1b | ||
|
7ab27a7a7f | ||
|
2bca029f6f | ||
|
8844b6b8e5 | ||
|
6ed62c14d3 | ||
|
744363597d | ||
|
7a6b5b6dd9 | ||
|
43fb63a063 |
261 changed files with 4702 additions and 2686 deletions
|
@ -27,15 +27,13 @@ forgejo.org/models/db
|
|||
TruncateBeans
|
||||
InTransaction
|
||||
DumpTables
|
||||
GetTableNames
|
||||
|
||||
forgejo.org/models/dbfs
|
||||
file.renameTo
|
||||
Create
|
||||
Rename
|
||||
|
||||
forgejo.org/models/forgefed
|
||||
GetFederationHost
|
||||
|
||||
forgejo.org/models/forgejo/semver
|
||||
GetVersion
|
||||
SetVersionString
|
||||
|
@ -67,7 +65,6 @@ forgejo.org/models/user
|
|||
DeleteUserSetting
|
||||
GetFederatedUser
|
||||
GetFederatedUserByUserID
|
||||
UpdateFederatedUser
|
||||
GetFollowersForUser
|
||||
AddFollower
|
||||
RemoveFollower
|
||||
|
@ -248,6 +245,9 @@ forgejo.org/routers/web/org
|
|||
forgejo.org/services/context
|
||||
GetPrivateContext
|
||||
|
||||
forgejo.org/services/federation
|
||||
Init
|
||||
|
||||
forgejo.org/services/repository
|
||||
IsErrForkAlreadyExist
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"version": "22"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/git-lfs:1.2.4": {},
|
||||
"ghcr.io/devcontainers/features/git-lfs:1.2.5": {},
|
||||
"ghcr.io/warrenbuckley/codespace-features/sqlite:1": {}
|
||||
},
|
||||
"customizations": {
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
#
|
||||
# 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
|
|
@ -28,7 +28,7 @@ jobs:
|
|||
|
||||
runs-on: docker
|
||||
container:
|
||||
image: data.forgejo.org/renovate/renovate:41.1.4
|
||||
image: data.forgejo.org/renovate/renovate:41.17.2
|
||||
|
||||
steps:
|
||||
- name: Load renovate repo cache
|
||||
|
|
|
@ -33,11 +33,8 @@ jobs:
|
|||
steps:
|
||||
- uses: https://data.forgejo.org/actions/checkout@v4
|
||||
- uses: ./.forgejo/workflows-composite/setup-env
|
||||
- name: install git 2.30
|
||||
uses: ./.forgejo/workflows-composite/apt-install-from
|
||||
with:
|
||||
packages: git/bullseye git-lfs/bullseye
|
||||
release: bullseye
|
||||
- name: install git 2.34.1 and git-lfs 3.0.2
|
||||
uses: ./.forgejo/workflows-composite/install-minimum-git-version
|
||||
- uses: ./.forgejo/workflows-composite/build-backend
|
||||
- run: |
|
||||
su forgejo -c 'make test-backend test-check'
|
||||
|
@ -55,11 +52,8 @@ jobs:
|
|||
steps:
|
||||
- uses: https://data.forgejo.org/actions/checkout@v4
|
||||
- uses: ./.forgejo/workflows-composite/setup-env
|
||||
- name: install git 2.30
|
||||
uses: ./.forgejo/workflows-composite/apt-install-from
|
||||
with:
|
||||
packages: git/bullseye git-lfs/bullseye
|
||||
release: bullseye
|
||||
- name: install git 2.34.1 and git-lfs 3.0.2
|
||||
uses: ./.forgejo/workflows-composite/install-minimum-git-version
|
||||
- uses: ./.forgejo/workflows-composite/build-backend
|
||||
- run: |
|
||||
su forgejo -c 'make test-sqlite-migration test-sqlite'
|
||||
|
|
2
Makefile
2
Makefile
|
@ -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
|
||||
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
|
||||
RENOVATE_NPM_PACKAGE ?= renovate@41.1.4 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate
|
||||
RENOVATE_NPM_PACKAGE ?= renovate@41.17.2 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate
|
||||
|
||||
# https://github.com/disposable-email-domains/disposable-email-domains/commits/main/
|
||||
DISPOSABLE_EMAILS_SHA ?= 0c27e671231d27cf66370034d7f6818037416989 # renovate: ...
|
||||
|
|
14
assets/go-licenses.json
generated
14
assets/go-licenses.json
generated
|
@ -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"
|
||||
},
|
||||
{
|
||||
"name": "github.com/jaytaylor/html2text",
|
||||
"path": "github.com/jaytaylor/html2text/LICENSE",
|
||||
"name": "github.com/inbucket/html2text",
|
||||
"path": "github.com/inbucket/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"
|
||||
},
|
||||
{
|
||||
|
@ -779,6 +779,16 @@
|
|||
"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"
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"path": "github.com/olekukonko/tablewriter/LICENSE.md",
|
||||
|
|
|
@ -82,6 +82,11 @@ wiki, issues, labels, releases, release_assets, milestones, pull_requests, comme
|
|||
}
|
||||
|
||||
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)
|
||||
defer cancel()
|
||||
|
||||
|
|
14
cmd/hook.go
14
cmd/hook.go
|
@ -231,8 +231,6 @@ Forgejo or set your environment appropriately.`, "")
|
|||
}
|
||||
}
|
||||
|
||||
supportProcReceive := git.CheckGitVersionAtLeast("2.29") == nil
|
||||
|
||||
for scanner.Scan() {
|
||||
// TODO: support news feeds for wiki
|
||||
if isWiki {
|
||||
|
@ -250,10 +248,7 @@ Forgejo or set your environment appropriately.`, "")
|
|||
total++
|
||||
lastline++
|
||||
|
||||
// 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() {
|
||||
// All references should be checked because permission check was delayed.
|
||||
oldCommitIDs[count] = oldCommitID
|
||||
newCommitIDs[count] = newCommitID
|
||||
refFullNames[count] = refFullName
|
||||
|
@ -273,9 +268,6 @@ Forgejo or set your environment appropriately.`, "")
|
|||
count = 0
|
||||
lastline = 0
|
||||
}
|
||||
} else {
|
||||
fmt.Fprint(out, ".")
|
||||
}
|
||||
if lastline >= hookBatchSize {
|
||||
fmt.Fprint(out, "\n")
|
||||
lastline = 0
|
||||
|
@ -513,10 +505,6 @@ Forgejo or set your environment appropriately.`, "")
|
|||
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)
|
||||
repoUser := os.Getenv(repo_module.EnvRepoUsername)
|
||||
repoName := os.Getenv(repo_module.EnvRepoName)
|
||||
|
|
|
@ -44,6 +44,11 @@ func defaultLoggingFlags() []cli.Flag {
|
|||
Aliases: []string{"e"},
|
||||
Usage: "Matching expression for the logger",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "exclusion",
|
||||
Aliases: []string{"x"},
|
||||
Usage: "Exclusion for the logger",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "prefix",
|
||||
Aliases: []string{"p"},
|
||||
|
@ -286,6 +291,9 @@ func commonAddLogger(ctx context.Context, c *cli.Command, mode string, vals map[
|
|||
if len(c.String("expression")) > 0 {
|
||||
vals["expression"] = c.String("expression")
|
||||
}
|
||||
if len(c.String("exclusion")) > 0 {
|
||||
vals["exclusion"] = c.String("exclusion")
|
||||
}
|
||||
if len(c.String("prefix")) > 0 {
|
||||
vals["prefix"] = c.String("prefix")
|
||||
}
|
||||
|
|
|
@ -193,13 +193,11 @@ func runServ(ctx context.Context, c *cli.Command) error {
|
|||
}
|
||||
|
||||
if len(words) < 2 {
|
||||
if git.CheckGitVersionAtLeast("2.29") == nil {
|
||||
// for AGit Flow
|
||||
if cmd == "ssh_info" {
|
||||
fmt.Print(`{"type":"agit","version":1}`)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fail(ctx, "Too few arguments", "Too few arguments in cmd: %s", cmd)
|
||||
}
|
||||
|
||||
|
|
|
@ -631,6 +631,7 @@ LEVEL = Info
|
|||
;LEVEL=
|
||||
;FLAGS = stdflags or journald
|
||||
;EXPRESSION =
|
||||
;EXCLUSION =
|
||||
;PREFIX =
|
||||
;COLORIZE = false
|
||||
;;
|
||||
|
|
20
go.mod
20
go.mod
|
@ -24,7 +24,7 @@ require (
|
|||
github.com/ProtonMail/go-crypto v1.3.0
|
||||
github.com/PuerkitoBio/goquery v1.10.3
|
||||
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.7.2
|
||||
github.com/alecthomas/chroma/v2 v2.18.0
|
||||
github.com/alecthomas/chroma/v2 v2.19.0
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
|
||||
github.com/blevesearch/bleve/v2 v2.5.2
|
||||
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/jsonld v0.0.0-20221030091449-f2a191312c73
|
||||
github.com/go-chi/chi/v5 v5.2.2
|
||||
github.com/go-chi/cors v1.2.1
|
||||
github.com/go-chi/cors v1.2.2
|
||||
github.com/go-co-op/gocron v1.37.0
|
||||
github.com/go-enry/go-enry/v2 v2.9.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/freetype v0.0.0-20170609003504-e2365dfdc4a0
|
||||
github.com/google/go-github/v64 v64.0.0
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e
|
||||
github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/feeds v1.2.0
|
||||
github.com/gorilla/sessions v1.4.0
|
||||
github.com/hashicorp/go-version v1.7.0
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7
|
||||
github.com/huandu/xstrings v1.5.0
|
||||
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056
|
||||
github.com/jhillyerd/enmime/v2 v2.1.0
|
||||
github.com/inbucket/html2text v0.9.0
|
||||
github.com/jhillyerd/enmime/v2 v2.2.0
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/klauspost/compress v1.18.0
|
||||
|
@ -79,7 +79,7 @@ require (
|
|||
github.com/minio/minio-go/v7 v7.0.94
|
||||
github.com/msteinert/pam/v2 v2.1.0
|
||||
github.com/nektos/act v0.2.52
|
||||
github.com/niklasfasching/go-org v1.8.0
|
||||
github.com/niklasfasching/go-org v1.9.0
|
||||
github.com/olivere/elastic/v7 v7.0.32
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/opencontainers/image-spec v1.1.1
|
||||
|
@ -158,7 +158,7 @@ require (
|
|||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/fatih/color v1.16.0 // indirect
|
||||
github.com/fatih/color v1.18.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-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/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/markbates/going v1.0.3 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mholt/acmez/v3 v3.1.2 // indirect
|
||||
github.com/miekg/dns v1.1.63 // indirect
|
||||
|
@ -205,7 +205,9 @@ require (
|
|||
github.com/mschoch/smat v0.2.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/nwaples/rardecode v1.1.3 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/olekukonko/errors v1.1.0 // 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/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||
|
|
43
go.sum
43
go.sum
|
@ -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/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.18.0 h1:6h53Q4hW83SuF+jcsp7CVhLsMozzvQvO8HBbKQW+gn4=
|
||||
github.com/alecthomas/chroma/v2 v2.18.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
|
||||
github.com/alecthomas/chroma/v2 v2.19.0 h1:Im+SLRgT8maArxv81mULDWN8oKxkzboH07CHesxElq4=
|
||||
github.com/alecthomas/chroma/v2 v2.19.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.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||
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/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
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/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.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
|
||||
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
|
||||
github.com/go-chi/cors v1.2.2/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/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY=
|
||||
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/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-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ=
|
||||
github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
|
||||
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.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/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
|
||||
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA=
|
||||
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
|
||||
github.com/inbucket/html2text v0.9.0 h1:ULJmVcBEMAcmLE+/rN815KG1Fx6+a4HhbUxiDiN+qks=
|
||||
github.com/inbucket/html2text v0.9.0/go.mod h1:QDaumzl+/OzlSVbNohhmg+yAy5pKjUjzCKW2BMvztKE=
|
||||
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/jhillyerd/enmime/v2 v2.1.0 h1:c8Qwi5Xq5EdtMN6byQWoZ/8I2RMTo6OJ7Xay+s1oPO0=
|
||||
github.com/jhillyerd/enmime/v2 v2.1.0/go.mod h1:EJ74dcRbBcqHSP2TBu08XRoy6y3Yx0cevwb1YkGMEmQ=
|
||||
github.com/jhillyerd/enmime/v2 v2.2.0 h1:Pe35MB96eZK5Q0XjlvPftOgWypQpd1gcbfJKAt7rsB8=
|
||||
github.com/jhillyerd/enmime/v2 v2.2.0/go.mod h1:SOBXlCemjhiV2DvHhAKnJiWrtJGS/Ffuw4Iy7NjBTaI=
|
||||
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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
|
@ -389,12 +389,10 @@ 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/goth v1.80.0 h1:NnvatczZDzOs1hn9Ug+dVYf2Viwwkp/ZDX5K+GLjan8=
|
||||
github.com/markbates/goth v1.80.0/go.mod h1:4/GYHo+W6NWisrMPZnq0Yr2Q70UntNLn7KXEFhrIdAY=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
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-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
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-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/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
|
||||
|
@ -428,16 +426,20 @@ 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/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/niklasfasching/go-org v1.8.0 h1:WyGLaajLLp8JbQzkmapZ1y0MOzKuKV47HkZRloi+HGY=
|
||||
github.com/niklasfasching/go-org v1.8.0/go.mod h1:e2A9zJs7cdONrEGs3gvxCcaAEpwwPNPG7csDpXckMNg=
|
||||
github.com/niklasfasching/go-org v1.9.0 h1:4/Sr68Qx06hjC9MVDB/4etGP67JionLHGscLMOClpnk=
|
||||
github.com/niklasfasching/go-org v1.9.0/go.mod h1:ZAGFFkWvUQcpazmi/8nHqwvARpr1xpb+Es67oUGX/48=
|
||||
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/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
||||
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/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
|
||||
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||
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/go.mod h1:c7PVmLe3Fxq77PIfY/bZmxY/TAamBhCzZ8xDOE09a9k=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
|
@ -645,7 +647,6 @@ 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-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-20220811171246-fbc7d0a398ab/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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
|
|
@ -284,16 +284,10 @@ func GetLatestRun(ctx context.Context, repoID int64) (*ActionRun, error) {
|
|||
return &run, nil
|
||||
}
|
||||
|
||||
// GetRunBefore returns the last run that completed a given timestamp (not inclusive).
|
||||
func GetRunBefore(ctx context.Context, repoID int64, timestamp timeutil.TimeStamp) (*ActionRun, error) {
|
||||
var run ActionRun
|
||||
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 GetRunBefore(ctx context.Context, _ *ActionRun) (*ActionRun, error) {
|
||||
// TODO return the most recent run related to the run given in argument
|
||||
// see https://codeberg.org/forgejo/user-research/issues/63 for context
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func GetLatestRunForBranchAndWorkflow(ctx context.Context, repoID int64, branch, workflowFile, event string) (*ActionRun, error) {
|
||||
|
|
|
@ -5,92 +5,7 @@ package actions
|
|||
|
||||
import (
|
||||
"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) {
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -15,8 +15,6 @@ func TestMain(m *testing.M) {
|
|||
"gpg_key.yml",
|
||||
"public_key.yml",
|
||||
"TestParseCommitWithSSHSignature/public_key.yml",
|
||||
"deploy_key.yml",
|
||||
"gpg_key_import.yml",
|
||||
"user.yml",
|
||||
"email_address.yml",
|
||||
},
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"forgejo.org/modules/container"
|
||||
"forgejo.org/modules/log"
|
||||
"forgejo.org/modules/setting"
|
||||
|
||||
|
@ -438,3 +439,12 @@ func GetMasterEngine(x Engine) (*xorm.Engine, error) {
|
|||
|
||||
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
|
||||
}
|
||||
|
|
40
models/db/table_names_test.go
Normal file
40
models/db/table_names_test.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
// 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"
|
||||
}
|
7
models/fixtures/TestActivateUserEmail/email_address.yml
Normal file
7
models/fixtures/TestActivateUserEmail/email_address.yml
Normal file
|
@ -0,0 +1,7 @@
|
|||
-
|
||||
id: 1001
|
||||
uid: 1001
|
||||
email: AnotherTestUserWithUpperCaseEmail@otto.splvs.net
|
||||
lower_email: anothertestuserwithuppercaseemail@otto.splvs.net
|
||||
is_activated: false
|
||||
is_primary: true
|
12
models/fixtures/TestActivateUserEmail/user.yml
Normal file
12
models/fixtures/TestActivateUserEmail/user.yml
Normal file
|
@ -0,0 +1,12 @@
|
|||
-
|
||||
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
|
|
@ -1 +0,0 @@
|
|||
[] # empty
|
|
@ -186,10 +186,46 @@
|
|||
type: 8 # milestone
|
||||
poster_id: 1
|
||||
issue_id: 1 # in repo_id 1
|
||||
milestone_id: 10 # not exsting milestone
|
||||
milestone_id: 10 # not existing milestone
|
||||
old_milestone_id: 0
|
||||
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
|
||||
type: 30 # project
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
[] # empty
|
|
@ -1 +0,0 @@
|
|||
[] # empty
|
|
@ -1 +0,0 @@
|
|||
[] # empty
|
|
@ -1 +0,0 @@
|
|||
[] # empty
|
|
@ -1 +0,0 @@
|
|||
[] # empty
|
|
@ -1 +0,0 @@
|
|||
[] # empty
|
|
@ -1 +0,0 @@
|
|||
[] # empty
|
|
@ -1 +0,0 @@
|
|||
[] # empty
|
|
@ -1 +0,0 @@
|
|||
[] # empty
|
|
@ -1 +0,0 @@
|
|||
[] # empty
|
|
@ -1 +0,0 @@
|
|||
[] # empty
|
|
@ -1 +0,0 @@
|
|||
[] # empty
|
|
@ -108,7 +108,7 @@ var migrations = []*Migration{
|
|||
// v33 -> v34
|
||||
NewMigration("Add `notify-email` column to `action_run` table", AddNotifyEmailToActionRun),
|
||||
// v34 -> v35
|
||||
NewMigration("Add index to `stopped` column in `action_run` table", AddIndexToActionRunStopped),
|
||||
NewMigration("Noop because of https://codeberg.org/forgejo/forgejo/issues/8373", NoopAddIndexToActionRunStopped),
|
||||
}
|
||||
|
||||
// GetCurrentDBVersion returns the current Forgejo database version.
|
||||
|
|
|
@ -4,16 +4,10 @@
|
|||
package forgejo_migrations //nolint:revive
|
||||
|
||||
import (
|
||||
"forgejo.org/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func AddIndexToActionRunStopped(x *xorm.Engine) error {
|
||||
type ActionRun struct {
|
||||
ID int64
|
||||
Stopped timeutil.TimeStamp `xorm:"index"`
|
||||
}
|
||||
|
||||
return x.Sync(&ActionRun{})
|
||||
// see https://codeberg.org/forgejo/forgejo/issues/8373
|
||||
func NoopAddIndexToActionRunStopped(x *xorm.Engine) error {
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -101,7 +101,7 @@ func (comments CommentList) loadMilestones(ctx context.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs))
|
||||
milestones := make(map[int64]*Milestone, len(milestoneIDs))
|
||||
left := len(milestoneIDs)
|
||||
for left > 0 {
|
||||
limit := db.DefaultMaxInSize
|
||||
|
@ -110,7 +110,7 @@ func (comments CommentList) loadMilestones(ctx context.Context) error {
|
|||
}
|
||||
err := db.GetEngine(ctx).
|
||||
In("id", milestoneIDs[:limit]).
|
||||
Find(&milestoneMaps)
|
||||
Find(&milestones)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -118,8 +118,8 @@ func (comments CommentList) loadMilestones(ctx context.Context) error {
|
|||
milestoneIDs = milestoneIDs[limit:]
|
||||
}
|
||||
|
||||
for _, issue := range comments {
|
||||
issue.Milestone = milestoneMaps[issue.MilestoneID]
|
||||
for _, comment := range comments {
|
||||
comment.Milestone = milestones[comment.MilestoneID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -140,7 +140,7 @@ func (comments CommentList) loadOldMilestones(ctx context.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs))
|
||||
milestones := make(map[int64]*Milestone, len(milestoneIDs))
|
||||
left := len(milestoneIDs)
|
||||
for left > 0 {
|
||||
limit := db.DefaultMaxInSize
|
||||
|
@ -149,7 +149,7 @@ func (comments CommentList) loadOldMilestones(ctx context.Context) error {
|
|||
}
|
||||
err := db.GetEngine(ctx).
|
||||
In("id", milestoneIDs[:limit]).
|
||||
Find(&milestoneMaps)
|
||||
Find(&milestones)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -157,8 +157,8 @@ func (comments CommentList) loadOldMilestones(ctx context.Context) error {
|
|||
milestoneIDs = milestoneIDs[limit:]
|
||||
}
|
||||
|
||||
for _, issue := range comments {
|
||||
issue.OldMilestone = milestoneMaps[issue.MilestoneID]
|
||||
for _, comment := range comments {
|
||||
comment.OldMilestone = milestones[comment.OldMilestoneID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -48,6 +48,8 @@ type IssuesOptions struct { //nolint
|
|||
UpdatedBeforeUnix int64
|
||||
// prioritize issues from this repo
|
||||
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]
|
||||
|
||||
// If combined with AllPublic, then private as well as public issues
|
||||
|
@ -60,7 +62,7 @@ type IssuesOptions struct { //nolint
|
|||
|
||||
// applySorts sort an issues-related session based on the provided
|
||||
// sortType string
|
||||
func applySorts(sess *xorm.Session, sortType string, priorityRepoID int64) {
|
||||
func applySorts(sess *xorm.Session, sortType string, priorityRepoID, priorityIssueIndex int64) {
|
||||
switch sortType {
|
||||
case "oldest":
|
||||
sess.Asc("issue.created_unix").Asc("issue.id")
|
||||
|
@ -97,8 +99,11 @@ func applySorts(sess *xorm.Session, sortType string, priorityRepoID int64) {
|
|||
case "priorityrepo":
|
||||
sess.OrderBy("CASE "+
|
||||
"WHEN issue.repo_id = ? THEN 1 "+
|
||||
"ELSE 2 END ASC", priorityRepoID).
|
||||
Desc("issue.created_unix").
|
||||
"ELSE 2 END ASC", priorityRepoID)
|
||||
if priorityIssueIndex != 0 {
|
||||
sess.OrderBy("issue.index = ? DESC", priorityIssueIndex)
|
||||
}
|
||||
sess.Desc("issue.created_unix").
|
||||
Desc("issue.id")
|
||||
case "project-column-sorting":
|
||||
sess.Asc("project_issue.sorting").Desc("issue.created_unix").Desc("issue.id")
|
||||
|
@ -470,7 +475,7 @@ func Issues(ctx context.Context, opts *IssuesOptions) (IssueList, error) {
|
|||
Join("INNER", "repository", "`issue`.repo_id = `repository`.id")
|
||||
applyLimit(sess, opts)
|
||||
applyConditions(sess, opts)
|
||||
applySorts(sess, opts.SortType, opts.PriorityRepoID)
|
||||
applySorts(sess, opts.SortType, opts.PriorityRepoID, opts.PriorityIssueIndex)
|
||||
|
||||
issues := IssueList{}
|
||||
if err := sess.Find(&issues); err != nil {
|
||||
|
@ -494,7 +499,7 @@ func IssueIDs(ctx context.Context, opts *IssuesOptions, otherConds ...builder.Co
|
|||
}
|
||||
|
||||
applyLimit(sess, opts)
|
||||
applySorts(sess, opts.SortType, opts.PriorityRepoID)
|
||||
applySorts(sess, opts.SortType, opts.PriorityRepoID, opts.PriorityIssueIndex)
|
||||
|
||||
var res []int64
|
||||
total, err := sess.Select("`issue`.id").Table(&Issue{}).FindAndCount(&res)
|
||||
|
|
|
@ -149,7 +149,7 @@ func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptio
|
|||
}
|
||||
|
||||
findSession := listPullRequestStatement(ctx, baseRepoID, opts)
|
||||
applySorts(findSession, opts.SortType, 0)
|
||||
applySorts(findSession, opts.SortType, 0, 0)
|
||||
findSession = db.SetSessionPagination(findSession, opts)
|
||||
prs := make([]*PullRequest, 0, opts.PageSize)
|
||||
found := findSession.Find(&prs)
|
||||
|
|
|
@ -96,6 +96,7 @@ func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, fu
|
|||
if err := unittest.InitFixtures(
|
||||
unittest.FixturesOptions{
|
||||
Dir: fixturesDir,
|
||||
SkipCleanRegistedModels: true,
|
||||
}, x); err != nil {
|
||||
t.Errorf("error whilst initializing fixtures from %s: %v", fixturesDir, err)
|
||||
return x, deferFn
|
||||
|
|
|
@ -100,7 +100,7 @@ type AbuseReport struct {
|
|||
// The abuse category selected by the reporter.
|
||||
Category AbuseCategoryType `xorm:"INDEX NOT NULL"`
|
||||
// Remarks provided by the reporter.
|
||||
Remarks string
|
||||
Remarks string `xorm:"VARCHAR(500)"`
|
||||
// The ID of the corresponding shadow-copied content when exists; otherwise null.
|
||||
ShadowCopyID sql.NullInt64 `xorm:"DEFAULT NULL"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
|
||||
|
|
|
@ -17,7 +17,7 @@ import (
|
|||
|
||||
type AbuseReportShadowCopy struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RawValue string `xorm:"NOT NULL"`
|
||||
RawValue string `xorm:"LONGTEXT NOT NULL"` // A JSON with relevant fields from user, repository, issue or comment table.
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,8 @@ import (
|
|||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"forgejo.org/modules/container"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
|
@ -32,13 +34,15 @@ type loader struct {
|
|||
fixtureFiles []*fixtureFile
|
||||
}
|
||||
|
||||
func newFixtureLoader(db *sql.DB, dialect string, fixturePaths []string) (*loader, error) {
|
||||
func newFixtureLoader(db *sql.DB, dialect string, fixturePaths []string, allTableNames container.Set[string]) (*loader, error) {
|
||||
l := &loader{
|
||||
db: db,
|
||||
dialect: dialect,
|
||||
fixtureFiles: []*fixtureFile{},
|
||||
}
|
||||
|
||||
tablesWithoutFixture := allTableNames
|
||||
|
||||
// Load fixtures
|
||||
for _, fixturePath := range fixturePaths {
|
||||
stat, err := os.Stat(fixturePath)
|
||||
|
@ -60,6 +64,7 @@ func newFixtureLoader(db *sql.DB, dialect string, fixturePaths []string) (*loade
|
|||
return nil, err
|
||||
}
|
||||
l.fixtureFiles = append(l.fixtureFiles, fixtureFile)
|
||||
tablesWithoutFixture.Remove(fixtureFile.name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -71,6 +76,14 @@ func newFixtureLoader(db *sql.DB, dialect string, fixturePaths []string) (*loade
|
|||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
|
@ -178,13 +191,13 @@ func (l *loader) Load() error {
|
|||
}()
|
||||
|
||||
// Clean the table and re-insert the fixtures.
|
||||
tableDeleted := map[string]struct{}{}
|
||||
tableDeleted := make(container.Set[string])
|
||||
for _, fixture := range l.fixtureFiles {
|
||||
if _, ok := tableDeleted[fixture.name]; !ok {
|
||||
if !tableDeleted.Contains(fixture.name) {
|
||||
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)
|
||||
}
|
||||
tableDeleted[fixture.name] = struct{}{}
|
||||
tableDeleted.Add(fixture.name)
|
||||
}
|
||||
|
||||
for _, insertSQL := range fixture.insertSQLs {
|
||||
|
|
|
@ -7,10 +7,12 @@ package unittest
|
|||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"forgejo.org/models/db"
|
||||
"forgejo.org/modules/auth/password/hash"
|
||||
"forgejo.org/modules/container"
|
||||
"forgejo.org/modules/setting"
|
||||
|
||||
"xorm.io/xorm"
|
||||
|
@ -44,6 +46,8 @@ func OverrideFixtures(dir string) func() {
|
|||
}
|
||||
}
|
||||
|
||||
var allTableNames = sync.OnceValue(db.GetTableNames)
|
||||
|
||||
// InitFixtures initialize test fixtures for a test database
|
||||
func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) {
|
||||
e, err := GetXORMEngine(engine...)
|
||||
|
@ -75,7 +79,12 @@ func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) {
|
|||
panic("Unsupported RDBMS for test")
|
||||
}
|
||||
|
||||
fixturesLoader, err = newFixtureLoader(e.DB().DB, dialect, fixturePaths)
|
||||
var allTables container.Set[string]
|
||||
if !opts.SkipCleanRegistedModels {
|
||||
allTables = allTableNames().Clone()
|
||||
}
|
||||
|
||||
fixturesLoader, err = newFixtureLoader(e.DB().DB, dialect, fixturePaths, allTables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -217,6 +217,10 @@ type FixturesOptions struct {
|
|||
Files []string
|
||||
Dirs []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
|
||||
|
|
|
@ -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)))
|
||||
}
|
||||
|
||||
// APActorKeyID returns the ID of the user's public key
|
||||
func (u *User) APActorKeyID() string {
|
||||
// KeyID returns the ID of the user's public key
|
||||
func (u *User) KeyID() string {
|
||||
return u.APActorID() + "#main-key"
|
||||
}
|
||||
|
|
|
@ -181,3 +181,20 @@ func TestDeletePrimaryEmailAddressOfUser(t *testing.T) {
|
|||
assert.True(t, user_model.IsErrEmailAddressNotExist(err))
|
||||
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")
|
||||
})
|
||||
}
|
||||
|
|
|
@ -182,11 +182,11 @@ func (u *User) BeforeUpdate() {
|
|||
u.MaxRepoCreation = -1
|
||||
}
|
||||
|
||||
// Organization does not need email
|
||||
u.Email = strings.ToLower(u.Email)
|
||||
// Ensure AvatarEmail is set for non-organization users, because organization
|
||||
// are not required to have a email set.
|
||||
if !u.IsOrganization() {
|
||||
if len(u.AvatarEmail) == 0 {
|
||||
u.AvatarEmail = u.Email
|
||||
u.AvatarEmail = strings.ToLower(u.Email)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -57,14 +57,6 @@ func CreateFederatedUser(ctx context.Context, user *User, federatedUser *Federat
|
|||
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) {
|
||||
federatedUser := new(FederatedUser)
|
||||
user := new(User)
|
||||
|
@ -219,7 +211,6 @@ func RemoveFollower(ctx context.Context, followedUser *User, followingUser *Fede
|
|||
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) {
|
||||
if res, err := validation.IsValid(followedUser); !res {
|
||||
return false, err
|
||||
|
|
|
@ -150,7 +150,7 @@ func TestAPActorID_APActorID(t *testing.T) {
|
|||
|
||||
func TestKeyID(t *testing.T) {
|
||||
user := user_model.User{ID: 1}
|
||||
url := user.APActorKeyID()
|
||||
url := user.KeyID()
|
||||
expected := "https://try.gitea.io/api/v1/activitypub/user-id/1#main-key"
|
||||
assert.Equal(t, expected, url)
|
||||
}
|
||||
|
|
|
@ -89,6 +89,7 @@ func NewClientFactory() (c *ClientFactory, err error) {
|
|||
|
||||
type APClientFactory interface {
|
||||
WithKeys(ctx context.Context, user *user_model.User, pubID string) (APClient, error)
|
||||
WithKeysDirect(ctx context.Context, privateKey, pubID string) (APClient, error)
|
||||
}
|
||||
|
||||
// Client struct
|
||||
|
@ -103,12 +104,8 @@ type Client struct {
|
|||
}
|
||||
|
||||
// NewRequest function
|
||||
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
|
||||
}
|
||||
privPem, _ := pem.Decode([]byte(priv))
|
||||
func (cf *ClientFactory) WithKeysDirect(ctx context.Context, privateKey, pubID string) (APClient, error) {
|
||||
privPem, _ := pem.Decode([]byte(privateKey))
|
||||
privParsed, err := x509.ParsePKCS1PrivateKey(privPem.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -126,6 +123,14 @@ func (cf *ClientFactory) WithKeys(ctx context.Context, user *user_model.User, pu
|
|||
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
|
||||
func (c *Client) newRequest(method string, b []byte, to string) (req *http.Request, err error) {
|
||||
buf := bytes.NewBuffer(b)
|
||||
|
@ -149,6 +154,7 @@ func (c *Client) Post(b []byte, to string) (resp *http.Response, err error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if c.pubID != "" {
|
||||
signer, _, err := httpsig.NewSigner(c.algs, c.digestAlg, c.postHeaders, httpsig.Signature, httpsigExpirationTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -156,6 +162,7 @@ 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 {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
resp, err = c.client.Do(req)
|
||||
return resp, err
|
||||
|
@ -167,6 +174,8 @@ func (c *Client) Get(to string) (resp *http.Response, err error) {
|
|||
if req, err = c.newRequest(http.MethodGet, nil, to); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c.pubID != "" {
|
||||
signer, _, err := httpsig.NewSigner(c.algs, c.digestAlg, c.getHeaders, httpsig.Signature, httpsigExpirationTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -174,6 +183,7 @@ func (c *Client) Get(to string) (resp *http.Response, err error) {
|
|||
if err := signer.SignRequest(c.priv, c.pubID, req, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
resp, err = c.client.Do(req)
|
||||
return resp, err
|
||||
|
|
|
@ -74,3 +74,8 @@ func (s Set[T]) Values() []T {
|
|||
func (s Set[T]) Seq() iter.Seq[T] {
|
||||
return maps.Keys(s)
|
||||
}
|
||||
|
||||
// Clone returns a identical shallow copy of this set.
|
||||
func (s Set[T]) Clone() Set[T] {
|
||||
return maps.Clone(s)
|
||||
}
|
||||
|
|
|
@ -47,4 +47,11 @@ func TestSet(t *testing.T) {
|
|||
assert.False(t, s.IsSubset([]string{"key1"}))
|
||||
|
||||
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"))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -132,7 +132,7 @@ func (r *BlameReader) Close() error {
|
|||
// 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) {
|
||||
var ignoreRevsFile *string
|
||||
if CheckGitVersionAtLeast("2.23") == nil && !bypassBlameIgnore {
|
||||
if !bypassBlameIgnore {
|
||||
ignoreRevsFile = tryCreateBlameIgnoreRevsFile(commit)
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"bufio"
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"forgejo.org/modules/log"
|
||||
|
@ -172,60 +173,43 @@ func (b *Blob) GetBlobContent(limit int64) (string, error) {
|
|||
return string(buf), err
|
||||
}
|
||||
|
||||
// GetBlobLineCount gets line count of the blob
|
||||
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'}
|
||||
|
||||
c, err := reader.Read(buf)
|
||||
if c == 0 && err == io.EOF {
|
||||
return 0, nil
|
||||
}
|
||||
for {
|
||||
count += bytes.Count(buf[:c], lineSep)
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
return count, nil
|
||||
case err != nil:
|
||||
return count, err
|
||||
}
|
||||
c, err = reader.Read(buf)
|
||||
}
|
||||
type BlobTooLargeError struct {
|
||||
Size, Limit int64
|
||||
}
|
||||
|
||||
// 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()
|
||||
func (b BlobTooLargeError) Error() string {
|
||||
return fmt.Sprintf("blob: content larger than limit (%d > %d)", b.Size, b.Limit)
|
||||
}
|
||||
|
||||
// GetContentBase64 Reads the content of the blob and returns it as base64 encoded string.
|
||||
// Returns [BlobTooLargeError] if the (unencoded) content is larger than the limit.
|
||||
func (b *Blob) GetContentBase64(limit int64) (string, error) {
|
||||
if b.Size() > limit {
|
||||
return "", BlobTooLargeError{
|
||||
Size: b.Size(),
|
||||
Limit: limit,
|
||||
}
|
||||
}
|
||||
|
||||
rc, size, err := b.NewTruncatedReader(limit)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer dataRc.Close()
|
||||
defer rc.Close()
|
||||
|
||||
pr, pw := io.Pipe()
|
||||
encoder := base64.NewEncoder(base64.StdEncoding, pw)
|
||||
encoding := base64.StdEncoding
|
||||
buf := bytes.NewBuffer(make([]byte, 0, encoding.EncodedLen(int(size))))
|
||||
|
||||
go func() {
|
||||
_, err := io.Copy(encoder, dataRc)
|
||||
_ = encoder.Close()
|
||||
encoder := base64.NewEncoder(encoding, buf)
|
||||
|
||||
if err != nil {
|
||||
_ = pw.CloseWithError(err)
|
||||
} else {
|
||||
_ = pw.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
out, err := io.ReadAll(pr)
|
||||
if err != nil {
|
||||
if _, err := io.Copy(encoder, rc); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(out), nil
|
||||
if err := encoder.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// GuessContentType guesses the content type of the blob.
|
||||
|
@ -236,7 +220,7 @@ func (b *Blob) GuessContentType() (typesniffer.SniffedType, error) {
|
|||
}
|
||||
defer r.Close()
|
||||
|
||||
return typesniffer.DetectContentTypeFromReader(r)
|
||||
return typesniffer.DetectContentTypeFromReader(r, b.Name())
|
||||
}
|
||||
|
||||
// GetBlob finds the blob object in the repository.
|
||||
|
|
|
@ -63,6 +63,24 @@ func TestBlob(t *testing.T) {
|
|||
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) {
|
||||
// read fewer than available
|
||||
rc, size, err := testBlob.NewTruncatedReader(100)
|
||||
|
|
|
@ -412,11 +412,7 @@ func (c *Commit) GetSubModule(entryname string) (string, error) {
|
|||
|
||||
// GetBranchName gets the closest branch name (as returned by 'git name-rev --name-only')
|
||||
func (c *Commit) GetBranchName() (string, error) {
|
||||
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())
|
||||
cmd := NewCommand(c.repo.Ctx, "name-rev", "--exclude", "refs/tags/*", "--name-only", "--no-undefined").AddDynamicArguments(c.ID.String())
|
||||
data, _, err := cmd.RunStdString(&RunOpts{Dir: c.repo.Path})
|
||||
if err != nil {
|
||||
// handle special case where git can not describe commit
|
||||
|
|
|
@ -23,7 +23,7 @@ import (
|
|||
)
|
||||
|
||||
// RequiredVersion is the minimum Git version required
|
||||
const RequiredVersion = "2.0.0"
|
||||
const RequiredVersion = "2.34.1"
|
||||
|
||||
var (
|
||||
// GitExecutable is the command name of git
|
||||
|
@ -33,7 +33,6 @@ var (
|
|||
// DefaultContext is the default context to run git commands in, must be initialized by git.InitXxx
|
||||
DefaultContext context.Context
|
||||
|
||||
SupportProcReceive bool // >= 2.29
|
||||
SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’
|
||||
InvertedGitFlushEnv bool // 2.43.1
|
||||
SupportCheckAttrOnBare bool // >= 2.40
|
||||
|
@ -113,7 +112,7 @@ func VersionInfo() string {
|
|||
format := "%s"
|
||||
args := []any{GitVersion.Original()}
|
||||
// Since git wire protocol has been released from git v2.18
|
||||
if setting.Git.EnableAutoGitWireProtocol && CheckGitVersionAtLeast("2.18") == nil {
|
||||
if setting.Git.EnableAutoGitWireProtocol {
|
||||
format += ", Wire Protocol %s Enabled"
|
||||
args = append(args, "Version 2") // for focus color
|
||||
}
|
||||
|
@ -172,16 +171,13 @@ func InitFull(ctx context.Context) (err error) {
|
|||
_ = os.Setenv("GNUPGHOME", filepath.Join(HomeDir(), ".gnupg"))
|
||||
}
|
||||
|
||||
// Since git wire protocol has been released from git v2.18
|
||||
if setting.Git.EnableAutoGitWireProtocol && CheckGitVersionAtLeast("2.18") == nil {
|
||||
if setting.Git.EnableAutoGitWireProtocol {
|
||||
globalCommandArgs = append(globalCommandArgs, "-c", "protocol.version=2")
|
||||
}
|
||||
|
||||
// Explicitly disable credential helper, otherwise Git credentials might leak
|
||||
if CheckGitVersionAtLeast("2.9") == nil {
|
||||
globalCommandArgs = append(globalCommandArgs, "-c", "credential.helper=")
|
||||
}
|
||||
SupportProcReceive = CheckGitVersionAtLeast("2.29") == nil
|
||||
|
||||
SupportHashSha256 = CheckGitVersionAtLeast("2.42") == nil
|
||||
SupportCheckAttrOnBare = CheckGitVersionAtLeast("2.40") == nil
|
||||
if SupportHashSha256 {
|
||||
|
@ -195,9 +191,6 @@ func InitFull(ctx context.Context) (err error) {
|
|||
SupportGrepMaxCount = CheckGitVersionAtLeast("2.38") == nil
|
||||
|
||||
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=")
|
||||
}
|
||||
|
||||
|
@ -234,18 +227,15 @@ func syncGitConfig() (err error) {
|
|||
}
|
||||
}
|
||||
|
||||
// Set git some configurations - these must be set to these values for gitea to work correctly
|
||||
// Set git some configurations - these must be set to these values for forgejo to work correctly
|
||||
if err := configSet("core.quotePath", "false"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if CheckGitVersionAtLeast("2.10") == nil {
|
||||
if err := configSet("receive.advertisePushOptions", "true"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if CheckGitVersionAtLeast("2.18") == nil {
|
||||
if err := configSet("core.commitGraph", "true"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -255,18 +245,11 @@ func syncGitConfig() (err error) {
|
|||
if err := configSet("fetch.writeCommitGraph", "true"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if SupportProcReceive {
|
||||
// set support for AGit flow
|
||||
if err := configAddNonExist("receive.procReceiveRefs", "refs/for"); err != nil {
|
||||
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
|
||||
// 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.)
|
||||
|
@ -284,11 +267,6 @@ func syncGitConfig() (err error) {
|
|||
|
||||
switch setting.Repository.Signing.Format {
|
||||
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.
|
||||
// This can be overridden in app.ini in [git.config] section, so we must
|
||||
// query this information.
|
||||
|
@ -325,8 +303,7 @@ func syncGitConfig() (err error) {
|
|||
}
|
||||
}
|
||||
|
||||
// By default partial clones are disabled, enable them from git v2.22
|
||||
if !setting.Git.DisablePartialClone && CheckGitVersionAtLeast("2.22") == nil {
|
||||
if !setting.Git.DisablePartialClone {
|
||||
if err = configSet("uploadpack.allowfilter", "true"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ import (
|
|||
"forgejo.org/modules/test"
|
||||
"forgejo.org/modules/util"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
@ -105,10 +104,6 @@ func TestSyncConfigGPGFormat(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())
|
||||
require.NoError(t, err)
|
||||
f, err := r.OpenFile("ssh-keygen", os.O_CREATE|os.O_TRUNC, 0o700)
|
||||
|
@ -121,13 +116,6 @@ func TestSyncConfigGPGFormat(t *testing.T) {
|
|||
assert.True(t, gitConfigContains("[gpg]"))
|
||||
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) {
|
||||
require.NoError(t, r.Remove("ssh-keygen"))
|
||||
require.ErrorContains(t, syncGitConfig(), "git signing requires a ssh-keygen binary")
|
||||
|
|
|
@ -16,26 +16,6 @@ import (
|
|||
"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
|
||||
func RevListObjects(ctx context.Context, revListWriter *io.PipeWriter, wg *sync.WaitGroup, tmpBasePath, headSHA, baseSHA string, errChan chan<- error) {
|
||||
defer wg.Done()
|
||||
|
|
|
@ -12,14 +12,7 @@ import (
|
|||
|
||||
// GetRemoteAddress returns remote url of git repository in the repoPath with special remote name
|
||||
func GetRemoteAddress(ctx context.Context, repoPath, remoteName string) (string, error) {
|
||||
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})
|
||||
result, _, err := NewCommand(ctx, "remote", "get-url").AddDynamicArguments(remoteName).RunStdString(&RunOpts{Dir: repoPath})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ package git
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
|
@ -197,7 +196,7 @@ func TestGitAttributeCheckerError(t *testing.T) {
|
|||
path := t.TempDir()
|
||||
|
||||
// we can't use unittest.CopyDir because of an import cycle (git.Init in unittest)
|
||||
require.NoError(t, CopyFS(path, os.DirFS(filepath.Join(testReposDir, "language_stats_repo"))))
|
||||
require.NoError(t, os.CopyFS(path, os.DirFS(filepath.Join(testReposDir, "language_stats_repo"))))
|
||||
|
||||
gitRepo, err := openRepositoryWithDefaultContext(path)
|
||||
require.NoError(t, err)
|
||||
|
@ -324,32 +323,3 @@ func TestGitAttributeCheckerError(t *testing.T) {
|
|||
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()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -443,7 +443,6 @@ func (repo *Repository) getCommitsBeforeLimit(id ObjectID, num int) ([]*Commit,
|
|||
}
|
||||
|
||||
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)
|
||||
|
||||
if limit != -1 {
|
||||
|
@ -457,29 +456,6 @@ func (repo *Repository) getBranches(commit *Commit, limit int) ([]string, error)
|
|||
|
||||
branches := strings.Fields(stdout)
|
||||
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
|
||||
|
|
|
@ -11,10 +11,8 @@ import (
|
|||
// WriteCommitGraph write commit graph to speed up repo access
|
||||
// this requires git v2.18 to be installed
|
||||
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 {
|
||||
return fmt.Errorf("unable to write commit-graph for '%s' : %w", repoPath, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -116,32 +116,37 @@ 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
|
||||
func (te *TreeEntry) FollowLink() (*TreeEntry, string, error) {
|
||||
if !te.IsLink() {
|
||||
return nil, "", ErrBadLink{te.Name(), "not a symlink"}
|
||||
}
|
||||
|
||||
// read the link
|
||||
r, err := te.Blob().DataAsync()
|
||||
lnk, err := te.LinkTarget()
|
||||
if err != nil {
|
||||
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
|
||||
|
||||
// traverse up directories
|
||||
|
|
|
@ -99,7 +99,7 @@ func setServeHeadersByFile(r *http.Request, w http.ResponseWriter, filePath stri
|
|||
Filename: path.Base(filePath),
|
||||
}
|
||||
|
||||
sniffedType := typesniffer.DetectContentType(mineBuf)
|
||||
sniffedType := typesniffer.DetectContentType(mineBuf, opts.Filename)
|
||||
|
||||
// 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") != ""
|
||||
|
|
|
@ -177,7 +177,7 @@ func (b *Indexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserErro
|
|||
fileContents, err := io.ReadAll(io.LimitReader(batchReader, size))
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !typesniffer.DetectContentType(fileContents).IsText() {
|
||||
} else if !typesniffer.DetectContentType(fileContents, update.Filename).IsText() {
|
||||
// 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.
|
||||
fileContents = nil
|
||||
|
|
|
@ -144,7 +144,7 @@ func (b *Indexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserErro
|
|||
fileContents, err := io.ReadAll(io.LimitReader(batchReader, size))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !typesniffer.DetectContentType(fileContents).IsText() {
|
||||
} else if !typesniffer.DetectContentType(fileContents, update.Filename).IsText() {
|
||||
// FIXME: UTF-16 files will probably fail here
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
@ -156,11 +156,12 @@ func (b *Indexer) Delete(_ context.Context, ids ...int64) error {
|
|||
func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (*internal.SearchResult, error) {
|
||||
var queries []query.Query
|
||||
|
||||
if options.Keyword != "" {
|
||||
tokens, err := options.Tokens()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(tokens) > 0 {
|
||||
q := bleve.NewBooleanQuery()
|
||||
for _, token := range tokens {
|
||||
innerQ := bleve.NewDisjunctionQuery(
|
||||
|
@ -170,7 +171,7 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
|
|||
|
||||
if issueID, err := token.ParseIssueReference(); err == nil {
|
||||
idQuery := inner_bleve.NumericEqualityQuery(issueID, "index")
|
||||
idQuery.SetBoost(5.0)
|
||||
idQuery.SetBoost(20.0)
|
||||
innerQ.AddQuery(idQuery)
|
||||
}
|
||||
|
||||
|
@ -197,6 +198,15 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
|
|||
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() {
|
||||
queries = append(queries, inner_bleve.BoolFieldQuery(options.IsPull.Value(), "is_pull"))
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@ func (i *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
|
|||
|
||||
cond := builder.NewCond()
|
||||
|
||||
var priorityIssueIndex int64
|
||||
if options.Keyword != "" {
|
||||
repoCond := builder.In("repo_id", options.RepoIDs)
|
||||
if len(options.RepoIDs) == 1 {
|
||||
|
@ -82,6 +83,7 @@ func (i *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
|
|||
builder.Eq{"`index`": issueID},
|
||||
cond,
|
||||
)
|
||||
priorityIssueIndex = issueID
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -89,6 +91,7 @@ func (i *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opt.PriorityIssueIndex = priorityIssueIndex
|
||||
|
||||
// If pagesize == 0, return total count only. It's a special case for search count.
|
||||
if options.Paginator != nil && options.Paginator.PageSize == 0 {
|
||||
|
|
|
@ -78,6 +78,11 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
|
|||
User: nil,
|
||||
}
|
||||
|
||||
if options.PriorityRepoID.Has() {
|
||||
opts.SortType = "priorityrepo"
|
||||
opts.PriorityRepoID = options.PriorityRepoID.Value()
|
||||
}
|
||||
|
||||
if len(options.MilestoneIDs) == 1 && options.MilestoneIDs[0] == 0 {
|
||||
opts.MilestoneIDs = []int64{db.NoConditionID}
|
||||
} else {
|
||||
|
|
|
@ -149,12 +149,13 @@ func (b *Indexer) Delete(ctx context.Context, ids ...int64) error {
|
|||
func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (*internal.SearchResult, error) {
|
||||
query := elastic.NewBoolQuery()
|
||||
|
||||
if options.Keyword != "" {
|
||||
q := elastic.NewBoolQuery()
|
||||
tokens, err := options.Tokens()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(tokens) > 0 {
|
||||
q := elastic.NewBoolQuery()
|
||||
for _, token := range tokens {
|
||||
innerQ := elastic.NewMultiMatchQuery(token.Term, "content", "comments").FieldWithBoost("title", 2.0).TieBreaker(0.5)
|
||||
if token.Fuzzy {
|
||||
|
@ -165,7 +166,7 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
|
|||
}
|
||||
var eitherQ elastic.Query = innerQ
|
||||
if issueID, err := token.ParseIssueReference(); err == nil {
|
||||
indexQ := elastic.NewTermQuery("index", issueID).Boost(15.0)
|
||||
indexQ := elastic.NewTermQuery("index", issueID).Boost(20)
|
||||
eitherQ = elastic.NewDisMaxQuery().Query(indexQ).Query(innerQ).TieBreaker(0.5)
|
||||
}
|
||||
switch token.Kind {
|
||||
|
@ -188,6 +189,10 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
|
|||
}
|
||||
query.Must(q)
|
||||
}
|
||||
if options.PriorityRepoID.Has() {
|
||||
q := elastic.NewTermQuery("repo_id", options.PriorityRepoID.Value()).Boost(10)
|
||||
query.Should(q)
|
||||
}
|
||||
|
||||
if options.IsPull.Has() {
|
||||
query.Must(elastic.NewTermQuery("is_pull", options.IsPull.Value()))
|
||||
|
|
|
@ -77,6 +77,7 @@ type SearchOptions struct {
|
|||
|
||||
RepoIDs []int64 // repository IDs which the issues belong to
|
||||
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
|
||||
IsClosed optional.Option[bool] // if the issues is closed
|
||||
|
|
|
@ -45,13 +45,10 @@ func (t *Tokenizer) next() (tk Token, err error) {
|
|||
|
||||
// skip all leading white space
|
||||
for {
|
||||
if r, _, err = t.in.ReadRune(); err == nil && r == ' ' {
|
||||
//nolint:staticcheck,wastedassign // SA4006 the variable is used after the loop
|
||||
r, _, err = t.in.ReadRune()
|
||||
continue
|
||||
}
|
||||
if r, _, err = t.in.ReadRune(); err != nil || r != ' ' {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return tk, err
|
||||
}
|
||||
|
@ -107,12 +104,18 @@ nextEnd:
|
|||
|
||||
// Tokenize the keyword
|
||||
func (o *SearchOptions) Tokens() (tokens []Token, err error) {
|
||||
if o.Keyword == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
in := strings.NewReader(o.Keyword)
|
||||
it := Tokenizer{in: in}
|
||||
|
||||
for token, err := it.next(); err == nil; token, err = it.next() {
|
||||
if token.Term != "" {
|
||||
tokens = append(tokens, token)
|
||||
}
|
||||
}
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -41,6 +41,36 @@ 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",
|
||||
Results: []Token{
|
||||
|
@ -156,6 +186,68 @@ 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) {
|
||||
|
|
|
@ -87,14 +87,44 @@ 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{
|
||||
{
|
||||
Name: "default",
|
||||
SearchOptions: &internal.SearchOptions{},
|
||||
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))
|
||||
Expected: allResults,
|
||||
},
|
||||
{
|
||||
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",
|
||||
|
@ -742,6 +772,25 @@ 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 {
|
||||
|
|
|
@ -39,16 +39,7 @@ func SearchPointerBlobs(ctx context.Context, repo *git.Repository, pointerChan c
|
|||
go pipeline.BlobsLessThan1024FromCatFileBatchCheck(catFileCheckReader, shasToBatchWriter, &wg)
|
||||
|
||||
// 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)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
close(pointerChan)
|
||||
|
|
|
@ -26,6 +26,7 @@ type WriterMode struct {
|
|||
Flags Flags
|
||||
|
||||
Expression string
|
||||
Exclusion string
|
||||
|
||||
StacktraceLevel Level
|
||||
|
||||
|
|
|
@ -68,6 +68,14 @@ 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() {
|
||||
if pause := b.GetPauseChan(); pause != nil {
|
||||
select {
|
||||
|
@ -95,6 +103,13 @@ func (b *EventWriterBaseImpl) Run(ctx context.Context) {
|
|||
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
|
||||
switch msg := event.Msg.(type) {
|
||||
|
|
|
@ -31,3 +31,49 @@ func TestBufferLogger(t *testing.T) {
|
|||
logger.Close()
|
||||
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")
|
||||
}
|
||||
|
|
|
@ -143,3 +143,19 @@ func TestLoggerExpressionFilter(t *testing.T) {
|
|||
|
||||
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())
|
||||
}
|
||||
|
|
|
@ -104,7 +104,7 @@ func TestRender_Images(t *testing.T) {
|
|||
|
||||
test(
|
||||
"",
|
||||
`<p><a href="`+result+`" target="_blank" rel="nofollow noopener"><img src="`+result+`" alt="`+title+`"/></a></p>`)
|
||||
`<p><a href="`+result+`" target="_blank" rel="nofollow noopener"><img src="`+result+`" alt="`+title+`" loading="lazy"/></a></p>`)
|
||||
|
||||
test(
|
||||
"[["+title+"|"+url+"]]",
|
||||
|
@ -115,7 +115,7 @@ func TestRender_Images(t *testing.T) {
|
|||
|
||||
test(
|
||||
"",
|
||||
`<p><a href="`+result+`" target="_blank" rel="nofollow noopener"><img src="`+result+`" alt="`+title+`"/></a></p>`)
|
||||
`<p><a href="`+result+`" target="_blank" rel="nofollow noopener"><img src="`+result+`" alt="`+title+`" loading="lazy"/></a></p>`)
|
||||
|
||||
test(
|
||||
"[["+title+"|"+url+"]]",
|
||||
|
@ -412,8 +412,8 @@ func TestRenderSiblingImages_Issue12925(t *testing.T) {
|
|||
testcase := `
|
||||

|
||||
`
|
||||
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"></a></p>
|
||||
expected := `<p><a href="/image1" target="_blank" rel="nofollow noopener"><img src="/image1" alt="image1" loading="lazy"></a><br>
|
||||
<a href="/image2" target="_blank" rel="nofollow noopener"><img src="/image2" alt="image2" loading="lazy"></a></p>
|
||||
`
|
||||
res, err := markdown.RenderRawString(&markup.RenderContext{Ctx: git.DefaultContext}, testcase)
|
||||
require.NoError(t, err)
|
||||
|
@ -845,10 +845,10 @@ mail@domain.com
|
|||
<a href="https://example.com" rel="nofollow">remote 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="/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"/></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"/></a><br/>
|
||||
<a href="/image.jpg" target="_blank" rel="nofollow noopener"><img src="/image.jpg" alt="local image" loading="lazy"/></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" 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" loading="lazy"/></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/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="/wiki/file.bin" rel="nofollow">local 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"/></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"/></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" target="_blank" rel="nofollow noopener"><img src="/wiki/raw/image.jpg" 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" loading="lazy"/></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="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="/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/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://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://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"/></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"/></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/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" 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" loading="lazy"/></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/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://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://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"/></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"/></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/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" 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" loading="lazy"/></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/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="/relative/path/src/file.bin" rel="nofollow">local 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"/></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"/></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" target="_blank" rel="nofollow noopener"><img src="/relative/path/image.jpg" 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" loading="lazy"/></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="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="/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/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="/relative/path/wiki/file.bin" rel="nofollow">local 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"/></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"/></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" 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/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" 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" loading="lazy"/></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/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="/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="/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"/></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"/></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/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" 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" loading="lazy"/></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/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="/relative/path/wiki/file.bin" rel="nofollow">local 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"/></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"/></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" 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/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" 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" loading="lazy"/></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/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="/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="/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"/></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"/></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/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" 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" loading="lazy"/></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/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="/relative/path/wiki/file.bin" rel="nofollow">local 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"/></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"/></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" 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/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" 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" loading="lazy"/></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/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="/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="/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"/></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"/></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/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" 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" loading="lazy"/></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/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="/relative/path/wiki/file.bin" rel="nofollow">local 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"/></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"/></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" 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/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" 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" loading="lazy"/></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/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a><br/>
|
||||
|
|
|
@ -44,6 +44,7 @@ func (g *ASTTransformer) transformImage(ctx *markup.RenderContext, v *ast.Image)
|
|||
for _, attr := range v.Attributes() {
|
||||
image.SetAttribute(attr.Name, attr.Value)
|
||||
}
|
||||
image.SetAttributeString("loading", []byte("lazy"))
|
||||
for child := v.FirstChild(); child != nil; {
|
||||
next := child.NextSibling()
|
||||
image.AppendChild(image, child)
|
||||
|
|
|
@ -108,6 +108,9 @@ func createDefaultPolicy() *bluemonday.Policy {
|
|||
// Allow classes for emojis
|
||||
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
|
||||
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")
|
||||
|
|
|
@ -75,6 +75,10 @@ func Test_Sanitizer(t *testing.T) {
|
|||
// 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="(+!)">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 {
|
||||
|
|
|
@ -133,6 +133,7 @@ func loadLogModeByName(rootCfg ConfigProvider, loggerName, modeName string) (wri
|
|||
writerMode.StacktraceLevel = log.LevelFromString(ConfigInheritedKeyString(sec, "STACKTRACE_LEVEL", Log.StacktraceLogLevel.String()))
|
||||
writerMode.Prefix = ConfigInheritedKeyString(sec, "PREFIX")
|
||||
writerMode.Expression = ConfigInheritedKeyString(sec, "EXPRESSION")
|
||||
writerMode.Exclusion = ConfigInheritedKeyString(sec, "EXCLUSION")
|
||||
// flags are updated and set below
|
||||
|
||||
switch writerType {
|
||||
|
|
|
@ -44,6 +44,7 @@ func TestLogConfigDefault(t *testing.T) {
|
|||
"BufferLen": 10000,
|
||||
"Colorize": false,
|
||||
"Expression": "",
|
||||
"Exclusion": "",
|
||||
"Flags": "stdflags",
|
||||
"Level": "info",
|
||||
"Prefix": "",
|
||||
|
@ -83,6 +84,7 @@ logger.xorm.MODE =
|
|||
"BufferLen": 10000,
|
||||
"Colorize": false,
|
||||
"Expression": "",
|
||||
"Exclusion": "",
|
||||
"Flags": "stdflags",
|
||||
"Level": "info",
|
||||
"Prefix": "",
|
||||
|
@ -121,6 +123,7 @@ MODE = console
|
|||
"BufferLen": 10000,
|
||||
"Colorize": false,
|
||||
"Expression": "",
|
||||
"Exclusion": "",
|
||||
"Flags": "stdflags",
|
||||
"Level": "info",
|
||||
"Prefix": "",
|
||||
|
@ -168,6 +171,7 @@ ACCESS = file
|
|||
"BufferLen": 10000,
|
||||
"Colorize": false,
|
||||
"Expression": "",
|
||||
"Exclusion": "",
|
||||
"Flags": "stdflags",
|
||||
"Level": "info",
|
||||
"Prefix": "",
|
||||
|
@ -191,6 +195,7 @@ ACCESS = file
|
|||
"BufferLen": 10000,
|
||||
"Colorize": false,
|
||||
"Expression": "",
|
||||
"Exclusion": "",
|
||||
"Flags": "none",
|
||||
"Level": "info",
|
||||
"Prefix": "",
|
||||
|
@ -257,6 +262,7 @@ STDERR = true
|
|||
"BufferLen": 10000,
|
||||
"Colorize": false,
|
||||
"Expression": "",
|
||||
"Exclusion": "",
|
||||
"Flags": "stdflags",
|
||||
"Level": "warn",
|
||||
"Prefix": "",
|
||||
|
@ -270,6 +276,7 @@ STDERR = true
|
|||
"BufferLen": 10000,
|
||||
"Colorize": false,
|
||||
"Expression": "",
|
||||
"Exclusion": "",
|
||||
"Flags": "stdflags",
|
||||
"Level": "error",
|
||||
"Prefix": "",
|
||||
|
@ -287,6 +294,7 @@ STDERR = true
|
|||
"BufferLen": 10000,
|
||||
"Colorize": false,
|
||||
"Expression": "",
|
||||
"Exclusion": "",
|
||||
"Flags": "none",
|
||||
"Level": "warn",
|
||||
"Prefix": "",
|
||||
|
@ -323,6 +331,7 @@ MODE = file
|
|||
LEVEL = error
|
||||
STACKTRACE_LEVEL = fatal
|
||||
EXPRESSION = filter
|
||||
EXCLUSION = not
|
||||
FLAGS = medfile
|
||||
PREFIX = "[Prefix] "
|
||||
FILE_NAME = file-xxx.log
|
||||
|
@ -341,6 +350,7 @@ COMPRESSION_LEVEL = 4
|
|||
"BufferLen": 10,
|
||||
"Colorize": false,
|
||||
"Expression": "",
|
||||
"Exclusion": "",
|
||||
"Flags": "stdflags",
|
||||
"Level": "info",
|
||||
"Prefix": "",
|
||||
|
@ -360,6 +370,7 @@ COMPRESSION_LEVEL = 4
|
|||
"BufferLen": 10,
|
||||
"Colorize": false,
|
||||
"Expression": "filter",
|
||||
"Exclusion": "not",
|
||||
"Flags": "medfile",
|
||||
"Level": "error",
|
||||
"Prefix": "[Prefix] ",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package structs
|
||||
|
@ -7,3 +8,15 @@ package structs
|
|||
type ActivityPub struct {
|
||||
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"`
|
||||
}
|
||||
|
|
|
@ -22,6 +22,12 @@ type Attachment struct {
|
|||
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
|
||||
// swagger:model
|
||||
type EditAttachmentOptions struct {
|
||||
|
|
|
@ -54,7 +54,6 @@ type CreateHookOption struct {
|
|||
AuthorizationHeader string `json:"authorization_header"`
|
||||
// default: false
|
||||
Active bool `json:"active"`
|
||||
IsSystemWebhook bool `json:"is_system_webhook"`
|
||||
}
|
||||
|
||||
// EditHookOption options when modify one hook
|
||||
|
|
|
@ -192,8 +192,8 @@ func TestRenderMarkdownToHtml(t *testing.T) {
|
|||
<a href="https://example.com" rel="nofollow">remote link</a>
|
||||
<a href="/src/file.bin" rel="nofollow">local 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"/></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" target="_blank" rel="nofollow noopener"><img src="/image.jpg" alt="local 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" loading="lazy"/></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/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a>
|
||||
|
|
|
@ -10,56 +10,79 @@ import (
|
|||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"forgejo.org/modules/util"
|
||||
)
|
||||
|
||||
type FederationServerMockPerson struct {
|
||||
ID int64
|
||||
Name string
|
||||
PubKey string
|
||||
PrivKey string
|
||||
}
|
||||
type FederationServerMockRepository struct {
|
||||
ID int64
|
||||
}
|
||||
type ApActorMock struct {
|
||||
PrivKey string
|
||||
PubKey string
|
||||
}
|
||||
type FederationServerMock struct {
|
||||
ApActor ApActorMock
|
||||
Persons []FederationServerMockPerson
|
||||
Repositories []FederationServerMockRepository
|
||||
LastPost string
|
||||
}
|
||||
|
||||
func NewFederationServerMockPerson(id int64, name string) FederationServerMockPerson {
|
||||
priv, pub, _ := util.GenerateKeyPair(3072)
|
||||
return FederationServerMockPerson{
|
||||
ID: id,
|
||||
Name: name,
|
||||
PubKey: `"-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA18H5s7N6ItZUAh9tneII\nIuZdTTa3cZlLa/9ejWAHTkcp3WLW+/zbsumlMrWYfBy2/yTm56qasWt38iY4D6ul\n` +
|
||||
`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"`,
|
||||
PubKey: pub,
|
||||
PrivKey: priv,
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
return FederationServerMockRepository{
|
||||
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 {
|
||||
return fmt.Sprintf(`{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1"],`+
|
||||
`"id":"http://%[1]v/api/activitypub/user-id/%[2]v",`+
|
||||
`"id":"http://%[1]v/api/v1/activitypub/user-id/%[2]v",`+
|
||||
`"type":"Person",`+
|
||||
`"icon":{"type":"Image","mediaType":"image/png","url":"http://%[1]v/avatars/1bb05d9a5f6675ed0272af9ea193063c"},`+
|
||||
`"url":"http://%[1]v/%[2]v",`+
|
||||
`"inbox":"http://%[1]v/api/activitypub/user-id/%[2]v/inbox",`+
|
||||
`"outbox":"http://%[1]v/api/activitypub/user-id/%[2]v/outbox",`+
|
||||
`"inbox":"http://%[1]v/api/v1/activitypub/user-id/%[2]v/inbox",`+
|
||||
`"outbox":"http://%[1]v/api/v1/activitypub/user-id/%[2]v/outbox",`+
|
||||
`"preferredUsername":"%[3]v",`+
|
||||
`"publicKey":{"id":"http://%[1]v/api/activitypub/user-id/%[2]v#main-key",`+
|
||||
`"owner":"http://%[1]v/api/activitypub/user-id/%[2]v",`+
|
||||
`"publicKeyPem":%[4]v}}`, host, p.ID, p.Name, p.PubKey)
|
||||
`"publicKey":{"id":"http://%[1]v/api/v1/activitypub/user-id/%[2]v#main-key",`+
|
||||
`"owner":"http://%[1]v/api/v1/activitypub/user-id/%[2]v",`+
|
||||
`"publicKeyPem":%[4]q}}`, host, p.ID, p.Name, p.PubKey)
|
||||
}
|
||||
|
||||
func NewFederationServerMock() *FederationServerMock {
|
||||
return &FederationServerMock{
|
||||
ApActor: NewApActorMock(),
|
||||
Persons: []FederationServerMockPerson{
|
||||
NewFederationServerMockPerson(15, "stargoose1"),
|
||||
NewFederationServerMockPerson(30, "stargoose2"),
|
||||
|
@ -71,8 +94,18 @@ 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 {
|
||||
federatedRoutes := http.NewServeMux()
|
||||
|
||||
federatedRoutes.HandleFunc("/.well-known/nodeinfo",
|
||||
func(res http.ResponseWriter, req *http.Request) {
|
||||
// curl -H "Accept: application/json" https://federated-repo.prod.meissa.de/.well-known/nodeinfo
|
||||
|
@ -87,30 +120,28 @@ func (mock *FederationServerMock) DistantServer(t *testing.T) *httptest.Server {
|
|||
`"protocols":["activitypub"],"services":{"inbound":[],"outbound":["rss2.0"]},`+
|
||||
`"openRegistrations":true,"usage":{"users":{"total":14,"activeHalfyear":2}},"metadata":{}}`)
|
||||
})
|
||||
|
||||
for _, person := range mock.Persons {
|
||||
federatedRoutes.HandleFunc(fmt.Sprintf("/api/v1/activitypub/user-id/%v", person.ID),
|
||||
func(res http.ResponseWriter, req *http.Request) {
|
||||
// curl -H "Accept: application/json" https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/2
|
||||
fmt.Fprint(res, person.marshal(req.Host))
|
||||
})
|
||||
}
|
||||
for _, repository := range mock.Repositories {
|
||||
federatedRoutes.HandleFunc(fmt.Sprintf("/api/v1/activitypub/repository-id/%v/inbox", repository.ID),
|
||||
federatedRoutes.HandleFunc(fmt.Sprintf("POST /api/v1/activitypub/user-id/%v/inbox", person.ID),
|
||||
func(res http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != "POST" {
|
||||
t.Errorf("POST expected at: %q", req.URL.EscapedPath())
|
||||
mock.recordLastPost(t, req)
|
||||
})
|
||||
}
|
||||
buf := new(strings.Builder)
|
||||
_, err := io.Copy(buf, req.Body)
|
||||
if err != nil {
|
||||
t.Errorf("Error reading body: %q", err)
|
||||
}
|
||||
mock.LastPost = buf.String()
|
||||
|
||||
for _, repository := range mock.Repositories {
|
||||
federatedRoutes.HandleFunc(fmt.Sprintf("POST /api/v1/activitypub/repository-id/%v/inbox", repository.ID),
|
||||
func(res http.ResponseWriter, req *http.Request) {
|
||||
mock.recordLastPost(t, req)
|
||||
})
|
||||
}
|
||||
federatedRoutes.HandleFunc("/",
|
||||
func(res http.ResponseWriter, req *http.Request) {
|
||||
t.Errorf("Unhandled request: %q", req.URL.EscapedPath())
|
||||
t.Errorf("Unhandled %v request: %q", req.Method, req.URL.EscapedPath())
|
||||
})
|
||||
federatedSrv := httptest.NewServer(federatedRoutes)
|
||||
return federatedSrv
|
||||
|
|
|
@ -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.
|
||||
func DetectContentType(data []byte) SniffedType {
|
||||
func DetectContentType(data []byte, filename string) SniffedType {
|
||||
if len(data) == 0 {
|
||||
return SniffedType{"text/unknown"}
|
||||
}
|
||||
|
@ -176,6 +176,13 @@ func DetectContentType(data []byte) 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
|
||||
// hexdump -n 4 -C glTF.glb
|
||||
if bytes.HasPrefix(data, []byte("glTF")) {
|
||||
|
@ -186,7 +193,7 @@ func DetectContentType(data []byte) SniffedType {
|
|||
}
|
||||
|
||||
// DetectContentTypeFromReader guesses the content type contained in the reader.
|
||||
func DetectContentTypeFromReader(r io.Reader) (SniffedType, error) {
|
||||
func DetectContentTypeFromReader(r io.Reader, filename string) (SniffedType, error) {
|
||||
buf := make([]byte, sniffLen)
|
||||
n, err := util.ReadAtMost(r, buf)
|
||||
if err != nil {
|
||||
|
@ -194,5 +201,5 @@ func DetectContentTypeFromReader(r io.Reader) (SniffedType, error) {
|
|||
}
|
||||
buf = buf[:n]
|
||||
|
||||
return DetectContentType(buf), nil
|
||||
return DetectContentType(buf, filename), nil
|
||||
}
|
||||
|
|
|
@ -16,63 +16,63 @@ import (
|
|||
|
||||
func TestDetectContentTypeLongerThanSniffLen(t *testing.T) {
|
||||
// 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.
|
||||
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) {
|
||||
assert.True(t, DetectContentType([]byte{}).IsText())
|
||||
assert.True(t, DetectContentType([]byte("lorem ipsum")).IsText())
|
||||
assert.True(t, DetectContentType([]byte{}, "").IsText())
|
||||
assert.True(t, DetectContentType([]byte("lorem ipsum"), "").IsText())
|
||||
}
|
||||
|
||||
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 width="100"></svg>`)).IsSvgImage())
|
||||
assert.True(t, DetectContentType([]byte(`<?xml version="1.0" encoding="UTF-8"?><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(`<?xml version="1.0" encoding="UTF-8"?><svg></svg>`), "").IsSvgImage())
|
||||
assert.True(t, DetectContentType([]byte(`<!-- Comment -->
|
||||
<svg></svg>`)).IsSvgImage())
|
||||
<svg></svg>`), "").IsSvgImage())
|
||||
assert.True(t, DetectContentType([]byte(`<!-- Multiple -->
|
||||
<!-- Comments -->
|
||||
<svg></svg>`)).IsSvgImage())
|
||||
<svg></svg>`), "").IsSvgImage())
|
||||
assert.True(t, DetectContentType([]byte(`<!-- Multiline
|
||||
Comment -->
|
||||
<svg></svg>`)).IsSvgImage())
|
||||
<svg></svg>`), "").IsSvgImage())
|
||||
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">
|
||||
<svg></svg>`)).IsSvgImage())
|
||||
<svg></svg>`), "").IsSvgImage())
|
||||
assert.True(t, DetectContentType([]byte(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Comment -->
|
||||
<svg></svg>`)).IsSvgImage())
|
||||
<svg></svg>`), "").IsSvgImage())
|
||||
assert.True(t, DetectContentType([]byte(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Multiple -->
|
||||
<!-- Comments -->
|
||||
<svg></svg>`)).IsSvgImage())
|
||||
<svg></svg>`), "").IsSvgImage())
|
||||
assert.True(t, DetectContentType([]byte(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Multiline
|
||||
Comment -->
|
||||
<svg></svg>`)).IsSvgImage())
|
||||
<svg></svg>`), "").IsSvgImage())
|
||||
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">
|
||||
<!-- Multiline
|
||||
Comment -->
|
||||
<svg></svg>`)).IsSvgImage())
|
||||
<svg></svg>`), "").IsSvgImage())
|
||||
|
||||
// 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("svg")).IsSvgImage())
|
||||
assert.False(t, DetectContentType([]byte("<svgfoo></svgfoo>")).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(`<script>"<svg></svg>"</script>`)).IsSvgImage())
|
||||
assert.False(t, DetectContentType([]byte{}, "").IsSvgImage())
|
||||
assert.False(t, DetectContentType([]byte("svg"), "").IsSvgImage())
|
||||
assert.False(t, DetectContentType([]byte("<svgfoo></svgfoo>"), "").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(`<script>"<svg></svg>"</script>`), "").IsSvgImage())
|
||||
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"?>
|
||||
<!-- <svg></svg> inside comment -->
|
||||
<foo></foo>`)).IsSvgImage())
|
||||
<foo></foo>`), "").IsSvgImage())
|
||||
|
||||
assert.False(t, DetectContentType([]byte(`
|
||||
<!-- comment1 -->
|
||||
|
@ -80,7 +80,7 @@ func TestIsSvgImage(t *testing.T) {
|
|||
<!-- comment2 -->
|
||||
<svg></svg>
|
||||
</div>
|
||||
`)).IsSvgImage())
|
||||
`), "").IsSvgImage())
|
||||
|
||||
assert.False(t, DetectContentType([]byte(`
|
||||
<!-- comment1
|
||||
|
@ -90,56 +90,56 @@ func TestIsSvgImage(t *testing.T) {
|
|||
-->
|
||||
<svg></svg>
|
||||
</div>
|
||||
`)).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())
|
||||
`), "").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())
|
||||
}
|
||||
|
||||
func TestIsPDF(t *testing.T) {
|
||||
pdf, _ := base64.StdEncoding.DecodeString("JVBERi0xLjYKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURlY29kZT4+CnN0cmVhbQp4nF3NPwsCMQwF8D2f4s2CNYk1baF0EHRwOwg4iJt/NsFb/PpevUE4Mjwe")
|
||||
assert.True(t, DetectContentType(pdf).IsPDF())
|
||||
assert.False(t, DetectContentType([]byte("plain text")).IsPDF())
|
||||
assert.True(t, DetectContentType(pdf, "").IsPDF())
|
||||
assert.False(t, DetectContentType([]byte("plain text"), "").IsPDF())
|
||||
}
|
||||
|
||||
func TestIsVideo(t *testing.T) {
|
||||
mp4, _ := base64.StdEncoding.DecodeString("AAAAGGZ0eXBtcDQyAAAAAGlzb21tcDQyAAEI721vb3YAAABsbXZoZAAAAADaBlwX2gZcFwAAA+gA")
|
||||
assert.True(t, DetectContentType(mp4).IsVideo())
|
||||
assert.False(t, DetectContentType([]byte("plain text")).IsVideo())
|
||||
assert.True(t, DetectContentType(mp4, "").IsVideo())
|
||||
assert.False(t, DetectContentType([]byte("plain text"), "").IsVideo())
|
||||
}
|
||||
|
||||
func TestIsAudio(t *testing.T) {
|
||||
mp3, _ := base64.StdEncoding.DecodeString("SUQzBAAAAAABAFRYWFgAAAASAAADbWFqb3JfYnJhbmQAbXA0MgBUWFhYAAAAEQAAA21pbm9yX3Zl")
|
||||
assert.True(t, DetectContentType(mp3).IsAudio())
|
||||
assert.False(t, DetectContentType([]byte("plain text")).IsAudio())
|
||||
assert.True(t, DetectContentType(mp3, "").IsAudio())
|
||||
assert.False(t, DetectContentType([]byte("plain text"), "").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 🌞, ..."+"🌛"[0:2])).IsText()) // test ID3 tag with incomplete UTF8 char
|
||||
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 🌞, ..."+"🌛"[0:2]), "").IsText()) // test ID3 tag with incomplete UTF8 char
|
||||
}
|
||||
|
||||
func TestIsGLB(t *testing.T) {
|
||||
glb, _ := hex.DecodeString("676c5446")
|
||||
assert.True(t, DetectContentType(glb).IsGLB())
|
||||
assert.True(t, DetectContentType(glb).Is3DModel())
|
||||
assert.False(t, DetectContentType([]byte("plain text")).IsGLB())
|
||||
assert.False(t, DetectContentType([]byte("plain text")).Is3DModel())
|
||||
assert.True(t, DetectContentType(glb, "").IsGLB())
|
||||
assert.True(t, DetectContentType(glb, "").Is3DModel())
|
||||
assert.False(t, DetectContentType([]byte("plain text"), "").IsGLB())
|
||||
assert.False(t, DetectContentType([]byte("plain text"), "").Is3DModel())
|
||||
}
|
||||
|
||||
func TestDetectContentTypeFromReader(t *testing.T) {
|
||||
mp3, _ := base64.StdEncoding.DecodeString("SUQzBAAAAAABAFRYWFgAAAASAAADbWFqb3JfYnJhbmQAbXA0MgBUWFhYAAAAEQAAA21pbm9yX3Zl")
|
||||
st, err := DetectContentTypeFromReader(bytes.NewReader(mp3))
|
||||
st, err := DetectContentTypeFromReader(bytes.NewReader(mp3), "")
|
||||
require.NoError(t, err)
|
||||
assert.True(t, st.IsAudio())
|
||||
}
|
||||
|
||||
func TestDetectContentTypeOgg(t *testing.T) {
|
||||
oggAudio, _ := hex.DecodeString("4f67675300020000000000000000352f0000000000007dc39163011e01766f72626973000000000244ac0000000000000071020000000000b8014f6767530000")
|
||||
st, err := DetectContentTypeFromReader(bytes.NewReader(oggAudio))
|
||||
st, err := DetectContentTypeFromReader(bytes.NewReader(oggAudio), "")
|
||||
require.NoError(t, err)
|
||||
assert.True(t, st.IsAudio())
|
||||
|
||||
oggVideo, _ := hex.DecodeString("4f676753000200000000000000007d9747ef000000009b59daf3012a807468656f7261030201001e00110001e000010e00020000001e00000001000001000001")
|
||||
st, err = DetectContentTypeFromReader(bytes.NewReader(oggVideo))
|
||||
st, err = DetectContentTypeFromReader(bytes.NewReader(oggVideo), "")
|
||||
require.NoError(t, err)
|
||||
assert.True(t, st.IsVideo())
|
||||
}
|
||||
|
@ -148,7 +148,7 @@ func TestDetectContentTypeAvif(t *testing.T) {
|
|||
avifImage, err := hex.DecodeString("000000206674797061766966")
|
||||
require.NoError(t, err)
|
||||
|
||||
st, err := DetectContentTypeFromReader(bytes.NewReader(avifImage))
|
||||
st, err := DetectContentTypeFromReader(bytes.NewReader(avifImage), "")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, st.IsImage())
|
||||
|
@ -158,10 +158,24 @@ func TestDetectContentTypeModelGLB(t *testing.T) {
|
|||
glb, err := hex.DecodeString("676c5446")
|
||||
require.NoError(t, err)
|
||||
|
||||
st, err := DetectContentTypeFromReader(bytes.NewReader(glb))
|
||||
st, err := DetectContentTypeFromReader(bytes.NewReader(glb), "")
|
||||
require.NoError(t, err)
|
||||
|
||||
// print st for debugging
|
||||
assert.Equal(t, "model/gltf-binary", st.GetMimeType())
|
||||
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())
|
||||
}
|
||||
|
|
|
@ -699,7 +699,7 @@ issues.filter_milestone_all = كل الأهداف
|
|||
issues.unlock.notice_2 = - يمكنك دوما إقفال هذه المسألة من جديد في المستقبل.
|
||||
issues.num_participants_few = %d متحاور
|
||||
release.title = عنوان الإصدار
|
||||
issues.closed_at = `أغلق هذه المسألة <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
issues.closed_at = `أغلق هذه المسألة %s`
|
||||
issues.lock.title = إقفال التحاور في هذه المسألة.
|
||||
issues.new.no_label = بلا تصنيف
|
||||
issues.filter_sort.mostforks = الأعلى اشتقاقا
|
||||
|
@ -759,7 +759,7 @@ branch.renamed = غُيّر اسم الفرع %s إلى %s.
|
|||
delete_preexisting = احذف الملفات الموجودة سابقا
|
||||
branch.included_desc = هذا الفرع جزء من الفرع المبدئي
|
||||
trust_model_helper_collaborator_committer = مشترك+مودع: ثق بتوقيعات المشتركين التي تطابق المودع
|
||||
issues.reopened_at = `أعاد فتح هذه المسألة <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
issues.reopened_at = `أعاد فتح هذه المسألة %s`
|
||||
issues.action_milestone = هدف
|
||||
issues.new.assignees = المكلَّفون
|
||||
release.tag_name_protected = اسم الوسم محمي.
|
||||
|
@ -1166,7 +1166,7 @@ pulls.status_checking = في انتظار بعض الفحوص
|
|||
pulls.status_checks_failure = بعض الفحوص فشلت
|
||||
pulls.status_checks_success = جميع الفحوص ناجحة
|
||||
pulls.status_checks_warning = بعض الفحوص تعطي تحذيرات
|
||||
pulls.commit_ref_at = `أشار إلى طلب الدمج من إيداع <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
pulls.commit_ref_at = `أشار إلى طلب الدمج من إيداع %s`
|
||||
pulls.cmd_instruction_hint = `أظهر شرح استخدام سطر الأوامر.`
|
||||
pulls.cmd_instruction_checkout_title = اسحب
|
||||
pulls.cmd_instruction_checkout_desc = من مستودع مشروعك، اسحب (check out) فرعا جديدا واختبر التغييرات.
|
||||
|
@ -1257,8 +1257,8 @@ pulls.status_checks_details = تفاصيل
|
|||
pulls.status_checks_hide_all = أخفِ كل الفحوص
|
||||
pulls.status_checks_show_all = أظهر كل الفحوص
|
||||
pulls.close = أغلق طلب الدمج
|
||||
pulls.closed_at = `أغلق طلب الدمج <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
pulls.reopened_at = `أعاد فتح طلب الدمج <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
pulls.closed_at = `أغلق طلب الدمج %s`
|
||||
pulls.reopened_at = `أعاد فتح طلب الدمج %s`
|
||||
milestones.title = العنوان
|
||||
milestones.desc = الوصف
|
||||
milestones.edit = عدّل الهدف
|
||||
|
@ -1302,11 +1302,11 @@ issues.closed_by_fake = من %[2]s أُغلقت %[1]s
|
|||
issues.num_comments_1 = %d تعليق
|
||||
issues.num_comments = %d تعليقا
|
||||
issues.commented_at = `علّق <a href="#%s">%s</a>`
|
||||
issues.commit_ref_at = `أشار إلى هذه المسألة من إيداع <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
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="%[3]s">أشار إلى هذا الطلب %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
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="%[3]s">أشار إلى طلب دمج %[4]s سيعيد فتح هذه المسألة</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
issues.commit_ref_at = `أشار إلى هذه المسألة من إيداع %s`
|
||||
issues.ref_issue_from = `<a href="%[2]s">أشار إلى هذه المسألة %[3]s</a> %[1]s`
|
||||
issues.ref_pull_from = `<a href="%[2]s">أشار إلى هذا الطلب %[3]s</a> %[1]s`
|
||||
issues.ref_closing_from = `<a href="%[2]s">أشار إلى طلب دمج %[3]s سيغلق هذه المسألة</a> %[1]s`
|
||||
issues.ref_reopening_from = `<a href="%[2]s">أشار إلى طلب دمج %[3]s سيعيد فتح هذه المسألة</a> %[1]s`
|
||||
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.reference_issue.body = المحتوى
|
||||
|
|
|
@ -749,7 +749,7 @@ settings.admin_settings = Администраторски настройки
|
|||
issues.role.owner = Притежател
|
||||
settings.transfer.title = Прехвърляне на притежанието
|
||||
issues.author = Автор
|
||||
issues.closed_at = `затвори тази задача <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
issues.closed_at = `затвори тази задача %s`
|
||||
settings.collaborator_deletion_desc = Премахването на сътрудник ще отнеме достъпа му до това хранилище. Продължаване?
|
||||
commits.message = Съобщение
|
||||
issues.due_date_not_set = Няма зададен краен срок.
|
||||
|
@ -773,9 +773,9 @@ issues.filter_type.all_issues = Всички задачи
|
|||
issues.filter_poster_no_select = Всички автори
|
||||
issues.opened_by = отворена %[1]s от <a href="%[2]s">%[3]s</a>
|
||||
issues.action_open = Отваряне
|
||||
pulls.closed_at = `затвори тази заявка за сливане <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
pulls.reopened_at = `отвори наново тази заявка за сливане <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
issues.reopened_at = `отвори наново тази задача <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
pulls.closed_at = `затвори тази заявка за сливане %s`
|
||||
pulls.reopened_at = `отвори наново тази заявка за сливане %s`
|
||||
issues.reopened_at = `отвори наново тази задача %s`
|
||||
projects.column.edit = Редактиране на колоната
|
||||
issues.close = Затваряне на задачата
|
||||
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_not_exist = Зависимостта не съществува.
|
||||
issues.remove_ref_at = `премахна препратката <b>%s</b> %s`
|
||||
issues.ref_pull_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.dependency.pr_no_dependencies = Няма зададени зависимости.
|
||||
issues.dependency.remove_info = Премахване на тази зависимост
|
||||
issues.dependency.removed_dependency = `премахна зависимостта %s`
|
||||
|
@ -1230,11 +1230,11 @@ issues.dependency.title = Зависимости
|
|||
issues.dependency.issue_no_dependencies = Няма зададени зависимости.
|
||||
issues.dependency.pr_close_blocked = Трябва да затворите всички задачи, блокиращи тази заявка за сливане, преди да можете да я слеете.
|
||||
issues.dependency.pr_close_blocks = Тази заявка за сливане блокира затварянето на следните задачи
|
||||
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 = `спомена тази задача в подаване <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
issues.ref_issue_from = `<a href="%[2]s">спомена тази задача %[3]s</a> %[1]s`
|
||||
issues.commit_ref_at = `спомена тази задача в подаване %s`
|
||||
issues.add_ref_at = `добави препратка <b>%s</b> %s`
|
||||
pulls.merged_info_text = Клонът %s вече може да бъде изтрит.
|
||||
pulls.commit_ref_at = `спомена тази заявка за сливане в подаване <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
pulls.commit_ref_at = `спомена тази заявка за сливане в подаване %s`
|
||||
issues.change_ref_at = `промени препратката от <b><strike>%s</strike></b> на <b>%s</b> %s`
|
||||
diff.review.reject = Поискване на промени
|
||||
diff.bin_not_shown = Двоичният файл не е показан.
|
||||
|
@ -1299,9 +1299,9 @@ branch.create_new_branch = Създаване на клон от клон:
|
|||
pulls.status_checks_show_all = Показване на всички проверки
|
||||
size_format = %[1]s: %[2]s; %[3]s: %[4]s
|
||||
pulls.filter_changes_by_commit = Филтриране по подаване
|
||||
issues.ref_closing_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_from = `от %[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_reopening_from = `<a href="%[2]s">спомена тази задача в заявка за сливане %[3]s, която ще я отвори наново </a>, %[1]s`
|
||||
issues.draft_title = Чернова
|
||||
pulls.reopen_to_merge = Моля, отворете наново тази заявка за сливане, за да извършите сливане.
|
||||
pulls.cant_reopen_deleted_branch = Тази заявка за сливане не може да бъде отворена наново, защото клонът е изтрит.
|
||||
|
|
|
@ -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.
|
||||
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.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.
|
||||
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.
|
||||
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.
|
||||
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.delete_branch_at=`odstranil/a větev <b>%s</b> %s`
|
||||
issues.filter_label=Štítek
|
||||
issues.filter_label_exclude=`Chcete-li vyloučit štítky, použijte <code>alt</code> + <code>click/enter</code>`
|
||||
issues.filter_label_exclude=Chcete-li vyloučit štítky, použijte <kbd>Alt</kbd> + <kbd>kliknutí</kbd>
|
||||
issues.filter_label_no_select=Všechny štítky
|
||||
issues.filter_label_select_no_label=Bez štítku
|
||||
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.previous=Předchozí
|
||||
issues.next=Další
|
||||
issues.open_title=Otevřeno
|
||||
issues.closed_title=Uzavřeno
|
||||
issues.open_title=Otevřené
|
||||
issues.closed_title=Uzavřené
|
||||
issues.draft_title=Koncept
|
||||
issues.num_comments_1=%d komentář
|
||||
issues.num_comments=%d komentářů
|
||||
issues.commented_at=`okomentoval/a <a href="#%s">%s</a>`
|
||||
issues.delete_comment_confirm=Jste si jist, že chcete smazat tento komentář?
|
||||
issues.delete_comment_confirm=Opravdu chcete smazat tento komentář?
|
||||
issues.context.copy_link=Kopírovat odkaz
|
||||
issues.context.quote_reply=Citovat odpověď
|
||||
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_comment_issue=Znovu otevřít s komentářem
|
||||
issues.create_comment=Komentovat
|
||||
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 <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
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="%[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="%[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="%[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="%[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.closed_at=`uzavřel/a tento problém %s`
|
||||
issues.reopened_at=`znovu otevřel/a tento problém %s`
|
||||
issues.commit_ref_at=`odkázal/a na tento problém z revize %s`
|
||||
issues.ref_issue_from=`<a href="%[2]s">odkázal/a na tento problém %[3]s</a> %[1]s`
|
||||
issues.ref_pull_from=`<a href="%[2]s">odkázal/a na tuto žádost o sloučení %[3]s</a> %[1]s`
|
||||
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_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_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_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.outdated_with_base_branch=Tato větev je zastaralá oproti základní větvi
|
||||
pulls.close=Zavřít žádost o sloučení
|
||||
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í <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
pulls.closed_at=`uzavřel/a tuto žádost o sloučení %s`
|
||||
pulls.reopened_at=`znovu otevřel/a tuto žádost o sloučení %s`
|
||||
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_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.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.
|
||||
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>`
|
||||
pulls.commit_ref_at = `odkázal/a na tuto žádost o sloučení z revize %s`
|
||||
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.
|
||||
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.
|
||||
follow_blocked_user = Tuto organizaci nemůžete sledovat, protože jste v ní zablokováni.
|
||||
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 dni. 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 dnu. 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.
|
||||
|
||||
[admin]
|
||||
|
|
|
@ -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_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.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_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_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_from = `fra %[1]s`
|
||||
issues.author = Forfatter
|
||||
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="%[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="%[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="%[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.commit_ref_at = `henviste til dette problem fra en commit %s`
|
||||
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_pull_from = `<a href="%[2]s">henviste til denne pull-anmodning %[3]s</a> %[1]s`
|
||||
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.author.tooltip.issue = Denne bruger er forfatteren til dette problem.
|
||||
issues.author.tooltip.pr = Denne bruger er forfatteren af denne pull-anmodning.
|
||||
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.reopen_comment_issue = Genåbner med kommentar
|
||||
issues.create_comment = Kommentar
|
||||
issues.closed_at = `lukkede dette problem <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
issues.reopened_at = `genåbnede dette problem <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
issues.closed_at = `lukkede dette problem %s`
|
||||
issues.reopened_at = `genåbnede dette problem %s`
|
||||
issues.remove_label = fjernede %s etiketten %s
|
||||
issues.remove_labels = fjernede %s etiketterne %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.status_checks_requested = Påkrævet
|
||||
pulls.close = Luk pull anmodning
|
||||
pulls.commit_ref_at = `henviste til denne pull-anmodning fra en commit <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
pulls.commit_ref_at = `henviste til denne pull-anmodning fra en commit %s`
|
||||
pulls.cmd_instruction_hint = Se instruktionerne på kommandolinjen
|
||||
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 <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
pulls.reopened_at = `genåbnede denne pull-anmodning %s`
|
||||
pulls.closed_at = `lukkede denne pull-anmodning %s`
|
||||
pulls.cmd_instruction_checkout_desc = Fra dit projektdepot, tjek en ny gren og test ændringerne.
|
||||
pulls.editable = Redigerbar
|
||||
pulls.made_using_agit = AGit
|
||||
|
|
|
@ -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.delete_branch_at=`löschte den Branch <b>%s</b> %s`
|
||||
issues.filter_label=Label
|
||||
issues.filter_label_exclude=`<code>Alt</code> + <code>Klick/Enter</code> verwenden, um Labels auszuschließen`
|
||||
issues.filter_label_exclude=`Verwende <kbd>Alt</kbd> + <kbd>Klick/Enter</kbd>, um Labels auszuschließen`
|
||||
issues.filter_label_no_select=Alle Labels
|
||||
issues.filter_label_select_no_label=Kein Label
|
||||
issues.filter_milestone=Meilenstein
|
||||
|
@ -1651,13 +1651,13 @@ issues.close_comment_issue=Mit Kommentar schließen
|
|||
issues.reopen_issue=Wieder öffnen
|
||||
issues.reopen_comment_issue=Mit Kommentar wieder öffnen
|
||||
issues.create_comment=Kommentieren
|
||||
issues.closed_at=`hat diesen Issue <a id="%[1]s" href="#%[1]s">%[2]s</a> geschlossen`
|
||||
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 <a id="%[1]s" href="#%[1]s">%[2]s</a> aus einem Commit referenziert`
|
||||
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="%[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="%[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="%[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.closed_at=`hat dieses Issue %s geschlossen`
|
||||
issues.reopened_at=`hat dieses Issue %s wieder geöffnet`
|
||||
issues.commit_ref_at=`hat dieses Issue %s 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_pull_from=`<a href="%[2]s">referenzierte diesen Pull-Request %[3]s</a> %[1]s`
|
||||
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_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_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_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.outdated_with_base_branch=Dieser Branch enthält nicht die neusten Commits des Basis-Branches
|
||||
pulls.close=Pull-Request schließen
|
||||
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 <a id="%[1]s" href="#%[1]s">%[2]s</a> wieder geöffnet`
|
||||
pulls.closed_at=`hat diesen Pull-Request %s geschlossen`
|
||||
pulls.reopened_at=`hat diesen Pull-Request %s wieder geöffnet`
|
||||
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.
|
||||
|
||||
|
@ -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.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.
|
||||
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.commit_ref_at = `referenzierte diesen Pull-Request aus einem Commit %s`
|
||||
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_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.
|
||||
follow_blocked_user = Du kannst dieser Organisation nicht folgen, weil diese Organisation dich blockiert hat.
|
||||
open_dashboard = Übersicht öffnen
|
||||
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 Abkühldauer von %[1]d Tagen 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.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.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.
|
||||
|
||||
[admin]
|
||||
dashboard=Übersicht
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue