Merge branch 'main' into lunny/project_workflow

pull/30205/head
Lunny Xiao 2025-11-24 11:29:51 +07:00 committed by GitHub
commit 79c014e003
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
87 changed files with 1826 additions and 1340 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

@ -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

@ -364,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)

@ -2334,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: {

@ -117,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.43.0
golang.org/x/crypto v0.45.0
golang.org/x/image v0.30.0
golang.org/x/net v0.45.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
@ -281,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

@ -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.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
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.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/net v0.45.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.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
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=

@ -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")
})
}

@ -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",

@ -3039,7 +3039,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" 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

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg octicon-spacing-large" width="16" height="16" aria-hidden="true"><path d="M13.25 2H2.75a.75.75 0 0 0 0 1.5h10.5a.75.75 0 0 0 0-1.5m-3 5h-4.5a.75.75 0 0 0 0 1.5h4.5a.75.75 0 0 0 0-1.5m3 5H2.75a.75.75 0 0 0 0 1.5h10.5a.75.75 0 0 0 0-1.5"/></svg>

After

Width:  |  Height:  |  Size: 314 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg octicon-spacing-medium" width="16" height="16" aria-hidden="true"><path d="M13.25 3H2.75a.75.75 0 0 0 0 1.5h10.5a.75.75 0 0 0 0-1.5m-3 4h-4.5a.75.75 0 0 0 0 1.5h4.5a.75.75 0 0 0 0-1.5m3 4H2.75a.75.75 0 0 0 0 1.5h10.5a.75.75 0 0 0 0-1.5"/></svg>

After

Width:  |  Height:  |  Size: 315 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg octicon-spacing-small" width="16" height="16" aria-hidden="true"><path d="M13.25 4H2.75a.75.75 0 0 0 0 1.5h10.5a.75.75 0 0 0 0-1.5m-3 3h-4.5a.75.75 0 0 0 0 1.5h4.5a.75.75 0 0 0 0-1.5m3 3H2.75a.75.75 0 0 0 0 1.5h10.5a.75.75 0 0 0 0-1.5"/></svg>

After

Width:  |  Height:  |  Size: 314 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg octicon-split-view" width="16" height="16" aria-hidden="true"><path d="M1.75 0h12.5C15.216 0 16 .784 16 1.75v12.5c0 .464-.184.909-.513 1.237A1.75 1.75 0 0 1 14.25 16H1.75c-.464 0-.909-.184-1.237-.513A1.75 1.75 0 0 1 0 14.25V1.75C0 .784.784 0 1.75 0M1.5 1.75v12.5c0 .138.112.25.25.25H7.5v-13H1.75a.25.25 0 0 0-.25.25M9 14.5h5.25a.25.25 0 0 0 .25-.25V1.75a.25.25 0 0 0-.25-.25H9Z"/></svg>

After

Width:  |  Height:  |  Size: 457 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg octicon-unwrap" width="16" height="16" aria-hidden="true"><path d="M1 2.75c0-.199.079-.39.22-.53A.75.75 0 0 1 1.75 2h12.5c.199 0 .39.079.53.22a.747.747 0 0 1 0 1.06.75.75 0 0 1-.53.22H1.75a.75.75 0 0 1-.53-.22.75.75 0 0 1-.22-.53m0 5c0-.199.079-.39.22-.53A.75.75 0 0 1 1.75 7h12.5c.199 0 .39.079.53.22a.747.747 0 0 1 0 1.06.75.75 0 0 1-.53.22H1.75a.75.75 0 0 1-.53-.22.75.75 0 0 1-.22-.53M1.75 12h3.5c.199 0 .39.079.53.22a.747.747 0 0 1 0 1.06.75.75 0 0 1-.53.22h-3.5a.75.75 0 0 1-.53-.22.747.747 0 0 1 0-1.06.75.75 0 0 1 .53-.22"/></svg>

After

Width:  |  Height:  |  Size: 609 B

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg octicon-vscode" width="16" height="16" aria-hidden="true"><path d="M12.515.537c1.169-1.215 3.48-.226 3.418 1.534a593 593 0 0 1 .062 11.538c.089 1.938-2.439 3.149-3.827 1.851A643 643 0 0 1 1.312 5.996a.93.93 0 0 1-.308-.609.92.92 0 0 1 .194-.655.87.87 0 0 1 1.232-.136l1.493 1.18a641 641 0 0 1 9.708 7.85c.008.011.036-.018.019-.017a606 606 0 0 1 .057-11.226c-1.308 1.157-2.63 2.275-3.926 3.411-.477.416-.948.831-1.424 1.253a.87.87 0 0 1-1.237-.061.9.9 0 0 1-.231-.641.94.94 0 0 1 .27-.628c.452-.456.902-.905 1.36-1.354 1.324-1.302 2.677-2.558 3.996-3.826M2.986 9.734a.8.8 0 0 1 1.184.06.95.95 0 0 1-.057 1.272l-1.228 1.2a.8.8 0 0 1-1.183-.06.95.95 0 0 1 .055-1.272z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 16 16" class="svg octicon-vscode" width="16" height="16"><path d="M10.863 13.919a.8.8 0 0 1-.644.025.8.8 0 0 1-.279-.183L4.816 9.063l-2.232 1.703a.54.54 0 0 1-.691-.031l-.716-.655a.546.546 0 0 1 0-.805L3.112 7.5 1.177 5.725a.546.546 0 0 1 0-.805l.716-.655a.54.54 0 0 1 .691-.031l2.232 1.703L9.94 1.239a.805.805 0 0 1 .923-.159l2.677 1.295c.281.136.46.422.46.736V8h-3.248V4.534L6.864 7.5l3.888 2.966V8H14v3.889c0 .314-.179.6-.46.736z"/></svg>

Before

Width:  |  Height:  |  Size: 744 B

After

Width:  |  Height:  |  Size: 513 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg octicon-wrap" width="16" height="16" aria-hidden="true"><path d="M1.75 7a.75.75 0 0 0-.53.22.747.747 0 0 0 0 1.06c.14.141.331.22.53.22h10c.464 0 .909.184 1.237.513a1.746 1.746 0 0 1 0 2.474A1.75 1.75 0 0 1 11.75 12h-1.464v-.464a.68.68 0 0 0-.375-.607.69.69 0 0 0-.711.064l-1.619 1.214a.68.68 0 0 0 0 1.086L9.2 14.507a.68.68 0 0 0 .711.064.69.69 0 0 0 .375-.607V13.5h1.464A3.247 3.247 0 0 0 15 10.25 3.247 3.247 0 0 0 11.75 7zM1 2.75c0-.199.079-.39.22-.53A.75.75 0 0 1 1.75 2h12.5c.199 0 .39.079.53.22a.747.747 0 0 1 0 1.06.75.75 0 0 1-.53.22H1.75a.75.75 0 0 1-.53-.22.75.75 0 0 1-.22-.53M1.75 12h3.5c.199 0 .39.079.53.22a.747.747 0 0 1 0 1.06.75.75 0 0 1-.53.22h-3.5a.75.75 0 0 1-.53-.22.747.747 0 0 1 0-1.06.75.75 0 0 1 .53-.22"/></svg>

After

Width:  |  Height:  |  Size: 808 B

