Merge commit '6d4554b01c' into forgejo
Some checks failed
Integration tests for the release process / release-simulation (push) Has been cancelled
/ release (push) Has been cancelled
testing-integration / test-unit (push) Has been cancelled
testing-integration / test-sqlite (push) Has been cancelled
testing / backend-checks (push) Has been cancelled
testing / frontend-checks (push) Has been cancelled
testing / test-unit (push) Has been cancelled
testing / test-e2e (push) Has been cancelled
testing / test-remote-cacher (redis) (push) Has been cancelled
testing / test-remote-cacher (valkey) (push) Has been cancelled
testing / test-remote-cacher (garnet) (push) Has been cancelled
testing / test-remote-cacher (redict) (push) Has been cancelled
testing / test-mysql (push) Has been cancelled
testing / test-pgsql (push) Has been cancelled
testing / test-sqlite (push) Has been cancelled
testing / security-check (push) Has been cancelled

This commit is contained in:
Minecon724 2025-07-07 20:30:58 +02:00
commit 9432959b58
Signed by: Minecon724
GPG key ID: A02E6E67AB961189
266 changed files with 6837 additions and 2053 deletions

View file

@ -13,6 +13,13 @@ forgejo.org/models
IsErrSHANotFound
IsErrMergeDivergingFastForwardOnly
forgejo.org/models/activities
GetActivityByID
NewFederatedUserActivity
CreateUserActivity
GetFollowingFeeds
FederatedUserActivity.loadActor
forgejo.org/models/auth
WebAuthnCredentials
@ -54,9 +61,17 @@ forgejo.org/models/user
IsErrExternalLoginUserAlreadyExist
IsErrExternalLoginUserNotExist
NewFederatedUser
NewFederatedUserFollower
IsErrUserSettingIsNotExist
GetUserAllSettings
DeleteUserSetting
GetFederatedUser
GetFederatedUserByUserID
UpdateFederatedUser
GetFollowersForUser
AddFollower
RemoveFollower
IsFollowingAp
forgejo.org/modules/activitypub
NewContext

View file

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

View file

@ -37,13 +37,9 @@ coverage.all
coverage/
cpu.out
/modules/migration/bindata.go
/modules/migration/bindata.go.hash
/modules/options/bindata.go
/modules/options/bindata.go.hash
/modules/public/bindata.go
/modules/public/bindata.go.hash
/modules/templates/bindata.go
/modules/templates/bindata.go.hash
*.db

View file

@ -6,7 +6,7 @@ body:
- type: markdown
attributes:
value: |
**NOTE: If your issue is a security concern, please email <security@forgejo.org> (GPG: `A4676E79`) instead of opening a public issue.**
**NOTE: If your issue is a security concern, please email <security@forgejo.org> ([security.txt](https://forgejo.org/.well-known/security.txt)) instead of opening a public issue.**
- type: markdown
attributes:
value: |

View file

@ -6,7 +6,7 @@ body:
- type: markdown
attributes:
value: |
**NOTE: If your issue is a security concern, please email <security@forgejo.org> (GPG: `A4676E79`) instead of opening a public issue.**
**NOTE: If your issue is a security concern, please email <security@forgejo.org> ([security.txt](https://forgejo.org/.well-known/security.txt)) instead of opening a public issue.**
- type: markdown
attributes:
value: |

View file

@ -1,4 +1,4 @@
FROM data.forgejo.org/oci/alpine:3.21
FROM data.forgejo.org/oci/alpine:3.22
ARG RELEASE_VERSION=unkown
LABEL maintainer="contact@forgejo.org" \
org.opencontainers.image.version="${RELEASE_VERSION}"

View file

@ -18,7 +18,7 @@ runs:
- name: install packages
run: |
apt-get update -qq
apt-get -q install -qq -y ${PACKAGES}
apt-get -q install --allow-downgrades -qq -y ${PACKAGES}
env:
PACKAGES: ${{inputs.packages}}
- name: remove temporary package list to prevent using it in other steps

View file

@ -164,7 +164,7 @@ jobs:
- name: build container & release
if: ${{ secrets.TOKEN != '' }}
uses: https://data.forgejo.org/forgejo/forgejo-build-publish/build@v5.3.4
uses: https://data.forgejo.org/forgejo/forgejo-build-publish/build@v5.3.5
with:
forgejo: "${{ env.GITHUB_SERVER_URL }}"
owner: "${{ env.GITHUB_REPOSITORY_OWNER }}"
@ -183,7 +183,7 @@ jobs:
- name: build rootless container
if: ${{ secrets.TOKEN != '' }}
uses: https://data.forgejo.org/forgejo/forgejo-build-publish/build@v5.3.4
uses: https://data.forgejo.org/forgejo/forgejo-build-publish/build@v5.3.5
with:
forgejo: "${{ env.GITHUB_SERVER_URL }}"
owner: "${{ env.GITHUB_REPOSITORY_OWNER }}"

View file

@ -44,7 +44,7 @@ jobs:
- uses: https://data.forgejo.org/actions/checkout@v4
- name: copy & sign
uses: https://data.forgejo.org/forgejo/forgejo-build-publish/publish@v5.3.4
uses: https://data.forgejo.org/forgejo/forgejo-build-publish/publish@v5.3.5
with:
from-forgejo: ${{ vars.FORGEJO }}
to-forgejo: ${{ vars.FORGEJO }}

View file

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

View file

@ -0,0 +1,71 @@
#
# Additional integration tests designed to run once a day when
# `mirror.yml` pushes to https://codeberg.org/forgejo-integration/forgejo
# and send a notification via email should they fail.
#
# For debug purposes:
#
# - uncomment [on].pull_request
# - swap 'forgejo-integration' and 'forgejo-coding'
# - open a pull request at https://codeberg.org/forgejo/forgejo and fix things
# - swap 'forgejo-integration' and 'forgejo-coding'
# - comment [on].pull_request
#
name: testing-integration
on:
# pull_request:
push:
tags: 'v[0-9]+.[0-9]+.*'
branches:
- 'forgejo'
- 'v*/forgejo'
jobs:
test-unit:
# if: vars.ROLE == 'forgejo-coding'
if: vars.ROLE == 'forgejo-integration'
runs-on: docker
container:
image: 'data.forgejo.org/oci/node:22-bookworm'
options: --tmpfs /tmp:exec,noatime
steps:
- uses: https://data.forgejo.org/actions/checkout@v4
- uses: ./.forgejo/workflows-composite/setup-env
- name: install git 2.30
uses: ./.forgejo/workflows-composite/apt-install-from
with:
packages: git/bullseye git-lfs/bullseye
release: bullseye
- uses: ./.forgejo/workflows-composite/build-backend
- run: |
su forgejo -c 'make test-backend test-check'
timeout-minutes: 120
env:
RACE_ENABLED: 'true'
TAGS: bindata
test-sqlite:
# if: vars.ROLE == 'forgejo-coding'
if: vars.ROLE == 'forgejo-integration'
runs-on: docker
container:
image: 'data.forgejo.org/oci/node:22-bookworm'
options: --tmpfs /tmp:exec,noatime
steps:
- uses: https://data.forgejo.org/actions/checkout@v4
- uses: ./.forgejo/workflows-composite/setup-env
- name: install git 2.30
uses: ./.forgejo/workflows-composite/apt-install-from
with:
packages: git/bullseye git-lfs/bullseye
release: bullseye
- uses: ./.forgejo/workflows-composite/build-backend
- run: |
su forgejo -c 'make test-sqlite-migration test-sqlite'
timeout-minutes: 120
env:
TAGS: sqlite sqlite_unlock_notify
RACE_ENABLED: true
TEST_TAGS: sqlite sqlite_unlock_notify
USE_REPO_TEST_DIR: 1

View file

@ -91,6 +91,7 @@ jobs:
RACE_ENABLED: 'true'
TAGS: bindata
TEST_ELASTICSEARCH_URL: http://elasticsearch:9200
TEST_MINIO_ENDPOINT: minio:9000
test-e2e:
if: vars.ROLE == 'forgejo-coding' || vars.ROLE == 'forgejo-testing'
runs-on: docker
@ -114,6 +115,11 @@ jobs:
run: |
su forgejo -c 'make deps-frontend frontend'
- uses: ./.forgejo/workflows-composite/build-backend
- name: Decide to run all tests
id: run-all
if: contains(github.event.pull_request.labels.*.name, 'run-all-playwright-tests') || contains(github.event.pull_request.title, 'playwright')
run: |
echo "all=1" >> "$GITHUB_OUTPUT"
- name: Get changed files
id: changed-files
uses: https://data.forgejo.org/tj-actions/changed-files@v46
@ -126,6 +132,7 @@ jobs:
USE_REPO_TEST_DIR: 1
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
CHANGED_FILES: ${{steps.changed-files.outputs.all_changed_files}}
RUN_ALL: ${{steps.run-all.all}}
- name: Upload test artifacts on failure
if: failure()
uses: https://data.forgejo.org/forgejo/upload-artifact@v4

View file

@ -10,6 +10,7 @@
# Javascript and CSS code.
web_src/.* @beowulf @gusted
web_src/css/.* @0ko
# HTML templates used by the backend.
templates/.* @beowulf @gusted

View file

@ -1,6 +1,6 @@
FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/xx AS xx
FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.24-alpine3.21 AS build-env
FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.24-alpine3.22 AS build-env
ARG GOPROXY
ENV GOPROXY=${GOPROXY:-https://proxy.golang.org,direct}
@ -33,10 +33,10 @@ RUN apk --no-cache add build-base git nodejs npm
COPY . ${GOPATH}/src/forgejo.org
WORKDIR ${GOPATH}/src/forgejo.org
RUN make clean
RUN make clean-no-bindata
RUN make frontend
RUN go build contrib/environment-to-ini/environment-to-ini.go && xx-verify environment-to-ini
RUN LDFLAGS="-buildid=" make RELEASE_VERSION=$RELEASE_VERSION GOFLAGS="-trimpath" go-check generate-backend static-executable && xx-verify gitea
RUN LDFLAGS="-buildid=" make FORGEJO_GENERATE_SKIP_HASH=true RELEASE_VERSION=$RELEASE_VERSION GOFLAGS="-trimpath" go-check generate-backend static-executable && xx-verify gitea
# Copy local files
COPY docker/root /tmp/local
@ -51,7 +51,7 @@ RUN chmod 755 /tmp/local/usr/bin/entrypoint \
/go/src/forgejo.org/environment-to-ini
RUN chmod 644 /go/src/forgejo.org/contrib/autocompletion/bash_autocomplete
FROM data.forgejo.org/oci/alpine:3.21
FROM data.forgejo.org/oci/alpine:3.22
ARG RELEASE_VERSION
LABEL maintainer="contact@forgejo.org" \
org.opencontainers.image.authors="Forgejo" \

View file

@ -1,6 +1,6 @@
FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/xx AS xx
FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.24-alpine3.21 AS build-env
FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.24-alpine3.22 AS build-env
ARG GOPROXY
ENV GOPROXY=${GOPROXY:-https://proxy.golang.org,direct}
@ -33,10 +33,10 @@ RUN apk --no-cache add build-base git nodejs npm
COPY . ${GOPATH}/src/forgejo.org
WORKDIR ${GOPATH}/src/forgejo.org
RUN make clean
RUN make clean-no-bindata
RUN make frontend
RUN go build contrib/environment-to-ini/environment-to-ini.go && xx-verify environment-to-ini
RUN make RELEASE_VERSION=$RELEASE_VERSION go-check generate-backend static-executable && xx-verify gitea
RUN make FORGEJO_GENERATE_SKIP_HASH=true RELEASE_VERSION=$RELEASE_VERSION go-check generate-backend static-executable && xx-verify gitea
# Copy local files
COPY docker/rootless /tmp/local
@ -49,7 +49,7 @@ RUN chmod 755 /tmp/local/usr/local/bin/docker-entrypoint.sh \
/go/src/forgejo.org/environment-to-ini
RUN chmod 644 /go/src/forgejo.org/contrib/autocompletion/bash_autocomplete
FROM data.forgejo.org/oci/alpine:3.21
FROM data.forgejo.org/oci/alpine:3.22
ARG RELEASE_VERSION
LABEL maintainer="contact@forgejo.org" \
org.opencontainers.image.authors="Forgejo" \

View file

@ -47,8 +47,7 @@ GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1.6.0 # renovate: datasour
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 # renovate: datasource=go
DEADCODE_PACKAGE ?= golang.org/x/tools/cmd/deadcode@v0.34.0 # renovate: datasource=go
GOMOCK_PACKAGE ?= go.uber.org/mock/mockgen@v0.5.2 # renovate: datasource=go
GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.18.1 # renovate: datasource=go
RENOVATE_NPM_PACKAGE ?= renovate@40.48.4 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate
RENOVATE_NPM_PACKAGE ?= renovate@41.1.4 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate
# https://github.com/disposable-email-domains/disposable-email-domains/commits/main/
DISPOSABLE_EMAILS_SHA ?= 0c27e671231d27cf66370034d7f6818037416989 # renovate: ...
@ -129,7 +128,7 @@ WEBPACK_CONFIGS := webpack.config.js tailwind.config.js
WEBPACK_DEST := public/assets/js/index.js public/assets/css/index.css
WEBPACK_DEST_ENTRIES := public/assets/js public/assets/css public/assets/fonts
BINDATA_DEST := modules/public/bindata.go modules/options/bindata.go modules/templates/bindata.go
BINDATA_DEST := modules/migration/bindata.go modules/public/bindata.go modules/options/bindata.go modules/templates/bindata.go
BINDATA_HASH := $(addsuffix .hash,$(BINDATA_DEST))
GENERATED_GO_DEST := modules/charset/invisible_gen.go modules/charset/ambiguous_gen.go
@ -222,7 +221,6 @@ help:
@echo " - lint-go lint go files"
@echo " - lint-go-fix lint go files and fix issues"
@echo " - lint-go-vet lint go files with vet"
@echo " - lint-go-gopls lint go files with gopls"
@echo " - lint-js lint js files"
@echo " - lint-js-fix lint js files and fix issues"
@echo " - lint-css lint css files"
@ -325,8 +323,12 @@ clean-all: clean
rm -rf $(WEBPACK_DEST_ENTRIES) node_modules
.PHONY: clean
clean:
rm -rf $(EXECUTABLE) $(DIST) $(BINDATA_DEST) $(BINDATA_HASH) \
clean: clean-no-bindata
rm -rf $(BINDATA_DEST) $(BINDATA_HASH)
.PHONY: clean-no-bindata
clean-no-bindata:
rm -rf $(EXECUTABLE) $(DIST) \
integrations*.test \
e2e*.test \
tests/integration/gitea-integration-* \
@ -483,11 +485,6 @@ lint-go-vet:
@echo "Running go vet..."
@$(GO) vet ./...
.PHONY: lint-go-gopls
lint-go-gopls:
@echo "Running gopls check..."
@GO=$(GO) GOPLS_PACKAGE=$(GOPLS_PACKAGE) tools/lint-go-gopls.sh $(GO_SOURCES_NO_BINDATA)
.PHONY: lint-editorconfig
lint-editorconfig:
$(GO) run $(EDITORCONFIG_CHECKER_PACKAGE) templates .forgejo/workflows
@ -928,7 +925,6 @@ deps-tools:
$(GO) install $(GO_LICENSES_PACKAGE)
$(GO) install $(GOVULNCHECK_PACKAGE)
$(GO) install $(GOMOCK_PACKAGE)
$(GO) install $(GOPLS_PACKAGE)
node_modules: package-lock.json
npm install --no-save

File diff suppressed because one or more lines are too long

View file

@ -1,14 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build vendor
package main
// Libraries that are included to vendor utilities used during build.
// These libraries will not be included in a normal compilation.
import (
// for embed
_ "github.com/shurcooL/vfsgen"
)

View file

@ -1,5 +1,6 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later
//go:build ignore
@ -7,30 +8,40 @@ package main
import (
"bytes"
"crypto/sha1"
"crypto/sha256"
"fmt"
"io"
"io/fs"
"log"
"net/http"
"os"
"path"
"path/filepath"
"strconv"
"text/template"
"github.com/shurcooL/vfsgen"
"github.com/klauspost/compress/zstd"
)
func needsUpdate(dir, filename string) (bool, []byte) {
needRegen := false
func fileExists(filename string) bool {
_, err := os.Stat(filename)
if err != nil {
needRegen = true
if err == nil {
return true
}
if os.IsNotExist(err) {
return false
}
panic(err)
}
func needsUpdate(dir, filename string) (bool, []byte) {
needRegen := !fileExists(filename)
oldHash, err := os.ReadFile(filename + ".hash")
if err != nil {
oldHash = []byte{}
}
hasher := sha1.New()
hasher := sha256.New()
err = filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
if err != nil {
@ -51,7 +62,7 @@ func needsUpdate(dir, filename string) (bool, []byte) {
newHash := hasher.Sum([]byte{})
if bytes.Compare(oldHash, newHash) != 0 {
if !bytes.Equal(oldHash, newHash) {
return true, newHash
}
@ -69,24 +80,280 @@ func main() {
useGlobalModTime, _ = strconv.ParseBool(os.Args[4])
}
update, newHash := needsUpdate(dir, filename)
if os.Getenv("FORGEJO_GENERATE_SKIP_HASH") == "true" && fileExists(filename) {
fmt.Printf("bindata %s already exists and FORGEJO_GENERATE_SKIP_HASH=true\n", packageName)
return
}
update, newHash := needsUpdate(dir, filename)
if !update {
fmt.Printf("bindata for %s already up-to-date\n", packageName)
fmt.Printf("bindata %s already exists and the checksum is a match\n", packageName)
return
}
fmt.Printf("generating bindata for %s\n", packageName)
var fsTemplates http.FileSystem = http.Dir(dir)
err := vfsgen.Generate(fsTemplates, vfsgen.Options{
PackageName: packageName,
BuildTags: "bindata",
VariableName: "Assets",
Filename: filename,
UseGlobalModTime: useGlobalModTime,
})
root, err := os.OpenRoot(dir)
if err != nil {
log.Fatalf("%v\n", err)
log.Fatal(err)
}
out, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644)
if err != nil {
log.Fatal(err)
}
defer out.Close()
if err := generate(root.FS(), packageName, useGlobalModTime, out); err != nil {
log.Fatal(err)
}
_ = os.WriteFile(filename+".hash", newHash, 0o666)
}
type file struct {
Path string
Name string
UncompressedSize int
CompressedData []byte
UncompressedData []byte
}
type direntry struct {
Name string
IsDir bool
}
func generate(fsRoot fs.FS, packageName string, globalTime bool, output io.Writer) error {
enc, err := zstd.NewWriter(nil, zstd.WithEncoderLevel(zstd.SpeedBestCompression))
if err != nil {
return err
}
files := []file{}
dirs := map[string][]direntry{}
if err := fs.WalkDir(fsRoot, ".", func(filePath string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
entries, err := fs.ReadDir(fsRoot, filePath)
if err != nil {
return err
}
dirEntries := make([]direntry, 0, len(entries))
for _, entry := range entries {
dirEntries = append(dirEntries, direntry{Name: entry.Name(), IsDir: entry.IsDir()})
}
dirs[filePath] = dirEntries
return nil
}
src, err := fs.ReadFile(fsRoot, filePath)
if err != nil {
return err
}
dst := enc.EncodeAll(src, nil)
if len(dst) < len(src) {
files = append(files, file{
Path: filePath,
Name: path.Base(filePath),
UncompressedSize: len(src),
CompressedData: dst,
})
} else {
files = append(files, file{
Path: filePath,
Name: path.Base(filePath),
UncompressedData: src,
})
}
return nil
}); err != nil {
return err
}
return generatedTmpl.Execute(output, map[string]any{
"Packagename": packageName,
"GlobalTime": globalTime,
"Files": files,
"Dirs": dirs,
})
}
var generatedTmpl = template.Must(template.New("").Parse(`// Code generated by efs-gen. DO NOT EDIT.
//go:build bindata
package {{.Packagename}}
import (
"bytes"
"time"
"io"
"io/fs"
"github.com/klauspost/compress/zstd"
)
type normalFile struct {
name string
content []byte
}
type compressedFile struct {
name string
uncompressedSize int64
data []byte
}
var files = map[string]any{
{{- range .Files}}
"{{.Path}}": {{if .CompressedData}}compressedFile{"{{.Name}}", {{.UncompressedSize}}, []byte({{printf "%+q" .CompressedData}})}{{else}}normalFile{"{{.Name}}", []byte({{printf "%+q" .UncompressedData}})}{{end}},
{{- end}}
}
var dirs = map[string][]fs.DirEntry{
{{- range $key, $entry := .Dirs}}
"{{$key}}": {
{{- range $entry}}
direntry{"{{.Name}}", {{.IsDir}}},
{{- end}}
},
{{- end}}
}
type assets struct{}
var Assets = assets{}
func (a assets) Open(name string) (fs.File, error) {
f, ok := files[name]
if !ok {
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
}
switch f := f.(type) {
case normalFile:
return file{name: f.name, size: int64(len(f.content)), data: bytes.NewReader(f.content)}, nil
case compressedFile:
r, _ := zstd.NewReader(bytes.NewReader(f.data))
return &compressFile{name: f.name, size: f.uncompressedSize, data: r, content: f.data}, nil
default:
panic("unknown file type")
}
}
func (a assets) ReadDir(name string) ([]fs.DirEntry, error) {
d, ok := dirs[name]
if !ok {
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
}
return d, nil
}
type file struct {
name string
size int64
data io.ReadSeeker
}
var _ io.ReadSeeker = (*file)(nil)
func (f file) Stat() (fs.FileInfo, error) {
return fileinfo{name: f.name, size: f.size}, nil
}
func (f file) Read(p []byte) (int, error) {
return f.data.Read(p)
}
func (f file) Seek(offset int64, whence int) (int64, error) {
return f.data.Seek(offset, whence)
}
func (f file) Close() error { return nil }
type compressFile struct {
name string
size int64
data *zstd.Decoder
content []byte
zstdPos int64
seekPos int64
}
var _ io.ReadSeeker = (*compressFile)(nil)
func (f *compressFile) Stat() (fs.FileInfo, error) {
return fileinfo{name: f.name, size: f.size}, nil
}
func (f *compressFile) Read(p []byte) (int, error) {
if f.zstdPos > f.seekPos {
if err := f.data.Reset(bytes.NewReader(f.content)); err != nil {
return 0, err
}
f.zstdPos = 0
}
if f.zstdPos < f.seekPos {
if _, err := io.CopyN(io.Discard, f.data, f.seekPos - f.zstdPos); err != nil {
return 0, err
}
f.zstdPos = f.seekPos
}
n, err := f.data.Read(p)
f.zstdPos += int64(n)
f.seekPos = f.zstdPos
return n, err
}
func (f *compressFile) Seek(offset int64, whence int) (int64, error) {
switch whence {
case io.SeekStart:
f.seekPos = 0 + offset
case io.SeekCurrent:
f.seekPos += offset
case io.SeekEnd:
f.seekPos = f.size + offset
}
return f.seekPos, nil
}
func (f *compressFile) Close() error {
f.data.Close()
return nil
}
func (f *compressFile) ZstdBytes() []byte { return f.content }
type fileinfo struct {
name string
size int64
}
func (f fileinfo) Name() string { return f.name }
func (f fileinfo) Size() int64 { return f.size }
func (f fileinfo) Mode() fs.FileMode { return 0o444 }
func (f fileinfo) ModTime() time.Time { return {{if .GlobalTime}}GlobalModTime(f.name){{else}}time.Unix(0, 0){{end}} }
func (f fileinfo) IsDir() bool { return false }
func (f fileinfo) Sys() any { return nil }
type direntry struct {
name string
isDir bool
}
func (d direntry) Name() string { return d.name }
func (d direntry) IsDir() bool { return d.isDir }
func (d direntry) Type() fs.FileMode {
if d.isDir {
return 0o755 | fs.ModeDir
}
return 0o444
}
func (direntry) Info() (fs.FileInfo, error) { return nil, fs.ErrNotExist }
`))

View file

@ -37,6 +37,7 @@ func TestLocalizationPolicy(t *testing.T) {
assert.Empty(t, checkLocaleContent([]byte("teams.specific_repositories_helper = Members will only have access to repositories explicitly added to the team. Selecting this <strong>will not</strong> automatically remove repositories already added with <i>All repositories</i>.")))
assert.Empty(t, checkLocaleContent([]byte("sqlite_helper = File path for the SQLite3 database.<br>Enter an absolute path if you run Forgejo as a service.")))
assert.Empty(t, checkLocaleContent([]byte("hi_user_x = Hi <b>%s</b>,")))
assert.Empty(t, checkLocaleContent([]byte("key = Press <kbd>Shift</kbd>")))
assert.Equal(t, []string{"error404: The page you are trying to reach either <strong\x1b[31m title='aaa'\x1b[0m>does not exist</strong> or <strong>you are not authorized</strong> to view it."}, checkLocaleContent([]byte("error404 = The page you are trying to reach either <strong title='aaa'>does not exist</strong> or <strong>you are not authorized</strong> to view it.")))
})

View file

@ -1,4 +1,3 @@
#! /bin/bash
# Heavily inspired by https://github.com/urfave/cli
_cli_bash_autocomplete() {
@ -7,9 +6,9 @@ _cli_bash_autocomplete() {
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
if [[ "$cur" == "-"* ]]; then
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-bash-completion )
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-shell-completion )
else
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion )
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-shell-completion )
fi
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0

View file

@ -9,9 +9,9 @@ _cli_zsh_autocomplete() {
local cur
cur=${words[-1]}
if [[ "$cur" == "-"* ]]; then
opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} ${cur} --generate-bash-completion)}")
opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} ${cur} --generate-shell-completion)}")
else
opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} --generate-bash-completion)}")
opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} --generate-shell-completion)}")
fi
if [[ "${opts[1]}" != "" ]]; then

