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
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:
commit
9432959b58
266 changed files with 6837 additions and 2053 deletions
|
@ -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
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: |
|
||||
|
|
|
@ -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: |
|
||||
|
|
2
.forgejo/testdata/build-release/Dockerfile
vendored
2
.forgejo/testdata/build-release/Dockerfile
vendored
|
@ -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}"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }}"
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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
|
||||
|
|
71
.forgejo/workflows/testing-integration.yml
Normal file
71
.forgejo/workflows/testing-integration.yml
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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" \
|
||||
|
|
|
@ -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" \
|
||||
|
|
20
Makefile
20
Makefile
|
@ -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
|
||||
|
|
30
assets/go-licenses.json
generated
30
assets/go-licenses.json
generated
File diff suppressed because one or more lines are too long
14
build.go
14
build.go
|
@ -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"
|
||||
)
|
|
@ -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 }
|
||||
`))
|
||||
|
|
|
@ -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.")))
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
6
flake.lock
generated
|
@ -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": {
|
||||
|
|
39
flake.nix
39
flake.nix
|
@ -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
20
go.mod
|
@ -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
36
go.sum
|
@ -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=
|
||||
|
|
2
main.go
2
main.go
|
@ -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
|
||||
|
|
|
@ -55,6 +55,7 @@ type ActionRun struct {
|
|||
PreviousDuration time.Duration
|
||||
Created timeutil.TimeStamp `xorm:"created"`
|
||||
Updated timeutil.TimeStamp `xorm:"updated"`
|
||||
NotifyEmail bool
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.")
|
||||
|
|
106
models/activities/federated_user_activity.go
Normal file
106
models/activities/federated_user_activity.go
Normal 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
|
||||
}
|
24
models/activities/federated_user_activity_test.go
Normal file
24
models/activities/federated_user_activity_test.go
Normal 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())
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
1
models/fixtures/pull_auto_merge.yml
Normal file
1
models/fixtures/pull_auto_merge.yml
Normal file
|
@ -0,0 +1 @@
|
|||
[] # empty
|
|
@ -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
|
||||
|
|
|
@ -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 ------------------------------------------------
|
||||
|
|
|
@ -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.
|
||||
|
|
126
models/forgejo_migrations/v33.go
Normal file
126
models/forgejo_migrations/v33.go
Normal 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
|
||||
}
|
46
models/forgejo_migrations/v33_test.go
Normal file
46
models/forgejo_migrations/v33_test.go
Normal 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)
|
||||
}
|
14
models/forgejo_migrations/v34.go
Normal file
14
models/forgejo_migrations/v34.go
Normal 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))
|
||||
}
|
19
models/forgejo_migrations/v35.go
Normal file
19
models/forgejo_migrations/v35.go
Normal 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{})
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
30
models/user/federated_user_follower.go
Normal file
30
models/user/federated_user_follower.go
Normal 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
|
||||
}
|
27
models/user/federated_user_follower_test.go
Normal file
27
models/user/federated_user_follower_test.go
Normal 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")
|
||||
}
|
|
@ -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")
|
||||
|
|
|
@ -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)"`
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">@#&@forgejo.org</a></p>`)
|
||||
}
|
||||
|
||||
func TestRender_emoji(t *testing.T) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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])
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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…
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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…
|
||||
|
|
|
@ -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. :-)
|
||||
|
|
|
@ -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…
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue