Merge branch 'main' into lunny/issue_dev

pull/31899/head
Lunny Xiao 2025-11-26 10:32:13 +07:00 committed by GitHub
commit 811c67640c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
225 changed files with 3241 additions and 2390 deletions

@ -25,6 +25,10 @@ insert_final_newline = false
[templates/user/auth/oidc_wellknown.tmpl]
indent_style = space
[templates/shared/actions/runner_badge_*.tmpl]
# editconfig lint requires these XML-like files to have charset defined, but the files don't have.
charset = unset
[Makefile]
indent_style = tab

1
.gitattributes vendored

@ -8,3 +8,4 @@
/vendor/** -text -eol linguist-vendored
/web_src/js/vendor/** -text -eol linguist-vendored
Dockerfile.* linguist-language=Dockerfile
Makefile.* linguist-language=Makefile

@ -51,7 +51,6 @@ modifies/internal:
- ".github/**"
- ".gitea/**"
- ".devcontainer/**"
- "build.go"
- "build/**"
- "contrib/**"

@ -1,35 +0,0 @@
name: e2e-tests
on:
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
files-changed:
uses: ./.github/workflows/files-changed.yml
test-e2e:
# the "test-e2e" won't pass, and it seems that there is no useful test, so skip
# if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true'
if: false
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v5
with:
node-version: 24
- run: make deps-frontend frontend deps-backend
- run: pnpm exec playwright install --with-deps
- run: make test-e2e-sqlite
timeout-minutes: 40
env:
USE_REPO_TEST_DIR: 1

3
.gitignore vendored

@ -127,3 +127,6 @@ prime/
# Ignore worktrees when working on multiple branches
.worktrees/
# A Makefile for custom make targets
Makefile.local

@ -14,6 +14,7 @@ linters:
- govet
- ineffassign
- mirror
- modernize
- nakedret
- nolintlint
- perfsprint
@ -55,6 +56,7 @@ linters:
disabled-checks:
- ifElseChain
- singleCaseSwitch # Every time this occurred in the code, there was no other way.
- deprecatedComment # conflicts with go-swagger comments
revive:
severity: error
rules:
@ -107,6 +109,11 @@ linters:
- require-error
usetesting:
os-temp-dir: true
modernize:
disable:
- stringsbuilder
perfsprint:
concat-loop: false
exclusions:
generated: lax
presets:

@ -166,19 +166,19 @@ Here's how to run the test suite:
- code lint
| | |
| :-------------------- | :---------------------------------------------------------------- |
| | |
| :-------------------- | :--------------------------------------------------------------------------- |
|``make lint`` | lint everything (not needed if you only change the front- **or** backend) |
|``make lint-frontend`` | lint frontend files |
|``make lint-backend`` | lint backend files |
|``make lint-frontend`` | lint frontend files |
|``make lint-backend`` | lint backend files |
- run tests (we suggest running them on Linux)
| Command | Action | |
| :------------------------------------- | :----------------------------------------------- | ------------ |
|``make test[\#SpecificTestName]`` | run unit test(s) | |
|``make test-sqlite[\#SpecificTestName]``| run [integration](tests/integration) test(s) for SQLite |[More details](tests/integration/README.md) |
|``make test-e2e-sqlite[\#SpecificTestName]``| run [end-to-end](tests/e2e) test(s) for SQLite |[More details](tests/e2e/README.md) |
| Command | Action | |
| :------------------------------------------ | :------------------------------------------------------- | ------------------------------------------- |
|``make test[\#SpecificTestName]`` | run unit test(s) | |
|``make test-sqlite[\#SpecificTestName]`` | run [integration](tests/integration) test(s) for SQLite | [More details](tests/integration/README.md) |
|``make test-e2e-sqlite[\#SpecificTestName]`` | run [end-to-end](tests/e2e) test(s) for SQLite | [More details](tests/e2e/README.md) |
## Translation

@ -32,16 +32,15 @@ XGO_VERSION := go-1.25.x
AIR_PACKAGE ?= github.com/air-verse/air@v1
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.9.2
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.5.0
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.6.0
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.7.0
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.1
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.9
GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.20.0
GOPLS_MODERNIZE_PACKAGE ?= golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@v0.20.0
DOCKER_IMAGE ?= gitea/gitea
DOCKER_TAG ?= latest
@ -199,6 +198,10 @@ TEST_MSSQL_DBNAME ?= gitea
TEST_MSSQL_USERNAME ?= sa
TEST_MSSQL_PASSWORD ?= MwantsaSecurePassword1
# Include local Makefile
# Makefile.local is listed in .gitignore
sinclude Makefile.local
.PHONY: all
all: build
@ -276,19 +279,6 @@ fmt-check: fmt
exit 1; \
fi
.PHONY: fix
fix: ## apply automated fixes to Go code
$(GO) run $(GOPLS_MODERNIZE_PACKAGE) -fix ./...
.PHONY: fix-check
fix-check: fix
@diff=$$(git diff --color=always $(GO_SOURCES)); \
if [ -n "$$diff" ]; then \
echo "Please run 'make fix' and commit the result:"; \
printf "%s" "$${diff}"; \
exit 1; \
fi
.PHONY: $(TAGS_EVIDENCE)
$(TAGS_EVIDENCE):
@mkdir -p $(MAKE_EVIDENCE_DIR)
@ -328,7 +318,7 @@ checks: checks-frontend checks-backend ## run various consistency checks
checks-frontend: lockfile-check svg-check ## check frontend files
.PHONY: checks-backend
checks-backend: tidy-check swagger-check fmt-check fix-check swagger-validate security-check ## check backend files
checks-backend: tidy-check swagger-check fmt-check swagger-validate security-check ## check backend files
.PHONY: lint
lint: lint-frontend lint-backend lint-spell ## lint everything
@ -374,6 +364,10 @@ lint-swagger: node_modules ## lint swagger files
lint-md: node_modules ## lint markdown files
$(NODE_VARS) pnpm exec markdownlint *.md
.PHONY: lint-md-fix
lint-md-fix: node_modules ## lint markdown files and fix issues
$(NODE_VARS) pnpm exec markdownlint --fix *.md
.PHONY: lint-spell
lint-spell: ## lint spelling
@go run $(MISSPELL_PACKAGE) -dict assets/misspellings.csv -error $(SPELLCHECK_FILES)
@ -400,8 +394,7 @@ lint-go-windows:
.PHONY: lint-go-gitea-vet
lint-go-gitea-vet: ## lint go files with gitea-vet
@echo "Running gitea-vet..."
@GOOS= GOARCH= $(GO) build code.gitea.io/gitea-vet
@$(GO) vet -vettool=gitea-vet ./...
@$(GO) vet -vettool="$(shell GOOS= GOARCH= go tool -n gitea-vet)" ./...
.PHONY: lint-go-gopls
lint-go-gopls: ## lint go files with gopls
@ -852,7 +845,6 @@ deps-tools: ## install tool dependencies
$(GO) install $(GOVULNCHECK_PACKAGE) & \
$(GO) install $(ACTIONLINT_PACKAGE) & \
$(GO) install $(GOPLS_PACKAGE) & \
$(GO) install $(GOPLS_MODERNIZE_PACKAGE) & \
wait
node_modules: pnpm-lock.yaml

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

@ -567,6 +567,11 @@ ENABLED = true
;; Alternative location to specify OAuth2 authentication secret. You cannot specify both this and JWT_SECRET, and must pick one
;JWT_SECRET_URI = file:/etc/gitea/oauth2_jwt_secret
;;
;; The "issuer" claim identifies the principal that issued the JWT.
;; Gitea 1.25 makes it default to "ROOT_URL without the last slash" to follow the standard.
;; If you have old logins from before 1.25, you may want to set it to the old (non-standard) value "ROOT_URL with the last slash".
;JWT_CLAIM_ISSUER =
;;
;; Lifetime of an OAuth2 access token in seconds
;ACCESS_TOKEN_EXPIRATION_TIME = 3600
;;
@ -2329,7 +2334,7 @@ LEVEL = Info
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Resynchronize pre-receive, update and post-receive hooks of all repositories.
;; Resynchronize git hooks of all repositories (pre-receive, update, post-receive, proc-receive, ...)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;[cron.resync_all_hooks]
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

@ -935,7 +935,6 @@ export default defineConfig([
},
{
files: ['**/*.test.ts', 'web_src/js/test/setup.ts'],
// @ts-expect-error - https://github.com/vitest-dev/eslint-plugin-vitest/issues/737
plugins: {vitest},
languageOptions: {globals: globals.vitest},
rules: {

@ -1,23 +1,5 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1760038930,
@ -36,24 +18,8 @@
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

@ -1,73 +1,94 @@
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs =
{ nixpkgs, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
devShells.default =
with pkgs;
let
# only bump toolchain versions here
go = go_1_25;
nodejs = nodejs_24;
python3 = python312;
pnpm = pnpm_10;
# Platform-specific dependencies
linuxOnlyInputs = lib.optionals pkgs.stdenv.isLinux [
glibc.static
];
{ nixpkgs, ... }:
let
supportedSystems = [
"aarch64-darwin"
"aarch64-linux"
"x86_64-darwin"
"x86_64-linux"
];
linuxOnlyEnv = lib.optionalAttrs pkgs.stdenv.isLinux {
CFLAGS = "-I${glibc.static.dev}/include";
LDFLAGS = "-L ${glibc.static}/lib";
forEachSupportedSystem =
f:
nixpkgs.lib.genAttrs supportedSystems (
system:
let
pkgs = import nixpkgs {
inherit system;
};
in
pkgs.mkShell (
{
buildInputs = [
# generic
git
git-lfs
gnumake
gnused
gnutar
gzip
zip
f { inherit pkgs; }
);
in
{
devShells = forEachSupportedSystem (
{ pkgs, ... }:
{
default =
let
inherit (pkgs) lib;
# only bump toolchain versions here
go = pkgs.go_1_25;
nodejs = pkgs.nodejs_24;
python3 = pkgs.python312;
pnpm = pkgs.pnpm_10;
# frontend
nodejs
pnpm
cairo
pixman
pkg-config
# Platform-specific dependencies
linuxOnlyInputs = lib.optionals pkgs.stdenv.isLinux [
pkgs.glibc.static
];
# linting
python3
uv
linuxOnlyEnv = lib.optionalAttrs pkgs.stdenv.isLinux {
CFLAGS = "-I${pkgs.glibc.static.dev}/include";
LDFLAGS = "-L ${pkgs.glibc.static}/lib";
};
in
pkgs.mkShell {
packages =
with pkgs;
[
# generic
git
git-lfs
gnumake
gnused
gnutar
gzip
zip
# backend
go
gofumpt
sqlite
]
++ linuxOnlyInputs;
# frontend
nodejs
pnpm
cairo
pixman
pkg-config
GO = "${go}/bin/go";
GOROOT = "${go}/share/go";
# linting
python3
uv
TAGS = "sqlite sqlite_unlock_notify";
STATIC = "true";
}
// linuxOnlyEnv
);
}
);
# backend
go
gofumpt
sqlite
]
++ linuxOnlyInputs;
env = {
GO = "${go}/bin/go";
GOROOT = "${go}/share/go";
TAGS = "sqlite sqlite_unlock_notify";
STATIC = "true";
}
// linuxOnlyEnv;
};
}
);
};
}