View file

@ -1,45 +1,46 @@
Environment To Ini
==================
Multiple docker users have requested that the Gitea docker is changed
to permit arbitrary configuration via environment variables.
This tool allows defining Forgejo's entire configuration via environment
variables, mostly geared towards usage in Docker.
Gitea needs to use an ini file for configuration because the running
environment that starts the docker may not be the same as that used
by the hooks. An ini file also gives a good default and means that
users do not have to completely provide a full environment.
Forgejo needs to use an INI file for configuration because the running
environment that starts the container may not be the same as the one used
by the hooks. An INI file also gives a good default and means that
users do not have to provide the entire set of environment variables.
With those caveats above, this command provides a generic way of
converting suitably structured environment variables into any ini
value.
To use the command is very simple just run it and the default gitea
app.ini will be rewritten to take account of the variables provided,
however there are various options to give slightly different
behavior and these can be interrogated with the `-h` option.
When run, `environment-to-ini` will write the config files based on the
environment variables provided.
Check with the `-h` flag for several options to alter this behaviour.
The environment variables should be of the form:
Environment variables of the form "FORGEJO__SECTION_NAME__KEY_NAME"
will be mapped to the ini section "[section_name]" and the key
"KEY_NAME" with the value as provided.
GITEA__SECTION_NAME__KEY_NAME
Note, SECTION_NAME in the notation above is case-insensitive.
Environment variables of the form "FORGEJO__SECTION_NAME__KEY_NAME__FILE"
will be mapped to the ini section "[section_name]" and the key
"KEY_NAME" with the value loaded from the specified file.
Environment variables are usually restricted to a reduced character
set "0-9A-Z_" - in order to allow the setting of sections with
characters outside of that set, they should be escaped as following:
"_0X2E_" for "." and "_0X2D_" for "-". The entire section and key names
can be escaped as a UTF8 byte string if necessary. E.g. to configure:
"_0X2E_" for ".". The entire section and key names can be escaped as
a UTF8 byte string if necessary. E.g. to configure:
"""
...
[log.console]
COLORIZE=false
STDERR=true
...
"""
"""
...
[log.console]
COLORIZE=false
STDERR=true
...
"""
You would set the environment variables: "GITEA__LOG_0x2E_CONSOLE__COLORIZE=false"
and "GITEA__LOG_0x2E_CONSOLE__STDERR=false". Other examples can be found
You would set the environment variables: "FORGEJO__LOG_0x2E_CONSOLE__COLORIZE=false"
and "FORGEJO__LOG_0x2E_CONSOLE__STDERR=false". Other examples can be found
on the configuration cheat sheet.
To build locally, run:

View file

