Merge branch 'main' into lunny/move_update_repo

This commit is contained in:
techknowlogick 2025-04-12 13:57:30 -04:00 committed by GitHub
commit 896a3a6bca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
170 changed files with 2060 additions and 1575 deletions

View File

@ -59,6 +59,8 @@ jobs:
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
nightly-docker-rootful:
runs-on: namespace-profile-gitea-release-docker
permissions:
packages: write # to publish to ghcr.io
steps:
- uses: actions/checkout@v4
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
@ -85,6 +87,12 @@ jobs:
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR using PAT
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: fetch go modules
run: make vendor
- name: build rootful docker image
@ -93,9 +101,13 @@ jobs:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: gitea/gitea:${{ steps.clean_name.outputs.branch }}
tags: |-
gitea/gitea:${{ steps.clean_name.outputs.branch }}
ghcr.io/go-gitea/gitea:${{ steps.clean_name.outputs.branch }}
nightly-docker-rootless:
runs-on: namespace-profile-gitea-release-docker
permissions:
packages: write # to publish to ghcr.io
steps:
- uses: actions/checkout@v4
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
@ -122,6 +134,12 @@ jobs:
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR using PAT
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: fetch go modules
run: make vendor
- name: build rootless docker image
@ -131,4 +149,6 @@ jobs:
platforms: linux/amd64,linux/arm64
push: true
file: Dockerfile.rootless
tags: gitea/gitea:${{ steps.clean_name.outputs.branch }}-rootless
tags: |-
gitea/gitea:${{ steps.clean_name.outputs.branch }}-rootless
ghcr.io/go-gitea/gitea:${{ steps.clean_name.outputs.branch }}-rootless

View File

@ -69,6 +69,8 @@ jobs:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
docker-rootful:
runs-on: namespace-profile-gitea-release-docker
permissions:
packages: write # to publish to ghcr.io
steps:
- uses: actions/checkout@v4
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
@ -79,7 +81,9 @@ jobs:
- uses: docker/metadata-action@v5
id: meta
with:
images: gitea/gitea
images: |-
gitea/gitea
ghcr.io/go-gitea/gitea
flavor: |
latest=false
# 1.2.3-rc0
@ -90,6 +94,12 @@ jobs:
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR using PAT
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: build rootful docker image
uses: docker/build-push-action@v5
with:
@ -100,6 +110,8 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
docker-rootless:
runs-on: namespace-profile-gitea-release-docker
permissions:
packages: write # to publish to ghcr.io
steps:
- uses: actions/checkout@v4
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
@ -110,7 +122,9 @@ jobs:
- uses: docker/metadata-action@v5
id: meta
with:
images: gitea/gitea
images: |-
gitea/gitea
ghcr.io/go-gitea/gitea
# each tag below will have the suffix of -rootless
flavor: |
latest=false
@ -123,6 +137,12 @@ jobs:
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR using PAT
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: build rootless docker image
uses: docker/build-push-action@v5
with:

View File

@ -14,6 +14,8 @@ concurrency:
jobs:
binary:
runs-on: namespace-profile-gitea-release-binary
permissions:
packages: write # to publish to ghcr.io
steps:
- uses: actions/checkout@v4
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
@ -71,6 +73,8 @@ jobs:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
docker-rootful:
runs-on: namespace-profile-gitea-release-docker
permissions:
packages: write # to publish to ghcr.io
steps:
- uses: actions/checkout@v4
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
@ -81,7 +85,9 @@ jobs:
- uses: docker/metadata-action@v5
id: meta
with:
images: gitea/gitea
images: |-
gitea/gitea
ghcr.io/go-gitea/gitea
# this will generate tags in the following format:
# latest
# 1
@ -96,6 +102,12 @@ jobs:
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR using PAT
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: build rootful docker image
uses: docker/build-push-action@v5
with:
@ -116,7 +128,9 @@ jobs:
- uses: docker/metadata-action@v5
id: meta
with:
images: gitea/gitea
images: |-
gitea/gitea
ghcr.io/go-gitea/gitea
# each tag below will have the suffix of -rootless
flavor: |
suffix=-rootless,onlatest=true
@ -134,6 +148,12 @@ jobs:
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR using PAT
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: build rootless docker image
uses: docker/build-push-action@v5
with:

View File

@ -213,6 +213,10 @@ func serveInstalled(ctx *cli.Context) error {
log.Fatal("Can not find APP_DATA_PATH %q", setting.AppDataPath)
}
// the AppDataTempDir is fully managed by us with a safe sub-path
// so it's safe to automatically remove the outdated files
setting.AppDataTempDir("").RemoveOutdated(3 * 24 * time.Hour)
// Override the provided port number within the configuration
if ctx.IsSet("port") {
if err := setPort(ctx.String("port")); err != nil {

View File

@ -197,13 +197,6 @@ RUN_USER = ; git
;; relative paths are made absolute relative to the APP_DATA_PATH
;SSH_SERVER_HOST_KEYS=ssh/gitea.rsa, ssh/gogs.rsa
;;
;; Directory to create temporary files in when testing public keys using ssh-keygen,
;; default is the system temporary directory.
;SSH_KEY_TEST_PATH =
;;
;; Use `ssh-keygen` to parse public SSH keys. The value is passed to the shell. By default, Gitea does the parsing itself.
;SSH_KEYGEN_PATH =
;;
;; Enable SSH Authorized Key Backup when rewriting all keys, default is false
;SSH_AUTHORIZED_KEYS_BACKUP = false
;;
@ -294,6 +287,9 @@ RUN_USER = ; git
;; Default path for App data
;APP_DATA_PATH = data ; relative paths will be made absolute with _`AppWorkPath`_
;;
;; Base path for App's temp files, leave empty to use the managed tmp directory in APP_DATA_PATH
;APP_TEMP_PATH =
;;
;; Enable gzip compression for runtime-generated content, static resources excluded
;ENABLE_GZIP = false
;;
@ -1069,15 +1065,6 @@ LEVEL = Info
;; Separate extensions with a comma. To line wrap files without an extension, just put a comma
;LINE_WRAP_EXTENSIONS = .txt,.md,.markdown,.mdown,.mkd,.livemd,
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;[repository.local]
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Path for local repository copy. Defaults to `tmp/local-repo` (content gets deleted on gitea restart)
;LOCAL_COPY_PATH = tmp/local-repo
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;[repository.upload]
@ -1087,9 +1074,6 @@ LEVEL = Info
;; Whether repository file uploads are enabled. Defaults to `true`
;ENABLED = true
;;
;; Path for uploads. Defaults to `data/tmp/uploads` (content gets deleted on gitea restart)
;TEMP_PATH = data/tmp/uploads
;;
;; Comma-separated list of allowed file extensions (`.zip`), mime types (`text/plain`) or wildcard type (`image/*`, `audio/*`, `video/*`). Empty value or `*/*` allows all types.
;ALLOWED_TYPES =
;;
@ -2473,7 +2457,7 @@ LEVEL = Info
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Set the maximum number of characters in a mermaid source. (Set to -1 to disable limits)
;MERMAID_MAX_SOURCE_CHARACTERS = 5000
;MERMAID_MAX_SOURCE_CHARACTERS = 50000
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -2594,9 +2578,6 @@ LEVEL = Info
;; Currently, only `minio` and `azureblob` is supported.
;SERVE_DIRECT = false
;;
;; Path for chunked uploads. Defaults to APP_DATA_PATH + `tmp/package-upload`
;CHUNKED_UPLOAD_PATH = tmp/package-upload
;;
;; Maximum count of package versions a single owner can have (`-1` means no limits)
;LIMIT_TOTAL_OWNER_COUNT = -1
;; Maximum size of packages a single owner can use (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)

View File

@ -31,16 +31,19 @@ if [ -e /data/ssh/ssh_host_ecdsa_cert ]; then
SSH_ECDSA_CERT=${SSH_ECDSA_CERT:-"/data/ssh/ssh_host_ecdsa_cert"}
fi
if [ -e /data/ssh/ssh_host_ed25519-cert.pub ]; then
SSH_ED25519_CERT=${SSH_ED25519_CERT:-"/data/ssh/ssh_host_ed25519-cert.pub"}
# In case someone wants to sign the `{keyname}.pub` key by `ssh-keygen -s ca -I identity ...` to
# make use of the ssh-key certificate authority feature (see ssh-keygen CERTIFICATES section),
# the generated key file name is `{keyname}-cert.pub`
if [ -e /data/ssh/ssh_host_ed25519_key-cert.pub ]; then
SSH_ED25519_CERT=${SSH_ED25519_CERT:-"/data/ssh/ssh_host_ed25519_key-cert.pub"}
fi
if [ -e /data/ssh/ssh_host_rsa-cert.pub ]; then
SSH_RSA_CERT=${SSH_RSA_CERT:-"/data/ssh/ssh_host_rsa-cert.pub"}
if [ -e /data/ssh/ssh_host_rsa_key-cert.pub ]; then
SSH_RSA_CERT=${SSH_RSA_CERT:-"/data/ssh/ssh_host_rsa_key-cert.pub"}
fi
if [ -e /data/ssh/ssh_host_ecdsa-cert.pub ]; then
SSH_ECDSA_CERT=${SSH_ECDSA_CERT:-"/data/ssh/ssh_host_ecdsa-cert.pub"}
if [ -e /data/ssh/ssh_host_ecdsa_key-cert.pub ]; then
SSH_ECDSA_CERT=${SSH_ECDSA_CERT:-"/data/ssh/ssh_host_ecdsa_key-cert.pub"}
fi
if [ -d /etc/ssh ]; then

View File

@ -240,3 +240,10 @@ func DeleteGPGKey(ctx context.Context, doer *user_model.User, id int64) (err err
return committer.Commit()
}
func FindGPGKeyWithSubKeys(ctx context.Context, keyID string) ([]*GPGKey, error) {
return db.Find[GPGKey](ctx, FindGPGKeyOptions{
KeyID: keyID,
IncludeSubKeys: true,
})
}

View File

@ -6,27 +6,13 @@ package asymkey
import (
"context"
"fmt"
"strings"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"golang.org/x/crypto/ssh"
"xorm.io/builder"
)
// ___________.__ .__ __
// \_ _____/|__| ____ ____ ________________________|__| _____/ |_
// | __) | |/ \ / ___\_/ __ \_ __ \____ \_ __ \ |/ \ __\
// | \ | | | \/ /_/ > ___/| | \/ |_> > | \/ | | \ |
// \___ / |__|___| /\___ / \___ >__| | __/|__| |__|___| /__|
// \/ \//_____/ \/ |__| \/
//
// This file contains functions for fingerprinting SSH keys
//
// The database is used in checkKeyFingerprint however most of these functions probably belong in a module
// checkKeyFingerprint only checks if key fingerprint has been used as public key,
@ -41,29 +27,6 @@ func checkKeyFingerprint(ctx context.Context, fingerprint string) error {
return nil
}
func calcFingerprintSSHKeygen(publicKeyContent string) (string, error) {
// Calculate fingerprint.
tmpPath, err := writeTmpKeyFile(publicKeyContent)
if err != nil {
return "", err
}
defer func() {
if err := util.Remove(tmpPath); err != nil {
log.Warn("Unable to remove temporary key file: %s: Error: %v", tmpPath, err)
}
}()
stdout, stderr, err := process.GetManager().Exec("AddPublicKey", "ssh-keygen", "-lf", tmpPath)
if err != nil {
if strings.Contains(stderr, "is not a public key file") {
return "", ErrKeyUnableVerify{stderr}
}
return "", util.NewInvalidArgumentErrorf("'ssh-keygen -lf %s' failed with error '%s': %s", tmpPath, err, stderr)
} else if len(stdout) < 2 {
return "", util.NewInvalidArgumentErrorf("not enough output for calculating fingerprint: %s", stdout)
}
return strings.Split(stdout, " ")[1], nil
}
func calcFingerprintNative(publicKeyContent string) (string, error) {
// Calculate fingerprint.
pk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(publicKeyContent))
@ -75,15 +38,12 @@ func calcFingerprintNative(publicKeyContent string) (string, error) {
// CalcFingerprint calculate public key's fingerprint
func CalcFingerprint(publicKeyContent string) (string, error) {
// Call the method based on configuration
useNative := setting.SSH.KeygenPath == ""
calcFn := util.Iif(useNative, calcFingerprintNative, calcFingerprintSSHKeygen)
fp, err := calcFn(publicKeyContent)
fp, err := calcFingerprintNative(publicKeyContent)
if err != nil {
if IsErrKeyUnableVerify(err) {
return "", err
}
return "", fmt.Errorf("CalcFingerprint(%s): %w", util.Iif(useNative, "native", "ssh-keygen"), err)
return "", fmt.Errorf("CalcFingerprint: %w", err)
}
return fp, nil
}

View File

@ -13,12 +13,9 @@ import (
"errors"
"fmt"
"math/big"
"os"
"strconv"
"strings"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
@ -175,20 +172,9 @@ func CheckPublicKeyString(content string) (_ string, err error) {
return content, nil
}
var (
fnName string
keyType string
length int
)
if len(setting.SSH.KeygenPath) == 0 {
fnName = "SSHNativeParsePublicKey"
keyType, length, err = SSHNativeParsePublicKey(content)
} else {
fnName = "SSHKeyGenParsePublicKey"
keyType, length, err = SSHKeyGenParsePublicKey(content)
}
keyType, length, err := SSHNativeParsePublicKey(content)
if err != nil {
return "", fmt.Errorf("%s: %w", fnName, err)
return "", fmt.Errorf("SSHNativeParsePublicKey: %w", err)
}
log.Trace("Key info [native: %v]: %s-%d", setting.SSH.StartBuiltinServer, keyType, length)
@ -258,56 +244,3 @@ func SSHNativeParsePublicKey(keyLine string) (string, int, error) {
}
return "", 0, fmt.Errorf("unsupported key length detection for type: %s", pkey.Type())
}
// writeTmpKeyFile writes key content to a temporary file
// and returns the name of that file, along with any possible errors.
func writeTmpKeyFile(content string) (string, error) {
tmpFile, err := os.CreateTemp(setting.SSH.KeyTestPath, "gitea_keytest")
if err != nil {
return "", fmt.Errorf("TempFile: %w", err)
}
defer tmpFile.Close()
if _, err = tmpFile.WriteString(content); err != nil {
return "", fmt.Errorf("WriteString: %w", err)
}
return tmpFile.Name(), nil
}
// SSHKeyGenParsePublicKey extracts key type and length using ssh-keygen.
func SSHKeyGenParsePublicKey(key string) (string, int, error) {
tmpName, err := writeTmpKeyFile(key)
if err != nil {
return "", 0, fmt.Errorf("writeTmpKeyFile: %w", err)
}
defer func() {
if err := util.Remove(tmpName); err != nil {
log.Warn("Unable to remove temporary key file: %s: Error: %v", tmpName, err)
}
}()
keygenPath := setting.SSH.KeygenPath
if len(keygenPath) == 0 {
keygenPath = "ssh-keygen"
}
stdout, stderr, err := process.GetManager().Exec("SSHKeyGenParsePublicKey", keygenPath, "-lf", tmpName)
if err != nil {
return "", 0, fmt.Errorf("fail to parse public key: %s - %s", err, stderr)
}
if strings.Contains(stdout, "is not a public key file") {
return "", 0, ErrKeyUnableVerify{stdout}
}
fields := strings.Split(stdout, " ")
if len(fields) < 4 {
return "", 0, fmt.Errorf("invalid public key line: %s", stdout)
}
keyType := strings.Trim(fields[len(fields)-1], "()\r\n")
length, err := strconv.ParseInt(fields[0], 10, 32)
if err != nil {
return "", 0, err
}
return strings.ToLower(keyType), int(length), nil
}

View File

@ -18,7 +18,6 @@ import (
"github.com/42wim/sshsig"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_SSHParsePublicKey(t *testing.T) {
@ -45,27 +44,6 @@ func Test_SSHParsePublicKey(t *testing.T) {
assert.Equal(t, tc.keyType, keyTypeN)
assert.Equal(t, tc.length, lengthN)
})
if tc.skipSSHKeygen {
return
}
t.Run("SSHKeygen", func(t *testing.T) {
keyTypeK, lengthK, err := SSHKeyGenParsePublicKey(tc.content)
if err != nil {
// Some servers do not support ecdsa format.
if !strings.Contains(err.Error(), "line 1 too long:") {
require.NoError(t, err)
}
}
assert.Equal(t, tc.keyType, keyTypeK)
assert.Equal(t, tc.length, lengthK)
})
t.Run("SSHParseKeyNative", func(t *testing.T) {
keyTypeK, lengthK, err := SSHNativeParsePublicKey(tc.content)
require.NoError(t, err)
assert.Equal(t, tc.keyType, keyTypeK)
assert.Equal(t, tc.length, lengthK)
})
})
}
}
@ -186,14 +164,6 @@ func Test_calcFingerprint(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, tc.fp, fpN)
})
if tc.skipSSHKeygen {
return
}
t.Run("SSHKeygen", func(t *testing.T) {
fpK, err := calcFingerprintSSHKeygen(tc.content)
assert.NoError(t, err)
assert.Equal(t, tc.fp, fpK)
})
})
}
}

View File