@ -5,12 +5,10 @@ package composer
import (
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
@ -23,8 +21,6 @@ import (
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
packages_service "code.gitea.io/gitea/services/packages"
"github.com/hashicorp/go-version"
)
func apiError(ctx *context.Context, status int, obj any) {
@ -193,7 +189,7 @@ func UploadPackage(ctx *context.Context) {
}
defer buf.Close()
cp, err := composer_module.ParsePackage(buf, buf.Size())
cp, err := composer_module.ParsePackage(buf, ctx.FormTrim("version"))
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
apiError(ctx, http.StatusBadRequest, err)
@ -209,12 +205,9 @@ func UploadPackage(ctx *context.Context) {
}
if cp.Version == "" {
v, err := version.NewVersion(ctx.FormTrim("version"))
if err != nil {
apiError(ctx, http.StatusBadRequest, composer_module.ErrInvalidVersion)
return
}
cp.Version = v.String()
// the version should be either set in the "composer.json", or as a query parameter "?version=xxx"
apiError(ctx, http.StatusBadRequest, composer_module.ErrInvalidVersion)
return
}
_, _, err = packages_service.CreatePackageAndAddFile(
@ -235,7 +228,7 @@ func UploadPackage(ctx *context.Context) {
},
&packages_service.PackageFileCreationInfo{
PackageFileInfo: packages_service.PackageFileInfo{
Filename: strings.ToLower(fmt.Sprintf("%s.%s.zip", strings.ReplaceAll(cp.Name, "/", "-"), cp.Version)),
Filename: cp.Filename,
},
Creator: ctx.Doer,
Data: buf,

@ -480,7 +480,7 @@ func RenameUser(ctx *context.APIContext) {
newName := web.GetForm(ctx).(*api.RenameUserOption).NewName
// Check if username has been changed
if err := user_service.RenameUser(ctx, ctx.ContextUser, newName); err != nil {
if err := user_service.RenameUser(ctx, ctx.ContextUser, newName, ctx.Doer); err != nil {
if user_model.IsErrUserAlreadyExist(err) || db.IsErrNameReserved(err) || db.IsErrNamePatternNotAllowed(err) || db.IsErrNameCharsNotAllowed(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else {

@ -81,6 +81,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/activitypub"
"code.gitea.io/gitea/routers/api/v1/admin"
@ -774,7 +775,9 @@ func apiAuth(authMethod auth.Method) func(*context.APIContext) {
return func(ctx *context.APIContext) {
ar, err := common.AuthShared(ctx.Base, nil, authMethod)
if err != nil {
ctx.APIError(http.StatusUnauthorized, err)
msg, ok := auth.ErrAsUserAuthMessage(err)
msg = util.Iif(ok, msg, "invalid username, password or token")
ctx.APIError(http.StatusUnauthorized, msg)
return
}
ctx.Doer = ar.Doer

@ -340,7 +340,7 @@ func Rename(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.RenameOrgOption)
orgUser := ctx.Org.Organization.AsUser()
if err := user_service.RenameUser(ctx, orgUser, form.NewName); err != nil {
if err := user_service.RenameUser(ctx, orgUser, form.NewName, ctx.Doer); err != nil {
if user_model.IsErrUserAlreadyExist(err) || db.IsErrNameReserved(err) || db.IsErrNamePatternNotAllowed(err) || db.IsErrNameCharsNotAllowed(err) {
ctx.APIError(http.StatusUnprocessableEntity, err)
} else {

@ -201,7 +201,7 @@ func CreateIssueDependency(ctx *context.APIContext) {
return
}
dependencyPerm := getPermissionForRepo(ctx, target.Repo)
dependencyPerm := getPermissionForRepo(ctx, dependency.Repo)
if ctx.Written() {
return
}
@ -262,7 +262,7 @@ func RemoveIssueDependency(ctx *context.APIContext) {
return
}
dependencyPerm := getPermissionForRepo(ctx, target.Repo)
dependencyPerm := getPermissionForRepo(ctx, dependency.Repo)
if ctx.Written() {
return
}

@ -7,6 +7,7 @@ import (
"net/http"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
release_service "code.gitea.io/gitea/services/release"
@ -58,6 +59,13 @@ func GetReleaseByTag(ctx *context.APIContext) {
return
}
if release.IsDraft { // only the users with write access can see draft releases
if !ctx.IsSigned || !ctx.Repo.CanWrite(unit_model.TypeReleases) {
ctx.APIErrorNotFound()
return
}
}
if err = release.LoadAttributes(ctx); err != nil {
ctx.APIErrorInternal(err)
return

@ -345,7 +345,7 @@ func EditUserPost(ctx *context.Context) {
}
if form.UserName != "" {
if err := user_service.RenameUser(ctx, u, form.UserName); err != nil {
if err := user_service.RenameUser(ctx, u, form.UserName, ctx.Doer); err != nil {
switch {
case user_model.IsErrUserIsNotLocal(err):
ctx.Data["Err_UserName"] = true

@ -213,7 +213,7 @@ func SettingsRenamePost(ctx *context.Context) {
return
}
if err := user_service.RenameUser(ctx, ctx.Org.Organization.AsUser(), newOrgName); err != nil {
if err := user_service.RenameUser(ctx, ctx.Org.Organization.AsUser(), newOrgName, ctx.Doer); err != nil {
if user_model.IsErrUserAlreadyExist(err) {
ctx.JSONError(ctx.Tr("org.form.name_been_taken", newOrgName))
} else if db.IsErrNameReserved(err) {

@ -146,7 +146,13 @@ func httpBase(ctx *context.Context) *serviceHandler {
// rely on the results of Contexter
if !ctx.IsSigned {
// TODO: support digit auth - which would be Authorization header with digit
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea"`)
if setting.OAuth2.Enabled {
// `Basic realm="Gitea"` tells the GCM to use builtin OAuth2 application: https://github.com/git-ecosystem/git-credential-manager/pull/1442
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea"`)
} else {
// If OAuth2 is disabled, then use another realm to avoid GCM OAuth2 attempt
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea (Basic Auth)"`)
}
ctx.HTTPError(http.StatusUnauthorized)
return nil
}

@ -206,12 +206,11 @@ func SoftDeleteContentHistory(ctx *context.Context) {
ctx.NotFound(issues_model.ErrCommentNotExist{})
return
}
if history.CommentID != commentID {
ctx.NotFound(issues_model.ErrCommentNotExist{})
return
}
if commentID != 0 {
if history.CommentID != commentID {
ctx.NotFound(issues_model.ErrCommentNotExist{})
return
}
if comment, err = issues_model.GetCommentByID(ctx, commentID); err != nil {
log.Error("can not get comment for issue content history %v. err=%v", historyID, err)
return

@ -75,7 +75,7 @@ func ProfilePost(ctx *context.Context) {
ctx.Redirect(setting.AppSubURL + "/user/settings")
return
}
if err := user_service.RenameUser(ctx, ctx.Doer, form.Name); err != nil {
if err := user_service.RenameUser(ctx, ctx.Doer, form.Name, ctx.Doer); err != nil {
switch {
case user_model.IsErrUserIsNotLocal(err):
ctx.Flash.Error(ctx.Tr("form.username_change_not_local_user"))

@ -5,6 +5,7 @@
package auth
import (
"errors"
"fmt"
"net/http"
"regexp"
@ -40,6 +41,20 @@ var globalVars = sync.OnceValue(func() *globalVarsStruct {
}
})
type ErrUserAuthMessage string
func (e ErrUserAuthMessage) Error() string {
return string(e)
}
func ErrAsUserAuthMessage(err error) (string, bool) {
var msg ErrUserAuthMessage
if errors.As(err, &msg) {
return msg.Error(), true
}
return "", false
}
// Init should be called exactly once when the application starts to allow plugins
// to allocate necessary resources
func Init() {

@ -5,7 +5,6 @@
package auth
import (
"errors"
"net/http"
actions_model "code.gitea.io/gitea/models/actions"
@ -146,7 +145,7 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
return nil, err
}
if hasWebAuthn {
return nil, errors.New("basic authorization is not allowed while WebAuthn enrolled")
return nil, ErrUserAuthMessage("basic authorization is not allowed while WebAuthn enrolled")
}
if err := validateTOTP(req, u); err != nil {

@ -542,8 +542,9 @@ func ToVerification(ctx context.Context, c *git.Commit) *api.PayloadCommitVerifi
}
if verif.SigningUser != nil {
commitVerification.Signer = &api.PayloadUser{
Name: verif.SigningUser.Name,
Email: verif.SigningUser.Email,
UserName: verif.SigningUser.Name,
Name: verif.SigningUser.DisplayName(),
Email: verif.SigningEmail, // Use the email from the signature, not from the user profile
}
}
return commitVerification

@ -547,11 +547,15 @@ var escapedSymbols = regexp.MustCompile(`([*[?! \\])`)
// IsUserAllowedToMerge check if user is allowed to merge PR with given permissions and branch protections
func IsUserAllowedToMerge(ctx context.Context, pr *issues_model.PullRequest, p access_model.Permission, user *user_model.User) (bool, error) {
return isUserAllowedToMergeInRepoBranch(ctx, pr.BaseRepoID, pr.BaseBranch, p, user)
}
func isUserAllowedToMergeInRepoBranch(ctx context.Context, repoID int64, branch string, p access_model.Permission, user *user_model.User) (bool, error) {
if user == nil {
return false, nil
}
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repoID, branch)
if err != nil {
return false, err
}

@ -71,7 +71,8 @@ func doMergeStyleSquash(ctx *mergeContext, message string) error {
}
cmdCommit := gitcmd.NewCommand("commit").
AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email).
AddOptionFormat("--message=%s", message)
AddOptionFormat("--message=%s", message).
AddArguments("--allow-empty")
if ctx.signKey == nil {
cmdCommit.AddArguments("--no-gpg-sign")
} else {

@ -13,6 +13,7 @@ import (
"regexp"
"strings"
"time"
"unicode/utf8"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
@ -838,51 +839,53 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ
stringBuilder := strings.Builder{}
if !setting.Repository.PullRequest.PopulateSquashCommentWithCommitMessages {
// use PR's title and description as squash commit message
message := strings.TrimSpace(pr.Issue.Content)
stringBuilder.WriteString(message)
if stringBuilder.Len() > 0 {
stringBuilder.WriteRune('\n')
if !commitMessageTrailersPattern.MatchString(message) {
// TODO: this trailer check doesn't work with the separator line added below for the co-authors
stringBuilder.WriteRune('\n')
}
}
}
// commits list is in reverse chronological order
first := true
for i := len(commits) - 1; i >= 0; i-- {
commit := commits[i]
if setting.Repository.PullRequest.PopulateSquashCommentWithCommitMessages {
maxSize := setting.Repository.PullRequest.DefaultMergeMessageSize
if maxSize < 0 || stringBuilder.Len() < maxSize {
var toWrite []byte
if first {
first = false
toWrite = []byte(strings.TrimPrefix(commit.CommitMessage, pr.Issue.Title))
} else {
toWrite = []byte(commit.CommitMessage)
}
if len(toWrite) > maxSize-stringBuilder.Len() && maxSize > -1 {
toWrite = append(toWrite[:maxSize-stringBuilder.Len()], "..."...)
}
if _, err := stringBuilder.Write(toWrite); err != nil {
log.Error("Unable to write commit message Error: %v", err)
return ""
}
} else {
// use PR's commit messages as squash commit message
// commits list is in reverse chronological order
maxMsgSize := setting.Repository.PullRequest.DefaultMergeMessageSize
for i := len(commits) - 1; i >= 0; i-- {
commit := commits[i]
msg := strings.TrimSpace(commit.CommitMessage)
if msg == "" {
continue
}
if _, err := stringBuilder.WriteRune('\n'); err != nil {
log.Error("Unable to write commit message Error: %v", err)
return ""
// This format follows GitHub's squash commit message style,
// even if there are other "* " in the commit message body, they are written as-is.
// Maybe, ideally, we should indent those lines too.
_, _ = fmt.Fprintf(&stringBuilder, "* %s\n\n", msg)
if maxMsgSize > 0 && stringBuilder.Len() >= maxMsgSize {
tmp := stringBuilder.String()
wasValidUtf8 := utf8.ValidString(tmp)
tmp = tmp[:maxMsgSize] + "..."
if wasValidUtf8 {
// If the message was valid UTF-8 before truncation, ensure it remains valid after truncation
// For non-utf8 messages, we can't do much about it, end users should use utf-8 as much as possible
tmp = strings.ToValidUTF8(tmp, "")
}
stringBuilder.Reset()
stringBuilder.WriteString(tmp)
break
}
}
}
// collect co-authors
for _, commit := range commits {
authorString := commit.Author.String()
if uniqueAuthors.Add(authorString) && authorString != posterSig {
// Compare use account as well to avoid adding the same author multiple times
// times when email addresses are private or multiple emails are used.
// when email addresses are private or multiple emails are used.
commitUser, _ := user_model.GetUserByEmail(ctx, commit.Author.Email)
if commitUser == nil || commitUser.ID != pr.Issue.Poster.ID {
authors = append(authors, authorString)
@ -890,12 +893,12 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ
}
}
// Consider collecting the remaining authors
// collect the remaining authors
if limit >= 0 && setting.Repository.PullRequest.DefaultMergeMessageAllAuthors {
skip := limit
limit = 30
for {
commits, err := gitRepo.CommitsBetweenLimit(headCommit, mergeBase, limit, skip)
commits, err = gitRepo.CommitsBetweenLimit(headCommit, mergeBase, limit, skip)
if err != nil {
log.Error("Unable to get commits between: %s %s Error: %v", pr.HeadBranch, pr.MergeBase, err)
return ""
@ -916,19 +919,15 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ
}
}
if stringBuilder.Len() > 0 && len(authors) > 0 {
// TODO: this separator line doesn't work with the trailer check (commitMessageTrailersPattern) above
stringBuilder.WriteString("---------\n\n")
}
for _, author := range authors {
if _, err := stringBuilder.WriteString("Co-authored-by: "); err != nil {
log.Error("Unable to write to string builder Error: %v", err)
return ""
}
if _, err := stringBuilder.WriteString(author); err != nil {
log.Error("Unable to write to string builder Error: %v", err)
return ""
}
if _, err := stringBuilder.WriteRune('\n'); err != nil {
log.Error("Unable to write to string builder Error: %v", err)
return ""
}
stringBuilder.WriteString("Co-authored-by: ")
stringBuilder.WriteString(author)
stringBuilder.WriteRune('\n')
}
return stringBuilder.String()

@ -101,11 +101,11 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.
}
// IsUserAllowedToUpdate check if user is allowed to update PR with given permissions and branch protections
// update PR means send new commits to PR head branch from base branch
func IsUserAllowedToUpdate(ctx context.Context, pull *issues_model.PullRequest, user *user_model.User) (mergeAllowed, rebaseAllowed bool, err error) {
if pull.Flow == issues_model.PullRequestFlowAGit {
return false, false, nil
}
if user == nil {
return false, false, nil
}
@ -121,54 +121,46 @@ func IsUserAllowedToUpdate(ctx context.Context, pull *issues_model.PullRequest,
return false, false, err
}
pr := &issues_model.PullRequest{
HeadRepoID: pull.BaseRepoID,
HeadRepo: pull.BaseRepo,
BaseRepoID: pull.HeadRepoID,
BaseRepo: pull.HeadRepo,
HeadBranch: pull.BaseBranch,
BaseBranch: pull.HeadBranch,
}
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
if err != nil {
return false, false, err
}
if err := pr.LoadBaseRepo(ctx); err != nil {
return false, false, err
}
prUnit, err := pr.BaseRepo.GetUnit(ctx, unit.TypePullRequests)
if err != nil {
// 1. check base repository's AllowRebaseUpdate configuration
// it is a config in base repo but controls the head (fork) repo's "Update" behavior
{
prBaseUnit, err := pull.BaseRepo.GetUnit(ctx, unit.TypePullRequests)
if repo_model.IsErrUnitTypeNotExist(err) {
return false, false, nil
return false, false, nil // the PR unit is disabled in base repo
} else if err != nil {
return false, false, fmt.Errorf("get base repo unit: %v", err)
}
log.Error("pr.BaseRepo.GetUnit(unit.TypePullRequests): %v", err)
return false, false, err
rebaseAllowed = prBaseUnit.PullRequestsConfig().AllowRebaseUpdate
}
rebaseAllowed = prUnit.PullRequestsConfig().AllowRebaseUpdate
// If branch protected, disable rebase unless user is whitelisted to force push (which extends regular push)
if pb != nil {
pb.Repo = pull.BaseRepo
if !pb.CanUserForcePush(ctx, user) {
rebaseAllowed = false
// 2. check head branch protection whether rebase is allowed, if pb not found then rebase depends on the above setting
{
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.HeadRepoID, pull.HeadBranch)
if err != nil {
return false, false, err
}
// If branch protected, disable rebase unless user is whitelisted to force push (which extends regular push)
if pb != nil {
pb.Repo = pull.HeadRepo
rebaseAllowed = rebaseAllowed && pb.CanUserForcePush(ctx, user)
}
}
// 3. check whether user has write access to head branch
baseRepoPerm, err := access_model.GetUserRepoPermission(ctx, pull.BaseRepo, user)
if err != nil {
return false, false, err
}
mergeAllowed, err = IsUserAllowedToMerge(ctx, pr, headRepoPerm, user)
mergeAllowed, err = isUserAllowedToMergeInRepoBranch(ctx, pull.HeadRepoID, pull.HeadBranch, headRepoPerm, user)
if err != nil {
return false, false, err
}
// 4. if the pull creator allows maintainer to edit, it means the write permissions of the head branch has been
// granted to the user with write permission of the base repository
if pull.AllowMaintainerEdit {
mergeAllowedMaintainer, err := IsUserAllowedToMerge(ctx, pr, baseRepoPerm, user)
mergeAllowedMaintainer, err := isUserAllowedToMergeInRepoBranch(ctx, pull.BaseRepoID, pull.BaseBranch, baseRepoPerm, user)
if err != nil {
return false, false, err
}
@ -176,6 +168,9 @@ func IsUserAllowedToUpdate(ctx context.Context, pull *issues_model.PullRequest,
mergeAllowed = mergeAllowed || mergeAllowedMaintainer
}
// if merge is not allowed, rebase is also not allowed
rebaseAllowed = rebaseAllowed && mergeAllowed
return mergeAllowed, rebaseAllowed, nil
}

@ -361,7 +361,7 @@ func DeleteReleaseByID(ctx context.Context, repo *repo_model.Repository, rel *re
if err != nil {
return fmt.Errorf("GetProtectedTags: %w", err)
}
isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, protectedTags, rel.TagName, rel.PublisherID)
isAllowed, err := git_model.IsUserAllowedToControlTag(ctx, protectedTags, rel.TagName, doer.ID)
if err != nil {
return err
}

@ -31,17 +31,15 @@ import (
)
// RenameUser renames a user
func RenameUser(ctx context.Context, u *user_model.User, newUserName string) error {
func RenameUser(ctx context.Context, u *user_model.User, newUserName string, doer *user_model.User) error {
if newUserName == u.Name {
return nil
}
// Non-local users are not allowed to change their username.
if !u.IsOrganization() && !u.IsLocal() {
return user_model.ErrUserIsNotLocal{
UID: u.ID,
Name: u.Name,
}
// Non-local users are not allowed to change their own username, but admins are
isExternalUser := !u.IsOrganization() && !u.IsLocal()
if isExternalUser && !doer.IsAdmin {
return user_model.ErrUserIsNotLocal{UID: u.ID, Name: u.Name}
}
if err := user_model.IsUsableUsername(newUserName); err != nil {

@ -20,6 +20,7 @@ import (
org_service "code.gitea.io/gitea/services/org"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMain(m *testing.M) {
@ -101,23 +102,31 @@ func TestRenameUser(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 21})
t.Run("Non-Local", func(t *testing.T) {
u := &user_model.User{
Type: user_model.UserTypeIndividual,
LoginType: auth.OAuth2,
t.Run("External user", func(t *testing.T) {
adminUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1, IsAdmin: true})
externalUser := &user_model.User{
Name: "external_user",
Email: "external_user@gitea.io",
LoginType: auth.LDAP,
}
assert.ErrorIs(t, RenameUser(t.Context(), u, "user_rename"), user_model.ErrUserIsNotLocal{})
require.NoError(t, user_model.CreateUser(t.Context(), externalUser, &user_model.Meta{}))
err := RenameUser(t.Context(), externalUser, externalUser.Name+"_changed", externalUser)
assert.True(t, user_model.IsErrUserIsNotLocal(err), "external user is not allowed to rename themselves")
err = RenameUser(t.Context(), externalUser, externalUser.Name+"_changed", adminUser)
assert.NoError(t, err, "admin can rename external user")
})
t.Run("Same username", func(t *testing.T) {
assert.NoError(t, RenameUser(t.Context(), user, user.Name))
assert.NoError(t, RenameUser(t.Context(), user, user.Name, user))
})
t.Run("Non usable username", func(t *testing.T) {
usernames := []string{"--diff", ".well-known", "gitea-actions", "aaa.atom", "aa.png"}
for _, username := range usernames {
assert.Error(t, user_model.IsUsableUsername(username), "non-usable username: %s", username)
assert.Error(t, RenameUser(t.Context(), user, username), "non-usable username: %s", username)
assert.Error(t, RenameUser(t.Context(), user, username, user), "non-usable username: %s", username)
}
})
@ -126,7 +135,7 @@ func TestRenameUser(t *testing.T) {
unittest.AssertNotExistsBean(t, &user_model.User{ID: user.ID, Name: caps})
unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: user.ID, OwnerName: user.Name})
assert.NoError(t, RenameUser(t.Context(), user, caps))
assert.NoError(t, RenameUser(t.Context(), user, caps, user))
unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: user.ID, Name: caps})
unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: user.ID, OwnerName: caps})
@ -135,17 +144,17 @@ func TestRenameUser(t *testing.T) {
t.Run("Already exists", func(t *testing.T) {
existUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
assert.ErrorIs(t, RenameUser(t.Context(), user, existUser.Name), user_model.ErrUserAlreadyExist{Name: existUser.Name})
assert.ErrorIs(t, RenameUser(t.Context(), user, existUser.LowerName), user_model.ErrUserAlreadyExist{Name: existUser.LowerName})
assert.ErrorIs(t, RenameUser(t.Context(), user, existUser.Name, user), user_model.ErrUserAlreadyExist{Name: existUser.Name})
assert.ErrorIs(t, RenameUser(t.Context(), user, existUser.LowerName, user), user_model.ErrUserAlreadyExist{Name: existUser.LowerName})
newUsername := fmt.Sprintf("uSEr%d", existUser.ID)
assert.ErrorIs(t, RenameUser(t.Context(), user, newUsername), user_model.ErrUserAlreadyExist{Name: newUsername})
assert.ErrorIs(t, RenameUser(t.Context(), user, newUsername, user), user_model.ErrUserAlreadyExist{Name: newUsername})
})
t.Run("Normal", func(t *testing.T) {
oldUsername := user.Name
newUsername := "User_Rename"
assert.NoError(t, RenameUser(t.Context(), user, newUsername))
assert.NoError(t, RenameUser(t.Context(), user, newUsername, user))
unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: user.ID, Name: newUsername, LowerName: strings.ToLower(newUsername)})
redirectUID, err := user_model.LookupUserRedirect(t.Context(), oldUsername)

@ -9,7 +9,7 @@
{{.CsrfTokenHtml}}
<div class="field {{if .Err_UserName}}error{{end}}">
<label for="user_name">{{ctx.Locale.Tr "username"}}</label>
<input id="user_name" name="user_name" value="{{.User.Name}}" {{if not .User.IsLocal}}disabled{{end}} maxlength="40">
<input id="user_name" name="user_name" value="{{.User.Name}}" maxlength="40">
</div>
<!-- Types and name -->
<div class="inline required field {{if .Err_LoginType}}error{{end}}">

@ -120,6 +120,13 @@
{{svg "octicon-question"}}
{{ctx.Locale.Tr "help"}}
</a>
{{if .IsAdmin}}
<div class="divider"></div>
<a class="{{if .PageIsAdmin}}active {{end}}item" href="{{AppSubUrl}}/-/admin">
{{svg "octicon-server"}}
{{ctx.Locale.Tr "admin_panel"}}
</a>
{{end}}
<div class="divider"></div>
<a class="item link-action" href data-url="{{AppSubUrl}}/user/logout">
{{svg "octicon-sign-out"}}

@ -6,8 +6,8 @@
<div class="flex-item-title">
<a class="item muted" href="{{.RepoLink}}/releases">
{{ctx.Locale.Tr "repo.releases"}}
<span class="ui small label">{{.NumReleases}}</span>
</a>
<span class="ui small label">{{.NumReleases}}</span>
</div>
<div class="flex-item">
<div class="flex-item-leading">

@ -9,7 +9,7 @@
<div class="flex-item">
<div class="flex-item-main">
<div class="flex-item-title">{{ctx.Locale.Tr "repo.repo_desc"}}</div>
<div class="flex-item-body tw-text-16">
<div class="flex-item-body tw-text-15">
<div class="tw-flex tw-flex-col tw-gap-2 tw-mt-2">
<div class="repo-description tw-break-anywhere tw-gap-2">
{{- $description := .Repository.DescriptionHTML ctx -}}

@ -11,7 +11,7 @@
</span>
</h2>
{{if $.Permission.IsAdmin}}<div>{{ctx.Locale.Tr "repo.default_branch"}}: {{.Repository.DefaultWikiBranch}}</div>{{end}}
<table class="ui table wiki-pages-list">
<table class="ui table selectable wiki-pages-list">
<tbody>
{{range .Pages}}
<tr>

@ -12,7 +12,7 @@
{{range $result := .SearchResults}}
{{$repo := or $.Repo (index $.RepoMaps .RepoID)}}
<div class="diff-file-box file-content non-diff-file-content repo-search-result">
<h4 class="ui top attached header tw-font-normal tw-flex tw-flex-wrap">
<h4 class="ui top attached header tw-font-normal flex-text-block tw-flex-wrap tw-py-2">
{{if not $.Repo}}
<span class="file tw-flex-1">
<a rel="nofollow" href="{{$repo.Link}}">{{$repo.FullName}}</a>

@ -0,0 +1,32 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"net/http"
"testing"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestAPIAuth(t *testing.T) {
defer tests.PrepareTestEnv(t)()
req := NewRequestf(t, "GET", "/api/v1/user").AddBasicAuth("user2")
MakeRequest(t, req, http.StatusOK)
req = NewRequestf(t, "GET", "/api/v1/user").AddBasicAuth("user2", "wrong-password")
resp := MakeRequest(t, req, http.StatusUnauthorized)
assert.Contains(t, resp.Body.String(), `{"message":"invalid username, password or token"`)
req = NewRequestf(t, "GET", "/api/v1/user").AddBasicAuth("user-not-exist")
resp = MakeRequest(t, req, http.StatusUnauthorized)
assert.Contains(t, resp.Body.String(), `{"message":"invalid username, password or token"`)
req = NewRequestf(t, "GET", "/api/v1/users/user2/repos").AddTokenAuth("Bearer wrong_token")
resp = MakeRequest(t, req, http.StatusUnauthorized)
assert.Contains(t, resp.Body.String(), `{"message":"invalid username, password or token"`)
}

@ -0,0 +1,152 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/http"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
repo_service "code.gitea.io/gitea/services/repository"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func enableRepoDependencies(t *testing.T, repoID int64) {
t.Helper()
repoUnit := unittest.AssertExistsAndLoadBean(t, &repo_model.RepoUnit{RepoID: repoID, Type: unit.TypeIssues})
repoUnit.IssuesConfig().EnableDependencies = true
assert.NoError(t, repo_model.UpdateRepoUnit(t.Context(), repoUnit))
}
func TestAPICreateIssueDependencyCrossRepoPermission(t *testing.T) {
defer tests.PrepareTestEnv(t)()
targetRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
targetIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: targetRepo.ID, Index: 1})
dependencyRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
assert.True(t, dependencyRepo.IsPrivate)
dependencyIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: dependencyRepo.ID, Index: 1})
enableRepoDependencies(t, targetIssue.RepoID)
enableRepoDependencies(t, dependencyRepo.ID)
// remove user 40 access from target repository
_, err := db.DeleteByID[access_model.Access](t.Context(), 30)
assert.NoError(t, err)
url := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/dependencies", "user2", "repo1", targetIssue.Index)
dependencyMeta := &api.IssueMeta{
Owner: "org3",
Name: "repo3",
Index: dependencyIssue.Index,
}
user40 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 40})
// user40 has no access to both target issue and dependency issue
writerToken := getUserToken(t, "user40", auth_model.AccessTokenScopeWriteIssue)
req := NewRequestWithJSON(t, "POST", url, dependencyMeta).
AddTokenAuth(writerToken)
MakeRequest(t, req, http.StatusNotFound)
unittest.AssertNotExistsBean(t, &issues_model.IssueDependency{
IssueID: targetIssue.ID,
DependencyID: dependencyIssue.ID,
})
// add user40 as a collaborator to dependency repository with read permission
assert.NoError(t, repo_service.AddOrUpdateCollaborator(t.Context(), dependencyRepo, user40, perm.AccessModeRead))
// try again after getting read permission to dependency repository
req = NewRequestWithJSON(t, "POST", url, dependencyMeta).
AddTokenAuth(writerToken)
MakeRequest(t, req, http.StatusNotFound)
unittest.AssertNotExistsBean(t, &issues_model.IssueDependency{
IssueID: targetIssue.ID,
DependencyID: dependencyIssue.ID,
})
// add user40 as a collaborator to target repository with write permission
assert.NoError(t, repo_service.AddOrUpdateCollaborator(t.Context(), targetRepo, user40, perm.AccessModeWrite))
req = NewRequestWithJSON(t, "POST", url, dependencyMeta).
AddTokenAuth(writerToken)
MakeRequest(t, req, http.StatusCreated)
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueDependency{
IssueID: targetIssue.ID,
DependencyID: dependencyIssue.ID,
})
}
func TestAPIDeleteIssueDependencyCrossRepoPermission(t *testing.T) {
defer tests.PrepareTestEnv(t)()
targetRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
targetIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: targetRepo.ID, Index: 1})
dependencyRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
assert.True(t, dependencyRepo.IsPrivate)
dependencyIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: dependencyRepo.ID, Index: 1})
enableRepoDependencies(t, targetIssue.RepoID)
enableRepoDependencies(t, dependencyRepo.ID)
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
assert.NoError(t, issues_model.CreateIssueDependency(t.Context(), user1, targetIssue, dependencyIssue))
// remove user 40 access from target repository
_, err := db.DeleteByID[access_model.Access](t.Context(), 30)
assert.NoError(t, err)
url := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/dependencies", "user2", "repo1", targetIssue.Index)
dependencyMeta := &api.IssueMeta{
Owner: "org3",
Name: "repo3",
Index: dependencyIssue.Index,
}
user40 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 40})
// user40 has no access to both target issue and dependency issue
writerToken := getUserToken(t, "user40", auth_model.AccessTokenScopeWriteIssue)
req := NewRequestWithJSON(t, "DELETE", url, dependencyMeta).
AddTokenAuth(writerToken)
MakeRequest(t, req, http.StatusNotFound)
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueDependency{
IssueID: targetIssue.ID,
DependencyID: dependencyIssue.ID,
})
// add user40 as a collaborator to dependency repository with read permission
assert.NoError(t, repo_service.AddOrUpdateCollaborator(t.Context(), dependencyRepo, user40, perm.AccessModeRead))
// try again after getting read permission to dependency repository
req = NewRequestWithJSON(t, "DELETE", url, dependencyMeta).
AddTokenAuth(writerToken)
MakeRequest(t, req, http.StatusNotFound)
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueDependency{
IssueID: targetIssue.ID,
DependencyID: dependencyIssue.ID,
})
// add user40 as a collaborator to target repository with write permission
assert.NoError(t, repo_service.AddOrUpdateCollaborator(t.Context(), targetRepo, user40, perm.AccessModeWrite))
req = NewRequestWithJSON(t, "DELETE", url, dependencyMeta).
AddTokenAuth(writerToken)
MakeRequest(t, req, http.StatusCreated)
unittest.AssertNotExistsBean(t, &issues_model.IssueDependency{
IssueID: targetIssue.ID,
DependencyID: dependencyIssue.ID,
})
}

@ -15,6 +15,8 @@ import (
"testing"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
@ -269,6 +271,42 @@ func TestAPIGetReleaseByTag(t *testing.T) {
assert.NotEmpty(t, err.Message)
}
func TestAPIGetDraftReleaseByTag(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
tag := "draft-release"
// anonymous should not be able to get draft release
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner.Name, repo.Name, tag))
MakeRequest(t, req, http.StatusNotFound)
// user 40 should be able to get draft release because he has write access to the repository
token := getUserToken(t, "user40", auth_model.AccessTokenScopeReadRepository)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner.Name, repo.Name, tag)).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
release := api.Release{}
DecodeJSON(t, resp, &release)
assert.Equal(t, "draft-release", release.Title)
// remove user 40 access from the repository
_, err := db.DeleteByID[access_model.Access](t.Context(), 30)
assert.NoError(t, err)
// user 40 should not be able to get draft release
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner.Name, repo.Name, tag)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
// user 2 should be able to get draft release because he is the publisher
user2Token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner.Name, repo.Name, tag)).AddTokenAuth(user2Token)
resp = MakeRequest(t, req, http.StatusOK)
release = api.Release{}
DecodeJSON(t, resp, &release)
assert.Equal(t, "draft-release", release.Title)
}
func TestAPIDeleteReleaseByTagName(t *testing.T) {
defer tests.PrepareTestEnv(t)()

@ -19,6 +19,9 @@ import (
type createFileInBranchOptions struct {
OldBranch, NewBranch string
CommitMessage string
CommitterName string
CommitterEmail string
}
func testCreateFileInBranch(t *testing.T, user *user_model.User, repo *repo_model.Repository, createOpts createFileInBranchOptions, files map[string]string) *api.FilesResponse {
@ -29,7 +32,17 @@ func testCreateFileInBranch(t *testing.T, user *user_model.User, repo *repo_mode
func createFileInBranch(user *user_model.User, repo *repo_model.Repository, createOpts createFileInBranchOptions, files map[string]string) (*api.FilesResponse, error) {
ctx := context.TODO()
opts := &files_service.ChangeRepoFilesOptions{OldBranch: createOpts.OldBranch, NewBranch: createOpts.NewBranch}
opts := &files_service.ChangeRepoFilesOptions{
OldBranch: createOpts.OldBranch,
NewBranch: createOpts.NewBranch,
Message: createOpts.CommitMessage,
}
if createOpts.CommitterName != "" || createOpts.CommitterEmail != "" {
opts.Committer = &files_service.IdentityOptions{
GitUserName: createOpts.CommitterName,
GitUserEmail: createOpts.CommitterEmail,
}
}
for path, content := range files {
opts.Files = append(opts.Files, &files_service.ChangeRepoFile{
Operation: "create",

@ -41,17 +41,6 @@ func TestAPIUserReposNotLogin(t *testing.T) {
}
}
func TestAPIUserReposWithWrongToken(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
wrongToken := "Bearer " + "wrong_token"
req := NewRequestf(t, "GET", "/api/v1/users/%s/repos", user.Name).
AddTokenAuth(wrongToken)
resp := MakeRequest(t, req, http.StatusUnauthorized)
assert.Contains(t, resp.Body.String(), "user does not exist")
}
func TestAPISearchRepo(t *testing.T) {
defer tests.PrepareTestEnv(t)()
const keyword = "test"

@ -271,8 +271,8 @@ type RequestWrapper struct {
*http.Request
}
func (req *RequestWrapper) AddBasicAuth(username string) *RequestWrapper {
req.Request.SetBasicAuth(username, userPassword)
func (req *RequestWrapper) AddBasicAuth(username string, password ...string) *RequestWrapper {
req.Request.SetBasicAuth(username, util.OptionalArg(password, userPassword))
return req
}

@ -33,6 +33,7 @@ import (
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/automerge"
"code.gitea.io/gitea/services/automergequeue"
pull_service "code.gitea.io/gitea/services/pull"
@ -41,6 +42,7 @@ import (
files_service "code.gitea.io/gitea/services/repository/files"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type MergeOptions struct {
@ -1180,3 +1182,172 @@ func TestPullNonMergeForAdminWithBranchProtection(t *testing.T) {
session.MakeRequest(t, mergeReq, http.StatusMethodNotAllowed)
})
}
func TestPullSquashMergeEmpty(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
session := loginUser(t, "user1")
testEditFileToNewBranch(t, session, "user2", "repo1", "master", "pr-squash-empty", "README.md", "Hello, World (Edited)\n")
resp := testPullCreate(t, session, "user2", "repo1", false, "master", "pr-squash-empty", "This is a pull title")
elem := strings.Split(test.RedirectURL(resp), "/")
assert.Equal(t, "pulls", elem[3])
httpContext := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository)
dstPath := t.TempDir()
u.Path = httpContext.GitPath()
u.User = url.UserPassword("user2", userPassword)
t.Run("Clone", doGitClone(dstPath, u))
doGitCheckoutBranch(dstPath, "-b", "pr-squash-empty", "remotes/origin/pr-squash-empty")(t)
doGitCheckoutBranch(dstPath, "master")(t)
_, _, err := gitcmd.NewCommand("cherry-pick").AddArguments("pr-squash-empty").
WithDir(dstPath).
RunStdString(t.Context())
assert.NoError(t, err)
doGitPushTestRepository(dstPath)(t)
testPullMerge(t, session, elem[1], elem[2], elem[4], MergeOptions{
Style: repo_model.MergeStyleSquash,
DeleteBranch: false,
})
})
}
func TestPullSquashMessage(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
user2Session := loginUser(t, user2.Name)
defer test.MockVariableValue(&setting.Repository.PullRequest.PopulateSquashCommentWithCommitMessages, true)()
defer test.MockVariableValue(&setting.Repository.PullRequest.DefaultMergeMessageSize, 80)()
repo, err := repo_service.CreateRepository(t.Context(), user2, user2, repo_service.CreateRepoOptions{
Name: "squash-message-test",
Description: "Test squash message",
AutoInit: true,
Readme: "Default",
DefaultBranch: "main",
})
require.NoError(t, err)
type commitInfo struct {
userName string
commitMessage string
}
testCases := []struct {
name string
commitInfos []*commitInfo
expectedMessage string
}{
{
name: "Single-line messages",
commitInfos: []*commitInfo{
{
userName: user2.Name,
commitMessage: "commit msg 1",
},
{
userName: user2.Name,
commitMessage: "commit msg 2",
},
},
expectedMessage: `* commit msg 1
* commit msg 2
`,
},
{
name: "Multiple-line messages",
commitInfos: []*commitInfo{
{
userName: user2.Name,
commitMessage: `commit msg 1
Commit description.`,
},
{
userName: user2.Name,
commitMessage: `commit msg 2
- Detail 1
- Detail 2`,
},
},
expectedMessage: `* commit msg 1
Commit description.
* commit msg 2
- Detail 1
- Detail 2
`,
},
{
name: "Too long message",
commitInfos: []*commitInfo{
{
userName: user2.Name,
commitMessage: `loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong message`,
},
},
expectedMessage: `* looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo...`,
},
{
name: "Test Co-authored-by",
commitInfos: []*commitInfo{
{
userName: user2.Name,
commitMessage: "commit msg 1",
},
{
userName: "user4",
commitMessage: "commit msg 2",
},
},
expectedMessage: `* commit msg 1
* commit msg 2
---------
Co-authored-by: user4 <user4@example.com>
`,
},
}
for tcNum, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
branchName := "test-branch-" + strconv.Itoa(tcNum)
for infoIdx, info := range tc.commitInfos {
createFileOpts := createFileInBranchOptions{
CommitMessage: info.commitMessage,
CommitterName: info.userName,
CommitterEmail: util.Iif(info.userName != "", info.userName+"@example.com", ""),
OldBranch: util.Iif(infoIdx == 0, "main", branchName),
NewBranch: branchName,
}
testCreateFileInBranch(t, user2, repo, createFileOpts, map[string]string{"dummy-file-" + strconv.Itoa(infoIdx): "dummy content"})
}
resp := testPullCreateDirectly(t, user2Session, createPullRequestOptions{
BaseRepoOwner: user2.Name,
BaseRepoName: repo.Name,
BaseBranch: repo.DefaultBranch,
HeadBranch: branchName,
Title: "Pull for " + branchName,
})
elems := strings.Split(test.RedirectURL(resp), "/")
pullIndex, err := strconv.ParseInt(elems[4], 10, 64)
assert.NoError(t, err)
pullRequest := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, Index: pullIndex})
squashMergeCommitMessage := pull_service.GetSquashMergeCommitMessages(t.Context(), pullRequest)
assert.Equal(t, tc.expectedMessage, squashMergeCommitMessage)
})
}
})
}

@ -11,7 +11,11 @@ import (
"time"
auth_model "code.gitea.io/gitea/models/auth"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/gitrepo"
@ -58,6 +62,14 @@ func TestAPIPullUpdate(t *testing.T) {
})
}
func enableRepoAllowUpdateWithRebase(t *testing.T, repoID int64, allow bool) {
t.Helper()
repoUnit := unittest.AssertExistsAndLoadBean(t, &repo_model.RepoUnit{RepoID: repoID, Type: unit.TypePullRequests})
repoUnit.PullRequestsConfig().AllowRebaseUpdate = allow
assert.NoError(t, repo_model.UpdateRepoUnit(t.Context(), repoUnit))
}
func TestAPIPullUpdateByRebase(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
// Create PR to test
@ -73,10 +85,32 @@ func TestAPIPullUpdateByRebase(t *testing.T) {
assert.Equal(t, 1, diffCount.Ahead)
assert.NoError(t, pr.LoadIssue(t.Context()))
enableRepoAllowUpdateWithRebase(t, pr.BaseRepo.ID, false)
session := loginUser(t, "user2")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
req := NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update?style=rebase", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
AddTokenAuth(token)
session.MakeRequest(t, req, http.StatusForbidden)
enableRepoAllowUpdateWithRebase(t, pr.BaseRepo.ID, true)
assert.NoError(t, pr.LoadHeadRepo(t.Context()))
// use a user which have write access to the pr but not write permission to the head repository to do the rebase
user40 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 40})
err = repo_service.AddOrUpdateCollaborator(t.Context(), pr.BaseRepo, user40, perm.AccessModeWrite)
assert.NoError(t, err)
token40 := getUserToken(t, "user40", auth_model.AccessTokenScopeWriteRepository)
req = NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update?style=rebase", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
AddTokenAuth(token40)
session.MakeRequest(t, req, http.StatusForbidden)
err = repo_service.AddOrUpdateCollaborator(t.Context(), pr.HeadRepo, user40, perm.AccessModeWrite)
assert.NoError(t, err)
req = NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update?style=rebase", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
AddTokenAuth(token40)
session.MakeRequest(t, req, http.StatusOK)
// Test GetDiverging after update
@ -87,6 +121,49 @@ func TestAPIPullUpdateByRebase(t *testing.T) {
})
}
func TestAPIPullUpdateByRebase2(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
// Create PR to test
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
org26 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 26})
pr := createOutdatedPR(t, user, org26)
assert.NoError(t, pr.LoadBaseRepo(t.Context()))
assert.NoError(t, pr.LoadIssue(t.Context()))
enableRepoAllowUpdateWithRebase(t, pr.BaseRepo.ID, false)
session := loginUser(t, "user2")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
req := NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update?style=rebase", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
AddTokenAuth(token)
session.MakeRequest(t, req, http.StatusForbidden)
enableRepoAllowUpdateWithRebase(t, pr.BaseRepo.ID, true)
assert.NoError(t, pr.LoadHeadRepo(t.Context()))
// add a protected branch rule to the head branch to block rebase
pb := git_model.ProtectedBranch{
RepoID: pr.HeadRepo.ID,
RuleName: pr.HeadBranch,
CanPush: false,
CanForcePush: false,
}
err := git_model.UpdateProtectBranch(t.Context(), pr.HeadRepo, &pb, git_model.WhitelistOptions{})
assert.NoError(t, err)
req = NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update?style=rebase", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
AddTokenAuth(token)
session.MakeRequest(t, req, http.StatusForbidden)
// remove the protected branch rule to allow rebase
err = git_model.DeleteProtectedBranch(t.Context(), pr.HeadRepo, pb.ID)
assert.NoError(t, err)
req = NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update?style=rebase", pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index).
AddTokenAuth(token)
session.MakeRequest(t, req, http.StatusOK)
})
}
func createOutdatedPR(t *testing.T, actor, forkOrg *user_model.User) *issues_model.PullRequest {
baseRepo, err := repo_service.CreateRepository(t.Context(), actor, actor, repo_service.CreateRepoOptions{
Name: "repo-pr-update",

@ -149,12 +149,18 @@ func TestRepushTag(t *testing.T) {
// delete the tag
_, _, err = gitcmd.NewCommand("push", "origin", "--delete", "v2.0").WithDir(dstPath).RunStdString(t.Context())
assert.NoError(t, err)
// query the release by API and it should be a draft
// query the release by API with no auth and it should be 404
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner.Name, repo.Name, "v2.0"))
MakeRequest(t, req, http.StatusNotFound)
// query the release by API and it should be a draft
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner.Name, repo.Name, "v2.0")).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var respRelease *api.Release
DecodeJSON(t, resp, &respRelease)
assert.True(t, respRelease.IsDraft)
// re-push the tag
_, _, err = gitcmd.NewCommand("push", "origin", "--tags", "v2.0").WithDir(dstPath).RunStdString(t.Context())
assert.NoError(t, err)

@ -528,9 +528,12 @@ td .commit-summary {
}
.repository.view.issue .comment-list .timeline-item .comment-text-line {
/* TODO: this "line-height" is not ideal (actually it is abused), many layouts depend on this magic value,
for example: alignment of the header arrow and the avatar, view PR commit list left icon layout, dismiss review with reason, etc */
line-height: 32px;
vertical-align: middle;
color: var(--color-text-light);
min-width: 0;
}
.repository.view.issue .comment-list .timeline-item .comment-text-line .ui.label {
@ -601,9 +604,6 @@ td .commit-summary {
width: 100%;
margin: 0;
}
.repository.view.issue .comment-list .comment .content .form .button:not(:last-child) {
margin-bottom: 1rem;
}
}
.repository.view.issue .comment-list .comment .merge-section {
@ -654,7 +654,7 @@ td .commit-summary {
.repository.view.issue .comment-list .code-comment {
border: 1px solid transparent;
margin: 0;
padding: 8px;
}
.repository.view.issue .comment-list .code-comment .comment-header {
@ -664,6 +664,7 @@ td .commit-summary {
}
.repository.view.issue .comment-list .code-comment .comment-content {
margin-top: 6px;
margin-left: 24px;
}
@ -1286,9 +1287,9 @@ td .commit-summary {
box-shadow: 0 0 0 3px var(--color-primary-alpha-30) !important;
}
.comment:target .header::before {
.comment:target .comment-header::before {
border-right-color: var(--color-primary) !important;
filter: drop-shadow(-3px 0 0 var(--color-primary-alpha-30)) !important;
filter: drop-shadow(-4px 0 0 var(--color-primary-alpha-30)) !important;
}
.code-comment:target,
@ -1308,7 +1309,6 @@ td .commit-summary {
padding: 0.5em 1rem;
position: relative;
color: var(--color-text);
min-height: 41px;
display: flex;
justify-content: space-between;
align-items: center;
@ -1316,6 +1316,10 @@ td .commit-summary {
gap: 0.25em;
}
.comment-header.avatar-content-left-arrow {
min-height: 41px; /* for a comment header with left arrow, the arrow is absolutely positioned, but the header content varies (for example: no "roles", etc), so it needs a min-height */
}
.comment-header.avatar-content-left-arrow::after {
border-right-color: var(--color-box-header);
}
@ -1339,7 +1343,7 @@ td .commit-summary {
.comment-header-right {
display: flex;
align-items: center;
gap: 0.5em;
gap: 6px;
}
.comment-header-right {
@ -1347,6 +1351,10 @@ td .commit-summary {
justify-content: end;
}
.comment-header-right > .item.action {
padding: 4px; /* add some padding to make click area larger for the "item action ... ui dropdown" items */
}
.comment-body {
background: var(--color-box-body);
border: none !important;

@ -41,16 +41,16 @@
margin-left: 4px;
}
.ui.dropdown.select-reaction .menu {
min-width: 170px; /* item-outer-width * 4 */
.ui.dropdown.select-reaction .menu.visible {
display: grid !important;
grid-template-columns: repeat(4, 1fr);
padding: 4px;
}
.ui.dropdown.select-reaction .menu > .item {
float: left;
margin: 4px;
font-size: 20px;
width: 34px;
height: 34px;
font-size: 16px;
border-radius: var(--border-radius);
display: flex;
align-items: center;

@ -1,7 +1,3 @@
.repository.wiki .wiki-pages-list tr:hover {
background-color: var(--color-hover);
}
.repository.wiki .wiki-pages-list .wiki-git-entry {
margin-left: 10px;
display: none;

@ -52,7 +52,7 @@
}
.comment-code-cloud {
padding: 0.5rem 1rem !important;
padding: 0.5rem !important;
position: relative;
}

@ -489,7 +489,7 @@ export default defineComponent({
<button class="ui basic small compact button red" @click="cancelRun()" v-else-if="run.canCancel">
{{ locale.cancel }}
</button>
<button class="ui basic small compact button link-action" :data-url="`${run.link}/rerun`" v-else-if="run.canRerun">
<button class="ui basic small compact button link-action tw-shrink-0" :data-url="`${run.link}/rerun`" v-else-if="run.canRerun">
{{ locale.rerun_all }}
</button>
</div>
@ -520,7 +520,7 @@ export default defineComponent({
<span class="job-brief-name tw-mx-2 gt-ellipsis">{{ job.name }}</span>
</div>
<span class="job-brief-item-right">
<SvgIcon name="octicon-sync" role="button" :data-tooltip-content="locale.rerun" class="job-brief-rerun tw-mx-2 link-action" :data-url="`${run.link}/jobs/${index}/rerun`" v-if="job.canRerun"/>
<SvgIcon name="octicon-sync" role="button" :data-tooltip-content="locale.rerun" class="job-brief-rerun tw-mx-2 link-action interact-fg" :data-url="`${run.link}/jobs/${index}/rerun`" v-if="job.canRerun"/>
<span class="step-summary-duration">{{ job.duration }}</span>
</span>
</a>

@ -1,6 +1,6 @@
export class InitPerformanceTracer {
results: {name: string, dur: number}[] = [];
recordCall(name: string, func: ()=>void) {
recordCall(name: string, func: () => void) {
const start = performance.now();
func();
this.results.push({name, dur: performance.now() - start});