mirror of
https://github.com/go-gitea/gitea.git
synced 2026-04-19 14:01:08 +00:00
Compare commits
171 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f913d90ab6 | ||
|
|
156d9ffb23 | ||
|
|
96515c0f20 | ||
|
|
4f562da975 | ||
|
|
76c539cd57 | ||
|
|
f7e3569fab | ||
|
|
b3290b62fc | ||
|
|
f7ac507671 | ||
|
|
e2517e0fa9 | ||
|
|
413074b1e1 | ||
|
|
3c46a3deb3 | ||
|
|
5552eff6e7 | ||
|
|
f44f7bf2d3 | ||
|
|
0f55eff0e7 | ||
|
|
b3bc79262d | ||
|
|
d1bd84f8cf | ||
|
|
19e36e8a70 | ||
|
|
00566cc953 | ||
|
|
579615936c | ||
|
|
2aee44cdd9 | ||
|
|
e7fca90a78 | ||
|
|
3422318545 | ||
|
|
996cc12bf7 | ||
|
|
99bc281856 | ||
|
|
8051056075 | ||
|
|
0b2f7575e7 | ||
|
|
216cf96cd4 | ||
|
|
e927a86586 | ||
|
|
76b7306daa | ||
|
|
8e412ababf | ||
|
|
4f1408cdcf | ||
|
|
5973437abb | ||
|
|
90843398ed | ||
|
|
9b3a9527ec | ||
|
|
7477f85e47 | ||
|
|
4098032aa8 | ||
|
|
dcce96c08d | ||
|
|
885f2b89d6 | ||
|
|
57ce10c0ca | ||
|
|
25785041e7 | ||
|
|
ff3d11034d | ||
|
|
750649c1ef | ||
|
|
eb95bbc1fd | ||
|
|
369830bada | ||
|
|
d7d6533311 | ||
|
|
c326369f47 | ||
|
|
4cdb8a7f96 | ||
|
|
38125a8d1d | ||
|
|
175a425825 | ||
|
|
6132f639e7 | ||
|
|
dfe4055b92 | ||
|
|
5fe9703586 | ||
|
|
53d67dae28 | ||
|
|
ef6ab681f7 | ||
|
|
812a3cffb3 | ||
|
|
669b22100b | ||
|
|
a0c77673ff | ||
|
|
d96b68cbf5 | ||
|
|
f8ec5b3e43 | ||
|
|
11891c2dac | ||
|
|
2c778ff067 | ||
|
|
83ce45b186 | ||
|
|
39e83bd3fd | ||
|
|
c2f9edd673 | ||
|
|
aa575672ac | ||
|
|
e9c14723b6 | ||
|
|
76b6e94b5b | ||
|
|
163113d173 | ||
|
|
7d010c6932 | ||
|
|
b71e688634 | ||
|
|
e147a8223a | ||
|
|
9a7cfd8620 | ||
|
|
79f4cd754b | ||
|
|
522cc25921 | ||
|
|
a99ccfdf74 | ||
|
|
d448ab9ad4 | ||
|
|
c97b89a662 | ||
|
|
2dd8ef8368 | ||
|
|
432e128074 | ||
|
|
8d6442a43e | ||
|
|
3d66e75a47 | ||
|
|
e98d9bb93e | ||
|
|
a601c09826 | ||
|
|
b5f50ff63b | ||
|
|
b1b35e934e | ||
|
|
544450a212 | ||
|
|
0ab447005d | ||
|
|
52902d4ece | ||
|
|
0e91c8a068 | ||
|
|
45cdc5d8fd | ||
|
|
b276849cd8 | ||
|
|
46d1d154e8 | ||
|
|
f164e38e04 | ||
|
|
d4d338f1c1 | ||
|
|
f6895f632e | ||
|
|
eaa916a786 | ||
|
|
91901c2a60 | ||
|
|
20cf4b7849 | ||
|
|
5e7207d428 | ||
|
|
e3bfee80dd | ||
|
|
f93e2cf301 | ||
|
|
1b01d6de82 | ||
|
|
d67cd622d0 | ||
|
|
15f3e9d5a5 | ||
|
|
01fa8b2b7e | ||
|
|
1d9ae7ac23 | ||
|
|
01873a99c1 | ||
|
|
ce70863793 | ||
|
|
327f2207dc | ||
|
|
db876d8f17 | ||
|
|
2b71bf283b | ||
|
|
1ca4fef611 | ||
|
|
70ee6b9029 | ||
|
|
e5b404ec53 | ||
|
|
5842cd23a6 | ||
|
|
289bd9694b | ||
|
|
154d7521a5 | ||
|
|
24189dcced | ||
|
|
f84bf259ad | ||
|
|
470b21056a | ||
|
|
61011f1648 | ||
|
|
7ea9722c1d | ||
|
|
297f63af42 | ||
|
|
6a55749359 | ||
|
|
8116742e2d | ||
|
|
0a9cbf3228 | ||
|
|
74dfadb543 | ||
|
|
8ffc1fbfbf | ||
|
|
e95378329b | ||
|
|
fddf6cd63f | ||
|
|
d253e2055b | ||
|
|
e194d89c74 | ||
|
|
04b6f90889 | ||
|
|
65a37572f3 | ||
|
|
f85cd7aeb5 | ||
|
|
cf644d565d | ||
|
|
88a8571b93 | ||
|
|
45a88e09af | ||
|
|
6aa1a1e54d | ||
|
|
18cc3160b5 | ||
|
|
123c8d2b81 | ||
|
|
b2f2f8528a | ||
|
|
0925089b5e | ||
|
|
c84d17b1bb | ||
|
|
cb338a2ba1 | ||
|
|
16f4f0d473 | ||
|
|
387a4e72f7 | ||
|
|
ac6d38e4b7 | ||
|
|
6df51d4ef5 | ||
|
|
46f695ac65 | ||
|
|
4af1d58c86 | ||
|
|
f71df88a6b | ||
|
|
18b178e63f | ||
|
|
1644b8743c | ||
|
|
53a2aaee35 | ||
|
|
5ae9bb4df9 | ||
|
|
ae2e8c1f00 | ||
|
|
602af1499e | ||
|
|
f4512426a1 | ||
|
|
a3458c669a | ||
|
|
609d88f029 | ||
|
|
3c78598217 | ||
|
|
b7bb0fa538 | ||
|
|
6de2151607 | ||
|
|
a99761d466 | ||
|
|
8d1c04bda4 | ||
|
|
aa57531aac | ||
|
|
006fe2a907 | ||
|
|
d94faf6d7e | ||
|
|
6c8879b832 | ||
|
|
94a6da3bc8 |
@@ -25,6 +25,10 @@ insert_final_newline = false
|
|||||||
[templates/user/auth/oidc_wellknown.tmpl]
|
[templates/user/auth/oidc_wellknown.tmpl]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
|
|
||||||
|
[templates/shared/actions/runner_badge_*.tmpl]
|
||||||
|
# editconfig lint requires these XML-like files to have charset defined, but the files don't have.
|
||||||
|
charset = unset
|
||||||
|
|
||||||
[Makefile]
|
[Makefile]
|
||||||
indent_style = tab
|
indent_style = tab
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/cron-licenses.yml
vendored
2
.github/workflows/cron-licenses.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
if: github.repository == 'go-gitea/gitea'
|
if: github.repository == 'go-gitea/gitea'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|||||||
14
.github/workflows/pull-compliance.yml
vendored
14
.github/workflows/pull-compliance.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -72,7 +72,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -84,7 +84,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -101,7 +101,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -116,7 +116,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -145,7 +145,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -190,7 +190,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|||||||
10
.github/workflows/pull-db-tests.yml
vendored
10
.github/workflows/pull-db-tests.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
|||||||
- "9000:9000"
|
- "9000:9000"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -67,7 +67,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -125,7 +125,7 @@ jobs:
|
|||||||
- 10000:10000
|
- 10000:10000
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -178,7 +178,7 @@ jobs:
|
|||||||
- "993:993"
|
- "993:993"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -218,7 +218,7 @@ jobs:
|
|||||||
- 10000:10000
|
- 10000:10000
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|||||||
2
.github/workflows/pull-e2e-tests.yml
vendored
2
.github/workflows/pull-e2e-tests.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|||||||
6
.github/workflows/release-nightly.yml
vendored
6
.github/workflows/release-nightly.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||||
- run: git fetch --unshallow --quiet --tags --force
|
- run: git fetch --unshallow --quiet --tags --force
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -65,7 +65,7 @@ jobs:
|
|||||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||||
- run: git fetch --unshallow --quiet --tags --force
|
- run: git fetch --unshallow --quiet --tags --force
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -107,7 +107,7 @@ jobs:
|
|||||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||||
- run: git fetch --unshallow --quiet --tags --force
|
- run: git fetch --unshallow --quiet --tags --force
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|||||||
2
.github/workflows/release-tag-rc.yml
vendored
2
.github/workflows/release-tag-rc.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||||
- run: git fetch --unshallow --quiet --tags --force
|
- run: git fetch --unshallow --quiet --tags --force
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|||||||
2
.github/workflows/release-tag-version.yml
vendored
2
.github/workflows/release-tag-version.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||||
- run: git fetch --unshallow --quiet --tags --force
|
- run: git fetch --unshallow --quiet --tags --force
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|||||||
509
CHANGELOG.md
509
CHANGELOG.md
@@ -4,7 +4,500 @@ This changelog goes through the changes that have been made in each release
|
|||||||
without substantial changes to our git log; to see the highlights of what has
|
without substantial changes to our git log; to see the highlights of what has
|
||||||
been added to each release, please refer to the [blog](https://blog.gitea.com).
|
been added to each release, please refer to the [blog](https://blog.gitea.com).
|
||||||
|
|
||||||
## [1.24.0](https://github.com/go-gitea/gitea/releases/tag/1.24.0) - 2025-05-26
|
## [1.25.5](https://github.com/go-gitea/gitea/releases/tag/1.25.5) - 2026-03-10
|
||||||
|
|
||||||
|
* SECURITY
|
||||||
|
* Toolchain Update to Go 1.25.6 (#36480) (#36487)
|
||||||
|
* Adjust the toolchain version (#36537) (#36542)
|
||||||
|
* Update toolchain to 1.25.8 for v1.25 (#36888)
|
||||||
|
* Prevent redirect bypasses via backslash-encoded paths (#36660) (#36716)
|
||||||
|
* Fix get release draft permission check (#36659) (#36715)
|
||||||
|
* Fix a bug user could change another user's primary email (#36586) (#36607)
|
||||||
|
* Fix OAuth2 authorization code expiry and reuse handling (#36797) (#36851)
|
||||||
|
* Add validation constraints for repository creation fields (#36671) (#36757)
|
||||||
|
* Fix bug to check whether user can update pull request branch or rebase branch (#36465) (#36838)
|
||||||
|
* Add migration http transport for push/sync mirror lfs (#36665) (#36691)
|
||||||
|
* Fix track time list permission check (#36662) (#36744)
|
||||||
|
* Fix track time issue id (#36664) (#36689)
|
||||||
|
* Fix path resolving (#36734) (#36746)
|
||||||
|
* Fix dump release asset bug (#36799) (#36839)
|
||||||
|
* Fix org permission API visibility checks for hidden members and private orgs (#36798) (#36841)
|
||||||
|
* Fix forwarded proto handling for public URL detection (#36810) (#36836)
|
||||||
|
* Add a git grep search timeout (#36809) (#36835)
|
||||||
|
* Fix oauth2 s256 (#36462) (#36477)
|
||||||
|
* ENHANCEMENTS
|
||||||
|
* Make `security-check` informational only (#36681) (#36852)
|
||||||
|
* Upgrade to github.com/cloudflare/circl 1.6.3, svgo 4.0.1, markdownlint-cli 0.48.0 (#36840)
|
||||||
|
* Add some validation on values provided to USER_DISABLED_FEATURES and EXTERNAL_USER_DISABLED_FEATURES (#36688) (#36692)
|
||||||
|
* Upgrade gogit to 5.16.5 (#36687)
|
||||||
|
* Add wrap to runner label list (#36565) (#36574)
|
||||||
|
* Add dnf5 command for Fedora in RPM package instructions (#36527) (#36572)
|
||||||
|
* Allow scroll propagation outside code editor (#36502) (#36510)
|
||||||
|
* BUGFIXES
|
||||||
|
* Fix non-admins unable to automerge PRs from forks (#36833) (#36843)
|
||||||
|
* Fix bug when pushing mirror with wiki (#36795) (#36807)
|
||||||
|
* Fix artifacts v4 backend upload problems (#36805) (#36834)
|
||||||
|
* Fix CRAN package version validation to allow more than 4 version components (#36813) (#36821)
|
||||||
|
* Fix force push time-line commit comments of pull request (#36653) (#36717)
|
||||||
|
* Fix SVG height calculation in diff viewer (#36748) (#36750)
|
||||||
|
* Fix push time bug (#36693) (#36713)
|
||||||
|
* Fix bug the protected branch rule name is conflicted with renamed branch name (#36650) (#36661)
|
||||||
|
* Fix bug when do LFS GC (#36500) (#36608)
|
||||||
|
* Fix focus lost bugs in the Monaco editor (#36609)
|
||||||
|
* Reprocess htmx content after loading more files (#36568) (#36577)
|
||||||
|
* Fix assignee sidebar links and empty placeholder (#36559) (#36563)
|
||||||
|
* Fix issues filter dropdown showing empty label scope section (#36535) (#36544)
|
||||||
|
* Fix various mermaid bugs (#36547) (#36552)
|
||||||
|
* Fix data race when uploading container blobs concurrently (#36524) (#36526)
|
||||||
|
* Correct spacing between username and bot label (#36473) (#36484)
|
||||||
|
|
||||||
|
## [1.25.4](https://github.com/go-gitea/gitea/releases/tag/1.25.4) - 2026-01-15
|
||||||
|
|
||||||
|
* SECURITY
|
||||||
|
* Release attachments must belong to the intended repo (#36347) (#36375)
|
||||||
|
* Fix permission check on org project operations (#36318) (#36373)
|
||||||
|
* Clean watches when make a repository private and check permission when send release emails (#36319) (#36370)
|
||||||
|
* Add more check for stopwatch read or list (#36340) (#36368)
|
||||||
|
* Fix openid setting check (#36346) (#36361)
|
||||||
|
* Fix cancel auto merge bug (#36341) (#36356)
|
||||||
|
* Fix delete attachment check (#36320) (#36355)
|
||||||
|
* LFS locks must belong to the intended repo (#36344) (#36349)
|
||||||
|
* Fix bug on notification read (#36339) #36387
|
||||||
|
* ENHANCEMENTS
|
||||||
|
* Add more routes to the "expensive" list (#36290)
|
||||||
|
* Make "commit statuses" API accept slashes in "ref" (#36264) (#36275)
|
||||||
|
* BUGFIXES
|
||||||
|
* Fix git http service handling #36396
|
||||||
|
* Fix markdown newline handling during IME composition (#36421) #36424
|
||||||
|
* Fix missing repository id when migrating release attachments (#36389)
|
||||||
|
* Fix bug when compare in the pull request (#36363) (#36372)
|
||||||
|
* Fix incorrect text content detection (#36364) (#36369)
|
||||||
|
* Fill missing `has_code` in repository api (#36338) (#36359)
|
||||||
|
* Fix notifications pagination query parameters (#36351) (#36358)
|
||||||
|
* Fix some trivial problems (#36336) (#36337)
|
||||||
|
* Prevent panic when GitLab release has more links than sources (#36295) (#36305)
|
||||||
|
* Fix stats bug when syncing release (#36285) (#36294)
|
||||||
|
* Always honor user's choice for "delete branch after merge" (#36281) (#36286)
|
||||||
|
* Use the requested host for LFS links (#36242) (#36258)
|
||||||
|
* Fix panic when get editor config file (#36241) (#36247)
|
||||||
|
* Fix regression in writing authorized principals (#36213) (#36218)
|
||||||
|
* Fix WebAuthn error checking (#36219) (#36235)
|
||||||
|
|
||||||
|
## [1.25.3](https://github.com/go-gitea/gitea/releases/tag/1.25.3) - 2025-12-17
|
||||||
|
|
||||||
|
* SECURITY
|
||||||
|
* Bump toolchain to go1.25.5, misc fixes (#36082)
|
||||||
|
* ENHANCEMENTS
|
||||||
|
* Add strikethrough button to markdown editor (#36087) (#36104)
|
||||||
|
* Add "site admin" back to profile menu (#36010) (#36013)
|
||||||
|
* Improve math rendering (#36124) (#36125)
|
||||||
|
* BUGFIXES
|
||||||
|
* Check user visibility when redirecting to a renamed user (#36148) (#36159)
|
||||||
|
* Fix various bugs (#36139) (#36151)
|
||||||
|
* Fix bug when viewing the commit diff page with non-ANSI files (#36149) (#36150)
|
||||||
|
* Hide RSS icon when viewing a file not under a branch (#36135) (#36141)
|
||||||
|
* Fix SVG size calulation, only use `style` attribute (#36133) (#36134)
|
||||||
|
* Make Golang correctly delete temp files during uploading (#36128) (#36129)
|
||||||
|
* Fix the bug when ssh clone with redirect user or repository (#36039) (#36090)
|
||||||
|
* Use Golang net/smtp instead of gomail's smtp to send email (#36055) (#36083)
|
||||||
|
* Fix edit user email bug in API (#36068) (#36081)
|
||||||
|
* Fix bug when updating user email (#36058) (#36066)
|
||||||
|
* Fix incorrect viewed files counter if file has changed (#36009) (#36047)
|
||||||
|
* Fix container registry error handling (#36021) (#36037)
|
||||||
|
* Fix webAuthn insecure error view (#36165) (#36179)
|
||||||
|
* Fix some file icon ui (#36078) (#36088)
|
||||||
|
* Fix Actions `pull_request.paths` being triggered incorrectly by rebase (#36045) (#36054)
|
||||||
|
* Fix error handling in mailer and wiki services (#36041) (#36053)
|
||||||
|
* Fix bugs when comparing and creating pull request (#36166) (#36144)
|
||||||
|
|
||||||
|
## [1.25.2](https://github.com/go-gitea/gitea/releases/tag/1.25.2) - 2025-11-23
|
||||||
|
|
||||||
|
* SECURITY
|
||||||
|
* Upgrade golang.org/x/crypto to 0.45.0 (#35985) (#35988)
|
||||||
|
* Fix various permission & login related bugs (#36002) (#36004)
|
||||||
|
* ENHANCEMENTS
|
||||||
|
* Display source code downloads last for release attachments (#35897) (#35903)
|
||||||
|
* Change project default column icon to 'star' (#35967) (#35979)
|
||||||
|
* BUGFIXES
|
||||||
|
* Allow empty commit when merging pull request with squash style (#35989) (#36003)
|
||||||
|
* Fix container push tag overwriting (#35936) (#35954)
|
||||||
|
* Fix corrupted external render content (#35946) and upgrade golang.org/x packages (#35950)
|
||||||
|
* Limit reading bytes instead of ReadAll (#35928) (#35934)
|
||||||
|
* Use correct form field for allowed force push users in branch protection API (#35894) (#35908)
|
||||||
|
* Fix team member access check (#35899) (#35905)
|
||||||
|
* Fix conda null depend issue (#35900) (#35902)
|
||||||
|
* Set the dates to now when not specified by the caller (#35861) (#35874)
|
||||||
|
* Fix gogit ListEntriesRecursiveWithSize (#35862)
|
||||||
|
* Misc CSS fixes (#35888) (#35981)
|
||||||
|
* Don't show unnecessary error message to end users for DeleteBranchAfterMerge (#35937) (#35941)
|
||||||
|
* Load jQuery as early as possible to support custom scripts (#35926) (#35929)
|
||||||
|
* Allow to display embed images/pdfs when SERVE_DIRECT was enabled on MinIO storage (#35882) (#35917)
|
||||||
|
* Make OAuth2 issuer configurable (#35915) (#35916)
|
||||||
|
* Fix #35763: Add proper page title for project pages (#35773) (#35909)
|
||||||
|
* Fix avatar upload error handling (#35887) (#35890)
|
||||||
|
* Contribution heatmap improvements (#35876) (#35880)
|
||||||
|
* Remove padding override on `.ui .sha.label` (#35864) (#35873)
|
||||||
|
* Fix pull description code label background (#35865) (#35870)
|
||||||
|
|
||||||
|
## [1.25.1](https://github.com/go-gitea/gitea/releases/tag/v1.25.1) - 2025-11-03
|
||||||
|
|
||||||
|
* BUGFIXES
|
||||||
|
* Make ACME email optional (#35849) #35857
|
||||||
|
* Add a doctor command to fix inconsistent run status (#35840) (#35845)
|
||||||
|
* Remove wrong code (#35846)
|
||||||
|
* Fix viewed files number is not right if not all files loaded (#35821) (#35844)
|
||||||
|
* Fix incorrect pull request counter (#35819) (#35841)
|
||||||
|
* Upgrade go mail to 0.7.2 and fix the bug (#35833) (#35837)
|
||||||
|
* Revert gomail to v0.7.0 to fix sending mail failed (#35816) (#35824)
|
||||||
|
* Fix clone mixed bug (#35810) (#35822)
|
||||||
|
* Fix cli "Before" handling (#35797) (#35808)
|
||||||
|
* Improve and fix markup code preview rendering (#35777) (#35787)
|
||||||
|
* Fix actions rerun bug (#35783) (#35784)
|
||||||
|
* Fix actions schedule update issue (#35767) (#35774)
|
||||||
|
* Fix circular spin animation direction (#35785) (#35823)
|
||||||
|
* Fix file extension on gogs.png (#35793) (#35799)
|
||||||
|
* Add pnpm to Snapcraft (#35778)
|
||||||
|
|
||||||
|
## [1.25.0](https://github.com/go-gitea/gitea/releases/tag/v1.25.0) - 2025-10-30
|
||||||
|
|
||||||
|
* BREAKING
|
||||||
|
* Return 201 Created for CreateVariable API responses (#34517)
|
||||||
|
* Add label 'state' to metric 'gitea_users' (#34326)
|
||||||
|
* SECURITY
|
||||||
|
* Upgrade security public key (#34956)
|
||||||
|
* Also include all security fixes in 1.24.x after 1.25.0-rc0
|
||||||
|
* FEATURES
|
||||||
|
* Stream repo zip/tar.gz/bundle achives by default (#35487)
|
||||||
|
* Use configurable remote name for git commands (#35172)
|
||||||
|
* Send email on Workflow Run Success/Failure (#34982)
|
||||||
|
* Refactor OpenIDConnect to support SSH/FullName sync (#34978)
|
||||||
|
* Refactor repo contents API and add "contents-ext" API (#34822)
|
||||||
|
* Add support for 3D/CAD file formats preview (#34794)
|
||||||
|
* Improve instance wide ssh commit signing (#34341)
|
||||||
|
* Edit file workflow for creating a fork and proposing changes (#34240)
|
||||||
|
* Follow file symlinks in the UI to their target (#28835)
|
||||||
|
* Allow renaming/moving binary/LFS files in the UI (#34350)
|
||||||
|
* PERFORMANCE
|
||||||
|
* Improve the performance when detecting the file editable (#34653)
|
||||||
|
* ENHANCEMENTS
|
||||||
|
* Enable more markdown paste features in textarea editor (#35494)
|
||||||
|
* Don't store repo archives on `gitea dump` (#35467)
|
||||||
|
* Always return the relevant status information, even if no status exists. (#35335)
|
||||||
|
* Add start time on perf trace because it seems some steps haven't been recorded. (#35282)
|
||||||
|
* Remove deprecated auth sources (#35272)
|
||||||
|
* When sorting issues by nearest due date, issues without due date should be sorted ascending (#35267)
|
||||||
|
* Disable field count validation of CSV viewer (#35228)
|
||||||
|
* Add `has_code` to repository REST API (#35214)
|
||||||
|
* Display pull request in merged commit view (#35202)
|
||||||
|
* Support Basic Authentication for archive downloads (#35087)
|
||||||
|
* Add hover background to table rows in user and repo admin page (#35072)
|
||||||
|
* Partially refresh notifications list (#35010)
|
||||||
|
* Also display "recently pushed branch" alert on PR view (#35001)
|
||||||
|
* Refactor time tracker UI (#34983)
|
||||||
|
* Improve CLI commands (#34973)
|
||||||
|
* Improve project & label color picker and image scroll (#34971)
|
||||||
|
* Improve NuGet API Parity (#21291) (#34940)
|
||||||
|
* Support getting last commit message using contents-ext API (#34904)
|
||||||
|
* Adds title on branch commit counts (#34869)
|
||||||
|
* Add "Cancel workflow run" button to Actions list page (#34817)
|
||||||
|
* Improve img lazy loading (#34804)
|
||||||
|
* Forks repository list page follow other repositories page (#34784)
|
||||||
|
* Add ff_only parameter to POST /repos/{owner}/{repo}/merge-upstream (#34770)
|
||||||
|
* Rework delete org and rename org UI (#34762)
|
||||||
|
* Improve nuget/rubygems package registries (#34741)
|
||||||
|
* Add repo file tree item link behavior (#34730)
|
||||||
|
* Add issue delete notifier (#34592)
|
||||||
|
* Improve Actions list (#34530)
|
||||||
|
* Add a default tab on repo header when migrating (#34503)
|
||||||
|
* Add post-installation redirect based on admin account status (#34493)
|
||||||
|
* Trigger 'unlabeled' event when label is Deleted from PR (#34316)
|
||||||
|
* Support annotated tags when using create release API (#31840)
|
||||||
|
* Use lfs label for lfs file rather than a long description (#34363)
|
||||||
|
* Add "View workflow file" to Actions list page (#34538)
|
||||||
|
* Move organization's visibility change to danger zone. (#34814)
|
||||||
|
* Don't block site admin's operation if SECRET_KEY is lost (#35721)
|
||||||
|
* Make restricted users can access public repositories (#35693)
|
||||||
|
* The status icon of the Action step is consistent with GitHub (#35618) #35621
|
||||||
|
* BUGFIXES
|
||||||
|
* Update tab title when navigating file tree (#35757) #35772
|
||||||
|
* Fix "ref-issue" handling in markup (#35739) #35771
|
||||||
|
* Fix webhook to prevent tag events from bypassing branch filters targets (#35567) #35577
|
||||||
|
* Fix markup init after issue comment editing (#35536) #35537
|
||||||
|
* Fix creating pull request failure when the target branch name is the same as some tag (#35552) #35582
|
||||||
|
* Fix auto-expand and auto-scroll for actions logs (#35570) (#35583) #35586
|
||||||
|
* Use inputs context when parsing workflows (#35590) #35595
|
||||||
|
* Fix diffpatch API endpoint (#35610) #35613
|
||||||
|
* Creating push comments before invoke pull request checking (#35647) #35668
|
||||||
|
* Fix missing Close when error occurs and abused connection pool (#35658) #35670
|
||||||
|
* Fix build (#35674)
|
||||||
|
* Use LFS object size instead of blob size when viewing a LFS file (#35679)
|
||||||
|
* Fix workflow run event status while rerunning a failed job (#35689)
|
||||||
|
* Avoid emoji mismatch and allow to only enable chosen emojis (#35692)
|
||||||
|
* Refactor legacy code, fix LFS auth bypass, fix symlink bypass (#35708)
|
||||||
|
* Fix various trivial problems (#35714)
|
||||||
|
* Fix attachment file size limit in server backend (#35519)
|
||||||
|
* Honor delete branch on merge repo setting when using merge API (#35488)
|
||||||
|
* Fix external render, make iframe render work (#35727, #35730)
|
||||||
|
* Upgrade go mail to 0.7.2 (#35748)
|
||||||
|
* Revert #18491, fix oauth2 client link account (#35745)
|
||||||
|
* Fix different behavior in status check pattern matching with double stars (#35474)
|
||||||
|
* Fix overflow in notifications list (#35446)
|
||||||
|
* Fix package link setting can only list limited repositories (#35394)
|
||||||
|
* Extend comment treepath length (#35389)
|
||||||
|
* Fix font-size in inline code comment preview (#35209)
|
||||||
|
* Move git config/remote to gitrepo package and add global lock to resolve possible conflict when updating repository git config file (#35151)
|
||||||
|
* Change some columns from text to longtext and fix column wrong type caused by xorm (#35141)
|
||||||
|
* Redirect to a presigned URL of HEAD for HEAD requests (#35088)
|
||||||
|
* Fix git commit committer parsing and add some tests (#35007)
|
||||||
|
* Fix OCI manifest parser (#34797)
|
||||||
|
* Refactor FindOrgOptions to use enum instead of bool, fix membership visibility (#34629)
|
||||||
|
* Fix notification count positioning for variable-width elements (#34597)
|
||||||
|
* Keeping consistent between UI and API about combined commit status state and fix some bugs (#34562)
|
||||||
|
* Fix possible panic (#34508)
|
||||||
|
* Fix autofocus behavior (#34397)
|
||||||
|
* Fix Actions API (#35204)
|
||||||
|
* Fix ListWorkflowRuns OpenAPI response model. (#35026)
|
||||||
|
* Small fix in Pull Requests page (#34612)
|
||||||
|
* Fix http auth header parsing (#34936)
|
||||||
|
* Fix modal + form abuse (#34921)
|
||||||
|
* Fix PR toggle WIP (#34920)
|
||||||
|
* Fix log fmt (#34810)
|
||||||
|
* Replace stopwatch toggle with explicit start/stop actions (#34818)
|
||||||
|
* Fix some package registry problems (#34759)
|
||||||
|
* Fix RPM package download routing & missing package version count (#34909)
|
||||||
|
* Fix repo search input height (#34330)
|
||||||
|
* Fix "The sidebar of the repository file list does not have a fixed height #34298" (#34321)
|
||||||
|
* Fix minor typos in two files #HSFDPMUW (#34944)
|
||||||
|
* Fix actions skipped commit status indicator (#34507)
|
||||||
|
* Fix job status aggregation logic (#35000)
|
||||||
|
* Fix broken OneDev migration caused by various REST API changes in OneDev 7.8.0 and later (#35216)
|
||||||
|
* Fix typo in oauth2_full_name_claim_name string (#35199)
|
||||||
|
* Fix typo in locale_en-US.ini (#35196)
|
||||||
|
* API
|
||||||
|
* Exposing TimeEstimate field in the API (#35475)
|
||||||
|
* UpdateBranch API supports renaming a branch (#35374)
|
||||||
|
* Add `owner` and `parent` fields clarification to docs (#35023)
|
||||||
|
* Improve OAuth2 provider (correct Issuer, respect ENABLED) (#34966)
|
||||||
|
* Add a `login`/`login-name`/`username` disambiguation to affected endpoint parameters and response/request models (#34901)
|
||||||
|
* Do not mutate incoming options to SearchRepositoryByName (#34553)
|
||||||
|
* Do not mutate incoming options to RenderUserSearch and SearchUsers (#34544)
|
||||||
|
* Export repo's manual merge settings (#34502)
|
||||||
|
* Add date range filtering to commit retrieval endpoints (#34497)
|
||||||
|
* Add endpoint deleting workflow run (#34337)
|
||||||
|
* Add workflow_run api + webhook (#33964)
|
||||||
|
* REFACTOR
|
||||||
|
* Move updateref and removeref to gitrepo and remove unnecessary open repository (#35511)
|
||||||
|
* Remove unused param `doer` (#34545)
|
||||||
|
* Split GetLatestCommitStatus as two functions (#34535)
|
||||||
|
* Use gitrepo.SetDefaultBranch when set default branch of wiki repository (#33911)
|
||||||
|
* Refactor editor (#34780)
|
||||||
|
* Refactor packages (#34777)
|
||||||
|
* Refactor container package (#34877)
|
||||||
|
* Refactor "change file" API (#34855)
|
||||||
|
* Rename pull request GetGitRefName to GetGitHeadRefName to prepare introducing GetGitMergeRefName (#35093)
|
||||||
|
* Move git command to git/gitcmd (#35483)
|
||||||
|
* Use db.WithTx/WithTx2 instead of TxContext when possible (#35428)
|
||||||
|
* Support Node.js 22.6 with type stripping (#35427)
|
||||||
|
* Migrate tools and configs to typescript, require node.js >= 22.18.0 (#35421)
|
||||||
|
* Check user and repo for redirects when using git via SSH transport (#35416)
|
||||||
|
* Remove the duplicated function GetTags (#35375)
|
||||||
|
* Refactor to use reflect.TypeFor (#35370)
|
||||||
|
* Deleting branch could delete broken branch which has database record but git branch is missing (#35360)
|
||||||
|
* Exit with success when already up to date (#35312)
|
||||||
|
* Split admin config settings templates to make it maintain easier (#35294)
|
||||||
|
* A small refactor to use context in the service layer (#35179)
|
||||||
|
* Refactor and update mail templates (#35150)
|
||||||
|
* Use db.WithTx/WithTx2 instead of TxContext when possible (#35130)
|
||||||
|
* Align `issue-title-buttons` with `list-header` (#35018)
|
||||||
|
* Add Notifications section in User Settings (#35008)
|
||||||
|
* Tweak placement of diff file menu (#34999)
|
||||||
|
* Refactor mail template and support preview (#34990)
|
||||||
|
* Rerun job only when run is done (#34970)
|
||||||
|
* Merge index.js (#34963)
|
||||||
|
* Refactor "delete-button" to "link-action" (#34962)
|
||||||
|
* Refactor webhook and fix feishu/lark secret (#34961)
|
||||||
|
* Exclude devtest.ts from tailwindcss (#34935)
|
||||||
|
* Refactor head navbar icons (#34922)
|
||||||
|
* Improve html escape (#34911)
|
||||||
|
* Improve tags list page (#34898)
|
||||||
|
* Improve `labels-list` rendering (#34846)
|
||||||
|
* Remove unused variable HUGO_VERSION (#34840)
|
||||||
|
* Correct migration tab name (#34826)
|
||||||
|
* Refactor template helper (#34819)
|
||||||
|
* Use `shallowRef` instead of `ref` in `.vue` files where possible (#34813)
|
||||||
|
* Use standalone function to update repository cols (#34811)
|
||||||
|
* Refactor wiki (#34805)
|
||||||
|
* Remove unnecessary duplicate code (#34733)
|
||||||
|
* Refactor embedded assets and drop unnecessary dependencies (#34692)
|
||||||
|
* Update x/crypto package and make builtin SSH use default parameters (#34667)
|
||||||
|
* Add `--color-logo`, matching the logo's primary color (#34639)
|
||||||
|
* Add openssh-keygen to rootless image (#34625)
|
||||||
|
* Replace update repository function in some places (#34566)
|
||||||
|
* Change "rejected" to "changes requested" in 3rd party PR review notification (#34481)
|
||||||
|
* Remove legacy template helper functions (#34426)
|
||||||
|
* Use run-name and evaluate workflow variables (#34301)
|
||||||
|
* Move HasWiki to repository service package (#33912)
|
||||||
|
* Move some functions from package git to gitrepo (#33910)
|
||||||
|
* TESTING
|
||||||
|
* Add webhook test for push event (#34442)
|
||||||
|
* Add a webhook push test for dev branch (#34421)
|
||||||
|
* Add migrations tests (#34456) (#34498)
|
||||||
|
* STYLE
|
||||||
|
* Enforce explanation for necessary nolints and fix bugs (#34883)
|
||||||
|
* Fix remaining issues after `gopls modernize` formatting (#34771)
|
||||||
|
* Update gofumpt, add go.mod ignore directive (#35434)
|
||||||
|
* Enforce nolint scope (#34851)
|
||||||
|
* Enable gocritic `equalFold` and fix issues (#34952)
|
||||||
|
* Run `gopls modernize` on codebase (#34751)
|
||||||
|
* Upgrade `gopls` to v0.19.0, add `make fix` (#34772)
|
||||||
|
* BUILD
|
||||||
|
* bump archives&rar dep (#35637) #35638
|
||||||
|
* Use github.com/mholt/archives replace github.com/mholt/archiver (#35390)
|
||||||
|
* Update JS and PY dependencies (#35444)
|
||||||
|
* Upgrade devcontainer go version to 1.24.6 (#35298)
|
||||||
|
* Upgrade golang to 1.25.1 and add descriptions for the swagger structs' fields (#35418)
|
||||||
|
* Update JS and PY deps (#35191)
|
||||||
|
* Update JS and PY dependencies (#34391)
|
||||||
|
* Update go tool dependencies (#34845)
|
||||||
|
* Update `uint8-to-base64`, remove type stub (#34844)
|
||||||
|
* Switch to `@resvg/resvg-wasm` for `generate-images` (#35415)
|
||||||
|
* Switch to pnpm (#35274)
|
||||||
|
* Update chroma to v2.20.0 (#35220)
|
||||||
|
* Migrate to urfave v3 (#34510)
|
||||||
|
* Update JS deps, regenerate SVGs (#34640)
|
||||||
|
* Upgrade dependencies (#35384)
|
||||||
|
* Bump `@github/relative-time-element` to v4.4.8 (#34413)
|
||||||
|
* Update JS dependencies (#34951)
|
||||||
|
* Upgrade orgmode to v1.8.0 (#34721)
|
||||||
|
* Raise minimum Node.js version to 20, test on 24 (#34713)
|
||||||
|
* Update JS deps (#34701)
|
||||||
|
* Upgrade htmx to 2.0.6 (#34887)
|
||||||
|
* Update eslint to v9 (#35485)
|
||||||
|
* Update js dependencies (#35429)
|
||||||
|
* Clean up npm dependencies (#35508)
|
||||||
|
* Clean up npm dependencies (#35484)
|
||||||
|
* Bump setup-node to v5 (#35448)
|
||||||
|
* MISC
|
||||||
|
* Add gitignore rules to exclude LLM instruction files (#35076)
|
||||||
|
* Gitignore: Visual Studio settings folder (#34375)
|
||||||
|
* Improve language in en-US locale strings (#35124)
|
||||||
|
* Fixed all grammatical errors in locale_en-US.ini (#35053)
|
||||||
|
* Docs/fix typo and grammar in CONTRIBUTING.md (#35024)
|
||||||
|
* Improve english grammar and readability in locale_en-US.ini (#35017)
|
||||||
|
|
||||||
|
## [1.24.7](https://github.com/go-gitea/gitea/releases/tag/v1.24.7) - 2025-10-24
|
||||||
|
|
||||||
|
* SECURITY
|
||||||
|
* Refactor legacy code (#35708) (#35713)
|
||||||
|
* Fixing issue #35530: Password Leak in Log Messages (#35584) (#35665)
|
||||||
|
* Fix a bug missed return (#35655) (#35671)
|
||||||
|
* BUGFIXES
|
||||||
|
* Fix inputing review comment will remove reviewer (#35591) (#35664)
|
||||||
|
* TESTING
|
||||||
|
* Mock external service in hcaptcha TestCaptcha (#35604) (#35663)
|
||||||
|
* Fix build (#35669)
|
||||||
|
|
||||||
|
## [1.24.6](https://github.com/go-gitea/gitea/releases/tag/v1.24.6) - 2025-09-10
|
||||||
|
|
||||||
|
* SECURITY
|
||||||
|
* Upgrade xz to v0.5.15 (#35385)
|
||||||
|
* BUGFIXES
|
||||||
|
* Fix a compare page 404 bug when the pull request disabled (#35441) (#35453)
|
||||||
|
* Fix bug when issue disabled, pull request number in the commit message cannot be redirected (#35420) (#35442)
|
||||||
|
* Add author.name field to Swift Package Registry API response (#35410) (#35431)
|
||||||
|
* Remove usernames when empty in discord webhook (#35412) (#35417)
|
||||||
|
* Allow foreachref parser to grow its buffer (#35365) (#35376)
|
||||||
|
* Allow deleting comment with content via API like web did (#35346) (#35354)
|
||||||
|
* Fix atom/rss mixed error (#35345) (#35347)
|
||||||
|
* Fix review request webhook bug (#35339)
|
||||||
|
* Remove duplicate html IDs (#35210) (#35325)
|
||||||
|
* Fix LFS range size header response (#35277) (#35293)
|
||||||
|
* Fix GitHub release assets URL validation (#35287) (#35290)
|
||||||
|
* Fix token lifetime, closes #35230 (#35271) (#35281)
|
||||||
|
* Fix push commits comments when changing the pull request target branch (#35386) (#35443)
|
||||||
|
|
||||||
|
## [1.24.5](https://github.com/go-gitea/gitea/releases/tag/v1.24.5) - 2025-08-12
|
||||||
|
|
||||||
|
* BUGFIXES
|
||||||
|
* Fix a bug where lfs gc never worked. (#35198) (#35255)
|
||||||
|
* Reload issue when sending webhook to make num comments is right. (#35243) (#35248)
|
||||||
|
* Fix bug when review pull request commits (#35192) (#35246)
|
||||||
|
* MISC
|
||||||
|
* Vertically center "Show Resolved" (#35211) (#35218)
|
||||||
|
|
||||||
|
## [1.24.4](https://github.com/go-gitea/gitea/releases/tag/v1.24.4) - 2025-08-03
|
||||||
|
|
||||||
|
* BUGFIXES
|
||||||
|
* Fix various bugs (1.24) (#35186)
|
||||||
|
* Fix migrate input box bug (#35166) (#35171)
|
||||||
|
* Only hide dropzone when no files have been uploaded (#35156) (#35167)
|
||||||
|
* Fix review comment/dimiss comment x reference can be refereced back (#35094) (#35099)
|
||||||
|
* Fix submodule nil check (#35096) (#35098)
|
||||||
|
* MISC
|
||||||
|
* Don't use full-file highlight when there is a git diff textconv (#35114) (#35119)
|
||||||
|
* Increase gap on latest commit (#35104) (#35113)
|
||||||
|
|
||||||
|
## [1.24.3](https://github.com/go-gitea/gitea/releases/tag/v1.24.3) - 2025-07-15
|
||||||
|
|
||||||
|
* BUGFIXES
|
||||||
|
* Fix form property assignment edge case (#35073) (#35078)
|
||||||
|
* Improve submodule relative path handling (#35056) (#35075)
|
||||||
|
* Fix incorrect comment diff hunk parsing, fix github asset ID nil panic (#35046) (#35055)
|
||||||
|
* Fix updating user visibility (#35036) (#35044)
|
||||||
|
* Support base64-encoded agit push options (#35037) (#35041)
|
||||||
|
* Make submodule link work with relative path (#35034) (#35038)
|
||||||
|
* Fix bug when displaying git user avatar in commits list (#35006)
|
||||||
|
* Fix API response for swagger spec (#35029)
|
||||||
|
* Start automerge check again after the conflict check and the schedule (#34988) (#35002)
|
||||||
|
* Fix the response format for actions/workflows (#35009) (#35016)
|
||||||
|
* Fix repo settings and protocol log problems (#35012) (#35013)
|
||||||
|
* Fix project images scroll (#34971) (#34972)
|
||||||
|
* Mark old reviews as stale on agit pr updates (#34933) (#34965)
|
||||||
|
* Fix git graph page (#34948) (#34949)
|
||||||
|
* Don't send trigger for a pending review's comment create/update/delete (#34928) (#34939)
|
||||||
|
* Fix some log and UI problems (#34863) (#34868)
|
||||||
|
* Fix archive API (#34853) (#34857)
|
||||||
|
* Ignore force pushes for changed files in a PR review (#34837) (#34843)
|
||||||
|
* Fix SSH LFS timeout (#34838) (#34842)
|
||||||
|
* Fix team permissions (#34827) (#34836)
|
||||||
|
* Fix job status aggregation logic (#34823) (#34835)
|
||||||
|
* Fix issue filter (#34914) (#34915)
|
||||||
|
* Fix typo in pull request merge warning message text (#34899) (#34903)
|
||||||
|
* Support the open-icon of folder (#34168) (#34896)
|
||||||
|
* Optimize flex layout of release attachment area (#34885) (#34886)
|
||||||
|
* Fix the issue of abnormal interface when there is no issue-item on the project page (#34791) (#34880)
|
||||||
|
* Skip updating timestamp when sync branch (#34875)
|
||||||
|
* Fix required contexts and commit status matching bug (#34815) (#34829)
|
||||||
|
|
||||||
|
## [1.24.2](https://github.com/go-gitea/gitea/releases/tag/v1.24.2) - 2025-06-20
|
||||||
|
|
||||||
|
* BUGFIXES
|
||||||
|
* Fix container range bug (#34795) (#34796)
|
||||||
|
* Upgrade chi to v5.2.2 (#34798) (#34799)
|
||||||
|
* BUILD
|
||||||
|
* Bump poetry feature to new url for dev container (#34787) (#34790)
|
||||||
|
|
||||||
|
## [1.24.1](https://github.com/go-gitea/gitea/releases/tag/v1.24.1) - 2025-06-18
|
||||||
|
|
||||||
|
* ENHANCEMENTS
|
||||||
|
* Improve alignment of commit status icon on commit page (#34750) (#34757)
|
||||||
|
* Support title and body query parameters for new PRs (#34537) (#34752)
|
||||||
|
|
||||||
|
* BUGFIXES
|
||||||
|
* When using rules to delete packages, remove unclean bugs (#34632) (#34761)
|
||||||
|
* Fix ghost user in feeds when pushing in an actions, it should be gitea-actions (#34703) (#34756)
|
||||||
|
* Prevent double markdown link brackets when pasting URL (#34745) (#34748)
|
||||||
|
* Prevent duplicate form submissions when creating forks (#34714) (#34735)
|
||||||
|
* Fix markdown wrap (#34697) (#34702)
|
||||||
|
* Fix pull requests API convert panic when head repository is deleted. (#34685) (#34687)
|
||||||
|
* Fix commit message rendering and some UI problems (#34680) (#34683)
|
||||||
|
* Fix container range bug (#34725) (#34732)
|
||||||
|
* Fix incorrect cli default values (#34765) (#34766)
|
||||||
|
* Fix dropdown filter (#34708) (#34711)
|
||||||
|
* Hide href attribute of a tag if there is no target_url (#34556) (#34684)
|
||||||
|
* Fix tag target (#34781) #34783
|
||||||
|
|
||||||
|
## [1.24.0](https://github.com/go-gitea/gitea/releases/tag/v1.24.0) - 2025-05-26
|
||||||
|
|
||||||
* BREAKING
|
* BREAKING
|
||||||
* Make Gitea always use its internal config, ignore `/etc/gitconfig` (#33076)
|
* Make Gitea always use its internal config, ignore `/etc/gitconfig` (#33076)
|
||||||
@@ -374,7 +867,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
|
|||||||
* Bump x/net (#32896) (#32900)
|
* Bump x/net (#32896) (#32900)
|
||||||
* Only activity tab needs heatmap data loading (#34652)
|
* Only activity tab needs heatmap data loading (#34652)
|
||||||
|
|
||||||
## [1.23.8](https://github.com/go-gitea/gitea/releases/tag/1.23.8) - 2025-05-11
|
## [1.23.8](https://github.com/go-gitea/gitea/releases/tag/v1.23.8) - 2025-05-11
|
||||||
|
|
||||||
* SECURITY
|
* SECURITY
|
||||||
* Fix a bug when uploading file via lfs ssh command (#34408) (#34411)
|
* Fix a bug when uploading file via lfs ssh command (#34408) (#34411)
|
||||||
@@ -401,7 +894,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
|
|||||||
* Bump go version in go.mod (#34160)
|
* Bump go version in go.mod (#34160)
|
||||||
* remove hardcoded 'code' string in clone_panel.tmpl (#34153) (#34158)
|
* remove hardcoded 'code' string in clone_panel.tmpl (#34153) (#34158)
|
||||||
|
|
||||||
## [1.23.7](https://github.com/go-gitea/gitea/releases/tag/1.23.7) - 2025-04-07
|
## [1.23.7](https://github.com/go-gitea/gitea/releases/tag/v1.23.7) - 2025-04-07
|
||||||
|
|
||||||
* Enhancements
|
* Enhancements
|
||||||
* Add a config option to block "expensive" pages for anonymous users (#34024) (#34071)
|
* Add a config option to block "expensive" pages for anonymous users (#34024) (#34071)
|
||||||
@@ -499,7 +992,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
|
|||||||
* BUGFIXES
|
* BUGFIXES
|
||||||
* Fix a bug caused by status webhook template #33512
|
* Fix a bug caused by status webhook template #33512
|
||||||
|
|
||||||
## [1.23.2](https://github.com/go-gitea/gitea/releases/tag/1.23.2) - 2025-02-04
|
## [1.23.2](https://github.com/go-gitea/gitea/releases/tag/v1.23.2) - 2025-02-04
|
||||||
|
|
||||||
* BREAKING
|
* BREAKING
|
||||||
* Add tests for webhook and fix some webhook bugs (#33396) (#33442)
|
* Add tests for webhook and fix some webhook bugs (#33396) (#33442)
|
||||||
@@ -3029,7 +3522,7 @@ Key highlights of this release encompass significant changes categorized under `
|
|||||||
* Improve decryption failure message (#24573) (#24575)
|
* Improve decryption failure message (#24573) (#24575)
|
||||||
* Makefile: Use portable !, not GNUish -not, with find(1). (#24565) (#24572)
|
* Makefile: Use portable !, not GNUish -not, with find(1). (#24565) (#24572)
|
||||||
|
|
||||||
## [1.19.3](https://github.com/go-gitea/gitea/releases/tag/1.19.3) - 2023-05-03
|
## [1.19.3](https://github.com/go-gitea/gitea/releases/tag/v1.19.3) - 2023-05-03
|
||||||
|
|
||||||
* SECURITY
|
* SECURITY
|
||||||
* Use golang 1.20.4 to fix CVE-2023-24539, CVE-2023-24540, and CVE-2023-29400
|
* Use golang 1.20.4 to fix CVE-2023-24539, CVE-2023-24540, and CVE-2023-29400
|
||||||
@@ -3042,7 +3535,7 @@ Key highlights of this release encompass significant changes categorized under `
|
|||||||
* Fix incorrect CurrentUser check for docker rootless (#24435)
|
* Fix incorrect CurrentUser check for docker rootless (#24435)
|
||||||
* Getting the tag list does not require being signed in (#24413) (#24416)
|
* Getting the tag list does not require being signed in (#24413) (#24416)
|
||||||
|
|
||||||
## [1.19.2](https://github.com/go-gitea/gitea/releases/tag/1.19.2) - 2023-04-26
|
## [1.19.2](https://github.com/go-gitea/gitea/releases/tag/v1.19.2) - 2023-04-26
|
||||||
|
|
||||||
* SECURITY
|
* SECURITY
|
||||||
* Require repo scope for PATs for private repos and basic authentication (#24362) (#24364)
|
* Require repo scope for PATs for private repos and basic authentication (#24362) (#24364)
|
||||||
@@ -3541,7 +4034,7 @@ Key highlights of this release encompass significant changes categorized under `
|
|||||||
* Display attachments of review comment when comment content is blank (#23035) (#23046)
|
* Display attachments of review comment when comment content is blank (#23035) (#23046)
|
||||||
* Return empty url for submodule tree entries (#23043) (#23048)
|
* Return empty url for submodule tree entries (#23043) (#23048)
|
||||||
|
|
||||||
## [1.18.4](https://github.com/go-gitea/gitea/releases/tag/1.18.4) - 2023-02-20
|
## [1.18.4](https://github.com/go-gitea/gitea/releases/tag/v1.18.4) - 2023-02-20
|
||||||
|
|
||||||
* SECURITY
|
* SECURITY
|
||||||
* Provide the ability to set password hash algorithm parameters (#22942) (#22943)
|
* Provide the ability to set password hash algorithm parameters (#22942) (#22943)
|
||||||
@@ -3968,7 +4461,7 @@ Key highlights of this release encompass significant changes categorized under `
|
|||||||
* Fix the mode of custom dir to 0700 in docker-rootless (#20861) (#20867)
|
* Fix the mode of custom dir to 0700 in docker-rootless (#20861) (#20867)
|
||||||
* Fix UI mis-align for PR commit history (#20845) (#20859)
|
* Fix UI mis-align for PR commit history (#20845) (#20859)
|
||||||
|
|
||||||
## [1.17.1](https://github.com/go-gitea/gitea/releases/tag/1.17.1) - 2022-08-17
|
## [1.17.1](https://github.com/go-gitea/gitea/releases/tag/v1.17.1) - 2022-08-17
|
||||||
|
|
||||||
* SECURITY
|
* SECURITY
|
||||||
* Correctly escape within tribute.js (#20831) (#20832)
|
* Correctly escape within tribute.js (#20831) (#20832)
|
||||||
|
|||||||
@@ -166,19 +166,19 @@ Here's how to run the test suite:
|
|||||||
|
|
||||||
- code lint
|
- code lint
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
| :-------------------- | :---------------------------------------------------------------- |
|
| :-------------------- | :------------------------------------------------------------------------ |
|
||||||
|``make lint`` | lint everything (not needed if you only change the front- **or** backend) |
|
|``make lint`` | lint everything (not needed if you only change the front- **or** backend) |
|
||||||
|``make lint-frontend`` | lint frontend files |
|
|``make lint-frontend`` | lint frontend files |
|
||||||
|``make lint-backend`` | lint backend files |
|
|``make lint-backend`` | lint backend files |
|
||||||
|
|
||||||
- run tests (we suggest running them on Linux)
|
- run tests (we suggest running them on Linux)
|
||||||
|
|
||||||
| Command | Action | |
|
| Command | Action | |
|
||||||
| :------------------------------------- | :----------------------------------------------- | ------------ |
|
| :----------------------------------------- | :------------------------------------------------------- | ------------------------------------------ |
|
||||||
|``make test[\#SpecificTestName]`` | run unit test(s) | |
|
|``make test[\#SpecificTestName]`` | run unit test(s) | |
|
||||||
|``make test-sqlite[\#SpecificTestName]``| run [integration](tests/integration) test(s) for SQLite |[More details](tests/integration/README.md) |
|
|``make test-sqlite[\#SpecificTestName]``. | run [integration](tests/integration) test(s) for SQLite |[More details](tests/integration/README.md) |
|
||||||
|``make test-e2e-sqlite[\#SpecificTestName]``| run [end-to-end](tests/e2e) test(s) for SQLite |[More details](tests/e2e/README.md) |
|
|``make test-e2e-sqlite[\#SpecificTestName]``| run [end-to-end](tests/e2e) test(s) for SQLite |[More details](tests/e2e/README.md) |
|
||||||
|
|
||||||
## Translation
|
## Translation
|
||||||
|
|
||||||
|
|||||||
4
Makefile
4
Makefile
@@ -35,7 +35,7 @@ SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@717e3cb29becaaf0
|
|||||||
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
|
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
|
||||||
GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1
|
GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1
|
||||||
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
|
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
|
||||||
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1
|
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.8
|
||||||
GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.20.0
|
GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.20.0
|
||||||
GOPLS_MODERNIZE_PACKAGE ?= golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@v0.20.0
|
GOPLS_MODERNIZE_PACKAGE ?= golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@v0.20.0
|
||||||
|
|
||||||
@@ -766,7 +766,7 @@ generate-go: $(TAGS_PREREQ)
|
|||||||
|
|
||||||
.PHONY: security-check
|
.PHONY: security-check
|
||||||
security-check:
|
security-check:
|
||||||
go run $(GOVULNCHECK_PACKAGE) -show color ./...
|
go run $(GOVULNCHECK_PACKAGE) -show color ./... || true
|
||||||
|
|
||||||
$(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ)
|
$(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ)
|
||||||
ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),)
|
ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),)
|
||||||
|
|||||||
13
assets/go-licenses.json
generated
13
assets/go-licenses.json
generated
File diff suppressed because one or more lines are too long
@@ -121,6 +121,12 @@ func globalBool(c *cli.Command, name string) bool {
|
|||||||
// Any log appears in git stdout pipe will break the git protocol, eg: client can't push and hangs forever.
|
// Any log appears in git stdout pipe will break the git protocol, eg: client can't push and hangs forever.
|
||||||
func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(context.Context, *cli.Command) (context.Context, error) {
|
func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(context.Context, *cli.Command) (context.Context, error) {
|
||||||
return func(ctx context.Context, c *cli.Command) (context.Context, error) {
|
return func(ctx context.Context, c *cli.Command) (context.Context, error) {
|
||||||
|
if setting.InstallLock {
|
||||||
|
// During config loading, there might also be logs (for example: deprecation warnings).
|
||||||
|
// It must make sure that console logger is set up before config is loaded.
|
||||||
|
log.Error("Config is loaded before console logger is setup, it will cause bugs. Please fix it.")
|
||||||
|
return nil, errors.New("console logger must be setup before config is loaded")
|
||||||
|
}
|
||||||
level := defaultLevel
|
level := defaultLevel
|
||||||
if globalBool(c, "quiet") {
|
if globalBool(c, "quiet") {
|
||||||
level = log.FATAL
|
level = log.FATAL
|
||||||
|
|||||||
28
cmd/hook.go
28
cmd/hook.go
@@ -163,6 +163,14 @@ func (n *nilWriter) WriteString(s string) (int, error) {
|
|||||||
return len(s), nil
|
return len(s), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseGitHookCommitRefLine(line string) (oldCommitID, newCommitID string, refFullName git.RefName, ok bool) {
|
||||||
|
fields := strings.Split(line, " ")
|
||||||
|
if len(fields) != 3 {
|
||||||
|
return "", "", "", false
|
||||||
|
}
|
||||||
|
return fields[0], fields[1], git.RefName(fields[2]), true
|
||||||
|
}
|
||||||
|
|
||||||
func runHookPreReceive(ctx context.Context, c *cli.Command) error {
|
func runHookPreReceive(ctx context.Context, c *cli.Command) error {
|
||||||
if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
|
if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
|
||||||
return nil
|
return nil
|
||||||
@@ -197,6 +205,7 @@ Gitea or set your environment appropriately.`, "")
|
|||||||
PullRequestID: prID,
|
PullRequestID: prID,
|
||||||
DeployKeyID: deployKeyID,
|
DeployKeyID: deployKeyID,
|
||||||
ActionPerm: int(actionPerm),
|
ActionPerm: int(actionPerm),
|
||||||
|
IsWiki: isWiki,
|
||||||
}
|
}
|
||||||
|
|
||||||
scanner := bufio.NewScanner(os.Stdin)
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
@@ -228,14 +237,11 @@ Gitea or set your environment appropriately.`, "")
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
fields := bytes.Fields(scanner.Bytes())
|
oldCommitID, newCommitID, refFullName, ok := parseGitHookCommitRefLine(scanner.Text())
|
||||||
if len(fields) != 3 {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
oldCommitID := string(fields[0])
|
|
||||||
newCommitID := string(fields[1])
|
|
||||||
refFullName := git.RefName(fields[2])
|
|
||||||
total++
|
total++
|
||||||
lastline++
|
lastline++
|
||||||
|
|
||||||
@@ -361,6 +367,7 @@ Gitea or set your environment appropriately.`, "")
|
|||||||
GitPushOptions: pushOptions(),
|
GitPushOptions: pushOptions(),
|
||||||
PullRequestID: prID,
|
PullRequestID: prID,
|
||||||
PushTrigger: repo_module.PushTrigger(os.Getenv(repo_module.EnvPushTrigger)),
|
PushTrigger: repo_module.PushTrigger(os.Getenv(repo_module.EnvPushTrigger)),
|
||||||
|
IsWiki: isWiki,
|
||||||
}
|
}
|
||||||
oldCommitIDs := make([]string, hookBatchSize)
|
oldCommitIDs := make([]string, hookBatchSize)
|
||||||
newCommitIDs := make([]string, hookBatchSize)
|
newCommitIDs := make([]string, hookBatchSize)
|
||||||
@@ -378,16 +385,13 @@ Gitea or set your environment appropriately.`, "")
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
fields := bytes.Fields(scanner.Bytes())
|
var ok bool
|
||||||
if len(fields) != 3 {
|
oldCommitIDs[count], newCommitIDs[count], refFullNames[count], ok = parseGitHookCommitRefLine(scanner.Text())
|
||||||
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(out, ".")
|
fmt.Fprintf(out, ".")
|
||||||
oldCommitIDs[count] = string(fields[0])
|
|
||||||
newCommitIDs[count] = string(fields[1])
|
|
||||||
refFullNames[count] = git.RefName(fields[2])
|
|
||||||
|
|
||||||
commitID, _ := git.NewIDFromString(newCommitIDs[count])
|
commitID, _ := git.NewIDFromString(newCommitIDs[count])
|
||||||
if refFullNames[count] == git.BranchPrefix+"master" && !commitID.IsZero() && count == total {
|
if refFullNames[count] == git.BranchPrefix+"master" && !commitID.IsZero() && count == total {
|
||||||
masterPushed = true
|
masterPushed = true
|
||||||
@@ -511,6 +515,7 @@ Gitea or set your environment appropriately.`, "")
|
|||||||
|
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
repoUser := os.Getenv(repo_module.EnvRepoUsername)
|
repoUser := os.Getenv(repo_module.EnvRepoUsername)
|
||||||
|
isWiki, _ := strconv.ParseBool(os.Getenv(repo_module.EnvRepoIsWiki))
|
||||||
repoName := os.Getenv(repo_module.EnvRepoName)
|
repoName := os.Getenv(repo_module.EnvRepoName)
|
||||||
pusherID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64)
|
pusherID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64)
|
||||||
pusherName := os.Getenv(repo_module.EnvPusherName)
|
pusherName := os.Getenv(repo_module.EnvPusherName)
|
||||||
@@ -588,6 +593,7 @@ Gitea or set your environment appropriately.`, "")
|
|||||||
UserName: pusherName,
|
UserName: pusherName,
|
||||||
UserID: pusherID,
|
UserID: pusherID,
|
||||||
GitPushOptions: make(map[string]string),
|
GitPushOptions: make(map[string]string),
|
||||||
|
IsWiki: isWiki,
|
||||||
}
|
}
|
||||||
hookOptions.OldCommitIDs = make([]string, 0, hookBatchSize)
|
hookOptions.OldCommitIDs = make([]string, 0, hookBatchSize)
|
||||||
hookOptions.NewCommitIDs = make([]string, 0, hookBatchSize)
|
hookOptions.NewCommitIDs = make([]string, 0, hookBatchSize)
|
||||||
|
|||||||
@@ -39,3 +39,17 @@ func TestPktLine(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, []byte("0007a\nb"), w.Bytes())
|
assert.Equal(t, []byte("0007a\nb"), w.Bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseGitHookCommitRefLine(t *testing.T) {
|
||||||
|
oldCommitID, newCommitID, refName, ok := parseGitHookCommitRefLine("a b c")
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "a", oldCommitID)
|
||||||
|
assert.Equal(t, "b", newCommitID)
|
||||||
|
assert.Equal(t, "c", string(refName))
|
||||||
|
|
||||||
|
_, _, _, ok = parseGitHookCommitRefLine("a\tb\tc")
|
||||||
|
assert.False(t, ok)
|
||||||
|
|
||||||
|
_, _, _, ok = parseGitHookCommitRefLine("a b")
|
||||||
|
assert.False(t, ok)
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import (
|
|||||||
var CmdKeys = &cli.Command{
|
var CmdKeys = &cli.Command{
|
||||||
Name: "keys",
|
Name: "keys",
|
||||||
Usage: "(internal) Should only be called by SSH server",
|
Usage: "(internal) Should only be called by SSH server",
|
||||||
Hidden: true, // internal commands shouldn't not be visible
|
Hidden: true, // internal commands shouldn't be visible
|
||||||
Description: "Queries the Gitea database to get the authorized command for a given ssh key fingerprint",
|
Description: "Queries the Gitea database to get the authorized command for a given ssh key fingerprint",
|
||||||
Before: PrepareConsoleLoggerLevel(log.FATAL),
|
Before: PrepareConsoleLoggerLevel(log.FATAL),
|
||||||
Action: runKeys,
|
Action: runKeys,
|
||||||
|
|||||||
10
cmd/main.go
10
cmd/main.go
@@ -50,11 +50,15 @@ DEFAULT CONFIGURATION:
|
|||||||
|
|
||||||
func prepareSubcommandWithGlobalFlags(originCmd *cli.Command) {
|
func prepareSubcommandWithGlobalFlags(originCmd *cli.Command) {
|
||||||
originBefore := originCmd.Before
|
originBefore := originCmd.Before
|
||||||
originCmd.Before = func(ctx context.Context, cmd *cli.Command) (context.Context, error) {
|
originCmd.Before = func(ctxOrig context.Context, cmd *cli.Command) (ctx context.Context, err error) {
|
||||||
prepareWorkPathAndCustomConf(cmd)
|
ctx = ctxOrig
|
||||||
if originBefore != nil {
|
if originBefore != nil {
|
||||||
return originBefore(ctx, cmd)
|
ctx, err = originBefore(ctx, cmd)
|
||||||
|
if err != nil {
|
||||||
|
return ctx, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
prepareWorkPathAndCustomConf(cmd)
|
||||||
return ctx, nil
|
return ctx, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/test"
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
@@ -28,11 +29,11 @@ func makePathOutput(workPath, customPath, customConf string) string {
|
|||||||
return fmt.Sprintf("WorkPath=%s\nCustomPath=%s\nCustomConf=%s", workPath, customPath, customConf)
|
return fmt.Sprintf("WorkPath=%s\nCustomPath=%s\nCustomConf=%s", workPath, customPath, customConf)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestApp(testCmdAction cli.ActionFunc) *cli.Command {
|
func newTestApp(testCmd cli.Command) *cli.Command {
|
||||||
app := NewMainApp(AppVersion{})
|
app := NewMainApp(AppVersion{})
|
||||||
testCmd := &cli.Command{Name: "test-cmd", Action: testCmdAction}
|
testCmd.Name = util.IfZero(testCmd.Name, "test-cmd")
|
||||||
prepareSubcommandWithGlobalFlags(testCmd)
|
prepareSubcommandWithGlobalFlags(&testCmd)
|
||||||
app.Commands = append(app.Commands, testCmd)
|
app.Commands = append(app.Commands, &testCmd)
|
||||||
app.DefaultCommand = testCmd.Name
|
app.DefaultCommand = testCmd.Name
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
@@ -156,9 +157,11 @@ func TestCliCmd(t *testing.T) {
|
|||||||
|
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
t.Run(c.cmd, func(t *testing.T) {
|
t.Run(c.cmd, func(t *testing.T) {
|
||||||
app := newTestApp(func(ctx context.Context, cmd *cli.Command) error {
|
app := newTestApp(cli.Command{
|
||||||
_, _ = fmt.Fprint(cmd.Root().Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf))
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
return nil
|
_, _ = fmt.Fprint(cmd.Root().Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf))
|
||||||
|
return nil
|
||||||
|
},
|
||||||
})
|
})
|
||||||
for k, v := range c.env {
|
for k, v := range c.env {
|
||||||
t.Setenv(k, v)
|
t.Setenv(k, v)
|
||||||
@@ -173,31 +176,54 @@ func TestCliCmd(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCliCmdError(t *testing.T) {
|
func TestCliCmdError(t *testing.T) {
|
||||||
app := newTestApp(func(ctx context.Context, cmd *cli.Command) error { return errors.New("normal error") })
|
app := newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return errors.New("normal error") }})
|
||||||
r, err := runTestApp(app, "./gitea", "test-cmd")
|
r, err := runTestApp(app, "./gitea", "test-cmd")
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Equal(t, 1, r.ExitCode)
|
assert.Equal(t, 1, r.ExitCode)
|
||||||
assert.Empty(t, r.Stdout)
|
assert.Empty(t, r.Stdout)
|
||||||
assert.Equal(t, "Command error: normal error\n", r.Stderr)
|
assert.Equal(t, "Command error: normal error\n", r.Stderr)
|
||||||
|
|
||||||
app = newTestApp(func(ctx context.Context, cmd *cli.Command) error { return cli.Exit("exit error", 2) })
|
app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return cli.Exit("exit error", 2) }})
|
||||||
r, err = runTestApp(app, "./gitea", "test-cmd")
|
r, err = runTestApp(app, "./gitea", "test-cmd")
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Equal(t, 2, r.ExitCode)
|
assert.Equal(t, 2, r.ExitCode)
|
||||||
assert.Empty(t, r.Stdout)
|
assert.Empty(t, r.Stdout)
|
||||||
assert.Equal(t, "exit error\n", r.Stderr)
|
assert.Equal(t, "exit error\n", r.Stderr)
|
||||||
|
|
||||||
app = newTestApp(func(ctx context.Context, cmd *cli.Command) error { return nil })
|
app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return nil }})
|
||||||
r, err = runTestApp(app, "./gitea", "test-cmd", "--no-such")
|
r, err = runTestApp(app, "./gitea", "test-cmd", "--no-such")
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Equal(t, 1, r.ExitCode)
|
assert.Equal(t, 1, r.ExitCode)
|
||||||
assert.Empty(t, r.Stdout)
|
assert.Empty(t, r.Stdout)
|
||||||
assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n\n", r.Stderr)
|
assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n\n", r.Stderr)
|
||||||
|
|
||||||
app = newTestApp(func(ctx context.Context, cmd *cli.Command) error { return nil })
|
app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return nil }})
|
||||||
r, err = runTestApp(app, "./gitea", "test-cmd")
|
r, err = runTestApp(app, "./gitea", "test-cmd")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, -1, r.ExitCode) // the cli.OsExiter is not called
|
assert.Equal(t, -1, r.ExitCode) // the cli.OsExiter is not called
|
||||||
assert.Empty(t, r.Stdout)
|
assert.Empty(t, r.Stdout)
|
||||||
assert.Empty(t, r.Stderr)
|
assert.Empty(t, r.Stderr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCliCmdBefore(t *testing.T) {
|
||||||
|
ctxNew := context.WithValue(context.Background(), any("key"), "value")
|
||||||
|
configValues := map[string]string{}
|
||||||
|
setting.CustomConf = "/tmp/any.ini"
|
||||||
|
var actionCtx context.Context
|
||||||
|
app := newTestApp(cli.Command{
|
||||||
|
Before: func(context.Context, *cli.Command) (context.Context, error) {
|
||||||
|
configValues["before"] = setting.CustomConf
|
||||||
|
return ctxNew, nil
|
||||||
|
},
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
configValues["action"] = setting.CustomConf
|
||||||
|
actionCtx = ctx
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
_, err := runTestApp(app, "./gitea", "--config", "/dev/null", "test-cmd")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, ctxNew, actionCtx)
|
||||||
|
assert.Equal(t, "/tmp/any.ini", configValues["before"], "BeforeFunc must be called before preparing config")
|
||||||
|
assert.Equal(t, "/dev/null", configValues["action"])
|
||||||
|
}
|
||||||
|
|||||||
41
cmd/serv.go
41
cmd/serv.go
@@ -13,13 +13,12 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
||||||
git_model "code.gitea.io/gitea/models/git"
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
"code.gitea.io/gitea/models/perm"
|
"code.gitea.io/gitea/models/perm"
|
||||||
"code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||||
"code.gitea.io/gitea/modules/json"
|
"code.gitea.io/gitea/modules/json"
|
||||||
@@ -32,7 +31,6 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/services/lfs"
|
"code.gitea.io/gitea/services/lfs"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
|
||||||
"github.com/kballard/go-shellquote"
|
"github.com/kballard/go-shellquote"
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
@@ -133,27 +131,6 @@ func getAccessMode(verb, lfsVerb string) perm.AccessMode {
|
|||||||
return perm.AccessModeNone
|
return perm.AccessModeNone
|
||||||
}
|
}
|
||||||
|
|
||||||
func getLFSAuthToken(ctx context.Context, lfsVerb string, results *private.ServCommandResults) (string, error) {
|
|
||||||
now := time.Now()
|
|
||||||
claims := lfs.Claims{
|
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
|
||||||
ExpiresAt: jwt.NewNumericDate(now.Add(setting.LFS.HTTPAuthExpiry)),
|
|
||||||
NotBefore: jwt.NewNumericDate(now),
|
|
||||||
},
|
|
||||||
RepoID: results.RepoID,
|
|
||||||
Op: lfsVerb,
|
|
||||||
UserID: results.UserID,
|
|
||||||
}
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
||||||
|
|
||||||
// Sign and get the complete encoded token as a string using the secret
|
|
||||||
tokenString, err := token.SignedString(setting.LFS.JWTSecretBytes)
|
|
||||||
if err != nil {
|
|
||||||
return "", fail(ctx, "Failed to sign JWT Token", "Failed to sign JWT token: %v", err)
|
|
||||||
}
|
|
||||||
return "Bearer " + tokenString, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func runServ(ctx context.Context, c *cli.Command) error {
|
func runServ(ctx context.Context, c *cli.Command) error {
|
||||||
// FIXME: This needs to internationalised
|
// FIXME: This needs to internationalised
|
||||||
setup(ctx, c.Bool("debug"))
|
setup(ctx, c.Bool("debug"))
|
||||||
@@ -230,7 +207,7 @@ func runServ(ctx context.Context, c *cli.Command) error {
|
|||||||
username := repoPathFields[0]
|
username := repoPathFields[0]
|
||||||
reponame := strings.TrimSuffix(repoPathFields[1], ".git") // “the-repo-name" or "the-repo-name.wiki"
|
reponame := strings.TrimSuffix(repoPathFields[1], ".git") // “the-repo-name" or "the-repo-name.wiki"
|
||||||
|
|
||||||
if !repo.IsValidSSHAccessRepoName(reponame) {
|
if !repo_model.IsValidSSHAccessRepoName(reponame) {
|
||||||
return fail(ctx, "Invalid repo name", "Invalid repo name: %s", reponame)
|
return fail(ctx, "Invalid repo name", "Invalid repo name: %s", reponame)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,14 +253,16 @@ func runServ(ctx context.Context, c *cli.Command) error {
|
|||||||
return fail(ctx, extra.UserMsg, "ServCommand failed: %s", extra.Error)
|
return fail(ctx, extra.UserMsg, "ServCommand failed: %s", extra.Error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LowerCase and trim the repoPath as that's how they are stored.
|
// because the original repoPath maybe redirected, we need to use the returned actual repository information
|
||||||
// This should be done after splitting the repoPath into username and reponame
|
if results.IsWiki {
|
||||||
// so that username and reponame are not affected.
|
repoPath = repo_model.RelativeWikiPath(results.OwnerName, results.RepoName)
|
||||||
repoPath = strings.ToLower(results.OwnerName + "/" + results.RepoName + ".git")
|
} else {
|
||||||
|
repoPath = repo_model.RelativePath(results.OwnerName, results.RepoName)
|
||||||
|
}
|
||||||
|
|
||||||
// LFS SSH protocol
|
// LFS SSH protocol
|
||||||
if verb == git.CmdVerbLfsTransfer {
|
if verb == git.CmdVerbLfsTransfer {
|
||||||
token, err := getLFSAuthToken(ctx, lfsVerb, results)
|
token, err := lfs.GetLFSAuthTokenWithBearer(lfs.AuthTokenOptions{Op: lfsVerb, UserID: results.UserID, RepoID: results.RepoID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -294,7 +273,7 @@ func runServ(ctx context.Context, c *cli.Command) error {
|
|||||||
if verb == git.CmdVerbLfsAuthenticate {
|
if verb == git.CmdVerbLfsAuthenticate {
|
||||||
url := fmt.Sprintf("%s%s/%s.git/info/lfs", setting.AppURL, url.PathEscape(results.OwnerName), url.PathEscape(results.RepoName))
|
url := fmt.Sprintf("%s%s/%s.git/info/lfs", setting.AppURL, url.PathEscape(results.OwnerName), url.PathEscape(results.RepoName))
|
||||||
|
|
||||||
token, err := getLFSAuthToken(ctx, lfsVerb, results)
|
token, err := lfs.GetLFSAuthTokenWithBearer(lfs.AuthTokenOptions{Op: lfsVerb, UserID: results.UserID, RepoID: results.RepoID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -567,6 +567,11 @@ ENABLED = true
|
|||||||
;; Alternative location to specify OAuth2 authentication secret. You cannot specify both this and JWT_SECRET, and must pick one
|
;; Alternative location to specify OAuth2 authentication secret. You cannot specify both this and JWT_SECRET, and must pick one
|
||||||
;JWT_SECRET_URI = file:/etc/gitea/oauth2_jwt_secret
|
;JWT_SECRET_URI = file:/etc/gitea/oauth2_jwt_secret
|
||||||
;;
|
;;
|
||||||
|
;; The "issuer" claim identifies the principal that issued the JWT.
|
||||||
|
;; Gitea 1.25 makes it default to "ROOT_URL without the last slash" to follow the standard.
|
||||||
|
;; If you have old logins from before 1.25, you may want to set it to the old (non-standard) value "ROOT_URL with the last slash".
|
||||||
|
;JWT_CLAIM_ISSUER =
|
||||||
|
;;
|
||||||
;; Lifetime of an OAuth2 access token in seconds
|
;; Lifetime of an OAuth2 access token in seconds
|
||||||
;ACCESS_TOKEN_EXPIRATION_TIME = 3600
|
;ACCESS_TOKEN_EXPIRATION_TIME = 3600
|
||||||
;;
|
;;
|
||||||
@@ -1343,6 +1348,10 @@ LEVEL = Info
|
|||||||
;; Dont mistake it for Reactions.
|
;; Dont mistake it for Reactions.
|
||||||
;CUSTOM_EMOJIS = gitea, codeberg, gitlab, git, github, gogs
|
;CUSTOM_EMOJIS = gitea, codeberg, gitlab, git, github, gogs
|
||||||
;;
|
;;
|
||||||
|
;; Comma separated list of enabled emojis, for example: smile, thumbsup, thumbsdown
|
||||||
|
;; Leave it empty to enable all emojis.
|
||||||
|
;ENABLED_EMOJIS =
|
||||||
|
;;
|
||||||
;; Whether the full name of the users should be shown where possible. If the full name isn't set, the username will be used.
|
;; Whether the full name of the users should be shown where possible. If the full name isn't set, the username will be used.
|
||||||
;DEFAULT_SHOW_FULL_NAME = false
|
;DEFAULT_SHOW_FULL_NAME = false
|
||||||
;;
|
;;
|
||||||
@@ -2536,7 +2545,19 @@ LEVEL = Info
|
|||||||
;; * sanitized: Sanitize the content and render it inside current page, default to only allow a few HTML tags and attributes. Customized sanitizer rules can be defined in [markup.sanitizer.*] .
|
;; * sanitized: Sanitize the content and render it inside current page, default to only allow a few HTML tags and attributes. Customized sanitizer rules can be defined in [markup.sanitizer.*] .
|
||||||
;; * no-sanitizer: Disable the sanitizer and render the content inside current page. It's **insecure** and may lead to XSS attack if the content contains malicious code.
|
;; * no-sanitizer: Disable the sanitizer and render the content inside current page. It's **insecure** and may lead to XSS attack if the content contains malicious code.
|
||||||
;; * iframe: Render the content in a separate standalone page and embed it into current page by iframe. The iframe is in sandbox mode with same-origin disabled, and the JS code are safely isolated from parent page.
|
;; * iframe: Render the content in a separate standalone page and embed it into current page by iframe. The iframe is in sandbox mode with same-origin disabled, and the JS code are safely isolated from parent page.
|
||||||
;RENDER_CONTENT_MODE=sanitized
|
;RENDER_CONTENT_MODE = sanitized
|
||||||
|
;; The sandbox applied to the iframe and Content-Security-Policy header when RENDER_CONTENT_MODE is `iframe`.
|
||||||
|
;; It defaults to a safe set of "allow-*" restrictions (space separated).
|
||||||
|
;; You can also set it by your requirements or use "disabled" to disable the sandbox completely.
|
||||||
|
;; When set it, make sure there is no security risk:
|
||||||
|
;; * PDF-only content: generally safe to use "disabled", and it needs to be "disabled" because PDF only renders with no sandbox.
|
||||||
|
;; * HTML content with JS: if the "RENDER_COMMAND" can guarantee there is no XSS, then it is safe, otherwise, you need to fine tune the "allow-*" restrictions.
|
||||||
|
;RENDER_CONTENT_SANDBOX =
|
||||||
|
;; Whether post-process the rendered HTML content, including:
|
||||||
|
;; resolve relative links and image sources, recognizing issue/commit references, escaping invisible characters,
|
||||||
|
;; mentioning users, rendering permlink code blocks, replacing emoji shorthands, etc.
|
||||||
|
;; By default, this is true when RENDER_CONTENT_MODE is `sanitized`, otherwise false.
|
||||||
|
;NEED_POST_PROCESS = false
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|||||||
44
go.mod
44
go.mod
@@ -1,6 +1,8 @@
|
|||||||
module code.gitea.io/gitea
|
module code.gitea.io/gitea
|
||||||
|
|
||||||
go 1.25.1
|
go 1.25.0
|
||||||
|
|
||||||
|
toolchain go1.25.8
|
||||||
|
|
||||||
// rfc5280 said: "The serial number is an integer assigned by the CA to each certificate."
|
// rfc5280 said: "The serial number is an integer assigned by the CA to each certificate."
|
||||||
// But some CAs use negative serial number, just relax the check. related:
|
// But some CAs use negative serial number, just relax the check. related:
|
||||||
@@ -35,7 +37,7 @@ require (
|
|||||||
github.com/bohde/codel v0.2.0
|
github.com/bohde/codel v0.2.0
|
||||||
github.com/buildkite/terminal-to-html/v3 v3.16.8
|
github.com/buildkite/terminal-to-html/v3 v3.16.8
|
||||||
github.com/caddyserver/certmagic v0.24.0
|
github.com/caddyserver/certmagic v0.24.0
|
||||||
github.com/charmbracelet/git-lfs-transfer v0.2.0
|
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20251013092601-6327009efd21
|
||||||
github.com/chi-middleware/proxy v1.1.1
|
github.com/chi-middleware/proxy v1.1.1
|
||||||
github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21
|
github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21
|
||||||
github.com/djherbis/buffer v1.2.0
|
github.com/djherbis/buffer v1.2.0
|
||||||
@@ -56,7 +58,7 @@ require (
|
|||||||
github.com/go-co-op/gocron v1.37.0
|
github.com/go-co-op/gocron v1.37.0
|
||||||
github.com/go-enry/go-enry/v2 v2.9.2
|
github.com/go-enry/go-enry/v2 v2.9.2
|
||||||
github.com/go-git/go-billy/v5 v5.6.2
|
github.com/go-git/go-billy/v5 v5.6.2
|
||||||
github.com/go-git/go-git/v5 v5.16.2
|
github.com/go-git/go-git/v5 v5.16.5
|
||||||
github.com/go-ldap/ldap/v3 v3.4.11
|
github.com/go-ldap/ldap/v3 v3.4.11
|
||||||
github.com/go-redsync/redsync/v4 v4.13.0
|
github.com/go-redsync/redsync/v4 v4.13.0
|
||||||
github.com/go-sql-driver/mysql v1.9.3
|
github.com/go-sql-driver/mysql v1.9.3
|
||||||
@@ -84,7 +86,7 @@ require (
|
|||||||
github.com/mattn/go-isatty v0.0.20
|
github.com/mattn/go-isatty v0.0.20
|
||||||
github.com/mattn/go-sqlite3 v1.14.32
|
github.com/mattn/go-sqlite3 v1.14.32
|
||||||
github.com/meilisearch/meilisearch-go v0.33.2
|
github.com/meilisearch/meilisearch-go v0.33.2
|
||||||
github.com/mholt/archives v0.1.3
|
github.com/mholt/archives v0.1.5-0.20251009205813-e30ac6010726
|
||||||
github.com/microcosm-cc/bluemonday v1.0.27
|
github.com/microcosm-cc/bluemonday v1.0.27
|
||||||
github.com/microsoft/go-mssqldb v1.9.3
|
github.com/microsoft/go-mssqldb v1.9.3
|
||||||
github.com/minio/minio-go/v7 v7.0.95
|
github.com/minio/minio-go/v7 v7.0.95
|
||||||
@@ -109,20 +111,20 @@ require (
|
|||||||
github.com/ulikunitz/xz v0.5.15
|
github.com/ulikunitz/xz v0.5.15
|
||||||
github.com/urfave/cli-docs/v3 v3.0.0-alpha6
|
github.com/urfave/cli-docs/v3 v3.0.0-alpha6
|
||||||
github.com/urfave/cli/v3 v3.4.1
|
github.com/urfave/cli/v3 v3.4.1
|
||||||
github.com/wneessen/go-mail v0.6.2
|
github.com/wneessen/go-mail v0.7.2
|
||||||
github.com/xeipuuv/gojsonschema v1.2.0
|
github.com/xeipuuv/gojsonschema v1.2.0
|
||||||
github.com/yohcop/openid-go v1.0.1
|
github.com/yohcop/openid-go v1.0.1
|
||||||
github.com/yuin/goldmark v1.7.13
|
github.com/yuin/goldmark v1.7.13
|
||||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||||
github.com/yuin/goldmark-meta v1.1.0
|
github.com/yuin/goldmark-meta v1.1.0
|
||||||
gitlab.com/gitlab-org/api/client-go v0.142.4
|
gitlab.com/gitlab-org/api/client-go v0.142.4
|
||||||
golang.org/x/crypto v0.41.0
|
golang.org/x/crypto v0.45.0
|
||||||
golang.org/x/image v0.30.0
|
golang.org/x/image v0.30.0
|
||||||
golang.org/x/net v0.43.0
|
golang.org/x/net v0.47.0
|
||||||
golang.org/x/oauth2 v0.30.0
|
golang.org/x/oauth2 v0.30.0
|
||||||
golang.org/x/sync v0.16.0
|
golang.org/x/sync v0.18.0
|
||||||
golang.org/x/sys v0.35.0
|
golang.org/x/sys v0.38.0
|
||||||
golang.org/x/text v0.28.0
|
golang.org/x/text v0.31.0
|
||||||
google.golang.org/grpc v1.75.0
|
google.golang.org/grpc v1.75.0
|
||||||
google.golang.org/protobuf v1.36.8
|
google.golang.org/protobuf v1.36.8
|
||||||
gopkg.in/ini.v1 v1.67.0
|
gopkg.in/ini.v1 v1.67.0
|
||||||
@@ -142,7 +144,7 @@ require (
|
|||||||
github.com/DataDog/zstd v1.5.7 // indirect
|
github.com/DataDog/zstd v1.5.7 // indirect
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/RoaringBitmap/roaring/v2 v2.10.0 // indirect
|
github.com/RoaringBitmap/roaring/v2 v2.10.0 // indirect
|
||||||
github.com/STARRY-S/zip v0.2.1 // indirect
|
github.com/STARRY-S/zip v0.2.3 // indirect
|
||||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
||||||
@@ -172,14 +174,14 @@ require (
|
|||||||
github.com/blevesearch/zapx/v16 v16.2.4 // indirect
|
github.com/blevesearch/zapx/v16 v16.2.4 // indirect
|
||||||
github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect
|
github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect
|
||||||
github.com/bodgit/plumbing v1.3.0 // indirect
|
github.com/bodgit/plumbing v1.3.0 // indirect
|
||||||
github.com/bodgit/sevenzip v1.6.0 // indirect
|
github.com/bodgit/sevenzip v1.6.1 // indirect
|
||||||
github.com/bodgit/windows v1.0.1 // indirect
|
github.com/bodgit/windows v1.0.1 // indirect
|
||||||
github.com/boombuler/barcode v1.1.0 // indirect
|
github.com/boombuler/barcode v1.1.0 // indirect
|
||||||
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect
|
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect
|
||||||
github.com/caddyserver/zerossl v0.1.3 // indirect
|
github.com/caddyserver/zerossl v0.1.3 // indirect
|
||||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
|
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cloudflare/circl v1.6.1 // indirect
|
github.com/cloudflare/circl v1.6.3 // indirect
|
||||||
github.com/couchbase/go-couchbase v0.1.1 // indirect
|
github.com/couchbase/go-couchbase v0.1.1 // indirect
|
||||||
github.com/couchbase/gomemcached v0.3.3 // indirect
|
github.com/couchbase/gomemcached v0.3.3 // indirect
|
||||||
github.com/couchbase/goutils v0.1.2 // indirect
|
github.com/couchbase/goutils v0.1.2 // indirect
|
||||||
@@ -233,14 +235,14 @@ require (
|
|||||||
github.com/mikelolasagasti/xz v1.0.1 // indirect
|
github.com/mikelolasagasti/xz v1.0.1 // indirect
|
||||||
github.com/minio/crc64nvme v1.1.1 // indirect
|
github.com/minio/crc64nvme v1.1.1 // indirect
|
||||||
github.com/minio/md5-simd v1.1.2 // indirect
|
github.com/minio/md5-simd v1.1.2 // indirect
|
||||||
github.com/minio/minlz v1.0.0 // indirect
|
github.com/minio/minlz v1.0.1 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 // indirect
|
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 // indirect
|
||||||
github.com/mschoch/smat v0.2.0 // indirect
|
github.com/mschoch/smat v0.2.0 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/nwaples/rardecode/v2 v2.1.0 // indirect
|
github.com/nwaples/rardecode/v2 v2.2.0 // indirect
|
||||||
github.com/olekukonko/cat v0.0.0-20250817074551-3280053e4e00 // indirect
|
github.com/olekukonko/cat v0.0.0-20250817074551-3280053e4e00 // indirect
|
||||||
github.com/olekukonko/errors v1.1.0 // indirect
|
github.com/olekukonko/errors v1.1.0 // indirect
|
||||||
github.com/olekukonko/ll v0.1.0 // indirect
|
github.com/olekukonko/ll v0.1.0 // indirect
|
||||||
@@ -259,7 +261,8 @@ require (
|
|||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||||
github.com/sorairolake/lzip-go v0.3.5 // indirect
|
github.com/sorairolake/lzip-go v0.3.8 // indirect
|
||||||
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
|
||||||
github.com/tinylib/msgp v1.4.0 // indirect
|
github.com/tinylib/msgp v1.4.0 // indirect
|
||||||
github.com/unknwon/com v1.0.1 // indirect
|
github.com/unknwon/com v1.0.1 // indirect
|
||||||
@@ -278,9 +281,9 @@ require (
|
|||||||
go.uber.org/zap/exp v0.3.0 // indirect
|
go.uber.org/zap/exp v0.3.0 // indirect
|
||||||
go4.org v0.0.0-20230225012048-214862532bf5 // 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-20250305212735-054e65f0b394 // indirect
|
||||||
golang.org/x/mod v0.27.0 // indirect
|
golang.org/x/mod v0.29.0 // indirect
|
||||||
golang.org/x/time v0.12.0 // indirect
|
golang.org/x/time v0.12.0 // indirect
|
||||||
golang.org/x/tools v0.36.0 // indirect
|
golang.org/x/tools v0.38.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
@@ -295,10 +298,7 @@ replace github.com/jaytaylor/html2text => github.com/Necoro/html2text v0.0.0-202
|
|||||||
|
|
||||||
replace github.com/hashicorp/go-version => github.com/6543/go-version v1.3.1
|
replace github.com/hashicorp/go-version => github.com/6543/go-version v1.3.1
|
||||||
|
|
||||||
replace github.com/nektos/act => gitea.com/gitea/act v0.261.6
|
replace github.com/nektos/act => gitea.com/gitea/act v0.261.7-0.20251003180512-ac6e4b751763
|
||||||
|
|
||||||
// TODO: the only difference is in `PutObject`: the fork doesn't use `NewVerifyingReader(r, sha256.New(), oid, expectedSize)`, need to figure out why
|
|
||||||
replace github.com/charmbracelet/git-lfs-transfer => gitea.com/gitea/git-lfs-transfer v0.2.0
|
|
||||||
|
|
||||||
replace git.sr.ht/~mariusor/go-xsd-duration => gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078
|
replace git.sr.ht/~mariusor/go-xsd-duration => gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078
|
||||||
|
|
||||||
|
|||||||
84
go.sum
84
go.sum
@@ -31,10 +31,8 @@ dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
|||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
gitea.com/gitea/act v0.261.6 h1:CjZwKOyejonNFDmsXOw3wGm5Vet573hHM6VMLsxtvPY=
|
gitea.com/gitea/act v0.261.7-0.20251003180512-ac6e4b751763 h1:ohdxegvslDEllZmRNDqpKun6L4Oq81jNdEDtGgHEV2c=
|
||||||
gitea.com/gitea/act v0.261.6/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok=
|
gitea.com/gitea/act v0.261.7-0.20251003180512-ac6e4b751763/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok=
|
||||||
gitea.com/gitea/git-lfs-transfer v0.2.0 h1:baHaNoBSRaeq/xKayEXwiDQtlIjps4Ac/Ll4KqLMB40=
|
|
||||||
gitea.com/gitea/git-lfs-transfer v0.2.0/go.mod h1:UrXUCm3xLQkq15fu7qlXHUMlrhdlXHoi13KH2Dfiits=
|
|
||||||
gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:BAFmdZpRW7zMQZQDClaCWobRj9uL1MR3MzpCVJvc5s4=
|
gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:BAFmdZpRW7zMQZQDClaCWobRj9uL1MR3MzpCVJvc5s4=
|
||||||
gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs=
|
gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs=
|
||||||
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed h1:EZZBtilMLSZNWtHHcgq2mt6NSGhJSZBuduAlinMEmso=
|
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed h1:EZZBtilMLSZNWtHHcgq2mt6NSGhJSZBuduAlinMEmso=
|
||||||
@@ -93,8 +91,8 @@ github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06
|
|||||||
github.com/RoaringBitmap/roaring v0.7.1/go.mod h1:jdT9ykXwHFNdJbEtxePexlFYH9LXucApeS0/+/g+p1I=
|
github.com/RoaringBitmap/roaring v0.7.1/go.mod h1:jdT9ykXwHFNdJbEtxePexlFYH9LXucApeS0/+/g+p1I=
|
||||||
github.com/RoaringBitmap/roaring/v2 v2.10.0 h1:HbJ8Cs71lfCJyvmSptxeMX2PtvOC8yonlU0GQcy2Ak0=
|
github.com/RoaringBitmap/roaring/v2 v2.10.0 h1:HbJ8Cs71lfCJyvmSptxeMX2PtvOC8yonlU0GQcy2Ak0=
|
||||||
github.com/RoaringBitmap/roaring/v2 v2.10.0/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0=
|
github.com/RoaringBitmap/roaring/v2 v2.10.0/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0=
|
||||||
github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg=
|
github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
|
||||||
github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4=
|
github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
|
||||||
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0 h1:tgjwQrDH5m6jIYB7kac5IQZmfUzQNseac/e3H4VoCNE=
|
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0 h1:tgjwQrDH5m6jIYB7kac5IQZmfUzQNseac/e3H4VoCNE=
|
||||||
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0/go.mod h1:1HmmMEVsr+0R1QWahSeMJkjSkq6CYAZu1aIbYSpfJ4o=
|
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0/go.mod h1:1HmmMEVsr+0R1QWahSeMJkjSkq6CYAZu1aIbYSpfJ4o=
|
||||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||||
@@ -193,8 +191,8 @@ github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTS
|
|||||||
github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b/go.mod h1:ac9efd0D1fsDb3EJvhqgXRbFx7bs2wqZ10HQPeU8U/Q=
|
github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b/go.mod h1:ac9efd0D1fsDb3EJvhqgXRbFx7bs2wqZ10HQPeU8U/Q=
|
||||||
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
|
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
|
||||||
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
|
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
|
||||||
github.com/bodgit/sevenzip v1.6.0 h1:a4R0Wu6/P1o1pP/3VV++aEOcyeBxeO/xE2Y9NSTrr6A=
|
github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4=
|
||||||
github.com/bodgit/sevenzip v1.6.0/go.mod h1:zOBh9nJUof7tcrlqJFv1koWRrhz3LbDbUNngkuZxLMc=
|
github.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8=
|
||||||
github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
|
github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
|
||||||
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
|
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
|
||||||
github.com/bohde/codel v0.2.0 h1:fzF7ibgKmCfQbOzQCblmQcwzDRmV7WO7VMLm/hDvD3E=
|
github.com/bohde/codel v0.2.0 h1:fzF7ibgKmCfQbOzQCblmQcwzDRmV7WO7VMLm/hDvD3E=
|
||||||
@@ -219,6 +217,8 @@ github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/
|
|||||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
|
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20251013092601-6327009efd21 h1:2d64+4Jek9vjYwhY93AjbleiVH+AeWvPwPmDi1mfKFQ=
|
||||||
|
github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20251013092601-6327009efd21/go.mod h1:fNlYtCHWTRC8MofQERZkVUNUWaOvZeTBqHn/amSbKZI=
|
||||||
github.com/chi-middleware/proxy v1.1.1 h1:4HaXUp8o2+bhHr1OhVy+VjN0+L7/07JDcn6v7YrTjrQ=
|
github.com/chi-middleware/proxy v1.1.1 h1:4HaXUp8o2+bhHr1OhVy+VjN0+L7/07JDcn6v7YrTjrQ=
|
||||||
github.com/chi-middleware/proxy v1.1.1/go.mod h1:jQwMEJct2tz9VmtCELxvnXoMfa+SOdikvbVJVHv/M+0=
|
github.com/chi-middleware/proxy v1.1.1/go.mod h1:jQwMEJct2tz9VmtCELxvnXoMfa+SOdikvbVJVHv/M+0=
|
||||||
github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||||
@@ -231,8 +231,8 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk
|
|||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
@@ -339,8 +339,8 @@ github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UN
|
|||||||
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
|
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
|
||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||||
github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
|
github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s=
|
||||||
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M=
|
||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||||
@@ -572,8 +572,8 @@ github.com/meilisearch/meilisearch-go v0.33.2 h1:YgsQSLYhAkRN2ias6I1KNRTjdYCN5w2
|
|||||||
github.com/meilisearch/meilisearch-go v0.33.2/go.mod h1:6eOPcQ+OAuwXvnONlfSgfgvr7TIAWM/6OdhcVHg8cF0=
|
github.com/meilisearch/meilisearch-go v0.33.2/go.mod h1:6eOPcQ+OAuwXvnONlfSgfgvr7TIAWM/6OdhcVHg8cF0=
|
||||||
github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc=
|
github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc=
|
||||||
github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
|
github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
|
||||||
github.com/mholt/archives v0.1.3 h1:aEAaOtNra78G+TvV5ohmXrJOAzf++dIlYeDW3N9q458=
|
github.com/mholt/archives v0.1.5-0.20251009205813-e30ac6010726 h1:WVjGWXBLI1Ggm2kHzNraCGgxFhLoK6gdpPSizCdxnx0=
|
||||||
github.com/mholt/archives v0.1.3/go.mod h1:LUCGp++/IbV/I0Xq4SzcIR6uwgeh2yjnQWamjRQfLTU=
|
github.com/mholt/archives v0.1.5-0.20251009205813-e30ac6010726/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||||
github.com/microsoft/go-mssqldb v1.9.3 h1:hy4p+LDC8LIGvI3JATnLVmBOLMJbmn5X400mr5j0lPs=
|
github.com/microsoft/go-mssqldb v1.9.3 h1:hy4p+LDC8LIGvI3JATnLVmBOLMJbmn5X400mr5j0lPs=
|
||||||
@@ -588,8 +588,8 @@ github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
|||||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||||
github.com/minio/minio-go/v7 v7.0.95 h1:ywOUPg+PebTMTzn9VDsoFJy32ZuARN9zhB+K3IYEvYU=
|
github.com/minio/minio-go/v7 v7.0.95 h1:ywOUPg+PebTMTzn9VDsoFJy32ZuARN9zhB+K3IYEvYU=
|
||||||
github.com/minio/minio-go/v7 v7.0.95/go.mod h1:wOOX3uxS334vImCNRVyIDdXX9OsXDm89ToynKgqUKlo=
|
github.com/minio/minio-go/v7 v7.0.95/go.mod h1:wOOX3uxS334vImCNRVyIDdXX9OsXDm89ToynKgqUKlo=
|
||||||
github.com/minio/minlz v1.0.0 h1:Kj7aJZ1//LlTP1DM8Jm7lNKvvJS2m74gyyXXn3+uJWQ=
|
github.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A=
|
||||||
github.com/minio/minlz v1.0.0/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
|
github.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
@@ -610,8 +610,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
|
|||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/niklasfasching/go-org v1.9.1 h1:/3s4uTPOF06pImGa2Yvlp24yKXZoTYM+nsIlMzfpg/0=
|
github.com/niklasfasching/go-org v1.9.1 h1:/3s4uTPOF06pImGa2Yvlp24yKXZoTYM+nsIlMzfpg/0=
|
||||||
github.com/niklasfasching/go-org v1.9.1/go.mod h1:ZAGFFkWvUQcpazmi/8nHqwvARpr1xpb+Es67oUGX/48=
|
github.com/niklasfasching/go-org v1.9.1/go.mod h1:ZAGFFkWvUQcpazmi/8nHqwvARpr1xpb+Es67oUGX/48=
|
||||||
github.com/nwaples/rardecode/v2 v2.1.0 h1:JQl9ZoBPDy+nIZGb1mx8+anfHp/LV3NE2MjMiv0ct/U=
|
github.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKiOqu0A=
|
||||||
github.com/nwaples/rardecode/v2 v2.1.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
|
github.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
|
||||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||||
@@ -714,9 +714,11 @@ github.com/smartystreets/assertions v1.1.1/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYl
|
|||||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
||||||
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8=
|
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8=
|
||||||
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
github.com/sorairolake/lzip-go v0.3.5 h1:ms5Xri9o1JBIWvOFAorYtUNik6HI3HgBTkISiqu0Cwg=
|
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
|
||||||
github.com/sorairolake/lzip-go v0.3.5/go.mod h1:N0KYq5iWrMXI0ZEXKXaS9hCyOjZUQdBDEIbXfoUwbdk=
|
github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=
|
||||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||||
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
|
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||||
@@ -729,6 +731,7 @@ github.com/steveyen/gtreap v0.1.0/go.mod h1:kl/5J7XbrOmlIbYIXdRHDDE5QxHqpk0cmkT7
|
|||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
@@ -765,8 +768,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 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
|
||||||
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
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/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.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=
|
||||||
github.com/wneessen/go-mail v0.6.2/go.mod h1:L/PYjPK3/2ZlNb2/FjEBIn9n1rUWjW+Toy531oVmeb4=
|
github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
|
||||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
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/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||||
@@ -837,9 +840,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
|
|||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
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.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||||
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=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
@@ -876,8 +878,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@@ -906,8 +908,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
|||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
@@ -930,9 +932,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
|||||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.7.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.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
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-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@@ -974,9 +975,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.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.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.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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
@@ -987,9 +987,8 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
|||||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
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.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.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||||
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=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
@@ -1003,9 +1002,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.15.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.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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
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.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||||
@@ -1041,8 +1039,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
|||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ func TestMain(m *testing.M) {
|
|||||||
unittest.MainTest(m, &unittest.TestOptions{
|
unittest.MainTest(m, &unittest.TestOptions{
|
||||||
FixtureFiles: []string{
|
FixtureFiles: []string{
|
||||||
"action_runner_token.yml",
|
"action_runner_token.yml",
|
||||||
|
"action_run.yml",
|
||||||
|
"repository.yml",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ func (run *ActionRun) IsSchedule() bool {
|
|||||||
func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) error {
|
func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) error {
|
||||||
_, err := db.GetEngine(ctx).ID(repo.ID).
|
_, err := db.GetEngine(ctx).ID(repo.ID).
|
||||||
NoAutoTime().
|
NoAutoTime().
|
||||||
|
Cols("num_action_runs", "num_closed_action_runs").
|
||||||
SetExpr("num_action_runs",
|
SetExpr("num_action_runs",
|
||||||
builder.Select("count(*)").From("action_run").
|
builder.Select("count(*)").From("action_run").
|
||||||
Where(builder.Eq{"repo_id": repo.ID}),
|
Where(builder.Eq{"repo_id": repo.ID}),
|
||||||
|
|||||||
35
models/actions/run_test.go
Normal file
35
models/actions/run_test.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUpdateRepoRunsNumbers(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
// update the number to a wrong one, the original is 3
|
||||||
|
_, err := db.GetEngine(t.Context()).ID(4).Cols("num_closed_action_runs").Update(&repo_model.Repository{
|
||||||
|
NumClosedActionRuns: 2,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||||
|
assert.Equal(t, 4, repo.NumActionRuns)
|
||||||
|
assert.Equal(t, 2, repo.NumClosedActionRuns)
|
||||||
|
|
||||||
|
// now update will correct them, only num_actionr_runs and num_closed_action_runs should be updated
|
||||||
|
err = updateRepoRunsNumbers(t.Context(), repo)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||||
|
assert.Equal(t, 5, repo.NumActionRuns)
|
||||||
|
assert.Equal(t, 3, repo.NumClosedActionRuns)
|
||||||
|
}
|
||||||
@@ -386,7 +386,7 @@ func SetNotificationStatus(ctx context.Context, notificationID int64, user *user
|
|||||||
|
|
||||||
notification.Status = status
|
notification.Status = status
|
||||||
|
|
||||||
_, err = db.GetEngine(ctx).ID(notificationID).Update(notification)
|
_, err = db.GetEngine(ctx).ID(notificationID).Cols("status").Update(notification)
|
||||||
return notification, err
|
return notification, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/json"
|
"code.gitea.io/gitea/modules/json"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/migration"
|
"code.gitea.io/gitea/modules/migration"
|
||||||
"code.gitea.io/gitea/modules/secret"
|
"code.gitea.io/gitea/modules/secret"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
@@ -123,17 +124,17 @@ func (task *Task) MigrateConfig() (*migration.MigrateOptions, error) {
|
|||||||
// decrypt credentials
|
// decrypt credentials
|
||||||
if opts.CloneAddrEncrypted != "" {
|
if opts.CloneAddrEncrypted != "" {
|
||||||
if opts.CloneAddr, err = secret.DecryptSecret(setting.SecretKey, opts.CloneAddrEncrypted); err != nil {
|
if opts.CloneAddr, err = secret.DecryptSecret(setting.SecretKey, opts.CloneAddrEncrypted); err != nil {
|
||||||
return nil, err
|
log.Error("Unable to decrypt CloneAddr, maybe SECRET_KEY is wrong: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if opts.AuthPasswordEncrypted != "" {
|
if opts.AuthPasswordEncrypted != "" {
|
||||||
if opts.AuthPassword, err = secret.DecryptSecret(setting.SecretKey, opts.AuthPasswordEncrypted); err != nil {
|
if opts.AuthPassword, err = secret.DecryptSecret(setting.SecretKey, opts.AuthPasswordEncrypted); err != nil {
|
||||||
return nil, err
|
log.Error("Unable to decrypt AuthPassword, maybe SECRET_KEY is wrong: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if opts.AuthTokenEncrypted != "" {
|
if opts.AuthTokenEncrypted != "" {
|
||||||
if opts.AuthToken, err = secret.DecryptSecret(setting.SecretKey, opts.AuthTokenEncrypted); err != nil {
|
if opts.AuthToken, err = secret.DecryptSecret(setting.SecretKey, opts.AuthTokenEncrypted); err != nil {
|
||||||
return nil, err
|
log.Error("Unable to decrypt AuthToken, maybe SECRET_KEY is wrong: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ func VerifyGPGKey(ctx context.Context, ownerID int64, keyID, token, signature st
|
|||||||
}
|
}
|
||||||
|
|
||||||
key.Verified = true
|
key.Verified = true
|
||||||
if _, err := db.GetEngine(ctx).ID(key.ID).SetExpr("verified", true).Update(new(GPGKey)); err != nil {
|
if _, err := db.GetEngine(ctx).ID(key.ID).Cols("verified").Update(key); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,13 +67,6 @@ func (key *PublicKey) OmitEmail() string {
|
|||||||
return strings.Join(strings.Split(key.Content, " ")[:2], " ")
|
return strings.Join(strings.Split(key.Content, " ")[:2], " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthorizedString returns formatted public key string for authorized_keys file.
|
|
||||||
//
|
|
||||||
// TODO: Consider dropping this function
|
|
||||||
func (key *PublicKey) AuthorizedString() string {
|
|
||||||
return AuthorizedStringForKey(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func addKey(ctx context.Context, key *PublicKey) (err error) {
|
func addKey(ctx context.Context, key *PublicKey) (err error) {
|
||||||
if len(key.Fingerprint) == 0 {
|
if len(key.Fingerprint) == 0 {
|
||||||
key.Fingerprint, err = CalcFingerprint(key.Content)
|
key.Fingerprint, err = CalcFingerprint(key.Content)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@@ -17,29 +18,13 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
// _____ __ .__ .__ .___
|
// AuthorizedStringCommentPrefix is a magic tag
|
||||||
// / _ \ __ ___/ |_| |__ ___________|__|_______ ____ __| _/
|
// some functions like RegeneratePublicKeys needs this tag to skip the keys generated by Gitea, while keep other keys
|
||||||
// / /_\ \| | \ __\ | \ / _ \_ __ \ \___ // __ \ / __ |
|
const AuthorizedStringCommentPrefix = `# gitea public key`
|
||||||
// / | \ | /| | | Y ( <_> ) | \/ |/ /\ ___// /_/ |
|
|
||||||
// \____|__ /____/ |__| |___| /\____/|__| |__/_____ \\___ >____ |
|
|
||||||
// \/ \/ \/ \/ \/
|
|
||||||
// ____ __.
|
|
||||||
// | |/ _|____ ___.__. ______
|
|
||||||
// | <_/ __ < | |/ ___/
|
|
||||||
// | | \ ___/\___ |\___ \
|
|
||||||
// |____|__ \___ > ____/____ >
|
|
||||||
// \/ \/\/ \/
|
|
||||||
//
|
|
||||||
// This file contains functions for creating authorized_keys files
|
|
||||||
//
|
|
||||||
// There is a dependence on the database within RegeneratePublicKeys however most of these functions probably belong in a module
|
|
||||||
|
|
||||||
const (
|
|
||||||
tplCommentPrefix = `# gitea public key`
|
|
||||||
tplPublicKey = tplCommentPrefix + "\n" + `command=%s,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict %s` + "\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
var sshOpLocker sync.Mutex
|
var sshOpLocker sync.Mutex
|
||||||
|
|
||||||
@@ -50,17 +35,73 @@ func WithSSHOpLocker(f func() error) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AuthorizedStringForKey creates the authorized keys string appropriate for the provided key
|
// AuthorizedStringForKey creates the authorized keys string appropriate for the provided key
|
||||||
func AuthorizedStringForKey(key *PublicKey) string {
|
func AuthorizedStringForKey(key *PublicKey) (string, error) {
|
||||||
sb := &strings.Builder{}
|
sb := &strings.Builder{}
|
||||||
_ = setting.SSH.AuthorizedKeysCommandTemplateTemplate.Execute(sb, map[string]any{
|
_, err := writeAuthorizedStringForKey(key, sb)
|
||||||
|
return sb.String(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteAuthorizedStringForValidKey writes the authorized key for the provided key. If the key is invalid, it does nothing.
|
||||||
|
func WriteAuthorizedStringForValidKey(key *PublicKey, w io.Writer) error {
|
||||||
|
validKey, err := writeAuthorizedStringForKey(key, w)
|
||||||
|
if !validKey {
|
||||||
|
log.Debug("WriteAuthorizedStringForValidKey: key %s is not valid: %v", key, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var globalVars = sync.OnceValue(func() (ret struct {
|
||||||
|
principalRegexp *regexp.Regexp
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
// principalRegexp expresses whether a principal is considered valid.
|
||||||
|
// This reverse engineers how sshd parses the authorized keys file,
|
||||||
|
// see e.g. https://github.com/openssh/openssh-portable/blob/32deb00b38b4ee2b3302f261ea1e68c04e020a08/auth2-pubkeyfile.c#L221-L256
|
||||||
|
// Any newline or # comment will be stripped when parsing, so don't allow
|
||||||
|
// those. Also, if any space or tab is present in the principal, the part
|
||||||
|
// proceeding this would be parsed as an option, so just avoid any whitespace
|
||||||
|
// altogether.
|
||||||
|
ret.principalRegexp = regexp.MustCompile(`^[^\s#]+$`)
|
||||||
|
return ret
|
||||||
|
})
|
||||||
|
|
||||||
|
func writeAuthorizedStringForKey(key *PublicKey, w io.Writer) (keyValid bool, err error) {
|
||||||
|
const tpl = AuthorizedStringCommentPrefix + "\n" + `command=%s,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict %s` + "\n"
|
||||||
|
|
||||||
|
var sshKey string
|
||||||
|
|
||||||
|
if key.Type == KeyTypePrincipal {
|
||||||
|
// TODO: actually using PublicKey to store "principal" is an abuse
|
||||||
|
if !globalVars().principalRegexp.MatchString(key.Content) {
|
||||||
|
return false, fmt.Errorf("invalid principal key: %s", key.Content)
|
||||||
|
}
|
||||||
|
sshKey = fmt.Sprintf("%s # user-%d", key.Content, key.OwnerID)
|
||||||
|
} else {
|
||||||
|
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key.Content))
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sshKeyMarshalled := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(pubKey)))
|
||||||
|
sshKey = fmt.Sprintf("%s user-%d", sshKeyMarshalled, key.OwnerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// now the key is valid, the code below could only return template/IO related errors
|
||||||
|
sbCmd := &strings.Builder{}
|
||||||
|
err = setting.SSH.AuthorizedKeysCommandTemplateTemplate.Execute(sbCmd, map[string]any{
|
||||||
"AppPath": util.ShellEscape(setting.AppPath),
|
"AppPath": util.ShellEscape(setting.AppPath),
|
||||||
"AppWorkPath": util.ShellEscape(setting.AppWorkPath),
|
"AppWorkPath": util.ShellEscape(setting.AppWorkPath),
|
||||||
"CustomConf": util.ShellEscape(setting.CustomConf),
|
"CustomConf": util.ShellEscape(setting.CustomConf),
|
||||||
"CustomPath": util.ShellEscape(setting.CustomPath),
|
"CustomPath": util.ShellEscape(setting.CustomPath),
|
||||||
"Key": key,
|
"Key": key,
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
return fmt.Sprintf(tplPublicKey, util.ShellEscape(sb.String()), key.Content)
|
return true, err
|
||||||
|
}
|
||||||
|
sshCommandEscaped := util.ShellEscape(sbCmd.String())
|
||||||
|
_, err = fmt.Fprintf(w, tpl, sshCommandEscaped, sshKey)
|
||||||
|
return true, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// appendAuthorizedKeysToFile appends new SSH keys' content to authorized_keys file.
|
// appendAuthorizedKeysToFile appends new SSH keys' content to authorized_keys file.
|
||||||
@@ -112,7 +153,7 @@ func appendAuthorizedKeysToFile(keys ...*PublicKey) error {
|
|||||||
if key.Type == KeyTypePrincipal {
|
if key.Type == KeyTypePrincipal {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, err = f.WriteString(key.AuthorizedString()); err != nil {
|
if err = WriteAuthorizedStringForValidKey(key, f); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,10 +161,9 @@ func appendAuthorizedKeysToFile(keys ...*PublicKey) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RegeneratePublicKeys regenerates the authorized_keys file
|
// RegeneratePublicKeys regenerates the authorized_keys file
|
||||||
func RegeneratePublicKeys(ctx context.Context, t io.StringWriter) error {
|
func RegeneratePublicKeys(ctx context.Context, t io.Writer) error {
|
||||||
if err := db.GetEngine(ctx).Where("type != ?", KeyTypePrincipal).Iterate(new(PublicKey), func(idx int, bean any) (err error) {
|
if err := db.GetEngine(ctx).Where("type != ?", KeyTypePrincipal).Iterate(new(PublicKey), func(idx int, bean any) (err error) {
|
||||||
_, err = t.WriteString((bean.(*PublicKey)).AuthorizedString())
|
return WriteAuthorizedStringForValidKey(bean.(*PublicKey), t)
|
||||||
return err
|
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -144,11 +184,11 @@ func RegeneratePublicKeys(ctx context.Context, t io.StringWriter) error {
|
|||||||
scanner := bufio.NewScanner(f)
|
scanner := bufio.NewScanner(f)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Text()
|
line := scanner.Text()
|
||||||
if strings.HasPrefix(line, tplCommentPrefix) {
|
if strings.HasPrefix(line, AuthorizedStringCommentPrefix) {
|
||||||
scanner.Scan()
|
scanner.Scan()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
_, err = t.WriteString(line + "\n")
|
_, err = io.WriteString(t, line+"\n")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
90
models/asymkey/ssh_key_authorized_keys_test.go
Normal file
90
models/asymkey/ssh_key_authorized_keys_test.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package asymkey
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWriteAuthorizedStringForKey(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&setting.AppPath, "/tmp/gitea")()
|
||||||
|
defer test.MockVariableValue(&setting.CustomConf, "/tmp/app.ini")()
|
||||||
|
writeKey := func(t *testing.T, key *PublicKey) (bool, string, error) {
|
||||||
|
sb := &strings.Builder{}
|
||||||
|
valid, err := writeAuthorizedStringForKey(key, sb)
|
||||||
|
return valid, sb.String(), err
|
||||||
|
}
|
||||||
|
const validKeyContent = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICV0MGX/W9IvLA4FXpIuUcdDcbj5KX4syHgsTy7soVgf`
|
||||||
|
|
||||||
|
testValid := func(t *testing.T, key *PublicKey, expected string) {
|
||||||
|
valid, content, err := writeKey(t, key)
|
||||||
|
assert.True(t, valid)
|
||||||
|
assert.Equal(t, expected, content)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testInvalid := func(t *testing.T, key *PublicKey) {
|
||||||
|
valid, content, err := writeKey(t, key)
|
||||||
|
assert.False(t, valid)
|
||||||
|
assert.Empty(t, content)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("PublicKey", func(t *testing.T) {
|
||||||
|
testValid(t, &PublicKey{
|
||||||
|
OwnerID: 123,
|
||||||
|
Content: validKeyContent + " any-comment",
|
||||||
|
Type: KeyTypeUser,
|
||||||
|
}, `# gitea public key
|
||||||
|
command="/tmp/gitea --config=/tmp/app.ini serv key-0",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICV0MGX/W9IvLA4FXpIuUcdDcbj5KX4syHgsTy7soVgf user-123
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("PublicKeyWithNewLine", func(t *testing.T) {
|
||||||
|
testValid(t, &PublicKey{
|
||||||
|
OwnerID: 123,
|
||||||
|
Content: validKeyContent + "\nany-more", // the new line should be ignored
|
||||||
|
Type: KeyTypeUser,
|
||||||
|
}, `# gitea public key
|
||||||
|
command="/tmp/gitea --config=/tmp/app.ini serv key-0",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICV0MGX/W9IvLA4FXpIuUcdDcbj5KX4syHgsTy7soVgf user-123
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("PublicKeyInvalid", func(t *testing.T) {
|
||||||
|
testInvalid(t, &PublicKey{
|
||||||
|
OwnerID: 123,
|
||||||
|
Content: validKeyContent + "any-more",
|
||||||
|
Type: KeyTypeUser,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Principal", func(t *testing.T) {
|
||||||
|
testValid(t, &PublicKey{
|
||||||
|
OwnerID: 123,
|
||||||
|
Content: "any-content",
|
||||||
|
Type: KeyTypePrincipal,
|
||||||
|
}, `# gitea public key
|
||||||
|
command="/tmp/gitea --config=/tmp/app.ini serv key-0",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict any-content # user-123
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("PrincipalInvalid", func(t *testing.T) {
|
||||||
|
testInvalid(t, &PublicKey{
|
||||||
|
OwnerID: 123,
|
||||||
|
Content: "a b",
|
||||||
|
Type: KeyTypePrincipal,
|
||||||
|
})
|
||||||
|
testInvalid(t, &PublicKey{
|
||||||
|
OwnerID: 123,
|
||||||
|
Content: "a\nb",
|
||||||
|
Type: KeyTypePrincipal,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/modules/container"
|
"code.gitea.io/gitea/modules/container"
|
||||||
@@ -27,6 +28,11 @@ import (
|
|||||||
"xorm.io/xorm"
|
"xorm.io/xorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Authorization codes should expire within 10 minutes per https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2
|
||||||
|
const oauth2AuthorizationCodeValidity = 10 * time.Minute
|
||||||
|
|
||||||
|
var ErrOAuth2AuthorizationCodeInvalidated = errors.New("oauth2 authorization code already invalidated")
|
||||||
|
|
||||||
// OAuth2Application represents an OAuth2 client (RFC 6749)
|
// OAuth2Application represents an OAuth2 client (RFC 6749)
|
||||||
type OAuth2Application struct {
|
type OAuth2Application struct {
|
||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
@@ -386,6 +392,14 @@ func (code *OAuth2AuthorizationCode) TableName() string {
|
|||||||
return "oauth2_authorization_code"
|
return "oauth2_authorization_code"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsExpired reports whether the authorization code is expired.
|
||||||
|
func (code *OAuth2AuthorizationCode) IsExpired() bool {
|
||||||
|
if code.ValidUntil.IsZero() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return code.ValidUntil <= timeutil.TimeStampNow()
|
||||||
|
}
|
||||||
|
|
||||||
// GenerateRedirectURI generates a redirect URI for a successful authorization request. State will be used if not empty.
|
// GenerateRedirectURI generates a redirect URI for a successful authorization request. State will be used if not empty.
|
||||||
func (code *OAuth2AuthorizationCode) GenerateRedirectURI(state string) (*url.URL, error) {
|
func (code *OAuth2AuthorizationCode) GenerateRedirectURI(state string) (*url.URL, error) {
|
||||||
redirect, err := url.Parse(code.RedirectURI)
|
redirect, err := url.Parse(code.RedirectURI)
|
||||||
@@ -403,8 +417,14 @@ func (code *OAuth2AuthorizationCode) GenerateRedirectURI(state string) (*url.URL
|
|||||||
|
|
||||||
// Invalidate deletes the auth code from the database to invalidate this code
|
// Invalidate deletes the auth code from the database to invalidate this code
|
||||||
func (code *OAuth2AuthorizationCode) Invalidate(ctx context.Context) error {
|
func (code *OAuth2AuthorizationCode) Invalidate(ctx context.Context) error {
|
||||||
_, err := db.GetEngine(ctx).ID(code.ID).NoAutoCondition().Delete(code)
|
affected, err := db.GetEngine(ctx).ID(code.ID).NoAutoCondition().Delete(code)
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if affected == 0 {
|
||||||
|
return ErrOAuth2AuthorizationCodeInvalidated
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateCodeChallenge validates the given verifier against the saved code challenge. This is part of the PKCE implementation.
|
// ValidateCodeChallenge validates the given verifier against the saved code challenge. This is part of the PKCE implementation.
|
||||||
@@ -472,6 +492,7 @@ func (grant *OAuth2Grant) GenerateNewAuthorizationCode(ctx context.Context, redi
|
|||||||
// for code scanners to grab sensitive tokens.
|
// for code scanners to grab sensitive tokens.
|
||||||
codeSecret := "gta_" + base32Lower.EncodeToString(rBytes)
|
codeSecret := "gta_" + base32Lower.EncodeToString(rBytes)
|
||||||
|
|
||||||
|
validUntil := time.Now().Add(oauth2AuthorizationCodeValidity)
|
||||||
code = &OAuth2AuthorizationCode{
|
code = &OAuth2AuthorizationCode{
|
||||||
Grant: grant,
|
Grant: grant,
|
||||||
GrantID: grant.ID,
|
GrantID: grant.ID,
|
||||||
@@ -479,6 +500,7 @@ func (grant *OAuth2Grant) GenerateNewAuthorizationCode(ctx context.Context, redi
|
|||||||
Code: codeSecret,
|
Code: codeSecret,
|
||||||
CodeChallenge: codeChallenge,
|
CodeChallenge: codeChallenge,
|
||||||
CodeChallengeMethod: codeChallengeMethod,
|
CodeChallengeMethod: codeChallengeMethod,
|
||||||
|
ValidUntil: timeutil.TimeStamp(validUntil.Unix()),
|
||||||
}
|
}
|
||||||
if err := db.Insert(ctx, code); err != nil {
|
if err := db.Insert(ctx, code); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -5,13 +5,45 @@ package auth_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
auth_model "code.gitea.io/gitea/models/auth"
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestOAuth2AuthorizationCodeValidity(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
t.Run("GenerateSetsValidUntil", func(t *testing.T) {
|
||||||
|
grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1})
|
||||||
|
expectedValidUntil := timeutil.TimeStamp(time.Now().Unix() + 600)
|
||||||
|
code, err := grant.GenerateNewAuthorizationCode(t.Context(), "http://127.0.0.1/", "", "")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedValidUntil, code.ValidUntil)
|
||||||
|
assert.False(t, code.IsExpired())
|
||||||
|
assert.NoError(t, code.Invalidate(t.Context()))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Expired", func(t *testing.T) {
|
||||||
|
defer timeutil.MockSet(time.Unix(2, 0).UTC())()
|
||||||
|
|
||||||
|
code := &auth_model.OAuth2AuthorizationCode{ValidUntil: timeutil.TimeStamp(1)}
|
||||||
|
assert.True(t, code.IsExpired())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("InvalidateTwice", func(t *testing.T) {
|
||||||
|
code, err := auth_model.GetOAuth2AuthorizationByCode(t.Context(), "authcode")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if assert.NotNil(t, code) {
|
||||||
|
assert.NoError(t, code.Invalidate(t.Context()))
|
||||||
|
assert.ErrorIs(t, code.Invalidate(t.Context()), auth_model.ErrOAuth2AuthorizationCodeInvalidated)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestOAuth2Application_GenerateClientSecret(t *testing.T) {
|
func TestOAuth2Application_GenerateClientSecret(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
app := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1})
|
app := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1})
|
||||||
|
|||||||
@@ -111,11 +111,11 @@ func (t *TwoFactor) SetSecret(secretString string) error {
|
|||||||
func (t *TwoFactor) ValidateTOTP(passcode string) (bool, error) {
|
func (t *TwoFactor) ValidateTOTP(passcode string) (bool, error) {
|
||||||
decodedStoredSecret, err := base64.StdEncoding.DecodeString(t.Secret)
|
decodedStoredSecret, err := base64.StdEncoding.DecodeString(t.Secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, fmt.Errorf("ValidateTOTP invalid base64: %w", err)
|
||||||
}
|
}
|
||||||
secretBytes, err := secret.AesDecrypt(t.getEncryptionKey(), decodedStoredSecret)
|
secretBytes, err := secret.AesDecrypt(t.getEncryptionKey(), decodedStoredSecret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, fmt.Errorf("ValidateTOTP unable to decrypt (maybe SECRET_KEY is wrong): %w", err)
|
||||||
}
|
}
|
||||||
secretStr := string(secretBytes)
|
secretStr := string(secretBytes)
|
||||||
return totp.Validate(passcode, secretStr), nil
|
return totp.Validate(passcode, secretStr), nil
|
||||||
|
|||||||
@@ -139,3 +139,24 @@
|
|||||||
updated: 1683636626
|
updated: 1683636626
|
||||||
need_approval: 0
|
need_approval: 0
|
||||||
approved_by: 0
|
approved_by: 0
|
||||||
|
|
||||||
|
-
|
||||||
|
id: 796
|
||||||
|
title: "update actions"
|
||||||
|
repo_id: 4
|
||||||
|
owner_id: 1
|
||||||
|
workflow_id: "artifact.yaml"
|
||||||
|
index: 191
|
||||||
|
trigger_user_id: 1
|
||||||
|
ref: "refs/heads/master"
|
||||||
|
commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0"
|
||||||
|
event: "push"
|
||||||
|
trigger_event: "push"
|
||||||
|
is_fork_pull_request: 0
|
||||||
|
status: 5
|
||||||
|
started: 1683636528
|
||||||
|
stopped: 1683636626
|
||||||
|
created: 1683636108
|
||||||
|
updated: 1683636626
|
||||||
|
need_approval: 0
|
||||||
|
approved_by: 0
|
||||||
|
|||||||
@@ -129,3 +129,18 @@
|
|||||||
status: 5
|
status: 5
|
||||||
started: 1683636528
|
started: 1683636528
|
||||||
stopped: 1683636626
|
stopped: 1683636626
|
||||||
|
|
||||||
|
-
|
||||||
|
id: 205
|
||||||
|
run_id: 796
|
||||||
|
repo_id: 4
|
||||||
|
owner_id: 1
|
||||||
|
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
|
||||||
|
is_fork_pull_request: 0
|
||||||
|
name: job_2
|
||||||
|
attempt: 1
|
||||||
|
job_id: job_2
|
||||||
|
task_id: 55
|
||||||
|
status: 3
|
||||||
|
started: 1683636528
|
||||||
|
stopped: 1683636626
|
||||||
|
|||||||
@@ -177,3 +177,24 @@
|
|||||||
log_length: 0
|
log_length: 0
|
||||||
log_size: 0
|
log_size: 0
|
||||||
log_expired: 0
|
log_expired: 0
|
||||||
|
|
||||||
|
-
|
||||||
|
id: 55
|
||||||
|
job_id: 205
|
||||||
|
attempt: 1
|
||||||
|
runner_id: 1
|
||||||
|
status: 3 # 3 is the status code for "cancelled"
|
||||||
|
started: 1683636528
|
||||||
|
stopped: 1683636626
|
||||||
|
repo_id: 4
|
||||||
|
owner_id: 1
|
||||||
|
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
|
||||||
|
is_fork_pull_request: 0
|
||||||
|
token_hash: 6d8ef48297195edcc8e22c70b3020eaa06c52976db67d39b4240c64a69a2cc1508825121b7b8394e48e00b1bf3718b2aaaab
|
||||||
|
token_salt: eeeeeeee
|
||||||
|
token_last_eight: eeeeeeee
|
||||||
|
log_filename: artifact-test2/2f/47.log
|
||||||
|
log_in_storage: 1
|
||||||
|
log_length: 707
|
||||||
|
log_size: 90179
|
||||||
|
log_expired: 0
|
||||||
|
|||||||
@@ -153,3 +153,16 @@
|
|||||||
download_count: 0
|
download_count: 0
|
||||||
size: 0
|
size: 0
|
||||||
created_unix: 946684800
|
created_unix: 946684800
|
||||||
|
|
||||||
|
-
|
||||||
|
id: 13
|
||||||
|
uuid: a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a23
|
||||||
|
repo_id: 1
|
||||||
|
issue_id: 0
|
||||||
|
release_id: 4
|
||||||
|
uploader_id: 2
|
||||||
|
comment_id: 0
|
||||||
|
name: draft-attach
|
||||||
|
download_count: 0
|
||||||
|
size: 0
|
||||||
|
created_unix: 946684800
|
||||||
|
|||||||
@@ -213,3 +213,15 @@
|
|||||||
is_deleted: false
|
is_deleted: false
|
||||||
deleted_by_id: 0
|
deleted_by_id: 0
|
||||||
deleted_unix: 0
|
deleted_unix: 0
|
||||||
|
|
||||||
|
-
|
||||||
|
id: 26
|
||||||
|
repo_id: 10
|
||||||
|
name: 'feature/1'
|
||||||
|
commit_id: '65f1bf27bc3bf70f64657658635e66094edbcb4d'
|
||||||
|
commit_message: 'Initial commit'
|
||||||
|
commit_time: 1489950479
|
||||||
|
pusher_id: 2
|
||||||
|
is_deleted: false
|
||||||
|
deleted_by_id: 0
|
||||||
|
deleted_unix: 0
|
||||||
|
|||||||
@@ -733,3 +733,10 @@
|
|||||||
type: 3
|
type: 3
|
||||||
config: "{\"IgnoreWhitespaceConflicts\":false,\"AllowMerge\":true,\"AllowRebase\":true,\"AllowRebaseMerge\":true,\"AllowSquash\":true}"
|
config: "{\"IgnoreWhitespaceConflicts\":false,\"AllowMerge\":true,\"AllowRebase\":true,\"AllowRebaseMerge\":true,\"AllowSquash\":true}"
|
||||||
created_unix: 946684810
|
created_unix: 946684810
|
||||||
|
|
||||||
|
-
|
||||||
|
id: 111
|
||||||
|
repo_id: 4
|
||||||
|
type: 10
|
||||||
|
config: "{}"
|
||||||
|
created_unix: 946684810
|
||||||
|
|||||||
@@ -110,6 +110,8 @@
|
|||||||
num_closed_milestones: 0
|
num_closed_milestones: 0
|
||||||
num_projects: 0
|
num_projects: 0
|
||||||
num_closed_projects: 1
|
num_closed_projects: 1
|
||||||
|
num_action_runs: 4
|
||||||
|
num_closed_action_runs: 3
|
||||||
is_private: false
|
is_private: false
|
||||||
is_empty: false
|
is_empty: false
|
||||||
is_archived: false
|
is_archived: false
|
||||||
|
|||||||
@@ -368,7 +368,7 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 1. update branch in database
|
// 1. update branch in database
|
||||||
if n, err := sess.Where("repo_id=? AND name=?", repo.ID, from).Update(&Branch{
|
if n, err := sess.Where("repo_id=? AND name=?", repo.ID, from).Cols("name").Update(&Branch{
|
||||||
Name: to,
|
Name: to,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -397,10 +397,16 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str
|
|||||||
|
|
||||||
if protectedBranch != nil {
|
if protectedBranch != nil {
|
||||||
// there is a protect rule for this branch
|
// there is a protect rule for this branch
|
||||||
protectedBranch.RuleName = to
|
existingRule, err := GetProtectedBranchRuleByName(ctx, repo.ID, to)
|
||||||
if _, err = sess.ID(protectedBranch.ID).Cols("branch_name").Update(protectedBranch); err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if existingRule == nil || existingRule.ID == protectedBranch.ID {
|
||||||
|
protectedBranch.RuleName = to
|
||||||
|
if _, err = sess.ID(protectedBranch.ID).Cols("branch_name").Update(protectedBranch); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// some glob protect rules may match this branch
|
// some glob protect rules may match this branch
|
||||||
protected, err := IsBranchProtected(ctx, repo.ID, from)
|
protected, err := IsBranchProtected(ctx, repo.ID, from)
|
||||||
@@ -444,7 +450,7 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str
|
|||||||
type FindRecentlyPushedNewBranchesOptions struct {
|
type FindRecentlyPushedNewBranchesOptions struct {
|
||||||
Repo *repo_model.Repository
|
Repo *repo_model.Repository
|
||||||
BaseRepo *repo_model.Repository
|
BaseRepo *repo_model.Repository
|
||||||
CommitAfterUnix int64
|
PushedAfterUnix int64
|
||||||
MaxCount int
|
MaxCount int
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,11 +460,11 @@ type RecentlyPushedNewBranch struct {
|
|||||||
BranchDisplayName string
|
BranchDisplayName string
|
||||||
BranchLink string
|
BranchLink string
|
||||||
BranchCompareURL string
|
BranchCompareURL string
|
||||||
CommitTime timeutil.TimeStamp
|
PushedTime timeutil.TimeStamp
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindRecentlyPushedNewBranches return at most 2 new branches pushed by the user in 2 hours which has no opened PRs created
|
// FindRecentlyPushedNewBranches return at most 2 new branches pushed by the user in 2 hours which has no opened PRs created
|
||||||
// if opts.CommitAfterUnix is 0, we will find the branches that were committed to in the last 2 hours
|
// if opts.PushedAfterUnix is 0, we will find the branches that were pushed in the last 2 hours
|
||||||
// if opts.ListOptions is not set, we will only display top 2 latest branches.
|
// if opts.ListOptions is not set, we will only display top 2 latest branches.
|
||||||
// Protected branches will be skipped since they are unlikely to be used to create new PRs.
|
// Protected branches will be skipped since they are unlikely to be used to create new PRs.
|
||||||
func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, opts FindRecentlyPushedNewBranchesOptions) ([]*RecentlyPushedNewBranch, error) {
|
func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, opts FindRecentlyPushedNewBranchesOptions) ([]*RecentlyPushedNewBranch, error) {
|
||||||
@@ -486,8 +492,8 @@ func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, o
|
|||||||
}
|
}
|
||||||
repoIDs := builder.Select("id").From("repository").Where(repoCond)
|
repoIDs := builder.Select("id").From("repository").Where(repoCond)
|
||||||
|
|
||||||
if opts.CommitAfterUnix == 0 {
|
if opts.PushedAfterUnix == 0 {
|
||||||
opts.CommitAfterUnix = time.Now().Add(-time.Hour * 2).Unix()
|
opts.PushedAfterUnix = time.Now().Add(-time.Hour * 2).Unix()
|
||||||
}
|
}
|
||||||
|
|
||||||
baseBranch, err := GetBranch(ctx, opts.BaseRepo.ID, opts.BaseRepo.DefaultBranch)
|
baseBranch, err := GetBranch(ctx, opts.BaseRepo.ID, opts.BaseRepo.DefaultBranch)
|
||||||
@@ -503,7 +509,7 @@ func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, o
|
|||||||
"pusher_id": doer.ID,
|
"pusher_id": doer.ID,
|
||||||
"is_deleted": false,
|
"is_deleted": false,
|
||||||
},
|
},
|
||||||
builder.Gte{"commit_time": opts.CommitAfterUnix},
|
builder.Gte{"updated_unix": opts.PushedAfterUnix},
|
||||||
builder.In("repo_id", repoIDs),
|
builder.In("repo_id", repoIDs),
|
||||||
// newly created branch have no changes, so skip them
|
// newly created branch have no changes, so skip them
|
||||||
builder.Neq{"commit_id": baseBranch.CommitID},
|
builder.Neq{"commit_id": baseBranch.CommitID},
|
||||||
@@ -556,7 +562,7 @@ func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, o
|
|||||||
BranchName: branch.Name,
|
BranchName: branch.Name,
|
||||||
BranchLink: fmt.Sprintf("%s/src/branch/%s", branch.Repo.Link(), util.PathEscapeSegments(branch.Name)),
|
BranchLink: fmt.Sprintf("%s/src/branch/%s", branch.Repo.Link(), util.PathEscapeSegments(branch.Name)),
|
||||||
BranchCompareURL: branch.Repo.ComposeBranchCompareURL(opts.BaseRepo, branch.Name),
|
BranchCompareURL: branch.Repo.ComposeBranchCompareURL(opts.BaseRepo, branch.Name),
|
||||||
CommitTime: branch.CommitTime,
|
PushedTime: branch.UpdatedUnix,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if len(newBranches) == opts.MaxCount {
|
if len(newBranches) == opts.MaxCount {
|
||||||
|
|||||||
@@ -6,14 +6,17 @@ package git_test
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
git_model "code.gitea.io/gitea/models/git"
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"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/git"
|
||||||
"code.gitea.io/gitea/modules/optional"
|
"code.gitea.io/gitea/modules/optional"
|
||||||
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@@ -63,6 +66,36 @@ func TestGetDeletedBranch(t *testing.T) {
|
|||||||
assert.NotNil(t, getDeletedBranch(t, firstBranch))
|
assert.NotNil(t, getDeletedBranch(t, firstBranch))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFindRecentlyPushedNewBranchesUsesPushTime(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
|
||||||
|
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 12})
|
||||||
|
branch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo.ID, Name: "outdated-new-branch"})
|
||||||
|
|
||||||
|
commitUnix := time.Now().Add(-3 * time.Hour).Unix()
|
||||||
|
pushUnix := time.Now().Add(-30 * time.Minute).Unix()
|
||||||
|
_, err := db.GetEngine(t.Context()).Exec(
|
||||||
|
"UPDATE branch SET commit_time = ?, updated_unix = ? WHERE id = ?",
|
||||||
|
commitUnix,
|
||||||
|
pushUnix,
|
||||||
|
branch.ID,
|
||||||
|
)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
branches, err := git_model.FindRecentlyPushedNewBranches(t.Context(), doer, git_model.FindRecentlyPushedNewBranchesOptions{
|
||||||
|
Repo: repo,
|
||||||
|
BaseRepo: repo,
|
||||||
|
PushedAfterUnix: time.Now().Add(-time.Hour).Unix(),
|
||||||
|
MaxCount: 1,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if assert.Len(t, branches, 1) {
|
||||||
|
assert.Equal(t, branch.Name, branches[0].BranchName)
|
||||||
|
assert.Equal(t, timeutil.TimeStamp(pushUnix), branches[0].PushedTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDeletedBranchLoadUser(t *testing.T) {
|
func TestDeletedBranchLoadUser(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
@@ -159,6 +192,53 @@ func TestRenameBranch(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRenameBranchProtectedRuleConflict(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
|
master := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "master"})
|
||||||
|
|
||||||
|
devBranch := &git_model.Branch{
|
||||||
|
RepoID: repo1.ID,
|
||||||
|
Name: "dev",
|
||||||
|
CommitID: master.CommitID,
|
||||||
|
CommitMessage: master.CommitMessage,
|
||||||
|
CommitTime: master.CommitTime,
|
||||||
|
PusherID: master.PusherID,
|
||||||
|
}
|
||||||
|
assert.NoError(t, db.Insert(t.Context(), devBranch))
|
||||||
|
|
||||||
|
pbDev := git_model.ProtectedBranch{
|
||||||
|
RepoID: repo1.ID,
|
||||||
|
RuleName: "dev",
|
||||||
|
CanPush: true,
|
||||||
|
}
|
||||||
|
assert.NoError(t, git_model.UpdateProtectBranch(t.Context(), repo1, &pbDev, git_model.WhitelistOptions{}))
|
||||||
|
|
||||||
|
pbMain := git_model.ProtectedBranch{
|
||||||
|
RepoID: repo1.ID,
|
||||||
|
RuleName: "main",
|
||||||
|
CanPush: true,
|
||||||
|
}
|
||||||
|
assert.NoError(t, git_model.UpdateProtectBranch(t.Context(), repo1, &pbMain, git_model.WhitelistOptions{}))
|
||||||
|
|
||||||
|
assert.NoError(t, git_model.RenameBranch(t.Context(), repo1, "dev", "main", func(ctx context.Context, isDefault bool) error {
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
unittest.AssertNotExistsBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "dev"})
|
||||||
|
unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "main"})
|
||||||
|
|
||||||
|
protectedDev, err := git_model.GetProtectedBranchRuleByName(t.Context(), repo1.ID, "dev")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, protectedDev)
|
||||||
|
assert.Equal(t, "dev", protectedDev.RuleName)
|
||||||
|
|
||||||
|
protectedMainByID, err := git_model.GetProtectedBranchRuleByID(t.Context(), repo1.ID, pbMain.ID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, protectedMainByID)
|
||||||
|
assert.Equal(t, "main", protectedMainByID.RuleName)
|
||||||
|
}
|
||||||
|
|
||||||
func TestOnlyGetDeletedBranchOnCorrectRepo(t *testing.T) {
|
func TestOnlyGetDeletedBranchOnCorrectRepo(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
|||||||
@@ -343,15 +343,12 @@ func IterateRepositoryIDsWithLFSMetaObjects(ctx context.Context, f func(ctx cont
|
|||||||
|
|
||||||
// IterateLFSMetaObjectsForRepoOptions provides options for IterateLFSMetaObjectsForRepo
|
// IterateLFSMetaObjectsForRepoOptions provides options for IterateLFSMetaObjectsForRepo
|
||||||
type IterateLFSMetaObjectsForRepoOptions struct {
|
type IterateLFSMetaObjectsForRepoOptions struct {
|
||||||
OlderThan timeutil.TimeStamp
|
OlderThan timeutil.TimeStamp
|
||||||
UpdatedLessRecentlyThan timeutil.TimeStamp
|
UpdatedLessRecentlyThan timeutil.TimeStamp
|
||||||
OrderByUpdated bool
|
|
||||||
LoopFunctionAlwaysUpdates bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IterateLFSMetaObjectsForRepo provides a iterator for LFSMetaObjects per Repo
|
// IterateLFSMetaObjectsForRepo provides a iterator for LFSMetaObjects per Repo
|
||||||
func IterateLFSMetaObjectsForRepo(ctx context.Context, repoID int64, f func(context.Context, *LFSMetaObject, int64) error, opts *IterateLFSMetaObjectsForRepoOptions) error {
|
func IterateLFSMetaObjectsForRepo(ctx context.Context, repoID int64, f func(context.Context, *LFSMetaObject, int64) error, opts *IterateLFSMetaObjectsForRepoOptions) error {
|
||||||
var start int
|
|
||||||
batchSize := setting.Database.IterateBufferSize
|
batchSize := setting.Database.IterateBufferSize
|
||||||
engine := db.GetEngine(ctx)
|
engine := db.GetEngine(ctx)
|
||||||
type CountLFSMetaObject struct {
|
type CountLFSMetaObject struct {
|
||||||
@@ -359,7 +356,7 @@ func IterateLFSMetaObjectsForRepo(ctx context.Context, repoID int64, f func(cont
|
|||||||
LFSMetaObject `xorm:"extends"`
|
LFSMetaObject `xorm:"extends"`
|
||||||
}
|
}
|
||||||
|
|
||||||
id := int64(0)
|
lastID := int64(0)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
beans := make([]*CountLFSMetaObject, 0, batchSize)
|
beans := make([]*CountLFSMetaObject, 0, batchSize)
|
||||||
@@ -372,29 +369,23 @@ func IterateLFSMetaObjectsForRepo(ctx context.Context, repoID int64, f func(cont
|
|||||||
if !opts.UpdatedLessRecentlyThan.IsZero() {
|
if !opts.UpdatedLessRecentlyThan.IsZero() {
|
||||||
sess.And("`lfs_meta_object`.updated_unix < ?", opts.UpdatedLessRecentlyThan)
|
sess.And("`lfs_meta_object`.updated_unix < ?", opts.UpdatedLessRecentlyThan)
|
||||||
}
|
}
|
||||||
sess.GroupBy("`lfs_meta_object`.id")
|
sess.GroupBy("`lfs_meta_object`.id").
|
||||||
if opts.OrderByUpdated {
|
And("`lfs_meta_object`.id > ?", lastID).
|
||||||
sess.OrderBy("`lfs_meta_object`.updated_unix ASC")
|
OrderBy("`lfs_meta_object`.id ASC")
|
||||||
} else {
|
|
||||||
sess.And("`lfs_meta_object`.id > ?", id)
|
if err := sess.Limit(batchSize).Find(&beans); err != nil {
|
||||||
sess.OrderBy("`lfs_meta_object`.id ASC")
|
|
||||||
}
|
|
||||||
if err := sess.Limit(batchSize, start).Find(&beans); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(beans) == 0 {
|
if len(beans) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if !opts.LoopFunctionAlwaysUpdates {
|
|
||||||
start += len(beans)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, bean := range beans {
|
for _, bean := range beans {
|
||||||
if err := f(ctx, &bean.LFSMetaObject, bean.Count); err != nil {
|
if err := f(ctx, &bean.LFSMetaObject, bean.Count); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
id = beans[len(beans)-1].ID
|
lastID = beans[len(beans)-1].ID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -108,10 +108,10 @@ func GetLFSLock(ctx context.Context, repo *repo_model.Repository, path string) (
|
|||||||
return rel, nil
|
return rel, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLFSLockByID returns release by given id.
|
// GetLFSLockByIDAndRepo returns lfs lock by given id and repository id.
|
||||||
func GetLFSLockByID(ctx context.Context, id int64) (*LFSLock, error) {
|
func GetLFSLockByIDAndRepo(ctx context.Context, id, repoID int64) (*LFSLock, error) {
|
||||||
lock := new(LFSLock)
|
lock := new(LFSLock)
|
||||||
has, err := db.GetEngine(ctx).ID(id).Get(lock)
|
has, err := db.GetEngine(ctx).ID(id).And("repo_id = ?", repoID).Get(lock)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if !has {
|
} else if !has {
|
||||||
@@ -160,7 +160,7 @@ func CountLFSLockByRepoID(ctx context.Context, repoID int64) (int64, error) {
|
|||||||
// DeleteLFSLockByID deletes a lock by given ID.
|
// DeleteLFSLockByID deletes a lock by given ID.
|
||||||
func DeleteLFSLockByID(ctx context.Context, id int64, repo *repo_model.Repository, u *user_model.User, force bool) (*LFSLock, error) {
|
func DeleteLFSLockByID(ctx context.Context, id int64, repo *repo_model.Repository, u *user_model.User, force bool) (*LFSLock, error) {
|
||||||
return db.WithTx2(ctx, func(ctx context.Context) (*LFSLock, error) {
|
return db.WithTx2(ctx, func(ctx context.Context) (*LFSLock, error) {
|
||||||
lock, err := GetLFSLockByID(ctx, id)
|
lock, err := GetLFSLockByIDAndRepo(ctx, id, repo.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
82
models/git/lfs_lock_test.go
Normal file
82
models/git/lfs_lock_test.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func createTestLock(t *testing.T, repo *repo_model.Repository, owner *user_model.User) *LFSLock {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
path := fmt.Sprintf("%s-%d-%d", t.Name(), repo.ID, time.Now().UnixNano())
|
||||||
|
lock, err := CreateLFSLock(t.Context(), repo, &LFSLock{
|
||||||
|
OwnerID: owner.ID,
|
||||||
|
Path: path,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
return lock
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetLFSLockByIDAndRepo(t *testing.T) {
|
||||||
|
require.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
|
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
||||||
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
|
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||||
|
|
||||||
|
lockRepo1 := createTestLock(t, repo1, user2)
|
||||||
|
lockRepo3 := createTestLock(t, repo3, user4)
|
||||||
|
|
||||||
|
fetched, err := GetLFSLockByIDAndRepo(t.Context(), lockRepo1.ID, repo1.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, lockRepo1.ID, fetched.ID)
|
||||||
|
assert.Equal(t, repo1.ID, fetched.RepoID)
|
||||||
|
|
||||||
|
_, err = GetLFSLockByIDAndRepo(t.Context(), lockRepo1.ID, repo3.ID)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.True(t, IsErrLFSLockNotExist(err))
|
||||||
|
|
||||||
|
_, err = GetLFSLockByIDAndRepo(t.Context(), lockRepo3.ID, repo1.ID)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.True(t, IsErrLFSLockNotExist(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteLFSLockByIDRequiresRepoMatch(t *testing.T) {
|
||||||
|
require.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
|
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
||||||
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
|
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||||
|
|
||||||
|
lockRepo1 := createTestLock(t, repo1, user2)
|
||||||
|
lockRepo3 := createTestLock(t, repo3, user4)
|
||||||
|
|
||||||
|
_, err := DeleteLFSLockByID(t.Context(), lockRepo3.ID, repo1, user2, true)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.True(t, IsErrLFSLockNotExist(err))
|
||||||
|
|
||||||
|
existing, err := GetLFSLockByIDAndRepo(t.Context(), lockRepo3.ID, repo3.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, lockRepo3.ID, existing.ID)
|
||||||
|
|
||||||
|
deleted, err := DeleteLFSLockByID(t.Context(), lockRepo3.ID, repo3, user4, true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, lockRepo3.ID, deleted.ID)
|
||||||
|
|
||||||
|
deleted, err = DeleteLFSLockByID(t.Context(), lockRepo1.ID, repo1, user2, false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, lockRepo1.ID, deleted.ID)
|
||||||
|
}
|
||||||
61
models/git/lfs_test.go
Normal file
61
models/git/lfs_test.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package git_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
"code.gitea.io/gitea/modules/lfs"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIterateLFSMetaObjectsForRepoUpdatesDoNotSkip(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
ctx := t.Context()
|
||||||
|
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, "user2", "repo1")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
defer test.MockVariableValue(&setting.Database.IterateBufferSize, 1)()
|
||||||
|
|
||||||
|
created := make([]*git_model.LFSMetaObject, 0, 3)
|
||||||
|
for i := range 3 {
|
||||||
|
content := []byte("gitea-lfs-" + strconv.Itoa(i))
|
||||||
|
pointer, err := lfs.GeneratePointer(bytes.NewReader(content))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
meta, err := git_model.NewLFSMetaObject(ctx, repo.ID, pointer)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
created = append(created, meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
iterated := make([]int64, 0, len(created))
|
||||||
|
cutoff := time.Now().Add(24 * time.Hour)
|
||||||
|
iterErr := git_model.IterateLFSMetaObjectsForRepo(ctx, repo.ID, func(ctx context.Context, meta *git_model.LFSMetaObject, count int64) error {
|
||||||
|
iterated = append(iterated, meta.ID)
|
||||||
|
_, err := db.GetEngine(ctx).ID(meta.ID).Cols("updated_unix").Update(&git_model.LFSMetaObject{
|
||||||
|
UpdatedUnix: timeutil.TimeStamp(time.Now().Unix()),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}, &git_model.IterateLFSMetaObjectsForRepoOptions{
|
||||||
|
OlderThan: timeutil.TimeStamp(cutoff.Unix()),
|
||||||
|
UpdatedLessRecentlyThan: timeutil.TimeStamp(cutoff.Unix()),
|
||||||
|
})
|
||||||
|
assert.NoError(t, iterErr)
|
||||||
|
|
||||||
|
expected := []int64{created[0].ID, created[1].ID, created[2].ID}
|
||||||
|
assert.Equal(t, expected, iterated)
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ package git
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -25,7 +24,7 @@ import (
|
|||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrBranchIsProtected = errors.New("branch is protected")
|
var ErrBranchIsProtected = util.ErrorWrap(util.ErrPermissionDenied, "branch is protected")
|
||||||
|
|
||||||
// ProtectedBranch struct
|
// ProtectedBranch struct
|
||||||
type ProtectedBranch struct {
|
type ProtectedBranch struct {
|
||||||
@@ -467,11 +466,13 @@ func updateApprovalWhitelist(ctx context.Context, repo *repo_model.Repository, c
|
|||||||
return currentWhitelist, nil
|
return currentWhitelist, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prUserIDs, err := access_model.GetUserIDsWithUnitAccess(ctx, repo, perm.AccessModeRead, unit.TypePullRequests)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
whitelist = make([]int64, 0, len(newWhitelist))
|
whitelist = make([]int64, 0, len(newWhitelist))
|
||||||
for _, userID := range newWhitelist {
|
for _, userID := range newWhitelist {
|
||||||
if reader, err := access_model.IsRepoReader(ctx, repo, userID); err != nil {
|
if !prUserIDs.Contains(userID) {
|
||||||
return nil, err
|
|
||||||
} else if !reader {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
whitelist = append(whitelist, userID)
|
whitelist = append(whitelist, userID)
|
||||||
|
|||||||
@@ -692,7 +692,7 @@ func (c *Comment) LoadTime(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
c.Time, err = GetTrackedTimeByID(ctx, c.TimeID)
|
c.Time, err = GetTrackedTimeByID(ctx, c.IssueID, c.TimeID)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -862,10 +862,7 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment
|
|||||||
if err = UpdateCommentAttachments(ctx, comment, opts.Attachments); err != nil {
|
if err = UpdateCommentAttachments(ctx, comment, opts.Attachments); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case CommentTypeReopen, CommentTypeClose:
|
// comment type reopen and close event have their own logic to update numbers but not here
|
||||||
if err = repo_model.UpdateRepoIssueNumbers(ctx, opts.Issue.RepoID, opts.Issue.IsPull, true); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// update the issue's updated_unix column
|
// update the issue's updated_unix column
|
||||||
return UpdateIssueCols(ctx, opts.Issue, "updated_unix")
|
return UpdateIssueCols(ctx, opts.Issue, "updated_unix")
|
||||||
|
|||||||
@@ -476,7 +476,7 @@ func applySubscribedCondition(sess *xorm.Session, subscriberID int64) {
|
|||||||
),
|
),
|
||||||
builder.Eq{"issue.poster_id": subscriberID},
|
builder.Eq{"issue.poster_id": subscriberID},
|
||||||
builder.In("issue.repo_id", builder.
|
builder.In("issue.repo_id", builder.
|
||||||
Select("id").
|
Select("repo_id").
|
||||||
From("watch").
|
From("watch").
|
||||||
Where(builder.And(builder.Eq{"user_id": subscriberID},
|
Where(builder.And(builder.Eq{"user_id": subscriberID},
|
||||||
builder.In("mode", repo_model.WatchModeNormal, repo_model.WatchModeAuto))),
|
builder.In("mode", repo_model.WatchModeNormal, repo_model.WatchModeAuto))),
|
||||||
|
|||||||
@@ -197,6 +197,12 @@ func TestIssues(t *testing.T) {
|
|||||||
},
|
},
|
||||||
[]int64{2},
|
[]int64{2},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
issues_model.IssuesOptions{
|
||||||
|
SubscriberID: 11,
|
||||||
|
},
|
||||||
|
[]int64{11, 5, 9, 8, 3, 2, 1},
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
issues, err := issues_model.Issues(t.Context(), &test.Opts)
|
issues, err := issues_model.Issues(t.Context(), &test.Opts)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|||||||
@@ -146,8 +146,19 @@ func updateIssueNumbers(ctx context.Context, issue *Issue, doer *user_model.User
|
|||||||
}
|
}
|
||||||
|
|
||||||
// update repository's issue closed number
|
// update repository's issue closed number
|
||||||
if err := repo_model.UpdateRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, true); err != nil {
|
switch cmtType {
|
||||||
return nil, err
|
case CommentTypeClose, CommentTypeMergePull:
|
||||||
|
// only increase closed count
|
||||||
|
if err := IncrRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, false); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case CommentTypeReopen:
|
||||||
|
// only decrease closed count
|
||||||
|
if err := DecrRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, false, true); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("invalid comment type: %d", cmtType)
|
||||||
}
|
}
|
||||||
|
|
||||||
return CreateComment(ctx, &CreateCommentOptions{
|
return CreateComment(ctx, &CreateCommentOptions{
|
||||||
@@ -318,7 +329,6 @@ type NewIssueOptions struct {
|
|||||||
Issue *Issue
|
Issue *Issue
|
||||||
LabelIDs []int64
|
LabelIDs []int64
|
||||||
Attachments []string // In UUID format.
|
Attachments []string // In UUID format.
|
||||||
IsPull bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewIssueWithIndex creates issue with given index
|
// NewIssueWithIndex creates issue with given index
|
||||||
@@ -369,7 +379,8 @@ func NewIssueWithIndex(ctx context.Context, doer *user_model.User, opts NewIssue
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := repo_model.UpdateRepoIssueNumbers(ctx, opts.Issue.RepoID, opts.IsPull, false); err != nil {
|
// Update repository issue total count
|
||||||
|
if err := IncrRepoIssueNumbers(ctx, opts.Repo.ID, opts.Issue.IsPull, true); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,6 +450,42 @@ func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *Issue, la
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IncrRepoIssueNumbers increments repository issue numbers.
|
||||||
|
func IncrRepoIssueNumbers(ctx context.Context, repoID int64, isPull, totalOrClosed bool) error {
|
||||||
|
dbSession := db.GetEngine(ctx)
|
||||||
|
var colName string
|
||||||
|
if totalOrClosed {
|
||||||
|
colName = util.Iif(isPull, "num_pulls", "num_issues")
|
||||||
|
} else {
|
||||||
|
colName = util.Iif(isPull, "num_closed_pulls", "num_closed_issues")
|
||||||
|
}
|
||||||
|
_, err := dbSession.Incr(colName).ID(repoID).
|
||||||
|
NoAutoCondition().NoAutoTime().
|
||||||
|
Update(new(repo_model.Repository))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecrRepoIssueNumbers decrements repository issue numbers.
|
||||||
|
func DecrRepoIssueNumbers(ctx context.Context, repoID int64, isPull, includeTotal, includeClosed bool) error {
|
||||||
|
if !includeTotal && !includeClosed {
|
||||||
|
return fmt.Errorf("no numbers to decrease for repo id %d", repoID)
|
||||||
|
}
|
||||||
|
|
||||||
|
dbSession := db.GetEngine(ctx)
|
||||||
|
if includeTotal {
|
||||||
|
colName := util.Iif(isPull, "num_pulls", "num_issues")
|
||||||
|
dbSession = dbSession.Decr(colName)
|
||||||
|
}
|
||||||
|
if includeClosed {
|
||||||
|
closedColName := util.Iif(isPull, "num_closed_pulls", "num_closed_issues")
|
||||||
|
dbSession = dbSession.Decr(closedColName)
|
||||||
|
}
|
||||||
|
_, err := dbSession.ID(repoID).
|
||||||
|
NoAutoCondition().NoAutoTime().
|
||||||
|
Update(new(repo_model.Repository))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateIssueMentions updates issue-user relations for mentioned users.
|
// UpdateIssueMentions updates issue-user relations for mentioned users.
|
||||||
func UpdateIssueMentions(ctx context.Context, issueID int64, mentions []*user_model.User) error {
|
func UpdateIssueMentions(ctx context.Context, issueID int64, mentions []*user_model.User) error {
|
||||||
if len(mentions) == 0 {
|
if len(mentions) == 0 {
|
||||||
|
|||||||
@@ -181,6 +181,7 @@ func updateMilestone(ctx context.Context, m *Milestone) error {
|
|||||||
func UpdateMilestoneCounters(ctx context.Context, id int64) error {
|
func UpdateMilestoneCounters(ctx context.Context, id int64) error {
|
||||||
e := db.GetEngine(ctx)
|
e := db.GetEngine(ctx)
|
||||||
_, err := e.ID(id).
|
_, err := e.ID(id).
|
||||||
|
Cols("num_issues", "num_closed_issues").
|
||||||
SetExpr("num_issues", builder.Select("count(*)").From("issue").Where(
|
SetExpr("num_issues", builder.Select("count(*)").From("issue").Where(
|
||||||
builder.Eq{"milestone_id": id},
|
builder.Eq{"milestone_id": id},
|
||||||
)).
|
)).
|
||||||
|
|||||||
@@ -471,13 +471,13 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *Iss
|
|||||||
|
|
||||||
issue.Index = idx
|
issue.Index = idx
|
||||||
issue.Title = util.EllipsisDisplayString(issue.Title, 255)
|
issue.Title = util.EllipsisDisplayString(issue.Title, 255)
|
||||||
|
issue.IsPull = true
|
||||||
|
|
||||||
if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{
|
if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{
|
||||||
Repo: repo,
|
Repo: repo,
|
||||||
Issue: issue,
|
Issue: issue,
|
||||||
LabelIDs: labelIDs,
|
LabelIDs: labelIDs,
|
||||||
Attachments: uuids,
|
Attachments: uuids,
|
||||||
IsPull: true,
|
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) {
|
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ func GetReviewsByIssueID(ctx context.Context, issueID int64) (latestReviews, mig
|
|||||||
reviewersMap := make(map[int64][]*Review) // key is reviewer id
|
reviewersMap := make(map[int64][]*Review) // key is reviewer id
|
||||||
originalReviewersMap := make(map[int64][]*Review) // key is original author id
|
originalReviewersMap := make(map[int64][]*Review) // key is original author id
|
||||||
reviewTeamsMap := make(map[int64][]*Review) // key is reviewer team id
|
reviewTeamsMap := make(map[int64][]*Review) // key is reviewer team id
|
||||||
countedReivewTypes := []ReviewType{ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest}
|
countedReivewTypes := []ReviewType{ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest, ReviewTypeComment}
|
||||||
for _, review := range reviews {
|
for _, review := range reviews {
|
||||||
if review.ReviewerTeamID == 0 && slices.Contains(countedReivewTypes, review.Type) && !review.Dismissed {
|
if review.ReviewerTeamID == 0 && slices.Contains(countedReivewTypes, review.Type) && !review.Dismissed {
|
||||||
if review.OriginalAuthorID != 0 {
|
if review.OriginalAuthorID != 0 {
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ func TestGetReviewersByIssueID(t *testing.T) {
|
|||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
|
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
|
||||||
|
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
|
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
|
||||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||||
@@ -129,6 +130,12 @@ func TestGetReviewersByIssueID(t *testing.T) {
|
|||||||
|
|
||||||
expectedReviews := []*issues_model.Review{}
|
expectedReviews := []*issues_model.Review{}
|
||||||
expectedReviews = append(expectedReviews,
|
expectedReviews = append(expectedReviews,
|
||||||
|
&issues_model.Review{
|
||||||
|
ID: 5,
|
||||||
|
Reviewer: user1,
|
||||||
|
Type: issues_model.ReviewTypeComment,
|
||||||
|
UpdatedUnix: 946684810,
|
||||||
|
},
|
||||||
&issues_model.Review{
|
&issues_model.Review{
|
||||||
ID: 7,
|
ID: 7,
|
||||||
Reviewer: org3,
|
Reviewer: org3,
|
||||||
@@ -167,8 +174,9 @@ func TestGetReviewersByIssueID(t *testing.T) {
|
|||||||
for _, review := range allReviews {
|
for _, review := range allReviews {
|
||||||
assert.NoError(t, review.LoadReviewer(t.Context()))
|
assert.NoError(t, review.LoadReviewer(t.Context()))
|
||||||
}
|
}
|
||||||
if assert.Len(t, allReviews, 5) {
|
if assert.Len(t, allReviews, 6) {
|
||||||
for i, review := range allReviews {
|
for i, review := range allReviews {
|
||||||
|
assert.Equal(t, expectedReviews[i].ID, review.ID)
|
||||||
assert.Equal(t, expectedReviews[i].Reviewer, review.Reviewer)
|
assert.Equal(t, expectedReviews[i].Reviewer, review.Reviewer)
|
||||||
assert.Equal(t, expectedReviews[i].Type, review.Type)
|
assert.Equal(t, expectedReviews[i].Type, review.Type)
|
||||||
assert.Equal(t, expectedReviews[i].UpdatedUnix, review.UpdatedUnix)
|
assert.Equal(t, expectedReviews[i].UpdatedUnix, review.UpdatedUnix)
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import (
|
|||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Stopwatch represents a stopwatch for time tracking.
|
// Stopwatch represents a stopwatch for time tracking.
|
||||||
@@ -232,3 +234,14 @@ func CancelStopwatch(ctx context.Context, user *user_model.User, issue *Issue) (
|
|||||||
})
|
})
|
||||||
return ok, err
|
return ok, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveStopwatchesByRepoID removes all stopwatches for a user in a specific repository
|
||||||
|
// this function should be called before removing all the issues of the repository
|
||||||
|
func RemoveStopwatchesByRepoID(ctx context.Context, userID, repoID int64) error {
|
||||||
|
_, err := db.GetEngine(ctx).
|
||||||
|
Where("`stopwatch`.user_id = ?", userID).
|
||||||
|
And(builder.In("`stopwatch`.issue_id",
|
||||||
|
builder.Select("id").From("issue").Where(builder.Eq{"repo_id": repoID}))).
|
||||||
|
Delete(new(Stopwatch))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|||||||
@@ -311,13 +311,13 @@ func deleteTime(ctx context.Context, t *TrackedTime) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetTrackedTimeByID returns raw TrackedTime without loading attributes by id
|
// GetTrackedTimeByID returns raw TrackedTime without loading attributes by id
|
||||||
func GetTrackedTimeByID(ctx context.Context, id int64) (*TrackedTime, error) {
|
func GetTrackedTimeByID(ctx context.Context, issueID, trackedTimeID int64) (*TrackedTime, error) {
|
||||||
time := new(TrackedTime)
|
time := new(TrackedTime)
|
||||||
has, err := db.GetEngine(ctx).ID(id).Get(time)
|
has, err := db.GetEngine(ctx).ID(trackedTimeID).Where("issue_id = ?", issueID).Get(time)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if !has {
|
} else if !has {
|
||||||
return nil, db.ErrNotExist{Resource: "tracked_time", ID: id}
|
return nil, db.ErrNotExist{Resource: "tracked_time", ID: trackedTimeID}
|
||||||
}
|
}
|
||||||
return time, nil
|
return time, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ func UpdateOpenMilestoneCounts(x *xorm.Engine) error {
|
|||||||
|
|
||||||
for _, id := range openMilestoneIDs {
|
for _, id := range openMilestoneIDs {
|
||||||
_, err := x.ID(id).
|
_, err := x.ID(id).
|
||||||
|
Cols("num_issues", "num_closed_issues").
|
||||||
SetExpr("num_issues", builder.Select("count(*)").From("issue").Where(
|
SetExpr("num_issues", builder.Select("count(*)").From("issue").Where(
|
||||||
builder.Eq{"milestone_id": id},
|
builder.Eq{"milestone_id": id},
|
||||||
)).
|
)).
|
||||||
|
|||||||
@@ -429,6 +429,10 @@ func HasOrgOrUserVisible(ctx context.Context, orgOrUser, user *user_model.User)
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !setting.Service.RequireSignInViewStrict && orgOrUser.Visibility == structs.VisibleTypePublic {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
if (orgOrUser.Visibility == structs.VisibleTypePrivate || user.IsRestricted) && !OrgFromUser(orgOrUser).hasMemberWithUserID(ctx, user.ID) {
|
if (orgOrUser.Visibility == structs.VisibleTypePrivate || user.IsRestricted) && !OrgFromUser(orgOrUser).hasMemberWithUserID(ctx, user.ID) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ import (
|
|||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/structs"
|
"code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -382,6 +384,12 @@ func TestHasOrgVisibleTypePublic(t *testing.T) {
|
|||||||
assert.True(t, test1) // owner of org
|
assert.True(t, test1) // owner of org
|
||||||
assert.True(t, test2) // user not a part of org
|
assert.True(t, test2) // user not a part of org
|
||||||
assert.True(t, test3) // logged out user
|
assert.True(t, test3) // logged out user
|
||||||
|
|
||||||
|
restrictedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29, IsRestricted: true})
|
||||||
|
require.True(t, restrictedUser.IsRestricted)
|
||||||
|
assert.True(t, organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), restrictedUser))
|
||||||
|
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
|
||||||
|
assert.False(t, organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), restrictedUser))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHasOrgVisibleTypeLimited(t *testing.T) {
|
func TestHasOrgVisibleTypeLimited(t *testing.T) {
|
||||||
|
|||||||
@@ -53,24 +53,45 @@ func RemoveTeamRepo(ctx context.Context, teamID, repoID int64) error {
|
|||||||
// GetTeamsWithAccessToAnyRepoUnit returns all teams in an organization that have given access level to the repository special unit.
|
// GetTeamsWithAccessToAnyRepoUnit returns all teams in an organization that have given access level to the repository special unit.
|
||||||
// This function is only used for finding some teams that can be used as branch protection allowlist or reviewers, it isn't really used for access control.
|
// This function is only used for finding some teams that can be used as branch protection allowlist or reviewers, it isn't really used for access control.
|
||||||
// FIXME: TEAM-UNIT-PERMISSION this logic is not complete, search the fixme keyword to see more details
|
// FIXME: TEAM-UNIT-PERMISSION this logic is not complete, search the fixme keyword to see more details
|
||||||
func GetTeamsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) ([]*Team, error) {
|
func GetTeamsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) (teams []*Team, err error) {
|
||||||
teams := make([]*Team, 0, 5)
|
teamIDs, err := getTeamIDsWithAccessToAnyRepoUnit(ctx, orgID, repoID, mode, unitType, unitTypesMore...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(teamIDs) == 0 {
|
||||||
|
return teams, nil
|
||||||
|
}
|
||||||
|
err = db.GetEngine(ctx).Where(builder.In("id", teamIDs)).OrderBy("team.name").Find(&teams)
|
||||||
|
return teams, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTeamIDsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) (teamIDs []int64, err error) {
|
||||||
sub := builder.Select("team_id").From("team_unit").
|
sub := builder.Select("team_id").From("team_unit").
|
||||||
Where(builder.Expr("team_unit.team_id = team.id")).
|
Where(builder.Expr("team_unit.team_id = team.id")).
|
||||||
And(builder.In("team_unit.type", append([]unit.Type{unitType}, unitTypesMore...))).
|
And(builder.In("team_unit.type", append([]unit.Type{unitType}, unitTypesMore...))).
|
||||||
And(builder.Expr("team_unit.access_mode >= ?", mode))
|
And(builder.Expr("team_unit.access_mode >= ?", mode))
|
||||||
|
|
||||||
err := db.GetEngine(ctx).
|
err = db.GetEngine(ctx).
|
||||||
|
Select("team.id").
|
||||||
|
Table("team").
|
||||||
Join("INNER", "team_repo", "team_repo.team_id = team.id").
|
Join("INNER", "team_repo", "team_repo.team_id = team.id").
|
||||||
And("team_repo.org_id = ?", orgID).
|
And("team_repo.org_id = ? AND team_repo.repo_id = ?", orgID, repoID).
|
||||||
And("team_repo.repo_id = ?", repoID).
|
|
||||||
And(builder.Or(
|
And(builder.Or(
|
||||||
builder.Expr("team.authorize >= ?", mode),
|
builder.Expr("team.authorize >= ?", mode),
|
||||||
builder.In("team.id", sub),
|
builder.In("team.id", sub),
|
||||||
)).
|
)).
|
||||||
OrderBy("name").
|
Find(&teamIDs)
|
||||||
Find(&teams)
|
return teamIDs, err
|
||||||
|
}
|
||||||
return teams, err
|
|
||||||
|
func GetTeamUserIDsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) (userIDs []int64, err error) {
|
||||||
|
teamIDs, err := getTeamIDsWithAccessToAnyRepoUnit(ctx, orgID, repoID, mode, unitType, unitTypesMore...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(teamIDs) == 0 {
|
||||||
|
return userIDs, nil
|
||||||
|
}
|
||||||
|
err = db.GetEngine(ctx).Table("team_user").Select("uid").Where(builder.In("team_id", teamIDs)).Find(&userIDs)
|
||||||
|
return userIDs, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,13 +43,15 @@ func GetOrInsertBlob(ctx context.Context, pb *PackageBlob) (*PackageBlob, bool,
|
|||||||
|
|
||||||
existing := &PackageBlob{}
|
existing := &PackageBlob{}
|
||||||
|
|
||||||
has, err := e.Where(builder.Eq{
|
hashCond := builder.Eq{
|
||||||
"size": pb.Size,
|
"size": pb.Size,
|
||||||
"hash_md5": pb.HashMD5,
|
"hash_md5": pb.HashMD5,
|
||||||
"hash_sha1": pb.HashSHA1,
|
"hash_sha1": pb.HashSHA1,
|
||||||
"hash_sha256": pb.HashSHA256,
|
"hash_sha256": pb.HashSHA256,
|
||||||
"hash_sha512": pb.HashSHA512,
|
"hash_sha512": pb.HashSHA512,
|
||||||
}).Get(existing)
|
}
|
||||||
|
|
||||||
|
has, err := e.Where(hashCond).Get(existing)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
@@ -57,6 +59,11 @@ func GetOrInsertBlob(ctx context.Context, pb *PackageBlob) (*PackageBlob, bool,
|
|||||||
return existing, true, nil
|
return existing, true, nil
|
||||||
}
|
}
|
||||||
if _, err = e.Insert(pb); err != nil {
|
if _, err = e.Insert(pb); err != nil {
|
||||||
|
// Handle race condition: another request may have inserted the same blob
|
||||||
|
// between our SELECT and INSERT. Retry the SELECT to get the existing blob.
|
||||||
|
if has, _ = e.Where(hashCond).Get(existing); has {
|
||||||
|
return existing, true, nil
|
||||||
|
}
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
return pb, false, nil
|
return pb, false, nil
|
||||||
|
|||||||
51
models/packages/package_blob_test.go
Normal file
51
models/packages/package_blob_test.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package packages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetOrInsertBlobConcurrent(t *testing.T) {
|
||||||
|
require.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
testBlob := PackageBlob{
|
||||||
|
Size: 123,
|
||||||
|
HashMD5: "md5",
|
||||||
|
HashSHA1: "sha1",
|
||||||
|
HashSHA256: "sha256",
|
||||||
|
HashSHA512: "sha512",
|
||||||
|
}
|
||||||
|
|
||||||
|
const numGoroutines = 3
|
||||||
|
var wg errgroup.Group
|
||||||
|
results := make([]*PackageBlob, numGoroutines)
|
||||||
|
existed := make([]bool, numGoroutines)
|
||||||
|
for idx := range numGoroutines {
|
||||||
|
wg.Go(func() error {
|
||||||
|
blob := testBlob // Create a copy of the test blob for each goroutine
|
||||||
|
var err error
|
||||||
|
results[idx], existed[idx], err = GetOrInsertBlob(t.Context(), &blob)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
require.NoError(t, wg.Wait())
|
||||||
|
|
||||||
|
// then: all GetOrInsertBlob succeeds with the same blob ID, and only one indicates it did not exist before
|
||||||
|
existedCount := 0
|
||||||
|
assert.NotNil(t, results[0])
|
||||||
|
for i := range numGoroutines {
|
||||||
|
assert.Equal(t, results[0].ID, results[i].ID)
|
||||||
|
if existed[i] {
|
||||||
|
existedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.Equal(t, numGoroutines-1, existedCount)
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@ import (
|
|||||||
"code.gitea.io/gitea/models/perm"
|
"code.gitea.io/gitea/models/perm"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/structs"
|
||||||
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
@@ -41,7 +43,12 @@ func accessLevel(ctx context.Context, user *user_model.User, repo *repo_model.Re
|
|||||||
restricted = user.IsRestricted
|
restricted = user.IsRestricted
|
||||||
}
|
}
|
||||||
|
|
||||||
if !restricted && !repo.IsPrivate {
|
if err := repo.LoadOwner(ctx); err != nil {
|
||||||
|
return mode, err
|
||||||
|
}
|
||||||
|
|
||||||
|
repoIsFullyPublic := !setting.Service.RequireSignInViewStrict && repo.Owner.Visibility == structs.VisibleTypePublic && !repo.IsPrivate
|
||||||
|
if (restricted && repoIsFullyPublic) || (!restricted && !repo.IsPrivate) {
|
||||||
mode = perm.AccessModeRead
|
mode = perm.AccessModeRead
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@@ -51,7 +52,14 @@ func TestAccessLevel(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, perm_model.AccessModeNone, level)
|
assert.Equal(t, perm_model.AccessModeNone, level)
|
||||||
|
|
||||||
// restricted user has no access to a public repo
|
// restricted user has default access to a public repo if no sign-in is required
|
||||||
|
setting.Service.RequireSignInViewStrict = false
|
||||||
|
level, err = access_model.AccessLevel(t.Context(), user29, repo1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, perm_model.AccessModeRead, level)
|
||||||
|
|
||||||
|
// restricted user has no access to a public repo if sign-in is required
|
||||||
|
setting.Service.RequireSignInViewStrict = true
|
||||||
level, err = access_model.AccessLevel(t.Context(), user29, repo1)
|
level, err = access_model.AccessLevel(t.Context(), user29, repo1)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, perm_model.AccessModeNone, level)
|
assert.Equal(t, perm_model.AccessModeNone, level)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/container"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
@@ -458,54 +459,44 @@ func HasAnyUnitAccess(ctx context.Context, userID int64, repo *repo_model.Reposi
|
|||||||
return perm.HasAnyUnitAccess(), nil
|
return perm.HasAnyUnitAccess(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getUsersWithAccessMode returns users that have at least given access mode to the repository.
|
func GetUsersWithUnitAccess(ctx context.Context, repo *repo_model.Repository, mode perm_model.AccessMode, unitType unit.Type) (users []*user_model.User, err error) {
|
||||||
func getUsersWithAccessMode(ctx context.Context, repo *repo_model.Repository, mode perm_model.AccessMode) (_ []*user_model.User, err error) {
|
userIDs, err := GetUserIDsWithUnitAccess(ctx, repo, mode, unitType)
|
||||||
if err = repo.LoadOwner(ctx); err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if len(userIDs) == 0 {
|
||||||
e := db.GetEngine(ctx)
|
return users, nil
|
||||||
accesses := make([]*Access, 0, 10)
|
}
|
||||||
if err = e.Where("repo_id = ? AND mode >= ?", repo.ID, mode).Find(&accesses); err != nil {
|
if err = db.GetEngine(ctx).In("id", userIDs.Values()).OrderBy("`name`").Find(&users); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leave a seat for owner itself to append later, but if owner is an organization
|
|
||||||
// and just waste 1 unit is cheaper than re-allocate memory once.
|
|
||||||
users := make([]*user_model.User, 0, len(accesses)+1)
|
|
||||||
if len(accesses) > 0 {
|
|
||||||
userIDs := make([]int64, len(accesses))
|
|
||||||
for i := 0; i < len(accesses); i++ {
|
|
||||||
userIDs[i] = accesses[i].UserID
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = e.In("id", userIDs).Find(&users); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !repo.Owner.IsOrganization() {
|
|
||||||
users = append(users, repo.Owner)
|
|
||||||
}
|
|
||||||
|
|
||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRepoReaders returns all users that have explicit read access or higher to the repository.
|
func GetUserIDsWithUnitAccess(ctx context.Context, repo *repo_model.Repository, mode perm_model.AccessMode, unitType unit.Type) (container.Set[int64], error) {
|
||||||
func GetRepoReaders(ctx context.Context, repo *repo_model.Repository) (_ []*user_model.User, err error) {
|
userIDs := container.Set[int64]{}
|
||||||
return getUsersWithAccessMode(ctx, repo, perm_model.AccessModeRead)
|
e := db.GetEngine(ctx)
|
||||||
}
|
accesses := make([]*Access, 0, 10)
|
||||||
|
if err := e.Where("repo_id = ? AND mode >= ?", repo.ID, mode).Find(&accesses); err != nil {
|
||||||
// GetRepoWriters returns all users that have write access to the repository.
|
return nil, err
|
||||||
func GetRepoWriters(ctx context.Context, repo *repo_model.Repository) (_ []*user_model.User, err error) {
|
|
||||||
return getUsersWithAccessMode(ctx, repo, perm_model.AccessModeWrite)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsRepoReader returns true if user has explicit read access or higher to the repository.
|
|
||||||
func IsRepoReader(ctx context.Context, repo *repo_model.Repository, userID int64) (bool, error) {
|
|
||||||
if repo.OwnerID == userID {
|
|
||||||
return true, nil
|
|
||||||
}
|
}
|
||||||
return db.GetEngine(ctx).Where("repo_id = ? AND user_id = ? AND mode >= ?", repo.ID, userID, perm_model.AccessModeRead).Get(&Access{})
|
for _, a := range accesses {
|
||||||
|
userIDs.Add(a.UserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := repo.LoadOwner(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !repo.Owner.IsOrganization() {
|
||||||
|
userIDs.Add(repo.Owner.ID)
|
||||||
|
} else {
|
||||||
|
teamUserIDs, err := organization.GetTeamUserIDsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, mode, unitType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
userIDs.AddMultiple(teamUserIDs...)
|
||||||
|
}
|
||||||
|
return userIDs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckRepoUnitUser check whether user could visit the unit of this repository
|
// CheckRepoUnitUser check whether user could visit the unit of this repository
|
||||||
|
|||||||
@@ -169,9 +169,9 @@ func TestGetUserRepoPermission(t *testing.T) {
|
|||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||||
team := &organization.Team{OrgID: org.ID, LowerName: "test_team"}
|
team := &organization.Team{OrgID: org.ID, LowerName: "test_team"}
|
||||||
require.NoError(t, db.Insert(ctx, team))
|
require.NoError(t, db.Insert(ctx, team))
|
||||||
|
require.NoError(t, db.Insert(ctx, &organization.TeamUser{OrgID: org.ID, TeamID: team.ID, UID: user.ID}))
|
||||||
|
|
||||||
t.Run("DoerInTeamWithNoRepo", func(t *testing.T) {
|
t.Run("DoerInTeamWithNoRepo", func(t *testing.T) {
|
||||||
require.NoError(t, db.Insert(ctx, &organization.TeamUser{OrgID: org.ID, TeamID: team.ID, UID: user.ID}))
|
|
||||||
perm, err := GetUserRepoPermission(ctx, repo32, user)
|
perm, err := GetUserRepoPermission(ctx, repo32, user)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode)
|
assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode)
|
||||||
@@ -219,6 +219,15 @@ func TestGetUserRepoPermission(t *testing.T) {
|
|||||||
assert.Equal(t, perm_model.AccessModeNone, perm.AccessMode)
|
assert.Equal(t, perm_model.AccessModeNone, perm.AccessMode)
|
||||||
assert.Equal(t, perm_model.AccessModeNone, perm.unitsMode[unit.TypeCode])
|
assert.Equal(t, perm_model.AccessModeNone, perm.unitsMode[unit.TypeCode])
|
||||||
assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeIssues])
|
assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeIssues])
|
||||||
|
|
||||||
|
users, err := GetUsersWithUnitAccess(ctx, repo3, perm_model.AccessModeRead, unit.TypeIssues)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, users, 1)
|
||||||
|
assert.Equal(t, user.ID, users[0].ID)
|
||||||
|
|
||||||
|
users, err = GetUsersWithUnitAccess(ctx, repo3, perm_model.AccessModeWrite, unit.TypeIssues)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Empty(t, users)
|
||||||
})
|
})
|
||||||
|
|
||||||
require.NoError(t, db.Insert(ctx, repo_model.Collaboration{RepoID: repo3.ID, UserID: user.ID, Mode: perm_model.AccessModeWrite}))
|
require.NoError(t, db.Insert(ctx, repo_model.Collaboration{RepoID: repo3.ID, UserID: user.ID, Mode: perm_model.AccessModeWrite}))
|
||||||
@@ -229,5 +238,10 @@ func TestGetUserRepoPermission(t *testing.T) {
|
|||||||
assert.Equal(t, perm_model.AccessModeWrite, perm.AccessMode)
|
assert.Equal(t, perm_model.AccessModeWrite, perm.AccessMode)
|
||||||
assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeCode])
|
assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeCode])
|
||||||
assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeIssues])
|
assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeIssues])
|
||||||
|
|
||||||
|
users, err := GetUsersWithUnitAccess(ctx, repo3, perm_model.AccessModeWrite, unit.TypeIssues)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, users, 1)
|
||||||
|
assert.Equal(t, user.ID, users[0].ID)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,6 +213,18 @@ func GetColumn(ctx context.Context, columnID int64) (*Column, error) {
|
|||||||
return column, nil
|
return column, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetColumnByIDAndProjectID(ctx context.Context, columnID, projectID int64) (*Column, error) {
|
||||||
|
column := new(Column)
|
||||||
|
has, err := db.GetEngine(ctx).ID(columnID).And("project_id=?", projectID).Get(column)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if !has {
|
||||||
|
return nil, ErrProjectColumnNotExist{ColumnID: columnID}
|
||||||
|
}
|
||||||
|
|
||||||
|
return column, nil
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateColumn updates a project column
|
// UpdateColumn updates a project column
|
||||||
func UpdateColumn(ctx context.Context, column *Column) error {
|
func UpdateColumn(ctx context.Context, column *Column) error {
|
||||||
var fieldToUpdate []string
|
var fieldToUpdate []string
|
||||||
|
|||||||
@@ -302,6 +302,18 @@ func GetProjectByID(ctx context.Context, id int64) (*Project, error) {
|
|||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetProjectByIDAndOwner(ctx context.Context, id, ownerID int64) (*Project, error) {
|
||||||
|
p := new(Project)
|
||||||
|
has, err := db.GetEngine(ctx).ID(id).And("owner_id = ?", ownerID).Get(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if !has {
|
||||||
|
return nil, ErrProjectNotExist{ID: id}
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetProjectForRepoByID returns the projects in a repository
|
// GetProjectForRepoByID returns the projects in a repository
|
||||||
func GetProjectForRepoByID(ctx context.Context, repoID, id int64) (*Project, error) {
|
func GetProjectForRepoByID(ctx context.Context, repoID, id int64) (*Project, error) {
|
||||||
p := new(Project)
|
p := new(Project)
|
||||||
|
|||||||
@@ -49,6 +49,19 @@ func init() {
|
|||||||
db.RegisterModel(new(ReviewState))
|
db.RegisterModel(new(ReviewState))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rs *ReviewState) GetViewedFileCount() int {
|
||||||
|
if len(rs.UpdatedFiles) == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
var numViewedFiles int
|
||||||
|
for _, state := range rs.UpdatedFiles {
|
||||||
|
if state == Viewed {
|
||||||
|
numViewedFiles++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return numViewedFiles
|
||||||
|
}
|
||||||
|
|
||||||
// GetReviewState returns the ReviewState with all given values prefilled, whether or not it exists in the database.
|
// GetReviewState returns the ReviewState with all given values prefilled, whether or not it exists in the database.
|
||||||
// If the review didn't exist before in the database, it won't afterwards either.
|
// If the review didn't exist before in the database, it won't afterwards either.
|
||||||
// The returned boolean shows whether the review exists in the database
|
// The returned boolean shows whether the review exists in the database
|
||||||
@@ -60,18 +73,18 @@ func GetReviewState(ctx context.Context, userID, pullID int64, commitSHA string)
|
|||||||
|
|
||||||
// UpdateReviewState updates the given review inside the database, regardless of whether it existed before or not
|
// UpdateReviewState updates the given review inside the database, regardless of whether it existed before or not
|
||||||
// The given map of files with their viewed state will be merged with the previous review, if present
|
// The given map of files with their viewed state will be merged with the previous review, if present
|
||||||
func UpdateReviewState(ctx context.Context, userID, pullID int64, commitSHA string, updatedFiles map[string]ViewedState) error {
|
func UpdateReviewState(ctx context.Context, userID, pullID int64, commitSHA string, updatedFiles map[string]ViewedState) (*ReviewState, error) {
|
||||||
log.Trace("Updating review for user %d, repo %d, commit %s with the updated files %v.", userID, pullID, commitSHA, updatedFiles)
|
log.Trace("Updating review for user %d, repo %d, commit %s with the updated files %v.", userID, pullID, commitSHA, updatedFiles)
|
||||||
|
|
||||||
review, exists, err := GetReviewState(ctx, userID, pullID, commitSHA)
|
review, exists, err := GetReviewState(ctx, userID, pullID, commitSHA)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if exists {
|
if exists {
|
||||||
review.UpdatedFiles = mergeFiles(review.UpdatedFiles, updatedFiles)
|
review.UpdatedFiles = mergeFiles(review.UpdatedFiles, updatedFiles)
|
||||||
} else if previousReview, err := getNewestReviewStateApartFrom(ctx, userID, pullID, commitSHA); err != nil {
|
} else if previousReview, err := getNewestReviewStateApartFrom(ctx, userID, pullID, commitSHA); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
|
|
||||||
// Overwrite the viewed files of the previous review if present
|
// Overwrite the viewed files of the previous review if present
|
||||||
} else if previousReview != nil {
|
} else if previousReview != nil {
|
||||||
@@ -85,11 +98,11 @@ func UpdateReviewState(ctx context.Context, userID, pullID int64, commitSHA stri
|
|||||||
if !exists {
|
if !exists {
|
||||||
log.Trace("Inserting new review for user %d, repo %d, commit %s with the updated files %v.", userID, pullID, commitSHA, review.UpdatedFiles)
|
log.Trace("Inserting new review for user %d, repo %d, commit %s with the updated files %v.", userID, pullID, commitSHA, review.UpdatedFiles)
|
||||||
_, err := engine.Insert(review)
|
_, err := engine.Insert(review)
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
log.Trace("Updating already existing review with ID %d (user %d, repo %d, commit %s) with the updated files %v.", review.ID, userID, pullID, commitSHA, review.UpdatedFiles)
|
log.Trace("Updating already existing review with ID %d (user %d, repo %d, commit %s) with the updated files %v.", review.ID, userID, pullID, commitSHA, review.UpdatedFiles)
|
||||||
_, err = engine.ID(review.ID).Update(&ReviewState{UpdatedFiles: review.UpdatedFiles})
|
_, err = engine.ID(review.ID).Cols("updated_files").Update(review)
|
||||||
return err
|
return review, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// mergeFiles merges the given maps of files with their viewing state into one map.
|
// mergeFiles merges the given maps of files with their viewing state into one map.
|
||||||
|
|||||||
@@ -166,6 +166,11 @@ func GetAttachmentByReleaseIDFileName(ctx context.Context, releaseID int64, file
|
|||||||
return attach, nil
|
return attach, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetUnlinkedAttachmentsByUserID(ctx context.Context, userID int64) ([]*Attachment, error) {
|
||||||
|
attachments := make([]*Attachment, 0, 10)
|
||||||
|
return attachments, db.GetEngine(ctx).Where("uploader_id = ? AND issue_id = 0 AND release_id = 0 AND comment_id = 0", userID).Find(&attachments)
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteAttachment deletes the given attachment and optionally the associated file.
|
// DeleteAttachment deletes the given attachment and optionally the associated file.
|
||||||
func DeleteAttachment(ctx context.Context, a *Attachment, remove bool) error {
|
func DeleteAttachment(ctx context.Context, a *Attachment, remove bool) error {
|
||||||
_, err := DeleteAttachments(ctx, []*Attachment{a}, remove)
|
_, err := DeleteAttachments(ctx, []*Attachment{a}, remove)
|
||||||
|
|||||||
@@ -101,3 +101,19 @@ func TestGetAttachmentsByUUIDs(t *testing.T) {
|
|||||||
assert.Equal(t, int64(1), attachList[0].IssueID)
|
assert.Equal(t, int64(1), attachList[0].IssueID)
|
||||||
assert.Equal(t, int64(5), attachList[1].IssueID)
|
assert.Equal(t, int64(5), attachList[1].IssueID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetUnlinkedAttachmentsByUserID(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
attachments, err := repo_model.GetUnlinkedAttachmentsByUserID(t.Context(), 8)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, attachments, 1)
|
||||||
|
assert.Equal(t, int64(10), attachments[0].ID)
|
||||||
|
assert.Zero(t, attachments[0].IssueID)
|
||||||
|
assert.Zero(t, attachments[0].ReleaseID)
|
||||||
|
assert.Zero(t, attachments[0].CommentID)
|
||||||
|
|
||||||
|
attachments, err = repo_model.GetUnlinkedAttachmentsByUserID(t.Context(), 1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Empty(t, attachments)
|
||||||
|
}
|
||||||
|
|||||||
@@ -93,15 +93,25 @@ func init() {
|
|||||||
db.RegisterModel(new(Release))
|
db.RegisterModel(new(Release))
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadAttributes load repo and publisher attributes for a release
|
// LegacyAttachmentMissingRepoIDCutoff marks the date when repo_id started to be written during uploads
|
||||||
func (r *Release) LoadAttributes(ctx context.Context) error {
|
// (2026-01-16T00:00:00Z). Older rows might have repo_id=0 and should be tolerated once.
|
||||||
var err error
|
const LegacyAttachmentMissingRepoIDCutoff timeutil.TimeStamp = 1768521600
|
||||||
if r.Repo == nil {
|
|
||||||
r.Repo, err = GetRepositoryByID(ctx, r.RepoID)
|
func (r *Release) LoadRepo(ctx context.Context) (err error) {
|
||||||
if err != nil {
|
if r.Repo != nil {
|
||||||
return err
|
return nil
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r.Repo, err = GetRepositoryByID(ctx, r.RepoID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadAttributes load repo and publisher attributes for a release
|
||||||
|
func (r *Release) LoadAttributes(ctx context.Context) (err error) {
|
||||||
|
if err := r.LoadRepo(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if r.Publisher == nil {
|
if r.Publisher == nil {
|
||||||
r.Publisher, err = user_model.GetUserByID(ctx, r.PublisherID)
|
r.Publisher, err = user_model.GetUserByID(ctx, r.PublisherID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -168,6 +178,11 @@ func UpdateReleaseNumCommits(ctx context.Context, rel *Release) error {
|
|||||||
|
|
||||||
// AddReleaseAttachments adds a release attachments
|
// AddReleaseAttachments adds a release attachments
|
||||||
func AddReleaseAttachments(ctx context.Context, releaseID int64, attachmentUUIDs []string) (err error) {
|
func AddReleaseAttachments(ctx context.Context, releaseID int64, attachmentUUIDs []string) (err error) {
|
||||||
|
rel, err := GetReleaseByID(ctx, releaseID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Check attachments
|
// Check attachments
|
||||||
attachments, err := GetAttachmentsByUUIDs(ctx, attachmentUUIDs)
|
attachments, err := GetAttachmentsByUUIDs(ctx, attachmentUUIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -175,6 +190,17 @@ func AddReleaseAttachments(ctx context.Context, releaseID int64, attachmentUUIDs
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i := range attachments {
|
for i := range attachments {
|
||||||
|
if attachments[i].RepoID == 0 && attachments[i].CreatedUnix < LegacyAttachmentMissingRepoIDCutoff {
|
||||||
|
attachments[i].RepoID = rel.RepoID
|
||||||
|
if _, err = db.GetEngine(ctx).ID(attachments[i].ID).Cols("repo_id").Update(attachments[i]); err != nil {
|
||||||
|
return fmt.Errorf("update attachment repo_id [%d]: %w", attachments[i].ID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if attachments[i].RepoID != rel.RepoID {
|
||||||
|
return util.NewPermissionDeniedErrorf("attachment belongs to different repository")
|
||||||
|
}
|
||||||
|
|
||||||
if attachments[i].ReleaseID != 0 {
|
if attachments[i].ReleaseID != 0 {
|
||||||
return util.NewPermissionDeniedErrorf("release permission denied")
|
return util.NewPermissionDeniedErrorf("release permission denied")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ package repo
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@@ -37,3 +39,54 @@ func Test_FindTagsByCommitIDs(t *testing.T) {
|
|||||||
assert.Equal(t, "delete-tag", rels[1].TagName)
|
assert.Equal(t, "delete-tag", rels[1].TagName)
|
||||||
assert.Equal(t, "v1.0", rels[2].TagName)
|
assert.Equal(t, "v1.0", rels[2].TagName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAddReleaseAttachmentsRejectsDifferentRepo(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
uuid := "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12" // attachment 2 belongs to repo 2
|
||||||
|
err := AddReleaseAttachments(t.Context(), 1, []string{uuid})
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.ErrorIs(t, err, util.ErrPermissionDenied)
|
||||||
|
|
||||||
|
attach, err := GetAttachmentByUUID(t.Context(), uuid)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Zero(t, attach.ReleaseID, "attachment should not be linked to release on failure")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddReleaseAttachmentsAllowsLegacyMissingRepoID(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
legacyUUID := "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a20" // attachment 10 has repo_id 0
|
||||||
|
err := AddReleaseAttachments(t.Context(), 1, []string{legacyUUID})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
attach, err := GetAttachmentByUUID(t.Context(), legacyUUID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, 1, attach.RepoID)
|
||||||
|
assert.EqualValues(t, 1, attach.ReleaseID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddReleaseAttachmentsRejectsRecentZeroRepoID(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
recentUUID := "a0eebc99-9c0b-4ef8-bb6d-6bb9bd3800aa"
|
||||||
|
attachment := &Attachment{
|
||||||
|
UUID: recentUUID,
|
||||||
|
RepoID: 0,
|
||||||
|
IssueID: 0,
|
||||||
|
ReleaseID: 0,
|
||||||
|
CommentID: 0,
|
||||||
|
Name: "recent-zero",
|
||||||
|
CreatedUnix: LegacyAttachmentMissingRepoIDCutoff + 1,
|
||||||
|
}
|
||||||
|
assert.NoError(t, db.Insert(t.Context(), attachment))
|
||||||
|
|
||||||
|
err := AddReleaseAttachments(t.Context(), 1, []string{recentUUID})
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.ErrorIs(t, err, util.ErrPermissionDenied)
|
||||||
|
|
||||||
|
attach, err := GetAttachmentByUUID(t.Context(), recentUUID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Zero(t, attach.ReleaseID)
|
||||||
|
assert.Zero(t, attach.RepoID)
|
||||||
|
}
|
||||||
|
|||||||
@@ -642,6 +642,17 @@ func SearchRepositoryIDsByCondition(ctx context.Context, cond builder.Cond) ([]i
|
|||||||
Find(&repoIDs)
|
Find(&repoIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func userAllPublicRepoCond(cond builder.Cond, orgVisibilityLimit []structs.VisibleType) builder.Cond {
|
||||||
|
return cond.Or(builder.And(
|
||||||
|
builder.Eq{"`repository`.is_private": false},
|
||||||
|
// Aren't in a private organisation or limited organisation if we're not logged in
|
||||||
|
builder.NotIn("`repository`.owner_id", builder.Select("id").From("`user`").Where(
|
||||||
|
builder.And(
|
||||||
|
builder.Eq{"type": user_model.UserTypeOrganization},
|
||||||
|
builder.In("visibility", orgVisibilityLimit)),
|
||||||
|
))))
|
||||||
|
}
|
||||||
|
|
||||||
// AccessibleRepositoryCondition takes a user a returns a condition for checking if a repository is accessible
|
// AccessibleRepositoryCondition takes a user a returns a condition for checking if a repository is accessible
|
||||||
func AccessibleRepositoryCondition(user *user_model.User, unitType unit.Type) builder.Cond {
|
func AccessibleRepositoryCondition(user *user_model.User, unitType unit.Type) builder.Cond {
|
||||||
cond := builder.NewCond()
|
cond := builder.NewCond()
|
||||||
@@ -651,15 +662,8 @@ func AccessibleRepositoryCondition(user *user_model.User, unitType unit.Type) bu
|
|||||||
if user == nil || user.ID <= 0 {
|
if user == nil || user.ID <= 0 {
|
||||||
orgVisibilityLimit = append(orgVisibilityLimit, structs.VisibleTypeLimited)
|
orgVisibilityLimit = append(orgVisibilityLimit, structs.VisibleTypeLimited)
|
||||||
}
|
}
|
||||||
// 1. Be able to see all non-private repositories that either:
|
// 1. Be able to see all non-private repositories
|
||||||
cond = cond.Or(builder.And(
|
cond = userAllPublicRepoCond(cond, orgVisibilityLimit)
|
||||||
builder.Eq{"`repository`.is_private": false},
|
|
||||||
// 2. Aren't in an private organisation or limited organisation if we're not logged in
|
|
||||||
builder.NotIn("`repository`.owner_id", builder.Select("id").From("`user`").Where(
|
|
||||||
builder.And(
|
|
||||||
builder.Eq{"type": user_model.UserTypeOrganization},
|
|
||||||
builder.In("visibility", orgVisibilityLimit)),
|
|
||||||
))))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if user != nil {
|
if user != nil {
|
||||||
@@ -683,6 +687,9 @@ func AccessibleRepositoryCondition(user *user_model.User, unitType unit.Type) bu
|
|||||||
if !user.IsRestricted {
|
if !user.IsRestricted {
|
||||||
// 5. Be able to see all public repos in private organizations that we are an org_user of
|
// 5. Be able to see all public repos in private organizations that we are an org_user of
|
||||||
cond = cond.Or(userOrgPublicRepoCond(user.ID))
|
cond = cond.Or(userOrgPublicRepoCond(user.ID))
|
||||||
|
} else if !setting.Service.RequireSignInViewStrict {
|
||||||
|
orgVisibilityLimit := []structs.VisibleType{structs.VisibleTypePrivate, structs.VisibleTypeLimited}
|
||||||
|
cond = userAllPublicRepoCond(cond, orgVisibilityLimit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,14 @@ import (
|
|||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/optional"
|
"code.gitea.io/gitea/modules/optional"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getTestCases() []struct {
|
func getTestCases() []struct {
|
||||||
@@ -182,7 +187,16 @@ func getTestCases() []struct {
|
|||||||
|
|
||||||
func TestSearchRepository(t *testing.T) {
|
func TestSearchRepository(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
t.Run("SearchRepositoryPublic", testSearchRepositoryPublic)
|
||||||
|
t.Run("SearchRepositoryPublicRestricted", testSearchRepositoryRestricted)
|
||||||
|
t.Run("SearchRepositoryPrivate", testSearchRepositoryPrivate)
|
||||||
|
t.Run("SearchRepositoryNonExistingOwner", testSearchRepositoryNonExistingOwner)
|
||||||
|
t.Run("SearchRepositoryWithInDescription", testSearchRepositoryWithInDescription)
|
||||||
|
t.Run("SearchRepositoryNotInDescription", testSearchRepositoryNotInDescription)
|
||||||
|
t.Run("SearchRepositoryCases", testSearchRepositoryCases)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSearchRepositoryPublic(t *testing.T) {
|
||||||
// test search public repository on explore page
|
// test search public repository on explore page
|
||||||
repos, count, err := repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{
|
repos, count, err := repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{
|
||||||
ListOptions: db.ListOptions{
|
ListOptions: db.ListOptions{
|
||||||
@@ -211,9 +225,54 @@ func TestSearchRepository(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, int64(2), count)
|
assert.Equal(t, int64(2), count)
|
||||||
assert.Len(t, repos, 2)
|
assert.Len(t, repos, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSearchRepositoryRestricted(t *testing.T) {
|
||||||
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
|
restrictedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29, IsRestricted: true})
|
||||||
|
|
||||||
|
performSearch := func(t *testing.T, user *user_model.User) (publicRepoIDs []int64) {
|
||||||
|
repos, count, err := repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{
|
||||||
|
ListOptions: db.ListOptions{Page: 1, PageSize: 10000},
|
||||||
|
Actor: user,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, repos, int(count))
|
||||||
|
for _, repo := range repos {
|
||||||
|
require.NoError(t, repo.LoadOwner(t.Context()))
|
||||||
|
if repo.Owner.Visibility == structs.VisibleTypePublic && !repo.IsPrivate {
|
||||||
|
publicRepoIDs = append(publicRepoIDs, repo.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return publicRepoIDs
|
||||||
|
}
|
||||||
|
|
||||||
|
normalPublicRepoIDs := performSearch(t, user2)
|
||||||
|
require.Greater(t, len(normalPublicRepoIDs), 10) // quite a lot
|
||||||
|
|
||||||
|
t.Run("RestrictedUser-NoSignInRequirement", func(t *testing.T) {
|
||||||
|
// restricted user can also see public repositories if no "required sign-in"
|
||||||
|
repoIDs := performSearch(t, restrictedUser)
|
||||||
|
assert.ElementsMatch(t, normalPublicRepoIDs, repoIDs)
|
||||||
|
})
|
||||||
|
|
||||||
|
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
|
||||||
|
|
||||||
|
t.Run("NormalUser-RequiredSignIn", func(t *testing.T) {
|
||||||
|
// normal user can still see all public repos, not affected by "required sign-in"
|
||||||
|
repoIDs := performSearch(t, user2)
|
||||||
|
assert.ElementsMatch(t, normalPublicRepoIDs, repoIDs)
|
||||||
|
})
|
||||||
|
t.Run("RestrictedUser-RequiredSignIn", func(t *testing.T) {
|
||||||
|
// restricted user can see only their own repo
|
||||||
|
repoIDs := performSearch(t, restrictedUser)
|
||||||
|
assert.Equal(t, []int64{4}, repoIDs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSearchRepositoryPrivate(t *testing.T) {
|
||||||
// test search private repository on explore page
|
// test search private repository on explore page
|
||||||
repos, count, err = repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{
|
repos, count, err := repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{
|
||||||
ListOptions: db.ListOptions{
|
ListOptions: db.ListOptions{
|
||||||
Page: 1,
|
Page: 1,
|
||||||
PageSize: 10,
|
PageSize: 10,
|
||||||
@@ -242,16 +301,18 @@ func TestSearchRepository(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, int64(3), count)
|
assert.Equal(t, int64(3), count)
|
||||||
assert.Len(t, repos, 3)
|
assert.Len(t, repos, 3)
|
||||||
|
}
|
||||||
|
|
||||||
// Test non existing owner
|
func testSearchRepositoryNonExistingOwner(t *testing.T) {
|
||||||
repos, count, err = repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{OwnerID: unittest.NonexistentID})
|
repos, count, err := repo_model.SearchRepositoryByName(t.Context(), repo_model.SearchRepoOptions{OwnerID: unittest.NonexistentID})
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Empty(t, repos)
|
assert.Empty(t, repos)
|
||||||
assert.Equal(t, int64(0), count)
|
assert.Equal(t, int64(0), count)
|
||||||
|
}
|
||||||
|
|
||||||
// Test search within description
|
func testSearchRepositoryWithInDescription(t *testing.T) {
|
||||||
repos, count, err = repo_model.SearchRepository(t.Context(), repo_model.SearchRepoOptions{
|
repos, count, err := repo_model.SearchRepository(t.Context(), repo_model.SearchRepoOptions{
|
||||||
ListOptions: db.ListOptions{
|
ListOptions: db.ListOptions{
|
||||||
Page: 1,
|
Page: 1,
|
||||||
PageSize: 10,
|
PageSize: 10,
|
||||||
@@ -266,9 +327,10 @@ func TestSearchRepository(t *testing.T) {
|
|||||||
assert.Equal(t, "test_repo_14", repos[0].Name)
|
assert.Equal(t, "test_repo_14", repos[0].Name)
|
||||||
}
|
}
|
||||||
assert.Equal(t, int64(1), count)
|
assert.Equal(t, int64(1), count)
|
||||||
|
}
|
||||||
|
|
||||||
// Test NOT search within description
|
func testSearchRepositoryNotInDescription(t *testing.T) {
|
||||||
repos, count, err = repo_model.SearchRepository(t.Context(), repo_model.SearchRepoOptions{
|
repos, count, err := repo_model.SearchRepository(t.Context(), repo_model.SearchRepoOptions{
|
||||||
ListOptions: db.ListOptions{
|
ListOptions: db.ListOptions{
|
||||||
Page: 1,
|
Page: 1,
|
||||||
PageSize: 10,
|
PageSize: 10,
|
||||||
@@ -281,7 +343,9 @@ func TestSearchRepository(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Empty(t, repos)
|
assert.Empty(t, repos)
|
||||||
assert.Equal(t, int64(0), count)
|
assert.Equal(t, int64(0), count)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSearchRepositoryCases(t *testing.T) {
|
||||||
testCases := getTestCases()
|
testCases := getTestCases()
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ func RemoveTopicsFromRepo(ctx context.Context, repoID int64) error {
|
|||||||
builder.In("id",
|
builder.In("id",
|
||||||
builder.Select("topic_id").From("repo_topic").Where(builder.Eq{"repo_id": repoID}),
|
builder.Select("topic_id").From("repo_topic").Where(builder.Eq{"repo_id": repoID}),
|
||||||
),
|
),
|
||||||
).Cols("repo_count").SetExpr("repo_count", "repo_count-1").Update(&Topic{})
|
).Decr("repo_count").Update(&Topic{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,16 +127,9 @@ func DeleteUploads(ctx context.Context, uploads ...*Upload) (err error) {
|
|||||||
|
|
||||||
for _, upload := range uploads {
|
for _, upload := range uploads {
|
||||||
localPath := upload.LocalPath()
|
localPath := upload.LocalPath()
|
||||||
isFile, err := util.IsFile(localPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Unable to check if %s is a file. Error: %v", localPath, err)
|
|
||||||
}
|
|
||||||
if !isFile {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := util.Remove(localPath); err != nil {
|
if err := util.Remove(localPath); err != nil {
|
||||||
return fmt.Errorf("remove upload: %w", err)
|
// just continue, don't fail the whole operation if a file is missing (removed by others)
|
||||||
|
log.Error("unable to remove upload file %s: %v", localPath, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -176,3 +176,13 @@ func WatchIfAuto(ctx context.Context, userID, repoID int64, isWrite bool) error
|
|||||||
}
|
}
|
||||||
return watchRepoMode(ctx, watch, WatchModeAuto)
|
return watchRepoMode(ctx, watch, WatchModeAuto)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClearRepoWatches clears all watches for a repository and from the user that watched it.
|
||||||
|
// Used when a repository is set to private.
|
||||||
|
func ClearRepoWatches(ctx context.Context, repoID int64) error {
|
||||||
|
if _, err := db.Exec(ctx, "UPDATE `repository` SET num_watches = 0 WHERE id = ?", repoID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.DeleteBeans(ctx, Watch{RepoID: repoID})
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestIsWatching(t *testing.T) {
|
func TestIsWatching(t *testing.T) {
|
||||||
@@ -119,3 +120,21 @@ func TestWatchIfAuto(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, watchers, prevCount)
|
assert.Len(t, watchers, prevCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestClearRepoWatches(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
const repoID int64 = 1
|
||||||
|
watchers, err := repo_model.GetRepoWatchersIDs(t.Context(), repoID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, watchers)
|
||||||
|
|
||||||
|
assert.NoError(t, repo_model.ClearRepoWatches(t.Context(), repoID))
|
||||||
|
|
||||||
|
watchers, err = repo_model.GetRepoWatchersIDs(t.Context(), repoID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Empty(t, watchers)
|
||||||
|
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
|
||||||
|
assert.Zero(t, repo.NumWatches)
|
||||||
|
}
|
||||||
|
|||||||
@@ -178,8 +178,8 @@ func GetSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) (map[
|
|||||||
for _, secret := range append(ownerSecrets, repoSecrets...) {
|
for _, secret := range append(ownerSecrets, repoSecrets...) {
|
||||||
v, err := secret_module.DecryptSecret(setting.SecretKey, secret.Data)
|
v, err := secret_module.DecryptSecret(setting.SecretKey, secret.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("decrypt secret %v %q: %v", secret.ID, secret.Name, err)
|
log.Error("Unable to decrypt Actions secret %v %q, maybe SECRET_KEY is wrong: %v", secret.ID, secret.Name, err)
|
||||||
return nil, err
|
continue
|
||||||
}
|
}
|
||||||
secrets[secret.Name] = v
|
secrets[secret.Name] = v
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -276,17 +276,22 @@ func updateActivation(ctx context.Context, email *EmailAddress, activate bool) e
|
|||||||
return UpdateUserCols(ctx, user, "rands")
|
return UpdateUserCols(ctx, user, "rands")
|
||||||
}
|
}
|
||||||
|
|
||||||
func MakeActiveEmailPrimary(ctx context.Context, emailID int64) error {
|
func MakeActiveEmailPrimary(ctx context.Context, ownerID, emailID int64) error {
|
||||||
return makeEmailPrimaryInternal(ctx, emailID, true)
|
return makeEmailPrimaryInternal(ctx, ownerID, emailID, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func MakeInactiveEmailPrimary(ctx context.Context, emailID int64) error {
|
func MakeInactiveEmailPrimary(ctx context.Context, ownerID, emailID int64) error {
|
||||||
return makeEmailPrimaryInternal(ctx, emailID, false)
|
return makeEmailPrimaryInternal(ctx, ownerID, emailID, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeEmailPrimaryInternal(ctx context.Context, emailID int64, isActive bool) error {
|
func makeEmailPrimaryInternal(ctx context.Context, ownerID, emailID int64, isActive bool) error {
|
||||||
email := &EmailAddress{}
|
email := &EmailAddress{}
|
||||||
if has, err := db.GetEngine(ctx).ID(emailID).Where(builder.Eq{"is_activated": isActive}).Get(email); err != nil {
|
if has, err := db.GetEngine(ctx).ID(emailID).
|
||||||
|
Where(builder.Eq{
|
||||||
|
"uid": ownerID,
|
||||||
|
"is_activated": isActive,
|
||||||
|
}).
|
||||||
|
Get(email); err != nil {
|
||||||
return err
|
return err
|
||||||
} else if !has {
|
} else if !has {
|
||||||
return ErrEmailAddressNotExist{}
|
return ErrEmailAddressNotExist{}
|
||||||
@@ -336,7 +341,7 @@ func ChangeInactivePrimaryEmail(ctx context.Context, uid int64, oldEmailAddr, ne
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return MakeInactiveEmailPrimary(ctx, newEmail.ID)
|
return MakeInactiveEmailPrimary(ctx, uid, newEmail.ID)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,22 +46,22 @@ func TestIsEmailUsed(t *testing.T) {
|
|||||||
func TestMakeEmailPrimary(t *testing.T) {
|
func TestMakeEmailPrimary(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
err := user_model.MakeActiveEmailPrimary(t.Context(), 9999999)
|
err := user_model.MakeActiveEmailPrimary(t.Context(), 1, 9999999)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.ErrorIs(t, err, user_model.ErrEmailAddressNotExist{})
|
assert.ErrorIs(t, err, user_model.ErrEmailAddressNotExist{})
|
||||||
|
|
||||||
email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user11@example.com"})
|
email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user11@example.com"})
|
||||||
err = user_model.MakeActiveEmailPrimary(t.Context(), email.ID)
|
err = user_model.MakeActiveEmailPrimary(t.Context(), email.UID, email.ID)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.ErrorIs(t, err, user_model.ErrEmailAddressNotExist{}) // inactive email is considered as not exist for "MakeActiveEmailPrimary"
|
assert.ErrorIs(t, err, user_model.ErrEmailAddressNotExist{}) // inactive email is considered as not exist for "MakeActiveEmailPrimary"
|
||||||
|
|
||||||
email = unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user9999999@example.com"})
|
email = unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user9999999@example.com"})
|
||||||
err = user_model.MakeActiveEmailPrimary(t.Context(), email.ID)
|
err = user_model.MakeActiveEmailPrimary(t.Context(), email.UID, email.ID)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.True(t, user_model.IsErrUserNotExist(err))
|
assert.True(t, user_model.IsErrUserNotExist(err))
|
||||||
|
|
||||||
email = unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user101@example.com"})
|
email = unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user101@example.com"})
|
||||||
err = user_model.MakeActiveEmailPrimary(t.Context(), email.ID)
|
err = user_model.MakeActiveEmailPrimary(t.Context(), email.UID, email.ID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
user, _ := user_model.GetUserByID(t.Context(), int64(10))
|
user, _ := user_model.GetUserByID(t.Context(), int64(10))
|
||||||
|
|||||||
@@ -102,7 +102,13 @@ func DeleteUserOpenID(ctx context.Context, openid *UserOpenID) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ToggleUserOpenIDVisibility toggles visibility of an openid address of given user.
|
// ToggleUserOpenIDVisibility toggles visibility of an openid address of given user.
|
||||||
func ToggleUserOpenIDVisibility(ctx context.Context, id int64) (err error) {
|
func ToggleUserOpenIDVisibility(ctx context.Context, id int64, user *User) error {
|
||||||
_, err = db.GetEngine(ctx).Exec("update `user_open_id` set `show` = not `show` where `id` = ?", id)
|
affected, err := db.GetEngine(ctx).Exec("update `user_open_id` set `show` = not `show` where `id` = ? AND uid = ?", id, user.ID)
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if n, _ := affected.RowsAffected(); n != 1 {
|
||||||
|
return util.NewNotExistErrorf("OpenID is unknown")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -33,12 +34,14 @@ func TestGetUserOpenIDs(t *testing.T) {
|
|||||||
|
|
||||||
func TestToggleUserOpenIDVisibility(t *testing.T) {
|
func TestToggleUserOpenIDVisibility(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
user, err := user_model.GetUserByID(t.Context(), int64(2))
|
||||||
|
require.NoError(t, err)
|
||||||
oids, err := user_model.GetUserOpenIDs(t.Context(), int64(2))
|
oids, err := user_model.GetUserOpenIDs(t.Context(), int64(2))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, oids, 1)
|
require.Len(t, oids, 1)
|
||||||
assert.True(t, oids[0].Show)
|
assert.True(t, oids[0].Show)
|
||||||
|
|
||||||
err = user_model.ToggleUserOpenIDVisibility(t.Context(), oids[0].ID)
|
err = user_model.ToggleUserOpenIDVisibility(t.Context(), oids[0].ID, user)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
oids, err = user_model.GetUserOpenIDs(t.Context(), int64(2))
|
oids, err = user_model.GetUserOpenIDs(t.Context(), int64(2))
|
||||||
@@ -46,7 +49,7 @@ func TestToggleUserOpenIDVisibility(t *testing.T) {
|
|||||||
require.Len(t, oids, 1)
|
require.Len(t, oids, 1)
|
||||||
|
|
||||||
assert.False(t, oids[0].Show)
|
assert.False(t, oids[0].Show)
|
||||||
err = user_model.ToggleUserOpenIDVisibility(t.Context(), oids[0].ID)
|
err = user_model.ToggleUserOpenIDVisibility(t.Context(), oids[0].ID, user)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
oids, err = user_model.GetUserOpenIDs(t.Context(), int64(2))
|
oids, err = user_model.GetUserOpenIDs(t.Context(), int64(2))
|
||||||
@@ -55,3 +58,13 @@ func TestToggleUserOpenIDVisibility(t *testing.T) {
|
|||||||
assert.True(t, oids[0].Show)
|
assert.True(t, oids[0].Show)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestToggleUserOpenIDVisibilityRequiresOwnership(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
unauthorizedUser, err := user_model.GetUserByID(t.Context(), int64(2))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = user_model.ToggleUserOpenIDVisibility(t.Context(), int64(1), unauthorizedUser)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.ErrorIs(t, err, util.ErrNotExist)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1444,3 +1444,15 @@ func DisabledFeaturesWithLoginType(user *User) *container.Set[string] {
|
|||||||
}
|
}
|
||||||
return &setting.Admin.UserDisabledFeatures
|
return &setting.Admin.UserDisabledFeatures
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserOrOrgByName returns the user or org by name
|
||||||
|
func GetUserOrOrgByName(ctx context.Context, name string) (*User, error) {
|
||||||
|
var u User
|
||||||
|
has, err := db.GetEngine(ctx).Where("lower_name = ?", strings.ToLower(name)).Get(&u)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if !has {
|
||||||
|
return nil, ErrUserNotExist{Name: name}
|
||||||
|
}
|
||||||
|
return &u, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ package actions
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -13,6 +12,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/glob"
|
"code.gitea.io/gitea/modules/glob"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
|
|
||||||
"github.com/nektos/act/pkg/jobparser"
|
"github.com/nektos/act/pkg/jobparser"
|
||||||
@@ -77,7 +77,7 @@ func GetContentFromEntry(entry *git.TreeEntry) ([]byte, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
content, err := io.ReadAll(f)
|
content, err := util.ReadWithLimit(f, 1024*1024)
|
||||||
_ = f.Close()
|
_ = f.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -26,9 +26,11 @@ type ConvertOpts struct {
|
|||||||
KeepBOM bool
|
KeepBOM bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ToUTF8WithFallbackReaderPrefetchSize = 16 * 1024
|
||||||
|
|
||||||
// ToUTF8WithFallbackReader detects the encoding of content and converts to UTF-8 reader if possible
|
// ToUTF8WithFallbackReader detects the encoding of content and converts to UTF-8 reader if possible
|
||||||
func ToUTF8WithFallbackReader(rd io.Reader, opts ConvertOpts) io.Reader {
|
func ToUTF8WithFallbackReader(rd io.Reader, opts ConvertOpts) io.Reader {
|
||||||
buf := make([]byte, 2048)
|
buf := make([]byte, ToUTF8WithFallbackReaderPrefetchSize)
|
||||||
n, err := util.ReadAtMost(rd, buf)
|
n, err := util.ReadAtMost(rd, buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return io.MultiReader(bytes.NewReader(MaybeRemoveBOM(buf[:n], opts)), rd)
|
return io.MultiReader(bytes.NewReader(MaybeRemoveBOM(buf[:n], opts)), rd)
|
||||||
@@ -41,6 +43,7 @@ func ToUTF8WithFallbackReader(rd io.Reader, opts ConvertOpts) io.Reader {
|
|||||||
|
|
||||||
encoding, _ := charset.Lookup(charsetLabel)
|
encoding, _ := charset.Lookup(charsetLabel)
|
||||||
if encoding == nil {
|
if encoding == nil {
|
||||||
|
log.Error("Unknown encoding: %s", charsetLabel)
|
||||||
return io.MultiReader(bytes.NewReader(buf[:n]), rd)
|
return io.MultiReader(bytes.NewReader(buf[:n]), rd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,17 +57,18 @@ func ToUTF8WithFallbackReader(rd io.Reader, opts ConvertOpts) io.Reader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ToUTF8 converts content to UTF8 encoding
|
// ToUTF8 converts content to UTF8 encoding
|
||||||
func ToUTF8(content []byte, opts ConvertOpts) (string, error) {
|
func ToUTF8(content []byte, opts ConvertOpts) ([]byte, error) {
|
||||||
charsetLabel, err := DetectEncoding(content)
|
charsetLabel, err := DetectEncoding(content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return content, err
|
||||||
} else if charsetLabel == "UTF-8" {
|
} else if charsetLabel == "UTF-8" {
|
||||||
return string(MaybeRemoveBOM(content, opts)), nil
|
return MaybeRemoveBOM(content, opts), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
encoding, _ := charset.Lookup(charsetLabel)
|
encoding, _ := charset.Lookup(charsetLabel)
|
||||||
if encoding == nil {
|
if encoding == nil {
|
||||||
return string(content), fmt.Errorf("Unknown encoding: %s", charsetLabel)
|
log.Error("Unknown encoding: %s", charsetLabel)
|
||||||
|
return content, fmt.Errorf("unknown encoding: %s", charsetLabel)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is an error, we concatenate the nicely decoded part and the
|
// If there is an error, we concatenate the nicely decoded part and the
|
||||||
@@ -76,7 +80,7 @@ func ToUTF8(content []byte, opts ConvertOpts) (string, error) {
|
|||||||
|
|
||||||
result = MaybeRemoveBOM(result, opts)
|
result = MaybeRemoveBOM(result, opts)
|
||||||
|
|
||||||
return string(result), err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToUTF8WithFallback detects the encoding of content and converts to UTF-8 if possible
|
// ToUTF8WithFallback detects the encoding of content and converts to UTF-8 if possible
|
||||||
@@ -94,6 +98,7 @@ func ToUTF8DropErrors(content []byte, opts ConvertOpts) []byte {
|
|||||||
|
|
||||||
encoding, _ := charset.Lookup(charsetLabel)
|
encoding, _ := charset.Lookup(charsetLabel)
|
||||||
if encoding == nil {
|
if encoding == nil {
|
||||||
|
log.Error("Unknown encoding: %s", charsetLabel)
|
||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,28 +135,37 @@ func MaybeRemoveBOM(content []byte, opts ConvertOpts) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DetectEncoding detect the encoding of content
|
// DetectEncoding detect the encoding of content
|
||||||
func DetectEncoding(content []byte) (string, error) {
|
// it always returns a detected or guessed "encoding" string, no matter error happens or not
|
||||||
|
func DetectEncoding(content []byte) (encoding string, _ error) {
|
||||||
// First we check if the content represents valid utf8 content excepting a truncated character at the end.
|
// First we check if the content represents valid utf8 content excepting a truncated character at the end.
|
||||||
|
|
||||||
// Now we could decode all the runes in turn but this is not necessarily the cheapest thing to do
|
// Now we could decode all the runes in turn but this is not necessarily the cheapest thing to do
|
||||||
// instead we walk backwards from the end to trim off a the incomplete character
|
// instead we walk backwards from the end to trim off the incomplete character
|
||||||
toValidate := content
|
toValidate := content
|
||||||
end := len(toValidate) - 1
|
end := len(toValidate) - 1
|
||||||
|
|
||||||
if end < 0 {
|
// U+0000 U+007F 0yyyzzzz
|
||||||
// no-op
|
// U+0080 U+07FF 110xxxyy 10yyzzzz
|
||||||
} else if toValidate[end]>>5 == 0b110 {
|
// U+0800 U+FFFF 1110wwww 10xxxxyy 10yyzzzz
|
||||||
// Incomplete 1 byte extension e.g. © <c2><a9> which has been truncated to <c2>
|
// U+010000 U+10FFFF 11110uvv 10vvwwww 10xxxxyy 10yyzzzz
|
||||||
toValidate = toValidate[:end]
|
cnt := 0
|
||||||
} else if end > 0 && toValidate[end]>>6 == 0b10 && toValidate[end-1]>>4 == 0b1110 {
|
for end >= 0 && cnt < 4 {
|
||||||
// Incomplete 2 byte extension e.g. ⛔ <e2><9b><94> which has been truncated to <e2><9b>
|
c := toValidate[end]
|
||||||
toValidate = toValidate[:end-1]
|
if c>>5 == 0b110 || c>>4 == 0b1110 || c>>3 == 0b11110 {
|
||||||
} else if end > 1 && toValidate[end]>>6 == 0b10 && toValidate[end-1]>>6 == 0b10 && toValidate[end-2]>>3 == 0b11110 {
|
// a leading byte
|
||||||
// Incomplete 3 byte extension e.g. 💩 <f0><9f><92><a9> which has been truncated to <f0><9f><92>
|
toValidate = toValidate[:end]
|
||||||
toValidate = toValidate[:end-2]
|
break
|
||||||
|
} else if c>>6 == 0b10 {
|
||||||
|
// a continuation byte
|
||||||
|
end--
|
||||||
|
} else {
|
||||||
|
// not an utf-8 byte
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cnt++
|
||||||
}
|
}
|
||||||
|
|
||||||
if utf8.Valid(toValidate) {
|
if utf8.Valid(toValidate) {
|
||||||
log.Debug("Detected encoding: utf-8 (fast)")
|
|
||||||
return "UTF-8", nil
|
return "UTF-8", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,7 +174,7 @@ func DetectEncoding(content []byte) (string, error) {
|
|||||||
if len(content) < 1024 {
|
if len(content) < 1024 {
|
||||||
// Check if original content is valid
|
// Check if original content is valid
|
||||||
if _, err := textDetector.DetectBest(content); err != nil {
|
if _, err := textDetector.DetectBest(content); err != nil {
|
||||||
return "", err
|
return util.IfZero(setting.Repository.AnsiCharset, "UTF-8"), err
|
||||||
}
|
}
|
||||||
times := 1024 / len(content)
|
times := 1024 / len(content)
|
||||||
detectContent = make([]byte, 0, times*len(content))
|
detectContent = make([]byte, 0, times*len(content))
|
||||||
@@ -171,14 +185,10 @@ func DetectEncoding(content []byte) (string, error) {
|
|||||||
detectContent = content
|
detectContent = content
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now we can't use DetectBest or just results[0] because the result isn't stable - so we need a tie break
|
// Now we can't use DetectBest or just results[0] because the result isn't stable - so we need a tie-break
|
||||||
results, err := textDetector.DetectAll(detectContent)
|
results, err := textDetector.DetectAll(detectContent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == chardet.NotDetectedError && len(setting.Repository.AnsiCharset) > 0 {
|
return util.IfZero(setting.Repository.AnsiCharset, "UTF-8"), err
|
||||||
log.Debug("Using default AnsiCharset: %s", setting.Repository.AnsiCharset)
|
|
||||||
return setting.Repository.AnsiCharset, nil
|
|
||||||
}
|
|
||||||
return "", err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
topConfidence := results[0].Confidence
|
topConfidence := results[0].Confidence
|
||||||
@@ -201,11 +211,9 @@ func DetectEncoding(content []byte) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: to properly decouple this function the fallback ANSI charset should be passed as an argument
|
// FIXME: to properly decouple this function the fallback ANSI charset should be passed as an argument
|
||||||
if topResult.Charset != "UTF-8" && len(setting.Repository.AnsiCharset) > 0 {
|
if topResult.Charset != "UTF-8" && setting.Repository.AnsiCharset != "" {
|
||||||
log.Debug("Using default AnsiCharset: %s", setting.Repository.AnsiCharset)
|
|
||||||
return setting.Repository.AnsiCharset, err
|
return setting.Repository.AnsiCharset, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("Detected encoding: %s", topResult.Charset)
|
return topResult.Charset, nil
|
||||||
return topResult.Charset, err
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,12 @@
|
|||||||
package charset
|
package charset
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@@ -47,12 +47,12 @@ func TestToUTF8(t *testing.T) {
|
|||||||
|
|
||||||
res, err := ToUTF8([]byte{0x41, 0x42, 0x43}, ConvertOpts{})
|
res, err := ToUTF8([]byte{0x41, 0x42, 0x43}, ConvertOpts{})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "ABC", res)
|
assert.Equal(t, "ABC", string(res))
|
||||||
|
|
||||||
// "áéíóú"
|
// "áéíóú"
|
||||||
res, err = ToUTF8([]byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, ConvertOpts{})
|
res, err = ToUTF8([]byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, ConvertOpts{})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, []byte(res))
|
assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, res)
|
||||||
|
|
||||||
// "áéíóú"
|
// "áéíóú"
|
||||||
res, err = ToUTF8([]byte{
|
res, err = ToUTF8([]byte{
|
||||||
@@ -60,7 +60,7 @@ func TestToUTF8(t *testing.T) {
|
|||||||
0xc3, 0xba,
|
0xc3, 0xba,
|
||||||
}, ConvertOpts{})
|
}, ConvertOpts{})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, []byte(res))
|
assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, res)
|
||||||
|
|
||||||
res, err = ToUTF8([]byte{
|
res, err = ToUTF8([]byte{
|
||||||
0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63,
|
0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63,
|
||||||
@@ -96,12 +96,11 @@ func TestToUTF8(t *testing.T) {
|
|||||||
assert.Equal(t, []byte{
|
assert.Equal(t, []byte{
|
||||||
0xE6, 0x97, 0xA5, 0xE5, 0xB1, 0x9E, 0xE7, 0xA7, 0x98, 0xE3,
|
0xE6, 0x97, 0xA5, 0xE5, 0xB1, 0x9E, 0xE7, 0xA7, 0x98, 0xE3,
|
||||||
0x81, 0x9E, 0xE3, 0x81, 0x97, 0xE3, 0x81, 0xA1, 0xE3, 0x82, 0x85, 0xE3, 0x80, 0x82,
|
0x81, 0x9E, 0xE3, 0x81, 0x97, 0xE3, 0x81, 0xA1, 0xE3, 0x82, 0x85, 0xE3, 0x80, 0x82,
|
||||||
},
|
}, res)
|
||||||
[]byte(res))
|
|
||||||
|
|
||||||
res, err = ToUTF8([]byte{0x00, 0x00, 0x00, 0x00}, ConvertOpts{})
|
res, err = ToUTF8([]byte{0x00, 0x00, 0x00, 0x00}, ConvertOpts{})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, []byte{0x00, 0x00, 0x00, 0x00}, []byte(res))
|
assert.Equal(t, []byte{0x00, 0x00, 0x00, 0x00}, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestToUTF8WithFallback(t *testing.T) {
|
func TestToUTF8WithFallback(t *testing.T) {
|
||||||
@@ -231,152 +230,42 @@ func TestDetectEncoding(t *testing.T) {
|
|||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func stringMustStartWith(t *testing.T, expected, value string) {
|
func stringMustStartWith(t *testing.T, expected string, value []byte) {
|
||||||
assert.Equal(t, expected, value[:len(expected)])
|
assert.Equal(t, expected, string(value[:len(expected)]))
|
||||||
}
|
}
|
||||||
|
|
||||||
func stringMustEndWith(t *testing.T, expected, value string) {
|
func stringMustEndWith(t *testing.T, expected string, value []byte) {
|
||||||
assert.Equal(t, expected, value[len(value)-len(expected):])
|
assert.Equal(t, expected, string(value[len(value)-len(expected):]))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestToUTF8WithFallbackReader(t *testing.T) {
|
func TestToUTF8WithFallbackReader(t *testing.T) {
|
||||||
resetDefaultCharsetsOrder()
|
resetDefaultCharsetsOrder()
|
||||||
|
test.MockVariableValue(&ToUTF8WithFallbackReaderPrefetchSize)
|
||||||
|
|
||||||
for testLen := range 2048 {
|
block := "aá啊🤔"
|
||||||
pattern := " test { () }\n"
|
runes := []rune(block)
|
||||||
input := ""
|
assert.Len(t, string(runes[0]), 1)
|
||||||
for len(input) < testLen {
|
assert.Len(t, string(runes[1]), 2)
|
||||||
input += pattern
|
assert.Len(t, string(runes[2]), 3)
|
||||||
}
|
assert.Len(t, string(runes[3]), 4)
|
||||||
input = input[:testLen]
|
|
||||||
input += "// Выключаем"
|
content := strings.Repeat(block, 2)
|
||||||
rd := ToUTF8WithFallbackReader(bytes.NewReader([]byte(input)), ConvertOpts{})
|
for i := 1; i < len(content); i++ {
|
||||||
|
encoding, err := DetectEncoding([]byte(content[:i]))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "UTF-8", encoding)
|
||||||
|
|
||||||
|
ToUTF8WithFallbackReaderPrefetchSize = i
|
||||||
|
rd := ToUTF8WithFallbackReader(strings.NewReader(content), ConvertOpts{})
|
||||||
r, _ := io.ReadAll(rd)
|
r, _ := io.ReadAll(rd)
|
||||||
assert.Equalf(t, input, string(r), "testing string len=%d", testLen)
|
assert.Equal(t, content, string(r))
|
||||||
|
}
|
||||||
|
for _, r := range runes {
|
||||||
|
content = "abc abc " + string(r) + string(r) + string(r)
|
||||||
|
for i := 0; i < len(content); i++ {
|
||||||
|
encoding, err := DetectEncoding([]byte(content[:i]))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "UTF-8", encoding)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
truncatedOneByteExtension := failFastBytes
|
|
||||||
encoding, _ := DetectEncoding(truncatedOneByteExtension)
|
|
||||||
assert.Equal(t, "UTF-8", encoding)
|
|
||||||
|
|
||||||
truncatedTwoByteExtension := failFastBytes
|
|
||||||
truncatedTwoByteExtension[len(failFastBytes)-1] = 0x9b
|
|
||||||
truncatedTwoByteExtension[len(failFastBytes)-2] = 0xe2
|
|
||||||
|
|
||||||
encoding, _ = DetectEncoding(truncatedTwoByteExtension)
|
|
||||||
assert.Equal(t, "UTF-8", encoding)
|
|
||||||
|
|
||||||
truncatedThreeByteExtension := failFastBytes
|
|
||||||
truncatedThreeByteExtension[len(failFastBytes)-1] = 0x92
|
|
||||||
truncatedThreeByteExtension[len(failFastBytes)-2] = 0x9f
|
|
||||||
truncatedThreeByteExtension[len(failFastBytes)-3] = 0xf0
|
|
||||||
|
|
||||||
encoding, _ = DetectEncoding(truncatedThreeByteExtension)
|
|
||||||
assert.Equal(t, "UTF-8", encoding)
|
|
||||||
}
|
|
||||||
|
|
||||||
var failFastBytes = []byte{
|
|
||||||
0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x20, 0x6f, 0x72, 0x67, 0x2e, 0x61, 0x70, 0x61, 0x63, 0x68, 0x65, 0x2e, 0x74, 0x6f,
|
|
||||||
0x6f, 0x6c, 0x73, 0x2e, 0x61, 0x6e, 0x74, 0x2e, 0x74, 0x61, 0x73, 0x6b, 0x64, 0x65, 0x66, 0x73, 0x2e, 0x63, 0x6f, 0x6e,
|
|
||||||
0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x4f, 0x73, 0x0a, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x20, 0x6f, 0x72, 0x67,
|
|
||||||
0x2e, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x62, 0x6f, 0x6f,
|
|
||||||
0x74, 0x2e, 0x67, 0x72, 0x61, 0x64, 0x6c, 0x65, 0x2e, 0x74, 0x61, 0x73, 0x6b, 0x73, 0x2e, 0x72, 0x75, 0x6e, 0x2e, 0x42,
|
|
||||||
0x6f, 0x6f, 0x74, 0x52, 0x75, 0x6e, 0x0a, 0x0a, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, 0x20, 0x7b, 0x0a, 0x20, 0x20,
|
|
||||||
0x20, 0x20, 0x69, 0x64, 0x28, 0x22, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d,
|
|
||||||
0x65, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x62, 0x6f, 0x6f, 0x74, 0x22, 0x29, 0x0a, 0x7d, 0x0a, 0x0a, 0x64, 0x65, 0x70, 0x65,
|
|
||||||
0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65,
|
|
||||||
0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x28, 0x22, 0x3a,
|
|
||||||
0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a, 0x61, 0x70, 0x69, 0x22, 0x29, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d,
|
|
||||||
0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74,
|
|
||||||
0x28, 0x22, 0x3a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a, 0x61, 0x70, 0x69, 0x2d, 0x64, 0x6f, 0x63, 0x73, 0x22, 0x29,
|
|
||||||
0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e,
|
|
||||||
0x28, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x28, 0x22, 0x3a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a, 0x64, 0x62,
|
|
||||||
0x22, 0x29, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69,
|
|
||||||
0x6f, 0x6e, 0x28, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x28, 0x22, 0x3a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a,
|
|
||||||
0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, 0x29, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65,
|
|
||||||
0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x28, 0x22, 0x3a,
|
|
||||||
0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x3a, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2d, 0x66,
|
|
||||||
0x73, 0x22, 0x29, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74,
|
|
||||||
0x69, 0x6f, 0x6e, 0x28, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x28, 0x22, 0x3a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72,
|
|
||||||
0x3a, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2d, 0x6d, 0x71, 0x22, 0x29, 0x29, 0x0a, 0x0a,
|
|
||||||
0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22,
|
|
||||||
0x6a, 0x66, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x65, 0x3a, 0x70, 0x65, 0x2d, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e,
|
|
||||||
0x2d, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2d, 0x73, 0x74, 0x61, 0x72, 0x74,
|
|
||||||
0x65, 0x72, 0x22, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74,
|
|
||||||
0x69, 0x6f, 0x6e, 0x28, 0x22, 0x6a, 0x66, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x65, 0x3a, 0x70, 0x65, 0x2d, 0x63,
|
|
||||||
0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2d, 0x68, 0x61, 0x6c, 0x22, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c,
|
|
||||||
0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x6a, 0x66, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x2e,
|
|
||||||
0x70, 0x65, 0x3a, 0x70, 0x65, 0x2d, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x22, 0x29, 0x0a,
|
|
||||||
0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28,
|
|
||||||
0x22, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, 0x72, 0x6b,
|
|
||||||
0x2e, 0x62, 0x6f, 0x6f, 0x74, 0x3a, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x62, 0x6f, 0x6f, 0x74, 0x2d, 0x73, 0x74,
|
|
||||||
0x61, 0x72, 0x74, 0x65, 0x72, 0x2d, 0x77, 0x65, 0x62, 0x22, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c,
|
|
||||||
0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x70, 0x72, 0x69,
|
|
||||||
0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x62, 0x6f, 0x6f, 0x74, 0x3a, 0x73, 0x70, 0x72,
|
|
||||||
0x69, 0x6e, 0x67, 0x2d, 0x62, 0x6f, 0x6f, 0x74, 0x2d, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x72, 0x2d, 0x61, 0x6f, 0x70,
|
|
||||||
0x22, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f,
|
|
||||||
0x6e, 0x28, 0x22, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f,
|
|
||||||
0x72, 0x6b, 0x2e, 0x62, 0x6f, 0x6f, 0x74, 0x3a, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x62, 0x6f, 0x6f, 0x74, 0x2d,
|
|
||||||
0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x72, 0x2d, 0x61, 0x63, 0x74, 0x75, 0x61, 0x74, 0x6f, 0x72, 0x22, 0x29, 0x0a, 0x20,
|
|
||||||
0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x6f,
|
|
||||||
0x72, 0x67, 0x2e, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x63,
|
|
||||||
0x6c, 0x6f, 0x75, 0x64, 0x3a, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2d, 0x73, 0x74,
|
|
||||||
0x61, 0x72, 0x74, 0x65, 0x72, 0x2d, 0x62, 0x6f, 0x6f, 0x74, 0x73, 0x74, 0x72, 0x61, 0x70, 0x22, 0x29, 0x0a, 0x20, 0x20,
|
|
||||||
0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x6f, 0x72,
|
|
||||||
0x67, 0x2e, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x63, 0x6c,
|
|
||||||
0x6f, 0x75, 0x64, 0x3a, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2d, 0x73, 0x74, 0x61,
|
|
||||||
0x72, 0x74, 0x65, 0x72, 0x2d, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2d, 0x61, 0x6c, 0x6c, 0x22, 0x29, 0x0a, 0x20, 0x20,
|
|
||||||
0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x6f, 0x72,
|
|
||||||
0x67, 0x2e, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x63, 0x6c,
|
|
||||||
0x6f, 0x75, 0x64, 0x3a, 0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2d, 0x73, 0x74, 0x61,
|
|
||||||
0x72, 0x74, 0x65, 0x72, 0x2d, 0x73, 0x6c, 0x65, 0x75, 0x74, 0x68, 0x22, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d,
|
|
||||||
0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x6f, 0x72, 0x67, 0x2e, 0x73, 0x70,
|
|
||||||
0x72, 0x69, 0x6e, 0x67, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x77, 0x6f, 0x72, 0x6b, 0x2e, 0x72, 0x65, 0x74, 0x72, 0x79, 0x3a,
|
|
||||||
0x73, 0x70, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x72, 0x65, 0x74, 0x72, 0x79, 0x22, 0x29, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20,
|
|
||||||
0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x63, 0x68, 0x2e, 0x71,
|
|
||||||
0x6f, 0x73, 0x2e, 0x6c, 0x6f, 0x67, 0x62, 0x61, 0x63, 0x6b, 0x3a, 0x6c, 0x6f, 0x67, 0x62, 0x61, 0x63, 0x6b, 0x2d, 0x63,
|
|
||||||
0x6c, 0x61, 0x73, 0x73, 0x69, 0x63, 0x22, 0x29, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d,
|
|
||||||
0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x69, 0x6f, 0x2e, 0x6d, 0x69, 0x63, 0x72, 0x6f, 0x6d, 0x65,
|
|
||||||
0x74, 0x65, 0x72, 0x3a, 0x6d, 0x69, 0x63, 0x72, 0x6f, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x2d, 0x72, 0x65, 0x67, 0x69, 0x73,
|
|
||||||
0x74, 0x72, 0x79, 0x2d, 0x70, 0x72, 0x6f, 0x6d, 0x65, 0x74, 0x68, 0x65, 0x75, 0x73, 0x22, 0x29, 0x0a, 0x0a, 0x20, 0x20,
|
|
||||||
0x20, 0x20, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x6b, 0x6f, 0x74,
|
|
||||||
0x6c, 0x69, 0x6e, 0x28, 0x22, 0x73, 0x74, 0x64, 0x6c, 0x69, 0x62, 0x22, 0x29, 0x29, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20,
|
|
||||||
0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f,
|
|
||||||
0x2f, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x2f, 0x2f, 0x20, 0x54, 0x65, 0x73, 0x74, 0x20, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64,
|
|
||||||
0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f,
|
|
||||||
0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x2f, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x74,
|
|
||||||
0x65, 0x73, 0x74, 0x49, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 0x22, 0x6a,
|
|
||||||
0x66, 0x75, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x65, 0x3a, 0x70, 0x65, 0x2d, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2d,
|
|
||||||
0x74, 0x65, 0x73, 0x74, 0x22, 0x29, 0x0a, 0x7d, 0x0a, 0x0a, 0x76, 0x61, 0x6c, 0x20, 0x70, 0x61, 0x74, 0x63, 0x68, 0x4a,
|
|
||||||
0x61, 0x72, 0x20, 0x62, 0x79, 0x20, 0x74, 0x61, 0x73, 0x6b, 0x73, 0x2e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72,
|
|
||||||
0x69, 0x6e, 0x67, 0x28, 0x4a, 0x61, 0x72, 0x3a, 0x3a, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x29, 0x20, 0x7b, 0x0a, 0x20, 0x20,
|
|
||||||
0x20, 0x20, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x69, 0x66, 0x69, 0x65, 0x72, 0x2e,
|
|
||||||
0x73, 0x65, 0x74, 0x28, 0x22, 0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x22, 0x29, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20,
|
|
||||||
0x76, 0x61, 0x6c, 0x20, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x70, 0x61, 0x74, 0x68,
|
|
||||||
0x20, 0x62, 0x79, 0x20, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x67,
|
|
||||||
0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x0a, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74,
|
|
||||||
0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65,
|
|
||||||
0x73, 0x28, 0x22, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x2d, 0x50, 0x61, 0x74, 0x68, 0x22, 0x20, 0x74, 0x6f, 0x20, 0x6f, 0x62,
|
|
||||||
0x6a, 0x65, 0x63, 0x74, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x70,
|
|
||||||
0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x20, 0x76, 0x61, 0x6c, 0x20, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x20, 0x3d,
|
|
||||||
0x20, 0x22, 0x66, 0x69, 0x6c, 0x65, 0x3a, 0x2f, 0x2b, 0x22, 0x2e, 0x74, 0x6f, 0x52, 0x65, 0x67, 0x65, 0x78, 0x28, 0x29,
|
|
||||||
0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64,
|
|
||||||
0x65, 0x20, 0x66, 0x75, 0x6e, 0x20, 0x74, 0x6f, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x28, 0x29, 0x3a, 0x20, 0x53, 0x74,
|
|
||||||
0x72, 0x69, 0x6e, 0x67, 0x20, 0x3d, 0x20, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x70,
|
|
||||||
0x61, 0x74, 0x68, 0x2e, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x2e, 0x6a, 0x6f, 0x69, 0x6e, 0x54, 0x6f, 0x53, 0x74, 0x72, 0x69,
|
|
||||||
0x6e, 0x67, 0x28, 0x22, 0x20, 0x22, 0x29, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
|
|
||||||
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x69, 0x74, 0x2e, 0x74, 0x6f, 0x55, 0x52, 0x49, 0x28, 0x29, 0x2e, 0x74, 0x6f, 0x55,
|
|
||||||
0x52, 0x4c, 0x28, 0x29, 0x2e, 0x74, 0x6f, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x28, 0x29, 0x2e, 0x72, 0x65, 0x70, 0x6c,
|
|
||||||
0x61, 0x63, 0x65, 0x46, 0x69, 0x72, 0x73, 0x74, 0x28, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x2c, 0x20, 0x22, 0x2f,
|
|
||||||
0x22, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x20, 0x20, 0x20,
|
|
||||||
0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x7d, 0x0a, 0x0a, 0x74, 0x61, 0x73,
|
|
||||||
0x6b, 0x73, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x64, 0x3c, 0x42, 0x6f, 0x6f, 0x74, 0x52, 0x75, 0x6e, 0x3e, 0x28, 0x22, 0x62,
|
|
||||||
0x6f, 0x6f, 0x74, 0x52, 0x75, 0x6e, 0x22, 0x29, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x69, 0x66, 0x20, 0x28, 0x4f,
|
|
||||||
0x73, 0x2e, 0x69, 0x73, 0x46, 0x61, 0x6d, 0x69, 0x6c, 0x79, 0x28, 0x4f, 0x73, 0x2e, 0x46, 0x41, 0x4d, 0x49, 0x4c, 0x59,
|
|
||||||
0x5f, 0x57, 0x49, 0x4e, 0x44, 0x4f, 0x57, 0x53, 0x29, 0x29, 0x20, 0x7b, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
|
|
||||||
0x20, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x70, 0x61, 0x74, 0x68, 0x20, 0x3d, 0x20, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x28, 0x73,
|
|
||||||
0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x74, 0x73, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x64, 0x28, 0x22, 0x6d, 0x61, 0x69,
|
|
||||||
0x6e, 0x22, 0x29, 0x2e, 0x6d, 0x61, 0x70, 0x20, 0x7b, 0x20, 0x69, 0x74, 0x2e, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x20,
|
|
||||||
0x7d, 0x2c, 0x20, 0x70, 0x61, 0x74, 0x63, 0x68, 0x4a, 0x61, 0x72, 0x29, 0x0a, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x0a, 0x0a,
|
|
||||||
0x20, 0x20, 0x20, 0x20, 0x2f, 0x2f, 0x20, 0xd0,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync/atomic"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Gemoji is a set of emoji data.
|
// Gemoji is a set of emoji data.
|
||||||
@@ -23,74 +25,78 @@ type Emoji struct {
|
|||||||
SkinTones bool
|
SkinTones bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
type globalVarsStruct struct {
|
||||||
// codeMap provides a map of the emoji unicode code to its emoji data.
|
codeMap map[string]int // emoji unicode code to its emoji data.
|
||||||
codeMap map[string]int
|
aliasMap map[string]int // the alias to its emoji data.
|
||||||
|
emptyReplacer *strings.Replacer // string replacer for emoji codes, used for finding emoji positions.
|
||||||
|
codeReplacer *strings.Replacer // string replacer for emoji codes.
|
||||||
|
aliasReplacer *strings.Replacer // string replacer for emoji aliases.
|
||||||
|
}
|
||||||
|
|
||||||
// aliasMap provides a map of the alias to its emoji data.
|
var globalVarsStore atomic.Pointer[globalVarsStruct]
|
||||||
aliasMap map[string]int
|
|
||||||
|
|
||||||
// emptyReplacer is the string replacer for emoji codes.
|
func globalVars() *globalVarsStruct {
|
||||||
emptyReplacer *strings.Replacer
|
vars := globalVarsStore.Load()
|
||||||
|
if vars != nil {
|
||||||
|
return vars
|
||||||
|
}
|
||||||
|
// although there can be concurrent calls, the result should be the same, and there is no performance problem
|
||||||
|
vars = &globalVarsStruct{}
|
||||||
|
vars.codeMap = make(map[string]int, len(GemojiData))
|
||||||
|
vars.aliasMap = make(map[string]int, len(GemojiData))
|
||||||
|
|
||||||
// codeReplacer is the string replacer for emoji codes.
|
// process emoji codes and aliases
|
||||||
codeReplacer *strings.Replacer
|
codePairs := make([]string, 0)
|
||||||
|
emptyPairs := make([]string, 0)
|
||||||
|
aliasPairs := make([]string, 0)
|
||||||
|
|
||||||
// aliasReplacer is the string replacer for emoji aliases.
|
// sort from largest to small so we match combined emoji first
|
||||||
aliasReplacer *strings.Replacer
|
sort.Slice(GemojiData, func(i, j int) bool {
|
||||||
|
return len(GemojiData[i].Emoji) > len(GemojiData[j].Emoji)
|
||||||
|
})
|
||||||
|
|
||||||
once sync.Once
|
for idx, emoji := range GemojiData {
|
||||||
)
|
if emoji.Emoji == "" || len(emoji.Aliases) == 0 {
|
||||||
|
continue
|
||||||
func loadMap() {
|
|
||||||
once.Do(func() {
|
|
||||||
// initialize
|
|
||||||
codeMap = make(map[string]int, len(GemojiData))
|
|
||||||
aliasMap = make(map[string]int, len(GemojiData))
|
|
||||||
|
|
||||||
// process emoji codes and aliases
|
|
||||||
codePairs := make([]string, 0)
|
|
||||||
emptyPairs := make([]string, 0)
|
|
||||||
aliasPairs := make([]string, 0)
|
|
||||||
|
|
||||||
// sort from largest to small so we match combined emoji first
|
|
||||||
sort.Slice(GemojiData, func(i, j int) bool {
|
|
||||||
return len(GemojiData[i].Emoji) > len(GemojiData[j].Emoji)
|
|
||||||
})
|
|
||||||
|
|
||||||
for i, e := range GemojiData {
|
|
||||||
if e.Emoji == "" || len(e.Aliases) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// setup codes
|
|
||||||
codeMap[e.Emoji] = i
|
|
||||||
codePairs = append(codePairs, e.Emoji, ":"+e.Aliases[0]+":")
|
|
||||||
emptyPairs = append(emptyPairs, e.Emoji, e.Emoji)
|
|
||||||
|
|
||||||
// setup aliases
|
|
||||||
for _, a := range e.Aliases {
|
|
||||||
if a == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
aliasMap[a] = i
|
|
||||||
aliasPairs = append(aliasPairs, ":"+a+":", e.Emoji)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// create replacers
|
// process aliases
|
||||||
emptyReplacer = strings.NewReplacer(emptyPairs...)
|
firstAlias := ""
|
||||||
codeReplacer = strings.NewReplacer(codePairs...)
|
for _, alias := range emoji.Aliases {
|
||||||
aliasReplacer = strings.NewReplacer(aliasPairs...)
|
if alias == "" {
|
||||||
})
|
continue
|
||||||
|
}
|
||||||
|
enabled := len(setting.UI.EnabledEmojisSet) == 0 || setting.UI.EnabledEmojisSet.Contains(alias)
|
||||||
|
if !enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if firstAlias == "" {
|
||||||
|
firstAlias = alias
|
||||||
|
}
|
||||||
|
vars.aliasMap[alias] = idx
|
||||||
|
aliasPairs = append(aliasPairs, ":"+alias+":", emoji.Emoji)
|
||||||
|
}
|
||||||
|
|
||||||
|
// process emoji code
|
||||||
|
if firstAlias != "" {
|
||||||
|
vars.codeMap[emoji.Emoji] = idx
|
||||||
|
codePairs = append(codePairs, emoji.Emoji, ":"+emoji.Aliases[0]+":")
|
||||||
|
emptyPairs = append(emptyPairs, emoji.Emoji, emoji.Emoji)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create replacers
|
||||||
|
vars.emptyReplacer = strings.NewReplacer(emptyPairs...)
|
||||||
|
vars.codeReplacer = strings.NewReplacer(codePairs...)
|
||||||
|
vars.aliasReplacer = strings.NewReplacer(aliasPairs...)
|
||||||
|
globalVarsStore.Store(vars)
|
||||||
|
return vars
|
||||||
}
|
}
|
||||||
|
|
||||||
// FromCode retrieves the emoji data based on the provided unicode code (ie,
|
// FromCode retrieves the emoji data based on the provided unicode code (ie,
|
||||||
// "\u2618" will return the Gemoji data for "shamrock").
|
// "\u2618" will return the Gemoji data for "shamrock").
|
||||||
func FromCode(code string) *Emoji {
|
func FromCode(code string) *Emoji {
|
||||||
loadMap()
|
i, ok := globalVars().codeMap[code]
|
||||||
i, ok := codeMap[code]
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -102,12 +108,11 @@ func FromCode(code string) *Emoji {
|
|||||||
// "alias" or ":alias:" (ie, "shamrock" or ":shamrock:" will return the Gemoji
|
// "alias" or ":alias:" (ie, "shamrock" or ":shamrock:" will return the Gemoji
|
||||||
// data for "shamrock").
|
// data for "shamrock").
|
||||||
func FromAlias(alias string) *Emoji {
|
func FromAlias(alias string) *Emoji {
|
||||||
loadMap()
|
|
||||||
if strings.HasPrefix(alias, ":") && strings.HasSuffix(alias, ":") {
|
if strings.HasPrefix(alias, ":") && strings.HasSuffix(alias, ":") {
|
||||||
alias = alias[1 : len(alias)-1]
|
alias = alias[1 : len(alias)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
i, ok := aliasMap[alias]
|
i, ok := globalVars().aliasMap[alias]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -119,15 +124,13 @@ func FromAlias(alias string) *Emoji {
|
|||||||
// alias (in the form of ":alias:") (ie, "\u2618" will be converted to
|
// alias (in the form of ":alias:") (ie, "\u2618" will be converted to
|
||||||
// ":shamrock:").
|
// ":shamrock:").
|
||||||
func ReplaceCodes(s string) string {
|
func ReplaceCodes(s string) string {
|
||||||
loadMap()
|
return globalVars().codeReplacer.Replace(s)
|
||||||
return codeReplacer.Replace(s)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReplaceAliases replaces all aliases of the form ":alias:" with its
|
// ReplaceAliases replaces all aliases of the form ":alias:" with its
|
||||||
// corresponding unicode value.
|
// corresponding unicode value.
|
||||||
func ReplaceAliases(s string) string {
|
func ReplaceAliases(s string) string {
|
||||||
loadMap()
|
return globalVars().aliasReplacer.Replace(s)
|
||||||
return aliasReplacer.Replace(s)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type rememberSecondWriteWriter struct {
|
type rememberSecondWriteWriter struct {
|
||||||
@@ -163,7 +166,6 @@ func (n *rememberSecondWriteWriter) WriteString(s string) (int, error) {
|
|||||||
|
|
||||||
// FindEmojiSubmatchIndex returns index pair of longest emoji in a string
|
// FindEmojiSubmatchIndex returns index pair of longest emoji in a string
|
||||||
func FindEmojiSubmatchIndex(s string) []int {
|
func FindEmojiSubmatchIndex(s string) []int {
|
||||||
loadMap()
|
|
||||||
secondWriteWriter := rememberSecondWriteWriter{}
|
secondWriteWriter := rememberSecondWriteWriter{}
|
||||||
|
|
||||||
// A faster and clean implementation would copy the trie tree formation in strings.NewReplacer but
|
// A faster and clean implementation would copy the trie tree formation in strings.NewReplacer but
|
||||||
@@ -175,7 +177,7 @@ func FindEmojiSubmatchIndex(s string) []int {
|
|||||||
// Therefore we can simply take the index of the second write as our first emoji
|
// Therefore we can simply take the index of the second write as our first emoji
|
||||||
//
|
//
|
||||||
// FIXME: just copy the trie implementation from strings.NewReplacer
|
// FIXME: just copy the trie implementation from strings.NewReplacer
|
||||||
_, _ = emptyReplacer.WriteString(&secondWriteWriter, s)
|
_, _ = globalVars().emptyReplacer.WriteString(&secondWriteWriter, s)
|
||||||
|
|
||||||
// if we wrote less than twice then we never "replaced"
|
// if we wrote less than twice then we never "replaced"
|
||||||
if secondWriteWriter.writecount < 2 {
|
if secondWriteWriter.writecount < 2 {
|
||||||
|
|||||||
@@ -7,14 +7,13 @@ package emoji
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/container"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDumpInfo(t *testing.T) {
|
|
||||||
t.Logf("codes: %d", len(codeMap))
|
|
||||||
t.Logf("aliases: %d", len(aliasMap))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLookup(t *testing.T) {
|
func TestLookup(t *testing.T) {
|
||||||
a := FromCode("\U0001f37a")
|
a := FromCode("\U0001f37a")
|
||||||
b := FromCode("🍺")
|
b := FromCode("🍺")
|
||||||
@@ -24,7 +23,6 @@ func TestLookup(t *testing.T) {
|
|||||||
assert.Equal(t, a, b)
|
assert.Equal(t, a, b)
|
||||||
assert.Equal(t, b, c)
|
assert.Equal(t, b, c)
|
||||||
assert.Equal(t, c, d)
|
assert.Equal(t, c, d)
|
||||||
assert.Equal(t, a, d)
|
|
||||||
|
|
||||||
m := FromCode("\U0001f44d")
|
m := FromCode("\U0001f44d")
|
||||||
n := FromAlias(":thumbsup:")
|
n := FromAlias(":thumbsup:")
|
||||||
@@ -32,7 +30,20 @@ func TestLookup(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, m, n)
|
assert.Equal(t, m, n)
|
||||||
assert.Equal(t, m, o)
|
assert.Equal(t, m, o)
|
||||||
assert.Equal(t, n, o)
|
|
||||||
|
defer test.MockVariableValue(&setting.UI.EnabledEmojisSet, container.SetOf("thumbsup"))()
|
||||||
|
defer globalVarsStore.Store(nil)
|
||||||
|
globalVarsStore.Store(nil)
|
||||||
|
a = FromCode("\U0001f37a")
|
||||||
|
c = FromAlias(":beer:")
|
||||||
|
m = FromCode("\U0001f44d")
|
||||||
|
n = FromAlias(":thumbsup:")
|
||||||
|
o = FromAlias("+1")
|
||||||
|
assert.Nil(t, a)
|
||||||
|
assert.Nil(t, c)
|
||||||
|
assert.NotNil(t, m)
|
||||||
|
assert.NotNil(t, n)
|
||||||
|
assert.Nil(t, o)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReplacers(t *testing.T) {
|
func TestReplacers(t *testing.T) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
activities_model "code.gitea.io/gitea/models/activities"
|
activities_model "code.gitea.io/gitea/models/activities"
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/graceful"
|
"code.gitea.io/gitea/modules/graceful"
|
||||||
"code.gitea.io/gitea/modules/json"
|
"code.gitea.io/gitea/modules/json"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
@@ -91,7 +92,13 @@ loop:
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, userStopwatches := range usersStopwatches {
|
for _, userStopwatches := range usersStopwatches {
|
||||||
apiSWs, err := convert.ToStopWatches(ctx, userStopwatches.StopWatches)
|
u, err := user_model.GetUserByID(ctx, userStopwatches.UserID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Unable to get user %d: %v", userStopwatches.UserID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
apiSWs, err := convert.ToStopWatches(ctx, u, userStopwatches.StopWatches)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !issues_model.IsErrIssueNotExist(err) {
|
if !issues_model.IsErrIssueNotExist(err) {
|
||||||
log.Error("Unable to APIFormat stopwatches: %v", err)
|
log.Error("Unable to APIFormat stopwatches: %v", err)
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ func (m *MaterialIconProvider) renderFileIconSVG(p *RenderedIconPool, name, svg,
|
|||||||
if p.IconSVGs[svgID] == "" {
|
if p.IconSVGs[svgID] == "" {
|
||||||
p.IconSVGs[svgID] = svgHTML
|
p.IconSVGs[svgID] = svgHTML
|
||||||
}
|
}
|
||||||
return template.HTML(`<svg ` + svgCommonAttrs + `><use xlink:href="#` + svgID + `"></use></svg>`)
|
return template.HTML(`<svg ` + svgCommonAttrs + `><use href="#` + svgID + `"></use></svg>`)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MaterialIconProvider) EntryIconHTML(p *RenderedIconPool, entry *EntryInfo) template.HTML {
|
func (m *MaterialIconProvider) EntryIconHTML(p *RenderedIconPool, entry *EntryInfo) template.HTML {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func (p *RenderedIconPool) RenderToHTML() template.HTML {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
sb := &strings.Builder{}
|
sb := &strings.Builder{}
|
||||||
sb.WriteString(`<div class=tw-hidden>`)
|
sb.WriteString(`<div class="svg-icon-container">`)
|
||||||
for _, icon := range p.IconSVGs {
|
for _, icon := range p.IconSVGs {
|
||||||
sb.WriteString(string(icon))
|
sb.WriteString(string(icon))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,24 +9,38 @@ package git
|
|||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Blob represents a Git object.
|
// Blob represents a Git object.
|
||||||
type Blob struct {
|
type Blob struct {
|
||||||
ID ObjectID
|
ID ObjectID
|
||||||
|
repo *Repository
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
gogitEncodedObj plumbing.EncodedObject
|
func (b *Blob) gogitEncodedObj() (plumbing.EncodedObject, error) {
|
||||||
name string
|
return b.repo.gogitRepo.Storer.EncodedObject(plumbing.AnyObject, plumbing.Hash(b.ID.RawValue()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// DataAsync gets a ReadCloser for the contents of a blob without reading it all.
|
// DataAsync gets a ReadCloser for the contents of a blob without reading it all.
|
||||||
// Calling the Close function on the result will discard all unread output.
|
// Calling the Close function on the result will discard all unread output.
|
||||||
func (b *Blob) DataAsync() (io.ReadCloser, error) {
|
func (b *Blob) DataAsync() (io.ReadCloser, error) {
|
||||||
return b.gogitEncodedObj.Reader()
|
obj, err := b.gogitEncodedObj()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return obj.Reader()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Size returns the uncompressed size of the blob
|
// Size returns the uncompressed size of the blob
|
||||||
func (b *Blob) Size() int64 {
|
func (b *Blob) Size() int64 {
|
||||||
return b.gogitEncodedObj.Size()
|
obj, err := b.gogitEncodedObj()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error getting gogit encoded object for blob %s(%s): %v", b.name, b.ID.String(), err)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return obj.Size()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,9 +30,11 @@ type Parser struct {
|
|||||||
func NewParser(r io.Reader, format Format) *Parser {
|
func NewParser(r io.Reader, format Format) *Parser {
|
||||||
scanner := bufio.NewScanner(r)
|
scanner := bufio.NewScanner(r)
|
||||||
|
|
||||||
// default MaxScanTokenSize = 64 kiB may be too small for some references,
|
// default Scanner.MaxScanTokenSize = 64 kiB may be too small for some references,
|
||||||
// so allow the buffer to grow up to 4x if needed
|
// so allow the buffer to be large enough in case the ref has long content (e.g.: a tag with long message)
|
||||||
scanner.Buffer(nil, 4*bufio.MaxScanTokenSize)
|
// as long as it doesn't exceed some reasonable limit (4 MiB here, or MAX_DISPLAY_FILE_SIZE=8MiB), it is OK
|
||||||
|
// there are still some choices: 1. add a config option for the limit; 2. don't use scanner and write our own parser to fully handle large contents
|
||||||
|
scanner.Buffer(nil, 4*1024*1024)
|
||||||
|
|
||||||
// in addition to the reference delimiter we specified in the --format,
|
// in addition to the reference delimiter we specified in the --format,
|
||||||
// `git for-each-ref` will always add a newline after every reference.
|
// `git for-each-ref` will always add a newline after every reference.
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
@@ -41,6 +42,10 @@ type GrepOptions struct {
|
|||||||
PathspecList []string
|
PathspecList []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// grepSearchTimeout is the timeout for git grep search, it should be long enough to get results
|
||||||
|
// but not too long to cause performance issues
|
||||||
|
const grepSearchTimeout = 30 * time.Second
|
||||||
|
|
||||||
func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepOptions) ([]*GrepResult, error) {
|
func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepOptions) ([]*GrepResult, error) {
|
||||||
stdoutReader, stdoutWriter, err := os.Pipe()
|
stdoutReader, stdoutWriter, err := os.Pipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -85,9 +90,10 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO
|
|||||||
opts.MaxResultLimit = util.IfZero(opts.MaxResultLimit, 50)
|
opts.MaxResultLimit = util.IfZero(opts.MaxResultLimit, 50)
|
||||||
stderr := bytes.Buffer{}
|
stderr := bytes.Buffer{}
|
||||||
err = cmd.Run(ctx, &gitcmd.RunOpts{
|
err = cmd.Run(ctx, &gitcmd.RunOpts{
|
||||||
Dir: repo.Path,
|
Dir: repo.Path,
|
||||||
Stdout: stdoutWriter,
|
Stdout: stdoutWriter,
|
||||||
Stderr: &stderr,
|
Stderr: &stderr,
|
||||||
|
Timeout: grepSearchTimeout,
|
||||||
PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
|
PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
|
||||||
_ = stdoutWriter.Close()
|
_ = stdoutWriter.Close()
|
||||||
defer stdoutReader.Close()
|
defer stdoutReader.Close()
|
||||||
|
|||||||
@@ -47,30 +47,16 @@ func GetHook(repoPath, name string) (*Hook, error) {
|
|||||||
name: name,
|
name: name,
|
||||||
path: filepath.Join(repoPath, "hooks", name+".d", name),
|
path: filepath.Join(repoPath, "hooks", name+".d", name),
|
||||||
}
|
}
|
||||||
isFile, err := util.IsFile(h.path)
|
if data, err := os.ReadFile(h.path); err == nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if isFile {
|
|
||||||
data, err := os.ReadFile(h.path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
h.IsActive = true
|
h.IsActive = true
|
||||||
h.Content = string(data)
|
h.Content = string(data)
|
||||||
return h, nil
|
return h, nil
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
samplePath := filepath.Join(repoPath, "hooks", name+".sample")
|
samplePath := filepath.Join(repoPath, "hooks", name+".sample")
|
||||||
isFile, err = util.IsFile(samplePath)
|
if data, err := os.ReadFile(samplePath); err == nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if isFile {
|
|
||||||
data, err := os.ReadFile(samplePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
h.Sample = string(data)
|
h.Sample = string(data)
|
||||||
}
|
}
|
||||||
return h, nil
|
return h, nil
|
||||||
|
|||||||
@@ -9,5 +9,11 @@ func (repo *Repository) GetBlob(idStr string) (*Blob, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return repo.getBlob(id)
|
if id.IsZero() {
|
||||||
|
return nil, ErrNotExist{id.String(), ""}
|
||||||
|
}
|
||||||
|
return &Blob{
|
||||||
|
ID: id,
|
||||||
|
repo: repo,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user