forked from fnetx/forgejo
Compare commits
70 commits
forgejo
...
bp-v7.0/fo
Author | SHA1 | Date | |
---|---|---|---|
|
71e63f541d | ||
|
c0fb79b436 | ||
|
6051722460 | ||
|
f3ce65a3fc | ||
|
96b2ec888a | ||
|
f3b6759ab7 | ||
|
2b8d95c8c2 | ||
|
470886bf52 | ||
|
d2cd1342bf | ||
|
e5212c8c96 | ||
|
24552ee9ee | ||
|
7b24b669ed | ||
|
482658a4d0 | ||
|
a89e146cb0 | ||
|
7befc34e68 | ||
|
69d9d66dda | ||
|
11feddc21d | ||
|
b4f566fdf5 | ||
|
5a23ce083d | ||
|
ba4f17b1d9 | ||
|
f25d2ce223 | ||
57b19874b8 | |||
|
46eeb884b4 | ||
|
0abf358c94 | ||
2937333e2d | |||
|
7b97ea7154 | ||
|
59e42fee1c | ||
e13854c305 | |||
|
71153ef8b4 | ||
|
7db4e374ca | ||
|
2e5aa42f20 | ||
|
7a783c3132 | ||
|
029bcd361a | ||
1a0c9df87f | |||
ce74e66b95 | |||
cf460b8b5f | |||
4706b644f8 | |||
b6dccc0fd4 | |||
966faddee4 | |||
|
c01935e9d0 | ||
|
22aedc6c96 | ||
|
4dd475dfe5 | ||
|
923035e418 | ||
|
9ecd041975 | ||
|
9f80081795 | ||
|
4cb3f331a2 | ||
|
fd8f51f2b6 | ||
|
e628d0e54b | ||
|
ece5c97931 | ||
|
b9dbd93ebc | ||
|
66d1cd89d1 | ||
|
45f39ce839 | ||
|
f0a2da40ff | ||
|
60a82b0890 | ||
|
6b560544fb | ||
|
4a4bd75989 | ||
84eeab59af | |||
7f03fdf9f9 | |||
|
6b2d02528f | ||
92acbb0a8e | |||
|
5503db386e | ||
005a9c7850 | |||
|
d0dccaec66 | ||
|
ea0e8caa6f | ||
|
9b4d32446c | ||
|
ce5f1b942f | ||
|
e428231b38 | ||
|
03df59ec95 | ||
|
a1bce73f5c | ||
19f7eb657b |
165 changed files with 3863 additions and 1375 deletions
|
@ -295,6 +295,7 @@ package "code.gitea.io/gitea/modules/translation"
|
|||
func (MockLocale).TrString
|
||||
func (MockLocale).Tr
|
||||
func (MockLocale).TrN
|
||||
func (MockLocale).TrSize
|
||||
func (MockLocale).PrettyNumber
|
||||
|
||||
package "code.gitea.io/gitea/modules/util/filebuffer"
|
||||
|
@ -341,5 +342,4 @@ package "code.gitea.io/gitea/services/repository/files"
|
|||
|
||||
package "code.gitea.io/gitea/services/webhook"
|
||||
func NewNotifier
|
||||
func List
|
||||
|
||||
|
|
|
@ -33,22 +33,18 @@ jobs:
|
|||
if: >
|
||||
!startsWith(vars.ROLE, 'forgejo-') && (
|
||||
github.event.pull_request.merged
|
||||
&& (
|
||||
(
|
||||
github.event.action == 'closed' &&
|
||||
&&
|
||||
contains(toJSON(github.event.pull_request.labels), 'backport/v')
|
||||
)
|
||||
||
|
||||
(
|
||||
github.event.action == 'labeled' &&
|
||||
contains(github.event.label.name, 'backport/v')
|
||||
)
|
||||
)
|
||||
)
|
||||
runs-on: docker
|
||||
container:
|
||||
image: 'docker.io/node:20-bookworm'
|
||||
steps:
|
||||
- name: event info
|
||||
run: |
|
||||
cat <<'EOF'
|
||||
${{ toJSON(github) }}
|
||||
EOF
|
||||
- name: Fetch labels
|
||||
id: fetch-labels
|
||||
shell: bash
|
||||
|
|
|
@ -14,6 +14,11 @@ jobs:
|
|||
container:
|
||||
image: 'docker.io/node:20-bookworm'
|
||||
steps:
|
||||
- name: event info
|
||||
run: |
|
||||
cat <<'EOF'
|
||||
${{ toJSON(github) }}
|
||||
EOF
|
||||
- uses: https://code.forgejo.org/actions/checkout@v3
|
||||
- uses: https://code.forgejo.org/actions/setup-go@v4
|
||||
with:
|
||||
|
|
12
Makefile
12
Makefile
|
@ -88,8 +88,13 @@ STORED_VERSION=$(shell cat $(STORED_VERSION_FILE) 2>/dev/null)
|
|||
ifneq ($(STORED_VERSION),)
|
||||
FORGEJO_VERSION ?= $(STORED_VERSION)
|
||||
else
|
||||
ifneq ($(GITEA_VERSION),)
|
||||
FORGEJO_VERSION ?= $(GITEA_VERSION)
|
||||
FORGEJO_VERSION_API ?= $(GITEA_VERSION)+${GITEA_COMPATIBILITY}
|
||||
else
|
||||
# drop the "g" prefix prepended by git describe to the commit hash
|
||||
FORGEJO_VERSION ?= $(shell git describe --exclude '*-test' --tags --always | sed 's/^v//' | sed 's/\-g/-/')+${GITEA_COMPATIBILITY}
|
||||
endif
|
||||
endif
|
||||
FORGEJO_VERSION_MAJOR=$(shell echo $(FORGEJO_VERSION) | sed -e 's/\..*//')
|
||||
FORGEJO_VERSION_MINOR=$(shell echo $(FORGEJO_VERSION) | sed -E -e 's/^([0-9]+\.[0-9]+).*/\1/')
|
||||
|
@ -106,7 +111,12 @@ show-version-minor:
|
|||
RELEASE_VERSION ?= ${FORGEJO_VERSION}
|
||||
VERSION ?= ${RELEASE_VERSION}
|
||||
|
||||
LDFLAGS := $(LDFLAGS) -X "main.ReleaseVersion=$(RELEASE_VERSION)" -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(FORGEJO_VERSION)" -X "main.Tags=$(TAGS)" -X "main.ForgejoVersion=$(FORGEJO_VERSION)"
|
||||
FORGEJO_VERSION_API ?= ${FORGEJO_VERSION}
|
||||
|
||||
show-version-api:
|
||||
@echo ${FORGEJO_VERSION_API}
|
||||
|
||||
LDFLAGS := $(LDFLAGS) -X "main.ReleaseVersion=$(RELEASE_VERSION)" -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(FORGEJO_VERSION)" -X "main.Tags=$(TAGS)" -X "main.ForgejoVersion=$(FORGEJO_VERSION_API)"
|
||||
|
||||
LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64
|
||||
|
||||
|
|
|
@ -347,11 +347,10 @@ Forgejo or set your environment appropriately.`, "")
|
|||
}
|
||||
|
||||
var out io.Writer
|
||||
var dWriter *delayWriter
|
||||
out = &nilWriter{}
|
||||
if setting.Git.VerbosePush {
|
||||
if setting.Git.VerbosePushDelay > 0 {
|
||||
dWriter = newDelayWriter(os.Stdout, setting.Git.VerbosePushDelay)
|
||||
dWriter := newDelayWriter(os.Stdout, setting.Git.VerbosePushDelay)
|
||||
defer dWriter.Close()
|
||||
out = dWriter
|
||||
} else {
|
||||
|
@ -414,7 +413,6 @@ Forgejo or set your environment appropriately.`, "")
|
|||
hookOptions.RefFullNames = refFullNames
|
||||
resp, extra := private.HookPostReceive(ctx, repoUser, repoName, hookOptions)
|
||||
if extra.HasError() {
|
||||
_ = dWriter.Close()
|
||||
hookPrintResults(results)
|
||||
return fail(ctx, extra.UserMsg, "HookPostReceive failed: %v", extra.Error)
|
||||
}
|
||||
|
@ -434,7 +432,6 @@ Forgejo or set your environment appropriately.`, "")
|
|||
}
|
||||
fmt.Fprintf(out, "Processed %d references in total\n", total)
|
||||
|
||||
_ = dWriter.Close()
|
||||
hookPrintResults(results)
|
||||
return nil
|
||||
}
|
||||
|
@ -447,7 +444,6 @@ Forgejo or set your environment appropriately.`, "")
|
|||
|
||||
resp, extra := private.HookPostReceive(ctx, repoUser, repoName, hookOptions)
|
||||
if resp == nil {
|
||||
_ = dWriter.Close()
|
||||
hookPrintResults(results)
|
||||
return fail(ctx, extra.UserMsg, "HookPostReceive failed: %v", extra.Error)
|
||||
}
|
||||
|
@ -463,9 +459,8 @@ Forgejo or set your environment appropriately.`, "")
|
|||
return fail(ctx, extra.UserMsg, "SetDefaultBranch failed: %v", extra.Error)
|
||||
}
|
||||
}
|
||||
_ = dWriter.Close()
|
||||
hookPrintResults(results)
|
||||
|
||||
hookPrintResults(results)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -7,10 +7,20 @@ import (
|
|||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func TestPktLine(t *testing.T) {
|
||||
|
@ -83,3 +93,72 @@ func TestPktLine(t *testing.T) {
|
|||
assert.Empty(t, w.Bytes())
|
||||
})
|
||||
}
|
||||
|
||||
func TestDelayWriter(t *testing.T) {
|
||||
// Setup the environment.
|
||||
defer test.MockVariableValue(&setting.InternalToken, "Random")()
|
||||
defer test.MockVariableValue(&setting.InstallLock, true)()
|
||||
defer test.MockVariableValue(&setting.Git.VerbosePush, true)()
|
||||
require.NoError(t, os.Setenv("SSH_ORIGINAL_COMMAND", "true"))
|
||||
|
||||
// Setup the Stdin.
|
||||
f, err := os.OpenFile(t.TempDir()+"/stdin", os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o666)
|
||||
require.NoError(t, err)
|
||||
_, err = f.Write([]byte("00000000000000000000 00000000000000000001 refs/head/main\n"))
|
||||
require.NoError(t, err)
|
||||
_, err = f.Seek(0, 0)
|
||||
require.NoError(t, err)
|
||||
defer test.MockVariableValue(os.Stdin, *f)()
|
||||
|
||||
// Setup the server that processes the hooks.
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(time.Millisecond * 600)
|
||||
}))
|
||||
defer ts.Close()
|
||||
defer test.MockVariableValue(&setting.LocalURL, ts.URL+"/")()
|
||||
|
||||
app := cli.NewApp()
|
||||
app.Commands = []*cli.Command{subcmdHookPreReceive}
|
||||
|
||||
// Capture what's being written into stdout
|
||||
captureStdout := func(t *testing.T) (finish func() (output string)) {
|
||||
t.Helper()
|
||||
|
||||
r, w, err := os.Pipe()
|
||||
require.NoError(t, err)
|
||||
resetStdout := test.MockVariableValue(os.Stdout, *w)
|
||||
|
||||
return func() (output string) {
|
||||
w.Close()
|
||||
resetStdout()
|
||||
|
||||
out, err := io.ReadAll(r)
|
||||
require.NoError(t, err)
|
||||
return string(out)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("Should delay", func(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Git.VerbosePushDelay, time.Millisecond*500)()
|
||||
finish := captureStdout(t)
|
||||
|
||||
err = app.Run([]string{"./forgejo", "pre-receive"})
|
||||
require.NoError(t, err)
|
||||
out := finish()
|
||||
|
||||
require.Contains(t, out, "* Checking 1 references")
|
||||
require.Contains(t, out, "Checked 1 references in total")
|
||||
})
|
||||
|
||||
t.Run("Shouldn't delay", func(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Git.VerbosePushDelay, time.Second*5)()
|
||||
finish := captureStdout(t)
|
||||
|
||||
err = app.Run([]string{"./forgejo", "pre-receive"})
|
||||
require.NoError(t, err)
|
||||
out := finish()
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, out)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -2338,6 +2338,8 @@ LEVEL = Info
|
|||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Set the maximum number of characters in a mermaid source. (Set to -1 to disable limits)
|
||||
;MERMAID_MAX_SOURCE_CHARACTERS = 5000
|
||||
;; Set the maximum number of lines allowed for a filepreview. (Set to -1 to disable limits; set to 0 to disable the feature)
|
||||
;FILEPREVIEW_MAX_LINES = 50
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
|
8
go.mod
8
go.mod
|
@ -31,7 +31,7 @@ require (
|
|||
github.com/editorconfig/editorconfig-core-go/v2 v2.6.1
|
||||
github.com/emersion/go-imap v1.2.1
|
||||
github.com/emirpasic/gods v1.18.1
|
||||
github.com/felixge/fgprof v0.9.3
|
||||
github.com/felixge/fgprof v0.9.4
|
||||
github.com/fsnotify/fsnotify v1.7.0
|
||||
github.com/gliderlabs/ssh v0.3.7
|
||||
github.com/go-ap/activitypub v0.0.0-20231114162308-e219254dc5c9
|
||||
|
@ -44,7 +44,7 @@ require (
|
|||
github.com/go-git/go-billy/v5 v5.5.0
|
||||
github.com/go-git/go-git/v5 v5.11.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.6
|
||||
github.com/go-sql-driver/mysql v1.8.0
|
||||
github.com/go-sql-driver/mysql v1.8.1
|
||||
github.com/go-swagger/go-swagger v0.30.5
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.10.0
|
||||
github.com/go-webauthn/webauthn v0.10.0
|
||||
|
@ -53,7 +53,7 @@ require (
|
|||
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/google/go-github/v57 v57.0.0
|
||||
github.com/google/pprof v0.0.0-20240117000934-35fc243c5815
|
||||
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/feeds v1.1.2
|
||||
github.com/gorilla/sessions v1.2.2
|
||||
|
@ -74,7 +74,7 @@ require (
|
|||
github.com/meilisearch/meilisearch-go v0.26.1
|
||||
github.com/mholt/archiver/v3 v3.5.1
|
||||
github.com/microcosm-cc/bluemonday v1.0.26
|
||||
github.com/minio/minio-go/v7 v7.0.66
|
||||
github.com/minio/minio-go/v7 v7.0.69
|
||||
github.com/msteinert/pam v1.2.0
|
||||
github.com/nektos/act v0.2.52
|
||||
github.com/niklasfasching/go-org v1.7.0
|
||||
|
|
32
go.sum
32
go.sum
|
@ -186,9 +186,15 @@ github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj
|
|||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chi-middleware/proxy v1.1.1 h1:4HaXUp8o2+bhHr1OhVy+VjN0+L7/07JDcn6v7YrTjrQ=
|
||||
github.com/chi-middleware/proxy v1.1.1/go.mod h1:jQwMEJct2tz9VmtCELxvnXoMfa+SOdikvbVJVHv/M+0=
|
||||
github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||
github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=
|
||||
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
|
||||
|
@ -256,8 +262,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
|
|||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
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/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
|
||||
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
|
||||
github.com/felixge/fgprof v0.9.4 h1:ocDNwMFlnA0NU0zSB3I52xkO4sFXk80VK9lXjLClu88=
|
||||
github.com/felixge/fgprof v0.9.4/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
||||
|
@ -336,8 +342,8 @@ github.com/go-openapi/validate v0.22.6 h1:+NhuwcEYpWdO5Nm4bmvhGLW0rt1Fcc532Mu3wp
|
|||
github.com/go-openapi/validate v0.22.6/go.mod h1:eaddXSqKeTg5XpSmj1dYyFTK/95n/XHwcOY+BMxKMyM=
|
||||
github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.8.0 h1:UtktXaU2Nb64z/pLiGIxY4431SJ4/dR5cjMmlVHgnT4=
|
||||
github.com/go-sql-driver/mysql v1.8.0/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-swagger/go-swagger v0.30.5 h1:SQ2+xSonWjjoEMOV5tcOnZJVlfyUfCBhGQGArS1b9+U=
|
||||
github.com/go-swagger/go-swagger v0.30.5/go.mod h1:cWUhSyCNqV7J1wkkxfr5QmbcnCewetCdvEXqgPvbc/Q=
|
||||
github.com/go-swagger/scan-repo-boundary v0.0.0-20180623220736-973b3573c013 h1:l9rI6sNaZgNC0LnF3MiE+qTmyBA/tZAg1rtyrGbUMK0=
|
||||
|
@ -353,6 +359,9 @@ github.com/go-webauthn/x v0.1.6 h1:QNAX+AWeqRt9loE8mULeWJCqhVG5D/jvdmJ47fIWCkQ=
|
|||
github.com/go-webauthn/x v0.1.6/go.mod h1:W8dFVZ79o4f+nY1eOUICy/uq5dhrRl7mxQkYhXTo0FA=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||
github.com/goccy/go-json v0.9.5/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.9.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
|
@ -441,9 +450,8 @@ github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hf
|
|||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
|
||||
github.com/google/pprof v0.0.0-20240117000934-35fc243c5815 h1:WzfWbQz/Ze8v6l++GGbGNFZnUShVpP/0xffCPLL+ax8=
|
||||
github.com/google/pprof v0.0.0-20240117000934-35fc243c5815/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
|
||||
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 h1:y3N7Bm7Y9/CtpiVkw/ZWj6lSlDF3F74SfKwfTCer72Q=
|
||||
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
|
@ -494,7 +502,7 @@ github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq
|
|||
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
|
||||
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
|
||||
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
|
||||
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
||||
|
@ -567,6 +575,7 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
|
||||
github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
|
||||
github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE=
|
||||
|
@ -614,8 +623,8 @@ github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
|
|||
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
|
||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.0.66 h1:bnTOXOHjOqv/gcMuiVbN9o2ngRItvqE774dG9nq0Dzw=
|
||||
github.com/minio/minio-go/v7 v7.0.66/go.mod h1:DHAgmyQEGdW3Cif0UooKOyrT3Vxs82zNdV6tkKhRtbs=
|
||||
github.com/minio/minio-go/v7 v7.0.69 h1:l8AnsQFyY1xiwa/DaQskY4NXSLA2yrGsW5iD9nRPVS0=
|
||||
github.com/minio/minio-go/v7 v7.0.69/go.mod h1:XAvOPJQ5Xlzk5o3o/ArO2NMbhSGkimC+bpW/ngRKDmQ=
|
||||
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
||||
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
|
||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||
|
@ -670,6 +679,7 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
|
|||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||
github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU=
|
||||
github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
|
||||
github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
|
||||
|
@ -1034,9 +1044,9 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
|
|
@ -5,18 +5,10 @@ package asymkey
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"hash"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/keybase/go-crypto/openpgp/packet"
|
||||
)
|
||||
|
||||
// __________________ ________ ____ __.
|
||||
|
@ -40,45 +32,22 @@ import (
|
|||
|
||||
// This file provides functions relating commit verification
|
||||
|
||||
// CommitVerification represents a commit validation of signature
|
||||
type CommitVerification struct {
|
||||
Verified bool
|
||||
Warning bool
|
||||
Reason string
|
||||
SigningUser *user_model.User
|
||||
CommittingUser *user_model.User
|
||||
SigningEmail string
|
||||
SigningKey *GPGKey
|
||||
SigningSSHKey *PublicKey
|
||||
TrustStatus string
|
||||
}
|
||||
|
||||
// SignCommit represents a commit with validation of signature.
|
||||
type SignCommit struct {
|
||||
Verification *CommitVerification
|
||||
Verification *ObjectVerification
|
||||
*user_model.UserCommit
|
||||
}
|
||||
|
||||
const (
|
||||
// BadSignature is used as the reason when the signature has a KeyID that is in the db
|
||||
// but no key that has that ID verifies the signature. This is a suspicious failure.
|
||||
BadSignature = "gpg.error.probable_bad_signature"
|
||||
// BadDefaultSignature is used as the reason when the signature has a KeyID that matches the
|
||||
// default Key but is not verified by the default key. This is a suspicious failure.
|
||||
BadDefaultSignature = "gpg.error.probable_bad_default_signature"
|
||||
// NoKeyFound is used as the reason when no key can be found to verify the signature.
|
||||
NoKeyFound = "gpg.error.no_gpg_keys_found"
|
||||
)
|
||||
|
||||
// ParseCommitsWithSignature checks if signaute of commits are corresponding to users gpg keys.
|
||||
func ParseCommitsWithSignature(ctx context.Context, oldCommits []*user_model.UserCommit, repoTrustModel repo_model.TrustModelType, isOwnerMemberCollaborator func(*user_model.User) (bool, error)) []*SignCommit {
|
||||
newCommits := make([]*SignCommit, 0, len(oldCommits))
|
||||
keyMap := map[string]bool{}
|
||||
|
||||
for _, c := range oldCommits {
|
||||
o := commitToGitObject(c.Commit)
|
||||
signCommit := &SignCommit{
|
||||
UserCommit: c,
|
||||
Verification: ParseCommitWithSignature(ctx, c.Commit),
|
||||
Verification: ParseObjectWithSignature(ctx, &o),
|
||||
}
|
||||
|
||||
_ = CalculateTrustStatus(signCommit.Verification, repoTrustModel, isOwnerMemberCollaborator, &keyMap)
|
||||
|
@ -88,456 +57,7 @@ func ParseCommitsWithSignature(ctx context.Context, oldCommits []*user_model.Use
|
|||
return newCommits
|
||||
}
|
||||
|
||||
// ParseCommitWithSignature check if signature is good against keystore.
|
||||
func ParseCommitWithSignature(ctx context.Context, c *git.Commit) *CommitVerification {
|
||||
var committer *user_model.User
|
||||
if c.Committer != nil {
|
||||
var err error
|
||||
// Find Committer account
|
||||
committer, err = user_model.GetUserByEmail(ctx, c.Committer.Email) // This finds the user by primary email or activated email so commit will not be valid if email is not
|
||||
if err != nil { // Skipping not user for committer
|
||||
committer = &user_model.User{
|
||||
Name: c.Committer.Name,
|
||||
Email: c.Committer.Email,
|
||||
}
|
||||
// We can expect this to often be an ErrUserNotExist. in the case
|
||||
// it is not, however, it is important to log it.
|
||||
if !user_model.IsErrUserNotExist(err) {
|
||||
log.Error("GetUserByEmail: %v", err)
|
||||
return &CommitVerification{
|
||||
CommittingUser: committer,
|
||||
Verified: false,
|
||||
Reason: "gpg.error.no_committer_account",
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// If no signature just report the committer
|
||||
if c.Signature == nil {
|
||||
return &CommitVerification{
|
||||
CommittingUser: committer,
|
||||
Verified: false, // Default value
|
||||
Reason: "gpg.error.not_signed_commit", // Default value
|
||||
}
|
||||
}
|
||||
|
||||
// If this a SSH signature handle it differently
|
||||
if strings.HasPrefix(c.Signature.Signature, "-----BEGIN SSH SIGNATURE-----") {
|
||||
return ParseCommitWithSSHSignature(ctx, c, committer)
|
||||
}
|
||||
|
||||
// Parsing signature
|
||||
sig, err := extractSignature(c.Signature.Signature)
|
||||
if err != nil { // Skipping failed to extract sign
|
||||
log.Error("SignatureRead err: %v", err)
|
||||
return &CommitVerification{
|
||||
CommittingUser: committer,
|
||||
Verified: false,
|
||||
Reason: "gpg.error.extract_sign",
|
||||
}
|
||||
}
|
||||
|
||||
keyID := ""
|
||||
if sig.IssuerKeyId != nil && (*sig.IssuerKeyId) != 0 {
|
||||
keyID = fmt.Sprintf("%X", *sig.IssuerKeyId)
|
||||
}
|
||||
if keyID == "" && sig.IssuerFingerprint != nil && len(sig.IssuerFingerprint) > 0 {
|
||||
keyID = fmt.Sprintf("%X", sig.IssuerFingerprint[12:20])
|
||||
}
|
||||
defaultReason := NoKeyFound
|
||||
|
||||
// First check if the sig has a keyID and if so just look at that
|
||||
if commitVerification := hashAndVerifyForKeyID(
|
||||
ctx,
|
||||
sig,
|
||||
c.Signature.Payload,
|
||||
committer,
|
||||
keyID,
|
||||
setting.AppName,
|
||||
""); commitVerification != nil {
|
||||
if commitVerification.Reason == BadSignature {
|
||||
defaultReason = BadSignature
|
||||
} else {
|
||||
return commitVerification
|
||||
}
|
||||
}
|
||||
|
||||
// Now try to associate the signature with the committer, if present
|
||||
if committer.ID != 0 {
|
||||
keys, err := db.Find[GPGKey](ctx, FindGPGKeyOptions{
|
||||
OwnerID: committer.ID,
|
||||
})
|
||||
if err != nil { // Skipping failed to get gpg keys of user
|
||||
log.Error("ListGPGKeys: %v", err)
|
||||
return &CommitVerification{
|
||||
CommittingUser: committer,
|
||||
Verified: false,
|
||||
Reason: "gpg.error.failed_retrieval_gpg_keys",
|
||||
}
|
||||
}
|
||||
|
||||
if err := GPGKeyList(keys).LoadSubKeys(ctx); err != nil {
|
||||
log.Error("LoadSubKeys: %v", err)
|
||||
return &CommitVerification{
|
||||
CommittingUser: committer,
|
||||
Verified: false,
|
||||
Reason: "gpg.error.failed_retrieval_gpg_keys",
|
||||
}
|
||||
}
|
||||
|
||||
committerEmailAddresses, _ := user_model.GetEmailAddresses(ctx, committer.ID)
|
||||
activated := false
|
||||
for _, e := range committerEmailAddresses {
|
||||
if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) {
|
||||
activated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, k := range keys {
|
||||
// Pre-check (& optimization) that emails attached to key can be attached to the committer email and can validate
|
||||
canValidate := false
|
||||
email := ""
|
||||
if k.Verified && activated {
|
||||
canValidate = true
|
||||
email = c.Committer.Email
|
||||
}
|
||||
if !canValidate {
|
||||
for _, e := range k.Emails {
|
||||
if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) {
|
||||
canValidate = true
|
||||
email = e.Email
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !canValidate {
|
||||
continue // Skip this key
|
||||
}
|
||||
|
||||
commitVerification := hashAndVerifyWithSubKeysCommitVerification(sig, c.Signature.Payload, k, committer, committer, email)
|
||||
if commitVerification != nil {
|
||||
return commitVerification
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if setting.Repository.Signing.SigningKey != "" && setting.Repository.Signing.SigningKey != "default" && setting.Repository.Signing.SigningKey != "none" {
|
||||
// OK we should try the default key
|
||||
gpgSettings := git.GPGSettings{
|
||||
Sign: true,
|
||||
KeyID: setting.Repository.Signing.SigningKey,
|
||||
Name: setting.Repository.Signing.SigningName,
|
||||
Email: setting.Repository.Signing.SigningEmail,
|
||||
}
|
||||
if err := gpgSettings.LoadPublicKeyContent(); err != nil {
|
||||
log.Error("Error getting default signing key: %s %v", gpgSettings.KeyID, err)
|
||||
} else if commitVerification := verifyWithGPGSettings(ctx, &gpgSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil {
|
||||
if commitVerification.Reason == BadSignature {
|
||||
defaultReason = BadSignature
|
||||
} else {
|
||||
return commitVerification
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defaultGPGSettings, err := c.GetRepositoryDefaultPublicGPGKey(false)
|
||||
if err != nil {
|
||||
log.Error("Error getting default public gpg key: %v", err)
|
||||
} else if defaultGPGSettings == nil {
|
||||
log.Warn("Unable to get defaultGPGSettings for unattached commit: %s", c.ID.String())
|
||||
} else if defaultGPGSettings.Sign {
|
||||
if commitVerification := verifyWithGPGSettings(ctx, defaultGPGSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil {
|
||||
if commitVerification.Reason == BadSignature {
|
||||
defaultReason = BadSignature
|
||||
} else {
|
||||
return commitVerification
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &CommitVerification{ // Default at this stage
|
||||
CommittingUser: committer,
|
||||
Verified: false,
|
||||
Warning: defaultReason != NoKeyFound,
|
||||
Reason: defaultReason,
|
||||
SigningKey: &GPGKey{
|
||||
KeyID: keyID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func verifyWithGPGSettings(ctx context.Context, gpgSettings *git.GPGSettings, sig *packet.Signature, payload string, committer *user_model.User, keyID string) *CommitVerification {
|
||||
// First try to find the key in the db
|
||||
if commitVerification := hashAndVerifyForKeyID(ctx, sig, payload, committer, gpgSettings.KeyID, gpgSettings.Name, gpgSettings.Email); commitVerification != nil {
|
||||
return commitVerification
|
||||
}
|
||||
|
||||
// Otherwise we have to parse the key
|
||||
ekeys, err := checkArmoredGPGKeyString(gpgSettings.PublicKeyContent)
|
||||
if err != nil {
|
||||
log.Error("Unable to get default signing key: %v", err)
|
||||
return &CommitVerification{
|
||||
CommittingUser: committer,
|
||||
Verified: false,
|
||||
Reason: "gpg.error.generate_hash",
|
||||
}
|
||||
}
|
||||
for _, ekey := range ekeys {
|
||||
pubkey := ekey.PrimaryKey
|
||||
content, err := base64EncPubKey(pubkey)
|
||||
if err != nil {
|
||||
return &CommitVerification{
|
||||
CommittingUser: committer,
|
||||
Verified: false,
|
||||
Reason: "gpg.error.generate_hash",
|
||||
}
|
||||
}
|
||||
k := &GPGKey{
|
||||
Content: content,
|
||||
CanSign: pubkey.CanSign(),
|
||||
KeyID: pubkey.KeyIdString(),
|
||||
}
|
||||
for _, subKey := range ekey.Subkeys {
|
||||
content, err := base64EncPubKey(subKey.PublicKey)
|
||||
if err != nil {
|
||||
return &CommitVerification{
|
||||
CommittingUser: committer,
|
||||
Verified: false,
|
||||
Reason: "gpg.error.generate_hash",
|
||||
}
|
||||
}
|
||||
k.SubsKey = append(k.SubsKey, &GPGKey{
|
||||
Content: content,
|
||||
CanSign: subKey.PublicKey.CanSign(),
|
||||
KeyID: subKey.PublicKey.KeyIdString(),
|
||||
})
|
||||
}
|
||||
if commitVerification := hashAndVerifyWithSubKeysCommitVerification(sig, payload, k, committer, &user_model.User{
|
||||
Name: gpgSettings.Name,
|
||||
Email: gpgSettings.Email,
|
||||
}, gpgSettings.Email); commitVerification != nil {
|
||||
return commitVerification
|
||||
}
|
||||
if keyID == k.KeyID {
|
||||
// This is a bad situation ... We have a key id that matches our default key but the signature doesn't match.
|
||||
return &CommitVerification{
|
||||
CommittingUser: committer,
|
||||
Verified: false,
|
||||
Warning: true,
|
||||
Reason: BadSignature,
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func verifySign(s *packet.Signature, h hash.Hash, k *GPGKey) error {
|
||||
// Check if key can sign
|
||||
if !k.CanSign {
|
||||
return fmt.Errorf("key can not sign")
|
||||
}
|
||||
// Decode key
|
||||
pkey, err := base64DecPubKey(k.Content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return pkey.VerifySignature(h, s)
|
||||
}
|
||||
|
||||
func hashAndVerify(sig *packet.Signature, payload string, k *GPGKey) (*GPGKey, error) {
|
||||
// Generating hash of commit
|
||||
hash, err := populateHash(sig.Hash, []byte(payload))
|
||||
if err != nil { // Skipping as failed to generate hash
|
||||
log.Error("PopulateHash: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
// We will ignore errors in verification as they don't need to be propagated up
|
||||
err = verifySign(sig, hash, k)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
return k, nil
|
||||
}
|
||||
|
||||
func hashAndVerifyWithSubKeys(sig *packet.Signature, payload string, k *GPGKey) (*GPGKey, error) {
|
||||
verified, err := hashAndVerify(sig, payload, k)
|
||||
if err != nil || verified != nil {
|
||||
return verified, err
|
||||
}
|
||||
for _, sk := range k.SubsKey {
|
||||
verified, err := hashAndVerify(sig, payload, sk)
|
||||
if err != nil || verified != nil {
|
||||
return verified, err
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func hashAndVerifyWithSubKeysCommitVerification(sig *packet.Signature, payload string, k *GPGKey, committer, signer *user_model.User, email string) *CommitVerification {
|
||||
key, err := hashAndVerifyWithSubKeys(sig, payload, k)
|
||||
if err != nil { // Skipping failed to generate hash
|
||||
return &CommitVerification{
|
||||
CommittingUser: committer,
|
||||
Verified: false,
|
||||
Reason: "gpg.error.generate_hash",
|
||||
}
|
||||
}
|
||||
|
||||
if key != nil {
|
||||
return &CommitVerification{ // Everything is ok
|
||||
CommittingUser: committer,
|
||||
Verified: true,
|
||||
Reason: fmt.Sprintf("%s / %s", signer.Name, key.KeyID),
|
||||
SigningUser: signer,
|
||||
SigningKey: key,
|
||||
SigningEmail: email,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hashAndVerifyForKeyID(ctx context.Context, sig *packet.Signature, payload string, committer *user_model.User, keyID, name, email string) *CommitVerification {
|
||||
if keyID == "" {
|
||||
return nil
|
||||
}
|
||||
keys, err := db.Find[GPGKey](ctx, FindGPGKeyOptions{
|
||||
KeyID: keyID,
|
||||
IncludeSubKeys: true,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("GetGPGKeysByKeyID: %v", err)
|
||||
return &CommitVerification{
|
||||
CommittingUser: committer,
|
||||
Verified: false,
|
||||
Reason: "gpg.error.failed_retrieval_gpg_keys",
|
||||
}
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, key := range keys {
|
||||
var primaryKeys []*GPGKey
|
||||
if key.PrimaryKeyID != "" {
|
||||
primaryKeys, err = db.Find[GPGKey](ctx, FindGPGKeyOptions{
|
||||
KeyID: key.PrimaryKeyID,
|
||||
IncludeSubKeys: true,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("GetGPGKeysByKeyID: %v", err)
|
||||
return &CommitVerification{
|
||||
CommittingUser: committer,
|
||||
Verified: false,
|
||||
Reason: "gpg.error.failed_retrieval_gpg_keys",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
activated, email := checkKeyEmails(ctx, email, append([]*GPGKey{key}, primaryKeys...)...)
|
||||
if !activated {
|
||||
continue
|
||||
}
|
||||
|
||||
signer := &user_model.User{
|
||||
Name: name,
|
||||
Email: email,
|
||||
}
|
||||
if key.OwnerID != 0 {
|
||||
owner, err := user_model.GetUserByID(ctx, key.OwnerID)
|
||||
if err == nil {
|
||||
signer = owner
|
||||
} else if !user_model.IsErrUserNotExist(err) {
|
||||
log.Error("Failed to user_model.GetUserByID: %d for key ID: %d (%s) %v", key.OwnerID, key.ID, key.KeyID, err)
|
||||
return &CommitVerification{
|
||||
CommittingUser: committer,
|
||||
Verified: false,
|
||||
Reason: "gpg.error.no_committer_account",
|
||||
}
|
||||
}
|
||||
}
|
||||
commitVerification := hashAndVerifyWithSubKeysCommitVerification(sig, payload, key, committer, signer, email)
|
||||
if commitVerification != nil {
|
||||
return commitVerification
|
||||
}
|
||||
}
|
||||
// This is a bad situation ... We have a key id that is in our database but the signature doesn't match.
|
||||
return &CommitVerification{
|
||||
CommittingUser: committer,
|
||||
Verified: false,
|
||||
Warning: true,
|
||||
Reason: BadSignature,
|
||||
}
|
||||
}
|
||||
|
||||
// CalculateTrustStatus will calculate the TrustStatus for a commit verification within a repository
|
||||
// There are several trust models in Gitea
|
||||
func CalculateTrustStatus(verification *CommitVerification, repoTrustModel repo_model.TrustModelType, isOwnerMemberCollaborator func(*user_model.User) (bool, error), keyMap *map[string]bool) error {
|
||||
if !verification.Verified {
|
||||
return nil
|
||||
}
|
||||
|
||||
// In the Committer trust model a signature is trusted if it matches the committer
|
||||
// - it doesn't matter if they're a collaborator, the owner, Gitea or Github
|
||||
// NB: This model is commit verification only
|
||||
if repoTrustModel == repo_model.CommitterTrustModel {
|
||||
// default to "unmatched"
|
||||
verification.TrustStatus = "unmatched"
|
||||
|
||||
// We can only verify against users in our database but the default key will match
|
||||
// against by email if it is not in the db.
|
||||
if (verification.SigningUser.ID != 0 &&
|
||||
verification.CommittingUser.ID == verification.SigningUser.ID) ||
|
||||
(verification.SigningUser.ID == 0 && verification.CommittingUser.ID == 0 &&
|
||||
verification.SigningUser.Email == verification.CommittingUser.Email) {
|
||||
verification.TrustStatus = "trusted"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Now we drop to the more nuanced trust models...
|
||||
verification.TrustStatus = "trusted"
|
||||
|
||||
if verification.SigningUser.ID == 0 {
|
||||
// This commit is signed by the default key - but this key is not assigned to a user in the DB.
|
||||
|
||||
// However in the repo_model.CollaboratorCommitterTrustModel we cannot mark this as trusted
|
||||
// unless the default key matches the email of a non-user.
|
||||
if repoTrustModel == repo_model.CollaboratorCommitterTrustModel && (verification.CommittingUser.ID != 0 ||
|
||||
verification.SigningUser.Email != verification.CommittingUser.Email) {
|
||||
verification.TrustStatus = "untrusted"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check we actually have a GPG SigningKey
|
||||
var err error
|
||||
if verification.SigningKey != nil {
|
||||
var isMember bool
|
||||
if keyMap != nil {
|
||||
var has bool
|
||||
isMember, has = (*keyMap)[verification.SigningKey.KeyID]
|
||||
if !has {
|
||||
isMember, err = isOwnerMemberCollaborator(verification.SigningUser)
|
||||
(*keyMap)[verification.SigningKey.KeyID] = isMember
|
||||
}
|
||||
} else {
|
||||
isMember, err = isOwnerMemberCollaborator(verification.SigningUser)
|
||||
}
|
||||
|
||||
if !isMember {
|
||||
verification.TrustStatus = "untrusted"
|
||||
if verification.CommittingUser.ID != verification.SigningUser.ID {
|
||||
// The committing user and the signing user are not the same
|
||||
// This should be marked as questionable unless the signing user is a collaborator/team member etc.
|
||||
verification.TrustStatus = "unmatched"
|
||||
}
|
||||
} else if repoTrustModel == repo_model.CollaboratorCommitterTrustModel && verification.CommittingUser.ID != verification.SigningUser.ID {
|
||||
// The committing user and the signing user are not the same and our trustmodel states that they must match
|
||||
verification.TrustStatus = "unmatched"
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
func ParseCommitWithSignature(ctx context.Context, c *git.Commit) *ObjectVerification {
|
||||
o := commitToGitObject(c)
|
||||
return ParseObjectWithSignature(ctx, &o)
|
||||
}
|
||||
|
|
527
models/asymkey/gpg_key_object_verification.go
Normal file
527
models/asymkey/gpg_key_object_verification.go
Normal file
|
@ -0,0 +1,527 @@
|
|||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package asymkey
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"hash"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/keybase/go-crypto/openpgp/packet"
|
||||
)
|
||||
|
||||
// This file provides functions related to object (commit, tag) verification
|
||||
|
||||
// ObjectVerification represents a commit validation of signature
|
||||
type ObjectVerification struct {
|
||||
Verified bool
|
||||
Warning bool
|
||||
Reason string
|
||||
SigningUser *user_model.User
|
||||
CommittingUser *user_model.User
|
||||
SigningEmail string
|
||||
SigningKey *GPGKey
|
||||
SigningSSHKey *PublicKey
|
||||
TrustStatus string
|
||||
}
|
||||
|
||||
const (
|
||||
// BadSignature is used as the reason when the signature has a KeyID that is in the db
|
||||
// but no key that has that ID verifies the signature. This is a suspicious failure.
|
||||
BadSignature = "gpg.error.probable_bad_signature"
|
||||
// BadDefaultSignature is used as the reason when the signature has a KeyID that matches the
|
||||
// default Key but is not verified by the default key. This is a suspicious failure.
|
||||
BadDefaultSignature = "gpg.error.probable_bad_default_signature"
|
||||
// NoKeyFound is used as the reason when no key can be found to verify the signature.
|
||||
NoKeyFound = "gpg.error.no_gpg_keys_found"
|
||||
)
|
||||
|
||||
type GitObject struct {
|
||||
ID git.ObjectID
|
||||
Committer *git.Signature
|
||||
Signature *git.ObjectSignature
|
||||
Commit *git.Commit
|
||||
}
|
||||
|
||||
func commitToGitObject(c *git.Commit) GitObject {
|
||||
return GitObject{
|
||||
ID: c.ID,
|
||||
Committer: c.Committer,
|
||||
Signature: c.Signature,
|
||||
Commit: c,
|
||||
}
|
||||
}
|
||||
|
||||
func tagToGitObject(t *git.Tag, gitRepo *git.Repository) GitObject {
|
||||
commit, _ := t.Commit(gitRepo)
|
||||
return GitObject{
|
||||
ID: t.ID,
|
||||
Committer: t.Tagger,
|
||||
Signature: t.Signature,
|
||||
Commit: commit,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseObjectWithSignature check if signature is good against keystore.
|
||||
func ParseObjectWithSignature(ctx context.Context, c *GitObject) *ObjectVerification {
|
||||
var committer *user_model.User
|
||||
if c.Committer != nil {
|
||||
var err error
|
||||
// Find Committer account
|
||||
committer, err = user_model.GetUserByEmail(ctx, c.Committer.Email) // This finds the user by primary email or activated email so commit will not be valid if email is not
|
||||
if err != nil { // Skipping not user for committer
|
||||
committer = &user_model.User{
|
||||
Name: c.Committer.Name,
|
||||
Email: c.Committer.Email,
|
||||
}
|
||||
// We can expect this to often be an ErrUserNotExist. in the case
|
||||
// it is not, however, it is important to log it.
|
||||
if !user_model.IsErrUserNotExist(err) {
|
||||
log.Error("GetUserByEmail: %v", err)
|
||||
return &ObjectVerification{
|
||||
CommittingUser: committer,
|
||||
Verified: false,
|
||||
Reason: "gpg.error.no_committer_account",
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// If no signature just report the committer
|
||||
if c.Signature == nil {
|
||||
return &ObjectVerification{
|
||||
CommittingUser: committer,
|
||||
Verified: false, // Default value
|
||||
Reason: "gpg.error.not_signed_commit", // Default value
|
||||
}
|
||||
}
|
||||
|
||||
// If this a SSH signature handle it differently
|
||||
if strings.HasPrefix(c.Signature.Signature, "-----BEGIN SSH SIGNATURE-----") {
|
||||
return ParseObjectWithSSHSignature(ctx, c, committer)
|
||||
}
|
||||
|
||||
// Parsing signature
|
||||
sig, err := extractSignature(c.Signature.Signature)
|
||||
if err != nil { // Skipping failed to extract sign
|
||||
log.Error("SignatureRead err: %v", err)
|
||||
return &ObjectVerification{
|
||||
CommittingUser: committer,
|
||||
Verified: false,
|
||||
Reason: "gpg.error.extract_sign",
|
||||
}
|
||||
}
|
||||
|
||||
keyID := ""
|
||||
if sig.IssuerKeyId != nil && (*sig.IssuerKeyId) != 0 {
|
||||
keyID = fmt.Sprintf("%X", *sig.IssuerKeyId)
|
||||
}
|
||||
if keyID == "" && sig.IssuerFingerprint != nil && len(sig.IssuerFingerprint) > 0 {
|
||||
keyID = fmt.Sprintf("%X", sig.IssuerFingerprint[12:20])
|
||||
}
|
||||
defaultReason := NoKeyFound
|
||||
|
||||
// First check if the sig has a keyID and if so just look at that
|
||||
if commitVerification := hashAndVerifyForKeyID(
|
||||
ctx,
|
||||
sig,
|
||||
c.Signature.Payload,
|
||||
committer,
|
||||
keyID,
|
||||
setting.AppName,
|
||||
""); commitVerification != nil {
|
||||
if commitVerification.Reason == BadSignature {
|
||||
defaultReason = BadSignature
|
||||
} else {
|
||||
return commitVerification
|
||||
}
|
||||
}
|
||||
|
||||
// Now try to associate the signature with the committer, if present
|
||||
if committer.ID != 0 {
|
||||
keys, err := db.Find[GPGKey](ctx, FindGPGKeyOptions{
|
||||
OwnerID: committer.ID,
|
||||
})
|
||||
if err != nil { // Skipping failed to get gpg keys of user
|
||||
log.Error("ListGPGKeys: %v", err)
|
||||
return &ObjectVerification{
|
||||
CommittingUser: committer,
|
||||
Verified: false,
|
||||
Reason: "gpg.error.failed_retrieval_gpg_keys",
|
||||
}
|
||||
}
|
||||
|
||||
if err := GPGKeyList(keys).LoadSubKeys(ctx); err != nil {
|
||||
log.Error("LoadSubKeys: %v", err)
|
||||
return &ObjectVerification{
|
||||
CommittingUser: committer,
|
||||
Verified: false,
|
||||
Reason: "gpg.error.failed_retrieval_gpg_keys",
|
||||
}
|
||||
}
|
||||
|
||||
committerEmailAddresses, _ := user_model.GetEmailAddresses(ctx, committer.ID)
|
||||
activated := false
|
||||
for _, e := range committerEmailAddresses {
|
||||
if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) {
|
||||
activated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, k := range keys {
|
||||
// Pre-check (& optimization) that emails attached to key can be attached to the committer email and can validate
|
||||
canValidate := false
|
||||
email := ""
|
||||
if k.Verified && activated {
|
||||
canValidate = true
|
||||
email = c.Committer.Email
|
||||
}
|
||||
if !canValidate {
|
||||
for _, e := range k.Emails {
|
||||
if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) {
|
||||
canValidate = true
|
||||
email = e.Email
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !canValidate {
|
||||
continue // Skip this key
|
||||
}
|
||||
|
||||
commitVerification := hashAndVerifyWithSubKeysObjectVerification(sig, c.Signature.Payload, k, committer, committer, email)
|
||||
if commitVerification != nil {
|
||||
return commitVerification
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if setting.Repository.Signing.SigningKey != "" && setting.Repository.Signing.SigningKey != "default" && setting.Repository.Signing.SigningKey != "none" {
|
||||
// OK we should try the default key
|
||||
gpgSettings := git.GPGSettings{
|
||||
Sign: true,
|
||||
KeyID: setting.Repository.Signing.SigningKey,
|
||||
Name: setting.Repository.Signing.SigningName,
|
||||
Email: setting.Repository.Signing.SigningEmail,
|
||||
}
|
||||
if err := gpgSettings.LoadPublicKeyContent(); err != nil {
|
||||
log.Error("Error getting default signing key: %s %v", gpgSettings.KeyID, err)
|
||||
} else if commitVerification := verifyWithGPGSettings(ctx, &gpgSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil {
|
||||
if commitVerification.Reason == BadSignature {
|
||||
defaultReason = BadSignature
|
||||
} else {
|
||||
return commitVerification
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defaultGPGSettings, err := c.Commit.GetRepositoryDefaultPublicGPGKey(false)
|
||||
if err != nil {
|
||||
log.Error("Error getting default public gpg key: %v", err)
|
||||
} else if defaultGPGSettings == nil {
|
||||
log.Warn("Unable to get defaultGPGSettings for unattached commit: %s", c.Commit.ID.String())
|
||||
} else if defaultGPGSettings.Sign {
|
||||
if commitVerification := verifyWithGPGSettings(ctx, defaultGPGSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil {
|
||||
if commitVerification.Reason == BadSignature {
|
||||
defaultReason = BadSignature
|
||||
} else {
|
||||
return commitVerification
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &ObjectVerification{ // Default at this stage
|
||||
CommittingUser: committer,
|
||||
Verified: false,
|
||||
Warning: defaultReason != NoKeyFound,
|
||||
Reason: defaultReason,
|
||||
SigningKey: &GPGKey{
|
||||
KeyID: keyID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func verifyWithGPGSettings(ctx context.Context, gpgSettings *git.GPGSettings, sig *packet.Signature, payload string, committer *user_model.User, keyID string) *ObjectVerification {
|
||||
// First try to find the key in the db
|
||||
if commitVerification := hashAndVerifyForKeyID(ctx, sig, payload, committer, gpgSettings.KeyID, gpgSettings.Name, gpgSettings.Email); commitVerification != nil {
|
||||
return commitVerification
|
||||
}
|
||||
|
||||
// Otherwise we have to parse the key
|
||||
ekeys, err := checkArmoredGPGKeyString(gpgSettings.PublicKeyContent)
|
||||
if err != nil {
|
||||
log.Error("Unable to get default signing key: %v", err)
|
||||
return &ObjectVerification{
|
||||
CommittingUser: committer,
|
||||
Verified: false,
|
||||
Reason: "gpg.error.generate_hash",
|
||||
}
|
||||
}
|
||||
for _, ekey := range ekeys {
|
||||
pubkey := ekey.PrimaryKey
|
||||
content, err := base64EncPubKey(pubkey)
|
||||
if err != nil {
|
||||
return &ObjectVerification{
|
||||
CommittingUser: committer,
|
||||
Verified: false,
|
||||
Reason: "gpg.error.generate_hash",
|
||||
}
|
||||
}
|
||||
k := &GPGKey{
|
||||
Content: content,
|
||||
CanSign: pubkey.CanSign(),
|
||||
KeyID: pubkey.KeyIdString(),
|
||||
}
|
||||
for _, subKey := range ekey.Subkeys {
|
||||
content, err := base64EncPubKey(subKey.PublicKey)
|
||||
if err != nil {
|
||||
return &ObjectVerification{
|
||||
CommittingUser: committer,
|
||||
Verified: false,
|
||||
Reason: "gpg.error.generate_hash",
|
||||
}
|
||||
}
|
||||
k.SubsKey = append(k.SubsKey, &GPGKey{
|
||||
Content: content,
|
||||
CanSign: subKey.PublicKey.CanSign(),
|
||||
KeyID: subKey.PublicKey.KeyIdString(),
|
||||
})
|
||||
}
|
||||
if commitVerification := hashAndVerifyWithSubKeysObjectVerification(sig, payload, k, committer, &user_model.User{
|
||||
Name: gpgSettings.Name,
|
||||
Email: gpgSettings.Email,
|
||||
}, gpgSettings.Email); commitVerification != nil {
|
||||
return commitVerification
|
||||
}
|
||||
if keyID == k.KeyID {
|
||||
// This is a bad situation ... We have a key id that matches our default key but the signature doesn't match.
|
||||
return &ObjectVerification{
|
||||
CommittingUser: committer,
|
||||
Verified: false,
|
||||
Warning: true,
|
||||
Reason: BadSignature,
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func verifySign(s *packet.Signature, h hash.Hash, k *GPGKey) error {
|
||||
// Check if key can sign
|
||||
if !k.CanSign {
|
||||
return fmt.Errorf("key can not sign")
|
||||
}
|
||||
// Decode key
|
||||
pkey, err := base64DecPubKey(k.Content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return pkey.VerifySignature(h, s)
|
||||
}
|
||||
|
||||
func hashAndVerify(sig *packet.Signature, payload string, k *GPGKey) (*GPGKey, error) {
|
||||
// Generating hash of commit
|
||||
hash, err := populateHash(sig.Hash, []byte(payload))
|
||||
if err != nil { // Skipping as failed to generate hash
|
||||
log.Error("PopulateHash: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
// We will ignore errors in verification as they don't need to be propagated up
|
||||
err = verifySign(sig, hash, k)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
return k, nil
|
||||
}
|
||||
|
||||
func hashAndVerifyWithSubKeys(sig *packet.Signature, payload string, k *GPGKey) (*GPGKey, error) {
|
||||
verified, err := hashAndVerify(sig, payload, k)
|
||||
if err != nil || verified != nil {
|
||||
return verified, err
|
||||
}
|
||||
for _, sk := range k.SubsKey {
|
||||
verified, err := hashAndVerify(sig, payload, sk)
|
||||
if err != nil || verified != nil {
|
||||
return verified, err
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func hashAndVerifyWithSubKeysObjectVerification(sig *packet.Signature, payload string, k *GPGKey, committer, signer *user_model.User, email string) *ObjectVerification {
|
||||
key, err := hashAndVerifyWithSubKeys(sig, payload, k)
|
||||
if err != nil { // Skipping failed to generate hash
|
||||
return &ObjectVerification{
|
||||
CommittingUser: committer,
|
||||
Verified: false,
|
||||
Reason: "gpg.error.generate_hash",
|
||||
}
|
||||
}
|
||||
|
||||
if key != nil {
|
||||
return &ObjectVerification{ // Everything is ok
|
||||
CommittingUser: committer,
|
||||
Verified: true,
|
||||
Reason: fmt.Sprintf("%s / %s", signer.Name, key.KeyID),
|
||||
SigningUser: signer,
|
||||
SigningKey: key,
|
||||
SigningEmail: email,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hashAndVerifyForKeyID(ctx context.Context, sig *packet.Signature, payload string, committer *user_model.User, keyID, name, email string) *ObjectVerification {
|
||||
if keyID == "" {
|
||||
return nil
|
||||
}
|
||||
keys, err := db.Find[GPGKey](ctx, FindGPGKeyOptions{
|
||||
KeyID: keyID,
|
||||
IncludeSubKeys: true,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("GetGPGKeysByKeyID: %v", err)
|
||||
return &ObjectVerification{
|
||||
CommittingUser: committer,
|
||||
Verified: false,
|
||||
Reason: "gpg.error.failed_retrieval_gpg_keys",
|
||||
}
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, key := range keys {
|
||||
var primaryKeys []*GPGKey
|
||||
if key.PrimaryKeyID != "" {
|
||||
primaryKeys, err = db.Find[GPGKey](ctx, FindGPGKeyOptions{
|
||||
KeyID: key.PrimaryKeyID,
|
||||
IncludeSubKeys: true,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("GetGPGKeysByKeyID: %v", err)
|
||||
return &ObjectVerification{
|
||||
CommittingUser: committer,
|
||||
Verified: false,
|
||||
Reason: "gpg.error.failed_retrieval_gpg_keys",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
activated, email := checkKeyEmails(ctx, email, append([]*GPGKey{key}, primaryKeys...)...)
|
||||
if !activated {
|
||||
continue
|
||||
}
|
||||
|
||||
signer := &user_model.User{
|
||||
Name: name,
|
||||
Email: email,
|
||||
}
|
||||
if key.OwnerID != 0 {
|
||||
owner, err := user_model.GetUserByID(ctx, key.OwnerID)
|
||||
if err == nil {
|
||||
signer = owner
|
||||
} else if !user_model.IsErrUserNotExist(err) {
|
||||
log.Error("Failed to user_model.GetUserByID: %d for key ID: %d (%s) %v", key.OwnerID, key.ID, key.KeyID, err)
|
||||
return &ObjectVerification{
|
||||
CommittingUser: committer,
|
||||
Verified: false,
|
||||
Reason: "gpg.error.no_committer_account",
|
||||
}
|
||||
}
|
||||
}
|
||||
commitVerification := hashAndVerifyWithSubKeysObjectVerification(sig, payload, key, committer, signer, email)
|
||||
if commitVerification != nil {
|
||||
return commitVerification
|
||||
}
|
||||
}
|
||||
// This is a bad situation ... We have a key id that is in our database but the signature doesn't match.
|
||||
return &ObjectVerification{
|
||||
CommittingUser: committer,
|
||||
Verified: false,
|
||||
Warning: true,
|
||||
Reason: BadSignature,
|
||||
}
|
||||
}
|
||||
|
||||
// CalculateTrustStatus will calculate the TrustStatus for a commit verification within a repository
|
||||
// There are several trust models in Gitea
|
||||
func CalculateTrustStatus(verification *ObjectVerification, repoTrustModel repo_model.TrustModelType, isOwnerMemberCollaborator func(*user_model.User) (bool, error), keyMap *map[string]bool) error {
|
||||
if !verification.Verified {
|
||||
return nil
|
||||
}
|
||||
|
||||
// In the Committer trust model a signature is trusted if it matches the committer
|
||||
// - it doesn't matter if they're a collaborator, the owner, Gitea or Github
|
||||
// NB: This model is commit verification only
|
||||
if repoTrustModel == repo_model.CommitterTrustModel {
|
||||
// default to "unmatched"
|
||||
verification.TrustStatus = "unmatched"
|
||||
|
||||
// We can only verify against users in our database but the default key will match
|
||||
// against by email if it is not in the db.
|
||||
if (verification.SigningUser.ID != 0 &&
|
||||
verification.CommittingUser.ID == verification.SigningUser.ID) ||
|
||||
(verification.SigningUser.ID == 0 && verification.CommittingUser.ID == 0 &&
|
||||
verification.SigningUser.Email == verification.CommittingUser.Email) {
|
||||
verification.TrustStatus = "trusted"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Now we drop to the more nuanced trust models...
|
||||
verification.TrustStatus = "trusted"
|
||||
|
||||
if verification.SigningUser.ID == 0 {
|
||||
// This commit is signed by the default key - but this key is not assigned to a user in the DB.
|
||||
|
||||
// However in the repo_model.CollaboratorCommitterTrustModel we cannot mark this as trusted
|
||||
// unless the default key matches the email of a non-user.
|
||||
if repoTrustModel == repo_model.CollaboratorCommitterTrustModel && (verification.CommittingUser.ID != 0 ||
|
||||
verification.SigningUser.Email != verification.CommittingUser.Email) {
|
||||
verification.TrustStatus = "untrusted"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check we actually have a GPG SigningKey
|
||||
var err error
|
||||
if verification.SigningKey != nil {
|
||||
var isMember bool
|
||||
if keyMap != nil {
|
||||
var has bool
|
||||
isMember, has = (*keyMap)[verification.SigningKey.KeyID]
|
||||
if !has {
|
||||
isMember, err = isOwnerMemberCollaborator(verification.SigningUser)
|
||||
(*keyMap)[verification.SigningKey.KeyID] = isMember
|
||||
}
|
||||
} else {
|
||||
isMember, err = isOwnerMemberCollaborator(verification.SigningUser)
|
||||
}
|
||||
|
||||
if !isMember {
|
||||
verification.TrustStatus = "untrusted"
|
||||
if verification.CommittingUser.ID != verification.SigningUser.ID {
|
||||
// The committing user and the signing user are not the same
|
||||
// This should be marked as questionable unless the signing user is a collaborator/team member etc.
|
||||
verification.TrustStatus = "unmatched"
|
||||
}
|
||||
} else if repoTrustModel == repo_model.CollaboratorCommitterTrustModel && verification.CommittingUser.ID != verification.SigningUser.ID {
|
||||
// The committing user and the signing user are not the same and our trustmodel states that they must match
|
||||
verification.TrustStatus = "unmatched"
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
15
models/asymkey/gpg_key_tag_verification.go
Normal file
15
models/asymkey/gpg_key_tag_verification.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package asymkey
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
)
|
||||
|
||||
func ParseTagWithSignature(ctx context.Context, gitRepo *git.Repository, t *git.Tag) *ObjectVerification {
|
||||
o := tagToGitObject(t, gitRepo)
|
||||
return ParseObjectWithSignature(ctx, &o)
|
||||
}
|
|
@ -11,14 +11,13 @@ import (
|
|||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"github.com/42wim/sshsig"
|
||||
)
|
||||
|
||||
// ParseCommitWithSSHSignature check if signature is good against keystore.
|
||||
func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer *user_model.User) *CommitVerification {
|
||||
// ParseObjectWithSSHSignature check if signature is good against keystore.
|
||||
func ParseObjectWithSSHSignature(ctx context.Context, c *GitObject, committer *user_model.User) *ObjectVerification {
|
||||
// Now try to associate the signature with the committer, if present
|
||||
if committer.ID != 0 {
|
||||
keys, err := db.Find[PublicKey](ctx, FindPublicKeyOptions{
|
||||
|
@ -27,7 +26,7 @@ func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer *
|
|||
})
|
||||
if err != nil { // Skipping failed to get ssh keys of user
|
||||
log.Error("ListPublicKeys: %v", err)
|
||||
return &CommitVerification{
|
||||
return &ObjectVerification{
|
||||
CommittingUser: committer,
|
||||
Verified: false,
|
||||
Reason: "gpg.error.failed_retrieval_gpg_keys",
|
||||
|
@ -55,7 +54,7 @@ func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer *
|
|||
|
||||
for _, k := range keys {
|
||||
if k.Verified && activated {
|
||||
commitVerification := verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, k, committer, committer, c.Committer.Email)
|
||||
commitVerification := verifySSHObjectVerification(c.Signature.Signature, c.Signature.Payload, k, committer, committer, c.Committer.Email)
|
||||
if commitVerification != nil {
|
||||
return commitVerification
|
||||
}
|
||||
|
@ -63,19 +62,19 @@ func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer *
|
|||
}
|
||||
}
|
||||
|
||||
return &CommitVerification{
|
||||
return &ObjectVerification{
|
||||
CommittingUser: committer,
|
||||
Verified: false,
|
||||
Reason: NoKeyFound,
|
||||
}
|
||||
}
|
||||
|
||||
func verifySSHCommitVerification(sig, payload string, k *PublicKey, committer, signer *user_model.User, email string) *CommitVerification {
|
||||
func verifySSHObjectVerification(sig, payload string, k *PublicKey, committer, signer *user_model.User, email string) *ObjectVerification {
|
||||
if err := sshsig.Verify(bytes.NewBuffer([]byte(payload)), []byte(sig), []byte(k.Content), "git"); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &CommitVerification{ // Everything is ok
|
||||
return &ObjectVerification{ // Everything is ok
|
||||
CommittingUser: committer,
|
||||
Verified: true,
|
||||
Reason: fmt.Sprintf("%s / %s", signer.Name, k.Fingerprint),
|
|
@ -22,7 +22,8 @@ func TestParseCommitWithSSHSignature(t *testing.T) {
|
|||
sshKey := unittest.AssertExistsAndLoadBean(t, &PublicKey{ID: 1000, OwnerID: 2})
|
||||
|
||||
t.Run("No commiter", func(t *testing.T) {
|
||||
commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, &git.Commit{}, &user_model.User{})
|
||||
o := commitToGitObject(&git.Commit{})
|
||||
commitVerification := ParseObjectWithSSHSignature(db.DefaultContext, &o, &user_model.User{})
|
||||
assert.False(t, commitVerification.Verified)
|
||||
assert.Equal(t, NoKeyFound, commitVerification.Reason)
|
||||
})
|
||||
|
@ -30,7 +31,8 @@ func TestParseCommitWithSSHSignature(t *testing.T) {
|
|||
t.Run("Commiter without keys", func(t *testing.T) {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
|
||||
commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, &git.Commit{Committer: &git.Signature{Email: user.Email}}, user)
|
||||
o := commitToGitObject(&git.Commit{Committer: &git.Signature{Email: user.Email}})
|
||||
commitVerification := ParseObjectWithSSHSignature(db.DefaultContext, &o, user)
|
||||
assert.False(t, commitVerification.Verified)
|
||||
assert.Equal(t, NoKeyFound, commitVerification.Reason)
|
||||
})
|
||||
|
@ -57,7 +59,8 @@ AAAAQIMufOuSjZeDUujrkVK4sl7ICa0WwEftas8UAYxx0Thdkiw2qWjR1U1PKfTLm16/w8
|
|||
`,
|
||||
},
|
||||
}
|
||||
commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, gitCommit, user2)
|
||||
o := commitToGitObject(gitCommit)
|
||||
commitVerification := ParseObjectWithSSHSignature(db.DefaultContext, &o, user2)
|
||||
assert.False(t, commitVerification.Verified)
|
||||
assert.Equal(t, NoKeyFound, commitVerification.Reason)
|
||||
})
|
||||
|
@ -79,7 +82,8 @@ Add content
|
|||
},
|
||||
}
|
||||
|
||||
commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, gitCommit, user2)
|
||||
o := commitToGitObject(gitCommit)
|
||||
commitVerification := ParseObjectWithSSHSignature(db.DefaultContext, &o, user2)
|
||||
assert.False(t, commitVerification.Verified)
|
||||
assert.Equal(t, NoKeyFound, commitVerification.Reason)
|
||||
})
|
||||
|
@ -107,7 +111,8 @@ fs9cMpZVM9BfIKNUSO8QY=
|
|||
},
|
||||
}
|
||||
|
||||
commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, gitCommit, user2)
|
||||
o := commitToGitObject(gitCommit)
|
||||
commitVerification := ParseObjectWithSSHSignature(db.DefaultContext, &o, user2)
|
||||
assert.True(t, commitVerification.Verified)
|
||||
assert.Equal(t, "user2 / SHA256:TKfwbZMR7e9OnlV2l1prfah1TXH8CmqR0PvFEXVCXA4", commitVerification.Reason)
|
||||
assert.Equal(t, sshKey, commitVerification.SigningSSHKey)
|
||||
|
@ -138,7 +143,8 @@ muPLbvEduU+Ze/1Ol1pgk=
|
|||
},
|
||||
}
|
||||
|
||||
commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, gitCommit, user2)
|
||||
o := commitToGitObject(gitCommit)
|
||||
commitVerification := ParseObjectWithSSHSignature(db.DefaultContext, &o, user2)
|
||||
assert.True(t, commitVerification.Verified)
|
||||
assert.Equal(t, "user2 / SHA256:TKfwbZMR7e9OnlV2l1prfah1TXH8CmqR0PvFEXVCXA4", commitVerification.Reason)
|
||||
assert.Equal(t, sshKey, commitVerification.SigningSSHKey)
|
|
@ -155,8 +155,14 @@ func InitEngine(ctx context.Context) error {
|
|||
Logger: log.GetLogger("xorm"),
|
||||
})
|
||||
}
|
||||
|
||||
errorLogger := log.GetLogger("xorm")
|
||||
if setting.IsInTesting {
|
||||
errorLogger = log.GetLogger(log.DEFAULT)
|
||||
}
|
||||
|
||||
xormEngine.AddHook(&ErrorQueryHook{
|
||||
Logger: log.GetLogger("xorm"),
|
||||
Logger: errorLogger,
|
||||
})
|
||||
|
||||
SetDefaultEngine(ctx, xormEngine)
|
||||
|
|
|
@ -54,6 +54,8 @@ var migrations = []*Migration{
|
|||
NewMigration("Add the `enable_repo_unit_hints` column to the `user` table", forgejo_v1_22.AddUserRepoUnitHintsSetting),
|
||||
// v7 -> v8
|
||||
NewMigration("Modify the `release`.`note` content to remove SSH signatures", forgejo_v1_22.RemoveSSHSignaturesFromReleaseNotes),
|
||||
// v8 -> v9
|
||||
NewMigration("Add the `apply_to_admins` column to the `protected_branch` table", forgejo_v1_22.AddApplyToAdminsSetting),
|
||||
}
|
||||
|
||||
// GetCurrentDBVersion returns the current Forgejo database version.
|
||||
|
|
15
models/forgejo_migrations/v1_22/v9.go
Normal file
15
models/forgejo_migrations/v1_22/v9.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_22 //nolint
|
||||
|
||||
import "xorm.io/xorm"
|
||||
|
||||
func AddApplyToAdminsSetting(x *xorm.Engine) error {
|
||||
type ProtectedBranch struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
ApplyToAdmins bool `xorm:"NOT NULL DEFAULT false"`
|
||||
}
|
||||
|
||||
return x.Sync(&ProtectedBranch{})
|
||||
}
|
|
@ -58,6 +58,7 @@ type ProtectedBranch struct {
|
|||
RequireSignedCommits bool `xorm:"NOT NULL DEFAULT false"`
|
||||
ProtectedFilePatterns string `xorm:"TEXT"`
|
||||
UnprotectedFilePatterns string `xorm:"TEXT"`
|
||||
ApplyToAdmins bool `xorm:"NOT NULL DEFAULT false"`
|
||||
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
|
||||
|
|
|
@ -16,7 +16,6 @@ import (
|
|||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
|
@ -24,6 +23,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
|
@ -234,6 +234,7 @@ type SizeDetail struct {
|
|||
}
|
||||
|
||||
// SizeDetails forms a struct with various size details about repository
|
||||
// Note: SizeDetailsString below expects it to have 2 entries
|
||||
func (repo *Repository) SizeDetails() []SizeDetail {
|
||||
sizeDetails := []SizeDetail{
|
||||
{
|
||||
|
@ -249,13 +250,9 @@ func (repo *Repository) SizeDetails() []SizeDetail {
|
|||
}
|
||||
|
||||
// SizeDetailsString returns a concatenation of all repository size details as a string
|
||||
func (repo *Repository) SizeDetailsString() string {
|
||||
var str strings.Builder
|
||||
func (repo *Repository) SizeDetailsString(locale translation.Locale) string {
|
||||
sizeDetails := repo.SizeDetails()
|
||||
for _, detail := range sizeDetails {
|
||||
str.WriteString(fmt.Sprintf("%s: %s, ", detail.Name, base.FileSize(detail.Size)))
|
||||
}
|
||||
return strings.TrimSuffix(str.String(), ", ")
|
||||
return locale.TrString("repo.size_format", sizeDetails[0].Name, locale.TrSize(sizeDetails[0].Size), sizeDetails[1].Name, locale.TrSize(sizeDetails[1].Size))
|
||||
}
|
||||
|
||||
func (repo *Repository) LogString() string {
|
||||
|
|
|
@ -4,18 +4,76 @@
|
|||
package git
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
)
|
||||
|
||||
var LinguistAttributes = []string{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language", "linguist-documentation", "linguist-detectable"}
|
||||
|
||||
// newCheckAttrStdoutReader parses the nul-byte separated output of git check-attr on each call of
|
||||
// the returned function. The first reading error will stop the reading and be returned on all
|
||||
// subsequent calls.
|
||||
func newCheckAttrStdoutReader(r io.Reader, count int) func() (map[string]GitAttribute, error) {
|
||||
scanner := bufio.NewScanner(r)
|
||||
|
||||
// adapted from bufio.ScanLines to split on nul-byte \x00
|
||||
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
if atEOF && len(data) == 0 {
|
||||
return 0, nil, nil
|
||||
}
|
||||
if i := bytes.IndexByte(data, '\x00'); i >= 0 {
|
||||
// We have a full nul-terminated line.
|
||||
return i + 1, data[0:i], nil
|
||||
}
|
||||
// If we're at EOF, we have a final, non-terminated line. Return it.
|
||||
if atEOF {
|
||||
return len(data), data, nil
|
||||
}
|
||||
// Request more data.
|
||||
return 0, nil, nil
|
||||
})
|
||||
|
||||
var err error
|
||||
nextText := func() string {
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if !scanner.Scan() {
|
||||
err = scanner.Err()
|
||||
if err == nil {
|
||||
err = io.ErrUnexpectedEOF
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return scanner.Text()
|
||||
}
|
||||
nextAttribute := func() (string, GitAttribute, error) {
|
||||
nextText() // discard filename
|
||||
key := nextText()
|
||||
value := GitAttribute(nextText())
|
||||
return key, value, err
|
||||
}
|
||||
return func() (map[string]GitAttribute, error) {
|
||||
values := make(map[string]GitAttribute, count)
|
||||
for range count {
|
||||
k, v, err := nextAttribute()
|
||||
if err != nil {
|
||||
return values, err
|
||||
}
|
||||
values[k] = v
|
||||
}
|
||||
return values, scanner.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// GitAttribute exposes an attribute from the .gitattribute file
|
||||
type GitAttribute string //nolint:revive
|
||||
|
||||
|
@ -54,29 +112,15 @@ func (ca GitAttribute) Bool() optional.Option[bool] {
|
|||
return optional.None[bool]()
|
||||
}
|
||||
|
||||
// GitAttributeFirst returns the first specified attribute
|
||||
//
|
||||
// If treeish is empty, the gitattribute will be read from the current repo (which MUST be a working directory and NOT bare).
|
||||
func (repo *Repository) GitAttributeFirst(treeish, filename string, attributes ...string) (GitAttribute, error) {
|
||||
values, err := repo.GitAttributes(treeish, filename, attributes...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, a := range attributes {
|
||||
if values[a].IsSpecified() {
|
||||
return values[a], nil
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// gitCheckAttrCommand prepares the "git check-attr" command for later use as one-shot or streaming
|
||||
// instanciation.
|
||||
func (repo *Repository) gitCheckAttrCommand(treeish string, attributes ...string) (*Command, *RunOpts, context.CancelFunc, error) {
|
||||
if len(attributes) == 0 {
|
||||
return nil, nil, nil, fmt.Errorf("no provided attributes to check-attr")
|
||||
}
|
||||
|
||||
env := os.Environ()
|
||||
var deleteTemporaryFile context.CancelFunc
|
||||
var removeTempFiles context.CancelFunc = func() {}
|
||||
|
||||
// git < 2.40 cannot run check-attr on bare repo, but needs INDEX + WORK_TREE
|
||||
hasIndex := treeish == ""
|
||||
|
@ -85,7 +129,7 @@ func (repo *Repository) gitCheckAttrCommand(treeish string, attributes ...string
|
|||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
deleteTemporaryFile = cancel
|
||||
removeTempFiles = cancel
|
||||
|
||||
env = append(env, "GIT_INDEX_FILE="+indexFilename, "GIT_WORK_TREE="+worktree)
|
||||
|
||||
|
@ -94,16 +138,8 @@ func (repo *Repository) gitCheckAttrCommand(treeish string, attributes ...string
|
|||
// clear treeish to read from provided index/work_tree
|
||||
treeish = ""
|
||||
}
|
||||
ctx, cancel := context.WithCancel(repo.Ctx)
|
||||
if deleteTemporaryFile != nil {
|
||||
ctxCancel := cancel
|
||||
cancel = func() {
|
||||
ctxCancel()
|
||||
deleteTemporaryFile()
|
||||
}
|
||||
}
|
||||
|
||||
cmd := NewCommand(ctx, "check-attr", "-z")
|
||||
cmd := NewCommand(repo.Ctx, "check-attr", "-z")
|
||||
|
||||
if hasIndex {
|
||||
cmd.AddArguments("--cached")
|
||||
|
@ -126,18 +162,34 @@ func (repo *Repository) gitCheckAttrCommand(treeish string, attributes ...string
|
|||
return cmd, &RunOpts{
|
||||
Env: env,
|
||||
Dir: repo.Path,
|
||||
}, cancel, nil
|
||||
}, removeTempFiles, nil
|
||||
}
|
||||
|
||||
// GitAttributes returns gitattribute.
|
||||
// GitAttributeFirst returns the first specified attribute of the given filename.
|
||||
//
|
||||
// If treeish is empty, the gitattribute will be read from the current repo (which MUST be a working directory and NOT bare).
|
||||
func (repo *Repository) GitAttributeFirst(treeish, filename string, attributes ...string) (GitAttribute, error) {
|
||||
values, err := repo.GitAttributes(treeish, filename, attributes...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, a := range attributes {
|
||||
if values[a].IsSpecified() {
|
||||
return values[a], nil
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// GitAttributes returns the gitattribute of the given filename.
|
||||
//
|
||||
// If treeish is empty, the gitattribute will be read from the current repo (which MUST be a working directory and NOT bare).
|
||||
func (repo *Repository) GitAttributes(treeish, filename string, attributes ...string) (map[string]GitAttribute, error) {
|
||||
cmd, runOpts, cancel, err := repo.gitCheckAttrCommand(treeish, attributes...)
|
||||
cmd, runOpts, removeTempFiles, err := repo.gitCheckAttrCommand(treeish, attributes...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cancel()
|
||||
defer removeTempFiles()
|
||||
|
||||
stdOut := new(bytes.Buffer)
|
||||
runOpts.Stdout = stdOut
|
||||
|
@ -151,163 +203,84 @@ func (repo *Repository) GitAttributes(treeish, filename string, attributes ...st
|
|||
return nil, fmt.Errorf("failed to run check-attr: %w\n%s\n%s", err, stdOut.String(), stdErr.String())
|
||||
}
|
||||
|
||||
// FIXME: This is incorrect on versions < 1.8.5
|
||||
fields := bytes.Split(stdOut.Bytes(), []byte{'\000'})
|
||||
|
||||
if len(fields)%3 != 1 {
|
||||
return nil, fmt.Errorf("wrong number of fields in return from check-attr")
|
||||
}
|
||||
|
||||
values := make(map[string]GitAttribute, len(attributes))
|
||||
for ; len(fields) >= 3; fields = fields[3:] {
|
||||
// filename := string(fields[0])
|
||||
attribute := string(fields[1])
|
||||
value := string(fields[2])
|
||||
values[attribute] = GitAttribute(value)
|
||||
}
|
||||
return values, nil
|
||||
return newCheckAttrStdoutReader(stdOut, len(attributes))()
|
||||
}
|
||||
|
||||
type attributeTriple struct {
|
||||
Filename string
|
||||
Attribute string
|
||||
Value string
|
||||
}
|
||||
|
||||
type nulSeparatedAttributeWriter struct {
|
||||
tmp []byte
|
||||
attributes chan attributeTriple
|
||||
closed chan struct{}
|
||||
working attributeTriple
|
||||
pos int
|
||||
}
|
||||
|
||||
func (wr *nulSeparatedAttributeWriter) Write(p []byte) (n int, err error) {
|
||||
l, read := len(p), 0
|
||||
|
||||
nulIdx := bytes.IndexByte(p, '\x00')
|
||||
for nulIdx >= 0 {
|
||||
wr.tmp = append(wr.tmp, p[:nulIdx]...)
|
||||
switch wr.pos {
|
||||
case 0:
|
||||
wr.working = attributeTriple{
|
||||
Filename: string(wr.tmp),
|
||||
}
|
||||
case 1:
|
||||
wr.working.Attribute = string(wr.tmp)
|
||||
case 2:
|
||||
wr.working.Value = string(wr.tmp)
|
||||
}
|
||||
wr.tmp = wr.tmp[:0]
|
||||
wr.pos++
|
||||
if wr.pos > 2 {
|
||||
wr.attributes <- wr.working
|
||||
wr.pos = 0
|
||||
}
|
||||
read += nulIdx + 1
|
||||
if l > read {
|
||||
p = p[nulIdx+1:]
|
||||
nulIdx = bytes.IndexByte(p, '\x00')
|
||||
} else {
|
||||
return l, nil
|
||||
}
|
||||
}
|
||||
wr.tmp = append(wr.tmp, p...)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (wr *nulSeparatedAttributeWriter) Close() error {
|
||||
select {
|
||||
case <-wr.closed:
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
close(wr.attributes)
|
||||
close(wr.closed)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GitAttributeChecker creates an AttributeChecker for the given repository and provided commit ID.
|
||||
// GitAttributeChecker creates an AttributeChecker for the given repository and provided commit ID
|
||||
// to retrieve the attributes of multiple files. The AttributeChecker must be closed after use.
|
||||
//
|
||||
// If treeish is empty, the gitattribute will be read from the current repo (which MUST be a working directory and NOT bare).
|
||||
func (repo *Repository) GitAttributeChecker(treeish string, attributes ...string) (AttributeChecker, error) {
|
||||
cmd, runOpts, cancel, err := repo.gitCheckAttrCommand(treeish, attributes...)
|
||||
cmd, runOpts, removeTempFiles, err := repo.gitCheckAttrCommand(treeish, attributes...)
|
||||
if err != nil {
|
||||
return AttributeChecker{}, err
|
||||
}
|
||||
|
||||
ac := AttributeChecker{
|
||||
attributeNumber: len(attributes),
|
||||
ctx: cmd.parentContext,
|
||||
cancel: cancel, // will be cancelled on Close
|
||||
}
|
||||
|
||||
stdinReader, stdinWriter, err := os.Pipe()
|
||||
if err != nil {
|
||||
ac.cancel()
|
||||
return AttributeChecker{}, err
|
||||
}
|
||||
ac.stdinWriter = stdinWriter // will be closed on Close
|
||||
|
||||
lw := new(nulSeparatedAttributeWriter)
|
||||
lw.attributes = make(chan attributeTriple, len(attributes))
|
||||
lw.closed = make(chan struct{})
|
||||
ac.attributesCh = lw.attributes
|
||||
|
||||
cmd.AddArguments("--stdin")
|
||||
|
||||
// os.Pipe is needed (and not io.Pipe), otherwise cmd.Wait will wait for the stdinReader
|
||||
// to be closed before returning (which would require another goroutine)
|
||||
// https://go.dev/issue/23019
|
||||
stdinReader, stdinWriter, err := os.Pipe() // reader closed in goroutine / writer closed on ac.Close
|
||||
if err != nil {
|
||||
return AttributeChecker{}, err
|
||||
}
|
||||
stdoutReader, stdoutWriter := io.Pipe() // closed in goroutine
|
||||
|
||||
ac := AttributeChecker{
|
||||
removeTempFiles: removeTempFiles, // called on ac.Close
|
||||
stdinWriter: stdinWriter,
|
||||
readStdout: newCheckAttrStdoutReader(stdoutReader, len(attributes)),
|
||||
err: &atomic.Value{},
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer stdinReader.Close()
|
||||
defer lw.Close()
|
||||
defer stdoutWriter.Close() // in case of a panic (no-op if already closed by CloseWithError at the end)
|
||||
|
||||
stdErr := new(bytes.Buffer)
|
||||
runOpts.Stdin = stdinReader
|
||||
runOpts.Stdout = lw
|
||||
runOpts.Stdout = stdoutWriter
|
||||
runOpts.Stderr = stdErr
|
||||
|
||||
err := cmd.Run(runOpts)
|
||||
|
||||
if err != nil && // If there is an error we need to return but:
|
||||
cmd.parentContext.Err() != err && // 1. Ignore the context error if the context is cancelled or exceeds the deadline (RunWithContext could return c.ctx.Err() which is Canceled or DeadlineExceeded)
|
||||
err.Error() != "signal: killed" { // 2. We should not pass up errors due to the program being killed
|
||||
log.Error("failed to run attr-check. Error: %v\nStderr: %s", err, stdErr.String())
|
||||
// if the context was cancelled, Run error is irrelevant
|
||||
if e := cmd.parentContext.Err(); e != nil {
|
||||
err = e
|
||||
}
|
||||
|
||||
if err != nil { // decorate the returned error
|
||||
err = fmt.Errorf("git check-attr (stderr: %q): %w", strings.TrimSpace(stdErr.String()), err)
|
||||
ac.err.Store(err)
|
||||
}
|
||||
stdoutWriter.CloseWithError(err)
|
||||
}()
|
||||
|
||||
return ac, nil
|
||||
}
|
||||
|
||||
type AttributeChecker struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
stdinWriter *os.File
|
||||
attributeNumber int
|
||||
attributesCh <-chan attributeTriple
|
||||
removeTempFiles context.CancelFunc
|
||||
stdinWriter io.WriteCloser
|
||||
readStdout func() (map[string]GitAttribute, error)
|
||||
err *atomic.Value
|
||||
}
|
||||
|
||||
func (ac AttributeChecker) CheckPath(path string) (map[string]GitAttribute, error) {
|
||||
if err := ac.ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := ac.stdinWriter.Write([]byte(path + "\x00")); err != nil {
|
||||
return nil, err
|
||||
// try to return the Run error if available, since it is likely more helpful
|
||||
// than just "broken pipe"
|
||||
if aerr, _ := ac.err.Load().(error); aerr != nil {
|
||||
return nil, aerr
|
||||
}
|
||||
return nil, fmt.Errorf("git check-attr: %w", err)
|
||||
}
|
||||
|
||||
rs := make(map[string]GitAttribute)
|
||||
for i := 0; i < ac.attributeNumber; i++ {
|
||||
select {
|
||||
case attr, ok := <-ac.attributesCh:
|
||||
if !ok {
|
||||
return nil, ac.ctx.Err()
|
||||
}
|
||||
rs[attr.Attribute] = GitAttribute(attr.Value)
|
||||
case <-ac.ctx.Done():
|
||||
return nil, ac.ctx.Err()
|
||||
}
|
||||
}
|
||||
return rs, nil
|
||||
return ac.readStdout()
|
||||
}
|
||||
|
||||
func (ac AttributeChecker) Close() error {
|
||||
ac.cancel()
|
||||
ac.removeTempFiles()
|
||||
return ac.stdinWriter.Close()
|
||||
}
|
||||
|
|
|
@ -4,7 +4,14 @@
|
|||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
@ -14,90 +21,63 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) {
|
||||
wr := &nulSeparatedAttributeWriter{
|
||||
attributes: make(chan attributeTriple, 5),
|
||||
}
|
||||
func TestNewCheckAttrStdoutReader(t *testing.T) {
|
||||
t.Run("two_times", func(t *testing.T) {
|
||||
read := newCheckAttrStdoutReader(strings.NewReader(
|
||||
".gitignore\x00linguist-vendored\x00unspecified\x00"+
|
||||
".gitignore\x00linguist-vendored\x00specified",
|
||||
), 1)
|
||||
|
||||
testStr := ".gitignore\"\n\x00linguist-vendored\x00unspecified\x00"
|
||||
|
||||
n, err := wr.Write([]byte(testStr))
|
||||
|
||||
assert.Len(t, testStr, n)
|
||||
// first read
|
||||
attr, err := read()
|
||||
assert.NoError(t, err)
|
||||
select {
|
||||
case attr := <-wr.attributes:
|
||||
assert.Equal(t, ".gitignore\"\n", attr.Filename)
|
||||
assert.Equal(t, "linguist-vendored", attr.Attribute)
|
||||
assert.Equal(t, "unspecified", attr.Value)
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
assert.FailNow(t, "took too long to read an attribute from the list")
|
||||
}
|
||||
// Write a second attribute again
|
||||
n, err = wr.Write([]byte(testStr))
|
||||
|
||||
assert.Len(t, testStr, n)
|
||||
assert.NoError(t, err)
|
||||
|
||||
select {
|
||||
case attr := <-wr.attributes:
|
||||
assert.Equal(t, ".gitignore\"\n", attr.Filename)
|
||||
assert.Equal(t, "linguist-vendored", attr.Attribute)
|
||||
assert.Equal(t, "unspecified", attr.Value)
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
assert.FailNow(t, "took too long to read an attribute from the list")
|
||||
}
|
||||
|
||||
// Write a partial attribute
|
||||
_, err = wr.Write([]byte("incomplete-file"))
|
||||
assert.NoError(t, err)
|
||||
_, err = wr.Write([]byte("name\x00"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
select {
|
||||
case <-wr.attributes:
|
||||
assert.FailNow(t, "There should not be an attribute ready to read")
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
_, err = wr.Write([]byte("attribute\x00"))
|
||||
assert.NoError(t, err)
|
||||
select {
|
||||
case <-wr.attributes:
|
||||
assert.FailNow(t, "There should not be an attribute ready to read")
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
|
||||
_, err = wr.Write([]byte("value\x00"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
attr := <-wr.attributes
|
||||
assert.Equal(t, "incomplete-filename", attr.Filename)
|
||||
assert.Equal(t, "attribute", attr.Attribute)
|
||||
assert.Equal(t, "value", attr.Value)
|
||||
|
||||
_, err = wr.Write([]byte("shouldbe.vendor\x00linguist-vendored\x00set\x00shouldbe.vendor\x00linguist-generated\x00unspecified\x00shouldbe.vendor\x00linguist-language\x00unspecified\x00"))
|
||||
assert.NoError(t, err)
|
||||
attr = <-wr.attributes
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, attributeTriple{
|
||||
Filename: "shouldbe.vendor",
|
||||
Attribute: "linguist-vendored",
|
||||
Value: "set",
|
||||
assert.Equal(t, map[string]GitAttribute{
|
||||
"linguist-vendored": GitAttribute("unspecified"),
|
||||
}, attr)
|
||||
attr = <-wr.attributes
|
||||
|
||||
// second read
|
||||
attr, err = read()
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, attributeTriple{
|
||||
Filename: "shouldbe.vendor",
|
||||
Attribute: "linguist-generated",
|
||||
Value: "unspecified",
|
||||
assert.Equal(t, map[string]GitAttribute{
|
||||
"linguist-vendored": GitAttribute("specified"),
|
||||
}, attr)
|
||||
attr = <-wr.attributes
|
||||
})
|
||||
t.Run("incomplete", func(t *testing.T) {
|
||||
read := newCheckAttrStdoutReader(strings.NewReader(
|
||||
"filename\x00linguist-vendored",
|
||||
), 1)
|
||||
|
||||
_, err := read()
|
||||
assert.Equal(t, io.ErrUnexpectedEOF, err)
|
||||
})
|
||||
t.Run("three_times", func(t *testing.T) {
|
||||
read := newCheckAttrStdoutReader(strings.NewReader(
|
||||
"shouldbe.vendor\x00linguist-vendored\x00set\x00"+
|
||||
"shouldbe.vendor\x00linguist-generated\x00unspecified\x00"+
|
||||
"shouldbe.vendor\x00linguist-language\x00unspecified\x00",
|
||||
), 1)
|
||||
|
||||
// first read
|
||||
attr, err := read()
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, attributeTriple{
|
||||
Filename: "shouldbe.vendor",
|
||||
Attribute: "linguist-language",
|
||||
Value: "unspecified",
|
||||
assert.Equal(t, map[string]GitAttribute{
|
||||
"linguist-vendored": GitAttribute("set"),
|
||||
}, attr)
|
||||
|
||||
// second read
|
||||
attr, err = read()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, map[string]GitAttribute{
|
||||
"linguist-generated": GitAttribute("unspecified"),
|
||||
}, attr)
|
||||
|
||||
// third read
|
||||
attr, err = read()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, map[string]GitAttribute{
|
||||
"linguist-language": GitAttribute("unspecified"),
|
||||
}, attr)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGitAttributeBareNonBare(t *testing.T) {
|
||||
|
@ -114,25 +94,6 @@ func TestGitAttributeBareNonBare(t *testing.T) {
|
|||
"8fee858da5796dfb37704761701bb8e800ad9ef3",
|
||||
"341fca5b5ea3de596dc483e54c2db28633cd2f97",
|
||||
} {
|
||||
t.Run("GitAttributeChecker/"+commitID, func(t *testing.T) {
|
||||
bareChecker, err := gitRepo.GitAttributeChecker(commitID, LinguistAttributes...)
|
||||
assert.NoError(t, err)
|
||||
t.Cleanup(func() { bareChecker.Close() })
|
||||
|
||||
bareStats, err := bareChecker.CheckPath("i-am-a-python.p")
|
||||
assert.NoError(t, err)
|
||||
|
||||
defer test.MockVariableValue(&SupportCheckAttrOnBare, false)()
|
||||
cloneChecker, err := gitRepo.GitAttributeChecker(commitID, LinguistAttributes...)
|
||||
assert.NoError(t, err)
|
||||
t.Cleanup(func() { cloneChecker.Close() })
|
||||
cloneStats, err := cloneChecker.CheckPath("i-am-a-python.p")
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, cloneStats, bareStats)
|
||||
})
|
||||
|
||||
t.Run("GitAttributes/"+commitID, func(t *testing.T) {
|
||||
bareStats, err := gitRepo.GitAttributes(commitID, "i-am-a-python.p", LinguistAttributes...)
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
@ -141,6 +102,27 @@ func TestGitAttributeBareNonBare(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, cloneStats, bareStats)
|
||||
refStats := cloneStats
|
||||
|
||||
t.Run("GitAttributeChecker/"+commitID+"/SupportBare", func(t *testing.T) {
|
||||
bareChecker, err := gitRepo.GitAttributeChecker(commitID, LinguistAttributes...)
|
||||
assert.NoError(t, err)
|
||||
defer bareChecker.Close()
|
||||
|
||||
bareStats, err := bareChecker.CheckPath("i-am-a-python.p")
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, refStats, bareStats)
|
||||
})
|
||||
t.Run("GitAttributeChecker/"+commitID+"/NoBareSupport", func(t *testing.T) {
|
||||
defer test.MockVariableValue(&SupportCheckAttrOnBare, false)()
|
||||
cloneChecker, err := gitRepo.GitAttributeChecker(commitID, LinguistAttributes...)
|
||||
assert.NoError(t, err)
|
||||
defer cloneChecker.Close()
|
||||
|
||||
cloneStats, err := cloneChecker.CheckPath("i-am-a-python.p")
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, refStats, cloneStats)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -208,3 +190,162 @@ func TestGitAttributeStruct(t *testing.T) {
|
|||
assert.Equal(t, "text?token=Error", GitAttribute("text?token=Error").String())
|
||||
assert.Equal(t, "text", GitAttribute("text?token=Error").Prefix())
|
||||
}
|
||||
|
||||
func TestGitAttributeCheckerError(t *testing.T) {
|
||||
prepareRepo := func(t *testing.T) *Repository {
|
||||
t.Helper()
|
||||
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"))))
|
||||
|
||||
gitRepo, err := openRepositoryWithDefaultContext(path)
|
||||
require.NoError(t, err)
|
||||
return gitRepo
|
||||
}
|
||||
|
||||
t.Run("RemoveAll/BeforeRun", func(t *testing.T) {
|
||||
gitRepo := prepareRepo(t)
|
||||
defer gitRepo.Close()
|
||||
|
||||
assert.NoError(t, os.RemoveAll(gitRepo.Path))
|
||||
|
||||
ac, err := gitRepo.GitAttributeChecker("", "linguist-language")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = ac.CheckPath("i-am-a-python.p")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), `git check-attr (stderr: ""):`)
|
||||
})
|
||||
|
||||
t.Run("RemoveAll/DuringRun", func(t *testing.T) {
|
||||
gitRepo := prepareRepo(t)
|
||||
defer gitRepo.Close()
|
||||
|
||||
ac, err := gitRepo.GitAttributeChecker("", "linguist-language")
|
||||
require.NoError(t, err)
|
||||
|
||||
// calling CheckPath before would allow git to cache part of it and succesfully return later
|
||||
assert.NoError(t, os.RemoveAll(gitRepo.Path))
|
||||
|
||||
_, err = ac.CheckPath("i-am-a-python.p")
|
||||
if err == nil {
|
||||
t.Skip(
|
||||
"git check-attr started too fast and CheckPath was succesful (and likely cached)",
|
||||
"https://codeberg.org/forgejo/forgejo/issues/2948",
|
||||
)
|
||||
}
|
||||
// Depending on the order of execution, the returned error can be:
|
||||
// - a launch error "fork/exec /usr/bin/git: no such file or directory" (when the removal happens before the Run)
|
||||
// - a git error (stderr: "fatal: Unable to read current working directory: No such file or directory"): exit status 128 (when the removal happens after the Run)
|
||||
// (pipe error "write |1: broken pipe" should be replaced by one of the Run errors above)
|
||||
assert.Contains(t, err.Error(), `git check-attr`)
|
||||
})
|
||||
|
||||
t.Run("Cancelled/BeforeRun", func(t *testing.T) {
|
||||
gitRepo := prepareRepo(t)
|
||||
defer gitRepo.Close()
|
||||
|
||||
var cancel context.CancelFunc
|
||||
gitRepo.Ctx, cancel = context.WithCancel(gitRepo.Ctx)
|
||||
cancel()
|
||||
|
||||
ac, err := gitRepo.GitAttributeChecker("8fee858da5796dfb37704761701bb8e800ad9ef3", "linguist-language")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = ac.CheckPath("i-am-a-python.p")
|
||||
assert.ErrorIs(t, err, context.Canceled)
|
||||
})
|
||||
|
||||
t.Run("Cancelled/DuringRun", func(t *testing.T) {
|
||||
gitRepo := prepareRepo(t)
|
||||
defer gitRepo.Close()
|
||||
|
||||
var cancel context.CancelFunc
|
||||
gitRepo.Ctx, cancel = context.WithCancel(gitRepo.Ctx)
|
||||
|
||||
ac, err := gitRepo.GitAttributeChecker("8fee858da5796dfb37704761701bb8e800ad9ef3", "linguist-language")
|
||||
require.NoError(t, err)
|
||||
|
||||
attr, err := ac.CheckPath("i-am-a-python.p")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Python", attr["linguist-language"].String())
|
||||
|
||||
errCh := make(chan error)
|
||||
go func() {
|
||||
cancel()
|
||||
|
||||
for err == nil {
|
||||
_, err = ac.CheckPath("i-am-a-python.p")
|
||||
runtime.Gosched() // the cancellation must have time to propagate
|
||||
}
|
||||
errCh <- err
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(time.Second):
|
||||
t.Error("CheckPath did not complete within 1s")
|
||||
case err = <-errCh:
|
||||
assert.ErrorIs(t, err, context.Canceled)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Closed/BeforeRun", func(t *testing.T) {
|
||||
gitRepo := prepareRepo(t)
|
||||
defer gitRepo.Close()
|
||||
|
||||
ac, err := gitRepo.GitAttributeChecker("8fee858da5796dfb37704761701bb8e800ad9ef3", "linguist-language")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NoError(t, ac.Close())
|
||||
|
||||
_, err = ac.CheckPath("i-am-a-python.p")
|
||||
assert.ErrorIs(t, err, fs.ErrClosed)
|
||||
})
|
||||
|
||||
t.Run("Closed/DuringRun", func(t *testing.T) {
|
||||
gitRepo := prepareRepo(t)
|
||||
defer gitRepo.Close()
|
||||
|
||||
ac, err := gitRepo.GitAttributeChecker("8fee858da5796dfb37704761701bb8e800ad9ef3", "linguist-language")
|
||||
require.NoError(t, err)
|
||||
|
||||
attr, err := ac.CheckPath("i-am-a-python.p")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Python", attr["linguist-language"].String())
|
||||
|
||||
assert.NoError(t, ac.Close())
|
||||
|
||||
_, err = ac.CheckPath("i-am-a-python.p")
|
||||
assert.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()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -238,7 +238,7 @@ func newRefsFromRefNames(refNames []byte) []git.Reference {
|
|||
type Commit struct {
|
||||
Commit *git.Commit
|
||||
User *user_model.User
|
||||
Verification *asymkey_model.CommitVerification
|
||||
Verification *asymkey_model.ObjectVerification
|
||||
Status *git_model.CommitStatus
|
||||
Flow int64
|
||||
Row int
|
||||
|
|
|
@ -136,7 +136,7 @@ func (g *Manager) doShutdown() {
|
|||
}
|
||||
g.lock.Lock()
|
||||
g.shutdownCtxCancel()
|
||||
atShutdownCtx := pprof.WithLabels(g.hammerCtx, pprof.Labels("graceful-lifecycle", "post-shutdown"))
|
||||
atShutdownCtx := pprof.WithLabels(g.hammerCtx, pprof.Labels("gracefulLifecycle", "post-shutdown"))
|
||||
pprof.SetGoroutineLabels(atShutdownCtx)
|
||||
for _, fn := range g.toRunAtShutdown {
|
||||
go fn()
|
||||
|
@ -167,7 +167,7 @@ func (g *Manager) doHammerTime(d time.Duration) {
|
|||
default:
|
||||
log.Warn("Setting Hammer condition")
|
||||
g.hammerCtxCancel()
|
||||
atHammerCtx := pprof.WithLabels(g.terminateCtx, pprof.Labels("graceful-lifecycle", "post-hammer"))
|
||||
atHammerCtx := pprof.WithLabels(g.terminateCtx, pprof.Labels("gracefulLifecycle", "post-hammer"))
|
||||
pprof.SetGoroutineLabels(atHammerCtx)
|
||||
}
|
||||
g.lock.Unlock()
|
||||
|
@ -183,7 +183,7 @@ func (g *Manager) doTerminate() {
|
|||
default:
|
||||
log.Warn("Terminating")
|
||||
g.terminateCtxCancel()
|
||||
atTerminateCtx := pprof.WithLabels(g.managerCtx, pprof.Labels("graceful-lifecycle", "post-terminate"))
|
||||
atTerminateCtx := pprof.WithLabels(g.managerCtx, pprof.Labels("gracefulLifecycle", "post-terminate"))
|
||||
pprof.SetGoroutineLabels(atTerminateCtx)
|
||||
|
||||
for _, fn := range g.toRunAtTerminate {
|
||||
|
|
|
@ -65,10 +65,10 @@ func (g *Manager) prepare(ctx context.Context) {
|
|||
g.hammerCtx, g.hammerCtxCancel = context.WithCancel(ctx)
|
||||
g.managerCtx, g.managerCtxCancel = context.WithCancel(ctx)
|
||||
|
||||
g.terminateCtx = pprof.WithLabels(g.terminateCtx, pprof.Labels("graceful-lifecycle", "with-terminate"))
|
||||
g.shutdownCtx = pprof.WithLabels(g.shutdownCtx, pprof.Labels("graceful-lifecycle", "with-shutdown"))
|
||||
g.hammerCtx = pprof.WithLabels(g.hammerCtx, pprof.Labels("graceful-lifecycle", "with-hammer"))
|
||||
g.managerCtx = pprof.WithLabels(g.managerCtx, pprof.Labels("graceful-lifecycle", "with-manager"))
|
||||
g.terminateCtx = pprof.WithLabels(g.terminateCtx, pprof.Labels("gracefulLifecycle", "with-terminate"))
|
||||
g.shutdownCtx = pprof.WithLabels(g.shutdownCtx, pprof.Labels("gracefulLifecycle", "with-shutdown"))
|
||||
g.hammerCtx = pprof.WithLabels(g.hammerCtx, pprof.Labels("gracefulLifecycle", "with-hammer"))
|
||||
g.managerCtx = pprof.WithLabels(g.managerCtx, pprof.Labels("gracefulLifecycle", "with-manager"))
|
||||
|
||||
if !g.setStateTransition(stateInit, stateRunning) {
|
||||
panic("invalid graceful manager state: transition from init to running failed")
|
||||
|
|
|
@ -44,7 +44,7 @@ func (g *Manager) notify(msg systemdNotifyMsg) {
|
|||
}
|
||||
|
||||
func (g *Manager) start() {
|
||||
// Now label this and all goroutines created by this goroutine with the graceful-lifecycle manager
|
||||
// Now label this and all goroutines created by this goroutine with the gracefulLifecycle manager
|
||||
pprof.SetGoroutineLabels(g.managerCtx)
|
||||
defer pprof.SetGoroutineLabels(g.ctx)
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ const (
|
|||
)
|
||||
|
||||
func (g *Manager) start() {
|
||||
// Now label this and all goroutines created by this goroutine with the graceful-lifecycle manager
|
||||
// Now label this and all goroutines created by this goroutine with the gracefulLifecycle manager
|
||||
pprof.SetGoroutineLabels(g.managerCtx)
|
||||
defer pprof.SetGoroutineLabels(g.ctx)
|
||||
|
||||
|
|
323
modules/markup/file_preview.go
Normal file
323
modules/markup/file_preview.go
Normal file
|
@ -0,0 +1,323 @@
|
|||
// Copyright The Forgejo Authors.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package markup
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"html/template"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/charset"
|
||||
"code.gitea.io/gitea/modules/highlight"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
"golang.org/x/net/html/atom"
|
||||
)
|
||||
|
||||
// filePreviewPattern matches "http://domain/org/repo/src/commit/COMMIT/filepath#L1-L2"
|
||||
var filePreviewPattern = regexp.MustCompile(`https?://((?:\S+/){3})src/commit/([0-9a-f]{4,64})/(\S+)#(L\d+(?:-L\d+)?)`)
|
||||
|
||||
type FilePreview struct {
|
||||
fileContent []template.HTML
|
||||
subTitle template.HTML
|
||||
lineOffset int
|
||||
urlFull string
|
||||
filePath string
|
||||
start int
|
||||
end int
|
||||
isTruncated bool
|
||||
}
|
||||
|
||||
func NewFilePreview(ctx *RenderContext, node *html.Node, locale translation.Locale) *FilePreview {
|
||||
if setting.FilePreviewMaxLines == 0 {
|
||||
// Feature is disabled
|
||||
return nil
|
||||
}
|
||||
|
||||
preview := &FilePreview{}
|
||||
|
||||
m := filePreviewPattern.FindStringSubmatchIndex(node.Data)
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure that every group has a match
|
||||
if slices.Contains(m, -1) {
|
||||
return nil
|
||||
}
|
||||
|
||||
preview.urlFull = node.Data[m[0]:m[1]]
|
||||
|
||||
// Ensure that we only use links to local repositories
|
||||
if !strings.HasPrefix(preview.urlFull, setting.AppURL+setting.AppSubURL) {
|
||||
return nil
|
||||
}
|
||||
|
||||
projPath := strings.TrimSuffix(node.Data[m[2]:m[3]], "/")
|
||||
|
||||
commitSha := node.Data[m[4]:m[5]]
|
||||
preview.filePath = node.Data[m[6]:m[7]]
|
||||
hash := node.Data[m[8]:m[9]]
|
||||
|
||||
preview.start = m[0]
|
||||
preview.end = m[1]
|
||||
|
||||
projPathSegments := strings.Split(projPath, "/")
|
||||
var language string
|
||||
fileBlob, err := DefaultProcessorHelper.GetRepoFileBlob(
|
||||
ctx.Ctx,
|
||||
projPathSegments[len(projPathSegments)-2],
|
||||
projPathSegments[len(projPathSegments)-1],
|
||||
commitSha, preview.filePath,
|
||||
&language,
|
||||
)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
lineSpecs := strings.Split(hash, "-")
|
||||
|
||||
commitLinkBuffer := new(bytes.Buffer)
|
||||
err = html.Render(commitLinkBuffer, createLink(node.Data[m[0]:m[5]], commitSha[0:7], "text black"))
|
||||
if err != nil {
|
||||
log.Error("failed to render commitLink: %v", err)
|
||||
}
|
||||
|
||||
var startLine, endLine int
|
||||
|
||||
if len(lineSpecs) == 1 {
|
||||
startLine, _ = strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L"))
|
||||
endLine = startLine
|
||||
preview.subTitle = locale.Tr(
|
||||
"markup.filepreview.line", startLine,
|
||||
template.HTML(commitLinkBuffer.String()),
|
||||
)
|
||||
|
||||
preview.lineOffset = startLine - 1
|
||||
} else {
|
||||
startLine, _ = strconv.Atoi(strings.TrimPrefix(lineSpecs[0], "L"))
|
||||
endLine, _ = strconv.Atoi(strings.TrimPrefix(lineSpecs[1], "L"))
|
||||
preview.subTitle = locale.Tr(
|
||||
"markup.filepreview.lines", startLine, endLine,
|
||||
template.HTML(commitLinkBuffer.String()),
|
||||
)
|
||||
|
||||
preview.lineOffset = startLine - 1
|
||||
}
|
||||
|
||||
lineCount := endLine - (startLine - 1)
|
||||
if startLine < 1 || endLine < 1 || lineCount < 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if setting.FilePreviewMaxLines > 0 && lineCount > setting.FilePreviewMaxLines {
|
||||
preview.isTruncated = true
|
||||
lineCount = setting.FilePreviewMaxLines
|
||||
}
|
||||
|
||||
dataRc, err := fileBlob.DataAsync()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer dataRc.Close()
|
||||
|
||||
reader := bufio.NewReader(dataRc)
|
||||
|
||||
// skip all lines until we find our startLine
|
||||
for i := 1; i < startLine; i++ {
|
||||
_, err := reader.ReadBytes('\n')
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// capture the lines we're interested in
|
||||
lineBuffer := new(bytes.Buffer)
|
||||
for i := 0; i < lineCount; i++ {
|
||||
buf, err := reader.ReadBytes('\n')
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
lineBuffer.Write(buf)
|
||||
}
|
||||
|
||||
// highlight the file...
|
||||
fileContent, _, err := highlight.File(fileBlob.Name(), language, lineBuffer.Bytes())
|
||||
if err != nil {
|
||||
log.Error("highlight.File failed, fallback to plain text: %v", err)
|
||||
fileContent = highlight.PlainText(lineBuffer.Bytes())
|
||||
}
|
||||
preview.fileContent = fileContent
|
||||
|
||||
return preview
|
||||
}
|
||||
|
||||
func (p *FilePreview) CreateHTML(locale translation.Locale) *html.Node {
|
||||
table := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.Table.String(),
|
||||
Attr: []html.Attribute{{Key: "class", Val: "file-preview"}},
|
||||
}
|
||||
tbody := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.Tbody.String(),
|
||||
}
|
||||
|
||||
status := &charset.EscapeStatus{}
|
||||
statuses := make([]*charset.EscapeStatus, len(p.fileContent))
|
||||
for i, line := range p.fileContent {
|
||||
statuses[i], p.fileContent[i] = charset.EscapeControlHTML(line, locale, charset.FileviewContext)
|
||||
status = status.Or(statuses[i])
|
||||
}
|
||||
|
||||
for idx, code := range p.fileContent {
|
||||
tr := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.Tr.String(),
|
||||
}
|
||||
|
||||
lineNum := strconv.Itoa(p.lineOffset + idx + 1)
|
||||
|
||||
tdLinesnum := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.Td.String(),
|
||||
Attr: []html.Attribute{
|
||||
{Key: "class", Val: "lines-num"},
|
||||
},
|
||||
}
|
||||
spanLinesNum := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.Span.String(),
|
||||
Attr: []html.Attribute{
|
||||
{Key: "data-line-number", Val: lineNum},
|
||||
},
|
||||
}
|
||||
tdLinesnum.AppendChild(spanLinesNum)
|
||||
tr.AppendChild(tdLinesnum)
|
||||
|
||||
if status.Escaped {
|
||||
tdLinesEscape := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.Td.String(),
|
||||
Attr: []html.Attribute{
|
||||
{Key: "class", Val: "lines-escape"},
|
||||
},
|
||||
}
|
||||
|
||||
if statuses[idx].Escaped {
|
||||
btnTitle := ""
|
||||
if statuses[idx].HasInvisible {
|
||||
btnTitle += locale.TrString("repo.invisible_runes_line") + " "
|
||||
}
|
||||
if statuses[idx].HasAmbiguous {
|
||||
btnTitle += locale.TrString("repo.ambiguous_runes_line")
|
||||
}
|
||||
|
||||
escapeBtn := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.Button.String(),
|
||||
Attr: []html.Attribute{
|
||||
{Key: "class", Val: "toggle-escape-button btn interact-bg"},
|
||||
{Key: "title", Val: btnTitle},
|
||||
},
|
||||
}
|
||||
tdLinesEscape.AppendChild(escapeBtn)
|
||||
}
|
||||
|
||||
tr.AppendChild(tdLinesEscape)
|
||||
}
|
||||
|
||||
tdCode := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.Td.String(),
|
||||
Attr: []html.Attribute{
|
||||
{Key: "class", Val: "lines-code chroma"},
|
||||
},
|
||||
}
|
||||
codeInner := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.Code.String(),
|
||||
Attr: []html.Attribute{{Key: "class", Val: "code-inner"}},
|
||||
}
|
||||
codeText := &html.Node{
|
||||
Type: html.RawNode,
|
||||
Data: string(code),
|
||||
}
|
||||
codeInner.AppendChild(codeText)
|
||||
tdCode.AppendChild(codeInner)
|
||||
tr.AppendChild(tdCode)
|
||||
|
||||
tbody.AppendChild(tr)
|
||||
}
|
||||
|
||||
table.AppendChild(tbody)
|
||||
|
||||
twrapper := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.Div.String(),
|
||||
Attr: []html.Attribute{{Key: "class", Val: "ui table"}},
|
||||
}
|
||||
twrapper.AppendChild(table)
|
||||
|
||||
header := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.Div.String(),
|
||||
Attr: []html.Attribute{{Key: "class", Val: "header"}},
|
||||
}
|
||||
afilepath := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.A.String(),
|
||||
Attr: []html.Attribute{
|
||||
{Key: "href", Val: p.urlFull},
|
||||
{Key: "class", Val: "muted"},
|
||||
},
|
||||
}
|
||||
afilepath.AppendChild(&html.Node{
|
||||
Type: html.TextNode,
|
||||
Data: p.filePath,
|
||||
})
|
||||
header.AppendChild(afilepath)
|
||||
|
||||
psubtitle := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.Span.String(),
|
||||
Attr: []html.Attribute{{Key: "class", Val: "text small grey"}},
|
||||
}
|
||||
psubtitle.AppendChild(&html.Node{
|
||||
Type: html.RawNode,
|
||||
Data: string(p.subTitle),
|
||||
})
|
||||
header.AppendChild(psubtitle)
|
||||
|
||||
node := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.Div.String(),
|
||||
Attr: []html.Attribute{{Key: "class", Val: "file-preview-box"}},
|
||||
}
|
||||
node.AppendChild(header)
|
||||
|
||||
if p.isTruncated {
|
||||
warning := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: atom.Div.String(),
|
||||
Attr: []html.Attribute{{Key: "class", Val: "ui warning message tw-text-left"}},
|
||||
}
|
||||
warning.AppendChild(&html.Node{
|
||||
Type: html.TextNode,
|
||||
Data: locale.TrString("markup.filepreview.truncated"),
|
||||
})
|
||||
node.AppendChild(warning)
|
||||
}
|
||||
|
||||
node.AppendChild(twrapper)
|
||||
|
||||
return node
|
||||
}
|
|
@ -171,6 +171,7 @@ type processor func(ctx *RenderContext, node *html.Node)
|
|||
var defaultProcessors = []processor{
|
||||
fullIssuePatternProcessor,
|
||||
comparePatternProcessor,
|
||||
filePreviewPatternProcessor,
|
||||
fullHashPatternProcessor,
|
||||
shortLinkProcessor,
|
||||
linkProcessor,
|
||||
|
@ -1054,6 +1055,47 @@ func comparePatternProcessor(ctx *RenderContext, node *html.Node) {
|
|||
}
|
||||
}
|
||||
|
||||
func filePreviewPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
if ctx.Metas == nil {
|
||||
return
|
||||
}
|
||||
if DefaultProcessorHelper.GetRepoFileBlob == nil {
|
||||
return
|
||||
}
|
||||
|
||||
next := node.NextSibling
|
||||
for node != nil && node != next {
|
||||
locale, ok := ctx.Ctx.Value(translation.ContextKey).(translation.Locale)
|
||||
if !ok {
|
||||
locale = translation.NewLocale("en-US")
|
||||
}
|
||||
|
||||
preview := NewFilePreview(ctx, node, locale)
|
||||
if preview == nil {
|
||||
return
|
||||
}
|
||||
|
||||
previewNode := preview.CreateHTML(locale)
|
||||
|
||||
// Specialized version of replaceContent, so the parent paragraph element is not destroyed from our div
|
||||
before := node.Data[:preview.start]
|
||||
after := node.Data[preview.end:]
|
||||
node.Data = before
|
||||
nextSibling := node.NextSibling
|
||||
node.Parent.InsertBefore(&html.Node{
|
||||
Type: html.RawNode,
|
||||
Data: "</p>",
|
||||
}, nextSibling)
|
||||
node.Parent.InsertBefore(previewNode, nextSibling)
|
||||
node.Parent.InsertBefore(&html.Node{
|
||||
Type: html.RawNode,
|
||||
Data: "<p>" + after,
|
||||
}, nextSibling)
|
||||
|
||||
node = node.NextSibling
|
||||
}
|
||||
}
|
||||
|
||||
// emojiShortCodeProcessor for rendering text like :smile: into emoji
|
||||
func emojiShortCodeProcessor(ctx *RenderContext, node *html.Node) {
|
||||
start := 0
|
||||
|
|
|
@ -17,9 +17,11 @@ import (
|
|||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var localMetas = map[string]string{
|
||||
|
@ -676,3 +678,68 @@ func TestIssue18471(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Equal(t, "<a href=\"http://domain/org/repo/compare/783b039...da951ce\" class=\"compare\"><code class=\"nohighlight\">783b039...da951ce</code></a>", res.String())
|
||||
}
|
||||
|
||||
func TestRender_FilePreview(t *testing.T) {
|
||||
setting.StaticRootPath = "../../"
|
||||
setting.Names = []string{"english"}
|
||||
setting.Langs = []string{"en-US"}
|
||||
translation.InitLocales(context.Background())
|
||||
|
||||
setting.AppURL = markup.TestAppURL
|
||||
markup.Init(&markup.ProcessorHelper{
|
||||
GetRepoFileBlob: func(ctx context.Context, ownerName, repoName, commitSha, filePath string, language *string) (*git.Blob, error) {
|
||||
gitRepo, err := git.OpenRepository(git.DefaultContext, "./tests/repo/repo1_filepreview")
|
||||
require.NoError(t, err)
|
||||
defer gitRepo.Close()
|
||||
|
||||
commit, err := gitRepo.GetCommit("HEAD")
|
||||
require.NoError(t, err)
|
||||
|
||||
blob, err := commit.GetBlobByPath("path/to/file.go")
|
||||
require.NoError(t, err)
|
||||
|
||||
return blob, nil
|
||||
},
|
||||
})
|
||||
|
||||
sha := "190d9492934af498c3f669d6a2431dc5459e5b20"
|
||||
commitFilePreview := util.URLJoin(markup.TestRepoURL, "src", "commit", sha, "path", "to", "file.go") + "#L2-L3"
|
||||
|
||||
test := func(input, expected string) {
|
||||
buffer, err := markup.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
RelativePath: ".md",
|
||||
Metas: localMetas,
|
||||
}, input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
}
|
||||
|
||||
test(
|
||||
commitFilePreview,
|
||||
`<p></p>`+
|
||||
`<div class="file-preview-box">`+
|
||||
`<div class="header">`+
|
||||
`<a href="http://localhost:3000/gogits/gogs/src/commit/190d9492934af498c3f669d6a2431dc5459e5b20/path/to/file.go#L2-L3" class="muted" rel="nofollow">path/to/file.go</a>`+
|
||||
`<span class="text small grey">`+
|
||||
`Lines 2 to 3 in <a href="http://localhost:3000/gogits/gogs/src/commit/190d9492934af498c3f669d6a2431dc5459e5b20" class="text black" rel="nofollow">190d949</a>`+
|
||||
`</span>`+
|
||||
`</div>`+
|
||||
`<div class="ui table">`+
|
||||
`<table class="file-preview">`+
|
||||
`<tbody>`+
|
||||
`<tr>`+
|
||||
`<td class="lines-num"><span data-line-number="2"></span></td>`+
|
||||
`<td class="lines-code chroma"><code class="code-inner"><span class="nx">B</span>`+"\n"+`</code></td>`+
|
||||
`</tr>`+
|
||||
`<tr>`+
|
||||
`<td class="lines-num"><span data-line-number="3"></span></td>`+
|
||||
`<td class="lines-code chroma"><code class="code-inner"><span class="nx">C</span>`+"\n"+`</code></td>`+
|
||||
`</tr>`+
|
||||
`</tbody>`+
|
||||
`</table>`+
|
||||
`</div>`+
|
||||
`</div>`+
|
||||
`<p></p>`,
|
||||
)
|
||||
}
|
||||
|
|
19
modules/markup/markdown/color_util.go
Normal file
19
modules/markup/markdown/color_util.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package markdown
|
||||
|
||||
import "regexp"
|
||||
|
||||
var (
|
||||
hexRGB = regexp.MustCompile(`^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$`)
|
||||
hsl = regexp.MustCompile(`^hsl\([ ]*([012]?[0-9]{1,2}|3[0-5][0-9]|360),[ ]*([0-9]{0,2}|100)\%,[ ]*([0-9]{0,2}|100)\%\)$`)
|
||||
hsla = regexp.MustCompile(`^hsla\(([ ]*[012]?[0-9]{1,2}|3[0-5][0-9]|360),[ ]*([0-9]{0,2}|100)\%,[ ]*([0-9]{0,2}|100)\%,[ ]*(1|1\.0|0|(0\.[0-9]+))\)$`)
|
||||
rgb = regexp.MustCompile(`^rgb\(([ ]*((([0-9]{1,2}|100)\%)|(([01]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))),){2}([ ]*((([0-9]{1,2}|100)\%)|(([01]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))))\)$`)
|
||||
rgba = regexp.MustCompile(`^rgba\(([ ]*((([0-9]{1,2}|100)\%)|(([01]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))),){3}[ ]*(1(\.0)?|0|(0\.[0-9]+))\)$`)
|
||||
)
|
||||
|
||||
// matchColor return if color is in the form of hex RGB, HSL(A) or RGB(A).
|
||||
func matchColor(color string) bool {
|
||||
return hexRGB.MatchString(color) || rgb.MatchString(color) || rgba.MatchString(color) || hsl.MatchString(color) || hsla.MatchString(color)
|
||||
}
|
50
modules/markup/markdown/color_util_test.go
Normal file
50
modules/markup/markdown/color_util_test.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMatchColor(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input string
|
||||
expected bool
|
||||
}{
|
||||
{"#ddeeffa0", true},
|
||||
{"#ddeefe", true},
|
||||
{"#abcdef", true},
|
||||
{"#abcdeg", false},
|
||||
{"#abcdefg0", false},
|
||||
{"black", false},
|
||||
{"violet", false},
|
||||
{"rgb(255, 255, 255)", true},
|
||||
{"rgb(0, 0, 0)", true},
|
||||
{"rgb(256, 0, 0)", false},
|
||||
{"rgb(0, 256, 0)", false},
|
||||
{"rgb(0, 0, 256)", false},
|
||||
{"rgb(0, 0, 0, 1)", false},
|
||||
{"rgba(0, 0, 0)", false},
|
||||
{"rgba(0, 255, 0, 1)", true},
|
||||
{"rgba(32, 255, 12, 0.55)", true},
|
||||
{"rgba(32, 256, 12, 0.55)", false},
|
||||
{"hsl(0, 0%, 0%)", true},
|
||||
{"hsl(360, 100%, 100%)", true},
|
||||
{"hsl(361, 100%, 50%)", false},
|
||||
{"hsl(360, 101%, 50%)", false},
|
||||
{"hsl(360, 100%, 101%)", false},
|
||||
{"hsl(0, 0%, 0%, 0)", false},
|
||||
{"hsla(0, 0%, 0%)", false},
|
||||
{"hsla(0, 0%, 0%, 0)", true},
|
||||
{"hsla(0, 0%, 0%, 1)", true},
|
||||
{"hsla(0, 0%, 0%, 0.5)", true},
|
||||
{"hsla(0, 0%, 0%, 1.5)", false},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
actual := matchColor(testCase.input)
|
||||
assert.Equal(t, testCase.expected, actual)
|
||||
}
|
||||
}
|
|
@ -16,7 +16,6 @@ import (
|
|||
"code.gitea.io/gitea/modules/setting"
|
||||
giteautil "code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/microcosm-cc/bluemonday/css"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
east "github.com/yuin/goldmark/extension/ast"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
|
@ -199,7 +198,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
|
|||
}
|
||||
case *ast.CodeSpan:
|
||||
colorContent := n.Text(reader.Source())
|
||||
if css.ColorHandler(strings.ToLower(string(colorContent))) {
|
||||
if matchColor(strings.ToLower(string(colorContent))) {
|
||||
v.AppendChild(v, NewColorPreview(colorContent))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ const (
|
|||
|
||||
type ProcessorHelper struct {
|
||||
IsUsernameMentionable func(ctx context.Context, username string) bool
|
||||
GetRepoFileBlob func(ctx context.Context, ownerName, repoName, commitSha, filePath string, language *string) (*git.Blob, error)
|
||||
|
||||
ElementDir string // the direction of the elements, eg: "ltr", "rtl", "auto", default to no direction attribute
|
||||
}
|
||||
|
|
|
@ -113,6 +113,23 @@ func createDefaultPolicy() *bluemonday.Policy {
|
|||
// Allow 'color' and 'background-color' properties for the style attribute on text elements.
|
||||
policy.AllowStyles("color", "background-color").OnElements("span", "p")
|
||||
|
||||
// Allow classes for file preview links...
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile("^(lines-num|lines-code chroma)$")).OnElements("td")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile("^code-inner$")).OnElements("code")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile("^file-preview-box$")).OnElements("div")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile("^ui table$")).OnElements("div")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile("^header$")).OnElements("div")
|
||||
policy.AllowAttrs("data-line-number").Matching(regexp.MustCompile("^[0-9]+$")).OnElements("span")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile("^text small grey$")).OnElements("span")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile("^file-preview*")).OnElements("table")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile("^lines-escape$")).OnElements("td")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile("^toggle-escape-button btn interact-bg$")).OnElements("button")
|
||||
policy.AllowAttrs("title").OnElements("button")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile("^ambiguous-code-point$")).OnElements("span")
|
||||
policy.AllowAttrs("data-tooltip-content").OnElements("span")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile("muted|(text black)")).OnElements("a")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile("^ui warning message tw-text-left$")).OnElements("div")
|
||||
|
||||
// Allow generally safe attributes
|
||||
generalSafeAttrs := []string{
|
||||
"abbr", "accept", "accept-charset",
|
||||
|
|
1
modules/markup/tests/repo/repo1_filepreview/HEAD
Normal file
1
modules/markup/tests/repo/repo1_filepreview/HEAD
Normal file
|
@ -0,0 +1 @@
|
|||
ref: refs/heads/master
|
6
modules/markup/tests/repo/repo1_filepreview/config
Normal file
6
modules/markup/tests/repo/repo1_filepreview/config
Normal file
|
@ -0,0 +1,6 @@
|
|||
[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = true
|
||||
[remote "origin"]
|
||||
url = /home/mai/projects/codeark/forgejo/forgejo/modules/markup/tests/repo/repo1_filepreview/../../__test_repo
|
1
modules/markup/tests/repo/repo1_filepreview/description
Normal file
1
modules/markup/tests/repo/repo1_filepreview/description
Normal file
|
@ -0,0 +1 @@
|
|||
Unnamed repository; edit this file 'description' to name the repository.
|
6
modules/markup/tests/repo/repo1_filepreview/info/exclude
Normal file
6
modules/markup/tests/repo/repo1_filepreview/info/exclude
Normal file
|
@ -0,0 +1,6 @@
|
|||
# git ls-files --others --exclude-from=.git/info/exclude
|
||||
# Lines that start with '#' are comments.
|
||||
# For a project mostly in C, the following would be a good set of
|
||||
# exclude patterns (uncomment them if you want to use them):
|
||||
# *.[oa]
|
||||
# *~
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1 @@
|
|||
x+)JMU06e040031QHËÌIÕKÏghQºÂ/TX'·7潊ç·såË#3‹ô
|
|
@ -0,0 +1 @@
|
|||
190d9492934af498c3f669d6a2431dc5459e5b20
|
|
@ -26,7 +26,7 @@ var (
|
|||
)
|
||||
|
||||
// DescriptionPProfLabel is a label set on goroutines that have a process attached
|
||||
const DescriptionPProfLabel = "process-description"
|
||||
const DescriptionPProfLabel = "processDescription"
|
||||
|
||||
// PIDPProfLabel is a label set on goroutines that have a process attached
|
||||
const PIDPProfLabel = "pid"
|
||||
|
@ -35,7 +35,7 @@ const PIDPProfLabel = "pid"
|
|||
const PPIDPProfLabel = "ppid"
|
||||
|
||||
// ProcessTypePProfLabel is a label set on goroutines that have a process attached
|
||||
const ProcessTypePProfLabel = "process-type"
|
||||
const ProcessTypePProfLabel = "processType"
|
||||
|
||||
// IDType is a pid type
|
||||
type IDType string
|
||||
|
|
|
@ -15,6 +15,7 @@ var (
|
|||
ExternalMarkupRenderers []*MarkupRenderer
|
||||
ExternalSanitizerRules []MarkupSanitizerRule
|
||||
MermaidMaxSourceCharacters int
|
||||
FilePreviewMaxLines int
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -62,6 +63,7 @@ func loadMarkupFrom(rootCfg ConfigProvider) {
|
|||
mustMapSetting(rootCfg, "markdown", &Markdown)
|
||||
|
||||
MermaidMaxSourceCharacters = rootCfg.Section("markup").Key("MERMAID_MAX_SOURCE_CHARACTERS").MustInt(5000)
|
||||
FilePreviewMaxLines = rootCfg.Section("markup").Key("FILEPREVIEW_MAX_LINES").MustInt(50)
|
||||
ExternalMarkupRenderers = make([]*MarkupRenderer, 0, 10)
|
||||
ExternalSanitizerRules = make([]MarkupSanitizerRule, 0, 10)
|
||||
|
||||
|
|
|
@ -47,6 +47,7 @@ type BranchProtection struct {
|
|||
RequireSignedCommits bool `json:"require_signed_commits"`
|
||||
ProtectedFilePatterns string `json:"protected_file_patterns"`
|
||||
UnprotectedFilePatterns string `json:"unprotected_file_patterns"`
|
||||
ApplyToAdmins bool `json:"apply_to_admins"`
|
||||
// swagger:strfmt date-time
|
||||
Created time.Time `json:"created_at"`
|
||||
// swagger:strfmt date-time
|
||||
|
@ -80,6 +81,7 @@ type CreateBranchProtectionOption struct {
|
|||
RequireSignedCommits bool `json:"require_signed_commits"`
|
||||
ProtectedFilePatterns string `json:"protected_file_patterns"`
|
||||
UnprotectedFilePatterns string `json:"unprotected_file_patterns"`
|
||||
ApplyToAdmins bool `json:"apply_to_admins"`
|
||||
}
|
||||
|
||||
// EditBranchProtectionOption options for editing a branch protection
|
||||
|
@ -106,4 +108,5 @@ type EditBranchProtectionOption struct {
|
|||
RequireSignedCommits *bool `json:"require_signed_commits"`
|
||||
ProtectedFilePatterns *string `json:"protected_file_patterns"`
|
||||
UnprotectedFilePatterns *string `json:"unprotected_file_patterns"`
|
||||
ApplyToAdmins *bool `json:"apply_to_admins"`
|
||||
}
|
||||
|
|
|
@ -63,7 +63,7 @@ func NewFuncMap() template.FuncMap {
|
|||
|
||||
// -----------------------------------------------------------------
|
||||
// time / number / format
|
||||
"FileSize": base.FileSize,
|
||||
"FileSize": FileSizePanic,
|
||||
"CountFmt": base.FormatNumberSI,
|
||||
"TimeSince": timeutil.TimeSince,
|
||||
"TimeSinceUnix": timeutil.TimeSinceUnix,
|
||||
|
@ -249,3 +249,7 @@ func Eval(tokens ...any) (any, error) {
|
|||
n, err := eval.Expr(tokens...)
|
||||
return n.Value, err
|
||||
}
|
||||
|
||||
func FileSizePanic(s int64) string {
|
||||
panic("Usage of FileSize in templates is deprecated in Forgejo. Locale.TrSize should be used instead.")
|
||||
}
|
||||
|
|
|
@ -43,9 +43,7 @@ func (w *testLoggerWriterCloser) pushT(t testing.TB) {
|
|||
}
|
||||
|
||||
func (w *testLoggerWriterCloser) Log(level log.Level, msg string) {
|
||||
if len(msg) > 0 && msg[len(msg)-1] == '\n' {
|
||||
msg = msg[:len(msg)-1]
|
||||
}
|
||||
msg = strings.TrimSpace(msg)
|
||||
|
||||
w.printMsg(msg)
|
||||
if level >= log.ERROR {
|
||||
|
@ -56,10 +54,13 @@ func (w *testLoggerWriterCloser) Log(level log.Level, msg string) {
|
|||
// list of error message which will not fail the test
|
||||
// ideally this list should be empty, however ensuring that it does not grow
|
||||
// is already a good first step.
|
||||
var ignoredErrorMessageSuffixes = []string{
|
||||
var ignoredErrorMessage = []string{
|
||||
// only seen on mysql tests https://codeberg.org/forgejo/forgejo/pulls/2657#issuecomment-1693055
|
||||
`table columns using inconsistent collation, they should use "utf8mb4_0900_ai_ci". Please go to admin panel Self Check page`,
|
||||
|
||||
// TestPullWIPConvertSidebar
|
||||
`:PullRequestPushCommits() [E] comment.LoadIssue: issue does not exist [id:`,
|
||||
|
||||
// TestAPIDeleteReleaseByTagName
|
||||
// action notification were a commit cannot be computed (because the commit got deleted)
|
||||
`Notify() [E] an error occurred while executing the DeleteRelease actions method: gitRepo.GetCommit: object does not exist [id: refs/tags/release-tag, rel_path: ]`,
|
||||
|
@ -76,6 +77,14 @@ var ignoredErrorMessageSuffixes = []string{
|
|||
// TestAPIGenerateRepo
|
||||
`Notify() [E] an error occurred while executing the CreateRepository actions method: gitRepo.GetCommit: object does not exist [id: , rel_path: ]`,
|
||||
|
||||
// TestAPIPullUpdateByRebase
|
||||
`:testPR() [E] Unable to GetPullRequestByID[`,
|
||||
`:PullRequestSynchronized() [E] LoadAttributes: getRepositoryByID `,
|
||||
`:PullRequestSynchronized() [E] pr.Issue.LoadRepo: getRepositoryByID [`,
|
||||
`:handler() [E] Was unable to create issue notification: issue does not exist [`,
|
||||
`:func1() [E] PullRequestList.LoadAttributes: issues and prs may be not in sync: cannot find issue`,
|
||||
`:func1() [E] checkForInvalidation: GetRepositoryByIDCtx: repository does not exist `,
|
||||
|
||||
// TestAPIPullReview
|
||||
`PullRequestReview() [E] Unsupported review webhook type`,
|
||||
|
||||
|
@ -111,11 +120,251 @@ var ignoredErrorMessageSuffixes = []string{
|
|||
|
||||
// TestRebuildCargo
|
||||
`RebuildCargoIndex() [E] RebuildIndex failed: GetRepositoryByOwnerAndName: repository does not exist [id: 0, uid: 0, owner_name: user2, name: _cargo-index]`,
|
||||
|
||||
// TestDangerZoneConfirmation/Convert_fork/Fail
|
||||
`/gitea-repositories/user20/big_test_public_fork_7.git Error: no such file or directory`,
|
||||
// TestGitSmartHTTP
|
||||
`:sendFile() [E] request file path contains invalid path: objects/info/..\..\..\..\custom\conf\app.ini`,
|
||||
// TestGit/HTTP/BranchProtectMerge
|
||||
`:SSHLog() [E] ssh: Not allowed to push to protected branch protected. HookPreReceive(last) failed: internal API error response, status=403`,
|
||||
// TestGit/HTTP/BranchProtectMerge
|
||||
`:SSHLog() [E] ssh: Not allowed to push to protected branch protected. HookPreReceive(last) failed: internal API error response, status=403`,
|
||||
// TestGit/HTTP/BranchProtectMerge
|
||||
`:SSHLog() [E] ssh: branch protected is protected from force push. HookPreReceive(last) failed: internal API error response, status=403`,
|
||||
// TestGit/HTTP/MergeFork/CreatePRAndMerge
|
||||
`:DeleteBranchPost() [E] DeleteBranch: GetBranch: branch does not exist [repo_id: 1099 name: user2:master]`, // sqlite
|
||||
"s/web/repo/branch.go:108:DeleteBranchPost() [E] DeleteBranch: GetBranch: branch does not exist [repo_id: 10000 name: user2:master]", // mysql
|
||||
"s/web/repo/branch.go:108:DeleteBranchPost() [E] DeleteBranch: GetBranch: branch does not exist [repo_id: 1060 name: user2:master]", // pgsql
|
||||
// TestGit/HTTP/BranchProtectMerge
|
||||
`:func1() [E] PushToBaseRepo: PushRejected Error: exit status 1 - remote: error: cannot lock ref`,
|
||||
// TestGit/SSH/BranchProtectMerge
|
||||
`:func1() [E] PushToBaseRepo: PushRejected Error: exit status 1 - remote: error: cannot lock ref`,
|
||||
// TestGit/SSH/LFS/PushCommit/Little
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/LFS/PushCommit/Little
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/LFS/PushCommit/Big
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/LFS/PushCommit/Big
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/LFS/Locks
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/LFS/Locks
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/LFS/Locks
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/LFS/Locks
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/LFS/Locks
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/CreateAgitFlowPull/PushParams/NoParams
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/CreateAgitFlowPull/PushParams/NoParams
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/CreateAgitFlowPull/PushParams/TitleOverride
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/CreateAgitFlowPull/PushParams/TitleOverride
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/CreateAgitFlowPull/PushParams/DescriptionOverride
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/CreateAgitFlowPull/PushParams/DescriptionOverride
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/CreateAgitFlowPull/Force_push/Fails
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/CreateAgitFlowPull/Force_push/Fails
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/CreateAgitFlowPull/Force_push/Succeeds
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/CreateAgitFlowPull/Force_push/Succeeds
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/CreateAgitFlowPull/Force_push
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/CreateAgitFlowPull/Force_push
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/CreateAgitFlowPull/Branch_already_contains_commit
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/CreateAgitFlowPull/Branch_already_contains_commit
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/CreateAgitFlowPull
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/CreateAgitFlowPull
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/CreateAgitFlowPull
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/CreateAgitFlowPull
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/CreateAgitFlowPull
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/CreateAgitFlowPull
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/CreateAgitFlowPull
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/CreateAgitFlowPull
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/BranchProtectMerge
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/BranchProtectMerge
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/BranchProtectMerge
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/BranchProtectMerge
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/BranchProtectMerge
|
||||
`:SSHLog() [E] ssh: Not allowed to push to protected branch protected. HookPreReceive(last) failed: internal API error response, status=403`,
|
||||
// TestGit/SSH/BranchProtectMerge
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/BranchProtectMerge
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/BranchProtectMerge
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/BranchProtectMerge
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/BranchProtectMerge
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/BranchProtectMerge
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/BranchProtectMerge
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/BranchProtectMerge
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/BranchProtectMerge
|
||||
`:SSHLog() [E] ssh: branch protected is protected from force push. HookPreReceive(last) failed: internal API error response, status=403`,
|
||||
// TestGit/SSH/BranchProtectMerge
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/BranchProtectMerge
|
||||
`:SSHLog() [E] ssh: Unknown git command. Unknown git command git-lfs-transfer`,
|
||||
// TestGit/SSH/MergeFork/CreatePRAndMerge
|
||||
`:DeleteBranchPost() [E] DeleteBranch: GetBranch: branch does not exist [repo_id: 1102 name: user2:master]`, // sqlite
|
||||
"s/web/repo/branch.go:108:DeleteBranchPost() [E] DeleteBranch: GetBranch: branch does not exist [repo_id: 10003 name: user2:master]", // mysql
|
||||
"s/web/repo/branch.go:108:DeleteBranchPost() [E] DeleteBranch: GetBranch: branch does not exist [repo_id: 1063 name: user2:master]", // pgsql
|
||||
// TestGit/SSH/PushCreate
|
||||
`:SSHLog() [E] ssh: Push to create is not enabled for users. ServCommand failed: internal API error response, status=403`,
|
||||
// TestGit/SSH/PushCreate
|
||||
`:SSHLog() [E] ssh: Cannot find repository: user2/repo-tmp-push-create-ssh. ServCommand failed: internal API error response, status=404`,
|
||||
// TestGit/SSH/PushCreate
|
||||
`:SSHLog() [E] ssh: Invalid repo name. Invalid repo name: invalid/repo-tmp-push-create-ssh`,
|
||||
// TestIssueReaction
|
||||
`:ChangeIssueReaction() [E] ChangeIssueReaction: '8ball' is not an allowed reaction`,
|
||||
// TestIssuePinMove
|
||||
`:IssuePinMove() [E] Issue does not belong to this repository`,
|
||||
// TestLinksLogin
|
||||
`:GetIssuesAllCommitStatus() [E] getAllCommitStatus: cant get commit statuses of pull [6]: object does not exist [id: refs/pull/2/head, rel_path: ]`,
|
||||
// TestLinksLogin
|
||||
`:GetIssuesAllCommitStatus() [E] getAllCommitStatus: cant get commit statuses of pull [6]: object does not exist [id: refs/pull/2/head, rel_path: ]`,
|
||||
// TestLinksLogin
|
||||
`:GetIssuesAllCommitStatus() [E] getAllCommitStatus: cant get commit statuses of pull [6]: object does not exist [id: refs/pull/2/head, rel_path: ]`,
|
||||
// TestLinksLogin
|
||||
`:GetIssuesAllCommitStatus() [E] Cannot open git repository <Repository 23:org17/big_test_public_4> for issue #1[20]. Error: no such file or directory`,
|
||||
// TestMigrate
|
||||
`] for OwnerID[2] failed: error while listing issues: token does not have at least one of required scope(s): [read:issue]`,
|
||||
// TestMigrate
|
||||
`:handler() [E] Run task failed: error while listing issues: token does not have at least one of required scope(s): [read:issue]`,
|
||||
// TestMigrate
|
||||
`] for OwnerID[2] failed: error while listing issues: token does not have at least one of required scope(s): [read:issue]`,
|
||||
// TestMigrate
|
||||
`:handler() [E] Run task failed: error while listing issues: token does not have at least one of required scope(s): [read:issue]`,
|
||||
// TestMirrorPush
|
||||
`:GetInfoRefs() [E] fork/exec /usr/bin/git: no such file or directory -`,
|
||||
|
||||
// TestOrgMembers
|
||||
`:loadOrganizationOwners() [E] Organization does not have owner team: 25`,
|
||||
// TestOrgMembers
|
||||
`:loadOrganizationOwners() [E] Organization does not have owner team: 25`,
|
||||
// TestOrgMembers
|
||||
`:loadOrganizationOwners() [E] Organization does not have owner team: 25`,
|
||||
// TestRecentlyPushed/unrelated_branches_are_not_shown
|
||||
`:SyncRepoBranches() [E] OpenRepository[user30/repo50]: %!w(*errors.errorString=&{no such file or directory})`,
|
||||
// TestRecentlyPushed/unrelated_branches_are_not_shown
|
||||
`:handlerBranchSync() [E] syncRepoBranches [50] failed: no such file or directory`,
|
||||
// TestRecentlyPushed/unrelated_branches_are_not_shown
|
||||
`:SyncRepoBranches() [E] OpenRepository[user30/repo51]: %!w(*errors.errorString=&{no such file or directory})`,
|
||||
// TestRecentlyPushed/unrelated_branches_are_not_shown
|
||||
`:handlerBranchSync() [E] syncRepoBranches [51] failed: no such file or directory`,
|
||||
// TestRecentlyPushed/unrelated_branches_are_not_shown
|
||||
`:SyncRepoBranches() [E] OpenRepository[user2/scoped_label]: %!w(*errors.errorString=&{no such file or directory})`,
|
||||
// TestRecentlyPushed/unrelated_branches_are_not_shown
|
||||
`:handlerBranchSync() [E] syncRepoBranches [55] failed: no such file or directory`,
|
||||
// TestCantMergeConflict
|
||||
"]user1/repo1#1[base...conflict]> Unable to merge tracking into base: Merge Conflict Error: exit status 1: \nAuto-merging README.md\nCONFLICT (content): Merge conflict in README.md\nAutomatic merge failed; fix conflicts and then commit the result.",
|
||||
|
||||
// TestCantMergeUnrelated
|
||||
`]user1/repo1#1[base...unrelated]> Unable to merge tracking into base: Merge UnrelatedHistories Error: exit status 128: fatal: refusing to merge unrelated histories`,
|
||||
// TestCantFastForwardOnlyMergeDiverging
|
||||
"]user1/repo1#1[master...diverging]> Unable to merge tracking into base: Merge DivergingFastForwardOnly Error: exit status 128: hint: Diverging branches can't be fast-forwarded, you need to either:\nhint: \nhint: \tgit merge --no-ff\nhint: \nhint: or:\nhint: \nhint: \tgit rebase\nhint: \nhint: Disable this message with \"git config advice.diverging false\"\nfatal: Not possible to fast-forward, aborting.",
|
||||
// TestPullrequestReopen/Base_branch_deleted
|
||||
`fatal: couldn't find remote ref base-branch`,
|
||||
// TestPullrequestReopen/Head_branch_deleted
|
||||
`]user2/reopen-base#1[base-branch...org26/reopen-head:head-branch]>]: branch does not exist [repo_id: 0 name: head-branch]`,
|
||||
// TestDatabaseMissingABranch
|
||||
`:SyncRepoBranches() [E] OpenRepository[user30/repo50]: %!w(*errors.errorString=&{no such file or directory})`,
|
||||
// TestDatabaseMissingABranch
|
||||
`:handlerBranchSync() [E] syncRepoBranches [50] failed: no such file or directory`,
|
||||
// TestDatabaseMissingABranch
|
||||
`:SyncRepoBranches() [E] OpenRepository[user30/repo51]: %!w(*errors.errorString=&{no such file or directory})`,
|
||||
// TestDatabaseMissingABranch
|
||||
`:handlerBranchSync() [E] syncRepoBranches [51] failed: no such file or directory`,
|
||||
// TestDatabaseMissingABranch
|
||||
`:SyncRepoBranches() [E] OpenRepository[user2/scoped_label]: %!w(*errors.errorString=&{no such file or directory})`,
|
||||
// TestDatabaseMissingABranch
|
||||
`:handlerBranchSync() [E] syncRepoBranches [55] failed: no such file or directory`,
|
||||
// TestDatabaseMissingABranch
|
||||
`:LoadBranches() [E] loadOneBranch() on repo #1, branch 'will-be-missing' failed: CountDivergingCommits: exit status 128 - fatal: bad revision 'master...refs/heads/will-be-missing'
|
||||
- fatal: bad revision 'master...refs/heads/will-be-missing'`,
|
||||
// TestDatabaseMissingABranch
|
||||
`:SyncRepoBranches() [E] OpenRepository[user30/repo50]: %!w(*errors.errorString=&{no such file or directory})`,
|
||||
// TestDatabaseMissingABranch
|
||||
`:handlerBranchSync() [E] syncRepoBranches [50] failed: no such file or directory`,
|
||||
// TestDatabaseMissingABranch
|
||||
`:SyncRepoBranches() [E] OpenRepository[user30/repo51]: %!w(*errors.errorString=&{no such file or directory})`,
|
||||
// TestDatabaseMissingABranch
|
||||
`:handlerBranchSync() [E] syncRepoBranches [51] failed: no such file or directory`,
|
||||
// TestDatabaseMissingABranch
|
||||
`:SyncRepoBranches() [E] OpenRepository[user2/scoped_label]: %!w(*errors.errorString=&{no such file or directory})`,
|
||||
// TestDatabaseMissingABranch
|
||||
`:handlerBranchSync() [E] syncRepoBranches [55] failed: no such file or directory`,
|
||||
// TestDatabaseMissingABranch
|
||||
"LoadBranches() [E] loadOneBranch() on repo #1, branch 'will-be-missing' failed: CountDivergingCommits: exit status 128 - fatal: bad revision 'master...refs/heads/will-be-missing'\n - fatal: bad revision 'master...refs/heads/will-be-missing'",
|
||||
|
||||
// TestCreateNewTagProtected/Git
|
||||
`:SSHLog() [E] ssh: Tag v-2 is protected. HookPreReceive(last) failed: internal API error response, status=403`,
|
||||
// TestMarkDownReadmeImage
|
||||
`:checkOutdatedBranch() [E] GetBranch: branch does not exist [repo_id: 1 name: home-md-img-check]`,
|
||||
// TestMarkDownReadmeImage
|
||||
`:checkOutdatedBranch() [E] GetBranch: branch does not exist [repo_id: 1 name: home-md-img-check]`,
|
||||
// TestMarkDownReadmeImageSubfolder
|
||||
`:checkOutdatedBranch() [E] GetBranch: branch does not exist [repo_id: 1 name: sub-home-md-img-check]`,
|
||||
// TestMarkDownReadmeImageSubfolder
|
||||
`:checkOutdatedBranch() [E] GetBranch: branch does not exist [repo_id: 1 name: sub-home-md-img-check]`,
|
||||
|
||||
// TestKeyOnlyOneType
|
||||
`:ssh-key-test-repo-push is not authorized to write to user2/ssh-key-test-repo. ServCommand failed: internal API error response, status=401`,
|
||||
|
||||
// TestPullRebase
|
||||
"/gitea-repositories/user2/repo1.git' does not appear to be a git repository\nfatal: Could not read from remote repository.\n\nPlease make sure you have the correct access rights\nand the repository exists.",
|
||||
|
||||
// TestPullRebaseMerge
|
||||
"]user2/repo1#3[master...branch2]>]: branch does not exist [repo_id: 0 name: branch2]",
|
||||
|
||||
// TestAuthorizeNoClientID
|
||||
`TrString() [E] Missing translation "form.ResponseType"`,
|
||||
|
||||
// TestWebhookForms
|
||||
`TrString() [E] Missing translation "form.AuthorizationHeader"`,
|
||||
`TrString() [E] Missing translation "form.Channel"`,
|
||||
`TrString() [E] Missing translation "form.ContentType"`,
|
||||
`TrString() [E] Missing translation "form.HTTPMethod"`,
|
||||
`TrString() [E] Missing translation "form.PayloadURL"`,
|
||||
|
||||
// TestRenameInvalidUsername
|
||||
`TrString() [E] Missing translation "form.Name"`,
|
||||
|
||||
// TestDatabaseCollation
|
||||
`[E] [Error SQL Query] INSERT INTO test_collation_tbl (txt) VALUES ('main') []`,
|
||||
}
|
||||
|
||||
func (w *testLoggerWriterCloser) recordError(msg string) {
|
||||
for _, s := range ignoredErrorMessageSuffixes {
|
||||
if strings.HasSuffix(msg, s) {
|
||||
for _, s := range ignoredErrorMessage {
|
||||
if strings.Contains(msg, s) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -128,6 +377,11 @@ func (w *testLoggerWriterCloser) recordError(msg string) {
|
|||
err = w.errs[len(w.errs)-1]
|
||||
}
|
||||
|
||||
if len(w.t) > 0 {
|
||||
// format error message to easily add it to the ignore list
|
||||
msg = fmt.Sprintf("// %s\n\t%q,", w.t[len(w.t)-1].Name(), msg)
|
||||
}
|
||||
|
||||
err = errors.Join(err, errors.New(msg))
|
||||
|
||||
if len(w.errs) > 0 {
|
||||
|
@ -231,7 +485,9 @@ func PrintCurrentTest(t testing.TB, skip ...int) func() {
|
|||
}
|
||||
|
||||
if err := WriterCloser.popT(); err != nil {
|
||||
t.Errorf("testlogger.go:recordError() FATAL ERROR: log.Error has been called: %v", err)
|
||||
// disable test failure for now (too flacky)
|
||||
_, _ = fmt.Fprintf(os.Stdout, "testlogger.go:recordError() FATAL ERROR: log.Error has been called: %v", err)
|
||||
// t.Errorf("testlogger.go:recordError() FATAL ERROR: log.Error has been called: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,10 @@ func (l MockLocale) TrN(cnt any, key1, keyN string, args ...any) template.HTML {
|
|||
return template.HTML(key1)
|
||||
}
|
||||
|
||||
func (l MockLocale) TrSize(s int64) ReadableSize {
|
||||
return ReadableSize{fmt.Sprint(s), ""}
|
||||
}
|
||||
|
||||
func (l MockLocale) PrettyNumber(v any) string {
|
||||
return fmt.Sprint(v)
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/translation/i18n"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/message"
|
||||
"golang.org/x/text/number"
|
||||
|
@ -33,6 +34,8 @@ type Locale interface {
|
|||
Tr(key string, args ...any) template.HTML
|
||||
TrN(cnt any, key1, keyN string, args ...any) template.HTML
|
||||
|
||||
TrSize(size int64) ReadableSize
|
||||
|
||||
PrettyNumber(v any) string
|
||||
}
|
||||
|
||||
|
@ -252,6 +255,35 @@ func (l *locale) TrN(cnt any, key1, keyN string, args ...any) template.HTML {
|
|||
return l.Tr(keyN, args...)
|
||||
}
|
||||
|
||||
type ReadableSize struct {
|
||||
PrettyNumber string
|
||||
TranslatedUnit string
|
||||
}
|
||||
|
||||
func (bs ReadableSize) String() string {
|
||||
return bs.PrettyNumber + " " + bs.TranslatedUnit
|
||||
}
|
||||
|
||||
// TrSize returns array containing pretty formatted size and localized output of FileSize
|
||||
// output of humanize.IBytes has to be split in order to be localized
|
||||
func (l *locale) TrSize(s int64) ReadableSize {
|
||||
us := uint64(s)
|
||||
if s < 0 {
|
||||
us = uint64(-s)
|
||||
}
|
||||
untranslated := humanize.IBytes(us)
|
||||
if s < 0 {
|
||||
untranslated = "-" + untranslated
|
||||
}
|
||||
numberVal, unitVal, found := strings.Cut(untranslated, " ")
|
||||
if !found {
|
||||
log.Error("no space in go-humanized size of %d: %q", s, untranslated)
|
||||
}
|
||||
numberVal = l.PrettyNumber(numberVal)
|
||||
unitVal = l.TrString("munits.data." + strings.ToLower(unitVal))
|
||||
return ReadableSize{numberVal, unitVal}
|
||||
}
|
||||
|
||||
func (l *locale) PrettyNumber(v any) string {
|
||||
// TODO: this mechanism is not good enough, the complete solution is to switch the translation system to ICU message format
|
||||
if s, ok := v.(string); ok {
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
package translation
|
||||
|
||||
// TODO: make this package friendly to testing
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
|
@ -11,9 +13,25 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPrettyNumber(t *testing.T) {
|
||||
// TODO: make this package friendly to testing
|
||||
func TestTrSize(t *testing.T) {
|
||||
l := NewLocale("")
|
||||
size := int64(1)
|
||||
assert.EqualValues(t, "1 munits.data.b", l.TrSize(size).String())
|
||||
size *= 2048
|
||||
assert.EqualValues(t, "2 munits.data.kib", l.TrSize(size).String())
|
||||
size *= 2048
|
||||
assert.EqualValues(t, "4 munits.data.mib", l.TrSize(size).String())
|
||||
size *= 2048
|
||||
assert.EqualValues(t, "8 munits.data.gib", l.TrSize(size).String())
|
||||
size *= 2048
|
||||
assert.EqualValues(t, "16 munits.data.tib", l.TrSize(size).String())
|
||||
size *= 2048
|
||||
assert.EqualValues(t, "32 munits.data.pib", l.TrSize(size).String())
|
||||
size *= 128
|
||||
assert.EqualValues(t, "4 munits.data.eib", l.TrSize(size).String())
|
||||
}
|
||||
|
||||
func TestPrettyNumber(t *testing.T) {
|
||||
i18n.ResetDefaultLocales()
|
||||
|
||||
allLangMap = make(map[string]*LangType)
|
||||
|
|
|
@ -680,7 +680,7 @@ issues.self_assign_at = `كلّف نفسه بها %s`
|
|||
issues.label_deletion = احذف التصنيف
|
||||
issues.filter_milestone_all = كل الأهداف
|
||||
issues.unlock.notice_2 = - يمكنك دوما إقفال هذه المسألة من جديد في المستقبل.
|
||||
issues.num_participants = %d متحاور
|
||||
issues.num_participants_few = %d متحاور
|
||||
release.title = عنوان الإصدار
|
||||
issues.closed_at = `أغلق هذه المسألة <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
issues.lock.title = إقفال التحاور في هذه المسألة.
|
||||
|
|
|
@ -125,10 +125,16 @@ orgs_none = Не сте участник в никакви организаци
|
|||
repos_none = Не притежавате никакви хранилища.
|
||||
blocked_users_none = Няма блокирани потребители.
|
||||
profile_desc = Контролирайте как вашият профил се показва на другите потребители. Вашият основен адрес на ел. поща ще се използва за известия, възстановяване на паролата и уеб базирани Git операции.
|
||||
permission_write = Четене и Писане
|
||||
permission_write = Четене и писане
|
||||
twofa_disable = Изключване на двуфакторното удостоверяване
|
||||
twofa_enroll = Включване на двуфакторно удостоверяване
|
||||
ssh_key_name_used = Вече съществува SSH ключ със същото име във вашия акаунт.
|
||||
email_notifications.enable = Включване на известията по ел. поща
|
||||
delete_prompt = Тази операция ще изтрие перманентно потребителския ви акаунт. Това <strong>НЕ МОЖЕ</strong> да бъде отменено.
|
||||
email_notifications.disable = Изключване на известията по ел. поща
|
||||
delete_account = Изтриване на акаунта ви
|
||||
confirm_delete_account = Потвърждаване на изтриването
|
||||
email_notifications.onmention = Ел. поща само при споменаване
|
||||
|
||||
[packages]
|
||||
container.labels.value = Стойност
|
||||
|
@ -258,7 +264,7 @@ new_fork = Ново разклонение на хранилище
|
|||
unpin = Откачване
|
||||
pin = Закачване
|
||||
filter = Филтър
|
||||
filter.clear = Изчистване на филтъра
|
||||
filter.clear = Изчистване на филтрите
|
||||
filter.is_archived = Архивирани
|
||||
filter.not_archived = Не архивирани
|
||||
filter.is_fork = Разклонени
|
||||
|
@ -269,6 +275,7 @@ filter.not_template = Не шаблони
|
|||
filter.private = Частни
|
||||
filter.is_mirror = Огледални
|
||||
filter.not_mirror = Не огледални
|
||||
copy_hash = Копиране на контролната сума
|
||||
|
||||
[repo]
|
||||
issues.context.edit = Редактиране
|
||||
|
@ -366,7 +373,7 @@ issues.keyword_search_unavailable = В момента търсенето по к
|
|||
repo_desc_helper = Въведете кратко описание (опционално)
|
||||
mirror_address = Клониране от URL
|
||||
owner_helper = Някои организации може да не се показват в падащото меню поради ограничение за максимален брой хранилища.
|
||||
new_repo_helper = Хранилище съдържа всички файлове на проекта, включително хронологията на ревизиите. Вече хоствате хранилище другаде? <a href="%s">Мигрирайте хранилище.</a>
|
||||
new_repo_helper = Хранилището съдържа всички файлове на проекта, включително хронологията на ревизиите. Вече хоствате хранилище другаде? <a href="%s">Мигрирайте хранилище.</a>
|
||||
repo_name_helper = Добрите имена на хранилища използват кратки, запомнящи се и уникални ключови думи.
|
||||
migrated_from = Мигрирано от <a href="%[1]s">%[2]s</a>
|
||||
visibility_description = Само притежателят или участниците в организацията, ако имат права, ще могат да го видят.
|
||||
|
@ -448,7 +455,7 @@ fork_from = Разклоняване от
|
|||
diff.comment.placeholder = Оставете коментар
|
||||
projects.edit = Редактиране на проекта
|
||||
projects.modify = Редактиране на проекта
|
||||
issues.new.no_label = Няма етикет
|
||||
issues.new.no_label = Няма етикети
|
||||
issues.new.title_empty = Заглавието не може да бъде празно
|
||||
issues.new.projects = Проекти
|
||||
issues.new.clear_projects = Изчистване на проектите
|
||||
|
@ -534,7 +541,7 @@ settings.collaboration.write = Писане
|
|||
settings.collaboration.read = Четене
|
||||
settings.collaboration.owner = Притежател
|
||||
settings.basic_settings = Основни настройки
|
||||
settings.wiki_desc = Включване на уики на хранилището
|
||||
settings.wiki_desc = Включване на уикито за хранилището
|
||||
settings.use_internal_wiki = Използване на вграденото уики
|
||||
settings.wiki_globally_editable = Позволяване на всеки да редактира уикито
|
||||
settings.add_collaborator = Добавяне на сътрудник
|
||||
|
@ -629,7 +636,7 @@ issues.filter_milestone_all = Всички етапи
|
|||
issues.filter_milestone_open = Отворени етапи
|
||||
issues.filter_milestone_none = Без етапи
|
||||
issues.filter_project = Проект
|
||||
issues.num_participants = %d участващи
|
||||
issues.num_participants_few = %d участващи
|
||||
issues.filter_assignee = Изпълнител
|
||||
issues.filter_milestone_closed = Затворени етапи
|
||||
issues.filter_assginee_no_select = Всички изпълнители
|
||||
|
@ -667,10 +674,10 @@ milestones.close = Затваряне
|
|||
issues.label_templates.use = Използване на набор от етикети
|
||||
issues.add_milestone_at = `добави това към етапа <b>%s</b> %s`
|
||||
issues.add_label = добави етикета %s %s
|
||||
issues.add_labels = добави етикетите %s %s
|
||||
issues.add_labels = добави етикети %s %s
|
||||
issues.remove_label = премахна етикета %s %s
|
||||
issues.remove_labels = премахна етикетите %s %s
|
||||
issues.add_remove_labels = добави етикетите %s и премахна %s %s
|
||||
issues.add_remove_labels = добави етикети %s и премахна %s %s
|
||||
issues.add_project_at = `добави това към проекта <b>%s</b> %s`
|
||||
issues.remove_project_at = `премахна това от проекта <b>%s</b> %s`
|
||||
issues.remove_milestone_at = `премахна това от етапа <b>%s</b> %s`
|
||||
|
@ -702,7 +709,7 @@ more_operations = Още операции
|
|||
download_archive = Изтегляне на хранилището
|
||||
branch = Клон
|
||||
tree = Дърво
|
||||
branches = Клони
|
||||
branches = Клонове
|
||||
tags = Маркери
|
||||
tag = Маркер
|
||||
filter_branch_and_tag = Филтр. на клон или маркер
|
||||
|
@ -759,7 +766,7 @@ pulls.merged_by = от <a href="%[2]s">%[3]s</a> бе слята %[1]s
|
|||
pulls.merged_by_fake = от %[2]s бе слята %[1]s
|
||||
issues.label_deletion = Изтриване на етикета
|
||||
issues.label_modify = Редактиране на етикета
|
||||
issues.due_date_added = добави крайния срок %s %s
|
||||
issues.due_date_added = добави краен срок %s %s
|
||||
issues.due_date_remove = премахна крайния срок %s %s
|
||||
release.new_release = Ново издание
|
||||
release.tag_helper_existing = Съществуващ маркер.
|
||||
|
@ -822,7 +829,7 @@ editor.fail_to_update_file = Неуспешно обновяване/създа
|
|||
editor.add_subdir = Добавяне на директория…
|
||||
commits.commits = Подавания
|
||||
commits.find = Търсене
|
||||
commits.search_all = Всички клони
|
||||
commits.search_all = Всички клонове
|
||||
commits.search = Потърсете подавания…
|
||||
commit.operations = Операции
|
||||
issues.deleted_milestone = `(изтрит)`
|
||||
|
@ -848,7 +855,7 @@ release.edit_release = Обновяване на изданието
|
|||
diff.committed_by = подадено от
|
||||
release.downloads = Изтегляния
|
||||
issues.sign_in_require_desc = <a href="%s">Влезте</a> за да се присъедините към това обсъждане.
|
||||
activity.git_stats_push_to_all_branches = към всички клони.
|
||||
activity.git_stats_push_to_all_branches = към всички клонове.
|
||||
release.deletion_tag_success = Маркерът е изтрит.
|
||||
release.cancel = Отказ
|
||||
release.deletion = Изтриване на изданието
|
||||
|
@ -928,9 +935,9 @@ settings.web_hook_name_discord = Discord
|
|||
settings.web_hook_name_telegram = Telegram
|
||||
settings.web_hook_name_matrix = Matrix
|
||||
settings.web_hook_name_gogs = Gogs
|
||||
settings.web_hook_name_feishu_or_larksuite = Feishu / Lark Suite
|
||||
settings.web_hook_name_feishu = Feishu
|
||||
settings.web_hook_name_larksuite = Lark Suite
|
||||
settings.web_hook_name_feishu = Feishu / Lark Suite
|
||||
settings.web_hook_name_feishu_only = Feishu
|
||||
settings.web_hook_name_larksuite_only = Lark Suite
|
||||
settings.web_hook_name_wechatwork = WeCom (Wechat Work)
|
||||
settings.web_hook_name_packagist = Packagist
|
||||
diff.file_byte_size = Размер
|
||||
|
@ -959,6 +966,110 @@ search.results = Резултати от търсенето на "%s" в <a href
|
|||
object_format = Формат на обектите
|
||||
release.releases_for = Издания за %s
|
||||
release.tags_for = Маркери за %s
|
||||
pulls.cmd_instruction_hint = `Вижте <a class="show-instruction">инструкциите за командния ред</a>.`
|
||||
pulls.showing_only_single_commit = Показани са само промените в подаване %[1]s
|
||||
issues.lock_no_reason = заключи и ограничи обсъждането до сътрудници %s
|
||||
pulls.expand_files = Разгъване на всички файлове
|
||||
pulls.title_desc_few = иска да слее %[1]d подавания от <code>%[2]s</code> в <code id="branch_target">%[3]s</code>
|
||||
issues.content_history.deleted = изтрито
|
||||
activity.git_stats_exclude_merges = С изключение на сливанията,
|
||||
activity.navbar.pulse = Последна дейност
|
||||
activity.no_git_activity = Не е имало никаква дейност с подавания през този период.
|
||||
pulls.merged_title_desc_few = сля %[1]d подавания от <code>%[2]s</code> в <code>%[3]s</code> %[4]s
|
||||
diff.stats_desc_file = %d промени: %d добавяния и %d изтривания
|
||||
issues.content_history.created = създадено
|
||||
pulls.status_checks_success = Всички проверки бяха успешни
|
||||
activity.git_stats_pushed_n = са изтласкали
|
||||
pulls.select_commit_hold_shift_for_range = Изберете подаване. Задръжте shift + click, за да изберете обхвата
|
||||
activity.git_stats_addition_1 = %d добавяне
|
||||
activity.git_stats_on_default_branch = В %s,
|
||||
activity.git_stats_files_changed_1 = е променен
|
||||
activity.git_stats_files_changed_n = са променени
|
||||
activity.git_stats_additions = и е имало
|
||||
pulls.collapse_files = Свиване на всички файлове
|
||||
pulls.show_all_commits = Показване на всички подавания
|
||||
diff.whitespace_button = Празни знаци
|
||||
issues.content_history.edited = редактирано
|
||||
pulls.title_desc_one = иска да слее %[1]d подаване от <code>%[2]s</code> в <code id="branch_target">%[3]s</code>
|
||||
pulls.showing_specified_commit_range = Показани са само промените между %[1]s..%[2]s
|
||||
pulls.merged_title_desc_one = сля %[1]d подаване от <code>%[2]s</code> в <code>%[3]s</code> %[4]s
|
||||
pulls.no_merge_access = Не сте упълномощени за сливане на тази заявка за сливане.
|
||||
activity.navbar.code_frequency = Честота на кода
|
||||
activity.git_stats_pushed_1 = е изтласкал
|
||||
activity.git_stats_push_to_branch = към %s и
|
||||
contributors.contribution_type.commits = Подавания
|
||||
stars = Звезди
|
||||
n_commit_few = %s подавания
|
||||
n_branch_one = %s клон
|
||||
n_branch_few = %s клона
|
||||
n_tag_one = %s маркер
|
||||
n_tag_few = %s маркера
|
||||
commit_graph = Граф с подавания
|
||||
commits.renamed_from = Преименувано от %s
|
||||
commits.view_path = Преглед на този момент в историята
|
||||
commits.search_branch = Този клон
|
||||
n_commit_one = %s подаване
|
||||
release.ahead.commits = <strong>%d</strong> подавания
|
||||
release.stable = Стабилно
|
||||
commits.gpg_key_id = ID на GPG ключ
|
||||
diff.options_button = Опции за разликите
|
||||
activity.title.unresolved_conv_1 = %d нерешено обсъждане
|
||||
activity.title.unresolved_conv_n = %d нерешени обсъждания
|
||||
issues.comment_pull_merged_at = сля подаване %[1]s в %[2]s %[3]s
|
||||
issues.comment_manually_pull_merged_at = ръчно сля подаване %[1]s в %[2]s %[3]s
|
||||
issues.dependency.add = Добавяне на зависимост…
|
||||
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.dependency.pr_no_dependencies = Няма зададени зависимости.
|
||||
issues.dependency.remove_info = Премахване на тази зависимост
|
||||
issues.dependency.removed_dependency = `премахна зависимост %s`
|
||||
issues.dependency.added_dependency = `добави нова зависимост %s`
|
||||
issues.dependency.issue_closing_blockedby = Затварянето на тази задача е блокирано от следните задачи
|
||||
issues.dependency.issue_close_blocks = Тази задача блокира затварянето на следните задачи
|
||||
issues.dependency.issue_close_blocked = Трябва да затворите всички задачи, блокиращи тази задача, преди да можете да я затворите.
|
||||
issues.dependency.blocks_short = Блокира
|
||||
issues.dependency.remove_header = Премахване на зависимост
|
||||
issues.dependency.issue_remove_text = Това ще премахне зависимостта от тази задача. Продължаване?
|
||||
issues.reference_link = Препратка: %s
|
||||
pulls.closed = Заявката за сливане е затворена
|
||||
pulls.merged_success = Заявката за сливане е успешно слята и затворена
|
||||
branch.confirm_create_branch = Създаване на клон
|
||||
branch.create_branch_operation = Създаване на клон
|
||||
tag.create_tag_operation = Създаване на маркер
|
||||
tag.confirm_create_tag = Създаване на маркер
|
||||
pulls.data_broken = Тази заявка за сливане е повредена поради липсваща информация за разклонението.
|
||||
issues.dependency.pr_closing_blockedby = Затварянето на тази заявка за сливане е блокирано от следните задачи
|
||||
issues.dependency.pr_remove_text = Това ще премахне зависимостта от тази заявка за сливане. Продължаване?
|
||||
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.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>`
|
||||
issues.change_ref_at = `промени препратката от <b><strike>%s</strike></b> на <b>%s</b> %s`
|
||||
diff.review.reject = Поискване на промени
|
||||
diff.bin_not_shown = Двоичният файл не е показан.
|
||||
settings.units.units = Елементи на хранилището
|
||||
settings.delete_notices_fork_1 = - Разклоненията на това хранилище ще станат независими след изтриване.
|
||||
settings.actions_desc = Включване на действията за хранилището
|
||||
settings.packages_desc = Включване на регистъра на пакетите за хранилището
|
||||
settings.units.add_more = Добавяне...
|
||||
settings.use_external_issue_tracker = Използване на външен тракер за задачи
|
||||
settings.releases_desc = Включване на изданията за хранилището
|
||||
settings.projects_desc = Включване на проектите за хранилището
|
||||
settings.pulls_desc = Включване на заявките за сливане за хранилището
|
||||
settings.issues_desc = Включване на задачите за хранилището
|
||||
settings.use_internal_issue_tracker = Използване на вградения тракер за задачи
|
||||
pulls.compare_changes_desc = Изберете клона, в който да слеете, и клона, от който да издърпате.
|
||||
pulls.compare_base = сливане в
|
||||
pulls.compare_compare = издърпване от
|
||||
pulls.title_wip_desc = `<a href="#">Започнете заглавието с <strong>%s</strong></a> за да предотвратите случайно сливане на заявката за сливане.`
|
||||
|
||||
[modal]
|
||||
confirm = Потвърждаване
|
||||
|
@ -1053,6 +1164,8 @@ members.owner = Притежател
|
|||
members.member_role = Роля на участника:
|
||||
members.member = Участник
|
||||
members.private_helper = да е видим
|
||||
teams.no_desc = Този екип няма описание
|
||||
settings.delete_org_desc = Тази организация ще бъде изтрита перманентно. Продължаване?
|
||||
|
||||
[install]
|
||||
admin_password = Парола
|
||||
|
@ -1090,6 +1203,7 @@ sqlite_helper = Път на файла за SQLite3 базата данни.<br>
|
|||
err_empty_admin_email = Администраторският адрес на ел. поща не може да бъде празен.
|
||||
password_algorithm = Алгоритъм за хеш. на паролите
|
||||
default_keep_email_private = Скриване на адресите на ел. поща по подразбиране
|
||||
invalid_password_algorithm = Невалиден алгоритъм за хеш. на паролите
|
||||
|
||||
[filter]
|
||||
string.asc = А - Я
|
||||
|
@ -1135,6 +1249,7 @@ change_avatar = Променете профилната си снимка…
|
|||
email_visibility.limited = Вашият адрес на ел. поща е видим за всички удостоверени потребители
|
||||
disabled_public_activity = Този потребител е изключил публичната видимост на дейността.
|
||||
email_visibility.private = Вашият адрес на ел. поща е видим само за вас и администраторите
|
||||
show_on_map = Показване на това място на картата
|
||||
|
||||
[home]
|
||||
filter = Други филтри
|
||||
|
@ -1263,6 +1378,8 @@ SSHTitle = Име на SSH ключ
|
|||
repo_name_been_taken = Името на хранилището вече е използвано.
|
||||
team_name_been_taken = Името на екипа вече е заето.
|
||||
org_name_been_taken = Името на организацията вече е заето.
|
||||
still_own_packages = Вашият акаунт притежава един или повече пакети, първо ги изтрийте.
|
||||
still_own_repo = Вашият акаунт притежава едно или повече хранилища, първо ги изтрийте или прехвърлете.
|
||||
|
||||
[action]
|
||||
close_issue = `затвори задача <a href="%[1]s">%[3]s#%[2]s</a>`
|
||||
|
@ -1288,6 +1405,7 @@ publish_release = `публикува издание <a href="%[2]s"> "%[4]s" </
|
|||
push_tag = изтласка маркер <a href="%[2]s">%[3]s</a> към <a href="%[1]s">%[4]s</a>
|
||||
approve_pull_request = `одобри <a href="%[1]s">%[3]s#%[2]s</a>`
|
||||
reject_pull_request = `предложи промени за <a href="%[1]s">%[3]s#%[2]s</a>`
|
||||
compare_branch = Сравняване
|
||||
|
||||
[auth]
|
||||
tab_openid = OpenID
|
||||
|
@ -1408,6 +1526,24 @@ component_loading_failed = Неуспешно зареждане на %s
|
|||
contributors.what = приноси
|
||||
recent_commits.what = скорошни подавания
|
||||
component_loading = Зареждане на %s...
|
||||
component_loading_info = Това може да отнеме известно време…
|
||||
|
||||
[projects]
|
||||
type-1.display_name = Индивидуален проект
|
||||
|
||||
|
||||
[search]
|
||||
no_results = Няма намерени съответстващи резултати.
|
||||
team_kind = Търсене на екипи...
|
||||
repo_kind = Търсене на хранилища...
|
||||
org_kind = Търсене на организации...
|
||||
user_kind = Търсене на потребители...
|
||||
code_kind = Търсене на код...
|
||||
commit_kind = Търсене на подавания...
|
||||
project_kind = Търсене на проекти...
|
||||
package_kind = Търсене на пакети...
|
||||
search = Търсене...
|
||||
|
||||
[markup]
|
||||
filepreview.lines = Редове от %[1]d до %[2]d в %[3]s
|
||||
filepreview.line = Ред %[1]d в %[2]s
|
|
@ -73,11 +73,11 @@ all=Vše
|
|||
sources=Zdrojové kódy
|
||||
mirrors=Zrcadla
|
||||
collaborative=Spolupráce
|
||||
forks=Rozštěpení
|
||||
forks=Forky
|
||||
|
||||
activities=Aktivity
|
||||
pull_requests=Požadavky na sloučení
|
||||
issues=Úkoly
|
||||
issues=Problémy
|
||||
milestones=Milníky
|
||||
|
||||
ok=OK
|
||||
|
@ -147,7 +147,7 @@ confirm_delete_artifact = Opravdu chcete odstranit artefakt „%s“?
|
|||
toggle_menu = Přepnout nabídku
|
||||
filter = Filtr
|
||||
filter.is_fork = Forknuto
|
||||
filter.not_fork = Není forkuto
|
||||
filter.not_fork = Není forknuto
|
||||
filter.is_mirror = Zrcadleno
|
||||
filter.is_template = Šablona
|
||||
filter.not_template = Není šablona
|
||||
|
@ -156,7 +156,9 @@ filter.private = Soukromé
|
|||
filter.is_archived = Archivováno
|
||||
filter.not_mirror = Není zrcadleno
|
||||
filter.not_archived = Není archivováno
|
||||
filter.clear = Vymazat filtr
|
||||
filter.clear = Vymazat filtry
|
||||
more_items = Další položky
|
||||
invalid_data = Neplatná data: %v
|
||||
|
||||
[aria]
|
||||
navbar=Navigační lišta
|
||||
|
@ -217,7 +219,7 @@ license_desc=Vše je na <a target="_blank" rel="noopener noreferrer" href="https
|
|||
install=Instalace
|
||||
title=Počáteční konfigurace
|
||||
docker_helper=Pokud spouštíte Forgejo v Dockeru, přečtěte si <a target="_blank" rel="noopener noreferrer" href="%s">dokumentaci</a>, než budete měnit jakákoliv nastavení.
|
||||
require_db_desc=Forgejo requires MySQL, PostgreSQL, MSSQL, SQLite3 or TiDB (MySQL protocol).
|
||||
require_db_desc=Forgejo vyžaduje MySQL, PostgreSQL, MSSQL, SQLite3 nebo TiDB (protokol MySQL).
|
||||
db_title=Nastavení databáze
|
||||
db_type=Typ databáze
|
||||
host=Hostitel
|
||||
|
@ -375,7 +377,7 @@ org_no_results=Nebyly nalezeny žádné odpovídající organizace.
|
|||
code_no_results=Nebyl nalezen žádný zdrojový kód odpovídající hledanému výrazu.
|
||||
code_search_results=Výsledky hledání pro „%s“
|
||||
code_last_indexed_at=Naposledy indexováno %s
|
||||
relevant_repositories_tooltip=Repozitáře, které jsou rozštěpení nebo nemají žádné téma, ikonu a žádný popis jsou skryty.
|
||||
relevant_repositories_tooltip=Repozitáře, které jsou forky nebo nemají žádné téma, žádnou ikonu a žádný popis, jsou skryty.
|
||||
relevant_repositories=Zobrazují se pouze relevantní repositáře, <a href="%s">zobrazit nefiltrované výsledky</a>.
|
||||
forks_one = %d fork
|
||||
forks_few = %d forků
|
||||
|
@ -486,8 +488,8 @@ reset_password.text=Pokud jste to byli vy, klikněte na následující odkaz pro
|
|||
|
||||
register_success=Registrace byla úspěšná
|
||||
|
||||
issue_assigned.pull=@%[1]s vás přiřadil/a k požadavku na natažení %[2]s repozitáři %[3]s.
|
||||
issue_assigned.issue=@%[1]s vás přiřadil/a k úkolu %[2]s repozitáři %[3]s.
|
||||
issue_assigned.pull=@%[1]s vás přiřadil/a k žádosti o sloučení %[2]s v repozitáři %[3]s.
|
||||
issue_assigned.issue=@%[1]s vás přiřadil/a k problému %[2]s v repozitáři %[3]s.
|
||||
|
||||
issue.x_mentioned_you=<b>@%s</b> vás zmínil/a:
|
||||
issue.action.force_push=<b>%[1]s</b> vynutil/a nahrání <b>%[2]s</b> z %[3]s do %[4]s.
|
||||
|
@ -496,11 +498,11 @@ issue.action.push_n=<b>@%[1]s</b> nahrál/a %[3]d commity do %[2]s
|
|||
issue.action.close=<b>@%[1]s</b> uzavřel/a #%[2]d.
|
||||
issue.action.reopen=<b>@%[1]s</b> znovu otevřel/a #%[2]d.
|
||||
issue.action.merge=<b>@%[1]s</b> sloučil/a #%[2]d do %[3]s.
|
||||
issue.action.approve=<b>@%[1]s</b> schválil/a tento požadavek na natažení.
|
||||
issue.action.reject=<b>@%[1]s</b> požadoval/a změny v tomto požadavku na natažení.
|
||||
issue.action.review=<b>@%[1]s</b> okomentoval/a tento požadavek na natažení.
|
||||
issue.action.review_dismissed=<b>@%[1]s</b> odmítl/a poslední kontrolu z %[2]s pro tento požadavek na natažení.
|
||||
issue.action.ready_for_review=<b>@%[1]s</b> označil/a tento požadavek na natažení jako připravený ke kontrole.
|
||||
issue.action.approve=<b>@%[1]s</b> schválil/a tuto žádost o sloučení.
|
||||
issue.action.reject=<b>@%[1]s</b> požaduje změny v této žádosti o sloučení.
|
||||
issue.action.review=<b>@%[1]s</b> okomentoval/a tuto žádost o sloučení.
|
||||
issue.action.review_dismissed=<b>@%[1]s</b> odmítl/a poslední kontrolu od %[2]s této žádosti o sloučení.
|
||||
issue.action.ready_for_review=<b>@%[1]s</b> označil/a tuto žádost o sloučení jako připravenou ke kontrole.
|
||||
issue.action.new=<b>@%[1]s</b> vytvořil/a #%[2]d.
|
||||
issue.in_tree_path=V %s:
|
||||
|
||||
|
@ -722,7 +724,7 @@ comment_type_group_lock=Stav zámku
|
|||
comment_type_group_review_request=Žádost o posouzení
|
||||
comment_type_group_pull_request_push=Přidané commity
|
||||
comment_type_group_project=Projekt
|
||||
comment_type_group_issue_ref=Referenční číslo úkolu
|
||||
comment_type_group_issue_ref=Referenční číslo problému
|
||||
saved_successfully=Vaše nastavení bylo úspěšně uloženo.
|
||||
privacy=Soukromí
|
||||
keep_activity_private=Skrýt aktivitu z profilové stránky
|
||||
|
@ -811,7 +813,7 @@ gpg_token=Token
|
|||
gpg_token_help=Podpis můžete vygenerovat pomocí:
|
||||
gpg_token_code=echo "%s" | gpg -a --default-key %s --detach-sig
|
||||
gpg_token_signature=Zakódovaný podpis GPG
|
||||
key_signature_gpg_placeholder=Začíná s „-----BEGIN PGP SIGNATURE-----“
|
||||
key_signature_gpg_placeholder=Začíná textem „-----BEGIN PGP SIGNATURE-----“
|
||||
verify_gpg_key_success=GPG klíč „%s“ byl ověřen.
|
||||
ssh_key_verified=Ověřený klíč
|
||||
ssh_key_verified_long=Klíč byl ověřen pomocí tokenu a může být použit k ověření commitů shodujících se s libovolnou vaší aktivovanou e-mailovou adresou pro tohoto uživatele.
|
||||
|
@ -880,7 +882,7 @@ permissions_access_all=Vše (veřejné, soukromé a omezené)
|
|||
select_permissions=Vyberte oprávnění
|
||||
permission_no_access=Bez přístupu
|
||||
permission_read=Přečtené
|
||||
permission_write=čtení i zápis
|
||||
permission_write=Čtení a zápis
|
||||
at_least_one_permission=Musíte vybrat alespoň jedno oprávnění pro vytvoření tokenu
|
||||
permissions_list=Oprávnění:
|
||||
|
||||
|
@ -931,7 +933,7 @@ scan_this_image=Naskenujte tento obrázek s vaší ověřovací aplikací:
|
|||
or_enter_secret=Nebo zadejte tajný kód: %s
|
||||
then_enter_passcode=A zadejte přístupový kód zobrazený ve vaší aplikaci:
|
||||
passcode_invalid=Přístupový kód není platný. Zkuste to znovu.
|
||||
twofa_enrolled=Ve vašem účtu bylo povoleno dvoufaktorové ověřování. Uložte si pomocný token (%s) na bezpečném místě, protože bude zobrazen pouze jednou!
|
||||
twofa_enrolled=Ve vašem účtu bylo povoleno dvoufaktorové ověřování. Uložte si jednorázový obnovovací klíč (%s) na bezpečné místo, jelikož již nebude znovu zobrazen.
|
||||
twofa_failed_get_secret=Nepodařilo se získat tajemství.
|
||||
|
||||
webauthn_desc=Bezpečnostní klíče jsou hardwarová zařízení obsahující kryptografické klíče. Mohou být použity pro dvoufaktorové ověřování. Bezpečnostní klíče musí podporovat <a rel="noreferrer" target="_blank" href="https://w3c.github.io/webauthn/#webauthn-authenticator">WebAuthn Authenticator</a> standard.
|
||||
|
@ -957,7 +959,7 @@ repos_none=Nevlastníte žádné repozitáře.
|
|||
|
||||
delete_account=Odstranit svůj účet
|
||||
delete_prompt=Tato operace natrvalo odstraní váš uživatelský účet. <strong>NELZE</strong> ji vrátit zpět.
|
||||
delete_with_all_comments=Váš účet je mladší než %s. Aby se zabránilo fantomovým komentářům, všechny komentáře k úkolům/požadavkům na natažení budou smazány.
|
||||
delete_with_all_comments=Váš účet je mladší než %s. Pro zabránění fantomovým komentářům budou společně s ním odstraněny všechny komentáře u problémů a ŽS.
|
||||
confirm_delete_account=Potvrdit odstranění
|
||||
delete_account_title=Odstranit uživatelský účet
|
||||
delete_account_desc=Jste si jisti, že chcete trvale smazat tento účet?
|
||||
|
@ -1026,7 +1028,7 @@ repo_lang=Jazyk
|
|||
repo_gitignore_helper=Vyberte šablony .gitignore.
|
||||
repo_gitignore_helper_desc=Vyberte soubory, které nechcete sledovat ze seznamu šablon pro běžné jazyky. Typické artefakty generované nástroji pro sestavení každého jazyka jsou ve výchozím stavu součástí .gitignore.
|
||||
issue_labels=Štítky problémů
|
||||
issue_labels_helper=Vyberte sadu štítků úkolů.
|
||||
issue_labels_helper=Vyberte sadu štítků problémů.
|
||||
license=Licence
|
||||
license_helper=Vyberte licenční soubor.
|
||||
license_helper_desc=Licence řídí, co ostatní mohou a nemohou dělat s vaším kódem. Nejste si jisti, která je pro váš projekt správná? Podívejte se na <a target="_blank" rel="noopener noreferrer" href="%s">Zvolte licenci</a>
|
||||
|
@ -1044,7 +1046,7 @@ trust_model_helper_default=Výchozí: Použít výchozí model důvěry pro tuto
|
|||
create_repo=Vytvořit repozitář
|
||||
default_branch=Výchozí větev
|
||||
default_branch_label=výchozí
|
||||
default_branch_helper=Výchozí větev je základní větev pro požadavky na natažení a commity kódu.
|
||||
default_branch_helper=Výchozí větev je základní větev pro žádosti o sloučení a commity kódu.
|
||||
mirror_prune=Vyčistit
|
||||
mirror_prune_desc=Odstranit zastaralé reference na vzdálené sledování
|
||||
mirror_interval=Interval zrcadlení (platné časové jednotky jsou „h“, „m“ a „s“). Nastavením na 0 zakážete periodickou synchronizaci. (Minimální interval: %s)
|
||||
|
@ -1065,7 +1067,7 @@ mirror_password_help=Změňte uživatelské jméno pro vymazání uloženého he
|
|||
watchers=Sledující
|
||||
stargazers=Sledující
|
||||
stars_remove_warning=Tímto odstraníte všechny hvězdičky z tohoto repozitáře.
|
||||
forks=Rozštěpení
|
||||
forks=Forky
|
||||
reactions_more=a %d dalších
|
||||
unit_disabled=Správce webu zakázal tuto sekci repozitáře.
|
||||
language_other=Jiný
|
||||
|
@ -1112,9 +1114,9 @@ template.one_item=Musíte vybrat alespoň jednu položku šablony
|
|||
template.invalid=Musíte vybrat repositář šablony
|
||||
|
||||
archive.title=Tento repozitář je archivovaný. Můžete prohlížet soubory, klonovat, ale nemůžete nahrávat a vytvářet nové problémy nebo žádosti o sloučení.
|
||||
archive.title_date=Tento repositář byl archivován %s. Můžete zobrazit soubory a klonovat je, ale nemůžete nahrávat ani otevírat problémy nebo požadavky na natažení.
|
||||
archive.issue.nocomment=Tento repozitář je archivovaný. Nemůžete komentovat úkoly.
|
||||
archive.pull.nocomment=Tento repozitář je archivovaný. Nemůžete komentovat požadavky na natažení.
|
||||
archive.title_date=Tento repozitář byl archivován %s. Můžete si prohlížet a klonovat soubory, ale nemůžete nahrávat ani otevírat problémy nebo žádosti o sloučení.
|
||||
archive.issue.nocomment=Tento repozitář je archivovaný. Nemůžete komentovat problémy.
|
||||
archive.pull.nocomment=Tento repozitář je archivovaný. Nemůžete komentovat žádosti o sloučení.
|
||||
|
||||
form.reach_limit_of_creation_1=Již jste dosáhli svůj limit %d repozitář.
|
||||
form.reach_limit_of_creation_n=Již jste dosáhli svůj limit %d repozitářů.
|
||||
|
@ -1134,7 +1136,7 @@ migrate_items=Položky pro migrování
|
|||
migrate_items_wiki=Wiki
|
||||
migrate_items_milestones=Milníky
|
||||
migrate_items_labels=Štítky
|
||||
migrate_items_issues=Úkoly
|
||||
migrate_items_issues=Problémy
|
||||
migrate_items_pullrequests=Žádosti o sloučení
|
||||
migrate_items_merge_requests=Sloučit žádosti
|
||||
migrate_items_releases=Vydání
|
||||
|
@ -1178,12 +1180,12 @@ mirror_from=zrcadlo
|
|||
forked_from=rozštěpen z
|
||||
generated_from=generováno z
|
||||
fork_from_self=Nemůžete rozštěpit váš vlastní repozitář.
|
||||
fork_guest_user=Přihlaste se pro rozštěpení tohoto repozitáře.
|
||||
fork_guest_user=Přihlaste se pro vytvoření forku tohoto repozitáře.
|
||||
watch_guest_user=Pro sledování tohoto repozitáře se přihlaste.
|
||||
star_guest_user=Pro hodnocení tohoto repozitáře se přihlaste.
|
||||
unwatch=Přestat sledovat
|
||||
watch=Sledovat
|
||||
unstar=Odoblíbit
|
||||
unstar=Zrušit oblíbení
|
||||
star=Oblíbit
|
||||
fork=Rozštěpit
|
||||
download_archive=Stáhnout repozitář
|
||||
|
@ -1207,7 +1209,7 @@ filter_branch_and_tag=Filtr pro větev nebo značku
|
|||
find_tag=Najít značku
|
||||
branches=Větve
|
||||
tags=Značky
|
||||
issues=Úkoly
|
||||
issues=Problémy
|
||||
pulls=Žádosti o sloučení
|
||||
project_board=Projekty
|
||||
packages=Balíčky
|
||||
|
@ -1284,8 +1286,8 @@ editor.name_your_file=Pojmenujte váš soubor…
|
|||
editor.filename_help=Přidejte adresář zapsáním jeho jména následovaného lomítkem („/“). Adresář odeberete stiskem backspace na začátku vstupního pole.
|
||||
editor.or=nebo
|
||||
editor.cancel_lower=Zrušit
|
||||
editor.commit_signed_changes=Odevzdat podepsané změny
|
||||
editor.commit_changes=Odevzdat změny
|
||||
editor.commit_signed_changes=Commitnout podepsané změny
|
||||
editor.commit_changes=Commitnout změny
|
||||
editor.add_tmpl=Přidat „<nazevsouboru>“
|
||||
editor.add=Přidat %s
|
||||
editor.update=Aktualizovat %s
|
||||
|
@ -1297,7 +1299,7 @@ editor.new_patch=Nová záplata
|
|||
editor.commit_message_desc=Přidat volitelný rozšířený popis…
|
||||
editor.signoff_desc=Přidat Signed-off-by podpis přispěvatele na konec zprávy o commitu.
|
||||
editor.commit_directly_to_this_branch=Odevzdat přímo do větve <strong class="branch-name">%s</strong>.
|
||||
editor.create_new_branch=Vytvořit <strong>novou větev</strong> pro tento commit a spustit požadavek na natažení.
|
||||
editor.create_new_branch=Vytvořit <strong>novou větev</strong> pro tento commit a vytvořit žádost o sloučení.
|
||||
editor.create_new_branch_np=Vytvořte <strong>novou větev</strong> z tohoto commitu.
|
||||
editor.propose_file_change=Navrhnout změnu souboru
|
||||
editor.new_branch_name=Pojmenujte novou větev pro tento commit
|
||||
|
@ -1312,7 +1314,7 @@ editor.file_is_a_symlink=`„%s“ je symbolický odkaz. Symbolické odkazy nemo
|
|||
editor.filename_is_a_directory=Jméno souboru „%s“ je již použito jako jméno adresáře v tomto repozitáři.
|
||||
editor.file_editing_no_longer_exists=Upravovaný soubor „%s“ již není součástí tohoto repozitáře.
|
||||
editor.file_deleting_no_longer_exists=Odstraňovaný soubor „%s“ již není součástí tohoto repozitáře.
|
||||
editor.file_changed_while_editing=Obsah souboru byl změněn od doby, kdy jste začaly s úpravou. <a target="_blank" rel="noopener noreferrer" href="%s">Klikněte zde</a>, abyste je zobrazili, nebo <strong>potvrďte změny ještě jednou</strong> pro jejich přepsání.
|
||||
editor.file_changed_while_editing=Obsah souboru se od zahájení úprav změnil. <a target="_blank" rel="noopener noreferrer" href="%s">Klikněte sem</a> pro jejich zobrazení nebo <strong>proveďte commit změn ještě jednou</strong> pro jejich přepsání.
|
||||
editor.file_already_exists=Soubor „%s“ již existuje v tomto repozitáři.
|
||||
editor.commit_empty_file_header=Odevzdat prázdný soubor
|
||||
editor.commit_empty_file_text=Soubor, který se chystáte odevzdat, je prázdný. Pokračovat?
|
||||
|
@ -1367,10 +1369,10 @@ commitstatus.pending=Čekající
|
|||
commitstatus.success=Úspěch
|
||||
|
||||
ext_issues=Přístup k externím problémům
|
||||
ext_issues.desc=Odkaz na externí systém úkolů.
|
||||
ext_issues.desc=Odkaz na externí systém problémů.
|
||||
|
||||
projects=Projekty
|
||||
projects.desc=Spravovat úkoly a požadavky na natažení na projektových nástěnkách.
|
||||
projects.desc=Spravovat problémy a žádosti o sloučení na projektových nástěnkách.
|
||||
projects.description=Popis (volitelné)
|
||||
projects.description_placeholder=Popis
|
||||
projects.create=Vytvořit projekt
|
||||
|
@ -1379,14 +1381,14 @@ projects.new=Nový projekt
|
|||
projects.new_subheader=Koordinujte, sledujte a aktualizujte svou práci na jednom místě, aby projekty zůstaly transparentní a v plánu.
|
||||
projects.create_success=Projekt „%s“ byl vytvořen.
|
||||
projects.deletion=Odstranit projekt
|
||||
projects.deletion_desc=Odstranění projektu jej odstraní ze všech souvisejících úkolů. Pokračovat?
|
||||
projects.deletion_desc=Odstraněním projektu jej odstraníte ze všech souvisejících problémů. Pokračovat?
|
||||
projects.deletion_success=Projekt byl odstraněn.
|
||||
projects.edit=Upravit projekt
|
||||
projects.edit_subheader=Projekty organizují úkoly a sledují pokrok.
|
||||
projects.edit_subheader=Projekty organizují problémy a sledují pokrok.
|
||||
projects.modify=Upravit projekt
|
||||
projects.edit_success=Projekt „%s“ byl aktualizován.
|
||||
projects.type.none=Žádný
|
||||
projects.type.basic_kanban=Základní Kanban
|
||||
projects.type.basic_kanban=Základní kanban
|
||||
projects.type.bug_triage=Třídění chyb
|
||||
projects.template.desc=Šablona
|
||||
projects.template.desc_helper=Začněte vybráním šablony projektu
|
||||
|
@ -1401,7 +1403,7 @@ projects.column.set_default_desc=Nastavit tento sloupec jako výchozí pro nekat
|
|||
projects.column.unset_default=Zrušit nastavení jako výchozí
|
||||
projects.column.unset_default_desc=Zrušit nastavení tohoto sloupce jako výchozí
|
||||
projects.column.delete=Odstranit sloupec
|
||||
projects.column.deletion_desc=Odstranění projektového sloupce přesune všechny související problémy do kategorie „Nezařazené“. Pokračovat?
|
||||
projects.column.deletion_desc=Odstranění projektového sloupce přesune všechny související problémy do výchozího sloupce. Pokračovat?
|
||||
projects.column.color=Barva
|
||||
projects.open=Otevřít
|
||||
projects.close=Zavřít
|
||||
|
@ -1419,7 +1421,7 @@ issues.filter_reviewers=Filtrovat posuzovatele
|
|||
issues.new=Nový problém
|
||||
issues.new.title_empty=Název nesmí být prázdný
|
||||
issues.new.labels=Štítky
|
||||
issues.new.no_label=Bez štítku
|
||||
issues.new.no_label=Bez štítků
|
||||
issues.new.clear_labels=Zrušit štítky
|
||||
issues.new.projects=Projekty
|
||||
issues.new.clear_projects=Vymazat projekty
|
||||
|
@ -1439,7 +1441,7 @@ issues.new.no_reviewers=Žádní posuzovatelé
|
|||
issues.choose.get_started=Začínáme
|
||||
issues.choose.open_external_link=Otevřít
|
||||
issues.choose.blank=Výchozí
|
||||
issues.choose.blank_about=Vytvořit úkol z výchozí šablony.
|
||||
issues.choose.blank_about=Vytvořit problém z výchozí šablony.
|
||||
issues.choose.ignore_invalid_templates=Neplatné šablony byly ignorovány
|
||||
issues.choose.invalid_templates=%v nalezených neplatných šablon
|
||||
issues.choose.invalid_config=Nastavení problému obsahuje chyby:
|
||||
|
@ -1494,7 +1496,7 @@ issues.filter_assginee_no_assignee=Bez zpracovatele
|
|||
issues.filter_poster=Autor
|
||||
issues.filter_poster_no_select=Všichni autoři
|
||||
issues.filter_type=Typ
|
||||
issues.filter_type.all_issues=Všechny úkoly
|
||||
issues.filter_type.all_issues=Všechny problémy
|
||||
issues.filter_type.assigned_to_you=Přiřazené vám
|
||||
issues.filter_type.created_by_you=Vytvořené vámi
|
||||
issues.filter_type.mentioning_you=Zmiňující vás
|
||||
|
@ -1511,8 +1513,8 @@ issues.filter_sort.nearduedate=Nejbližší datum dokončení
|
|||
issues.filter_sort.farduedate=Nejvzdálenější datum dokončení
|
||||
issues.filter_sort.moststars=Nejvíce hvězdiček
|
||||
issues.filter_sort.feweststars=Nejméně hvězdiček
|
||||
issues.filter_sort.mostforks=Nejvíce rozštěpení
|
||||
issues.filter_sort.fewestforks=Nejméně rozštěpení
|
||||
issues.filter_sort.mostforks=Nejvíce forků
|
||||
issues.filter_sort.fewestforks=Nejméně forků
|
||||
issues.keyword_search_unavailable=Hledání podle klíčového slova není momentálně dostupné. Obraťte se na správce webu.
|
||||
issues.action_open=Otevřít
|
||||
issues.action_close=Zavřít
|
||||
|
@ -1531,8 +1533,8 @@ 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řený
|
||||
issues.closed_title=zavřený
|
||||
issues.open_title=Otevřeno
|
||||
issues.closed_title=Uzavřeno
|
||||
issues.draft_title=Koncept
|
||||
issues.num_comments_1=%d komentář
|
||||
issues.num_comments=%d komentářů
|
||||
|
@ -1551,15 +1553,15 @@ issues.close_comment_issue=Okomentovat a zavřít
|
|||
issues.reopen_issue=Znovuotevřít
|
||||
issues.reopen_comment_issue=Okomentovat a znovu otevřít
|
||||
issues.create_comment=Okomentovat
|
||||
issues.closed_at=`uzavřel/a tento úkol <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
issues.reopened_at=`znovuotevřel/a tento úkol <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
issues.commit_ref_at=`odkázal na tento úkol z commitu <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
issues.ref_issue_from=`<a href="%[3]s">odkazoval/a na tento úkol %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
issues.ref_pull_from=`<a href="%[3]s">odkazoval/a na tento požadavek na nataž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 požadavek na natažení %[4]s, který uzavře tento úkol</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
issues.ref_reopening_from=`<a href="%[3]s">odkazoval/a na požadavek na natažení %[4]s, který znovu otevře tento úkol</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
issues.ref_closed_from=`<a href="%[3]s">uzavřel/a tento úkol %[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 úkol %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
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 commitu <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 žádost o sloučení %[4]s, která uzavře tento problém</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
issues.ref_reopening_from=`<a href="%[3]s">odkazoval/a na žádost o sloučení %[4]s, která znovu otevře tento problém</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
issues.ref_closed_from=`<a href="%[3]s">uzavřel/a tento problém %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
issues.ref_reopened_from=`<a href="%[3]s">znovu otevřel/a tento problém %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
issues.ref_from=`z %[1]s`
|
||||
issues.author=Autor
|
||||
issues.author_helper=Tento uživatel je autor.
|
||||
|
@ -1573,7 +1575,7 @@ issues.role.first_time_contributor_helper=Toto je první příspěvek tohoto už
|
|||
issues.role.contributor=Přispěvatel
|
||||
issues.role.contributor_helper=Tento uživatel již dříve přispíval do repozitáře.
|
||||
issues.re_request_review=Znovu požádat o posouzení
|
||||
issues.is_stale=Od tohoto posouzení došlo ke změnám v tomto požadavku na natažení
|
||||
issues.is_stale=Od tohoto posouzení došlo v této žádosti ke změnám
|
||||
issues.remove_request_review=Odstranit žádost o posouzení
|
||||
issues.remove_request_review_block=Nelze odstranit žádost o posouzení
|
||||
issues.dismiss_review=Zamítnout posouzení
|
||||
|
@ -1592,18 +1594,18 @@ issues.label_archive_tooltip=Archivované štítky jsou ve výchozím nastavení
|
|||
issues.label_exclusive_desc=Pojmenujte štítek <code>rozsah/položka</code>, aby se stal vzájemně exkluzivním s jinými štítky <code>rozsah/</code>.
|
||||
issues.label_exclusive_warning=Jakékoliv protichůdné rozsahy štítků budou odstraněny při úpravě štítků u úkolů nebo u požadavku na natažení.
|
||||
issues.label_count=%d štítků
|
||||
issues.label_open_issues=%d otevřených úkolů
|
||||
issues.label_open_issues=%d otevřených problémů / žádostí o sloučení
|
||||
issues.label_edit=Upravit
|
||||
issues.label_delete=Smazat
|
||||
issues.label_modify=Upravit štítek
|
||||
issues.label_deletion=Odstranit štítek
|
||||
issues.label_deletion_desc=Odstranění štítku jej smaže ze všech úkolů. Pokračovat?
|
||||
issues.label_deletion_desc=Odstraněním štítku jej odeberete ze všech problémů. Pokračovat?
|
||||
issues.label_deletion_success=Štítek byl odstraněn.
|
||||
issues.label.filter_sort.alphabetically=Od začátku abecedy
|
||||
issues.label.filter_sort.reverse_alphabetically=Od konce abecedy
|
||||
issues.label.filter_sort.by_size=Nejmenší velikost
|
||||
issues.label.filter_sort.reverse_by_size=Největší velikost
|
||||
issues.num_participants=%d účastníků
|
||||
issues.num_participants_few=%d účastníků
|
||||
issues.attachment.open_tab=`Klikněte pro zobrazení „%s“ v nové záložce`
|
||||
issues.attachment.download=`Klikněte pro stažení „%s“`
|
||||
issues.subscribe=Odebírat
|
||||
|
@ -1614,9 +1616,9 @@ issues.pin_comment=připnul/a tento %s
|
|||
issues.unpin_comment=odepnul/a tento %s
|
||||
issues.lock=Uzamknout konverzaci
|
||||
issues.unlock=Odemknout konverzaci
|
||||
issues.lock.unknown_reason=Úkol nelze z neznámého důvodu uzamknout.
|
||||
issues.lock_duplicate=Úkol nemůže být uzamčený dvakrát.
|
||||
issues.unlock_error=Nelze odemknout úkol, který je uzamčený.
|
||||
issues.lock.unknown_reason=Problém nelze z neznámého důvodu uzamknout.
|
||||
issues.lock_duplicate=Problém nemůže být uzamčený dvakrát.
|
||||
issues.unlock_error=Nelze odemknout problém, který není uzamčený.
|
||||
issues.lock_with_reason=uzamkl/a jako <strong>%s</strong> a omezil/a konverzaci na spolupracovníky %s
|
||||
issues.lock_no_reason=uzamkl/a a omezil/a konverzaci na spolupracovníky %s
|
||||
issues.unlock_comment=odemkl/a tuto konverzaci %s
|
||||
|
@ -1624,22 +1626,22 @@ issues.lock_confirm=Uzamknout
|
|||
issues.unlock_confirm=Odemknout
|
||||
issues.lock.notice_1=- Další uživatelé nemohou komentovat tento problém.
|
||||
issues.lock.notice_2=- Vy a ostatní spolupracovníci s přístupem k tomuto repozitáři můžete stále přidávat komentáře, které ostatní uvidí.
|
||||
issues.lock.notice_3=- V budoucnu budete moci vždy znovu tento úkol odemknout.
|
||||
issues.unlock.notice_1=- Všichni budou moci znovu komentovat tento úkol.
|
||||
issues.unlock.notice_2=- V budoucnu budete moci vždy znovu tento úkol uzamknout.
|
||||
issues.lock.notice_3=- Vždy budete moci tento problém znovu odemknout.
|
||||
issues.unlock.notice_1=- Všichni budou moci znovu komentovat tento problém.
|
||||
issues.unlock.notice_2=- Vždy budete moci tento problém znovu uzamknout.
|
||||
issues.lock.reason=Důvod pro uzamčení
|
||||
issues.lock.title=Uzamknout konverzaci u tohoto úkolu.
|
||||
issues.unlock.title=Odemknout konverzaci u tohoto úkolu.
|
||||
issues.comment_on_locked=Nemůžete komentovat uzamčený úkol.
|
||||
issues.lock.title=Uzamknout konverzaci u tohoto problému.
|
||||
issues.unlock.title=Odemknout konverzaci u tohoto problému.
|
||||
issues.comment_on_locked=Nemůžete komentovat uzamčený problém.
|
||||
issues.delete=Smazat
|
||||
issues.delete.title=Smazat tento úkol?
|
||||
issues.delete.text=Opravdu chcete tento úkol smazat? (Tím se trvale odstraní veškerý obsah. Pokud jej hodláte archivovat, zvažte raději jeho uzavření.)
|
||||
issues.delete.title=Smazat tento problém?
|
||||
issues.delete.text=Opravdu chcete smazat tento problém? (Tím se trvale odstraní veškerý obsah. Pokud jej hodláte archivovat, zvažte raději jeho uzavření)
|
||||
issues.tracker=Sledování času
|
||||
issues.start_tracking_short=Spustit časovač
|
||||
issues.start_tracking=Spustit sledování času
|
||||
issues.start_tracking_history=`započal/a práci %s`
|
||||
issues.tracker_auto_close=Časovač se automaticky zastaví po zavření tohoto úkolu
|
||||
issues.tracking_already_started=`Již jste spustili sledování času na <a href="%s">jiném úkolu</a>!`
|
||||
issues.tracker_auto_close=Časovač se automaticky zastaví po zavření tohoto problému
|
||||
issues.tracking_already_started=`Sledování času jste již spustili u <a href="%s">jiného problému</a>!`
|
||||
issues.stop_tracking=Zastavit časovač
|
||||
issues.stop_tracking_history=`ukončil/a práci %s`
|
||||
issues.cancel_tracking=Zahodit
|
||||
|
@ -1686,27 +1688,27 @@ issues.dependency.remove=Odstranit
|
|||
issues.dependency.remove_info=Odstranit tuto závislost
|
||||
issues.dependency.added_dependency=`přidal/a novou závislost %s`
|
||||
issues.dependency.removed_dependency=`odstranil/a závislost %s`
|
||||
issues.dependency.pr_closing_blockedby=Uzavření tohoto požadavku na natažení je blokováno následujícími úkoly
|
||||
issues.dependency.issue_closing_blockedby=Uzavření tohoto úkolu je blokováno následujícími úkoly
|
||||
issues.dependency.issue_close_blocks=Tento úkol blokuje uzavření následujících úkolů
|
||||
issues.dependency.pr_close_blocks=Tento požadavek na natažení blokuje uzavření následujících úkolů
|
||||
issues.dependency.issue_close_blocked=Musíte zavřít všechny úkoly, které blokují tento úkol, aby jej bylo možné zavřít.
|
||||
issues.dependency.pr_closing_blockedby=Uzavření této žádosti o sloučení je blokováno následujícími problémy
|
||||
issues.dependency.issue_closing_blockedby=Uzavření tohoto problému je blokováno následujícími problémy
|
||||
issues.dependency.issue_close_blocks=Tento problém blokuje uzavření následujících problémů
|
||||
issues.dependency.pr_close_blocks=Tato žádost o sloučení blokuje uzavření následujících problémů
|
||||
issues.dependency.issue_close_blocked=Aby bylo možné uzavřít tento problém, musíte uzavřít všechny ostatní problémy, které jej blokují.
|
||||
issues.dependency.issue_batch_close_blocked=Nelze uzavřít úkoly, které jste vybrali, protože úkol #%d má stále otevřené závislosti
|
||||
issues.dependency.pr_close_blocked=Musíte zavřít všechny úkoly, které blokují tento požadavek na natažení, aby jej bylo možné sloučit.
|
||||
issues.dependency.pr_close_blocked=Aby bylo možné sloučit tuto žádost, musíte uzavřít všechny problémy, které ji blokují.
|
||||
issues.dependency.blocks_short=Blokuje
|
||||
issues.dependency.blocked_by_short=Závisí na
|
||||
issues.dependency.remove_header=Odstranit závislost
|
||||
issues.dependency.issue_remove_text=Tímto krokem odeberete závislost z úkolu. Pokračovat?
|
||||
issues.dependency.pr_remove_text=Tímto krokem odeberete závislost z požadavku na natažení. Pokračovat?
|
||||
issues.dependency.issue_remove_text=Tímto krokem odeberete závislost z tohoto problému. Pokračovat?
|
||||
issues.dependency.pr_remove_text=Tímto krokem odeberete závislost z této žádosti o sloučení. Pokračovat?
|
||||
issues.dependency.setting=Povolit závislosti pro problémy a žádosti o sloučení
|
||||
issues.dependency.add_error_same_issue=Úkol nemůže záviset sám na sobě.
|
||||
issues.dependency.add_error_dep_issue_not_exist=Související úkol neexistuje.
|
||||
issues.dependency.add_error_same_issue=Problém nemůže záviset sám na sobě.
|
||||
issues.dependency.add_error_dep_issue_not_exist=Závislý problém neexistuje.
|
||||
issues.dependency.add_error_dep_not_exist=Závislost neexistuje.
|
||||
issues.dependency.add_error_dep_exists=Závislost již existuje.
|
||||
issues.dependency.add_error_cannot_create_circular=Nemůžete vytvořit závislost dvou úkolů, které se vzájemně blokují.
|
||||
issues.dependency.add_error_dep_not_same_repo=Oba úkoly musí být ve stejném repozitáři.
|
||||
issues.review.self.approval=Nemůžete schválit svůj požadavek na natažení.
|
||||
issues.review.self.rejection=Nemůžete požadovat změny ve svém vlastním požadavku na natažení.
|
||||
issues.dependency.add_error_cannot_create_circular=Nelze vytvořit závislost dvou problémů, které se vzájemně blokují.
|
||||
issues.dependency.add_error_dep_not_same_repo=Oba problémy musí být ve stejném repozitáři.
|
||||
issues.review.self.approval=Nemůžete schválit vlastní žádost o sloučení.
|
||||
issues.review.self.rejection=Nemůžete požadovat změny ve své vlastní žádosti o sloučení.
|
||||
issues.review.approve=schválil/a tyto změny %s
|
||||
issues.review.comment=posoudil/a %s
|
||||
issues.review.dismissed=zamítl/a posouzení uživatele %s %s
|
||||
|
@ -1746,14 +1748,14 @@ issues.reference_link=Reference: %s
|
|||
compare.compare_base=základní
|
||||
compare.compare_head=porovnat
|
||||
|
||||
pulls.desc=Povolit požadavky na natažení a posuzování kódu.
|
||||
pulls.desc=Povolit žádosti o sloučení a posuzování kódu.
|
||||
pulls.new=Nová žádost o sloučení
|
||||
pulls.view=Zobrazit žádost o sloučení
|
||||
pulls.compare_changes=Nová žádost o sloučení
|
||||
pulls.allow_edits_from_maintainers=Povolit úpravy od správců
|
||||
pulls.allow_edits_from_maintainers_desc=Uživatelé s přístupem k zápisu do základní větve mohou také nahrávat do této větve
|
||||
pulls.allow_edits_from_maintainers_err=Aktualizace se nezdařila
|
||||
pulls.compare_changes_desc=Vyberte větev pro sloučení a větev pro natažení.
|
||||
pulls.compare_changes_desc=Vyberte větev pro sloučení a větev, ze které provést pull.
|
||||
pulls.has_viewed_file=Zobrazeno
|
||||
pulls.has_changed_since_last_review=Změněno od vašeho posledního posouzení
|
||||
pulls.viewed_files_label=%[1]d / %[2]d souborů zobrazeno
|
||||
|
@ -1772,10 +1774,10 @@ pulls.showing_specified_commit_range=Zobrazují se pouze změny mezi %[1]s..%[2]
|
|||
pulls.select_commit_hold_shift_for_range=Vyberte commit. Podržte klávesu shift + klepněte pro výběr rozsahu
|
||||
pulls.review_only_possible_for_full_diff=Posouzení je možné pouze při zobrazení plného rozlišení
|
||||
pulls.filter_changes_by_commit=Filtrovat podle commitu
|
||||
pulls.nothing_to_compare=Tyto větve jsou stejné. Není potřeba vytvářet požadavek na natažení.
|
||||
pulls.nothing_to_compare=Tyto větve jsou stejné. Není třeba vytvářet žádost o sloučení.
|
||||
pulls.nothing_to_compare_have_tag=Vybraná větev/značka je stejná.
|
||||
pulls.nothing_to_compare_and_allow_empty_pr=Tyto větve jsou stejné. Tento požadavek na natažení bude prázdný.
|
||||
pulls.has_pull_request=`Požadavek na natažení mezi těmito větvemi již existuje: <a href="%[1]s">%[2]s#%[3]d</a>`
|
||||
pulls.nothing_to_compare_and_allow_empty_pr=Tyto větve jsou stejné. Tato žádost o sloučení bude prázdná.
|
||||
pulls.has_pull_request=`Žádost o sloučení mezi těmito větvemi již existuje: <a href="%[1]s">%[2]s#%[3]d</a>`
|
||||
pulls.create=Vytvořit žádost o sloučení
|
||||
pulls.title_desc_few=chce sloučit %[1]d commity z větve <code>%[2]s</code> do <code id="branch_target">%[3]s</code>
|
||||
pulls.merged_title_desc_few=sloučil %[1]d commity z větve <code>%[2]s</code> do větve <code>%[3]s</code> před %[4]s
|
||||
|
@ -1783,35 +1785,35 @@ pulls.change_target_branch_at=`změnil/a cílovou větev z <b>%s</b> na <b>%s</b
|
|||
pulls.tab_conversation=Konverzace
|
||||
pulls.tab_commits=Commity
|
||||
pulls.tab_files=Změněné soubory
|
||||
pulls.reopen_to_merge=Prosíme, otevřete znovu tento požadavek na natažení, aby se provedlo sloučení.
|
||||
pulls.cant_reopen_deleted_branch=Tento požadavek na natažení nemůže být znovu otevřen protože větev byla smazána.
|
||||
pulls.reopen_to_merge=Otevřete znovu tuto žádost pro provedení sloučení.
|
||||
pulls.cant_reopen_deleted_branch=Tuto žádost o sloučení nelze znovu otevřít, protože větev byla smazána.
|
||||
pulls.merged=Sloučený
|
||||
pulls.merged_success=Požadavek na natažení byl úspěšně sloučen a uzavřen
|
||||
pulls.closed=Požadavek na natažení uzavřen
|
||||
pulls.merged_success=Žádost byla úspěšně sloučena a uzavřena
|
||||
pulls.closed=Žádost o sloučení uzavřena
|
||||
pulls.manually_merged=Sloučeno ručně
|
||||
pulls.merged_info_text=Větev %s může být nyní odstraněna.
|
||||
pulls.is_closed=Požadavek na natažení byl uzavřen.
|
||||
pulls.title_wip_desc=`<a href="#">Začněte název s <strong>%s</strong></a> a zamezíte tak nechtěnému sloučení požadavku na natažení.`
|
||||
pulls.cannot_merge_work_in_progress=Tento požadavek na natažení je označen jako probíhající práce.
|
||||
pulls.is_closed=Žádost o sloučení byla uzavřena.
|
||||
pulls.title_wip_desc=`<a href="#">Začněte název textem <strong>%s</strong></a> pro zamezení nechtěnému sloučení žádosti.`
|
||||
pulls.cannot_merge_work_in_progress=Tato žádost o slolučení je označena jako rozpracovaná.
|
||||
pulls.still_in_progress=Stále probíhá?
|
||||
pulls.add_prefix=Přidat prefix <strong>%s</strong>
|
||||
pulls.remove_prefix=Odstranit prefix <strong>%s</strong>
|
||||
pulls.data_broken=Tento požadavek na natažení je rozbitý kvůli chybějícím informacím o rozštěpení.
|
||||
pulls.files_conflicted=Tento požadavek na natažení obsahuje změny, které kolidují s cílovou větví.
|
||||
pulls.data_broken=Tato žádost o sloučení je rozbitá kvůli chybějícím informacím o forku.
|
||||
pulls.files_conflicted=Tato žádost o sloučení obsahuje změny, které jsou v rozporu s cílovou větví.
|
||||
pulls.is_checking=Právě probíhá kontrola konfliktů při sloučení. Zkuste to za chvíli.
|
||||
pulls.is_ancestor=Tato větev je již součástí cílové větve. Není co sloučit.
|
||||
pulls.is_empty=Změny na této větvi jsou již na cílové větvi. Toto bude prázdný commit.
|
||||
pulls.required_status_check_failed=Některé požadované kontroly nebyly úspěšné.
|
||||
pulls.required_status_check_missing=Některé požadované kontroly chybí.
|
||||
pulls.required_status_check_administrator=Jako administrátor stále můžete sloučit tento požadavek na natažení.
|
||||
pulls.required_status_check_administrator=Jako administrátor stále můžete sloučit tuto žádost.
|
||||
pulls.blocked_by_approvals=Tato žádost o sloučení ještě nemá dostatek schválení. Uděleno %d z %d schválení.
|
||||
pulls.blocked_by_rejection=Tato žádost o sloučení obsahuje změny požadované oficiálním posuzovatelem.
|
||||
pulls.blocked_by_official_review_requests=Tato žádost o sloučení je zablokována, protože jí chybí schválení oficiálních posuzovatelů.
|
||||
pulls.blocked_by_outdated_branch=Tato žádost o sloučení je zablokována, protože je zastaralá.
|
||||
pulls.blocked_by_changed_protected_files_1=Tato žádost o sloučení je zablokována, protože mění chráněný soubor:
|
||||
pulls.blocked_by_changed_protected_files_n=Tato žádost o sloučení je zablokována, protože mění chráněné soubory:
|
||||
pulls.can_auto_merge_desc=Tento požadavek na natažení může být automaticky sloučen.
|
||||
pulls.cannot_auto_merge_desc=Tento požadavek na natažení nemůže být automaticky sloučen, neboť se v něm nachází konflikty.
|
||||
pulls.can_auto_merge_desc=Tato žádost může být automaticky sloučena.
|
||||
pulls.cannot_auto_merge_desc=Tato žádost nemůže být automaticky sloučena, neboť se v ní nachází konflikty.
|
||||
pulls.cannot_auto_merge_helper=Pro vyřešení konfliktů proveďte ruční sloučení.
|
||||
pulls.num_conflicting_files_1=%d konfliktní soubor
|
||||
pulls.num_conflicting_files_n=%d konfliktních souborů
|
||||
|
@ -1823,11 +1825,11 @@ pulls.waiting_count_1=%d čekající posouzení
|
|||
pulls.waiting_count_n=%d čekajících posouzení
|
||||
pulls.wrong_commit_id=id commitu musí být id commitu v cílové větvi
|
||||
|
||||
pulls.no_merge_desc=Tento požadavek na natažení nemůže být sloučen, protože všechny možnosti repozitáře na sloučení jsou zakázány.
|
||||
pulls.no_merge_helper=Povolte možnosti sloučení v nastavení repozitáře nebo proveďte sloučení požadavku na natažení ručně.
|
||||
pulls.no_merge_wip=Požadavek na natažení nemůže být sloučen protože je označen jako nedokončený.
|
||||
pulls.no_merge_not_ready=Tento požadavek na natažení není připraven na sloučení, zkontrolujte stav posouzení a kontrolu stavu.
|
||||
pulls.no_merge_access=Nemáte oprávnění sloučit tento požadavek na natažení.
|
||||
pulls.no_merge_desc=Tato žádost nemůže být sloučena, protože všechny možnosti repozitáře na sloučení jsou zakázány.
|
||||
pulls.no_merge_helper=Povolte možnosti sloučení v nastavení repozitáře nebo proveďte sloučení žádosti ručně.
|
||||
pulls.no_merge_wip=Tato žádost nemůže být sloučena, protože je označena jako rozpracovaná.
|
||||
pulls.no_merge_not_ready=Tento žádost není připravena na sloučení, zkontrolujte stav posouzení a kontroly stavu.
|
||||
pulls.no_merge_access=Nemáte oprávnění sloučit tuto žádost.
|
||||
pulls.merge_pull_request=Vytvořit slučovací commit
|
||||
pulls.rebase_merge_pull_request=Rebase pak fast-forward
|
||||
pulls.rebase_merge_commit_pull_request=Rebase a poté vytvořit slučovací commit
|
||||
|
@ -1836,7 +1838,7 @@ pulls.merge_manually=Sloučeno ručně
|
|||
pulls.merge_commit_id=ID slučovacího commitu
|
||||
pulls.require_signed_wont_sign=Větev vyžaduje podepsané commity, ale toto sloučení nebude podepsáno
|
||||
|
||||
pulls.invalid_merge_option=Nemůžete použít tuto možnost sloučení pro tento požadavek na natažení.
|
||||
pulls.invalid_merge_option=Pro tuto žádost nemůžete použít tuto možnost sloučení.
|
||||
pulls.merge_conflict=Sloučení selhalo: Došlo ke konfliktu při sloučení. Tip: Zkuste jinou strategii
|
||||
pulls.merge_conflict_summary=Chybové hlášení
|
||||
pulls.rebase_conflict=Sloučení selhalo: Došlo ke konfliktu při rebase commitu: %[1]s. Tip: Zkuste jinou strategii
|
||||
|
@ -1844,7 +1846,7 @@ pulls.rebase_conflict_summary=Chybové hlášení
|
|||
pulls.unrelated_histories=Sloučení selhalo: Hlavní a základní revize nesdílí společnou historii. Tip: Zkuste jinou strategii
|
||||
pulls.merge_out_of_date=Sloučení selhalo: Základ byl aktualizován při generování sloučení. Tip: Zkuste to znovu.
|
||||
pulls.head_out_of_date=Sloučení selhalo: Hlavní revize byla aktualizován při generování sloučení. Tip: Zkuste to znovu.
|
||||
pulls.has_merged=Chyba: Požadavek na natažení byl sloučen, nelze znovu sloučit nebo změnit cílovou větev.
|
||||
pulls.has_merged=Chyba: žádost byla sloučena, nelze ji znovu sloučit nebo změnit cílovou větev.
|
||||
pulls.push_rejected=Push selhal: nahrání bylo zamítnuto. Zkontrolujte Git hooky pro tento repozitář.
|
||||
pulls.push_rejected_summary=Úplná zpráva o odmítnutí
|
||||
pulls.push_rejected_no_message=Push selhal: nahrání bylo odmítnuto, ale nebyla nalezena žádná vzdálená zpráva. Zkontrolujte Git hooky pro tento repozitář
|
||||
|
@ -1897,28 +1899,28 @@ milestones.no_due_date=Bez lhůty dokončení
|
|||
milestones.open=Otevřít
|
||||
milestones.close=Zavřít
|
||||
milestones.new_subheader=Milníky vám pomohou organizovat úkoly a sledovat jejich pokrok.
|
||||
milestones.completeness=%d%% Dokončeno
|
||||
milestones.completeness=Dokončeno <strong>%d%%</strong>
|
||||
milestones.create=Vytvořit milník
|
||||
milestones.title=Název
|
||||
milestones.desc=Popis
|
||||
milestones.due_date=Termín (volitelný)
|
||||
milestones.clear=Zrušit
|
||||
milestones.clear=Vymazat
|
||||
milestones.invalid_due_date_format=Termín dokončení musí být ve formátu „rrrr-mm-dd“.
|
||||
milestones.create_success=Milník „%s“ byl vytvořen.
|
||||
milestones.edit=Upravit milník
|
||||
milestones.edit_subheader=Milník organizuje úkoly a sledují pokrok.
|
||||
milestones.edit_subheader=Milníky organizují problémy a sledují pokrok.
|
||||
milestones.cancel=Zrušit
|
||||
milestones.modify=Upravit milník
|
||||
milestones.edit_success=Milník „%s“ byl aktualizován.
|
||||
milestones.deletion=Odstranit milník
|
||||
milestones.deletion_desc=Odstranění milníku jej smaže ze všech souvisejících úkolů. Pokračovat?
|
||||
milestones.deletion_desc=Smazáním milníku jej odstraníte ze všech souvisejících problémů. Pokračovat?
|
||||
milestones.deletion_success=Milník byl odstraněn.
|
||||
milestones.filter_sort.earliest_due_data=Nejbližší termín dokončení
|
||||
milestones.filter_sort.latest_due_date=Nejzazší termín dokončení
|
||||
milestones.filter_sort.least_complete=Nejméně dokončené
|
||||
milestones.filter_sort.most_complete=Nejvíce dokončené
|
||||
milestones.filter_sort.most_issues=Nejvíce úkolů
|
||||
milestones.filter_sort.least_issues=Nejméně úkolů
|
||||
milestones.filter_sort.most_issues=Nejvíce problémů
|
||||
milestones.filter_sort.least_issues=Nejméně problémů
|
||||
|
||||
signing.will_sign=Tento commit bude podepsána klíčem „%s“.
|
||||
signing.wont_sign.error=Došlo k chybě při kontrole, zda může být commit podepsán.
|
||||
|
@ -2002,7 +2004,7 @@ activity.new_issues_count_n=Nové problémy
|
|||
activity.new_issue_label=Otevřený
|
||||
activity.title.unresolved_conv_1=%d nevyřešená konverzace
|
||||
activity.title.unresolved_conv_n=%d nevyřešených konverzací
|
||||
activity.unresolved_conv_desc=Tyto nedávno změněné úkolu a požadavky na natažení ještě nebyly vyřešeny.
|
||||
activity.unresolved_conv_desc=Tyto nedávno změněné problémy a žádosti o sloučení zatím nebyly vyřešeny.
|
||||
activity.unresolved_conv_label=Otevřít
|
||||
activity.title.releases_1=%d vydání
|
||||
activity.title.releases_n=%d vydání
|
||||
|
@ -2093,17 +2095,17 @@ settings.issues_desc=Povolit systém problémů repozitáře
|
|||
settings.use_internal_issue_tracker=Použít vestavěný systém problémů
|
||||
settings.use_external_issue_tracker=Použít externí systém problémů
|
||||
settings.external_tracker_url=Adresa URL externího systému problémů
|
||||
settings.external_tracker_url_error=URL externího systému úkolu není platné URL.
|
||||
settings.external_tracker_url_desc=Když návštěvníci kliknou na záložku úkolů, jsou přesměrování na externí systém úkolů.
|
||||
settings.external_tracker_url_error=Adresa URL externího systému problémů není platnou adresou URL.
|
||||
settings.external_tracker_url_desc=Pokud návštěvníci kliknou na záložku problémů, budou přesměrování na externí systém problémů.
|
||||
settings.tracker_url_format=Formát adresy URL externího systému problémů
|
||||
settings.tracker_url_format_error=Formát URL externího systému úkolu není platné URL.
|
||||
settings.tracker_url_format_error=Formát adresy URL externího systému problémů není platná adresa URL.
|
||||
settings.tracker_issue_style=Formát čísel externího systému problémů
|
||||
settings.tracker_issue_style.numeric=Číselný
|
||||
settings.tracker_issue_style.alphanumeric=Alfanumerický
|
||||
settings.tracker_issue_style.regexp=Regulární výraz
|
||||
settings.tracker_issue_style.regexp_pattern=Vzor regulárního výrazu
|
||||
settings.tracker_issue_style.regexp_pattern_desc=První zachycená skupina bude použita místo <code>{index}</code>.
|
||||
settings.tracker_url_format_desc=Použijte zástupné symboly <code>{user}</code>, <code>{repo}</code> a <code>{index}</code> pro uživatelské jméno, jméno repozitáře a číslo úkolu.
|
||||
settings.tracker_url_format_desc=Použijte proměnné <code>{user}</code>, <code>{repo}</code> a <code>{index}</code> pro uživatelské jméno, název repozitáře a číslo problému.
|
||||
settings.enable_timetracker=Povolit sledování času
|
||||
settings.allow_only_contributors_to_track_time=Povolit sledování času pouze přispěvatelům
|
||||
settings.pulls_desc=Povolit žádosti o sloučení
|
||||
|
@ -2124,7 +2126,7 @@ settings.admin_indexer_commit_sha=Poslední indexovaná SHA
|
|||
settings.admin_indexer_unindexed=Neindexováno
|
||||
settings.reindex_button=Přidat do fronty reindexace
|
||||
settings.reindex_requested=Požadováno reindexování
|
||||
settings.admin_enable_close_issues_via_commit_in_any_branch=Zavřít úkol pomocí commitu v jiné než výchozí větvi
|
||||
settings.admin_enable_close_issues_via_commit_in_any_branch=Zavřít problém pomocí commitu v jiné než výchozí větvi
|
||||
settings.danger_zone=Nebezpečná zóna
|
||||
settings.new_owner_has_same_repo=Nový vlastník již repozitář se stejným názvem má. Vyberte prosím jiné jméno.
|
||||
settings.convert=Převést na běžný repozitář
|
||||
|
@ -2133,10 +2135,10 @@ settings.convert_notices_1=Tato operace převede toto zrcadlo na běžný repozi
|
|||
settings.convert_confirm=Převést repozitář
|
||||
settings.convert_succeed=Zrcadlo bylo převedeno na běžný repozitář.
|
||||
settings.convert_fork=Převést na běžný repozitář
|
||||
settings.convert_fork_desc=Můžete převést toto rozštěpení na běžný repozitář. Tuto akci nelze vrátit zpět.
|
||||
settings.convert_fork_notices_1=Tato operace převede rozštěpení na běžný repozitář a nelze ji vrátit zpět.
|
||||
settings.convert_fork_desc=Tento fork můžete převést na běžný repozitář. Tato akce je nevratná.
|
||||
settings.convert_fork_notices_1=Tato operace převede fork na běžný repozitář a nelze ji vrátit zpět.
|
||||
settings.convert_fork_confirm=Převést repozitář
|
||||
settings.convert_fork_succeed=Rozštěpení bylo překonvertován na běžný repozitář.
|
||||
settings.convert_fork_succeed=Fork bylo převeden na běžný repozitář.
|
||||
settings.transfer=Předat vlastnictví
|
||||
settings.transfer.rejected=Převod repozitáře byl zamítnut.
|
||||
settings.transfer.success=Převod repozitáře byl úspěšný.
|
||||
|
@ -2174,8 +2176,8 @@ settings.wiki_deletion_success=Wiki data repozitáře byla odstraněna.
|
|||
settings.delete=Odstranit tento repozitář
|
||||
settings.delete_desc=Smazání repozitáře je trvalé a nemůže být vráceno zpět.
|
||||
settings.delete_notices_1=- Tuto operaci <strong>nelze</strong> zvrátit.
|
||||
settings.delete_notices_2=- Tato operace trvale smaže repozitář <strong>%s</strong> včetně kódu, úkolů, komentářů, Wiki dat a nastavení spolupracovníků.
|
||||
settings.delete_notices_fork_1=- Rozštěpení repozitáře bude nezávislé po smazání.
|
||||
settings.delete_notices_2=- Tato operace trvale smaže repozitář <strong>%s</strong> včetně kódu, problémů, komentářů, dat wiki a nastavení spolupracovníků.
|
||||
settings.delete_notices_fork_1=- Fork tohoto repozitáře bude po smazání nezávislý.
|
||||
settings.deletion_success=Repozitář byl odstraněn.
|
||||
settings.update_settings_success=Nastavení repozitáře bylo aktualizováno.
|
||||
settings.update_settings_no_unit=Repozitář by měl povolit alespoň určitý druh interakce.
|
||||
|
@ -2253,16 +2255,16 @@ settings.event_push_desc=Nahrání pomocí Gitu do repozitáře.
|
|||
settings.event_repository=Repozitář
|
||||
settings.event_repository_desc=Repozitář vytvořen nebo smazán.
|
||||
settings.event_header_issue=Události problémů
|
||||
settings.event_issues=Úkoly
|
||||
settings.event_issues_desc=Úkol otevřen, uzavřen, znovu otevřen nebo upraven.
|
||||
settings.event_issues=Problémy
|
||||
settings.event_issues_desc=Problém otevřen, uzavřen, znovu otevřen nebo upraven.
|
||||
settings.event_issue_assign=Problém přiřazen
|
||||
settings.event_issue_assign_desc=Úkol přiřazen nebo nepřiřazen.
|
||||
settings.event_issue_assign_desc=Problém přiřazen nebo nepřiřazen.
|
||||
settings.event_issue_label=Problém označen
|
||||
settings.event_issue_label_desc=Štítky úkolu aktualizovány nebo vymazány.
|
||||
settings.event_issue_label_desc=Štítky problému upraveny nebo vymazány.
|
||||
settings.event_issue_milestone=K problému přidán milník
|
||||
settings.event_issue_milestone_desc=Úkolu přidán nebo odebrán milník.
|
||||
settings.event_issue_milestone_desc=K problému přidán nebo odebrán milník.
|
||||
settings.event_issue_comment=Komentář k problému
|
||||
settings.event_issue_comment_desc=Komentář úkolu přidán, upraven nebo smazán.
|
||||
settings.event_issue_comment_desc=Přidán, upraven nebo smazán komentář problému.
|
||||
settings.event_header_pull_request=Události žádosti o sloučení
|
||||
settings.event_pull_request=Žádost o sloučení
|
||||
settings.event_pull_request_desc=Požadavek na natažení otevřen, uzavřen, znovu otevřen nebo upraven.
|
||||
|
@ -2306,9 +2308,9 @@ settings.web_hook_name_dingtalk=DingTalk
|
|||
settings.web_hook_name_telegram=Telegram
|
||||
settings.web_hook_name_matrix=Matrix
|
||||
settings.web_hook_name_msteams=Microsoft Teams
|
||||
settings.web_hook_name_feishu_or_larksuite=Feishu / Lark Suite
|
||||
settings.web_hook_name_feishu=Feishu
|
||||
settings.web_hook_name_larksuite=Lark Suite
|
||||
settings.web_hook_name_feishu=Feishu / Lark Suite
|
||||
settings.web_hook_name_feishu_only =Feishu
|
||||
settings.web_hook_name_larksuite_only =Lark Suite
|
||||
settings.web_hook_name_wechatwork=WeCom (Wechat Work)
|
||||
settings.web_hook_name_packagist=Packagist
|
||||
settings.packagist_username=Uživatelské jméno pro Packagist
|
||||
|
@ -2426,7 +2428,7 @@ settings.archive.branchsettings_unavailable=Nastavení větví není dostupné,
|
|||
settings.archive.tagsettings_unavailable=Nastavení značek není k dispozici, pokud je repozitář archivován.
|
||||
settings.unarchive.button=Zrušit archivaci repozitáře
|
||||
settings.unarchive.header=Obnovit tento repozitář
|
||||
settings.unarchive.text=Obnovení repozitáře vrátí možnost přijímání commitů a nahrávání. Stejně tak se obnoví i možnost zadávání nových úkolů a požadavků na natažení.
|
||||
settings.unarchive.text=Obnovení repozitáře vrátí možnost přijímání commitů a nahrávání. Stejně tak se obnoví i možnost vytváření nových problémů a žádostí o sloučení.
|
||||
settings.unarchive.success=Repozitář byl úspěšně obnoven.
|
||||
settings.unarchive.error=Nastala chyba při obnovování repozitáře. Prohlédněte si záznam pro více detailů.
|
||||
settings.update_avatar_success=Avatar repozitáře byl aktualizován.
|
||||
|
@ -2705,6 +2707,20 @@ pulls.title_desc_one = žádá o sloučení %[1]d commitu z <code>%[2]s</code> d
|
|||
pulls.merged_title_desc_one = sloučil %[1]d commit z <code>%[2]s</code> do <code>%[3]s</code> %[4]s
|
||||
open_with_editor = Otevřít pomocí %s
|
||||
commits.search_branch = Tato větev
|
||||
editor.commit_id_not_matching = ID commitu se neshoduje s ID commitu, který jste upravovali. Proveďte commit do nové větve a poté je slučte.
|
||||
pulls.ready_for_review = Připraveni na posouzení?
|
||||
settings.rename_branch_failed_protected = Nepodařilo se přejmenovat větev %s, jelikož se jedná o chráněnou větev.
|
||||
editor.push_out_of_date = Push je nejspíše zastaralý.
|
||||
stars = Oblíbení
|
||||
n_commit_one = %s commit
|
||||
n_commit_few = %s commitů
|
||||
n_branch_one = %s větev
|
||||
n_tag_one = %s značka
|
||||
n_tag_few = %s značek
|
||||
n_branch_few = %s větví
|
||||
settings.event_pull_request_enforcement = Vynucení
|
||||
settings.enforce_on_admins = Vynutit toto pravidlo pro správce repozitáře
|
||||
settings.enforce_on_admins_desc = Správci repozitáře nemohou obejít toto pravidlo.
|
||||
|
||||
[graphs]
|
||||
component_loading_info = Tohle může chvíli trvat…
|
||||
|
@ -2770,7 +2786,7 @@ settings.delete_org_title=Odstranit organizaci
|
|||
settings.delete_org_desc=Tato organizace bude trvale smazána. Pokračovat?
|
||||
settings.hooks_desc=Přidat webové háčky, které budou spouštěny pro <strong>všechny repozitáře</strong> v této organizaci.
|
||||
|
||||
settings.labels_desc=Přidejte štítky, které mohou být použity pro úkoly <strong>všech repositářů</strong> v rámci této organizace.
|
||||
settings.labels_desc=Přidejte štítky, které mohou být použity pro problémy <strong>všech repozitářů</strong> v rámci této organizace.
|
||||
|
||||
members.membership_visibility=Viditelnost členství:
|
||||
members.public=Viditelný
|
||||
|
@ -3028,7 +3044,7 @@ repos.private=Soukromý
|
|||
repos.watches=Sledovače
|
||||
repos.stars=Oblíbení
|
||||
repos.forks=Rozštěpení
|
||||
repos.issues=Úkoly
|
||||
repos.issues=Problémy
|
||||
repos.size=Velikost
|
||||
repos.lfs_size=Velikost LFS
|
||||
|
||||
|
@ -3153,7 +3169,7 @@ auths.tip.google_plus=Získejte klientské pověření OAuth2 z Google API konzo
|
|||
auths.tip.openid_connect=Použijte OpenID URL pro objevování spojení (<server>/.well-known/openid-configuration) k nastavení koncových bodů
|
||||
auths.tip.twitter=Jděte na https://dev.twitter.com/apps, vytvořte aplikaci a ujistěte se, že volba „Allow this application to be used to Sign in with Twitter“ je povolená
|
||||
auths.tip.discord=Registrujte novou aplikaci na https://discordapp.com/developers/applications/me
|
||||
auths.tip.gitea=Registrovat novou Oauth2 aplikaci. Návod naleznete na https://docs.gitea.com/development/oauth2-provider
|
||||
auths.tip.gitea=Registrovat novou Oauth2 aplikaci. Návod naleznete na https://forgejo.org/docs/latest/user/oauth2-provider
|
||||
auths.tip.yandex=Vytvořte novou aplikaci na https://oauth.yandex.com/client/new. Vyberte následující oprávnění z „Yandex.Passport API“ sekce: „Přístup k e-mailové adrese“, „Přístup k uživatelskému avataru“ a „Přístup k uživatelskému jménu, jménu a příjmení, pohlaví“
|
||||
auths.tip.mastodon=Vložte vlastní URL instance pro mastodon, kterou se chcete autentizovat (nebo použijte výchozí)
|
||||
auths.edit=Upravit zdroj ověřování
|
||||
|
@ -3188,7 +3204,7 @@ config.repo_root_path=Kořenový adresář repozitářů
|
|||
config.lfs_root_path=Kořenový adresář LFS
|
||||
config.log_file_root_path=Adresář protokolů
|
||||
config.script_type=Typ skriptu
|
||||
config.reverse_auth_user=Obrátit uživatele ověření
|
||||
config.reverse_auth_user=Obrátit uživatele ověření proxy
|
||||
|
||||
config.ssh_config=Nastavení SSH
|
||||
config.ssh_enabled=Zapnutý
|
||||
|
@ -3237,7 +3253,7 @@ config.allow_dots_in_usernames = Povolit uživatelům používat tečky ve svýc
|
|||
config.default_allow_only_contributors_to_track_time=Povolit sledování času pouze přispěvatelům
|
||||
config.no_reply_address=Skrytá e-mailová doména
|
||||
config.default_visibility_organization=Výchozí viditelnost nových organizací
|
||||
config.default_enable_dependencies=Povolit ve výchozím nastavení závislosti úkolů
|
||||
config.default_enable_dependencies=Povolit ve výchozím nastavení závislosti problémů
|
||||
|
||||
config.webhook_config=Nastavení webhooků
|
||||
config.queue_length=Délka fronty
|
||||
|
@ -3391,14 +3407,15 @@ auths.tips.gmail_settings = Nastavení služby Gmail:
|
|||
config_summary = Souhrn
|
||||
config.open_with_editor_app_help = Editory v nabídce „Otevřít pomocí“ v nabídce klonování. Ponechte prázdné pro použití výchozího editoru (zobrazíte jej rozšířením).
|
||||
config_settings = Nastavení
|
||||
auths.tip.gitlab_new = Zaregistrujte si novou aplikaci na https://gitlab.com/-/profile/applications
|
||||
|
||||
[action]
|
||||
create_repo=vytvořil/a repozitář <a href="%s">%s</a>
|
||||
rename_repo=přejmenoval/a repozitář z <code>%[1]s</code> na <a href="%[2]s">%[3]s</a>
|
||||
commit_repo=nahrál/a do <a href="%[2]s">%[3]s</a> v <a href="%[1]s">%[4]s</a>
|
||||
create_issue=`otevřel/a úkol <a href="%[1]s">%[3]s#%[2]s</a>`
|
||||
close_issue=`uzavřel/a úkol <a href="%[1]s">%[3]s#%[2]s</a>`
|
||||
reopen_issue=`znovuotevřel/a úkol <a href="%[1]s">%[3]s#%[2]s</a>`
|
||||
create_issue=`otevřel/a problém <a href="%[1]s">%[3]s#%[2]s</a>`
|
||||
close_issue=`uzavřel/a problém <a href="%[1]s">%[3]s#%[2]s</a>`
|
||||
reopen_issue=`znovu otevřel/a problém <a href="%[1]s">%[3]s#%[2]s</a>`
|
||||
create_pull_request=`vytvořil/a požadavek na natažení <a href="%[1]s">%[3]s#%[2]s</a>`
|
||||
close_pull_request=`uzavřel/a požadavek na natažení <a href="%[1]s">%[3]s#%[2]s</a>`
|
||||
reopen_pull_request=`znovuotevřel/a požadavek na natažení <a href="%[1]s">%[3]s#%[2]s</a>`
|
||||
|
@ -3745,6 +3762,7 @@ runners = Runnery
|
|||
runs.pushed_by = pushnuto uživatelem
|
||||
need_approval_desc = Potřebovat schválení pro spouštění workflowů pro žádosti o sloučení forků.
|
||||
runners.runner_manage_panel = Správa runnerů
|
||||
runs.no_job_without_needs = Workflow musí obsahovat alespoň jednu práci bez závislostí.
|
||||
|
||||
[projects]
|
||||
type-1.display_name=Samostatný projekt
|
||||
|
@ -3782,3 +3800,18 @@ no_results = Nenalezeny žádné odpovídající výsledky.
|
|||
fuzzy_tooltip = Zahrnout také výsledky, které úzce odpovídají hledanému výrazu
|
||||
search = Hledat...
|
||||
keyword_search_unavailable = Hledání pomocí klíčových slov momentálně není dostupné. Kontaktujte prosím administrátora webu.
|
||||
code_search_by_git_grep = Aktuální výsledky vyhledávání kódu jsou poskytovány službou „git grep“. Lepší výsledky dostanete, když administrátor webu povolí indexování repozitářů.
|
||||
|
||||
[markup]
|
||||
filepreview.lines = Řádky %[1]d až %[2]d v souboru %[3]s
|
||||
filepreview.line = Řádek %[1]d v souboru %[2]s
|
||||
filepreview.truncated = Náhled byl zkrácen
|
||||
|
||||
[munits.data]
|
||||
b = B
|
||||
kib = KiB
|
||||
mib = MiB
|
||||
gib = GiB
|
||||
tib = TiB
|
||||
pib = PiB
|
||||
eib = EiB
|
|
@ -155,6 +155,8 @@ filter.is_template = Vorlage
|
|||
filter.not_template = Keine Vorlage
|
||||
filter.public = Öffentlich
|
||||
filter.private = Privat
|
||||
more_items = Mehr Einträge
|
||||
invalid_data = Ungültige Daten: %v
|
||||
|
||||
[aria]
|
||||
navbar=Navigationsleiste
|
||||
|
@ -979,10 +981,10 @@ user_unblock_success = Die Blockierung dieses Benutzers wurde erfolgreich zurüc
|
|||
blocked_users = Blockierte Benutzer
|
||||
blocked_since = Blockiert seit %s
|
||||
change_password = Passwort ändern
|
||||
hints = Tipps
|
||||
hints = Hinweise
|
||||
additional_repo_units_hint = Zur Aktivierung zusätzlicher Repository-Einheiten ermutigen
|
||||
update_hints = Tipps aktualisieren
|
||||
update_hints_success = Tipps wurden aktualisiert.
|
||||
update_hints = Hinweise aktualisieren
|
||||
update_hints_success = Hinweise wurden aktualisiert.
|
||||
additional_repo_units_hint_description = Einen „Mehr Einheiten hinzufügen …“-Button für Repositorys, welche nicht alle verfügbaren Einheiten aktiviert haben, anzeigen.
|
||||
|
||||
[repo]
|
||||
|
@ -1393,7 +1395,7 @@ projects.column.set_default_desc=Diese Spalte als Standard für nicht kategorisi
|
|||
projects.column.unset_default=Standard entfernen
|
||||
projects.column.unset_default_desc=Diese Spalte nicht als Standard verwenden
|
||||
projects.column.delete=Spalte löschen
|
||||
projects.column.deletion_desc=Beim Löschen einer Projektspalte werden alle dazugehörigen Issues nach „Nicht kategorisiert“ verschoben. Fortfahren?
|
||||
projects.column.deletion_desc=Beim Löschen einer Projektspalte werden alle dazugehörigen Issues zur Standardspalte verschoben. Fortfahren?
|
||||
projects.column.color=Farbe
|
||||
projects.open=Öffnen
|
||||
projects.close=Schließen
|
||||
|
@ -1411,7 +1413,7 @@ issues.filter_reviewers=Reviewer filtern
|
|||
issues.new=Neues Issue
|
||||
issues.new.title_empty=Der Titel kann nicht leer sein
|
||||
issues.new.labels=Labels
|
||||
issues.new.no_label=Kein Label
|
||||
issues.new.no_label=Keine Label
|
||||
issues.new.clear_labels=Labels entfernen
|
||||
issues.new.projects=Projekte
|
||||
issues.new.clear_projects=Projekte löschen
|
||||
|
@ -1596,7 +1598,7 @@ issues.label.filter_sort.alphabetically=Alphabetisch
|
|||
issues.label.filter_sort.reverse_alphabetically=Umgekehrt alphabetisch
|
||||
issues.label.filter_sort.by_size=Kleinste Größe
|
||||
issues.label.filter_sort.reverse_by_size=Größte Größe
|
||||
issues.num_participants=%d Beteiligte
|
||||
issues.num_participants_few=%d Beteiligte
|
||||
issues.attachment.open_tab=`Klicken, um „%s“ in einem neuen Tab zu öffnen`
|
||||
issues.attachment.download=`Klicken, um „%s“ herunterzuladen`
|
||||
issues.subscribe=Abonnieren
|
||||
|
@ -2302,9 +2304,9 @@ settings.web_hook_name_dingtalk=DingTalk
|
|||
settings.web_hook_name_telegram=Telegram
|
||||
settings.web_hook_name_matrix=Matrix
|
||||
settings.web_hook_name_msteams=Microsoft Teams
|
||||
settings.web_hook_name_feishu_or_larksuite=Feishu / Lark Suite
|
||||
settings.web_hook_name_feishu=Feishu
|
||||
settings.web_hook_name_larksuite=Lark Suite
|
||||
settings.web_hook_name_feishu=Feishu / Lark Suite
|
||||
settings.web_hook_name_feishu_only =Feishu
|
||||
settings.web_hook_name_larksuite_only =Lark Suite
|
||||
settings.web_hook_name_wechatwork=WeCom (Wechat Work)
|
||||
settings.web_hook_name_packagist=Packagist
|
||||
settings.packagist_username=Benutzername für Packagist
|
||||
|
@ -2690,6 +2692,16 @@ pulls.title_desc_one = möchte %[1]d Commit von <code>%[2]s</code> nach <code id
|
|||
open_with_editor = Öffnen mit %s
|
||||
commits.search_branch = Dieser Branch
|
||||
pulls.ready_for_review = Bereit zum Review?
|
||||
settings.rename_branch_failed_protected = Branch %s kann nicht umbenannt werden, weil er ein geschützter Branch ist.
|
||||
editor.commit_id_not_matching = Die Commit-ID passt nicht zur ID die du bearbeitet hast hast. Committe in einen neuen Branch, dann mach einen Merge.
|
||||
editor.push_out_of_date = Der Push scheint veraltet zu sein.
|
||||
n_commit_few = %s Commits
|
||||
n_branch_one = %s Branch
|
||||
n_branch_few = %s Branches
|
||||
n_tag_one = %s Tag
|
||||
n_tag_few = %s Tags
|
||||
stars = Favorisierungen
|
||||
n_commit_one = %s Commit
|
||||
|
||||
[graphs]
|
||||
|
||||
|
@ -3134,7 +3146,7 @@ auths.tip.google_plus=Du erhältst die OAuth2-Client-Zugangsdaten in der Google-
|
|||
auths.tip.openid_connect=Benutze die OpenID-Connect-Discovery-URL (<server>/.well-known/openid-configuration), um die Endpunkte zu spezifizieren
|
||||
auths.tip.twitter=Gehe auf https://dev.twitter.com/apps, erstelle eine Anwendung und stelle sicher, dass die Option „Allow this application to be used to Sign in with Twitter“ aktiviert ist
|
||||
auths.tip.discord=Erstelle unter https://discordapp.com/developers/applications/me eine neue Anwendung.
|
||||
auths.tip.gitea=Registriere eine neue OAuth2-Anwendung. Eine Anleitung findest du unter https://docs.gitea.com/development/oauth2-provider/
|
||||
auths.tip.gitea=Registriere eine neue OAuth2-Anwendung. Eine Anleitung findest du unter https://forgejo.org/docs/latest/user/oauth2-provider
|
||||
auths.tip.yandex=`Erstelle eine neue Anwendung auf https://oauth.yandex.com/client/new. Wähle folgende Berechtigungen aus dem Abschnitt „Yandex.Passport API“: „Zugriff auf E-Mail-Adresse“, „Zugriff auf Benutzeravatar“ und „Zugriff auf Benutzername, Vor- und Nachname, Geschlecht“`
|
||||
auths.tip.mastodon=Gib eine benutzerdefinierte URL für die Mastodon-Instanz ein, mit der du dich authentifizieren möchtest (oder benutze die standardmäßige)
|
||||
auths.edit=Authentifikationsquelle bearbeiten
|
||||
|
@ -3169,7 +3181,7 @@ config.repo_root_path=Repository-Wurzelpfad
|
|||
config.lfs_root_path=LFS-Wurzelpfad
|
||||
config.log_file_root_path=Logdateipfad
|
||||
config.script_type=Skript-Typ
|
||||
config.reverse_auth_user=Nutzer bei Reverse-Authentifizierung
|
||||
config.reverse_auth_user=Nutzer bei Reverse-Proxy-Authentifizierung
|
||||
|
||||
config.ssh_config=SSH-Konfiguration
|
||||
config.ssh_enabled=Aktiviert
|
||||
|
@ -3358,6 +3370,7 @@ auths.tips.gmail_settings = Gmail-Einstellungen:
|
|||
config_settings = Einstellungen
|
||||
config.open_with_editor_app_help = Die „Öffnen mit“-Editoren für das Klonmenü. Falls es leer gelassen wird, wird der Standardwert benutzt. Erweitern, um den Standardwert zu sehen.
|
||||
config_summary = Zusammenfassung
|
||||
auths.tip.gitlab_new = Registriere eine neue Anwendung auf https://gitlab.com/-/profile/applications
|
||||
|
||||
|
||||
[action]
|
||||
|
@ -3711,6 +3724,7 @@ runs.no_workflows.documentation = Für weitere Informationen über Forgejo Actio
|
|||
runs.empty_commit_message = (leere Commit-Nachricht)
|
||||
variables.id_not_exist = Variable mit ID %d existiert nicht.
|
||||
runs.workflow = Workflow
|
||||
runs.no_job_without_needs = Der Workflow muss mindestens einen Job ohne Abhängigkeiten enthalten.
|
||||
|
||||
[projects]
|
||||
type-1.display_name=Individuelles Projekt
|
||||
|
@ -3758,3 +3772,9 @@ runner_kind = Runners suchen …
|
|||
no_results = Keine passenden Ergebnisse gefunden.
|
||||
code_search_unavailable = Die Code-Suche ist momentan nicht verfügbar. Bitte kontaktiere den Webseitenadministrator.
|
||||
keyword_search_unavailable = Suche nach Schlüsselwörtern ist momentan nicht unterstüzt. Bitte kontaktiere den Webseitenadministrator.
|
||||
code_search_by_git_grep = Die derzeitigen Codesuchergebnisse werden durch „git grep“ bereitgestellt. Es könnten bessere Ergebnisse erzielt werden, wenn der Administrator die Repository-Indizierung aktiviert.
|
||||
|
||||
[markup]
|
||||
filepreview.line = Zeile %[1]d in %[2]s
|
||||
filepreview.truncated = Vorschau wurde gekürzt
|
||||
filepreview.lines = Zeilen %[1]d bis %[2]d in %[3]s
|
|
@ -1580,7 +1580,7 @@ issues.label.filter_sort.alphabetically=Αλφαβητικά
|
|||
issues.label.filter_sort.reverse_alphabetically=Αντίστροφα αλφαβητικά
|
||||
issues.label.filter_sort.by_size=Μικρότερο μέγεθος
|
||||
issues.label.filter_sort.reverse_by_size=Μεγαλύτερο μέγεθος
|
||||
issues.num_participants=%d Συμμετέχοντες
|
||||
issues.num_participants_few=%d Συμμετέχοντες
|
||||
issues.attachment.open_tab=`Πατήστε εδώ για να ανοίξετε το «%s» σε μια νέα καρτέλα`
|
||||
issues.attachment.download=`Πατήστε εδώ για να κατεβάσετε το «%s»`
|
||||
issues.subscribe=Εγγραφή
|
||||
|
@ -2293,9 +2293,9 @@ settings.web_hook_name_dingtalk=DingTalk
|
|||
settings.web_hook_name_telegram=Telegram
|
||||
settings.web_hook_name_matrix=Matrix
|
||||
settings.web_hook_name_msteams=Microsoft Teams
|
||||
settings.web_hook_name_feishu_or_larksuite=Feishu / Lark Suite
|
||||
settings.web_hook_name_feishu=Feishu
|
||||
settings.web_hook_name_larksuite=Lark Suite
|
||||
settings.web_hook_name_feishu=Feishu / Lark Suite
|
||||
settings.web_hook_name_feishu_only =Feishu
|
||||
settings.web_hook_name_larksuite_only =Lark Suite
|
||||
settings.web_hook_name_wechatwork=WeCom (Wechat Work)
|
||||
settings.web_hook_name_packagist=Packagist
|
||||
settings.packagist_username=Όνομα χρήστη Packagist
|
||||
|
|
|
@ -146,15 +146,15 @@ name = Name
|
|||
value = Value
|
||||
|
||||
filter = Filter
|
||||
filter.clear = Clear Filter
|
||||
filter.clear = Clear filters
|
||||
filter.is_archived = Archived
|
||||
filter.not_archived = Not Archived
|
||||
filter.not_archived = Not archived
|
||||
filter.is_fork = Forked
|
||||
filter.not_fork = Not Forked
|
||||
filter.not_fork = Not forked
|
||||
filter.is_mirror = Mirrored
|
||||
filter.not_mirror = Not Mirrored
|
||||
filter.not_mirror = Not mirrored
|
||||
filter.is_template = Template
|
||||
filter.not_template = Not Template
|
||||
filter.not_template = Not template
|
||||
filter.public = Public
|
||||
filter.private = Private
|
||||
|
||||
|
@ -222,7 +222,7 @@ missing_csrf = Bad Request: no CSRF token present
|
|||
invalid_csrf = Bad Request: invalid CSRF token
|
||||
not_found = The target couldn't be found.
|
||||
network_error = Network error
|
||||
server_internal = Internal Server Error
|
||||
server_internal = Internal server error
|
||||
|
||||
[startpage]
|
||||
app_desc = A painless, self-hosted Git service
|
||||
|
@ -762,7 +762,6 @@ password_incorrect = The current password is incorrect.
|
|||
change_password_success = Your password has been updated. Sign in using your new password from now on.
|
||||
password_change_disabled = Non-local users cannot update their password through the Forgejo web interface.
|
||||
|
||||
emails = Email addresses
|
||||
manage_emails = Manage email addresses
|
||||
manage_themes = Select default theme
|
||||
manage_openid = Manage OpenID addresses
|
||||
|
@ -893,9 +892,9 @@ repo_and_org_access = Repository and Organization Access
|
|||
permissions_public_only = Public only
|
||||
permissions_access_all = All (public, private, and limited)
|
||||
select_permissions = Select permissions
|
||||
permission_no_access = No Access
|
||||
permission_no_access = No access
|
||||
permission_read = Read
|
||||
permission_write = Read and Write
|
||||
permission_write = Read and write
|
||||
access_token_desc = Selected token permissions limit authorization only to the corresponding <a %s>API</a> routes. Read the <a %s>documentation</a> for more information.
|
||||
at_least_one_permission = You must select at least one permission to create a token
|
||||
permissions_list = Permissions:
|
||||
|
@ -915,8 +914,8 @@ oauth2_confidential_client = Confidential client. Select for apps that keep the
|
|||
oauth2_redirect_uris = Redirect URIs. Please use a new line for every URI.
|
||||
save_application = Save
|
||||
oauth2_client_id = Client ID
|
||||
oauth2_client_secret = Client Secret
|
||||
oauth2_regenerate_secret = Regenerate Secret
|
||||
oauth2_client_secret = Client secret
|
||||
oauth2_regenerate_secret = Regenerate secret
|
||||
oauth2_regenerate_secret_hint = Lost your secret?
|
||||
oauth2_client_secret_hint = The secret will not be shown again after you leave or refresh this page. Please ensure that you have saved it.
|
||||
oauth2_application_edit = Edit
|
||||
|
@ -927,7 +926,7 @@ oauth2_application_locked = Forgejo pre-registers some OAuth2 applications on st
|
|||
authorized_oauth2_applications = Authorized OAuth2 applications
|
||||
authorized_oauth2_applications_description = You have granted access to your personal Forgejo account to these third party applications. Please revoke access for applications that are no longer in use.
|
||||
revoke_key = Revoke
|
||||
revoke_oauth2_grant = Revoke Access
|
||||
revoke_oauth2_grant = Revoke access
|
||||
revoke_oauth2_grant_description = Revoking access for this third party application will prevent this application from accessing your data. Are you sure?
|
||||
revoke_oauth2_grant_success = Access revoked successfully.
|
||||
|
||||
|
@ -1012,6 +1011,7 @@ owner_helper = Some organizations may not show up in the dropdown due to a maxim
|
|||
repo_name = Repository name
|
||||
repo_name_helper = Good repository names use short, memorable and unique keywords.
|
||||
repo_size = Repository Size
|
||||
size_format = %[1]s: %[2]s, %[3]s: %[4]s
|
||||
template = Template
|
||||
template_select = Select a template.
|
||||
template_helper = Make repository a template
|
||||
|
@ -1143,7 +1143,6 @@ form.name_pattern_not_allowed = The pattern "%s" is not allowed in a repository
|
|||
|
||||
need_auth = Authorization
|
||||
migrate_options = Migration options
|
||||
migrate_service = Migration service
|
||||
migrate_options_mirror_helper = This repository will be a mirror
|
||||
migrate_options_lfs = Migrate LFS files
|
||||
migrate_options_lfs_endpoint.label = LFS endpoint
|
||||
|
@ -1254,11 +1253,11 @@ n_tag_few=%s tags
|
|||
released_this = released this
|
||||
file.title = %s at %s
|
||||
file_raw = Raw
|
||||
file_follow = Follow Symlink
|
||||
file_follow = Follow symlink
|
||||
file_history = History
|
||||
file_view_source = View Source
|
||||
file_view_rendered = View Rendered
|
||||
file_view_raw = View Raw
|
||||
file_view_source = View source
|
||||
file_view_rendered = View rendered
|
||||
file_view_raw = View raw
|
||||
file_permalink = Permalink
|
||||
file_too_large = The file is too large to be shown.
|
||||
invisible_runes_header = `This file contains invisible Unicode characters`
|
||||
|
@ -1313,8 +1312,8 @@ editor.name_your_file = Name your file…
|
|||
editor.filename_help = Add a directory by typing its name followed by a slash ("/"). Remove a directory by typing backspace at the beginning of the input field.
|
||||
editor.or = or
|
||||
editor.cancel_lower = Cancel
|
||||
editor.commit_signed_changes = Commit Signed Changes
|
||||
editor.commit_changes = Commit Changes
|
||||
editor.commit_signed_changes = Commit signed changes
|
||||
editor.commit_changes = Commit changes
|
||||
editor.add_tmpl = Add "<filename>"
|
||||
editor.add = Add %s
|
||||
editor.update = Update %s
|
||||
|
@ -1342,9 +1341,9 @@ editor.file_is_a_symlink = `"%s" is a symbolic link. Symbolic links cannot be ed
|
|||
editor.filename_is_a_directory = Filename "%s" is already used as a directory name in this repository.
|
||||
editor.file_editing_no_longer_exists = The file being edited, "%s", no longer exists in this repository.
|
||||
editor.file_deleting_no_longer_exists = The file being deleted, "%s", no longer exists in this repository.
|
||||
editor.file_changed_while_editing = The file contents have changed since you started editing. <a target="_blank" rel="noopener noreferrer" href="%s">Click here</a> to see them or <strong>Commit Changes again</strong> to overwrite them.
|
||||
editor.file_changed_while_editing = The file contents have changed since you started editing. <a target="_blank" rel="noopener noreferrer" href="%s">Click here</a> to see them or <strong>Commit changes again</strong> to overwrite them.
|
||||
editor.file_already_exists = A file named "%s" already exists in this repository.
|
||||
editor.commit_id_not_matching = The Commit ID does not match the ID when you began editing. Commit into a patch branch and then merge.
|
||||
editor.commit_id_not_matching = The commit ID does not match the one you was editing. Commit to a new branch and then merge.
|
||||
editor.push_out_of_date = The push appears to be out of date.
|
||||
editor.commit_empty_file_header = Commit an empty file
|
||||
editor.commit_empty_file_text = The file you're about to commit is empty. Proceed?
|
||||
|
@ -1419,26 +1418,26 @@ projects.edit_subheader = Projects organize issues and track progress.
|
|||
projects.modify = Edit project
|
||||
projects.edit_success = Project "%s" has been updated.
|
||||
projects.type.none = None
|
||||
projects.type.basic_kanban = Basic Kanban
|
||||
projects.type.bug_triage = Bug Triage
|
||||
projects.type.basic_kanban = Basic kanban
|
||||
projects.type.bug_triage = Bug triage
|
||||
projects.template.desc = Template
|
||||
projects.template.desc_helper = Select a project template to get started
|
||||
projects.column.edit = Edit Column
|
||||
projects.column.edit = Edit column
|
||||
projects.column.edit_title = Name
|
||||
projects.column.new_title = Name
|
||||
projects.column.new_submit = Create Column
|
||||
projects.column.new = New Column
|
||||
projects.column.set_default = Set Default
|
||||
projects.column.new_submit = Create column
|
||||
projects.column.new = New column
|
||||
projects.column.set_default = Set default
|
||||
projects.column.set_default_desc = Set this column as default for uncategorized issues and pulls
|
||||
projects.column.delete = Delete Column
|
||||
projects.column.delete = Delete column
|
||||
projects.column.deletion_desc = Deleting a project column moves all related issues to the default column. Continue?
|
||||
projects.column.color = Color
|
||||
projects.open = Open
|
||||
projects.close = Close
|
||||
projects.column.assigned_to = Assigned to
|
||||
projects.card_type.desc = Card Previews
|
||||
projects.card_type.images_and_text = Images and Text
|
||||
projects.card_type.text_only = Text Only
|
||||
projects.card_type.desc = Card previews
|
||||
projects.card_type.images_and_text = Images and text
|
||||
projects.card_type.text_only = Text only
|
||||
|
||||
issues.desc = Organize bug reports, tasks and milestones.
|
||||
issues.filter_assignees = Filter Assignee
|
||||
|
@ -1449,7 +1448,7 @@ issues.filter_reviewers = Filter Reviewer
|
|||
issues.new = New issue
|
||||
issues.new.title_empty = Title cannot be empty
|
||||
issues.new.labels = Labels
|
||||
issues.new.no_label = No label
|
||||
issues.new.no_label = No labels
|
||||
issues.new.clear_labels = Clear labels
|
||||
issues.new.projects = Projects
|
||||
issues.new.clear_projects = Clear projects
|
||||
|
@ -1606,7 +1605,7 @@ issues.re_request_review=Re-request review
|
|||
issues.is_stale = There have been changes to this PR since this review
|
||||
issues.remove_request_review=Remove review request
|
||||
issues.remove_request_review_block=Can't remove review request
|
||||
issues.dismiss_review = Dismiss Review
|
||||
issues.dismiss_review = Dismiss review
|
||||
issues.dismiss_review_warning = Are you sure you want to dismiss this review?
|
||||
issues.sign_in_require_desc = <a href="%s">Sign in</a> to join this conversation.
|
||||
issues.edit = Edit
|
||||
|
@ -1633,7 +1632,8 @@ issues.label.filter_sort.alphabetically = Alphabetically
|
|||
issues.label.filter_sort.reverse_alphabetically = Reverse alphabetically
|
||||
issues.label.filter_sort.by_size = Smallest size
|
||||
issues.label.filter_sort.reverse_by_size = Largest size
|
||||
issues.num_participants = %d participants
|
||||
issues.num_participants_one = %d participant
|
||||
issues.num_participants_few = %d participants
|
||||
issues.attachment.open_tab = `Click to see "%s" in a new tab`
|
||||
issues.attachment.download = `Click to download "%s"`
|
||||
issues.subscribe = Subscribe
|
||||
|
@ -1980,7 +1980,7 @@ signing.wont_sign.commitssigned = The merge will not be signed as all the associ
|
|||
signing.wont_sign.approved = The merge will not be signed as the PR is not approved.
|
||||
signing.wont_sign.not_signed_in = You are not signed in.
|
||||
|
||||
ext_wiki = Access to External Wiki
|
||||
ext_wiki = Access to external Wiki
|
||||
ext_wiki.desc = Link to an external wiki.
|
||||
|
||||
wiki = Wiki
|
||||
|
@ -2345,6 +2345,7 @@ settings.event_pull_request_review_request = Pull request review requested
|
|||
settings.event_pull_request_review_request_desc = Pull request review requested or review request removed.
|
||||
settings.event_pull_request_approvals = Pull request approvals
|
||||
settings.event_pull_request_merge = Pull request merge
|
||||
settings.event_pull_request_enforcement = Enforcement
|
||||
settings.event_package = Package
|
||||
settings.event_package_desc = Package created or deleted in a repository.
|
||||
settings.branch_filter = Branch filter
|
||||
|
@ -2372,9 +2373,9 @@ settings.web_hook_name_dingtalk = DingTalk
|
|||
settings.web_hook_name_telegram = Telegram
|
||||
settings.web_hook_name_matrix = Matrix
|
||||
settings.web_hook_name_msteams = Microsoft Teams
|
||||
settings.web_hook_name_feishu_or_larksuite = Feishu / Lark Suite
|
||||
settings.web_hook_name_feishu = Feishu
|
||||
settings.web_hook_name_larksuite = Lark Suite
|
||||
settings.web_hook_name_feishu = Feishu / Lark Suite
|
||||
settings.web_hook_name_feishu_only = Feishu
|
||||
settings.web_hook_name_larksuite_only = Lark Suite
|
||||
settings.web_hook_name_wechatwork = WeCom (Wechat Work)
|
||||
settings.web_hook_name_packagist = Packagist
|
||||
settings.packagist_username = Packagist username
|
||||
|
@ -2459,6 +2460,8 @@ settings.block_on_official_review_requests = Block merge on official review requ
|
|||
settings.block_on_official_review_requests_desc = Merging will not be possible when it has official review requests, even if there are enough approvals.
|
||||
settings.block_outdated_branch = Block merge if pull request is outdated
|
||||
settings.block_outdated_branch_desc = Merging will not be possible when head branch is behind base branch.
|
||||
settings.enforce_on_admins = Enforce this rule for repository admins
|
||||
settings.enforce_on_admins_desc = Repository admins cannot bypass this rule.
|
||||
settings.default_branch_desc = Select a default repository branch for pull requests and code commits:
|
||||
settings.merge_style_desc = Merge styles
|
||||
settings.default_merge_style_desc = Default merge style
|
||||
|
@ -3180,7 +3183,7 @@ config.repo_root_path = Repository root path
|
|||
config.lfs_root_path = LFS root path
|
||||
config.log_file_root_path = Log path
|
||||
config.script_type = Script type
|
||||
config.reverse_auth_user = Reverse authentication user
|
||||
config.reverse_auth_user = Reverse proxy authentication user
|
||||
|
||||
config.ssh_config = SSH configuration
|
||||
config.ssh_enabled = Enabled
|
||||
|
@ -3416,6 +3419,15 @@ years = %d years
|
|||
raw_seconds = seconds
|
||||
raw_minutes = minutes
|
||||
|
||||
[munits.data]
|
||||
b = B
|
||||
kib = KiB
|
||||
mib = MiB
|
||||
gib = GiB
|
||||
tib = TiB
|
||||
pib = PiB
|
||||
eib = EiB
|
||||
|
||||
[dropzone]
|
||||
default_message = Drop files or click here to upload.
|
||||
invalid_input_type = You cannot upload files of this type.
|
||||
|
@ -3727,3 +3739,8 @@ normal_file = Normal file
|
|||
executable_file = Executable file
|
||||
symbolic_link = Symbolic link
|
||||
submodule = Submodule
|
||||
|
||||
[markup]
|
||||
filepreview.line = Line %[1]d in %[2]s
|
||||
filepreview.lines = Lines %[1]d to %[2]d in %[3]s
|
||||
filepreview.truncated = Preview has been truncated
|
||||
|
|
|
@ -1561,7 +1561,7 @@ issues.label.filter_sort.alphabetically=Alfabéticamente
|
|||
issues.label.filter_sort.reverse_alphabetically=Invertir alfabéticamente
|
||||
issues.label.filter_sort.by_size=Tamaño más pequeño
|
||||
issues.label.filter_sort.reverse_by_size=Tamaño más grande
|
||||
issues.num_participants=%d participantes
|
||||
issues.num_participants_few=%d participantes
|
||||
issues.attachment.open_tab='Haga clic para ver "%s" en una pestaña nueva'
|
||||
issues.attachment.download=`Haga clic para descargar "%s"`
|
||||
issues.subscribe=Suscribir
|
||||
|
@ -2267,9 +2267,9 @@ settings.web_hook_name_dingtalk=DingTalk
|
|||
settings.web_hook_name_telegram=Telegram
|
||||
settings.web_hook_name_matrix=Matrix
|
||||
settings.web_hook_name_msteams=Microsoft Teams
|
||||
settings.web_hook_name_feishu_or_larksuite=Feishu / Lark Suite
|
||||
settings.web_hook_name_feishu=Feishu
|
||||
settings.web_hook_name_larksuite=Lark Suite
|
||||
settings.web_hook_name_feishu=Feishu / Lark Suite
|
||||
settings.web_hook_name_feishu_only =Feishu
|
||||
settings.web_hook_name_larksuite_only =Lark Suite
|
||||
settings.web_hook_name_wechatwork=WeCom (Wechat Work)
|
||||
settings.web_hook_name_packagist=Packagist
|
||||
settings.packagist_username=Nombre de usuario Packagist
|
||||
|
|
|
@ -1189,7 +1189,7 @@ issues.label.filter_sort.alphabetically=الفبایی
|
|||
issues.label.filter_sort.reverse_alphabetically=برعکس ترتیب الفبا
|
||||
issues.label.filter_sort.by_size=کوچکترین اندازه
|
||||
issues.label.filter_sort.reverse_by_size=بزرگترین اندازه
|
||||
issues.num_participants=%d مشارکت کننده
|
||||
issues.num_participants_few=%d مشارکت کننده
|
||||
issues.attachment.open_tab=برای مشاهده "%s" در زبانه جدید، کلیک کنید
|
||||
issues.attachment.download=`برای دریافت "%s" کلیک کنید`
|
||||
issues.subscribe=مشترک شدن
|
||||
|
|
|
@ -920,7 +920,7 @@ issues.label.filter_sort.alphabetically=Aakkosjärjestyksessä
|
|||
issues.label.filter_sort.reverse_alphabetically=Käänteisessä aakkosjärjestyksessä
|
||||
issues.label.filter_sort.by_size=Pienin koko
|
||||
issues.label.filter_sort.reverse_by_size=Suurin koko
|
||||
issues.num_participants=%d osallistujaa
|
||||
issues.num_participants_few=%d osallistujaa
|
||||
issues.subscribe=Tilaa
|
||||
issues.unsubscribe=Lopeta tilaus
|
||||
issues.lock=Lukitse keskustelu
|
||||
|
@ -1184,8 +1184,8 @@ settings.web_hook_name_discord=Discord
|
|||
settings.web_hook_name_dingtalk=DingTalk
|
||||
settings.web_hook_name_telegram=Telegram
|
||||
settings.web_hook_name_matrix=Matrix
|
||||
settings.web_hook_name_feishu=Feishu
|
||||
settings.web_hook_name_larksuite=Lark Suite
|
||||
settings.web_hook_name_feishu_only =Feishu
|
||||
settings.web_hook_name_larksuite_only =Lark Suite
|
||||
settings.web_hook_name_packagist=Packagist
|
||||
settings.deploy_keys=Julkaisuavaimet
|
||||
settings.add_deploy_key=Lisää julkaisuavain
|
||||
|
|
|
@ -7,7 +7,7 @@ language = Wika
|
|||
mirrors = Mga Mirror
|
||||
forks = Mga Fork
|
||||
activities = Mga Aktibidad
|
||||
pull_requests = Mga Pull Request
|
||||
pull_requests = Mga pull pequest
|
||||
issues = Mga Isyu
|
||||
milestones = Mga Milestone
|
||||
ok = OK
|
||||
|
@ -18,10 +18,10 @@ save = I-save
|
|||
add = Magdagdag
|
||||
remove_all = Tanggalin lahat
|
||||
remove_label_str = Tanggalin ang item "%s"
|
||||
edit = I-edit
|
||||
edit = Baguhin
|
||||
enabled = Naka-enable
|
||||
copy = Kopyahin
|
||||
copy_content = Kopyahin ang content
|
||||
copy_content = Kopyahin ang nilalaman
|
||||
copy_branch = Kopyahin ang pangalan ng branch
|
||||
copy_success = Kinopya!
|
||||
copy_error = Nabigo ang pagkopya
|
||||
|
@ -35,11 +35,11 @@ copy_type_unsupported = Hindi makokopya ang itong uri ng file
|
|||
error404 = Ang pahina na sinusubukan mong bisitahin ay alinman <strong>hindi umiiral</strong> o <strong>wala kang pahintulot</strong> para itignan.
|
||||
version = Bersyon
|
||||
powered_by = Pinapatakbo ng %s
|
||||
explore = Mag-explore
|
||||
explore = Tuklasin
|
||||
help = Tulong
|
||||
logo = Logo
|
||||
sign_in = Mag-Sign In
|
||||
sign_in_with_provider = Mag-sign in gamit ng %s
|
||||
sign_in_with_provider = Mag-sign in gamit ang %s
|
||||
sign_in_or = o
|
||||
sign_out = Mag-Sign Out
|
||||
sign_up = Magrehistro
|
||||
|
@ -79,19 +79,19 @@ webauthn_error_empty = Kailangan mong maglapat ng pangalan para sa key na ito.
|
|||
webauthn_reload = I-reload
|
||||
repository = Repository
|
||||
organization = Organisasyon
|
||||
mirror = Mirror
|
||||
mirror = Salamin
|
||||
new_repo = Bagong repository
|
||||
new_migrate = Bagong migration
|
||||
new_mirror = Bagong mirror
|
||||
new_mirror = Bagong salamin
|
||||
new_fork = Bagong repository fork
|
||||
new_org = Bagong organisasyon
|
||||
new_project = Bagong proyekto
|
||||
new_project_column = Bagong column
|
||||
admin_panel = Pangangasiwa ng Site
|
||||
account_settings = Mga Setting ng Account
|
||||
admin_panel = Pangangasiwa ng site
|
||||
account_settings = Mga setting ng Account
|
||||
settings = Mga Setting
|
||||
your_profile = Profile
|
||||
your_starred = Naka-star
|
||||
your_starred = Naka-bitwin
|
||||
your_settings = Mga Setting
|
||||
all = Lahat
|
||||
go_back = Bumalik
|
||||
|
@ -126,10 +126,10 @@ filter.public = Publiko
|
|||
filter.private = Pribado
|
||||
notifications = Mga Abiso
|
||||
active_stopwatch = Aktibong Tagasubaybay ng Oras
|
||||
locked = Naka-lock
|
||||
locked = Naka-kandado
|
||||
preview = I-preview
|
||||
confirm_delete_artifact = Sigurado ka bang gusto mong burahin ang artifact na "%s"?
|
||||
rerun_all = Patakbuhin muli ang lahat ng mga job
|
||||
rerun_all = Patakbuhin muli ang lahat ng mga trabaho
|
||||
add_all = Idagdag lahat
|
||||
copy_hash = Kopyahin ang hash
|
||||
error = Error
|
||||
|
@ -138,6 +138,8 @@ loading = Naglo-load…
|
|||
confirm_delete_selected = Kumpirmahin na burahin ang lahat ng piniling item?
|
||||
home = Panimula
|
||||
dashboard = Dashboard
|
||||
more_items = Higit pang mga item
|
||||
invalid_data = Hindi wastong data: %v
|
||||
|
||||
[home]
|
||||
search_repos = Maghanap ng Repository…
|
||||
|
@ -229,7 +231,7 @@ err_empty_admin_email = Hindi maaring walang laman ang administrator email.
|
|||
err_admin_name_is_reserved = Hindi angkop ang Administrator Username, naka-reserve ang username
|
||||
err_admin_name_is_invalid = Hindi angkop ang Administrator Username
|
||||
general_title = Mga General Setting
|
||||
app_name = Pangalan ng Instansya
|
||||
app_name = Pamagat ng instansya
|
||||
app_name_helper = Maari mong ilagay ang pangalan ng iyong kompanya dito.
|
||||
repo_path_helper = Ang mga remote Git repository ay mase-save sa directory na ito.
|
||||
repo_path = Root path ng Repository
|
||||
|
@ -288,7 +290,7 @@ invalid_db_setting = Hindi angkop ang mga database setting: %v
|
|||
invalid_db_table = Hindi angkop ang database table na "%s": %v
|
||||
invalid_repo_path = Hindi angkop ang repository root path: %v
|
||||
invalid_app_data_path = Hindi angkop ang app data path: %v
|
||||
run_user_not_match = Ang "tumakbo bilang" na username ay hindi ang kasulukuyang username: %s -> %s
|
||||
run_user_not_match = Ang "user na tatakbo bilang" na username ay hindi ang kasulukuyang username: %s -> %s
|
||||
internal_token_failed = Nabigong maka-generate ng internal token: %v
|
||||
secret_key_failed = Nabigong maka-generate ng secret key: %v
|
||||
save_config_failed = Nabigong i-save ang configuration: %v
|
||||
|
@ -352,7 +354,7 @@ app_desc = Isang hindi masakit, at naka self-host na Git service
|
|||
install = Madaling i-install
|
||||
platform = Cross-platform
|
||||
platform_desc = Tumatakbo kahit saan ang Forgejo na ang <a target="_blank" rel="noopener noreferrer" href="https://go.dev/">Go</a> ay nakaka-compile para sa: Windows, macOS, Linux, ARM, atbp. Piliin ang isa na gusto mo!
|
||||
lightweight = Hindi Mabigat
|
||||
lightweight = Magaan
|
||||
lightweight_desc = Mababa ang minimal requirements ng Forgejo at tatakbo sa isang murang Raspberry Pi. Tipirin ang enerhiya ng iyong machine!
|
||||
license = Open Source
|
||||
install_desc = <a target="_blank" rel="noopener noreferrer" href="https://forgejo.org/download/#installation-from-binary">Patakbuhin ang binary</a> para sa iyong platform, i-ship gamit ang <a target="_blank" rel="noopener noreferrer" href="https://forgejo.org/download/#container-image">Docker</a>, o kunin ito nang <a target="_blank" rel="noopener noreferrer" href="https://forgejo.org/download">naka-package</a>.
|
||||
|
@ -921,6 +923,11 @@ oauth2_application_create_description = Ang mga OAuth2 application ay pinapayaga
|
|||
oauth2_application_locked = Ang Forgejo ay pini-pre register ang ibang mga OAuth2 application sa startup kapag naka-enable sa config. Para iwasan ang hindi inaasahang gawain, hindi ito maaring i-edit o tanggalin. Mangyaring sumangguni sa dokumentasyon ng OAuth2 para sa karagdagang impormasyon.
|
||||
remove_account_link_desc = Ang pagtanggal ng naka-link na account ay babawiin ang pag-access nito sa iyong Forgejo account. Magpatuloy?
|
||||
visibility.public_tooltip = Makikita ng lahat
|
||||
hints = Mga Pahiwatig
|
||||
additional_repo_units_hint_description = Mag-display ng "Magdagdag pa ng mga unit..." na button para sa mga repository na hindi naka-enable ang lahat ng mga available na unit.
|
||||
additional_repo_units_hint = Hikayatin ang pag-enable ng karagdagang mga repository unit
|
||||
update_hints = I-update ang mga pahiwatig
|
||||
update_hints_success = Na-update na ang mga pahiwatig.
|
||||
|
||||
[repo]
|
||||
template_description = Ang mga template repository ay pinapayagan ang mga gumagamit na mag-generate ng mga bagong repository na may magkatulad na istraktura ng direktoryo, mga file, at opsyonal na mga setting.
|
||||
|
@ -950,3 +957,103 @@ fork_from = I-fork mula sa
|
|||
already_forked = Na-fork mo na ang %s
|
||||
fork_to_different_account = Mag-fork sa ibang account
|
||||
fork_visibility_helper = Ang visibility ng isang naka-fork na repository ay hindi maaring baguhin.
|
||||
open_with_editor = Buksan gamit ang %s
|
||||
download_bundle = I-download ang BUNDLE
|
||||
repo_gitignore_helper_desc = Piliin kung anong mga file na hindi susubaybayin sa listahan ng mga template para sa mga karaniwang wika. Ang mga tipikal na artifact na ginagawa ng mga build tool ng wika ay kasama sa .gitignore ng default.
|
||||
adopt_preexisting = Mag-adopt ng mga umiiral na file
|
||||
repo_gitignore_helper = Pumili ng mga .gitignore template.
|
||||
readme_helper_desc = Ito ang lugar kung saan makakasulat ka ng kumpletong deskripsyon para sa iyong proyekto.
|
||||
trust_model_helper_collaborator_committer = Katulong+Committer: I-trust ang mga signature batay sa mga katulong na tumutugma sa committer
|
||||
mirror_interval = Interval ng mirror (ang mga wastong unit ng oras ay "h", "m", "s"). 0 para i-disable ang periodic sync. (Pinakamababang interval: %s)
|
||||
transfer.reject_desc = Kanselahin ang pag-transfer mula sa "%s"
|
||||
mirror_lfs_endpoint_desc = Ang sync ay susubukang gamitin ang clone url upang <a target="_blank" rel="noopener noreferrer" href="%s">matukoy ang LFS server</a>. Maari ka rin tumukoy ng isang custom na endpoint kapag ang repository LFS data ay nilalagay sa ibang lugar.
|
||||
adopt_search = Ilagay ang username para maghanap ng mga unadopted repository... (iwanang walang laman para hanapin lahat)
|
||||
object_format = Format ng object
|
||||
readme_helper = Pumili ng README file template.
|
||||
default_branch_helper = Ang default branch ay ang base branch para sa mga pull request at mga commit ng code.
|
||||
mirror_interval_invalid = Hindi wasto ang mirror interval.
|
||||
mirror_sync = na-sync
|
||||
mirror_sync_on_commit = I-sync kapag na-push ang mga commit
|
||||
mirror_address = Mag-clone mula sa URL
|
||||
mirror_address_desc = Maglagay ng anumang mga kinakailangang kredensyal sa Awtorisasyon na seksyon.
|
||||
desc.archived = Naka-archive
|
||||
desc.sha256 = SHA256
|
||||
template.items = Mga template item
|
||||
template.git_content = Nilalaman ng Git (Default na branch)
|
||||
reactions_more = at %d pa
|
||||
unit_disabled = Na-disable ng tagapangasiwa ng site ang itong seksyon ng repository.
|
||||
create_repo = Gumawa ng Repository
|
||||
generate_from = I-generate mula sa
|
||||
repo_desc = Deskripsyon
|
||||
fork_branch = Branch na mako-clone sa fork
|
||||
all_branches = Lahat ng mga branch
|
||||
fork_no_valid_owners = Hindi mapo-fork ang repository dahil walang mga wastong may-ari.
|
||||
use_template = Gamitin ang template na ito
|
||||
download_zip = I-download ang ZIP
|
||||
download_tar = I-download ang TAR.GZ
|
||||
issue_labels = Mga label ng isyu
|
||||
generate_repo = I-generate ang repository
|
||||
repo_desc_helper = Maglagay ng maikling deskripsyon (opsyonal)
|
||||
repo_lang = Wika
|
||||
issue_labels_helper = Pumili ng label set ng isyu.
|
||||
license = Lisensya
|
||||
license_helper = Pumili ng file ng lisensya.
|
||||
license_helper_desc = Ang lisensya ay namamahala kung ano ang pwede at hindi pwedeng gawin ng mga ibang tao sa iyong code. Hindi sigurado kung alin ang wasto para sa iyong proyekto? Tignan ang <a target="_blank" rel="noopener noreferrer" href="%s">Pumili ng lisensya.</a>
|
||||
object_format_helper = Object format ng repository. Hindi mababago mamaya. Ang SHA1 ang pinaka-compatible.
|
||||
readme = README
|
||||
auto_init = I-initialize ang repository (Nagdadagdag ng .gitignore, Lisensya, at README)
|
||||
trust_model_helper = Pumili ng trust model para sa signature verification. Ang mga posibleng opsyon ay:
|
||||
trust_model_helper_collaborator = Katulong: I-trust ang mga signature batay sa mga katulong
|
||||
trust_model_helper_committer = Commiter: I-trust ang mga signature na tumutugma sa mga commiter
|
||||
trust_model_helper_default = Default: Gamitin ang default trust model para sa installation na ito
|
||||
default_branch = Default na branch
|
||||
default_branch_label = default
|
||||
mirror_prune = Pungusan
|
||||
mirror_prune_desc = Tanggalin ang mga antikuwado na sangguni ng remote-tracking
|
||||
mirror_address_url_invalid = Ang ibinigay na url ay hindi wasto. Kailangan mong i-escape ang lahat ng mga components ng URL ng tama.
|
||||
mirror_address_protocol_invalid = Ang ibinigay na URL ay hindi wasto. Ang http(s):// o git:// na lokasyon lamang ay magagamit para sa pag-mirror.
|
||||
mirror_lfs = Large File Storage (LFS)
|
||||
mirror_lfs_desc = I-activate ang pag-mirror ng LFS data.
|
||||
mirror_lfs_endpoint = Endpoint ng LFS
|
||||
mirror_last_synced = Huling na-synchronize
|
||||
mirror_password_placeholder = (Hindi nabago)
|
||||
mirror_password_blank_placeholder = (Hindi tinakda)
|
||||
mirror_password_help = Palitan ang username para burahin ang na-store na password.
|
||||
watchers = Mga nanonood
|
||||
stargazers = Mga Stargazer
|
||||
stars_remove_warning = Tatanggalin nito ang lahat ng mga star sa repository na ito.
|
||||
forks = Mga fork
|
||||
language_other = Iba
|
||||
adopt_preexisting_label = Mag-adopt ng mga file
|
||||
adopt_preexisting_content = Gumawa ng repository mula sa %s
|
||||
transfer.accept = Tanggapin ang paglipat
|
||||
transfer.accept_desc = Ilipat sa "%s"
|
||||
transfer.reject = Tanggihan ang paglipat
|
||||
transfer.no_permission_to_accept = Wala kang pahintulot para tanggapin ang palilipat na ito.
|
||||
transfer.no_permission_to_reject = Wala kang pahintulot para tanggihan ang palilipat na ito.
|
||||
desc.private = Pribado
|
||||
desc.public = Publiko
|
||||
desc.template = Template
|
||||
desc.internal = Internal
|
||||
template.git_hooks = Mga hook ng Git
|
||||
|
||||
[search]
|
||||
commit_kind = Maghanap ng mga commit...
|
||||
keyword_search_unavailable = Kasalukuyang hindi available ang paghahanap sa pamamagitan ng keyword. Mangyaring makipag-ugnayan sa tagapangasiwa ng site.
|
||||
search = Maghanap...
|
||||
type_tooltip = Uri ng paghahanap
|
||||
fuzzy = Fuzzy
|
||||
fuzzy_tooltip = Samahan ang mga resulta na tumutugma rin sa search term nang malapit
|
||||
match = Tugma
|
||||
match_tooltip = Samahan lang ang mga resulta na tumutugma sa eksaktong search term
|
||||
repo_kind = Maghanap ng mga repo...
|
||||
user_kind = Maghanap ng mga user...
|
||||
org_kind = Maghanap ng mga org...
|
||||
team_kind = Maghanap ng mga koponan...
|
||||
code_kind = Maghanap ng code...
|
||||
code_search_unavailable = Kasalukuyang hindi available ang paghahanap ng code. Mangyaring makipag-ugnayan sa tagapangasiwa ng site.
|
||||
package_kind = Maghanap ng mga pakete...
|
||||
project_kind = Maghanap ng mga proyekto...
|
||||
branch_kind = Maghanap ng mga branch...
|
||||
runner_kind = Maghanap ng mga runner...
|
||||
no_results = Walang mga tumutugma na resulta na nahanap.
|
|
@ -156,6 +156,8 @@ filter.private = Privé
|
|||
filter = Filtre
|
||||
filter.is_mirror = Répliqué
|
||||
toggle_menu = Menu va-et-vient
|
||||
more_items = Plus d'éléments
|
||||
invalid_data = Données invalides : %v
|
||||
|
||||
[aria]
|
||||
navbar=Barre de navigation
|
||||
|
@ -982,6 +984,11 @@ blocked_since = Bloqué depuis %s
|
|||
user_unblock_success = Cet utilisateur a été débloqué avec succès.
|
||||
user_block_success = Cet utilisateur a été bloqué avec succès.
|
||||
change_password = Modifier le mot de passe
|
||||
hints = Suggestions
|
||||
additional_repo_units_hint_description = Afficher un bouton "Ajouter plus d'unités..." pour les dépôts qui n'ont pas toutes les unités disponibles activées.
|
||||
additional_repo_units_hint = Encourager l'ajout de nouvelles unités pour le dépôt
|
||||
update_hints = Mettre à jour les suggestions
|
||||
update_hints_success = Les suggestions ont été mises à jour.
|
||||
|
||||
[repo]
|
||||
new_repo_helper=Un dépôt contient tous les fichiers d’un projet, ainsi que l’historique de leurs modifications. Vous avez déjà ça ailleurs ? <a href="%s">Migrez-le ici.</a>
|
||||
|
@ -1224,7 +1231,7 @@ file_raw=Brut
|
|||
file_history=Historique
|
||||
file_view_source=Voir le code source
|
||||
file_view_rendered=Voir le rendu
|
||||
file_view_raw=Voir le Raw
|
||||
file_view_raw=Voir le contenu brut
|
||||
file_permalink=Lien permanent
|
||||
file_too_large=Le fichier est trop gros pour être affiché.
|
||||
invisible_runes_header=`Ce fichier contient des caractères Unicode invisibles.`
|
||||
|
@ -1396,7 +1403,7 @@ projects.column.set_default_desc=Les tickets et demandes d’ajout non-catégori
|
|||
projects.column.unset_default=Défaire par défaut
|
||||
projects.column.unset_default_desc=Les tickets et demandes d'ajouts non-catégorisés seront placés dans une colonne idoine.
|
||||
projects.column.delete=Supprimer la colonne
|
||||
projects.column.deletion_desc=La suppression d'une colonne de projet déplace tous les tickets liés à "Non catégorisé" Continuer ?
|
||||
projects.column.deletion_desc=La suppression d'une colonne de projet déplace tous les tickets liés à la colonne par défaut. Continuer ?
|
||||
projects.column.color=Couleur
|
||||
projects.open=Ouvrir
|
||||
projects.close=Fermer
|
||||
|
@ -1599,7 +1606,7 @@ issues.label.filter_sort.alphabetically=Par ordre alphabétique
|
|||
issues.label.filter_sort.reverse_alphabetically=Par ordre alphabétique inversé
|
||||
issues.label.filter_sort.by_size=Plus petite taille
|
||||
issues.label.filter_sort.reverse_by_size=Plus grande taille
|
||||
issues.num_participants=%d participants
|
||||
issues.num_participants_few=%d participants
|
||||
issues.attachment.open_tab=`Cliquez ici pour voir « %s » dans un nouvel onglet.`
|
||||
issues.attachment.download=`Cliquez pour télécharger « %s ».`
|
||||
issues.subscribe=S’abonner
|
||||
|
@ -2231,7 +2238,7 @@ settings.githook_edit_desc=Si un Hook est inactif, un exemple de contenu vous se
|
|||
settings.githook_name=Nom du hook
|
||||
settings.githook_content=Contenu du Hook
|
||||
settings.update_githook=Mettre le Hook à jour
|
||||
settings.add_webhook_desc=Forgejo enverra à l'URL cible des requêtes <code>POST</code> avec un type de contenu spécifié. Lire la suite dans le <a target="_blank" rel="noopener noreferrer" href="%s">guide des webhooks</a>.
|
||||
settings.add_webhook_desc=Forgejo enverra à l'URL cible des requêtes <code>POST</code> avec le Content-Type spécifié. Lire la suite dans le <a target="_blank" rel="noopener noreferrer" href="%s">guide des webhooks</a>.
|
||||
settings.payload_url=URL cible
|
||||
settings.http_method=Méthode HTTP
|
||||
settings.content_type=Type de contenu POST
|
||||
|
@ -2317,9 +2324,9 @@ settings.web_hook_name_dingtalk=DingTalk
|
|||
settings.web_hook_name_telegram=Telegram
|
||||
settings.web_hook_name_matrix=Matrix
|
||||
settings.web_hook_name_msteams=Microsoft Teams
|
||||
settings.web_hook_name_feishu_or_larksuite=Feishu / Lark Suite
|
||||
settings.web_hook_name_feishu=Feishu
|
||||
settings.web_hook_name_larksuite=Lark Suite
|
||||
settings.web_hook_name_feishu=Feishu / Lark Suite
|
||||
settings.web_hook_name_feishu_only =Feishu
|
||||
settings.web_hook_name_larksuite_only =Lark Suite
|
||||
settings.web_hook_name_wechatwork=WeCom (Wechat Work)
|
||||
settings.web_hook_name_packagist=Packagist
|
||||
settings.packagist_username=Nom d'utilisateur Packagist
|
||||
|
@ -2697,6 +2704,18 @@ settings.confirmation_string = Chaine de confirmation
|
|||
pulls.agit_explanation = Créé par le workflow AGit. AGit permet aux contributeurs de proposer des modifications en utilisant "git push" sans créer une bifurcation ou une nouvelle branche.
|
||||
pulls.merged_title_desc_one = fusionné %[1]d commit depuis <code>%[2]s</code> vers <code>%[3]s</code> %[4]s
|
||||
pulls.title_desc_one = veut fusionner %[1]d commit depuis <code>%[2]s</code> vers <code id="branch_target">%[3]s</code>
|
||||
stars = Étoiles
|
||||
n_tag_few = %s étiquettes
|
||||
editor.commit_id_not_matching = L'ID de la révision ne correspond pas à celui que vous éditiez. Appliquez les modifications à une nouvelle branche puis procédez à la fusion.
|
||||
commits.search_branch = Cette branche
|
||||
open_with_editor = Ouvrir avec %s
|
||||
pulls.ready_for_review = Prêt à être évalué ?
|
||||
n_commit_one = %s commit
|
||||
n_commit_few = %s commits
|
||||
n_branch_one = %s branch
|
||||
n_branch_few = %s branches
|
||||
n_tag_one = %s étiquettes
|
||||
editor.push_out_of_date = Le push semble obsolète.
|
||||
|
||||
[graphs]
|
||||
component_loading=Chargement de %s…
|
||||
|
@ -3754,6 +3773,7 @@ component_failed_to_load = Une erreur inattendue s'est produite.
|
|||
contributors.what = contributions
|
||||
component_loading = Chargement %s...
|
||||
component_loading_failed = Échec de chargement de %s
|
||||
|
||||
code_frequency.what = fŕequence de code
|
||||
recent_commits.what = commits récents
|
||||
|
||||
|
@ -3762,3 +3782,20 @@ recent_commits.what = commits récents
|
|||
search = Rechercher...
|
||||
type_tooltip = Type de recherche
|
||||
fuzzy = Approximatif
|
||||
code_search_by_git_grep = Les résultats de recherche dans le code sont fournis par "git grep". Les résultats pourraient être plus pertinents si l'administrateur du site active les indexeurs de dépôt.
|
||||
runner_kind = Chercher les runners...
|
||||
no_results = Aucun résultat n'a été trouvé.
|
||||
keyword_search_unavailable = La recherche par mot-clé n'est pas disponible actuellement. Veuillez contacter l'administrateur du site.
|
||||
fuzzy_tooltip = Inclure les résultats proches des termes recherchés
|
||||
match = Correspondance
|
||||
match_tooltip = Uniquement inclure les résultats correspondant exactement aux termes recherchés
|
||||
repo_kind = Chercher dans le dépôt...
|
||||
user_kind = Chercher les utilisateurs...
|
||||
org_kind = Chercher les organisations...
|
||||
team_kind = Chercher les équipes...
|
||||
code_kind = Chercher le code...
|
||||
code_search_unavailable = La recherche dans le code n'est pas disponible. Veuillez contacter l'administrateur du site.
|
||||
package_kind = Chercher les paquets...
|
||||
project_kind = Chercher les projets...
|
||||
branch_kind = Chercher les branches...
|
||||
commit_kind = Chercher les commits...
|
|
@ -857,7 +857,7 @@ issues.label.filter_sort.alphabetically=Betűrendben
|
|||
issues.label.filter_sort.reverse_alphabetically=Fordított betűrendben
|
||||
issues.label.filter_sort.by_size=Legkisebb méret
|
||||
issues.label.filter_sort.reverse_by_size=Legnagyobb méret
|
||||
issues.num_participants=%d Résztvevő
|
||||
issues.num_participants_few=%d Résztvevő
|
||||
issues.attachment.open_tab=`A(z) "%s" megnyitása új fülön`
|
||||
issues.attachment.download=`Kattintson a(z) "%s" letöltéséhez`
|
||||
issues.subscribe=Feliratkozás
|
||||
|
|
|
@ -729,7 +729,7 @@ issues.label_edit=Sunting
|
|||
issues.label_delete=Hapus
|
||||
issues.label.filter_sort.alphabetically=Urutan abjad
|
||||
issues.label.filter_sort.reverse_alphabetically=Membalikkan menurut abjad
|
||||
issues.num_participants=%d peserta
|
||||
issues.num_participants_few=%d peserta
|
||||
issues.attachment.open_tab=`Klik untuk melihat "%s" di tab baru`
|
||||
issues.attachment.download=`Klik untuk mengunduh "%s"`
|
||||
issues.subscribe=Berlangganan
|
||||
|
|
|
@ -1065,7 +1065,7 @@ settings.web_hook_name_discord=Discord
|
|||
settings.web_hook_name_dingtalk=DingTalk
|
||||
settings.web_hook_name_telegram=Telegram
|
||||
settings.web_hook_name_msteams=Microsoft Teams
|
||||
settings.web_hook_name_feishu=Feishu
|
||||
settings.web_hook_name_feishu_only =Feishu
|
||||
settings.title=Heiti
|
||||
settings.deploy_key_content=Innihald
|
||||
settings.branches=Greinar
|
||||
|
|
|
@ -1478,7 +1478,7 @@ issues.label.filter_sort.alphabetically=In ordine alfabetico
|
|||
issues.label.filter_sort.reverse_alphabetically=In ordine alfabetico inverso
|
||||
issues.label.filter_sort.by_size=Dimensione più piccola
|
||||
issues.label.filter_sort.reverse_by_size=Dimensione più grande
|
||||
issues.num_participants=%d partecipanti
|
||||
issues.num_participants_few=%d partecipanti
|
||||
issues.attachment.open_tab=`Clicca per vedere "%s" in una nuova scheda`
|
||||
issues.attachment.download=`Clicca qui per scaricare "%s"`
|
||||
issues.subscribe=Iscriviti
|
||||
|
@ -2085,9 +2085,9 @@ settings.web_hook_name_dingtalk=DingTalk
|
|||
settings.web_hook_name_telegram=Telegram
|
||||
settings.web_hook_name_matrix=Matrix
|
||||
settings.web_hook_name_msteams=Microsoft Teams
|
||||
settings.web_hook_name_feishu_or_larksuite=Feishu / Lark Suite
|
||||
settings.web_hook_name_feishu=Feishu
|
||||
settings.web_hook_name_larksuite=Lark Suite
|
||||
settings.web_hook_name_feishu=Feishu / Lark Suite
|
||||
settings.web_hook_name_feishu_only =Feishu
|
||||
settings.web_hook_name_larksuite_only =Lark Suite
|
||||
settings.web_hook_name_wechatwork=WeCom (Wechat Work)
|
||||
settings.web_hook_name_packagist=Packagist
|
||||
settings.packagist_username=Nome utente Packagist
|
||||
|
|
|
@ -55,7 +55,7 @@ repository=リポジトリ
|
|||
organization=組織
|
||||
mirror=ミラー
|
||||
new_repo=新しいリポジトリ
|
||||
new_migrate=新しい移行
|
||||
new_migrate=新しいマイグレーション
|
||||
new_mirror=新しいミラー
|
||||
new_fork=新しいフォーク
|
||||
new_org=新しい組織
|
||||
|
@ -155,6 +155,8 @@ filter.public = 公開
|
|||
filter.private = 非公開
|
||||
toggle_menu = トグルメニュー
|
||||
filter.not_template = テンプレートではない
|
||||
invalid_data = 無効なデータ: %v
|
||||
more_items = さらに表示
|
||||
|
||||
[aria]
|
||||
navbar=ナビゲーションバー
|
||||
|
@ -1587,7 +1589,7 @@ issues.label.filter_sort.alphabetically=アルファベット順
|
|||
issues.label.filter_sort.reverse_alphabetically=逆アルファベット順
|
||||
issues.label.filter_sort.by_size=サイズの小さい順
|
||||
issues.label.filter_sort.reverse_by_size=サイズの大きい順
|
||||
issues.num_participants=%d 人の参加者
|
||||
issues.num_participants_few=%d 人の参加者
|
||||
issues.attachment.open_tab=`クリックして新しいタブで "%s" を見る`
|
||||
issues.attachment.download=`クリックして "%s" をダウンロード`
|
||||
issues.subscribe=購読する
|
||||
|
@ -2301,9 +2303,9 @@ settings.web_hook_name_dingtalk=DingTalk
|
|||
settings.web_hook_name_telegram=Telegram
|
||||
settings.web_hook_name_matrix=Matrix
|
||||
settings.web_hook_name_msteams=Microsoft Teams
|
||||
settings.web_hook_name_feishu_or_larksuite=Feishu / Lark Suite
|
||||
settings.web_hook_name_feishu=Feishu
|
||||
settings.web_hook_name_larksuite=Lark Suite
|
||||
settings.web_hook_name_feishu=Feishu / Lark Suite
|
||||
settings.web_hook_name_feishu_only =Feishu
|
||||
settings.web_hook_name_larksuite_only =Lark Suite
|
||||
settings.web_hook_name_wechatwork=WeCom (Wechat Work)
|
||||
settings.web_hook_name_packagist=Packagist
|
||||
settings.packagist_username=Packagist ユーザー名
|
||||
|
@ -3679,3 +3681,8 @@ executable_file=実行可能ファイル
|
|||
symbolic_link=シンボリックリンク
|
||||
submodule=サブモジュール
|
||||
|
||||
|
||||
|
||||
[search]
|
||||
search = 検索...
|
||||
type_tooltip = 検索タイプ
|
|
@ -782,7 +782,7 @@ issues.label_deletion_desc=라벨을 삭제하면 모든 이슈로부터도 삭
|
|||
issues.label_deletion_success=라벨이 삭제되었습니다.
|
||||
issues.label.filter_sort.alphabetically=알파벳순
|
||||
issues.label.filter_sort.reverse_alphabetically=이름 역순으로 정렬
|
||||
issues.num_participants=참여자 %d명
|
||||
issues.num_participants_few=참여자 %d명
|
||||
issues.attachment.open_tab=`클릭하여 "%s" 새탭으로 보기`
|
||||
issues.attachment.download=' "%s"를 다운로드 하려면 클릭 하십시오 '
|
||||
issues.subscribe=구독하기
|
||||
|
|
|
@ -1540,7 +1540,7 @@ issues.label.filter_sort.alphabetically=Alfabētiski
|
|||
issues.label.filter_sort.reverse_alphabetically=Pretēji alfabētiski
|
||||
issues.label.filter_sort.by_size=Mazākais izmērs
|
||||
issues.label.filter_sort.reverse_by_size=Lielākais izmērs
|
||||
issues.num_participants=%d dalībnieki
|
||||
issues.num_participants_few=%d dalībnieki
|
||||
issues.attachment.open_tab=`Noklikšķiniet, lai apskatītos "%s" jaunā logā`
|
||||
issues.attachment.download=`Noklikšķiniet, lai lejupielādētu "%s"`
|
||||
issues.subscribe=Abonēt
|
||||
|
@ -2253,9 +2253,9 @@ settings.web_hook_name_dingtalk=DingTalk
|
|||
settings.web_hook_name_telegram=Telegram
|
||||
settings.web_hook_name_matrix=Matrix
|
||||
settings.web_hook_name_msteams=Microsoft Teams
|
||||
settings.web_hook_name_feishu_or_larksuite=Feishu / Lark Suite
|
||||
settings.web_hook_name_feishu=Feishu
|
||||
settings.web_hook_name_larksuite=Lark Suite
|
||||
settings.web_hook_name_feishu=Feishu / Lark Suite
|
||||
settings.web_hook_name_feishu_only =Feishu
|
||||
settings.web_hook_name_larksuite_only =Lark Suite
|
||||
settings.web_hook_name_wechatwork=WeCom (Wechat Work)
|
||||
settings.web_hook_name_packagist=Packagist
|
||||
settings.packagist_username=Packagist lietotājvārds
|
||||
|
|
|
@ -155,6 +155,8 @@ filter.public = Publiek
|
|||
filter.private = Privé
|
||||
filter = Filter
|
||||
filter.not_archived = Niet gearchiveerd
|
||||
more_items = Meer items
|
||||
invalid_data = Ongeldige data: %v
|
||||
|
||||
[aria]
|
||||
navbar = Navigatiebalk
|
||||
|
@ -226,7 +228,7 @@ db_schema=Schema
|
|||
db_schema_helper=Laat leeg voor de standaard database ("openbaar").
|
||||
ssl_mode=SSL
|
||||
path=Pad
|
||||
sqlite_helper=Bestandspad voor de SQLite3-database.<br>Vul een volledig pad in als je GItea als een service uitvoert.
|
||||
sqlite_helper=Bestandspad voor de SQLite3-database.<br>Vul een volledig pad in als je Forgejo als een service uitvoert.
|
||||
reinstall_error=U probeert te installeren in een bestaande Forgejo database
|
||||
reinstall_confirm_message=Herinstalleren met een bestaande Forgejo-database kan meerdere problemen veroorzaken. In de meeste gevallen kun je het bestaande "app.ini" gebruiken om Forgejo te laten draaien. Als je weet wat je aan het doen bent, bevestig dan het volgende:
|
||||
reinstall_confirm_check_1=De gegevens versleuteld door de SECRET_KEY in de app.ini kan verloren gaan: gebruikers kunnen mogelijk niet meer inloggen met 2FA/OTP & spiegels werken mogelijk niet meer. Door dit vakje aan te vinken bevestigt u dat het huidige app.ini bestand de juiste SECRET_KEY bevat.
|
||||
|
@ -934,7 +936,7 @@ retype_new_password = Nieuw wachtwoord bevestigen
|
|||
email_desc = Je primaire e-mailadres zal gebruikt worden voor notificaties, wachtwoord herstel en web-gebaseerde Git-operaties, mits het e-mailadres niet verborgen is.
|
||||
can_not_add_email_activations_pending = Er is een activering in gang, probeer het over een paar minuten nogmaals als u een nieuwe e-mail wilt toevoegen.
|
||||
select_permissions = Selecteer machtigingen
|
||||
permission_no_access = Geen Toegang
|
||||
permission_no_access = Geen toegang
|
||||
permissions_list = Machtigingen:
|
||||
update_oauth2_application_success = U heeft met succes een OAuth2 applicatie bijgewerkt.
|
||||
twofa_recovery_tip = Als u uw apparaat verliest, kunt u gebruik maken van de eenmalige herstelcode om weer toegang te krijgen tot uw account.
|
||||
|
@ -952,7 +954,7 @@ unbind_success = De sociale account is succesvol verwijderd.
|
|||
permissions_public_only = Alleen publiek
|
||||
repo_and_org_access = Repository en Organisatie Toegang
|
||||
at_least_one_permission = Je moet minstens één machtiging kiezen om een token te kunnen creëren
|
||||
permission_write = Lees en Schrijf
|
||||
permission_write = Lees en schrijf
|
||||
oauth2_client_secret_hint = Dit geheim zal niet meer worden getoond nadat u deze pagina heeft verlaten of vernieuwd. Zorg ervoor dat u het heeft opgeslagen.
|
||||
revoke_oauth2_grant_success = Toegang succesvol ingetrokken.
|
||||
keep_email_private_popup = Dit zal uw e-mailadres verbergen van uw profielpagina en ook wanneer u een web-gebaseerde Git-operatie uitvoert. Gepushte commits zullen niet aangepast worden. Gebruik %s in commits om deze met uw account te associëren.
|
||||
|
@ -1237,7 +1239,7 @@ editor.name_your_file=Bestandsnaam…
|
|||
editor.filename_help=Voeg een map toe door zijn naam te typen, gevolgd door een slash ("/"). Verwijder een map door op backspace te drukken aan het begin van het tekstveld.
|
||||
editor.or=of
|
||||
editor.cancel_lower=Annuleer
|
||||
editor.commit_signed_changes=Commit Ondertekende Wijzigingen
|
||||
editor.commit_signed_changes=Commit ondertekende wijzigingen
|
||||
editor.commit_changes=Wijzigingen doorvoeren
|
||||
editor.add_tmpl="<bestandsnaam>" toevoegen
|
||||
editor.patch=Patch toepassen
|
||||
|
@ -1312,8 +1314,8 @@ projects.edit=Projecten bewerken
|
|||
projects.edit_subheader=Projecten organiseren issues en houden voortgang bij.
|
||||
projects.modify=Project bewerken
|
||||
projects.type.none=Geen
|
||||
projects.type.basic_kanban=Basis Kanban
|
||||
projects.type.bug_triage=Bug Triage
|
||||
projects.type.basic_kanban=Basis kanban
|
||||
projects.type.bug_triage=Bug triage
|
||||
projects.template.desc=Project sjabloon
|
||||
projects.template.desc_helper=Selecteer een projectsjabloon om aan de slag te gaan
|
||||
projects.type.uncategorized=Ongecategoriseerd
|
||||
|
@ -1482,7 +1484,7 @@ issues.label.filter_sort.alphabetically=Alfabetisch
|
|||
issues.label.filter_sort.reverse_alphabetically=Omgekeerd alfabetisch
|
||||
issues.label.filter_sort.by_size=Kleinste grootte
|
||||
issues.label.filter_sort.reverse_by_size=Grootste grootte
|
||||
issues.num_participants=%d deelnemers
|
||||
issues.num_participants_few=%d deelnemers
|
||||
issues.attachment.open_tab=`Klik om "%s" in een nieuw tabblad te bekijken`
|
||||
issues.attachment.download=`Klik om "%s" te downloaden`
|
||||
issues.subscribe=Abonneren
|
||||
|
@ -1749,7 +1751,7 @@ milestones.filter_sort.most_issues=Meeste problemen
|
|||
milestones.filter_sort.least_issues=Minste problemen
|
||||
|
||||
|
||||
ext_wiki=Toegang tot Externe Wiki
|
||||
ext_wiki=Toegang tot externe wiki
|
||||
ext_wiki.desc=Koppelen aan een externe wiki.
|
||||
|
||||
wiki=Wiki
|
||||
|
@ -2030,9 +2032,9 @@ settings.web_hook_name_dingtalk=DingTalk
|
|||
settings.web_hook_name_telegram=Telegram
|
||||
settings.web_hook_name_matrix=Matrix
|
||||
settings.web_hook_name_msteams=Microsoft Teams
|
||||
settings.web_hook_name_feishu_or_larksuite=Feishu / Lark Suite
|
||||
settings.web_hook_name_feishu=Feishu
|
||||
settings.web_hook_name_larksuite=Lark Suite
|
||||
settings.web_hook_name_feishu=Feishu / Lark Suite
|
||||
settings.web_hook_name_feishu_only =Feishu
|
||||
settings.web_hook_name_larksuite_only =Lark Suite
|
||||
settings.web_hook_name_wechatwork=WeCom (Wechat Work)
|
||||
settings.web_hook_name_packagist=Packagist
|
||||
settings.packagist_username=Packagist gebruikersnaam
|
||||
|
@ -2354,15 +2356,15 @@ commitstatus.failure = Mislukking
|
|||
commitstatus.success = Succes
|
||||
projects.create_success = Het project "%s" is gecreëerd.
|
||||
projects.edit_success = Project "%s" is bijgewerkt.
|
||||
projects.column.edit = Kolom Bewerken
|
||||
projects.column.new_submit = Kolom Maken
|
||||
projects.column.new = Nieuwe Kolom
|
||||
projects.column.set_default = Standaard Instellen
|
||||
projects.column.edit = Kolom bewerken
|
||||
projects.column.new_submit = Kolom maken
|
||||
projects.column.new = Nieuwe kolom
|
||||
projects.column.set_default = Standaard instellen
|
||||
projects.column.unset_default = Standaardinstelling ongedaan maken
|
||||
projects.column.delete = Kolom verwijderen
|
||||
projects.column.assigned_to = Toegewezen aan
|
||||
projects.card_type.images_and_text = Afbeeldingen en Tekst
|
||||
projects.card_type.text_only = Alleen Tekst
|
||||
projects.card_type.images_and_text = Afbeeldingen en tekst
|
||||
projects.card_type.text_only = Alleen tekst
|
||||
issues.choose.ignore_invalid_templates = Ongeldige sjablonen zijn genegeerd
|
||||
issues.choose.invalid_templates = %v ongeldige sjablon(en) gevonden
|
||||
issues.choose.invalid_config = Deze issue configuratie bevat fouten:
|
||||
|
@ -2428,7 +2430,7 @@ editor.invalid_commit_mail = Ongeldige mail voor het aanmaken van een commit.
|
|||
editor.branch_does_not_exist = Branch "%s" bestaat niet in deze repository.
|
||||
editor.directory_is_a_file = Mapnaam "%s" wordt al gebruikt als bestandsnaam in deze repository.
|
||||
commits.renamed_from = Hernoemd van %s
|
||||
projects.card_type.desc = Kaart Voorbeeld
|
||||
projects.card_type.desc = Kaart voorbeeld
|
||||
pulls.filter_changes_by_commit = Filter op commit
|
||||
pulls.nothing_to_compare_have_tag = De geselecteerde branch/tag zijn gelijk.
|
||||
pulls.merged_success = Pull request succesvol samengevoegd en gesloten
|
||||
|
@ -2514,7 +2516,7 @@ settings.admin_stats_indexer = Code statistieken indexer
|
|||
settings.new_owner_blocked_doer = De nieuwe eigenaar heeft u geblokkeerd.
|
||||
settings.transfer_notices_2 = - Je behoudt toegang tot de repository als je het overdraagt aan een organisatie waarvan je (mede-)eigenaar bent.
|
||||
commits.search.tooltip = U kunt zoektermen voorvoegen met "author:", "committer:", "after:", of "before:", bijvoorbeeld: "revert author:Alice before:2019-01-13".
|
||||
projects.column.deletion_desc = Het verwijderen van een projectkolom verplaatst alle issues naar "Ongecategoriseerd". Wilt u doorgaan?
|
||||
projects.column.deletion_desc = Het verwijderen van een projectkolom verplaatst alle gerelateerde problemen naar de standaard kolom. Doorgaan?
|
||||
projects.column.set_default_desc = Stel deze kolom in als standaard voor ongecategoriseerde issues and pulls
|
||||
issues.action_check = Aanvinken/uitvinken
|
||||
issues.dependency.issue_batch_close_blocked = Het is niet mogelijk om de issues die u gekozen heeft in bulk te sluiten, omdat issue #%d nog open afhankelijkheden heeft
|
||||
|
@ -2679,13 +2681,23 @@ pulls.agit_explanation = Gemaakt met behulp van de AGit workflow. AGit laat bijd
|
|||
settings.confirmation_string = Confirmatie string
|
||||
activity.navbar.code_frequency = Code frequentie
|
||||
activity.navbar.recent_commits = Recente commits
|
||||
file_follow = Volg Symlink
|
||||
file_follow = Volg symlink
|
||||
error.broken_git_hook = it hooks van deze repository lijken kapot te zijn. Volg alsjeblieft <a target="_blank" rel="noreferrer" href="%s">de documentatie</a> om ze te repareren, push daarna wat commits om de status te vernieuwen.
|
||||
pulls.title_desc_one = wilt %[1]d commit van <code>%[2]s</code> samenvoegen in <code id="branch_target">%[3]s</code>
|
||||
open_with_editor = Open met %s
|
||||
commits.search_branch = Deze branch
|
||||
pulls.merged_title_desc_one = heeft %[1]d commit van <code>%[2]s</code> samengevoegd in <code>%[3]s</code> %[4]s
|
||||
pulls.ready_for_review = Klaar voor een beoordeling?
|
||||
editor.push_out_of_date = De push lijkt verouderd.
|
||||
editor.commit_id_not_matching = De commit ID komt niet overeen met degene die je aan het bewerken was. Committeer naar een nieuwe branch en voeg dan samen.
|
||||
settings.rename_branch_failed_protected = Kan branch %s niet hernoemen omdat het een beschermde branch is.
|
||||
stars = Sterren
|
||||
n_commit_few = %s commits
|
||||
n_branch_one = %s branch
|
||||
n_branch_few = %s branches
|
||||
n_tag_one = %s tag
|
||||
n_tag_few = %s tags
|
||||
n_commit_one = %d commit
|
||||
|
||||
|
||||
|
||||
|
@ -3072,7 +3084,7 @@ config.repo_root_path=Repository basis pad
|
|||
config.lfs_root_path=LFS rootpad
|
||||
config.log_file_root_path=Log-pad
|
||||
config.script_type=Script soort
|
||||
config.reverse_auth_user=Omgekeerde verificatie gebruiker
|
||||
config.reverse_auth_user=Reverse proxy verificatie gebruiker
|
||||
|
||||
config.ssh_config=SSH-configuratie
|
||||
config.ssh_enabled=Ingeschakeld
|
||||
|
@ -3318,7 +3330,7 @@ auths.skip_local_two_fa_helper = Niet ingesteld betekent dat lokale gebruikers m
|
|||
auths.skip_local_two_fa = Lokale 2FA overslaan
|
||||
auths.oauth2_icon_url = Pictogram URL
|
||||
auths.pam_email_domain = PAM e-maildomein (optioneel)
|
||||
auths.tip.gitea = Registreer een nieuwe OAuth2-toepassing. De handleiding is te vinden op https://docs.gitea.com/development/oauth2-provider
|
||||
auths.tip.gitea = Registreer een nieuwe OAuth2-toepassing. De handleiding is te vinden op https://forgejo.org/docs/latest/user/oauth2-provider
|
||||
auths.tip.discord = Registreer een nieuwe toepassing op https://discordapp.com/developers/applications/me
|
||||
auths.tip.bitbucket = Registreer een nieuwe OAuth consumer op https://bitbucket.org/account/user/<uw gebruikersnaam>/oauth-consumers/new en voeg de rechten "Account" - "Read"
|
||||
auths.tips.oauth2.general.tip = Bij het registreren van een nieuwe OAuth2-authenticatie moet de callback/redirect URL zijn:
|
||||
|
@ -3356,6 +3368,7 @@ config_settings = Instellingen
|
|||
auths.tips.gmail_settings = Gmail instellingen:
|
||||
config_summary = Samenvatting
|
||||
config.open_with_editor_app_help = De "Openen met" editors voor het kloonmenu. Als deze leeg blijft, wordt de standaardwaarde gebruikt. Uitvouwen om de standaard te zien.
|
||||
auths.tip.gitlab_new = Registreer een nieuwe applicatie op https://gitlab.com/-/profile/applications
|
||||
|
||||
|
||||
[action]
|
||||
|
@ -3704,6 +3717,7 @@ variables.description = Variabelen worden doorgegeven aan bepaalde acties en kun
|
|||
runners.delete_runner_success = Runner succesvol verwijderd
|
||||
runs.no_matching_online_runner_helper = Geen overeenkomende online runner met label: %s
|
||||
runs.workflow = Workflow
|
||||
runs.no_job_without_needs = De workflow moet ten minste één taak zonder afhankelijkheden bevatten.
|
||||
|
||||
|
||||
|
||||
|
@ -3754,3 +3768,4 @@ type_tooltip = Zoektype
|
|||
fuzzy_tooltip = Neem resultaten op die ook sterk overeenkomen met de zoekterm
|
||||
code_search_unavailable = Code zoeken is momenteel niet beschikbaar. Neem contact op met de sitebeheerder.
|
||||
keyword_search_unavailable = Zoeken op trefwoord is momenteel niet beschikbaar. Neem contact op met de beheerder van de site.
|
||||
code_search_by_git_grep = Huidige code zoekresultaten worden geleverd door "git grep". Er kunnen betere resultaten zijn als de sitebeheerder Repository Indexer inschakelt.
|
|
@ -1179,7 +1179,7 @@ issues.label.filter_sort.alphabetically=Alfabetycznie
|
|||
issues.label.filter_sort.reverse_alphabetically=Alfabetycznie odwrotnie
|
||||
issues.label.filter_sort.by_size=Najmniejszy rozmiar
|
||||
issues.label.filter_sort.reverse_by_size=Największy rozmiar
|
||||
issues.num_participants=Uczestnicy %d
|
||||
issues.num_participants_few=Uczestnicy %d
|
||||
issues.attachment.open_tab=`Kliknij, aby zobaczyć "%s" w nowej karcie`
|
||||
issues.attachment.download=`Kliknij, aby pobrać "%s"`
|
||||
issues.subscribe=Subskrybuj
|
||||
|
|
|
@ -1552,7 +1552,7 @@ issues.label.filter_sort.alphabetically=Alfabeticamente
|
|||
issues.label.filter_sort.reverse_alphabetically=Alfabeticamente inverso
|
||||
issues.label.filter_sort.by_size=Menor tamanho
|
||||
issues.label.filter_sort.reverse_by_size=Maior tamanho
|
||||
issues.num_participants=%d participante(s)
|
||||
issues.num_participants_few=%d participante(s)
|
||||
issues.attachment.open_tab=`Clique para ver "%s" em uma nova aba`
|
||||
issues.attachment.download=`Clique para baixar "%s"`
|
||||
issues.subscribe=Inscrever-se
|
||||
|
@ -2236,9 +2236,9 @@ settings.web_hook_name_dingtalk=DingTalk
|
|||
settings.web_hook_name_telegram=Telegram
|
||||
settings.web_hook_name_matrix=Matrix
|
||||
settings.web_hook_name_msteams=Microsoft Teams
|
||||
settings.web_hook_name_feishu_or_larksuite=Feishu / Lark Suite
|
||||
settings.web_hook_name_feishu=Feishu
|
||||
settings.web_hook_name_larksuite=Lark Suite
|
||||
settings.web_hook_name_feishu=Feishu / Lark Suite
|
||||
settings.web_hook_name_feishu_only =Feishu
|
||||
settings.web_hook_name_larksuite_only =Lark Suite
|
||||
settings.web_hook_name_wechatwork=WeCom (Wechat Work)
|
||||
settings.web_hook_name_packagist=Packagist
|
||||
settings.packagist_username=Nome de usuário no Packagist
|
||||
|
|
|
@ -1548,7 +1548,7 @@ issues.label.filter_sort.alphabetically=por ordem alfabética
|
|||
issues.label.filter_sort.reverse_alphabetically=por ordem alfabética inversa
|
||||
issues.label.filter_sort.by_size=Menor tamanho
|
||||
issues.label.filter_sort.reverse_by_size=Maior tamanho
|
||||
issues.num_participants=%d Participantes
|
||||
issues.num_participants_few=%d Participantes
|
||||
issues.attachment.open_tab=`Clique para ver "%s" num separador novo`
|
||||
issues.attachment.download=`Clique para descarregar "%s"`
|
||||
issues.subscribe=Subscrever
|
||||
|
@ -2269,9 +2269,9 @@ settings.web_hook_name_dingtalk=DingTalk
|
|||
settings.web_hook_name_telegram=Telegram
|
||||
settings.web_hook_name_matrix=Matrix
|
||||
settings.web_hook_name_msteams=Microsoft Teams
|
||||
settings.web_hook_name_feishu_or_larksuite=Feishu / Lark Suite
|
||||
settings.web_hook_name_feishu=Feishu
|
||||
settings.web_hook_name_larksuite=Lark Suite
|
||||
settings.web_hook_name_feishu=Feishu / Lark Suite
|
||||
settings.web_hook_name_feishu_only =Feishu
|
||||
settings.web_hook_name_larksuite_only =Lark Suite
|
||||
settings.web_hook_name_wechatwork=WeCom (Wechat Work)
|
||||
settings.web_hook_name_packagist=Packagist
|
||||
settings.packagist_username=Nome de utilizador no Packagist
|
||||
|
|
|
@ -155,6 +155,8 @@ filter.public = Публичные
|
|||
filter.private = Приватные
|
||||
filter.is_archived = Архивированные
|
||||
filter.not_mirror = Не зеркала
|
||||
more_items = Больше элементов
|
||||
invalid_data = Неверные данные: %v
|
||||
|
||||
[aria]
|
||||
navbar=Панель навигации
|
||||
|
@ -251,16 +253,16 @@ run_user=Запуск от имени пользователя
|
|||
run_user_helper=Имя пользователя операционной системы, под которым работает Forgejo. Обратите внимание, что этот пользователь должен иметь доступ к корневому пути репозиториев.
|
||||
domain=Домен сервера
|
||||
domain_helper=Домен или адрес хоста для сервера.
|
||||
ssh_port=Порт SSH сервера
|
||||
ssh_port_helper=Номер порта, который использует SSH сервер. Оставьте пустым, чтобы отключить SSH.
|
||||
http_port=Forgejo HTTP порт
|
||||
http_port_helper=Номер порта, который будет прослушиваться Forgejo веб-сервером.
|
||||
ssh_port=Порт SSH-сервера
|
||||
ssh_port_helper=Номер порта, используемый SSH-сервером. Оставьте пустым для отключения доступа по SSH.
|
||||
http_port=Порт HTTP-сервера
|
||||
http_port_helper=Номер порта, используемый веб-сервером Forgejo.
|
||||
app_url=Базовый URL Forgejo
|
||||
app_url_helper=Этот параметр влияет на URL для клонирования по HTTP/HTTPS и на некоторые уведомления по эл. почте.
|
||||
log_root_path=Путь журналов
|
||||
log_root_path_helper=Файлы журнала будут записываться в этот каталог.
|
||||
|
||||
optional_title=Расширенные настройки
|
||||
optional_title=Дополнительные настройки
|
||||
email_title=Настройки эл. почты
|
||||
smtp_addr=Адрес SMTP
|
||||
smtp_port=Порт SMTP
|
||||
|
@ -991,6 +993,7 @@ owner_helper=Некоторые организации могут не отоб
|
|||
repo_name=Название репозитория
|
||||
repo_name_helper=Лучшие названия репозиториев состоят из коротких, легко запоминаемых и уникальных ключевых слов.
|
||||
repo_size=Размер репозитория
|
||||
size_format = `%[1]s: %[2]s; %[3]s: %[4]s`
|
||||
template=Шаблон
|
||||
template_select=Выбрать шаблон.
|
||||
template_helper=Сделать репозиторий шаблоном
|
||||
|
@ -1142,7 +1145,7 @@ migrate.migrating_failed_no_addr=Перенос не удался.
|
|||
migrate.github.description=Перенесите данные с github.com или сервера GitHub Enterprise.
|
||||
migrate.git.description=Перенести только репозиторий из любого Git сервиса.
|
||||
migrate.gitlab.description=Перенести данные с gitlab.com или других серверов GitLab.
|
||||
migrate.gitea.description=Перенести данные с gitea.com или других серверов Gitea/Forgejo.
|
||||
migrate.gitea.description=Перенести данные с gitea.com или других серверов Gitea.
|
||||
migrate.gogs.description=Перенести данные с notabug.org или других серверов Gogs.
|
||||
migrate.onedev.description=Перенести данные с code.onedev.io или других серверов OneDev.
|
||||
migrate.codebase.description=Перенос данных с codebasehq.com.
|
||||
|
@ -1232,7 +1235,7 @@ symbolic_link=Символическая ссылка
|
|||
executable_file=Исполняемый файл
|
||||
commit_graph=Граф коммитов
|
||||
commit_graph.select=Выбрать ветку
|
||||
commit_graph.hide_pr_refs=Скрыть запросы на слияние
|
||||
commit_graph.hide_pr_refs=Скрыть запросы слияний
|
||||
commit_graph.monochrome=Моно
|
||||
commit_graph.color=Цвет
|
||||
commit.contained_in=Этот коммит содержится в:
|
||||
|
@ -1344,7 +1347,7 @@ commitstatus.failure=Неудача
|
|||
commitstatus.pending=Ожидание
|
||||
commitstatus.success=Успешно
|
||||
|
||||
ext_issues=Доступ к внешним задачам
|
||||
ext_issues=Доступ ко внешним задачам
|
||||
ext_issues.desc=Ссылка на внешнюю систему отслеживания задач.
|
||||
|
||||
projects=Проекты
|
||||
|
@ -1359,7 +1362,7 @@ projects.create_success=Проект «%s» создан.
|
|||
projects.deletion=Удалить проект
|
||||
projects.deletion_desc=Удаление проекта приведёт к его удалению из всех связанных задач. Продолжить?
|
||||
projects.deletion_success=Проект удалён.
|
||||
projects.edit=Редактировать проекты
|
||||
projects.edit=Изменить проект
|
||||
projects.edit_subheader=Создавайте и организуйте задачи и отслеживайте прогресс.
|
||||
projects.modify=Обновить проект
|
||||
projects.edit_success=Проект «%s» обновлён.
|
||||
|
@ -1372,14 +1375,14 @@ projects.type.uncategorized=Без категории
|
|||
projects.column.edit=Изменить столбец
|
||||
projects.column.edit_title=Название
|
||||
projects.column.new_title=Название
|
||||
projects.column.new_submit=Создать столбец
|
||||
projects.column.new=Новый столбец
|
||||
projects.column.new_submit=Добавить столбец
|
||||
projects.column.new=Добавить столбец
|
||||
projects.column.set_default=Установить по умолчанию
|
||||
projects.column.set_default_desc=Назначить этот столбец по умолчанию для задач и запросов на слияние без категории
|
||||
projects.column.unset_default=Снять установку по умолчанию
|
||||
projects.column.unset_default_desc=Снять установку этого столбца по умолчанию
|
||||
projects.column.delete=Удалить столбец
|
||||
projects.column.deletion_desc=При удалении столбца проекта все связанные задачи перемещаются в «Без категории». Продолжить?
|
||||
projects.column.deletion_desc=При удалении столбца все задачи в нём будут перемещены в столбец по умолчанию. Продолжить?
|
||||
projects.column.color=Цвет
|
||||
projects.open=Открыть
|
||||
projects.close=Закрыть
|
||||
|
@ -1581,7 +1584,8 @@ issues.label.filter_sort.alphabetically=По алфавиту
|
|||
issues.label.filter_sort.reverse_alphabetically=С конца алфавита
|
||||
issues.label.filter_sort.by_size=Меньший размер
|
||||
issues.label.filter_sort.reverse_by_size=Больший размер
|
||||
issues.num_participants=%d участвующих
|
||||
issues.num_participants_one=%d участник
|
||||
issues.num_participants_few=%d участников
|
||||
issues.attachment.open_tab=`Нажмите, чтобы увидеть «%s» в новой вкладке`
|
||||
issues.attachment.download=`Нажмите, чтобы скачать «%s»`
|
||||
issues.subscribe=Подписаться
|
||||
|
@ -1901,7 +1905,7 @@ signing.wont_sign.commitssigned=Слияние не будет подписан
|
|||
signing.wont_sign.approved=Слияние не будет подписано, так как запрос на слияние не одобрен.
|
||||
signing.wont_sign.not_signed_in=Вы не вошли в систему.
|
||||
|
||||
ext_wiki=Доступ к внешней вики
|
||||
ext_wiki=Доступ ко внешней вики
|
||||
ext_wiki.desc=Ссылка на внешнюю вики.
|
||||
|
||||
wiki=Вики
|
||||
|
@ -2276,9 +2280,9 @@ settings.web_hook_name_dingtalk=DingTalk
|
|||
settings.web_hook_name_telegram=Telegram
|
||||
settings.web_hook_name_matrix=Matrix
|
||||
settings.web_hook_name_msteams=Microsoft Teams
|
||||
settings.web_hook_name_feishu_or_larksuite=Feishu или Lark Suite
|
||||
settings.web_hook_name_feishu=Feishu
|
||||
settings.web_hook_name_larksuite=Lark Suite
|
||||
settings.web_hook_name_feishu=Feishu или Lark Suite
|
||||
settings.web_hook_name_feishu_only =Feishu
|
||||
settings.web_hook_name_larksuite_only =Lark Suite
|
||||
settings.web_hook_name_wechatwork=WeCom (Wechat Work)
|
||||
settings.web_hook_name_packagist=Packagist
|
||||
settings.packagist_username=Имя пользователя Packagist
|
||||
|
@ -2653,7 +2657,7 @@ signing.wont_sign.nokey = Нет ключей для подписи этого
|
|||
settings.wiki_globally_editable = Разрешить редактирование Вики всем пользователям
|
||||
settings.webhook.test_delivery_desc_disabled = Активируйте этот веб-хук для проверки тестовым событием.
|
||||
commits.browse_further = Смотреть далее
|
||||
vendored = Предоставленный
|
||||
vendored = Сторонний
|
||||
settings.units.add_more = Доб. больше...
|
||||
pulls.fast_forward_only_merge_pull_request = Только fast-forward
|
||||
settings.units.overview = Обзор
|
||||
|
@ -2685,6 +2689,16 @@ settings.mirror_settings.docs.doc_link_pull_section = раздел докуме
|
|||
wiki.original_git_entry_tooltip = Перейти по настоящему пути вместо читабельной ссылки.
|
||||
open_with_editor = Открыть в %s
|
||||
commits.search_branch = В этой ветке
|
||||
stars = Добавившие в избранное
|
||||
n_tag_one = %s тег
|
||||
n_branch_few = %s веток
|
||||
n_commit_few = %s коммитов
|
||||
n_commit_one = %s коммит
|
||||
n_tag_few = %s тегов
|
||||
n_branch_one = %s ветка
|
||||
pulls.ready_for_review = Готово к рецензии?
|
||||
editor.commit_id_not_matching = ID коммита не совпадает с тем, который вы редактировали. Сохраните изменения в новую ветку и выполните слияние.
|
||||
editor.push_out_of_date = Похоже, отправка устарела.
|
||||
|
||||
[graphs]
|
||||
|
||||
|
@ -2940,10 +2954,10 @@ users.prohibit_login=Запретить вход в учётную запись
|
|||
users.is_admin=У этой учётной записи есть права администратора
|
||||
users.is_restricted=Ограничен
|
||||
users.allow_git_hook=Может создавать Git-хуки
|
||||
users.allow_git_hook_tooltip=Git Hooks выполняется как пользователь ОС с Forgejo и будет иметь одинаковый уровень доступа к хосту. В результате пользователи с привилегией Git Hook могут получить доступ и модифицировать все репозитории Forgejo, а также базу данных, используемую Forgejo. Следовательно, они также могут получить привилегии администратора Forgejo.
|
||||
users.allow_import_local=Пользователь имеет право импортировать локальные репозитории
|
||||
users.allow_create_organization=Эта учётная запись имеет разрешения на создание организаций
|
||||
users.update_profile=Обновить профиль пользователя
|
||||
users.allow_git_hook_tooltip=Git hooks выполняются от пользователя ОС, под которым работает Forgejo. Они будут иметь такой же доступ к хосту. Из-за этого пользователи с правами на Git hook будут иметь возможность получать доступ и модифицировать все репозитории в Forgejo, а также базу данных Forgejo. Следовательно, они также могут получить права администратора Forgejo.
|
||||
users.allow_import_local=Может импортировать локальные репозитории
|
||||
users.allow_create_organization=Может создавать организации
|
||||
users.update_profile=Обновить учётную запись
|
||||
users.delete_account=Удалить эту учётную запись
|
||||
users.cannot_delete_self=Вы не можете удалить свою учётную запись
|
||||
users.still_own_repo=Этот пользователь всё ещё является владельцем одного или более репозиториев. Сначала удалите или передайте эти репозитории.
|
||||
|
@ -2955,16 +2969,16 @@ users.deletion_success=Учётная запись успешно удалена
|
|||
users.reset_2fa=Сброс 2FA
|
||||
users.list_status_filter.menu_text=Фильтр
|
||||
users.list_status_filter.reset=Сбросить
|
||||
users.list_status_filter.is_active=Активный
|
||||
users.list_status_filter.not_active=Неактивный
|
||||
users.list_status_filter.is_admin=Администратор
|
||||
users.list_status_filter.not_admin=Не администратор
|
||||
users.list_status_filter.is_restricted=Ограничено
|
||||
users.list_status_filter.not_restricted=Не ограничено
|
||||
users.list_status_filter.is_prohibit_login=Запретить вход
|
||||
users.list_status_filter.not_prohibit_login=Разрешить вход
|
||||
users.list_status_filter.is_2fa_enabled=2FA включено
|
||||
users.list_status_filter.not_2fa_enabled=2FA отключено
|
||||
users.list_status_filter.is_active=Активные
|
||||
users.list_status_filter.not_active=Неактивные
|
||||
users.list_status_filter.is_admin=Администраторы
|
||||
users.list_status_filter.not_admin=Не администраторы
|
||||
users.list_status_filter.is_restricted=Ограниченные
|
||||
users.list_status_filter.not_restricted=Не ограниченные
|
||||
users.list_status_filter.is_prohibit_login=Вход запрещён
|
||||
users.list_status_filter.not_prohibit_login=Вход разрешён
|
||||
users.list_status_filter.is_2fa_enabled=2FA включена
|
||||
users.list_status_filter.not_2fa_enabled=2FA выключена
|
||||
users.details=О пользователе
|
||||
|
||||
emails.email_manage_panel=Управление адресами эл. почты пользователей
|
||||
|
@ -3155,7 +3169,7 @@ config.repo_root_path=Путь до каталога репозиториев
|
|||
config.lfs_root_path=Корневой путь LFS
|
||||
config.log_file_root_path=Путь журналов
|
||||
config.script_type=Тип сценария
|
||||
config.reverse_auth_user=Имя пользователя для авторизации на reverse proxy
|
||||
config.reverse_auth_user=Пользователь для авторизации на обратном прокси
|
||||
|
||||
config.ssh_config=Конфигурация SSH
|
||||
config.ssh_enabled=SSH включён
|
||||
|
@ -3329,7 +3343,7 @@ notices.desc=Описание
|
|||
notices.op=Oп.
|
||||
notices.delete_success=Уведомления системы были удалены.
|
||||
self_check.no_problem_found = Пока проблем не обнаружено.
|
||||
auths.tip.gitea = Зарегистрируйте новое приложение OAuth2. Доступна инструкция: https://docs.gitea.com/development/oauth2-provider
|
||||
auths.tip.gitea = Зарегистрируйте новое приложение OAuth2. Доступна инструкция: https://forgejo.org/docs/latest/user/oauth2-provider
|
||||
auths.tips.oauth2.general.tip = При регистрации нового приложения OAuth2 ссылка обратного перенаправления должна быть:
|
||||
self_check.database_fix_mssql = В настоящий момент пользователи MSSQL могут исправить проблемы с сопоставлением только ручным прописыванием "ALTER ... COLLATE ..." в SQL.
|
||||
self_check.database_fix_mysql = Пользователи MySQL и MariaDB могут исправить проблемы с сопоставлением командой "gitea doctor convert". Также можно вручную вписать "ALTER ... COLLATE ..." в SQL.
|
||||
|
@ -3352,6 +3366,7 @@ config_summary = Сводка
|
|||
config.open_with_editor_app_help = Приложения для "Открыть в" в меню. Оставьте пустым для приложений по умолчанию. Разверните для просмотра.
|
||||
config_settings = Настройки
|
||||
auths.tips.gmail_settings = Настройки Gmail:
|
||||
auths.tip.gitlab_new = Создайте новое приложение в https://gitlab.com/-/profile/applications
|
||||
|
||||
|
||||
[action]
|
||||
|
@ -3407,6 +3422,15 @@ years=%d лет
|
|||
raw_seconds=секунд
|
||||
raw_minutes=минут
|
||||
|
||||
[munits.data]
|
||||
b = Б
|
||||
kib = КиБ
|
||||
mib = МиБ
|
||||
gib = ГиБ
|
||||
tib = ТиБ
|
||||
pib = ПиБ
|
||||
eib = ЕиБ
|
||||
|
||||
[dropzone]
|
||||
default_message=Перетащите файл или кликните сюда для загрузки.
|
||||
invalid_input_type=Вы не можете загружать файлы этого типа.
|
||||
|
@ -3596,6 +3620,7 @@ rpm.repository = О репозитории
|
|||
rpm.repository.architectures = Архитектуры
|
||||
rpm.repository.multiple_groups = Этот пакет доступен в нескольких группах.
|
||||
owner.settings.chef.keypair.description = Для аутентификации реестра Chef необходима пара ключей. Если до этого вы уже сгенерировали пару ключей, генерация новой приведёт к прекращению действия предыдущей.
|
||||
owner.settings.cargo.rebuild.no_index = Невозможно выполнить пересборку. Нет инициализированного индекса.
|
||||
|
||||
[secrets]
|
||||
secrets=Секреты
|
||||
|
@ -3748,3 +3773,5 @@ no_results = По запросу ничего не найдено.
|
|||
keyword_search_unavailable = Поиск по ключевым словам недоступен. Уточните подробности у администратора.
|
||||
match_tooltip = Включать только результаты, точно соответствующие запросу
|
||||
code_search_unavailable = Поиск по коду сейчас недоступен. Уточните подробности у администратора.
|
||||
runner_kind = Поиск раннеров...
|
||||
code_search_by_git_grep = Эти результаты получены через «git grep». Результатов может быть больше, если администратор сервера включит индексатор кода.
|
||||
|
|
|
@ -1132,7 +1132,7 @@ issues.label.filter_sort.alphabetically=අකාරාදී
|
|||
issues.label.filter_sort.reverse_alphabetically=අකාරාදී ප්රතිවිකුණුම්
|
||||
issues.label.filter_sort.by_size=කුඩාම ප්රමාණය
|
||||
issues.label.filter_sort.reverse_by_size=විශාලම ප්රමාණය
|
||||
issues.num_participants=සහභාගිවන්නන් %d
|
||||
issues.num_participants_few=සහභාගිවන්නන් %d
|
||||
issues.attachment.open_tab=`නව වගුවක "%s" බැලීමට ක්ලික් කරන්න`
|
||||
issues.attachment.download=`"%s" බාගැනීමට ඔබන්න`
|
||||
issues.subscribe=දායක වන්න
|
||||
|
|
|
@ -972,7 +972,7 @@ issues.label.filter_sort.alphabetically=Alfabetiskt A-Ö
|
|||
issues.label.filter_sort.reverse_alphabetically=Alfabetiskt Ö-A
|
||||
issues.label.filter_sort.by_size=Minsta storlek
|
||||
issues.label.filter_sort.reverse_by_size=Största storlek
|
||||
issues.num_participants=%d Deltagare
|
||||
issues.num_participants_few=%d Deltagare
|
||||
issues.attachment.open_tab=`Klicka för att se "%s" i en ny flik`
|
||||
issues.attachment.download=`Klicka för att hämta "%s"`
|
||||
issues.subscribe=Prenumerera
|
||||
|
|
|
@ -1539,7 +1539,7 @@ issues.label.filter_sort.alphabetically=Alfabetik
|
|||
issues.label.filter_sort.reverse_alphabetically=Ters alfabetik
|
||||
issues.label.filter_sort.by_size=En küçük boyut
|
||||
issues.label.filter_sort.reverse_by_size=En büyük boyut
|
||||
issues.num_participants=%d Katılımcı
|
||||
issues.num_participants_few=%d Katılımcı
|
||||
issues.attachment.open_tab=`Yeni bir sekmede "%s" görmek için tıkla`
|
||||
issues.attachment.download=`"%s" indirmek için tıkla`
|
||||
issues.subscribe=Abone Ol
|
||||
|
@ -2252,9 +2252,9 @@ settings.web_hook_name_dingtalk=DingTalk
|
|||
settings.web_hook_name_telegram=Telegram
|
||||
settings.web_hook_name_matrix=Matrix
|
||||
settings.web_hook_name_msteams=Microsoft Teams
|
||||
settings.web_hook_name_feishu_or_larksuite=Feishu / Lark Suite
|
||||
settings.web_hook_name_feishu=Feishu
|
||||
settings.web_hook_name_larksuite=Lark Suite
|
||||
settings.web_hook_name_feishu=Feishu / Lark Suite
|
||||
settings.web_hook_name_feishu_only =Feishu
|
||||
settings.web_hook_name_larksuite_only =Lark Suite
|
||||
settings.web_hook_name_wechatwork=WeCom (Wechat Work)
|
||||
settings.web_hook_name_packagist=Packagist
|
||||
settings.packagist_username=Packagist kullanıcı adı
|
||||
|
|
|
@ -1248,7 +1248,7 @@ issues.label.filter_sort.alphabetically=За алфавітом
|
|||
issues.label.filter_sort.reverse_alphabetically=З кінця алфавіту
|
||||
issues.label.filter_sort.by_size=Найменший розмір
|
||||
issues.label.filter_sort.reverse_by_size=Найбільший розмір
|
||||
issues.num_participants=%d учасників
|
||||
issues.num_participants_few=%d учасників
|
||||
issues.attachment.open_tab=`Натисніть щоб побачити "%s" у новій вкладці`
|
||||
issues.attachment.download=`Натисніть щоб завантажити "%s"`
|
||||
issues.subscribe=Підписатися
|
||||
|
|
|
@ -155,6 +155,8 @@ filter.not_template = 非模板
|
|||
filter.public = 公开
|
||||
filter.private = 私有
|
||||
toggle_menu = 菜单
|
||||
invalid_data = 无效数据: %v
|
||||
more_items = 显示更多
|
||||
|
||||
[aria]
|
||||
navbar=导航栏
|
||||
|
@ -1597,7 +1599,7 @@ issues.label.filter_sort.alphabetically=按字母顺序排序
|
|||
issues.label.filter_sort.reverse_alphabetically=按字母逆序排序
|
||||
issues.label.filter_sort.by_size=最小尺寸
|
||||
issues.label.filter_sort.reverse_by_size=最大尺寸
|
||||
issues.num_participants=%d 名参与者
|
||||
issues.num_participants_few=%d 名参与者
|
||||
issues.attachment.open_tab=`在新的标签页中查看 '%s'`
|
||||
issues.attachment.download=`点击下载 '%s'`
|
||||
issues.subscribe=订阅
|
||||
|
@ -2316,9 +2318,9 @@ settings.web_hook_name_dingtalk=钉钉
|
|||
settings.web_hook_name_telegram=Telegram
|
||||
settings.web_hook_name_matrix=Matrix
|
||||
settings.web_hook_name_msteams=Microsoft Teams
|
||||
settings.web_hook_name_feishu_or_larksuite=飞书 / Lark Suite
|
||||
settings.web_hook_name_feishu=飞书
|
||||
settings.web_hook_name_larksuite=Lark Suite
|
||||
settings.web_hook_name_feishu=飞书 / Lark Suite
|
||||
settings.web_hook_name_feishu_only =飞书
|
||||
settings.web_hook_name_larksuite_only =Lark Suite
|
||||
settings.web_hook_name_wechatwork=企业微信
|
||||
settings.web_hook_name_packagist=Packagist
|
||||
settings.packagist_username=Packagist 用户名
|
||||
|
@ -2688,6 +2690,20 @@ activity.navbar.code_frequency = 代码频率
|
|||
activity.navbar.recent_commits = 近期提交
|
||||
pulls.agit_explanation = 该合并请求是用 AGit 创建的。AGit 是一种可以让贡献者直接通过 “git push” 提出更改代码而不需要派生或建立新分支。
|
||||
error.broken_git_hook = 该仓库的 Git 钩子似乎已经损坏,请按照 <a target="_blank" rel="noreferrer" href="%s">此文档</a>来修复这些问题,然后推送一些提交来刷新状态。
|
||||
pulls.merged_title_desc_one = 已将来自 <code>%[2]s</code> 的 %[1]d 提交合并入 <code>%[3]s</code> %[4]s
|
||||
commits.search_branch = 此分支
|
||||
open_with_editor = 使用 %s 打开
|
||||
pulls.title_desc_one = 想要将来自 <code>%[2]s</code> 的 %[1]d 提交合并到 <code id="branch_target">%[3]s</code>
|
||||
settings.rename_branch_failed_protected = 无法重命名受保护的分支 %s。
|
||||
stars = 点赞
|
||||
settings.confirmation_string = 确认输入
|
||||
n_commit_one = %s 提交
|
||||
n_commit_few = %s 提交
|
||||
n_branch_one = %s 分支
|
||||
n_branch_few = %s 分支
|
||||
n_tag_one = %s 标签
|
||||
n_tag_few = %s 标签
|
||||
editor.commit_id_not_matching = 此提交ID与您当前编辑的不匹配,将提交至新分支后合并。
|
||||
|
||||
[graphs]
|
||||
component_loading=正在加载 %s...
|
||||
|
@ -2958,7 +2974,7 @@ users.max_repo_creation_desc=(设置为 -1 表示使用全局默认值)
|
|||
users.is_activated=该用户已被激活
|
||||
users.prohibit_login=禁用登录
|
||||
users.is_admin=是管理员
|
||||
users.is_restricted=受限制的
|
||||
users.is_restricted=受限
|
||||
users.allow_git_hook=允许创建 Git 钩子
|
||||
users.allow_git_hook_tooltip=Git 钩子将会被以操作系统用户运行,将会拥有同样的主机访问权限。因此,拥有此特殊的Git 钩子权限将能够访问合修改所有的 Forgejo 仓库或者Forgejo的数据库。同时也能获得Forgejo的管理员权限。
|
||||
users.allow_import_local=允许导入本地仓库
|
||||
|
@ -3370,6 +3386,8 @@ self_check.database_inconsistent_collation_columns=数据库正在使用%s的排
|
|||
self_check.database_fix_mysql=对于MySQL/MariaDB用户,您可以使用“gitea doctor convert”命令来解决校验问题。 或者您也可以通过 "ALTER ... COLLATE ..." 这样的SQL 来手动解决这个问题。
|
||||
self_check.database_fix_mssql=对于MSSQL用户,您现在只能通过"ALTER ... COLLATE ..."SQLs手动解决这个问题。
|
||||
auths.tips.gmail_settings = Gmail 设置:
|
||||
auths.tip.gitlab_new = 在 https://gitlab.com/-/profile/applications 上注册新应用
|
||||
config_settings = 设置
|
||||
|
||||
[action]
|
||||
create_repo=创建了仓库 <a href="%s">%s</a>
|
||||
|
@ -3738,3 +3756,25 @@ executable_file=可执行文件
|
|||
symbolic_link=符号链接
|
||||
submodule=子模块
|
||||
|
||||
|
||||
|
||||
[search]
|
||||
keyword_search_unavailable = 关键词搜索目前不可用,请联系站点管理员。
|
||||
search = 搜索...
|
||||
repo_kind = 搜索仓库...
|
||||
user_kind = 搜索用户...
|
||||
org_kind = 搜索组织...
|
||||
team_kind = 搜索团队...
|
||||
code_kind = 搜索代码...
|
||||
code_search_unavailable = 代码搜索目前不可用,请联系站点管理员。
|
||||
package_kind = 搜索软件包...
|
||||
project_kind = 搜索项目...
|
||||
branch_kind = 搜索分支...
|
||||
commit_kind = 搜索提交...
|
||||
runner_kind = 搜索Runners...
|
||||
no_results = 未找到匹配的结果。
|
||||
type_tooltip = 搜索类型
|
||||
fuzzy = 模糊
|
||||
code_search_by_git_grep = 当前搜索结果由 git grep 提供,如果站点管理员启用了仓库索引可能会有更好的结果。
|
||||
match = 匹配
|
||||
match_tooltip = 仅包含与搜索词完全匹配的结果
|
|
@ -467,7 +467,7 @@ issues.label_edit=編輯
|
|||
issues.label_delete=刪除
|
||||
issues.label.filter_sort.alphabetically=按字母顺序排序
|
||||
issues.label.filter_sort.reverse_alphabetically=按字母反向排序
|
||||
issues.num_participants=%d 參與者
|
||||
issues.num_participants_few=%d 參與者
|
||||
issues.attachment.open_tab=`在新的標籤頁中查看 '%s'`
|
||||
issues.attachment.download=`點擊下載 '%s'`
|
||||
issues.subscribe=訂閱
|
||||
|
|
|
@ -1433,7 +1433,7 @@ issues.label.filter_sort.alphabetically=按字母順序排序
|
|||
issues.label.filter_sort.reverse_alphabetically=按字母反向排序
|
||||
issues.label.filter_sort.by_size=檔案由小到大
|
||||
issues.label.filter_sort.reverse_by_size=檔案由大到小
|
||||
issues.num_participants=%d 參與者
|
||||
issues.num_participants_few=%d 參與者
|
||||
issues.attachment.open_tab=`在新分頁中查看「%s」`
|
||||
issues.attachment.download=`點擊下載「%s」`
|
||||
issues.subscribe=訂閱
|
||||
|
@ -2071,9 +2071,9 @@ settings.web_hook_name_dingtalk=DingTalk
|
|||
settings.web_hook_name_telegram=Telegram
|
||||
settings.web_hook_name_matrix=Matrix
|
||||
settings.web_hook_name_msteams=Microsoft Teams
|
||||
settings.web_hook_name_feishu_or_larksuite=Feishu / Lark Suite
|
||||
settings.web_hook_name_feishu=Feishu
|
||||
settings.web_hook_name_larksuite=Lark Suite
|
||||
settings.web_hook_name_feishu=Feishu / Lark Suite
|
||||
settings.web_hook_name_feishu_only =Feishu
|
||||
settings.web_hook_name_larksuite_only =Lark Suite
|
||||
settings.web_hook_name_wechatwork=WeCom (Wechat Work)
|
||||
settings.web_hook_name_packagist=Packagist
|
||||
settings.packagist_username=Packagist 帳號
|
||||
|
|
179
package-lock.json
generated
179
package-lock.json
generated
|
@ -70,6 +70,7 @@
|
|||
"@stylistic/eslint-plugin-js": "1.7.0",
|
||||
"@stylistic/stylelint-plugin": "2.1.0",
|
||||
"@vitejs/plugin-vue": "5.0.4",
|
||||
"@vue/test-utils": "2.4.5",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-plugin-array-func": "4.0.0",
|
||||
"eslint-plugin-github": "4.10.2",
|
||||
|
@ -85,7 +86,7 @@
|
|||
"eslint-plugin-vue": "9.24.0",
|
||||
"eslint-plugin-vue-scoped-css": "2.8.0",
|
||||
"eslint-plugin-wc": "2.0.4",
|
||||
"happy-dom": "14.3.7",
|
||||
"happy-dom": "14.3.10",
|
||||
"markdownlint-cli": "0.39.0",
|
||||
"postcss-html": "1.6.0",
|
||||
"stylelint": "16.3.0",
|
||||
|
@ -1329,6 +1330,12 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@one-ini/wasm": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
|
||||
"integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
|
@ -2680,6 +2687,16 @@
|
|||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.21.tgz",
|
||||
"integrity": "sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g=="
|
||||
},
|
||||
"node_modules/@vue/test-utils": {
|
||||
"version": "2.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.5.tgz",
|
||||
"integrity": "sha512-oo2u7vktOyKUked36R93NB7mg2B+N7Plr8lxp2JBGwr18ch6EggFjixSCdIVVLkT6Qr0z359Xvnafc9dcKyDUg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"js-beautify": "^1.14.9",
|
||||
"vue-component-type-helpers": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/ast": {
|
||||
"version": "1.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz",
|
||||
|
@ -2862,6 +2879,15 @@
|
|||
"resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
|
||||
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="
|
||||
},
|
||||
"node_modules/abbrev": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
|
||||
"integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
|
@ -3799,6 +3825,22 @@
|
|||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
|
||||
},
|
||||
"node_modules/config-chain": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
|
||||
"integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ini": "^1.3.4",
|
||||
"proto-list": "~1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/config-chain/node_modules/ini": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/core-js-compat": {
|
||||
"version": "3.36.1",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.1.tgz",
|
||||
|
@ -4775,6 +4817,48 @@
|
|||
"marked": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/editorconfig": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz",
|
||||
"integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@one-ini/wasm": "0.1.1",
|
||||
"commander": "^10.0.0",
|
||||
"minimatch": "9.0.1",
|
||||
"semver": "^7.5.3"
|
||||
},
|
||||
"bin": {
|
||||
"editorconfig": "bin/editorconfig"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/editorconfig/node_modules/commander": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
|
||||
"integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/editorconfig/node_modules/minimatch": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz",
|
||||
"integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.4.716",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.716.tgz",
|
||||
|
@ -6513,9 +6597,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/happy-dom": {
|
||||
"version": "14.3.7",
|
||||
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-14.3.7.tgz",
|
||||
"integrity": "sha512-lUfDRGzjrVJF2pnvh13OL+qEJ9eDpcedVLm77a3aMg8gPGKXfG+xFMNk3cOWetjucU8FveJ4qcSC/EX55nJ4fQ==",
|
||||
"version": "14.3.10",
|
||||
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-14.3.10.tgz",
|
||||
"integrity": "sha512-Rh5li9vA9MF9Gkg85CbFABKTa3uoSAByILRNGb92u/vswDd561gBg2p1UW1ZauvDWWwRxPcbACK5zv3BR+gHnQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"entities": "^4.5.0",
|
||||
|
@ -7398,6 +7482,58 @@
|
|||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
|
||||
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg=="
|
||||
},
|
||||
"node_modules/js-beautify": {
|
||||
"version": "1.15.1",
|
||||
"resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz",
|
||||
"integrity": "sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"config-chain": "^1.1.13",
|
||||
"editorconfig": "^1.0.4",
|
||||
"glob": "^10.3.3",
|
||||
"js-cookie": "^3.0.5",
|
||||
"nopt": "^7.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"css-beautify": "js/bin/css-beautify.js",
|
||||
"html-beautify": "js/bin/html-beautify.js",
|
||||
"js-beautify": "js/bin/js-beautify.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/js-beautify/node_modules/glob": {
|
||||
"version": "10.3.12",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz",
|
||||
"integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
"jackspeak": "^2.3.6",
|
||||
"minimatch": "^9.0.1",
|
||||
"minipass": "^7.0.4",
|
||||
"path-scurry": "^1.10.2"
|
||||
},
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/js-cookie": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
|
||||
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/js-levenshtein-esm": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/js-levenshtein-esm/-/js-levenshtein-esm-1.2.0.tgz",
|
||||
|
@ -8801,6 +8937,21 @@
|
|||
"resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz",
|
||||
"integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw=="
|
||||
},
|
||||
"node_modules/nopt": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.0.tgz",
|
||||
"integrity": "sha512-CVDtwCdhYIvnAzFoJ6NJ6dX3oga9/HyciQDnG1vQDjSLMeKLJ4A93ZqYKDrgYSr1FBY5/hMYC+2VCi24pgpkGA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"abbrev": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"nopt": "bin/nopt.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-package-data": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
|
||||
|
@ -9140,11 +9291,11 @@
|
|||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz",
|
||||
"integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==",
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz",
|
||||
"integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==",
|
||||
"dependencies": {
|
||||
"lru-cache": "^9.1.1 || ^10.0.0",
|
||||
"lru-cache": "^10.2.0",
|
||||
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -9727,6 +9878,12 @@
|
|||
"integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/proto-list": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
|
||||
"integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/proto-props": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/proto-props/-/proto-props-2.0.0.tgz",
|
||||
|
@ -12089,6 +12246,12 @@
|
|||
"vue": "^3.0.0-0 || ^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-component-type-helpers": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.0.7.tgz",
|
||||
"integrity": "sha512-7e12Evdll7JcTIocojgnCgwocX4WzIYStGClBQ+QuWPinZo/vQolv2EMq4a3lg16TKfwWafLimG77bxb56UauA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/vue-eslint-parser": {
|
||||
"version": "9.4.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.2.tgz",
|
||||
|
|
|
@ -69,6 +69,7 @@
|
|||
"@stylistic/eslint-plugin-js": "1.7.0",
|
||||
"@stylistic/stylelint-plugin": "2.1.0",
|
||||
"@vitejs/plugin-vue": "5.0.4",
|
||||
"@vue/test-utils": "2.4.5",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-plugin-array-func": "4.0.0",
|
||||
"eslint-plugin-github": "4.10.2",
|
||||
|
@ -84,7 +85,7 @@
|
|||
"eslint-plugin-vue": "9.24.0",
|
||||
"eslint-plugin-vue-scoped-css": "2.8.0",
|
||||
"eslint-plugin-wc": "2.0.4",
|
||||
"happy-dom": "14.3.7",
|
||||
"happy-dom": "14.3.10",
|
||||
"markdownlint-cli": "0.39.0",
|
||||
"postcss-html": "1.6.0",
|
||||
"stylelint": "16.3.0",
|
||||
|
|
|
@ -621,6 +621,7 @@ func CreateBranchProtection(ctx *context.APIContext) {
|
|||
ProtectedFilePatterns: form.ProtectedFilePatterns,
|
||||
UnprotectedFilePatterns: form.UnprotectedFilePatterns,
|
||||
BlockOnOutdatedBranch: form.BlockOnOutdatedBranch,
|
||||
ApplyToAdmins: form.ApplyToAdmins,
|
||||
}
|
||||
|
||||
err = git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{
|
||||
|
@ -808,6 +809,10 @@ func EditBranchProtection(ctx *context.APIContext) {
|
|||
protectBranch.BlockOnOutdatedBranch = *form.BlockOnOutdatedBranch
|
||||
}
|
||||
|
||||
if form.ApplyToAdmins != nil {
|
||||
protectBranch.ApplyToAdmins = *form.ApplyToAdmins
|
||||
}
|
||||
|
||||
var whitelistUsers []int64
|
||||
if form.PushWhitelistUsernames != nil {
|
||||
whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.PushWhitelistUsernames, false)
|
||||
|
|
|
@ -337,13 +337,9 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
|
|||
return
|
||||
}
|
||||
|
||||
// If we're an admin for the repository we can ignore status checks, reviews and override protected files
|
||||
if ctx.userPerm.IsAdmin() {
|
||||
return
|
||||
}
|
||||
|
||||
// Now if we're not an admin - we can't overwrite protected files so fail now
|
||||
if changedProtectedfiles {
|
||||
// It's not allowed t overwrite protected files. Unless if the user is an
|
||||
// admin and the protected branch rule doesn't apply to admins.
|
||||
if changedProtectedfiles && (!ctx.user.IsAdmin || protectBranch.ApplyToAdmins) {
|
||||
log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath)
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath),
|
||||
|
@ -352,8 +348,12 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
|
|||
}
|
||||
|
||||
// Check all status checks and reviews are ok
|
||||
if err := pull_service.CheckPullBranchProtections(ctx, pr, true); err != nil {
|
||||
if pb, err := pull_service.CheckPullBranchProtections(ctx, pr, true); err != nil {
|
||||
if models.IsErrDisallowedToMerge(err) {
|
||||
// Allow this if the rule doesn't apply to admins and the user is an admin.
|
||||
if ctx.user.IsAdmin && !pb.ApplyToAdmins {
|
||||
return
|
||||
}
|
||||
log.Warn("Forbidden: User %d is not allowed push to protected branch %s in %-v and pr #%d is not ready to be merged: %s", ctx.opts.UserID, branchName, repo, pr.Index, err.Error())
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: fmt.Sprintf("Not allowed to push to protected branch %s and pr #%d is not ready to be merged: %s", branchName, ctx.opts.PullRequestID, err.Error()),
|
||||
|
|
|
@ -26,6 +26,7 @@ import (
|
|||
org_service "code.gitea.io/gitea/services/org"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
webhook_service "code.gitea.io/gitea/services/webhook"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -210,6 +211,7 @@ func Webhooks(ctx *context.Context) {
|
|||
ctx.Data["PageIsSettingsHooks"] = true
|
||||
ctx.Data["BaseLink"] = ctx.Org.OrgLink + "/settings/hooks"
|
||||
ctx.Data["BaseLinkNew"] = ctx.Org.OrgLink + "/settings/hooks"
|
||||
ctx.Data["WebhookList"] = webhook_service.List()
|
||||
ctx.Data["Description"] = ctx.Tr("org.settings.hooks_desc")
|
||||
|
||||
ws, err := db.Find[webhook.Webhook](ctx, webhook.ListWebhookOptions{OwnerID: ctx.Org.Organization.ID})
|
||||
|
|
|
@ -950,7 +950,7 @@ func getGitIdentity(ctx *context.Context, commitMailID int64, tpl base.TplName,
|
|||
|
||||
if email == nil || !email.IsActivated {
|
||||
ctx.Data["Err_CommitMailID"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_mail"), tplEditFile, form)
|
||||
ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_mail"), tpl, form)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/asymkey"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
|
@ -18,6 +19,7 @@ import (
|
|||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
|
@ -192,6 +194,7 @@ func Releases(ctx *context.Context) {
|
|||
}
|
||||
|
||||
ctx.Data["Releases"] = releases
|
||||
addVerifyTagToContext(ctx)
|
||||
|
||||
numReleases := ctx.Data["NumReleases"].(int64)
|
||||
pager := context.NewPagination(int(numReleases), listOptions.PageSize, listOptions.Page, 5)
|
||||
|
@ -201,6 +204,44 @@ func Releases(ctx *context.Context) {
|
|||
ctx.HTML(http.StatusOK, tplReleasesList)
|
||||
}
|
||||
|
||||
func verifyTagSignature(ctx *context.Context, r *repo_model.Release) (*asymkey.ObjectVerification, error) {
|
||||
if err := r.LoadAttributes(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gitRepo, err := gitrepo.OpenRepository(ctx, r.Repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
tag, err := gitRepo.GetTag(r.TagName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tag.Signature == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
verification := asymkey.ParseTagWithSignature(ctx, gitRepo, tag)
|
||||
return verification, nil
|
||||
}
|
||||
|
||||
func addVerifyTagToContext(ctx *context.Context) {
|
||||
ctx.Data["VerifyTag"] = func(r *repo_model.Release) *asymkey.ObjectVerification {
|
||||
v, err := verifyTagSignature(ctx, r)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return v
|
||||
}
|
||||
ctx.Data["HasSignature"] = func(verification *asymkey.ObjectVerification) bool {
|
||||
if verification == nil {
|
||||
return false
|
||||
}
|
||||
return verification.Reason != "gpg.error.not_signed_commit"
|
||||
}
|
||||
}
|
||||
|
||||
// TagsList render tags list page
|
||||
func TagsList(ctx *context.Context) {
|
||||
ctx.Data["PageIsTagList"] = true
|
||||
|
@ -240,6 +281,7 @@ func TagsList(ctx *context.Context) {
|
|||
}
|
||||
|
||||
ctx.Data["Releases"] = releases
|
||||
addVerifyTagToContext(ctx)
|
||||
|
||||
numTags := ctx.Data["NumTags"].(int64)
|
||||
pager := context.NewPagination(int(numTags), opts.PageSize, opts.Page, 5)
|
||||
|
@ -304,6 +346,7 @@ func SingleRelease(ctx *context.Context) {
|
|||
if release.IsTag && release.Title == "" {
|
||||
release.Title = release.TagName
|
||||
}
|
||||
addVerifyTagToContext(ctx)
|
||||
|
||||
ctx.Data["PageIsSingleTag"] = release.IsTag
|
||||
if release.IsTag {
|
||||
|
|
|
@ -237,6 +237,7 @@ func SettingsProtectedBranchPost(ctx *context.Context) {
|
|||
protectBranch.ProtectedFilePatterns = f.ProtectedFilePatterns
|
||||
protectBranch.UnprotectedFilePatterns = f.UnprotectedFilePatterns
|
||||
protectBranch.BlockOnOutdatedBranch = f.BlockOnOutdatedBranch
|
||||
protectBranch.ApplyToAdmins = f.ApplyToAdmins
|
||||
|
||||
err = git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{
|
||||
UserIDs: whitelistUsers,
|
||||
|
|
|
@ -45,6 +45,7 @@ func WebhookList(ctx *context.Context) {
|
|||
ctx.Data["PageIsSettingsHooks"] = true
|
||||
ctx.Data["BaseLink"] = ctx.Repo.RepoLink + "/settings/hooks"
|
||||
ctx.Data["BaseLinkNew"] = ctx.Repo.RepoLink + "/settings/hooks"
|
||||
ctx.Data["WebhookList"] = webhook_service.List()
|
||||
ctx.Data["Description"] = ctx.Tr("repo.settings.hooks_desc", "https://forgejo.org/docs/latest/user/webhooks/")
|
||||
|
||||
ws, err := db.Find[webhook.Webhook](ctx, webhook.ListWebhookOptions{RepoID: ctx.Repo.Repository.ID})
|
||||
|
@ -132,13 +133,16 @@ func WebhookNew(ctx *context.Context) {
|
|||
}
|
||||
|
||||
hookType := ctx.Params(":type")
|
||||
if webhook_service.GetWebhookHandler(hookType) == nil {
|
||||
handler := webhook_service.GetWebhookHandler(hookType)
|
||||
if handler == nil {
|
||||
ctx.NotFound("GetWebhookHandler", nil)
|
||||
return
|
||||
}
|
||||
ctx.Data["HookType"] = hookType
|
||||
ctx.Data["WebhookHandler"] = handler
|
||||
ctx.Data["BaseLink"] = orCtx.LinkNew
|
||||
ctx.Data["BaseLinkNew"] = orCtx.LinkNew
|
||||
ctx.Data["WebhookList"] = webhook_service.List()
|
||||
|
||||
ctx.HTML(http.StatusOK, orCtx.NewTemplate)
|
||||
}
|
||||
|
@ -194,6 +198,7 @@ func WebhookCreate(ctx *context.Context) {
|
|||
ctx.Data["PageIsSettingsHooksNew"] = true
|
||||
ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook_module.HookEvent{}}
|
||||
ctx.Data["HookType"] = hookType
|
||||
ctx.Data["WebhookHandler"] = handler
|
||||
|
||||
orCtx, err := getOwnerRepoCtx(ctx)
|
||||
if err != nil {
|
||||
|
@ -202,6 +207,7 @@ func WebhookCreate(ctx *context.Context) {
|
|||
}
|
||||
ctx.Data["BaseLink"] = orCtx.LinkNew
|
||||
ctx.Data["BaseLinkNew"] = orCtx.LinkNew
|
||||
ctx.Data["WebhookList"] = webhook_service.List()
|
||||
|
||||
if ctx.HasError() {
|
||||
// pre-fill the form with the submitted data
|
||||
|
@ -336,6 +342,7 @@ func checkWebhook(ctx *context.Context) (*ownerRepoCtx, *webhook.Webhook) {
|
|||
}
|
||||
ctx.Data["BaseLink"] = orCtx.Link
|
||||
ctx.Data["BaseLinkNew"] = orCtx.LinkNew
|
||||
ctx.Data["WebhookList"] = webhook_service.List()
|
||||
|
||||
var w *webhook.Webhook
|
||||
if orCtx.RepoID > 0 {
|
||||
|
@ -358,6 +365,7 @@ func checkWebhook(ctx *context.Context) (*ownerRepoCtx, *webhook.Webhook) {
|
|||
|
||||
if handler := webhook_service.GetWebhookHandler(w.Type); handler != nil {
|
||||
ctx.Data["HookMetadata"] = handler.Metadata(w)
|
||||
ctx.Data["WebhookHandler"] = handler
|
||||
}
|
||||
|
||||
ctx.Data["History"], err = w.History(ctx, 1)
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
webhook_service "code.gitea.io/gitea/services/webhook"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -23,6 +24,7 @@ func Webhooks(ctx *context.Context) {
|
|||
ctx.Data["PageIsSettingsHooks"] = true
|
||||
ctx.Data["BaseLink"] = setting.AppSubURL + "/user/settings/hooks"
|
||||
ctx.Data["BaseLinkNew"] = setting.AppSubURL + "/user/settings/hooks"
|
||||
ctx.Data["WebhookList"] = webhook_service.List()
|
||||
ctx.Data["Description"] = ctx.Tr("settings.hooks.desc")
|
||||
|
||||
ws, err := db.Find[webhook.Webhook](ctx, webhook.ListWebhookOptions{OwnerID: ctx.Doer.ID})
|
||||
|
|
|
@ -118,4 +118,5 @@ func WebfingerQuery(ctx *context.Context) {
|
|||
Aliases: aliases,
|
||||
Links: links,
|
||||
})
|
||||
ctx.Resp.Header().Set("Content-Type", "application/jrd+json")
|
||||
}
|
||||
|
|
|
@ -162,6 +162,7 @@ func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch) *api
|
|||
RequireSignedCommits: bp.RequireSignedCommits,
|
||||
ProtectedFilePatterns: bp.ProtectedFilePatterns,
|
||||
UnprotectedFilePatterns: bp.UnprotectedFilePatterns,
|
||||
ApplyToAdmins: bp.ApplyToAdmins,
|
||||
Created: bp.CreatedUnix.AsTime(),
|
||||
Updated: bp.UpdatedUnix.AsTime(),
|
||||
}
|
||||
|
|
|
@ -219,6 +219,7 @@ type ProtectBranchForm struct {
|
|||
RequireSignedCommits bool
|
||||
ProtectedFilePatterns string
|
||||
UnprotectedFilePatterns string
|
||||
ApplyToAdmins bool
|
||||
}
|
||||
|
||||
// Validate validates the fields
|
||||
|
|
|
@ -5,10 +5,18 @@ package markup
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/perm/access"
|
||||
"code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
gitea_context "code.gitea.io/gitea/services/context"
|
||||
file_service "code.gitea.io/gitea/services/repository/files"
|
||||
)
|
||||
|
||||
func ProcessorHelper() *markup.ProcessorHelper {
|
||||
|
@ -29,5 +37,51 @@ func ProcessorHelper() *markup.ProcessorHelper {
|
|||
// when using gitea context (web context), use user's visibility and user's permission to check
|
||||
return user.IsUserVisibleToViewer(giteaCtx, mentionedUser, giteaCtx.Doer)
|
||||
},
|
||||
GetRepoFileBlob: func(ctx context.Context, ownerName, repoName, commitSha, filePath string, language *string) (*git.Blob, error) {
|
||||
repo, err := repo.GetRepositoryByOwnerAndName(ctx, ownerName, repoName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var user *user.User
|
||||
|
||||
giteaCtx, ok := ctx.(*gitea_context.Context)
|
||||
if ok {
|
||||
user = giteaCtx.Doer
|
||||
}
|
||||
|
||||
perms, err := access.GetUserRepoPermission(ctx, repo, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !perms.CanRead(unit.TypeCode) {
|
||||
return nil, fmt.Errorf("cannot access repository code")
|
||||
}
|
||||
|
||||
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
commit, err := gitRepo.GetCommit(commitSha)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if language != nil {
|
||||
*language, err = file_service.TryGetContentLanguage(gitRepo, commitSha, filePath)
|
||||
if err != nil {
|
||||
log.Error("Unable to get file language for %-v:%s. Error: %v", repo, filePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
blob, err := commit.GetBlobByPath(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return blob, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -104,7 +104,7 @@ func CheckPullMergable(stdCtx context.Context, doer *user_model.User, perm *acce
|
|||
return ErrIsChecking
|
||||
}
|
||||
|
||||
if err := CheckPullBranchProtections(ctx, pr, false); err != nil {
|
||||
if pb, err := CheckPullBranchProtections(ctx, pr, false); err != nil {
|
||||
if !models.IsErrDisallowedToMerge(err) {
|
||||
log.Error("Error whilst checking pull branch protection for %-v: %v", pr, err)
|
||||
return err
|
||||
|
@ -117,8 +117,9 @@ func CheckPullMergable(stdCtx context.Context, doer *user_model.User, perm *acce
|
|||
err = nil
|
||||
}
|
||||
|
||||
// * if the doer is admin, they could skip the branch protection check
|
||||
if adminSkipProtectionCheck {
|
||||
// * if the doer is admin, they could skip the branch protection check,
|
||||
// if that's allowed by the protected branch rule.
|
||||
if adminSkipProtectionCheck && !pb.ApplyToAdmins {
|
||||
if isRepoAdmin, errCheckAdmin := access_model.IsUserRepoAdmin(ctx, pr.BaseRepo, doer); errCheckAdmin != nil {
|
||||
log.Error("Unable to check if %-v is a repo admin in %-v: %v", doer, pr.BaseRepo, errCheckAdmin)
|
||||
return errCheckAdmin
|
||||
|
|
|
@ -424,63 +424,64 @@ func IsUserAllowedToMerge(ctx context.Context, pr *issues_model.PullRequest, p a
|
|||
return false, nil
|
||||
}
|
||||
|
||||
// CheckPullBranchProtections checks whether the PR is ready to be merged (reviews and status checks)
|
||||
func CheckPullBranchProtections(ctx context.Context, pr *issues_model.PullRequest, skipProtectedFilesCheck bool) (err error) {
|
||||
// CheckPullBranchProtections checks whether the PR is ready to be merged (reviews and status checks).
|
||||
// Returns the protected branch rule when `ErrDisallowedToMerge` is returned as error.
|
||||
func CheckPullBranchProtections(ctx context.Context, pr *issues_model.PullRequest, skipProtectedFilesCheck bool) (protectedBranchRule *git_model.ProtectedBranch, err error) {
|
||||
if err = pr.LoadBaseRepo(ctx); err != nil {
|
||||
return fmt.Errorf("LoadBaseRepo: %w", err)
|
||||
return nil, fmt.Errorf("LoadBaseRepo: %w", err)
|
||||
}
|
||||
|
||||
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
|
||||
if err != nil {
|
||||
return fmt.Errorf("LoadProtectedBranch: %v", err)
|
||||
return nil, fmt.Errorf("LoadProtectedBranch: %v", err)
|
||||
}
|
||||
if pb == nil {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
isPass, err := IsPullCommitStatusPass(ctx, pr)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if !isPass {
|
||||
return models.ErrDisallowedToMerge{
|
||||
return pb, models.ErrDisallowedToMerge{
|
||||
Reason: "Not all required status checks successful",
|
||||
}
|
||||
}
|
||||
|
||||
if !issues_model.HasEnoughApprovals(ctx, pb, pr) {
|
||||
return models.ErrDisallowedToMerge{
|
||||
return pb, models.ErrDisallowedToMerge{
|
||||
Reason: "Does not have enough approvals",
|
||||
}
|
||||
}
|
||||
if issues_model.MergeBlockedByRejectedReview(ctx, pb, pr) {
|
||||
return models.ErrDisallowedToMerge{
|
||||
return pb, models.ErrDisallowedToMerge{
|
||||
Reason: "There are requested changes",
|
||||
}
|
||||
}
|
||||
if issues_model.MergeBlockedByOfficialReviewRequests(ctx, pb, pr) {
|
||||
return models.ErrDisallowedToMerge{
|
||||
return pb, models.ErrDisallowedToMerge{
|
||||
Reason: "There are official review requests",
|
||||
}
|
||||
}
|
||||
|
||||
if issues_model.MergeBlockedByOutdatedBranch(pb, pr) {
|
||||
return models.ErrDisallowedToMerge{
|
||||
return pb, models.ErrDisallowedToMerge{
|
||||
Reason: "The head branch is behind the base branch",
|
||||
}
|
||||
}
|
||||
|
||||
if skipProtectedFilesCheck {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if pb.MergeBlockedByProtectedFiles(pr.ChangedProtectedFiles) {
|
||||
return models.ErrDisallowedToMerge{
|
||||
return pb, models.ErrDisallowedToMerge{
|
||||
Reason: "Changed protected files",
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// MergedManually mark pr as merged manually
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
@ -17,6 +18,7 @@ import (
|
|||
|
||||
webhook_model "code.gitea.io/gitea/models/webhook"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/svg"
|
||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
)
|
||||
|
@ -34,6 +36,14 @@ func (dh defaultHandler) Type() webhook_module.HookType {
|
|||
return webhook_module.GITEA
|
||||
}
|
||||
|
||||
func (dh defaultHandler) Icon(size int) template.HTML {
|
||||
if dh.forgejo {
|
||||
// forgejo.svg is not in web_src/svg/, so svg.RenderHTML does not work
|
||||
return imgIcon("forgejo.svg", size)
|
||||
}
|
||||
return svg.RenderHTML("gitea-gitea", size, "img")
|
||||
}
|
||||
|
||||
func (defaultHandler) Metadata(*webhook_model.Webhook) any { return nil }
|
||||
|
||||
func (defaultHandler) FormFields(bind func(any)) FormFields {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue