Compare commits

...

16 Commits

Author SHA1 Message Date
m.huber
a48eb669ec optional 2025-10-02 04:42:44 +02:00
6543
cf290d4fdd
Merge branch 'main' into fix-remove-stale-reviewrequests 2025-10-01 23:17:04 +02:00
silverwind
b907b9fb1a
Enable a few more tsconfig options (#35553)
Enable a few more useful options in tsconfig. `noImplicitReturns` had
two cases which I've fixed. Also, partially sort the file.
2025-09-30 21:43:41 -07:00
dependabot[bot]
c5d74e5869
Bump github.com/wneessen/go-mail from 0.6.2 to 0.7.1 (#35557) 2025-10-01 00:14:53 +00:00
Steven Noonan
c5332fdc55
add more routes to the "expensive" list (#35547)
Signed-off-by: Steven Noonan <steven@uplinklabs.net>
2025-09-29 17:33:28 +08:00
wxiaoguang
0f668145e9
Drop json-iterator dependency (#35544) 2025-09-28 22:30:28 +08:00
6543
fbe80e6df2
Add proper error message if session provider can not be created (#35520)
the middleware that creates the session provider just panics if on
creation the config is wrong.
this is not catched and so you just get an cryptic stacktrace with no
point where to look at (as user).

## Before

```
2025/09/16 03:56:37 ...xer/stats/indexer.go:87:populateRepoIndexer() [I] Done (re)populating the repo stats indexer with existing repositories
2025/09/16 03:56:37 modules/ssh/ssh.go:387:Listen() [I] Adding SSH host key: /var/lib/gitea/data/ssh/gitea.rsa
2025/09/16 03:56:37 modules/ssh/init.go:26:Init() [I] SSH server started on :1234. Cipher list ([chacha20-poly1305@openssh.com aes128-ctr aes192-ctr aes256-ctr aes128-gcm@openssh.com aes256-gcm@openssh.com]), key exchange algorithms ([curve25519-sha256 ecdh-sha2-nistp256 ecdh-sha2-nistp384 ecdh-sha2-nistp521 diffie-hellman-group14-sha256 diffie-hellman-group14-sha1]), MACs ([hmac-sha2-256-etm@openssh.com hmac-sha2-256 hmac-sha1])
2025/09/16 03:56:37 ...s/graceful/server.go:50:NewServer() [I] Starting new SSH server: tcp::1234 on PID: 83337
2025/09/16 03:56:38 cmd/web.go:231:func1() [F] PANIC: dial tcp 127.0.0.1:6379: connect: connection refused
gitea.com/go-chi/session@v0.0.0-20240316035857-16768d98ec96/session.go:239 (0x1cdb908)
code.gitea.io/gitea/routers/common/middleware.go:108 (0x2547f5a)
code.gitea.io/gitea/routers/web/web.go:270 (0x278b8e9)
code.gitea.io/gitea/routers/init.go:185 (0x2850d89)
code.gitea.io/gitea/cmd/web.go:211 (0x295c5ad)
code.gitea.io/gitea/cmd/web.go:262 (0x295cacb)
code.gitea.io/gitea/cmd/main.go:111 (0x2953422)
github.com/urfave/cli/v2@v2.27.2/command.go:276 (0x1cc3dfd)
github.com/urfave/cli/v2@v2.27.2/command.go:269 (0x1cc4084)
github.com/urfave/cli/v2@v2.27.2/app.go:333 (0x1cc086a)
github.com/urfave/cli/v2@v2.27.2/app.go:307 (0x2953f18)
code.gitea.io/gitea/cmd/main.go:172 (0x2953efc)
code.gitea.io/gitea/main.go:46 (0x2998498)
runtime/proc.go:283 (0x4471ca)
runtime/asm_amd64.s:1700 (0x484a20)
```

## After

```
2025/09/22 22:52:35 .../templates/htmlrenderer.go:118:initHTMLRenderer() [D] Creating static HTML Renderer
2025/09/22 22:52:35 routers/web/web.go:273:Routes() [F] common.Sessioner failed: failed to create session middleware: dial tcp 127.0.0.1:6379: connect: connection refused
```

---------

Signed-off-by: 6543 <6543@obermui.de>
2025-09-28 12:24:19 +00:00
junoberryferry
151ef80e28
use experimental go json v2 library (#35392)
details: https://pkg.go.dev/encoding/json/v2

---------

Co-authored-by: techknowlogick <matti@mdranta.net>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-09-28 08:03:36 +00:00
Lunny Xiao
8106d95577
Use global lock instead of status pool for cron lock (#35507) 2025-09-27 10:11:52 -07:00
Lunny Xiao
1f32170060
Move some functions to gitrepo package (#35503) 2025-09-26 10:14:20 -07:00
Lunny Xiao
7bf2972379
Move GetDiverging functions to gitrepo (#35524)
Extracted from #35469

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-09-26 15:15:42 +00:00
GiteaBot
8ad2a538da [skip ci] Updated translations via Crowdin 2025-09-26 00:34:11 +00:00
Fabien Benetou
53dfbbb2ee
Update issue.go with labels documentation (labels content, not ids) (#35522)
For https://github.com/go-gitea/gitea/issues/35521

---------

Signed-off-by: Fabien Benetou <fabien-services@benetou.fr>
2025-09-25 16:56:49 +00:00
wxiaoguang
d83676c97a
Fix markup init after issue comment editing (#35536)
Fix #35533
2025-09-25 09:29:32 -07:00
Sumit
f09bea7af1
[Fix] Trigger 'unlabeled' event when label is Deleted from PR (#34316)
This pull request updates the handling of issue label events in
workflows to distinguish between label additions and deletions,
introduces corresponding test cases, and extends the `IssuePayload`
structure to support this functionality.

### Enhancements to issue label event handling:
* Updated `matchIssuesEvent` in `modules/actions/workflows.go` to
differentiate between "labeled" and "unlabeled" events based on whether
labels were added or removed.
* Added a new field, `RemovedLabels`, to the `IssuePayload` struct in
`modules/structs/hook.go` to track labels that were removed during an
issue event.

### Testing improvements:
* Added `TestMatchIssuesEvent` in `modules/actions/workflows_test.go` to
cover scenarios such as label addition, label deletion, and label
clearing, ensuring the correct event type is triggered.

---------

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2025-09-24 09:45:38 -07:00
GiteaBot
0b706b0825 [skip ci] Updated translations via Crowdin 2025-09-24 00:34:35 +00:00
85 changed files with 1216 additions and 709 deletions

View File

@ -72,13 +72,13 @@ jobs:
go-version-file: go.mod
check-latest: true
- run: make deps-backend
- run: make backend
- run: GOEXPERIMENT='' make backend
env:
TAGS: bindata gogit sqlite sqlite_unlock_notify
- name: run migration tests
run: make test-sqlite-migration
- name: run tests
run: make test-sqlite
run: GOEXPERIMENT='' make test-sqlite
timeout-minutes: 50
env:
TAGS: bindata gogit sqlite sqlite_unlock_notify
@ -142,7 +142,7 @@ jobs:
RACE_ENABLED: true
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
- name: unit-tests-gogit
run: make unit-test-coverage test-check
run: GOEXPERIMENT='' make unit-test-coverage test-check
env:
TAGS: bindata gogit
RACE_ENABLED: true

View File

@ -18,6 +18,10 @@ DIST := dist
DIST_DIRS := $(DIST)/binaries $(DIST)/release
IMPORT := code.gitea.io/gitea
# By default use go's 1.25 experimental json v2 library when building
# TODO: remove when no longer experimental
export GOEXPERIMENT ?= jsonv2
GO ?= go
SHASUM ?= shasum -a 256
HAS_GO := $(shell hash $(GO) > /dev/null 2>&1 && echo yes)
@ -766,7 +770,7 @@ generate-go: $(TAGS_PREREQ)
.PHONY: security-check
security-check:
go run $(GOVULNCHECK_PACKAGE) -show color ./...
GOEXPERIMENT= go run $(GOVULNCHECK_PACKAGE) -show color ./...
$(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ)
ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),)

14
go.mod
View File

@ -16,7 +16,7 @@ require (
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed
gitea.com/go-chi/cache v0.2.1
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098
gitea.com/go-chi/session v0.0.0-20240316035857-16768d98ec96
gitea.com/go-chi/session v0.0.0-20250926004215-636cadd82e15
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4
github.com/42wim/httpsig v1.2.3
@ -61,6 +61,7 @@ require (
github.com/go-redsync/redsync/v4 v4.13.0
github.com/go-sql-driver/mysql v1.9.3
github.com/go-webauthn/webauthn v0.13.4
github.com/goccy/go-json v0.10.5
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
github.com/golang-jwt/jwt/v5 v5.3.0
@ -75,7 +76,6 @@ require (
github.com/huandu/xstrings v1.5.0
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056
github.com/jhillyerd/enmime v1.3.0
github.com/json-iterator/go v1.1.12
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/klauspost/compress v1.18.0
github.com/klauspost/cpuid/v2 v2.3.0
@ -109,7 +109,7 @@ require (
github.com/ulikunitz/xz v0.5.15
github.com/urfave/cli-docs/v3 v3.0.0-alpha6
github.com/urfave/cli/v3 v3.4.1
github.com/wneessen/go-mail v0.6.2
github.com/wneessen/go-mail v0.7.1
github.com/xeipuuv/gojsonschema v1.2.0
github.com/yohcop/openid-go v1.0.1
github.com/yuin/goldmark v1.7.13
@ -120,9 +120,9 @@ require (
golang.org/x/image v0.30.0
golang.org/x/net v0.43.0
golang.org/x/oauth2 v0.30.0
golang.org/x/sync v0.16.0
golang.org/x/sync v0.17.0
golang.org/x/sys v0.35.0
golang.org/x/text v0.28.0
golang.org/x/text v0.29.0
google.golang.org/grpc v1.75.0
google.golang.org/protobuf v1.36.8
gopkg.in/ini.v1 v1.67.0
@ -200,7 +200,6 @@ require (
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-webauthn/x v0.1.24 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
@ -220,6 +219,7 @@ require (
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kevinburke/ssh_config v1.4.0 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/libdns/libdns v1.1.1 // indirect
@ -277,7 +277,7 @@ require (
go.uber.org/zap v1.27.0 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.36.0 // indirect

25
go.sum
View File

@ -43,8 +43,8 @@ gitea.com/go-chi/cache v0.2.1 h1:bfAPkvXlbcZxPCpcmDVCWoHgiBSBmZN/QosnZvEC0+g=
gitea.com/go-chi/cache v0.2.1/go.mod h1:Qic0HZ8hOHW62ETGbonpwz8WYypj9NieU9659wFUJ8Q=
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098 h1:p2ki+WK0cIeNQuqjR98IP2KZQKRzJJiV7aTeMAFwaWo=
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098/go.mod h1:LjzIOHlRemuUyO7WR12fmm18VZIlCAaOt9L3yKw40pk=
gitea.com/go-chi/session v0.0.0-20240316035857-16768d98ec96 h1:IFDiMBObsP6CZIRaDLd54SR6zPYAffPXiXck5Xslu0Q=
gitea.com/go-chi/session v0.0.0-20240316035857-16768d98ec96/go.mod h1:0iEpFKnwO5dG0aF98O4eq6FMsAiXkNBaDIlUOlq4BtM=
gitea.com/go-chi/session v0.0.0-20250926004215-636cadd82e15 h1:qFYmz05u/s9664o7+XEgrlHXSPQ4uHO8/ccZGUb1uxA=
gitea.com/go-chi/session v0.0.0-20250926004215-636cadd82e15/go.mod h1:0iEpFKnwO5dG0aF98O4eq6FMsAiXkNBaDIlUOlq4BtM=
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 h1:+wWBi6Qfruqu7xJgjOIrKVQGiLUZdpKYCZewJ4clqhw=
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96/go.mod h1:VyMQP6ue6MKHM8UsOXfNfuMKD0oSAWZdXVcpHIN2yaY=
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 h1:IFT+hup2xejHqdhS7keYWioqfmxdnfblFDTGoOwcZ+o=
@ -765,8 +765,8 @@ github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZ
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/wneessen/go-mail v0.6.2 h1:c6V7c8D2mz868z9WJ+8zDKtUyLfZ1++uAZmo2GRFji8=
github.com/wneessen/go-mail v0.6.2/go.mod h1:L/PYjPK3/2ZlNb2/FjEBIn9n1rUWjW+Toy531oVmeb4=
github.com/wneessen/go-mail v0.7.1 h1:rvy63sp14N06/kdGqCYwW8Na5gDCXjTQM1E7So4PuKk=
github.com/wneessen/go-mail v0.7.1/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
@ -837,7 +837,6 @@ 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.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -848,8 +847,8 @@ golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
@ -930,9 +929,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.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.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=
@ -974,7 +972,6 @@ 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.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
@ -987,7 +984,6 @@ 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.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -1003,9 +999,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.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
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=

View File

@ -417,10 +417,6 @@ func (pr *PullRequest) GetGitHeadRefName() string {
return fmt.Sprintf("%s%d/head", git.PullPrefix, pr.Index)
}
func (pr *PullRequest) GetGitHeadBranchRefName() string {
return fmt.Sprintf("%s%s", git.BranchPrefix, pr.HeadBranch)
}
// GetReviewCommentsCount returns the number of review comments made on the diff of a PR review (not including comments on commits or issues in a PR)
func (pr *PullRequest) GetReviewCommentsCount(ctx context.Context) int {
opts := FindCommentsOptions{
@ -646,9 +642,8 @@ func (pr *PullRequest) UpdateCols(ctx context.Context, cols ...string) error {
}
// UpdateColsIfNotMerged updates specific fields of a pull request if it has not been merged
func (pr *PullRequest) UpdateColsIfNotMerged(ctx context.Context, cols ...string) error {
_, err := db.GetEngine(ctx).Where("id = ? AND has_merged = ?", pr.ID, false).Cols(cols...).Update(pr)
return err
func (pr *PullRequest) UpdateColsIfNotMerged(ctx context.Context, cols ...string) (int64, error) {
return db.GetEngine(ctx).Where("id = ? AND has_merged = ?", pr.ID, false).Cols(cols...).Update(pr)
}
// IsWorkInProgress determine if the Pull Request is a Work In Progress by its title

View File

@ -534,15 +534,15 @@ func SubmitReview(ctx context.Context, doer *user_model.User, issue *Issue, revi
}
// GetReviewByIssueIDAndUserID get the latest review of reviewer for a pull request
func GetReviewByIssueIDAndUserID(ctx context.Context, issueID, userID int64, dismissed ...bool) (*Review, error) {
func GetReviewByIssueIDAndUserID(ctx context.Context, issueID, userID int64, dismissed optional.Option[bool]) (*Review, error) {
review := new(Review)
cond := builder.In("type", ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest).
And(builder.Eq{"issue_id": issueID, "reviewer_id": userID, "original_author_id": 0})
// apply optional filter for dismissed
if len(dismissed) != 0 {
cond = cond.And(builder.Eq{"dismissed": dismissed[0]})
if dismissed.Has() {
cond = cond.And(builder.Eq{"dismissed": dismissed.Value()})
}
has, err := db.GetEngine(ctx).Where(cond).
@ -655,7 +655,7 @@ func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_mo
return db.WithTx2(ctx, func(ctx context.Context) (*Comment, error) {
sess := db.GetEngine(ctx)
review, err := GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID)
review, err := GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID, optional.None[bool]())
if err != nil && !IsErrReviewNotExist(err) {
return nil, err
}
@ -725,7 +725,7 @@ func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_mo
// RemoveReviewRequest remove a review request from one reviewer
func RemoveReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_model.User) (*Comment, error) {
return db.WithTx2(ctx, func(ctx context.Context) (*Comment, error) {
review, err := GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID, false)
review, err := GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID, optional.Some(false))
if err != nil && !IsErrReviewNotExist(err) {
return nil, err
}
@ -760,7 +760,7 @@ func RemoveReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user
// Recalculate the latest official review for reviewer
func restoreLatestOfficialReview(ctx context.Context, issueID, reviewerID int64) error {
review, err := GetReviewByIssueIDAndUserID(ctx, issueID, reviewerID)
review, err := GetReviewByIssueIDAndUserID(ctx, issueID, reviewerID, optional.None[bool]())
if err != nil && !IsErrReviewNotExist(err) {
return err
}
@ -852,7 +852,7 @@ func RemoveTeamReviewRequest(ctx context.Context, issue *Issue, reviewer *organi
if official {
// recalculate which is the latest official review from that team
review, err := GetReviewByIssueIDAndUserID(ctx, issue.ID, -reviewer.ID)
review, err := GetReviewByIssueIDAndUserID(ctx, issue.ID, -reviewer.ID, optional.None[bool]())
if err != nil && !IsErrReviewNotExist(err) {
return nil, err
}

View File

@ -6,11 +6,10 @@ package v1_12
import (
"fmt"
"math"
"path/filepath"
"strings"
"time"
"code.gitea.io/gitea/modules/git"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
@ -85,12 +84,9 @@ func AddCommitDivergenceToPulls(x *xorm.Engine) error {
log.Error("Missing base repo with id %d for PR ID %d", pr.BaseRepoID, pr.ID)
continue
}
userPath := filepath.Join(setting.RepoRootPath, strings.ToLower(baseRepo.OwnerName))
repoPath := filepath.Join(userPath, strings.ToLower(baseRepo.Name)+".git")
repoStore := repo_model.StorageRepo(repo_model.RelativePath(baseRepo.OwnerName, baseRepo.Name))
gitRefName := fmt.Sprintf("refs/pull/%d/head", pr.Index)
divergence, err := git.GetDivergingCommits(graceful.GetManager().HammerContext(), repoPath, pr.BaseBranch, gitRefName)
divergence, err := gitrepo.GetDivergingCommits(graceful.GetManager().HammerContext(), repoStore, pr.BaseBranch, gitRefName)
if err != nil {
log.Warn("Could not recalculate Divergence for pull: %d", pr.ID)
pr.CommitsAhead = 0

View File

@ -377,20 +377,28 @@ func matchIssuesEvent(issuePayload *api.IssuePayload, evt *jobparser.Event) bool
// Actions with the same name:
// opened, edited, closed, reopened, assigned, unassigned, milestoned, demilestoned
// Actions need to be converted:
// label_updated -> labeled
// label_updated -> labeled (when adding) or unlabeled (when removing)
// label_cleared -> unlabeled
// Unsupported activity types:
// deleted, transferred, pinned, unpinned, locked, unlocked
action := issuePayload.Action
switch action {
actions := []string{}
switch issuePayload.Action {
case api.HookIssueLabelUpdated:
action = "labeled"
if len(issuePayload.Changes.AddedLabels) > 0 {
actions = append(actions, "labeled")
}
if len(issuePayload.Changes.RemovedLabels) > 0 {
actions = append(actions, "unlabeled")
}
case api.HookIssueLabelCleared:
action = "unlabeled"
actions = append(actions, "unlabeled")
default:
actions = append(actions, string(issuePayload.Action))
}
for _, val := range vals {
if glob.MustCompile(val, '/').Match(string(action)) {
if slices.ContainsFunc(actions, glob.MustCompile(val, '/').Match) {
matchTimes++
break
}

View File

@ -154,3 +154,184 @@ func TestDetectMatched(t *testing.T) {
})
}
}
func TestMatchIssuesEvent(t *testing.T) {
testCases := []struct {
desc string
payload *api.IssuePayload
yamlOn string
expected bool
eventType string
}{
{
desc: "Label deletion should trigger unlabeled event",
payload: &api.IssuePayload{
Action: api.HookIssueLabelUpdated,
Issue: &api.Issue{
Labels: []*api.Label{},
},
Changes: &api.ChangesPayload{
RemovedLabels: []*api.Label{
{ID: 123, Name: "deleted-label"},
},
},
},
yamlOn: "on:\n issues:\n types: [unlabeled]",
expected: true,
eventType: "unlabeled",
},
{
desc: "Label deletion with existing labels should trigger unlabeled event",
payload: &api.IssuePayload{
Action: api.HookIssueLabelUpdated,
Issue: &api.Issue{
Labels: []*api.Label{
{ID: 456, Name: "existing-label"},
},
},
Changes: &api.ChangesPayload{
AddedLabels: nil,
RemovedLabels: []*api.Label{
{ID: 123, Name: "deleted-label"},
},
},
},
yamlOn: "on:\n issues:\n types: [unlabeled]",
expected: true,
eventType: "unlabeled",
},
{
desc: "Label addition should trigger labeled event",
payload: &api.IssuePayload{
Action: api.HookIssueLabelUpdated,
Issue: &api.Issue{
Labels: []*api.Label{
{ID: 123, Name: "new-label"},
},
},
Changes: &api.ChangesPayload{
AddedLabels: []*api.Label{
{ID: 123, Name: "new-label"},
},
RemovedLabels: []*api.Label{}, // Empty array, no labels removed
},
},
yamlOn: "on:\n issues:\n types: [labeled]",
expected: true,
eventType: "labeled",
},
{
desc: "Label clear should trigger unlabeled event",
payload: &api.IssuePayload{
Action: api.HookIssueLabelCleared,
Issue: &api.Issue{
Labels: []*api.Label{},
},
},
yamlOn: "on:\n issues:\n types: [unlabeled]",
expected: true,
eventType: "unlabeled",
},
{
desc: "Both adding and removing labels should trigger labeled event",
payload: &api.IssuePayload{
Action: api.HookIssueLabelUpdated,
Issue: &api.Issue{
Labels: []*api.Label{
{ID: 789, Name: "new-label"},
},
},
Changes: &api.ChangesPayload{
AddedLabels: []*api.Label{
{ID: 789, Name: "new-label"},
},
RemovedLabels: []*api.Label{
{ID: 123, Name: "deleted-label"},
},
},
},
yamlOn: "on:\n issues:\n types: [labeled]",
expected: true,
eventType: "labeled",
},
{
desc: "Both adding and removing labels should trigger unlabeled event",
payload: &api.IssuePayload{
Action: api.HookIssueLabelUpdated,
Issue: &api.Issue{
Labels: []*api.Label{
{ID: 789, Name: "new-label"},
},
},
Changes: &api.ChangesPayload{
AddedLabels: []*api.Label{
{ID: 789, Name: "new-label"},
},
RemovedLabels: []*api.Label{
{ID: 123, Name: "deleted-label"},
},
},
},
yamlOn: "on:\n issues:\n types: [unlabeled]",
expected: true,
eventType: "unlabeled",
},
{
desc: "Both adding and removing labels should trigger both events",
payload: &api.IssuePayload{
Action: api.HookIssueLabelUpdated,
Issue: &api.Issue{
Labels: []*api.Label{
{ID: 789, Name: "new-label"},
},
},
Changes: &api.ChangesPayload{
AddedLabels: []*api.Label{
{ID: 789, Name: "new-label"},
},
RemovedLabels: []*api.Label{
{ID: 123, Name: "deleted-label"},
},
},
},
yamlOn: "on:\n issues:\n types: [labeled, unlabeled]",
expected: true,
eventType: "multiple",
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
evts, err := GetEventsFromContent([]byte(tc.yamlOn))
assert.NoError(t, err)
assert.Len(t, evts, 1)
// Test if the event matches as expected
assert.Equal(t, tc.expected, matchIssuesEvent(tc.payload, evts[0]))
// For extra validation, check that action mapping works correctly
if tc.eventType == "multiple" {
// Skip direct action mapping validation for multiple events case
// as one action can map to multiple event types
return
}
// Determine expected action for single event case
var expectedAction string
switch tc.payload.Action {
case api.HookIssueLabelUpdated:
if tc.eventType == "labeled" {
expectedAction = "labeled"
} else if tc.eventType == "unlabeled" {
expectedAction = "unlabeled"
}
case api.HookIssueLabelCleared:
expectedAction = "unlabeled"
default:
expectedAction = string(tc.payload.Action)
}
assert.Equal(t, expectedAction, tc.eventType, "Event type should match expected")
})
}
}

View File

@ -365,11 +365,11 @@ func GenerateEmbedBindata(fsRootPath, outputFile string) error {
if err = embedFiles(meta.Root, fsRootPath, ""); err != nil {
return err
}
jsonBuf, err := json.Marshal(meta) // can't use json.NewEncoder here because it writes extra EOL
jsonBuf, err := json.Marshal(meta)
if err != nil {
return err
}
_, _ = output.Write([]byte{'\n'})
_, err = output.Write(jsonBuf)
_, err = output.Write(bytes.TrimSpace(jsonBuf))
return err
}

View File

@ -243,36 +243,6 @@ func GetLatestCommitTime(ctx context.Context, repoPath string) (time.Time, error
return time.Parse("Mon Jan _2 15:04:05 2006 -0700", commitTime)
}
// DivergeObject represents commit count diverging commits
type DivergeObject struct {
Ahead int
Behind int
}
// GetDivergingCommits returns the number of commits a targetBranch is ahead or behind a baseBranch
func GetDivergingCommits(ctx context.Context, repoPath, baseBranch, targetBranch string) (do DivergeObject, err error) {
cmd := gitcmd.NewCommand("rev-list", "--count", "--left-right").
AddDynamicArguments(baseBranch + "..." + targetBranch).AddArguments("--")
stdout, _, err := cmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath})
if err != nil {
return do, err
}
left, right, found := strings.Cut(strings.Trim(stdout, "\n"), "\t")
if !found {
return do, fmt.Errorf("git rev-list output is missing a tab: %q", stdout)
}
do.Behind, err = strconv.Atoi(left)
if err != nil {
return do, err
}
do.Ahead, err = strconv.Atoi(right)
if err != nil {
return do, err
}
return do, nil
}
// CreateBundle create bundle content to the target path
func (repo *Repository) CreateBundle(ctx context.Context, commit string, out io.Writer) error {
tmp, cleanup, err := setting.AppDataTempDir("git-repo-content").MkdirTempRandom("gitea-bundle")

View File

@ -1,25 +0,0 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"fmt"
"code.gitea.io/gitea/modules/git/gitcmd"
)
// LineBlame returns the latest commit at the given line
func (repo *Repository) LineBlame(revision, path, file string, line uint) (*Commit, error) {
res, _, err := gitcmd.NewCommand("blame").
AddOptionFormat("-L %d,%d", line, line).
AddOptionValues("-p", revision).
AddDashesAndList(file).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: path})
if err != nil {
return nil, err
}
if len(res) < 40 {
return nil, fmt.Errorf("invalid result of blame: %s", res)
}
return repo.GetCommit(res[:40])
}

View File

@ -5,70 +5,12 @@
package git
import (
"context"
"errors"
"strings"
"code.gitea.io/gitea/modules/git/gitcmd"
)
// BranchPrefix base dir of the branch information file store on git
const BranchPrefix = "refs/heads/"
// IsReferenceExist returns true if given reference exists in the repository.
func IsReferenceExist(ctx context.Context, repoPath, name string) bool {
_, _, err := gitcmd.NewCommand("show-ref", "--verify").AddDashesAndList(name).RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath})
return err == nil
}
// IsBranchExist returns true if given branch exists in the repository.
func IsBranchExist(ctx context.Context, repoPath, name string) bool {
return IsReferenceExist(ctx, repoPath, BranchPrefix+name)
}
func GetDefaultBranch(ctx context.Context, repoPath string) (string, error) {
stdout, _, err := gitcmd.NewCommand("symbolic-ref", "HEAD").RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath})
if err != nil {
return "", err
}
stdout = strings.TrimSpace(stdout)
if !strings.HasPrefix(stdout, BranchPrefix) {
return "", errors.New("the HEAD is not a branch: " + stdout)
}
return strings.TrimPrefix(stdout, BranchPrefix), nil
}
// DeleteBranchOptions Option(s) for delete branch
type DeleteBranchOptions struct {
Force bool
}
// DeleteBranch delete a branch by name on repository.
func (repo *Repository) DeleteBranch(name string, opts DeleteBranchOptions) error {
cmd := gitcmd.NewCommand("branch")
if opts.Force {
cmd.AddArguments("-D")
} else {
cmd.AddArguments("-d")
}
cmd.AddDashesAndList(name)
_, _, err := cmd.RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path})
return err
}
// CreateBranch create a new branch
func (repo *Repository) CreateBranch(branch, oldbranchOrCommit string) error {
cmd := gitcmd.NewCommand("branch")
cmd.AddDashesAndList(branch, oldbranchOrCommit)
_, _, err := cmd.RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path})
return err
}
// AddRemote adds a new remote to repository.
func (repo *Repository) AddRemote(name, url string, fetch bool) error {
cmd := gitcmd.NewCommand("remote", "add")
@ -80,9 +22,3 @@ func (repo *Repository) AddRemote(name, url string, fetch bool) error {
_, _, err := cmd.RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path})
return err
}
// RenameBranch rename a branch
func (repo *Repository) RenameBranch(from, to string) error {
_, _, err := gitcmd.NewCommand("branch", "-m").AddDynamicArguments(from, to).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path})
return err
}

View File

@ -7,14 +7,12 @@ package git
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"code.gitea.io/gitea/modules/git/gitcmd"
@ -87,57 +85,8 @@ func (repo *Repository) GetDiffNumChangedFiles(base, head string, directComparis
return w.numLines, nil
}
// GetDiffShortStatByCmdArgs counts number of changed files, number of additions and deletions
// TODO: it can be merged with another "GetDiffShortStat" in the future
func GetDiffShortStatByCmdArgs(ctx context.Context, repoPath string, trustedArgs gitcmd.TrustedCmdArgs, dynamicArgs ...string) (numFiles, totalAdditions, totalDeletions int, err error) {
// Now if we call:
// $ git diff --shortstat 1ebb35b98889ff77299f24d82da426b434b0cca0...788b8b1440462d477f45b0088875
// we get:
// " 9902 files changed, 2034198 insertions(+), 298800 deletions(-)\n"
cmd := gitcmd.NewCommand("diff", "--shortstat").AddArguments(trustedArgs...).AddDynamicArguments(dynamicArgs...)
stdout, _, err := cmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath})
if err != nil {
return 0, 0, 0, err
}
return parseDiffStat(stdout)
}
var shortStatFormat = regexp.MustCompile(
`\s*(\d+) files? changed(?:, (\d+) insertions?\(\+\))?(?:, (\d+) deletions?\(-\))?`)
var patchCommits = regexp.MustCompile(`^From\s(\w+)\s`)
func parseDiffStat(stdout string) (numFiles, totalAdditions, totalDeletions int, err error) {
if len(stdout) == 0 || stdout == "\n" {
return 0, 0, 0, nil
}
groups := shortStatFormat.FindStringSubmatch(stdout)
if len(groups) != 4 {
return 0, 0, 0, fmt.Errorf("unable to parse shortstat: %s groups: %s", stdout, groups)
}
numFiles, err = strconv.Atoi(groups[1])
if err != nil {
return 0, 0, 0, fmt.Errorf("unable to parse shortstat: %s. Error parsing NumFiles %w", stdout, err)
}
if len(groups[2]) != 0 {
totalAdditions, err = strconv.Atoi(groups[2])
if err != nil {
return 0, 0, 0, fmt.Errorf("unable to parse shortstat: %s. Error parsing NumAdditions %w", stdout, err)
}
}
if len(groups[3]) != 0 {
totalDeletions, err = strconv.Atoi(groups[3])
if err != nil {
return 0, 0, 0, fmt.Errorf("unable to parse shortstat: %s. Error parsing NumDeletions %w", stdout, err)
}
}
return numFiles, totalAdditions, totalDeletions, err
}
// GetDiff generates and returns patch data between given revisions, optimized for human readability
func (repo *Repository) GetDiff(compareArg string, w io.Writer) error {
stderr := new(bytes.Buffer)

View File

@ -29,27 +29,3 @@ func TestRepoIsEmpty(t *testing.T) {
assert.NoError(t, err)
assert.True(t, isEmpty)
}
func TestRepoGetDivergingCommits(t *testing.T) {
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
do, err := GetDivergingCommits(t.Context(), bareRepo1Path, "master", "branch2")
assert.NoError(t, err)
assert.Equal(t, DivergeObject{
Ahead: 1,
Behind: 5,
}, do)
do, err = GetDivergingCommits(t.Context(), bareRepo1Path, "master", "master")
assert.NoError(t, err)
assert.Equal(t, DivergeObject{
Ahead: 0,
Behind: 0,
}, do)
do, err = GetDivergingCommits(t.Context(), bareRepo1Path, "master", "test")
assert.NoError(t, err)
assert.Equal(t, DivergeObject{
Ahead: 0,
Behind: 2,
}, do)
}

18
modules/gitrepo/blame.go Normal file
View File

@ -0,0 +1,18 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package gitrepo
import (
"context"
"code.gitea.io/gitea/modules/git/gitcmd"
)
func LineBlame(ctx context.Context, repo Repository, revision, file string, line uint) (string, error) {
return runCmdString(ctx, repo,
gitcmd.NewCommand("blame").
AddOptionFormat("-L %d,%d", line, line).
AddOptionValues("-p", revision).
AddDashesAndList(file))
}

View File

@ -5,6 +5,8 @@ package gitrepo
import (
"context"
"errors"
"strings"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
@ -34,23 +36,61 @@ func GetBranchCommitID(ctx context.Context, repo Repository, branch string) (str
// SetDefaultBranch sets default branch of repository.
func SetDefaultBranch(ctx context.Context, repo Repository, name string) error {
_, _, err := gitcmd.NewCommand("symbolic-ref", "HEAD").
AddDynamicArguments(git.BranchPrefix+name).
RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath(repo)})
_, err := runCmdString(ctx, repo, gitcmd.NewCommand("symbolic-ref", "HEAD").
AddDynamicArguments(git.BranchPrefix+name))
return err
}
// GetDefaultBranch gets default branch of repository.
func GetDefaultBranch(ctx context.Context, repo Repository) (string, error) {
return git.GetDefaultBranch(ctx, repoPath(repo))
stdout, err := runCmdString(ctx, repo, gitcmd.NewCommand("symbolic-ref", "HEAD"))
if err != nil {
return "", err
}
stdout = strings.TrimSpace(stdout)
if !strings.HasPrefix(stdout, git.BranchPrefix) {
return "", errors.New("the HEAD is not a branch: " + stdout)
}
return strings.TrimPrefix(stdout, git.BranchPrefix), nil
}
// IsReferenceExist returns true if given reference exists in the repository.
func IsReferenceExist(ctx context.Context, repo Repository, name string) bool {
return git.IsReferenceExist(ctx, repoPath(repo), name)
_, err := runCmdString(ctx, repo, gitcmd.NewCommand("show-ref", "--verify").AddDashesAndList(name))
return err == nil
}
// IsBranchExist returns true if given branch exists in the repository.
func IsBranchExist(ctx context.Context, repo Repository, name string) bool {
return IsReferenceExist(ctx, repo, git.BranchPrefix+name)
}
// DeleteBranch delete a branch by name on repository.
func DeleteBranch(ctx context.Context, repo Repository, name string, force bool) error {
cmd := gitcmd.NewCommand("branch")
if force {
cmd.AddArguments("-D")
} else {
cmd.AddArguments("-d")
}
cmd.AddDashesAndList(name)
_, err := runCmdString(ctx, repo, cmd)
return err
}
// CreateBranch create a new branch
func CreateBranch(ctx context.Context, repo Repository, branch, oldbranchOrCommit string) error {
cmd := gitcmd.NewCommand("branch")
cmd.AddDashesAndList(branch, oldbranchOrCommit)
_, err := runCmdString(ctx, repo, cmd)
return err
}
// RenameBranch rename a branch
func RenameBranch(ctx context.Context, repo Repository, from, to string) error {
_, err := runCmdString(ctx, repo, gitcmd.NewCommand("branch", "-m").AddDynamicArguments(from, to))
return err
}

View File

@ -0,0 +1,15 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package gitrepo
import (
"context"
"code.gitea.io/gitea/modules/git/gitcmd"
)
func runCmdString(ctx context.Context, repo Repository, cmd *gitcmd.Command) (string, error) {
res, _, err := cmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath(repo)})
return res, err
}

View File

@ -0,0 +1,44 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package gitrepo
import (
"context"
"fmt"
"strconv"
"strings"
"code.gitea.io/gitea/modules/git/gitcmd"
)
// DivergeObject represents commit count diverging commits
type DivergeObject struct {
Ahead int
Behind int
}
// GetDivergingCommits returns the number of commits a targetBranch is ahead or behind a baseBranch
func GetDivergingCommits(ctx context.Context, repo Repository, baseBranch, targetBranch string) (*DivergeObject, error) {
cmd := gitcmd.NewCommand("rev-list", "--count", "--left-right").
AddDynamicArguments(baseBranch + "..." + targetBranch).AddArguments("--")
stdout, _, err1 := cmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath(repo)})
if err1 != nil {
return nil, err1
}
left, right, found := strings.Cut(strings.Trim(stdout, "\n"), "\t")
if !found {
return nil, fmt.Errorf("git rev-list output is missing a tab: %q", stdout)
}
behind, err := strconv.Atoi(left)
if err != nil {
return nil, err
}
ahead, err := strconv.Atoi(right)
if err != nil {
return nil, err
}
return &DivergeObject{Ahead: ahead, Behind: behind}, nil
}

View File

@ -0,0 +1,42 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package gitrepo
import (
"testing"
"github.com/stretchr/testify/assert"
)
type mockRepository struct {
path string
}
func (r *mockRepository) RelativePath() string {
return r.path
}
func TestRepoGetDivergingCommits(t *testing.T) {
repo := &mockRepository{path: "repo1_bare"}
do, err := GetDivergingCommits(t.Context(), repo, "master", "branch2")
assert.NoError(t, err)
assert.Equal(t, &DivergeObject{
Ahead: 1,
Behind: 5,
}, do)
do, err = GetDivergingCommits(t.Context(), repo, "master", "master")
assert.NoError(t, err)
assert.Equal(t, &DivergeObject{
Ahead: 0,
Behind: 0,
}, do)
do, err = GetDivergingCommits(t.Context(), repo, "master", "test")
assert.NoError(t, err)
assert.Equal(t, &DivergeObject{
Ahead: 0,
Behind: 2,
}, do)
}

View File

@ -12,9 +12,8 @@ import (
)
func GitConfigGet(ctx context.Context, repo Repository, key string) (string, error) {
result, _, err := gitcmd.NewCommand("config", "--get").
AddDynamicArguments(key).
RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath(repo)})
result, err := runCmdString(ctx, repo, gitcmd.NewCommand("config", "--get").
AddDynamicArguments(key))
if err != nil {
return "", err
}
@ -28,9 +27,8 @@ func getRepoConfigLockKey(repoStoragePath string) string {
// GitConfigAdd add a git configuration key to a specific value for the given repository.
func GitConfigAdd(ctx context.Context, repo Repository, key, value string) error {
return globallock.LockAndDo(ctx, getRepoConfigLockKey(repo.RelativePath()), func(ctx context.Context) error {
_, _, err := gitcmd.NewCommand("config", "--add").
AddDynamicArguments(key, value).
RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath(repo)})
_, err := runCmdString(ctx, repo, gitcmd.NewCommand("config", "--add").
AddDynamicArguments(key, value))
return err
})
}
@ -40,9 +38,8 @@ func GitConfigAdd(ctx context.Context, repo Repository, key, value string) error
// If the key exists, it will be updated to the new value.
func GitConfigSet(ctx context.Context, repo Repository, key, value string) error {
return globallock.LockAndDo(ctx, getRepoConfigLockKey(repo.RelativePath()), func(ctx context.Context) error {
_, _, err := gitcmd.NewCommand("config").
AddDynamicArguments(key, value).
RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath(repo)})
_, err := runCmdString(ctx, repo, gitcmd.NewCommand("config").
AddDynamicArguments(key, value))
return err
})
}

62
modules/gitrepo/diff.go Normal file
View File

@ -0,0 +1,62 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package gitrepo
import (
"context"
"fmt"
"regexp"
"strconv"
"code.gitea.io/gitea/modules/git/gitcmd"
)
// GetDiffShortStatByCmdArgs counts number of changed files, number of additions and deletions
// TODO: it can be merged with another "GetDiffShortStat" in the future
func GetDiffShortStatByCmdArgs(ctx context.Context, repo Repository, trustedArgs gitcmd.TrustedCmdArgs, dynamicArgs ...string) (numFiles, totalAdditions, totalDeletions int, err error) {
// Now if we call:
// $ git diff --shortstat 1ebb35b98889ff77299f24d82da426b434b0cca0...788b8b1440462d477f45b0088875
// we get:
// " 9902 files changed, 2034198 insertions(+), 298800 deletions(-)\n"
cmd := gitcmd.NewCommand("diff", "--shortstat").AddArguments(trustedArgs...).AddDynamicArguments(dynamicArgs...)
stdout, err := runCmdString(ctx, repo, cmd)
if err != nil {
return 0, 0, 0, err
}
return parseDiffStat(stdout)
}
var shortStatFormat = regexp.MustCompile(
`\s*(\d+) files? changed(?:, (\d+) insertions?\(\+\))?(?:, (\d+) deletions?\(-\))?`)
func parseDiffStat(stdout string) (numFiles, totalAdditions, totalDeletions int, err error) {
if len(stdout) == 0 || stdout == "\n" {
return 0, 0, 0, nil
}
groups := shortStatFormat.FindStringSubmatch(stdout)
if len(groups) != 4 {
return 0, 0, 0, fmt.Errorf("unable to parse shortstat: %s groups: %s", stdout, groups)
}
numFiles, err = strconv.Atoi(groups[1])
if err != nil {
return 0, 0, 0, fmt.Errorf("unable to parse shortstat: %s. Error parsing NumFiles %w", stdout, err)
}
if len(groups[2]) != 0 {
totalAdditions, err = strconv.Atoi(groups[2])
if err != nil {
return 0, 0, 0, fmt.Errorf("unable to parse shortstat: %s. Error parsing NumAdditions %w", stdout, err)
}
}
if len(groups[3]) != 0 {
totalDeletions, err = strconv.Atoi(groups[3])
if err != nil {
return 0, 0, 0, fmt.Errorf("unable to parse shortstat: %s. Error parsing NumDeletions %w", stdout, err)
}
}
return numFiles, totalAdditions, totalDeletions, err
}

View File

@ -20,9 +20,9 @@ type Repository interface {
RelativePath() string // We don't assume how the directory structure of the repository is, so we only need the relative path
}
// RelativePath should be an unix style path like username/reponame.git
// This method should change it according to the current OS.
func repoPath(repo Repository) string {
// repoPath resolves the Repository.RelativePath (which is a unix-style path like "username/reponame.git")
// to a local filesystem path according to setting.RepoRootPath
var repoPath = func(repo Repository) string {
return filepath.Join(setting.RepoRootPath, filepath.FromSlash(repo.RelativePath()))
}

View File

@ -0,0 +1,32 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package gitrepo
import (
"os"
"path/filepath"
"testing"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/tempdir"
"code.gitea.io/gitea/modules/test"
)
func TestMain(m *testing.M) {
gitHomePath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("git-home")
if err != nil {
log.Fatal("Unable to create temp dir: %v", err)
}
defer cleanup()
// resolve repository path relative to the test directory
testRootDir := test.SetupGiteaRoot()
repoPath = func(repo Repository) string {
return filepath.Join(testRootDir, "/modules/git/tests/repos", repo.RelativePath())
}
setting.Git.HomePath = gitHomePath
os.Exit(m.Run())
}

View File

@ -36,9 +36,7 @@ func GitRemoteAdd(ctx context.Context, repo Repository, remoteName, remoteURL st
return errors.New("unknown remote option: " + string(options[0]))
}
}
_, _, err := cmd.
AddDynamicArguments(remoteName, remoteURL).
RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath(repo)})
_, err := runCmdString(ctx, repo, cmd.AddDynamicArguments(remoteName, remoteURL))
return err
})
}
@ -46,7 +44,7 @@ func GitRemoteAdd(ctx context.Context, repo Repository, remoteName, remoteURL st
func GitRemoteRemove(ctx context.Context, repo Repository, remoteName string) error {
return globallock.LockAndDo(ctx, getRepoConfigLockKey(repo.RelativePath()), func(ctx context.Context) error {
cmd := gitcmd.NewCommand("remote", "rm").AddDynamicArguments(remoteName)
_, _, err := cmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath(repo)})
_, err := runCmdString(ctx, repo, cmd)
return err
})
}