@ -1,5 +1,5 @@
import eslintCommunityEslintPluginEslintComments from '@eslint-community/eslint-plugin-eslint-comments';
import stylisticEslintPluginJs from '@stylistic/eslint-plugin-js';
import stylisticEslintPlugin from '@stylistic/eslint-plugin';
import vitest from '@vitest/eslint-plugin';
import arrayFunc from 'eslint-plugin-array-func';
import eslintPluginImportX from 'eslint-plugin-import-x';
@ -26,7 +26,7 @@ export default tseslint.config(
{
plugins: {
'@eslint-community/eslint-comments': eslintCommunityEslintPluginEslintComments,
'@stylistic/js': stylisticEslintPluginJs,
'@stylistic': stylisticEslintPlugin,
'@vitest': vitest,
'array-func': arrayFunc,
'no-jquery': noJquery,
@ -69,62 +69,62 @@ export default tseslint.config(
'@eslint-community/eslint-comments/no-unused-enable': [2],
'@eslint-community/eslint-comments/no-use': [0],
'@eslint-community/eslint-comments/require-description': [0],
'@stylistic/js/array-bracket-newline': [0],
'@stylistic/js/array-bracket-spacing': [2, 'never'],
'@stylistic/js/array-element-newline': [0],
'@stylistic/js/arrow-parens': [2, 'always'],
'@stylistic/array-bracket-newline': [0],
'@stylistic/array-bracket-spacing': [2, 'never'],
'@stylistic/array-element-newline': [0],
'@stylistic/arrow-parens': [2, 'always'],
'@stylistic/js/arrow-spacing': [2, {
'@stylistic/arrow-spacing': [2, {
before: true,
after: true,
}],
'@stylistic/js/block-spacing': [0],
'@stylistic/block-spacing': [0],
'@stylistic/js/brace-style': [2, '1tbs', {
'@stylistic/brace-style': [2, '1tbs', {
allowSingleLine: true,
}],
'@stylistic/js/comma-dangle': [2, 'always-multiline'],
'@stylistic/comma-dangle': [2, 'always-multiline'],
'@stylistic/js/comma-spacing': [2, {
'@stylistic/comma-spacing': [2, {
before: false,
after: true,
}],
'@stylistic/js/comma-style': [2, 'last'],
'@stylistic/js/computed-property-spacing': [2, 'never'],
'@stylistic/js/dot-location': [2, 'property'],
'@stylistic/js/eol-last': [2],
'@stylistic/js/function-call-spacing': [2, 'never'],
'@stylistic/js/function-call-argument-newline': [0],
'@stylistic/js/function-paren-newline': [0],
'@stylistic/js/generator-star-spacing': [0],
'@stylistic/js/implicit-arrow-linebreak': [0],
'@stylistic/comma-style': [2, 'last'],
'@stylistic/computed-property-spacing': [2, 'never'],
'@stylistic/dot-location': [2, 'property'],
'@stylistic/eol-last': [2],
'@stylistic/function-call-spacing': [2, 'never'],
'@stylistic/function-call-argument-newline': [0],
'@stylistic/function-paren-newline': [0],
'@stylistic/generator-star-spacing': [0],
'@stylistic/implicit-arrow-linebreak': [0],
'@stylistic/js/indent': [2, 2, {
'@stylistic/indent': [2, 2, {
ignoreComments: true,
SwitchCase: 1,
}],
'@stylistic/js/key-spacing': [2],
'@stylistic/js/keyword-spacing': [2],
'@stylistic/js/linebreak-style': [2, 'unix'],
'@stylistic/js/lines-around-comment': [0],
'@stylistic/js/lines-between-class-members': [0],
'@stylistic/js/max-len': [0],
'@stylistic/js/max-statements-per-line': [0],
'@stylistic/js/multiline-ternary': [0],
'@stylistic/js/new-parens': [2],
'@stylistic/js/newline-per-chained-call': [0],
'@stylistic/js/no-confusing-arrow': [0],
'@stylistic/js/no-extra-parens': [0],
'@stylistic/js/no-extra-semi': [2],
'@stylistic/js/no-floating-decimal': [0],
'@stylistic/js/no-mixed-operators': [0],
'@stylistic/js/no-mixed-spaces-and-tabs': [2],
'@stylistic/key-spacing': [2],
'@stylistic/keyword-spacing': [2],
'@stylistic/linebreak-style': [2, 'unix'],
'@stylistic/lines-around-comment': [0],
'@stylistic/lines-between-class-members': [0],
'@stylistic/max-len': [0],
'@stylistic/max-statements-per-line': [0],
'@stylistic/multiline-ternary': [0],
'@stylistic/new-parens': [2],
'@stylistic/newline-per-chained-call': [0],
'@stylistic/no-confusing-arrow': [0],
'@stylistic/no-extra-parens': [0],
'@stylistic/no-extra-semi': [2],
'@stylistic/no-floating-decimal': [0],
'@stylistic/no-mixed-operators': [0],
'@stylistic/no-mixed-spaces-and-tabs': [2],
'@stylistic/js/no-multi-spaces': [2, {
'@stylistic/no-multi-spaces': [2, {
ignoreEOLComments: true,
exceptions: {
@ -132,60 +132,60 @@ export default tseslint.config(
},
}],
'@stylistic/js/no-multiple-empty-lines': [2, {
'@stylistic/no-multiple-empty-lines': [2, {
max: 1,
maxEOF: 0,
maxBOF: 0,
}],
'@stylistic/js/no-tabs': [2],
'@stylistic/js/no-trailing-spaces': [2],
'@stylistic/js/no-whitespace-before-property': [2],
'@stylistic/js/nonblock-statement-body-position': [2],
'@stylistic/js/object-curly-newline': [0],
'@stylistic/js/object-curly-spacing': [2, 'never'],
'@stylistic/js/object-property-newline': [0],
'@stylistic/js/one-var-declaration-per-line': [0],
'@stylistic/js/operator-linebreak': [2, 'after'],
'@stylistic/js/padded-blocks': [2, 'never'],
'@stylistic/js/padding-line-between-statements': [0],
'@stylistic/js/quote-props': [0],
'@stylistic/no-tabs': [2],
'@stylistic/no-trailing-spaces': [2],
'@stylistic/no-whitespace-before-property': [2],
'@stylistic/nonblock-statement-body-position': [2],
'@stylistic/object-curly-newline': [0],
'@stylistic/object-curly-spacing': [2, 'never'],
'@stylistic/object-property-newline': [0],
'@stylistic/one-var-declaration-per-line': [0],
'@stylistic/operator-linebreak': [2, 'after'],
'@stylistic/padded-blocks': [2, 'never'],
'@stylistic/padding-line-between-statements': [0],
'@stylistic/quote-props': [0],
'@stylistic/js/quotes': [2, 'single', {
'@stylistic/quotes': [2, 'single', {
avoidEscape: true,
allowTemplateLiterals: true,
}],
'@stylistic/js/rest-spread-spacing': [2, 'never'],
'@stylistic/rest-spread-spacing': [2, 'never'],
'@stylistic/js/semi': [2, 'always', {
'@stylistic/semi': [2, 'always', {
omitLastInOneLineBlock: true,
}],
'@stylistic/js/semi-spacing': [2, {
'@stylistic/semi-spacing': [2, {
before: false,
after: true,
}],
'@stylistic/js/semi-style': [2, 'last'],
'@stylistic/js/space-before-blocks': [2, 'always'],
'@stylistic/semi-style': [2, 'last'],
'@stylistic/space-before-blocks': [2, 'always'],
'@stylistic/js/space-before-function-paren': [2, {
'@stylistic/space-before-function-paren': [2, {
anonymous: 'ignore',
named: 'never',
asyncArrow: 'always',
}],
'@stylistic/js/space-in-parens': [2, 'never'],
'@stylistic/js/space-infix-ops': [2],
'@stylistic/js/space-unary-ops': [2],
'@stylistic/js/spaced-comment': [2, 'always'],
'@stylistic/js/switch-colon-spacing': [2],
'@stylistic/js/template-curly-spacing': [2, 'never'],
'@stylistic/js/template-tag-spacing': [2, 'never'],
'@stylistic/js/wrap-iife': [2, 'inside'],
'@stylistic/js/wrap-regex': [0],
'@stylistic/js/yield-star-spacing': [2, 'after'],
'@stylistic/space-in-parens': [2, 'never'],
'@stylistic/space-infix-ops': [2],
'@stylistic/space-unary-ops': [2],
'@stylistic/spaced-comment': [2, 'always'],
'@stylistic/switch-colon-spacing': [2],
'@stylistic/template-curly-spacing': [2, 'never'],
'@stylistic/template-tag-spacing': [2, 'never'],
'@stylistic/wrap-iife': [2, 'inside'],
'@stylistic/wrap-regex': [0],
'@stylistic/yield-star-spacing': [2, 'after'],
'accessor-pairs': [2],
'array-callback-return': [2, {

6
flake.lock generated
View file

@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1733392399,
"narHash": "sha256-kEsTJTUQfQFIJOcLYFt/RvNxIK653ZkTBIs4DG+cBns=",
"lastModified": 1749285348,
"narHash": "sha256-frdhQvPbmDYaScPFiCnfdh3B/Vh81Uuoo0w5TkWmmjU=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "d0797a04b81caeae77bcff10a9dde78bc17f5661",
"rev": "3e3afe5174c561dee0df6f2c2b2236990146329f",
"type": "github"
},
"original": {

View file

@ -3,35 +3,20 @@
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = {
nixpkgs,
flake-utils,
...
}:
outputs =
{
nixpkgs,
flake-utils,
...
}:
flake-utils.lib.eachDefaultSystem (
system: let
system:
let
pkgs = nixpkgs.legacyPackages.${system};
in {
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
# generic
git
git-lfs
gnumake
gnused
gnutar
gzip
# frontend
nodejs_20
# backend
gofumpt
sqlite
go
gopls
];
};
in
{
devShells.default = import ./shell.nix { inherit pkgs; };
formatter = pkgs.nixfmt-rfc-style;
}
);
}

20
go.mod
View file

@ -41,14 +41,14 @@ require (
github.com/gliderlabs/ssh v0.3.8
github.com/go-ap/activitypub v0.0.0-20231114162308-e219254dc5c9
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73
github.com/go-chi/chi/v5 v5.2.1
github.com/go-chi/chi/v5 v5.2.2
github.com/go-chi/cors v1.2.1
github.com/go-co-op/gocron v1.37.0
github.com/go-enry/go-enry/v2 v2.9.2
github.com/go-git/go-git/v5 v5.13.2
github.com/go-ldap/ldap/v3 v3.4.6
github.com/go-openapi/spec v0.21.0
github.com/go-sql-driver/mysql v1.9.2
github.com/go-sql-driver/mysql v1.9.3
github.com/go-webauthn/webauthn v0.13.0
github.com/gobwas/glob v0.2.3
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
@ -76,7 +76,7 @@ require (
github.com/meilisearch/meilisearch-go v0.31.0
github.com/mholt/archiver/v3 v3.5.1
github.com/microcosm-cc/bluemonday v1.0.27
github.com/minio/minio-go/v7 v7.0.91
github.com/minio/minio-go/v7 v7.0.94
github.com/msteinert/pam/v2 v2.1.0
github.com/nektos/act v0.2.52
github.com/niklasfasching/go-org v1.8.0
@ -89,17 +89,15 @@ require (
github.com/robfig/cron/v3 v3.0.1
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
github.com/sergi/go-diff v1.4.0
github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92
github.com/stretchr/testify v1.10.0
github.com/syndtr/goleveldb v1.0.0
github.com/ulikunitz/xz v0.5.12
github.com/urfave/cli/v2 v2.27.6
github.com/urfave/cli/v3 v3.3.3
github.com/valyala/fastjson v1.6.4
github.com/yohcop/openid-go v1.0.1
github.com/yuin/goldmark v1.7.12
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
gitlab.com/gitlab-org/api/client-go v0.129.0
gitlab.com/gitlab-org/api/client-go v0.130.1
go.uber.org/mock v0.5.2
golang.org/x/crypto v0.39.0
golang.org/x/image v0.27.0
@ -153,7 +151,6 @@ require (
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/cyphar/filepath-securejoin v0.3.6 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect
@ -210,6 +207,7 @@ require (
github.com/nwaples/rardecode v1.1.3 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
@ -221,15 +219,13 @@ require (
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/skeema/knownhosts v1.3.0 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/tinylib/msgp v1.3.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
github.com/zeebo/assert v1.3.0 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
go.etcd.io/bbolt v1.4.0 // indirect
@ -246,9 +242,7 @@ require (
replace github.com/hashicorp/go-version => github.com/6543/go-version v1.3.1
replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0
replace github.com/nektos/act => code.forgejo.org/forgejo/act v1.26.0
replace github.com/nektos/act => code.forgejo.org/forgejo/act v1.28.0
replace github.com/mholt/archiver/v3 => code.forgejo.org/forgejo/archiver/v3 v3.5.1

36
go.sum
View file

@ -4,8 +4,8 @@ code.forgejo.org/f3/gof3/v3 v3.11.0 h1:f/xToKwqTgxG6PYxvewywjDQyCcyHEEJ6sZqUitFs
code.forgejo.org/f3/gof3/v3 v3.11.0/go.mod h1:4FaRUNSQGBiD1M0DuB0yNv+Z2wMtlOeckgygHSSq4KQ=
code.forgejo.org/forgejo-contrib/go-libravatar v0.0.0-20191008002943-06d1c002b251 h1:HTZl3CBk3ABNYtFI6TPLvJgGKFIhKT5CBk0sbOtkDKU=
code.forgejo.org/forgejo-contrib/go-libravatar v0.0.0-20191008002943-06d1c002b251/go.mod h1:PphB88CPbx601QrWPMZATeorACeVmQlyv3u+uUMbSaM=
code.forgejo.org/forgejo/act v1.26.0 h1:6mTmoaw7d/WpYiw/Pw6AaypxFdgJog5OFi/PMEgEbxs=
code.forgejo.org/forgejo/act v1.26.0/go.mod h1:HFDFrXPrqfM9aH2RCnMiBdo/3ThxDmZjp58InPjGOfo=
code.forgejo.org/forgejo/act v1.28.0 h1:96njNC7C1YNyjWq5OWvLZMF/nw0PMthzIA8Nwbnn7jo=
code.forgejo.org/forgejo/act v1.28.0/go.mod h1:dFuiwAmD5vyrzecysHB2kL/GM3wRpoVPl+WdbCTC8Bs=
code.forgejo.org/forgejo/archiver/v3 v3.5.1 h1:UmmbA7D5550uf71SQjarmrn6yKwOGxtEjb3jaYYtmSE=
code.forgejo.org/forgejo/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
code.forgejo.org/forgejo/go-rpmutils v1.0.0 h1:RZGGeKt70p/WaIEL97pyT6uiiEIoN8/aLmS5Z6WmX0M=
@ -152,8 +152,6 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM=
github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
@ -215,8 +213,8 @@ github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73/go.mod h1:jyveZeGw5La
github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA=
github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0=
@ -247,8 +245,8 @@ github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9Z
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
@ -384,8 +382,6 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/libdns/libdns v1.0.0-beta.1 h1:KIf4wLfsrEpXpZ3vmc/poM8zCATXT2klbdPe6hyOBjQ=
github.com/libdns/libdns v1.0.0-beta.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0 h1:F/3FfGmKdiKFa8kL3YrpZ7pe9H4l4AzA1pbaOUnRvPI=
github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0/go.mod h1:JEfTc3+2DF9Z4PXhLLvXL42zexJyh8rIq3OzUj/0rAk=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
@ -415,8 +411,8 @@ github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY
github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
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.91 h1:tWLZnEfo3OZl5PoXQwcwTAPNNrjyWwOh6cbZitW5JQc=
github.com/minio/minio-go/v7 v7.0.91/go.mod h1:uvMUcGrpgeSAAI6+sD3818508nUyMULw94j2Nxku/Go=
github.com/minio/minio-go/v7 v7.0.94 h1:1ZoksIKPyaSt64AVOyaQvhDOgVC3MfZsWM6mZXRUGtM=
github.com/minio/minio-go/v7 v7.0.94/go.mod h1:71t2CqDt3ThzESgZUlU1rBN54mksGGlkLcFgguDnnAc=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -459,6 +455,8 @@ github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3I
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
@ -497,15 +495,11 @@ github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c h1:aqg5Vm5dwtvL+YgDpBcK1ITf3o96N/K7/wsRXQnUTEs=
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c/go.mod h1:owqhoLW1qZoYLZzLnBw+QkPP9WZnjlSWihhxAJC1+/M=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
@ -529,12 +523,12 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/urfave/cli/v3 v3.3.3 h1:byCBaVdIXuLPIDm5CYZRVG6NvT7tv1ECqdU4YzlEa3I=
github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
@ -545,8 +539,6 @@ github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yohcop/openid-go v1.0.1 h1:DPRd3iPO5F6O5zX2e62XpVAbPT6wV51cuucH0z9g3js=
@ -564,8 +556,8 @@ github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
gitlab.com/gitlab-org/api/client-go v0.129.0 h1:o9KLn6fezmxBQWYnQrnilwyuOjlx4206KP0bUn3HuBE=
gitlab.com/gitlab-org/api/client-go v0.129.0/go.mod h1:ZhSxLAWadqP6J9lMh40IAZOlOxBLPRh7yFOXR/bMJWM=
gitlab.com/gitlab-org/api/client-go v0.130.1 h1:1xF5C5Zq3sFeNg3PzS2z63oqrxifne3n/OnbI7nptRc=
gitlab.com/gitlab-org/api/client-go v0.130.1/go.mod h1:ZhSxLAWadqP6J9lMh40IAZOlOxBLPRh7yFOXR/bMJWM=
go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=

View file

@ -21,7 +21,7 @@ import (
_ "forgejo.org/modules/markup/markdown"
_ "forgejo.org/modules/markup/orgmode"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
// these flags will be set by the build flags

View file

@ -55,6 +55,7 @@ type ActionRun struct {
PreviousDuration time.Duration
Created timeutil.TimeStamp `xorm:"created"`
Updated timeutil.TimeStamp `xorm:"updated"`
NotifyEmail bool
}
func init() {

View file

@ -442,6 +442,12 @@ func (a *Action) GetIssueContent(ctx context.Context) string {
return a.Issue.Content
}
func GetActivityByID(ctx context.Context, id int64) (*Action, error) {
var act Action
_, err := db.GetEngine(ctx).ID(id).Get(&act)
return &act, err
}
// GetFeedsOptions options for retrieving feeds
type GetFeedsOptions struct {
db.ListOptions
@ -467,11 +473,8 @@ func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, int64, err
return nil, 0, err
}
sess := db.GetEngine(ctx).Where(cond).
Select("`action`.*"). // this line will avoid select other joined table's columns
Join("INNER", "repository", "`repository`.id = `action`.repo_id")
opts.SetDefaultValues()
sess := db.GetEngine(ctx).Where(cond)
sess = db.SetSessionPagination(sess, &opts)
actions := make([]*Action, 0, opts.PageSize)
@ -598,13 +601,14 @@ func DeleteOldActions(ctx context.Context, olderThan time.Duration) (err error)
}
// NotifyWatchers creates batch of actions for every watcher.
func NotifyWatchers(ctx context.Context, actions ...*Action) error {
func NotifyWatchers(ctx context.Context, actions ...*Action) ([]Action, error) {
var watchers []*repo_model.Watch
var repo *repo_model.Repository
var err error
var permCode []bool
var permIssue []bool
var permPR []bool
var out []Action
e := db.GetEngine(ctx)
@ -615,14 +619,14 @@ func NotifyWatchers(ctx context.Context, actions ...*Action) error {
// Add feeds for user self and all watchers.
watchers, err = repo_model.GetWatchers(ctx, act.RepoID)
if err != nil {
return fmt.Errorf("get watchers: %w", err)
return nil, fmt.Errorf("get watchers: %w", err)
}
// Be aware that optimizing this correctly into the `GetWatchers` SQL
// query is for most cases less performant than doing this.
blockedDoerUserIDs, err := user_model.ListBlockedByUsersID(ctx, act.ActUserID)
if err != nil {
return fmt.Errorf("user_model.ListBlockedByUsersID: %w", err)
return nil, fmt.Errorf("user_model.ListBlockedByUsersID: %w", err)
}
if len(blockedDoerUserIDs) > 0 {
@ -637,8 +641,9 @@ func NotifyWatchers(ctx context.Context, actions ...*Action) error {
// Add feed for actioner.
act.UserID = act.ActUserID
if _, err = e.Insert(act); err != nil {
return fmt.Errorf("insert new actioner: %w", err)
return nil, fmt.Errorf("insert new actioner: %w", err)
}
out = append(out, *act)
if repoChanged {
act.loadRepo(ctx)
@ -646,7 +651,7 @@ func NotifyWatchers(ctx context.Context, actions ...*Action) error {
// check repo owner exist.
if err := act.Repo.LoadOwner(ctx); err != nil {
return fmt.Errorf("can't get repo owner: %w", err)
return nil, fmt.Errorf("can't get repo owner: %w", err)
}
} else if act.Repo == nil {
act.Repo = repo
@ -657,7 +662,7 @@ func NotifyWatchers(ctx context.Context, actions ...*Action) error {
act.ID = 0
act.UserID = act.Repo.Owner.ID
if err = db.Insert(ctx, act); err != nil {
return fmt.Errorf("insert new actioner: %w", err)
return nil, fmt.Errorf("insert new actioner: %w", err)
}
}
@ -710,26 +715,29 @@ func NotifyWatchers(ctx context.Context, actions ...*Action) error {
}
if err = db.Insert(ctx, act); err != nil {
return fmt.Errorf("insert new action: %w", err)
return nil, fmt.Errorf("insert new action: %w", err)
}
}
}
return nil
return out, nil
}
// NotifyWatchersActions creates batch of actions for every watcher.
func NotifyWatchersActions(ctx context.Context, acts []*Action) error {
func NotifyWatchersActions(ctx context.Context, acts []*Action) ([]Action, error) {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
return nil, err
}
defer committer.Close()
var out []Action
for _, act := range acts {
if err := NotifyWatchers(ctx, act); err != nil {
return err
as, err := NotifyWatchers(ctx, act)
if err != nil {
return nil, err
}
out = append(out, as...)
}
return committer.Commit()
return out, committer.Commit()
}
// DeleteIssueActions delete all actions related with issueID

View file

@ -197,7 +197,8 @@ func TestNotifyWatchers(t *testing.T) {
RepoID: 1,
OpType: activities_model.ActionStarRepo,
}
require.NoError(t, activities_model.NotifyWatchers(db.DefaultContext, action))
_, err := activities_model.NotifyWatchers(db.DefaultContext, action)
require.NoError(t, err)
// One watchers are inactive, thus action is only created for user 8, 1, 4, 11
unittest.AssertExistsAndLoadBean(t, &activities_model.Action{
@ -226,24 +227,6 @@ func TestNotifyWatchers(t *testing.T) {
})
}
func TestGetFeedsCorrupted(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
unittest.AssertExistsAndLoadBean(t, &activities_model.Action{
ID: 8,
RepoID: 1700,
})
actions, count, err := activities_model.GetFeeds(db.DefaultContext, activities_model.GetFeedsOptions{
RequestedUser: user,
Actor: user,
IncludePrivate: true,
})
require.NoError(t, err)
assert.Empty(t, actions)
assert.Equal(t, int64(0), count)
}
func TestConsistencyUpdateAction(t *testing.T) {
if !setting.Database.Type.IsSQLite3() {
t.Skip("Test is only for SQLite database.")

View file

@ -0,0 +1,106 @@
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package activities
import (
"context"
"fmt"
"forgejo.org/models/db"
user_model "forgejo.org/models/user"
"forgejo.org/modules/json"
"forgejo.org/modules/log"
"forgejo.org/modules/timeutil"
"forgejo.org/modules/validation"
ap "github.com/go-ap/activitypub"
)
type FederatedUserActivity struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"NOT NULL"`
ActorID int64
ActorURI string
Actor *user_model.User `xorm:"-"` // transient
NoteContent string `xorm:"TEXT"`
NoteURL string `xorm:"VARCHAR(255)"`
OriginalNote string `xorm:"TEXT"`
Created timeutil.TimeStamp `xorm:"created"`
}
func init() {
db.RegisterModel(new(FederatedUserActivity))
}
func NewFederatedUserActivity(userID, actorID int64, actorURI, noteContent, noteURL string, originalNote ap.Activity) (FederatedUserActivity, error) {
jsonString, err := json.Marshal(originalNote)
if err != nil {
return FederatedUserActivity{}, err
}
result := FederatedUserActivity{
UserID: userID,
ActorID: actorID,
ActorURI: actorURI,
NoteContent: noteContent,
NoteURL: noteURL,
OriginalNote: string(jsonString),
}
if valid, err := validation.IsValid(result); !valid {
return FederatedUserActivity{}, err
}
return result, nil
}
func (federatedUserActivity FederatedUserActivity) Validate() []string {
var result []string
result = append(result, validation.ValidateNotEmpty(federatedUserActivity.UserID, "UserID")...)
result = append(result, validation.ValidateNotEmpty(federatedUserActivity.ActorID, "ActorID")...)
result = append(result, validation.ValidateNotEmpty(federatedUserActivity.ActorURI, "ActorURI")...)
result = append(result, validation.ValidateNotEmpty(federatedUserActivity.NoteContent, "NoteContent")...)
result = append(result, validation.ValidateNotEmpty(federatedUserActivity.NoteURL, "NoteURL")...)
result = append(result, validation.ValidateNotEmpty(federatedUserActivity.OriginalNote, "OriginalNote")...)
return result
}
func CreateUserActivity(ctx context.Context, federatedUserActivity *FederatedUserActivity) error {
if valid, err := validation.IsValid(federatedUserActivity); !valid {
return err
}
_, err := db.GetEngine(ctx).Insert(federatedUserActivity)
return err
}
type GetFollowingFeedsOptions struct {
db.ListOptions
}
func GetFollowingFeeds(ctx context.Context, actorID int64, opts GetFollowingFeedsOptions) ([]*FederatedUserActivity, int64, error) {
log.Debug("user_id = %s", actorID)
sess := db.GetEngine(ctx).Where("user_id = ?", actorID)
opts.SetDefaultValues()
sess = db.SetSessionPagination(sess, &opts)
actions := make([]*FederatedUserActivity, 0, opts.PageSize)
count, err := sess.FindAndCount(&actions)
if err != nil {
return nil, 0, fmt.Errorf("FindAndCount: %w", err)
}
for _, act := range actions {
if err := act.loadActor(ctx); err != nil {
return nil, 0, err
}
}
return actions, count, err
}
func (federatedUserActivity *FederatedUserActivity) loadActor(ctx context.Context) error {
log.Debug("for activity %s", federatedUserActivity)
actorUser, _, err := user_model.GetFederatedUserByUserID(ctx, federatedUserActivity.ActorID)
if err != nil {
return err
}
federatedUserActivity.Actor = actorUser
return nil
}

View file

@ -0,0 +1,24 @@
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package activities
import (
"testing"
"forgejo.org/modules/validation"
)
func Test_FederatedUserActivityValidation(t *testing.T) {
sut := FederatedUserActivity{}
sut.UserID = 13
sut.ActorID = 33
sut.ActorURI = "33"
sut.NoteContent = "Any content!"
sut.NoteURL = "https://example.org/note/17"
sut.OriginalNote = "federatedUserActivityNote-17"
if res, _ := validation.IsValid(sut); !res {
t.Errorf("sut expected to be valid: %v\n", sut.Validate())
}
}

View file

@ -414,7 +414,7 @@ func IsErrSHAOrCommitIDNotProvided(err error) bool {
}
func (err ErrSHAOrCommitIDNotProvided) Error() string {
return "a SHA or commit ID must be proved when updating a file"
return "a SHA or commit ID must be provided when updating a file"
}
// ErrInvalidMergeStyle represents an error if merging with disabled merge strategy

View file

@ -59,14 +59,6 @@
created_unix: 1603011540 # grouped with id:7
- id: 8
user_id: 1
op_type: 12 # close issue
act_user_id: 1
repo_id: 1700 # dangling intentional
is_private: false
created_unix: 1603011541
- id: 9
user_id: 34
op_type: 12 # close issue
act_user_id: 34

View file

@ -113,3 +113,344 @@
review_id: 22
assignee_id: 5
created_unix: 946684817
-
id: 13
type: 29 # push
poster_id: 2
issue_id: 19 # in repo_id 58
content: '{"is_force_push":false,"commit_ids":["4ca8bcaf27e28504df7bf996819665986b01c847","96cef4a7b72b3c208340ae6f0cf55a93e9077c93","c5626fc9eff57eb1bb7b796b01d4d0f2f3f792a2"]}'
created_unix: 1688672373
-
id: 14
type: 29 # push
poster_id: 2
issue_id: 19 # in repo_id 58
content: '{"is_force_push":false,"commit_ids":["23576dd018294e476c06e569b6b0f170d0558705"]}'
created_unix: 1688672374
-
id: 15
type: 29 # push
poster_id: 2
issue_id: 19 # in repo_id 58
content: '{"is_force_push":false,"commit_ids":["3e64625bd6eb5bcba69ac97de6c8f507402df861", "c704db5794097441aa2d9dd834d5b7e2f8f08108"]}'
created_unix: 1688672375
-
id: 16
type: 29 # push
poster_id: 2
issue_id: 19 # in repo_id 58
content: '{"is_force_push":false,"commit_ids":["811d46c7e518f4f180afb862c0db5cb8c80529ce", "747ddb3506a4fa04a7747808eb56ae16f9e933dc", "837d5c8125633d7d258f93b998e867eab0145520", "1978192d98bb1b65e11c2cf37da854fbf94bffd6"]}'
created_unix: 1688672376
-
id: 17
type: 29 # push
poster_id: 2
issue_id: 19 # in repo_id 58
content: '{"is_force_push":true,"commit_ids":["1978192d98bb1b65e11c2cf37da854fbf94bffd6", "9b93963cf6de4dc33f915bb67f192d099c301f43"]}'
created_unix: 1749734240
-
id: 2000
type: 8 # milestone
poster_id: 1
issue_id: 1 # in repo_id 1
milestone_id: 1
old_milestone_id: 0
created_unix: 946684820
-
id: 2001
type: 8 # milestone
poster_id: 1
issue_id: 1 # in repo_id 1
milestone_id: 2
old_milestone_id: 1
created_unix: 946684920
-
id: 2002
type: 8 # milestone
poster_id: 1
issue_id: 1 # in repo_id 1
milestone_id: 0
old_milestone_id: 2
created_unix: 946685020
-
id: 2003
type: 8 # milestone
poster_id: 1
issue_id: 1 # in repo_id 1
milestone_id: 10 # not exsting milestone
old_milestone_id: 0
created_unix: 946685080
-
id: 2010
type: 30 # project
poster_id: 1
issue_id: 1 # in repo_id 1
project_id: 1
old_project_id: 0
created_unix: 946685120
-
id: 2011
type: 30 # project
poster_id: 1
issue_id: 1 # in repo_id 1
project_id: 2
old_project_id: 1
created_unix: 946685220
-
id: 2012
type: 30 # project
poster_id: 1
issue_id: 1 # in repo_id 1
project_id: 0
old_project_id: 2
created_unix: 946685320
-
id: 2013
type: 30 # project
poster_id: 1
issue_id: 1 # in repo_id 1
project_id: 10 # not existing project
old_project_id: 0
created_unix: 946685420
-
id: 2020
type: 7 # label
poster_id: 1
issue_id: 1 # in repo_id 1
label_id: 1
content: 1 # add label
created_unix: 946685520
-
id: 2021
type: 7 # label
poster_id: 1
issue_id: 1
label_id: 2
content: 1 # add label
created_unix: 946685620
-
id: 2022
type: 7 # label
poster_id: 2
issue_id: 1 # in repo_id 1
label_id: 1
content: 0 # remove label
created_unix: 946685720
-
id: 2023
type: 7 # label
poster_id: 1
issue_id: 1 # in repo_id 1
label_id: 1
content: 1 # add label
created_unix: 946685720
-
id: 2024
type: 7 # label
poster_id: 1
issue_id: 1 # in repo_id 1
label_id: 2
content: 0 # remove label
created_unix: 946685720
-
id: 2025
type: 7 # label
poster_id: 2
issue_id: 1 # in repo_id 1
label_id: 2
content: 1 # add label
created_unix: 946685820
-
id: 2026
type: 7 # label
poster_id: 1
issue_id: 1 # in repo_id 1
label_id: 1
content: 0 # remove label
created_unix: 946685920
-
id: 2027
type: 7 # label
poster_id: 1
issue_id: 1 # in repo_id 1
label_id: 2
content: 0 # remove label
created_unix: 946685920
- id: 2040
type: 9 # assignee
poster_id: 1
issue_id: 1 # in repo_id 1
assignee_id: 1 # self
removed_assignee: false # add assignee
created_unix: 946688020
- id: 2041
type: 9 # assignee
poster_id: 2
issue_id: 1 # in repo_id 1
assignee_id: 1
removed_assignee: true # remove assignee
created_unix: 946688120
- id: 2042
type: 9 # assignee
poster_id: 1
issue_id: 1 # in repo_id 1
assignee_id: 2
removed_assignee: false # add assignee
created_unix: 946688220
- id: 2043
type: 9 # assignee
poster_id: 2
issue_id: 1 # in repo_id 1
assignee_id: 2 # self
removed_assignee: true # remove assignee
created_unix: 946688320
- id: 2050
type: 23 # lock
poster_id: 1
issue_id: 1 # in repo_id 1
created_unix: 946688420
- id: 2051
type: 24 # unlock
poster_id: 1
issue_id: 1 # in repo_id 1
created_unix: 946688520
- id: 2052
type: 23 # lock
poster_id: 1
issue_id: 1 # in repo_id 1
content: "Too heated"
created_unix: 946688620
- id: 2053
type: 24 # unlock
poster_id: 1
issue_id: 1 # in repo_id 1
created_unix: 946688720
- id: 2060
type: 36 # pin
poster_id: 1
issue_id: 1 # in repo_id 1
created_unix: 946688820
- id: 2061
type: 37 # unpin
poster_id: 1
issue_id: 1 # in repo_id 1
created_unix: 946688920
- id: 2070
type: 2 # close
poster_id: 1
issue_id: 1 # in repo_id 1
created_unix: 946689020
- id: 2071
type: 1 # reopen
poster_id: 2
issue_id: 1 # in repo_id 1
created_unix: 946689220
- id: 2072
type: 2 # close
poster_id: 1
issue_id: 2 # pull in repo_id 1
created_unix: 946689320
- id: 2073
type: 1 # reopen
poster_id: 2
issue_id: 2 # pull in repo_id 1
created_unix: 946689420
- id: 2080
type: 3 # issue reference
poster_id: 1
issue_id: 1 # issue in repo_id 1
ref_repo_id: 1
ref_issue_id: 5 # issue in repo_id 1
created_unix: 946689500
- id: 2081
type: 3 # issue reference
poster_id: 1
issue_id: 1 # issue in repo_id 1
ref_repo_id: 1
ref_issue_id: 2 # pull in repo_id 1
created_unix: 946689600
- id: 2082
type: 3 # issue reference
poster_id: 1
issue_id: 1 # issue in repo_id 1
ref_repo_id: 32
ref_issue_id: 16 # issue in repo_id 32
created_unix: 946689700
- id: 2083
type: 3 # issue reference
poster_id: 1
issue_id: 1 # issue in repo_id 1
ref_repo_id: 10
ref_issue_id: 8 # pull in repo_id 10
created_unix: 946689800
- id: 2090
type: 6 # pull reference
poster_id: 1
issue_id: 2 # pull in repo_id 1
ref_repo_id: 1
ref_issue_id: 1 # issue in repo_id 1
created_unix: 946689900
- id: 2091
type: 6 # pull reference
poster_id: 1
issue_id: 2 # pull in repo_id 1
ref_repo_id: 1
ref_issue_id: 2 # pull in repo_id 1
created_unix: 946690000
- id: 2092
type: 6 # pull reference
poster_id: 1
issue_id: 2 # pull in repo_id 1
ref_repo_id: 32
ref_issue_id: 16 # issue in repo_id 32
created_unix: 946690050
- id: 2093
type: 6 # pull reference
poster_id: 1
issue_id: 2 # pull in repo_id 1
ref_repo_id: 10
ref_issue_id: 8 # pull in repo_id 10
created_unix: 946690100

View file

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

View file

@ -6,6 +6,7 @@ package forgefed
import (
"database/sql"
"fmt"
"net/url"
"strings"
"time"
@ -17,9 +18,9 @@ import (
// swagger:model
type FederationHost struct {
ID int64 `xorm:"pk autoincr"`
HostFqdn string `xorm:"host_fqdn UNIQUE INDEX VARCHAR(255) NOT NULL"`
HostFqdn string `xorm:"host_fqdn UNIQUE(federation_host) INDEX VARCHAR(255) NOT NULL"`
HostPort uint16 `xorm:" UNIQUE(federation_host) INDEX NOT NULL DEFAULT 443"`
NodeInfo NodeInfo `xorm:"extends NOT NULL"`
HostPort uint16 `xorm:"NOT NULL DEFAULT 443"`
HostSchema string `xorm:"NOT NULL DEFAULT 'https'"`
LatestActivity time.Time `xorm:"NOT NULL"`
KeyID sql.NullString `xorm:"key_id UNIQUE"`
@ -42,6 +43,13 @@ func NewFederationHost(hostFqdn string, nodeInfo NodeInfo, port uint16, schema s
return result, nil
}
func (host FederationHost) AsURL() url.URL {
return url.URL{
Scheme: host.HostSchema,
Host: fmt.Sprintf("%v:%v", host.HostFqdn, host.HostPort),
}
}
// Validate collects error strings in a slice and returns this
func (host FederationHost) Validate() []string {
var result []string

View file

@ -17,12 +17,14 @@ type (
)
const (
ForgejoSourceType SoftwareNameType = "forgejo"
GiteaSourceType SoftwareNameType = "gitea"
ForgejoSourceType SoftwareNameType = "forgejo"
GiteaSourceType SoftwareNameType = "gitea"
MastodonSourceType SoftwareNameType = "mastodon"
GoToSocialSourceType SoftwareNameType = "gotosocial"
)
var KnownSourceTypes = []any{
ForgejoSourceType, GiteaSourceType,
ForgejoSourceType, GiteaSourceType, MastodonSourceType, GoToSocialSourceType,
}
// ------------------------------------------------ NodeInfoWellKnown ------------------------------------------------

View file

@ -103,6 +103,12 @@ var migrations = []*Migration{
NewMigration("Normalize repository.topics to empty slice instead of null", SetTopicsAsEmptySlice),
// v31 -> v32
NewMigration("Migrate maven package name concatenation", ChangeMavenArtifactConcatenation),
// v32 -> v33
NewMigration("Add federated user activity tables, update the `federated_user` table & add indexes", FederatedUserActivityMigration),
// v33 -> v34
NewMigration("Add `notify-email` column to `action_run` table", AddNotifyEmailToActionRun),
// v34 -> v35
NewMigration("Add index to `stopped` column in `action_run` table", AddIndexToActionRunStopped),
}
// GetCurrentDBVersion returns the current Forgejo database version.

View file

@ -0,0 +1,126 @@
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package forgejo_migrations //nolint:revive
import (
"fmt"
"forgejo.org/modules/log"
"forgejo.org/modules/timeutil"
"xorm.io/xorm"
)
func dropOldFederationHostIndexes(x *xorm.Engine) {
// drop unique index on HostFqdn
type FederationHost struct {
ID int64 `xorm:"pk autoincr"`
HostFqdn string `xorm:"host_fqdn UNIQUE INDEX VARCHAR(255) NOT NULL"`
}
err := x.DropIndexes(FederationHost{})
if err != nil {
log.Warn("migration[33]: There was an issue: %v", err)
return
}
}
func addFederatedUserActivityTables(x *xorm.Engine) {
type FederatedUserActivity struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"NOT NULL INDEX user_id"`
ActorID int64
ActorURI string
NoteContent string `xorm:"TEXT"`
NoteURL string `xorm:"VARCHAR(255)"`
OriginalNote string `xorm:"TEXT"`
Created timeutil.TimeStamp `xorm:"created"`
}
// add unique index on HostFqdn+HostPort
type FederationHost struct {
ID int64 `xorm:"pk autoincr"`
HostFqdn string `xorm:"host_fqdn UNIQUE(federation_host) INDEX VARCHAR(255) NOT NULL"`
HostPort uint16 `xorm:"UNIQUE(federation_host) INDEX NOT NULL DEFAULT 443"`
}
type FederatedUserFollower struct {
ID int64 `xorm:"pk autoincr"`
FollowedUserID int64 `xorm:"NOT NULL unique(fuf_rel)"`
FollowingUserID int64 `xorm:"NOT NULL unique(fuf_rel)"`
}
// Add InboxPath to FederatedUser & add index fo UserID
type FederatedUser struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"NOT NULL INDEX user_id"`
InboxPath string
}
var err error
err = x.Sync(&FederationHost{})
if err != nil {
log.Warn("migration[33]: There was an issue: %v", err)
return
}
err = x.Sync(&FederatedUserActivity{})
if err != nil {
log.Warn("migration[33]: There was an issue: %v", err)
return
}
err = x.Sync(&FederatedUserFollower{})
if err != nil {
log.Warn("migration[33]: There was an issue: %v", err)
return
}
err = x.Sync(&FederatedUser{})
if err != nil {
log.Warn("migration[33]: There was an issue: %v", err)
return
}
// Migrate
sessMigration := x.NewSession()
defer sessMigration.Close()
if err := sessMigration.Begin(); err != nil {
log.Warn("migration[33]: There was an issue: %v", err)
return
}
federatedUsers := make([]*FederatedUser, 0)
err = sessMigration.OrderBy("id").Find(&federatedUsers)
if err != nil {
log.Warn("migration[33]: There was an issue: %v", err)
return
}
for _, federatedUser := range federatedUsers {
if federatedUser.InboxPath != "" {
log.Info("migration[33]: This user was already migrated: %v", federatedUser)
} else {
// Migrate User.InboxPath
sql := "UPDATE `federated_user` SET `inbox_path` = ? WHERE `id` = ?"
if _, err := sessMigration.Exec(sql, fmt.Sprintf("/api/v1/activitypub/user-id/%v/inbox", federatedUser.UserID), federatedUser.ID); err != nil {
log.Warn("migration[33]: There was an issue: %v", err)
return
}
}
}
err = sessMigration.Commit()
if err != nil {
log.Warn("migration[33]: There was an issue: %v", err)
return
}
}
func FederatedUserActivityMigration(x *xorm.Engine) error {
dropOldFederationHostIndexes(x)
addFederatedUserActivityTables(x)
return nil
}

View file

@ -0,0 +1,46 @@
// Copyright 2025 The Forgejo Authors.
// SPDX-License-Identifier: GPL-3.0-or-later
package forgejo_migrations //nolint:revive
import (
"testing"
"time"
migration_tests "forgejo.org/models/migrations/test"
"forgejo.org/modules/log"
ft "forgejo.org/modules/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_FederatedUserActivityMigration(t *testing.T) {
lc, cl := ft.NewLogChecker(log.DEFAULT, log.WARN)
lc.Filter("migration[33]")
defer cl()
// intentionally conflicting definition
type FederatedUser struct {
ID int64 `xorm:"pk autoincr"`
UserID string
}
// Prepare TestEnv
x, deferable := migration_tests.PrepareTestEnv(t, 0,
new(FederatedUser),
)
sessTest := x.NewSession()
sessTest.Insert(FederatedUser{UserID: "1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" +
"1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" +
"1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"})
sessTest.Commit()
defer deferable()
if x == nil || t.Failed() {
return
}
require.NoError(t, FederatedUserActivityMigration(x))
logFiltered, _ := lc.Check(5 * time.Second)
assert.NotEmpty(t, logFiltered)
}

View file

@ -0,0 +1,14 @@
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later
package forgejo_migrations //nolint:revive
import "xorm.io/xorm"
func AddNotifyEmailToActionRun(x *xorm.Engine) error {
type ActionRun struct {
ID int64
NotifyEmail bool
}
return x.Sync(new(ActionRun))
}

View file

@ -0,0 +1,19 @@
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later
package forgejo_migrations //nolint:revive
import (
"forgejo.org/modules/timeutil"
"xorm.io/xorm"
)
func AddIndexToActionRunStopped(x *xorm.Engine) error {
type ActionRun struct {
ID int64
Stopped timeutil.TimeStamp `xorm:"index"`
}
return x.Sync(&ActionRun{})
}

View file

@ -179,25 +179,6 @@ func (status *CommitStatus) LocaleString(lang translation.Locale) string {
return lang.TrString("repo.commitstatus." + status.State.String())
}
// HideActionsURL set `TargetURL` to an empty string if the status comes from Gitea Actions
func (status *CommitStatus) HideActionsURL(ctx context.Context) {
if status.RepoID == 0 {
return
}
if status.Repo == nil {
if err := status.loadRepository(ctx); err != nil {
log.Error("loadRepository: %v", err)
return
}
}
prefix := fmt.Sprintf("%s/actions", status.Repo.Link())
if strings.HasPrefix(status.TargetURL, prefix) {
status.TargetURL = ""
}
}
// CalcCommitStatus returns commit status state via some status, the commit statues should order by id desc
func CalcCommitStatus(statuses []*CommitStatus) *CommitStatus {
if len(statuses) == 0 {
@ -453,11 +434,19 @@ type SignCommitWithStatuses struct {
*asymkey_model.SignCommit
}
// ParseCommitsWithStatus checks commits latest statuses and calculates its worst status state
func ParseCommitsWithStatus(ctx context.Context, oldCommits []*asymkey_model.SignCommit, repo *repo_model.Repository) []*SignCommitWithStatuses {
newCommits := make([]*SignCommitWithStatuses, 0, len(oldCommits))
// ParseCommitsWithStatus converts git commits into SignCommitWithStatuses (checks signature and calculates its worst status state)
func ParseCommitsWithStatus(ctx context.Context, commits []*git.Commit, repo *repo_model.Repository) []*SignCommitWithStatuses {
commitsWithSignature := asymkey_model.ParseCommitsWithSignature(
ctx,
user_model.ValidateCommitsWithEmails(ctx, commits),
repo.GetTrustModel(),
func(user *user_model.User) (bool, error) {
return repo_model.IsOwnerMemberCollaborator(ctx, repo, user.ID)
},
)
for _, c := range oldCommits {
commitsWithStatus := make([]*SignCommitWithStatuses, 0, len(commitsWithSignature))
for _, c := range commitsWithSignature {
commit := &SignCommitWithStatuses{
SignCommit: c,
}
@ -469,43 +458,12 @@ func ParseCommitsWithStatus(ctx context.Context, oldCommits []*asymkey_model.Sig
commit.Status = CalcCommitStatus(statuses)
}
newCommits = append(newCommits, commit)
commitsWithStatus = append(commitsWithStatus, commit)
}
return newCommits
return commitsWithStatus
}
// hashCommitStatusContext hash context
func hashCommitStatusContext(context string) string {
return fmt.Sprintf("%x", sha1.Sum([]byte(context)))
}
// ConvertFromGitCommit converts git commits into SignCommitWithStatuses
func ConvertFromGitCommit(ctx context.Context, commits []*git.Commit, repo *repo_model.Repository) []*SignCommitWithStatuses {
return ParseCommitsWithStatus(ctx,
asymkey_model.ParseCommitsWithSignature(
ctx,
user_model.ValidateCommitsWithEmails(ctx, commits),
repo.GetTrustModel(),
func(user *user_model.User) (bool, error) {
return repo_model.IsOwnerMemberCollaborator(ctx, repo, user.ID)
},
),
repo,
)
}
// CommitStatusesHideActionsURL hide Gitea Actions urls
func CommitStatusesHideActionsURL(ctx context.Context, statuses []*CommitStatus) {
idToRepos := make(map[int64]*repo_model.Repository)
for _, status := range statuses {
if status == nil {
continue
}
if status.Repo == nil {
status.Repo = idToRepos[status.RepoID]
}
status.HideActionsURL(ctx)
idToRepos[status.RepoID] = status.Repo
}
}

View file

@ -4,11 +4,9 @@
package git_test
import (
"fmt"
"testing"
"time"
actions_model "forgejo.org/models/actions"
"forgejo.org/models/db"
git_model "forgejo.org/models/git"
repo_model "forgejo.org/models/repo"
@ -246,26 +244,3 @@ func TestFindRepoRecentCommitStatusContexts(t *testing.T) {
assert.Equal(t, "compliance/lint-backend", contexts[0])
}
}
func TestCommitStatusesHideActionsURL(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 791, RepoID: repo.ID})
require.NoError(t, run.LoadAttributes(db.DefaultContext))
statuses := []*git_model.CommitStatus{
{
RepoID: repo.ID,
TargetURL: fmt.Sprintf("%s/jobs/%d", run.Link(), run.Index),
},
{
RepoID: repo.ID,
TargetURL: "https://mycicd.org/1",
},
}
git_model.CommitStatusesHideActionsURL(db.DefaultContext, statuses)
assert.Empty(t, statuses[0].TargetURL)
assert.Equal(t, "https://mycicd.org/1", statuses[1].TargetURL)
}

View file

@ -802,7 +802,7 @@ func (c *Comment) LoadPushCommits(ctx context.Context) (err error) {
}
defer closer.Close()
c.Commits = git_model.ConvertFromGitCommit(ctx, gitRepo.GetCommitsFromIDs(data.CommitIDs), c.Issue.Repo)
c.Commits = git_model.ParseCommitsWithStatus(ctx, gitRepo.GetCommitsFromIDs(data.CommitIDs), c.Issue.Repo)
c.CommitsNum = int64(len(c.Commits))
}

View file

@ -66,6 +66,8 @@ func applySorts(sess *xorm.Session, sortType string, priorityRepoID int64) {
sess.Asc("issue.created_unix").Asc("issue.id")
case "recentupdate":
sess.Desc("issue.updated_unix").Desc("issue.created_unix").Desc("issue.id")
case "recentclose":
sess.Desc("issue.closed_unix").Desc("issue.created_unix").Desc("issue.id")
case "leastupdate":
sess.Asc("issue.updated_unix").Asc("issue.created_unix").Asc("issue.id")
case "mostcomment":

View file

@ -67,6 +67,13 @@ type Milestone struct {
TotalTrackedTime int64 `xorm:"-"`
}
// Ghost milestone is a milestone which has been deleted
const GhostMilestoneID = -1
func (m *Milestone) IsGhost() bool {
return m.ID == GhostMilestoneID
}
func init() {
db.RegisterModel(new(Milestone))
}

View file

@ -5,6 +5,7 @@
package issues
import (
"bufio"
"context"
"errors"
"fmt"
@ -923,31 +924,30 @@ func MergeBlockedByOutdatedBranch(protectBranch *git_model.ProtectedBranch, pr *
return protectBranch.BlockOnOutdatedBranch && pr.CommitsBehind > 0
}
// GetCodeOwnersFromContent returns the code owners configuration
// Return empty slice if files missing
// GetCodeOwnersFromReader returns the code owners configuration
// Return warning messages on parsing errors
// We're trying to do the best we can when parsing a file.
// Invalid lines are skipped. Non-existent users and teams too.
func GetCodeOwnersFromContent(ctx context.Context, data string) ([]*CodeOwnerRule, []string) {
if len(data) == 0 {
return nil, nil
}
func GetCodeOwnersFromReader(ctx context.Context, rc io.ReadCloser, truncated bool) ([]*CodeOwnerRule, []string) {
defer rc.Close()
scanner := bufio.NewScanner(rc)
rules := make([]*CodeOwnerRule, 0)
lines := strings.Split(data, "\n")
warnings := make([]string, 0)
var rules []*CodeOwnerRule
var warnings []string
line := 0
for scanner.Scan() {
line++
for i, line := range lines {
tokens := TokenizeCodeOwnersLine(line)
tokens := TokenizeCodeOwnersLine(scanner.Text())
if len(tokens) == 0 {
continue
} else if len(tokens) < 2 {
warnings = append(warnings, fmt.Sprintf("Line: %d: incorrect format", i+1))
warnings = append(warnings, fmt.Sprintf("Line: %d: incorrect format", line))
continue
}
rule, wr := ParseCodeOwnersLine(ctx, tokens)
for _, w := range wr {
warnings = append(warnings, fmt.Sprintf("Line: %d: %s", i+1, w))
warnings = append(warnings, fmt.Sprintf("Line: %d: %s", line, w))
}
if rule == nil {
continue
@ -955,6 +955,12 @@ func GetCodeOwnersFromContent(ctx context.Context, data string) ([]*CodeOwnerRul
rules = append(rules, rule)
}
if err := scanner.Err(); err != nil {
warnings = append(warnings, err.Error())
}
if truncated {
warnings = append(warnings, fmt.Sprintf("File too big: truncated while on line %d", line))
}
return rules, warnings
}

View file

@ -152,7 +152,8 @@ func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptio
applySorts(findSession, opts.SortType, 0)
findSession = db.SetSessionPagination(findSession, opts)
prs := make([]*PullRequest, 0, opts.PageSize)
return prs, maxResults, findSession.Find(&prs)
found := findSession.Find(&prs)
return prs, maxResults, found
}
// PullRequestList defines a list of pull requests

View file

@ -79,6 +79,47 @@ func TestPullRequestsNewest(t *testing.T) {
}
}
func TestPullRequests_Closed_RecentSortType(t *testing.T) {
// Issue ID | Closed At. | Updated At
// 2 | 1707270001 | 1707270001
// 3 | 1707271000 | 1707279999
// 11 | 1707279999 | 1707275555
tests := []struct {
sortType string
expectedIssueIDOrder []int64
}{
{"recentupdate", []int64{3, 11, 2}},
{"recentclose", []int64{11, 3, 2}},
}
require.NoError(t, unittest.PrepareTestDatabase())
_, err := db.Exec(db.DefaultContext, "UPDATE issue SET closed_unix = 1707270001, updated_unix = 1707270001, is_closed = true WHERE id = 2")
require.NoError(t, err)
_, err = db.Exec(db.DefaultContext, "UPDATE issue SET closed_unix = 1707271000, updated_unix = 1707279999, is_closed = true WHERE id = 3")
require.NoError(t, err)
_, err = db.Exec(db.DefaultContext, "UPDATE issue SET closed_unix = 1707279999, updated_unix = 1707275555, is_closed = true WHERE id = 11")
require.NoError(t, err)
for _, test := range tests {
t.Run(test.sortType, func(t *testing.T) {
prs, _, err := issues_model.PullRequests(db.DefaultContext, 1, &issues_model.PullRequestsOptions{
ListOptions: db.ListOptions{
Page: 1,
},
State: "closed",
SortType: test.sortType,
})
require.NoError(t, err)
if assert.Len(t, prs, len(test.expectedIssueIDOrder)) {
for i := range test.expectedIssueIDOrder {
assert.Equal(t, test.expectedIssueIDOrder[i], prs[i].IssueID)
}
}
})
}
}
func TestLoadRequestedReviewers(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())

View file

@ -10,6 +10,7 @@ import (
"forgejo.org/models/db"
repo_model "forgejo.org/models/repo"
user_model "forgejo.org/models/user"
"forgejo.org/modules/log"
"forgejo.org/modules/timeutil"
)
@ -58,13 +59,15 @@ func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pullID int64,
return ErrAlreadyScheduledToAutoMerge{PullID: pullID}
}
_, err := db.GetEngine(ctx).Insert(&AutoMerge{
scheduledPRM, err := db.GetEngine(ctx).Insert(&AutoMerge{
DoerID: doer.ID,
PullID: pullID,
MergeStyle: style,
Message: message,
DeleteBranchAfterMerge: deleteBranch,
})
log.Trace("ScheduleAutoMerge %+v for PR %d", scheduledPRM, pullID)
return err
}
@ -81,6 +84,8 @@ func GetScheduledMergeByPullID(ctx context.Context, pullID int64) (bool, *AutoMe
return false, nil, err
}
log.Trace("GetScheduledMergeByPullID found %+v for PR %d", scheduledPRM, pullID)
scheduledPRM.Doer = doer
return true, scheduledPRM, nil
}
@ -94,6 +99,8 @@ func DeleteScheduledAutoMerge(ctx context.Context, pullID int64) error {
return db.ErrNotExist{Resource: "auto_merge", ID: pullID}
}
log.Trace("DeleteScheduledAutoMerge %+v for PR %d", scheduledPRM, pullID)
_, err = db.GetEngine(ctx).ID(scheduledPRM.ID).Delete(&AutoMerge{})
return err
}

View file

@ -11,19 +11,21 @@ import (
type FederatedUser struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"NOT NULL"`
UserID int64 `xorm:"NOT NULL INDEX user_id"`
ExternalID string `xorm:"UNIQUE(federation_user_mapping) NOT NULL"`
FederationHostID int64 `xorm:"UNIQUE(federation_user_mapping) NOT NULL"`
KeyID sql.NullString `xorm:"key_id UNIQUE"`
PublicKey sql.Null[sql.RawBytes] `xorm:"BLOB"`
NormalizedOriginalURL string // This field is just to keep original information. Pls. do not use for search or as ID!
InboxPath string
NormalizedOriginalURL string // This field is just to keep original information. Pls. do not use for search or as ID!
}
func NewFederatedUser(userID int64, externalID string, federationHostID int64, normalizedOriginalURL string) (FederatedUser, error) {
func NewFederatedUser(userID int64, externalID string, federationHostID int64, inboxPath, normalizedOriginalURL string) (FederatedUser, error) {
result := FederatedUser{
UserID: userID,
ExternalID: externalID,
FederationHostID: federationHostID,
InboxPath: inboxPath,
NormalizedOriginalURL: normalizedOriginalURL,
}
if valid, err := validation.IsValid(result); !valid {
@ -32,10 +34,11 @@ func NewFederatedUser(userID int64, externalID string, federationHostID int64, n
return result, nil
}
func (user FederatedUser) Validate() []string {
func (federatedUser FederatedUser) Validate() []string {
var result []string
result = append(result, validation.ValidateNotEmpty(user.UserID, "UserID")...)
result = append(result, validation.ValidateNotEmpty(user.ExternalID, "ExternalID")...)
result = append(result, validation.ValidateNotEmpty(user.FederationHostID, "FederationHostID")...)
result = append(result, validation.ValidateNotEmpty(federatedUser.UserID, "UserID")...)
result = append(result, validation.ValidateNotEmpty(federatedUser.ExternalID, "ExternalID")...)
result = append(result, validation.ValidateNotEmpty(federatedUser.FederationHostID, "FederationHostID")...)
result = append(result, validation.ValidateNotEmpty(federatedUser.InboxPath, "InboxPath")...)
return result
}

View file

@ -0,0 +1,30 @@
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user
import "forgejo.org/modules/validation"
type FederatedUserFollower struct {
ID int64 `xorm:"pk autoincr"`
FollowedUserID int64 `xorm:"NOT NULL unique(fuf_rel)"`
FollowingUserID int64 `xorm:"NOT NULL unique(fuf_rel)"`
}
func NewFederatedUserFollower(followedUserID, federatedUserID int64) (FederatedUserFollower, error) {
result := FederatedUserFollower{
FollowedUserID: followedUserID,
FollowingUserID: federatedUserID,
}
if valid, err := validation.IsValid(result); !valid {
return FederatedUserFollower{}, err
}
return result, nil
}
func (user FederatedUserFollower) Validate() []string {
var result []string
result = append(result, validation.ValidateNotEmpty(user.FollowedUserID, "FollowedUserID")...)
result = append(result, validation.ValidateNotEmpty(user.FollowingUserID, "FollowingUserID")...)
return result
}

View file

@ -0,0 +1,27 @@
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user
import (
"testing"
"forgejo.org/modules/validation"
"github.com/stretchr/testify/assert"
)
func Test_FederatedUserFollowerValidation(t *testing.T) {
sut := FederatedUserFollower{
FollowedUserID: 12,
FollowingUserID: 1,
}
res, err := validation.IsValid(sut)
assert.Truef(t, res, "sut should be valid but was %q", err)
sut = FederatedUserFollower{
FollowedUserID: 1,
}
res, _ = validation.IsValid(sut)
assert.False(t, res, "sut should be invalid")
}

View file

@ -14,6 +14,7 @@ func Test_FederatedUserValidation(t *testing.T) {
UserID: 12,
ExternalID: "12",
FederationHostID: 1,
InboxPath: "/api/v1/activitypub/user-id/12/inbox",
}
if res, err := validation.IsValid(sut); !res {
t.Errorf("sut should be valid but was %q", err)
@ -22,6 +23,7 @@ func Test_FederatedUserValidation(t *testing.T) {
sut = FederatedUser{
ExternalID: "12",
FederationHostID: 1,
InboxPath: "/api/v1/activitypub/user-id/12/inbox",
}
if res, _ := validation.IsValid(sut); res {
t.Error("sut should be invalid")

View file

@ -11,6 +11,7 @@ import (
)
// Follow represents relations of user and their followers.
// TODO: We should unify Activity-pub-following and classical following (see models/user/user_repository.go#IsFollowingAp)
type Follow struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"UNIQUE(follow)"`

View file

@ -1,4 +1,4 @@
// Copyright 2024 The Forgejo Authors. All rights reserved.
// Copyright 2024, 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user
@ -8,12 +8,14 @@ import (
"fmt"
"forgejo.org/models/db"
"forgejo.org/modules/log"
"forgejo.org/modules/optional"
"forgejo.org/modules/validation"
)
func init() {
db.RegisterModel(new(FederatedUser))
db.RegisterModel(new(FederatedUserFollower))
}
func CreateFederatedUser(ctx context.Context, user *User, federatedUser *FederatedUser) error {
@ -30,7 +32,12 @@ func CreateFederatedUser(ctx context.Context, user *User, federatedUser *Federat
if err != nil {
return err
}
defer committer.Close()
defer func() {
err := committer.Close()
if err != nil {
log.Error("Error closing committer: %v", err)
}
}()
if err := CreateUser(ctx, user, &overwrite); err != nil {
return err
@ -50,6 +57,14 @@ func CreateFederatedUser(ctx context.Context, user *User, federatedUser *Federat
return committer.Commit()
}
func (federatedUser *FederatedUser) UpdateFederatedUser(ctx context.Context) error {
if _, err := validation.IsValid(federatedUser); err != nil {
return err
}
_, err := db.GetEngine(ctx).ID(federatedUser.ID).Cols("inbox_path").Update(federatedUser)
return err
}
func FindFederatedUser(ctx context.Context, externalID string, federationHostID int64) (*User, *FederatedUser, error) {
federatedUser := new(FederatedUser)
user := new(User)
@ -75,6 +90,41 @@ func FindFederatedUser(ctx context.Context, externalID string, federationHostID
return user, federatedUser, nil
}
func GetFederatedUser(ctx context.Context, externalID string, federationHostID int64) (*User, *FederatedUser, error) {
user, federatedUser, err := FindFederatedUser(ctx, externalID, federationHostID)
if err != nil {
return nil, nil, err
} else if federatedUser == nil {
return nil, nil, fmt.Errorf("FederatedUser for externalId = %v and federationHostId = %v does not exist", externalID, federationHostID)
}
return user, federatedUser, nil
}
func GetFederatedUserByUserID(ctx context.Context, userID int64) (*User, *FederatedUser, error) {
federatedUser := new(FederatedUser)
user := new(User)
has, err := db.GetEngine(ctx).Where("user_id=?", userID).Get(federatedUser)
if err != nil {
return nil, nil, err
} else if !has {
return nil, nil, fmt.Errorf("Federated user %v does not exist", federatedUser.UserID)
}
has, err = db.GetEngine(ctx).ID(federatedUser.UserID).Get(user)
if err != nil {
return nil, nil, err
} else if !has {
return nil, nil, fmt.Errorf("User %v for federated user is missing", federatedUser.UserID)
}
if res, err := validation.IsValid(*user); !res {
return nil, nil, err
}
if res, err := validation.IsValid(*federatedUser); !res {
return nil, nil, err
}
return user, federatedUser, nil
}
func FindFederatedUserByKeyID(ctx context.Context, keyID string) (*User, *FederatedUser, error) {
federatedUser := new(FederatedUser)
user := new(User)
@ -101,7 +151,85 @@ func FindFederatedUserByKeyID(ctx context.Context, keyID string) (*User, *Federa
return user, federatedUser, nil
}
func UpdateFederatedUser(ctx context.Context, federatedUser *FederatedUser) error {
if res, err := validation.IsValid(federatedUser); !res {
return err
}
_, err := db.GetEngine(ctx).ID(federatedUser.ID).Update(federatedUser)
return err
}
func DeleteFederatedUser(ctx context.Context, userID int64) error {
_, err := db.GetEngine(ctx).Delete(&FederatedUser{UserID: userID})
return err
}
func GetFollowersForUser(ctx context.Context, user *User) ([]*FederatedUserFollower, error) {
if res, err := validation.IsValid(user); !res {
return nil, err
}
followers := make([]*FederatedUserFollower, 0, 8)
err := db.GetEngine(ctx).
Where("followed_user_id = ?", user.ID).
Find(&followers)
if err != nil {
return nil, err
}
for _, element := range followers {
if res, err := validation.IsValid(*element); !res {
return nil, err
}
}
return followers, nil
}
func AddFollower(ctx context.Context, followedUser *User, followingUser *FederatedUser) (*FederatedUserFollower, error) {
if res, err := validation.IsValid(followedUser); !res {
return nil, err
}
if res, err := validation.IsValid(followingUser); !res {
return nil, err
}
federatedUserFollower, err := NewFederatedUserFollower(followedUser.ID, followingUser.UserID)
if err != nil {
return nil, err
}
_, err = db.GetEngine(ctx).Insert(&federatedUserFollower)
if err != nil {
return nil, err
}
return &federatedUserFollower, err
}
func RemoveFollower(ctx context.Context, followedUser *User, followingUser *FederatedUser) error {
if res, err := validation.IsValid(followedUser); !res {
return err
}
if res, err := validation.IsValid(followingUser); !res {
return err
}
_, err := db.GetEngine(ctx).Delete(&FederatedUserFollower{
FollowedUserID: followedUser.ID,
FollowingUserID: followingUser.UserID,
})
return err
}
// TODO: We should unify Activity-pub-following and classical following (see models/user/follow.go)
func IsFollowingAp(ctx context.Context, followedUser *User, followingUser *FederatedUser) (bool, error) {
if res, err := validation.IsValid(followedUser); !res {
return false, err
}
if res, err := validation.IsValid(followingUser); !res {
return false, err
}
return db.GetEngine(ctx).Get(&FederatedUserFollower{
FollowedUserID: followedUser.ID,
FollowingUserID: followingUser.UserID,
})
}

View file

@ -12,6 +12,13 @@ import (
"forgejo.org/modules/structs"
)
// IsSystem returns true if the user has a fixed
// negative ID, is never stored in the database and
// is generated on the fly when needed.
func (u *User) IsSystem() bool {
return u.IsGhost() || u.IsActions()
}
const (
GhostUserID = -1
GhostUserName = "Ghost"

View file

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

View file

@ -323,6 +323,10 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
matchTimes++
}
case "paths":
if refName.IsTag() {
matchTimes++
break
}
filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before)
if err != nil {
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err)
@ -336,6 +340,10 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
}
}
case "paths-ignore":
if refName.IsTag() {
matchTimes++
break
}
filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before)
if err != nil {
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err)

View file

@ -150,6 +150,24 @@ func TestDetectMatched(t *testing.T) {
yamlOn: "on: workflow_dispatch",
expected: true,
},
{
desc: "push to tag matches workflow with paths condition (should skip paths check)",
triggeredEvent: webhook_module.HookEventPush,
payload: &api.PushPayload{
Ref: "refs/tags/v1.0.0",
Before: "0000000",
Commits: []*api.PayloadCommit{
{
ID: "abcdef123456",
Added: []string{"src/main.go"},
Message: "Release v1.0.0",
},
},
},
commit: nil,
yamlOn: "on:\n push:\n paths:\n - src/**",
expected: true,
},
}
for _, tc := range testCases {

View file

@ -5,10 +5,10 @@ package assetfs
import (
"context"
"errors"
"fmt"
"io"
"io/fs"
"net/http"
"os"
"path/filepath"
"slices"
@ -25,7 +25,7 @@ import (
// Layer represents a layer in a layered asset file-system. It has a name and works like http.FileSystem
type Layer struct {
name string
fs http.FileSystem
fs fs.FS
localPath string
}
@ -34,10 +34,18 @@ func (l *Layer) Name() string {
}
// Open opens the named file. The caller is responsible for closing the file.
func (l *Layer) Open(name string) (http.File, error) {
func (l *Layer) Open(name string) (fs.File, error) {
return l.fs.Open(name)
}
func (l *Layer) ReadDir(name string) ([]fs.DirEntry, error) {
dirEntries, err := fs.ReadDir(l.fs, name)
if err != nil && errors.Is(err, fs.ErrNotExist) {
err = nil
}
return dirEntries, err
}
// Local returns a new Layer with the given name, it serves files from the given local path.
func Local(name, base string, sub ...string) *Layer {
// TODO: the old behavior (StaticRootPath might not be absolute), not ideal, just keep the same as before
@ -48,11 +56,18 @@ func Local(name, base string, sub ...string) *Layer {
panic(fmt.Sprintf("Unable to get absolute path for %q: %v", base, err))
}
root := util.FilePathJoinAbs(base, sub...)
return &Layer{name: name, fs: http.Dir(root), localPath: root}
fsRoot, err := os.OpenRoot(root)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil
}
panic(fmt.Sprintf("Unable to open layer %q", err))
}
return &Layer{name: name, fs: fsRoot.FS(), localPath: root}
}
// Bindata returns a new Layer with the given name, it serves files from the given bindata asset.
func Bindata(name string, fs http.FileSystem) *Layer {
func Bindata(name string, fs fs.FS) *Layer {
return &Layer{name: name, fs: fs}
}
@ -65,11 +80,11 @@ type LayeredFS struct {
// Layered returns a new LayeredFS with the given layers. The first layer is the top layer.
func Layered(layers ...*Layer) *LayeredFS {
return &LayeredFS{layers: layers}
return &LayeredFS{layers: slices.DeleteFunc(layers, func(layer *Layer) bool { return layer == nil })}
}
// Open opens the named file. The caller is responsible for closing the file.
func (l *LayeredFS) Open(name string) (http.File, error) {
func (l *LayeredFS) Open(name string) (fs.File, error) {
for _, layer := range l.layers {
f, err := layer.Open(name)
if err == nil || !os.IsNotExist(err) {
@ -102,29 +117,18 @@ func (l *LayeredFS) ReadLayeredFile(elems ...string) ([]byte, string, error) {
return nil, "", fs.ErrNotExist
}
func shouldInclude(info fs.FileInfo, fileMode ...bool) bool {
func shouldInclude(info fs.DirEntry, fileMode ...bool) bool {
if util.CommonSkip(info.Name()) {
return false
}
if len(fileMode) == 0 {
return true
} else if len(fileMode) == 1 {
return fileMode[0] == !info.Mode().IsDir()
return fileMode[0] == !info.IsDir()
}
panic("too many arguments for fileMode in shouldInclude")
}
func readDir(layer *Layer, name string) ([]fs.FileInfo, error) {
f, err := layer.Open(name)
if os.IsNotExist(err) {
return nil, nil
} else if err != nil {
return nil, err
}
defer f.Close()
return f.Readdir(-1)
}
// ListFiles lists files/directories in the given directory. The fileMode controls the returned files.
// * omitted: all files and directories will be returned.
// * true: only files will be returned.
@ -133,7 +137,7 @@ func readDir(layer *Layer, name string) ([]fs.FileInfo, error) {
func (l *LayeredFS) ListFiles(name string, fileMode ...bool) ([]string, error) {
fileSet := make(container.Set[string])
for _, layer := range l.layers {
infos, err := readDir(layer, name)
infos, err := layer.ReadDir(name)
if err != nil {
return nil, err
}
@ -162,7 +166,7 @@ func listAllFiles(layers []*Layer, name string, fileMode ...bool) ([]string, err
var list func(dir string) error
list = func(dir string) error {
for _, layer := range layers {
infos, err := readDir(layer, dir)
infos, err := layer.ReadDir(dir)
if err != nil {
return err
}

View file

@ -12,7 +12,6 @@ import (
"forgejo.org/modules/log"
"forgejo.org/modules/typesniffer"
"forgejo.org/modules/util"
)
// Blob represents a Git object.
@ -25,42 +24,25 @@ type Blob struct {
repo *Repository
}
// DataAsync gets a ReadCloser for the contents of a blob without reading it all.
// Calling the Close function on the result will discard all unread output.
func (b *Blob) DataAsync() (io.ReadCloser, error) {
func (b *Blob) newReader() (*bufio.Reader, int64, func(), error) {
wr, rd, cancel, err := b.repo.CatFileBatch(b.repo.Ctx)
if err != nil {
return nil, err
return nil, 0, nil, err
}
_, err = wr.Write([]byte(b.ID.String() + "\n"))
if err != nil {
cancel()
return nil, err
return nil, 0, nil, err
}
_, _, size, err := ReadBatchLine(rd)
if err != nil {
cancel()
return nil, err
return nil, 0, nil, err
}
b.gotSize = true
b.size = size
if size < 4096 {
bs, err := io.ReadAll(io.LimitReader(rd, size))
defer cancel()
if err != nil {
return nil, err
}
_, err = rd.Discard(1)
return io.NopCloser(bytes.NewReader(bs)), err
}
return &blobReader{
rd: rd,
n: size,
cancel: cancel,
}, nil
return rd, size, cancel, err
}
// Size returns the uncompressed size of the blob
@ -91,10 +73,36 @@ func (b *Blob) Size() int64 {
return b.size
}
// DataAsync gets a ReadCloser for the contents of a blob without reading it all.
// Calling the Close function on the result will discard all unread output.
func (b *Blob) DataAsync() (io.ReadCloser, error) {
rd, size, cancel, err := b.newReader()
if err != nil {
return nil, err
}
if size < 4096 {
bs, err := io.ReadAll(io.LimitReader(rd, size))
defer cancel()
if err != nil {
return nil, err
}
_, err = rd.Discard(1)
return io.NopCloser(bytes.NewReader(bs)), err
}
return &blobReader{
rd: rd,
n: size,
cancel: cancel,
}, nil
}
type blobReader struct {
rd *bufio.Reader
n int64
cancel func()
rd *bufio.Reader
n int64 // number of bytes to read
additionalDiscard int64 // additional number of bytes to discard
cancel func()
}
func (b *blobReader) Read(p []byte) (n int, err error) {
@ -117,7 +125,8 @@ func (b *blobReader) Close() error {
defer b.cancel()
if err := DiscardFull(b.rd, b.n+1); err != nil {
// discard the unread bytes, the truncated bytes and the trailing newline
if err := DiscardFull(b.rd, b.n+b.additionalDiscard+1); err != nil {
return err
}
@ -131,17 +140,35 @@ func (b *Blob) Name() string {
return b.name
}
// GetBlobContent Gets the limited content of the blob as raw text
// NewTruncatedReader return a blob-reader which silently truncates when the limit is reached (io.EOF will be returned)
func (b *Blob) NewTruncatedReader(limit int64) (rc io.ReadCloser, fullSize int64, err error) {
r, fullSize, cancel, err := b.newReader()
if err != nil {
return nil, fullSize, err
}
limit = min(limit, fullSize)
return &blobReader{
rd: r,
n: limit,
additionalDiscard: fullSize - limit,
cancel: cancel,
}, fullSize, nil
}
// GetBlobContent Gets the truncated content of the blob as raw text
func (b *Blob) GetBlobContent(limit int64) (string, error) {
if limit <= 0 {
return "", nil
}
dataRc, err := b.DataAsync()
rc, fullSize, err := b.NewTruncatedReader(limit)
if err != nil {
return "", err
}
defer dataRc.Close()
buf, err := util.ReadWithLimit(dataRc, int(limit))
defer rc.Close()
buf := make([]byte, min(fullSize, limit))
_, err = io.ReadFull(rc, buf)
return string(buf), err
}

View file

@ -35,6 +35,106 @@ func TestBlob_Data(t *testing.T) {
assert.Equal(t, output, string(data))
}
func TestBlob(t *testing.T) {
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
repo, err := openRepositoryWithDefaultContext(bareRepo1Path)
require.NoError(t, err)
defer repo.Close()
testBlob, err := repo.GetBlob("6c493ff740f9380390d5c9ddef4af18697ac9375")
require.NoError(t, err)
t.Run("GetBlobContent", func(t *testing.T) {
r, err := testBlob.GetBlobContent(100)
require.NoError(t, err)
require.Equal(t, "file2\n", r)
r, err = testBlob.GetBlobContent(-1)
require.NoError(t, err)
require.Empty(t, r)
r, err = testBlob.GetBlobContent(4)
require.NoError(t, err)
require.Equal(t, "file", r)
r, err = testBlob.GetBlobContent(6)
require.NoError(t, err)
require.Equal(t, "file2\n", r)
})
t.Run("NewTruncatedReader", func(t *testing.T) {
// read fewer than available
rc, size, err := testBlob.NewTruncatedReader(100)
require.NoError(t, err)
require.Equal(t, int64(6), size)
buf := make([]byte, 1)
n, err := rc.Read(buf)
require.NoError(t, err)
require.Equal(t, 1, n)
require.Equal(t, "f", string(buf))
n, err = rc.Read(buf)
require.NoError(t, err)
require.Equal(t, 1, n)
require.Equal(t, "i", string(buf))
require.NoError(t, rc.Close())
// read more than available
rc, size, err = testBlob.NewTruncatedReader(100)
require.NoError(t, err)
require.Equal(t, int64(6), size)
buf = make([]byte, 100)
n, err = rc.Read(buf)
require.NoError(t, err)
require.Equal(t, 6, n)
require.Equal(t, "file2\n", string(buf[:n]))
n, err = rc.Read(buf)
require.Error(t, err)
require.Equal(t, io.EOF, err)
require.Equal(t, 0, n)
require.NoError(t, rc.Close())
// read more than truncated
rc, size, err = testBlob.NewTruncatedReader(4)
require.NoError(t, err)
require.Equal(t, int64(6), size)
buf = make([]byte, 10)
n, err = rc.Read(buf)
require.NoError(t, err)
require.Equal(t, 4, n)
require.Equal(t, "file", string(buf[:n]))
n, err = rc.Read(buf)
require.Error(t, err)
require.Equal(t, io.EOF, err)
require.Equal(t, 0, n)
require.NoError(t, rc.Close())
})
t.Run("NonExisting", func(t *testing.T) {
nonExistingBlob, err := repo.GetBlob("00003ff740f9380390d5c9ddef4af18690000000")
require.NoError(t, err)
r, err := nonExistingBlob.GetBlobContent(100)
require.Error(t, err)
require.IsType(t, ErrNotExist{}, err)
require.Empty(t, r)
rc, size, err := nonExistingBlob.NewTruncatedReader(100)
require.Error(t, err)
require.IsType(t, ErrNotExist{}, err)
require.Empty(t, rc)
require.Empty(t, size)
})
}
func Benchmark_Blob_Data(b *testing.B) {
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
repo, err := openRepositoryWithDefaultContext(bareRepo1Path)

View file

@ -38,6 +38,7 @@ var (
InvertedGitFlushEnv bool // 2.43.1
SupportCheckAttrOnBare bool // >= 2.40
SupportGitMergeTree bool // >= 2.38
SupportGrepMaxCount bool // >= 2.38
HasSSHExecutable bool
@ -191,6 +192,7 @@ func InitFull(ctx context.Context) (err error) {
InvertedGitFlushEnv = CheckGitVersionEqual("2.43.1") == nil
SupportGitMergeTree = CheckGitVersionAtLeast("2.38") == nil
SupportGrepMaxCount = CheckGitVersionAtLeast("2.38") == nil
if setting.LFS.StartServer {
if CheckGitVersionAtLeast("2.1.2") != nil {

View file

@ -105,6 +105,10 @@ func TestSyncConfigGPGFormat(t *testing.T) {
})
t.Run("SSH format", func(t *testing.T) {
if CheckGitVersionAtLeast("2.34.0") != nil {
t.SkipNow()
}
r, err := os.OpenRoot(t.TempDir())
require.NoError(t, err)
f, err := r.OpenFile("ssh-keygen", os.O_CREATE|os.O_TRUNC, 0o700)

View file

@ -98,8 +98,7 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO
cmd.AddOptionValues("--context", fmt.Sprint(opts.ContextLineNumber))
// --max-count requires at least git 2.38
if CheckGitVersionAtLeast("2.38.0") == nil {
if SupportGrepMaxCount {
cmd.AddOptionValues("--max-count", fmt.Sprint(opts.MatchesPerFile))
} else {
log.Warn("git-grep: --max-count requires at least git 2.38")

View file

@ -59,48 +59,55 @@ func TestGrepSearch(t *testing.T) {
},
}, res)
res, err = GrepSearch(t.Context(), repo, "world", GrepOptions{MatchesPerFile: 1})
require.NoError(t, err)
assert.Equal(t, []*GrepResult{
{
Filename: "i-am-a-python.p",
LineNumbers: []int{1},
LineCodes: []string{"## This is a simple file to do a hello world"},
HighlightedRanges: [][3]int{{0, 39, 44}},
},
{
Filename: "java-hello/main.java",
LineNumbers: []int{1},
LineCodes: []string{"public class HelloWorld"},
HighlightedRanges: [][3]int{{0, 18, 23}},
},
{
Filename: "main.vendor.java",
LineNumbers: []int{1},
LineCodes: []string{"public class HelloWorld"},
HighlightedRanges: [][3]int{{0, 18, 23}},
},
{
Filename: "python-hello/hello.py",
LineNumbers: []int{1},
LineCodes: []string{"## This is a simple file to do a hello world"},
HighlightedRanges: [][3]int{{0, 39, 44}},
},
}, res)
t.Run("Max count", func(t *testing.T) {
if !SupportGrepMaxCount {
t.Skip("Skipping, git grep --max-count is not supported")
return
}
res, err = GrepSearch(t.Context(), repo, "world", GrepOptions{
MatchesPerFile: 1,
Filename: "java-hello/",
res, err = GrepSearch(t.Context(), repo, "world", GrepOptions{MatchesPerFile: 1})
require.NoError(t, err)
assert.Equal(t, []*GrepResult{
{
Filename: "i-am-a-python.p",
LineNumbers: []int{1},
LineCodes: []string{"## This is a simple file to do a hello world"},
HighlightedRanges: [][3]int{{0, 39, 44}},
},
{
Filename: "java-hello/main.java",
LineNumbers: []int{1},
LineCodes: []string{"public class HelloWorld"},
HighlightedRanges: [][3]int{{0, 18, 23}},
},
{
Filename: "main.vendor.java",
LineNumbers: []int{1},
LineCodes: []string{"public class HelloWorld"},
HighlightedRanges: [][3]int{{0, 18, 23}},
},
{
Filename: "python-hello/hello.py",
LineNumbers: []int{1},
LineCodes: []string{"## This is a simple file to do a hello world"},
HighlightedRanges: [][3]int{{0, 39, 44}},
},
}, res)
res, err = GrepSearch(t.Context(), repo, "world", GrepOptions{
MatchesPerFile: 1,
Filename: "java-hello/",
})
require.NoError(t, err)
assert.Equal(t, []*GrepResult{
{
Filename: "java-hello/main.java",
LineNumbers: []int{1},
LineCodes: []string{"public class HelloWorld"},
HighlightedRanges: [][3]int{{0, 18, 23}},
},
}, res)
})
require.NoError(t, err)
assert.Equal(t, []*GrepResult{
{
Filename: "java-hello/main.java",
LineNumbers: []int{1},
LineCodes: []string{"public class HelloWorld"},
HighlightedRanges: [][3]int{{0, 18, 23}},
},
}, res)
res, err = GrepSearch(t.Context(), repo, "no-such-content", GrepOptions{})
require.NoError(t, err)

View file

@ -251,10 +251,14 @@ func TestGitAttributeCheckerError(t *testing.T) {
cancel()
ac, err := gitRepo.GitAttributeChecker("8fee858da5796dfb37704761701bb8e800ad9ef3", "linguist-language")
require.NoError(t, err)
if SupportCheckAttrOnBare {
require.NoError(t, err)
_, err = ac.CheckPath("i-am-a-python.p")
require.Error(t, err)
_, err = ac.CheckPath("i-am-a-python.p")
require.Error(t, err)
} else {
require.Error(t, err)
}
})
t.Run("Cancelled/DuringRun", func(t *testing.T) {

View file

@ -16,16 +16,10 @@ import (
)
func TestElasticsearchIndexer(t *testing.T) {
// The elasticsearch instance started by testing.yml > test-unit > services > elasticsearch
url := "http://elastic:changeme@elasticsearch:9200"
if os.Getenv("CI") == "" {
// Make it possible to run tests against a local elasticsearch instance
url = os.Getenv("TEST_ELASTICSEARCH_URL")
if url == "" {
t.Skip("TEST_ELASTICSEARCH_URL not set and not running in CI")
return
}
url := os.Getenv("TEST_ELASTICSEARCH_URL")
if url == "" {
t.Skip("TEST_ELASTICSEARCH_URL not set")
return
}
require.Eventually(t, func() bool {

View file

@ -25,7 +25,7 @@ type Token struct {
func (tk *Token) ParseIssueReference() (int64, error) {
term := tk.Term
if term[0] == '#' || term[0] == '!' {
if len(term) > 1 && (term[0] == '#' || term[0] == '!') {
term = term[1:]
}
return strconv.ParseInt(term, 10, 64)

View file

@ -169,3 +169,35 @@ func TestIssueQueryString(t *testing.T) {
})
}
}
func TestToken_ParseIssueReference(t *testing.T) {
var tk Token
{
tk.Term = "123"
id, err := tk.ParseIssueReference()
require.NoError(t, err)
assert.Equal(t, int64(123), id)
}
{
tk.Term = "#123"
id, err := tk.ParseIssueReference()
require.NoError(t, err)
assert.Equal(t, int64(123), id)
}
{
tk.Term = "!123"
id, err := tk.ParseIssueReference()
require.NoError(t, err)
assert.Equal(t, int64(123), id)
}
{
tk.Term = "text"
_, err := tk.ParseIssueReference()
require.Error(t, err)
}
{
tk.Term = ""
_, err := tk.ParseIssueReference()
require.Error(t, err)
}
}

View file

@ -69,6 +69,10 @@ var (
// https://html.spec.whatwg.org/multipage/input.html#e-mail-state-(type%3Demail)
emailRegex = regexp.MustCompile("(?:\\s|^|\\(|\\[)([a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9]{2,}(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+)(?:\\s|$|\\)|\\]|;|,|\\?|!|\\.(\\s|$))")
// Fediverse handle regex (same as emailRegex but with additonal @ or !
// at start)
fediRegex = regexp.MustCompile("(?:\\s|^|\\(|\\[)([@!]([a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+)@([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9]{2,}(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+))(?:\\s|$|\\)|\\]|;|,|\\?|!|\\.(\\s|$))")
// blackfriday extensions create IDs like fn:user-content-footnote
blackfridayExtRegex = regexp.MustCompile(`[^:]*:user-content-`)
@ -153,6 +157,7 @@ var defaultProcessors = []processor{
issueIndexPatternProcessor,
commitCrossReferencePatternProcessor,
hashCurrentPatternProcessor,
fediAddressProcessor,
emailAddressProcessor,
emojiProcessor,
emojiShortCodeProcessor,
@ -1237,6 +1242,21 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) {
}
}
// fediAddressProcessor replaces raw fediverse handles with toolforge links
func fediAddressProcessor(ctx *RenderContext, node *html.Node) {
next := node.NextSibling
for node != nil && node != next {
m := fediRegex.FindStringSubmatchIndex(node.Data)
if m == nil {
return
}
fedihandle := node.Data[m[2]:m[3]]
replaceContent(node, m[2], m[3], createLink("https://fedirect.toolforge.org/?id="+url.QueryEscape(fedihandle), fedihandle, "fedihandle"))
node = node.NextSibling.NextSibling
}
}
// emailAddressProcessor replaces raw email addresses with a mailto: link.
func emailAddressProcessor(ctx *RenderContext, node *html.Node) {
next := node.NextSibling

View file

@ -307,6 +307,19 @@ func TestRender_email(t *testing.T) {
test(
"email@domain..com",
`<p>email@domain..com</p>`)
// Test fediverse handle
test(
"@forgejo@floss.social",
`<p><a href="https://fedirect.toolforge.org/?id=%40forgejo%40floss.social" rel="nofollow">@forgejo@floss.social</a></p>`)
test(
"!forgejo@programming.dev",
`<p><a href="https://fedirect.toolforge.org/?id=%21forgejo%40programming.dev" rel="nofollow">!forgejo@programming.dev</a></p>`)
test(
"@#&@forgejo.org",
`<p><a href="https://fedirect.toolforge.org/?id=%40%23%26%40forgejo.org" rel="nofollow">@#&amp;@forgejo.org</a></p>`)
}
func TestRender_emoji(t *testing.T) {

View file

@ -267,8 +267,13 @@ func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error
// RenderString renders Markdown string to HTML with all specific handling stuff and return string
func RenderString(ctx *markup.RenderContext, content string) (template.HTML, error) {
return RenderReader(ctx, strings.NewReader(content))
}
// RenderReader renders Markdown io.Reader to HTML with all specific handling stuff and return string
func RenderReader(ctx *markup.RenderContext, input io.Reader) (template.HTML, error) {
var buf strings.Builder
if err := Render(ctx, strings.NewReader(content), &buf); err != nil {
if err := Render(ctx, input, &buf); err != nil {
return "", err
}
return template.HTML(buf.String()), nil

View file

@ -23,6 +23,11 @@ var wellKnownMimeTypesLower = map[string]string{
".wasm": "application/wasm",
".webp": "image/webp",
".xml": "text/xml; charset=utf-8",
".glb": "model/gltf-binary",
".gltf": "model/gltf+json",
".obj": "model/obj",
".stl": "model/stl",
".3mf": "model/3mf",
// well, there are some types missing from the builtin list
".txt": "text/plain; charset=utf-8",

View file

@ -6,6 +6,7 @@ package public
import (
"bytes"
"io"
"io/fs"
"net/http"
"os"
"path"
@ -59,7 +60,7 @@ func setWellKnownContentType(w http.ResponseWriter, file string) {
}
}
func handleRequest(w http.ResponseWriter, req *http.Request, fs http.FileSystem, file string) {
func handleRequest(w http.ResponseWriter, req *http.Request, fs fs.FS, file string) {
// actually, fs (http.FileSystem) is designed to be a safe interface, relative paths won't bypass its parent directory, it's also fine to do a clean here
f, err := fs.Open(util.PathJoinRelX(file))
if err != nil {
@ -86,33 +87,31 @@ func handleRequest(w http.ResponseWriter, req *http.Request, fs http.FileSystem,
return
}
serveContent(w, req, fi, fi.ModTime(), f)
serveContent(w, req, fi.Name(), fi.ModTime(), f.(io.ReadSeeker))
}
type GzipBytesProvider interface {
GzipBytes() []byte
type ZstdBytesProvider interface {
ZstdBytes() []byte
}
// serveContent serve http content
func serveContent(w http.ResponseWriter, req *http.Request, fi os.FileInfo, modtime time.Time, content io.ReadSeeker) {
setWellKnownContentType(w, fi.Name())
func serveContent(w http.ResponseWriter, req *http.Request, name string, modtime time.Time, content io.ReadSeeker) {
setWellKnownContentType(w, name)
encodings := parseAcceptEncoding(req.Header.Get("Accept-Encoding"))
if encodings.Contains("gzip") {
// try to provide gzip content directly from bindata (provided by vfsgen۰CompressedFileInfo)
if compressed, ok := fi.(GzipBytesProvider); ok {
rdGzip := bytes.NewReader(compressed.GzipBytes())
// all gzipped static files (from bindata) are managed by Gitea, so we can make sure every file has the correct ext name
// then we can get the correct Content-Type, we do not need to do http.DetectContentType on the decompressed data
if encodings.Contains("zstd") {
// If the file was compressed, use the bytes directly.
if compressed, ok := content.(ZstdBytesProvider); ok {
rdZstd := bytes.NewReader(compressed.ZstdBytes())
if w.Header().Get("Content-Type") == "" {
w.Header().Set("Content-Type", "application/octet-stream")
}
w.Header().Set("Content-Encoding", "gzip")
httpcache.ServeContentWithCacheControl(w, req, fi.Name(), modtime, rdGzip)
w.Header().Set("Content-Encoding", "zstd")
httpcache.ServeContentWithCacheControl(w, req, name, modtime, rdZstd)
return
}
}
httpcache.ServeContentWithCacheControl(w, req, fi.Name(), modtime, content)
httpcache.ServeContentWithCacheControl(w, req, name, modtime, content)
return
}

View file

@ -12,8 +12,6 @@ import (
"forgejo.org/modules/timeutil"
)
var _ GzipBytesProvider = (*vfsgen۰CompressedFileInfo)(nil)
// GlobalModTime provide a global mod time for embedded asset files
func GlobalModTime(filename string) time.Time {
return timeutil.GetExecutableModTime()

View file

@ -18,13 +18,14 @@ import (
)
func TestMinioStorageIterator(t *testing.T) {
if os.Getenv("CI") == "" {
t.Skip("minioStorage not present outside of CI")
endpoint := os.Getenv("TEST_MINIO_ENDPOINT")
if endpoint == "" {
t.Skip("TEST_MINIO_ENDPOINT not set")
return
}
testStorageIterator(t, setting.MinioStorageType, &setting.Storage{
MinioConfig: setting.MinioStorageConfig{
Endpoint: "minio:9000",
Endpoint: endpoint,
AccessKeyID: "123456",
SecretAccessKey: "12345678",
Bucket: "gitea",
@ -34,13 +35,14 @@ func TestMinioStorageIterator(t *testing.T) {
}
func TestVirtualHostMinioStorage(t *testing.T) {
if os.Getenv("CI") == "" {
t.Skip("minioStorage not present outside of CI")
endpoint := os.Getenv("TEST_MINIO_ENDPOINT")
if endpoint == "" {
t.Skip("TEST_MINIO_ENDPOINT not set")
return
}
testStorageIterator(t, setting.MinioStorageType, &setting.Storage{
MinioConfig: setting.MinioStorageConfig{
Endpoint: "minio:9000",
Endpoint: endpoint,
AccessKeyID: "123456",
SecretAccessKey: "12345678",
Bucket: "gitea",
@ -85,13 +87,14 @@ func TestMinioStoragePath(t *testing.T) {
}
func TestS3StorageBadRequest(t *testing.T) {
if os.Getenv("CI") == "" {
t.Skip("S3Storage not present outside of CI")
endpoint := os.Getenv("TEST_MINIO_ENDPOINT")
if endpoint == "" {
t.Skip("TEST_MINIO_ENDPOINT not set")
return
}
cfg := &setting.Storage{
MinioConfig: setting.MinioStorageConfig{
Endpoint: "minio:9000",
Endpoint: endpoint,
AccessKeyID: "123456",
SecretAccessKey: "12345678",
Bucket: "bucket",

View file

@ -78,3 +78,9 @@ type ActionRun struct {
// the url of this action run
HTMLURL string `json:"html_url"`
}
// ListActionRunResponse return a list of ActionRun
type ListActionRunResponse struct {
Entries []*ActionRun `json:"workflow_runs"`
TotalCount int64 `json:"total_count"`
}

View file

@ -3,8 +3,8 @@
package structs
// GitBlobResponse represents a git blob
type GitBlobResponse struct {
// GitBlob represents a git blob
type GitBlob struct {
Content string `json:"content"`
Encoding string `json:"encoding"`
URL string `json:"url"`

View file

@ -32,23 +32,3 @@ type ActionTaskResponse struct {
Entries []*ActionTask `json:"workflow_runs"`
TotalCount int64 `json:"total_count"`
}
// ActionRun represents an ActionRun
type RepoActionRun struct {
ID int64 `json:"id"`
Name string `json:"name"`
RunNumber int64 `json:"run_number"`
Event string `json:"event"`
Status string `json:"status"`
HeadBranch string `json:"head_branch"`
HeadSHA string `json:"head_sha"`
WorkflowID string `json:"workflow_id"`
URL string `json:"url"`
TriggeringActor *User `json:"triggering_actor"`
}
// ListActionRunResponse return a list of ActionRun
type ListRepoActionRunResponse struct {
Entries []*RepoActionRun `json:"workflow_runs"`
TotalCount int64 `json:"total_count"`
}

View file

@ -24,6 +24,16 @@ const (
AvifMimeType = "image/avif"
// ApplicationOctetStream MIME type of binary files.
ApplicationOctetStream = "application/octet-stream"
// GLTFMimeType MIME type of GLTF files.
GLTFMimeType = "model/gltf+json"
// GLBMimeType MIME type of GLB files.
GLBMimeType = "model/gltf-binary"
// OBJMimeType MIME type of OBJ files.
OBJMimeType = "model/obj"
// STLMimeType MIME type of STL files.
STLMimeType = "model/stl"
// 3MFMimeType MIME type of 3MF files.
ThreeMFMimeType = "model/3mf"
)
var (
@ -67,6 +77,36 @@ func (ct SniffedType) IsAudio() bool {
return strings.Contains(ct.contentType, "audio/")
}
// Is3DModel detects if data is a 3D format
func (ct SniffedType) Is3DModel() bool {
return strings.Contains(ct.contentType, "model/")
}
// IsGLTFFile detects if data is an SVG image format
func (ct SniffedType) IsGLTF() bool {
return strings.Contains(ct.contentType, GLTFMimeType)
}
// IsGLBFile detects if data is an GLB image format
func (ct SniffedType) IsGLB() bool {
return strings.Contains(ct.contentType, GLBMimeType)
}
// IsOBJFile detects if data is an OBJ image format
func (ct SniffedType) IsOBJ() bool {
return strings.Contains(ct.contentType, OBJMimeType)
}
// IsSTLTextFile detects if data is an STL text format
func (ct SniffedType) IsSTL() bool {
return strings.Contains(ct.contentType, STLMimeType)
}
// Is3MFFile detects if data is an 3MF image format
func (ct SniffedType) Is3MF() bool {
return strings.Contains(ct.contentType, ThreeMFMimeType)
}
// IsRepresentableAsText returns true if file content can be represented as
// plain text or is empty.
func (ct SniffedType) IsRepresentableAsText() bool {
@ -75,7 +115,7 @@ func (ct SniffedType) IsRepresentableAsText() bool {
// IsBrowsableBinaryType returns whether a non-text type can be displayed in a browser
func (ct SniffedType) IsBrowsableBinaryType() bool {
return ct.IsImage() || ct.IsSvgImage() || ct.IsPDF() || ct.IsVideo() || ct.IsAudio()
return ct.IsImage() || ct.IsSvgImage() || ct.IsPDF() || ct.IsVideo() || ct.IsAudio() || ct.Is3DModel()
}
// GetMimeType returns the mime type
@ -135,6 +175,13 @@ func DetectContentType(data []byte) SniffedType {
ct = "audio/ogg" // for most cases, it is used as an audio container
}
}
// GLTF is unsupported by http.DetectContentType
// hexdump -n 4 -C glTF.glb
if bytes.HasPrefix(data, []byte("glTF")) {
ct = GLBMimeType
}
return SniffedType{ct}
}

View file

@ -117,6 +117,14 @@ func TestIsAudio(t *testing.T) {
assert.True(t, DetectContentType([]byte("ID3Toy\n====\t* hi 🌞, ..."+"🌛"[0:2])).IsText()) // test ID3 tag with incomplete UTF8 char
}
func TestIsGLB(t *testing.T) {
glb, _ := hex.DecodeString("676c5446")
assert.True(t, DetectContentType(glb).IsGLB())
assert.True(t, DetectContentType(glb).Is3DModel())
assert.False(t, DetectContentType([]byte("plain text")).IsGLB())
assert.False(t, DetectContentType([]byte("plain text")).Is3DModel())
}
func TestDetectContentTypeFromReader(t *testing.T) {
mp3, _ := base64.StdEncoding.DecodeString("SUQzBAAAAAABAFRYWFgAAAASAAADbWFqb3JfYnJhbmQAbXA0MgBUWFhYAAAAEQAAA21pbm9yX3Zl")
st, err := DetectContentTypeFromReader(bytes.NewReader(mp3))
@ -145,3 +153,15 @@ func TestDetectContentTypeAvif(t *testing.T) {
assert.True(t, st.IsImage())
}
func TestDetectContentTypeModelGLB(t *testing.T) {
glb, err := hex.DecodeString("676c5446")
require.NoError(t, err)
st, err := DetectContentTypeFromReader(bytes.NewReader(glb))
require.NoError(t, err)
// print st for debugging
assert.Equal(t, "model/gltf-binary", st.GetMimeType())
assert.True(t, st.IsGLB())
}

View file

@ -4,7 +4,6 @@
package util
import (
"bytes"
"errors"
"io"
)
@ -20,42 +19,6 @@ func ReadAtMost(r io.Reader, buf []byte) (n int, err error) {
return n, err
}
// ReadWithLimit reads at most "limit" bytes from r into buf.
// If EOF or ErrUnexpectedEOF occurs while reading, err will be nil.
func ReadWithLimit(r io.Reader, n int) (buf []byte, err error) {
return readWithLimit(r, 1024, n)
}
func readWithLimit(r io.Reader, batch, limit int) ([]byte, error) {
if limit <= batch {
buf := make([]byte, limit)
n, err := ReadAtMost(r, buf)
if err != nil {
return nil, err
}
return buf[:n], nil
}
res := bytes.NewBuffer(make([]byte, 0, batch))
bufFix := make([]byte, batch)
eof := false
for res.Len() < limit && !eof {
bufTmp := bufFix
if res.Len()+batch > limit {
bufTmp = bufFix[:limit-res.Len()]
}
n, err := io.ReadFull(r, bufTmp)
if err == io.EOF || err == io.ErrUnexpectedEOF {
eof = true
} else if err != nil {
return nil, err
}
if _, err = res.Write(bufTmp[:n]); err != nil {
return nil, err
}
}
return res.Bytes(), nil
}
// ErrNotEmpty is an error reported when there is a non-empty reader
var ErrNotEmpty = errors.New("not-empty")

View file

@ -1,67 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package util
import (
"bytes"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type readerWithError struct {
buf *bytes.Buffer
}
func (r *readerWithError) Read(p []byte) (n int, err error) {
if r.buf.Len() < 2 {
return 0, errors.New("test error")
}
return r.buf.Read(p)
}
func TestReadWithLimit(t *testing.T) {
bs := []byte("0123456789abcdef")
// normal test
buf, err := readWithLimit(bytes.NewBuffer(bs), 5, 2)
require.NoError(t, err)
assert.Equal(t, []byte("01"), buf)
buf, err = readWithLimit(bytes.NewBuffer(bs), 5, 5)
require.NoError(t, err)
assert.Equal(t, []byte("01234"), buf)
buf, err = readWithLimit(bytes.NewBuffer(bs), 5, 6)
require.NoError(t, err)
assert.Equal(t, []byte("012345"), buf)
buf, err = readWithLimit(bytes.NewBuffer(bs), 5, len(bs))
require.NoError(t, err)
assert.Equal(t, []byte("0123456789abcdef"), buf)
buf, err = readWithLimit(bytes.NewBuffer(bs), 5, 100)
require.NoError(t, err)
assert.Equal(t, []byte("0123456789abcdef"), buf)
// test with error
buf, err = readWithLimit(&readerWithError{bytes.NewBuffer(bs)}, 5, 10)
require.NoError(t, err)
assert.Equal(t, []byte("0123456789"), buf)
buf, err = readWithLimit(&readerWithError{bytes.NewBuffer(bs)}, 5, 100)
require.ErrorContains(t, err, "test error")
assert.Empty(t, buf)
// test public function
buf, err = ReadWithLimit(bytes.NewBuffer(bs), 2)
require.NoError(t, err)
assert.Equal(t, []byte("01"), buf)
buf, err = ReadWithLimit(bytes.NewBuffer(bs), 9999999)
require.NoError(t, err)
assert.Equal(t, []byte("0123456789abcdef"), buf)
}

View file

@ -54,3 +54,12 @@ func SplitTrimSpace(input, sep string) []string {
return stringList
}
// TruncateRunes returns a truncated string with given rune limit,
// it returns input string if its rune length doesn't exceed the limit.
func TruncateRunes(str string, limit int) string {
if utf8.RuneCountInString(str) < limit {
return str
}
return string([]rune(str)[:limit])
}

View file

@ -44,3 +44,18 @@ func TestSplitString(t *testing.T) {
}
test(tc, SplitStringAtByteN)
}
func TestTruncateRunes(t *testing.T) {
assert.Empty(t, TruncateRunes("", 0))
assert.Empty(t, TruncateRunes("", 1))
assert.Empty(t, TruncateRunes("ab", 0))
assert.Equal(t, "a", TruncateRunes("ab", 1))
assert.Equal(t, "ab", TruncateRunes("ab", 2))
assert.Equal(t, "ab", TruncateRunes("ab", 3))
assert.Empty(t, TruncateRunes("测试", 0))
assert.Equal(t, "测", TruncateRunes("测试", 1))
assert.Equal(t, "测试", TruncateRunes("测试", 2))
assert.Equal(t, "测试", TruncateRunes("测试", 3))
}

View file

@ -11,7 +11,7 @@ copy_content = Копиране на съдържанието
user_profile_and_more = Профил и настройки…
view = Преглед
your_settings = Настройки
mirrors = Огледала
mirrors = Огледални
explore = Разглеждане
write = Писане
twofa = Двуфакторно удостоверяване
@ -36,7 +36,7 @@ dashboard = Табло
logo = Лого
toc = Съдържание
copy_url = Копиране на URL
new_mirror = Ново огледало
new_mirror = Ново огледално
re_type = Потвърдете паролата
copy = Копиране
enabled = Включено
@ -61,7 +61,7 @@ ok = Добре
manage_org = Управление на организациите
new_repo = Ново хранилище
register = Регистрация
mirror = Огледало
mirror = Огледално
username = Потребителско име
password = Парола
template = Шаблон
@ -69,7 +69,7 @@ signed_in_as = Влезли сте като
sign_up = Регистриране
enable_javascript = Този сайт изисква JavaScript.
home = Начало
email = Адрес на ел. поща
email = Адрес за ел. поща
issues = Задачи
retry = Повторен опит
remove = Премахване
@ -93,8 +93,8 @@ filter.not_fork = Не разклонения
filter.is_template = Шаблони
filter.not_template = Не шаблони
filter.private = Частни
filter.is_mirror = Огледала
filter.not_mirror = Не огледала
filter.is_mirror = Огледални
filter.not_mirror = Не огледални
copy_hash = Копиране на контролната сума
artifacts = Артефакти
show_log_seconds = Показване на секундите
@ -102,7 +102,7 @@ remove_all = Премахване на всичко
test = Проба
remove_label_str = Премахване на елемента „%s“
copy_branch = Копиране на името на клона
error404 = Страницата, която се опитвате да отворите, или <strong>не съществува</strong> или <strong>не сте упълномощени</strong> да я видите.
error404 = Страницата, която се опитвате да отворите, или <strong>не съществува</strong>, или <strong>е премахната</strong>, или <strong>не сте упълномощени</strong> да я видите.
new_repo.link = Ново хранилище
new_migrate.title = Нова миграция
new_repo.title = Ново хранилище
@ -112,6 +112,35 @@ new_org.link = Нова организация
copy_generic = Копиране в клипборда
copy_error = Неуспешно копиране
copy_path = Копиране на пътя
toggle_menu = Превключване на менюто
confirm_delete_artifact = Сигурни ли сте, че искате да изтриете артефакта „%s“?
more_items = Още елементи
twofa_scratch = Резервен код за двуфакторно удостоверяване
webauthn_use_twofa = Използвайте двуфакторен код от телефона си
webauthn_error_insecure = WebAuthn поддържа само сигурни връзки. За тестване през HTTP можете да използвате произход „localhost“ или „127.0.0.1“
error413 = Изчерпали сте квотата си.
go_back = Връщане
invalid_data = Невалидни данни: %v
archived = Архивирано
concept_system_global = Глобално
concept_user_individual = Индивидуално
show_full_screen = Показване на цял екран
show_timestamps = Показване на времеви отпечатъци
rerun = Повторно изпълнение
copy_type_unsupported = Този тип файл не може да бъде копиран
webauthn_error_unknown = Възникна неизвестна грешка. Моля, опитайте отново.
webauthn_error_unable_to_process = Сървърът не можа да обработи заявката ви.
webauthn_error_empty = Трябва да зададете име за този ключ.
webauthn_error_timeout = Времето за изчакване изтече преди ключът ви да бъде прочетен. Моля, презаредете страницата и опитайте отново.
return_to_forgejo = Връщане към Forgejo
unknown = Неизвестно
confirm_delete_selected = Потвърждавате ли изтриването на всички избрани елементи?
webauthn_insert_key = Поставете вашия ключ за сигурност
webauthn_press_button = Моля, натиснете бутона на вашия ключ за сигурност…
webauthn_sign_in = Натиснете бутона на вашия ключ за сигурност. Ако ключът ви за сигурност няма бутон, поставете го отново.
webauthn_error = Неуспешно прочитане на вашия ключ за сигурност.
webauthn_unsupported_browser = Вашият браузър в момента не поддържа WebAuthn.
webauthn_error_duplicated = Ключът за сигурност не е разрешен за тази заявка. Моля, уверете се, че ключът не е вече регистриран.
[settings]
ui = Тема
@ -157,7 +186,7 @@ account = Акаунт
update_avatar = Обновяване на профилната снимка
ssh_gpg_keys = SSH / GPG ключове
comment_type_group_milestone = Етап
manage_emails = Управление на адресите на ел. поща
manage_emails = Управление на адресите за ел. поща
permission_read = Четене
update_password = Обновяване на паролата
biography_placeholder = Разкажете на другите малко за себе си! (Можете да използвате Маркдаун)
@ -183,7 +212,7 @@ user_block_success = Потребителят е блокиран успешно
update_profile_success = Профилът ви е обновен.
update_user_avatar_success = Профилната снимка на потребителя е обновена.
remove_oauth2_application_success = Приложението е изтрито.
email_deletion_success = Адресът на ел. поща е премахнат.
email_deletion_success = Адресът за ел. поща е премахнат.
update_avatar_success = Профилната ви снимка е обновена.
change_username = Потребителското ви име е променено.
comment_type_group_assignee = Изпълнител
@ -191,22 +220,22 @@ enable_custom_avatar = Използване на персонализирана
requires_activation = Изисква активиране
activated = Активиран
primary = Основен
email_deletion = Премахване на адреса на ел. поща
add_new_email = Добавяне на нов адрес на ел. поща
add_email = Добавяне на адрес на ел. поща
email_deletion = Премахване на адреса за ел. поща
add_new_email = Добавяне на нов адрес за ел. поща
add_email = Добавяне на адрес за ел. поща
key_content_gpg_placeholder = Започва с „-----BEGIN PGP PUBLIC KEY BLOCK-----“
comment_type_group_title = Заглавие
comment_type_group_label = Етикет
change_username_prompt = Забележка: Промяната на потребителското ви име променя също URL на вашия акаунт.
change_username_prompt = Бележка: Промяната на потребителското ви име променя също URL на вашия акаунт.
update_language_not_found = Езикът „%s“ не е наличен.
keep_activity_private_popup = Вашата дейност ще бъде видима само за вас и администраторите на сайта
uploaded_avatar_not_a_image = Каченият файл не е изображение.
uploaded_avatar_is_too_big = Размерът на качения файл (%d KiB) надвишава максималния размер (%d KiB).
change_password_success = Паролата ви е обновена. Влизайте с новата си парола от сега нататък.
change_password_success = Паролата ви е обновена. Отсега нататък използвайте новата си парола, за да влезете.
manage_themes = Тема по подразбиране
manage_openid = OpenID адреси
primary_email = Да е основен
keep_email_private = Скриване на адреса на ел. поща
keep_email_private = Скриване на адреса за ел. поща
theme_update_error = Избраната тема не съществува.
theme_update_success = Темата ви е обновена.
key_content_ssh_placeholder = Започва с „ssh-ed25519“, „ssh-rsa“, „ecdsa-sha2-nistp256“, „ecdsa-sha2-nistp384“, „ecdsa-sha2-nistp521“, „sk-ecdsa-sha2-nistp256@openssh.com“, или „sk-ssh-ed25519@openssh.com“
@ -227,7 +256,7 @@ saved_successfully = Настройките бяха запазени успеш
no_activity = Няма скорошна дейност
theme_desc = Тази тема ще се използва за уеб интерфейса, когато сте влезли.
keep_activity_private = Скриване на дейността от профилната страница
lookup_avatar_by_mail = Търсене на профилна снимка по адреса на ел. поща
lookup_avatar_by_mail = Търсене на профилна снимка по адреса за ел. поща
password_incorrect = Текущата парола е неправилна.
change_username_redirect_prompt = Старото потребителско име ще се пренасочва, докато някой не го вземе.
principal_content = Съдържание
@ -246,7 +275,7 @@ delete_prompt = Тази операция ще изтрие перманентн
email_notifications.disable = Изключване на известията по ел. поща
delete_account = Изтриване на акаунта ви
confirm_delete_account = Потвърждаване на изтриването
email_notifications.onmention = Ел. поща само при споменаване
email_notifications.onmention = Ел. писмо само при споменаване
pronouns_unspecified = Непосочени
pronouns = Местоимения
gpg_token_code = echo "%s" | gpg -a --default-key %s --detach-sig
@ -254,8 +283,103 @@ language.title = Език по подразбиране
language.localization_project = Помогнете ни да преведем Forgejo на вашия език! <a href="%s">Научете повече</a>.
language.description = Този език ще бъде запазен във вашия акаунт и ще се използва като език по подразбиране, след като влезете.
pronouns_custom = Персонализирани
visibility.limited_tooltip = Видимо само за влезли потребители
visibility.limited_tooltip = Видим само за влезли потребители
pronouns_custom_label = Персонализирани местоимения
comment_type_group_review_request = Искане за рецензия
ssh_key_been_used = Този SSH ключ вече е добавен към сървъра.
create_oauth2_application = Създаване на ново OAuth2 приложение
update_oauth2_application_success = Успешно обновихте OAuth2 приложението.
authorized_oauth2_applications = Упълномощени OAuth2 приложения
manage_account_links = Свързани акаунти
revoke_oauth2_grant = Отнемане на достъпа
added_on = Добавен на %s
comment_type_group_dependency = Зависимост
update_hints_success = Подсказките са обновени.
manage_oauth2_applications = Управление на OAuth2 приложения
gpg_key_id_used = Вече съществува публичен GPG ключ със същото ID.
oauth2_applications_desc = OAuth2 приложенията позволяват на вашето приложение от трета страна да удостоверява сигурно потребители в тази инстанция на Forgejo.
blocked_since = Блокиран от %s
hidden_comment_types.ref_tooltip = Коментари, в които тази задача е спомената от друга задача/подаване/…
create_oauth2_application_success = Успешно създадохте ново OAuth2 приложение.
quota.applies_to_org = Следните правила за квота се прилагат за тази организация
keep_activity_private.description = Вашата <a href="%s">публична дейност</a> ще бъде видима само за вас и администраторите на инстанцията.
ssh_helper = <strong>Нуждаете се от помощ?</strong> Разгледайте ръководството за <a href="%s">създаване на собствени SSH ключове</a> или за решаване на <a href="%s">често срещани проблеми</a>, които може да срещнете при използване на SSH.
twofa_desc = За да защитите акаунта си от кражба на парола, можете да използвате смартфон или друго устройство за получаване на еднократни пароли, базирани на време („TOTP“).
scan_this_image = Сканирайте това изображение с вашето приложение за удостоверяване:
quota.rule.exceeded.helper = Общият размер на обектите за това правило надвиши квотата.
password_change_disabled = Нелокални потребители не могат да обновяват паролата си през уеб интерфейса на Forgejo.
twofa_disable_note = Можете да изключите двуфакторното удостоверяване, ако е необходимо.
hooks.desc = Добавете уеб-куки, които ще се задействат за <strong>всички хранилища</strong>, които притежавате.
delete_account_desc = Сигурни ли сте, че искате да изтриете перманентно този потребителски акаунт?
last_used = Последно използван на
revoke_oauth2_grant_description = Отнемането на достъпа за това приложение от трета страна ще му попречи да има достъп до вашите данни. Сигурни ли сте?
password_username_disabled = Нелокални потребители не могат да променят потребителското си име. Моля, свържете се с администратора на сайта за повече подробности.
change_username_redirect_prompt.with_cooldown.one = Старото потребителско име ще бъде достъпно за всички след период на изчакване от %[1]d ден. Все още можете да си върнете старото потребителско име по време на периода на изчакване.
change_username_redirect_prompt.with_cooldown.few = Старото потребителско име ще бъде достъпно за всички след период на изчакване от %[1]d дни. Все още можете да си върнете старото потребителско име по време на периода на изчакване.
generate_token_name_duplicate = <strong>%s</strong> вече е използвано като име на приложение. Моля, използвайте ново.
quota.rule.exceeded = Надвишена
repo_and_org_access = Достъп до хранилища и организации
permissions_public_only = Само публични
permissions_list = Разрешения:
edit_oauth2_application = Редактиране на OAuth2 приложение
remove_oauth2_application = Премахване на OAuth2 приложение
twofa_recovery_tip = Ако загубите устройството си, ще можете да използвате ключ за еднократно възстановяване, за да си върнете достъпа до акаунта.
visibility.private_tooltip = Видим само за членове на организации, в които участвате
quota.applies_to_user = Следните правила за квота се прилагат за вашия акаунт
quota.rule.no_limit = Неограничена
hints = Подсказки
comment_type_group_issue_ref = Препратка към задача
activate_email = Изпращане на активация
ssh_disabled = SSH е изключен
twofa_disable_desc = Изключването на двуфакторното удостоверяване ще направи акаунта ви по-малко сигурен. Продължаване?
keep_pronouns_private = Показване на местоименията само на удостоверени потребители
keep_pronouns_private.description = Това ще скрие вашите местоимения от посетители, които не са влезли в системата.
gpg_helper = <strong>Нуждаете се от помощ?</strong> Разгледайте ръководството <a href="%s">относно GPG</a>.
valid_until_date = Валиден до %s
ssh_externally_managed = Този SSH ключ се управлява външно за този потребител
regenerate_scratch_token_desc = Ако сте загубили ключа си за възстановяване или вече сте го използвали, за да влезете, можете да го нулирате тук.
create_oauth2_application_button = Създаване на приложение
revoke_oauth2_grant_success = Достъпът е отнет успешно.
comment_type_group_deadline = Краен срок
comment_type_group_time_tracking = Проследяване на времето
activations_pending = Чакащи активации
valid_forever = Валиден завинаги
key_state_desc = Този ключ е използван през последните 7 дни
revoke_key = Отнемане
delete_account_title = Изтриване на потребителския акаунт
update_hints = Обновяване на подсказките
permissions_access_all = Всички (публични, частни и ограничени)
oauth2_application_name = Име на приложението
visibility.public_tooltip = Видим за всички
user_block_yourself = Не можете да блокирате себе си.
hidden_comment_types.issue_ref_tooltip = Коментари, в които потребителят променя клона/маркера, свързан със задачата
comment_type_group_reference = Препратка
comment_type_group_branch = Клон
comment_type_group_pull_request_push = Добавени подавания
quota = Квота
webauthn_delete_key = Премахване на ключ за сигурност
webauthn_register_key = Добавяне на ключ за сигурност
webauthn_nickname = Прякор
webauthn_delete_key_desc = Ако премахнете ключ за сигурност, вече няма да можете да влизате с него. Продължаване?
additional_repo_units_hint = Предлагане за включване на допълнителни елементи на хранилището
twofa_is_enrolled = Вашият акаунт в момента е <strong>включен</strong> в двуфакторно удостоверяване.
twofa_not_enrolled = Вашият акаунт в момента не е включен в двуфакторно удостоверяване.
webauthn_key_loss_warning = Ако загубите ключовете си за сигурност, ще загубите достъп до акаунта си.
email_desc = Вашият основен адрес за ел. поща ще се използва за известия, възстановяване на парола и, при условие че не е скрит, за уеб-базирани Git операции.
email_preference_set_success = Предпочитанията за ел. поща са зададени успешно.
add_email_confirmation_sent = Изпратено е ел. писмо за потвърждение до „%s“. За да потвърдите адреса си за ел. поща, моля, проверете входящата си кутия и последвайте предоставената връзка в рамките на следващите %s.
additional_repo_units_hint_description = Показване на подсказка „Включване на повече“ за хранилища, които нямат включени всички налични елементи.
email_notifications.submit = Задаване на предпочит. за ел. поща
email_notifications.andyourown = И вашите собствени известия
email_deletion_desc = Адресът за ел. поща и свързаната информация ще бъдат премахнати от вашия акаунт. Git подаванията от този адрес за ел. поща ще останат непроменени. Продължаване?
add_email_success = Новият адрес за ел. поща е добавен.
remove_account_link = Премахване на свързан акаунт
webauthn_alternative_tip = Може да искате да конфигурирате допълнителен метод за удостоверяване.
hidden_comment_types_description = Типовете коментари, отметнати тук, няма да се показват в страниците на задачите. Например, отмятането на „Етикет“ премахва всички коментари от типа „<потребител> добави/премахна <етикет>“.
hidden_comment_types = Скрити типове коментари
comment_type_group_lock = Състояние на заключване
can_not_add_email_activations_pending = Има чакаща активация, опитайте отново след няколко минути, ако искате да добавите нова ел. поща.
storage_overview = Преглед на съхранението
[packages]
container.labels.value = Стойност
@ -287,6 +411,33 @@ generic.download = Изтеглете пакета от командния ре
container.details.type = Тип образ
alpine.repository = За хранилището
container.images.title = Образи
arch.version.description = Описание
search_in_external_registry = Търсене в %s
filter.type = Тип
filter.container.untagged = Без маркер
filter.type.all = Всички
registry.documentation = За повече информация относно регистъра %s, вижте <a target="_blank" rel="noopener noreferrer" href="%s">документацията</a>.
filter.no_result = Вашият филтър не даде резултати.
filter.container.tagged = С маркер
arch.pacman.repo.multi = %s има същата версия в различни дистрибуции.
arch.pacman.helper.gpg = Добавете доверителен сертификат за pacman:
alpine.repository.architectures = Архитектури
arch.version.provides = Доставя
arch.version.groups = Група
details.project_site = Уебсайт на проекта
arch.pacman.conf = Добавете сървър със свързаната дистрибуция и архитектура към <code>/etc/pacman.conf</code> :
arch.pacman.sync = Синхронизирайте пакета с pacman:
details.repository_site = Уебсайт на хранилището
arch.version.depends = Зависимости
arch.version.optdepends = Допълнителни зависимости
arch.version.replaces = Заменя
go.install = Инсталирайте пакета от командния ред:
cargo.registry = Настройте този регистър в конфигурационния файл на Cargo (например <code>~/.cargo/config.toml</code>):
cargo.install = За да инсталирате пакета с Cargo, изпълнете следната команда:
details.documentation_site = Уебсайт на документацията
arch.version.conflicts = В конфликт
alpine.repository.branches = Клонове
arch.pacman.repo.multi.item = Конфигурация за %s
[tool]
hours = %d часа
@ -447,7 +598,7 @@ projects.template.desc = Шаблон
projects.card_type.text_only = Само текст
projects.card_type.images_and_text = Изображения и текст
wiki = Уики
wiki.welcome = Добре дошли в Уикито.
wiki.welcome = Добре дошли в уикито.
wiki.create_first_page = Създаване на първата страница
editor.upload_file = Качване на файл
projects.column.color = Цвят
@ -644,7 +795,7 @@ milestones.filter_sort.latest_due_date = Най-далечен краен сро
diff.view_file = Преглед на файла
release.deletion_success = Изданието е изтрито.
projects.column.delete = Изтриване на колоната
migrate.migrating = Мигриране от <b>%s</b> ...
migrate.migrating = Мигриране от <b>%s</b>
escape_control_characters = Екраниране
issues.label_deletion_success = Етикетът е изтрит.
pulls.is_closed = Заявката за сливане е затворена.
@ -1025,7 +1176,7 @@ issues.content_history.edited = редактирано
pulls.title_desc_one = иска да слее %[1]d подаване от <code>%[2]s</code> в <code id="%[4]s">%[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 = Не сте упълномощени за сливане на тази заявка за сливане.
pulls.no_merge_access = Не сте упълномощени да слеете тази заявка за сливане.
activity.navbar.code_frequency = Честота на промените
activity.git_stats_pushed_1 = е изтласкал
activity.git_stats_push_to_branch = към %s и
@ -1139,7 +1290,7 @@ issues.review.review = Рецензия
issues.review.comment = рецензира %s
branch.deleted_by = Изтрит от %s
branch.restore = Възстановяване на клона „%s“
archive.title_date = Това хранилище е архивирано на %s. Можете да преглеждате файлове и да го клонирате, но не можете да изтласквате или отваряте задачи или заявки за сливане.
archive.title_date = Това хранилище е архивирано на %s. Можете да преглеждате файлове и да го клонирате, но не можете да правите промени в състоянието му, като изтласкване и създаване на нови задачи, заявки за сливане или коментари.
release.download_count_one = %s изтегляне
release.download_count_few = %s изтегляния
branch.restore_success = Клонът „%s“ е възстановен.
@ -1156,7 +1307,7 @@ pulls.reopen_to_merge = Моля, отворете наново тази зая
pulls.cant_reopen_deleted_branch = Тази заявка за сливане не може да бъде отворена наново, защото клонът е изтрит.
pulls.status_checks_hide_all = Скриване на всички проверки
pulls.status_checks_failure = Някои проверки са неуспешни
issues.review.add_review_request = поиска рецензия от %s %s
issues.review.add_review_request = поиска рецензия от %[1]s %[2]s
wiki.no_search_results = Няма резултати
wiki.search = Търсене в уикито
issues.author.tooltip.pr = Този потребител е авторът на тази заявка за сливане.
@ -1290,6 +1441,140 @@ issues.reaction.alt_few = %[1]s реагира с %[2]s.
issues.reaction.alt_many = %[1]s и още %[2]d реагираха с %[3]s.
issues.reaction.alt_add = Добавяне на реакция %[1]s към коментара.
issues.reaction.alt_remove = Премахване на реакция %[1]s от коментара.
already_forked = Вече сте разклонили %s
generated_from = генерирано от
clear_ref = `Изчистване на текущата препратка`
file_follow = Последване на символната връзка
commitstatus.failure = Неуспех
issues.filter_label_exclude = `Използвайте <code>alt</code> + <code>click/enter</code>, за да изключите етикети`
migrate.migrating_failed = Мигрирането от <b>%s</b> е неуспешно.
migrate.migrating_issues = Мигриране на задачи
mirror_from = огледално на
fork_from_self = Не можете да разклоните хранилище, което притежавате.
commit_graph.hide_pr_refs = Скриване на заявките за сливане
generated = Генерирано
broken_message = Git данните, лежащи в основата на това хранилище, не могат да бъдат прочетени. Свържете се с администратора на тази инстанция или изтрийте това хранилище.
editor.file_is_a_symlink = `„%s“ е символна връзка. Символните връзки не могат да се редактират в уеб редактора`
commits.browse_further = Разглеждане нататък
commits.older = По-стари
form.reach_limit_of_creation_n = Притежателят вече е достигнал лимита от %d хранилища.
issues.edit.already_changed = Неуспешно запазване на промените в задачата. Изглежда съдържанието вече е променено от друг потребител. Моля, презаредете страницата и опитайте да редактирате отново, за да избегнете презаписването на техните промени
transfer.accept_desc = Прехвърляне към „%s“
archive.title = Това хранилище е архивирано. Можете да преглеждате файлове и да го клонирате, но не можете да правите промени в състоянието му, като изтласкване и създаване на нови задачи, заявки за сливане или коментари.
form.reach_limit_of_creation_1 = Притежателят вече е достигнал лимита от %d хранилище.
editor.patching = Прилагане на кръпка:
editor.fail_to_apply_patch = Неуспешно прилагане на кръпка „%s“
commits.no_commits = Няма общи подавания. „%s“ и „%s“ имат напълно различни истории.
migrate.migrating_pulls = Мигриране на заявки за сливане
migrate.migrating_topics = Мигриране на теми
projects.desc = Управлявайте задачи и заявки за сливане в проектни табла.
issues.choose.invalid_templates = %v невалидни шаблона са намерени
pulls.edit.already_changed = Неуспешно запазване на промените в заявката за сливане. Изглежда съдържанието вече е променено от друг потребител. Моля, презаредете страницата и опитайте да редактирате отново, за да избегнете презаписването на техните промени
migrate.gitbucket.description = Мигриране на данни от GitBucket инстанции.
migrate.migrating_git = Мигриране на Git данни
commits.newer = По-нови
issues.choose.blank_about = Създаване на задача от стандартен шаблон.
issues.filter_no_results = Няма резултати
issues.filter_no_results_placeholder = Опитайте да коригирате филтрите си за търсене.
archive.nocomment = Коментирането не е възможно, тъй като хранилището е архивирано.
migrate.gitlab.description = Мигриране на данни от gitlab.com или други GitLab инстанции.
transfer.no_permission_to_accept = Нямате разрешение да приемете това прехвърляне.
transfer.no_permission_to_reject = Нямате разрешение да отхвърлите това прехвърляне.
editor.file_changed_while_editing = Съдържанието на файла е променено, откакто сте го отворили. <a target="_blank" rel="noopener noreferrer" href="%s">Щракнете тук</a>, за да го видите, или <strong>Подайте промените отново</strong>, за да ги презапишете.
sync_fork.button = Синхронизиране
migrate.onedev.description = Мигриране на данни от code.onedev.io или други OneDev инстанции.
migrate.codebase.description = Мигриране на данни от codebasehq.com.
migrate.migrating_labels = Мигриране на етикети
migrate.migrating_releases = Мигриране на издания
editor.push_rejected_no_message = Промяната беше отхвърлена от сървъра без съобщение. Моля, проверете Git куките.
issues.choose.open_external_link = Отваряне
comments.edit.already_changed = Неуспешно запазване на промените в коментара. Изглежда съдържанието вече е променено от друг потребител. Моля, презаредете страницата и опитайте да редактирате отново, за да избегнете презаписването на техните промени
commits.nothing_to_compare = Тези клонове са равни.
transfer.reject_desc = Отказ от прехвърляне към „%s“
subscribe.pull.guest.tooltip = Влезте, за да се абонирате за тази заявка за сливане.
commit.contained_in_default_branch = Това подаване е част от стандартния клон
normal_view = Нормален изглед
issues.context.menu = Меню за коментара
form.name_reserved = Името на хранилището „%s“ е резервирано.
need_auth = Упълномощаване
subscribe.issue.guest.tooltip = Влезте, за да се абонирате за тази задача.
commitstatus.pending = В очакване
commitstatus.success = Успех
editor.cannot_commit_to_protected_branch = Не може да се подава в защитения клон „%s“.
editor.no_commit_to_branch = Не може да се подава директно в клона, защото:
editor.push_rejected = Промяната беше отхвърлена от сървъра. Моля, проверете Git куките.
cite_this_repo = Цитиране на това хранилище
migrate.gitea.description = Мигриране на данни от gitea.com или други Gitea инстанции.
editor.push_rejected_summary = Пълно съобщение на отхвърлянето:
sync_fork.branch_behind_one = Този клон е %[1]d подаване зад %[2]s
sync_fork.branch_behind_few = Този клон е %[1]d подавания зад %[2]s
form.string_too_long = Даденият низ е по-дълъг от %d знака.
editor.commit_id_not_matching = Файлът е променен, докато сте го редактирали. Подайте в нов клон и след това слейте.
editor.user_no_push_to_branch = Потребителят не може да изтласква в клона
archive.pull.noreview = Това хранилище е архивирано. Не можете да рецензирате заявки за сливане.
migrate.migrating_failed.error = Неуспешно мигриране: %s
migrate.github.description = Мигриране на данни от github.com или GitHub Enterprise сървър.
migrate.forgejo.description = Мигриране на данни от codeberg.org или други Forgejo инстанции.
migrate.gogs.description = Мигриране на данни от notabug.org или други Gogs инстанции.
migrate.migrating_milestones = Мигриране на етапи
migrate.failed = Мигрирането е неуспешно: %v
pulls.nothing_to_compare_and_allow_empty_pr = Тези клонове са равни. Тази заявка за сливане ще бъде празна.
pulls.has_pull_request = `Вече съществува заявка за сливане между тези клонове: <a href="%[1]s">%[2]s#%[3]d</a>`
pulls.is_checking = Проверката за конфликти при сливане е в ход. Опитайте отново след няколко минути.
pulls.cannot_merge_work_in_progress = Тази заявка за сливане е отбелязана като в процес на работа.
pulls.blocked_by_approvals = Тази заявка за сливане все още няма достатъчно одобрения. Дадени са %d от %d одобрения.
pulls.blocked_by_rejection = Тази заявка за сливане има поискани промени от официален рецензент.
pulls.waiting_count_1 = %d чакаща рецензия
pulls.status_checks_requested = Задължително
pulls.update_branch_success = Обновяването на клона е успешно
pulls.cannot_auto_merge_helper = Слейте ръчно, за да разрешите конфликтите.
migrate.clone_address_desc = HTTP(S) или Git „clone“ URL на съществуващо хранилище
pulls.add_prefix = Добавете префикс <strong>%s</strong>
pulls.merge_pull_request = Създаване на подаване със сливане
pulls.waiting_count_n = %d чакащи рецензии
pulls.is_ancestor = Този клон вече е включен в целевия клон. Няма какво да се слива.
pulls.required_status_check_missing = Някои задължителни проверки липсват.
pulls.change_target_branch_at = `промени целевия клон от <b>%s</b> на <b>%s</b> %s`
issues.time_spent_total = Общо изразходвано време
issues.del_time_history = `изтри изразходваното време %s`
pulls.nothing_to_compare_have_tag = Избраните клон/маркер са равни.
pulls.cannot_auto_merge_desc = Тази заявка за сливане не може да бъде слята автоматично поради конфликти.
issues.tracker_auto_close = Таймерът ще бъде спрян автоматично, когато тази задача бъде затворена
issues.force_push_codes = `изтласка принудително %[1]s от <a class="%[7]s" href="%[3]s"><code>%[2]s</code></a> към <a class="%[7]s" href="%[5]s"><code>%[4]s</code></a> %[6]s`
pulls.blocked_by_official_review_requests = Тази заявка за сливане е блокирана, защото липсва одобрение от един или повече официални рецензенти.
issues.tracker = Проследяване на времето
issues.add_time_history = `добави изразходвано време %s`
migrate.repo_desc_helper = Оставете празно, за да внесете съществуващото описание
migrate.git.description = Мигриране само на хранилище от всяка Git услуга.
mirror_sync = синхронизирано
migrate_repo = Мигриране на хранилище
migrate_options = Опции за мигрирането
editor.fork_before_edit = Трябва да разклоните това хранилище, за да направите или предложите промени в този файл.
editor.must_have_write_access = Трябва да имате право на запис, за да правите или предлагате промени в този файл.
editor.new_branch_name = Дайте име на новия клон за това подаване
editor.invalid_commit_mail = Невалидна ел. поща за създаване на подаване.
pulls.required_status_check_failed = Някои задължителни проверки не са успешни.
issues.time_spent_from_all_authors = `Общо изразходвано време: %s`
issues.attachment.download = `Щракнете, за да изтеглите „%s“`
issues.attachment.open_tab = `Щракнете, за да видите „%s“ в нов раздел`
pulls.update_branch = Обновяване на клона чрез сливане
migrate_items = Елементи за мигриране
commit.load_referencing_branches_and_tags = Зареждане на клонове и маркери, препращащи към това подаване
pulls.files_conflicted = Тази заявка за сливане има промени, които са в конфликт с целевия клон.
pulls.still_in_progress = Все още е в процес на работа?
pulls.ready_for_review = Готово е за рецензиране?
pulls.is_empty = Промените в този клон вече са в целевия клон. Това ще бъде празно подаване.
issues.start_tracking = Започване на проследяване на времето
migrate_options_mirror_helper = Това хранилище ще бъде огледално
migrate_options_lfs = Мигриране на LFS файлове
editor.upload_file_is_locked = Файлът „%s“ е заключен от %s.
issues.tracking_already_started = `Вече сте започнали проследяване на времето по <a href="%s">друга задача</a>!`
pulls.remove_prefix = Премахнете префикса <strong>%s</strong>
author_search_tooltip = Показва максимум 30 потребители
migrate.migrating_failed_no_addr = Мигрирането е неуспешно.
issues.force_push_compare = Сравняване
pulls.status_checking = Някои проверки са в очакване
pulls.nothing_to_compare = Тези клонове са равни. Не е нужно да създавате заявка за сливане.
[modal]
confirm = Потвърждаване
@ -1319,6 +1604,11 @@ table_modal.placeholder.content = Съдържание
table_modal.placeholder.header = Заглавка
buttons.new_table.tooltip = Добавяне на таблица
table_modal.header = Добавяне на таблица
link_modal.description = Описание
link_modal.header = Добавяне на връзка
buttons.indent.tooltip = Вмъкване на елементи с едно ниво
buttons.unindent.tooltip = Изваждане на елементи с едно ниво
link_modal.paste_reminder = Подсказка: С URL адрес в клипборда можете да поставите директно в редактора, за да създадете връзка.
[org]
teams.write_access = Писане
@ -1393,11 +1683,12 @@ members.private_helper = Да е видим
teams.no_desc = Този екип няма описание
settings.delete_org_desc = Тази организация ще бъде изтрита перманентно. Продължаване?
open_dashboard = Отваряне на таблото
settings.change_orgname_prompt = Бележка: Промяната на името на организацията ще промени и URL адреса на вашата организация и ще освободи старото име.
[install]
admin_password = Парола
user = Потребителско име
admin_email = Адрес на ел. поща
admin_email = Адрес за ел. поща
path = Път
password = Парола
host = Хост
@ -1427,13 +1718,17 @@ admin_title = Настройки на администраторския ака
err_empty_admin_password = Администраторската парола не може да бъде празна.
docker_helper = Ако стартирате Forgejo в Docker, моля, прочетете <a target="_blank" rel="noopener noreferrer" href="%s">документацията</a> преди да промените настройки.
sqlite_helper = Път на файла за SQLite3 базата данни.<br>Въведете абсолютен път, ако стартирате Forgejo като service.
err_empty_admin_email = Администраторският адрес на ел. поща не може да бъде празен.
err_empty_admin_email = Администраторският адрес за ел. поща не може да бъде празен.
password_algorithm = Алгоритъм за хеш. на паролите
default_keep_email_private = Скриване на адресите на ел. поща по подразбиране
default_keep_email_private = Скриване на адресите за ел. поща по подразбиране
invalid_password_algorithm = Невалиден алгоритъм за хеш. на паролите
err_admin_name_is_reserved = Потребителското име на администратора е невалидно, потребителското име е резервирано
err_admin_name_pattern_not_allowed = Потребителското име на администратора е невалидно, потребителското име съответства с резервиран шаблон
err_admin_name_is_invalid = Потребителското име на администратора е невалидно
db_schema_helper = Оставете празно за схемата по подразбиране на базата данни („public“).
reinstall_error = Опитвате се да инсталирате върху съществуваща Forgejo база данни
reinstall_confirm_message = Преинсталирането със съществуваща Forgejo база данни може да причини множество проблеми. В повечето случаи трябва да използвате съществуващия си „app.ini“, за да стартирате Forgejo. Ако знаете какво правите, потвърдете следното:
app_slogan = Слоган на инстанцията
[filter]
string.asc = А - Я
@ -1462,8 +1757,8 @@ link_not_working_do_paste = Ако връзката не работи, опит
activate_account = Моля, активирайте своя акаунт
admin.new_user.subject = Нов потребител %s току-що се регистрира
activate_account.text_1 = Здравейте, <b>%[1]s</b>, благодарим ви за регистрацията в %[2]s!
activate_email.text = Моля, щракнете върху следната връзка, за да потвърдите своя адрес на ел. поща в рамките на <b>%s</b>:
activate_email = Потвърдете своя адрес на ел. поща
activate_email.text = Моля, щракнете върху следната връзка, за да потвърдите своя адрес за ел. поща в рамките на <b>%s</b>:
activate_email = Потвърдете своя адрес за ел. поща
activate_account.text_2 = Моля, щракнете върху следната връзка, за да активирате своя акаунт в рамките на <b>%s</b>:
issue_assigned.issue = @%[1]s ви възложи задача %[2]s в хранилище %[3]s.
issue.action.push_n = <b>@%[1]s</b> изтласка %[3]d подавания към %[2]s
@ -1473,6 +1768,28 @@ issue.action.merge = <b>@%[1]s</b> сля #%[2]d в %[3]s.
issue_assigned.pull = @%[1]s ви възложи заявката за сливане %[2]s в хранилище %[3]s.
issue.action.ready_for_review = <b>@%[1]s</b> отбеляза тази заявка за сливане като готова за рецензиране.
repo.transfer.subject_to = %s иска да прехвърли хранилище "%s" към %s
password_change.subject = Вашата парола е променена
admin.new_user.text = Моля, <a href="%s">щракнете тук</a>, за да управлявате този потребител от администраторския панел.
password_change.text_1 = Паролата за вашия акаунт току-що беше променена.
reset_password = Възстановете своя акаунт
account_security_caution.text_1 = Ако това сте били вие, можете спокойно да игнорирате това ел. писмо.
issue.action.force_push = <b>%[1]s</b> изтласка принудително <b>%[2]s</b> от %[3]s към %[4]s.
team_invite.text_3 = Бележка: Тази покана е предназначена за %[1]s. Ако не сте очаквали тази покана, можете да игнорирате това ел. писмо.
view_it_on = Вижте го на %s
register_notify.text_1 = това е ел. писмо за потвърждение на вашата регистрация в %s!
register_notify.text_2 = Можете да влезете в акаунта си с потребителско име: %s
register_notify.text_3 = Ако някой друг е създал този акаунт за вас, първо ще трябва да <a href="%s">зададете парола</a>.
repo.collaborator.added.subject = %s ви добави към %s като сътрудник
primary_mail_change.text_1 = Основният адрес за ел. поща на вашия акаунт току-що беше променен на %[1]s. Това означава, че този адрес за ел. поща повече няма да получава известия по ел. поща за вашия акаунт.
team_invite.text_2 = Моля, щракнете върху следната връзка, за да се присъедините към екипа:
repo.transfer.body = За да го приемете или отхвърлите, посетете %s или просто го игнорирайте.
repo.collaborator.added.text = Бяхте добавени като сътрудник в хранилище:
team_invite.subject = %[1]s ви покани да се присъедините към организацията %[2]s
team_invite.text_1 = %[1]s ви покани да се присъедините към екип %[2]s в организация %[3]s.
reply = или отговорете директно на това ел. писмо
reset_password.text = Ако това сте вие, моля, щракнете върху следната връзка, за да възстановите акаунта си в рамките на <b>%s</b>:
primary_mail_change.subject = Основният ви адрес за ел. поща е променен
account_security_caution.text_2 = Ако това не сте били вие, акаунтът ви е компрометиран. Моля, свържете се с администраторите на този сайт.
[user]
joined_on = Присъединени на %s
@ -1493,7 +1810,7 @@ follow = Последване
followers_few = %d последователи
block_user = Блокиране на потребителя
change_avatar = Променете профилната си снимка…
email_visibility.limited = Вашият адрес на ел. поща е видим за всички удостоверени потребители
email_visibility.limited = Вашият адрес за ел. поща е видим за всички удостоверени потребители
disabled_public_activity = Този потребител е изключил публичната видимост на дейността.
email_visibility.private = Вашият адрес на ел. поща е видим само за вас и администраторите
show_on_map = Показване на това място на картата
@ -1507,6 +1824,15 @@ public_activity.visibility_hint.self_public = Вашата дейност е в
form.name_pattern_not_allowed = Шаблонът "%s" не е разрешен в потребителско име.
form.name_reserved = Потребителското име "%s" е резервирано.
public_activity.visibility_hint.self_private_profile = Вашата дейност е видима само за вас и администраторите на инстанцията, тъй като вашият профил е частен. <a href="%s">Конфигуриране</a>.
block_user.detail = Моля, имайте предвид, че блокирането на потребител има и други ефекти, като например:
block_user.detail_2 = Този потребител няма да може да взаимодейства с хранилищата, които притежавате, или със задачите и коментарите, които сте създали.
block_user.detail_3 = Няма да можете да се добавяте един друг като сътрудници на хранилище.
public_activity.visibility_hint.self_private = Вашата дейност е видима само за вас и администраторите на инстанцията. <a href="%s">Конфигуриране</a>.
form.name_chars_not_allowed = Потребителското име „%s“ съдържа невалидни знаци.
public_activity.visibility_hint.admin_private = Тази дейност е видима за вас, защото сте администратор, но потребителят иска тя да остане частна.
public_activity.visibility_hint.admin_public = Тази дейност е видима за всички, но като администратор можете да виждате и взаимодействия в частни пространства.
follow_blocked_user = Не можете да последвате този потребител, защото сте го блокирали или той ви е блокирал.
block_user.detail_1 = Ще спрете да се следвате един друг и няма да можете да се последвате отново.
[home]
filter = Други филтри
@ -1530,6 +1856,7 @@ view_home = Преглед на %s
collaborative_repos = Съвместни хранилища
switch_dashboard_context = Превключване на контекста на таблото
show_only_public = Показване само на публични
filter_by_team_repositories = Филтриране по хранилища на екипа
[admin]
packages.version = Версия
@ -1587,7 +1914,7 @@ config.server_config = Сървърна конфигурация
packages.size = Размер
settings = Админ. настройки
users = Потребителски акаунти
emails.duplicate_active = Този адрес на ел. поща вече е активен за друг потребител.
emails.duplicate_active = Този адрес за ел. поща вече е активен за друг потребител.
config.app_ver = Forgejo версия
config.custom_conf = Път на конфигурационния файл
config.git_version = Git версия
@ -1606,16 +1933,20 @@ users.details = Потребителски данни
packages.total_size = Общ размер: %s
dashboard.new_version_hint = Forgejo %s вече е наличен, вие изпълнявате %s. Проверете <a target="_blank" rel="noreferrer" href="%s">блога</a> за повече подробности.
total = Общо: %d
config.db_type = Тип
monitor.queue.type = Тип
notices.type = Тип
[error]
not_found = Целта не може да бъде намерена.
report_message = Ако смятате, че това е грешка на Forgejo, моля, потърсете в задачите на <a href="%s" target="_blank">Codeberg</a> или отворете нова задача, ако е необходимо.
network_error = Мрежова грешка
occurred = Възникна грешка
server_internal = Вътрешна грешка на сървъра
[form]
UserName = Потребителско име
Email = Адрес на ел. поща
Email = Адрес за ел. поща
Password = Парола
RepoName = Име на хранилището
username_been_taken = Потребителското име вече е заето.
@ -1633,8 +1964,8 @@ url_error = `„%s“ не е валиден URL.`
Content = Съдържание
team_not_exist = Екипът не съществува.
TeamName = Име на екипа
email_error = ` не е валиден адрес на ел. поща.`
email_invalid = Адресът на ел. поща е невалиден.
email_error = ` не е валиден адрес за ел. поща.`
email_invalid = Адресът за ел. поща е невалиден.
SSHTitle = Име на SSH ключ
repo_name_been_taken = Името на хранилището вече е използвано.
team_name_been_taken = Името на екипа вече е заето.
@ -1647,6 +1978,44 @@ Pronouns = Местоимения
Biography = Биография
Website = Уебсайт
Location = Местоположение
cannot_add_org_to_team = Организация не може да бъде добавена като член на екип.
auth_failed = Неуспешно удостоверяване: %v
team_no_units_error = Разрешете достъп до поне една секция на хранилището.
password_uppercase_one = Поне един голям знак
CommitSummary = Обобщение на подаването
username_error = ` може да съдържа само буквено-цифрови знаци („0-9“, „a-z“, „A-Z“), тире („-“), долна черта („_“) и точка („.“). Не може да започва или завършва с не-буквено-цифрови знаци, като също така са забранени и последователни не-буквено-цифрови знаци.`
username_error_no_dots = ` може да съдържа само буквено-цифрови знаци („0-9“, „a-z“, „A-Z“), тире („-“) и долна черта („_“). Не може да започва или завършва с не-буквено-цифрови знаци, като също така са забранени и последователни не-буквено-цифрови знаци.`
duplicate_invite_to_team = Потребителят вече е поканен като член на екипа.
must_use_public_key = Ключът, който предоставихте, е частен ключ. Моля, не качвайте частния си ключ никъде. Вместо това използвайте публичния си ключ.
org_still_own_packages = Тази организация все още притежава един или повече пакети, първо ги изтрийте.
admin_cannot_delete_self = Не можете да изтриете себе си, когато сте администратор. Моля, първо премахнете администраторските си привилегии.
To = Име на клон
CommitMessage = Съобщение на подаването
include_error = ` трябва да съдържа подниз „%s“.`
alpha_dash_error = ` трябва да съдържа само буквено-цифрови знаци, тире („-“) и долна черта („_“).`
alpha_dash_dot_error = ` трябва да съдържа само буквено-цифрови знаци, тире („-“), долна черта („_“) и точка („.“).`
size_error = ` трябва да е с размер %s.`
min_size_error = ` трябва да съдържа поне %s знака.`
max_size_error = ` трябва да съдържа най-много %s знака.`
invalid_group_team_map_error = ` съпоставянето е невалидно: %s`
password_complexity = Паролата не отговаря на изискванията за сложност:
password_lowercase_one = Поне един малък знак
password_digit_one = Поне една цифра
password_special_one = Поне един специален знак (препинателни знаци, скоби, кавички и др.)
enterred_invalid_repo_name = Името на хранилището, което въведохте, е неправилно.
enterred_invalid_org_name = Името на организацията, което въведохте, е неправилно.
enterred_invalid_password = Паролата, която въведохте, е неправилна.
organization_leave_success = Успешно напуснахте организацията %s.
still_has_org = Вашият акаунт е член на една или повече организации, първо ги напуснете.
org_still_own_repo = Тази организация все още притежава едно или повече хранилища, първо ги изтрийте или прехвърлете.
target_branch_not_exist = Целевият клон не съществува.
glob_pattern_error = ` glob шаблонът е невалиден: %s.`
openid_been_used = OpenID адресът „%s“ вече е използван.
unknown_error = Неизвестна грешка:
TreeName = Път до файла
AdminEmail = Администраторски адрес за ел. поща
email_domain_is_not_allowed = Домейнът на адреса за ел. поща на потребителя <b>%s</b> е в конфликт с EMAIL_DOMAIN_ALLOWLIST или EMAIL_DOMAIN_BLOCKLIST. Уверете се, че сте въвели правилно адреса за ел. поща.
email_been_used = Адресът за ел. поща вече се използва.
[action]
close_issue = `затвори задача <a href="%[1]s">%[3]s#%[2]s</a>`
@ -1709,11 +2078,38 @@ sign_up_button = Регистрирайте се.
back_to_sign_in = Назад към Вход
sign_in_openid = Продължаване с OpenID
send_reset_mail = Изпращане на ел. писмо за възстановяване
authorize_application = Упълномощаване на приложение
password_pwned_err = Неуспешно завършване на заявката към HaveIBeenPwned
last_admin = Не можете да премахнете последния администратор. Трябва да има поне един администратор.
allow_password_change = Изискване потребителят да смени паролата си (препоръчително)
authorize_title = Упълномощавате ли „%s“ да има достъп до вашия акаунт?
reset_password_mail_sent_prompt = Изпратено е ел. писмо за потвърждение до <b>%s</b>. За да завършите процеса по възстановяване на акаунта, моля, проверете входящата си поща и последвайте предоставената връзка в рамките на следващите %s.
reset_password_wrong_user = Вие сте влезли като %s, но връзката за възстановяване на акаунта е предназначена за %s
authorize_redirect_notice = Ще бъдете пренасочени към %s, ако упълномощите това приложение.
authorize_application_description = Ако предоставите достъп, то ще може да осъществява достъп и да записва цялата информация за вашия акаунт, включително частни хранилища и организации.
twofa_scratch_used = Използвали сте своя резервен код. Пренасочени сте към страницата с настройки за двуфакторно удостоверяване, за да можете да премахнете регистрацията на устройството си или да генерирате нов резервен код.
reset_password_helper = Възстановяване на акаунт
invalid_password = Вашата парола не съвпада с паролата, използвана за създаване на акаунта.
invalid_code = Вашият код за потвърждение е невалиден или е изтекъл.
invalid_code_forgot_password = Вашият код за потвърждение е невалиден или е изтекъл. Щракнете <a href="%s">тук</a>, за да започнете нова сесия.
scratch_code = Резервен код
use_scratch_code = Използвайте резервен код
use_onetime_code = Използвайте еднократен код
twofa_scratch_token_incorrect = Вашият резервен код е неправилен.
authorize_application_created_by = Това приложение е създадено от %s.
authorization_failed = Неуспешно упълномощаване
resent_limit_prompt = Вече сте поискали ел. писмо за активация наскоро. Моля, изчакайте 3 минути и опитайте отново.
has_unconfirmed_mail = Здравейте, %s, имате непотвърден адрес за ел. поща (<b>%s</b>). Ако не сте получили ел. писмо за потвърждение или трябва да изпратите ново, моля, щракнете върху бутона по-долу.
change_unconfirmed_email_error = Неуспешна промяна на адреса за ел. поща: %v
resend_mail = Щракнете тук, за повторно изпращане на ел. писмо за активация
change_unconfirmed_email_summary = Промяна на адреса, на който се изпраща ел. писмо за активация.
change_unconfirmed_email = Ако сте въвели грешен адрес за ел. поща по време на регистрацията, можете да го промените по-долу и потвърждение ще бъде изпратено на новия адрес.
[aria]
footer.software = Относно този софтуер
footer.links = Връзки
footer = Долен колонтитул
navbar = Навигационна лента
[startpage]
install = Лесен за инсталиране
@ -1780,6 +2176,7 @@ runs.no_workflows.help_no_write_access = За да научите повече
variables.management = Управление на променливи
variables.not_found = Променливата не е открита.
variables.id_not_exist = Променлива с идентификатор %d не съществува.
runners.owner_type = Тип
[heatmap]
less = По-малко
@ -1809,9 +2206,10 @@ invalid_input_type = Не можете да качвате файлове от
component_loading_failed = Неуспешно зареждане на %s
contributors.what = приноси
recent_commits.what = скорошни подавания
component_loading = Зареждане на %s...
component_loading = Зареждане на %s
component_loading_info = Това може да отнеме известно време…
code_frequency.what = честота на промените
component_failed_to_load = Възникна неочаквана грешка.
[projects]
type-1.display_name = Индивидуален проект
@ -1820,20 +2218,29 @@ deleted.display_name = Изтрит проект
[search]
no_results = Няма намерени съответстващи резултати.
team_kind = Търсене на екипи...
repo_kind = Търсене на хранилища...
org_kind = Търсене на организации...
user_kind = Търсене на потребители...
code_kind = Търсене на код...
commit_kind = Търсене на подавания...
project_kind = Търсене на проекти...
package_kind = Търсене на пакети...
search = Търсене...
branch_kind = Търсене на клонове...
pull_kind = Търсене на заявки за сливане...
issue_kind = Търсене на задачи...
team_kind = Търсене на екипи
repo_kind = Търсене на хранилища
org_kind = Търсене на организации
user_kind = Търсене на потребители
code_kind = Търсене на код
commit_kind = Търсене на подавания
project_kind = Търсене на проекти
package_kind = Търсене на пакети
search = Търсене
branch_kind = Търсене на клонове
pull_kind = Търсене на заявки за сливане
issue_kind = Търсене на задачи
fuzzy = Приблизително
exact = Прецизно
regexp = Регекс
regexp_tooltip = Третиране на термина за търсене като регулярен израз
fuzzy_tooltip = Включване на резултати, които също съвпадат приблизително с термина за търсене
exact_tooltip = Включване само на резултати, които съвпадат точно с термина за търсене
code_search_unavailable = Търсенето на код в момента не е достъпно. Моля, свържете се с администратора на сайта.
keyword_search_unavailable = Търсенето по ключова дума в момента не е достъпно. Моля, свържете се с администратора на сайта.
union_tooltip = Включване на резултати, които съвпадат с някоя от ключовите думи, разделени с интервал
union = Обединение
type_tooltip = Тип търсене
[markup]
filepreview.lines = Редове от %[1]d до %[2]d в %[3]s

View file

@ -493,11 +493,11 @@ use_onetime_code = Použít jednorázový kód
view_it_on=Zobrazit na %s
reply=nebo přímo odpovědět na tento e-mail
link_not_working_do_paste=Odkaz nefunguje? Zkuste jej zkopírovat a vložit do adresního řádku svého prohlížeče.
hi_user_x=Ahoj <b>%s</b>,
hi_user_x=Dobrý den, uživateli <b>%s</b>,
activate_account=Prosíme, aktivujte si váš účet
activate_account.title=%s, prosím aktivujte si váš účet
activate_account.text_1=Ahoj <b>%[1]s</b>, děkujeme za registraci na %[2]s!
activate_account.text_1=Dobrý den, uživateli <b>%[1]s</b>, děkujeme za registraci ve službě %[2]s!
activate_account.text_2=Pro aktivaci vašeho účtu klikněte <b>%s</b> na následující odkaz :
activate_email=Ověřte vaši e-mailovou adresu
@ -932,7 +932,7 @@ generate_new_token=Vygenerovat nový token
tokens_desc=Tyto tokeny umožňují přístup k vašemu účtu pomocí Forgejo API.
token_name=Název tokenu
generate_token=Vygenerovat token
generate_token_success=Nový token byl vygenerován. Zkopírujte jej nyní, jelikož již nebude znovu zobrazen.
generate_token_success=Nový token byl vygenerován. Zkopírujte si jej nyní, jelikož již nebude znovu zobrazen.
generate_token_name_duplicate=<strong>%s</strong> byl již použit jako název aplikace. Použijte prosím nový.
delete_token=Smazat
access_token_deletion=Odstranit přístupový token
@ -2922,6 +2922,7 @@ settings.event_action_success = Úspěch
settings.event_action_success_desc = Běh akce byl úspěšný.
settings.event_header_action = Události běhu akce
settings.event_action_recover_desc = Běh akce byl úspěšný, předchozí běh akce ve stejném workflow selhal.
issues.filter_type.all_pull_requests = Všechny žádosti o sloučení
[graphs]
component_loading_info = Tohle může chvíli trvat…

View file

@ -2735,6 +2735,7 @@ settings.event_action_success = Success
settings.event_action_recover_desc = Handlingskørsel lykkedes efter at den sidste handlingskørsel i samme arbejdsgang mislykkedes.
settings.event_action_failure_desc = Handlingskørsel sluttede som en fejl.
settings.event_action_recover = Gendan
issues.filter_type.all_pull_requests = Alle pull-anmodninger
[notification]
watching = Overvåger

View file

@ -2917,6 +2917,14 @@ comment.blocked_by_user = Kommentieren ist nicht möglich, da du vom Repository-
sync_fork.branch_behind_one = Dieser Branch ist %[1]d Commit hinter %[2]s
sync_fork.branch_behind_few = Dieser Branch ist %[1]d Commits hinter %[2]s
sync_fork.button = Sync
settings.event_action_failure_desc = Action-Run endete im Fehlschlag.
settings.event_action_success_desc = Action-Run war erfolgreich.
settings.event_action_failure = Fehlschlag
settings.event_action_success = Erfolg
settings.event_header_action = Action-Run-Ereignisse
settings.event_action_recover_desc = Action-Run war erfolgreich, nachdem der letzte Action-Run im selben Arbeitsablauf fehlgeschlagen ist.
settings.event_action_recover = Wiederherstellen
issues.filter_type.all_pull_requests = Alle Pull-Requests
[graphs]
component_loading_failed = Konnte %s nicht laden

View file

@ -768,8 +768,8 @@ update_profile_success = Your profile has been updated.
change_username = Your username has been changed.
change_username_prompt = Note: Changing your username also changes your account URL.
change_username_redirect_prompt = The old username will redirect until someone claims it.
change_username_redirect_prompt.with_cooldown.one = The old username will be available to everyone after a cooldown period of %[1]d day, you can still reclaim the old username during the cooldown period.
change_username_redirect_prompt.with_cooldown.few = The old username will be available to everyone after a cooldown period of %[1]d days, you can still reclaim the old username during the cooldown period.
change_username_redirect_prompt.with_cooldown.one = The old username will be available to everyone after a cooldown period of %[1]d day. You can still reclaim the old username during the cooldown period.
change_username_redirect_prompt.with_cooldown.few = The old username will be available to everyone after a cooldown period of %[1]d days. You can still reclaim the old username during the cooldown period.
continue = Continue
cancel = Cancel
language = Language
@ -1610,7 +1610,7 @@ issues.remove_ref_at = `removed reference <b>%s</b> %s`
issues.add_ref_at = `added reference <b>%s</b> %s`
issues.delete_branch_at = `deleted branch <b>%s</b> %s`
issues.filter_label = Label
issues.filter_label_exclude = `Use <code>alt</code> + <code>click/enter</code> to exclude labels`
issues.filter_label_exclude = Use <kbd>Alt</kbd> + <kbd>Click</kbd> to exclude labels
issues.filter_label_no_select = All labels
issues.filter_label_select_no_label = No label
issues.filter_milestone = Milestone
@ -1628,6 +1628,7 @@ issues.filter_poster = Author
issues.filter_poster_no_select = All authors
issues.filter_type = Type
issues.filter_type.all_issues = All issues
issues.filter_type.all_pull_requests = All pull requests
issues.filter_type.assigned_to_you = Assigned to you
issues.filter_type.created_by_you = Created by you
issues.filter_type.mentioning_you = Mentioning you
@ -1693,15 +1694,13 @@ issues.close_comment_issue = Close with comment
issues.reopen_issue = Reopen
issues.reopen_comment_issue = Reopen with comment
issues.create_comment = Comment
issues.closed_at = `closed this issue <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.reopened_at = `reopened this issue <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.commit_ref_at = `referenced this issue from a commit <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.ref_issue_from = `<a href="%[3]s">referenced this issue %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.ref_pull_from = `<a href="%[3]s">referenced this pull request %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.ref_closing_from = `<a href="%[3]s">referenced this issue from a pull request %[4]s that will close it</a>, <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.ref_reopening_from = `<a href="%[3]s">referenced this issue from a pull request %[4]s that will reopen it</a>, <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.ref_closed_from = `<a href="%[3]s">closed this issue %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.ref_reopened_from = `<a href="%[3]s">reopened this issue %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.closed_at = `closed this issue %s`
issues.reopened_at = `reopened this issue %s`
issues.commit_ref_at = `referenced this issue from a commit %s`
issues.ref_issue_from = `<a href="%[2]s">referenced this issue %[3]s</a> %[1]s`
issues.ref_pull_from = `<a href="%[2]s">referenced this pull request %[3]s</a> %[1]s`
issues.ref_closing_from = `<a href="%[2]s">referenced this issue from a pull request %[3]s that will close it</a>, %[1]s`
issues.ref_reopening_from = `<a href="%[2]s">referenced this issue from a pull request %[3]s that will reopen it</a>, %[1]s`
issues.ref_from = `from %[1]s`
issues.author = Author
issues.author.tooltip.issue = This user is the author of this issue.
@ -2013,9 +2012,9 @@ pulls.update_branch_success = Branch update was successful
pulls.update_not_allowed = You are not allowed to update branch
pulls.outdated_with_base_branch = This branch is out-of-date with the base branch
pulls.close = Close pull request
pulls.closed_at = `closed this pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>`
pulls.reopened_at = `reopened this pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>`
pulls.commit_ref_at = `referenced this pull request from a commit <a id="%[1]s" href="#%[1]s">%[2]s</a>`
pulls.closed_at = `closed this pull request %s`
pulls.reopened_at = `reopened this pull request %s`
pulls.commit_ref_at = `referenced this pull request from a commit %s`
pulls.cmd_instruction_hint = View command line instructions
pulls.cmd_instruction_checkout_title = Checkout
pulls.cmd_instruction_checkout_desc = From your project repository, check out a new branch and test the changes.
@ -2932,8 +2931,8 @@ settings.update_settings = Update settings
settings.update_setting_success = Organization settings have been updated.
settings.change_orgname_prompt = Note: Changing the organization name will also change your organization's URL and free the old name.
settings.change_orgname_redirect_prompt = The old name will redirect until it is claimed.
settings.change_orgname_redirect_prompt.with_cooldown.one = The old organization name will be available to everyone after a cooldown period of %[1]d day, you can still reclaim the old name during the cooldown period.
settings.change_orgname_redirect_prompt.with_cooldown.few = The old organization name will be available to everyone after a cooldown period of %[1]d days, you can still reclaim the old name during the cooldown period.
settings.change_orgname_redirect_prompt.with_cooldown.one = The old organization name will be available to everyone after a cooldown period of %[1]d day. You can still reclaim the old name during the cooldown period.
settings.change_orgname_redirect_prompt.with_cooldown.few = The old organization name will be available to everyone after a cooldown period of %[1]d days. You can still reclaim the old name during the cooldown period.
settings.update_avatar_success = The organization's avatar has been updated.
settings.delete = Delete organization
settings.delete_account = Delete this organization

View file

@ -1500,7 +1500,7 @@ issues.content_history.created = ginawa
editor.patching = Pina-patch:
editor.fail_to_apply_patch = Hindi malapat ang patch na "%s"
settings.danger_zone = Mapanganib na lugar
issues.closed_at = `isinara ang isyung <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.closed_at = `isinara ang isyung ito <a id="%[1]s" href="#%[1]s">%[2]s</a>`
settings.collaboration.admin = Tagapangasiwa
settings.admin_settings = Mga setting ng tagapangasiwa
issues.start_tracking_history = `sinimulan ang trabaho %s`
@ -2773,6 +2773,13 @@ comment.blocked_by_user = Hindi posible ang pagkomento dahil hinarang ka ng may-
sync_fork.button = I-sync
sync_fork.branch_behind_one = Ang branch na ito ay %[1]d commit sa likod ng %[2]s
sync_fork.branch_behind_few = Ang branch na ito ay %[1]d mga commit sa likod ng %[2]s
settings.event_header_action = Mga event sa run ng aksyon
settings.event_action_failure = Pagkabigo
settings.event_action_failure_desc = Natapos ang action run bilang pagkabigo.
settings.event_action_recover = I-recover
settings.event_action_success = Matagumpay
settings.event_action_success_desc = Matagumpay na natapos ang Action Run.
settings.event_action_recover_desc = Matagumpay na natapos ang Action Run pagkatapos na nabigo ang huling Action Run sa katulad na workflow.
[search]
commit_kind = Maghanap ng mga commit…

View file

@ -9,7 +9,7 @@ sign_in_with_provider=Se connecter avec %s
sign_in_or=ou
sign_out=Déconnexion
sign_up=S'inscrire
link_account=Lier un Compte
link_account=Lier un compte
register=S'inscrire
version=Version
powered_by=Propulsé par %s
@ -1516,7 +1516,7 @@ issues.desc=Organiser les rapports de bug, les tâches et les jalons.
issues.filter_assignees=Filtrer par assignation
issues.filter_milestones=Filtrer le jalon
issues.filter_projects=Filtrer par projet
issues.filter_labels=Filtrer par labels
issues.filter_labels=Filtrer par étiquettes
issues.filter_reviewers=Filtrer par évaluateur
issues.new=Nouveau ticket
issues.new.title_empty=Le titre ne peut pas être vide
@ -2920,6 +2920,7 @@ settings.event_header_action = Événements d'exécution d'action
settings.event_action_success_desc = L'exécution de l'action a réussi.
settings.event_action_failure_desc = L'exécution de l'action a échoué.
settings.event_action_recover_desc = L'exécution de l'action a réussi après l'échec de la dernière exécution de l'action dans le même workflow.
issues.filter_type.all_pull_requests = Toutes les demandes d'ajout
[graphs]
component_loading = Chargement %s…
@ -4090,4 +4091,4 @@ issues.write = <b>Écrire :</b> Fermer des tickets et gérer les métadonnées t
pulls.read = <b>Lire :</b> Lire et créer des demandes de tirage.
[translation_meta]
test = Ceci est une chaîne de test. Elle n'est pas affichée dans l'interface de Forgejo mais est utilisée à des fins de test. N'hésitez pas à entrer 'ok' pour gagner du temps (ou un fait amusant de votre choix) pour atteindre ce doux 100 % de complétion. :-)
test = Ceci est une chaîne de test. Elle n'est pas affichée dans l'interface de Forgejo mais est utilisée à des fins de test. N'hésitez pas à entrer 'ok' pour gagner du temps (ou un fait amusant de votre choix) pour atteindre ce difficile 100 % de complétion. :-)

View file

@ -2913,6 +2913,14 @@ comment.blocked_by_user = Piebilžu pievienošana nav iespējama, jo glabātavas
sync_fork.branch_behind_one = Šis zars ir %[1]d iesūtījumu aiz %[2]s
sync_fork.button = Sinhronizēt
sync_fork.branch_behind_few = Šis zars ir %[1]d iesūtījumus aiz %[2]s
settings.event_action_failure = Kļūme
settings.event_action_failure_desc = Darbības izpilde beidzās ar kļūmi.
settings.event_header_action = Darbības izpildes notikumi
settings.event_action_recover = Atgūt
settings.event_action_recover_desc = Darbības izpilde bija sekmīga pēc kļūmes iepriekšējā darbības izpildē tajā pašā darbplūsmā.
settings.event_action_success = Sekmīgi
settings.event_action_success_desc = Darbības izpilde bija sekmīga.
issues.filter_type.all_pull_requests = Visi izmaiņu pieprasījumi
[graphs]
component_loading=Ielādē %s…

View file

@ -2621,6 +2621,7 @@ settings.event_action_recover = Verhaalt
settings.event_header_action = Aktioons-Loop-Vörfallen
settings.event_action_failure_desc = Aktioons-Loop is as fehlslagen ennt.
settings.event_action_recover_desc = Aktioons-Loop is daankregen worden, nadeem de leste Aktioons-Loop in de sülven Warkwies fehlslagen is.
issues.filter_type.all_pull_requests = All Haalvörslagen
[repo.permissions]
code.read = <b>Lesen:</b> De Quelltext vun deesem Repositorium ankieken un klonen.

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