@ -21,6 +21,8 @@ import (
"xorm.io/xorm"
)
const ScopeSortPrefix = "scope-"
// IssuesOptions represents options of an issue.
type IssuesOptions struct { //nolint
Paginator *db.ListOptions
@ -70,6 +72,17 @@ func (o *IssuesOptions) Copy(edit ...func(options *IssuesOptions)) *IssuesOption
// applySorts sort an issues-related session based on the provided
// sortType string
func applySorts(sess *xorm.Session, sortType string, priorityRepoID int64) {
// Since this sortType is dynamically created, it has to be treated specially.
if strings.HasPrefix(sortType, ScopeSortPrefix) {
scope := strings.TrimPrefix(sortType, ScopeSortPrefix)
sess.Join("LEFT", "issue_label", "issue.id = issue_label.issue_id")
// "exclusive_order=0" means "no order is set", so exclude it from the JOIN criteria and then "LEFT JOIN" result is also null
sess.Join("LEFT", "label", "label.id = issue_label.label_id AND label.exclusive_order <> 0 AND label.name LIKE ?", scope+"/%")
// Use COALESCE to make sure we sort NULL last regardless of backend DB (2147483647 == max int)
sess.OrderBy("COALESCE(label.exclusive_order, 2147483647) ASC").Desc("issue.id")
return
}
switch sortType {
case "oldest":
sess.Asc("issue.created_unix").Asc("issue.id")

View File

@ -845,6 +845,7 @@ func DeleteOrphanedIssues(ctx context.Context) error {
// Remove issue attachment files.
for i := range attachmentPaths {
// FIXME: it's not right, because the attachment might not be on local filesystem
system_model.RemoveAllWithNotice(ctx, "Delete issue attachment", attachmentPaths[i])
}
return nil

View File

@ -87,6 +87,7 @@ type Label struct {
OrgID int64 `xorm:"INDEX"`
Name string
Exclusive bool
ExclusiveOrder int `xorm:"DEFAULT 0"` // 0 means no exclusive order
Description string
Color string `xorm:"VARCHAR(7)"`
NumIssues int
@ -236,7 +237,7 @@ func UpdateLabel(ctx context.Context, l *Label) error {
}
l.Color = color
return updateLabelCols(ctx, l, "name", "description", "color", "exclusive", "archived_unix")
return updateLabelCols(ctx, l, "name", "description", "color", "exclusive", "exclusive_order", "archived_unix")
}
// DeleteLabel delete a label

View File

@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/tempdir"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/testlogger"
@ -114,15 +115,16 @@ func MainTest(m *testing.M) {
setting.CustomConf = giteaConf
}
tmpDataPath, err := os.MkdirTemp("", "data")
tmpDataPath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("data")
if err != nil {
testlogger.Fatalf("Unable to create temporary data path %v\n", err)
}
defer cleanup()
setting.CustomPath = filepath.Join(setting.AppWorkPath, "custom")
setting.AppDataPath = tmpDataPath
unittest.InitSettings()
unittest.InitSettingsForTesting()
if err = git.InitFull(context.Background()); err != nil {
testlogger.Fatalf("Unable to InitFull: %v\n", err)
}
@ -134,8 +136,5 @@ func MainTest(m *testing.M) {
if err := removeAllWithRetry(setting.RepoRootPath); err != nil {
fmt.Fprintf(os.Stderr, "os.RemoveAll: %v\n", err)
}
if err := removeAllWithRetry(tmpDataPath); err != nil {
fmt.Fprintf(os.Stderr, "os.RemoveAll: %v\n", err)
}
os.Exit(exitStatus)
}

View File

@ -380,6 +380,7 @@ func prepareMigrationTasks() []*migration {
newMigration(316, "Add description for secrets and variables", v1_24.AddDescriptionForSecretsAndVariables),
newMigration(317, "Add new index for action for heatmap", v1_24.AddNewIndexForUserDashboard),
newMigration(318, "Add anonymous_access_mode for repo_unit", v1_24.AddRepoUnitAnonymousAccessMode),
newMigration(319, "Add ExclusiveOrder to Label table", v1_24.AddExclusiveOrderColumnToLabelTable),
}
return preparedMigrations
}

View File

@ -0,0 +1,16 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_24 //nolint
import (
"xorm.io/xorm"
)
func AddExclusiveOrderColumnToLabelTable(x *xorm.Engine) error {
type Label struct {
ExclusiveOrder int `xorm:"DEFAULT 0"`
}
return x.Sync(new(Label))
}

View File

@ -51,14 +51,10 @@ func init() {
db.RegisterModel(new(Upload))
}
// UploadLocalPath returns where uploads is stored in local file system based on given UUID.
func UploadLocalPath(uuid string) string {
return filepath.Join(setting.Repository.Upload.TempPath, uuid[0:1], uuid[1:2], uuid)
}
// LocalPath returns where uploads are temporarily stored in local file system.
// LocalPath returns where uploads are temporarily stored in local file system based on given UUID.
func (upload *Upload) LocalPath() string {
return UploadLocalPath(upload.UUID)
uuid := upload.UUID
return setting.AppDataTempDir("repo-uploads").JoinPath(uuid[0:1], uuid[1:2], uuid)
}
// NewUpload creates a new upload object.

View File

@ -20,6 +20,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/setting/config"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/tempdir"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/util"
@ -35,8 +36,8 @@ func fatalTestError(fmtStr string, args ...any) {
os.Exit(1)
}
// InitSettings initializes config provider and load common settings for tests
func InitSettings() {
// InitSettingsForTesting initializes config provider and load common settings for tests
func InitSettingsForTesting() {
setting.IsInTesting = true
log.OsExiter = func(code int) {
if code != 0 {
@ -75,7 +76,7 @@ func MainTest(m *testing.M, testOptsArg ...*TestOptions) {
testOpts := util.OptionalArg(testOptsArg, &TestOptions{})
giteaRoot = test.SetupGiteaRoot()
setting.CustomPath = filepath.Join(giteaRoot, "custom")
InitSettings()
InitSettingsForTesting()
fixturesOpts := FixturesOptions{Dir: filepath.Join(giteaRoot, "models", "fixtures"), Files: testOpts.FixtureFiles}
if err := CreateTestEngine(fixturesOpts); err != nil {
@ -92,15 +93,19 @@ func MainTest(m *testing.M, testOptsArg ...*TestOptions) {
setting.SSH.Domain = "try.gitea.io"
setting.Database.Type = "sqlite3"
setting.Repository.DefaultBranch = "master" // many test code still assume that default branch is called "master"
repoRootPath, err := os.MkdirTemp(os.TempDir(), "repos")
repoRootPath, cleanup1, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("repos")
if err != nil {
fatalTestError("TempDir: %v\n", err)
}
defer cleanup1()
setting.RepoRootPath = repoRootPath
appDataPath, err := os.MkdirTemp(os.TempDir(), "appdata")
appDataPath, cleanup2, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("appdata")
if err != nil {
fatalTestError("TempDir: %v\n", err)
}
defer cleanup2()
setting.AppDataPath = appDataPath
setting.AppWorkPath = giteaRoot
setting.StaticRootPath = giteaRoot
@ -153,13 +158,6 @@ func MainTest(m *testing.M, testOptsArg ...*TestOptions) {
fatalTestError("tear down failed: %v\n", err)
}
}
if err = util.RemoveAll(repoRootPath); err != nil {
fatalTestError("util.RemoveAll: %v\n", err)
}
if err = util.RemoveAll(appDataPath); err != nil {
fatalTestError("util.RemoveAll: %v\n", err)
}
os.Exit(exitStatus)
}

View File

@ -1187,29 +1187,28 @@ func GetUsersByEmails(ctx context.Context, emails []string) (map[string]*User, e
for _, email := range emailAddresses {
userIDs.Add(email.UID)
}
users, err := GetUsersMapByIDs(ctx, userIDs.Values())
if err != nil {
return nil, err
}
results := make(map[string]*User, len(emails))
for _, email := range emailAddresses {
user := users[email.UID]
if user != nil {
if user.KeepEmailPrivate {
results[user.LowerName+"@"+setting.Service.NoReplyAddress] = user
} else {
results[email.Email] = user
if len(userIDs) > 0 {
users, err := GetUsersMapByIDs(ctx, userIDs.Values())
if err != nil {
return nil, err
}
for _, email := range emailAddresses {
user := users[email.UID]
if user != nil {
results[user.GetEmail()] = user
}
}
}
users = make(map[int64]*User, len(needCheckUserNames))
users := make(map[int64]*User, len(needCheckUserNames))
if err := db.GetEngine(ctx).In("lower_name", needCheckUserNames.Values()).Find(&users); err != nil {
return nil, err
}
for _, user := range users {
results[user.LowerName+"@"+setting.Service.NoReplyAddress] = user
results[user.GetPlaceholderEmail()] = user
}
return results, nil
}

View File

@ -166,15 +166,15 @@ func RemoveContextData(ctx context.Context, tp, key any) {
}
// GetWithContextCache returns the cache value of the given key in the given context.
func GetWithContextCache[T any](ctx context.Context, cacheGroupKey string, cacheTargetID any, f func() (T, error)) (T, error) {
v := GetContextData(ctx, cacheGroupKey, cacheTargetID)
func GetWithContextCache[T, K any](ctx context.Context, groupKey string, targetKey K, f func(context.Context, K) (T, error)) (T, error) {
v := GetContextData(ctx, groupKey, targetKey)
if vv, ok := v.(T); ok {
return vv, nil
}
t, err := f()
t, err := f(ctx, targetKey)
if err != nil {
return t, err
}
SetContextData(ctx, cacheGroupKey, cacheTargetID, t)
SetContextData(ctx, groupKey, targetKey, t)
return t, nil
}

View File

@ -4,6 +4,7 @@
package cache
import (
"context"
"testing"
"time"
@ -30,7 +31,7 @@ func TestWithCacheContext(t *testing.T) {
v = GetContextData(ctx, field, "my_config1")
assert.Nil(t, v)
vInt, err := GetWithContextCache(ctx, field, "my_config1", func() (int, error) {
vInt, err := GetWithContextCache(ctx, field, "my_config1", func(context.Context, string) (int, error) {
return 1, nil
})
assert.NoError(t, err)

View File

@ -0,0 +1,12 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cachegroup
const (
User = "user"
EmailAvatarLink = "email_avatar_link"
UserEmailAddresses = "user_email_addresses"
GPGKeyWithSubKeys = "gpg_key_with_subkeys"
RepoUserPermission = "repo_user_permission"
)

View File

@ -1,35 +0,0 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"code.gitea.io/gitea/modules/optional"
)
const (
AttributeLinguistVendored = "linguist-vendored"
AttributeLinguistGenerated = "linguist-generated"
AttributeLinguistDocumentation = "linguist-documentation"
AttributeLinguistDetectable = "linguist-detectable"
AttributeLinguistLanguage = "linguist-language"
AttributeGitlabLanguage = "gitlab-language"
)
// true if "set"/"true", false if "unset"/"false", none otherwise
func AttributeToBool(attr map[string]string, name string) optional.Option[bool] {
switch attr[name] {
case "set", "true":
return optional.Some(true)
case "unset", "false":
return optional.Some(false)
}
return optional.None[bool]()
}
func AttributeToString(attr map[string]string, name string) optional.Option[string] {
if value, has := attr[name]; has && value != "unspecified" {
return optional.Some(value)
}
return optional.None[string]()
}

View File

@ -0,0 +1,114 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package attribute
import (
"strings"
"code.gitea.io/gitea/modules/optional"
)
type Attribute string
const (
LinguistVendored = "linguist-vendored"
LinguistGenerated = "linguist-generated"
LinguistDocumentation = "linguist-documentation"
LinguistDetectable = "linguist-detectable"
LinguistLanguage = "linguist-language"
GitlabLanguage = "gitlab-language"
Lockable = "lockable"
Filter = "filter"
)
var LinguistAttributes = []string{
LinguistVendored,
LinguistGenerated,
LinguistDocumentation,
LinguistDetectable,
LinguistLanguage,
GitlabLanguage,
}
func (a Attribute) IsUnspecified() bool {
return a == "" || a == "unspecified"
}
func (a Attribute) ToString() optional.Option[string] {
if !a.IsUnspecified() {
return optional.Some(string(a))
}
return optional.None[string]()
}
// ToBool converts the attribute value to optional boolean: true if "set"/"true", false if "unset"/"false", none otherwise
func (a Attribute) ToBool() optional.Option[bool] {
switch a {
case "set", "true":
return optional.Some(true)
case "unset", "false":
return optional.Some(false)
}
return optional.None[bool]()
}
type Attributes struct {
m map[string]Attribute
}
func NewAttributes() *Attributes {
return &Attributes{m: make(map[string]Attribute)}
}
func (attrs *Attributes) Get(name string) Attribute {
if value, has := attrs.m[name]; has {
return value
}
return ""
}
func (attrs *Attributes) GetVendored() optional.Option[bool] {
return attrs.Get(LinguistVendored).ToBool()
}
func (attrs *Attributes) GetGenerated() optional.Option[bool] {
return attrs.Get(LinguistGenerated).ToBool()
}
func (attrs *Attributes) GetDocumentation() optional.Option[bool] {
return attrs.Get(LinguistDocumentation).ToBool()
}
func (attrs *Attributes) GetDetectable() optional.Option[bool] {
return attrs.Get(LinguistDetectable).ToBool()
}
func (attrs *Attributes) GetLinguistLanguage() optional.Option[string] {
return attrs.Get(LinguistLanguage).ToString()
}
func (attrs *Attributes) GetGitlabLanguage() optional.Option[string] {
attrStr := attrs.Get(GitlabLanguage).ToString()
if attrStr.Has() {
raw := attrStr.Value()
// gitlab-language may have additional parameters after the language
// ignore them and just use the main language
// https://docs.gitlab.com/ee/user/project/highlighting.html#override-syntax-highlighting-for-a-file-type
if idx := strings.IndexByte(raw, '?'); idx >= 0 {
return optional.Some(raw[:idx])
}
}
return attrStr
}
func (attrs *Attributes) GetLanguage() optional.Option[string] {
// prefer linguist-language over gitlab-language
// if linguist-language is not set, use gitlab-language
// if both are not set, return none
language := attrs.GetLinguistLanguage()
if language.Value() == "" {
language = attrs.GetGitlabLanguage()
}
return language
}

View File

@ -0,0 +1,37 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package attribute
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_Attribute(t *testing.T) {
assert.Empty(t, Attribute("").ToString().Value())
assert.Empty(t, Attribute("unspecified").ToString().Value())
assert.Equal(t, "python", Attribute("python").ToString().Value())
assert.Equal(t, "Java", Attribute("Java").ToString().Value())
attributes := Attributes{
m: map[string]Attribute{
LinguistGenerated: "true",
LinguistDocumentation: "false",
LinguistDetectable: "set",
LinguistLanguage: "Python",
GitlabLanguage: "Java",
"filter": "unspecified",
"test": "",
},
}
assert.Empty(t, attributes.Get("test").ToString().Value())
assert.Empty(t, attributes.Get("filter").ToString().Value())
assert.Equal(t, "Python", attributes.Get(LinguistLanguage).ToString().Value())
assert.Equal(t, "Java", attributes.Get(GitlabLanguage).ToString().Value())
assert.True(t, attributes.Get(LinguistGenerated).ToBool().Value())
assert.False(t, attributes.Get(LinguistDocumentation).ToBool().Value())
assert.True(t, attributes.Get(LinguistDetectable).ToBool().Value())
}

View File

@ -0,0 +1,216 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package attribute
import (
"bytes"
"context"
"fmt"
"os"
"path/filepath"
"time"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
)
// BatchChecker provides a reader for check-attribute content that can be long running
type BatchChecker struct {
attributesNum int
repo *git.Repository
stdinWriter *os.File
stdOut *nulSeparatedAttributeWriter
ctx context.Context
cancel context.CancelFunc
cmd *git.Command
}
// NewBatchChecker creates a check attribute reader for the current repository and provided commit ID
// If treeish is empty, then it will use current working directory, otherwise it will use the provided treeish on the bare repo
func NewBatchChecker(repo *git.Repository, treeish string, attributes []string) (checker *BatchChecker, returnedErr error) {
ctx, cancel := context.WithCancel(repo.Ctx)
defer func() {
if returnedErr != nil {
cancel()
}
}()
cmd, envs, cleanup, err := checkAttrCommand(repo, treeish, nil, attributes)
if err != nil {
return nil, err
}
defer func() {
if returnedErr != nil {
cleanup()
}
}()
cmd.AddArguments("--stdin")
checker = &BatchChecker{
attributesNum: len(attributes),
repo: repo,
ctx: ctx,
cmd: cmd,
cancel: func() {
cancel()
cleanup()
},
}
stdinReader, stdinWriter, err := os.Pipe()
if err != nil {
return nil, err
}
checker.stdinWriter = stdinWriter
lw := new(nulSeparatedAttributeWriter)
lw.attributes = make(chan attributeTriple, len(attributes))
lw.closed = make(chan struct{})
checker.stdOut = lw
go func() {
defer func() {
_ = stdinReader.Close()
_ = lw.Close()
}()
stdErr := new(bytes.Buffer)
err := cmd.Run(ctx, &git.RunOpts{
Env: envs,
Dir: repo.Path,
Stdin: stdinReader,
Stdout: lw,
Stderr: stdErr,
})
if err != nil && !git.IsErrCanceledOrKilled(err) {
log.Error("Attribute checker for commit %s exits with error: %v", treeish, err)
}
checker.cancel()
}()
return checker, nil
}
// CheckPath check attr for given path
func (c *BatchChecker) CheckPath(path string) (rs *Attributes, err error) {
defer func() {
if err != nil && err != c.ctx.Err() {
log.Error("Unexpected error when checking path %s in %s, error: %v", path, filepath.Base(c.repo.Path), err)
}
}()
select {
case <-c.ctx.Done():
return nil, c.ctx.Err()
default:
}
if _, err = c.stdinWriter.Write([]byte(path + "\x00")); err != nil {
defer c.Close()
return nil, err
}
reportTimeout := func() error {
stdOutClosed := false
select {
case <-c.stdOut.closed:
stdOutClosed = true
default:
}
debugMsg := fmt.Sprintf("check path %q in repo %q", path, filepath.Base(c.repo.Path))
debugMsg += fmt.Sprintf(", stdOut: tmp=%q, pos=%d, closed=%v", string(c.stdOut.tmp), c.stdOut.pos, stdOutClosed)
if c.cmd != nil {
debugMsg += fmt.Sprintf(", process state: %q", c.cmd.ProcessState())
}
_ = c.Close()
return fmt.Errorf("CheckPath timeout: %s", debugMsg)
}
rs = NewAttributes()
for i := 0; i < c.attributesNum; i++ {
select {
case <-time.After(5 * time.Second):
// there is no "hang" problem now. This code is just used to catch other potential problems.
return nil, reportTimeout()
case attr, ok := <-c.stdOut.ReadAttribute():
if !ok {
return nil, c.ctx.Err()
}
rs.m[attr.Attribute] = Attribute(attr.Value)
case <-c.ctx.Done():
return nil, c.ctx.Err()
}
}
return rs, nil
}
func (c *BatchChecker) Close() error {
c.cancel()
err := c.stdinWriter.Close()
return err
}
type attributeTriple struct {
Filename string
Attribute string
Value string
}
type nulSeparatedAttributeWriter struct {
tmp []byte
attributes chan attributeTriple
closed chan struct{}
working attributeTriple
pos int
}
func (wr *nulSeparatedAttributeWriter) Write(p []byte) (n int, err error) {
l, read := len(p), 0
nulIdx := bytes.IndexByte(p, '\x00')
for nulIdx >= 0 {
wr.tmp = append(wr.tmp, p[:nulIdx]...)
switch wr.pos {
case 0:
wr.working = attributeTriple{
Filename: string(wr.tmp),
}
case 1:
wr.working.Attribute = string(wr.tmp)
case 2:
wr.working.Value = string(wr.tmp)
}
wr.tmp = wr.tmp[:0]
wr.pos++
if wr.pos > 2 {
wr.attributes <- wr.working
wr.pos = 0
}
read += nulIdx + 1
if l > read {
p = p[nulIdx+1:]
nulIdx = bytes.IndexByte(p, '\x00')
} else {
return l, nil
}
}
wr.tmp = append(wr.tmp, p...)
return l, nil
}
func (wr *nulSeparatedAttributeWriter) ReadAttribute() <-chan attributeTriple {
return wr.attributes
}
func (wr *nulSeparatedAttributeWriter) Close() error {
select {
case <-wr.closed:
return nil
default:
}
close(wr.attributes)
close(wr.closed)
return nil
}

View File

@ -1,13 +1,19 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
package attribute
import (
"path/filepath"
"testing"
"time"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) {
@ -24,7 +30,7 @@ func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) {
select {
case attr := <-wr.ReadAttribute():
assert.Equal(t, ".gitignore\"\n", attr.Filename)
assert.Equal(t, AttributeLinguistVendored, attr.Attribute)
assert.Equal(t, LinguistVendored, attr.Attribute)
assert.Equal(t, "unspecified", attr.Value)
case <-time.After(100 * time.Millisecond):
assert.FailNow(t, "took too long to read an attribute from the list")
@ -38,7 +44,7 @@ func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) {
select {
case attr := <-wr.ReadAttribute():
assert.Equal(t, ".gitignore\"\n", attr.Filename)
assert.Equal(t, AttributeLinguistVendored, attr.Attribute)
assert.Equal(t, LinguistVendored, attr.Attribute)
assert.Equal(t, "unspecified", attr.Value)
case <-time.After(100 * time.Millisecond):
assert.FailNow(t, "took too long to read an attribute from the list")
@ -77,21 +83,90 @@ func Test_nulSeparatedAttributeWriter_ReadAttribute(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, attributeTriple{
Filename: "shouldbe.vendor",
Attribute: AttributeLinguistVendored,
Attribute: LinguistVendored,
Value: "set",
}, attr)
attr = <-wr.ReadAttribute()
assert.NoError(t, err)
assert.Equal(t, attributeTriple{
Filename: "shouldbe.vendor",
Attribute: AttributeLinguistGenerated,
Attribute: LinguistGenerated,
Value: "unspecified",
}, attr)
attr = <-wr.ReadAttribute()
assert.NoError(t, err)
assert.Equal(t, attributeTriple{
Filename: "shouldbe.vendor",
Attribute: AttributeLinguistLanguage,
Attribute: LinguistLanguage,
Value: "unspecified",
}, attr)
}
func expectedAttrs() *Attributes {
return &Attributes{
m: map[string]Attribute{
LinguistGenerated: "unspecified",
LinguistDetectable: "unspecified",
LinguistDocumentation: "unspecified",
LinguistVendored: "unspecified",
LinguistLanguage: "Python",
GitlabLanguage: "unspecified",
},
}
}
func Test_BatchChecker(t *testing.T) {
setting.AppDataPath = t.TempDir()
repoPath := "../tests/repos/language_stats_repo"
gitRepo, err := git.OpenRepository(t.Context(), repoPath)
require.NoError(t, err)
defer gitRepo.Close()
commitID := "8fee858da5796dfb37704761701bb8e800ad9ef3"
t.Run("Create index file to run git check-attr", func(t *testing.T) {
defer test.MockVariableValue(&git.DefaultFeatures().SupportCheckAttrOnBare, false)()
checker, err := NewBatchChecker(gitRepo, commitID, LinguistAttributes)
assert.NoError(t, err)
defer checker.Close()
attributes, err := checker.CheckPath("i-am-a-python.p")
assert.NoError(t, err)
assert.Equal(t, expectedAttrs(), attributes)
})
// run git check-attr on work tree
t.Run("Run git check-attr on git work tree", func(t *testing.T) {
dir := filepath.Join(t.TempDir(), "test-repo")
err := git.Clone(t.Context(), repoPath, dir, git.CloneRepoOptions{
Shared: true,
Branch: "master",
})
assert.NoError(t, err)
tempRepo, err := git.OpenRepository(t.Context(), dir)
assert.NoError(t, err)
defer tempRepo.Close()
checker, err := NewBatchChecker(tempRepo, "", LinguistAttributes)
assert.NoError(t, err)
defer checker.Close()
attributes, err := checker.CheckPath("i-am-a-python.p")
assert.NoError(t, err)
assert.Equal(t, expectedAttrs(), attributes)
})
if !git.DefaultFeatures().SupportCheckAttrOnBare {
t.Skip("git version 2.40 is required to support run check-attr on bare repo")
return
}
t.Run("Run git check-attr in bare repository", func(t *testing.T) {
checker, err := NewBatchChecker(gitRepo, commitID, LinguistAttributes)
assert.NoError(t, err)
defer checker.Close()
attributes, err := checker.CheckPath("i-am-a-python.p")
assert.NoError(t, err)
assert.Equal(t, expectedAttrs(), attributes)
})
}

View File

@ -0,0 +1,96 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package attribute
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"code.gitea.io/gitea/modules/git"
)
func checkAttrCommand(gitRepo *git.Repository, treeish string, filenames, attributes []string) (*git.Command, []string, func(), error) {
cancel := func() {}
envs := []string{"GIT_FLUSH=1"}
cmd := git.NewCommand("check-attr", "-z")
if len(attributes) == 0 {
cmd.AddArguments("--all")
}
// there is treeish, read from bare repo or temp index created by "read-tree"
if treeish != "" {
if git.DefaultFeatures().SupportCheckAttrOnBare {
cmd.AddArguments("--source")
cmd.AddDynamicArguments(treeish)
} else {
indexFilename, worktree, deleteTemporaryFile, err := gitRepo.ReadTreeToTemporaryIndex(treeish)
if err != nil {
return nil, nil, nil, err
}
cmd.AddArguments("--cached")
envs = append(envs,
"GIT_INDEX_FILE="+indexFilename,
"GIT_WORK_TREE="+worktree,
)
cancel = deleteTemporaryFile
}
} // else: no treeish, assume it is a not a bare repo, read from working directory
cmd.AddDynamicArguments(attributes...)
if len(filenames) > 0 {
cmd.AddDashesAndList(filenames...)
}
return cmd, envs, cancel, nil
}
type CheckAttributeOpts struct {
Filenames []string
Attributes []string
}
// CheckAttributes return the attributes of the given filenames and attributes in the given treeish.
// If treeish is empty, then it will use current working directory, otherwise it will use the provided treeish on the bare repo
func CheckAttributes(ctx context.Context, gitRepo *git.Repository, treeish string, opts CheckAttributeOpts) (map[string]*Attributes, error) {
cmd, envs, cancel, err := checkAttrCommand(gitRepo, treeish, opts.Filenames, opts.Attributes)
if err != nil {
return nil, err
}
defer cancel()
stdOut := new(bytes.Buffer)
stdErr := new(bytes.Buffer)
if err := cmd.Run(ctx, &git.RunOpts{
Env: append(os.Environ(), envs...),
Dir: gitRepo.Path,
Stdout: stdOut,
Stderr: stdErr,
}); err != nil {
return nil, fmt.Errorf("failed to run check-attr: %w\n%s\n%s", err, stdOut.String(), stdErr.String())
}
fields := bytes.Split(stdOut.Bytes(), []byte{'\000'})
if len(fields)%3 != 1 {
return nil, errors.New("wrong number of fields in return from check-attr")
}
attributesMap := make(map[string]*Attributes)
for i := 0; i < (len(fields) / 3); i++ {
filename := string(fields[3*i])
attribute := string(fields[3*i+1])
info := string(fields[3*i+2])
attribute2info, ok := attributesMap[filename]
if !ok {
attribute2info = NewAttributes()
attributesMap[filename] = attribute2info
}
attribute2info.m[attribute] = Attribute(info)
}
return attributesMap, nil
}

View File

@ -0,0 +1,74 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package attribute
import (
"path/filepath"
"testing"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_Checker(t *testing.T) {
setting.AppDataPath = t.TempDir()
repoPath := "../tests/repos/language_stats_repo"
gitRepo, err := git.OpenRepository(t.Context(), repoPath)
require.NoError(t, err)
defer gitRepo.Close()
commitID := "8fee858da5796dfb37704761701bb8e800ad9ef3"
t.Run("Create index file to run git check-attr", func(t *testing.T) {
defer test.MockVariableValue(&git.DefaultFeatures().SupportCheckAttrOnBare, false)()
attrs, err := CheckAttributes(t.Context(), gitRepo, commitID, CheckAttributeOpts{
Filenames: []string{"i-am-a-python.p"},
Attributes: LinguistAttributes,
})
assert.NoError(t, err)
assert.Len(t, attrs, 1)
assert.Equal(t, expectedAttrs(), attrs["i-am-a-python.p"])
})
// run git check-attr on work tree
t.Run("Run git check-attr on git work tree", func(t *testing.T) {
dir := filepath.Join(t.TempDir(), "test-repo")
err := git.Clone(t.Context(), repoPath, dir, git.CloneRepoOptions{
Shared: true,
Branch: "master",
})
assert.NoError(t, err)
tempRepo, err := git.OpenRepository(t.Context(), dir)
assert.NoError(t, err)
defer tempRepo.Close()
attrs, err := CheckAttributes(t.Context(), tempRepo, "", CheckAttributeOpts{
Filenames: []string{"i-am-a-python.p"},
Attributes: LinguistAttributes,
})
assert.NoError(t, err)
assert.Len(t, attrs, 1)
assert.Equal(t, expectedAttrs(), attrs["i-am-a-python.p"])
})
if !git.DefaultFeatures().SupportCheckAttrOnBare {
t.Skip("git version 2.40 is required to support run check-attr on bare repo")
return
}
t.Run("Run git check-attr in bare repository", func(t *testing.T) {
attrs, err := CheckAttributes(t.Context(), gitRepo, commitID, CheckAttributeOpts{
Filenames: []string{"i-am-a-python.p"},
Attributes: LinguistAttributes,
})
assert.NoError(t, err)
assert.Len(t, attrs, 1)
assert.Equal(t, expectedAttrs(), attrs["i-am-a-python.p"])
})
}

View File

@ -0,0 +1,41 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package attribute
import (
"context"
"fmt"
"os"
"testing"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)
func testRun(m *testing.M) error {
gitHomePath, err := os.MkdirTemp(os.TempDir(), "git-home")
if err != nil {
return fmt.Errorf("unable to create temp dir: %w", err)
}
defer util.RemoveAll(gitHomePath)
setting.Git.HomePath = gitHomePath
if err = git.InitFull(context.Background()); err != nil {
return fmt.Errorf("failed to call Init: %w", err)
}
exitCode := m.Run()
if exitCode != 0 {
return fmt.Errorf("run test failed, ExitCode=%d", exitCode)
}
return nil
}
func TestMain(m *testing.M) {
if err := testRun(m); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Test failed: %v", err)
os.Exit(1)
}
}

View File

@ -11,7 +11,7 @@ import (
"os"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/setting"
)
// BlamePart represents block of blame - continuous lines with one sha
@ -29,12 +29,13 @@ type BlameReader struct {
bufferedReader *bufio.Reader
done chan error
lastSha *string
ignoreRevsFile *string
ignoreRevsFile string
objectFormat ObjectFormat
cleanupFuncs []func()
}
func (r *BlameReader) UsesIgnoreRevs() bool {
return r.ignoreRevsFile != nil
return r.ignoreRevsFile != ""
}
// NextPart returns next part of blame (sequential code lines with the same commit)
@ -122,36 +123,37 @@ func (r *BlameReader) Close() error {
r.bufferedReader = nil
_ = r.reader.Close()
_ = r.output.Close()
if r.ignoreRevsFile != nil {
_ = util.Remove(*r.ignoreRevsFile)
for _, cleanup := range r.cleanupFuncs {
if cleanup != nil {
cleanup()
}
}
return err
}
// CreateBlameReader creates reader for given repository, commit and file
func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath string, commit *Commit, file string, bypassBlameIgnore bool) (*BlameReader, error) {
var ignoreRevsFile *string
if DefaultFeatures().CheckVersionAtLeast("2.23") && !bypassBlameIgnore {
ignoreRevsFile = tryCreateBlameIgnoreRevsFile(commit)
}
cmd := NewCommandNoGlobals("blame", "--porcelain")
if ignoreRevsFile != nil {
// Possible improvement: use --ignore-revs-file /dev/stdin on unix
// There is no equivalent on Windows. May be implemented if Gitea uses an external git backend.
cmd.AddOptionValues("--ignore-revs-file", *ignoreRevsFile)
}
cmd.AddDynamicArguments(commit.ID.String()).AddDashesAndList(file)
reader, stdout, err := os.Pipe()
if err != nil {
if ignoreRevsFile != nil {
_ = util.Remove(*ignoreRevsFile)
}
return nil, err
}
done := make(chan error, 1)
cmd := NewCommandNoGlobals("blame", "--porcelain")
var ignoreRevsFileName string
var ignoreRevsFileCleanup func() // TODO: maybe it should check the returned err in a defer func to make sure the cleanup could always be executed correctly
if DefaultFeatures().CheckVersionAtLeast("2.23") && !bypassBlameIgnore {
ignoreRevsFileName, ignoreRevsFileCleanup = tryCreateBlameIgnoreRevsFile(commit)
if ignoreRevsFileName != "" {
// Possible improvement: use --ignore-revs-file /dev/stdin on unix
// There is no equivalent on Windows. May be implemented if Gitea uses an external git backend.
cmd.AddOptionValues("--ignore-revs-file", ignoreRevsFileName)
}
}
cmd.AddDynamicArguments(commit.ID.String()).AddDashesAndList(file)
done := make(chan error, 1)
go func() {
stderr := bytes.Buffer{}
// TODO: it doesn't work for directories (the directories shouldn't be "blamed"), and the "err" should be returned by "Read" but not by "Close"
@ -169,40 +171,44 @@ func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath
}()
bufferedReader := bufio.NewReader(reader)
return &BlameReader{
output: stdout,
reader: reader,
bufferedReader: bufferedReader,
done: done,
ignoreRevsFile: ignoreRevsFile,
ignoreRevsFile: ignoreRevsFileName,
objectFormat: objectFormat,
cleanupFuncs: []func(){ignoreRevsFileCleanup},
}, nil
}
func tryCreateBlameIgnoreRevsFile(commit *Commit) *string {
func tryCreateBlameIgnoreRevsFile(commit *Commit) (string, func()) {
entry, err := commit.GetTreeEntryByPath(".git-blame-ignore-revs")
if err != nil {
return nil
log.Error("Unable to get .git-blame-ignore-revs file: GetTreeEntryByPath: %v", err)
return "", nil
}
r, err := entry.Blob().DataAsync()
if err != nil {
return nil
log.Error("Unable to get .git-blame-ignore-revs file data: DataAsync: %v", err)
return "", nil
}
defer r.Close()
f, err := os.CreateTemp("", "gitea_git-blame-ignore-revs")
f, cleanup, err := setting.AppDataTempDir("git-repo-content").CreateTempFileRandom("git-blame-ignore-revs")
if err != nil {
return nil
log.Error("Unable to get .git-blame-ignore-revs file data: CreateTempFileRandom: %v", err)
return "", nil
}
filename := f.Name()
_, err = io.Copy(f, r)
_ = f.Close()
if err != nil {
_ = util.Remove(f.Name())
return nil
cleanup()
log.Error("Unable to get .git-blame-ignore-revs file data: Copy: %v", err)
return "", nil
}
return util.ToPointer(f.Name())
return filename, cleanup
}

View File

@ -7,10 +7,13 @@ import (
"context"
"testing"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
)
func TestReadingBlameOutputSha256(t *testing.T) {
setting.AppDataPath = t.TempDir()
ctx, cancel := context.WithCancel(t.Context())
defer cancel()

View File

@ -7,10 +7,13 @@ import (
"context"
"testing"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
)
func TestReadingBlameOutput(t *testing.T) {
setting.AppDataPath = t.TempDir()
ctx, cancel := context.WithCancel(t.Context())
defer cancel()

View File

@ -80,6 +80,13 @@ func (c *Command) LogString() string {
return strings.Join(a, " ")
}
func (c *Command) ProcessState() string {
if c.cmd == nil {
return ""
}
return c.cmd.ProcessState.String()
}
// NewCommand creates and returns a new Git Command based on given command and arguments.
// Each argument should be safe to be trusted. User-provided arguments should be passed to AddDynamicArguments instead.
func NewCommand(args ...internal.CmdArg) *Command {

View File

@ -30,6 +30,7 @@ type Features struct {
SupportProcReceive bool // >= 2.29
SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an experimental curiosity
SupportedObjectFormats []ObjectFormat // sha1, sha256
SupportCheckAttrOnBare bool // >= 2.40
}
var (
@ -77,6 +78,7 @@ func loadGitVersionFeatures() (*Features, error) {
if features.SupportHashSha256 {
features.SupportedObjectFormats = append(features.SupportedObjectFormats, Sha256ObjectFormat)
}
features.SupportCheckAttrOnBare = features.CheckVersionAtLeast("2.40")
return features, nil
}

View File

@ -10,18 +10,19 @@ import (
"testing"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/tempdir"
"github.com/hashicorp/go-version"
"github.com/stretchr/testify/assert"
)
func testRun(m *testing.M) error {
gitHomePath, err := os.MkdirTemp(os.TempDir(), "git-home")
gitHomePath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("git-home")
if err != nil {
return fmt.Errorf("unable to create temp dir: %w", err)
}
defer util.RemoveAll(gitHomePath)
defer cleanup()
setting.Git.HomePath = gitHomePath
if err = InitFull(context.Background()); err != nil {

View File

@ -1,13 +1,15 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
package languagestats
import (
"context"
"strings"
"unicode"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/attribute"
)
const (
@ -49,19 +51,15 @@ func mergeLanguageStats(stats map[string]int64) map[string]int64 {
return res
}
func TryReadLanguageAttribute(attrs map[string]string) optional.Option[string] {
language := AttributeToString(attrs, AttributeLinguistLanguage)
if language.Value() == "" {
language = AttributeToString(attrs, AttributeGitlabLanguage)
if language.Has() {
raw := language.Value()
// gitlab-language may have additional parameters after the language
// ignore them and just use the main language
// https://docs.gitlab.com/ee/user/project/highlighting.html#override-syntax-highlighting-for-a-file-type
if idx := strings.IndexByte(raw, '?'); idx >= 0 {
language = optional.Some(raw[:idx])
}
}
// GetFileLanguage tries to get the (linguist) language of the file content
func GetFileLanguage(ctx context.Context, gitRepo *git.Repository, treeish, treePath string) (string, error) {
attributesMap, err := attribute.CheckAttributes(ctx, gitRepo, treeish, attribute.CheckAttributeOpts{
Attributes: []string{attribute.LinguistLanguage, attribute.GitlabLanguage},
Filenames: []string{treePath},
})
if err != nil {
return "", err
}
return language
return attributesMap[treePath].GetLanguage().Value(), nil
}

View File

@ -3,13 +3,15 @@
//go:build gogit
package git
package languagestats
import (
"bytes"
"io"
"code.gitea.io/gitea/modules/analyze"
git_module "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/attribute"
"code.gitea.io/gitea/modules/optional"
"github.com/go-enry/go-enry/v2"
@ -19,7 +21,7 @@ import (
)
// GetLanguageStats calculates language stats for git repository at specified commit
func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, error) {
func GetLanguageStats(repo *git_module.Repository, commitID string) (map[string]int64, error) {
r, err := git.PlainOpen(repo.Path)
if err != nil {
return nil, err
@ -40,8 +42,11 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
return nil, err
}
checker, deferable := repo.CheckAttributeReader(commitID)
defer deferable()
checker, err := attribute.NewBatchChecker(repo, commitID, attribute.LinguistAttributes)
if err != nil {
return nil, err
}
defer checker.Close()
// sizes contains the current calculated size of all files by language
sizes := make(map[string]int64)
@ -62,43 +67,41 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
isDocumentation := optional.None[bool]()
isDetectable := optional.None[bool]()
if checker != nil {
attrs, err := checker.CheckPath(f.Name)
if err == nil {
isVendored = AttributeToBool(attrs, AttributeLinguistVendored)
if isVendored.ValueOrDefault(false) {
return nil
attrs, err := checker.CheckPath(f.Name)
if err == nil {
isVendored = attrs.GetVendored()
if isVendored.ValueOrDefault(false) {
return nil
}
isGenerated = attrs.GetGenerated()
if isGenerated.ValueOrDefault(false) {
return nil
}
isDocumentation = attrs.GetDocumentation()
if isDocumentation.ValueOrDefault(false) {
return nil
}
isDetectable = attrs.GetDetectable()
if !isDetectable.ValueOrDefault(true) {
return nil
}
hasLanguage := attrs.GetLanguage()
if hasLanguage.Value() != "" {
language := hasLanguage.Value()
// group languages, such as Pug -> HTML; SCSS -> CSS
group := enry.GetLanguageGroup(language)
if len(group) != 0 {
language = group
}
isGenerated = AttributeToBool(attrs, AttributeLinguistGenerated)
if isGenerated.ValueOrDefault(false) {
return nil
}
isDocumentation = AttributeToBool(attrs, AttributeLinguistDocumentation)
if isDocumentation.ValueOrDefault(false) {
return nil
}
isDetectable = AttributeToBool(attrs, AttributeLinguistDetectable)
if !isDetectable.ValueOrDefault(true) {
return nil
}
hasLanguage := TryReadLanguageAttribute(attrs)
if hasLanguage.Value() != "" {
language := hasLanguage.Value()
// group languages, such as Pug -> HTML; SCSS -> CSS
group := enry.GetLanguageGroup(language)
if len(group) != 0 {
language = group
}
// this language will always be added to the size
sizes[language] += f.Size
return nil
}
// this language will always be added to the size
sizes[language] += f.Size
return nil
}
}

View File

@ -3,13 +3,15 @@
//go:build !gogit
package git
package languagestats
import (
"bytes"
"io"
"code.gitea.io/gitea/modules/analyze"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/attribute"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
@ -17,7 +19,7 @@ import (
)
// GetLanguageStats calculates language stats for git repository at specified commit
func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, error) {
func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64, error) {
// We will feed the commit IDs in order into cat-file --batch, followed by blobs as necessary.
// so let's create a batch stdin and stdout
batchStdinWriter, batchReader, cancel, err := repo.CatFileBatch(repo.Ctx)
@ -34,19 +36,19 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
if err := writeID(commitID); err != nil {
return nil, err
}
shaBytes, typ, size, err := ReadBatchLine(batchReader)
shaBytes, typ, size, err := git.ReadBatchLine(batchReader)
if typ != "commit" {
log.Debug("Unable to get commit for: %s. Err: %v", commitID, err)
return nil, ErrNotExist{commitID, ""}
return nil, git.ErrNotExist{ID: commitID}
}
sha, err := NewIDFromString(string(shaBytes))
sha, err := git.NewIDFromString(string(shaBytes))
if err != nil {
log.Debug("Unable to get commit for: %s. Err: %v", commitID, err)
return nil, ErrNotExist{commitID, ""}
return nil, git.ErrNotExist{ID: commitID}
}
commit, err := CommitFromReader(repo, sha, io.LimitReader(batchReader, size))
commit, err := git.CommitFromReader(repo, sha, io.LimitReader(batchReader, size))
if err != nil {
log.Debug("Unable to get commit for: %s. Err: %v", commitID, err)
return nil, err
@ -62,8 +64,11 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
return nil, err
}
checker, deferable := repo.CheckAttributeReader(commitID)
defer deferable()
checker, err := attribute.NewBatchChecker(repo, commitID, attribute.LinguistAttributes)
if err != nil {
return nil, err
}
defer checker.Close()
contentBuf := bytes.Buffer{}
var content []byte
@ -96,43 +101,36 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
isDocumentation := optional.None[bool]()
isDetectable := optional.None[bool]()
if checker != nil {
attrs, err := checker.CheckPath(f.Name())
if err == nil {
isVendored = AttributeToBool(attrs, AttributeLinguistVendored)
if isVendored.ValueOrDefault(false) {
continue
attrs, err := checker.CheckPath(f.Name())
if err == nil {
if isVendored = attrs.GetVendored(); isVendored.ValueOrDefault(false) {
continue
}
if isGenerated = attrs.GetGenerated(); isGenerated.ValueOrDefault(false) {
continue
}
if isDocumentation = attrs.GetDocumentation(); isDocumentation.ValueOrDefault(false) {
continue
}
if isDetectable = attrs.GetDetectable(); !isDetectable.ValueOrDefault(true) {
continue
}
if hasLanguage := attrs.GetLanguage(); hasLanguage.Value() != "" {
language := hasLanguage.Value()
// group languages, such as Pug -> HTML; SCSS -> CSS
group := enry.GetLanguageGroup(language)
if len(group) != 0 {
language = group
}
isGenerated = AttributeToBool(attrs, AttributeLinguistGenerated)
if isGenerated.ValueOrDefault(false) {
continue
}
isDocumentation = AttributeToBool(attrs, AttributeLinguistDocumentation)
if isDocumentation.ValueOrDefault(false) {
continue
}
isDetectable = AttributeToBool(attrs, AttributeLinguistDetectable)
if !isDetectable.ValueOrDefault(true) {
continue
}
hasLanguage := TryReadLanguageAttribute(attrs)
if hasLanguage.Value() != "" {
language := hasLanguage.Value()
// group languages, such as Pug -> HTML; SCSS -> CSS
group := enry.GetLanguageGroup(language)
if len(group) != 0 {
language = group
}
// this language will always be added to the size
sizes[language] += f.Size()
continue
}
// this language will always be added to the size
sizes[language] += f.Size()
continue
}
}
@ -149,7 +147,7 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
if err := writeID(f.ID.String()); err != nil {
return nil, err
}
_, _, size, err := ReadBatchLine(batchReader)
_, _, size, err := git.ReadBatchLine(batchReader)
if err != nil {
log.Debug("Error reading blob: %s Err: %v", f.ID.String(), err)
return nil, err
@ -167,7 +165,7 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
return nil, err
}
content = contentBuf.Bytes()
if err := DiscardFull(batchReader, discard); err != nil {
if err := git.DiscardFull(batchReader, discard); err != nil {
return nil, err
}
}

View File

@ -3,24 +3,26 @@
//go:build !gogit
package git
package languagestats
import (
"path/filepath"
"testing"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRepository_GetLanguageStats(t *testing.T) {
repoPath := filepath.Join(testReposDir, "language_stats_repo")
gitRepo, err := openRepositoryWithDefaultContext(repoPath)
setting.AppDataPath = t.TempDir()
repoPath := "../tests/repos/language_stats_repo"
gitRepo, err := git.OpenRepository(t.Context(), repoPath)
require.NoError(t, err)
defer gitRepo.Close()
stats, err := gitRepo.GetLanguageStats("8fee858da5796dfb37704761701bb8e800ad9ef3")
stats, err := GetLanguageStats(gitRepo, "8fee858da5796dfb37704761701bb8e800ad9ef3")
require.NoError(t, err)
assert.Equal(t, map[string]int64{

View File

@ -0,0 +1,41 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package languagestats
import (
"context"
"fmt"
"os"
"testing"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)
func testRun(m *testing.M) error {
gitHomePath, err := os.MkdirTemp(os.TempDir(), "git-home")
if err != nil {
return fmt.Errorf("unable to create temp dir: %w", err)
}
defer util.RemoveAll(gitHomePath)
setting.Git.HomePath = gitHomePath
if err = git.InitFull(context.Background()); err != nil {
return fmt.Errorf("failed to call Init: %w", err)
}
exitCode := m.Run()
if exitCode != 0 {
return fmt.Errorf("run test failed, ExitCode=%d", exitCode)
}
return nil
}
func TestMain(m *testing.M) {
if err := testRun(m); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Test failed: %v", err)
os.Exit(1)
}
}

View File

@ -18,6 +18,7 @@ import (
"time"
"code.gitea.io/gitea/modules/proxy"
"code.gitea.io/gitea/modules/setting"
)
// GPGSettings represents the default GPG settings for this repository
@ -266,11 +267,11 @@ func GetDivergingCommits(ctx context.Context, repoPath, baseBranch, targetBranch
// CreateBundle create bundle content to the target path
func (repo *Repository) CreateBundle(ctx context.Context, commit string, out io.Writer) error {
tmp, err := os.MkdirTemp(os.TempDir(), "gitea-bundle")
tmp, cleanup, err := setting.AppDataTempDir("git-repo-content").MkdirTempRandom("gitea-bundle")
if err != nil {
return err
}
defer os.RemoveAll(tmp)
defer cleanup()
env := append(os.Environ(), "GIT_OBJECT_DIRECTORY="+filepath.Join(repo.Path, "objects"))
_, _, err = NewCommand("init", "--bare").RunStdString(ctx, &RunOpts{Dir: tmp, Env: env})

View File

@ -1,341 +0,0 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"time"
"code.gitea.io/gitea/modules/log"
)
// CheckAttributeOpts represents the possible options to CheckAttribute
type CheckAttributeOpts struct {
CachedOnly bool
AllAttributes bool
Attributes []string
Filenames []string
IndexFile string
WorkTree string
}
// CheckAttribute return the Blame object of file
func (repo *Repository) CheckAttribute(opts CheckAttributeOpts) (map[string]map[string]string, error) {
env := []string{}
if len(opts.IndexFile) > 0 {
env = append(env, "GIT_INDEX_FILE="+opts.IndexFile)
}
if len(opts.WorkTree) > 0 {
env = append(env, "GIT_WORK_TREE="+opts.WorkTree)
}
if len(env) > 0 {
env = append(os.Environ(), env...)
}
stdOut := new(bytes.Buffer)
stdErr := new(bytes.Buffer)
cmd := NewCommand("check-attr", "-z")
if opts.AllAttributes {
cmd.AddArguments("-a")
} else {
for _, attribute := range opts.Attributes {
if attribute != "" {
cmd.AddDynamicArguments(attribute)
}
}
}
if opts.CachedOnly {
cmd.AddArguments("--cached")
}
cmd.AddDashesAndList(opts.Filenames...)
if err := cmd.Run(repo.Ctx, &RunOpts{
Env: env,
Dir: repo.Path,
Stdout: stdOut,
Stderr: stdErr,
}); err != nil {
return nil, fmt.Errorf("failed to run check-attr: %w\n%s\n%s", err, stdOut.String(), stdErr.String())
}
// FIXME: This is incorrect on versions < 1.8.5
fields := bytes.Split(stdOut.Bytes(), []byte{'\000'})
if len(fields)%3 != 1 {
return nil, errors.New("wrong number of fields in return from check-attr")
}
name2attribute2info := make(map[string]map[string]string)
for i := 0; i < (len(fields) / 3); i++ {
filename := string(fields[3*i])
attribute := string(fields[3*i+1])
info := string(fields[3*i+2])
attribute2info := name2attribute2info[filename]
if attribute2info == nil {
attribute2info = make(map[string]string)
}
attribute2info[attribute] = info
name2attribute2info[filename] = attribute2info
}
return name2attribute2info, nil
}
// CheckAttributeReader provides a reader for check-attribute content that can be long running
type CheckAttributeReader struct {
// params
Attributes []string
Repo *Repository
IndexFile string
WorkTree string
stdinReader io.ReadCloser
stdinWriter *os.File
stdOut *nulSeparatedAttributeWriter
cmd *Command
env []string
ctx context.Context
cancel context.CancelFunc
}
// Init initializes the CheckAttributeReader
func (c *CheckAttributeReader) Init(ctx context.Context) error {
if len(c.Attributes) == 0 {
lw := new(nulSeparatedAttributeWriter)
lw.attributes = make(chan attributeTriple)
lw.closed = make(chan struct{})
c.stdOut = lw
c.stdOut.Close()
return errors.New("no provided Attributes to check")
}
c.ctx, c.cancel = context.WithCancel(ctx)
c.cmd = NewCommand("check-attr", "--stdin", "-z")
if len(c.IndexFile) > 0 {
c.cmd.AddArguments("--cached")
c.env = append(c.env, "GIT_INDEX_FILE="+c.IndexFile)
}
if len(c.WorkTree) > 0 {
c.env = append(c.env, "GIT_WORK_TREE="+c.WorkTree)
}
c.env = append(c.env, "GIT_FLUSH=1")
c.cmd.AddDynamicArguments(c.Attributes...)
var err error
c.stdinReader, c.stdinWriter, err = os.Pipe()
if err != nil {
c.cancel()
return err
}
lw := new(nulSeparatedAttributeWriter)
lw.attributes = make(chan attributeTriple, 5)
lw.closed = make(chan struct{})
c.stdOut = lw
return nil
}
func (c *CheckAttributeReader) Run() error {
defer func() {
_ = c.stdinReader.Close()
_ = c.stdOut.Close()
}()
stdErr := new(bytes.Buffer)
err := c.cmd.Run(c.ctx, &RunOpts{
Env: c.env,
Dir: c.Repo.Path,
Stdin: c.stdinReader,
Stdout: c.stdOut,
Stderr: stdErr,
})
if err != nil && !IsErrCanceledOrKilled(err) {
return fmt.Errorf("failed to run attr-check. Error: %w\nStderr: %s", err, stdErr.String())
}
return nil
}
// CheckPath check attr for given path
func (c *CheckAttributeReader) CheckPath(path string) (rs map[string]string, err error) {
defer func() {
if err != nil && err != c.ctx.Err() {
log.Error("Unexpected error when checking path %s in %s, error: %v", path, filepath.Base(c.Repo.Path), err)
}
}()
select {
case <-c.ctx.Done():
return nil, c.ctx.Err()
default:
}
if _, err = c.stdinWriter.Write([]byte(path + "\x00")); err != nil {
defer c.Close()
return nil, err
}
reportTimeout := func() error {
stdOutClosed := false
select {
case <-c.stdOut.closed:
stdOutClosed = true
default:
}
debugMsg := fmt.Sprintf("check path %q in repo %q", path, filepath.Base(c.Repo.Path))
debugMsg += fmt.Sprintf(", stdOut: tmp=%q, pos=%d, closed=%v", string(c.stdOut.tmp), c.stdOut.pos, stdOutClosed)
if c.cmd.cmd != nil {
debugMsg += fmt.Sprintf(", process state: %q", c.cmd.cmd.ProcessState.String())
}
_ = c.Close()
return fmt.Errorf("CheckPath timeout: %s", debugMsg)
}
rs = make(map[string]string)
for range c.Attributes {
select {
case <-time.After(5 * time.Second):
// There is a strange "hang" problem in gitdiff.GetDiff -> CheckPath
// So add a timeout here to mitigate the problem, and output more logs for debug purpose
// In real world, if CheckPath runs long than seconds, it blocks the end user's operation,
// and at the moment the CheckPath result is not so important, so we can just ignore it.
return nil, reportTimeout()
case attr, ok := <-c.stdOut.ReadAttribute():
if !ok {
return nil, c.ctx.Err()
}
rs[attr.Attribute] = attr.Value
case <-c.ctx.Done():
return nil, c.ctx.Err()
}
}
return rs, nil
}
func (c *CheckAttributeReader) Close() error {
c.cancel()
err := c.stdinWriter.Close()
return err
}
type attributeTriple struct {
Filename string
Attribute string
Value string
}
type nulSeparatedAttributeWriter struct {
tmp []byte
attributes chan attributeTriple
closed chan struct{}
working attributeTriple
pos int
}
func (wr *nulSeparatedAttributeWriter) Write(p []byte) (n int, err error) {
l, read := len(p), 0
nulIdx := bytes.IndexByte(p, '\x00')
for nulIdx >= 0 {
wr.tmp = append(wr.tmp, p[:nulIdx]...)
switch wr.pos {
case 0:
wr.working = attributeTriple{
Filename: string(wr.tmp),
}
case 1:
wr.working.Attribute = string(wr.tmp)
case 2:
wr.working.Value = string(wr.tmp)
}
wr.tmp = wr.tmp[:0]
wr.pos++
if wr.pos > 2 {
wr.attributes <- wr.working
wr.pos = 0
}
read += nulIdx + 1
if l > read {
p = p[nulIdx+1:]
nulIdx = bytes.IndexByte(p, '\x00')
} else {
return l, nil
}
}
wr.tmp = append(wr.tmp, p...)
return l, nil
}
func (wr *nulSeparatedAttributeWriter) ReadAttribute() <-chan attributeTriple {
return wr.attributes
}
func (wr *nulSeparatedAttributeWriter) Close() error {
select {
case <-wr.closed:
return nil
default:
}
close(wr.attributes)
close(wr.closed)
return nil
}
// CheckAttributeReader creates a check attribute reader for the current repository and provided commit ID
func (repo *Repository) CheckAttributeReader(commitID string) (*CheckAttributeReader, context.CancelFunc) {
indexFilename, worktree, deleteTemporaryFile, err := repo.ReadTreeToTemporaryIndex(commitID)
if err != nil {
return nil, func() {}
}
checker := &CheckAttributeReader{
Attributes: []string{
AttributeLinguistVendored,
AttributeLinguistGenerated,
AttributeLinguistDocumentation,
AttributeLinguistDetectable,
AttributeLinguistLanguage,
AttributeGitlabLanguage,
},
Repo: repo,
IndexFile: indexFilename,
WorkTree: worktree,
}
ctx, cancel := context.WithCancel(repo.Ctx)
if err := checker.Init(ctx); err != nil {
log.Error("Unable to open attribute checker for commit %s, error: %v", commitID, err)
} else {
go func() {
err := checker.Run()
if err != nil && !IsErrCanceledOrKilled(err) {
log.Error("Attribute checker for commit %s exits with error: %v", commitID, err)
}
cancel()
}()
}
deferrable := func() {
_ = checker.Close()
cancel()
deleteTemporaryFile()
}
return checker, deferrable
}

View File

@ -10,8 +10,7 @@ import (
"path/filepath"
"strings"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/setting"
)
// ReadTreeToIndex reads a treeish to the index
@ -59,26 +58,18 @@ func (repo *Repository) ReadTreeToTemporaryIndex(treeish string) (tmpIndexFilena
}
}()
removeDirFn := func(dir string) func() { // it can't use the return value "tmpDir" directly because it is empty when error occurs
return func() {
if err := util.RemoveAll(dir); err != nil {
log.Error("failed to remove tmp index dir: %v", err)
}
}
}
tmpDir, err = os.MkdirTemp("", "index")
tmpDir, cancel, err = setting.AppDataTempDir("git-repo-content").MkdirTempRandom("index")
if err != nil {
return "", "", nil, err
}
tmpIndexFilename = filepath.Join(tmpDir, ".tmp-index")
cancel = removeDirFn(tmpDir)
err = repo.ReadTreeToIndex(treeish, tmpIndexFilename)
if err != nil {
return "", "", cancel, err
}
return tmpIndexFilename, tmpDir, cancel, err
return tmpIndexFilename, tmpDir, cancel, nil
}
// EmptyIndex empties the index

View File

@ -1,5 +1,5 @@
[core]
repositoryformatversion = 0
filemode = true
bare = false
bare = true
logallrefupdates = true

View File

@ -1,7 +1,7 @@
[core]
repositoryformatversion = 0
filemode = false
bare = false
bare = true
logallrefupdates = true
symlinks = false
ignorecase = true

View File

@ -1,7 +1,7 @@
[core]
repositoryformatversion = 0
filemode = false
bare = false
bare = true
logallrefupdates = true
symlinks = false
ignorecase = true

View File

@ -6,6 +6,7 @@ package db
import (
"context"
"strings"
"sync"
"code.gitea.io/gitea/models/db"
issue_model "code.gitea.io/gitea/models/issues"
@ -18,7 +19,7 @@ import (
"xorm.io/builder"
)
var _ internal.Indexer = &Indexer{}
var _ internal.Indexer = (*Indexer)(nil)
// Indexer implements Indexer interface to use database's like search
type Indexer struct {
@ -29,11 +30,9 @@ func (i *Indexer) SupportedSearchModes() []indexer.SearchMode {
return indexer.SearchModesExactWords()
}
func NewIndexer() *Indexer {
return &Indexer{
Indexer: &inner_db.Indexer{},
}
}
var GetIndexer = sync.OnceValue(func() *Indexer {
return &Indexer{Indexer: &inner_db.Indexer{}}
})
// Index dummy function
func (i *Indexer) Index(_ context.Context, _ ...*internal.IndexerData) error {
@ -122,7 +121,11 @@ func (i *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
}, nil
}
ids, total, err := issue_model.IssueIDs(ctx, opt, cond)
return i.FindWithIssueOptions(ctx, opt, cond)
}
func (i *Indexer) FindWithIssueOptions(ctx context.Context, opt *issue_model.IssuesOptions, otherConds ...builder.Cond) (*internal.SearchResult, error) {
ids, total, err := issue_model.IssueIDs(ctx, opt, otherConds...)
if err != nil {
return nil, err
}

View File

@ -6,6 +6,7 @@ package db
import (
"context"
"fmt"
"strings"
"code.gitea.io/gitea/models/db"
issue_model "code.gitea.io/gitea/models/issues"
@ -34,7 +35,11 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
case internal.SortByDeadlineAsc:
sortType = "nearduedate"
default:
sortType = "newest"
if strings.HasPrefix(string(options.SortBy), issue_model.ScopeSortPrefix) {
sortType = string(options.SortBy)
} else {
sortType = "newest"
}
}
// See the comment of issues_model.SearchOptions for the reason why we need to convert
@ -68,7 +73,6 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
ExcludedLabelNames: nil,
IncludeMilestones: nil,
SortType: sortType,
IssueIDs: nil,
UpdatedAfterUnix: options.UpdatedAfterUnix.Value(),
UpdatedBeforeUnix: options.UpdatedBeforeUnix.Value(),
PriorityRepoID: 0,

View File

@ -4,12 +4,19 @@
package issues
import (
"strings"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/indexer/issues/internal"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
)
func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOptions {
if opts.IssueIDs != nil {
setting.PanicInDevOrTesting("Indexer SearchOptions doesn't support IssueIDs")
}
searchOpt := &SearchOptions{
Keyword: keyword,
RepoIDs: opts.RepoIDs,
@ -95,7 +102,11 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
// Unsupported sort type for search
fallthrough
default:
searchOpt.SortBy = SortByUpdatedDesc
if strings.HasPrefix(opts.SortType, issues_model.ScopeSortPrefix) {
searchOpt.SortBy = internal.SortBy(opts.SortType)
} else {
searchOpt.SortBy = SortByUpdatedDesc
}
}
return searchOpt

View File

@ -103,7 +103,7 @@ func InitIssueIndexer(syncReindex bool) {
log.Fatal("Unable to issueIndexer.Init with connection %s Error: %v", setting.Indexer.IssueConnStr, err)
}
case "db":
issueIndexer = db.NewIndexer()
issueIndexer = db.GetIndexer()
case "meilisearch":
issueIndexer = meilisearch.NewIndexer(setting.Indexer.IssueConnStr, setting.Indexer.IssueConnAuth, setting.Indexer.IssueIndexerName)
existed, err = issueIndexer.Init(ctx)
@ -291,19 +291,22 @@ func SearchIssues(ctx context.Context, opts *SearchOptions) ([]int64, int64, err
// So if the user creates an issue and list issues immediately, the issue may not be listed because the indexer needs time to index the issue.
// Even worse, the external indexer like elastic search may not be available for a while,
// and the user may not be able to list issues completely until it is available again.
ix = db.NewIndexer()
ix = db.GetIndexer()
}
result, err := ix.Search(ctx, opts)
if err != nil {
return nil, 0, err
}
return SearchResultToIDSlice(result), result.Total, nil
}
func SearchResultToIDSlice(result *internal.SearchResult) []int64 {
ret := make([]int64, 0, len(result.Hits))
for _, hit := range result.Hits {
ret = append(ret, hit.ID)
}
return ret, result.Total, nil
return ret
}
// CountIssues counts issues by options. It is a shortcut of SearchIssues(ctx, opts) but only returns the total count.

View File

@ -8,6 +8,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/languagestats"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
@ -62,7 +63,7 @@ func (db *DBIndexer) Index(id int64) error {
}
// Calculate and save language statistics to database
stats, err := gitRepo.GetLanguageStats(commitID)
stats, err := languagestats.GetLanguageStats(gitRepo, commitID)
if err != nil {
if !setting.IsInTesting {
log.Error("Unable to get language stats for ID %s for default branch %s in %s. Error: %v", commitID, repo.DefaultBranch, repo.FullName(), err)

View File

@ -14,10 +14,11 @@ var colorPattern = regexp.MustCompile("^#?(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})$")
// Label represents label information loaded from template
type Label struct {
Name string `yaml:"name"`
Color string `yaml:"color"`
Description string `yaml:"description,omitempty"`
Exclusive bool `yaml:"exclusive,omitempty"`
Name string `yaml:"name"`
Color string `yaml:"color"`
Description string `yaml:"description,omitempty"`
Exclusive bool `yaml:"exclusive,omitempty"`
ExclusiveOrder int `yaml:"exclusive_order,omitempty"`
}
// NormalizeColor normalizes a color string to a 6-character hex code

View File

@ -12,11 +12,9 @@ import (
"runtime"
"strings"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)
// RegisterRenderers registers all supported third part renderers according settings
@ -88,16 +86,11 @@ func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.
if p.IsInputFile {
// write to temp file
f, err := os.CreateTemp("", "gitea_input")
f, cleanup, err := setting.AppDataTempDir("git-repo-content").CreateTempFileRandom("gitea_input")
if err != nil {
return fmt.Errorf("%s create temp file when rendering %s failed: %w", p.Name(), p.Command, err)
}
tmpPath := f.Name()
defer func() {
if err := util.Remove(tmpPath); err != nil {
log.Warn("Unable to remove temporary file: %s: Error: %v", tmpPath, err)
}
}()
defer cleanup()
_, err = io.Copy(f, input)
if err != nil {

View File

@ -6,6 +6,7 @@ package packages
import (
"io"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util/filebuffer"
)
@ -34,11 +35,11 @@ func NewHashedBuffer() (*HashedBuffer, error) {
// NewHashedBufferWithSize creates a hashed buffer with a specific memory size
func NewHashedBufferWithSize(maxMemorySize int) (*HashedBuffer, error) {
b, err := filebuffer.New(maxMemorySize)
tempDir, err := setting.AppDataTempDir("package-hashed-buffer").MkdirAllSub("")
if err != nil {
return nil, err
}
b := filebuffer.New(maxMemorySize, tempDir)
hash := NewMultiHasher()
combinedWriter := io.MultiWriter(b, hash)

View File

@ -9,10 +9,13 @@ import (
"strings"
"testing"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
)
func TestHashedBuffer(t *testing.T) {
setting.AppDataPath = t.TempDir()
cases := []struct {
MaxMemorySize int
Data string

View File

@ -9,6 +9,8 @@ import (
"encoding/base64"
"testing"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
)
@ -17,6 +19,7 @@ fgAA3AEAAAQAAAAjU3RyaW5ncwAAAADgAQAABAAAACNVUwDkAQAAMAAAACNHVUlEAAAAFAIAACgB
AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`
func TestExtractPortablePdb(t *testing.T) {
setting.AppDataPath = t.TempDir()
createArchive := func(name string, content []byte) []byte {
var buf bytes.Buffer
archive := zip.NewWriter(&buf)

View File

@ -13,6 +13,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/cachegroup"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
@ -131,7 +132,7 @@ func (pc *PushCommits) ToAPIPayloadCommits(ctx context.Context, repo *repo_model
func (pc *PushCommits) AvatarLink(ctx context.Context, email string) string {
size := avatars.DefaultAvatarPixelSize * setting.Avatar.RenderedSizeFactor
v, _ := cache.GetWithContextCache(ctx, "push_commits", email, func() (string, error) {
v, _ := cache.GetWithContextCache(ctx, cachegroup.EmailAvatarLink, email, func(ctx context.Context, email string) (string, error) {
u, err := user_model.GetUserByEmail(ctx, email)
if err != nil {
if !user_model.IsErrUserNotExist(err) {

View File

@ -127,10 +127,11 @@ func InitializeLabels(ctx context.Context, id int64, labelTemplate string, isOrg
labels := make([]*issues_model.Label, len(list))
for i := 0; i < len(list); i++ {
labels[i] = &issues_model.Label{
Name: list[i].Name,
Exclusive: list[i].Exclusive,
Description: list[i].Description,
Color: list[i].Color,
Name: list[i].Name,
Exclusive: list[i].Exclusive,
ExclusiveOrder: list[i].ExclusiveOrder,
Description: list[i].Description,
Color: list[i].Color,
}
if isOrg {
labels[i].OrgID = id

View File

@ -4,41 +4,19 @@
package repository
import (
"context"
"fmt"
"os"
"path/filepath"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)
// LocalCopyPath returns the local repository temporary copy path.
func LocalCopyPath() string {
if filepath.IsAbs(setting.Repository.Local.LocalCopyPath) {
return setting.Repository.Local.LocalCopyPath
}
return filepath.Join(setting.AppDataPath, setting.Repository.Local.LocalCopyPath)
}
// CreateTemporaryPath creates a temporary path
func CreateTemporaryPath(prefix string) (string, error) {
if err := os.MkdirAll(LocalCopyPath(), os.ModePerm); err != nil {
log.Error("Unable to create localcopypath directory: %s (%v)", LocalCopyPath(), err)
return "", fmt.Errorf("Failed to create localcopypath directory %s: %w", LocalCopyPath(), err)
}
basePath, err := os.MkdirTemp(LocalCopyPath(), prefix+".git")
func CreateTemporaryPath(prefix string) (string, context.CancelFunc, error) {
basePath, cleanup, err := setting.AppDataTempDir("local-repo").MkdirTempRandom(prefix + ".git")
if err != nil {
log.Error("Unable to create temporary directory: %s-*.git (%v)", prefix, err)
return "", fmt.Errorf("Failed to create dir %s-*.git: %w", prefix, err)
return "", nil, fmt.Errorf("failed to create dir %s-*.git: %w", prefix, err)
}
return basePath, nil
}
// RemoveTemporaryPath removes the temporary path
func RemoveTemporaryPath(basePath string) error {
if _, err := os.Stat(basePath); !os.IsNotExist(err) {
return util.RemoveAll(basePath)
}
return nil
return basePath, cleanup, nil
}

View File

@ -127,7 +127,7 @@ func loadMarkupFrom(rootCfg ConfigProvider) {
}
}
MermaidMaxSourceCharacters = rootCfg.Section("markup").Key("MERMAID_MAX_SOURCE_CHARACTERS").MustInt(5000)
MermaidMaxSourceCharacters = rootCfg.Section("markup").Key("MERMAID_MAX_SOURCE_CHARACTERS").MustInt(50000)
ExternalMarkupRenderers = make([]*MarkupRenderer, 0, 10)
ExternalSanitizerRules = make([]MarkupSanitizerRule, 0, 10)

View File

@ -6,8 +6,6 @@ package setting
import (
"fmt"
"math"
"os"
"path/filepath"
"github.com/dustin/go-humanize"
)
@ -67,14 +65,10 @@ func loadPackagesFrom(rootCfg ConfigProvider) (err error) {
return err
}
Packages.ChunkedUploadPath = filepath.ToSlash(sec.Key("CHUNKED_UPLOAD_PATH").MustString("tmp/package-upload"))
if !filepath.IsAbs(Packages.ChunkedUploadPath) {
Packages.ChunkedUploadPath = filepath.ToSlash(filepath.Join(AppDataPath, Packages.ChunkedUploadPath))
}
if HasInstallLock(rootCfg) {
if err := os.MkdirAll(Packages.ChunkedUploadPath, os.ModePerm); err != nil {
return fmt.Errorf("unable to create chunked upload directory: %s (%v)", Packages.ChunkedUploadPath, err)
Packages.ChunkedUploadPath, err = AppDataTempDir("package-upload").MkdirAllSub("")
if err != nil {
return fmt.Errorf("unable to create chunked upload directory: %w", err)
}
}

View File

@ -11,6 +11,7 @@ import (
"strings"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/tempdir"
)
var (
@ -196,3 +197,18 @@ func InitWorkPathAndCfgProvider(getEnvFn func(name string) string, args ArgWorkP
CustomPath = tmpCustomPath.Value
CustomConf = tmpCustomConf.Value
}
// AppDataTempDir returns a managed temporary directory for the application data.
// Using empty sub will get the managed base temp directory, and it's safe to delete it.
// Gitea only creates subdirectories under it, but not the APP_TEMP_PATH directory itself.
// * When APP_TEMP_PATH="/tmp": the managed temp directory is "/tmp/gitea-tmp"
// * When APP_TEMP_PATH is not set: the managed temp directory is "/{APP_DATA_PATH}/tmp"
func AppDataTempDir(sub string) *tempdir.TempDir {
if appTempPathInternal != "" {
return tempdir.New(appTempPathInternal, "gitea-tmp/"+sub)
}
if AppDataPath == "" {
panic("setting.AppDataPath is not set")
}
return tempdir.New(AppDataPath, "tmp/"+sub)
}

View File

@ -62,17 +62,11 @@ var (
// Repository upload settings
Upload struct {
Enabled bool
TempPath string
AllowedTypes string
FileMaxSize int64
MaxFiles int
} `ini:"-"`
// Repository local settings
Local struct {
LocalCopyPath string
} `ini:"-"`
// Pull request settings
PullRequest struct {
WorkInProgressPrefixes []string
@ -181,25 +175,16 @@ var (
// Repository upload settings
Upload: struct {
Enabled bool
TempPath string
AllowedTypes string
FileMaxSize int64
MaxFiles int
}{
Enabled: true,
TempPath: "data/tmp/uploads",
AllowedTypes: "",
FileMaxSize: 50,
MaxFiles: 5,
},
// Repository local settings
Local: struct {
LocalCopyPath string
}{
LocalCopyPath: "tmp/local-repo",
},
// Pull request settings
PullRequest: struct {
WorkInProgressPrefixes []string
@ -308,8 +293,6 @@ func loadRepositoryFrom(rootCfg ConfigProvider) {
log.Fatal("Failed to map Repository.Editor settings: %v", err)
} else if err = rootCfg.Section("repository.upload").MapTo(&Repository.Upload); err != nil {
log.Fatal("Failed to map Repository.Upload settings: %v", err)
} else if err = rootCfg.Section("repository.local").MapTo(&Repository.Local); err != nil {
log.Fatal("Failed to map Repository.Local settings: %v", err)
} else if err = rootCfg.Section("repository.pull-request").MapTo(&Repository.PullRequest); err != nil {
log.Fatal("Failed to map Repository.PullRequest settings: %v", err)
}
@ -361,10 +344,6 @@ func loadRepositoryFrom(rootCfg ConfigProvider) {
}
}
if !filepath.IsAbs(Repository.Upload.TempPath) {
Repository.Upload.TempPath = filepath.Join(AppWorkPath, Repository.Upload.TempPath)
}
if err := loadRepoArchiveFrom(rootCfg); err != nil {
log.Fatal("loadRepoArchiveFrom: %v", err)
}

View File

@ -7,6 +7,7 @@ import (
"encoding/base64"
"net"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
@ -59,6 +60,8 @@ var (
// AssetVersion holds a opaque value that is used for cache-busting assets
AssetVersion string
appTempPathInternal string // the temporary path for the app, it is only an internal variable, do not use it, always use AppDataTempDir
Protocol Scheme
UseProxyProtocol bool // `ini:"USE_PROXY_PROTOCOL"`
ProxyProtocolTLSBridging bool //`ini:"PROXY_PROTOCOL_TLS_BRIDGING"`
@ -330,6 +333,19 @@ func loadServerFrom(rootCfg ConfigProvider) {
if !filepath.IsAbs(AppDataPath) {
AppDataPath = filepath.ToSlash(filepath.Join(AppWorkPath, AppDataPath))
}
if IsInTesting && HasInstallLock(rootCfg) {
// FIXME: in testing, the "app data" directory is not correctly initialized before loading settings
if _, err := os.Stat(AppDataPath); err != nil {
_ = os.MkdirAll(AppDataPath, os.ModePerm)
}
}
appTempPathInternal = sec.Key("APP_TEMP_PATH").String()
if appTempPathInternal != "" {
if _, err := os.Stat(appTempPathInternal); err != nil {
log.Fatal("APP_TEMP_PATH %q is not accessible: %v", appTempPathInternal, err)
}
}
EnableGzip = sec.Key("ENABLE_GZIP").MustBool()
EnablePprof = sec.Key("ENABLE_PPROF").MustBool(false)

View File

@ -4,7 +4,6 @@
package setting
import (
"os"
"path/filepath"
"strings"
"text/template"
@ -31,8 +30,6 @@ var SSH = struct {
ServerKeyExchanges []string `ini:"SSH_SERVER_KEY_EXCHANGES"`
ServerMACs []string `ini:"SSH_SERVER_MACS"`
ServerHostKeys []string `ini:"SSH_SERVER_HOST_KEYS"`
KeyTestPath string `ini:"SSH_KEY_TEST_PATH"`
KeygenPath string `ini:"SSH_KEYGEN_PATH"`
AuthorizedKeysBackup bool `ini:"SSH_AUTHORIZED_KEYS_BACKUP"`
AuthorizedPrincipalsBackup bool `ini:"SSH_AUTHORIZED_PRINCIPALS_BACKUP"`
AuthorizedKeysCommandTemplate string `ini:"SSH_AUTHORIZED_KEYS_COMMAND_TEMPLATE"`
@ -57,7 +54,6 @@ var SSH = struct {
ServerCiphers: []string{"chacha20-poly1305@openssh.com", "aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-gcm@openssh.com", "aes256-gcm@openssh.com"},
ServerKeyExchanges: []string{"curve25519-sha256", "ecdh-sha2-nistp256", "ecdh-sha2-nistp384", "ecdh-sha2-nistp521", "diffie-hellman-group14-sha256", "diffie-hellman-group14-sha1"},
ServerMACs: []string{"hmac-sha2-256-etm@openssh.com", "hmac-sha2-256", "hmac-sha1"},
KeygenPath: "",
MinimumKeySizeCheck: true,
MinimumKeySizes: map[string]int{"ed25519": 256, "ed25519-sk": 256, "ecdsa": 256, "ecdsa-sk": 256, "rsa": 3071},
ServerHostKeys: []string{"ssh/gitea.rsa", "ssh/gogs.rsa"},
@ -123,7 +119,6 @@ func loadSSHFrom(rootCfg ConfigProvider) {
if len(serverMACs) > 0 {
SSH.ServerMACs = serverMACs
}
SSH.KeyTestPath = os.TempDir()
if err = sec.MapTo(&SSH); err != nil {
log.Fatal("Failed to map SSH settings: %v", err)
}
@ -133,7 +128,6 @@ func loadSSHFrom(rootCfg ConfigProvider) {
}
}
SSH.KeygenPath = sec.Key("SSH_KEYGEN_PATH").String()
SSH.Port = sec.Key("SSH_PORT").MustInt(22)
SSH.ListenPort = sec.Key("SSH_LISTEN_PORT").MustInt(SSH.Port)
SSH.UseProxyProtocol = sec.Key("SSH_SERVER_USE_PROXY_PROTOCOL").MustBool(false)

View File

@ -32,11 +32,6 @@ func Init() error {
builtinUnused()
// FIXME: why 0o644 for a directory .....
if err := os.MkdirAll(setting.SSH.KeyTestPath, 0o644); err != nil {
return fmt.Errorf("failed to create directory %q for ssh key test: %w", setting.SSH.KeyTestPath, err)
}
if len(setting.SSH.TrustedUserCAKeys) > 0 && setting.SSH.AuthorizedPrincipalsEnabled {
caKeysFileName := setting.SSH.TrustedUserCAKeysFile
caKeysFileDir := filepath.Dir(caKeysFileName)

112
modules/tempdir/tempdir.go Normal file
View File

@ -0,0 +1,112 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package tempdir
import (
"os"
"path/filepath"
"time"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
)
type TempDir struct {
// base is the base directory for temporary files, it must exist before accessing and won't be created automatically.
// for example: base="/system-tmpdir", sub="gitea-tmp"
base, sub string
}
func (td *TempDir) JoinPath(elems ...string) string {
return filepath.Join(append([]string{td.base, td.sub}, elems...)...)
}
// MkdirAllSub works like os.MkdirAll, but the base directory must exist
func (td *TempDir) MkdirAllSub(dir string) (string, error) {
if _, err := os.Stat(td.base); err != nil {
return "", err
}
full := filepath.Join(td.base, td.sub, dir)
if err := os.MkdirAll(full, os.ModePerm); err != nil {
return "", err
}
return full, nil
}
func (td *TempDir) prepareDirWithPattern(elems ...string) (dir, pattern string, err error) {
if _, err = os.Stat(td.base); err != nil {
return "", "", err
}
dir, pattern = filepath.Split(filepath.Join(append([]string{td.base, td.sub}, elems...)...))
if err = os.MkdirAll(dir, os.ModePerm); err != nil {
return "", "", err
}
return dir, pattern, nil
}
// MkdirTempRandom works like os.MkdirTemp, the last path field is the "pattern"
func (td *TempDir) MkdirTempRandom(elems ...string) (string, func(), error) {
dir, pattern, err := td.prepareDirWithPattern(elems...)
if err != nil {
return "", nil, err
}
dir, err = os.MkdirTemp(dir, pattern)
if err != nil {
return "", nil, err
}
return dir, func() {
if err := util.RemoveAll(dir); err != nil {
log.Error("Failed to remove temp directory %s: %v", dir, err)
}
}, nil
}
// CreateTempFileRandom works like os.CreateTemp, the last path field is the "pattern"
func (td *TempDir) CreateTempFileRandom(elems ...string) (*os.File, func(), error) {
dir, pattern, err := td.prepareDirWithPattern(elems...)
if err != nil {
return nil, nil, err
}
f, err := os.CreateTemp(dir, pattern)
if err != nil {
return nil, nil, err
}
filename := f.Name()
return f, func() {
_ = f.Close()
if err := util.Remove(filename); err != nil {
log.Error("Unable to remove temporary file: %s: Error: %v", filename, err)
}
}, err
}
func (td *TempDir) RemoveOutdated(d time.Duration) {
var remove func(path string)
remove = func(path string) {
entries, _ := os.ReadDir(path)
for _, entry := range entries {
full := filepath.Join(path, entry.Name())
if entry.IsDir() {
remove(full)
_ = os.Remove(full)
continue
}
info, err := entry.Info()
if err == nil && time.Since(info.ModTime()) > d {
_ = os.Remove(full)
}
}
}
remove(td.JoinPath(""))
}
// New create a new TempDir instance, "base" must be an existing directory,
// "sub" could be a multi-level directory and will be created if not exist
func New(base, sub string) *TempDir {
return &TempDir{base: base, sub: sub}
}
func OsTempDir(sub string) *TempDir {
return New(os.TempDir(), sub)
}

View File

@ -0,0 +1,75 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package tempdir
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestTempDir(t *testing.T) {
base := t.TempDir()
t.Run("Create", func(t *testing.T) {
td := New(base, "sub1/sub2") // make sure the sub dir supports "/" in the path
assert.Equal(t, filepath.Join(base, "sub1", "sub2"), td.JoinPath())
assert.Equal(t, filepath.Join(base, "sub1", "sub2/test"), td.JoinPath("test"))
t.Run("MkdirTempRandom", func(t *testing.T) {
s, cleanup, err := td.MkdirTempRandom("foo")
assert.NoError(t, err)
assert.True(t, strings.HasPrefix(s, filepath.Join(base, "sub1/sub2", "foo")))
_, err = os.Stat(s)
assert.NoError(t, err)
cleanup()
_, err = os.Stat(s)
assert.ErrorIs(t, err, os.ErrNotExist)
})
t.Run("CreateTempFileRandom", func(t *testing.T) {
f, cleanup, err := td.CreateTempFileRandom("foo", "bar")
filename := f.Name()
assert.NoError(t, err)
assert.True(t, strings.HasPrefix(filename, filepath.Join(base, "sub1/sub2", "foo", "bar")))
_, err = os.Stat(filename)
assert.NoError(t, err)
cleanup()
_, err = os.Stat(filename)
assert.ErrorIs(t, err, os.ErrNotExist)
})
t.Run("RemoveOutDated", func(t *testing.T) {
fa1, _, err := td.CreateTempFileRandom("dir-a", "f1")
assert.NoError(t, err)
fa2, _, err := td.CreateTempFileRandom("dir-a", "f2")
assert.NoError(t, err)
_ = os.Chtimes(fa2.Name(), time.Now().Add(-time.Hour), time.Now().Add(-time.Hour))
fb1, _, err := td.CreateTempFileRandom("dir-b", "f1")
assert.NoError(t, err)
_ = os.Chtimes(fb1.Name(), time.Now().Add(-time.Hour), time.Now().Add(-time.Hour))
_, _, _ = fa1.Close(), fa2.Close(), fb1.Close()
td.RemoveOutdated(time.Minute)
_, err = os.Stat(fa1.Name())
assert.NoError(t, err)
_, err = os.Stat(fa2.Name())
assert.ErrorIs(t, err, os.ErrNotExist)
_, err = os.Stat(fb1.Name())
assert.ErrorIs(t, err, os.ErrNotExist)
})
})
t.Run("BaseNotExist", func(t *testing.T) {
td := New(filepath.Join(base, "not-exist"), "sub")
_, _, err := td.MkdirTempRandom("foo")
assert.ErrorIs(t, err, os.ErrNotExist)
})
}

View File

@ -170,13 +170,28 @@ func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML {
itemColor := "#" + hex.EncodeToString(itemBytes)
scopeColor := "#" + hex.EncodeToString(scopeBytes)
if label.ExclusiveOrder > 0 {
// <scope> | <label> | <order>
return htmlutil.HTMLFormat(`<span class="ui label %s scope-parent" data-tooltip-content title="%s">`+
`<div class="ui label scope-left" style="color: %s !important; background-color: %s !important">%s</div>`+
`<div class="ui label scope-middle" style="color: %s !important; background-color: %s !important">%s</div>`+
`<div class="ui label scope-right">%d</div>`+
`</span>`,
extraCSSClasses, descriptionText,
textColor, scopeColor, scopeHTML,
textColor, itemColor, itemHTML,
label.ExclusiveOrder)
}
// <scope> | <label>
return htmlutil.HTMLFormat(`<span class="ui label %s scope-parent" data-tooltip-content title="%s">`+
`<div class="ui label scope-left" style="color: %s !important; background-color: %s !important">%s</div>`+
`<div class="ui label scope-right" style="color: %s !important; background-color: %s !important">%s</div>`+
`</span>`,
extraCSSClasses, descriptionText,
textColor, scopeColor, scopeHTML,
textColor, itemColor, itemHTML)
textColor, itemColor, itemHTML,
)
}
// RenderEmoji renders html text with emoji post processors

View File

@ -56,7 +56,7 @@ var testMetas = map[string]string{
}
func TestMain(m *testing.M) {
unittest.InitSettings()
unittest.InitSettingsForTesting()
if err := git.InitSimple(context.Background()); err != nil {
log.Fatal("git init failed, err: %v", err)
}

View File

@ -7,16 +7,10 @@ import (
"bytes"
"errors"
"io"
"math"
"os"
)
var (
// ErrInvalidMemorySize occurs if the memory size is not in a valid range
ErrInvalidMemorySize = errors.New("Memory size must be greater 0 and lower math.MaxInt32")
// ErrWriteAfterRead occurs if Write is called after a read operation
ErrWriteAfterRead = errors.New("Write is unsupported after a read operation")
)
var ErrWriteAfterRead = errors.New("write is unsupported after a read operation") // occurs if Write is called after a read operation
type readAtSeeker interface {
io.ReadSeeker
@ -30,34 +24,17 @@ type FileBackedBuffer struct {
maxMemorySize int64
size int64
buffer bytes.Buffer
tempDir string
file *os.File
reader readAtSeeker
}
// New creates a file backed buffer with a specific maximum memory size
func New(maxMemorySize int) (*FileBackedBuffer, error) {
if maxMemorySize < 0 || maxMemorySize > math.MaxInt32 {
return nil, ErrInvalidMemorySize
}
func New(maxMemorySize int, tempDir string) *FileBackedBuffer {
return &FileBackedBuffer{
maxMemorySize: int64(maxMemorySize),
}, nil
}
// CreateFromReader creates a file backed buffer and copies the provided reader data into it.
func CreateFromReader(r io.Reader, maxMemorySize int) (*FileBackedBuffer, error) {
b, err := New(maxMemorySize)
if err != nil {
return nil, err
tempDir: tempDir,
}
_, err = io.Copy(b, r)
if err != nil {
return nil, err
}
return b, nil
}
// Write implements io.Writer
@ -73,7 +50,7 @@ func (b *FileBackedBuffer) Write(p []byte) (int, error) {
n, err = b.file.Write(p)
} else {
if b.size+int64(len(p)) > b.maxMemorySize {
b.file, err = os.CreateTemp("", "gitea-buffer-")
b.file, err = os.CreateTemp(b.tempDir, "gitea-buffer-")
if err != nil {
return 0, err
}
@ -148,7 +125,7 @@ func (b *FileBackedBuffer) Seek(offset int64, whence int) (int64, error) {
func (b *FileBackedBuffer) Close() error {
if b.file != nil {
err := b.file.Close()
os.Remove(b.file.Name())
_ = os.Remove(b.file.Name())
b.file = nil
return err
}

View File

@ -21,7 +21,8 @@ func TestFileBackedBuffer(t *testing.T) {
}
for _, c := range cases {
buf, err := CreateFromReader(strings.NewReader(c.Data), c.MaxMemorySize)
buf := New(c.MaxMemorySize, t.TempDir())
_, err := io.Copy(buf, strings.NewReader(c.Data))
assert.NoError(t, err)
assert.EqualValues(t, len(c.Data), buf.Size())

View File

@ -22,49 +22,60 @@ labels:
description: Breaking change that won't be backward compatible
- name: "Reviewed/Duplicate"
exclusive: true
exclusive_order: 2
color: 616161
description: This issue or pull request already exists
- name: "Reviewed/Invalid"
exclusive: true
exclusive_order: 3
color: 546e7a
description: Invalid issue
- name: "Reviewed/Confirmed"
exclusive: true
exclusive_order: 1
color: 795548
description: Issue has been confirmed
- name: "Reviewed/Won't Fix"
exclusive: true
exclusive_order: 3
color: eeeeee
description: This issue won't be fixed
- name: "Status/Need More Info"
exclusive: true
exclusive_order: 2
color: 424242
description: Feedback is required to reproduce issue or to continue work
- name: "Status/Blocked"
exclusive: true
exclusive_order: 1
color: 880e4f
description: Something is blocking this issue or pull request
- name: "Status/Abandoned"
exclusive: true
exclusive_order: 3
color: "222222"
description: Somebody has started to work on this but abandoned work
- name: "Priority/Critical"
exclusive: true
exclusive_order: 1
color: b71c1c
description: The priority is critical
priority: critical
- name: "Priority/High"
exclusive: true
exclusive_order: 2
color: d32f2f
description: The priority is high
priority: high
- name: "Priority/Medium"
exclusive: true
exclusive_order: 3
color: e64a19
description: The priority is medium
priority: medium
- name: "Priority/Low"
exclusive: true
exclusive_order: 4
color: 4caf50
description: The priority is low
priority: low

View File

@ -3238,8 +3238,6 @@ config.ssh_domain=Doména SSH serveru
config.ssh_port=Port
config.ssh_listen_port=Port pro naslouchání
config.ssh_root_path=Kořenová cesta
config.ssh_key_test_path=Cesta testu klíčů
config.ssh_keygen_path=Cesta ke generátoru klíčů ('ssh-keygen')
config.ssh_minimum_key_size_check=Kontrola minimální velikosti klíčů
config.ssh_minimum_key_sizes=Minimální velikost klíčů

View File

@ -3235,8 +3235,6 @@ config.ssh_domain=SSH-Server-Domain
config.ssh_port=Port
config.ssh_listen_port=Listen-Port
config.ssh_root_path=Wurzelverzeichnis
config.ssh_key_test_path=Schlüssel-Test-Pfad
config.ssh_keygen_path=Keygen-Pfad („ssh-keygen“)
config.ssh_minimum_key_size_check=Prüfung der Mindestschlüssellänge
config.ssh_minimum_key_sizes=Mindestschlüssellängen

View File

@ -2927,8 +2927,6 @@ config.ssh_domain=Domain Διακομιστή SSH
config.ssh_port=Θύρα
config.ssh_listen_port=Θύρα Ακρόασης
config.ssh_root_path=Ριζική Διαδρομή
config.ssh_key_test_path=Διαδρομή Δοκιμής Κλειδιού
config.ssh_keygen_path=Διαδρομή Keygen ('ssh-keygen')
config.ssh_minimum_key_size_check=Έλεγχος Ελάχιστου Μεγέθους Κλειδιού
config.ssh_minimum_key_sizes=Ελάχιστα Μεγέθη Κλειδιών

View File

@ -1655,6 +1655,8 @@ issues.label_archived_filter = Show archived labels
issues.label_archive_tooltip = Archived labels are excluded by default from the suggestions when searching by label.
issues.label_exclusive_desc = Name the label <code>scope/item</code> to make it mutually exclusive with other <code>scope/</code> labels.
issues.label_exclusive_warning = Any conflicting scoped labels will be removed when editing the labels of an issue or pull request.
issues.label_exclusive_order = Sort Order
issues.label_exclusive_order_tooltip = Exclusive labels in the same scope will be sorted according to this numeric order.
issues.label_count = %d labels
issues.label_open_issues = %d open issues/pull requests
issues.label_edit = Edit
@ -3287,8 +3289,6 @@ config.ssh_domain = SSH Server Domain
config.ssh_port = Port
config.ssh_listen_port = Listen Port
config.ssh_root_path = Root Path
config.ssh_key_test_path = Key Test Path
config.ssh_keygen_path = Keygen ('ssh-keygen') Path
config.ssh_minimum_key_size_check = Minimum Key Size Check
config.ssh_minimum_key_sizes = Minimum Key Sizes

View File

@ -2907,8 +2907,6 @@ config.ssh_domain=Dominio del servidor SSH
config.ssh_port=Puerto
config.ssh_listen_port=Puerto de escucha
config.ssh_root_path=Ruta raíz
config.ssh_key_test_path=Ruta de la clave de prueba
config.ssh_keygen_path=Ruta del generador de claves ('ssh-keygen')
config.ssh_minimum_key_size_check=Tamaño mínimo de la clave de verificación
config.ssh_minimum_key_sizes=Tamaños de clave mínimos

View File

@ -2279,8 +2279,6 @@ config.ssh_domain=دامنه سرور SSH
config.ssh_port=درگاه (پورت)
config.ssh_listen_port=گوش دادن به پورت
config.ssh_root_path=مسیر ریشه
config.ssh_key_test_path=مسیر کلید آزمایش
config.ssh_keygen_path=مسیر فایل ssh-keygen
config.ssh_minimum_key_size_check=بررسی حداقل طول کلید
config.ssh_minimum_key_sizes=حداقل اندازه‌ی کلید ها

View File

@ -1538,8 +1538,6 @@ config.ssh_enabled=Käytössä
config.ssh_port=Portti
config.ssh_listen_port=Kuuntele porttia
config.ssh_root_path=Juuren polku
config.ssh_key_test_path=Polku jossa avaimet testataan
config.ssh_keygen_path=Keygen ('ssh-keygen') polku
config.ssh_minimum_key_size_check=Avaimen vähimmäiskoko tarkistus
config.ssh_minimum_key_sizes=Avaimen vähimmäiskoot

View File

@ -730,6 +730,8 @@ public_profile=Profil public
biography_placeholder=Parlez-nous un peu de vous! (Vous pouvez utiliser Markdown)
location_placeholder=Partagez votre position approximative avec d'autres personnes
profile_desc=Contrôlez comment votre profil est affiché aux autres utilisateurs. Votre adresse courriel principale sera utilisée pour les notifications, la récupération de mot de passe et les opérations Git basées sur le Web.
password_username_disabled=Vous nêtes pas autorisé à modifier votre nom dutilisateur. Veuillez contacter ladministrateur de votre site pour plus de détails.
password_full_name_disabled=Vous nêtes pas autorisé à modifier votre nom complet. Veuillez contacter ladministrateur du site pour plus de détails.
full_name=Nom complet
website=Site Web
location=Localisation
@ -924,6 +926,9 @@ permission_not_set=Non défini
permission_no_access=Aucun accès
permission_read=Lecture
permission_write=Lecture et écriture
permission_anonymous_read=Consultation anonyme
permission_everyone_read=Consultation collective
permission_everyone_write=Participation collective
access_token_desc=Les autorisations des jetons sélectionnées se limitent aux <a %s>routes API</a> correspondantes. Lisez la <a %s>documentation</a> pour plus dinformations.
at_least_one_permission=Vous devez sélectionner au moins une permission pour créer un jeton.
permissions_list=Autorisations :
@ -1136,6 +1141,7 @@ transfer.no_permission_to_reject=Vous nêtes pas autorisé à rejeter ce tran
desc.private=Privé
desc.public=Publique
desc.public_access=Accès public
desc.template=Modèle
desc.internal=Interne
desc.archived=Archivé
@ -1648,6 +1654,8 @@ issues.label_archived_filter=Afficher les labels archivés
issues.label_archive_tooltip=Les labels archivés sont par défaut exclus des suggestions lors de la recherche par label.
issues.label_exclusive_desc=Remarque : pour rendre des labels mutuellement exclusifs, préfixez leur nom dune portée au format <code>portée/label</code>.
issues.label_exclusive_warning=Tout label d'une portée en conflit sera retiré lors de la modification des labels dun ticket ou dune demande dajout.
issues.label_exclusive_order=Ordre de tri
issues.label_exclusive_order_tooltip=Les labels exclusifs partageant la même portée seront triées selon cet ordre numérique.
issues.label_count=%d labels
issues.label_open_issues=%d tickets ouverts
issues.label_edit=Éditer
@ -2130,6 +2138,12 @@ contributors.contribution_type.deletions=Suppressions
settings=Paramètres
settings.desc=Les paramètres sont l'endroit où gérer les options du dépôt
settings.options=Dépôt
settings.public_access=Accès public
settings.public_access_desc=Configurer les permissions des visiteurs publics remplaçant les valeurs par défaut de ce dépôt.
settings.public_access.docs.not_set=Non défini : ne donne aucune permission supplémentaire. Les règles du dépôt et les permissions des utilisateurs font foi.
settings.public_access.docs.anonymous_read=Lecture anonyme : les utilisateurs qui ne sont pas connectés peuvent consulter la ressource.
settings.public_access.docs.everyone_read=Consultation publique : tous les utilisateurs connectés peuvent consulter la ressource. Mettre les tickets et demandes dajouts en accès public signifie que les utilisateurs connectés peuvent en créer.
settings.public_access.docs.everyone_write=Participation publique : tous les utilisateurs connectés ont la permission décrire sur la ressource. Seule le Wiki supporte cette autorisation.
settings.collaboration=Collaborateurs
settings.collaboration.admin=Administrateur
settings.collaboration.write=Écriture
@ -3274,8 +3288,6 @@ config.ssh_domain=Domaine du serveur SSH
config.ssh_port=Port
config.ssh_listen_port=Port d'écoute
config.ssh_root_path=Emplacement racine
config.ssh_key_test_path=Chemin de test des clés
config.ssh_keygen_path=Chemin vers le générateur de clés (« ssh-keygen »)
config.ssh_minimum_key_size_check=Vérification de la longueur de clé minimale
config.ssh_minimum_key_sizes=Tailles de clé minimales

View File

@ -3282,8 +3282,6 @@ config.ssh_domain=Fearainn Freastalaí SSH
config.ssh_port=Calafort
config.ssh_listen_port=Éist Calafort
config.ssh_root_path=Cosán Fréimhe
config.ssh_key_test_path=Cosán Tástáil Eochair
config.ssh_keygen_path=Keygen ('ssh-keygen') Cosán
config.ssh_minimum_key_size_check=Seiceáil Íosta Méid Eochair
config.ssh_minimum_key_sizes=Méideanna Íosta Eochrach

View File

@ -1412,8 +1412,6 @@ config.ssh_start_builtin_server=Beépített szerver használata
config.ssh_port=Port
config.ssh_listen_port=Figyelő port
config.ssh_root_path=Gyökérkönyvtár
config.ssh_key_test_path=Kulcs ellenőrzés útvonala
config.ssh_keygen_path=Kulcsgeneráló ('ssh-keygen') elérési útja
config.ssh_minimum_key_size_check=Kulcsok minimum méretének ellenőrzése
config.ssh_minimum_key_sizes=Minimális kulcsok méretek

View File

@ -1219,8 +1219,6 @@ config.ssh_enabled=Aktif
config.ssh_port=Port
config.ssh_listen_port=Listen Port
config.ssh_root_path=Path Induk
config.ssh_key_test_path=Path Key Test
config.ssh_keygen_path=Path Keygen ('ssh-keygen')
config.ssh_minimum_key_size_check=Periksa ukuran kunci minimum
config.ssh_minimum_key_sizes=Ukuran kunci minimum

View File

@ -2464,8 +2464,6 @@ config.ssh_domain=Dominio Server Ssh
config.ssh_port=Porta
config.ssh_listen_port=Porta in ascolto
config.ssh_root_path=Percorso Root
config.ssh_key_test_path=Percorso chiave di test
config.ssh_keygen_path=Percorso Keygen ('ssh-keygen')
config.ssh_minimum_key_size_check=Verifica delle dimensioni minime della chiave
config.ssh_minimum_key_sizes=Dimensioni minime della chiave

View File

@ -3272,8 +3272,6 @@ config.ssh_domain=SSHサーバーのドメイン
config.ssh_port=ポート
config.ssh_listen_port=待受ポート
config.ssh_root_path=ルートパス
config.ssh_key_test_path=キーテストパス
config.ssh_keygen_path=キージェネレータ('ssh-keygen')パス
config.ssh_minimum_key_size_check=最小キー長のチェック
config.ssh_minimum_key_sizes=最小キー長

View File

@ -1372,8 +1372,6 @@ config.ssh_start_builtin_server=빌트-인 서버 사용
config.ssh_port=포트
config.ssh_listen_port=수신 대기 포트
config.ssh_root_path=최상위 경로
config.ssh_key_test_path=주 테스트 경로
config.ssh_keygen_path=키 생성 ('ssh-keygen') 경로
config.ssh_minimum_key_size_check=최소 키 사이즈 검사
config.ssh_minimum_key_sizes=최소 키 사이즈

View File

@ -2930,8 +2930,6 @@ config.ssh_domain=SSH servera domēns
config.ssh_port=Ports
config.ssh_listen_port=Klausīšanās ports
config.ssh_root_path=Saknes ceļš
config.ssh_key_test_path=Atslēgu pārbaudes ceļš
config.ssh_keygen_path=Keygen ('ssh-keygen') ceļš
config.ssh_minimum_key_size_check=Minimālā atslēgas lieluma pārbaude
config.ssh_minimum_key_sizes=Minimālais atslēgas lielums

View File

@ -2310,8 +2310,6 @@ config.ssh_start_builtin_server=Gebruik de ingebouwde server
config.ssh_port=Poort
config.ssh_listen_port=Luister op poort
config.ssh_root_path=Root-pad
config.ssh_key_test_path=Pad voor key-tests
config.ssh_keygen_path=Pad van keygen ('ssh-keygen')
config.ssh_minimum_key_size_check=Controleer minimale key-lengte
config.ssh_minimum_key_sizes=Minimale key-lengtes

View File

@ -2198,8 +2198,6 @@ config.ssh_start_builtin_server=Wykorzystaj wbudowany serwer
config.ssh_port=Port
config.ssh_listen_port=Port nasłuchiwania
config.ssh_root_path=Ścieżka do katalogu głównego
config.ssh_key_test_path=Ścieżka do klucza testowego
config.ssh_keygen_path=Ścieżka do generatora ('ssh-keygen')
config.ssh_minimum_key_size_check=Sprawdzanie minimalnej długości klucza
config.ssh_minimum_key_sizes=Minimalne rozmiary kluczy

View File

@ -2878,8 +2878,6 @@ config.ssh_domain=Domínio do servidor SSH
config.ssh_port=Porta
config.ssh_listen_port=Porta de escuta
config.ssh_root_path=Caminho da raiz
config.ssh_key_test_path=Caminho da chave de teste
config.ssh_keygen_path=Caminho do keygen ('ssh-keygen')
config.ssh_minimum_key_size_check=Verificar tamanho mínimo da chave
config.ssh_minimum_key_sizes=Tamanhos mínimos da chave

View File

@ -1654,6 +1654,8 @@ issues.label_archived_filter=Mostrar rótulos arquivados
issues.label_archive_tooltip=Os rótulos arquivados são, por norma, excluídos das sugestões ao pesquisar por rótulo.
issues.label_exclusive_desc=Nomeie o rótulo <code>âmbito/item</code> para torná-lo mutuamente exclusivo com outros rótulos do <code>âmbito/</code>.
issues.label_exclusive_warning=Quaisquer rótulos com âmbito que estejam em conflito irão ser removidos ao editar os rótulos de uma questão ou de um pedido de integração.
issues.label_exclusive_order=Ordenação
issues.label_exclusive_order_tooltip=Rótulos exclusivos no mesmo âmbito serão ordenados de acordo com esta ordem numérica.
issues.label_count=%d rótulos
issues.label_open_issues=%d questões abertas
issues.label_edit=Editar
@ -3286,8 +3288,6 @@ config.ssh_domain=Domínio do servidor SSH
config.ssh_port=Porto
config.ssh_listen_port=Porto de escuta
config.ssh_root_path=Localização base
config.ssh_key_test_path=Localização do teste das chaves
config.ssh_keygen_path=Localização do gerador de chaves ('ssh-keygen')
config.ssh_minimum_key_size_check=Verificação de tamanho mínimo da chave
config.ssh_minimum_key_sizes=Tamanhos mínimos da chave

View File

@ -2870,8 +2870,6 @@ config.ssh_domain=Домен SSH сервера
config.ssh_port=Порт
config.ssh_listen_port=Прослушиваемый порт
config.ssh_root_path=Корневой путь
config.ssh_key_test_path=Путь к тестовому ключу
config.ssh_keygen_path=Путь к генератору ключей ('ssh-keygen')
config.ssh_minimum_key_size_check=Минимальный размер ключа проверки
config.ssh_minimum_key_sizes=Минимальные размеры ключа

View File

@ -2240,8 +2240,6 @@ config.ssh_domain=SSH සේවාදායකය වසම්
config.ssh_port=වරාය
config.ssh_listen_port=සවන් වරාය
config.ssh_root_path=මූල මාර්ගය
config.ssh_key_test_path=ප්රධාන ටෙස්ට් මාර්ගය
config.ssh_keygen_path=Keygen ('ssh-keygen') මාර්ගය
config.ssh_minimum_key_size_check=අවම කී ප්රමාණය පරීක්ෂා
config.ssh_minimum_key_sizes=අවම යතුරෙහි ප්‍රමාණ

View File

@ -1794,8 +1794,6 @@ config.ssh_start_builtin_server=Använd inbyggd Server
config.ssh_port=Port
config.ssh_listen_port=Lyssningsport
config.ssh_root_path=Rotsökväg
config.ssh_key_test_path=Testsökväg för nyckel
config.ssh_keygen_path=Sökväg för nyckelgenerator ('ssh-keygen')
config.ssh_minimum_key_size_check=Kontroll av minsta tillåtna nyckelstorlek
config.ssh_minimum_key_sizes=Minsta tillåtna nyckelstorlek

View File

@ -3106,8 +3106,6 @@ config.ssh_domain=SSH Sunucusu Alan Adı
config.ssh_port=Bağlantı Noktası
config.ssh_listen_port=Port'u Dinle
config.ssh_root_path=Kök Yol
config.ssh_key_test_path=Anahtar Test Yolu
config.ssh_keygen_path=Keygen ('ssh-keygen') Yolu
config.ssh_minimum_key_size_check=Minimum Anahtar Uzunluğu Kontrolü
config.ssh_minimum_key_sizes=Minimum Anahtar Uzunlukları

View File

@ -2290,8 +2290,6 @@ config.ssh_domain=Домен SSH сервера
config.ssh_port=Порт
config.ssh_listen_port=Порт що прослуховується
config.ssh_root_path=Шлях до кореню
config.ssh_key_test_path=Шлях до тестового ключа
config.ssh_keygen_path=Шлях до генератора ключів ('ssh-keygen')
config.ssh_minimum_key_size_check=Мінімальний розмір ключа перевірки
config.ssh_minimum_key_sizes=Мінімальні розміри ключів

View File

@ -113,6 +113,7 @@ copy_type_unsupported=无法复制此类型的文件内容
write=撰写
preview=预览
loading=正在加载...
files=文件
error=错误
error404=您正尝试访问的页面 <strong>不存在</strong> 或 <strong>您尚未被授权</strong> 查看该页面。
@ -169,6 +170,10 @@ search=搜索...
type_tooltip=搜索类型
fuzzy=模糊
fuzzy_tooltip=包含近似匹配搜索词的结果
words=
words_tooltip=仅包含匹配搜索词的结果
regexp=正则表达式
regexp_tooltip=仅包含匹配正则表达式搜索词的结果
exact=精确
exact_tooltip=仅包含精确匹配搜索词的结果
repo_kind=搜索仓库...
@ -385,6 +390,12 @@ show_only_public=只显示公开的
issues.in_your_repos=在您的仓库中
guide_title=无活动
guide_desc=您目前没有关注任何仓库或用户,所以没有要显示的内容。 您可以从下面的链接中探索感兴趣的仓库或用户。
explore_repos=探索仓库
explore_users=探索用户
empty_org=目前还没有组织。
empty_repo=目前还没有仓库。
[explore]
repos=仓库
@ -446,6 +457,7 @@ oauth_signup_submit=完成账号
oauth_signin_tab=绑定到现有帐号
oauth_signin_title=登录以授权绑定帐户
oauth_signin_submit=绑定账号
oauth.signin.error.general=处理授权请求时出错:%s。如果此错误仍然存在请与站点管理员联系。
oauth.signin.error.access_denied=授权请求被拒绝。
oauth.signin.error.temporarily_unavailable=授权失败,因为认证服务器暂时不可用。请稍后再试。
oauth_callback_unable_auto_reg=自动注册已启用但OAuth2 提供商 %[1]s 返回缺失的字段:%[2]s无法自动创建帐户请创建或链接到一个帐户或联系站点管理员。
@ -718,6 +730,8 @@ public_profile=公开信息
biography_placeholder=告诉我们一点您自己! (您可以使用Markdown)
location_placeholder=与他人分享你的大概位置
profile_desc=控制您的个人资料对其他用户的显示方式。您的主要电子邮件地址将用于通知、密码恢复和基于网页界面的 Git 操作
password_username_disabled=您不被允许更改你的用户名。更多详情请联系您的系统管理员。
password_full_name_disabled=您不被允许更改你的全名。请联系您的站点管理员了解更多详情。
full_name=自定义名称
website=个人网站
location=所在地区
@ -912,6 +926,9 @@ permission_not_set=未设置
permission_no_access=无访问权限
permission_read=可读
permission_write=读写
permission_anonymous_read=匿名读
permission_everyone_read=所有人可读
permission_everyone_write=所有人可写
access_token_desc=所选令牌权限仅限于对应的 <a %s>API</a> 路由的授权。阅读 <a %s>文档</a> 以获取更多信息。
at_least_one_permission=你需要选择至少一个权限才能创建令牌
permissions_list=权限:
@ -1014,6 +1031,9 @@ new_repo_helper=代码仓库包含了所有的项目文件,包括版本历史
owner=拥有者
owner_helper=由于最大仓库数量限制,一些组织可能不会显示在下拉列表中。
repo_name=仓库名称
repo_name_profile_public_hint=.profile 是一个特殊的存储库,您可以使用它将 README.md 添加到您的公共组织资料中,任何人都可以看到。请确保它是公开的,并使用个人资料目录中的 README 对其进行初始化以开始使用。
repo_name_profile_private_hint=.profile-private 是一个特殊的存储库,您可以使用它向您的组织成员个人资料添加 README.md仅对组织成员可见。请确保它是私有的并使用个人资料目录中的 README 对其进行初始化以开始使用。
repo_name_helper=理想的仓库名称应由简短、有意义和独特的关键词组成。".profile" 和 ".profile-private" 可用于为用户/组织添加 README.md。
repo_size=仓库大小
template=模板
template_select=选择模板
@ -1110,6 +1130,7 @@ blame.ignore_revs=忽略 <a href="%s">.git-blame-ignore-revs</a> 的修订。点
blame.ignore_revs.failed=忽略 <a href="%s">.git-blame-ignore-revs</a> 版本失败。
user_search_tooltip=最多显示30名用户
tree_path_not_found=%[2]s 中不存在路径 %[1]s
transfer.accept=接受转移
transfer.accept_desc=`转移到 "%s"`
@ -1120,6 +1141,7 @@ transfer.no_permission_to_reject=您没有权限拒绝此转让。
desc.private=私有库
desc.public=公开
desc.public_access=公开访问
desc.template=模板
desc.internal=内部
desc.archived=已存档
@ -1227,6 +1249,7 @@ create_new_repo_command=从命令行创建一个新的仓库
push_exist_repo=从命令行推送已经创建的仓库
empty_message=这个家伙很懒,什么都没有推送。
broken_message=无法读取此仓库下的 Git 数据。 联系此实例的管理员或删除此仓库。
no_branch=该仓库没有任何分支。
code=代码
code.desc=查看源码、文件、提交和分支。
@ -1339,6 +1362,8 @@ editor.new_branch_name_desc=新的分支名称...
editor.cancel=取消
editor.filename_cannot_be_empty=文件名不能为空。
editor.filename_is_invalid=文件名 %s 无效
editor.commit_email=提交邮箱地址
editor.invalid_commit_email=提交的邮箱地址无效。
editor.branch_does_not_exist=此仓库中不存在名为 %s 的分支。
editor.branch_already_exists=此仓库已存在名为 %s 的分支。
editor.directory_is_a_file=%s 已经作为文件名在此仓库中存在。
@ -1387,6 +1412,7 @@ commits.signed_by_untrusted_user_unmatched=由与提交者不匹配的未授信
commits.gpg_key_id=GPG 密钥 ID
commits.ssh_key_fingerprint=SSH 密钥指纹
commits.view_path=在历史记录中的此处查看
commits.view_file_diff=查看提交中的文件更改
commit.operations=操作
commit.revert=还原
@ -1398,7 +1424,7 @@ commit.cherry-pick-content=选择 cherry-pick 的目标分支:
commitstatus.error=错误
commitstatus.failure=失败
commitstatus.pending=待定
commitstatus.pending=队列
commitstatus.success=成功
ext_issues=访问外部工单
@ -1433,7 +1459,7 @@ projects.column.set_default=设为默认
projects.column.set_default_desc=设置此列为未分类问题和合并请求的默认值
projects.column.delete=删除列
projects.column.deletion_desc=删除项目列会将所有相关问题移到“未分类”。是否继续?
projects.column.color=
projects.column.color=
projects.open=开启
projects.close=关闭
projects.column.assigned_to=指派给
@ -1447,6 +1473,8 @@ issues.filter_milestones=筛选里程碑
issues.filter_projects=筛选项目
issues.filter_labels=筛选标签
issues.filter_reviewers=筛选审核者
issues.filter_no_results=没有结果
issues.filter_no_results_placeholder=请尝试调整您的搜索过滤器。
issues.new=创建工单
issues.new.title_empty=标题不能为空
issues.new.labels=标签
@ -1520,8 +1548,10 @@ issues.filter_milestone_open=进行中的里程碑
issues.filter_milestone_closed=已关闭的里程碑
issues.filter_project=项目
issues.filter_project_all=所有项目
issues.filter_project_none=暂无项目
issues.filter_project_none=未加项目
issues.filter_assignee=指派人筛选
issues.filter_assignee_no_assignee=未指派给任何人
issues.filter_assignee_any_assignee=已有指派
issues.filter_poster=作者
issues.filter_user_placeholder=搜索用户
issues.filter_user_no_select=所有用户
@ -1595,9 +1625,9 @@ issues.ref_reopened_from=`<a href="%[3]s">重新打开这个工单 %[4]s</a> <a
issues.ref_from=`来自 %[1]s`
issues.author=作者
issues.author_helper=此用户是作者。
issues.role.owner=管理员
issues.role.owner=所有者
issues.role.owner_helper=该用户是该仓库的所有者。
issues.role.member=普通成员
issues.role.member=成员
issues.role.member_helper=该用户是拥有该仓库的组织成员。
issues.role.collaborator=协作者
issues.role.collaborator_helper=该用户已被邀请在仓库上进行协作。
@ -1618,12 +1648,14 @@ issues.save=保存
issues.label_title=标签名称
issues.label_description=标签描述
issues.label_color=标签颜色
issues.label_exclusive=独有
issues.label_exclusive=互斥标签
issues.label_archive=归档标签
issues.label_archived_filter=显示存档标签
issues.label_archive_tooltip=在标签搜索时,默认情况下存档标签将被排除在外。
issues.label_exclusive_desc=命名标签为 <code>scope/item</code> 以使其与其他以 <code>scope/</code> 开头的标签互斥。
issues.label_exclusive_warning=在编辑工单或合并请求的标签时,任何冲突的范围标签都将被删除。
issues.label_exclusive_order=排序顺序
issues.label_exclusive_order_tooltip=在同一个范围内的互斥标签将按照这个数字进行排序
issues.label_count=%d 个标签
issues.label_open_issues=%d 个开启的工单
issues.label_edit=编辑
@ -1676,13 +1708,18 @@ issues.timetracker_timer_manually_add=添加时间
issues.time_estimate_set=设置预计时间
issues.time_estimate_display=预计: %s
issues.change_time_estimate_at=预估时间已修改为 <b>%[1]s</b> %[2]s
issues.remove_time_estimate_at=删除预计时间 %s
issues.time_estimate_invalid=预计时间格式无效
issues.start_tracking_history=`开始工作 %s`
issues.tracker_auto_close=当此工单关闭时,自动停止计时器
issues.tracking_already_started=`你已经开始对 <a href="%s">另一个工单</a> 进行时间跟踪!`
issues.stop_tracking=停止计时器
issues.stop_tracking_history=工作 <b>%[1]s</b> 于 %[2]s 停止
issues.cancel_tracking=取消
issues.cancel_tracking_history=`取消时间跟踪 %s`
issues.del_time=删除此时间跟踪日志
issues.add_time_history=已于 %[2]s 添加计时 <b>%[1]</b>
issues.del_time_history=`已删除时间 %s`
issues.add_time_manually=手动添加时间
issues.add_time_hours=小时
@ -1912,6 +1949,7 @@ pulls.outdated_with_base_branch=此分支相比基础分支已过期
pulls.close=关闭合并请求
pulls.closed_at=`于 <a id="%[1]s" href="#%[1]s">%[2]s</a> 关闭此合并请求 `
pulls.reopened_at=`重新打开此合并请求 <a id="%[1]s" href="#%[1]s">%[2]s</a>`
pulls.cmd_instruction_hint=查看命令行提示
pulls.cmd_instruction_checkout_title=检出
pulls.cmd_instruction_checkout_desc=从你的仓库中检出一个新的分支并测试变更。
pulls.cmd_instruction_merge_title=合并
@ -1940,6 +1978,7 @@ pulls.upstream_diverging_prompt_behind_1=该分支落后于 %[2]s %[1]d 个提
pulls.upstream_diverging_prompt_behind_n=该分支落后于 %[2]s %[1]d 个提交
pulls.upstream_diverging_prompt_base_newer=基础分支 %s 有新的更改
pulls.upstream_diverging_merge=同步派生
pulls.upstream_diverging_merge_confirm=要将 %[1]s 合并到 %[2]s 吗?
pull.deleted_branch=(已删除): %s
pull.agit_documentation=查看有关 AGit 的文档
@ -2099,6 +2138,12 @@ contributors.contribution_type.deletions=删除
settings=设置
settings.desc=设置是你可以管理仓库设置的地方
settings.options=仓库
settings.public_access=公开访问
settings.public_access_desc=配置公共访客访问权限以覆盖此存储库的默认值。
settings.public_access.docs.not_set=未设置:没有额外的公共访问权限。访客权限遵循存储库的可见性和成员权限。
settings.public_access.docs.anonymous_read=匿名可读:未登录的用户可以通过读取权限访问单元。
settings.public_access.docs.everyone_read=所有人可读:所有登录用户都可以通过读取权限访问单元。读取问题/拉取请求单元的权限也意味着用户可以创建新的问题/拉取请求。
settings.public_access.docs.everyone_write=所有人可写:所有登录用户都有写入权限。只有百科支持此权限。
settings.collaboration=协作者
settings.collaboration.admin=管理员
settings.collaboration.write=可写权限
@ -2312,6 +2357,8 @@ settings.event_fork=派生
settings.event_fork_desc=仓库被派生。
settings.event_wiki=百科
settings.event_wiki_desc=创建、重命名、编辑或删除了百科页面。
settings.event_statuses=状态
settings.event_statuses_desc=已从 API 更新提交状态。
settings.event_release=版本发布
settings.event_release_desc=发布、更新或删除版本时。
settings.event_push=推送
@ -2349,6 +2396,9 @@ settings.event_pull_request_review_request=发起合并请求评审
settings.event_pull_request_review_request_desc=合并请求评审已请求或已取消
settings.event_pull_request_approvals=合并请求批准
settings.event_pull_request_merge=合并请求合并
settings.event_header_workflow=工作流程事件
settings.event_workflow_job=工作流任务
settings.event_workflow_job_desc=Gitea Actions 工作流队列中、等待中、正在进行或已完成任务。
settings.event_package=软件包
settings.event_package_desc=软件包已在仓库中被创建或删除。
settings.branch_filter=分支过滤
@ -2611,6 +2661,9 @@ diff.image.overlay=叠加
diff.has_escaped=这一行有隐藏的 Unicode 字符
diff.show_file_tree=显示文件树
diff.hide_file_tree=隐藏文件树
diff.submodule_added=子模块 %[1]s 已添加到 %[2]s
diff.submodule_deleted=子模块 %[1]s 已从 %[2]s 中删除
diff.submodule_updated=子模块 %[1]s 已更新:%[2]s
releases.desc=跟踪项目版本和下载。
release.releases=版本发布
@ -2681,6 +2734,7 @@ branch.restore_success=分支 "%s"已还原。
branch.restore_failed=还原分支 "%s"失败。
branch.protected_deletion_failed=不能删除受保护的分支 "%s"。
branch.default_deletion_failed=不能删除默认分支"%s"。
branch.default_branch_not_exist=默认分支 %s 不存在。
branch.restore=`还原分支 "%s"`
branch.download=`下载分支 "%s"`
branch.rename=`重命名分支 "%s"`
@ -2695,6 +2749,8 @@ branch.create_branch_operation=创建分支
branch.new_branch=创建新分支
branch.new_branch_from=基于"%s"创建新分支
branch.renamed=分支 %s 被重命名为 %s。
branch.rename_default_or_protected_branch_error=只有管理员能重命名默认分支和受保护的分支。
branch.rename_protected_branch_failed=此分支受到 glob 语法规则的保护。
tag.create_tag=创建标签 %s
tag.create_tag_operation=创建标签
@ -2849,7 +2905,18 @@ teams.invite.title=您已被邀请加入组织 <strong>%s</strong> 中的团队
teams.invite.by=邀请人 %s
teams.invite.description=请点击下面的按钮加入团队。
view_as_role=以 %s 身份查看
view_as_public_hint=您正在以公开用户的身份查看 README
view_as_member_hint=您正在以组织成员的身份查看 README
worktime=工作时间
worktime.date_range_start=起始日期
worktime.date_range_end=结束日期
worktime.query=查询
worktime.time=时间
worktime.by_repositories=按仓库
worktime.by_milestones=按里程碑
worktime.by_members=按成员
[admin]
maintenance=维护
@ -3221,8 +3288,6 @@ config.ssh_domain=SSH 服务器域名
config.ssh_port=端口
config.ssh_listen_port=监听端口
config.ssh_root_path=根目录
config.ssh_key_test_path=密钥测试路径
config.ssh_keygen_path=密钥生成器('ssh-keygen')路径
config.ssh_minimum_key_size_check=密钥最小长度检查
config.ssh_minimum_key_sizes=密钥最小长度限制
@ -3345,6 +3410,7 @@ monitor.previous=上次执行时间
monitor.execute_times=执行次数
monitor.process=运行中进程
monitor.stacktrace=调用栈踪迹
monitor.trace=追踪
monitor.performance_logs=性能日志
monitor.processes_count=%d 个进程
monitor.download_diagnosis_report=下载诊断报告
@ -3520,10 +3586,11 @@ versions=版本
versions.view_all=查看全部
dependency.id=ID
dependency.version=版本
search_in_external_registry=在 %s 中搜索
alpine.registry=通过在您的 <code>/etc/apk/repositories</code> 文件中添加 URL 来设置此注册中心:
alpine.registry.key=下载注册中心公开的 RSA 密钥到 <code>/etc/apk/keys/</code> 文件夹来验证索引签名:
alpine.registry.info=从下面的列表中选择 $branch 和 $repository。
alpine.install=要安装包,请运行以下命令:
alpine.install=要安装软件包,请运行以下命令:
alpine.repository=仓库信息
alpine.repository.branches=分支
alpine.repository.repositories=仓库
@ -3536,7 +3603,7 @@ arch.repository.architectures=架构
cargo.registry=在 Cargo 配置文件中设置此注册中心(例如:<code>~/.cargo/config.toml</code>)
cargo.install=要使用 Cargo 安装软件包,请运行以下命令:
chef.registry=在您的 <code>~/.chef/config.rb</code> 文件中设置此注册中心:
chef.install=要安装包,请运行以下命令:
chef.install=要安装软件包,请运行以下命令:
composer.registry=在您的 <code>~/.composer/config.json</code> 文件中设置此注册中心:
composer.install=要使用 Composer 安装软件包,请运行以下命令:
composer.dependencies=依赖
@ -3550,16 +3617,17 @@ container.details.type=镜像类型
container.details.platform=平台
container.pull=从命令行拉取镜像:
container.images=镜像
container.digest=摘要
container.multi_arch=OS / Arch
container.layers=镜像层
container.labels=标签
container.labels.key=
container.labels.value=
cran.registry=在您的 <code>Rprofile.site</code> 文件中设置此注册中心:
cran.install=要安装包,请运行以下命令:
cran.install=要安装软件包,请运行以下命令:
debian.registry=从命令行设置此注册中心:
debian.registry.info=从下面的列表中选择 $distribution 和 $component。
debian.install=要安装包,请运行以下命令:
debian.install=要安装软件包,请运行以下命令:
debian.repository=仓库信息
debian.repository.distributions=发行版
debian.repository.components=组件
@ -3590,7 +3658,7 @@ pypi.install=要使用 pip 安装软件包,请运行以下命令:
rpm.registry=从命令行设置此注册中心:
rpm.distros.redhat=在基于 RedHat 的发行版
rpm.distros.suse=在基于 SUSE 的发行版
rpm.install=要安装包,请运行以下命令:
rpm.install=要安装软件包,请运行以下命令:
rpm.repository=仓库信息
rpm.repository.architectures=架构
rpm.repository.multiple_groups=此软件包可在多个组中使用。
@ -3656,6 +3724,7 @@ creation=添加密钥
creation.description=组织描述
creation.name_placeholder=不区分大小写字母数字或下划线不能以GITEA_ 或 GITHUB_ 开头。
creation.value_placeholder=输入任何内容,开头和结尾的空白都会被省略
creation.description_placeholder=输入简短描述(可选)。
creation.success=您的密钥 '%s' 添加成功。
creation.failed=添加密钥失败。
deletion=删除密钥
@ -3686,7 +3755,7 @@ runners.status=状态
runners.id=ID
runners.name=名称
runners.owner_type=类型
runners.description=组织描述
runners.description=描述
runners.labels=标签
runners.last_online=上次在线时间
runners.runner_title=Runner
@ -3709,10 +3778,11 @@ runners.delete_runner_notice=如果一个任务正在运行在此运行器上,
runners.none=无可用的 Runner
runners.status.unspecified=未知
runners.status.idle=空闲
runners.status.active=激活
runners.status.active=启用
runners.status.offline=离线
runners.version=版本
runners.reset_registration_token=重置注册令牌
runners.reset_registration_token_confirm=是否吊销当前令牌并生成一个新令牌?
runners.reset_registration_token_success=成功重置运行器注册令牌
runs.all_workflows=所有工作流
@ -3745,6 +3815,7 @@ workflow.not_found=工作流 %s 未找到。
workflow.run_success=工作流 %s 已成功运行。
workflow.from_ref=使用工作流从
workflow.has_workflow_dispatch=此 Workflow 有一个 Workflow_dispatch 事件触发器。
workflow.has_no_workflow_dispatch=工作流 %s 没有 workflow_dispatch 事件的触发器。
need_approval_desc=该工作流由派生仓库的合并请求所触发,需要批准方可运行。
@ -3764,6 +3835,8 @@ variables.creation.success=变量 “%s” 添加成功。
variables.update.failed=编辑变量失败。
variables.update.success=该变量已被编辑。
logs.always_auto_scroll=总是自动滚动日志
logs.always_expand_running=总是展开运行日志
[projects]
deleted.display_name=已删除项目

View File

@ -816,8 +816,6 @@ config.ssh_enabled=已啟用
config.ssh_port=
config.ssh_listen_port=監聽埠
config.ssh_root_path=根路徑
config.ssh_key_test_path=金鑰測試路徑
config.ssh_keygen_path=金鑰產生 (' ssh-keygen ') 路徑
config.ssh_minimum_key_size_check=金鑰最小大小檢查
config.ssh_minimum_key_sizes=金鑰最小大小

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