View File

@ -8,8 +8,6 @@ import (
"encoding/binary"
"encoding/json" //nolint:depguard // this package wraps it
"io"
jsoniter "github.com/json-iterator/go"
)
// Encoder represents an encoder for json
@ -31,71 +29,7 @@ type Interface interface {
Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error
}
var (
// DefaultJSONHandler default json handler
DefaultJSONHandler Interface = JSONiter{jsoniter.ConfigCompatibleWithStandardLibrary}
_ Interface = StdJSON{}
_ Interface = JSONiter{}
)
// StdJSON implements Interface via encoding/json
type StdJSON struct{}
// Marshal implements Interface
func (StdJSON) Marshal(v any) ([]byte, error) {
return json.Marshal(v)
}
// Unmarshal implements Interface
func (StdJSON) Unmarshal(data []byte, v any) error {
return json.Unmarshal(data, v)
}
// NewEncoder implements Interface
func (StdJSON) NewEncoder(writer io.Writer) Encoder {
return json.NewEncoder(writer)
}
// NewDecoder implements Interface
func (StdJSON) NewDecoder(reader io.Reader) Decoder {
return json.NewDecoder(reader)
}
// Indent implements Interface
func (StdJSON) Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error {
return json.Indent(dst, src, prefix, indent)
}
// JSONiter implements Interface via jsoniter
type JSONiter struct {
jsoniter.API
}
// Marshal implements Interface
func (j JSONiter) Marshal(v any) ([]byte, error) {
return j.API.Marshal(v)
}
// Unmarshal implements Interface
func (j JSONiter) Unmarshal(data []byte, v any) error {
return j.API.Unmarshal(data, v)
}
// NewEncoder implements Interface
func (j JSONiter) NewEncoder(writer io.Writer) Encoder {
return j.API.NewEncoder(writer)
}
// NewDecoder implements Interface
func (j JSONiter) NewDecoder(reader io.Reader) Decoder {
return j.API.NewDecoder(reader)
}
// Indent implements Interface, since jsoniter don't support Indent, just use encoding/json's
func (j JSONiter) Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error {
return json.Indent(dst, src, prefix, indent)
}
var DefaultJSONHandler = getDefaultJSONHandler()
// Marshal converts object as bytes
func Marshal(v any) ([]byte, error) {

View File

@ -4,6 +4,7 @@
package json
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
@ -16,3 +17,12 @@ func TestGiteaDBJSONUnmarshal(t *testing.T) {
err = UnmarshalHandleDoubleEncode([]byte(""), &m)
assert.NoError(t, err)
}
func TestIndent(t *testing.T) {
buf := &bytes.Buffer{}
err := Indent(buf, []byte(`{"a":1}`), ">", " ")
assert.NoError(t, err)
assert.Equal(t, `{
> "a": 1
>}`, buf.String())
}

35
modules/json/jsongoccy.go Normal file
View File

@ -0,0 +1,35 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package json
import (
"bytes"
"io"
"github.com/goccy/go-json"
)
var _ Interface = jsonGoccy{}
type jsonGoccy struct{}
func (jsonGoccy) Marshal(v any) ([]byte, error) {
return json.Marshal(v)
}
func (jsonGoccy) Unmarshal(data []byte, v any) error {
return json.Unmarshal(data, v)
}
func (jsonGoccy) NewEncoder(writer io.Writer) Encoder {
return json.NewEncoder(writer)
}
func (jsonGoccy) NewDecoder(reader io.Reader) Decoder {
return json.NewDecoder(reader)
}
func (jsonGoccy) Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error {
return json.Indent(dst, src, prefix, indent)
}

View File

@ -0,0 +1,22 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !goexperiment.jsonv2
package json
import (
"io"
)
func getDefaultJSONHandler() Interface {
return jsonGoccy{}
}
func MarshalKeepOptionalEmpty(v any) ([]byte, error) {
return DefaultJSONHandler.Marshal(v)
}
func NewDecoderCaseInsensitive(reader io.Reader) Decoder {
return DefaultJSONHandler.NewDecoder(reader)
}

34
modules/json/jsonv1.go Normal file
View File

@ -0,0 +1,34 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package json
import (
"bytes"
"encoding/json" //nolint:depguard // this package wraps it
"io"
)
type jsonV1 struct{}
var _ Interface = jsonV1{}
func (jsonV1) Marshal(v any) ([]byte, error) {
return json.Marshal(v)
}
func (jsonV1) Unmarshal(data []byte, v any) error {
return json.Unmarshal(data, v)
}
func (jsonV1) NewEncoder(writer io.Writer) Encoder {
return json.NewEncoder(writer)
}
func (jsonV1) NewDecoder(reader io.Reader) Decoder {
return json.NewDecoder(reader)
}
func (jsonV1) Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error {
return json.Indent(dst, src, prefix, indent)
}

92
modules/json/jsonv2.go Normal file
View File

@ -0,0 +1,92 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build goexperiment.jsonv2
package json
import (
"bytes"
jsonv1 "encoding/json" //nolint:depguard // this package wraps it
jsonv2 "encoding/json/v2" //nolint:depguard // this package wraps it
"io"
)
// JSONv2 implements Interface via encoding/json/v2
// Requires GOEXPERIMENT=jsonv2 to be set at build time
type JSONv2 struct {
marshalOptions jsonv2.Options
marshalKeepOptionalEmptyOptions jsonv2.Options
unmarshalOptions jsonv2.Options
unmarshalCaseInsensitiveOptions jsonv2.Options
}
var jsonV2 JSONv2
func init() {
commonMarshalOptions := []jsonv2.Options{
jsonv2.FormatNilSliceAsNull(true),
jsonv2.FormatNilMapAsNull(true),
}
jsonV2.marshalOptions = jsonv2.JoinOptions(commonMarshalOptions...)
jsonV2.unmarshalOptions = jsonv2.DefaultOptionsV2()
// By default, "json/v2" omitempty removes all `""` empty strings, no matter where it comes from.
// v1 has a different behavior: if the `""` is from a null pointer, or a Marshal function, it is kept.
// Golang issue: https://github.com/golang/go/issues/75623 encoding/json/v2: unable to make omitempty work with pointer or Optional type with goexperiment.jsonv2
jsonV2.marshalKeepOptionalEmptyOptions = jsonv2.JoinOptions(append(commonMarshalOptions, jsonv1.OmitEmptyWithLegacySemantics(true))...)
// Some legacy code uses case-insensitive matching (for example: parsing oci.ImageConfig)
jsonV2.unmarshalCaseInsensitiveOptions = jsonv2.JoinOptions(jsonv2.MatchCaseInsensitiveNames(true))
}
func getDefaultJSONHandler() Interface {
return &jsonV2
}
func MarshalKeepOptionalEmpty(v any) ([]byte, error) {
return jsonv2.Marshal(v, jsonV2.marshalKeepOptionalEmptyOptions)
}
func (j *JSONv2) Marshal(v any) ([]byte, error) {
return jsonv2.Marshal(v, j.marshalOptions)
}
func (j *JSONv2) Unmarshal(data []byte, v any) error {
return jsonv2.Unmarshal(data, v, j.unmarshalOptions)
}
func (j *JSONv2) NewEncoder(writer io.Writer) Encoder {
return &jsonV2Encoder{writer: writer, opts: j.marshalOptions}
}
func (j *JSONv2) NewDecoder(reader io.Reader) Decoder {
return &jsonV2Decoder{reader: reader, opts: j.unmarshalOptions}
}
// Indent implements Interface using standard library (JSON v2 doesn't have Indent yet)
func (*JSONv2) Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error {
return jsonv1.Indent(dst, src, prefix, indent)
}
type jsonV2Encoder struct {
writer io.Writer
opts jsonv2.Options
}
func (e *jsonV2Encoder) Encode(v any) error {
return jsonv2.MarshalWrite(e.writer, v, e.opts)
}
type jsonV2Decoder struct {
reader io.Reader
opts jsonv2.Options
}
func (d *jsonV2Decoder) Decode(v any) error {
return jsonv2.UnmarshalRead(d.reader, v, d.opts)
}
func NewDecoderCaseInsensitive(reader io.Reader) Decoder {
return &jsonV2Decoder{reader: reader, opts: jsonV2.unmarshalCaseInsensitiveOptions}
}

View File

@ -193,7 +193,7 @@ func TestHTTPClientDownload(t *testing.T) {
},
{
endpoint: "https://invalid-json-response.io",
expectedError: "invalid json",
expectedError: "/(invalid json|invalid character)/",
},
{
endpoint: "https://valid-batch-request-download.io",
@ -258,7 +258,11 @@ func TestHTTPClientDownload(t *testing.T) {
return nil
})
if c.expectedError != "" {
assert.ErrorContains(t, err, c.expectedError)
if strings.HasPrefix(c.expectedError, "/") && strings.HasSuffix(c.expectedError, "/") {
assert.Regexp(t, strings.Trim(c.expectedError, "/"), err.Error())
} else {
assert.ErrorContains(t, err, c.expectedError)
}
} else {
assert.NoError(t, err)
}
@ -297,7 +301,7 @@ func TestHTTPClientUpload(t *testing.T) {
},
{
endpoint: "https://invalid-json-response.io",
expectedError: "invalid json",
expectedError: "/(invalid json|invalid character)/",
},
{
endpoint: "https://valid-batch-request-upload.io",
@ -352,7 +356,11 @@ func TestHTTPClientUpload(t *testing.T) {
return io.NopCloser(new(bytes.Buffer)), objectError
})
if c.expectedError != "" {
assert.ErrorContains(t, err, c.expectedError)
if strings.HasPrefix(c.expectedError, "/") && strings.HasSuffix(c.expectedError, "/") {
assert.Regexp(t, strings.Trim(c.expectedError, "/"), err.Error())
} else {
assert.ErrorContains(t, err, c.expectedError)
}
} else {
assert.NoError(t, err)
}

View File

@ -15,12 +15,17 @@ import (
)
type testSerializationStruct struct {
NormalString string `json:"normal_string" yaml:"normal_string"`
NormalBool bool `json:"normal_bool" yaml:"normal_bool"`
OptBool optional.Option[bool] `json:"optional_bool,omitempty" yaml:"optional_bool,omitempty"`
OptString optional.Option[string] `json:"optional_string,omitempty" yaml:"optional_string,omitempty"`
NormalString string `json:"normal_string" yaml:"normal_string"`
NormalBool bool `json:"normal_bool" yaml:"normal_bool"`
OptBool optional.Option[bool] `json:"optional_bool,omitempty" yaml:"optional_bool,omitempty"`
// It causes an undefined behavior: should the "omitempty" tag only omit "null", or also the empty string?
// The behavior is inconsistent between json and v2 packages, and there is no such use case in Gitea.
// If anyone really needs it, they can use json.MarshalKeepOptionalEmpty to revert the v1 behavior
OptString optional.Option[string] `json:"optional_string,omitempty" yaml:"optional_string,omitempty"`
OptTwoBool optional.Option[bool] `json:"optional_two_bool" yaml:"optional_two_bool"`
OptTwoString optional.Option[string] `json:"optional_twostring" yaml:"optional_two_string"`
OptTwoString optional.Option[string] `json:"optional_two_string" yaml:"optional_two_string"`
}
func TestOptionalToJson(t *testing.T) {
@ -32,7 +37,7 @@ func TestOptionalToJson(t *testing.T) {
{
name: "empty",
obj: new(testSerializationStruct),
want: `{"normal_string":"","normal_bool":false,"optional_two_bool":null,"optional_twostring":null}`,
want: `{"normal_string":"","normal_bool":false,"optional_two_bool":null,"optional_two_string":null}`,
},
{
name: "some",
@ -44,12 +49,12 @@ func TestOptionalToJson(t *testing.T) {
OptTwoBool: optional.None[bool](),
OptTwoString: optional.None[string](),
},
want: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_twostring":null}`,
want: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_two_string":null}`,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
b, err := json.Marshal(tc.obj)
b, err := json.MarshalKeepOptionalEmpty(tc.obj)
assert.NoError(t, err)
assert.Equal(t, tc.want, string(b), "gitea json module returned unexpected")
@ -75,7 +80,7 @@ func TestOptionalFromJson(t *testing.T) {
},
{
name: "some",
data: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_twostring":null}`,
data: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_two_string":null}`,
want: testSerializationStruct{
NormalString: "a string",
NormalBool: true,
@ -169,7 +174,7 @@ normal_bool: true
optional_bool: false
optional_string: ""
optional_two_bool: null
optional_twostring: null
optional_two_string: null
`,
want: testSerializationStruct{
NormalString: "a string",

View File

@ -103,7 +103,9 @@ func ParseImageConfig(mediaType string, r io.Reader) (*Metadata, error) {
func parseOCIImageConfig(r io.Reader) (*Metadata, error) {
var image oci.Image
if err := json.NewDecoder(r).Decode(&image); err != nil {
// FIXME: JSON-KEY-CASE: here seems a abuse of the case-insensitive decoding feature, spec is case-sensitive
// https://github.com/opencontainers/image-spec/blob/main/schema/config-schema.json
if err := json.NewDecoderCaseInsensitive(r).Decode(&image); err != nil {
return nil, err
}

View File

@ -22,6 +22,8 @@ func TestParseImageConfig(t *testing.T) {
repositoryURL := "https://gitea.com/gitea"
documentationURL := "https://docs.gitea.com"
// FIXME: JSON-KEY-CASE: the test case is not right, the config fields are capitalized in the spec
// https://github.com/opencontainers/image-spec/blob/main/schema/config-schema.json
configOCI := `{"config": {"labels": {"` + labelAuthors + `": "` + author + `", "` + labelLicenses + `": "` + license + `", "` + labelURL + `": "` + projectURL + `", "` + labelSource + `": "` + repositoryURL + `", "` + labelDocumentation + `": "` + documentationURL + `", "` + labelDescription + `": "` + description + `"}}, "history": [{"created_by": "do it 1"}, {"created_by": "dummy #(nop) do it 2"}]}`
metadata, err := ParseImageConfig(oci.MediaTypeImageManifest, strings.NewReader(configOCI))

View File

@ -5,6 +5,7 @@ package session
import (
"context"
"fmt"
"log"
"sync"
@ -121,12 +122,12 @@ func (p *DBProvider) Read(sid string) (session.RawStore, error) {
}
// Exist returns true if session with given ID exists.
func (p *DBProvider) Exist(sid string) bool {
func (p *DBProvider) Exist(sid string) (bool, error) {
has, err := auth.ExistSession(dbContext(), sid)
if err != nil {
panic("session/DB: error checking existence: " + err.Error())
return false, fmt.Errorf("session/DB: error checking existence: %w", err)
}
return has
return has, nil
}
// Destroy deletes a session by session ID.
@ -155,12 +156,12 @@ func (p *DBProvider) Regenerate(oldsid, sid string) (_ session.RawStore, err err
}
// Count counts and returns number of sessions.
func (p *DBProvider) Count() int {
func (p *DBProvider) Count() (int, error) {
total, err := auth.CountSessions(dbContext())
if err != nil {
panic("session/DB: error counting records: " + err.Error())
return 0, fmt.Errorf("session/DB: error counting records: %w", err)
}
return int(total)
return int(total), nil
}
// GC calls GC to clean expired sessions.

View File

@ -135,10 +135,12 @@ func (p *RedisProvider) Init(maxlifetime int64, configs string) (err error) {
// Read returns raw session store by session ID.
func (p *RedisProvider) Read(sid string) (session.RawStore, error) {
psid := p.prefix + sid
if !p.Exist(sid) {
if exist, err := p.Exist(sid); err == nil && !exist {
if err := p.c.Set(graceful.GetManager().HammerContext(), psid, "", p.duration).Err(); err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
var kv map[any]any
@ -159,9 +161,9 @@ func (p *RedisProvider) Read(sid string) (session.RawStore, error) {
}
// Exist returns true if session with given ID exists.
func (p *RedisProvider) Exist(sid string) bool {
func (p *RedisProvider) Exist(sid string) (bool, error) {
v, err := p.c.Exists(graceful.GetManager().HammerContext(), p.prefix+sid).Result()
return err == nil && v == 1
return err == nil && v == 1, err
}
// Destroy deletes a session by session ID.
@ -174,13 +176,18 @@ func (p *RedisProvider) Regenerate(oldsid, sid string) (_ session.RawStore, err
poldsid := p.prefix + oldsid
psid := p.prefix + sid
if p.Exist(sid) {
if exist, err := p.Exist(sid); err != nil {
return nil, err
} else if exist {
return nil, fmt.Errorf("new sid '%s' already exists", sid)
} else if !p.Exist(oldsid) {
}
if exist, err := p.Exist(oldsid); err == nil && !exist {
// Make a fake old session.
if err = p.c.Set(graceful.GetManager().HammerContext(), poldsid, "", p.duration).Err(); err != nil {
if err := p.c.Set(graceful.GetManager().HammerContext(), poldsid, "", p.duration).Err(); err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
// do not use Rename here, because the old sid and new sid may be in different redis cluster slot.
@ -211,12 +218,9 @@ func (p *RedisProvider) Regenerate(oldsid, sid string) (_ session.RawStore, err
}
// Count counts and returns number of sessions.
func (p *RedisProvider) Count() int {
func (p *RedisProvider) Count() (int, error) {
size, err := p.c.DBSize(graceful.GetManager().HammerContext()).Result()
if err != nil {
return 0
}
return int(size)
return int(size), err
}
// GC calls GC to clean expired sessions.

View File

@ -59,8 +59,10 @@ func (o *VirtualSessionProvider) Init(gcLifetime int64, config string) error {
func (o *VirtualSessionProvider) Read(sid string) (session.RawStore, error) {
o.lock.RLock()
defer o.lock.RUnlock()
if o.provider.Exist(sid) {
if exist, err := o.provider.Exist(sid); err == nil && exist {
return o.provider.Read(sid)
} else if err != nil {
return nil, fmt.Errorf("check if '%s' exist failed: %w", sid, err)
}
kv := make(map[any]any)
kv["_old_uid"] = "0"
@ -68,8 +70,8 @@ func (o *VirtualSessionProvider) Read(sid string) (session.RawStore, error) {
}
// Exist returns true if session with given ID exists.
func (o *VirtualSessionProvider) Exist(sid string) bool {
return true
func (o *VirtualSessionProvider) Exist(sid string) (bool, error) {
return true, nil
}
// Destroy deletes a session by session ID.
@ -87,7 +89,7 @@ func (o *VirtualSessionProvider) Regenerate(oldsid, sid string) (session.RawStor
}
// Count counts and returns number of sessions.
func (o *VirtualSessionProvider) Count() int {
func (o *VirtualSessionProvider) Count() (int, error) {
o.lock.RLock()
defer o.lock.RUnlock()
return o.provider.Count()
@ -162,9 +164,13 @@ func (s *VirtualStore) Release() error {
// Now ensure that we don't exist!
realProvider := s.p.provider
if !s.released && realProvider.Exist(s.sid) {
// This is an error!
return fmt.Errorf("new sid '%s' already exists", s.sid)
if !s.released {
if exist, err := realProvider.Exist(s.sid); err == nil && exist {
// This is an error!
return fmt.Errorf("new sid '%s' already exists", s.sid)
} else if err != nil {
return fmt.Errorf("check if '%s' exist failed: %w", s.sid, err)
}
}
realStore, err := realProvider.Read(s.sid)
if err != nil {

View File

@ -418,6 +418,10 @@ type ChangesPayload struct {
Body *ChangesFromPayload `json:"body,omitempty"`
// Changes made to the reference
Ref *ChangesFromPayload `json:"ref,omitempty"`
// Changes made to the labels added
AddedLabels []*Label `json:"added_labels"`
// Changes made to the labels removed
RemovedLabels []*Label `json:"removed_labels"`
}
// PullRequestPayload represents a payload information of pull request event.

View File

@ -1,57 +0,0 @@
// Copyright 2016 The Gogs Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package sync
import (
"sync"
"code.gitea.io/gitea/modules/container"
)
// StatusTable is a table maintains true/false values.
//
// This table is particularly useful for un/marking and checking values
// in different goroutines.
type StatusTable struct {
lock sync.RWMutex
pool container.Set[string]
}
// NewStatusTable initializes and returns a new StatusTable object.
func NewStatusTable() *StatusTable {
return &StatusTable{
pool: make(container.Set[string]),
}
}
// StartIfNotRunning sets value of given name to true if not already in pool.
// Returns whether set value was set to true
func (p *StatusTable) StartIfNotRunning(name string) bool {
p.lock.Lock()
added := p.pool.Add(name)
p.lock.Unlock()
return added
}
// Start sets value of given name to true in the pool.
func (p *StatusTable) Start(name string) {
p.lock.Lock()
p.pool.Add(name)
p.lock.Unlock()
}
// Stop sets value of given name to false in the pool.
func (p *StatusTable) Stop(name string) {
p.lock.Lock()
p.pool.Remove(name)
p.lock.Unlock()
}
// IsRunning checks if value of given name is set to true in the pool.
func (p *StatusTable) IsRunning(name string) bool {
p.lock.RLock()
exists := p.pool.Contains(name)
p.lock.RUnlock()
return exists
}

View File

@ -1,31 +0,0 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package sync
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_StatusTable(t *testing.T) {
table := NewStatusTable()
assert.False(t, table.IsRunning("xyz"))
table.Start("xyz")
assert.True(t, table.IsRunning("xyz"))
assert.False(t, table.StartIfNotRunning("xyz"))
assert.True(t, table.IsRunning("xyz"))
table.Stop("xyz")
assert.False(t, table.IsRunning("xyz"))
assert.True(t, table.StartIfNotRunning("xyz"))
assert.True(t, table.IsRunning("xyz"))
table.Stop("xyz")
assert.False(t, table.IsRunning("xyz"))
}

View File

@ -551,6 +551,13 @@ repo.transfer.body=承認または拒否するには %s を開きます。 も
repo.collaborator.added.subject=%s が %s にあなたを追加しました
repo.collaborator.added.text=あなたは次のリポジトリの共同作業者に追加されました:
repo.actions.run.failed=実行に失敗しました
repo.actions.run.succeeded=実行に成功しました
repo.actions.run.cancelled=実行がキャンセルされました
repo.actions.jobs.all_succeeded=すべてのジョブが成功しました
repo.actions.jobs.all_failed=すべてのジョブが失敗しました
repo.actions.jobs.some_not_successful=いくつかのジョブが成功しませんでした
repo.actions.jobs.all_cancelled=すべてのジョブがキャンセルされました
team_invite.subject=%[1]s さんが %[2]s への参加にあなたを招待しました
team_invite.text_1=%[1]s さんが、組織 %[3]s 内のチーム %[2]s への参加に、あなたを招待しました。
@ -3722,10 +3729,14 @@ swift.install=あなたの <code>Package.swift</code> ファイルにパッケ
swift.install2=そして次のコマンドを実行します:
vagrant.install=Vagrant ボックスを追加するには、次のコマンドを実行します。
settings.link=このパッケージをリポジトリにリンク
settings.link.description=パッケージをリポジトリにリンクすると、リポジトリのパッケージ一覧にそのパッケージが表示されます。 リンクできるのは、同じオーナーが所有するリポジトリのみです。 入力欄を空にするとリンクを削除します。
settings.link.select=リポジトリを選択
settings.link.button=リポジトリのリンクを更新
settings.link.success=リポジトリのリンクが正常に更新されました。
settings.link.success=リポジトリのリンクを更新しました。
settings.link.error=リポジトリのリンクの更新に失敗しました。
settings.link.repo_not_found=リポジトリ %s が見つかりません。
settings.unlink.error=リポジトリのリンクの削除に失敗しました。
settings.unlink.success=リポジトリのリンクを削除しました。
settings.delete=パッケージ削除
settings.delete.description=パッケージの削除は恒久的で元に戻すことはできません。
settings.delete.notice=%s (%s) を削除しようとしています。この操作は元に戻せません。よろしいですか?

View File

@ -1052,6 +1052,8 @@ license=Licença
license_helper=Selecione um arquivo de licença.
license_helper_desc=Uma licença define o que os outros podem e não podem fazer com o seu código. Não tem certeza qual é a mais adequada para o seu projeto? Veja <a target="_blank" rel="noopener noreferrer" href="%s">Escolher uma licença.</a>
multiple_licenses=Múltiplas Licenças
object_format=Formato de Objeto
object_format_helper=Formato do objeto do repositório. Não pode ser alterado mais tarde. SHA1 é mais compatível.
readme=README
readme_helper=Selecione um modelo de arquivo README.
readme_helper_desc=Aqui você pode escrever uma descrição completa para o seu projeto.
@ -1101,6 +1103,8 @@ delete_preexisting=Excluir arquivos pré-existentes
delete_preexisting_content=Excluir arquivos em %s
delete_preexisting_success=Arquivos órfãos excluídos em %s
blame_prior=Ver a responsabilização anterior a esta modificação
blame.ignore_revs=Ignorando revisões presentes em <a href="%s">.git-blame-ignore-revs</a>. Clique <a href="%s">aqui para ignorar</a> e ver o blame normalmente.
blame.ignore_revs.failed=Falha ao ignorar revisões em <a href="%s">.git-blame-ignore-revs</a>.
transfer.accept=Aceitar Transferência
@ -1264,6 +1268,7 @@ ambiguous_runes_header=`Este arquivo contém caracteres Unicode ambíguos`
ambiguous_runes_description=`Este arquivo contém caracteres Unicode que podem ser confundidos com outros caracteres. Se você acha que isso é intencional, pode ignorar esse aviso com segurança. Use o botão Escapar para revelá-los`
invisible_runes_line=`Esta linha tem caracteres unicode invisíveis`
ambiguous_runes_line=`Esta linha tem caracteres unicode ambíguos`
ambiguous_character=`%[1]c [U+%04[1]X] pode ser confundido com %[2]c [U+%04[2]X]`
escape_control_characters=Escapar
unescape_control_characters=Desescapar
@ -1273,6 +1278,7 @@ video_not_supported_in_browser=Seu navegador não suporta a tag 'video' do HTML5
audio_not_supported_in_browser=Seu navegador não suporta a tag 'audio' do HTML5.
symbolic_link=Link simbólico
executable_file=Arquivo Executável
vendored=Externo
generated=Gerado
commit_graph=Gráfico de Commits
commit_graph.select=Selecionar branches
@ -1392,6 +1398,7 @@ commitstatus.success=Sucesso
ext_issues=Acesso a Issues Externos
ext_issues.desc=Link para o issue tracker externo.
projects.desc=Gerencie issues e PRs nos quadros do projeto.
projects.description=Descrição (opcional)
projects.description_placeholder=Descrição
projects.create=Criar Projeto
@ -1419,6 +1426,7 @@ projects.column.new=Nova Coluna
projects.column.set_default=Atribuir como Padrão
projects.column.set_default_desc=Definir esta coluna como padrão para pull e issues sem categoria
projects.column.delete=Excluir Coluna
projects.column.deletion_desc=Excluir uma coluna do projeto move todas as issues relacionadas para a coluna padrão. Continuar?
projects.column.color=Cor
projects.open=Abrir
projects.close=Fechar
@ -1452,6 +1460,7 @@ issues.new.clear_milestone=Limpar marco
issues.new.assignees=Responsáveis
issues.new.clear_assignees=Limpar responsáveis
issues.new.no_assignees=Sem Responsáveis
issues.new.no_reviewers=Sem Revisores
issues.choose.get_started=Primeiros Passos
issues.choose.open_external_link=Abrir
issues.choose.blank=Padrão
@ -1516,6 +1525,7 @@ issues.filter_type.reviewed_by_you=Revisado por você
issues.filter_sort=Ordenação
issues.filter_sort.latest=Mais recentes
issues.filter_sort.oldest=Mais antigos
issues.filter_sort.recentupdate=Mais recentemente atualizados
issues.filter_sort.leastupdate=Menos recentemente atualizados
issues.filter_sort.mostcomment=Mais comentados
issues.filter_sort.leastcomment=Menos comentados
@ -1635,6 +1645,7 @@ issues.lock_no_reason=bloqueada e conversação limitada para colaboradores %s
issues.unlock_comment=desbloqueada esta conversação %s
issues.lock_confirm=Bloquear
issues.unlock_confirm=Desbloquear
issues.lock.notice_1=- Outros usuários não podem adicionar novos comentários a esta issue.
issues.lock.notice_2=- Você e outros colaboradores com acesso a este repositório ainda podem deixar comentários que outros podem ver.
issues.lock.notice_3=- Você pode sempre desbloquear esta issue novamente no futuro.
issues.unlock.notice_1=- Todos poderão comentar mais uma vez nesta issue.
@ -1660,6 +1671,7 @@ issues.stop_tracking=Parar Cronômetro
issues.stop_tracking_history=trabalhou por <b>%[1]s</b> %[2]s
issues.cancel_tracking_history=`cronômetro cancelado %s`
issues.del_time=Apagar este registro de tempo
issues.add_time_history=adicionou tempo gasto <b>%[1]s</b> %[2]s
issues.del_time_history=`removeu tempo gasto %s`
issues.add_time_hours=Horas
issues.add_time_minutes=Minutos
@ -1679,6 +1691,7 @@ issues.due_date_form=dd/mm/aaaa
issues.due_date_form_add=Adicionar data limite
issues.due_date_form_edit=Editar
issues.due_date_form_remove=Remover
issues.due_date_not_writer=Você precisa de acesso de escrita neste repositório para atualizar a data limite de uma issue.
issues.due_date_not_set=Data limite não definida.
issues.due_date_added=adicionou a data limite %s %s
issues.due_date_modified=modificou a data limite de %[2]s para %[1]s %[3]s
@ -1701,7 +1714,9 @@ issues.dependency.pr_closing_blockedby=Fechamento deste pull request está bloqu
issues.dependency.issue_closing_blockedby=Fechamento desta issue está bloqueado pelas seguintes issues
issues.dependency.issue_close_blocks=Esta issue bloqueia o fechamento das seguintes issues
issues.dependency.pr_close_blocks=Este pull request bloqueia o fechamento das seguintes issues
issues.dependency.issue_close_blocked=Você precisa fechar todas as issues que bloqueiam esta issue antes de poder fechá-la.
issues.dependency.issue_batch_close_blocked=Não é possível fechar as issues que você escolheu, porque a issue #%d ainda tem dependências abertas
issues.dependency.pr_close_blocked=Você precisa fechar todas issues que bloqueiam este pull request antes de poder fazer o merge.
issues.dependency.blocks_short=Bloqueia
issues.dependency.blocked_by_short=Depende de
issues.dependency.remove_header=Remover Dependência
@ -1712,11 +1727,13 @@ issues.dependency.add_error_same_issue=Você não pode fazer uma issue depender
issues.dependency.add_error_dep_issue_not_exist=Issue dependente não existe.
issues.dependency.add_error_dep_not_exist=Dependência não existe.
issues.dependency.add_error_dep_exists=Dependência já existe.
issues.dependency.add_error_cannot_create_circular=Você não pode criar uma dependência com duas issues que se bloqueiam mutuamente.
issues.dependency.add_error_dep_not_same_repo=Ambas as issues devem estar no mesmo repositório.
issues.review.self.approval=Você não pode aprovar o seu próprio pull request.
issues.review.self.rejection=Você não pode solicitar alterações em seu próprio pull request.
issues.review.approve=aprovou estas alterações %s
issues.review.comment=revisou %s
issues.review.dismissed=dispensou a revisão de %s %s
issues.review.dismissed_label=Rejeitada
issues.review.left_comment=deixou um comentário
issues.review.content.empty=Você precisa deixar um comentário indicando as alterações solicitadas.
@ -1776,9 +1793,11 @@ pulls.show_all_commits=Mostrar todos os commits
pulls.show_changes_since_your_last_review=Mostrar alterações desde sua última revisão
pulls.showing_only_single_commit=Mostrando apenas as alterações do commit %[1]s
pulls.showing_specified_commit_range=Mostrando apenas as alterações entre %[1]s..%[2]s
pulls.select_commit_hold_shift_for_range=Selecionar commit. Mantenha pressionado shift + clique para selecionar um intervalo
pulls.review_only_possible_for_full_diff=A revisão só é possível ao visualizar todas as diferenças
pulls.filter_changes_by_commit=Filtrar por commit
pulls.nothing_to_compare=Estes branches são iguais. Não há nenhuma necessidade para criar um pull request.
pulls.nothing_to_compare_have_tag=Os branches/tags selecionados são iguais.
pulls.nothing_to_compare_and_allow_empty_pr=Estes branches são iguais. Este PR ficará vazio.
pulls.has_pull_request=`Um pull request entre esses branches já existe: <a href="%[1]s">%[2]s#%[3]d</a>`
pulls.create=Criar Pull Request
@ -1803,11 +1822,13 @@ pulls.add_prefix=Adicione o prefixo <strong>%s</strong>
pulls.remove_prefix=Remover o prefixo <strong>%s</strong>
pulls.data_broken=Este pull request está quebrado devido a falta de informação do fork.
pulls.files_conflicted=Este pull request tem alterações conflitantes com o branch de destino.
pulls.is_checking=Verificando conflitos de merge…
pulls.is_ancestor=Este branch já está incluído no branch de destino. Não há nada para mesclar.
pulls.is_empty=As alterações neste branch já estão na branch de destino. Este será um commit vazio.
pulls.required_status_check_failed=Algumas verificações necessárias não foram bem sucedidas.
pulls.required_status_check_missing=Estão faltando algumas verificações necessárias.
pulls.required_status_check_administrator=Como administrador, você ainda pode aplicar o merge deste pull request.
pulls.blocked_by_approvals=Este pull request ainda não tem aprovações suficientes. %d de %d aprovações concedidas.
pulls.blocked_by_rejection=Este pull request tem alterações solicitadas por um revisor oficial.
pulls.blocked_by_official_review_requests=Este pull request tem solicitações de revisão oficiais.
pulls.blocked_by_outdated_branch=Este pull request está bloqueado porque está desatualizado.
@ -1824,12 +1845,16 @@ pulls.reject_count_1=%d pedido de alteração
pulls.reject_count_n=%d pedidos de alteração
pulls.waiting_count_1=aguardando %d revisão
pulls.waiting_count_n=aguardando %d revisões
pulls.wrong_commit_id=id de commit tem que ser um id de commit no branch de destino
pulls.no_merge_desc=O merge deste pull request não pode ser aplicado porque todas as opções de mesclagem do repositório estão desabilitadas.
pulls.no_merge_helper=Habilite as opções de merge nas configurações do repositório ou faça o merge do pull request manualmente.
pulls.no_merge_wip=O merge deste pull request não pode ser aplicado porque está marcado como um trabalho em andamento.
pulls.no_merge_not_ready=Este pull request não está pronto para o merge, verifique o status da revisão e as verificações de status.
pulls.no_merge_access=Você não está autorizado para realizar o merge deste pull request.
pulls.merge_pull_request=Criar commit de merge
pulls.rebase_merge_pull_request=Rebase e fast-forward
pulls.rebase_merge_commit_pull_request=Rebase e criar commit de merge
pulls.squash_merge_pull_request=Criar commit de squash
pulls.fast_forward_only_merge_pull_request=Apenas Fast-forward
pulls.merge_manually=Merge feito manualmente
@ -1837,9 +1862,17 @@ pulls.merge_commit_id=A ID de merge commit
pulls.require_signed_wont_sign=O branch requer commits assinados, mas este merge não será assinado
pulls.invalid_merge_option=Você não pode usar esta opção de merge neste pull request.
pulls.merge_conflict=Merge Falhou: houve um conflito ao fazer merge. Dica: Tente uma estratégia diferente.
pulls.merge_conflict_summary=Mensagem de Erro
pulls.rebase_conflict=Merge Falhou: houve um conflito durante o rebase do commit %[1]s. Dica: Tente uma estratégia diferente.
pulls.rebase_conflict_summary=Mensagem de Erro
pulls.unrelated_histories=Merge Falhou: O merge do principal e da base não compartilham uma história comum. Dica: Tente uma estratégia diferente.
pulls.merge_out_of_date=Merge Falhou: durante a geração do merge, a base não foi atualizada. Dica: Tente novamente.
pulls.head_out_of_date=Merge Falhou: qnquanto gerava o merge, a head foi atualizada. Dica: Tente novamente.
pulls.has_merged=Falha: o merge do pull request foi realizado, você não pode fazer o merge novamente ou alterar o branch de destino.
pulls.push_rejected=Merge Falhou: O push foi rejeitado. Revise os Git Hooks para este repositório.
pulls.push_rejected_summary=Mensagem Completa da Rejeição
pulls.push_rejected_no_message=Merge Falhou: O push foi rejeitado mas não houve mensagem remota. Revise os Git Hooks para este repositório.
pulls.open_unmerged_pull_exists=`Não é possível executar uma operação de reabertura pois há um pull request pendente (#%d) com propriedades idênticas.`
pulls.status_checking=Algumas verificações estão pendentes
pulls.status_checks_success=Todas as verificações foram bem sucedidas
@ -1858,10 +1891,13 @@ pulls.outdated_with_base_branch=Este branch está desatualizado com o branch bas
pulls.close=Fechar Pull Request
pulls.closed_at=`fechou este pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>`
pulls.reopened_at=`reabriu este pull request <a id="%[1]s" href="#%[1]s">%[2]s</a>`
pulls.cmd_instruction_hint=Ver instruções de linha de comando
pulls.cmd_instruction_checkout_title=Checkout
pulls.cmd_instruction_checkout_desc=No repositório do seu projeto, faça checkout de um novo branch e teste as alterações.
pulls.cmd_instruction_merge_title=Merge
pulls.cmd_instruction_merge_desc=Faça merge das alterações e atualize no Gitea.
pulls.clear_merge_message=Limpar mensagem do merge
pulls.clear_merge_message_hint=Limpar a mensagem de merge só irá remover o conteúdo da mensagem de commit e manter trailers git gerados, como "Co-Authored-By …".
pulls.auto_merge_button_when_succeed=(Quando a verificação for bem-sucedida)
pulls.auto_merge_when_succeed=Mesclar automaticamente quando todas as verificações forem bem sucedidas
@ -1917,12 +1953,15 @@ milestones.filter_sort.most_issues=Com mais issues
milestones.filter_sort.least_issues=Com menos issues
signing.will_sign=Esse commit será assinado com a chave "%s".
signing.wont_sign.error=Ocorreu um erro ao verificar se o commit poderia ser assinado.
signing.wont_sign.nokey=Não há nenhuma chave disponível para assinar esse commit.
signing.wont_sign.never=Commits nunca são assinados.
signing.wont_sign.always=Commits são sempre assinados.
signing.wont_sign.pubkey=O commit não será assinado porque você não tem uma chave pública associada à sua conta.
signing.wont_sign.twofa=Você deve ter a autenticação de dois fatores ativada para que os commits sejam assinados.
signing.wont_sign.parentsigned=O commit não será assinado, pois o commit pai não está assinado.
signing.wont_sign.basesigned=O merge não será assinado, pois o commit base não está assinado.
signing.wont_sign.headsigned=O merge não será assinado, pois o commit mais recente não está assinado.
signing.wont_sign.commitssigned=O merge não será assinado, pois todos os commits associados não estão assinados.
signing.wont_sign.approved=O merge não será assinado porque o PR não foi aprovado.
signing.wont_sign.not_signed_in=Você não está conectado.
@ -1959,6 +1998,7 @@ wiki.original_git_entry_tooltip=Ver o arquivo Git original em vez de usar o link
activity=Atividade
activity.navbar.pulse=Pulso
activity.navbar.code_frequency=Frequência de Código
activity.navbar.contributors=Contribuidores
activity.navbar.recent_commits=Commits Recentes
activity.period.filter_label=Período:
@ -2004,6 +2044,7 @@ activity.title.releases_1=%d Versão
activity.title.releases_n=%d Versões
activity.title.releases_published_by=%s publicada(s) por %s
activity.published_release_label=Publicado
activity.no_git_activity=Não houve nenhuma atividade de commit neste período.
activity.git_stats_exclude_merges=Excluindo merges,
activity.git_stats_author_1=%d autor
activity.git_stats_author_n=%d autores
@ -2031,6 +2072,7 @@ contributors.contribution_type.additions=Adições
contributors.contribution_type.deletions=Exclusões
settings=Configurações
settings.desc=Configurações é onde você pode gerenciar as opções para o repositório.
settings.options=Repositório
settings.public_access=Acesso Público
settings.collaboration=Colaboradores
@ -2044,10 +2086,19 @@ settings.githooks=Hooks do Git
settings.basic_settings=Configurações Básicas
settings.mirror_settings=Opções de Espelhamento
settings.mirror_settings.docs=Configure o seu repositório para sincronizar automaticamente commits, tags e branches de outro repositório.
settings.mirror_settings.docs.disabled_pull_mirror.instructions=Configure seu projeto para enviar automaticamente commits, tags e branches para outro repositório. Os espelhamentos pull foram desativados pelo administrador do site.
settings.mirror_settings.docs.disabled_push_mirror.instructions=Configure seu projeto para puxar automaticamente commits, tags e branches de outro repositório.
settings.mirror_settings.docs.disabled_push_mirror.pull_mirror_warning=No momento, isso só pode ser feito no menu "Nova migração". Para obter mais informações, consulte:
settings.mirror_settings.docs.disabled_push_mirror.info=Os espelhamentos push foram desativados pelo administrador do site.
settings.mirror_settings.docs.no_new_mirrors=Seu repositório está espelhando alterações de ou para outro repositório. Lembre-se de que, no momento, não é possível criar novos espelhamentos.
settings.mirror_settings.docs.can_still_use=Embora não seja possível modificar os espelhos existentes ou criar novos espelhos, você ainda pode usar o espelho existente.
settings.mirror_settings.docs.pull_mirror_instructions=Para configurar um espelhamento de pull, consulte:
settings.mirror_settings.docs.more_information_if_disabled=Você pode saber mais sobre espelhos de push e pull aqui:
settings.mirror_settings.docs.doc_link_title=Como posso espelhar repositórios?
settings.mirror_settings.docs.doc_link_pull_section=a seção "Extração de um repositório remoto" da documentação.
settings.mirror_settings.docs.pulling_remote_title=Extração de um repositório remoto
settings.mirror_settings.mirrored_repository=Repositório espelhado
settings.mirror_settings.pushed_repository=Repositório enviado
settings.mirror_settings.direction=Sentido
settings.mirror_settings.direction.pull=Pull
settings.mirror_settings.direction.push=Push
@ -2058,6 +2109,8 @@ settings.mirror_settings.push_mirror.add=Adicionar Espelho de Push
settings.mirror_settings.push_mirror.edit_sync_time=Editar intervalo de sincronização de espelhos
settings.sync_mirror=Sincronizar Agora
settings.pull_mirror_sync_in_progress=Puxando modificações a partir do repositório remoto %s, neste momento.
settings.push_mirror_sync_in_progress=Enviando modificações para o repositório remoto %s, neste momento.
settings.site=Site
settings.update_settings=Atualizar Configurações
settings.update_mirror_settings=Atualizar Configurações de Espelhamento
@ -2073,24 +2126,24 @@ settings.external_wiki_url=URL Externa da Wiki
settings.external_wiki_url_error=A URL da wiki externa não é válida.
settings.external_wiki_url_desc=Visitantes são redirecionados para a URL da wiki externa ao clicar na aba da wiki.
settings.issues_desc=Habilitar Issue Tracker para o Repositório
settings.use_internal_issue_tracker=Usar o issue tracker nativo
settings.use_external_issue_tracker=Usar issue tracker externo
settings.use_internal_issue_tracker=Usar o Issue Tracker Nativo
settings.use_external_issue_tracker=Usar Issue Tracker Externo
settings.external_tracker_url=URL do Issue Tracker Externo
settings.external_tracker_url_error=A URL do issue tracker externo não é válida.
settings.external_tracker_url_desc=Visitantes são redirecionados para a URL do issue tracker externo ao clicar na aba de issues.
settings.tracker_url_format=Formato de URL do issue tracker externo
settings.tracker_url_format_error=O formato da URL do issue tracker externo não é válido.
settings.tracker_issue_style=Formato de número do issue tracker externo
settings.tracker_issue_style=Formato de Número do Issue Tracker Externo
settings.tracker_issue_style.numeric=Numérico
settings.tracker_issue_style.alphanumeric=Alfanumérico
settings.tracker_issue_style.regexp=Expressão Regular
settings.tracker_issue_style.regexp_pattern=Padrão de expressão regular
settings.tracker_issue_style.regexp_pattern=Padrão de Expressão Regular
settings.tracker_issue_style.regexp_pattern_desc=O primeiro grupo capturado será usado no lugar de <code>{index}</code>.
settings.tracker_url_format_desc=Use os espaços reservados <code>{user}</code>, <code>{repo}</code> e <code>{index}</code> para o nome de usuário, nome do repositório e o índice de problemas.
settings.enable_timetracker=Habilitar Cronômetro
settings.allow_only_contributors_to_track_time=Permitir que apenas os colaboradores acompanhem o contador de tempo
settings.pulls_desc=Habilitar pull requests no repositório
settings.pulls.ignore_whitespace=Ignorar espaço em branco em conflitos
settings.pulls_desc=Habilitar Pull Requests no Repositório
settings.pulls.ignore_whitespace=Ignorar Espaço em Branco em Conflitos
settings.pulls.enable_autodetect_manual_merge=Habilitar a detecção automática de merge manual (Nota: Em alguns casos especiais, podem ocorrer julgamentos errados)
settings.pulls.allow_rebase_update=Ativar atualização do branch do pull request por rebase
settings.pulls.default_delete_branch_after_merge=Excluir o branch de pull request após o merge por padrão
@ -2098,6 +2151,8 @@ settings.pulls.default_allow_edits_from_maintainers=Permitir edições de manten
settings.releases_desc=Habilitar versões do Repositório
settings.packages_desc=Habilitar Registro de Pacotes de Repositório
settings.projects_desc=Habilitar Projetos
settings.projects_mode_repo=Somente projetos no repositório
settings.projects_mode_owner=Apenas projetos do usuário ou org
settings.projects_mode_all=Todos os projetos
settings.actions_desc=Habilitar Ações do Repositório
settings.admin_settings=Configurações do Administrador
@ -2111,7 +2166,7 @@ settings.reindex_requested=Reindexação Requisitada
settings.admin_enable_close_issues_via_commit_in_any_branch=Fechar issue via commit em um branch não padrão
settings.danger_zone=Zona de Perigo
settings.new_owner_has_same_repo=O novo proprietário já tem um repositório com o mesmo nome. Por favor, escolha outro nome.
settings.convert=Converter para repositório tradicional
settings.convert=Converter para Repositório Tradicional
settings.convert_desc=Você pode converter este espelhamento em um repositório tradicional. Esta ação não pode ser revertida.
settings.convert_notices_1=Esta operação vai converter este espelhamento em um repositório tradicional. Esta ação não pode ser desfeita.
settings.convert_confirm=Converter o Repositório
@ -2129,6 +2184,7 @@ settings.transfer_abort_invalid=Não é possível cancelar uma transferência de
settings.transfer_abort_success=A transferência do repositório para %s foi cancelada com sucesso.
settings.transfer_desc=Transferir este repositório para outro usuário ou para uma organização onde você tem direitos de administrador.
settings.transfer_form_title=Digite o nome do repositório para confirmar:
settings.transfer_in_progress=Há uma transferência em andamento. Por favor, cancele se você gostaria de transferir este repositório para outro usuário.
settings.transfer_notices_1=- Você perderá o acesso ao repositório se transferir para um usuário individual.
settings.transfer_notices_2=- Você manterá acesso ao repositório se transferi-lo para uma organização que você também é proprietário.
settings.transfer_notices_3=- Se o repositório for privado e for transferido para um usuário individual, esta ação certifica que o usuário tem pelo menos permissão de leitura (e altera as permissões se necessário).
@ -2142,9 +2198,13 @@ settings.trust_model.default=Modelo Padrão de Confiança
settings.trust_model.default.desc=Use o modelo de confiança de repositório padrão para esta instalação.
settings.trust_model.collaborator=Colaborador
settings.trust_model.collaborator.long=Colaborador: Confiar em assinaturas feitas por colaboradores
settings.trust_model.collaborator.desc=Assinaturas válidas dos colaboradores deste repositório serão marcadas como "confiáveis" - (quer correspondam ao autor do commit ou não). Caso contrário, assinaturas válidas serão marcadas como "não confiáveis" se a assinatura corresponder ao autor do commit e "não corresponde" se não corresponder.
settings.trust_model.committer=Committer
settings.trust_model.committer.long=Committer: Confiar nas assinaturas que correspondam aos autores de commits (isso corresponde ao GitHub e forçará commits assinados pelo Gitea a ter o Gitea como o autor do commit).
settings.trust_model.committer.desc=As assinaturas válidas só serão marcadas como "confiáveis" se corresponderem ao autor do commit; caso contrário, serão marcadas como "não correspondentes". Isso força o Gitea a ser o autor do commit em commits assinados com o autor do commit real marcado como Co-authored-by: e Co-committed-by: no rodapé do commit. A chave padrão do Gitea deve corresponder a um usuário no banco de dados.
settings.trust_model.collaboratorcommitter=Colaborador+Commiter
settings.trust_model.collaboratorcommitter.long=Colaborador+Committer: Confiar na assinatura dos colaboradores que correspondem ao autor do commit
settings.trust_model.collaboratorcommitter.desc=Assinaturas válidas dos colaboradores deste repositório serão marcadas como "confiáveis" se corresponderem ao autor do commit. Caso contrário, as assinaturas válidas serão marcadas como "não confiáveis" se a assinatura corresponder ao autor do commit e "não corresponde" caso contrário. Isso forçará o Gitea a ser marcado como o autor do commit nos commits assinados com o autor marcado como Co-Authored-By: e o Committed-By: resumo do commit. A chave padrão do Gitea tem que corresponder a um usuário no banco de dados.
settings.wiki_delete=Excluir Dados da Wiki
settings.wiki_delete_desc=A exclusão de dados da wiki é permanente e não pode ser desfeita.
settings.wiki_delete_notices_1=- Isso excluirá e desabilitará permanentemente a wiki do repositório %s.
@ -2153,9 +2213,11 @@ settings.wiki_deletion_success=Os dados da wiki do repositório foi excluídos.
settings.delete=Excluir Este Repositório
settings.delete_desc=A exclusão de um repositório é permanente e não pode ser desfeita.
settings.delete_notices_1=- Esta operação <strong>NÃO PODERÁ</strong> ser desfeita.
settings.delete_notices_2=- Essa operação excluirá permanentemente o repositório <strong>%s</strong>, incluindo código, issues, comentários, dados da wiki e configurações do colaborador.
settings.delete_notices_fork_1=- Forks deste repositório se tornarão independentes após a exclusão.
settings.deletion_success=O repositório foi excluído.
settings.update_settings_success=As configurações do repositório foram atualizadas.
settings.update_settings_no_unit=O repositório deve permitir pelo menos algum tipo de interação.
settings.confirm_delete=Excluir Repositório
settings.add_collaborator=Adicionar Colaborador
settings.add_collaborator_success=O colaborador foi adicionado.
@ -2172,6 +2234,8 @@ settings.team_not_in_organization=A equipe não está na mesma organização que
settings.teams=Equipes
settings.add_team=Adicionar Equipe
settings.add_team_duplicate=A equipe já tem o repositório
settings.add_team_success=A equipe agora tem acesso ao repositório.
settings.change_team_permission_tip=A permissão da equipe está definida na página de configurações da equipe e não pode ser alterada por repositório
settings.delete_team_tip=Esta equipe tem acesso a todos os repositórios e não pode ser removida
settings.remove_team_success=O acesso da equipe ao repositório foi removido.
settings.add_webhook=Adicionar Webhook
@ -2181,12 +2245,15 @@ settings.webhook_deletion=Remover Webhook
settings.webhook_deletion_desc=A exclusão de um webhook exclui suas configurações e o histórico de entrega. Continuar?
settings.webhook_deletion_success=O webhook foi removido.
settings.webhook.test_delivery=Testar Evento de Push
settings.webhook.test_delivery_desc=Teste este webhook com um evento falso.
settings.webhook.test_delivery_desc_disabled=Para testar este webhook com um evento falso, ative-o.
settings.webhook.request=Solicitação
settings.webhook.response=Resposta
settings.webhook.headers=Cabeçalhos
settings.webhook.payload=Conteúdo
settings.webhook.body=Corpo
settings.webhook.replay.description=Executar novamente esse webhook.
settings.webhook.replay.description_disabled=Para repetir este webhook, ative-o.
settings.webhook.delivery.success=Um evento foi adicionado à fila de envio. Pode levar alguns segundos até que ele apareça no histórico de envio.
settings.githooks_desc=Hooks do Git são executados pelo próprio Git. Você pode editar arquivos de hook abaixo para configurar operações personalizadas.
settings.githook_edit_desc=Se o hook não estiver ativo, o conteúdo de exemplo será apresentado. Deixar o conteúdo em branco irá desabilitar esse hook.
@ -2249,6 +2316,8 @@ settings.event_pull_request_review=Pull Request Revisado
settings.event_pull_request_review_desc=Pull request aprovado, rejeitado ou revisão comentada.
settings.event_pull_request_sync=Pull Request Sincronizado
settings.event_pull_request_sync_desc=Pull request sincronizado.
settings.event_pull_request_review_request=Revisão de Pull Request Solicitada
settings.event_pull_request_review_request_desc=Revisão de pull request solicitada ou solicitação de revisão removida.
settings.event_package=Pacote
settings.event_package_desc=Pacote criado ou excluído em um repositório.
settings.branch_filter=Filtro de branch
@ -2317,7 +2386,7 @@ settings.protect_check_status_contexts_list=Verificações de status encontradas
settings.protect_required_approvals=Aprovações necessárias:
settings.dismiss_stale_approvals=Descartar aprovações obsoletas
settings.dismiss_stale_approvals_desc=Quando novos commits que mudam o conteúdo do pull request são enviados para o branch, as antigas aprovações serão descartadas.
settings.require_signed_commits=Exigir commits assinados
settings.require_signed_commits=Exigir Commits Assinados
settings.require_signed_commits_desc=Rejeitar pushes para este branch se não estiverem assinados ou não forem validáveis.
settings.protect_branch_name_pattern=Padrão de Nome de Branch Protegida
settings.protect_patterns=Padrões
@ -2328,6 +2397,7 @@ settings.delete_protected_branch=Desabilitar proteção
settings.update_protect_branch_success=Proteção do branch "%s" foi atualizada.
settings.remove_protected_branch_success=Proteção do branch "%s" foi desabilitada.
settings.remove_protected_branch_failed=Removendo regra de proteção de branch "%s" falhou.
settings.protected_branch_deletion=Excluir Proteção de Branch
settings.protected_branch_deletion_desc=Desabilitar a proteção de branch permite que os usuários com permissão de escrita realizem push. Continuar?
settings.block_rejected_reviews=Bloquear merge em revisões rejeitadas
settings.block_rejected_reviews_desc=O merge não será possível quando são solicitadas alterações pelos revisores oficiais, mesmo que haja aprovação suficiente.
@ -2359,6 +2429,7 @@ settings.matrix.room_id=ID da Sala
settings.matrix.message_type=Tipo de Mensagem
settings.visibility.private.button=Tornar Privado
settings.visibility.public.button=Tornar Público
settings.visibility.public.bullet_one=Deixe o repositório visível para qualquer pessoa.
settings.archive.button=Arquivar Repositório
settings.archive.header=Arquivar Este Repositório
settings.archive.success=O repositório foi arquivado com sucesso.
@ -2787,6 +2858,7 @@ users.activated=Ativado
users.admin=Administrador
users.restricted=Restrito
users.reserved=Reservado
users.bot=Bot
users.remote=Remoto
users.2fa=2FA
users.repos=Repositórios
@ -3536,6 +3608,7 @@ variables.update.success=A variável foi editada.
type-1.display_name=Projeto Individual
type-2.display_name=Projeto do Repositório
type-3.display_name=Projeto da Organização
exit_fullscreen=Sair da Tela Cheia
[git.filemode]
changed_filemode=%[1]s → %[2]s

View File

@ -551,6 +551,13 @@ repo.transfer.body=访问 %s 以接受或拒绝转移,亦可忽略此邮件。
repo.collaborator.added.subject=%s 把您添加到 %s
repo.collaborator.added.text=您已被添加为仓库的协作者:
repo.actions.run.failed=运行失败
repo.actions.run.succeeded=运行成功
repo.actions.run.cancelled=运行已取消
repo.actions.jobs.all_succeeded=所有任务已成功
repo.actions.jobs.all_failed=所有任务已失败
repo.actions.jobs.some_not_successful=一些任务未成功
repo.actions.jobs.all_cancelled=所有任务已取消
team_invite.subject=%[1]s 邀请您加入组织 %[2]s
team_invite.text_1=%[1]s 邀请您加入组织 %[3]s 中的团队 %[2]s。
@ -3723,10 +3730,14 @@ swift.install=在您的 <code>Package.swift</code> 文件中添加该包:
swift.install2=并运行以下命令:
vagrant.install=若要添加一个 Vagrant box请运行以下命令
settings.link=将此软件包链接到仓库
settings.link.description=如果您将一个软件包与一个仓库链接起来,软件包将显示在仓库的软件包列表中。
settings.link.select=选择仓库
settings.link.button=更新仓库链接
settings.link.success=仓库链接已成功更新。
settings.link.error=更新仓库链接失败。
settings.link.repo_not_found=仓库 %s 未找到。
settings.unlink.error=删除仓库链接失败。
settings.unlink.success=仓库链接已成功删除。
settings.delete=删除软件包
settings.delete.description=删除软件包是永久性的,无法撤消。
settings.delete.notice=您将要删除 %s (%s)。此操作是不可逆的,您确定吗?

View File

@ -351,7 +351,7 @@ func ListIssues(ctx *context.APIContext) {
// enum: [closed, open, all]
// - name: labels
// in: query
// description: comma separated list of labels. Fetch only issues that have any of this labels. Non existent labels are discarded
// description: comma separated list of label names. Fetch only issues that have any of this label names. Non existent labels are discarded.
// type: string
// - name: q
// in: query

View File

@ -1620,7 +1620,7 @@ func GetPullRequestFiles(ctx *context.APIContext) {
return
}
diffShortStat, err := gitdiff.GetDiffShortStat(baseGitRepo, startCommitID, endCommitID)
diffShortStat, err := gitdiff.GetDiffShortStat(ctx, ctx.Repo.Repository, baseGitRepo, startCommitID, endCommitID)
if err != nil {
ctx.APIErrorInternal(err)
return

View File

@ -44,9 +44,11 @@ func isRoutePathExpensive(routePattern string) bool {
"/{username}/{reponame}/blame/",
"/{username}/{reponame}/commit/",
"/{username}/{reponame}/commits/",
"/{username}/{reponame}/compare/",
"/{username}/{reponame}/graph",
"/{username}/{reponame}/media/",
"/{username}/{reponame}/raw/",
"/{username}/{reponame}/rss/branch/",
"/{username}/{reponame}/src/",
// issue & PR related (no trailing slash)

View File

@ -107,8 +107,8 @@ func ForwardedHeadersHandler(limit int, trustedProxies []string) func(h http.Han
return proxy.ForwardedHeaders(opt)
}
func Sessioner() func(next http.Handler) http.Handler {
return session.Sessioner(session.Options{
func Sessioner() (func(next http.Handler) http.Handler, error) {
middleware, err := session.Sessioner(session.Options{
Provider: setting.SessionConfig.Provider,
ProviderConfig: setting.SessionConfig.ProviderConfig,
CookieName: setting.SessionConfig.CookieName,
@ -119,4 +119,9 @@ func Sessioner() func(next http.Handler) http.Handler {
SameSite: setting.SessionConfig.SameSite,
Domain: setting.SessionConfig.Domain,
})
if err != nil {
return nil, fmt.Errorf("failed to create session middleware: %w", err)
}
return middleware, nil
}

View File

@ -177,7 +177,7 @@ func InitWebInstalled(ctx context.Context) {
mustInit(repo_service.InitLicenseClassifier)
// Finally start up the cron
cron.NewContext(ctx)
cron.Init(ctx)
}
// NormalRoutes represents non install routes

View File

@ -8,6 +8,7 @@ import (
"html"
"net/http"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/public"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
@ -23,7 +24,11 @@ func Routes() *web.Router {
base.Methods("GET, HEAD", "/assets/*", public.FileHandlerFunc())
r := web.NewRouter()
r.Use(common.Sessioner(), Contexter())
if sessionMid, err := common.Sessioner(); err == nil && sessionMid != nil {
r.Use(sessionMid, Contexter())
} else {
log.Fatal("common.Sessioner failed: %v", err)
}
r.Get("/", Install) // it must be on the root, because the "install.js" use the window.location to replace the "localhost" AppURL
r.Post("/", web.Bind(forms.InstallForm{}), SubmitInstall)
r.Get("/post-install", InstallDone)

View File

@ -8,7 +8,7 @@ import (
"time"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/git"
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/services/context"
@ -56,7 +56,7 @@ func Activity(ctx *context.Context) {
canReadCode := ctx.Repo.CanRead(unit.TypeCode)
if canReadCode {
// GetActivityStats needs to read the default branch to get some information
branchExist, _ := git.IsBranchExist(ctx, ctx.Repo.Repository.ID, ctx.Repo.Repository.DefaultBranch)
branchExist, _ := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, ctx.Repo.Repository.DefaultBranch)
if !branchExist {
ctx.Data["NotFoundPrompt"] = ctx.Tr("repo.branch.default_branch_not_exist", ctx.Repo.Repository.DefaultBranch)
ctx.NotFound(nil)

View File

@ -324,7 +324,7 @@ func Diff(ctx *context.Context) {
ctx.NotFound(err)
return
}
diffShortStat, err := gitdiff.GetDiffShortStat(gitRepo, "", commitID)
diffShortStat, err := gitdiff.GetDiffShortStat(ctx, ctx.Repo.Repository, gitRepo, "", commitID)
if err != nil {
ctx.ServerError("GetDiffShortStat", err)
return

View File

@ -631,7 +631,7 @@ func PrepareCompareDiff(
ctx.ServerError("GetDiff", err)
return false
}
diffShortStat, err := gitdiff.GetDiffShortStat(ci.HeadGitRepo, beforeCommitID, headCommitID)
diffShortStat, err := gitdiff.GetDiffShortStat(ctx, ci.HeadRepo, ci.HeadGitRepo, beforeCommitID, headCommitID)
if err != nil {
ctx.ServerError("GetDiffShortStat", err)
return false

View File

@ -9,12 +9,14 @@ import (
"html/template"
"net/http"
"strconv"
"strings"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/renderhelper"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup/markdown"
repo_module "code.gitea.io/gitea/modules/repository"
@ -124,8 +126,8 @@ func NewComment(ctx *context.Context) {
ctx.JSONError("The origin branch is delete, cannot reopen.")
return
}
headBranchRef := pull.GetGitHeadBranchRefName()
headBranchCommitID, err := git.GetFullCommitID(ctx, pull.HeadRepo.RepoPath(), headBranchRef)
headBranchRef := git.RefNameFromBranch(pull.HeadBranch)
headBranchCommitID, err := git.GetFullCommitID(ctx, pull.HeadRepo.RepoPath(), headBranchRef.String())
if err != nil {
ctx.ServerError("Get head commit Id of head branch fail", err)
return
@ -287,9 +289,10 @@ func UpdateCommentContent(ctx *context.Context) {
ctx.ServerError("RenderString", err)
return
}
} else {
contentEmpty := fmt.Sprintf(`<span class="no-content">%s</span>`, ctx.Tr("repo.issues.no_content"))
renderedContent = template.HTML(contentEmpty)
}
if strings.TrimSpace(string(renderedContent)) == "" {
renderedContent = htmlutil.HTMLFormat(`<span class="no-content">%s</span>`, ctx.Tr("repo.issues.no_content"))
}
ctx.JSON(http.StatusOK, map[string]any{

View File

@ -201,7 +201,7 @@ func GetPullDiffStats(ctx *context.Context) {
log.Error("Failed to GetRefCommitID: %v, repo: %v", err, ctx.Repo.Repository.FullName())
return
}
diffShortStat, err := gitdiff.GetDiffShortStat(ctx.Repo.GitRepo, mergeBaseCommitID, headCommitID)
diffShortStat, err := gitdiff.GetDiffShortStat(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, mergeBaseCommitID, headCommitID)
if err != nil {
log.Error("Failed to GetDiffShortStat: %v, repo: %v", err, ctx.Repo.Repository.FullName())
return
@ -761,7 +761,7 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) {
}
}
diffShortStat, err := gitdiff.GetDiffShortStat(ctx.Repo.GitRepo, beforeCommitID, afterCommitID)
diffShortStat, err := gitdiff.GetDiffShortStat(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, beforeCommitID, afterCommitID)
if err != nil {
ctx.ServerError("GetDiffShortStat", err)
return

View File

@ -267,7 +267,11 @@ func Routes() *web.Router {
routes.Get("/ssh_info", misc.SSHInfo)
routes.Get("/api/healthz", healthcheck.Check)
mid = append(mid, common.Sessioner(), context.Contexter())
if sessionMid, err := common.Sessioner(); err == nil && sessionMid != nil {
mid = append(mid, sessionMid, context.Contexter())
} else {
log.Fatal("common.Sessioner failed: %v", err)
}
// Get user from session if logged in.
mid = append(mid, webAuth(buildAuthGroup()))

View File

@ -169,7 +169,7 @@ func (n *actionsNotifier) IssueChangeAssignee(ctx context.Context, doer *user_mo
hookEvent = webhook_module.HookEventPullRequestAssign
}
notifyIssueChange(ctx, doer, issue, hookEvent, action)
notifyIssueChange(ctx, doer, issue, hookEvent, action, nil, nil)
}
// IssueChangeMilestone notifies assignee to notifiers
@ -188,11 +188,11 @@ func (n *actionsNotifier) IssueChangeMilestone(ctx context.Context, doer *user_m
hookEvent = webhook_module.HookEventPullRequestMilestone
}
notifyIssueChange(ctx, doer, issue, hookEvent, action)
notifyIssueChange(ctx, doer, issue, hookEvent, action, nil, nil)
}
func (n *actionsNotifier) IssueChangeLabels(ctx context.Context, doer *user_model.User, issue *issues_model.Issue,
_, _ []*issues_model.Label,
addedLabels, removedLabels []*issues_model.Label,
) {
ctx = withMethod(ctx, "IssueChangeLabels")
@ -201,10 +201,10 @@ func (n *actionsNotifier) IssueChangeLabels(ctx context.Context, doer *user_mode
hookEvent = webhook_module.HookEventPullRequestLabel
}
notifyIssueChange(ctx, doer, issue, hookEvent, api.HookIssueLabelUpdated)
notifyIssueChange(ctx, doer, issue, hookEvent, api.HookIssueLabelUpdated, addedLabels, removedLabels)
}
func notifyIssueChange(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, event webhook_module.HookEventType, action api.HookIssueAction) {
func notifyIssueChange(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, event webhook_module.HookEventType, action api.HookIssueAction, addedLabels, removedLabels []*issues_model.Label) {
var err error
if err = issue.LoadRepo(ctx); err != nil {
log.Error("LoadRepo: %v", err)
@ -216,34 +216,65 @@ func notifyIssueChange(ctx context.Context, doer *user_model.User, issue *issues
return
}
var addedAPILabels []*api.Label
if addedLabels != nil {
addedAPILabels = make([]*api.Label, 0, len(addedLabels))
for _, label := range addedLabels {
addedAPILabels = append(addedAPILabels, convert.ToLabel(label, issue.Repo, doer))
}
}
// Get removed labels from context if present
var removedAPILabels []*api.Label
if removedLabels != nil {
removedAPILabels = make([]*api.Label, 0, len(removedLabels))
for _, label := range removedLabels {
removedAPILabels = append(removedAPILabels, convert.ToLabel(label, issue.Repo, doer))
}
}
if issue.IsPull {
if err = issue.LoadPullRequest(ctx); err != nil {
log.Error("loadPullRequest: %v", err)
return
}
payload := &api.PullRequestPayload{
Action: action,
Index: issue.Index,
PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil),
Repository: convert.ToRepo(ctx, issue.Repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}),
Sender: convert.ToUser(ctx, doer, nil),
Changes: &api.ChangesPayload{
AddedLabels: addedAPILabels,
RemovedLabels: removedAPILabels,
},
}
newNotifyInputFromIssue(issue, event).
WithDoer(doer).
WithPayload(&api.PullRequestPayload{
Action: action,
Index: issue.Index,
PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil),
Repository: convert.ToRepo(ctx, issue.Repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}),
Sender: convert.ToUser(ctx, doer, nil),
}).
WithPayload(payload).
WithPullRequest(issue.PullRequest).
Notify(ctx)
return
}
permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster)
payload := &api.IssuePayload{
Action: action,
Index: issue.Index,
Issue: convert.ToAPIIssue(ctx, doer, issue),
Repository: convert.ToRepo(ctx, issue.Repo, permission),
Sender: convert.ToUser(ctx, doer, nil),
Changes: &api.ChangesPayload{
AddedLabels: addedAPILabels,
RemovedLabels: removedAPILabels,
},
}
newNotifyInputFromIssue(issue, event).
WithDoer(doer).
WithPayload(&api.IssuePayload{
Action: action,
Index: issue.Index,
Issue: convert.ToAPIIssue(ctx, doer, issue),
Repository: convert.ToRepo(ctx, issue.Repo, permission),
Sender: convert.ToUser(ctx, doer, nil),
}).
WithPayload(payload).
Notify(ctx)
}

View File

@ -210,7 +210,7 @@ func ToCommit(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Rep
// Get diff stats for commit
if opts.Stat {
diffShortStat, err := gitdiff.GetDiffShortStat(gitRepo, "", commit.ID.String())
diffShortStat, err := gitdiff.GetDiffShortStat(ctx, repo, gitRepo, "", commit.ID.String())
if err != nil {
return nil, err
}

View File

@ -235,7 +235,7 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u
// Calculate diff
startCommitID = pr.MergeBase
diffShortStats, err := gitdiff.GetDiffShortStat(gitRepo, startCommitID, endCommitID)
diffShortStats, err := gitdiff.GetDiffShortStat(ctx, pr.BaseRepo, gitRepo, startCommitID, endCommitID)
if err != nil {
log.Error("GetDiffShortStat: %v", err)
} else {

View File

@ -11,7 +11,6 @@ import (
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/sync"
"code.gitea.io/gitea/modules/translation"
"github.com/go-co-op/gocron"
@ -19,13 +18,10 @@ import (
var scheduler = gocron.NewScheduler(time.Local)
// Prevent duplicate running tasks.
var taskStatusTable = sync.NewStatusTable()
// NewContext begins cron tasks
// Init begins cron tasks
// Each cron task is run within the shutdown context as a running server
// AtShutdown the cron server is stopped
func NewContext(original context.Context) {
func Init(original context.Context) {
defer pprof.SetGoroutineLabels(original)
_, _, finished := process.GetManager().AddTypedContext(graceful.GetManager().ShutdownContext(), "Service: Cron", process.SystemProcessType, true)
initBasicTasks()

View File

@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/models/db"
system_model "code.gitea.io/gitea/models/system"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/globallock"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process"
@ -71,20 +72,30 @@ func (t *Task) Run() {
}, t.config)
}
func getCronTaskLockKey(name string) string {
return "cron_task:" + name
}
// RunWithUser will run the task incrementing the cron counter at the time with User
func (t *Task) RunWithUser(doer *user_model.User, config Config) {
if !taskStatusTable.StartIfNotRunning(t.Name) {
locked, releaser, err := globallock.TryLock(graceful.GetManager().ShutdownContext(), getCronTaskLockKey(t.Name))
if err != nil {
log.Error("Failed to acquire lock for cron task %q: %v", t.Name, err)
return
}
if !locked {
log.Trace("a cron task %q is already running", t.Name)
return
}
defer releaser()
t.lock.Lock()
if config == nil {
config = t.config
}
t.ExecTimes++
t.lock.Unlock()
defer func() {
taskStatusTable.Stop(t.Name)
}()
graceful.GetManager().RunWithShutdownContext(func(baseCtx context.Context) {
defer func() {
if err := recover(); err != nil {

View File

@ -21,12 +21,14 @@ import (
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
pull_model "code.gitea.io/gitea/models/pull"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/analyze"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/attribute"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/highlight"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
@ -1271,9 +1273,7 @@ type DiffShortStat struct {
NumFiles, TotalAddition, TotalDeletion int
}
func GetDiffShortStat(gitRepo *git.Repository, beforeCommitID, afterCommitID string) (*DiffShortStat, error) {
repoPath := gitRepo.Path
func GetDiffShortStat(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, beforeCommitID, afterCommitID string) (*DiffShortStat, error) {
afterCommit, err := gitRepo.GetCommit(afterCommitID)
if err != nil {
return nil, err
@ -1285,7 +1285,7 @@ func GetDiffShortStat(gitRepo *git.Repository, beforeCommitID, afterCommitID str
}
diff := &DiffShortStat{}
diff.NumFiles, diff.TotalAddition, diff.TotalDeletion, err = git.GetDiffShortStatByCmdArgs(gitRepo.Ctx, repoPath, nil, actualBeforeCommitID.String(), afterCommitID)
diff.NumFiles, diff.TotalAddition, diff.TotalDeletion, err = gitrepo.GetDiffShortStatByCmdArgs(ctx, repo, nil, actualBeforeCommitID.String(), afterCommitID)
if err != nil {
return nil, err
}

View File

@ -14,6 +14,7 @@ import (
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
notify_service "code.gitea.io/gitea/services/notify"
)
@ -116,7 +117,7 @@ func isValidReviewRequest(ctx context.Context, reviewer, doer *user_model.User,
}
}
lastReview, err := issues_model.GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID)
lastReview, err := issues_model.GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID, optional.None[bool]())
if err != nil && !issues_model.IsErrReviewNotExist(err) {
return err
}

View File

@ -50,7 +50,7 @@ var (
func markPullRequestStatusAsChecking(ctx context.Context, pr *issues_model.PullRequest) bool {
pr.Status = issues_model.PullRequestStatusChecking
err := pr.UpdateColsIfNotMerged(ctx, "status")
_, err := pr.UpdateColsIfNotMerged(ctx, "status")
if err != nil {
log.Error("UpdateColsIfNotMerged failed, pr: %-v, err: %v", pr, err)
return false
@ -256,7 +256,7 @@ func markPullRequestAsMergeable(ctx context.Context, pr *issues_model.PullReques
return
}
if err := pr.UpdateColsIfNotMerged(ctx, "merge_base", "status", "conflicted_files", "changed_protected_files"); err != nil {
if _, err := pr.UpdateColsIfNotMerged(ctx, "merge_base", "status", "conflicted_files", "changed_protected_files"); err != nil {
log.Error("Update[%-v]: %v", pr, err)
}

View File

@ -100,13 +100,6 @@ func NewPullRequest(ctx context.Context, opts *NewPullRequestOptions) error {
return err
}
divergence, err := git.GetDivergingCommits(ctx, prCtx.tmpBasePath, baseBranch, trackingBranch)
if err != nil {
return err
}
pr.CommitsAhead = divergence.Ahead
pr.CommitsBehind = divergence.Behind
assigneeCommentMap := make(map[int64]*issues_model.Comment)
var reviewNotifiers []*issue_service.ReviewRequestNotifier
@ -135,6 +128,12 @@ func NewPullRequest(ctx context.Context, opts *NewPullRequestOptions) error {
return err
}
// Update Commit Divergence
err = syncCommitDivergence(ctx, pr)
if err != nil {
return err
}
// add first push codes comment
if _, err := CreatePushPullComment(ctx, issue.Poster, pr, git.BranchPrefix+pr.BaseBranch, pr.GetGitHeadRefName(), false); err != nil {
return err
@ -288,25 +287,21 @@ func ChangeTargetBranch(ctx context.Context, pr *issues_model.PullRequest, doer
pr.Status = issues_model.PullRequestStatusMergeable
}
// Update Commit Divergence
divergence, err := GetDiverging(ctx, pr)
if err != nil {
return err
}
pr.CommitsAhead = divergence.Ahead
pr.CommitsBehind = divergence.Behind
// add first push codes comment
baseGitRepo, err := gitrepo.OpenRepository(ctx, pr.BaseRepo)
if err != nil {
return err
}
defer baseGitRepo.Close()
return db.WithTx(ctx, func(ctx context.Context) error {
if err := pr.UpdateColsIfNotMerged(ctx, "merge_base", "status", "conflicted_files", "changed_protected_files", "base_branch", "commits_ahead", "commits_behind"); err != nil {
// The UPDATE acquires the transaction lock, if the UPDATE succeeds, it should have updated one row (the "base_branch" is changed)
// If no row is updated, it means the PR has been merged or closed in the meantime
updated, err := pr.UpdateColsIfNotMerged(ctx, "merge_base", "status", "conflicted_files", "changed_protected_files", "base_branch")
if err != nil {
return err
}
if updated == 0 {
return util.ErrorWrap(util.ErrInvalidArgument, "pull request status has changed")
}
if err := syncCommitDivergence(ctx, pr); err != nil {
return fmt.Errorf("syncCommitDivergence: %w", err)
}
// Create comment
options := &issues_model.CreateCommentOptions{
@ -345,7 +340,7 @@ func checkForInvalidation(ctx context.Context, requests issues_model.PullRequest
}
go func() {
// FIXME: graceful: We need to tell the manager we're doing something...
err := InvalidateCodeComments(ctx, requests, doer, gitRepo, branch)
err := InvalidateCodeComments(ctx, requests, doer, repo, gitRepo, branch)
if err != nil {
log.Error("PullRequestList.InvalidateCodeComments: %v", err)
}
@ -373,15 +368,21 @@ func AddTestPullRequestTask(opts TestPullRequestOptions) {
// If you don't let it run all the way then you will lose data
// TODO: graceful: AddTestPullRequestTask needs to become a queue!
repo, err := repo_model.GetRepositoryByID(ctx, opts.RepoID)
if err != nil {
log.Error("GetRepositoryByID: %v", err)
return
}
// GetUnmergedPullRequestsByHeadInfo() only return open and unmerged PR.
prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, opts.RepoID, opts.Branch)
headBranchPRs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(ctx, opts.RepoID, opts.Branch)
if err != nil {
log.Error("Find pull requests [head_repo_id: %d, head_branch: %s]: %v", opts.RepoID, opts.Branch, err)
return
}
for _, pr := range prs {
for _, pr := range headBranchPRs {
log.Trace("Updating PR[%d]: composing new test task", pr.ID)
pr.HeadRepo = repo // avoid loading again
if pr.Flow == issues_model.PullRequestFlowGithub {
if err := PushToBaseRepo(ctx, pr); err != nil {
log.Error("PushToBaseRepo: %v", err)
@ -399,14 +400,14 @@ func AddTestPullRequestTask(opts TestPullRequestOptions) {
}
if opts.IsSync {
if err = prs.LoadAttributes(ctx); err != nil {
if err = headBranchPRs.LoadAttributes(ctx); err != nil {
log.Error("PullRequestList.LoadAttributes: %v", err)
}
if invalidationErr := checkForInvalidation(ctx, prs, opts.RepoID, opts.Doer, opts.Branch); invalidationErr != nil {
if invalidationErr := checkForInvalidation(ctx, headBranchPRs, opts.RepoID, opts.Doer, opts.Branch); invalidationErr != nil {
log.Error("checkForInvalidation: %v", invalidationErr)
}
if err == nil {
for _, pr := range prs {
for _, pr := range headBranchPRs {
objectFormat := git.ObjectFormatFromName(pr.BaseRepo.ObjectFormatName)
if opts.NewCommitID != "" && opts.NewCommitID != objectFormat.EmptyObjectID().String() {
changed, err := checkIfPRContentChanged(ctx, pr, opts.OldCommitID, opts.NewCommitID)
@ -433,14 +434,8 @@ func AddTestPullRequestTask(opts TestPullRequestOptions) {
if err := issues_model.MarkReviewsAsNotStale(ctx, pr.IssueID, opts.NewCommitID); err != nil {
log.Error("MarkReviewsAsNotStale: %v", err)
}
divergence, err := GetDiverging(ctx, pr)
if err != nil {
log.Error("GetDiverging: %v", err)
} else {
err = pr.UpdateCommitDivergence(ctx, divergence.Ahead, divergence.Behind)
if err != nil {
log.Error("UpdateCommitDivergence: %v", err)
}
if err = syncCommitDivergence(ctx, pr); err != nil {
log.Error("syncCommitDivergence: %v", err)
}
}
@ -460,24 +455,22 @@ func AddTestPullRequestTask(opts TestPullRequestOptions) {
}
log.Trace("AddTestPullRequestTask [base_repo_id: %d, base_branch: %s]: finding pull requests", opts.RepoID, opts.Branch)
prs, err = issues_model.GetUnmergedPullRequestsByBaseInfo(ctx, opts.RepoID, opts.Branch)
// The base repositories of baseBranchPRs are the same one (opts.RepoID)
baseBranchPRs, err := issues_model.GetUnmergedPullRequestsByBaseInfo(ctx, opts.RepoID, opts.Branch)
if err != nil {
log.Error("Find pull requests [base_repo_id: %d, base_branch: %s]: %v", opts.RepoID, opts.Branch, err)
return
}
for _, pr := range prs {
divergence, err := GetDiverging(ctx, pr)
for _, pr := range baseBranchPRs {
pr.BaseRepo = repo // avoid loading again
err = syncCommitDivergence(ctx, pr)
if err != nil {
if git_model.IsErrBranchNotExist(err) && !gitrepo.IsBranchExist(ctx, pr.HeadRepo, pr.HeadBranch) {
log.Warn("Cannot test PR %s/%d: head_branch %s no longer exists", pr.BaseRepo.Name, pr.IssueID, pr.HeadBranch)
if errors.Is(err, util.ErrNotExist) {
log.Warn("Cannot test PR %s/%d with base=%s head=%s: no longer exists", pr.BaseRepo.FullName(), pr.IssueID, pr.BaseBranch, pr.HeadBranch)
} else {
log.Error("GetDiverging: %v", err)
}
} else {
err = pr.UpdateCommitDivergence(ctx, divergence.Ahead, divergence.Behind)
if err != nil {
log.Error("UpdateCommitDivergence: %v", err)
log.Error("syncCommitDivergence: %v", err)
}
continue
}
StartPullRequestCheckDelayable(ctx, pr)
}
@ -487,7 +480,7 @@ func AddTestPullRequestTask(opts TestPullRequestOptions) {
// checkIfPRContentChanged checks if diff to target branch has changed by push
// A commit can be considered to leave the PR untouched if the patch/diff with its merge base is unchanged
func checkIfPRContentChanged(ctx context.Context, pr *issues_model.PullRequest, oldCommitID, newCommitID string) (hasChanged bool, err error) {
prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr)
prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr) // FIXME: why it still needs to create a temp repo, since the alongside calls like GetDiverging doesn't do so anymore
if err != nil {
log.Error("CreateTemporaryRepoForPR %-v: %v", pr, err)
return false, err

View File

@ -47,11 +47,25 @@ func (err ErrDismissRequestOnClosedPR) Unwrap() error {
// ErrSubmitReviewOnClosedPR represents an error when an user tries to submit an approve or reject review associated to a closed or merged PR.
var ErrSubmitReviewOnClosedPR = errors.New("can't submit review for a closed or merged PR")
// LineBlame returns the latest commit at the given line
func lineBlame(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, branch, file string, line uint) (*git.Commit, error) {
sha, err := gitrepo.LineBlame(ctx, repo, branch, file, line)
if err != nil {
return nil, err
}
if len(sha) < 40 {
return nil, fmt.Errorf("invalid result of blame: %s", sha)
}
objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
return gitRepo.GetCommit(sha[:objectFormat.FullLength()])
}
// checkInvalidation checks if the line of code comment got changed by another commit.
// If the line got changed the comment is going to be invalidated.
func checkInvalidation(ctx context.Context, c *issues_model.Comment, repo *git.Repository, branch string) error {
func checkInvalidation(ctx context.Context, c *issues_model.Comment, repo *repo_model.Repository, gitRepo *git.Repository, branch string) error {
// FIXME differentiate between previous and proposed line
commit, err := repo.LineBlame(branch, repo.Path, c.TreePath, uint(c.UnsignedLine()))
commit, err := lineBlame(ctx, repo, gitRepo, branch, c.TreePath, uint(c.UnsignedLine()))
if err != nil && (strings.Contains(err.Error(), "fatal: no such path") || notEnoughLines.MatchString(err.Error())) {
c.Invalidated = true
return issues_model.UpdateCommentInvalidate(ctx, c)
@ -67,7 +81,7 @@ func checkInvalidation(ctx context.Context, c *issues_model.Comment, repo *git.R
}
// InvalidateCodeComments will lookup the prs for code comments which got invalidated by change
func InvalidateCodeComments(ctx context.Context, prs issues_model.PullRequestList, doer *user_model.User, repo *git.Repository, branch string) error {
func InvalidateCodeComments(ctx context.Context, prs issues_model.PullRequestList, doer *user_model.User, repo *repo_model.Repository, gitRepo *git.Repository, branch string) error {
if len(prs) == 0 {
return nil
}
@ -83,7 +97,7 @@ func InvalidateCodeComments(ctx context.Context, prs issues_model.PullRequestLis
return fmt.Errorf("find code comments: %v", err)
}
for _, comment := range codeComments {
if err := checkInvalidation(ctx, comment, repo, branch); err != nil {
if err := checkInvalidation(ctx, comment, repo, gitRepo, branch); err != nil {
return err
}
}
@ -233,7 +247,7 @@ func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_mo
// FIXME validate treePath
// Get latest commit referencing the commented line
// No need for get commit for base branch changes
commit, err := gitRepo.LineBlame(head, gitRepo.Path, treePath, uint(line))
commit, err := lineBlame(ctx, pr.BaseRepo, gitRepo, head, treePath, uint(line))
if err == nil {
commitID = commit.ID.String()
} else if !(strings.Contains(err.Error(), "exit status 128 - fatal: no such path") || notEnoughLines.MatchString(err.Error())) {

View File

@ -14,7 +14,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/globallock"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/repository"
@ -34,17 +34,21 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.
}
defer releaser()
diffCount, err := GetDiverging(ctx, pr)
if err != nil {
return err
} else if diffCount.Behind == 0 {
return fmt.Errorf("HeadBranch of PR %d is up to date", pr.Index)
}
if err := pr.LoadBaseRepo(ctx); err != nil {
log.Error("unable to load BaseRepo for %-v during update-by-merge: %v", pr, err)
return fmt.Errorf("unable to load BaseRepo for PR[%d] during update-by-merge: %w", pr.ID, err)
}
// TODO: FakePR: if the PR is a fake PR (for example: from Merge Upstream), then no need to check diverging
if pr.ID > 0 {
diffCount, err := gitrepo.GetDivergingCommits(ctx, pr.BaseRepo, pr.BaseBranch, pr.GetGitHeadRefName())
if err != nil {
return err
} else if diffCount.Behind == 0 {
return fmt.Errorf("HeadBranch of PR %d is up to date", pr.Index)
}
}
if err := pr.LoadHeadRepo(ctx); err != nil {
log.Error("unable to load HeadRepo for PR %-v during update-by-merge: %v", pr, err)
return fmt.Errorf("unable to load HeadRepo for PR[%d] during update-by-merge: %w", pr.ID, err)
@ -172,18 +176,13 @@ func IsUserAllowedToUpdate(ctx context.Context, pull *issues_model.PullRequest,
return mergeAllowed, rebaseAllowed, nil
}
// GetDiverging determines how many commits a PR is ahead or behind the PR base branch
func GetDiverging(ctx context.Context, pr *issues_model.PullRequest) (*git.DivergeObject, error) {
log.Trace("GetDiverging[%-v]: compare commits", pr)
prCtx, cancel, err := createTemporaryRepoForPR(ctx, pr)
if err != nil {
if !git_model.IsErrBranchNotExist(err) {
log.Error("CreateTemporaryRepoForPR %-v: %v", pr, err)
}
return nil, err
func syncCommitDivergence(ctx context.Context, pr *issues_model.PullRequest) error {
if err := pr.LoadBaseRepo(ctx); err != nil {
return err
}
defer cancel()
diff, err := git.GetDivergingCommits(ctx, prCtx.tmpBasePath, baseBranch, trackingBranch)
return &diff, err
divergence, err := gitrepo.GetDivergingCommits(ctx, pr.BaseRepo, pr.BaseBranch, pr.GetGitHeadRefName())
if err != nil {
return err
}
return pr.UpdateCommitDivergence(ctx, divergence.Ahead, divergence.Behind)
}

View File

@ -33,7 +33,6 @@ import (
actions_service "code.gitea.io/gitea/services/actions"
notify_service "code.gitea.io/gitea/services/notify"
release_service "code.gitea.io/gitea/services/release"
files_service "code.gitea.io/gitea/services/repository/files"
"xorm.io/builder"
)
@ -123,9 +122,9 @@ func getDivergenceCacheKey(repoID int64, branchName string) string {
}
// getDivergenceFromCache gets the divergence from cache
func getDivergenceFromCache(repoID int64, branchName string) (*git.DivergeObject, bool) {
func getDivergenceFromCache(repoID int64, branchName string) (*gitrepo.DivergeObject, bool) {
data, ok := cache.GetCache().Get(getDivergenceCacheKey(repoID, branchName))
res := git.DivergeObject{
res := gitrepo.DivergeObject{
Ahead: -1,
Behind: -1,
}
@ -139,7 +138,7 @@ func getDivergenceFromCache(repoID int64, branchName string) (*git.DivergeObject
return &res, true
}
func putDivergenceFromCache(repoID int64, branchName string, divergence *git.DivergeObject) error {
func putDivergenceFromCache(repoID int64, branchName string, divergence *gitrepo.DivergeObject) error {
bs, err := json.Marshal(divergence)
if err != nil {
return err
@ -178,7 +177,7 @@ func loadOneBranch(ctx context.Context, repo *repo_model.Repository, dbBranch *g
p := protectedBranches.GetFirstMatched(branchName)
isProtected := p != nil
var divergence *git.DivergeObject
var divergence *gitrepo.DivergeObject
// it's not default branch
if repo.DefaultBranch != dbBranch.Name && !dbBranch.IsDeleted {
@ -186,9 +185,9 @@ func loadOneBranch(ctx context.Context, repo *repo_model.Repository, dbBranch *g
divergence, cached = getDivergenceFromCache(repo.ID, dbBranch.Name)
if !cached {
var err error
divergence, err = files_service.CountDivergingCommits(ctx, repo, git.BranchPrefix+branchName)
divergence, err = gitrepo.GetDivergingCommits(ctx, repo, repo.DefaultBranch, git.BranchPrefix+branchName)
if err != nil {
log.Error("CountDivergingCommits: %v", err)
log.Error("GetDivergingCommits: %v", err)
} else {
if err = putDivergenceFromCache(repo.ID, dbBranch.Name, divergence); err != nil {
log.Error("putDivergenceFromCache: %v", err)
@ -199,7 +198,7 @@ func loadOneBranch(ctx context.Context, repo *repo_model.Repository, dbBranch *g
if divergence == nil {
// tolerate the error that we cannot get divergence
divergence = &git.DivergeObject{Ahead: -1, Behind: -1}
divergence = &gitrepo.DivergeObject{Ahead: -1, Behind: -1}
}
pr, err := issues_model.GetLatestPullRequestByHeadInfo(ctx, repo.ID, branchName)
@ -442,7 +441,7 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, doer *user_m
}
if err := git_model.RenameBranch(ctx, repo, from, to, func(ctx context.Context, isDefault bool) error {
err2 := gitRepo.RenameBranch(from, to)
err2 := gitrepo.RenameBranch(ctx, repo, from, to)
if err2 != nil {
return err2
}
@ -553,9 +552,7 @@ func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.R
return nil
}
return gitRepo.DeleteBranch(branchName, git.DeleteBranchOptions{
Force: true,
})
return gitrepo.DeleteBranch(ctx, repo, branchName, true)
}); err != nil {
return err
}
@ -720,7 +717,7 @@ func GetBranchDivergingInfo(ctx reqctx.RequestContext, baseRepo *repo_model.Repo
// if the fork repo has new commits, this call will fail because they are not in the base repo
// exit status 128 - fatal: Invalid symmetric difference expression aaaaaaaaaaaa...bbbbbbbbbbbb
// so at the moment, we first check the update time, then check whether the fork branch has base's head
diff, err := git.GetDivergingCommits(ctx, baseRepo.RepoPath(), baseGitBranch.CommitID, headGitBranch.CommitID)
diff, err := gitrepo.GetDivergingCommits(ctx, baseRepo, baseGitBranch.CommitID, headGitBranch.CommitID)
if err != nil {
info.BaseHasNewCommits = baseGitBranch.UpdatedUnix > headGitBranch.UpdatedUnix
if headRepo.IsFork && info.BaseHasNewCommits {

View File

@ -6,21 +6,11 @@ package files
import (
"context"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/structs"
asymkey_service "code.gitea.io/gitea/services/asymkey"
)
// CountDivergingCommits determines how many commits a branch is ahead or behind the repository's base branch
func CountDivergingCommits(ctx context.Context, repo *repo_model.Repository, branch string) (*git.DivergeObject, error) {
divergence, err := git.GetDivergingCommits(ctx, repo.RepoPath(), repo.DefaultBranch, branch)
if err != nil {
return nil, err
}
return &divergence, nil
}
// GetPayloadCommitVerification returns the verification information of a commit
func GetPayloadCommitVerification(ctx context.Context, commit *git.Commit) *structs.PayloadCommitVerification {
verification := &structs.PayloadCommitVerification{}

View File

@ -27,8 +27,8 @@ import (
"code.gitea.io/gitea/modules/util"
)
func cloneWiki(ctx context.Context, u *user_model.User, opts migration.MigrateOptions, migrateTimeout time.Duration) (string, error) {
wikiPath := repo_model.WikiPath(u.Name, opts.RepoName)
func cloneWiki(ctx context.Context, repo *repo_model.Repository, opts migration.MigrateOptions, migrateTimeout time.Duration) (string, error) {
wikiPath := repo.WikiPath()
wikiRemotePath := repo_module.WikiRemoteURL(ctx, opts.CloneAddr)
if wikiRemotePath == "" {
return "", nil
@ -59,7 +59,7 @@ func cloneWiki(ctx context.Context, u *user_model.User, opts migration.MigrateOp
return "", err
}
defaultBranch, err := git.GetDefaultBranch(ctx, wikiPath)
defaultBranch, err := gitrepo.GetDefaultBranch(ctx, repo.WikiStorageRepo())
if err != nil {
cleanIncompleteWikiPath()
return "", fmt.Errorf("failed to get wiki repo default branch for %q, err: %w", wikiPath, err)
@ -73,7 +73,7 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
repo *repo_model.Repository, opts migration.MigrateOptions,
httpTransport *http.Transport,
) (*repo_model.Repository, error) {
repoPath := repo_model.RepoPath(u.Name, opts.RepoName)
repoPath := repo.RepoPath()
if u.IsOrganization() {
t, err := organization.OrgFromUser(u).GetOwnerTeam(ctx)
@ -108,7 +108,7 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
}
if opts.Wiki {
defaultWikiBranch, err := cloneWiki(ctx, u, opts, migrateTimeout)
defaultWikiBranch, err := cloneWiki(ctx, repo, opts, migrateTimeout)
if err != nil {
return repo, fmt.Errorf("clone wiki error: %w", err)
}
@ -137,7 +137,7 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
if !repo.IsEmpty {
if len(repo.DefaultBranch) == 0 {
// Try to get HEAD branch and set it as default branch.
headBranchName, err := git.GetDefaultBranch(ctx, repoPath)
headBranchName, err := gitrepo.GetDefaultBranch(ctx, repo)
if err != nil {
return repo, fmt.Errorf("GetHEADBranch: %w", err)
}

View File

@ -131,24 +131,74 @@ func TestWebhookDeliverHookTask(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
done := make(chan struct{}, 1)
version2Body := `{
"body": "[[test/repo](http://localhost:3000/test/repo)] user1 pushed 2 commits to [test](http://localhost:3000/test/repo/src/branch/test):\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778): commit message - user1",
"msgtype": "",
"format": "org.matrix.custom.html",
"formatted_body": "[<a href=\"http://localhost:3000/test/repo\">test/repo</a>] user1 pushed 2 commits to <a href=\"http://localhost:3000/test/repo/src/branch/test\">test</a>:<br><a href=\"http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778\">2020558</a>: commit message - user1<br><a href=\"http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778\">2020558</a>: commit message - user1",
"io.gitea.commits": [
{
"id": "2020558fe2e34debb818a514715839cabd25e778",
"message": "commit message",
"url": "http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778",
"author": {
"name": "user1",
"email": "user1@localhost",
"username": "user1"
},
"committer": {
"name": "user1",
"email": "user1@localhost",
"username": "user1"
},
"verification": null,
"timestamp": "0001-01-01T00:00:00Z",
"added": null,
"removed": null,
"modified": null
},
{
"id": "2020558fe2e34debb818a514715839cabd25e778",
"message": "commit message",
"url": "http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778",
"author": {
"name": "user1",
"email": "user1@localhost",
"username": "user1"
},
"committer": {
"name": "user1",
"email": "user1@localhost",
"username": "user1"
},
"verification": null,
"timestamp": "0001-01-01T00:00:00Z",
"added": null,
"removed": null,
"modified": null
}
]
}`
testVersion := 0
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "PUT", r.Method)
switch r.URL.Path {
case "/webhook/66d222a5d6349e1311f551e50722d837e30fce98":
// Version 1
assert.True(t, strings.HasPrefix(r.URL.Path, "/webhook/"))
assert.Len(t, r.URL.Path, len("/webhook/")+40) // +40 for txnID, a unique ID from payload's sha1 hash
switch testVersion {
case 1: // Version 1
assert.Equal(t, "push", r.Header.Get("X-GitHub-Event"))
assert.Empty(t, r.Header.Get("Content-Type"))
body, err := io.ReadAll(r.Body)
assert.NoError(t, err)
assert.Equal(t, `{"data": 42}`, string(body))
case "/webhook/6db5dc1e282529a8c162c7fe93dd2667494eeb51":
// Version 2
case 2: // Version 2
assert.Equal(t, "push", r.Header.Get("X-GitHub-Event"))
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
body, err := io.ReadAll(r.Body)
assert.NoError(t, err)
assert.Len(t, body, 2147)
assert.JSONEq(t, version2Body, string(body))
default:
w.WriteHeader(http.StatusNotFound)
@ -172,6 +222,7 @@ func TestWebhookDeliverHookTask(t *testing.T) {
assert.NoError(t, webhook_model.CreateWebhook(t.Context(), hook))
t.Run("Version 1", func(t *testing.T) {
testVersion = 1
hookTask := &webhook_model.HookTask{
HookID: hook.ID,
EventType: webhook_module.HookEventPush,
@ -198,6 +249,7 @@ func TestWebhookDeliverHookTask(t *testing.T) {
data, err := p.JSONPayload()
assert.NoError(t, err)
testVersion = 2
hookTask := &webhook_model.HookTask{
HookID: hook.ID,
EventType: webhook_module.HookEventPush,

View File

@ -274,6 +274,7 @@ func getMessageBody(htmlText string) string {
// getMatrixTxnID computes the transaction ID to ensure idempotency
func getMatrixTxnID(payload []byte) (string, error) {
payload = bytes.TrimSpace(payload)
if len(payload) >= matrixPayloadSizeLimit {
return "", fmt.Errorf("getMatrixTxnID: payload size %d > %d", len(payload), matrixPayloadSizeLimit)
}

View File

@ -4,6 +4,7 @@
package webhook
import (
"strings"
"testing"
webhook_model "code.gitea.io/gitea/models/webhook"
@ -216,7 +217,9 @@ func TestMatrixJSONPayload(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "PUT", req.Method)
assert.Equal(t, "/_matrix/client/r0/rooms/ROOM_ID/send/m.room.message/6db5dc1e282529a8c162c7fe93dd2667494eeb51", req.URL.Path)
txnID, ok := strings.CutPrefix(req.URL.Path, "/_matrix/client/r0/rooms/ROOM_ID/send/m.room.message/")
assert.True(t, ok)
assert.Len(t, txnID, 40) // txnID is just a unique ID for a webhook request, it is a sha1 hash from the payload
assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
var body MatrixPayload

View File

@ -6,7 +6,6 @@ package wiki
import (
"context"
"errors"
"fmt"
"os"
"strings"
@ -22,7 +21,6 @@ import (
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/util"
asymkey_service "code.gitea.io/gitea/services/asymkey"
repo_service "code.gitea.io/gitea/services/repository"
)
@ -393,15 +391,7 @@ func ChangeDefaultWikiBranch(ctx context.Context, repo *repo_model.Repository, n
return nil
}
gitRepo, err := gitrepo.OpenRepository(ctx, repo.WikiStorageRepo())
if errors.Is(err, util.ErrNotExist) {
return nil // no git repo on storage, no need to do anything else
} else if err != nil {
return fmt.Errorf("unable to open repository: %w", err)
}
defer gitRepo.Close()
err = gitRepo.RenameBranch(oldDefBranch, newBranch)
err = gitrepo.RenameBranch(ctx, repo.WikiStorageRepo(), oldDefBranch, newBranch)
if err != nil {
return fmt.Errorf("unable to rename default branch: %w", err)
}

View File

@ -168,10 +168,6 @@
{{template "repo/issue/view_content/reference_issue_dialog" .}}
{{template "shared/user/block_user_dialog" .}}
<div class="tw-hidden" id="no-content">
<span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span>
</div>
<div class="ui g-modal-confirm delete modal">
<div class="header">
{{svg "octicon-trash"}}

View File

@ -9109,7 +9109,7 @@
},
{
"type": "string",
"description": "comma separated list of labels. Fetch only issues that have any of this labels. Non existent labels are discarded",
"description": "comma separated list of label names. Fetch only issues that have any of this label names. Non existent labels are discarded.",
"name": "labels",
"in": "query"
},

View File

@ -318,7 +318,7 @@ func TestPackageSwift(t *testing.T) {
AddBasicAuth(user.Name)
resp = MakeRequest(t, req, http.StatusOK)
assert.Equal(t, body, resp.Body.String())
assert.JSONEq(t, body, resp.Body.String())
})
t.Run("PackageVersionMetadata", func(t *testing.T) {

View File

@ -121,10 +121,10 @@ func TestAPIRepoBranchesMirror(t *testing.T) {
resp = MakeRequest(t, req, http.StatusForbidden)
bs, err = io.ReadAll(resp.Body)
assert.NoError(t, err)
assert.Equal(t, "{\"message\":\"Git Repository is a mirror.\",\"url\":\""+setting.AppURL+"api/swagger\"}\n", string(bs))
assert.JSONEq(t, "{\"message\":\"Git Repository is a mirror.\",\"url\":\""+setting.AppURL+"api/swagger\"}", string(bs))
resp = MakeRequest(t, NewRequest(t, "DELETE", link2.String()).AddTokenAuth(token), http.StatusForbidden)
bs, err = io.ReadAll(resp.Body)
assert.NoError(t, err)
assert.Equal(t, "{\"message\":\"Git Repository is a mirror.\",\"url\":\""+setting.AppURL+"api/swagger\"}\n", string(bs))
assert.JSONEq(t, "{\"message\":\"Git Repository is a mirror.\",\"url\":\""+setting.AppURL+"api/swagger\"}", string(bs))
}

View File

@ -12,7 +12,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
@ -43,7 +43,7 @@ func TestChangeDefaultBranch(t *testing.T) {
session.MakeRequest(t, req, http.StatusNotFound)
}
func checkDivergence(t *testing.T, session *TestSession, branchesURL, expectedDefaultBranch string, expectedBranchToDivergence map[string]git.DivergeObject) {
func checkDivergence(t *testing.T, session *TestSession, branchesURL, expectedDefaultBranch string, expectedBranchToDivergence map[string]*gitrepo.DivergeObject) {
req := NewRequest(t, "GET", branchesURL)
resp := session.MakeRequest(t, req, http.StatusOK)
@ -92,7 +92,7 @@ func TestChangeDefaultBranchDivergence(t *testing.T) {
settingsBranchesURL := fmt.Sprintf("/%s/%s/settings/branches", owner.Name, repo.Name)
// check branch divergence before switching default branch
expectedBranchToDivergenceBefore := map[string]git.DivergeObject{
expectedBranchToDivergenceBefore := map[string]*gitrepo.DivergeObject{
"not-signed": {
Ahead: 0,
Behind: 0,
@ -119,7 +119,7 @@ func TestChangeDefaultBranchDivergence(t *testing.T) {
session.MakeRequest(t, req, http.StatusSeeOther)
// check branch divergence after switching default branch
expectedBranchToDivergenceAfter := map[string]git.DivergeObject{
expectedBranchToDivergenceAfter := map[string]*gitrepo.DivergeObject{
"master": {
Ahead: 1,
Behind: 0,

View File

@ -413,7 +413,8 @@ func logUnexpectedResponse(t testing.TB, recorder *httptest.ResponseRecorder) {
func DecodeJSON(t testing.TB, resp *httptest.ResponseRecorder, v any) {
t.Helper()
decoder := json.NewDecoder(resp.Body)
// FIXME: JSON-KEY-CASE: for testing purpose only, because many structs don't provide `json` tags, they just use capitalized field names
decoder := json.NewDecoderCaseInsensitive(resp.Body)
require.NoError(t, decoder.Decode(v))
}

View File

@ -14,11 +14,13 @@ import (
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/gitrepo"
pull_service "code.gitea.io/gitea/services/pull"
repo_service "code.gitea.io/gitea/services/repository"
files_service "code.gitea.io/gitea/services/repository/files"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAPIPullUpdate(t *testing.T) {
@ -27,14 +29,16 @@ func TestAPIPullUpdate(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
org26 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 26})
pr := createOutdatedPR(t, user, org26)
require.NoError(t, pr.LoadBaseRepo(t.Context()))
require.NoError(t, pr.LoadIssue(t.Context()))
// Test GetDiverging
diffCount, err := pull_service.GetDiverging(t.Context(), pr)
assert.NoError(t, err)
diffCount, err := gitrepo.GetDivergingCommits(t.Context(), pr.BaseRepo, pr.BaseBranch, pr.GetGitHeadRefName())
require.NoError(t, err)
assert.Equal(t, 1, diffCount.Behind)
assert.Equal(t, 1, diffCount.Ahead)
assert.NoError(t, pr.LoadBaseRepo(t.Context()))
assert.NoError(t, pr.LoadIssue(t.Context()))
assert.Equal(t, diffCount.Behind, pr.CommitsBehind)
assert.Equal(t, diffCount.Ahead, pr.CommitsAhead)
session := loginUser(t, "user2")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
@ -43,10 +47,14 @@ func TestAPIPullUpdate(t *testing.T) {
session.MakeRequest(t, req, http.StatusOK)
// Test GetDiverging after update
diffCount, err = pull_service.GetDiverging(t.Context(), pr)
assert.NoError(t, err)
diffCount, err = gitrepo.GetDivergingCommits(t.Context(), pr.BaseRepo, pr.BaseBranch, pr.GetGitHeadRefName())
require.NoError(t, err)
assert.Equal(t, 0, diffCount.Behind)
assert.Equal(t, 2, diffCount.Ahead)
assert.Eventually(t, func() bool {
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID})
return diffCount.Behind == pr.CommitsBehind && diffCount.Ahead == pr.CommitsAhead
}, 5*time.Second, 20*time.Millisecond)
})
}
@ -56,13 +64,13 @@ func TestAPIPullUpdateByRebase(t *testing.T) {
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()))
// Test GetDiverging
diffCount, err := pull_service.GetDiverging(t.Context(), pr)
diffCount, err := gitrepo.GetDivergingCommits(t.Context(), pr.BaseRepo, pr.BaseBranch, pr.GetGitHeadRefName())
assert.NoError(t, err)
assert.Equal(t, 1, diffCount.Behind)
assert.Equal(t, 1, diffCount.Ahead)
assert.NoError(t, pr.LoadBaseRepo(t.Context()))
assert.NoError(t, pr.LoadIssue(t.Context()))
session := loginUser(t, "user2")
@ -72,7 +80,7 @@ func TestAPIPullUpdateByRebase(t *testing.T) {
session.MakeRequest(t, req, http.StatusOK)
// Test GetDiverging after update
diffCount, err = pull_service.GetDiverging(t.Context(), pr)
diffCount, err = gitrepo.GetDivergingCommits(t.Context(), pr.BaseRepo, pr.BaseBranch, pr.GetGitHeadRefName())
assert.NoError(t, err)
assert.Equal(t, 0, diffCount.Behind)
assert.Equal(t, 1, diffCount.Ahead)

View File

@ -1,7 +1,7 @@
{
"include": [
"${configDir}/.*",
"${configDir}/*",
"${configDir}/.*",
"${configDir}/tests/e2e/**/*",
"${configDir}/tests/e2e/**/.*",
"${configDir}/tools/**/*",
@ -17,27 +17,31 @@
"allowImportingTsExtensions": true,
"allowJs": true,
"allowSyntheticDefaultImports": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"alwaysStrict": true,
"erasableSyntaxOnly": true,
"esModuleInterop": true,
"exactOptionalPropertyTypes": false,
"isolatedModules": true,
"libReplacement": false,
"noEmit": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noPropertyAccessFromIndexSignature": false,
"noUnusedLocals": true,
"noUnusedParameters": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"verbatimModuleSyntax": true,
"stripInternal": true,
"sourceMap": true,
"strict": false,
"strictBindCallApply": true,
"strictBuiltinIteratorReturn": true,
"strictFunctionTypes": true,
"noImplicitAny": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noPropertyAccessFromIndexSignature": false,
"exactOptionalPropertyTypes": false,
"sourceMap": true,
"stripInternal": true,
"verbatimModuleSyntax": true,
"types": [
"vitest/globals",
"./web_src/js/globals.d.ts",

View File

@ -13,10 +13,10 @@ async function tryOnEditContent(e: DOMEvent<MouseEvent>) {
if (!clickTarget) return;
e.preventDefault();
const segment = clickTarget.closest('.comment-header').nextElementSibling;
const editContentZone = segment.querySelector('.edit-content-zone');
const renderContent = segment.querySelector('.render-content');
const rawContent = segment.querySelector('.raw-content');
const commentContent = clickTarget.closest('.comment-header').nextElementSibling;
const editContentZone = commentContent.querySelector('.edit-content-zone');
let renderContent = commentContent.querySelector('.render-content');
const rawContent = commentContent.querySelector('.raw-content');
let comboMarkdownEditor : ComboMarkdownEditor;
@ -47,30 +47,32 @@ async function tryOnEditContent(e: DOMEvent<MouseEvent>) {
const response = await POST(editContentZone.getAttribute('data-update-url'), {data: params});
const data = await response.json();
if (response.status === 400) {
showErrorToast(data.errorMessage);
if (!response.ok) {
showErrorToast(data?.errorMessage ?? window.config.i18n.error_occurred);
return;
}
reinitializeAreYouSure(editContentZone.querySelector('form')); // the form is no longer dirty
editContentZone.setAttribute('data-content-version', data.contentVersion);
if (!data.content) {
renderContent.innerHTML = document.querySelector('#no-content').innerHTML;
rawContent.textContent = '';
} else {
renderContent.innerHTML = data.content;
rawContent.textContent = comboMarkdownEditor.value();
const refIssues = renderContent.querySelectorAll<HTMLElement>('p .ref-issue');
attachRefIssueContextPopup(refIssues);
}
const content = segment;
if (!content.querySelector('.dropzone-attachments')) {
// replace the render content with new one, to trigger re-initialization of all features
const newRenderContent = renderContent.cloneNode(false) as HTMLElement;
newRenderContent.innerHTML = data.content;
renderContent.replaceWith(newRenderContent);
renderContent = newRenderContent;
rawContent.textContent = comboMarkdownEditor.value();
const refIssues = renderContent.querySelectorAll<HTMLElement>('p .ref-issue');
attachRefIssueContextPopup(refIssues);
if (!commentContent.querySelector('.dropzone-attachments')) {
if (data.attachments !== '') {
content.insertAdjacentHTML('beforeend', data.attachments);
commentContent.insertAdjacentHTML('beforeend', data.attachments);
}
} else if (data.attachments === '') {
content.querySelector('.dropzone-attachments').remove();
commentContent.querySelector('.dropzone-attachments').remove();
} else {
content.querySelector('.dropzone-attachments').outerHTML = data.attachments;
commentContent.querySelector('.dropzone-attachments').outerHTML = data.attachments;
}
comboMarkdownEditor.dropzoneSubmitReload();
} catch (error) {

View File

@ -10,7 +10,7 @@ export function initTableSort() {
}
function tableSort(normSort: string, revSort: string, isDefault: string) {
if (!normSort) return false;
if (!normSort) return;
if (!revSort) revSort = '';
const url = new URL(window.location.href);

View File

@ -43,7 +43,7 @@ type ToastOpts = {
type ToastifyElement = HTMLElement & {_giteaToastifyInstance?: Toast};
/** See https://github.com/apvarun/toastify-js#api for options */
function showToast(message: string, level: Intent, {gravity, position, duration, useHtmlBody, preventDuplicates = true, ...other}: ToastOpts = {}): Toast {
function showToast(message: string, level: Intent, {gravity, position, duration, useHtmlBody, preventDuplicates = true, ...other}: ToastOpts = {}): Toast | null {
const body = useHtmlBody ? message : htmlEscape(message);
const parent = document.querySelector('.ui.dimmer.active') ?? document.body;
const duplicateKey = preventDuplicates ? (preventDuplicates === true ? `${level}-${body}` : preventDuplicates) : '';
@ -56,7 +56,7 @@ function showToast(message: string, level: Intent, {gravity, position, duration,
showElem(toastDupNumEl);
toastDupNumEl.textContent = String(Number(toastDupNumEl.textContent) + 1);
animateOnce(toastDupNumEl, 'pulse-1p5-200');
return;
return null;
}
}
@ -83,15 +83,15 @@ function showToast(message: string, level: Intent, {gravity, position, duration,
return toast;
}
export function showInfoToast(message: string, opts?: ToastOpts): Toast {
export function showInfoToast(message: string, opts?: ToastOpts): Toast | null {
return showToast(message, 'info', opts);
}
export function showWarningToast(message: string, opts?: ToastOpts): Toast {
export function showWarningToast(message: string, opts?: ToastOpts): Toast | null {
return showToast(message, 'warning', opts);
}
export function showErrorToast(message: string, opts?: ToastOpts): Toast {
export function showErrorToast(message: string, opts?: ToastOpts): Toast | null {
return showToast(message, 'error', opts);
}