@ -1,6 +1,8 @@
module code.gitea.io/gitea
go 1.25.3
go 1.25.0
toolchain go1.25.4
// rfc5280 said: "The serial number is an integer assigned by the CA to each certificate."
// But some CAs use negative serial number, just relax the check. related:
@ -9,14 +11,13 @@ godebug x509negativeserial=1
require (
code.gitea.io/actions-proto-go v0.4.1
code.gitea.io/gitea-vet v0.2.3
code.gitea.io/sdk/gitea v0.22.0
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570
connectrpc.com/connect v1.18.1
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed
gitea.com/go-chi/cache v0.2.1
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098
gitea.com/go-chi/session v0.0.0-20250926004215-636cadd82e15
gitea.com/go-chi/session v0.0.0-20251124165456-68e0254e989e
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4
github.com/42wim/httpsig v1.2.3
@ -116,13 +117,13 @@ require (
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
github.com/yuin/goldmark-meta v1.1.0
gitlab.com/gitlab-org/api/client-go v0.142.4
golang.org/x/crypto v0.42.0
golang.org/x/crypto v0.45.0
golang.org/x/image v0.30.0
golang.org/x/net v0.44.0
golang.org/x/net v0.47.0
golang.org/x/oauth2 v0.30.0
golang.org/x/sync v0.17.0
golang.org/x/sys v0.37.0
golang.org/x/text v0.30.0
golang.org/x/sync v0.18.0
golang.org/x/sys v0.38.0
golang.org/x/text v0.31.0
google.golang.org/grpc v1.75.0
google.golang.org/protobuf v1.36.8
gopkg.in/ini.v1 v1.67.0
@ -135,6 +136,7 @@ require (
require (
cloud.google.com/go/compute/metadata v0.8.0 // indirect
code.gitea.io/gitea-vet v0.2.3 // indirect
dario.cat/mergo v1.0.2 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect
@ -279,9 +281,9 @@ require (
go.uber.org/zap/exp v0.3.0 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect
golang.org/x/mod v0.28.0 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.37.0 // indirect
golang.org/x/tools v0.38.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
@ -307,3 +309,5 @@ exclude github.com/gofrs/uuid v4.0.0+incompatible
exclude github.com/goccy/go-json v0.4.11
exclude github.com/satori/go.uuid v1.2.0
tool code.gitea.io/gitea-vet

@ -41,8 +41,8 @@ gitea.com/go-chi/cache v0.2.1 h1:bfAPkvXlbcZxPCpcmDVCWoHgiBSBmZN/QosnZvEC0+g=
gitea.com/go-chi/cache v0.2.1/go.mod h1:Qic0HZ8hOHW62ETGbonpwz8WYypj9NieU9659wFUJ8Q=
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098 h1:p2ki+WK0cIeNQuqjR98IP2KZQKRzJJiV7aTeMAFwaWo=
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098/go.mod h1:LjzIOHlRemuUyO7WR12fmm18VZIlCAaOt9L3yKw40pk=
gitea.com/go-chi/session v0.0.0-20250926004215-636cadd82e15 h1:qFYmz05u/s9664o7+XEgrlHXSPQ4uHO8/ccZGUb1uxA=
gitea.com/go-chi/session v0.0.0-20250926004215-636cadd82e15/go.mod h1:0iEpFKnwO5dG0aF98O4eq6FMsAiXkNBaDIlUOlq4BtM=
gitea.com/go-chi/session v0.0.0-20251124165456-68e0254e989e h1:4bugwPyGMLvblEm3pZ8fZProSPVxE4l0UXF2Kv6IJoY=
gitea.com/go-chi/session v0.0.0-20251124165456-68e0254e989e/go.mod h1:KDvcfMUoXfATPHs2mbMoXFTXT45/FAFAS39waz9tPk0=
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 h1:+wWBi6Qfruqu7xJgjOIrKVQGiLUZdpKYCZewJ4clqhw=
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96/go.mod h1:VyMQP6ue6MKHM8UsOXfNfuMKD0oSAWZdXVcpHIN2yaY=
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 h1:IFT+hup2xejHqdhS7keYWioqfmxdnfblFDTGoOwcZ+o=
@ -840,8 +840,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -878,8 +878,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -908,8 +908,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -932,8 +932,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -975,8 +975,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -987,8 +987,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -1002,8 +1002,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
@ -1039,8 +1039,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

@ -13,6 +13,8 @@ func TestMain(m *testing.M) {
unittest.MainTest(m, &unittest.TestOptions{
FixtureFiles: []string{
"action_runner_token.yml",
"action_run.yml",
"repository.yml",
},
})
}

@ -193,9 +193,11 @@ func (run *ActionRun) IsSchedule() bool {
return run.ScheduleID > 0
}
// UpdateRepoRunsNumbers updates the number of runs and closed runs of a repository.
func UpdateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) error {
_, err := db.GetEngine(ctx).ID(repo.ID).
NoAutoTime().
Cols("num_action_runs", "num_closed_action_runs").
SetExpr("num_action_runs",
builder.Select("count(*)").From("action_run").
Where(builder.Eq{"repo_id": repo.ID}),

@ -0,0 +1,35 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"testing"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"github.com/stretchr/testify/assert"
)
func TestUpdateRepoRunsNumbers(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
// update the number to a wrong one, the original is 3
_, err := db.GetEngine(t.Context()).ID(4).Cols("num_closed_action_runs").Update(&repo_model.Repository{
NumClosedActionRuns: 2,
})
assert.NoError(t, err)
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
assert.Equal(t, 4, repo.NumActionRuns)
assert.Equal(t, 2, repo.NumClosedActionRuns)
// now update will correct them, only num_actionr_runs and num_closed_action_runs should be updated
err = UpdateRepoRunsNumbers(t.Context(), repo)
assert.NoError(t, err)
repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
assert.Equal(t, 5, repo.NumActionRuns)
assert.Equal(t, 3, repo.NumClosedActionRuns)
}

@ -386,7 +386,7 @@ func SetNotificationStatus(ctx context.Context, notificationID int64, user *user
notification.Status = status
_, err = db.GetEngine(ctx).ID(notificationID).Update(notification)
_, err = db.GetEngine(ctx).ID(notificationID).Cols("status").Update(notification)
return notification, err
}

@ -78,7 +78,7 @@ func VerifyGPGKey(ctx context.Context, ownerID int64, keyID, token, signature st
}
key.Verified = true
if _, err := db.GetEngine(ctx).ID(key.ID).SetExpr("verified", true).Update(new(GPGKey)); err != nil {
if _, err := db.GetEngine(ctx).ID(key.ID).Cols("verified").Update(key); err != nil {
return "", err
}

@ -159,3 +159,23 @@
updated: 1683636626
need_approval: 0
approved_by: 0
-
id: 805
title: "update actions"
repo_id: 4
owner_id: 1
workflow_id: "artifact.yaml"
index: 191
trigger_user_id: 1
ref: "refs/heads/master"
commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
event: "push"
trigger_event: "push"
is_fork_pull_request: 0
status: 5
started: 1683636528
stopped: 1683636626
created: 1683636108
updated: 1683636626
need_approval: 0
approved_by: 0

@ -143,3 +143,17 @@
status: 1
started: 1683636528
stopped: 1683636626
-
id: 206
run_id: 805
repo_id: 4
owner_id: 1
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
is_fork_pull_request: 0
name: job_2
attempt: 1
job_id: job_2
task_id: 56
status: 3
started: 1683636528
stopped: 1683636626

@ -197,3 +197,22 @@
log_length: 707
log_size: 90179
log_expired: 0
-
id: 56
attempt: 1
runner_id: 1
status: 3 # 3 is the status code for "cancelled"
started: 1683636528
stopped: 1683636626
repo_id: 4
owner_id: 1
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
is_fork_pull_request: 0
token_hash: 6d8ef48297195edcc8e22c70b3020eaa06c52976db67d39b4240c64a69a2cc1508825121b7b8394e48e00b1bf3718b2aaaab
token_salt: eeeeeeee
token_last_eight: eeeeeeee
log_filename: artifact-test2/2f/47.log
log_in_storage: 1
log_length: 707
log_size: 90179
log_expired: 0

@ -740,3 +740,10 @@
type: 10
config: "{}"
created_unix: 946684810
-
id: 112
repo_id: 4
type: 10
config: "{}"
created_unix: 946684810

@ -110,6 +110,8 @@
num_closed_milestones: 0
num_projects: 0
num_closed_projects: 1
num_action_runs: 4
num_closed_action_runs: 3
is_private: false
is_empty: false
is_archived: false

@ -381,7 +381,7 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str
}
// 1. update branch in database
if n, err := sess.Where("repo_id=? AND name=?", repo.ID, from).Update(&Branch{
if n, err := sess.Where("repo_id=? AND name=?", repo.ID, from).Cols("name").Update(&Branch{
Name: to,
}); err != nil {
return err

@ -466,11 +466,13 @@ func updateApprovalWhitelist(ctx context.Context, repo *repo_model.Repository, c
return currentWhitelist, nil
}
prUserIDs, err := access_model.GetUserIDsWithUnitAccess(ctx, repo, perm.AccessModeRead, unit.TypePullRequests)
if err != nil {
return nil, err
}
whitelist = make([]int64, 0, len(newWhitelist))
for _, userID := range newWhitelist {
if reader, err := access_model.IsRepoReader(ctx, repo, userID); err != nil {
return nil, err
} else if !reader {
if !prUserIDs.Contains(userID) {
continue
}
whitelist = append(whitelist, userID)

@ -862,10 +862,7 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment
if err = UpdateCommentAttachments(ctx, comment, opts.Attachments); err != nil {
return err
}
case CommentTypeReopen, CommentTypeClose:
if err = repo_model.UpdateRepoIssueNumbers(ctx, opts.Issue.RepoID, opts.Issue.IsPull, true); err != nil {
return err
}
// comment type reopen and close event have their own logic to update numbers but not here
}
// update the issue's updated_unix column
return UpdateIssueCols(ctx, opts.Issue, "updated_unix")

@ -146,8 +146,19 @@ func updateIssueNumbers(ctx context.Context, issue *Issue, doer *user_model.User
}
// update repository's issue closed number
if err := repo_model.UpdateRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, true); err != nil {
return nil, err
switch cmtType {
case CommentTypeClose, CommentTypeMergePull:
// only increase closed count
if err := IncrRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, false); err != nil {
return nil, err
}
case CommentTypeReopen:
// only decrease closed count
if err := DecrRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, false, true); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("invalid comment type: %d", cmtType)
}
return CreateComment(ctx, &CreateCommentOptions{
@ -318,7 +329,6 @@ type NewIssueOptions struct {
Issue *Issue
LabelIDs []int64
Attachments []string // In UUID format.
IsPull bool
}
// NewIssueWithIndex creates issue with given index
@ -369,7 +379,8 @@ func NewIssueWithIndex(ctx context.Context, doer *user_model.User, opts NewIssue
}
}
if err := repo_model.UpdateRepoIssueNumbers(ctx, opts.Issue.RepoID, opts.IsPull, false); err != nil {
// Update repository issue total count
if err := IncrRepoIssueNumbers(ctx, opts.Repo.ID, opts.Issue.IsPull, true); err != nil {
return err
}
@ -439,6 +450,42 @@ func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *Issue, la
})
}
// IncrRepoIssueNumbers increments repository issue numbers.
func IncrRepoIssueNumbers(ctx context.Context, repoID int64, isPull, totalOrClosed bool) error {
dbSession := db.GetEngine(ctx)
var colName string
if totalOrClosed {
colName = util.Iif(isPull, "num_pulls", "num_issues")
} else {
colName = util.Iif(isPull, "num_closed_pulls", "num_closed_issues")
}
_, err := dbSession.Incr(colName).ID(repoID).
NoAutoCondition().NoAutoTime().
Update(new(repo_model.Repository))
return err
}
// DecrRepoIssueNumbers decrements repository issue numbers.
func DecrRepoIssueNumbers(ctx context.Context, repoID int64, isPull, includeTotal, includeClosed bool) error {
if !includeTotal && !includeClosed {
return fmt.Errorf("no numbers to decrease for repo id %d", repoID)
}
dbSession := db.GetEngine(ctx)
if includeTotal {
colName := util.Iif(isPull, "num_pulls", "num_issues")
dbSession = dbSession.Decr(colName)
}
if includeClosed {
closedColName := util.Iif(isPull, "num_closed_pulls", "num_closed_issues")
dbSession = dbSession.Decr(closedColName)
}
_, err := dbSession.ID(repoID).
NoAutoCondition().NoAutoTime().
Update(new(repo_model.Repository))
return err
}
// UpdateIssueMentions updates issue-user relations for mentioned users.
func UpdateIssueMentions(ctx context.Context, issueID int64, mentions []*user_model.User) error {
if len(mentions) == 0 {

@ -181,6 +181,7 @@ func updateMilestone(ctx context.Context, m *Milestone) error {
func UpdateMilestoneCounters(ctx context.Context, id int64) error {
e := db.GetEngine(ctx)
_, err := e.ID(id).
Cols("num_issues", "num_closed_issues").
SetExpr("num_issues", builder.Select("count(*)").From("issue").Where(
builder.Eq{"milestone_id": id},
)).

@ -467,13 +467,13 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *Iss
issue.Index = idx
issue.Title = util.EllipsisDisplayString(issue.Title, 255)
issue.IsPull = true
if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{
Repo: repo,
Issue: issue,
LabelIDs: labelIDs,
Attachments: uuids,
IsPull: true,
}); err != nil {
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) {
return err

@ -21,6 +21,7 @@ func UpdateOpenMilestoneCounts(x *xorm.Engine) error {
for _, id := range openMilestoneIDs {
_, err := x.ID(id).
Cols("num_issues", "num_closed_issues").
SetExpr("num_issues", builder.Select("count(*)").From("issue").Where(
builder.Eq{"milestone_id": id},
)).

@ -53,24 +53,45 @@ func RemoveTeamRepo(ctx context.Context, teamID, repoID int64) error {
// GetTeamsWithAccessToAnyRepoUnit returns all teams in an organization that have given access level to the repository special unit.
// This function is only used for finding some teams that can be used as branch protection allowlist or reviewers, it isn't really used for access control.
// FIXME: TEAM-UNIT-PERMISSION this logic is not complete, search the fixme keyword to see more details
func GetTeamsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) ([]*Team, error) {
teams := make([]*Team, 0, 5)
func GetTeamsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) (teams []*Team, err error) {
teamIDs, err := getTeamIDsWithAccessToAnyRepoUnit(ctx, orgID, repoID, mode, unitType, unitTypesMore...)
if err != nil {
return nil, err
}
if len(teamIDs) == 0 {
return teams, nil
}
err = db.GetEngine(ctx).Where(builder.In("id", teamIDs)).OrderBy("team.name").Find(&teams)
return teams, err
}
func getTeamIDsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) (teamIDs []int64, err error) {
sub := builder.Select("team_id").From("team_unit").
Where(builder.Expr("team_unit.team_id = team.id")).
And(builder.In("team_unit.type", append([]unit.Type{unitType}, unitTypesMore...))).
And(builder.Expr("team_unit.access_mode >= ?", mode))
err := db.GetEngine(ctx).
err = db.GetEngine(ctx).
Select("team.id").
Table("team").
Join("INNER", "team_repo", "team_repo.team_id = team.id").
And("team_repo.org_id = ?", orgID).
And("team_repo.repo_id = ?", repoID).
And("team_repo.org_id = ? AND team_repo.repo_id = ?", orgID, repoID).
And(builder.Or(
builder.Expr("team.authorize >= ?", mode),
builder.In("team.id", sub),
)).
OrderBy("name").
Find(&teams)
Find(&teamIDs)
return teamIDs, err
}
return teams, err
func GetTeamUserIDsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) (userIDs []int64, err error) {
teamIDs, err := getTeamIDsWithAccessToAnyRepoUnit(ctx, orgID, repoID, mode, unitType, unitTypesMore...)
if err != nil {
return nil, err
}
if len(teamIDs) == 0 {
return userIDs, nil
}
err = db.GetEngine(ctx).Table("team_user").Select("uid").Where(builder.In("team_id", teamIDs)).Find(&userIDs)
return userIDs, err
}

@ -16,6 +16,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
@ -498,54 +499,44 @@ func HasAnyUnitAccess(ctx context.Context, userID int64, repo *repo_model.Reposi
return perm.HasAnyUnitAccess(), nil
}
// getUsersWithAccessMode returns users that have at least given access mode to the repository.
func getUsersWithAccessMode(ctx context.Context, repo *repo_model.Repository, mode perm_model.AccessMode) (_ []*user_model.User, err error) {
if err = repo.LoadOwner(ctx); err != nil {
func GetUsersWithUnitAccess(ctx context.Context, repo *repo_model.Repository, mode perm_model.AccessMode, unitType unit.Type) (users []*user_model.User, err error) {
userIDs, err := GetUserIDsWithUnitAccess(ctx, repo, mode, unitType)
if err != nil {
return nil, err
}
if len(userIDs) == 0 {
return users, nil
}
if err = db.GetEngine(ctx).In("id", userIDs.Values()).OrderBy("`name`").Find(&users); err != nil {
return nil, err
}
return users, nil
}
func GetUserIDsWithUnitAccess(ctx context.Context, repo *repo_model.Repository, mode perm_model.AccessMode, unitType unit.Type) (container.Set[int64], error) {
userIDs := container.Set[int64]{}
e := db.GetEngine(ctx)
accesses := make([]*Access, 0, 10)
if err = e.Where("repo_id = ? AND mode >= ?", repo.ID, mode).Find(&accesses); err != nil {
if err := e.Where("repo_id = ? AND mode >= ?", repo.ID, mode).Find(&accesses); err != nil {
return nil, err
}
for _, a := range accesses {
userIDs.Add(a.UserID)
}
// Leave a seat for owner itself to append later, but if owner is an organization
// and just waste 1 unit is cheaper than re-allocate memory once.
users := make([]*user_model.User, 0, len(accesses)+1)
if len(accesses) > 0 {
userIDs := make([]int64, len(accesses))
for i := 0; i < len(accesses); i++ {
userIDs[i] = accesses[i].UserID
}
if err = e.In("id", userIDs).Find(&users); err != nil {
return nil, err
}
if err := repo.LoadOwner(ctx); err != nil {
return nil, err
}
if !repo.Owner.IsOrganization() {
users = append(users, repo.Owner)
}
return users, nil
}
// GetRepoReaders returns all users that have explicit read access or higher to the repository.
func GetRepoReaders(ctx context.Context, repo *repo_model.Repository) (_ []*user_model.User, err error) {
return getUsersWithAccessMode(ctx, repo, perm_model.AccessModeRead)
}
// GetRepoWriters returns all users that have write access to the repository.
func GetRepoWriters(ctx context.Context, repo *repo_model.Repository) (_ []*user_model.User, err error) {
return getUsersWithAccessMode(ctx, repo, perm_model.AccessModeWrite)
}
// IsRepoReader returns true if user has explicit read access or higher to the repository.
func IsRepoReader(ctx context.Context, repo *repo_model.Repository, userID int64) (bool, error) {
if repo.OwnerID == userID {
return true, nil
userIDs.Add(repo.Owner.ID)
} else {
teamUserIDs, err := organization.GetTeamUserIDsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, mode, unitType)
if err != nil {
return nil, err
}
userIDs.AddMultiple(teamUserIDs...)
}
return db.GetEngine(ctx).Where("repo_id = ? AND user_id = ? AND mode >= ?", repo.ID, userID, perm_model.AccessModeRead).Get(&Access{})
return userIDs, nil
}
// CheckRepoUnitUser check whether user could visit the unit of this repository

@ -169,9 +169,9 @@ func TestGetUserRepoPermission(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
team := &organization.Team{OrgID: org.ID, LowerName: "test_team"}
require.NoError(t, db.Insert(ctx, team))
require.NoError(t, db.Insert(ctx, &organization.TeamUser{OrgID: org.ID, TeamID: team.ID, UID: user.ID}))
t.Run("DoerInTeamWithNoRepo", func(t *testing.T) {
require.NoError(t, db.Insert(ctx, &organization.TeamUser{OrgID: org.ID, TeamID: team.ID, UID: user.ID}))
perm, err := GetUserRepoPermission(ctx, repo32, user)
require.NoError(t, err)
assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode)
@ -219,6 +219,15 @@ func TestGetUserRepoPermission(t *testing.T) {
assert.Equal(t, perm_model.AccessModeNone, perm.AccessMode)
assert.Equal(t, perm_model.AccessModeNone, perm.unitsMode[unit.TypeCode])
assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeIssues])
users, err := GetUsersWithUnitAccess(ctx, repo3, perm_model.AccessModeRead, unit.TypeIssues)
require.NoError(t, err)
require.Len(t, users, 1)
assert.Equal(t, user.ID, users[0].ID)
users, err = GetUsersWithUnitAccess(ctx, repo3, perm_model.AccessModeWrite, unit.TypeIssues)
require.NoError(t, err)
require.Empty(t, users)
})
require.NoError(t, db.Insert(ctx, repo_model.Collaboration{RepoID: repo3.ID, UserID: user.ID, Mode: perm_model.AccessModeWrite}))
@ -229,5 +238,10 @@ func TestGetUserRepoPermission(t *testing.T) {
assert.Equal(t, perm_model.AccessModeWrite, perm.AccessMode)
assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeCode])
assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeIssues])
users, err := GetUsersWithUnitAccess(ctx, repo3, perm_model.AccessModeWrite, unit.TypeIssues)
require.NoError(t, err)
require.Len(t, users, 1)
assert.Equal(t, user.ID, users[0].ID)
})
}

@ -49,6 +49,19 @@ func init() {
db.RegisterModel(new(ReviewState))
}
func (rs *ReviewState) GetViewedFileCount() int {
if len(rs.UpdatedFiles) == 0 {
return 0
}
var numViewedFiles int
for _, state := range rs.UpdatedFiles {
if state == Viewed {
numViewedFiles++
}
}
return numViewedFiles
}
// GetReviewState returns the ReviewState with all given values prefilled, whether or not it exists in the database.
// If the review didn't exist before in the database, it won't afterwards either.
// The returned boolean shows whether the review exists in the database

@ -159,7 +159,7 @@ func RemoveTopicsFromRepo(ctx context.Context, repoID int64) error {
builder.In("id",
builder.Select("topic_id").From("repo_topic").Where(builder.Eq{"repo_id": repoID}),
),
).Cols("repo_count").SetExpr("repo_count", "repo_count-1").Update(&Topic{})
).Decr("repo_count").Update(&Topic{})
if err != nil {
return err
}

@ -1262,8 +1262,8 @@ func GetUserByEmail(ctx context.Context, email string) (*User, error) {
}
// Finally, if email address is the protected email address:
if strings.HasSuffix(email, "@"+setting.Service.NoReplyAddress) {
username := strings.TrimSuffix(email, "@"+setting.Service.NoReplyAddress)
if before, ok := strings.CutSuffix(email, "@"+setting.Service.NoReplyAddress); ok {
username := before
user := &User{}
has, err := db.GetEngine(ctx).Where("lower_name=?", username).Get(user)
if err != nil {

@ -5,7 +5,6 @@ package actions
import (
"bytes"
"io"
"slices"
"strings"
@ -13,6 +12,7 @@ import (
"code.gitea.io/gitea/modules/glob"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
webhook_module "code.gitea.io/gitea/modules/webhook"
"github.com/nektos/act/pkg/jobparser"
@ -77,7 +77,7 @@ func GetContentFromEntry(entry *git.TreeEntry) ([]byte, error) {
if err != nil {
return nil, err
}
content, err := io.ReadAll(f)
content, err := util.ReadWithLimit(f, 1024*1024)
_ = f.Close()
if err != nil {
return nil, err

@ -22,7 +22,7 @@ var WebAuthn *webauthn.WebAuthn
// Init initializes the WebAuthn instance from the config.
func Init() {
gob.Register(&webauthn.SessionData{})
gob.Register(&webauthn.SessionData{}) // TODO: CHI-SESSION-GOB-REGISTER.
appURL, _ := protocol.FullyQualifiedOrigin(setting.AppURL)

@ -41,8 +41,8 @@ func naturalSortAdvance(str string, pos int) (end int, isNumber bool) {
return end, isNumber
}
// NaturalSortLess compares two strings so that they could be sorted in natural order
func NaturalSortLess(s1, s2 string) bool {
// NaturalSortCompare compares two strings so that they could be sorted in natural order
func NaturalSortCompare(s1, s2 string) int {
// There is a bug in Golang's collate package: https://github.com/golang/go/issues/67997
// text/collate: CompareString(collate.Numeric) returns wrong result for "0.0" vs "1.0" #67997
// So we need to handle the number parts by ourselves
@ -55,16 +55,16 @@ func NaturalSortLess(s1, s2 string) bool {
if isNum1 && isNum2 {
if part1 != part2 {
if len(part1) != len(part2) {
return len(part1) < len(part2)
return len(part1) - len(part2)
}
return part1 < part2
return c.CompareString(part1, part2)
}
} else {
if cmp := c.CompareString(part1, part2); cmp != 0 {
return cmp < 0
return cmp
}
}
pos1, pos2 = end1, end2
}
return len(s1) < len(s2)
return len(s1) - len(s2)
}

@ -11,12 +11,10 @@ import (
func TestNaturalSortLess(t *testing.T) {
testLess := func(s1, s2 string) {
assert.True(t, NaturalSortLess(s1, s2), "s1<s2 should be true: s1=%q, s2=%q", s1, s2)
assert.False(t, NaturalSortLess(s2, s1), "s2<s1 should be false: s1=%q, s2=%q", s1, s2)
assert.Negative(t, NaturalSortCompare(s1, s2), "s1<s2 should be true: s1=%q, s2=%q", s1, s2)
}
testEqual := func(s1, s2 string) {
assert.False(t, NaturalSortLess(s1, s2), "s1<s2 should be false: s1=%q, s2=%q", s1, s2)
assert.False(t, NaturalSortLess(s2, s1), "s2<s1 should be false: s1=%q, s2=%q", s1, s2)
assert.Zero(t, NaturalSortCompare(s1, s2), "s1<s2 should be false: s1=%q, s2=%q", s1, s2)
}
testEqual("", "")

@ -173,7 +173,6 @@ func BenchmarkEntries_GetCommitsInfo(b *testing.B) {
} else if entries, err = commit.Tree.ListEntries(); err != nil {
b.Fatal(err)
}
entries.Sort()
b.ResetTimer()
b.Run(benchmark.name, func(b *testing.B) {
for b.Loop() {

@ -7,6 +7,7 @@ package git
import (
"context"
"fmt"
"io"
"code.gitea.io/gitea/modules/log"
@ -30,7 +31,11 @@ func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note)
remainingCommitID := commitID
path := ""
currentTree := notes.Tree.gogitTree
currentTree, err := notes.Tree.gogitTreeObject()
if err != nil {
return fmt.Errorf("unable to get tree object for notes commit %q: %w", notes.ID.String(), err)
}
log.Trace("Found tree with ID %q while searching for git note corresponding to the commit %q", currentTree.Entries[0].Name, commitID)
var file *object.File
for len(remainingCommitID) > 2 {

@ -1,96 +0,0 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"bytes"
"fmt"
"strconv"
"strings"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/filemode"
"github.com/go-git/go-git/v5/plumbing/hash"
"github.com/go-git/go-git/v5/plumbing/object"
)
// ParseTreeEntries parses the output of a `git ls-tree -l` command.
func ParseTreeEntries(data []byte) ([]*TreeEntry, error) {
return parseTreeEntries(data, nil)
}
func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) {
entries := make([]*TreeEntry, 0, 10)
for pos := 0; pos < len(data); {
// expect line to be of the form "<mode> <type> <sha> <space-padded-size>\t<filename>"
entry := new(TreeEntry)
entry.gogitTreeEntry = &object.TreeEntry{}
entry.ptree = ptree
if pos+6 > len(data) {
return nil, fmt.Errorf("Invalid ls-tree output: %s", string(data))
}
switch string(data[pos : pos+6]) {
case "100644":
entry.gogitTreeEntry.Mode = filemode.Regular
pos += 12 // skip over "100644 blob "
case "100755":
entry.gogitTreeEntry.Mode = filemode.Executable
pos += 12 // skip over "100755 blob "
case "120000":
entry.gogitTreeEntry.Mode = filemode.Symlink
pos += 12 // skip over "120000 blob "
case "160000":
entry.gogitTreeEntry.Mode = filemode.Submodule
pos += 14 // skip over "160000 object "
case "040000":
entry.gogitTreeEntry.Mode = filemode.Dir
pos += 12 // skip over "040000 tree "
default:
return nil, fmt.Errorf("unknown type: %v", string(data[pos:pos+6]))
}
// in hex format, not byte format ....
if pos+hash.Size*2 > len(data) {
return nil, fmt.Errorf("Invalid ls-tree output: %s", string(data))
}
var err error
entry.ID, err = NewIDFromString(string(data[pos : pos+hash.Size*2]))
if err != nil {
return nil, fmt.Errorf("invalid ls-tree output: %w", err)
}
entry.gogitTreeEntry.Hash = plumbing.Hash(entry.ID.RawValue())
pos += 41 // skip over sha and trailing space
end := pos + bytes.IndexByte(data[pos:], '\t')
if end < pos {
return nil, fmt.Errorf("Invalid ls-tree -l output: %s", string(data))
}
entry.size, _ = strconv.ParseInt(strings.TrimSpace(string(data[pos:end])), 10, 64)
entry.sized = true
pos = end + 1
end = pos + bytes.IndexByte(data[pos:], '\n')
if end < pos {
return nil, fmt.Errorf("Invalid ls-tree output: %s", string(data))
}
// In case entry name is surrounded by double quotes(it happens only in git-shell).
if data[pos] == '"' {
var err error
entry.gogitTreeEntry.Name, err = strconv.Unquote(string(data[pos:end]))
if err != nil {
return nil, fmt.Errorf("Invalid ls-tree output: %w", err)
}
} else {
entry.gogitTreeEntry.Name = string(data[pos:end])
}
pos = end + 1
entries = append(entries, entry)
}
return entries, nil
}

@ -1,78 +0,0 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"fmt"
"testing"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/filemode"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/stretchr/testify/assert"
)
func TestParseTreeEntries(t *testing.T) {
testCases := []struct {
Input string
Expected []*TreeEntry
}{
{
Input: "",
Expected: []*TreeEntry{},
},
{
Input: "100644 blob 61ab7345a1a3bbc590068ccae37b8515cfc5843c 1022\texample/file2.txt\n",
Expected: []*TreeEntry{
{
ID: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"),
gogitTreeEntry: &object.TreeEntry{
Hash: plumbing.Hash(MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c").RawValue()),
Name: "example/file2.txt",
Mode: filemode.Regular,
},
size: 1022,
sized: true,
},
},
},
{
Input: "120000 blob 61ab7345a1a3bbc590068ccae37b8515cfc5843c 234131\t\"example/\\n.txt\"\n" +
"040000 tree 1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8 -\texample\n",
Expected: []*TreeEntry{
{
ID: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"),
gogitTreeEntry: &object.TreeEntry{
Hash: plumbing.Hash(MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c").RawValue()),
Name: "example/\n.txt",
Mode: filemode.Symlink,
},
size: 234131,
sized: true,
},
{
ID: MustIDFromString("1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8"),
sized: true,
gogitTreeEntry: &object.TreeEntry{
Hash: plumbing.Hash(MustIDFromString("1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8").RawValue()),
Name: "example",
Mode: filemode.Dir,
},
},
},
},
}
for _, testCase := range testCases {
entries, err := ParseTreeEntries([]byte(testCase.Input))
assert.NoError(t, err)
if len(entries) > 1 {
fmt.Println(testCase.Expected[0].ID)
fmt.Println(entries[0].ID)
}
assert.EqualValues(t, testCase.Expected, entries)
}
}

@ -1,8 +1,6 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
import (

@ -1,8 +1,6 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !gogit
package git
import (

@ -107,7 +107,7 @@ func (repo *Repository) getCommit(id ObjectID) (*Commit, error) {
}
commit.Tree.ID = ParseGogitHash(tree.Hash)
commit.Tree.gogitTree = tree
commit.Tree.resolvedGogitTreeObject = tree
return commit, nil
}

@ -26,7 +26,7 @@ func (repo *Repository) getTree(id ObjectID) (*Tree, error) {
}
tree := NewTree(repo, id)
tree.gogitTree = gogitTree
tree.resolvedGogitTreeObject = gogitTree
return tree, nil
}

@ -11,11 +11,21 @@ import (
"code.gitea.io/gitea/modules/git/gitcmd"
)
type TreeCommon struct {
ID ObjectID
ResolvedID ObjectID
repo *Repository
ptree *Tree // parent tree
}
// NewTree create a new tree according the repository and tree id
func NewTree(repo *Repository, id ObjectID) *Tree {
return &Tree{
ID: id,
repo: repo,
TreeCommon: TreeCommon{
ID: id,
repo: repo,
},
}
}

@ -11,22 +11,16 @@ import (
"strings"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/filemode"
"github.com/go-git/go-git/v5/plumbing/object"
)
// GetTreeEntryByPath get the tree entries according the sub dir
func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) {
if len(relpath) == 0 {
return &TreeEntry{
ID: t.ID,
// Type: ObjectTree,
ptree: t,
gogitTreeEntry: &object.TreeEntry{
Name: "",
Mode: filemode.Dir,
Hash: plumbing.Hash(t.ID.RawValue()),
},
ID: t.ID,
ptree: t,
name: "",
entryMode: EntryModeTree,
}, nil
}

@ -6,12 +6,60 @@ package git
import (
"path"
"sort"
"slices"
"strings"
"code.gitea.io/gitea/modules/util"
)
// TreeEntry the leaf in the git tree
type TreeEntry struct {
ID ObjectID
name string
ptree *Tree
entryMode EntryMode
size int64
sized bool
}
// Name returns the name of the entry (base name)
func (te *TreeEntry) Name() string {
return te.name
}
// Mode returns the mode of the entry
func (te *TreeEntry) Mode() EntryMode {
return te.entryMode
}
// IsSubModule if the entry is a submodule
func (te *TreeEntry) IsSubModule() bool {
return te.entryMode.IsSubModule()
}
// IsDir if the entry is a sub dir
func (te *TreeEntry) IsDir() bool {
return te.entryMode.IsDir()
}
// IsLink if the entry is a symlink
func (te *TreeEntry) IsLink() bool {
return te.entryMode.IsLink()
}
// IsRegular if the entry is a regular file
func (te *TreeEntry) IsRegular() bool {
return te.entryMode.IsRegular()
}
// IsExecutable if the entry is an executable file (not necessarily binary)
func (te *TreeEntry) IsExecutable() bool {
return te.entryMode.IsExecutable()
}
// Type returns the type of the entry (commit, tree, blob)
func (te *TreeEntry) Type() string {
switch te.Mode() {
@ -109,49 +157,16 @@ func (te *TreeEntry) GetSubJumpablePathName() string {
// Entries a list of entry
type Entries []*TreeEntry
type customSortableEntries struct {
Comparer func(s1, s2 string) bool
Entries
}
var sorter = []func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool{
func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool {
return (t1.IsDir() || t1.IsSubModule()) && !t2.IsDir() && !t2.IsSubModule()
},
func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool {
return cmp(t1.Name(), t2.Name())
},
}
func (ctes customSortableEntries) Len() int { return len(ctes.Entries) }
func (ctes customSortableEntries) Swap(i, j int) {
ctes.Entries[i], ctes.Entries[j] = ctes.Entries[j], ctes.Entries[i]
}
func (ctes customSortableEntries) Less(i, j int) bool {
t1, t2 := ctes.Entries[i], ctes.Entries[j]
var k int
for k = 0; k < len(sorter)-1; k++ {
s := sorter[k]
switch {
case s(t1, t2, ctes.Comparer):
return true
case s(t2, t1, ctes.Comparer):
return false
}
}
return sorter[k](t1, t2, ctes.Comparer)
}
// Sort sort the list of entry
func (tes Entries) Sort() {
sort.Sort(customSortableEntries{func(s1, s2 string) bool {
return s1 < s2
}, tes})
}
// CustomSort customizable string comparing sort entry list
func (tes Entries) CustomSort(cmp func(s1, s2 string) bool) {
sort.Sort(customSortableEntries{cmp, tes})
func (tes Entries) CustomSort(cmp func(s1, s2 string) int) {
slices.SortFunc(tes, func(a, b *TreeEntry) int {
s1Dir, s2Dir := a.IsDir() || a.IsSubModule(), b.IsDir() || b.IsSubModule()
if s1Dir != s2Dir {
if s1Dir {
return -1
}
return 1
}
return cmp(a.Name(), b.Name())
})
}

@ -12,25 +12,21 @@ import (
"github.com/go-git/go-git/v5/plumbing/object"
)
// TreeEntry the leaf in the git tree
type TreeEntry struct {
ID ObjectID
gogitTreeEntry *object.TreeEntry
ptree *Tree
size int64
sized bool
// gogitFileModeToEntryMode converts go-git filemode to EntryMode
func gogitFileModeToEntryMode(mode filemode.FileMode) EntryMode {
return EntryMode(mode)
}
// Name returns the name of the entry
func (te *TreeEntry) Name() string {
return te.gogitTreeEntry.Name
func entryModeToGogitFileMode(mode EntryMode) filemode.FileMode {
return filemode.FileMode(mode)
}
// Mode returns the mode of the entry
func (te *TreeEntry) Mode() EntryMode {
return EntryMode(te.gogitTreeEntry.Mode)
func (te *TreeEntry) toGogitTreeEntry() *object.TreeEntry {
return &object.TreeEntry{
Name: te.name,
Mode: entryModeToGogitFileMode(te.entryMode),
Hash: plumbing.Hash(te.ID.RawValue()),
}
}
// Size returns the size of the entry
@ -41,7 +37,11 @@ func (te *TreeEntry) Size() int64 {
return te.size
}
file, err := te.ptree.gogitTree.TreeEntryFile(te.gogitTreeEntry)
ptreeGogitTree, err := te.ptree.gogitTreeObject()
if err != nil {
return 0
}
file, err := ptreeGogitTree.TreeEntryFile(te.toGogitTreeEntry())
if err != nil {
return 0
}
@ -51,40 +51,15 @@ func (te *TreeEntry) Size() int64 {
return te.size
}
// IsSubModule if the entry is a submodule
func (te *TreeEntry) IsSubModule() bool {
return te.gogitTreeEntry.Mode == filemode.Submodule
}
// IsDir if the entry is a sub dir
func (te *TreeEntry) IsDir() bool {
return te.gogitTreeEntry.Mode == filemode.Dir
}
// IsLink if the entry is a symlink
func (te *TreeEntry) IsLink() bool {
return te.gogitTreeEntry.Mode == filemode.Symlink
}
// IsRegular if the entry is a regular file
func (te *TreeEntry) IsRegular() bool {
return te.gogitTreeEntry.Mode == filemode.Regular
}
// IsExecutable if the entry is an executable file (not necessarily binary)
func (te *TreeEntry) IsExecutable() bool {
return te.gogitTreeEntry.Mode == filemode.Executable
}
// Blob returns the blob object the entry
func (te *TreeEntry) Blob() *Blob {
encodedObj, err := te.ptree.repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, te.gogitTreeEntry.Hash)
encodedObj, err := te.ptree.repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, te.toGogitTreeEntry().Hash)
if err != nil {
return nil
}
return &Blob{
ID: ParseGogitHash(te.gogitTreeEntry.Hash),
ID: te.ID,
gogitEncodedObj: encodedObj,
name: te.Name(),
}

@ -0,0 +1,27 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"testing"
"github.com/go-git/go-git/v5/plumbing/filemode"
"github.com/stretchr/testify/assert"
)
func TestEntryGogit(t *testing.T) {
cases := map[EntryMode]filemode.FileMode{
EntryModeBlob: filemode.Regular,
EntryModeCommit: filemode.Submodule,
EntryModeExec: filemode.Executable,
EntryModeSymlink: filemode.Symlink,
EntryModeTree: filemode.Dir,
}
for emode, fmode := range cases {
assert.EqualValues(t, fmode, entryModeToGogitFileMode(emode))
assert.EqualValues(t, emode, gogitFileModeToEntryMode(fmode))
}
}

@ -7,27 +7,6 @@ package git
import "code.gitea.io/gitea/modules/log"
// TreeEntry the leaf in the git tree
type TreeEntry struct {
ID ObjectID
ptree *Tree
entryMode EntryMode
name string
size int64
sized bool
}
// Name returns the name of the entry (base name)
func (te *TreeEntry) Name() string {
return te.name
}
// Mode returns the mode of the entry
func (te *TreeEntry) Mode() EntryMode {
return te.entryMode
}
// Size returns the size of the entry
func (te *TreeEntry) Size() int64 {
if te.IsDir() {
@ -57,31 +36,6 @@ func (te *TreeEntry) Size() int64 {
return te.size
}
// IsSubModule if the entry is a submodule
func (te *TreeEntry) IsSubModule() bool {
return te.entryMode.IsSubModule()
}
// IsDir if the entry is a sub dir
func (te *TreeEntry) IsDir() bool {
return te.entryMode.IsDir()
}
// IsLink if the entry is a symlink
func (te *TreeEntry) IsLink() bool {
return te.entryMode.IsLink()
}
// IsRegular if the entry is a regular file
func (te *TreeEntry) IsRegular() bool {
return te.entryMode.IsRegular()
}
// IsExecutable if the entry is an executable file (not necessarily binary)
func (te *TreeEntry) IsExecutable() bool {
return te.entryMode.IsExecutable()
}
// Blob returns the blob object the entry
func (te *TreeEntry) Blob() *Blob {
return &Blob{

@ -1,55 +1,29 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build gogit
package git
import (
"math/rand/v2"
"slices"
"strings"
"testing"
"github.com/go-git/go-git/v5/plumbing/filemode"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/stretchr/testify/assert"
)
func getTestEntries() Entries {
return Entries{
&TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v1.0", Mode: filemode.Dir}},
&TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.0", Mode: filemode.Dir}},
&TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.1", Mode: filemode.Dir}},
&TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.12", Mode: filemode.Dir}},
&TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.2", Mode: filemode.Dir}},
&TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v12.0", Mode: filemode.Dir}},
&TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "abc", Mode: filemode.Regular}},
&TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "bcd", Mode: filemode.Regular}},
}
}
func TestEntriesSort(t *testing.T) {
entries := getTestEntries()
entries.Sort()
assert.Equal(t, "v1.0", entries[0].Name())
assert.Equal(t, "v12.0", entries[1].Name())
assert.Equal(t, "v2.0", entries[2].Name())
assert.Equal(t, "v2.1", entries[3].Name())
assert.Equal(t, "v2.12", entries[4].Name())
assert.Equal(t, "v2.2", entries[5].Name())
assert.Equal(t, "abc", entries[6].Name())
assert.Equal(t, "bcd", entries[7].Name())
}
func TestEntriesCustomSort(t *testing.T) {
entries := getTestEntries()
entries.CustomSort(func(s1, s2 string) bool {
return s1 > s2
})
assert.Equal(t, "v2.2", entries[0].Name())
assert.Equal(t, "v2.12", entries[1].Name())
assert.Equal(t, "v2.1", entries[2].Name())
assert.Equal(t, "v2.0", entries[3].Name())
assert.Equal(t, "v12.0", entries[4].Name())
assert.Equal(t, "v1.0", entries[5].Name())
assert.Equal(t, "bcd", entries[6].Name())
assert.Equal(t, "abc", entries[7].Name())
entries := Entries{
&TreeEntry{name: "a-dir", entryMode: EntryModeTree},
&TreeEntry{name: "a-submodule", entryMode: EntryModeCommit},
&TreeEntry{name: "b-dir", entryMode: EntryModeTree},
&TreeEntry{name: "b-submodule", entryMode: EntryModeCommit},
&TreeEntry{name: "a-file", entryMode: EntryModeBlob},
&TreeEntry{name: "b-file", entryMode: EntryModeBlob},
}
expected := slices.Clone(entries)
rand.Shuffle(len(entries), func(i, j int) { entries[i], entries[j] = entries[j], entries[i] })
assert.NotEqual(t, expected, entries)
entries.CustomSort(strings.Compare)
assert.Equal(t, expected, entries)
}

@ -15,41 +15,34 @@ import (
// Tree represents a flat directory listing.
type Tree struct {
ID ObjectID
ResolvedID ObjectID
repo *Repository
TreeCommon
gogitTree *object.Tree
// parent tree
ptree *Tree
resolvedGogitTreeObject *object.Tree
}
func (t *Tree) loadTreeObject() error {
gogitTree, err := t.repo.gogitRepo.TreeObject(plumbing.Hash(t.ID.RawValue()))
if err != nil {
return err
func (t *Tree) gogitTreeObject() (_ *object.Tree, err error) {
if t.resolvedGogitTreeObject == nil {
t.resolvedGogitTreeObject, err = t.repo.gogitRepo.TreeObject(plumbing.Hash(t.ID.RawValue()))
if err != nil {
return nil, err
}
}
t.gogitTree = gogitTree
return nil
return t.resolvedGogitTreeObject, nil
}
// ListEntries returns all entries of current tree.
func (t *Tree) ListEntries() (Entries, error) {
if t.gogitTree == nil {
err := t.loadTreeObject()
if err != nil {
return nil, err
}
gogitTree, err := t.gogitTreeObject()
if err != nil {
return nil, err
}
entries := make([]*TreeEntry, len(t.gogitTree.Entries))
for i, entry := range t.gogitTree.Entries {
entries := make([]*TreeEntry, len(gogitTree.Entries))
for i, gogitTreeEntry := range gogitTree.Entries {
entries[i] = &TreeEntry{
ID: ParseGogitHash(entry.Hash),
gogitTreeEntry: &t.gogitTree.Entries[i],
ptree: t,
ID: ParseGogitHash(gogitTreeEntry.Hash),
ptree: t,
name: gogitTreeEntry.Name,
entryMode: gogitFileModeToEntryMode(gogitTreeEntry.Mode),
}
}
@ -57,37 +50,28 @@ func (t *Tree) ListEntries() (Entries, error) {
}
// ListEntriesRecursiveWithSize returns all entries of current tree recursively including all subtrees
func (t *Tree) ListEntriesRecursiveWithSize() (Entries, error) {
if t.gogitTree == nil {
err := t.loadTreeObject()
if err != nil {
return nil, err
}
func (t *Tree) ListEntriesRecursiveWithSize() (entries Entries, _ error) {
gogitTree, err := t.gogitTreeObject()
if err != nil {
return nil, err
}
var entries []*TreeEntry
seen := map[plumbing.Hash]bool{}
walker := object.NewTreeWalker(t.gogitTree, true, seen)
walker := object.NewTreeWalker(gogitTree, true, nil)
for {
_, entry, err := walker.Next()
fullName, gogitTreeEntry, err := walker.Next()
if err == io.EOF {
break
}
if err != nil {
} else if err != nil {
return nil, err
}
if seen[entry.Hash] {
continue
}
convertedEntry := &TreeEntry{
ID: ParseGogitHash(entry.Hash),
gogitTreeEntry: &entry,
ptree: t,
ID: ParseGogitHash(gogitTreeEntry.Hash),
name: fullName, // FIXME: the "name" field is abused, here it is a full path
ptree: t, // FIXME: this ptree is not right, fortunately it isn't really used
entryMode: gogitFileModeToEntryMode(gogitTreeEntry.Mode),
}
entries = append(entries, convertedEntry)
}
return entries, nil
}

@ -14,18 +14,10 @@ import (
// Tree represents a flat directory listing.
type Tree struct {
ID ObjectID
ResolvedID ObjectID
repo *Repository
// parent tree
ptree *Tree
TreeCommon
entries Entries
entriesParsed bool
entriesRecursive Entries
entriesRecursiveParsed bool
}
// ListEntries returns all entries of current tree.
@ -94,10 +86,6 @@ func (t *Tree) ListEntries() (Entries, error) {
// listEntriesRecursive returns all entries of current tree recursively including all subtrees
// extraArgs could be "-l" to get the size, which is slower
func (t *Tree) listEntriesRecursive(extraArgs gitcmd.TrustedCmdArgs) (Entries, error) {
if t.entriesRecursiveParsed {
return t.entriesRecursive, nil
}
stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-t", "-r").
AddArguments(extraArgs...).
AddDynamicArguments(t.ID.String()).
@ -107,13 +95,9 @@ func (t *Tree) listEntriesRecursive(extraArgs gitcmd.TrustedCmdArgs) (Entries, e
return nil, runErr
}
var err error
t.entriesRecursive, err = parseTreeEntries(stdout, t)
if err == nil {
t.entriesRecursiveParsed = true
}
return t.entriesRecursive, err
// FIXME: the "name" field is abused, here it is a full path
// FIXME: this ptree is not right, fortunately it isn't really used
return parseTreeEntries(stdout, t)
}
// ListEntriesRecursiveFast returns all entries of current tree recursively including all subtrees, no size

@ -5,7 +5,6 @@ package template
import (
"fmt"
"io"
"path"
"strconv"
@ -76,7 +75,7 @@ func unmarshalFromEntry(entry *git.TreeEntry, filename string) (*api.IssueTempla
}
defer r.Close()
content, err := io.ReadAll(r)
content, err := util.ReadWithLimit(r, 1024*1024)
if err != nil {
return nil, fmt.Errorf("read all: %w", err)
}

@ -4,8 +4,13 @@
package composer
import (
"archive/tar"
"archive/zip"
"compress/bzip2"
"compress/gzip"
"errors"
"io"
"io/fs"
"path"
"regexp"
"strings"
@ -29,8 +34,10 @@ var (
ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
)
// Package represents a Composer package
type Package struct {
// PackageInfo represents Composer package info
type PackageInfo struct {
Filename string
Name string
Version string
Type string
@ -44,7 +51,7 @@ type Metadata struct {
Description string `json:"description,omitempty"`
Readme string `json:"readme,omitempty"`
Keywords []string `json:"keywords,omitempty"`
Comments Comments `json:"_comments,omitempty"`
Comments Comments `json:"_comment,omitempty"`
Homepage string `json:"homepage,omitempty"`
License Licenses `json:"license,omitempty"`
Authors []Author `json:"authors,omitempty"`
@ -75,7 +82,7 @@ func (l *Licenses) UnmarshalJSON(data []byte) error {
if err := json.Unmarshal(data, &values); err != nil {
return err
}
*l = Licenses(values)
*l = values
}
return nil
}
@ -97,7 +104,7 @@ func (c *Comments) UnmarshalJSON(data []byte) error {
if err := json.Unmarshal(data, &values); err != nil {
return err
}
*c = Comments(values)
*c = values
}
return nil
}
@ -111,39 +118,121 @@ type Author struct {
var nameMatch = regexp.MustCompile(`\A[a-z0-9]([_\.-]?[a-z0-9]+)*/[a-z0-9](([_\.]?|-{0,2})[a-z0-9]+)*\z`)
// ParsePackage parses the metadata of a Composer package file
func ParsePackage(r io.ReaderAt, size int64) (*Package, error) {
archive, err := zip.NewReader(r, size)
type ReadSeekAt interface {
io.Reader
io.ReaderAt
io.Seeker
Size() int64
}
func readPackageFileZip(r ReadSeekAt, filename string, limit int) ([]byte, error) {
archive, err := zip.NewReader(r, r.Size())
if err != nil {
return nil, err
}
for _, file := range archive.File {
if strings.Count(file.Name, "/") > 1 {
continue
}
if strings.HasSuffix(strings.ToLower(file.Name), "composer.json") {
filePath := path.Clean(file.Name)
if util.AsciiEqualFold(filePath, filename) {
f, err := archive.Open(file.Name)
if err != nil {
return nil, err
}
defer f.Close()
return ParseComposerFile(archive, path.Dir(file.Name), f)
return util.ReadWithLimit(f, limit)
}
}
return nil, fs.ErrNotExist
}
func readPackageFileTar(r io.Reader, filename string, limit int) ([]byte, error) {
tarReader := tar.NewReader(r)
for {
header, err := tarReader.Next()
if err == io.EOF {
break
} else if err != nil {
return nil, err
}
filePath := path.Clean(header.Name)
if util.AsciiEqualFold(filePath, filename) {
return util.ReadWithLimit(tarReader, limit)
}
}
return nil, ErrMissingComposerFile
return nil, fs.ErrNotExist
}
// ParseComposerFile parses a composer.json file to retrieve the metadata of a Composer package
func ParseComposerFile(archive *zip.Reader, pathPrefix string, r io.Reader) (*Package, error) {
const (
pkgExtZip = ".zip"
pkgExtTarGz = ".tar.gz"
pkgExtTarBz2 = ".tar.bz2"
)
func detectPackageExtName(r ReadSeekAt) (string, error) {
headBytes := make([]byte, 4)
_, err := r.ReadAt(headBytes, 0)
if err != nil {
return "", err
}
_, err = r.Seek(0, io.SeekStart)
if err != nil {
return "", err
}
switch {
case headBytes[0] == 'P' && headBytes[1] == 'K':
return pkgExtZip, nil
case string(headBytes[:3]) == "BZh":
return pkgExtTarBz2, nil
case headBytes[0] == 0x1f && headBytes[1] == 0x8b:
return pkgExtTarGz, nil
}
return "", util.NewInvalidArgumentErrorf("not a valid package file")
}
func readPackageFile(pkgExt string, r ReadSeekAt, filename string, limit int) ([]byte, error) {
_, err := r.Seek(0, io.SeekStart)
if err != nil {
return nil, err
}
switch pkgExt {
case pkgExtZip:
return readPackageFileZip(r, filename, limit)
case pkgExtTarBz2:
bzip2Reader := bzip2.NewReader(r)
return readPackageFileTar(bzip2Reader, filename, limit)
case pkgExtTarGz:
gzReader, err := gzip.NewReader(r)
if err != nil {
return nil, err
}
return readPackageFileTar(gzReader, filename, limit)
}
return nil, util.NewInvalidArgumentErrorf("not a valid package file")
}
// ParsePackage parses the metadata of a Composer package file
func ParsePackage(r ReadSeekAt, optVersion ...string) (*PackageInfo, error) {
pkgExt, err := detectPackageExtName(r)
if err != nil {
return nil, err
}
dataComposerJSON, err := readPackageFile(pkgExt, r, "composer.json", 10*1024*1024)
if errors.Is(err, fs.ErrNotExist) {
return nil, ErrMissingComposerFile
} else if err != nil {
return nil, err
}
var cj struct {
Name string `json:"name"`
Version string `json:"version"`
Type string `json:"type"`
Metadata
}
if err := json.NewDecoder(r).Decode(&cj); err != nil {
if err := json.Unmarshal(dataComposerJSON, &cj); err != nil {
return nil, err
}
@ -151,6 +240,9 @@ func ParseComposerFile(archive *zip.Reader, pathPrefix string, r io.Reader) (*Pa
return nil, ErrInvalidName
}
if cj.Version == "" {
cj.Version = util.OptionalArg(optVersion)
}
if cj.Version != "" {
if _, err := version.NewSemver(cj.Version); err != nil {
return nil, ErrInvalidVersion
@ -168,17 +260,23 @@ func ParseComposerFile(archive *zip.Reader, pathPrefix string, r io.Reader) (*Pa
if cj.Readme == "" {
cj.Readme = "README.md"
}
f, err := archive.Open(path.Join(pathPrefix, cj.Readme))
if err == nil {
// 10kb limit for readme content
buf, _ := io.ReadAll(io.LimitReader(f, 10*1024))
cj.Readme = string(buf)
_ = f.Close()
} else {
dataReadmeMd, _ := readPackageFile(pkgExt, r, cj.Readme, 10*1024)
// FIXME: legacy problem, the "Readme" field is abused, it should always be the path to the readme file
if len(dataReadmeMd) == 0 {
cj.Readme = ""
} else {
cj.Readme = string(dataReadmeMd)
}
return &Package{
// FIXME: legacy format: strings.ToLower(fmt.Sprintf("%s.%s.zip", strings.ReplaceAll(cp.Name, "/", "-"), cp.Version)), doesn't read good
pkgFilename := strings.ReplaceAll(cj.Name, "/", "-")
if cj.Version != "" {
pkgFilename += "." + cj.Version
}
pkgFilename += pkgExt
return &PackageInfo{
Filename: pkgFilename,
Name: cj.Name,
Version: cj.Version,
Type: cj.Type,

@ -4,14 +4,19 @@
package composer
import (
"archive/tar"
"archive/zip"
"bytes"
"compress/gzip"
"io"
"strings"
"testing"
"code.gitea.io/gitea/modules/json"
"github.com/dsnet/compress/bzip2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
@ -26,8 +31,10 @@ const (
license = "MIT"
)
const composerContent = `{
func buildComposerContent(version string) string {
return `{
"name": "` + name + `",
"version": "` + version + `",
"description": "` + description + `",
"type": "` + packageType + `",
"license": "` + license + `",
@ -44,8 +51,9 @@ const composerContent = `{
"require": {
"php": ">=7.2 || ^8.0"
},
"_comments": "` + comments + `"
"_comment": "` + comments + `"
}`
}
func TestLicenseUnmarshal(t *testing.T) {
var l Licenses
@ -73,16 +81,34 @@ func TestParsePackage(t *testing.T) {
archive := zip.NewWriter(&buf)
for name, content := range files {
w, _ := archive.Create(name)
w.Write([]byte(content))
_, _ = w.Write([]byte(content))
}
_ = archive.Close()
return buf.Bytes()
}
createArchiveTar := func(comp func(io.Writer) io.WriteCloser, files map[string]string) []byte {
var buf bytes.Buffer
w := comp(&buf)
archive := tar.NewWriter(w)
for name, content := range files {
hdr := &tar.Header{
Name: name,
Mode: 0o600,
Size: int64(len(content)),
}
_ = archive.WriteHeader(hdr)
_, _ = archive.Write([]byte(content))
}
archive.Close()
_ = w.Close()
_ = archive.Close()
return buf.Bytes()
}
t.Run("MissingComposerFile", func(t *testing.T) {
data := createArchive(map[string]string{"dummy.txt": ""})
cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
cp, err := ParsePackage(bytes.NewReader(data))
assert.Nil(t, cp)
assert.ErrorIs(t, err, ErrMissingComposerFile)
})
@ -90,7 +116,7 @@ func TestParsePackage(t *testing.T) {
t.Run("MissingComposerFileInRoot", func(t *testing.T) {
data := createArchive(map[string]string{"sub/sub/composer.json": ""})
cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
cp, err := ParsePackage(bytes.NewReader(data))
assert.Nil(t, cp)
assert.ErrorIs(t, err, ErrMissingComposerFile)
})
@ -98,7 +124,7 @@ func TestParsePackage(t *testing.T) {
t.Run("InvalidComposerFile", func(t *testing.T) {
data := createArchive(map[string]string{"composer.json": ""})
cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
cp, err := ParsePackage(bytes.NewReader(data))
assert.Nil(t, cp)
assert.Error(t, err)
})
@ -106,7 +132,7 @@ func TestParsePackage(t *testing.T) {
t.Run("InvalidPackageName", func(t *testing.T) {
data := createArchive(map[string]string{"composer.json": "{}"})
cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
cp, err := ParsePackage(bytes.NewReader(data))
assert.Nil(t, cp)
assert.ErrorIs(t, err, ErrInvalidName)
})
@ -114,7 +140,7 @@ func TestParsePackage(t *testing.T) {
t.Run("InvalidPackageVersion", func(t *testing.T) {
data := createArchive(map[string]string{"composer.json": `{"name": "gitea/composer-package", "version": "1.a.3"}`})
cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
cp, err := ParsePackage(bytes.NewReader(data))
assert.Nil(t, cp)
assert.ErrorIs(t, err, ErrInvalidVersion)
})
@ -122,22 +148,21 @@ func TestParsePackage(t *testing.T) {
t.Run("InvalidReadmePath", func(t *testing.T) {
data := createArchive(map[string]string{"composer.json": `{"name": "gitea/composer-package", "readme": "sub/README.md"}`})
cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
cp, err := ParsePackage(bytes.NewReader(data))
assert.NoError(t, err)
assert.NotNil(t, cp)
assert.Empty(t, cp.Metadata.Readme)
})
t.Run("Valid", func(t *testing.T) {
data := createArchive(map[string]string{"composer.json": composerContent, "README.md": readme})
cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
assert.NoError(t, err)
assertValidPackage := func(t *testing.T, data []byte, version, filename string) {
cp, err := ParsePackage(bytes.NewReader(data))
require.NoError(t, err)
assert.NotNil(t, cp)
assert.Equal(t, filename, cp.Filename)
assert.Equal(t, name, cp.Name)
assert.Empty(t, cp.Version)
assert.Equal(t, version, cp.Version)
assert.Equal(t, description, cp.Metadata.Description)
assert.Equal(t, readme, cp.Metadata.Readme)
assert.Len(t, cp.Metadata.Comments, 1)
@ -149,5 +174,25 @@ func TestParsePackage(t *testing.T) {
assert.Equal(t, packageType, cp.Type)
assert.Len(t, cp.Metadata.License, 1)
assert.Equal(t, license, cp.Metadata.License[0])
}
t.Run("ValidZip", func(t *testing.T) {
data := createArchive(map[string]string{"composer.json": buildComposerContent(""), "README.md": readme})
assertValidPackage(t, data, "", "gitea-composer-package.zip")
})
t.Run("ValidTarBz2", func(t *testing.T) {
data := createArchiveTar(func(w io.Writer) io.WriteCloser {
bz2Writer, _ := bzip2.NewWriter(w, nil)
return bz2Writer
}, map[string]string{"composer.json": buildComposerContent("1.0"), "README.md": readme})
assertValidPackage(t, data, "1.0", "gitea-composer-package.1.0.tar.bz2")
})
t.Run("ValidTarGz", func(t *testing.T) {
data := createArchiveTar(func(w io.Writer) io.WriteCloser {
return gzip.NewWriter(w)
}, map[string]string{"composer.json": buildComposerContent(""), "README.md": readme})
assertValidPackage(t, data, "", "gitea-composer-package.tar.gz")
})
}

@ -216,7 +216,7 @@ func ParseNuspecMetaData(archive *zip.Reader, r io.Reader) (*Package, error) {
if p.Metadata.Readme != "" {
f, err := archive.Open(p.Metadata.Readme)
if err == nil {
buf, _ := io.ReadAll(f)
buf, _ := util.ReadWithLimit(f, 1024*1024)
m.Readme = string(buf)
_ = f.Close()
}

@ -89,7 +89,7 @@ func ParsePackage(r io.Reader) (*Package, error) {
return nil, err
}
} else if strings.EqualFold(hd.Name, "readme.md") {
data, err := io.ReadAll(tr)
data, err := util.ReadWithLimit(tr, 1024*1024)
if err != nil {
return nil, err
}

@ -25,6 +25,7 @@ const (
EnvKeyID = "GITEA_KEY_ID" // public key ID
EnvDeployKeyID = "GITEA_DEPLOY_KEY_ID"
EnvPRID = "GITEA_PR_ID"
EnvPRIndex = "GITEA_PR_INDEX" // not used by Gitea at the moment, it is for custom git hooks
EnvPushTrigger = "GITEA_PUSH_TRIGGER"
EnvIsInternal = "GITEA_INTERNAL_PUSH"
EnvAppURL = "GITEA_ROOT_URL"
@ -50,11 +51,11 @@ func InternalPushingEnvironment(doer *user_model.User, repo *repo_model.Reposito
// PushingEnvironment returns an os environment to allow hooks to work on push
func PushingEnvironment(doer *user_model.User, repo *repo_model.Repository) []string {
return FullPushingEnvironment(doer, doer, repo, repo.Name, 0)
return FullPushingEnvironment(doer, doer, repo, repo.Name, 0, 0)
}
// FullPushingEnvironment returns an os environment to allow hooks to work on push
func FullPushingEnvironment(author, committer *user_model.User, repo *repo_model.Repository, repoName string, prID int64) []string {
func FullPushingEnvironment(author, committer *user_model.User, repo *repo_model.Repository, repoName string, prID, prIndex int64) []string {
isWiki := "false"
if strings.HasSuffix(repoName, ".wiki") {
isWiki = "true"
@ -75,6 +76,7 @@ func FullPushingEnvironment(author, committer *user_model.User, repo *repo_model
EnvPusherID+"="+strconv.FormatInt(committer.ID, 10),
EnvRepoID+"="+strconv.FormatInt(repo.ID, 10),
EnvPRID+"="+strconv.FormatInt(prID, 10),
EnvPRIndex+"="+strconv.FormatInt(prIndex, 10),
EnvAppURL+"="+setting.AppURL,
"SSH_ORIGINAL_COMMAND=gitea-internal",
)

@ -96,6 +96,7 @@ var OAuth2 = struct {
InvalidateRefreshTokens bool
JWTSigningAlgorithm string `ini:"JWT_SIGNING_ALGORITHM"`
JWTSigningPrivateKeyFile string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"`
JWTClaimIssuer string `ini:"JWT_CLAIM_ISSUER"`
MaxTokenLength int
DefaultApplications []string
}{

@ -235,9 +235,6 @@ func loadServerFrom(rootCfg ConfigProvider) {
deprecatedSetting(rootCfg, "server", "LETSENCRYPT_EMAIL", "server", "ACME_EMAIL", "v1.19.0")
AcmeEmail = sec.Key("LETSENCRYPT_EMAIL").MustString("")
}
if AcmeEmail == "" {
log.Fatal("ACME Email is not set (ACME_EMAIL).")
}
} else {
CertFile = sec.Key("CERT_FILE").String()
KeyFile = sec.Key("KEY_FILE").String()

@ -250,6 +250,7 @@ func (a *AzureBlobStorage) Delete(path string) error {
func (a *AzureBlobStorage) URL(path, name, _ string, reqParams url.Values) (*url.URL, error) {
blobClient := a.getBlobClient(path)
// TODO: OBJECT-STORAGE-CONTENT-TYPE: "browser inline rendering images/PDF" needs proper Content-Type header from storage
startTime := time.Now()
u, err := blobClient.GetSASURL(sas.BlobPermissions{
Read: true,

@ -279,20 +279,44 @@ func (m *MinioStorage) Delete(path string) error {
}
// URL gets the redirect URL to a file. The presigned link is valid for 5 minutes.
func (m *MinioStorage) URL(path, name, method string, serveDirectReqParams url.Values) (*url.URL, error) {
func (m *MinioStorage) URL(storePath, name, method string, serveDirectReqParams url.Values) (*url.URL, error) {
// copy serveDirectReqParams
reqParams, err := url.ParseQuery(serveDirectReqParams.Encode())
if err != nil {
return nil, err
}
// TODO it may be good to embed images with 'inline' like ServeData does, but we don't want to have to read the file, do we?
reqParams.Set("response-content-disposition", "attachment; filename=\""+quoteEscaper.Replace(name)+"\"")
// Here we might not know the real filename, and it's quite inefficient to detect the mine type by pre-fetching the object head.
// So we just do a quick detection by extension name, at least if works for the "View Raw File" for an LFS file on the Web UI.
// Detect content type by extension name, only support the well-known safe types for inline rendering.
// TODO: OBJECT-STORAGE-CONTENT-TYPE: need a complete solution and refactor for Azure in the future
ext := path.Ext(name)
inlineExtMimeTypes := map[string]string{
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".webp": "image/webp",
".avif": "image/avif",
// ATTENTION! Don't support unsafe types like HTML/SVG due to security concerns: they can contain JS code, and maybe they need proper Content-Security-Policy
// HINT: PDF-RENDER-SANDBOX: PDF won't render in sandboxed context, it seems fine to render it inline
".pdf": "application/pdf",
// TODO: refactor with "modules/public/mime_types.go", for example: "DetectWellKnownSafeInlineMimeType"
}
if mimeType, ok := inlineExtMimeTypes[ext]; ok {
reqParams.Set("response-content-type", mimeType)
reqParams.Set("response-content-disposition", "inline")
} else {
reqParams.Set("response-content-disposition", fmt.Sprintf(`attachment; filename="%s"`, quoteEscaper.Replace(name)))
}
expires := 5 * time.Minute
if method == http.MethodHead {
u, err := m.client.PresignedHeadObject(m.ctx, m.bucket, m.buildMinioPath(path), expires, reqParams)
u, err := m.client.PresignedHeadObject(m.ctx, m.bucket, m.buildMinioPath(storePath), expires, reqParams)
return u, convertMinioErr(err)
}
u, err := m.client.PresignedGetObject(m.ctx, m.bucket, m.buildMinioPath(path), expires, reqParams)
u, err := m.client.PresignedGetObject(m.ctx, m.bucket, m.buildMinioPath(storePath), expires, reqParams)
return u, convertMinioErr(err)
}

@ -29,7 +29,7 @@ func ReadAtMost(r io.Reader, buf []byte) (n int, err error) {
// ReadWithLimit reads at most "limit" bytes from r into buf.
// If EOF or ErrUnexpectedEOF occurs while reading, err will be nil.
func ReadWithLimit(r io.Reader, n int) (buf []byte, err error) {
return readWithLimit(r, 1024, n)
return readWithLimit(r, 4*1024, n)
}
func readWithLimit(r io.Reader, batch, limit int) ([]byte, error) {

@ -592,10 +592,10 @@
".settings": "folder-config",
"_settings": "folder-config",
"__settings__": "folder-config",
"META-INF": "folder-config",
".META-INF": "folder-config",
"_META-INF": "folder-config",
"__META-INF__": "folder-config",
"meta-inf": "folder-config",
".meta-inf": "folder-config",
"_meta-inf": "folder-config",
"__meta-inf__": "folder-config",
"option": "folder-config",
".option": "folder-config",
"_option": "folder-config",
@ -2196,14 +2196,14 @@
".templates": "folder-template",
"_templates": "folder-template",
"__templates__": "folder-template",
"github/ISSUE_TEMPLATE": "folder-template",
".github/ISSUE_TEMPLATE": "folder-template",
"_github/ISSUE_TEMPLATE": "folder-template",
"__github/ISSUE_TEMPLATE__": "folder-template",
"github/PULL_REQUEST_TEMPLATE": "folder-template",
".github/PULL_REQUEST_TEMPLATE": "folder-template",
"_github/PULL_REQUEST_TEMPLATE": "folder-template",
"__github/PULL_REQUEST_TEMPLATE__": "folder-template",
"github/issue_template": "folder-template",
".github/issue_template": "folder-template",
"_github/issue_template": "folder-template",
"__github/issue_template__": "folder-template",
"github/pull_request_template": "folder-template",
".github/pull_request_template": "folder-template",
"_github/pull_request_template": "folder-template",
"__github/pull_request_template__": "folder-template",
"util": "folder-utils",
".util": "folder-utils",
"_util": "folder-utils",
@ -2328,22 +2328,22 @@
".osx": "folder-macos",
"_osx": "folder-macos",
"__osx__": "folder-macos",
"DS_Store": "folder-macos",
".DS_Store": "folder-macos",
"_DS_Store": "folder-macos",
"__DS_Store__": "folder-macos",
"iPhone": "folder-macos",
".iPhone": "folder-macos",
"_iPhone": "folder-macos",
"__iPhone__": "folder-macos",
"iPad": "folder-macos",
".iPad": "folder-macos",
"_iPad": "folder-macos",
"__iPad__": "folder-macos",
"iPod": "folder-macos",
".iPod": "folder-macos",
"_iPod": "folder-macos",
"__iPod__": "folder-macos",
"ds_store": "folder-macos",
".ds_store": "folder-macos",
"_ds_store": "folder-macos",
"__ds_store__": "folder-macos",
"iphone": "folder-macos",
".iphone": "folder-macos",
"_iphone": "folder-macos",
"__iphone__": "folder-macos",
"ipad": "folder-macos",
".ipad": "folder-macos",
"_ipad": "folder-macos",
"__ipad__": "folder-macos",
"ipod": "folder-macos",
".ipod": "folder-macos",
"_ipod": "folder-macos",
"__ipod__": "folder-macos",
"macbook": "folder-macos",
".macbook": "folder-macos",
"_macbook": "folder-macos",
@ -3474,35 +3474,7 @@
"cues": "folder-cue",
".cues": "folder-cue",
"_cues": "folder-cue",
"__cues__": "folder-cue",
"meta-inf": "folder-config",
".meta-inf": "folder-config",
"_meta-inf": "folder-config",
"__meta-inf__": "folder-config",
"github/issue_template": "folder-template",
".github/issue_template": "folder-template",
"_github/issue_template": "folder-template",
"__github/issue_template__": "folder-template",
"github/pull_request_template": "folder-template",
".github/pull_request_template": "folder-template",
"_github/pull_request_template": "folder-template",
"__github/pull_request_template__": "folder-template",
"ds_store": "folder-macos",
".ds_store": "folder-macos",
"_ds_store": "folder-macos",
"__ds_store__": "folder-macos",
"iphone": "folder-macos",
".iphone": "folder-macos",
"_iphone": "folder-macos",
"__iphone__": "folder-macos",
"ipad": "folder-macos",
".ipad": "folder-macos",
"_ipad": "folder-macos",
"__ipad__": "folder-macos",
"ipod": "folder-macos",
".ipod": "folder-macos",
"_ipod": "folder-macos",
"__ipod__": "folder-macos"
"__cues__": "folder-cue"
},
"folderNamesExpanded": {
"rust": "folder-rust-open",
@ -7021,7 +6993,7 @@
"twee": "twine",
"yml.dist": "yaml",
"yaml.dist": "yaml",
"YAML-tmLanguage": "yaml",
"yaml-tmlanguage": "yaml",
"xml": "xml",
"plist": "xml",
"xsd": "xml",
@ -7031,7 +7003,7 @@
"resx": "xml",
"iml": "xml",
"xquery": "xml",
"tmLanguage": "xml",
"tmlanguage": "xml",
"manifest": "xml",
"project": "xml",
"xml.dist": "xml",
@ -8133,8 +8105,6 @@
"css.jsx": "vanilla-extract",
"toc": "toc",
"cue": "cue",
"yaml-tmlanguage": "yaml",
"tmlanguage": "xml",
"cljx": "clojure",
"clojure": "clojure",
"edn": "clojure",
@ -8624,7 +8594,7 @@
"graphql.config.mts": "graphql",
"graphql.config.cts": "graphql",
".graphqlconfig": "graphql",
"XamlStyler.json": "xaml",
"xamlstyler.json": "xaml",
".happo.js": "happo",
".happo.mjs": "happo",
".happo.cjs": "happo",
@ -8648,11 +8618,11 @@
".git-for-windows-updater": "git",
"git-history": "git",
".luacheckrc": "lua",
".Rhistory": "r",
".rhistory": "r",
".pubignore": "dart",
"cmakelists.txt": "cmake",
"cmakecache.txt": "cmake",
"CMakePresets.json": "cmake",
"cmakepresets.json": "cmake",
"semgrep.yml": "semgrep",
".semgrepignore": "semgrep",
"vue.config.js": "vue-config",
@ -8783,7 +8753,7 @@
"cabal.project": "cabal",
"cabal.project.freeze": "cabal",
"cabal.project.local": "cabal",
"CNAME": "http",
"cname": "http",
"project.graphcool": "graphcool",
"webpack.base.js": "webpack",
"webpack.base.mjs": "webpack",
@ -9215,7 +9185,7 @@
"sonar-project.properties": "sonarcloud",
".sonarcloud.properties": "sonarcloud",
"sonarcloud.yaml": "sonarcloud",
"SonarQube.Analysis.xml": "sonarcloud",
"sonarqube.analysis.xml": "sonarcloud",
"protractor.conf.js": "protractor",
"protractor.conf.ts": "protractor",
"protractor.conf.coffee": "protractor",
@ -9577,7 +9547,7 @@
".gitpod.yml": "gitpod",
".stackblitzrc": "stackblitz",
"codeowners": "codeowners",
"OWNERS": "codeowners",
"owners": "codeowners",
".gcloudignore": "gcp",
"amplify.yml": "amplify",
".huskyrc": "husky",
@ -9939,7 +9909,7 @@
"steadybit.yml": "steadybit",
".steadybit.yaml": "steadybit",
"steadybit.yaml": "steadybit",
"Caddyfile": "caddy",
"caddyfile": "caddy",
"openapi.json": "openapi",
"openapi.yml": "openapi",
"openapi.yaml": "openapi",
@ -10157,8 +10127,8 @@
"project.garden.yml": "garden",
"project.garden.yaml": "garden",
".gardenignore": "garden",
"PklProject": "pkl",
"PklProject.deps.json": "pkl",
"pklproject": "pkl",
"pklproject.deps.json": "pkl",
"k8s.yml": "kubernetes",
"k8s.yaml": "kubernetes",
"kubernetes.yml": "kubernetes",
@ -10266,7 +10236,7 @@
".histoire.cts": "histoire",
"install": "installation",
"installation": "installation",
".github/FUNDING.yml": "github-sponsors",
".github/funding.yml": "github-sponsors",
"fabric.mod.json": "minecraft-fabric",
".umirc.js": "umi",
".umirc.mjs": "umi",
@ -10297,15 +10267,15 @@
"packship.config.mjs": "packship",
"packship.config.mts": "packship",
"packship.config.json": "packship",
"Snakefile": "snakemake",
"snakefile": "snakemake",
".hadolint.yaml": "hadolint",
".hadolint.yml": "hadolint",
"hadolint.yaml": "hadolint",
"hadolint.yml": "hadolint",
"tsdoc.json": "tsdoc",
".oxlintrc.json": "oxlint",
"CLAUDE.md": "claude",
"CLAUDE.local.md": "claude",
"claude.md": "claude",
"claude.local.md": "claude",
".cursorignore": "cursor",
".cursorindexingignore": "cursor",
".cursorrules": "cursor",
@ -10323,23 +10293,9 @@
"src/bashly-strings.yaml": "bashly-strings",
"src/bashly-strings.yml": "bashly-strings",
"google-services.json": "google",
"GoogleService-Info.plist": "google",
"googleservice-info.plist": "google",
".shellcheckrc": "shellcheck",
"shellcheckrc": "shellcheck",
"xamlstyler.json": "xaml",
".rhistory": "r",
"cmakepresets.json": "cmake",
"cname": "http",
"sonarqube.analysis.xml": "sonarcloud",
"owners": "codeowners",
"caddyfile": "caddy",
"pklproject": "pkl",
"pklproject.deps.json": "pkl",
".github/funding.yml": "github-sponsors",
"snakefile": "snakemake",
"claude.md": "claude",
"claude.local.md": "claude",
"googleservice-info.plist": "google",
"language-configuration.json": "jsonc",
"icon-theme.json": "jsonc",
"color-theme.json": "jsonc",

@ -1482,6 +1482,7 @@ projects.column.new_submit = "Create Column"
projects.column.new = "New Column"
projects.column.set_default = "Set Default"
projects.column.set_default_desc = "Set this column as default for uncategorized issues and pulls"
projects.column.default_column_hint = "New issues added to this project will be added to this column"
projects.column.delete = "Delete Column"
projects.column.deletion_desc = "Deleting a project column moves all related issues to the default column. Continue?"
projects.column.color = "Color"
@ -3050,7 +3051,7 @@ dashboard.update_migration_poster_id = Update migration poster IDs
dashboard.git_gc_repos = Garbage-collect all repositories
dashboard.resync_all_sshkeys = Update the '.ssh/authorized_keys' file with Gitea SSH keys
dashboard.resync_all_sshprincipals = Update the '.ssh/authorized_principals' file with Gitea SSH principals
dashboard.resync_all_hooks = Resynchronize pre-receive, update and post-receive hooks of all repositories
dashboard.resync_all_hooks = Resynchronize git hooks of all repositories (pre-receive, update, post-receive, proc-receive, ...)
dashboard.reinit_missing_repos = Reinitialize all missing Git repositories for which records exist
dashboard.sync_external_users = Synchronize external user data
dashboard.cleanup_hook_task_table = Clean up hook_task table

@ -3038,7 +3038,6 @@ dashboard.update_migration_poster_id=Actualiser les ID des affiches de migration
dashboard.git_gc_repos=Exécuter le ramasse-miette des dépôts
dashboard.resync_all_sshkeys=Mettre à jour le fichier « ssh/authorized_keys » avec les clés SSH Gitea.
dashboard.resync_all_sshprincipals=Mettre à jour le fichier « .ssh/authorized_principals » avec les principaux de Gitea SSH.
dashboard.resync_all_hooks=Re-synchroniser les déclencheurs Git pre-receive, update et post-receive de tous les dépôts.
dashboard.reinit_missing_repos=Réinitialiser tous les dépôts Git manquants pour lesquels un enregistrement existe
dashboard.sync_external_users=Synchroniser les données de lutilisateur externe
dashboard.cleanup_hook_task_table=Nettoyer la table hook_task

@ -3038,7 +3038,6 @@ dashboard.update_migration_poster_id=Nuashonraigh ID póstaer imir
dashboard.git_gc_repos=Bailitheoir bruscair gach stórais
dashboard.resync_all_sshkeys=Nuashonraigh an comhad '.ssh/authorized_keys' le heochracha SSH Gitea
dashboard.resync_all_sshprincipals=Nuashonraigh an comhad '.ssh/authorized_principals' le príomhoidí SSH Gitea
dashboard.resync_all_hooks=Athshioncrónaigh crúcaí réamhghlactha, nuashonraithe agus iarghlactha na stórais uile
dashboard.reinit_missing_repos=Aththosaigh gach stórais Git atá in easnamh a bhfuil taifid ann dóibh
dashboard.sync_external_users=Sioncrónaigh sonraí úsáideoirí seachtracha
dashboard.cleanup_hook_task_table=Glan suas an tábla hook_task

@ -3038,7 +3038,6 @@ dashboard.update_migration_poster_id=移行する投稿者IDの更新
dashboard.git_gc_repos=すべてのリポジトリでガベージコレクションを実行
dashboard.resync_all_sshkeys='.ssh/authorized_keys' ファイルをGitea上のSSHキーで更新
dashboard.resync_all_sshprincipals='.ssh/authorized_principals' ファイルをGitea上のSSHプリンシパルで更新
dashboard.resync_all_hooks=すべてのリポジトリの pre-receive, update, post-receive フックを再同期する
dashboard.reinit_missing_repos=レコードが存在するが見当たらないすべてのGitリポジトリを再初期化する
dashboard.sync_external_users=外部ユーザーデータの同期
dashboard.cleanup_hook_task_table=hook_taskテーブルのクリーンアップ

@ -1482,6 +1482,7 @@ projects.column.new_submit=Criar coluna
projects.column.new=Nova coluna
projects.column.set_default=Tornar predefinida
projects.column.set_default_desc=Definir esta coluna como a predefinida para questões e pedidos de integração não categorizados
projects.column.default_column_hint=Novas questões adicionadas a este planeamento serão adicionadas a esta coluna
projects.column.delete=Eliminar coluna
projects.column.deletion_desc=Eliminar uma coluna de um planeamento faz com que todas as questões que nela constam sejam movidas para a coluna predefinida. Continuar?
projects.column.color=Colorido
@ -3038,7 +3039,6 @@ dashboard.update_migration_poster_id=Sincronizar os IDs do remetente da migraç
dashboard.git_gc_repos=Fazer a recolha do lixo em todos os repositórios
dashboard.resync_all_sshkeys=Sincronizar o ficheiro '.ssh/authorized_keys' com as chaves SSH do Gitea
dashboard.resync_all_sshprincipals=Modificar o ficheiro '.ssh/authorized_principals' com os protagonistas SSH do Gitea
dashboard.resync_all_hooks=Voltar a sincronizar automatismos de pré-acolhimento, modificação e pós-acolhimento de todos os repositórios
dashboard.reinit_missing_repos=Reinicializar todos os repositórios Git em falta para os quais existam registos
dashboard.sync_external_users=Sincronizar dados externos do utilizador
dashboard.cleanup_hook_task_table=Limpar a tabela hook_task

@ -3032,7 +3032,6 @@ dashboard.update_migration_poster_id=Taşıma poster kimliklerini güncelle
dashboard.git_gc_repos=Tüm depolardaki atıkları temizle
dashboard.resync_all_sshkeys='.ssh/authority_keys' dosyasını Gitea SSH anahtarlarıyla güncelle
dashboard.resync_all_sshprincipals='.ssh/authorized_principals' dosyasını Gitea SSH sorumlularıyla güncelleyin
dashboard.resync_all_hooks=Tüm depoların alma öncesi, güncelleme ve alma sonrası kancalarını yeniden senkronize edin
dashboard.reinit_missing_repos=Kayıtları bulunanlar için tüm eksik Git depolarını yeniden başlat
dashboard.sync_external_users=Harici kullanıcı verisini senkronize et
dashboard.cleanup_hook_task_table=Hook_task tablosunu temizle

@ -3036,7 +3036,6 @@ dashboard.update_migration_poster_id=更新迁移的发表者ID
dashboard.git_gc_repos=对仓库进行垃圾回收
dashboard.resync_all_sshkeys=使用 Gitea 的 SSH 密钥更新「.ssh/authorized_keys」文件。
dashboard.resync_all_sshprincipals=使用 Gitea 的 SSH 规则更新「.ssh/authorized_principals」文件。
dashboard.resync_all_hooks=重新同步所有仓库的 pre-receive、update 和 post-receive 钩子
dashboard.reinit_missing_repos=重新初始化所有丢失的 Git 仓库存在的记录
dashboard.sync_external_users=同步外部用户数据
dashboard.cleanup_hook_task_table=清理 hook_task 表

@ -1,6 +1,6 @@
{
"type": "module",
"packageManager": "pnpm@10.19.0",
"packageManager": "pnpm@10.22.0",
"engines": {
"node": ">= 22.6.0",
"pnpm": ">= 10.0.0"
@ -15,7 +15,7 @@
"@github/relative-time-element": "4.5.0",
"@github/text-expander-element": "2.9.2",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@primer/octicons": "19.19.0",
"@primer/octicons": "19.21.0",
"@resvg/resvg-wasm": "2.6.2",
"@silverwind/vue3-calendar-heatmap": "2.0.6",
"@techknowlogick/license-checker-webpack-plugin": "0.3.0",
@ -25,10 +25,10 @@
"chart.js": "4.5.1",
"chartjs-adapter-dayjs-4": "1.0.4",
"chartjs-plugin-zoom": "2.2.0",
"clippie": "4.1.8",
"clippie": "4.1.9",
"cropperjs": "1.6.2",
"css-loader": "7.1.2",
"dayjs": "1.11.18",
"dayjs": "1.11.19",
"dropzone": "6.0.0-beta.2",
"easymde": "2.20.0",
"esbuild-loader": "4.4.0",
@ -46,7 +46,7 @@
"postcss": "8.5.6",
"postcss-loader": "8.2.0",
"sortablejs": "1.15.6",
"swagger-ui-dist": "5.30.0",
"swagger-ui-dist": "5.30.2",
"tailwindcss": "3.4.17",
"throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0",
@ -56,18 +56,18 @@
"typescript": "5.9.3",
"uint8-to-base64": "0.2.1",
"vanilla-colorful": "0.7.2",
"vue": "3.5.22",
"vue": "3.5.24",
"vue-bar-graph": "2.2.0",
"vue-chartjs": "5.3.2",
"vue-chartjs": "5.3.3",
"vue-loader": "17.4.2",
"webpack": "5.102.1",
"webpack": "5.103.0",
"webpack-cli": "6.0.1",
"wrap-ansi": "9.0.2"
},
"devDependencies": {
"@eslint-community/eslint-plugin-eslint-comments": "4.5.0",
"@playwright/test": "1.56.1",
"@stylistic/eslint-plugin": "5.5.0",
"@stylistic/eslint-plugin": "5.6.1",
"@stylistic/stylelint-plugin": "4.0.0",
"@types/codemirror": "5.60.17",
"@types/dropzone": "5.7.9",
@ -79,25 +79,25 @@
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
"@types/toastify-js": "1.12.4",
"@typescript-eslint/parser": "8.46.2",
"@vitejs/plugin-vue": "6.0.1",
"@vitest/eslint-plugin": "1.3.26",
"eslint": "9.38.0",
"@typescript-eslint/parser": "8.47.0",
"@vitejs/plugin-vue": "6.0.2",
"@vitest/eslint-plugin": "1.4.3",
"eslint": "9.39.1",
"eslint-import-resolver-typescript": "4.4.4",
"eslint-plugin-array-func": "5.1.0",
"eslint-plugin-github": "6.0.0",
"eslint-plugin-import-x": "4.16.1",
"eslint-plugin-no-use-extend-native": "0.7.2",
"eslint-plugin-playwright": "2.2.2",
"eslint-plugin-playwright": "2.3.0",
"eslint-plugin-regexp": "2.10.0",
"eslint-plugin-sonarjs": "3.0.5",
"eslint-plugin-unicorn": "62.0.0",
"eslint-plugin-vue": "10.5.1",
"eslint-plugin-vue-scoped-css": "2.12.0",
"eslint-plugin-wc": "3.0.2",
"globals": "16.4.0",
"happy-dom": "20.0.8",
"markdownlint-cli": "0.45.0",
"globals": "16.5.0",
"happy-dom": "20.0.10",
"markdownlint-cli": "0.46.0",
"material-icon-theme": "5.28.0",
"nolyfill": "1.0.44",
"postcss-html": "1.8.0",
@ -108,11 +108,11 @@
"stylelint-declaration-strict-value": "1.10.11",
"stylelint-value-no-unknown-custom-properties": "6.0.1",
"svgo": "4.0.0",
"typescript-eslint": "8.46.2",
"typescript-eslint": "8.47.0",
"updates": "16.9.1",
"vite-string-plugin": "1.4.6",
"vitest": "4.0.4",
"vue-tsc": "3.1.2"
"vite-string-plugin": "1.4.9",
"vitest": "4.0.10",
"vue-tsc": "3.1.4"
},
"browserslist": [
"defaults"

File diff suppressed because it is too large Load Diff

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 40 40" class="svg gitea-colorblind-blueyellow" width="16" height="16" aria-hidden="true"><g clip-path="url(#gitea-colorblind-blueyellow__a)"><rect width="40" height="40" fill="#0000" rx="20"/><path fill="#1d69e0" d="M34.284 34.284c7.81-7.81 7.81-20.474 0-28.284L6 34.284c7.81 7.81 20.474 7.81 28.284 0"/><path fill="#fa4549" d="M34.283 34.284c7.81-7.81 7.81-20.474 0-28.284L20.14 20.142z"/><circle cx="20" cy="20" r="18" fill="#0000" stroke="#aaa" stroke-width="4"/></g><defs><clipPath id="gitea-colorblind-blueyellow__a"><rect width="40" height="40" fill="#0000" rx="20"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 662 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg octicon-boolean-off" width="16" height="16" aria-hidden="true"><path d="M13.25 1c.966 0 1.75.784 1.75 1.75v10.5c0 .464-.184.909-.513 1.237A1.75 1.75 0 0 1 13.25 15H2.75c-.464 0-.909-.184-1.237-.513A1.75 1.75 0 0 1 1 13.25V2.75C1 1.784 1.784 1 2.75 1zM2.75 2.5a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V2.75a.25.25 0 0 0-.25-.25z"/><path d="M9.5 8a1.5 1.5 0 1 0-3.001.001A1.5 1.5 0 0 0 9.5 8M11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0"/></svg>

After

Width:  |  Height:  |  Size: 532 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg octicon-boolean-on" width="16" height="16" aria-hidden="true"><path d="M13.25 1c.966 0 1.75.784 1.75 1.75v10.5c0 .464-.184.909-.513 1.237A1.75 1.75 0 0 1 13.25 15H2.75c-.464 0-.909-.184-1.237-.513A1.75 1.75 0 0 1 1 13.25V2.75C1 1.784 1.784 1 2.75 1zM2.75 2.5a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V2.75a.25.25 0 0 0-.25-.25z"/><path d="M8.75 5.75a.75.75 0 0 0-1.5 0v4.5a.75.75 0 0 0 1.5 0z"/></svg>

After

Width:  |  Height:  |  Size: 501 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg octicon-compose" width="16" height="16" aria-hidden="true"><path d="m14.515.456.965.965a1.555 1.555 0 0 1 0 2.2L9.745 9.355a1.55 1.55 0 0 1-.672.396l-2.89.826a.67.67 0 0 1-.828-.474.66.66 0 0 1 .004-.35l.825-2.89c.073-.254.209-.486.396-.673L12.315.456c.144-.145.316-.259.505-.337a1.54 1.54 0 0 1 1.19 0c.189.078.361.192.505.337m-3.322 3.008-3.67 3.669a.2.2 0 0 0-.057.096L6.97 8.965l1.736-.496a.2.2 0 0 0 .096-.056l3.67-3.67Zm2.065-2.066L12.135 2.52l1.28 1.28 1.122-1.122a.22.22 0 0 0 .065-.157.22.22 0 0 0-.065-.157l-.965-.966a.22.22 0 0 0-.157-.065.23.23 0 0 0-.157.065"/><path d="M0 14.25V2.75A1.75 1.75 0 0 1 1.75 1H7a.75.75 0 0 1 0 1.5H1.75a.25.25 0 0 0-.25.25v11.5a.25.25 0 0 0 .25.25h11.5a.25.25 0 0 0 .25-.25V8.5a.75.75 0 0 1 1.5 0v5.75c0 .464-.184.909-.513 1.237A1.75 1.75 0 0 1 13.25 16H1.75A1.75 1.75 0 0 1 0 14.25"/></svg>

After

Width:  |  Height:  |  Size: 905 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg octicon-crosshairs" width="16" height="16" aria-hidden="true"><path d="M14 8A6 6 0 1 1 2 8a6 6 0 0 1 12 0m-1.5 0a4.5 4.5 0 1 0-9 0 4.5 4.5 0 0 0 9 0"/><path d="M5 7.25a.75.75 0 0 1 0 1.5H1a.75.75 0 0 1 0-1.5Zm3-7a.75.75 0 0 1 .75.75v4a.75.75 0 0 1-1.5 0V1A.75.75 0 0 1 8 .25m7 7a.75.75 0 0 1 0 1.5h-4a.75.75 0 0 1 0-1.5Zm-7 3a.75.75 0 0 1 .75.75v4a.75.75 0 0 1-1.5 0v-4a.75.75 0 0 1 .75-.75"/></svg>

After

Width:  |  Height:  |  Size: 470 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg octicon-dice" width="16" height="16" aria-hidden="true"><path d="M13.25 1c.966 0 1.75.784 1.75 1.75v10.5c0 .464-.184.909-.513 1.237A1.75 1.75 0 0 1 13.25 15H2.75c-.464 0-.909-.184-1.237-.513A1.75 1.75 0 0 1 1 13.25V2.75C1 1.784 1.784 1 2.75 1zM2.75 2.5a.25.25 0 0 0-.25.25v10.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V2.75a.25.25 0 0 0-.25-.25z"/><path d="M5 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2m6-6a1 1 0 1 0 0-2 1 1 0 0 0 0 2M8 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2"/></svg>

After

Width:  |  Height:  |  Size: 539 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg octicon-exclamation" width="16" height="16" aria-hidden="true"><path d="M8 11a2 2 0 1 1 .001 3.999A2 2 0 0 1 8 11M8 1a1.5 1.5 0 0 1 1.5 1.5v6a1.5 1.5 0 0 1-3 0v-6A1.5 1.5 0 0 1 8 1"/></svg>

After

Width:  |  Height:  |  Size: 260 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg octicon-file-check" width="16" height="16" aria-hidden="true"><path d="M10.336 0c.464 0 .91.184 1.237.513l2.914 2.914c.33.328.513.773.513 1.237v3.587c0 .199-.079.39-.22.53a.747.747 0 0 1-1.06 0 .75.75 0 0 1-.22-.53V6h-2.75c-.464 0-.909-.184-1.237-.513A1.75 1.75 0 0 1 9 4.25V1.5H3.75a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25H7c.199 0 .39.079.53.22a.747.747 0 0 1 0 1.06A.75.75 0 0 1 7 16H3.75c-.464 0-.909-.184-1.237-.513A1.75 1.75 0 0 1 2 14.25V1.75C2 .784 2.784 0 3.75 0Zm.164 4.25c0 .138.112.25.25.25h2.688l-.011-.013-2.914-2.914-.013-.011Z"/><path d="M15.259 10a.75.75 0 0 1 .686.472.75.75 0 0 1-.171.815l-4.557 4.45a.75.75 0 0 1-1.055-.01L8.22 13.778a.754.754 0 0 1 .04-1.02.75.75 0 0 1 1.02-.038l1.42 1.425 4.025-3.932a.75.75 0 0 1 .534-.213"/></svg>

After

Width:  |  Height:  |  Size: 831 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg octicon-flowchart" width="16" height="16" aria-hidden="true"><path d="M9.25 5c.199 0 .39-.079.53-.22a.747.747 0 0 0 0-1.06.75.75 0 0 0-.53-.22zm-2-1.5a.75.75 0 0 0-.53.22.747.747 0 0 0 0 1.06c.14.141.331.22.53.22zm7 8.75.53.53c.3-.3.3-.77 0-1.06zm-1.47-2.53a.748.748 0 0 0-1.244.23.76.76 0 0 0 .184.83l.53-.53zm-1.06 4a.748.748 0 0 0 .23 1.244.76.76 0 0 0 .83-.184l-.53-.53zm1.53-.72c.199 0 .39-.079.53-.22a.747.747 0 0 0 0-1.06.75.75 0 0 0-.53-.22zm-6-1.5a.75.75 0 0 0-.53.22.747.747 0 0 0 0 1.06c.14.141.331.22.53.22zm3.53-4.72.53-.53-1.06-1.06-.53.53.53.53zM5.72 9.72l-.53.53 1.06 1.06.53-.53-.53-.53zm3.53-5.47V3.5h-2V5h2zm5 8 .53-.53-2-2-.53.53-.53.53 2 2zm0 0-.53-.53-2 2 .53.53.53.53 2-2zm-1 0v-.75h-6V13h6zm-3-6-.53-.53-4 4 .53.53.53.53 4-4zM3.25 2v.75h2v-1.5h-2zm2 0v.75c.28 0 .5.22.5.5h1.5a2 2 0 0 0-2-2zM6.5 3.25h-.75v2h1.5v-2zm0 2h-.75c0 .133-.053.26-.146.354a.5.5 0 0 1-.354.146v1.5a2 2 0 0 0 2-2zM5.25 6.5v-.75h-2v1.5h2zm-2 0v-.75a.5.5 0 0 1-.354-.146.5.5 0 0 1-.146-.354h-1.5c0 1.1.9 2 2 2zM2 5.25h.75v-2h-1.5v2zm0-2h.75c0-.28.22-.5.5-.5v-1.5a2 2 0 0 0-2 2zM11.25 2v.75h2v-1.5h-2zm2 0v.75c.28 0 .5.22.5.5h1.5a2 2 0 0 0-2-2zm1.25 1.25h-.75v2h1.5v-2zm0 2h-.75c0 .133-.053.26-.146.354a.5.5 0 0 1-.354.146v1.5a2 2 0 0 0 2-2zM13.25 6.5v-.75h-2v1.5h2zm-2 0v-.75a.5.5 0 0 1-.354-.146.5.5 0 0 1-.146-.354h-1.5c0 1.1.9 2 2 2zM10 5.25h.75v-2h-1.5v2zm0-2h.75c0-.28.22-.5.5-.5v-1.5a2 2 0 0 0-2 2zM3.25 10v.75h2v-1.5h-2zm2 0v.75c.28 0 .5.22.5.5h1.5a2 2 0 0 0-2-2zm1.25 1.25h-.75v2h1.5v-2zm0 2h-.75c0 .133-.053.26-.146.354a.5.5 0 0 1-.354.146v1.5a2 2 0 0 0 2-2zM5.25 14.5v-.75h-2v1.5h2zm-2 0v-.75a.5.5 0 0 1-.354-.146.5.5 0 0 1-.146-.354h-1.5c0 1.1.9 2 2 2zM2 13.25h.75v-2h-1.5v2zm0-2h.75c0-.28.22-.5.5-.5v-1.5a2 2 0 0 0-2 2z"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg octicon-focus-center" width="16" height="16" aria-hidden="true"><path d="M2.75 2.5a.25.25 0 0 0-.25.25v2.5a.75.75 0 0 1-1.5 0v-2.5C1 1.784 1.784 1 2.75 1h2.5a.75.75 0 0 1 0 1.5zM10 1.75a.75.75 0 0 1 .75-.75h2.5c.966 0 1.75.784 1.75 1.75v2.5a.75.75 0 0 1-1.5 0v-2.5a.25.25 0 0 0-.25-.25h-2.5a.75.75 0 0 1-.75-.75M1.75 10a.75.75 0 0 1 .75.75v2.5c0 .138.112.25.25.25h2.5a.75.75 0 0 1 0 1.5h-2.5A1.75 1.75 0 0 1 1 13.25v-2.5a.75.75 0 0 1 .75-.75m12.5 0a.75.75 0 0 1 .75.75v2.5A1.75 1.75 0 0 1 13.25 15h-2.5a.75.75 0 0 1 0-1.5h2.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 .75-.75M8 10a2 2 0 1 0 .001-3.999A2 2 0 0 0 8 10"/><path d="M8 10a2 2 0 1 0 .001-3.999A2 2 0 0 0 8 10"/></svg>

After

Width:  |  Height:  |  Size: 746 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg octicon-git-branch-check" width="16" height="16" aria-hidden="true"><path d="M15.26 10a.74.74 0 0 1 .414.133.75.75 0 0 1 .1 1.154l-4.557 4.45a.753.753 0 0 1-1.055-.008l-1.943-1.95a.755.755 0 0 1 .024-1.038.753.753 0 0 1 1.038-.022l1.42 1.427 4.026-3.933A.75.75 0 0 1 15.26 10m-3.51-9a2.252 2.252 0 0 1 1.942 3.389 2.25 2.25 0 0 1-1.192.983V6A2.5 2.5 0 0 1 10 8.5H6a.997.997 0 0 0-1 1v1.128a2.256 2.256 0 0 1 1.469 2.503A2.252 2.252 0 1 1 3.5 10.628V5.372a2.255 2.255 0 0 1-1.469-2.503A2.252 2.252 0 1 1 5 5.372v1.836A2.5 2.5 0 0 1 6 7h4a.997.997 0 0 0 1-1v-.628A2.252 2.252 0 0 1 11.75 1m-7.5 1.5a.75.75 0 0 0-.53.22.747.747 0 0 0 0 1.06.747.747 0 0 0 1.06 0 .747.747 0 0 0 0-1.06.75.75 0 0 0-.53-.22m0 9.5a.75.75 0 0 0-.53.22.747.747 0 0 0 0 1.06.747.747 0 0 0 1.06 0 .747.747 0 0 0 0-1.06.75.75 0 0 0-.53-.22m7.5-9.5a.75.75 0 0 0-.53.22.747.747 0 0 0 0 1.06.747.747 0 0 0 1.06 0 .747.747 0 0 0 0-1.06.75.75 0 0 0-.53-.22"/></svg>

After

Width:  |  Height:  |  Size: 1002 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg octicon-graph-bar-horizontal" width="16" height="16" aria-hidden="true"><path d="M15.25 15H.75q-.311 0-.53-.22-.22-.219-.22-.53t.22-.53q.219-.22.53-.22h14.5q.311 0 .53.22.22.219.22.53t-.22.53q-.219.22-.53.22"/><path d="M2.25 7h2.5a.25.25 0 0 1 .25.25v6.5a.25.25 0 0 1-.25.25h-2.5a.25.25 0 0 1-.25-.25v-6.5A.25.25 0 0 1 2.25 7m4-4h2.5a.25.25 0 0 1 .25.25v10.5a.25.25 0 0 1-.25.25h-2.5a.25.25 0 0 1-.25-.25V3.25A.25.25 0 0 1 6.25 3m4 6h2.5a.25.25 0 0 1 .25.25v4.5a.25.25 0 0 1-.25.25h-2.5a.25.25 0 0 1-.25-.25v-4.5a.25.25 0 0 1 .25-.25"/></svg>

After

Width:  |  Height:  |  Size: 613 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg octicon-graph-bar-vertical" width="16" height="16" aria-hidden="true"><path d="M1 15.25V.75q0-.311.22-.53.219-.22.53-.22t.53.22q.22.219.22.53v14.5q0 .311-.22.53-.219.22-.53.22t-.53-.22Q1 15.561 1 15.25"/><path d="M9 3.25v2.5a.25.25 0 0 1-.25.25h-6.5A.25.25 0 0 1 2 5.75v-2.5A.25.25 0 0 1 2.25 3h6.5a.25.25 0 0 1 .25.25m4 4v2.5a.25.25 0 0 1-.25.25H2.25A.25.25 0 0 1 2 9.75v-2.5A.25.25 0 0 1 2.25 7h10.5a.25.25 0 0 1 .25.25m-6 4v2.5a.25.25 0 0 1-.25.25h-4.5a.25.25 0 0 1-.25-.25v-2.5a.25.25 0 0 1 .25-.25h4.5a.25.25 0 0 1 .25.25"/></svg>

After

Width:  |  Height:  |  Size: 606 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg octicon-inbox-fill" width="16" height="16" aria-hidden="true"><path d="M2.8 2.06A1.75 1.75 0 0 1 4.41 1h7.18c.7 0 1.333.417 1.61 1.06l2.74 6.395a.8.8 0 0 1 .06.295v4.5A1.75 1.75 0 0 1 14.25 15H1.75A1.75 1.75 0 0 1 0 13.25v-4.5a.8.8 0 0 1 .06-.295zm1.61.44a.25.25 0 0 0-.23.152L1.887 8H4.75a.75.75 0 0 1 .6.3L6.625 10h2.75l1.275-1.7a.75.75 0 0 1 .6-.3h2.863L11.82 2.652a.25.25 0 0 0-.23-.152z"/></svg>

After

Width:  |  Height:  |  Size: 471 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg octicon-node" width="16" height="16" aria-hidden="true"><path d="M8 5a1.5 1.5 0 0 0 1.061-.439 1.507 1.507 0 0 0 0-2.122 1.507 1.507 0 0 0-2.122 0 1.503 1.503 0 0 0 0 2.122C7.221 4.842 7.602 5 8 5m0 9a1.5 1.5 0 0 0 1.061-.439 1.507 1.507 0 0 0 0-2.122 1.507 1.507 0 0 0-2.122 0 1.503 1.503 0 0 0 0 2.122c.282.281.663.439 1.061.439m-7-2.5v-7a1.75 1.75 0 0 1 1.75-1.75H4.5a.75.75 0 0 1 0 1.5H2.75a.25.25 0 0 0-.25.25v7l.005.049a.246.246 0 0 0 .245.201H4.5a.75.75 0 0 1 0 1.5H2.75A1.75 1.75 0 0 1 1 11.5m12.5 0v-7a.25.25 0 0 0-.201-.245l-.049-.005H11.5a.75.75 0 0 1 0-1.5h1.75A1.75 1.75 0 0 1 15 4.5v7c0 .464-.184.909-.513 1.237a1.75 1.75 0 0 1-1.237.513H11.5a.75.75 0 0 1 0-1.5h1.75a.25.25 0 0 0 .25-.25"/></svg>

After

Width:  |  Height:  |  Size: 781 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg octicon-pencil-ai" width="16" height="16" aria-hidden="true"><path d="M11.013 1.427a1.75 1.75 0 0 1 2.474 0l1.086 1.086a1.75 1.75 0 0 1 0 2.474l-8.609 8.61c-.21.21-.471.363-.757.445l-3.251.929a.75.75 0 0 1-.736-.191.75.75 0 0 1-.191-.736l.929-3.251a1.76 1.76 0 0 1 .445-.757Zm-7.549 9.67a.25.25 0 0 0-.064.108l-.558 1.953 1.953-.558a.25.25 0 0 0 .108-.064l6.286-6.286L9.75 4.811Zm8.963-8.61a.25.25 0 0 0-.354 0L10.811 3.75l1.439 1.44 1.263-1.263a.25.25 0 0 0 0-.354Zm-.158 6.676A.25.25 0 0 1 12.502 9a.25.25 0 0 1 .232.163l.238.648a3.72 3.72 0 0 0 2.219 2.219l.649.238a.25.25 0 0 1 .16.202v.063a.25.25 0 0 1-.16.202l-.649.238a3.72 3.72 0 0 0-2.219 2.218l-.238.649a.25.25 0 0 1-.193.16h-.079a.25.25 0 0 1-.193-.16l-.239-.649a3.74 3.74 0 0 0-2.218-2.218l-.649-.238a.248.248 0 0 1-.118-.376.25.25 0 0 1 .118-.091l.649-.238a3.72 3.72 0 0 0 2.218-2.219Z"/></svg>

After

Width:  |  Height:  |  Size: 928 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg octicon-smiley-frown" width="16" height="16" aria-hidden="true"><path d="M8 0a7.996 7.996 0 0 1 8 8 8 8 0 1 1-8-8m0 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13m0 7.996a3.78 3.78 0 0 1 2.127.629q.289.195.53.445.1.1.184.213l.015.019.004.008.002.002.001.002v.001a1 1 0 0 1-.07.05l.071-.05a.753.753 0 0 1-.175 1.046.75.75 0 0 1-1.047-.175l-.007-.009a1.8 1.8 0 0 0-.35-.31c-.265-.179-.683-.371-1.285-.371s-1.021.192-1.285.37a1.8 1.8 0 0 0-.35.31l-.007.01a.747.747 0 0 1-1.038.174h-.001a.75.75 0 0 1-.183-1.044l.614.43-.612-.432v-.002l.002-.002.005-.007.014-.02a3.3 3.3 0 0 1 .715-.657c.474-.322 1.18-.63 2.126-.63M5 6a1 1 0 1 1 0 1.998A1 1 0 0 1 5 6m6 0a1 1 0 1 1 0 1.998A1 1 0 0 1 11 6"/></svg>

After

Width:  |  Height:  |  Size: 759 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg octicon-smiley-frustrated" width="16" height="16" aria-hidden="true"><path d="M8 0a7.996 7.996 0 0 1 8 8 8 8 0 1 1-8-8m0 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13m0 6.75c2.487 0 3.518 1.98 3.727 2.818A.75.75 0 0 1 11 12H5a.75.75 0 0 1-.728-.932C4.482 10.23 5.513 8.25 8 8.25m3.259-3.854a.651.651 0 0 1 .482 1.208L10.75 6l.991.396a.651.651 0 0 1-.482 1.208L8.99 6.696a.75.75 0 0 1 0-1.392Zm-7.363.363a.65.65 0 0 1 .845-.363l2.268.908a.75.75 0 0 1 0 1.392l-2.268.908a.651.651 0 0 1-.483-1.208L5.25 6l-.992-.396a.65.65 0 0 1-.362-.845M8 9.75c-.822 0-1.383.351-1.746.75h3.492c-.363-.399-.924-.75-1.746-.75"/></svg>

After

Width:  |  Height:  |  Size: 681 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg octicon-smiley-grin" width="16" height="16" aria-hidden="true"><path d="M8 0a7.996 7.996 0 0 1 8 8 8 8 0 1 1-8-8m0 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13m3 7.75a.75.75 0 0 1 .727.932C11.518 11.02 10.487 13 8 13s-3.518-1.98-3.728-2.818A.75.75 0 0 1 5 9.25ZM8.329 6.164c.895-1.788 3.447-1.788 4.342 0a.75.75 0 0 1-1.342.671.927.927 0 0 0-1.658 0 .75.75 0 0 1-1.342-.671m-5 0c.895-1.788 3.447-1.788 4.342 0a.75.75 0 0 1-1.342.671.927.927 0 0 0-1.658 0 .75.75 0 0 1-1.342-.671m2.925 4.586c.363.399.924.75 1.746.75s1.383-.351 1.746-.75Z"/></svg>

After

Width:  |  Height:  |  Size: 614 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg octicon-smiley-neutral" width="16" height="16" aria-hidden="true"><path d="M8 0a7.996 7.996 0 0 1 8 8 8 8 0 1 1-8-8m0 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13m2 8.75a.75.75 0 0 1 0 1.5H6a.75.75 0 0 1 0-1.5ZM5 6a1 1 0 1 1 0 1.998A1 1 0 0 1 5 6m6 0a1 1 0 1 1 0 1.998A1 1 0 0 1 11 6"/></svg>

After

Width:  |  Height:  |  Size: 360 B

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