mirror of
https://github.com/go-gitea/gitea.git
synced 2025-08-21 09:48:58 +02:00
Compare commits
118 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
486d274be6 | ||
|
ab3d2a944c | ||
|
12bfa9e83d | ||
|
dd661e92df | ||
|
0b31272c7e | ||
|
ec0c418719 | ||
|
6dc19fc29a | ||
|
9f1baa7d18 | ||
|
e13deb7a16 | ||
|
e5c1b8b632 | ||
|
e931b62f33 | ||
|
81ee93e5bc | ||
|
053f9186bc | ||
|
68fcdb6122 | ||
|
14ca309c39 | ||
|
4aba42519d | ||
|
9adf175df0 | ||
|
c3fa2a8729 | ||
|
89dfed32e0 | ||
|
d5062d0c27 | ||
|
90e9e79232 | ||
|
c6467edcb1 | ||
|
5d5b695527 | ||
|
0af7a7b79f | ||
|
9339661078 | ||
|
1e69f085d6 | ||
|
0bfccd8ecf | ||
|
534b9b35dd | ||
|
dbadc59b56 | ||
|
a57e2c4bc3 | ||
|
acd4e10990 | ||
|
0a1df294c8 | ||
|
52a964d1fc | ||
|
d3dbe0d9ce | ||
|
cdbbdbef06 | ||
|
79f555d465 | ||
|
ae2b795693 | ||
|
d1fdbf46bd | ||
|
f27a75564a | ||
|
958d0db4f4 | ||
|
4c2441ba5d | ||
|
6f5f0be9e3 | ||
|
23d2d224c2 | ||
|
a43d829de8 | ||
|
8ab1363fef | ||
|
178fd90852 | ||
|
b39f7a37d1 | ||
|
b9ed8fceff | ||
|
e6ce72b14a | ||
|
2eecd58bbe | ||
|
64b9b21790 | ||
|
3290aff964 | ||
|
7ed1e8987e | ||
|
f10e909fce | ||
|
a3b25436f2 | ||
|
b947bc4363 | ||
|
18dc41d6f8 | ||
|
bf5d00074d | ||
|
fb4e9f92f9 | ||
|
468d1919b5 | ||
|
1b788946a7 | ||
|
e8646ad1d8 | ||
|
29dc9c784e | ||
|
b1cc4bf77f | ||
|
d35161ceb8 | ||
|
8defca6d39 | ||
|
fac434da0a | ||
|
e18eae7129 | ||
|
c60bc26fd3 | ||
|
bacc69db83 | ||
|
c5da032193 | ||
|
3ace45c118 | ||
|
5d6c5ce71a | ||
|
7baa6fa47c | ||
|
f9a0b077a7 | ||
|
d3317ebabe | ||
|
e9481e1da3 | ||
|
8965c068e9 | ||
|
eaaa158df3 | ||
|
f5498421c4 | ||
|
a6a14c9a92 | ||
|
d0ec1788b8 | ||
|
c1202f1b57 | ||
|
1162cbccc0 | ||
|
038990e0ff | ||
|
03ff09870d | ||
|
8bf4f2cc8f | ||
|
21731c1370 | ||
|
a0e272d95a | ||
|
47537a8361 | ||
|
d018c1b4b1 | ||
|
d2cbe2fba0 | ||
|
d6233c25b5 | ||
|
2bf2d00c8a | ||
|
9bd56a8ba0 | ||
|
a1dc3c9bd1 | ||
|
47ee84d1f3 | ||
|
89f1df033a | ||
|
94b67f1967 | ||
|
0a9a84df11 | ||
|
cdac263bb8 | ||
|
a5c7df7a4c | ||
|
6d738fecc4 | ||
|
38cc7453e2 | ||
|
b44175c071 | ||
|
947358dffe | ||
|
be1090cb2d | ||
|
c8f3402841 | ||
|
a3a95a0b67 | ||
|
ed527b664d | ||
|
e4717d426e | ||
|
16f15d2f7b | ||
|
b3f5196241 | ||
|
6c5f0af45d | ||
|
c95cb7c7e2 | ||
|
6747e3e0eb | ||
|
a12b5b3640 | ||
|
834dad8cef |
@ -7,7 +7,7 @@
|
|||||||
"version": "20"
|
"version": "20"
|
||||||
},
|
},
|
||||||
"ghcr.io/devcontainers/features/git-lfs:1.2.2": {},
|
"ghcr.io/devcontainers/features/git-lfs:1.2.2": {},
|
||||||
"ghcr.io/devcontainers-contrib/features/poetry:2": {},
|
"ghcr.io/devcontainers-extra/features/poetry:2": {},
|
||||||
"ghcr.io/devcontainers/features/python:1": {
|
"ghcr.io/devcontainers/features/python:1": {
|
||||||
"version": "3.12"
|
"version": "3.12"
|
||||||
},
|
},
|
||||||
|
2
.github/workflows/pull-db-tests.yml
vendored
2
.github/workflows/pull-db-tests.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
services:
|
services:
|
||||||
pgsql:
|
pgsql:
|
||||||
image: postgres:12
|
image: postgres:14
|
||||||
env:
|
env:
|
||||||
POSTGRES_DB: test
|
POSTGRES_DB: test
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
|
12
.gitignore
vendored
12
.gitignore
vendored
@ -39,14 +39,10 @@ _testmain.go
|
|||||||
coverage.all
|
coverage.all
|
||||||
cpu.out
|
cpu.out
|
||||||
|
|
||||||
/modules/migration/bindata.go
|
/modules/migration/bindata.*
|
||||||
/modules/migration/bindata.go.hash
|
/modules/options/bindata.*
|
||||||
/modules/options/bindata.go
|
/modules/public/bindata.*
|
||||||
/modules/options/bindata.go.hash
|
/modules/templates/bindata.*
|
||||||
/modules/public/bindata.go
|
|
||||||
/modules/public/bindata.go.hash
|
|
||||||
/modules/templates/bindata.go
|
|
||||||
/modules/templates/bindata.go.hash
|
|
||||||
|
|
||||||
*.db
|
*.db
|
||||||
*.log
|
*.log
|
||||||
|
483
CHANGELOG.md
483
CHANGELOG.md
@ -4,6 +4,489 @@ 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.3](https://github.com/go-gitea/gitea/releases/tag/1.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/1.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/1.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/1.24.0) - 2025-05-26
|
||||||
|
|
||||||
|
* BREAKING
|
||||||
|
* Make Gitea always use its internal config, ignore `/etc/gitconfig` (#33076)
|
||||||
|
* Improve log format (#33814)
|
||||||
|
* Fix markdown render behaviors (#34122)
|
||||||
|
* Add package version api endpoints (#34173)
|
||||||
|
|
||||||
|
* FEATURES
|
||||||
|
* Enforce two-factor auth (2FA: TOTP or WebAuthn) (#34187)
|
||||||
|
* Add fullscreen mode as a more efficient operation way to view projects (#34081)
|
||||||
|
* Add anonymous access support for private/unlisted repositories (#34051)
|
||||||
|
* Support public code/issue access for private repositories (#33127)
|
||||||
|
* Add middleware for request prioritization (#33951)
|
||||||
|
* Add cli flags LDAP group configuration (#33933)
|
||||||
|
* Add file tree to file view page (#32721)
|
||||||
|
* Add material icons for file list (#33837)
|
||||||
|
* Artifacts download api for artifact actions v4 (#33510)
|
||||||
|
* Support choose email when creating a commit via web UI (#33432)
|
||||||
|
* Add basic auth support to rss/atom feeds (#33371)
|
||||||
|
* Add sorting by exclusive labels (issue priority) (#33206)
|
||||||
|
* Add sub issue list support (#32940)
|
||||||
|
* Private README.md for organization (#32872)
|
||||||
|
* Email option to embed images as base64 instead of link (#32061)
|
||||||
|
* Option to delay conflict checking of old pull requests until page view (#27779)
|
||||||
|
* Worktime tracking for the organization level (#19808)
|
||||||
|
|
||||||
|
* PERFORMANCE
|
||||||
|
* Add cache for common package queries (#22491)
|
||||||
|
* Move issue pin to an standalone table for querying performance (#33452)
|
||||||
|
* Improve commits list performance to reduce unnecessary database queries (#33528)
|
||||||
|
* Optimize total count of feed when loading activities in user dashboard. (#33841)
|
||||||
|
* Optimize heatmap query (#33853)
|
||||||
|
* Only use prev and next buttons for pagination on user dashboard (#33981)
|
||||||
|
* Improve pull request list API performance (#34052)
|
||||||
|
* Cache GPG keys, emails and users when list commits (#34086)
|
||||||
|
* Refactor Git Attribute & performance optimization (#34154)
|
||||||
|
* Performance optimization for tags synchronization (#34355) #34522
|
||||||
|
|
||||||
|
* ENHANCEMENTS
|
||||||
|
* Code
|
||||||
|
* Display when a release attachment was uploaded (#34261)
|
||||||
|
* Support creating relative link to raw path in markdown (#34105)
|
||||||
|
* Improve code block readability and isolate copy button (#34009)
|
||||||
|
* Improve repository commit view (#33877)
|
||||||
|
* Full-file syntax highlighting for diff pages (#33766)
|
||||||
|
* Clone repository with Tea CLI (#33725)
|
||||||
|
* Improve sync fork behavior (#33319)
|
||||||
|
* Make git clone URL could use current signed-in user (#33091)
|
||||||
|
* Add submodule diff links (#33097)
|
||||||
|
* Link to tree views of submodules if possible (#33424)
|
||||||
|
* Only keep popular licenses (#33832)
|
||||||
|
* De-emphasize signed commits (#31160)
|
||||||
|
|
||||||
|
* Actions
|
||||||
|
* Add flat-square action badge style (#34062)
|
||||||
|
* Update action status badge layout (#34018)
|
||||||
|
* Download actions job logs from API (#33858)
|
||||||
|
* Always show the "rerun" button for action jobs (#33692)
|
||||||
|
* Add auto-expanding running actions step (#30058)
|
||||||
|
* Update status check for all supported on.pull_request.types in Gitea (#33117)
|
||||||
|
* Workflow_dispatch use workflow from trigger branch (#33098)
|
||||||
|
* Add action auto-scroll (#30057)
|
||||||
|
* Add workflow_job webhook (#33694)
|
||||||
|
* Add a button editing action secret (#34462)
|
||||||
|
|
||||||
|
* Pull Request
|
||||||
|
* Auto expand "New PR" form (#33971)
|
||||||
|
* Mark parent directory as viewed when all files are viewed (#33958)
|
||||||
|
* Show info about maintainers are allowed to edit a PR (#33738)
|
||||||
|
* Automerge supports deleting branch automatically after merging (#32343)
|
||||||
|
* Add additional command hints for PowerShell & CMD (#33548)
|
||||||
|
|
||||||
|
* Issues
|
||||||
|
* Allow filtering issues by any assignee (#33343)
|
||||||
|
* Show warning on navigation if currently editing comment or title (#32920)
|
||||||
|
* Make tracked time representation display as hours (#33315)
|
||||||
|
* Add No Results Prompt Message on Issue List Page (#33699)
|
||||||
|
* Add sort option recentclose for issues and pulls (#34525) #34539
|
||||||
|
|
||||||
|
* Packages
|
||||||
|
* Link to nuget dependencies (#26554)
|
||||||
|
* Add composor source field (#33502)
|
||||||
|
|
||||||
|
* Administration
|
||||||
|
* Improve navbar: add "admin" tip, add "active" style (#32927)
|
||||||
|
* Add a option "--user-type bot" to admin user create, improve role display (#27885)
|
||||||
|
* Improve admin user view page (#33735)
|
||||||
|
* Support performance trace (#32973)
|
||||||
|
* Change pprof labels to be prometheus compatible (#32865)
|
||||||
|
* Allow admins and org owners to change org member public status (#28294)
|
||||||
|
* Optimize the installation page (#32994)
|
||||||
|
* Make public URL generation configurable (#34250)
|
||||||
|
* Add a --fullname arg to gitea admin user create. (#34241)
|
||||||
|
|
||||||
|
* Others
|
||||||
|
* Improve oauth2 error handling (#33969)
|
||||||
|
* Fail mirroring more gracefully (#34002)
|
||||||
|
* Align User Details Page Header Layout with Design Specifications (#34192)
|
||||||
|
* Webhook add X-Gitea-Hook-Installation-Target-Type Header (#33752)
|
||||||
|
* Optimize the dashboard (#32990)
|
||||||
|
* Improve button layout on small screens (#33633)
|
||||||
|
* Add cropping support when modifying the user/org/repo avatar (#33498)
|
||||||
|
* Make ROOT_URL support using request Host header (#32564)
|
||||||
|
* Add `show more` organizations icon in user's profile (#32986)
|
||||||
|
* Introduce `--page-space-bottom` at 64px (#30692)
|
||||||
|
* Improve theme display (#30671)
|
||||||
|
* Add alphabetical project sorting (#33504)
|
||||||
|
* Add global lock for migrations to make upgrade more safe with multiple replications (#33706)
|
||||||
|
* Add descriptions for private repo public access settings and improve the UI (#34057)
|
||||||
|
|
||||||
|
* API
|
||||||
|
* Actions Runner rest api (#33873)
|
||||||
|
* Inclusion of rename organization api (#33303)
|
||||||
|
* Add API to support link package to repository and unlink it (#33481)
|
||||||
|
* Add API endpoint to request contents of multiple files simultaniously (#34139)
|
||||||
|
* Actions artifacts API list/download check status upload confirmed (#34273)
|
||||||
|
* Add API routes to lock and unlock issues (#34165)
|
||||||
|
* Fix some user name usages (#33689)
|
||||||
|
* Allow filtering /repos/{owner}/{repo}/pulls by target base branch queryparam (#33684)
|
||||||
|
* Improve swagger generation (#33664)
|
||||||
|
* Support Ephemeral action runners (#33570)
|
||||||
|
* Support workflow event dispatch via API (#33545)
|
||||||
|
* Support workflow event dispatch via API (#32059)
|
||||||
|
* Added Description Field for Secrets and Variables (#33526)
|
||||||
|
* Reject star-related requests if stars are disabled (#33208)
|
||||||
|
* Let API create and edit system webhooks, attempt 2 (#33180)
|
||||||
|
* Use `Project-URL` metadata field to get a PyPI package's homepage URL (#33089)
|
||||||
|
* Add `last_committer_date` and `last_author_date` for file contents API (#32921)
|
||||||
|
|
||||||
|
* REFACTORS
|
||||||
|
* Remove context from git struct (#33793)
|
||||||
|
* Refactor admin/common.ts (#33788)
|
||||||
|
* Refactor repo-settings.ts (#33785)
|
||||||
|
* Refactor repo-issue.ts (#33784)
|
||||||
|
* Small refactor to reduce unnecessary database queries and remove duplicated functions (#33779)
|
||||||
|
* Refactor initRepoBranchTagSelector to use new init framework (#33776)
|
||||||
|
* Refactor buttons to use new init framework (#33774)
|
||||||
|
* Refactor markup and pdf-viewer to use new init framework (#33772)
|
||||||
|
* Refactor error system (#33771)
|
||||||
|
* Refactor mail code (#33768)
|
||||||
|
* Update TypeScript types (#33799)
|
||||||
|
* Refactor older tests to use testify (#33140)
|
||||||
|
* Move notifywatch to service layer (#33825)
|
||||||
|
* Decouple context from repository related structs (#33823)
|
||||||
|
* Remove context from mail struct (#33811)
|
||||||
|
* Refactor dropdown ellipsis (#34123)
|
||||||
|
* Refactor functions to reduce repopath expose (#33892)
|
||||||
|
* Refactor repo-diff.ts (#33746)
|
||||||
|
* Refactor web route handler (#33488)
|
||||||
|
* Refactor user & avatar (#33433)
|
||||||
|
* Refactor user package (#33423)
|
||||||
|
* Refactor decouple context from migration structs (#33399)
|
||||||
|
* Refactor context flash msg and global variables (#33375)
|
||||||
|
* Refactor response writer & access logger (#33323)
|
||||||
|
* Refactor ref type (#33242)
|
||||||
|
* Refactor context repository (#33202)
|
||||||
|
* Refactor legacy JS (#33115)
|
||||||
|
* Refactor legacy line-number and scroll code (#33094)
|
||||||
|
* Refactor env var related code (#33075)
|
||||||
|
* Move SetMerged to service layer (#33045)
|
||||||
|
* Merge updatecommentattachment functions (#33044)
|
||||||
|
* Refactor pull-request compare&create page (#33071)
|
||||||
|
* Refactor repo-new.ts (#33070)
|
||||||
|
* Refactor pagination (#33037)
|
||||||
|
* Refactor tests (#33021)
|
||||||
|
* Refactor markup render to fix various path problems (#34114)
|
||||||
|
* Refactor Branch struct in package modules/git (#33980)
|
||||||
|
* Don't create duplicated functions for code repositories and wiki repositories (#33924)
|
||||||
|
* Move git references checking to gitrepo packages to reduce expose of repository path (#33891)
|
||||||
|
* Refactor cache-control (#33861)
|
||||||
|
* Decouple diff stats query from actual diffing (#33810)
|
||||||
|
* Move part of updating protected branch logic to service layer (#33742)
|
||||||
|
* Decouple Batch from git.Repository to simplify usage without requiring the creation of a Repository struct. (#34001)
|
||||||
|
* Refactor tmpl and blob_excerpt (#32967)
|
||||||
|
* Refactor template & test related code (#32938)
|
||||||
|
* Refactor db package and remove unnecessary `DumpTables` (#32930)
|
||||||
|
* Refactor pprof labels and process desc (#32909)
|
||||||
|
* Refactor repo-projects.ts (#32892)
|
||||||
|
* Refactor getpatch/getdiff functions and remove unnecessary fallback (#32817)
|
||||||
|
* Uniform all temporary directories and allow customizing temp path (#32352)
|
||||||
|
* Remove context from retry downloader (#33871)
|
||||||
|
* Refactor global init code and add more comments (#33755)
|
||||||
|
* Remove some unnecessary template helpers (#33069)
|
||||||
|
* Move and rename UpdateRepository (#34136)
|
||||||
|
* Move hooks function to gitrepo and reduce expose repopath (#33890)
|
||||||
|
* Add abstraction layer to delete repository from disk (#33879)
|
||||||
|
* Add abstraction layer to check if the repository exists on disk (#33874)
|
||||||
|
* Move ParseCommitWithSSHSignature to service layer (#34087)
|
||||||
|
* Move duplicated functions (#33977)
|
||||||
|
* Extract code to their own functions for push update (#33944)
|
||||||
|
* Move gitgraph from modules to services layer (#33527)
|
||||||
|
* Move commits signature and verify functions to service layers (#33605)
|
||||||
|
* Use `CloseIssue` and `ReopenIssue` instead of `ChangeStatus` (#32467)
|
||||||
|
* Refactor arch route handlers (#32993)
|
||||||
|
* Refactor "string truncate" (#32984)
|
||||||
|
* Refactor arch route handlers (#32972)
|
||||||
|
* Clarify path param naming (#32969)
|
||||||
|
* Refactor request context (#32956)
|
||||||
|
* Move some errors to their own sub packages (#32880)
|
||||||
|
* Move RepoTransfer from models to models/repo sub package (#32506)
|
||||||
|
* Move delete deploy keys into service layer (#32201)
|
||||||
|
* Refactor webhook events (#33337)
|
||||||
|
* Move some Actions related functions from `routers` to `services` (#33280)
|
||||||
|
* Refactor RefName (#33234)
|
||||||
|
* Refactor context RefName and RepoAssignment (#33226)
|
||||||
|
* Refactor repository transfer (#33211)
|
||||||
|
* Refactor error system (#33626)
|
||||||
|
* Refactor error system (#33610)
|
||||||
|
* Refactor package (routes and error handling, npm peer dependency) (#33111)
|
||||||
|
* Use test context in tests and new loop system in benchmarks (#33648)
|
||||||
|
* Some small refactors (#33144)
|
||||||
|
* Simplify context ref name (#33267)
|
||||||
|
|
||||||
|
* BUGFIXES
|
||||||
|
* Fix some dropdown problems on the issue sidebar (#34308) #34327
|
||||||
|
* Do not return archive download URLs in API if downloads are disabled (#34324) #34338
|
||||||
|
* Fix LFS files being editable in web UI (#34356) #34362
|
||||||
|
* Fix only text/* being viewable in web UI (#34374) #34378
|
||||||
|
* Fix LFS file not stored in LFS when uploaded/edited via API or web UI (#34367)
|
||||||
|
* Grey out expired artifact on Artifacts list (#34314) #34404
|
||||||
|
* Fix incorrect divergence cache after switching default branch (#34370) #34406
|
||||||
|
* Refactor commit message rendering and fix bugs (#34412) #34414
|
||||||
|
* Merge and tweak markup editor expander CSS (#34409) #34415
|
||||||
|
* Fix GetUsersByEmails (#34423) #34425
|
||||||
|
* Only git operations should update last changed of a repository (#34388) #34427
|
||||||
|
* Fix comment textarea scroll issue in Firefox (#34438) #34446
|
||||||
|
* Fix repo broken check (#34444) #34452
|
||||||
|
* Fix remove org user failure on mssql (#34449) #34453
|
||||||
|
* Fix Workflow run Not Found page (#34459) #34466
|
||||||
|
* When updating comment, if the content is the same, just return and not update the database (#34422) #34464
|
||||||
|
* Fix project board view (#34470) #34475
|
||||||
|
* Fix get / delete runner to use consistent http 404 and 500 status (#34480) #34488
|
||||||
|
* Fix url validation in webhook add/edit API (#34492) #34496
|
||||||
|
* Fix edithook api can not update package, status and workflow_job events (#34495) #34499
|
||||||
|
* Fix ephemeral runner deletion (#34447) #34513
|
||||||
|
* Don't display error log when .git-blame-ignore-revs doesn't exist (#34457)
|
||||||
|
* Only allow admins to rename default/protected branches (#33276)
|
||||||
|
* Improve "lock conversation" UI (#34207)
|
||||||
|
* Fix incorrect file links (#34189)
|
||||||
|
* Optimize Overflow Menu (#34183)
|
||||||
|
* Check user/org repo limit instead of doer (#34147)
|
||||||
|
* Make markdown render match GitHub's behavior (#34129)
|
||||||
|
* Fix team permission (#34128)
|
||||||
|
* Correctly handle submodule view and avoid throwing 500 error (#34121)
|
||||||
|
* Fix users being able bypass limits with repo transfers (#34031)
|
||||||
|
* Avoid creating unnecessary temporary cat file sub process (#33942)
|
||||||
|
* Refactor organization menu (#33928)
|
||||||
|
* Fix various Fomantic UI and htmx problems (#33851)
|
||||||
|
* Fix 500 error when error occurred in migration page (#33256)
|
||||||
|
* Validate that the tag doesn't exist when creating a tag via the web (#33241)
|
||||||
|
* Add missed transaction on setmerged (#33079)
|
||||||
|
* Rework create/fork/adopt/generate repository to make sure resources will be cleanup once failed (#31035)
|
||||||
|
* Valid email address should only start with alphanumeric (#28174)
|
||||||
|
* Fix webhook url (#34186)
|
||||||
|
* Fix "toAbsoluteLocaleDate" test when system locale is not en-US (#33939)
|
||||||
|
* Fix file name could not be searched if the file was not a text file when using the Bleve indexer (#33959)
|
||||||
|
* Fix cannot delete runners via the modal dialog (#33895)
|
||||||
|
* Fix unpin hint on the pinned pull requests (#33207)
|
||||||
|
* Fix parentCommit invalid memory address or nil pointer dereference. (#33204)
|
||||||
|
* Fix comment header padding (#33377)
|
||||||
|
* Fix some migration and repo name problems (#33986)
|
||||||
|
* Fix various trivial frontend problems (#34263)
|
||||||
|
* Fix Set Email Preference dropdown and button placement (#34255)
|
||||||
|
* Fix quoted replies incorrectly render user input as part of the quote (#34216)
|
||||||
|
* Fix button alignments and remove unnecessary styles (#34206)
|
||||||
|
* Restore form inputs on organization create error (#34201)
|
||||||
|
* Try to fix ACME (3rd) (#33807)
|
||||||
|
* Fix incorrect ref "blob" (#33240)
|
||||||
|
* Fix dynamic content loading init problem (#33748)
|
||||||
|
* Fix git empty check and HEAD request (#33690)
|
||||||
|
* Fix Untranslated Text on Actions Page (#33635)
|
||||||
|
* Fix issue label delete incorrect labels webhook payload (#34575)
|
||||||
|
* Fix incorrect page navigation with up and down arrow on last item of dashboard repos (#34570)
|
||||||
|
* Fix/improve avatar sync from LDAP (#34573)
|
||||||
|
* Fix some trivial problems (#34579)
|
||||||
|
* Retain issue sort type when a keyword search is introduced (#34559)
|
||||||
|
* Always use an empty line to separate the commit message and trailer (#34512)
|
||||||
|
* Fix line-button issue after file selection in file tree (#34574)
|
||||||
|
* Fix doctor deleting orphaned issues attachments (#34142)
|
||||||
|
* Add webhook assigning test and fix possible bug (#34420)
|
||||||
|
* Fix possible nil description of pull request when migrating from CodeCommit (#34541)
|
||||||
|
* Refactor commit reader (#34542)
|
||||||
|
* Fix possible pull request broken when leave the page immediately after clicking the update button #34509
|
||||||
|
* Ignore "Close" error when uploading container blob (#34620)
|
||||||
|
* Fix missed merge commit sha and time when migrating from codecommit (#34645)
|
||||||
|
* Fix GetUsersByEmails (#34643)
|
||||||
|
* Misc CSS fixes (#34638)
|
||||||
|
* Add codecommit to supported services in api docs (#34626)
|
||||||
|
* Validate hex colors when creating/editing labels (#34623)
|
||||||
|
* Fix possible pull request broken when leave the page immediately after clicking the update button (#34509)
|
||||||
|
* Fix margin issue in markup paragraph rendering (#34599)
|
||||||
|
* Fix migration pull request title too long (#34577)
|
||||||
|
* Fix footnote jump behavior on the issue page. (#34621)
|
||||||
|
* Fix "oras" OCI client compatibility (#34666)
|
||||||
|
* Fix last admin check when syncing users (#34649)
|
||||||
|
* Fix skip paths check on tag push events in workflows (#34602) #34670
|
||||||
|
|
||||||
|
* MISC
|
||||||
|
|
||||||
|
* Bump to alpine 3.22 (#34613)
|
||||||
|
* Make pull request and issue history more compact (#34588)
|
||||||
|
* Run integration tests against postgres 14 (#34514) #34536
|
||||||
|
* Enable addtional linters (#34085)
|
||||||
|
* Enable testifylint rules (#34075)
|
||||||
|
* Enable staticcheck QFxxxx rules (#34064)
|
||||||
|
* Improve Actions test (#32883)
|
||||||
|
* Drop fomantic build (#33845)
|
||||||
|
* Go1.24 (#33562)
|
||||||
|
* Run yamllint with strict mode, fix issue (#33551)
|
||||||
|
* Disable cron task to update license (#33486)
|
||||||
|
* Optimize makefile help information generation (#33390)
|
||||||
|
* Convert github.com/xanzy/go-gitlab into gitlab.com/gitlab-org/api/client-go (#33126)
|
||||||
|
* Add missed changelogs (#33649)
|
||||||
|
* Update .changelog file to add performance label group (#33472)
|
||||||
|
* Add missing POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES in app.example.ini (#33363)
|
||||||
|
* Update README screenshots (#33347)
|
||||||
|
* Update unrs-resolver (#34279)
|
||||||
|
* Update go&js dependencies (#34262)
|
||||||
|
* Optimize the calling code of queryElems (#34235)
|
||||||
|
* Update protected_branch.tmpl (#34193)
|
||||||
|
* Feat/optimize span svg layout (#34185)
|
||||||
|
* Set MERMAID_MAX_SOURCE_CHARACTERS to 50000 (#34152)
|
||||||
|
* Update JS and PY deps (#34143)
|
||||||
|
* Add Chinese translations for README files (#34132)
|
||||||
|
* Use `overflow-wrap: anywhere` to replace `word-break: break-all` (#34126)
|
||||||
|
* Clarify ownership in password change error messages (#34092)
|
||||||
|
* Add toggleClass function in dom.ts (#34063)
|
||||||
|
* Update to golangci-lint v2 (#34054)
|
||||||
|
* Update Makefile test comments (#34013)
|
||||||
|
* Update go mod dependencies (#33988)
|
||||||
|
* Use filepath.Join instead of path.Join for file system file operations (#33978)
|
||||||
|
* Prepare common tmpl functions in a middleware (#33957)
|
||||||
|
* Remove unused or abused styles (#33918)
|
||||||
|
* Update JS and PY deps, misc tweaks (#33903)
|
||||||
|
* Try to figure out attribute checker problem (#33901)
|
||||||
|
* Add lock for a repository pull mirror (#33876)
|
||||||
|
* Fine tune push mirror UI (#33866)
|
||||||
|
* Improve issue & code search (#33860)
|
||||||
|
* Use pullrequestlist instead of []*pullrequest (#33765)
|
||||||
|
* Upgrade act to 0.261.4 and actions-proto-go to v0.4.1 (#33760)
|
||||||
|
* Align sidebar gears to the right (#33721)
|
||||||
|
* Update Go dependencies (skip blevesearch, meilisearch) (#33655)
|
||||||
|
* Add migrations and doctor fixes (#33556)
|
||||||
|
* Remove "class-name" from svg icon (#33540)
|
||||||
|
* Update MAINTAINERS (#33529)
|
||||||
|
* Add "No data available" display when list is empty (#33517)
|
||||||
|
* Use `git diff-tree` for `DiffFileTree` on diff pages (#33514)
|
||||||
|
* Give organisation members access to organisation feeds (#33508)
|
||||||
|
* Update feishu icon (#33470)
|
||||||
|
* Hide/disable unusable UI elements when a repository is archived (#33459)
|
||||||
|
* Update `@github/text-expander-element` to 2.9.0 (#33435)
|
||||||
|
* Do not access GitRepo when a repo is being created (#33380)
|
||||||
|
* Fix incorrect ref usages (#33301)
|
||||||
|
* Prepare for support performance trace (#33286)
|
||||||
|
* Enable Typescript `noImplicitThis` (#33250)
|
||||||
|
* Remove unused CSS styles and move some styles to proper files (#33217)
|
||||||
|
* Add .run to gitignore (#33175)
|
||||||
|
* Fix typo in gitea downloader test and add missing codebase in `ToGitServiceType` (#33146)
|
||||||
|
* Remove extended glob pattern from branch protection UI (#33125)
|
||||||
|
* Clean up legacy form CSS styles (#33081)
|
||||||
|
* Unset XDG_HOME_CONFIG as gitea manages configuration locations (#33067)
|
||||||
|
* Add IntelliJ Gateway's .uuid to gitignore (#33052)
|
||||||
|
* User facing messages for AGit errors (#33012)
|
||||||
|
* Always show assignees on right (#33006)
|
||||||
|
* Fix eslint (#33002)
|
||||||
|
* Update JS dependencies (#32914)
|
||||||
|
* Bump x/net (#32896) (#32900)
|
||||||
|
* 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
|
||||||
|
|
||||||
|
* SECURITY
|
||||||
|
* Fix a bug when uploading file via lfs ssh command (#34408) (#34411)
|
||||||
|
* Update net package (#34228) (#34232)
|
||||||
|
* BUGFIXES
|
||||||
|
* Fix releases sidebar navigation link (#34436) #34439
|
||||||
|
* Fix bug webhook milestone is not right. (#34419) #34429
|
||||||
|
* Fix two missed null value checks on the wiki page. (#34205) (#34215)
|
||||||
|
* Swift files can be passed either as file or as form value (#34068) (#34236)
|
||||||
|
* Fix bug when API get pull changed files for deleted head repository (#34333) (#34368)
|
||||||
|
* Upgrade github v61 -> v71 to fix migrating bug (#34389)
|
||||||
|
* Fix bug when visiting comparation page (#34334) (#34364)
|
||||||
|
* Fix wrong review requests when updating the pull request (#34286) (#34304)
|
||||||
|
* Fix github migration error when using multiple tokens (#34144) (#34302)
|
||||||
|
* Explicitly not update indexes when sync database schemas (#34281) (#34295)
|
||||||
|
* Fix panic when comment is nil (#34257) (#34277)
|
||||||
|
* Fix project board links to related Pull Requests (#34213) (#34222)
|
||||||
|
* Don't assume the default wiki branch is master in the wiki API (#34244) (#34245)
|
||||||
|
* DOCUMENTATION
|
||||||
|
* Update token creation API swagger documentation (#34288) (#34296)
|
||||||
|
* MISC
|
||||||
|
* Fix CI Build (#34315)
|
||||||
|
* Add riscv64 support (#34199) (#34204)
|
||||||
|
* Bump go version in go.mod (#34160)
|
||||||
|
* 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
|
||||||
|
|
||||||
|
* Enhancements
|
||||||
|
* Add a config option to block "expensive" pages for anonymous users (#34024) (#34071)
|
||||||
|
* Also check default ssh-cert location for host (#34099) (#34100) (#34116)
|
||||||
|
* BUGFIXES
|
||||||
|
* Fix discord webhook 400 status code when description limit is exceeded (#34084) (#34124)
|
||||||
|
* Get changed files based on merge base when checking `pull_request` actions trigger (#34106) (#34120)
|
||||||
|
* Fix invalid version in RPM package path (#34112) (#34115)
|
||||||
|
* Return default avatar url when user id is zero rather than updating database (#34094) (#34095)
|
||||||
|
* Add additional ReplaceAll in pathsep to cater for different pathsep (#34061) (#34070)
|
||||||
|
* Try to fix check-attr bug (#34029) (#34033)
|
||||||
|
* Git client will follow 301 but 307 (#34005) (#34010)
|
||||||
|
* Fix block expensive for 1.23 (#34127)
|
||||||
|
* Fix markdown frontmatter rendering (#34102) (#34107)
|
||||||
|
* Add new CLI flags to set name and scopes when creating a user with access token (#34080) (#34103)
|
||||||
|
* Do not show 500 error when default branch doesn't exist (#34096) (#34097)
|
||||||
|
* Hide activity contributors, recent commits and code frequrency left tabs if there is no code permission (#34053) (#34065)
|
||||||
|
* Simplify emoji rendering (#34048) (#34049)
|
||||||
|
* Adjust the layout of the toolbar on the Issues/Projects page (#33667) (#34047)
|
||||||
|
* Pull request updates will also trigger code owners review requests (#33744) (#34045)
|
||||||
|
* Fix org repo creation being limited by user limits (#34030) (#34044)
|
||||||
|
* Fix git client accessing renamed repo (#34034) (#34043)
|
||||||
|
* Fix the issue with error message logging for the `check-attr` command on Windows OS. (#34035) (#34036)
|
||||||
|
* Polyfill WeakRef (#34025) (#34028)
|
||||||
|
|
||||||
## [1.23.6](https://github.com/go-gitea/gitea/releases/tag/v1.23.6) - 2025-03-24
|
## [1.23.6](https://github.com/go-gitea/gitea/releases/tag/v1.23.6) - 2025-03-24
|
||||||
|
|
||||||
* SECURITY
|
* SECURITY
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Build stage
|
# Build stage
|
||||||
FROM docker.io/library/golang:1.24-alpine3.21 AS build-env
|
FROM docker.io/library/golang:1.24-alpine3.22 AS build-env
|
||||||
|
|
||||||
ARG GOPROXY
|
ARG GOPROXY
|
||||||
ENV GOPROXY=${GOPROXY:-direct}
|
ENV GOPROXY=${GOPROXY:-direct}
|
||||||
@ -41,7 +41,7 @@ RUN chmod 755 /tmp/local/usr/bin/entrypoint \
|
|||||||
/go/src/code.gitea.io/gitea/environment-to-ini
|
/go/src/code.gitea.io/gitea/environment-to-ini
|
||||||
RUN chmod 644 /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete
|
RUN chmod 644 /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete
|
||||||
|
|
||||||
FROM docker.io/library/alpine:3.21
|
FROM docker.io/library/alpine:3.22
|
||||||
LABEL maintainer="maintainers@gitea.io"
|
LABEL maintainer="maintainers@gitea.io"
|
||||||
|
|
||||||
EXPOSE 22 3000
|
EXPOSE 22 3000
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Build stage
|
# Build stage
|
||||||
FROM docker.io/library/golang:1.24-alpine3.21 AS build-env
|
FROM docker.io/library/golang:1.24-alpine3.22 AS build-env
|
||||||
|
|
||||||
ARG GOPROXY
|
ARG GOPROXY
|
||||||
ENV GOPROXY=${GOPROXY:-direct}
|
ENV GOPROXY=${GOPROXY:-direct}
|
||||||
@ -39,7 +39,7 @@ RUN chmod 755 /tmp/local/usr/local/bin/docker-entrypoint.sh \
|
|||||||
/go/src/code.gitea.io/gitea/environment-to-ini
|
/go/src/code.gitea.io/gitea/environment-to-ini
|
||||||
RUN chmod 644 /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete
|
RUN chmod 644 /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete
|
||||||
|
|
||||||
FROM docker.io/library/alpine:3.21
|
FROM docker.io/library/alpine:3.22
|
||||||
LABEL maintainer="maintainers@gitea.io"
|
LABEL maintainer="maintainers@gitea.io"
|
||||||
|
|
||||||
EXPOSE 2222 3000
|
EXPOSE 2222 3000
|
||||||
|
4
assets/go-licenses.json
generated
4
assets/go-licenses.json
generated
File diff suppressed because one or more lines are too long
@ -38,12 +38,10 @@ var (
|
|||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: "force-smtps",
|
Name: "force-smtps",
|
||||||
Usage: "SMTPS is always used on port 465. Set this to force SMTPS on other ports.",
|
Usage: "SMTPS is always used on port 465. Set this to force SMTPS on other ports.",
|
||||||
Value: true,
|
|
||||||
},
|
},
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: "skip-verify",
|
Name: "skip-verify",
|
||||||
Usage: "Skip TLS verify.",
|
Usage: "Skip TLS verify.",
|
||||||
Value: true,
|
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "helo-hostname",
|
Name: "helo-hostname",
|
||||||
@ -53,7 +51,6 @@ var (
|
|||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: "disable-helo",
|
Name: "disable-helo",
|
||||||
Usage: "Disable SMTP helo.",
|
Usage: "Disable SMTP helo.",
|
||||||
Value: true,
|
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "allowed-domains",
|
Name: "allowed-domains",
|
||||||
@ -63,7 +60,6 @@ var (
|
|||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: "skip-local-2fa",
|
Name: "skip-local-2fa",
|
||||||
Usage: "Skip 2FA to log on.",
|
Usage: "Skip 2FA to log on.",
|
||||||
Value: true,
|
|
||||||
},
|
},
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: "active",
|
Name: "active",
|
||||||
|
@ -80,6 +80,11 @@ wiki, issues, labels, releases, release_assets, milestones, pull_requests, comme
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runDumpRepository(ctx *cli.Context) error {
|
func runDumpRepository(ctx *cli.Context) error {
|
||||||
|
setupConsoleLogger(log.INFO, log.CanColorStderr, os.Stderr)
|
||||||
|
|
||||||
|
setting.DisableLoggerInit()
|
||||||
|
setting.LoadSettings() // cannot access skip_tls_verify settings otherwise
|
||||||
|
|
||||||
stdCtx, cancel := installSignals()
|
stdCtx, cancel := installSignals()
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
hookBatchSize = 30
|
hookBatchSize = 500
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -118,7 +118,6 @@ var (
|
|||||||
Name: "rotate",
|
Name: "rotate",
|
||||||
Aliases: []string{"r"},
|
Aliases: []string{"r"},
|
||||||
Usage: "Rotate logs",
|
Usage: "Rotate logs",
|
||||||
Value: true,
|
|
||||||
},
|
},
|
||||||
&cli.Int64Flag{
|
&cli.Int64Flag{
|
||||||
Name: "max-size",
|
Name: "max-size",
|
||||||
@ -129,7 +128,6 @@ var (
|
|||||||
Name: "daily",
|
Name: "daily",
|
||||||
Aliases: []string{"d"},
|
Aliases: []string{"d"},
|
||||||
Usage: "Rotate logs daily",
|
Usage: "Rotate logs daily",
|
||||||
Value: true,
|
|
||||||
},
|
},
|
||||||
&cli.IntFlag{
|
&cli.IntFlag{
|
||||||
Name: "max-days",
|
Name: "max-days",
|
||||||
@ -140,7 +138,6 @@ var (
|
|||||||
Name: "compress",
|
Name: "compress",
|
||||||
Aliases: []string{"z"},
|
Aliases: []string{"z"},
|
||||||
Usage: "Compress rotated logs",
|
Usage: "Compress rotated logs",
|
||||||
Value: true,
|
|
||||||
},
|
},
|
||||||
&cli.IntFlag{
|
&cli.IntFlag{
|
||||||
Name: "compression-level",
|
Name: "compression-level",
|
||||||
|
89
cmd/serv.go
89
cmd/serv.go
@ -11,7 +11,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -20,7 +19,7 @@ import (
|
|||||||
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/modules/container"
|
"code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/json"
|
"code.gitea.io/gitea/modules/json"
|
||||||
"code.gitea.io/gitea/modules/lfstransfer"
|
"code.gitea.io/gitea/modules/lfstransfer"
|
||||||
@ -37,14 +36,6 @@ import (
|
|||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
verbUploadPack = "git-upload-pack"
|
|
||||||
verbUploadArchive = "git-upload-archive"
|
|
||||||
verbReceivePack = "git-receive-pack"
|
|
||||||
verbLfsAuthenticate = "git-lfs-authenticate"
|
|
||||||
verbLfsTransfer = "git-lfs-transfer"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CmdServ represents the available serv sub-command.
|
// CmdServ represents the available serv sub-command.
|
||||||
var CmdServ = &cli.Command{
|
var CmdServ = &cli.Command{
|
||||||
Name: "serv",
|
Name: "serv",
|
||||||
@ -78,22 +69,6 @@ func setup(ctx context.Context, debug bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
// keep getAccessMode() in sync
|
|
||||||
allowedCommands = container.SetOf(
|
|
||||||
verbUploadPack,
|
|
||||||
verbUploadArchive,
|
|
||||||
verbReceivePack,
|
|
||||||
verbLfsAuthenticate,
|
|
||||||
verbLfsTransfer,
|
|
||||||
)
|
|
||||||
allowedCommandsLfs = container.SetOf(
|
|
||||||
verbLfsAuthenticate,
|
|
||||||
verbLfsTransfer,
|
|
||||||
)
|
|
||||||
alphaDashDotPattern = regexp.MustCompile(`[^\w-\.]`)
|
|
||||||
)
|
|
||||||
|
|
||||||
// fail prints message to stdout, it's mainly used for git serv and git hook commands.
|
// fail prints message to stdout, it's mainly used for git serv and git hook commands.
|
||||||
// The output will be passed to git client and shown to user.
|
// The output will be passed to git client and shown to user.
|
||||||
func fail(ctx context.Context, userMessage, logMsgFmt string, args ...any) error {
|
func fail(ctx context.Context, userMessage, logMsgFmt string, args ...any) error {
|
||||||
@ -139,19 +114,20 @@ func handleCliResponseExtra(extra private.ResponseExtra) error {
|
|||||||
|
|
||||||
func getAccessMode(verb, lfsVerb string) perm.AccessMode {
|
func getAccessMode(verb, lfsVerb string) perm.AccessMode {
|
||||||
switch verb {
|
switch verb {
|
||||||
case verbUploadPack, verbUploadArchive:
|
case git.CmdVerbUploadPack, git.CmdVerbUploadArchive:
|
||||||
return perm.AccessModeRead
|
return perm.AccessModeRead
|
||||||
case verbReceivePack:
|
case git.CmdVerbReceivePack:
|
||||||
return perm.AccessModeWrite
|
return perm.AccessModeWrite
|
||||||
case verbLfsAuthenticate, verbLfsTransfer:
|
case git.CmdVerbLfsAuthenticate, git.CmdVerbLfsTransfer:
|
||||||
switch lfsVerb {
|
switch lfsVerb {
|
||||||
case "upload":
|
case git.CmdSubVerbLfsUpload:
|
||||||
return perm.AccessModeWrite
|
return perm.AccessModeWrite
|
||||||
case "download":
|
case git.CmdSubVerbLfsDownload:
|
||||||
return perm.AccessModeRead
|
return perm.AccessModeRead
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// should be unreachable
|
// should be unreachable
|
||||||
|
setting.PanicInDevOrTesting("unknown verb: %s %s", verb, lfsVerb)
|
||||||
return perm.AccessModeNone
|
return perm.AccessModeNone
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -230,12 +206,12 @@ func runServ(c *cli.Context) error {
|
|||||||
log.Debug("SSH_ORIGINAL_COMMAND: %s", os.Getenv("SSH_ORIGINAL_COMMAND"))
|
log.Debug("SSH_ORIGINAL_COMMAND: %s", os.Getenv("SSH_ORIGINAL_COMMAND"))
|
||||||
}
|
}
|
||||||
|
|
||||||
words, err := shellquote.Split(cmd)
|
sshCmdArgs, err := shellquote.Split(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fail(ctx, "Error parsing arguments", "Failed to parse arguments: %v", err)
|
return fail(ctx, "Error parsing arguments", "Failed to parse arguments: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(words) < 2 {
|
if len(sshCmdArgs) < 2 {
|
||||||
if git.DefaultFeatures().SupportProcReceive {
|
if git.DefaultFeatures().SupportProcReceive {
|
||||||
// for AGit Flow
|
// for AGit Flow
|
||||||
if cmd == "ssh_info" {
|
if cmd == "ssh_info" {
|
||||||
@ -246,25 +222,21 @@ func runServ(c *cli.Context) error {
|
|||||||
return fail(ctx, "Too few arguments", "Too few arguments in cmd: %s", cmd)
|
return fail(ctx, "Too few arguments", "Too few arguments in cmd: %s", cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
verb := words[0]
|
repoPath := strings.TrimPrefix(sshCmdArgs[1], "/")
|
||||||
repoPath := strings.TrimPrefix(words[1], "/")
|
repoPathFields := strings.SplitN(repoPath, "/", 2)
|
||||||
|
if len(repoPathFields) != 2 {
|
||||||
var lfsVerb string
|
|
||||||
|
|
||||||
rr := strings.SplitN(repoPath, "/", 2)
|
|
||||||
if len(rr) != 2 {
|
|
||||||
return fail(ctx, "Invalid repository path", "Invalid repository path: %v", repoPath)
|
return fail(ctx, "Invalid repository path", "Invalid repository path: %v", repoPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
username := rr[0]
|
username := repoPathFields[0]
|
||||||
reponame := strings.TrimSuffix(rr[1], ".git")
|
reponame := strings.TrimSuffix(repoPathFields[1], ".git") // “the-repo-name" or "the-repo-name.wiki"
|
||||||
|
|
||||||
// LowerCase and trim the repoPath as that's how they are stored.
|
// LowerCase and trim the repoPath as that's how they are stored.
|
||||||
// This should be done after splitting the repoPath into username and reponame
|
// This should be done after splitting the repoPath into username and reponame
|
||||||
// so that username and reponame are not affected.
|
// so that username and reponame are not affected.
|
||||||
repoPath = strings.ToLower(strings.TrimSpace(repoPath))
|
repoPath = strings.ToLower(strings.TrimSpace(repoPath))
|
||||||
|
|
||||||
if alphaDashDotPattern.MatchString(reponame) {
|
if !repo.IsValidSSHAccessRepoName(reponame) {
|
||||||
return fail(ctx, "Invalid repo name", "Invalid repo name: %s", reponame)
|
return fail(ctx, "Invalid repo name", "Invalid repo name: %s", reponame)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -286,22 +258,23 @@ func runServ(c *cli.Context) error {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
if allowedCommands.Contains(verb) {
|
verb, lfsVerb := sshCmdArgs[0], ""
|
||||||
if allowedCommandsLfs.Contains(verb) {
|
if !git.IsAllowedVerbForServe(verb) {
|
||||||
if !setting.LFS.StartServer {
|
|
||||||
return fail(ctx, "LFS Server is not enabled", "")
|
|
||||||
}
|
|
||||||
if verb == verbLfsTransfer && !setting.LFS.AllowPureSSH {
|
|
||||||
return fail(ctx, "LFS SSH transfer is not enabled", "")
|
|
||||||
}
|
|
||||||
if len(words) > 2 {
|
|
||||||
lfsVerb = words[2]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return fail(ctx, "Unknown git command", "Unknown git command %s", verb)
|
return fail(ctx, "Unknown git command", "Unknown git command %s", verb)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if git.IsAllowedVerbForServeLfs(verb) {
|
||||||
|
if !setting.LFS.StartServer {
|
||||||
|
return fail(ctx, "LFS Server is not enabled", "")
|
||||||
|
}
|
||||||
|
if verb == git.CmdVerbLfsTransfer && !setting.LFS.AllowPureSSH {
|
||||||
|
return fail(ctx, "LFS SSH transfer is not enabled", "")
|
||||||
|
}
|
||||||
|
if len(sshCmdArgs) > 2 {
|
||||||
|
lfsVerb = sshCmdArgs[2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
requestedMode := getAccessMode(verb, lfsVerb)
|
requestedMode := getAccessMode(verb, lfsVerb)
|
||||||
|
|
||||||
results, extra := private.ServCommand(ctx, keyID, username, reponame, requestedMode, verb, lfsVerb)
|
results, extra := private.ServCommand(ctx, keyID, username, reponame, requestedMode, verb, lfsVerb)
|
||||||
@ -310,7 +283,7 @@ func runServ(c *cli.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LFS SSH protocol
|
// LFS SSH protocol
|
||||||
if verb == verbLfsTransfer {
|
if verb == git.CmdVerbLfsTransfer {
|
||||||
token, err := getLFSAuthToken(ctx, lfsVerb, results)
|
token, err := getLFSAuthToken(ctx, lfsVerb, results)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -319,7 +292,7 @@ func runServ(c *cli.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LFS token authentication
|
// LFS token authentication
|
||||||
if verb == verbLfsAuthenticate {
|
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 := getLFSAuthToken(ctx, lfsVerb, results)
|
||||||
|
@ -18,7 +18,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/google/go-github/v61/github"
|
"github.com/google/go-github/v71/github"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
6
flake.lock
generated
6
flake.lock
generated
@ -20,11 +20,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1739214665,
|
"lastModified": 1747179050,
|
||||||
"narHash": "sha256-26L8VAu3/1YRxS8MHgBOyOM8xALdo6N0I04PgorE7UM=",
|
"narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "64e75cd44acf21c7933d61d7721e812eac1b5a0a",
|
"rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
6
go.mod
6
go.mod
@ -51,7 +51,7 @@ require (
|
|||||||
github.com/gliderlabs/ssh v0.3.8
|
github.com/gliderlabs/ssh v0.3.8
|
||||||
github.com/go-ap/activitypub v0.0.0-20250409143848-7113328b1f3d
|
github.com/go-ap/activitypub v0.0.0-20250409143848-7113328b1f3d
|
||||||
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73
|
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73
|
||||||
github.com/go-chi/chi/v5 v5.2.1
|
github.com/go-chi/chi/v5 v5.2.2
|
||||||
github.com/go-chi/cors v1.2.1
|
github.com/go-chi/cors v1.2.1
|
||||||
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
|
||||||
@ -66,7 +66,7 @@ require (
|
|||||||
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
|
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f
|
||||||
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
|
github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||||
github.com/google/go-github/v61 v61.0.0
|
github.com/google/go-github/v71 v71.0.0
|
||||||
github.com/google/licenseclassifier/v2 v2.0.0
|
github.com/google/licenseclassifier/v2 v2.0.0
|
||||||
github.com/google/pprof v0.0.0-20250422154841-e1f9c1950416
|
github.com/google/pprof v0.0.0-20250422154841-e1f9c1950416
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
@ -325,6 +325,8 @@ replace github.com/charmbracelet/git-lfs-transfer => gitea.com/gitea/git-lfs-tra
|
|||||||
// TODO: This could be removed after https://github.com/mholt/archiver/pull/396 merged
|
// TODO: This could be removed after https://github.com/mholt/archiver/pull/396 merged
|
||||||
replace github.com/mholt/archiver/v3 => github.com/anchore/archiver/v3 v3.5.2
|
replace github.com/mholt/archiver/v3 => github.com/anchore/archiver/v3 v3.5.2
|
||||||
|
|
||||||
|
replace git.sr.ht/~mariusor/go-xsd-duration => gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078
|
||||||
|
|
||||||
exclude github.com/gofrs/uuid v3.2.0+incompatible
|
exclude github.com/gofrs/uuid v3.2.0+incompatible
|
||||||
|
|
||||||
exclude github.com/gofrs/uuid v4.0.0+incompatible
|
exclude github.com/gofrs/uuid v4.0.0+incompatible
|
||||||
|
12
go.sum
12
go.sum
@ -14,12 +14,12 @@ dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
|||||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||||
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=
|
||||||
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:cliQ4HHsCo6xi2oWZYKWW4bly/Ory9FuTpFPRxj/mAg=
|
|
||||||
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs=
|
|
||||||
gitea.com/gitea/act v0.261.4 h1:Tf9eLlvsYFtKcpuxlMvf9yT3g4Hshb2Beqw6C1STuH8=
|
gitea.com/gitea/act v0.261.4 h1:Tf9eLlvsYFtKcpuxlMvf9yT3g4Hshb2Beqw6C1STuH8=
|
||||||
gitea.com/gitea/act v0.261.4/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok=
|
gitea.com/gitea/act v0.261.4/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 h1:baHaNoBSRaeq/xKayEXwiDQtlIjps4Ac/Ll4KqLMB40=
|
||||||
gitea.com/gitea/git-lfs-transfer v0.2.0/go.mod h1:UrXUCm3xLQkq15fu7qlXHUMlrhdlXHoi13KH2Dfiits=
|
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/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=
|
||||||
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed/go.mod h1:E3i3cgB04dDx0v3CytCgRTTn9Z/9x891aet3r456RVw=
|
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed/go.mod h1:E3i3cgB04dDx0v3CytCgRTTn9Z/9x891aet3r456RVw=
|
||||||
gitea.com/go-chi/cache v0.2.1 h1:bfAPkvXlbcZxPCpcmDVCWoHgiBSBmZN/QosnZvEC0+g=
|
gitea.com/go-chi/cache v0.2.1 h1:bfAPkvXlbcZxPCpcmDVCWoHgiBSBmZN/QosnZvEC0+g=
|
||||||
@ -301,8 +301,8 @@ github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73/go.mod h1:jyveZeGw5La
|
|||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||||
github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||||
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
|
||||||
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||||
github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0=
|
github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0=
|
||||||
@ -420,8 +420,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/go-github/v61 v61.0.0 h1:VwQCBwhyE9JclCI+22/7mLB1PuU9eowCXKY5pNlu1go=
|
github.com/google/go-github/v71 v71.0.0 h1:Zi16OymGKZZMm8ZliffVVJ/Q9YZreDKONCr+WUd0Z30=
|
||||||
github.com/google/go-github/v61 v61.0.0/go.mod h1:0WR+KmsWX75G2EbpyGsGmradjo3IiciuI4BmdVCobQY=
|
github.com/google/go-github/v71 v71.0.0/go.mod h1:URZXObp2BLlMjwu0O8g4y6VBneUj2bCHgnI8FfgZ51M=
|
||||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||||
github.com/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc=
|
github.com/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc=
|
||||||
|
@ -171,6 +171,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().
|
||||||
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}),
|
||||||
|
@ -185,10 +185,10 @@ func AggregateJobStatus(jobs []*ActionRunJob) Status {
|
|||||||
return StatusSuccess
|
return StatusSuccess
|
||||||
case hasCancelled:
|
case hasCancelled:
|
||||||
return StatusCancelled
|
return StatusCancelled
|
||||||
case hasFailure:
|
|
||||||
return StatusFailure
|
|
||||||
case hasRunning:
|
case hasRunning:
|
||||||
return StatusRunning
|
return StatusRunning
|
||||||
|
case hasFailure:
|
||||||
|
return StatusFailure
|
||||||
case hasWaiting:
|
case hasWaiting:
|
||||||
return StatusWaiting
|
return StatusWaiting
|
||||||
case hasBlocked:
|
case hasBlocked:
|
||||||
|
@ -58,14 +58,14 @@ func TestAggregateJobStatus(t *testing.T) {
|
|||||||
{[]Status{StatusCancelled, StatusRunning}, StatusCancelled},
|
{[]Status{StatusCancelled, StatusRunning}, StatusCancelled},
|
||||||
{[]Status{StatusCancelled, StatusBlocked}, StatusCancelled},
|
{[]Status{StatusCancelled, StatusBlocked}, StatusCancelled},
|
||||||
|
|
||||||
// failure with other status, fail fast
|
// failure with other status, usually fail fast, but "running" wins to match GitHub's behavior
|
||||||
// Should "running" win? Maybe no: old code does make "running" win, but GitHub does fail fast.
|
// another reason that we can't make "failure" wins over "running": it would cause a weird behavior that user cannot cancel a workflow or get current running workflows correctly by filter after a job fail.
|
||||||
{[]Status{StatusFailure}, StatusFailure},
|
{[]Status{StatusFailure}, StatusFailure},
|
||||||
{[]Status{StatusFailure, StatusSuccess}, StatusFailure},
|
{[]Status{StatusFailure, StatusSuccess}, StatusFailure},
|
||||||
{[]Status{StatusFailure, StatusSkipped}, StatusFailure},
|
{[]Status{StatusFailure, StatusSkipped}, StatusFailure},
|
||||||
{[]Status{StatusFailure, StatusCancelled}, StatusCancelled},
|
{[]Status{StatusFailure, StatusCancelled}, StatusCancelled},
|
||||||
{[]Status{StatusFailure, StatusWaiting}, StatusFailure},
|
{[]Status{StatusFailure, StatusWaiting}, StatusFailure},
|
||||||
{[]Status{StatusFailure, StatusRunning}, StatusFailure},
|
{[]Status{StatusFailure, StatusRunning}, StatusRunning},
|
||||||
{[]Status{StatusFailure, StatusBlocked}, StatusFailure},
|
{[]Status{StatusFailure, StatusBlocked}, StatusFailure},
|
||||||
|
|
||||||
// skipped with other status
|
// skipped with other status
|
||||||
|
@ -5,6 +5,7 @@ package actions
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -298,6 +299,23 @@ func DeleteRunner(ctx context.Context, id int64) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteEphemeralRunner deletes a ephemeral runner by given ID.
|
||||||
|
func DeleteEphemeralRunner(ctx context.Context, id int64) error {
|
||||||
|
runner, err := GetRunnerByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, util.ErrNotExist) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !runner.Ephemeral {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.DeleteByID[ActionRunner](ctx, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// CreateRunner creates new runner.
|
// CreateRunner creates new runner.
|
||||||
func CreateRunner(ctx context.Context, t *ActionRunner) error {
|
func CreateRunner(ctx context.Context, t *ActionRunner) error {
|
||||||
if t.OwnerID != 0 && t.RepoID != 0 {
|
if t.OwnerID != 0 && t.RepoID != 0 {
|
||||||
|
@ -336,6 +336,11 @@ func UpdateTask(ctx context.Context, task *ActionTask, cols ...string) error {
|
|||||||
sess.Cols(cols...)
|
sess.Cols(cols...)
|
||||||
}
|
}
|
||||||
_, err := sess.Update(task)
|
_, err := sess.Update(task)
|
||||||
|
|
||||||
|
// Automatically delete the ephemeral runner if the task is done
|
||||||
|
if err == nil && task.Status.IsDone() && util.SliceContainsString(cols, "status") {
|
||||||
|
return DeleteEphemeralRunner(ctx, task.RunnerID)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,7 +191,7 @@ func (a *Action) LoadActUser(ctx context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
a.ActUser, err = user_model.GetUserByID(ctx, a.ActUserID)
|
a.ActUser, err = user_model.GetPossibleUserByID(ctx, a.ActUserID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return
|
return
|
||||||
} else if user_model.IsErrUserNotExist(err) {
|
} else if user_model.IsErrUserNotExist(err) {
|
||||||
@ -530,7 +530,7 @@ func ActivityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder.
|
|||||||
|
|
||||||
if opts.RequestedTeam != nil {
|
if opts.RequestedTeam != nil {
|
||||||
env := repo_model.AccessibleTeamReposEnv(organization.OrgFromUser(opts.RequestedUser), opts.RequestedTeam)
|
env := repo_model.AccessibleTeamReposEnv(organization.OrgFromUser(opts.RequestedUser), opts.RequestedTeam)
|
||||||
teamRepoIDs, err := env.RepoIDs(ctx, 1, opts.RequestedUser.NumRepos)
|
teamRepoIDs, err := env.RepoIDs(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("GetTeamRepositories: %w", err)
|
return nil, fmt.Errorf("GetTeamRepositories: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,7 @@ func getUserHeatmapData(ctx context.Context, user *user_model.User, team *organi
|
|||||||
Select(groupBy+" AS timestamp, count(user_id) as contributions").
|
Select(groupBy+" AS timestamp, count(user_id) as contributions").
|
||||||
Table("action").
|
Table("action").
|
||||||
Where(cond).
|
Where(cond).
|
||||||
And("created_unix > ?", timeutil.TimeStampNow()-31536000).
|
And("created_unix > ?", timeutil.TimeStampNow()-(366+7)*86400). // (366+7) days to include the first week for the heatmap
|
||||||
GroupBy(groupByName).
|
GroupBy(groupByName).
|
||||||
OrderBy("timestamp").
|
OrderBy("timestamp").
|
||||||
Find(&hdata)
|
Find(&hdata)
|
||||||
|
@ -91,7 +91,7 @@ func AddGPGKey(ctx context.Context, ownerID int64, content, token, signature str
|
|||||||
signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\r\n"), strings.NewReader(signature), nil)
|
signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\r\n"), strings.NewReader(signature), nil)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Unable to validate token signature. Error: %v", err)
|
log.Debug("AddGPGKey CheckArmoredDetachedSignature failed: %v", err)
|
||||||
return nil, ErrGPGInvalidTokenSignature{
|
return nil, ErrGPGInvalidTokenSignature{
|
||||||
ID: ekeys[0].PrimaryKey.KeyIdString(),
|
ID: ekeys[0].PrimaryKey.KeyIdString(),
|
||||||
Wrapped: err,
|
Wrapped: err,
|
||||||
|
@ -85,7 +85,7 @@ func VerifyGPGKey(ctx context.Context, ownerID int64, keyID, token, signature st
|
|||||||
}
|
}
|
||||||
|
|
||||||
if signer == nil {
|
if signer == nil {
|
||||||
log.Error("Unable to validate token signature. Error: %v", err)
|
log.Debug("VerifyGPGKey failed: no signer")
|
||||||
return "", ErrGPGInvalidTokenSignature{
|
return "", ErrGPGInvalidTokenSignature{
|
||||||
ID: key.KeyID,
|
ID: key.KeyID,
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ func VerifySSHKey(ctx context.Context, ownerID int64, fingerprint, token, signat
|
|||||||
// edge case for Windows based shells that will add CR LF if piped to ssh-keygen command
|
// edge case for Windows based shells that will add CR LF if piped to ssh-keygen command
|
||||||
// see https://github.com/PowerShell/PowerShell/issues/5974
|
// see https://github.com/PowerShell/PowerShell/issues/5974
|
||||||
if sshsig.Verify(strings.NewReader(token+"\r\n"), []byte(signature), []byte(key.Content), "gitea") != nil {
|
if sshsig.Verify(strings.NewReader(token+"\r\n"), []byte(signature), []byte(key.Content), "gitea") != nil {
|
||||||
log.Error("Unable to validate token signature. Error: %v", err)
|
log.Debug("VerifySSHKey sshsig.Verify failed: %v", err)
|
||||||
return "", ErrSSHInvalidTokenSignature{
|
return "", ErrSSHInvalidTokenSignature{
|
||||||
Fingerprint: key.Fingerprint,
|
Fingerprint: key.Fingerprint,
|
||||||
}
|
}
|
||||||
|
@ -38,3 +38,14 @@
|
|||||||
repo_id: 0
|
repo_id: 0
|
||||||
description: "This runner is going to be deleted"
|
description: "This runner is going to be deleted"
|
||||||
agent_labels: '["runner_to_be_deleted","linux"]'
|
agent_labels: '["runner_to_be_deleted","linux"]'
|
||||||
|
-
|
||||||
|
id: 34350
|
||||||
|
name: runner_to_be_deleted-org-ephemeral
|
||||||
|
uuid: 3FF231BD-FBB7-4E4B-9602-E6F28363EF20
|
||||||
|
token_hash: 3FF231BD-FBB7-4E4B-9602-E6F28363EF20
|
||||||
|
ephemeral: true
|
||||||
|
version: "1.0.0"
|
||||||
|
owner_id: 3
|
||||||
|
repo_id: 0
|
||||||
|
description: "This runner is going to be deleted"
|
||||||
|
agent_labels: '["runner_to_be_deleted","linux"]'
|
||||||
|
@ -117,3 +117,23 @@
|
|||||||
log_length: 707
|
log_length: 707
|
||||||
log_size: 90179
|
log_size: 90179
|
||||||
log_expired: 0
|
log_expired: 0
|
||||||
|
-
|
||||||
|
id: 52
|
||||||
|
job_id: 196
|
||||||
|
attempt: 1
|
||||||
|
runner_id: 34350
|
||||||
|
status: 6 # running
|
||||||
|
started: 1683636528
|
||||||
|
stopped: 1683636626
|
||||||
|
repo_id: 4
|
||||||
|
owner_id: 1
|
||||||
|
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
|
||||||
|
is_fork_pull_request: 0
|
||||||
|
token_hash: f8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784222
|
||||||
|
token_salt: ffffffffff
|
||||||
|
token_last_eight: ffffffff
|
||||||
|
log_filename: artifact-test2/2f/47.log
|
||||||
|
log_in_storage: 1
|
||||||
|
log_length: 707
|
||||||
|
log_size: 90179
|
||||||
|
log_expired: 0
|
||||||
|
@ -81,7 +81,7 @@
|
|||||||
-
|
-
|
||||||
id: 11
|
id: 11
|
||||||
uid: 4
|
uid: 4
|
||||||
email: user4@example.com
|
email: User4@Example.Com
|
||||||
lower_email: user4@example.com
|
lower_email: user4@example.com
|
||||||
is_activated: true
|
is_activated: true
|
||||||
is_primary: true
|
is_primary: true
|
||||||
|
@ -518,7 +518,7 @@ func updateTeamWhitelist(ctx context.Context, repo *repo_model.Repository, curre
|
|||||||
return currentWhitelist, nil
|
return currentWhitelist, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
teams, err := organization.GetTeamsWithAccessToRepo(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead)
|
teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("GetTeamsWithAccessToRepo [org_id: %d, repo_id: %d]: %v", repo.OwnerID, repo.ID, err)
|
return nil, fmt.Errorf("GetTeamsWithAccessToRepo [org_id: %d, repo_id: %d]: %v", repo.OwnerID, repo.ID, err)
|
||||||
}
|
}
|
||||||
|
@ -719,7 +719,8 @@ func (c *Comment) LoadReactions(ctx context.Context, repo *repo_model.Repository
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Comment) loadReview(ctx context.Context) (err error) {
|
// LoadReview loads the associated review
|
||||||
|
func (c *Comment) LoadReview(ctx context.Context) (err error) {
|
||||||
if c.ReviewID == 0 {
|
if c.ReviewID == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -736,11 +737,6 @@ func (c *Comment) loadReview(ctx context.Context) (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadReview loads the associated review
|
|
||||||
func (c *Comment) LoadReview(ctx context.Context) error {
|
|
||||||
return c.loadReview(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DiffSide returns "previous" if Comment.Line is a LOC of the previous changes and "proposed" if it is a LOC of the proposed changes.
|
// DiffSide returns "previous" if Comment.Line is a LOC of the previous changes and "proposed" if it is a LOC of the proposed changes.
|
||||||
func (c *Comment) DiffSide() string {
|
func (c *Comment) DiffSide() string {
|
||||||
if c.Line < 0 {
|
if c.Line < 0 {
|
||||||
@ -860,7 +856,7 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment
|
|||||||
}
|
}
|
||||||
if comment.ReviewID != 0 {
|
if comment.ReviewID != 0 {
|
||||||
if comment.Review == nil {
|
if comment.Review == nil {
|
||||||
if err := comment.loadReview(ctx); err != nil {
|
if err := comment.LoadReview(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ package issues
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/models/renderhelper"
|
"code.gitea.io/gitea/models/renderhelper"
|
||||||
@ -114,7 +115,9 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
|
|||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo)
|
rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo, renderhelper.RepoCommentOptions{
|
||||||
|
FootnoteContextID: strconv.FormatInt(comment.ID, 10),
|
||||||
|
})
|
||||||
if comment.RenderedContent, err = markdown.RenderString(rctx, comment.Content); err != nil {
|
if comment.RenderedContent, err = markdown.RenderString(rctx, comment.Content); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -206,6 +206,7 @@ func DeleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *use
|
|||||||
}
|
}
|
||||||
|
|
||||||
issue.Labels = nil
|
issue.Labels = nil
|
||||||
|
issue.isLabelsLoaded = false
|
||||||
return issue.LoadLabels(ctx)
|
return issue.LoadLabels(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,6 +88,8 @@ func applySorts(sess *xorm.Session, sortType string, priorityRepoID int64) {
|
|||||||
sess.Asc("issue.created_unix").Asc("issue.id")
|
sess.Asc("issue.created_unix").Asc("issue.id")
|
||||||
case "recentupdate":
|
case "recentupdate":
|
||||||
sess.Desc("issue.updated_unix").Desc("issue.created_unix").Desc("issue.id")
|
sess.Desc("issue.updated_unix").Desc("issue.created_unix").Desc("issue.id")
|
||||||
|
case "recentclose":
|
||||||
|
sess.Desc("issue.closed_unix").Desc("issue.created_unix").Desc("issue.id")
|
||||||
case "leastupdate":
|
case "leastupdate":
|
||||||
sess.Asc("issue.updated_unix").Asc("issue.created_unix").Asc("issue.id")
|
sess.Asc("issue.updated_unix").Asc("issue.created_unix").Asc("issue.id")
|
||||||
case "mostcomment":
|
case "mostcomment":
|
||||||
|
@ -12,9 +12,7 @@ import (
|
|||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/models/organization"
|
"code.gitea.io/gitea/models/organization"
|
||||||
access_model "code.gitea.io/gitea/models/perm/access"
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
project_model "code.gitea.io/gitea/models/project"
|
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
system_model "code.gitea.io/gitea/models/system"
|
|
||||||
"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/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
@ -715,138 +713,13 @@ func UpdateReactionsMigrationsByType(ctx context.Context, gitServiceType api.Git
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteIssuesByRepoID deletes issues by repositories id
|
func GetOrphanedIssueRepoIDs(ctx context.Context) ([]int64, error) {
|
||||||
func DeleteIssuesByRepoID(ctx context.Context, repoID int64) (attachmentPaths []string, err error) {
|
var repoIDs []int64
|
||||||
// MariaDB has a performance bug: https://jira.mariadb.org/browse/MDEV-16289
|
if err := db.GetEngine(ctx).Table("issue").Distinct("issue.repo_id").
|
||||||
// so here it uses "DELETE ... WHERE IN" with pre-queried IDs.
|
Join("LEFT", "repository", "issue.repo_id=repository.id").
|
||||||
sess := db.GetEngine(ctx)
|
Where(builder.IsNull{"repository.id"}).
|
||||||
|
Find(&repoIDs); err != nil {
|
||||||
for {
|
return nil, err
|
||||||
issueIDs := make([]int64, 0, db.DefaultMaxInSize)
|
|
||||||
|
|
||||||
err := sess.Table(&Issue{}).Where("repo_id = ?", repoID).OrderBy("id").Limit(db.DefaultMaxInSize).Cols("id").Find(&issueIDs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(issueIDs) == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete content histories
|
|
||||||
_, err = sess.In("issue_id", issueIDs).Delete(&ContentHistory{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete comments and attachments
|
|
||||||
_, err = sess.In("issue_id", issueIDs).Delete(&Comment{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dependencies for issues in this repository
|
|
||||||
_, err = sess.In("issue_id", issueIDs).Delete(&IssueDependency{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete dependencies for issues in other repositories
|
|
||||||
_, err = sess.In("dependency_id", issueIDs).Delete(&IssueDependency{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = sess.In("issue_id", issueIDs).Delete(&IssueUser{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = sess.In("issue_id", issueIDs).Delete(&Reaction{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = sess.In("issue_id", issueIDs).Delete(&IssueWatch{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = sess.In("issue_id", issueIDs).Delete(&Stopwatch{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = sess.In("issue_id", issueIDs).Delete(&TrackedTime{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = sess.In("issue_id", issueIDs).Delete(&project_model.ProjectIssue{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = sess.In("dependent_issue_id", issueIDs).Delete(&Comment{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var attachments []*repo_model.Attachment
|
|
||||||
err = sess.In("issue_id", issueIDs).Find(&attachments)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for j := range attachments {
|
|
||||||
attachmentPaths = append(attachmentPaths, attachments[j].RelativePath())
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = sess.In("issue_id", issueIDs).Delete(&repo_model.Attachment{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = sess.In("id", issueIDs).Delete(&Issue{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return repoIDs, nil
|
||||||
return attachmentPaths, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteOrphanedIssues delete issues without a repo
|
|
||||||
func DeleteOrphanedIssues(ctx context.Context) error {
|
|
||||||
var attachmentPaths []string
|
|
||||||
err := db.WithTx(ctx, func(ctx context.Context) error {
|
|
||||||
var ids []int64
|
|
||||||
|
|
||||||
if err := db.GetEngine(ctx).Table("issue").Distinct("issue.repo_id").
|
|
||||||
Join("LEFT", "repository", "issue.repo_id=repository.id").
|
|
||||||
Where(builder.IsNull{"repository.id"}).GroupBy("issue.repo_id").
|
|
||||||
Find(&ids); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range ids {
|
|
||||||
paths, err := DeleteIssuesByRepoID(ctx, ids[i])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
attachmentPaths = append(attachmentPaths, paths...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove issue attachment files.
|
|
||||||
for i := range attachmentPaths {
|
|
||||||
// FIXME: it's not right, because the attachment might not be on local filesystem
|
|
||||||
system_model.RemoveAllWithNotice(ctx, "Delete issue attachment", attachmentPaths[i])
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
@ -152,7 +152,8 @@ func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptio
|
|||||||
applySorts(findSession, opts.SortType, 0)
|
applySorts(findSession, opts.SortType, 0)
|
||||||
findSession = db.SetSessionPagination(findSession, opts)
|
findSession = db.SetSessionPagination(findSession, opts)
|
||||||
prs := make([]*PullRequest, 0, opts.PageSize)
|
prs := make([]*PullRequest, 0, opts.PageSize)
|
||||||
return prs, maxResults, findSession.Find(&prs)
|
found := findSession.Find(&prs)
|
||||||
|
return prs, maxResults, found
|
||||||
}
|
}
|
||||||
|
|
||||||
// PullRequestList defines a list of pull requests
|
// PullRequestList defines a list of pull requests
|
||||||
|
@ -14,6 +14,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 TestPullRequest_LoadAttributes(t *testing.T) {
|
func TestPullRequest_LoadAttributes(t *testing.T) {
|
||||||
@ -76,6 +77,47 @@ func TestPullRequestsNewest(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPullRequests_Closed_RecentSortType(t *testing.T) {
|
||||||
|
// Issue ID | Closed At. | Updated At
|
||||||
|
// 2 | 1707270001 | 1707270001
|
||||||
|
// 3 | 1707271000 | 1707279999
|
||||||
|
// 11 | 1707279999 | 1707275555
|
||||||
|
tests := []struct {
|
||||||
|
sortType string
|
||||||
|
expectedIssueIDOrder []int64
|
||||||
|
}{
|
||||||
|
{"recentupdate", []int64{3, 11, 2}},
|
||||||
|
{"recentclose", []int64{11, 3, 2}},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
_, err := db.Exec(db.DefaultContext, "UPDATE issue SET closed_unix = 1707270001, updated_unix = 1707270001, is_closed = true WHERE id = 2")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = db.Exec(db.DefaultContext, "UPDATE issue SET closed_unix = 1707271000, updated_unix = 1707279999, is_closed = true WHERE id = 3")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = db.Exec(db.DefaultContext, "UPDATE issue SET closed_unix = 1707279999, updated_unix = 1707275555, is_closed = true WHERE id = 11")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.sortType, func(t *testing.T) {
|
||||||
|
prs, _, err := issues_model.PullRequests(db.DefaultContext, 1, &issues_model.PullRequestsOptions{
|
||||||
|
ListOptions: db.ListOptions{
|
||||||
|
Page: 1,
|
||||||
|
},
|
||||||
|
State: "closed",
|
||||||
|
SortType: test.sortType,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
if assert.Len(t, prs, len(test.expectedIssueIDOrder)) {
|
||||||
|
for i := range test.expectedIssueIDOrder {
|
||||||
|
assert.Equal(t, test.expectedIssueIDOrder[i], prs[i].IssueID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLoadRequestedReviewers(t *testing.T) {
|
func TestLoadRequestedReviewers(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
@ -14,5 +14,8 @@ func AddIndexToActionTaskStoppedLogExpired(x *xorm.Engine) error {
|
|||||||
Stopped timeutil.TimeStamp `xorm:"index(stopped_log_expired)"`
|
Stopped timeutil.TimeStamp `xorm:"index(stopped_log_expired)"`
|
||||||
LogExpired bool `xorm:"index(stopped_log_expired)"`
|
LogExpired bool `xorm:"index(stopped_log_expired)"`
|
||||||
}
|
}
|
||||||
return x.Sync(new(ActionTask))
|
_, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||||
|
IgnoreDropIndices: true,
|
||||||
|
}, new(ActionTask))
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
51
models/migrations/v1_23/v302_test.go
Normal file
51
models/migrations/v1_23/v302_test.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package v1_23 //nolint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/migrations/base"
|
||||||
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_AddIndexToActionTaskStoppedLogExpired(t *testing.T) {
|
||||||
|
type ActionTask struct {
|
||||||
|
ID int64
|
||||||
|
JobID int64
|
||||||
|
Attempt int64
|
||||||
|
RunnerID int64 `xorm:"index"`
|
||||||
|
Status int `xorm:"index"`
|
||||||
|
Started timeutil.TimeStamp `xorm:"index"`
|
||||||
|
Stopped timeutil.TimeStamp `xorm:"index(stopped_log_expired)"`
|
||||||
|
|
||||||
|
RepoID int64 `xorm:"index"`
|
||||||
|
OwnerID int64 `xorm:"index"`
|
||||||
|
CommitSHA string `xorm:"index"`
|
||||||
|
IsForkPullRequest bool
|
||||||
|
|
||||||
|
Token string `xorm:"-"`
|
||||||
|
TokenHash string `xorm:"UNIQUE"` // sha256 of token
|
||||||
|
TokenSalt string
|
||||||
|
TokenLastEight string `xorm:"index token_last_eight"`
|
||||||
|
|
||||||
|
LogFilename string // file name of log
|
||||||
|
LogInStorage bool // read log from database or from storage
|
||||||
|
LogLength int64 // lines count
|
||||||
|
LogSize int64 // blob size
|
||||||
|
LogIndexes []int64 `xorm:"LONGBLOB"` // line number to offset
|
||||||
|
LogExpired bool `xorm:"index(stopped_log_expired)"` // files that are too old will be deleted
|
||||||
|
|
||||||
|
Created timeutil.TimeStamp `xorm:"created"`
|
||||||
|
Updated timeutil.TimeStamp `xorm:"updated index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare and load the testing database
|
||||||
|
x, deferable := base.PrepareTestEnv(t, 0, new(ActionTask))
|
||||||
|
defer deferable()
|
||||||
|
|
||||||
|
assert.NoError(t, AddIndexToActionTaskStoppedLogExpired(x))
|
||||||
|
}
|
@ -9,5 +9,8 @@ func AddIndexForReleaseSha1(x *xorm.Engine) error {
|
|||||||
type Release struct {
|
type Release struct {
|
||||||
Sha1 string `xorm:"INDEX VARCHAR(64)"`
|
Sha1 string `xorm:"INDEX VARCHAR(64)"`
|
||||||
}
|
}
|
||||||
return x.Sync(new(Release))
|
_, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||||
|
IgnoreDropIndices: true,
|
||||||
|
}, new(Release))
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
40
models/migrations/v1_23/v304_test.go
Normal file
40
models/migrations/v1_23/v304_test.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package v1_23 //nolint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/migrations/base"
|
||||||
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_AddIndexForReleaseSha1(t *testing.T) {
|
||||||
|
type Release struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
RepoID int64 `xorm:"INDEX UNIQUE(n)"`
|
||||||
|
PublisherID int64 `xorm:"INDEX"`
|
||||||
|
TagName string `xorm:"INDEX UNIQUE(n)"`
|
||||||
|
OriginalAuthor string
|
||||||
|
OriginalAuthorID int64 `xorm:"index"`
|
||||||
|
LowerTagName string
|
||||||
|
Target string
|
||||||
|
Title string
|
||||||
|
Sha1 string `xorm:"VARCHAR(64)"`
|
||||||
|
NumCommits int64
|
||||||
|
Note string `xorm:"TEXT"`
|
||||||
|
IsDraft bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
|
IsPrerelease bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
|
IsTag bool `xorm:"NOT NULL DEFAULT false"` // will be true only if the record is a tag and has no related releases
|
||||||
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare and load the testing database
|
||||||
|
x, deferable := base.PrepareTestEnv(t, 0, new(Release))
|
||||||
|
defer deferable()
|
||||||
|
|
||||||
|
assert.NoError(t, AddIndexForReleaseSha1(x))
|
||||||
|
}
|
@ -602,8 +602,3 @@ func getUserTeamIDsQueryBuilder(orgID, userID int64) *builder.Builder {
|
|||||||
"team_user.uid": userID,
|
"team_user.uid": userID,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TeamsWithAccessToRepo returns all teams that have given access level to the repository.
|
|
||||||
func (org *Organization) TeamsWithAccessToRepo(ctx context.Context, repoID int64, mode perm.AccessMode) ([]*Team, error) {
|
|
||||||
return GetTeamsWithAccessToRepo(ctx, org.ID, repoID, mode)
|
|
||||||
}
|
|
||||||
|
@ -334,7 +334,7 @@ func TestAccessibleReposEnv_RepoIDs(t *testing.T) {
|
|||||||
testSuccess := func(userID int64, expectedRepoIDs []int64) {
|
testSuccess := func(userID int64, expectedRepoIDs []int64) {
|
||||||
env, err := repo_model.AccessibleReposEnv(db.DefaultContext, org, userID)
|
env, err := repo_model.AccessibleReposEnv(db.DefaultContext, org, userID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
repoIDs, err := env.RepoIDs(db.DefaultContext, 1, 100)
|
repoIDs, err := env.RepoIDs(db.DefaultContext)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, expectedRepoIDs, repoIDs)
|
assert.Equal(t, expectedRepoIDs, repoIDs)
|
||||||
}
|
}
|
||||||
@ -342,25 +342,6 @@ func TestAccessibleReposEnv_RepoIDs(t *testing.T) {
|
|||||||
testSuccess(4, []int64{3, 32})
|
testSuccess(4, []int64{3, 32})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAccessibleReposEnv_Repos(t *testing.T) {
|
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
|
||||||
testSuccess := func(userID int64, expectedRepoIDs []int64) {
|
|
||||||
env, err := repo_model.AccessibleReposEnv(db.DefaultContext, org, userID)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
repos, err := env.Repos(db.DefaultContext, 1, 100)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
expectedRepos := make(repo_model.RepositoryList, len(expectedRepoIDs))
|
|
||||||
for i, repoID := range expectedRepoIDs {
|
|
||||||
expectedRepos[i] = unittest.AssertExistsAndLoadBean(t,
|
|
||||||
&repo_model.Repository{ID: repoID})
|
|
||||||
}
|
|
||||||
assert.Equal(t, expectedRepos, repos)
|
|
||||||
}
|
|
||||||
testSuccess(2, []int64{3, 5, 32})
|
|
||||||
testSuccess(4, []int64{3, 32})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAccessibleReposEnv_MirrorRepos(t *testing.T) {
|
func TestAccessibleReposEnv_MirrorRepos(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
||||||
|
@ -9,6 +9,8 @@ import (
|
|||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/models/perm"
|
"code.gitea.io/gitea/models/perm"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
|
|
||||||
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TeamRepo represents an team-repository relation.
|
// TeamRepo represents an team-repository relation.
|
||||||
@ -48,26 +50,27 @@ func RemoveTeamRepo(ctx context.Context, teamID, repoID int64) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTeamsWithAccessToRepo returns all teams in an organization that have given access level to the repository.
|
// GetTeamsWithAccessToAnyRepoUnit returns all teams in an organization that have given access level to the repository special unit.
|
||||||
func GetTeamsWithAccessToRepo(ctx context.Context, orgID, repoID int64, mode perm.AccessMode) ([]*Team, error) {
|
// This function is only used for finding some teams that can be used as branch protection allowlist or reviewers, it isn't really used for access control.
|
||||||
|
// FIXME: TEAM-UNIT-PERMISSION this logic is not complete, search the fixme keyword to see more details
|
||||||
|
func GetTeamsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) ([]*Team, error) {
|
||||||
teams := make([]*Team, 0, 5)
|
teams := make([]*Team, 0, 5)
|
||||||
return teams, db.GetEngine(ctx).Where("team.authorize >= ?", mode).
|
|
||||||
Join("INNER", "team_repo", "team_repo.team_id = team.id").
|
|
||||||
And("team_repo.org_id = ?", orgID).
|
|
||||||
And("team_repo.repo_id = ?", repoID).
|
|
||||||
OrderBy("name").
|
|
||||||
Find(&teams)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTeamsWithAccessToRepoUnit returns all teams in an organization that have given access level to the repository special unit.
|
sub := builder.Select("team_id").From("team_unit").
|
||||||
func GetTeamsWithAccessToRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type) ([]*Team, error) {
|
Where(builder.Expr("team_unit.team_id = team.id")).
|
||||||
teams := make([]*Team, 0, 5)
|
And(builder.In("team_unit.type", append([]unit.Type{unitType}, unitTypesMore...))).
|
||||||
return teams, db.GetEngine(ctx).Where("team_unit.access_mode >= ?", mode).
|
And(builder.Expr("team_unit.access_mode >= ?", mode))
|
||||||
|
|
||||||
|
err := db.GetEngine(ctx).
|
||||||
Join("INNER", "team_repo", "team_repo.team_id = team.id").
|
Join("INNER", "team_repo", "team_repo.team_id = team.id").
|
||||||
Join("INNER", "team_unit", "team_unit.team_id = team.id").
|
|
||||||
And("team_repo.org_id = ?", orgID).
|
And("team_repo.org_id = ?", orgID).
|
||||||
And("team_repo.repo_id = ?", repoID).
|
And("team_repo.repo_id = ?", repoID).
|
||||||
And("team_unit.type = ?", unitType).
|
And(builder.Or(
|
||||||
|
builder.Expr("team.authorize >= ?", mode),
|
||||||
|
builder.In("team.id", sub),
|
||||||
|
)).
|
||||||
OrderBy("name").
|
OrderBy("name").
|
||||||
Find(&teams)
|
Find(&teams)
|
||||||
|
|
||||||
|
return teams, err
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ func TestGetTeamsWithAccessToRepoUnit(t *testing.T) {
|
|||||||
org41 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 41})
|
org41 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 41})
|
||||||
repo61 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 61})
|
repo61 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 61})
|
||||||
|
|
||||||
teams, err := organization.GetTeamsWithAccessToRepoUnit(db.DefaultContext, org41.ID, repo61.ID, perm.AccessModeRead, unit.TypePullRequests)
|
teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(db.DefaultContext, org41.ID, repo61.ID, perm.AccessModeRead, unit.TypePullRequests)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
if assert.Len(t, teams, 2) {
|
if assert.Len(t, teams, 2) {
|
||||||
assert.EqualValues(t, 21, teams[0].ID)
|
assert.EqualValues(t, 21, teams[0].ID)
|
||||||
|
@ -33,7 +33,7 @@ func SearchVersions(ctx context.Context, opts *packages_model.PackageSearchOptio
|
|||||||
Where(cond).
|
Where(cond).
|
||||||
OrderBy("package.name ASC")
|
OrderBy("package.name ASC")
|
||||||
if opts.Paginator != nil {
|
if opts.Paginator != nil {
|
||||||
skip, take := opts.GetSkipTake()
|
skip, take := opts.Paginator.GetSkipTake()
|
||||||
inner = inner.Limit(take, skip)
|
inner = inner.Limit(take, skip)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
|
"xorm.io/xorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrDuplicatePackageVersion indicates a duplicated package version error
|
// ErrDuplicatePackageVersion indicates a duplicated package version error
|
||||||
@ -187,7 +188,7 @@ type PackageSearchOptions struct {
|
|||||||
HasFileWithName string // only results are found which are associated with a file with the specific name
|
HasFileWithName string // only results are found which are associated with a file with the specific name
|
||||||
HasFiles optional.Option[bool] // only results are found which have associated files
|
HasFiles optional.Option[bool] // only results are found which have associated files
|
||||||
Sort VersionSort
|
Sort VersionSort
|
||||||
db.Paginator
|
Paginator db.Paginator
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts *PackageSearchOptions) ToConds() builder.Cond {
|
func (opts *PackageSearchOptions) ToConds() builder.Cond {
|
||||||
@ -282,6 +283,18 @@ func (opts *PackageSearchOptions) configureOrderBy(e db.Engine) {
|
|||||||
e.Desc("package_version.id") // Sort by id for stable order with duplicates in the other field
|
e.Desc("package_version.id") // Sort by id for stable order with duplicates in the other field
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func searchVersionsBySession(sess *xorm.Session, opts *PackageSearchOptions) ([]*PackageVersion, int64, error) {
|
||||||
|
opts.configureOrderBy(sess)
|
||||||
|
pvs := make([]*PackageVersion, 0, 10)
|
||||||
|
if opts.Paginator != nil {
|
||||||
|
sess = db.SetSessionPagination(sess, opts.Paginator)
|
||||||
|
count, err := sess.FindAndCount(&pvs)
|
||||||
|
return pvs, count, err
|
||||||
|
}
|
||||||
|
err := sess.Find(&pvs)
|
||||||
|
return pvs, int64(len(pvs)), err
|
||||||
|
}
|
||||||
|
|
||||||
// SearchVersions gets all versions of packages matching the search options
|
// SearchVersions gets all versions of packages matching the search options
|
||||||
func SearchVersions(ctx context.Context, opts *PackageSearchOptions) ([]*PackageVersion, int64, error) {
|
func SearchVersions(ctx context.Context, opts *PackageSearchOptions) ([]*PackageVersion, int64, error) {
|
||||||
sess := db.GetEngine(ctx).
|
sess := db.GetEngine(ctx).
|
||||||
@ -289,16 +302,7 @@ func SearchVersions(ctx context.Context, opts *PackageSearchOptions) ([]*Package
|
|||||||
Table("package_version").
|
Table("package_version").
|
||||||
Join("INNER", "package", "package.id = package_version.package_id").
|
Join("INNER", "package", "package.id = package_version.package_id").
|
||||||
Where(opts.ToConds())
|
Where(opts.ToConds())
|
||||||
|
return searchVersionsBySession(sess, opts)
|
||||||
opts.configureOrderBy(sess)
|
|
||||||
|
|
||||||
if opts.Paginator != nil {
|
|
||||||
sess = db.SetSessionPagination(sess, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
pvs := make([]*PackageVersion, 0, 10)
|
|
||||||
count, err := sess.FindAndCount(&pvs)
|
|
||||||
return pvs, count, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchLatestVersions gets the latest version of every package matching the search options
|
// SearchLatestVersions gets the latest version of every package matching the search options
|
||||||
@ -316,15 +320,7 @@ func SearchLatestVersions(ctx context.Context, opts *PackageSearchOptions) ([]*P
|
|||||||
Join("INNER", "package", "package.id = package_version.package_id").
|
Join("INNER", "package", "package.id = package_version.package_id").
|
||||||
Where(builder.In("package_version.id", in))
|
Where(builder.In("package_version.id", in))
|
||||||
|
|
||||||
opts.configureOrderBy(sess)
|
return searchVersionsBySession(sess, opts)
|
||||||
|
|
||||||
if opts.Paginator != nil {
|
|
||||||
sess = db.SetSessionPagination(sess, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
pvs := make([]*PackageVersion, 0, 10)
|
|
||||||
count, err := sess.FindAndCount(&pvs)
|
|
||||||
return pvs, count, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExistVersion checks if a version matching the search options exist
|
// ExistVersion checks if a version matching the search options exist
|
||||||
|
@ -42,6 +42,7 @@ func (p *Permission) IsAdmin() bool {
|
|||||||
|
|
||||||
// HasAnyUnitAccess returns true if the user might have at least one access mode to any unit of this repository.
|
// HasAnyUnitAccess returns true if the user might have at least one access mode to any unit of this repository.
|
||||||
// It doesn't count the "public(anonymous/everyone) access mode".
|
// It doesn't count the "public(anonymous/everyone) access mode".
|
||||||
|
// TODO: most calls to this function should be replaced with `HasAnyUnitAccessOrPublicAccess`
|
||||||
func (p *Permission) HasAnyUnitAccess() bool {
|
func (p *Permission) HasAnyUnitAccess() bool {
|
||||||
for _, v := range p.unitsMode {
|
for _, v := range p.unitsMode {
|
||||||
if v >= perm_model.AccessModeRead {
|
if v >= perm_model.AccessModeRead {
|
||||||
@ -267,7 +268,6 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
|
|||||||
perm.units = repo.Units
|
perm.units = repo.Units
|
||||||
|
|
||||||
// anonymous user visit private repo.
|
// anonymous user visit private repo.
|
||||||
// TODO: anonymous user visit public unit of private repo???
|
|
||||||
if user == nil && repo.IsPrivate {
|
if user == nil && repo.IsPrivate {
|
||||||
perm.AccessMode = perm_model.AccessModeNone
|
perm.AccessMode = perm_model.AccessModeNone
|
||||||
return perm, nil
|
return perm, nil
|
||||||
@ -286,7 +286,8 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Prevent strangers from checking out public repo of private organization/users
|
// Prevent strangers from checking out public repo of private organization/users
|
||||||
// Allow user if they are collaborator of a repo within a private user or a private organization but not a member of the organization itself
|
// Allow user if they are a collaborator of a repo within a private user or a private organization but not a member of the organization itself
|
||||||
|
// TODO: rename it to "IsOwnerVisibleToDoer"
|
||||||
if !organization.HasOrgOrUserVisible(ctx, repo.Owner, user) && !isCollaborator {
|
if !organization.HasOrgOrUserVisible(ctx, repo.Owner, user) && !isCollaborator {
|
||||||
perm.AccessMode = perm_model.AccessModeNone
|
perm.AccessMode = perm_model.AccessModeNone
|
||||||
return perm, nil
|
return perm, nil
|
||||||
@ -304,7 +305,7 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
|
|||||||
return perm, nil
|
return perm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// plain user
|
// plain user TODO: this check should be replaced, only need to check collaborator access mode
|
||||||
perm.AccessMode, err = accessLevel(ctx, user, repo)
|
perm.AccessMode, err = accessLevel(ctx, user, repo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return perm, err
|
return perm, err
|
||||||
@ -314,6 +315,19 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
|
|||||||
return perm, nil
|
return perm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// now: the owner is visible to doer, if the repo is public, then the min access mode is read
|
||||||
|
minAccessMode := util.Iif(!repo.IsPrivate && !user.IsRestricted, perm_model.AccessModeRead, perm_model.AccessModeNone)
|
||||||
|
perm.AccessMode = max(perm.AccessMode, minAccessMode)
|
||||||
|
|
||||||
|
// get units mode from teams
|
||||||
|
teams, err := organization.GetUserRepoTeams(ctx, repo.OwnerID, user.ID, repo.ID)
|
||||||
|
if err != nil {
|
||||||
|
return perm, err
|
||||||
|
}
|
||||||
|
if len(teams) == 0 {
|
||||||
|
return perm, nil
|
||||||
|
}
|
||||||
|
|
||||||
perm.unitsMode = make(map[unit.Type]perm_model.AccessMode)
|
perm.unitsMode = make(map[unit.Type]perm_model.AccessMode)
|
||||||
|
|
||||||
// Collaborators on organization
|
// Collaborators on organization
|
||||||
@ -323,12 +337,6 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// get units mode from teams
|
|
||||||
teams, err := organization.GetUserRepoTeams(ctx, repo.OwnerID, user.ID, repo.ID)
|
|
||||||
if err != nil {
|
|
||||||
return perm, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// if user in an owner team
|
// if user in an owner team
|
||||||
for _, team := range teams {
|
for _, team := range teams {
|
||||||
if team.HasAdminAccess() {
|
if team.HasAdminAccess() {
|
||||||
@ -339,19 +347,12 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, u := range repo.Units {
|
for _, u := range repo.Units {
|
||||||
var found bool
|
|
||||||
for _, team := range teams {
|
for _, team := range teams {
|
||||||
|
unitAccessMode := minAccessMode
|
||||||
if teamMode, exist := team.UnitAccessModeEx(ctx, u.Type); exist {
|
if teamMode, exist := team.UnitAccessModeEx(ctx, u.Type); exist {
|
||||||
perm.unitsMode[u.Type] = max(perm.unitsMode[u.Type], teamMode)
|
unitAccessMode = max(perm.unitsMode[u.Type], unitAccessMode, teamMode)
|
||||||
found = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// for a public repo on an organization, a non-restricted user has read permission on non-team defined units.
|
|
||||||
if !found && !repo.IsPrivate && !user.IsRestricted {
|
|
||||||
if _, ok := perm.unitsMode[u.Type]; !ok {
|
|
||||||
perm.unitsMode[u.Type] = perm_model.AccessModeRead
|
|
||||||
}
|
}
|
||||||
|
perm.unitsMode[u.Type] = unitAccessMode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,12 +6,16 @@ package access
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
"code.gitea.io/gitea/models/organization"
|
||||||
perm_model "code.gitea.io/gitea/models/perm"
|
perm_model "code.gitea.io/gitea/models/perm"
|
||||||
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"
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHasAnyUnitAccess(t *testing.T) {
|
func TestHasAnyUnitAccess(t *testing.T) {
|
||||||
@ -152,3 +156,45 @@ func TestUnitAccessMode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
assert.Equal(t, perm_model.AccessModeRead, perm.UnitAccessMode(unit.TypeWiki), "has unit, and map, use map")
|
assert.Equal(t, perm_model.AccessModeRead, perm.UnitAccessMode(unit.TypeWiki), "has unit, and map, use map")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetUserRepoPermission(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
ctx := t.Context()
|
||||||
|
repo32 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 32}) // org public repo
|
||||||
|
require.NoError(t, repo32.LoadOwner(ctx))
|
||||||
|
require.True(t, repo32.Owner.IsOrganization())
|
||||||
|
|
||||||
|
require.NoError(t, db.TruncateBeans(ctx, &organization.Team{}, &organization.TeamUser{}, &organization.TeamRepo{}, &organization.TeamUnit{}))
|
||||||
|
org := repo32.Owner
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||||
|
team := &organization.Team{OrgID: org.ID, LowerName: "test_team"}
|
||||||
|
require.NoError(t, db.Insert(ctx, team))
|
||||||
|
|
||||||
|
t.Run("DoerInTeamWithNoRepo", func(t *testing.T) {
|
||||||
|
require.NoError(t, db.Insert(ctx, &organization.TeamUser{OrgID: org.ID, TeamID: team.ID, UID: user.ID}))
|
||||||
|
perm, err := GetUserRepoPermission(ctx, repo32, user)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode)
|
||||||
|
assert.Nil(t, perm.unitsMode) // doer in the team, but has no access to the repo
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NoError(t, db.Insert(ctx, &organization.TeamRepo{OrgID: org.ID, TeamID: team.ID, RepoID: repo32.ID}))
|
||||||
|
require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeCode, AccessMode: perm_model.AccessModeNone}))
|
||||||
|
t.Run("DoerWithTeamUnitAccessNone", func(t *testing.T) {
|
||||||
|
perm, err := GetUserRepoPermission(ctx, repo32, user)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode)
|
||||||
|
assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeCode])
|
||||||
|
assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeIssues])
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NoError(t, db.TruncateBeans(ctx, &organization.TeamUnit{}))
|
||||||
|
require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeCode, AccessMode: perm_model.AccessModeWrite}))
|
||||||
|
t.Run("DoerWithTeamUnitAccessWrite", func(t *testing.T) {
|
||||||
|
perm, err := GetUserRepoPermission(ctx, repo32, user)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode)
|
||||||
|
assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeCode])
|
||||||
|
assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeIssues])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -5,12 +5,14 @@ package pull
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"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"
|
||||||
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AutoMerge represents a pull request scheduled for merging when checks succeed
|
// AutoMerge represents a pull request scheduled for merging when checks succeed
|
||||||
@ -76,7 +78,10 @@ func GetScheduledMergeByPullID(ctx context.Context, pullID int64) (bool, *AutoMe
|
|||||||
return false, nil, err
|
return false, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
doer, err := user_model.GetUserByID(ctx, scheduledPRM.DoerID)
|
doer, err := user_model.GetPossibleUserByID(ctx, scheduledPRM.DoerID)
|
||||||
|
if errors.Is(err, util.ErrNotExist) {
|
||||||
|
doer, err = user_model.NewGhostUser(), nil
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, nil, err
|
return false, nil, err
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@ func (c *commitChecker) IsCommitIDExisting(commitID string) bool {
|
|||||||
c.gitRepo, c.gitRepoCloser = r, closer
|
c.gitRepo, c.gitRepoCloser = r, closer
|
||||||
}
|
}
|
||||||
|
|
||||||
exist = c.gitRepo.IsReferenceExist(commitID) // Don't use IsObjectExist since it doesn't support short hashs with gogit edition.
|
exist = c.gitRepo.IsReferenceExist(commitID) // Don't use IsObjectExist since it doesn't support short hashes with gogit edition.
|
||||||
c.commitCache[commitID] = exist
|
c.commitCache[commitID] = exist
|
||||||
return exist
|
return exist
|
||||||
}
|
}
|
||||||
|
@ -44,30 +44,31 @@ type RepoCommentOptions struct {
|
|||||||
DeprecatedRepoName string // it is only a patch for the non-standard "markup" api
|
DeprecatedRepoName string // it is only a patch for the non-standard "markup" api
|
||||||
DeprecatedOwnerName string // it is only a patch for the non-standard "markup" api
|
DeprecatedOwnerName string // it is only a patch for the non-standard "markup" api
|
||||||
CurrentRefPath string // eg: "branch/main" or "commit/11223344"
|
CurrentRefPath string // eg: "branch/main" or "commit/11223344"
|
||||||
|
FootnoteContextID string // the extra context ID for footnotes, used to avoid conflicts with other footnotes in the same page
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRenderContextRepoComment(ctx context.Context, repo *repo_model.Repository, opts ...RepoCommentOptions) *markup.RenderContext {
|
func NewRenderContextRepoComment(ctx context.Context, repo *repo_model.Repository, opts ...RepoCommentOptions) *markup.RenderContext {
|
||||||
helper := &RepoComment{
|
helper := &RepoComment{opts: util.OptionalArg(opts)}
|
||||||
repoLink: repo.Link(),
|
|
||||||
opts: util.OptionalArg(opts),
|
|
||||||
}
|
|
||||||
rctx := markup.NewRenderContext(ctx)
|
rctx := markup.NewRenderContext(ctx)
|
||||||
helper.ctx = rctx
|
helper.ctx = rctx
|
||||||
|
var metas map[string]string
|
||||||
if repo != nil {
|
if repo != nil {
|
||||||
helper.repoLink = repo.Link()
|
helper.repoLink = repo.Link()
|
||||||
helper.commitChecker = newCommitChecker(ctx, repo)
|
helper.commitChecker = newCommitChecker(ctx, repo)
|
||||||
rctx = rctx.WithMetas(repo.ComposeCommentMetas(ctx))
|
metas = repo.ComposeCommentMetas(ctx)
|
||||||
} else {
|
} else {
|
||||||
// this is almost dead code, only to pass the incorrect tests
|
// repo can be nil when rendering a commit message in user's dashboard feedback whose repository has been deleted
|
||||||
helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName)
|
metas = map[string]string{}
|
||||||
rctx = rctx.WithMetas(map[string]string{
|
if helper.opts.DeprecatedOwnerName != "" {
|
||||||
"user": helper.opts.DeprecatedOwnerName,
|
// this is almost dead code, only to pass the incorrect tests
|
||||||
"repo": helper.opts.DeprecatedRepoName,
|
helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName)
|
||||||
|
metas["user"] = helper.opts.DeprecatedOwnerName
|
||||||
"markdownNewLineHardBreak": "true",
|
metas["repo"] = helper.opts.DeprecatedRepoName
|
||||||
"markupAllowShortIssuePattern": "true",
|
}
|
||||||
})
|
metas["markdownNewLineHardBreak"] = "true"
|
||||||
|
metas["markupAllowShortIssuePattern"] = "true"
|
||||||
}
|
}
|
||||||
rctx = rctx.WithHelper(helper)
|
metas["footnoteContextId"] = helper.opts.FootnoteContextID
|
||||||
|
rctx = rctx.WithMetas(metas).WithHelper(helper)
|
||||||
return rctx
|
return rctx
|
||||||
}
|
}
|
||||||
|
@ -72,4 +72,11 @@ func TestRepoComment(t *testing.T) {
|
|||||||
<a href="/user2/repo1/commit/1234/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/commit/1234/image" alt="./image"/></a></p>
|
<a href="/user2/repo1/commit/1234/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/commit/1234/image" alt="./image"/></a></p>
|
||||||
`, rendered)
|
`, rendered)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("NoRepo", func(t *testing.T) {
|
||||||
|
rctx := NewRenderContextRepoComment(t.Context(), nil).WithMarkupType(markdown.MarkupName)
|
||||||
|
rendered, err := markup.RenderString(rctx, "any")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "<p>any</p>\n", rendered)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -48,8 +48,7 @@ func GetTeamRepositories(ctx context.Context, opts *SearchTeamRepoOptions) (Repo
|
|||||||
// accessible to a particular user
|
// accessible to a particular user
|
||||||
type AccessibleReposEnvironment interface {
|
type AccessibleReposEnvironment interface {
|
||||||
CountRepos(ctx context.Context) (int64, error)
|
CountRepos(ctx context.Context) (int64, error)
|
||||||
RepoIDs(ctx context.Context, page, pageSize int) ([]int64, error)
|
RepoIDs(ctx context.Context) ([]int64, error)
|
||||||
Repos(ctx context.Context, page, pageSize int) (RepositoryList, error)
|
|
||||||
MirrorRepos(ctx context.Context) (RepositoryList, error)
|
MirrorRepos(ctx context.Context) (RepositoryList, error)
|
||||||
AddKeyword(keyword string)
|
AddKeyword(keyword string)
|
||||||
SetSort(db.SearchOrderBy)
|
SetSort(db.SearchOrderBy)
|
||||||
@ -132,40 +131,18 @@ func (env *accessibleReposEnv) CountRepos(ctx context.Context) (int64, error) {
|
|||||||
return repoCount, nil
|
return repoCount, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (env *accessibleReposEnv) RepoIDs(ctx context.Context, page, pageSize int) ([]int64, error) {
|
func (env *accessibleReposEnv) RepoIDs(ctx context.Context) ([]int64, error) {
|
||||||
if page <= 0 {
|
var repoIDs []int64
|
||||||
page = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
repoIDs := make([]int64, 0, pageSize)
|
|
||||||
return repoIDs, db.GetEngine(ctx).
|
return repoIDs, db.GetEngine(ctx).
|
||||||
Table("repository").
|
Table("repository").
|
||||||
Join("INNER", "team_repo", "`team_repo`.repo_id=`repository`.id").
|
Join("INNER", "team_repo", "`team_repo`.repo_id=`repository`.id").
|
||||||
Where(env.cond()).
|
Where(env.cond()).
|
||||||
GroupBy("`repository`.id,`repository`."+strings.Fields(string(env.orderBy))[0]).
|
GroupBy("`repository`.id,`repository`." + strings.Fields(string(env.orderBy))[0]).
|
||||||
OrderBy(string(env.orderBy)).
|
OrderBy(string(env.orderBy)).
|
||||||
Limit(pageSize, (page-1)*pageSize).
|
|
||||||
Cols("`repository`.id").
|
Cols("`repository`.id").
|
||||||
Find(&repoIDs)
|
Find(&repoIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (env *accessibleReposEnv) Repos(ctx context.Context, page, pageSize int) (RepositoryList, error) {
|
|
||||||
repoIDs, err := env.RepoIDs(ctx, page, pageSize)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("GetUserRepositoryIDs: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
repos := make([]*Repository, 0, len(repoIDs))
|
|
||||||
if len(repoIDs) == 0 {
|
|
||||||
return repos, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return repos, db.GetEngine(ctx).
|
|
||||||
In("`repository`.id", repoIDs).
|
|
||||||
OrderBy(string(env.orderBy)).
|
|
||||||
Find(&repos)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (env *accessibleReposEnv) MirrorRepoIDs(ctx context.Context) ([]int64, error) {
|
func (env *accessibleReposEnv) MirrorRepoIDs(ctx context.Context) ([]int64, error) {
|
||||||
repoIDs := make([]int64, 0, 10)
|
repoIDs := make([]int64, 0, 10)
|
||||||
return repoIDs, db.GetEngine(ctx).
|
return repoIDs, db.GetEngine(ctx).
|
||||||
|
@ -161,6 +161,11 @@ func UpdateRelease(ctx context.Context, rel *Release) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func UpdateReleaseNumCommits(ctx context.Context, rel *Release) error {
|
||||||
|
_, err := db.GetEngine(ctx).ID(rel.ID).Cols("num_commits").Update(rel)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
// Check attachments
|
// Check attachments
|
||||||
@ -418,8 +423,8 @@ func UpdateReleasesMigrationsByType(ctx context.Context, gitServiceType structs.
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// PushUpdateDeleteTagsContext updates a number of delete tags with context
|
// PushUpdateDeleteTags updates a number of delete tags with context
|
||||||
func PushUpdateDeleteTagsContext(ctx context.Context, repo *Repository, tags []string) error {
|
func PushUpdateDeleteTags(ctx context.Context, repo *Repository, tags []string) error {
|
||||||
if len(tags) == 0 {
|
if len(tags) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -448,58 +453,6 @@ func PushUpdateDeleteTagsContext(ctx context.Context, repo *Repository, tags []s
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PushUpdateDeleteTag must be called for any push actions to delete tag
|
|
||||||
func PushUpdateDeleteTag(ctx context.Context, repo *Repository, tagName string) error {
|
|
||||||
rel, err := GetRelease(ctx, repo.ID, tagName)
|
|
||||||
if err != nil {
|
|
||||||
if IsErrReleaseNotExist(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("GetRelease: %w", err)
|
|
||||||
}
|
|
||||||
if rel.IsTag {
|
|
||||||
if _, err = db.DeleteByID[Release](ctx, rel.ID); err != nil {
|
|
||||||
return fmt.Errorf("Delete: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rel.IsDraft = true
|
|
||||||
rel.NumCommits = 0
|
|
||||||
rel.Sha1 = ""
|
|
||||||
if _, err = db.GetEngine(ctx).ID(rel.ID).AllCols().Update(rel); err != nil {
|
|
||||||
return fmt.Errorf("Update: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveOrUpdateTag must be called for any push actions to add tag
|
|
||||||
func SaveOrUpdateTag(ctx context.Context, repo *Repository, newRel *Release) error {
|
|
||||||
rel, err := GetRelease(ctx, repo.ID, newRel.TagName)
|
|
||||||
if err != nil && !IsErrReleaseNotExist(err) {
|
|
||||||
return fmt.Errorf("GetRelease: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if rel == nil {
|
|
||||||
rel = newRel
|
|
||||||
if _, err = db.GetEngine(ctx).Insert(rel); err != nil {
|
|
||||||
return fmt.Errorf("InsertOne: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rel.Sha1 = newRel.Sha1
|
|
||||||
rel.CreatedUnix = newRel.CreatedUnix
|
|
||||||
rel.NumCommits = newRel.NumCommits
|
|
||||||
rel.IsDraft = false
|
|
||||||
if rel.IsTag && newRel.PublisherID > 0 {
|
|
||||||
rel.PublisherID = newRel.PublisherID
|
|
||||||
}
|
|
||||||
if _, err = db.GetEngine(ctx).ID(rel.ID).AllCols().Update(rel); err != nil {
|
|
||||||
return fmt.Errorf("Update: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemapExternalUser ExternalUserRemappable interface
|
// RemapExternalUser ExternalUserRemappable interface
|
||||||
func (r *Release) RemapExternalUser(externalName string, externalID, userID int64) error {
|
func (r *Release) RemapExternalUser(externalName string, externalID, userID int64) error {
|
||||||
r.OriginalAuthor = externalName
|
r.OriginalAuthor = externalName
|
||||||
|
@ -64,18 +64,18 @@ func (err ErrRepoIsArchived) Error() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type globalVarsStruct struct {
|
type globalVarsStruct struct {
|
||||||
validRepoNamePattern *regexp.Regexp
|
validRepoNamePattern *regexp.Regexp
|
||||||
invalidRepoNamePattern *regexp.Regexp
|
invalidRepoNamePattern *regexp.Regexp
|
||||||
reservedRepoNames []string
|
reservedRepoNames []string
|
||||||
reservedRepoPatterns []string
|
reservedRepoNamePatterns []string
|
||||||
}
|
}
|
||||||
|
|
||||||
var globalVars = sync.OnceValue(func() *globalVarsStruct {
|
var globalVars = sync.OnceValue(func() *globalVarsStruct {
|
||||||
return &globalVarsStruct{
|
return &globalVarsStruct{
|
||||||
validRepoNamePattern: regexp.MustCompile(`[-.\w]+`),
|
validRepoNamePattern: regexp.MustCompile(`^[-.\w]+$`),
|
||||||
invalidRepoNamePattern: regexp.MustCompile(`[.]{2,}`),
|
invalidRepoNamePattern: regexp.MustCompile(`[.]{2,}`),
|
||||||
reservedRepoNames: []string{".", "..", "-"},
|
reservedRepoNames: []string{".", "..", "-"},
|
||||||
reservedRepoPatterns: []string{"*.git", "*.wiki", "*.rss", "*.atom"},
|
reservedRepoNamePatterns: []string{"*.wiki", "*.git", "*.rss", "*.atom"},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -86,7 +86,16 @@ func IsUsableRepoName(name string) error {
|
|||||||
// Note: usually this error is normally caught up earlier in the UI
|
// Note: usually this error is normally caught up earlier in the UI
|
||||||
return db.ErrNameCharsNotAllowed{Name: name}
|
return db.ErrNameCharsNotAllowed{Name: name}
|
||||||
}
|
}
|
||||||
return db.IsUsableName(vars.reservedRepoNames, vars.reservedRepoPatterns, name)
|
return db.IsUsableName(vars.reservedRepoNames, vars.reservedRepoNamePatterns, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValidSSHAccessRepoName is like IsUsableRepoName, but it allows "*.wiki" because wiki repo needs to be accessed in SSH code
|
||||||
|
func IsValidSSHAccessRepoName(name string) bool {
|
||||||
|
vars := globalVars()
|
||||||
|
if !vars.validRepoNamePattern.MatchString(name) || vars.invalidRepoNamePattern.MatchString(name) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return db.IsUsableName(vars.reservedRepoNames, vars.reservedRepoNamePatterns[1:], name) == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TrustModelType defines the types of trust model for this repository
|
// TrustModelType defines the types of trust model for this repository
|
||||||
|
@ -216,8 +216,23 @@ func TestIsUsableRepoName(t *testing.T) {
|
|||||||
|
|
||||||
assert.Error(t, IsUsableRepoName("-"))
|
assert.Error(t, IsUsableRepoName("-"))
|
||||||
assert.Error(t, IsUsableRepoName("🌞"))
|
assert.Error(t, IsUsableRepoName("🌞"))
|
||||||
|
assert.Error(t, IsUsableRepoName("the/repo"))
|
||||||
assert.Error(t, IsUsableRepoName("the..repo"))
|
assert.Error(t, IsUsableRepoName("the..repo"))
|
||||||
assert.Error(t, IsUsableRepoName("foo.wiki"))
|
assert.Error(t, IsUsableRepoName("foo.wiki"))
|
||||||
assert.Error(t, IsUsableRepoName("foo.git"))
|
assert.Error(t, IsUsableRepoName("foo.git"))
|
||||||
assert.Error(t, IsUsableRepoName("foo.RSS"))
|
assert.Error(t, IsUsableRepoName("foo.RSS"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIsValidSSHAccessRepoName(t *testing.T) {
|
||||||
|
assert.True(t, IsValidSSHAccessRepoName("a"))
|
||||||
|
assert.True(t, IsValidSSHAccessRepoName("-1_."))
|
||||||
|
assert.True(t, IsValidSSHAccessRepoName(".profile"))
|
||||||
|
assert.True(t, IsValidSSHAccessRepoName("foo.wiki"))
|
||||||
|
|
||||||
|
assert.False(t, IsValidSSHAccessRepoName("-"))
|
||||||
|
assert.False(t, IsValidSSHAccessRepoName("🌞"))
|
||||||
|
assert.False(t, IsValidSSHAccessRepoName("the/repo"))
|
||||||
|
assert.False(t, IsValidSSHAccessRepoName("the..repo"))
|
||||||
|
assert.False(t, IsValidSSHAccessRepoName("foo.git"))
|
||||||
|
assert.False(t, IsValidSSHAccessRepoName("foo.RSS"))
|
||||||
|
}
|
||||||
|
@ -249,7 +249,7 @@ func CreatePendingRepositoryTransfer(ctx context.Context, doer, newOwner *user_m
|
|||||||
}
|
}
|
||||||
|
|
||||||
repo.Status = RepositoryPendingTransfer
|
repo.Status = RepositoryPendingTransfer
|
||||||
if err := UpdateRepositoryCols(ctx, repo, "status"); err != nil {
|
if err := UpdateRepositoryColsNoAutoTime(ctx, repo, "status"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ func UpdateRepositoryOwnerNames(ctx context.Context, ownerID int64, ownerName st
|
|||||||
}
|
}
|
||||||
defer committer.Close()
|
defer committer.Close()
|
||||||
|
|
||||||
if _, err := db.GetEngine(ctx).Where("owner_id = ?", ownerID).Cols("owner_name").Update(&Repository{
|
if _, err := db.GetEngine(ctx).Where("owner_id = ?", ownerID).Cols("owner_name").NoAutoTime().Update(&Repository{
|
||||||
OwnerName: ownerName,
|
OwnerName: ownerName,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -40,8 +40,8 @@ func UpdateRepositoryUpdatedTime(ctx context.Context, repoID int64, updateTime t
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateRepositoryCols updates repository's columns
|
// UpdateRepositoryColsWithAutoTime updates repository's columns
|
||||||
func UpdateRepositoryCols(ctx context.Context, repo *Repository, cols ...string) error {
|
func UpdateRepositoryColsWithAutoTime(ctx context.Context, repo *Repository, cols ...string) error {
|
||||||
_, err := db.GetEngine(ctx).ID(repo.ID).Cols(cols...).Update(repo)
|
_, err := db.GetEngine(ctx).ID(repo.ID).Cols(cols...).Update(repo)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ package user
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/md5"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"image/png"
|
"image/png"
|
||||||
"io"
|
"io"
|
||||||
@ -106,7 +105,7 @@ func (u *User) IsUploadAvatarChanged(data []byte) bool {
|
|||||||
if !u.UseCustomAvatar || len(u.Avatar) == 0 {
|
if !u.UseCustomAvatar || len(u.Avatar) == 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
avatarID := fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%d-%x", u.ID, md5.Sum(data)))))
|
avatarID := avatar.HashAvatar(u.ID, data)
|
||||||
return u.Avatar != avatarID
|
return u.Avatar != avatarID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1146,8 +1146,8 @@ func ValidateCommitsWithEmails(ctx context.Context, oldCommits []*git.Commit) ([
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range oldCommits {
|
for _, c := range oldCommits {
|
||||||
user, ok := emailUserMap[c.Author.Email]
|
user := emailUserMap.GetByEmail(c.Author.Email) // FIXME: why ValidateCommitsWithEmails uses "Author", but ParseCommitsWithSignature uses "Committer"?
|
||||||
if !ok {
|
if user == nil {
|
||||||
user = &User{
|
user = &User{
|
||||||
Name: c.Author.Name,
|
Name: c.Author.Name,
|
||||||
Email: c.Author.Email,
|
Email: c.Author.Email,
|
||||||
@ -1161,19 +1161,29 @@ func ValidateCommitsWithEmails(ctx context.Context, oldCommits []*git.Commit) ([
|
|||||||
return newCommits, nil
|
return newCommits, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetUsersByEmails(ctx context.Context, emails []string) (map[string]*User, error) {
|
type EmailUserMap struct {
|
||||||
|
m map[string]*User
|
||||||
|
}
|
||||||
|
|
||||||
|
func (eum *EmailUserMap) GetByEmail(email string) *User {
|
||||||
|
return eum.m[strings.ToLower(email)]
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUsersByEmails(ctx context.Context, emails []string) (*EmailUserMap, error) {
|
||||||
if len(emails) == 0 {
|
if len(emails) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
needCheckEmails := make(container.Set[string])
|
needCheckEmails := make(container.Set[string])
|
||||||
needCheckUserNames := make(container.Set[string])
|
needCheckUserNames := make(container.Set[string])
|
||||||
|
noReplyAddressSuffix := "@" + strings.ToLower(setting.Service.NoReplyAddress)
|
||||||
for _, email := range emails {
|
for _, email := range emails {
|
||||||
if strings.HasSuffix(email, "@"+setting.Service.NoReplyAddress) {
|
emailLower := strings.ToLower(email)
|
||||||
username := strings.TrimSuffix(email, "@"+setting.Service.NoReplyAddress)
|
if noReplyUserNameLower, ok := strings.CutSuffix(emailLower, noReplyAddressSuffix); ok {
|
||||||
needCheckUserNames.Add(username)
|
needCheckUserNames.Add(noReplyUserNameLower)
|
||||||
|
needCheckEmails.Add(emailLower)
|
||||||
} else {
|
} else {
|
||||||
needCheckEmails.Add(strings.ToLower(email))
|
needCheckEmails.Add(emailLower)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1198,7 +1208,7 @@ func GetUsersByEmails(ctx context.Context, emails []string) (map[string]*User, e
|
|||||||
for _, email := range emailAddresses {
|
for _, email := range emailAddresses {
|
||||||
user := users[email.UID]
|
user := users[email.UID]
|
||||||
if user != nil {
|
if user != nil {
|
||||||
results[user.GetEmail()] = user
|
results[email.LowerEmail] = user
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1208,9 +1218,9 @@ func GetUsersByEmails(ctx context.Context, emails []string) (map[string]*User, e
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
results[user.GetPlaceholderEmail()] = user
|
results[strings.ToLower(user.GetPlaceholderEmail())] = user
|
||||||
}
|
}
|
||||||
return results, nil
|
return &EmailUserMap{results}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserByEmail returns the user object by given e-mail if exists.
|
// GetUserByEmail returns the user object by given e-mail if exists.
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestIsUsableUsername(t *testing.T) {
|
func TestIsUsableUsername(t *testing.T) {
|
||||||
@ -48,14 +49,47 @@ func TestOAuth2Application_LoadUser(t *testing.T) {
|
|||||||
assert.NotNil(t, user)
|
assert.NotNil(t, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetUserEmailsByNames(t *testing.T) {
|
func TestUserEmails(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
t.Run("GetUserEmailsByNames", func(t *testing.T) {
|
||||||
// ignore none active user email
|
// ignore none active user email
|
||||||
assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "user9"}))
|
assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "user9"}))
|
||||||
assert.ElementsMatch(t, []string{"user8@example.com", "user5@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "user5"}))
|
assert.ElementsMatch(t, []string{"user8@example.com", "user5@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "user5"}))
|
||||||
|
assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "org7"}))
|
||||||
assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "org7"}))
|
})
|
||||||
|
t.Run("GetUsersByEmails", func(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&setting.Service.NoReplyAddress, "NoReply.gitea.internal")()
|
||||||
|
testGetUserByEmail := func(t *testing.T, email string, uid int64) {
|
||||||
|
m, err := user_model.GetUsersByEmails(db.DefaultContext, []string{email})
|
||||||
|
require.NoError(t, err)
|
||||||
|
user := m.GetByEmail(email)
|
||||||
|
if uid == 0 {
|
||||||
|
require.Nil(t, user)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NotNil(t, user)
|
||||||
|
assert.Equal(t, uid, user.ID)
|
||||||
|
}
|
||||||
|
cases := []struct {
|
||||||
|
Email string
|
||||||
|
UID int64
|
||||||
|
}{
|
||||||
|
{"UseR1@example.com", 1},
|
||||||
|
{"user1-2@example.COM", 1},
|
||||||
|
{"USER2@" + setting.Service.NoReplyAddress, 2},
|
||||||
|
{"user4@example.com", 4},
|
||||||
|
{"no-such", 0},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(c.Email, func(t *testing.T) {
|
||||||
|
testGetUserByEmail(t, c.Email, c.UID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
t.Run("NoReplyConflict", func(t *testing.T) {
|
||||||
|
setting.Service.NoReplyAddress = "example.com"
|
||||||
|
testGetUserByEmail(t, "user1-2@example.COM", 1)
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCanCreateOrganization(t *testing.T) {
|
func TestCanCreateOrganization(t *testing.T) {
|
||||||
|
@ -311,6 +311,10 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
|
|||||||
matchTimes++
|
matchTimes++
|
||||||
}
|
}
|
||||||
case "paths":
|
case "paths":
|
||||||
|
if refName.IsTag() {
|
||||||
|
matchTimes++
|
||||||
|
break
|
||||||
|
}
|
||||||
filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before)
|
filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err)
|
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err)
|
||||||
@ -324,6 +328,10 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "paths-ignore":
|
case "paths-ignore":
|
||||||
|
if refName.IsTag() {
|
||||||
|
matchTimes++
|
||||||
|
break
|
||||||
|
}
|
||||||
filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before)
|
filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err)
|
log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err)
|
||||||
|
@ -125,6 +125,24 @@ func TestDetectMatched(t *testing.T) {
|
|||||||
yamlOn: "on: schedule",
|
yamlOn: "on: schedule",
|
||||||
expected: true,
|
expected: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "push to tag matches workflow with paths condition (should skip paths check)",
|
||||||
|
triggedEvent: webhook_module.HookEventPush,
|
||||||
|
payload: &api.PushPayload{
|
||||||
|
Ref: "refs/tags/v1.0.0",
|
||||||
|
Before: "0000000",
|
||||||
|
Commits: []*api.PayloadCommit{
|
||||||
|
{
|
||||||
|
ID: "abcdef123456",
|
||||||
|
Added: []string{"src/main.go"},
|
||||||
|
Message: "Release v1.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
commit: nil,
|
||||||
|
yamlOn: "on:\n push:\n paths:\n - src/**",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
|
@ -6,22 +6,26 @@ package fileicon
|
|||||||
import (
|
import (
|
||||||
"html/template"
|
"html/template"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
|
||||||
"code.gitea.io/gitea/modules/svg"
|
"code.gitea.io/gitea/modules/svg"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func BasicThemeIcon(entry *git.TreeEntry) template.HTML {
|
func BasicEntryIconName(entry *EntryInfo) string {
|
||||||
svgName := "octicon-file"
|
svgName := "octicon-file"
|
||||||
switch {
|
switch {
|
||||||
case entry.IsLink():
|
case entry.EntryMode.IsLink():
|
||||||
svgName = "octicon-file-symlink-file"
|
svgName = "octicon-file-symlink-file"
|
||||||
if te, err := entry.FollowLink(); err == nil && te.IsDir() {
|
if entry.SymlinkToMode.IsDir() {
|
||||||
svgName = "octicon-file-directory-symlink"
|
svgName = "octicon-file-directory-symlink"
|
||||||
}
|
}
|
||||||
case entry.IsDir():
|
case entry.EntryMode.IsDir():
|
||||||
svgName = "octicon-file-directory-fill"
|
svgName = util.Iif(entry.IsOpen, "octicon-file-directory-open-fill", "octicon-file-directory-fill")
|
||||||
case entry.IsSubModule():
|
case entry.EntryMode.IsSubModule():
|
||||||
svgName = "octicon-file-submodule"
|
svgName = "octicon-file-submodule"
|
||||||
}
|
}
|
||||||
return svg.RenderHTML(svgName)
|
return svgName
|
||||||
|
}
|
||||||
|
|
||||||
|
func BasicEntryIconHTML(entry *EntryInfo) template.HTML {
|
||||||
|
return svg.RenderHTML(BasicEntryIconName(entry))
|
||||||
}
|
}
|
||||||
|
31
modules/fileicon/entry.go
Normal file
31
modules/fileicon/entry.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package fileicon
|
||||||
|
|
||||||
|
import "code.gitea.io/gitea/modules/git"
|
||||||
|
|
||||||
|
type EntryInfo struct {
|
||||||
|
FullName string
|
||||||
|
EntryMode git.EntryMode
|
||||||
|
SymlinkToMode git.EntryMode
|
||||||
|
IsOpen bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func EntryInfoFromGitTreeEntry(gitEntry *git.TreeEntry) *EntryInfo {
|
||||||
|
ret := &EntryInfo{FullName: gitEntry.Name(), EntryMode: gitEntry.Mode()}
|
||||||
|
if gitEntry.IsLink() {
|
||||||
|
if te, err := gitEntry.FollowLink(); err == nil && te.IsDir() {
|
||||||
|
ret.SymlinkToMode = te.Mode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func EntryInfoFolder() *EntryInfo {
|
||||||
|
return &EntryInfo{EntryMode: git.EntryModeTree}
|
||||||
|
}
|
||||||
|
|
||||||
|
func EntryInfoFolderOpen() *EntryInfo {
|
||||||
|
return &EntryInfo{EntryMode: git.EntryModeTree, IsOpen: true}
|
||||||
|
}
|
@ -9,11 +9,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
|
||||||
"code.gitea.io/gitea/modules/json"
|
"code.gitea.io/gitea/modules/json"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/options"
|
"code.gitea.io/gitea/modules/options"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/svg"
|
"code.gitea.io/gitea/modules/svg"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type materialIconRulesData struct {
|
type materialIconRulesData struct {
|
||||||
@ -69,41 +70,51 @@ func (m *MaterialIconProvider) renderFileIconSVG(p *RenderedIconPool, name, svg,
|
|||||||
}
|
}
|
||||||
svgID := "svg-mfi-" + name
|
svgID := "svg-mfi-" + name
|
||||||
svgCommonAttrs := `class="svg git-entry-icon ` + extraClass + `" width="16" height="16" aria-hidden="true"`
|
svgCommonAttrs := `class="svg git-entry-icon ` + extraClass + `" width="16" height="16" aria-hidden="true"`
|
||||||
|
svgHTML := template.HTML(`<svg id="` + svgID + `" ` + svgCommonAttrs + svg[4:])
|
||||||
|
if p == nil {
|
||||||
|
return svgHTML
|
||||||
|
}
|
||||||
if p.IconSVGs[svgID] == "" {
|
if p.IconSVGs[svgID] == "" {
|
||||||
p.IconSVGs[svgID] = template.HTML(`<svg id="` + svgID + `" ` + svgCommonAttrs + svg[4:])
|
p.IconSVGs[svgID] = svgHTML
|
||||||
}
|
}
|
||||||
return template.HTML(`<svg ` + svgCommonAttrs + `><use xlink:href="#` + svgID + `"></use></svg>`)
|
return template.HTML(`<svg ` + svgCommonAttrs + `><use xlink:href="#` + svgID + `"></use></svg>`)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MaterialIconProvider) FileIcon(p *RenderedIconPool, entry *git.TreeEntry) template.HTML {
|
func (m *MaterialIconProvider) EntryIconHTML(p *RenderedIconPool, entry *EntryInfo) template.HTML {
|
||||||
if m.rules == nil {
|
if m.rules == nil {
|
||||||
return BasicThemeIcon(entry)
|
return BasicEntryIconHTML(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
if entry.IsLink() {
|
if entry.EntryMode.IsLink() {
|
||||||
if te, err := entry.FollowLink(); err == nil && te.IsDir() {
|
if entry.SymlinkToMode.IsDir() {
|
||||||
// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work
|
// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work
|
||||||
return svg.RenderHTML("material-folder-symlink", 16, "octicon-file-directory-symlink")
|
return svg.RenderHTML("material-folder-symlink", 16, "octicon-file-directory-symlink")
|
||||||
}
|
}
|
||||||
return svg.RenderHTML("octicon-file-symlink-file") // TODO: find some better icons for them
|
return svg.RenderHTML("octicon-file-symlink-file") // TODO: find some better icons for them
|
||||||
}
|
}
|
||||||
|
|
||||||
name := m.findIconNameByGit(entry)
|
name := m.FindIconName(entry)
|
||||||
// the material icon pack's "folder" icon doesn't look good, so use our built-in one
|
iconSVG := m.svgs[name]
|
||||||
// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work
|
if iconSVG == "" {
|
||||||
if iconSVG, ok := m.svgs[name]; ok && name != "folder" && iconSVG != "" {
|
name = "file"
|
||||||
// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work
|
if entry.EntryMode.IsDir() {
|
||||||
extraClass := "octicon-file"
|
name = util.Iif(entry.IsOpen, "folder-open", "folder")
|
||||||
switch {
|
}
|
||||||
case entry.IsDir():
|
iconSVG = m.svgs[name]
|
||||||
extraClass = "octicon-file-directory-fill"
|
if iconSVG == "" {
|
||||||
case entry.IsSubModule():
|
setting.PanicInDevOrTesting("missing file icon for %s", name)
|
||||||
extraClass = "octicon-file-submodule"
|
|
||||||
}
|
}
|
||||||
return m.renderFileIconSVG(p, name, iconSVG, extraClass)
|
|
||||||
}
|
}
|
||||||
// TODO: use an interface or wrapper for git.Entry to make the code testable.
|
|
||||||
return BasicThemeIcon(entry)
|
// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work
|
||||||
|
extraClass := "octicon-file"
|
||||||
|
switch {
|
||||||
|
case entry.EntryMode.IsDir():
|
||||||
|
extraClass = BasicEntryIconName(entry)
|
||||||
|
case entry.EntryMode.IsSubModule():
|
||||||
|
extraClass = "octicon-file-submodule"
|
||||||
|
}
|
||||||
|
return m.renderFileIconSVG(p, name, iconSVG, extraClass)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MaterialIconProvider) findIconNameWithLangID(s string) string {
|
func (m *MaterialIconProvider) findIconNameWithLangID(s string) string {
|
||||||
@ -118,13 +129,17 @@ func (m *MaterialIconProvider) findIconNameWithLangID(s string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MaterialIconProvider) FindIconName(name string, isDir bool) string {
|
func (m *MaterialIconProvider) FindIconName(entry *EntryInfo) string {
|
||||||
fileNameLower := strings.ToLower(path.Base(name))
|
if entry.EntryMode.IsSubModule() {
|
||||||
if isDir {
|
return "folder-git"
|
||||||
|
}
|
||||||
|
|
||||||
|
fileNameLower := strings.ToLower(path.Base(entry.FullName))
|
||||||
|
if entry.EntryMode.IsDir() {
|
||||||
if s, ok := m.rules.FolderNames[fileNameLower]; ok {
|
if s, ok := m.rules.FolderNames[fileNameLower]; ok {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
return "folder"
|
return util.Iif(entry.IsOpen, "folder-open", "folder")
|
||||||
}
|
}
|
||||||
|
|
||||||
if s, ok := m.rules.FileNames[fileNameLower]; ok {
|
if s, ok := m.rules.FileNames[fileNameLower]; ok {
|
||||||
@ -146,10 +161,3 @@ func (m *MaterialIconProvider) FindIconName(name string, isDir bool) string {
|
|||||||
|
|
||||||
return "file"
|
return "file"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MaterialIconProvider) findIconNameByGit(entry *git.TreeEntry) string {
|
|
||||||
if entry.IsSubModule() {
|
|
||||||
return "folder-git"
|
|
||||||
}
|
|
||||||
return m.FindIconName(entry.Name(), entry.IsDir())
|
|
||||||
}
|
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
"code.gitea.io/gitea/modules/fileicon"
|
"code.gitea.io/gitea/modules/fileicon"
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@ -19,8 +20,8 @@ func TestMain(m *testing.M) {
|
|||||||
func TestFindIconName(t *testing.T) {
|
func TestFindIconName(t *testing.T) {
|
||||||
unittest.PrepareTestEnv(t)
|
unittest.PrepareTestEnv(t)
|
||||||
p := fileicon.DefaultMaterialIconProvider()
|
p := fileicon.DefaultMaterialIconProvider()
|
||||||
assert.Equal(t, "php", p.FindIconName("foo.php", false))
|
assert.Equal(t, "php", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.php", EntryMode: git.EntryModeBlob}))
|
||||||
assert.Equal(t, "php", p.FindIconName("foo.PHP", false))
|
assert.Equal(t, "php", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.PHP", EntryMode: git.EntryModeBlob}))
|
||||||
assert.Equal(t, "javascript", p.FindIconName("foo.js", false))
|
assert.Equal(t, "javascript", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.js", EntryMode: git.EntryModeBlob}))
|
||||||
assert.Equal(t, "visualstudio", p.FindIconName("foo.vba", false))
|
assert.Equal(t, "visualstudio", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.vba", EntryMode: git.EntryModeBlob}))
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,6 @@ import (
|
|||||||
"html/template"
|
"html/template"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -34,19 +33,9 @@ func (p *RenderedIconPool) RenderToHTML() template.HTML {
|
|||||||
return template.HTML(sb.String())
|
return template.HTML(sb.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: use an interface or struct to replace "*git.TreeEntry", to decouple the fileicon module from git module
|
func RenderEntryIconHTML(renderedIconPool *RenderedIconPool, entry *EntryInfo) template.HTML {
|
||||||
|
|
||||||
func RenderEntryIcon(renderedIconPool *RenderedIconPool, entry *git.TreeEntry) template.HTML {
|
|
||||||
if setting.UI.FileIconTheme == "material" {
|
if setting.UI.FileIconTheme == "material" {
|
||||||
return DefaultMaterialIconProvider().FileIcon(renderedIconPool, entry)
|
return DefaultMaterialIconProvider().EntryIconHTML(renderedIconPool, entry)
|
||||||
}
|
}
|
||||||
return BasicThemeIcon(entry)
|
return BasicEntryIconHTML(entry)
|
||||||
}
|
|
||||||
|
|
||||||
func RenderEntryIconOpen(renderedIconPool *RenderedIconPool, entry *git.TreeEntry) template.HTML {
|
|
||||||
// TODO: add "open icon" support
|
|
||||||
if setting.UI.FileIconTheme == "material" {
|
|
||||||
return DefaultMaterialIconProvider().FileIcon(renderedIconPool, entry)
|
|
||||||
}
|
|
||||||
return BasicThemeIcon(entry)
|
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,12 @@ func checkAttrCommand(gitRepo *git.Repository, treeish string, filenames, attrib
|
|||||||
)
|
)
|
||||||
cancel = deleteTemporaryFile
|
cancel = deleteTemporaryFile
|
||||||
}
|
}
|
||||||
} // else: no treeish, assume it is a not a bare repo, read from working directory
|
} else {
|
||||||
|
// Read from existing index, in cases where the repo is bare and has an index,
|
||||||
|
// or the work tree contains unstaged changes that shouldn't affect the attribute check.
|
||||||
|
// It is caller's responsibility to add changed ".gitattributes" into the index if they want to respect the new changes.
|
||||||
|
cmd.AddArguments("--cached")
|
||||||
|
}
|
||||||
|
|
||||||
cmd.AddDynamicArguments(attributes...)
|
cmd.AddDynamicArguments(attributes...)
|
||||||
if len(filenames) > 0 {
|
if len(filenames) > 0 {
|
||||||
|
@ -57,8 +57,18 @@ func Test_Checker(t *testing.T) {
|
|||||||
assert.Equal(t, expectedAttrs(), attrs["i-am-a-python.p"])
|
assert.Equal(t, expectedAttrs(), attrs["i-am-a-python.p"])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Run git check-attr in bare repository using index", func(t *testing.T) {
|
||||||
|
attrs, err := CheckAttributes(t.Context(), gitRepo, "", CheckAttributeOpts{
|
||||||
|
Filenames: []string{"i-am-a-python.p"},
|
||||||
|
Attributes: LinguistAttributes,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, attrs, 1)
|
||||||
|
assert.Equal(t, expectedAttrs(), attrs["i-am-a-python.p"])
|
||||||
|
})
|
||||||
|
|
||||||
if !git.DefaultFeatures().SupportCheckAttrOnBare {
|
if !git.DefaultFeatures().SupportCheckAttrOnBare {
|
||||||
t.Skip("git version 2.40 is required to support run check-attr on bare repo")
|
t.Skip("git version 2.40 is required to support run check-attr on bare repo without using index")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,18 +132,22 @@ func (r *BlameReader) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateBlameReader creates reader for given repository, commit and file
|
// CreateBlameReader creates reader for given repository, commit and file
|
||||||
func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath string, commit *Commit, file string, bypassBlameIgnore bool) (*BlameReader, error) {
|
func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath string, commit *Commit, file string, bypassBlameIgnore bool) (rd *BlameReader, err error) {
|
||||||
reader, stdout, err := os.Pipe()
|
var ignoreRevsFileName string
|
||||||
if err != nil {
|
var ignoreRevsFileCleanup func()
|
||||||
return nil, err
|
defer func() {
|
||||||
}
|
if err != nil && ignoreRevsFileCleanup != nil {
|
||||||
|
ignoreRevsFileCleanup()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
cmd := NewCommandNoGlobals("blame", "--porcelain")
|
cmd := NewCommandNoGlobals("blame", "--porcelain")
|
||||||
|
|
||||||
var ignoreRevsFileName string
|
|
||||||
var ignoreRevsFileCleanup func() // TODO: maybe it should check the returned err in a defer func to make sure the cleanup could always be executed correctly
|
|
||||||
if DefaultFeatures().CheckVersionAtLeast("2.23") && !bypassBlameIgnore {
|
if DefaultFeatures().CheckVersionAtLeast("2.23") && !bypassBlameIgnore {
|
||||||
ignoreRevsFileName, ignoreRevsFileCleanup = tryCreateBlameIgnoreRevsFile(commit)
|
ignoreRevsFileName, ignoreRevsFileCleanup, err = tryCreateBlameIgnoreRevsFile(commit)
|
||||||
|
if err != nil && !IsErrNotExist(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if ignoreRevsFileName != "" {
|
if ignoreRevsFileName != "" {
|
||||||
// Possible improvement: use --ignore-revs-file /dev/stdin on unix
|
// Possible improvement: use --ignore-revs-file /dev/stdin on unix
|
||||||
// There is no equivalent on Windows. May be implemented if Gitea uses an external git backend.
|
// There is no equivalent on Windows. May be implemented if Gitea uses an external git backend.
|
||||||
@ -154,6 +158,10 @@ func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath
|
|||||||
cmd.AddDynamicArguments(commit.ID.String()).AddDashesAndList(file)
|
cmd.AddDynamicArguments(commit.ID.String()).AddDashesAndList(file)
|
||||||
|
|
||||||
done := make(chan error, 1)
|
done := make(chan error, 1)
|
||||||
|
reader, stdout, err := os.Pipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
go func() {
|
go func() {
|
||||||
stderr := bytes.Buffer{}
|
stderr := bytes.Buffer{}
|
||||||
// TODO: it doesn't work for directories (the directories shouldn't be "blamed"), and the "err" should be returned by "Read" but not by "Close"
|
// TODO: it doesn't work for directories (the directories shouldn't be "blamed"), and the "err" should be returned by "Read" but not by "Close"
|
||||||
@ -182,33 +190,29 @@ func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func tryCreateBlameIgnoreRevsFile(commit *Commit) (string, func()) {
|
func tryCreateBlameIgnoreRevsFile(commit *Commit) (string, func(), error) {
|
||||||
entry, err := commit.GetTreeEntryByPath(".git-blame-ignore-revs")
|
entry, err := commit.GetTreeEntryByPath(".git-blame-ignore-revs")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Unable to get .git-blame-ignore-revs file: GetTreeEntryByPath: %v", err)
|
return "", nil, err
|
||||||
return "", nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := entry.Blob().DataAsync()
|
r, err := entry.Blob().DataAsync()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Unable to get .git-blame-ignore-revs file data: DataAsync: %v", err)
|
return "", nil, err
|
||||||
return "", nil
|
|
||||||
}
|
}
|
||||||
defer r.Close()
|
defer r.Close()
|
||||||
|
|
||||||
f, cleanup, err := setting.AppDataTempDir("git-repo-content").CreateTempFileRandom("git-blame-ignore-revs")
|
f, cleanup, err := setting.AppDataTempDir("git-repo-content").CreateTempFileRandom("git-blame-ignore-revs")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Unable to get .git-blame-ignore-revs file data: CreateTempFileRandom: %v", err)
|
return "", nil, err
|
||||||
return "", nil
|
|
||||||
}
|
}
|
||||||
filename := f.Name()
|
filename := f.Name()
|
||||||
_, err = io.Copy(f, r)
|
_, err = io.Copy(f, r)
|
||||||
_ = f.Close()
|
_ = f.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cleanup()
|
cleanup()
|
||||||
log.Error("Unable to get .git-blame-ignore-revs file data: Copy: %v", err)
|
return "", nil, err
|
||||||
return "", nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return filename, cleanup
|
return filename, cleanup, nil
|
||||||
}
|
}
|
||||||
|
36
modules/git/cmdverb.go
Normal file
36
modules/git/cmdverb.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
const (
|
||||||
|
CmdVerbUploadPack = "git-upload-pack"
|
||||||
|
CmdVerbUploadArchive = "git-upload-archive"
|
||||||
|
CmdVerbReceivePack = "git-receive-pack"
|
||||||
|
CmdVerbLfsAuthenticate = "git-lfs-authenticate"
|
||||||
|
CmdVerbLfsTransfer = "git-lfs-transfer"
|
||||||
|
|
||||||
|
CmdSubVerbLfsUpload = "upload"
|
||||||
|
CmdSubVerbLfsDownload = "download"
|
||||||
|
)
|
||||||
|
|
||||||
|
func IsAllowedVerbForServe(verb string) bool {
|
||||||
|
switch verb {
|
||||||
|
case CmdVerbUploadPack,
|
||||||
|
CmdVerbUploadArchive,
|
||||||
|
CmdVerbReceivePack,
|
||||||
|
CmdVerbLfsAuthenticate,
|
||||||
|
CmdVerbLfsTransfer:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsAllowedVerbForServeLfs(verb string) bool {
|
||||||
|
switch verb {
|
||||||
|
case CmdVerbLfsAuthenticate,
|
||||||
|
CmdVerbLfsTransfer:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
@ -34,7 +34,7 @@ type Commit struct {
|
|||||||
// CommitSignature represents a git commit signature part.
|
// CommitSignature represents a git commit signature part.
|
||||||
type CommitSignature struct {
|
type CommitSignature struct {
|
||||||
Signature string
|
Signature string
|
||||||
Payload string // TODO check if can be reconstruct from the rest of commit information to not have duplicate data
|
Payload string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Message returns the commit message. Same as retrieving CommitMessage directly.
|
// Message returns the commit message. Same as retrieving CommitMessage directly.
|
||||||
|
@ -3,9 +3,20 @@
|
|||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
|
import "path"
|
||||||
|
|
||||||
// CommitInfo describes the first commit with the provided entry
|
// CommitInfo describes the first commit with the provided entry
|
||||||
type CommitInfo struct {
|
type CommitInfo struct {
|
||||||
Entry *TreeEntry
|
Entry *TreeEntry
|
||||||
Commit *Commit
|
Commit *Commit
|
||||||
SubmoduleFile *CommitSubmoduleFile
|
SubmoduleFile *CommitSubmoduleFile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getCommitInfoSubmoduleFile(repoLink string, entry *TreeEntry, commit *Commit, treePathDir string) (*CommitSubmoduleFile, error) {
|
||||||
|
fullPath := path.Join(treePathDir, entry.Name())
|
||||||
|
submodule, err := commit.GetSubModule(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewCommitSubmoduleFile(repoLink, fullPath, submodule.URL, entry.ID.String()), nil
|
||||||
|
}
|
||||||
|
@ -16,7 +16,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// GetCommitsInfo gets information of all commits that are corresponding to these entries
|
// GetCommitsInfo gets information of all commits that are corresponding to these entries
|
||||||
func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) {
|
func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) {
|
||||||
entryPaths := make([]string, len(tes)+1)
|
entryPaths := make([]string, len(tes)+1)
|
||||||
// Get the commit for the treePath itself
|
// Get the commit for the treePath itself
|
||||||
entryPaths[0] = ""
|
entryPaths[0] = ""
|
||||||
@ -71,22 +71,12 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
|
|||||||
commitsInfo[i].Commit = entryCommit
|
commitsInfo[i].Commit = entryCommit
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the entry is a submodule add a submodule file for this
|
// If the entry is a submodule, add a submodule file for this
|
||||||
if entry.IsSubModule() {
|
if entry.IsSubModule() {
|
||||||
subModuleURL := ""
|
commitsInfo[i].SubmoduleFile, err = getCommitInfoSubmoduleFile(repoLink, entry, commit, treePath)
|
||||||
var fullPath string
|
if err != nil {
|
||||||
if len(treePath) > 0 {
|
|
||||||
fullPath = treePath + "/" + entry.Name()
|
|
||||||
} else {
|
|
||||||
fullPath = entry.Name()
|
|
||||||
}
|
|
||||||
if subModule, err := commit.GetSubModule(fullPath); err != nil {
|
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
} else if subModule != nil {
|
|
||||||
subModuleURL = subModule.URL
|
|
||||||
}
|
}
|
||||||
subModuleFile := NewCommitSubmoduleFile(subModuleURL, entry.ID.String())
|
|
||||||
commitsInfo[i].SubmoduleFile = subModuleFile
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// GetCommitsInfo gets information of all commits that are corresponding to these entries
|
// GetCommitsInfo gets information of all commits that are corresponding to these entries
|
||||||
func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) {
|
func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) {
|
||||||
entryPaths := make([]string, len(tes)+1)
|
entryPaths := make([]string, len(tes)+1)
|
||||||
// Get the commit for the treePath itself
|
// Get the commit for the treePath itself
|
||||||
entryPaths[0] = ""
|
entryPaths[0] = ""
|
||||||
@ -65,22 +65,12 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
|
|||||||
log.Debug("missing commit for %s", entry.Name())
|
log.Debug("missing commit for %s", entry.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the entry is a submodule add a submodule file for this
|
// If the entry is a submodule, add a submodule file for this
|
||||||
if entry.IsSubModule() {
|
if entry.IsSubModule() {
|
||||||
subModuleURL := ""
|
commitsInfo[i].SubmoduleFile, err = getCommitInfoSubmoduleFile(repoLink, entry, commit, treePath)
|
||||||
var fullPath string
|
if err != nil {
|
||||||
if len(treePath) > 0 {
|
|
||||||
fullPath = treePath + "/" + entry.Name()
|
|
||||||
} else {
|
|
||||||
fullPath = entry.Name()
|
|
||||||
}
|
|
||||||
if subModule, err := commit.GetSubModule(fullPath); err != nil {
|
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
} else if subModule != nil {
|
|
||||||
subModuleURL = subModule.URL
|
|
||||||
}
|
}
|
||||||
subModuleFile := NewCommitSubmoduleFile(subModuleURL, entry.ID.String())
|
|
||||||
commitsInfo[i].SubmoduleFile = subModuleFile
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,7 +82,7 @@ func testGetCommitsInfo(t *testing.T, repo1 *Repository) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Context.TODO() - if graceful has started we should use its Shutdown context otherwise use install signals in TestMain.
|
// FIXME: Context.TODO() - if graceful has started we should use its Shutdown context otherwise use install signals in TestMain.
|
||||||
commitsInfo, treeCommit, err := entries.GetCommitsInfo(t.Context(), commit, testCase.Path)
|
commitsInfo, treeCommit, err := entries.GetCommitsInfo(t.Context(), "/any/repo-link", commit, testCase.Path)
|
||||||
assert.NoError(t, err, "Unable to get commit information for entries of subtree: %s in commit: %s from testcase due to error: %v", testCase.Path, testCase.CommitID, err)
|
assert.NoError(t, err, "Unable to get commit information for entries of subtree: %s in commit: %s from testcase due to error: %v", testCase.Path, testCase.CommitID, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
@ -159,7 +159,7 @@ func BenchmarkEntries_GetCommitsInfo(b *testing.B) {
|
|||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
b.Run(benchmark.name, func(b *testing.B) {
|
b.Run(benchmark.name, func(b *testing.B) {
|
||||||
for b.Loop() {
|
for b.Loop() {
|
||||||
_, _, err := entries.GetCommitsInfo(b.Context(), commit, "")
|
_, _, err := entries.GetCommitsInfo(b.Context(), "/any/repo-link", commit, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -6,10 +6,44 @@ package git
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
commitHeaderGpgsig = "gpgsig"
|
||||||
|
commitHeaderGpgsigSha256 = "gpgsig-sha256"
|
||||||
|
)
|
||||||
|
|
||||||
|
func assignCommitFields(gitRepo *Repository, commit *Commit, headerKey string, headerValue []byte) error {
|
||||||
|
if len(headerValue) > 0 && headerValue[len(headerValue)-1] == '\n' {
|
||||||
|
headerValue = headerValue[:len(headerValue)-1] // remove trailing newline
|
||||||
|
}
|
||||||
|
switch headerKey {
|
||||||
|
case "tree":
|
||||||
|
objID, err := NewIDFromString(string(headerValue))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid tree ID %q: %w", string(headerValue), err)
|
||||||
|
}
|
||||||
|
commit.Tree = *NewTree(gitRepo, objID)
|
||||||
|
case "parent":
|
||||||
|
objID, err := NewIDFromString(string(headerValue))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid parent ID %q: %w", string(headerValue), err)
|
||||||
|
}
|
||||||
|
commit.Parents = append(commit.Parents, objID)
|
||||||
|
case "author":
|
||||||
|
commit.Author.Decode(headerValue)
|
||||||
|
case "committer":
|
||||||
|
commit.Committer.Decode(headerValue)
|
||||||
|
case commitHeaderGpgsig, commitHeaderGpgsigSha256:
|
||||||
|
// if there are duplicate "gpgsig" and "gpgsig-sha256" headers, then the signature must have already been invalid
|
||||||
|
// so we don't need to handle duplicate headers here
|
||||||
|
commit.Signature = &CommitSignature{Signature: string(headerValue)}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// CommitFromReader will generate a Commit from a provided reader
|
// CommitFromReader will generate a Commit from a provided reader
|
||||||
// We need this to interpret commits from cat-file or cat-file --batch
|
// We need this to interpret commits from cat-file or cat-file --batch
|
||||||
//
|
//
|
||||||
@ -21,90 +55,46 @@ func CommitFromReader(gitRepo *Repository, objectID ObjectID, reader io.Reader)
|
|||||||
Committer: &Signature{},
|
Committer: &Signature{},
|
||||||
}
|
}
|
||||||
|
|
||||||
payloadSB := new(strings.Builder)
|
bufReader := bufio.NewReader(reader)
|
||||||
signatureSB := new(strings.Builder)
|
inHeader := true
|
||||||
messageSB := new(strings.Builder)
|
var payloadSB, messageSB bytes.Buffer
|
||||||
message := false
|
var headerKey string
|
||||||
pgpsig := false
|
var headerValue []byte
|
||||||
|
|
||||||
bufReader, ok := reader.(*bufio.Reader)
|
|
||||||
if !ok {
|
|
||||||
bufReader = bufio.NewReader(reader)
|
|
||||||
}
|
|
||||||
|
|
||||||
readLoop:
|
|
||||||
for {
|
for {
|
||||||
line, err := bufReader.ReadBytes('\n')
|
line, err := bufReader.ReadBytes('\n')
|
||||||
if err != nil {
|
if err != nil && err != io.EOF {
|
||||||
if err == io.EOF {
|
return nil, fmt.Errorf("unable to read commit %q: %w", objectID.String(), err)
|
||||||
if message {
|
}
|
||||||
_, _ = messageSB.Write(line)
|
if len(line) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if inHeader {
|
||||||
|
inHeader = !(len(line) == 1 && line[0] == '\n') // still in header if line is not just a newline
|
||||||
|
k, v, _ := bytes.Cut(line, []byte{' '})
|
||||||
|
if len(k) != 0 || !inHeader {
|
||||||
|
if headerKey != "" {
|
||||||
|
if err = assignCommitFields(gitRepo, commit, headerKey, headerValue); err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to parse commit %q: %w", objectID.String(), err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_, _ = payloadSB.Write(line)
|
headerKey = string(k) // it also resets the headerValue to empty string if not inHeader
|
||||||
break readLoop
|
headerValue = v
|
||||||
|
} else {
|
||||||
|
headerValue = append(headerValue, v...)
|
||||||
}
|
}
|
||||||
return nil, err
|
if headerKey != commitHeaderGpgsig && headerKey != commitHeaderGpgsigSha256 {
|
||||||
}
|
|
||||||
if pgpsig {
|
|
||||||
if len(line) > 0 && line[0] == ' ' {
|
|
||||||
_, _ = signatureSB.Write(line[1:])
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
pgpsig = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if !message {
|
|
||||||
// This is probably not correct but is copied from go-gits interpretation...
|
|
||||||
trimmed := bytes.TrimSpace(line)
|
|
||||||
if len(trimmed) == 0 {
|
|
||||||
message = true
|
|
||||||
_, _ = payloadSB.Write(line)
|
_, _ = payloadSB.Write(line)
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
split := bytes.SplitN(trimmed, []byte{' '}, 2)
|
|
||||||
var data []byte
|
|
||||||
if len(split) > 1 {
|
|
||||||
data = split[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
switch string(split[0]) {
|
|
||||||
case "tree":
|
|
||||||
commit.Tree = *NewTree(gitRepo, MustIDFromString(string(data)))
|
|
||||||
_, _ = payloadSB.Write(line)
|
|
||||||
case "parent":
|
|
||||||
commit.Parents = append(commit.Parents, MustIDFromString(string(data)))
|
|
||||||
_, _ = payloadSB.Write(line)
|
|
||||||
case "author":
|
|
||||||
commit.Author = &Signature{}
|
|
||||||
commit.Author.Decode(data)
|
|
||||||
_, _ = payloadSB.Write(line)
|
|
||||||
case "committer":
|
|
||||||
commit.Committer = &Signature{}
|
|
||||||
commit.Committer.Decode(data)
|
|
||||||
_, _ = payloadSB.Write(line)
|
|
||||||
case "encoding":
|
|
||||||
_, _ = payloadSB.Write(line)
|
|
||||||
case "gpgsig":
|
|
||||||
fallthrough
|
|
||||||
case "gpgsig-sha256": // FIXME: no intertop, so only 1 exists at present.
|
|
||||||
_, _ = signatureSB.Write(data)
|
|
||||||
_ = signatureSB.WriteByte('\n')
|
|
||||||
pgpsig = true
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_, _ = messageSB.Write(line)
|
_, _ = messageSB.Write(line)
|
||||||
_, _ = payloadSB.Write(line)
|
_, _ = payloadSB.Write(line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
commit.CommitMessage = messageSB.String()
|
|
||||||
commit.Signature = &CommitSignature{
|
|
||||||
Signature: signatureSB.String(),
|
|
||||||
Payload: payloadSB.String(),
|
|
||||||
}
|
|
||||||
if len(commit.Signature.Signature) == 0 {
|
|
||||||
commit.Signature = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
|
commit.CommitMessage = messageSB.String()
|
||||||
|
if commit.Signature != nil {
|
||||||
|
commit.Signature.Payload = payloadSB.String()
|
||||||
|
}
|
||||||
return commit, nil
|
return commit, nil
|
||||||
}
|
}
|
||||||
|
@ -60,8 +60,7 @@ func TestGetFullCommitIDErrorSha256(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCommitFromReaderSha256(t *testing.T) {
|
func TestCommitFromReaderSha256(t *testing.T) {
|
||||||
commitString := `9433b2a62b964c17a4485ae180f45f595d3e69d31b786087775e28c6b6399df0 commit 1114
|
commitString := `tree e7f9e96dd79c09b078cac8b303a7d3b9d65ff9b734e86060a4d20409fd379f9e
|
||||||
tree e7f9e96dd79c09b078cac8b303a7d3b9d65ff9b734e86060a4d20409fd379f9e
|
|
||||||
parent 26e9ccc29fad747e9c5d9f4c9ddeb7eff61cc45ef6a8dc258cbeb181afc055e8
|
parent 26e9ccc29fad747e9c5d9f4c9ddeb7eff61cc45ef6a8dc258cbeb181afc055e8
|
||||||
author Adam Majer <amajer@suse.de> 1698676906 +0100
|
author Adam Majer <amajer@suse.de> 1698676906 +0100
|
||||||
committer Adam Majer <amajer@suse.de> 1698676906 +0100
|
committer Adam Majer <amajer@suse.de> 1698676906 +0100
|
||||||
@ -112,8 +111,7 @@ VAEUo6ecdDxSpyt2naeg9pKus/BRi7P6g4B1hkk/zZstUX/QP4IQuAJbXjkvsC+X
|
|||||||
HKRr3NlRM/DygzTyj0gN74uoa0goCIbyAQhiT42nm0cuhM7uN/W0ayrlZjGF1cbR
|
HKRr3NlRM/DygzTyj0gN74uoa0goCIbyAQhiT42nm0cuhM7uN/W0ayrlZjGF1cbR
|
||||||
8NCJUL2Nwj0ywKIavC99Ipkb8AsFwpVT6U6effs6
|
8NCJUL2Nwj0ywKIavC99Ipkb8AsFwpVT6U6effs6
|
||||||
=xybZ
|
=xybZ
|
||||||
-----END PGP SIGNATURE-----
|
-----END PGP SIGNATURE-----`, commitFromReader.Signature.Signature)
|
||||||
`, commitFromReader.Signature.Signature)
|
|
||||||
assert.Equal(t, `tree e7f9e96dd79c09b078cac8b303a7d3b9d65ff9b734e86060a4d20409fd379f9e
|
assert.Equal(t, `tree e7f9e96dd79c09b078cac8b303a7d3b9d65ff9b734e86060a4d20409fd379f9e
|
||||||
parent 26e9ccc29fad747e9c5d9f4c9ddeb7eff61cc45ef6a8dc258cbeb181afc055e8
|
parent 26e9ccc29fad747e9c5d9f4c9ddeb7eff61cc45ef6a8dc258cbeb181afc055e8
|
||||||
author Adam Majer <amajer@suse.de> 1698676906 +0100
|
author Adam Majer <amajer@suse.de> 1698676906 +0100
|
||||||
|
@ -6,49 +6,61 @@ package git
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
giturl "code.gitea.io/gitea/modules/git/url"
|
giturl "code.gitea.io/gitea/modules/git/url"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CommitSubmoduleFile represents a file with submodule type.
|
// CommitSubmoduleFile represents a file with submodule type.
|
||||||
type CommitSubmoduleFile struct {
|
type CommitSubmoduleFile struct {
|
||||||
refURL string
|
repoLink string
|
||||||
parsedURL *giturl.RepositoryURL
|
fullPath string
|
||||||
parsed bool
|
refURL string
|
||||||
refID string
|
refID string
|
||||||
repoLink string
|
|
||||||
|
parsed bool
|
||||||
|
parsedTargetLink string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCommitSubmoduleFile create a new submodule file
|
// NewCommitSubmoduleFile create a new submodule file
|
||||||
func NewCommitSubmoduleFile(refURL, refID string) *CommitSubmoduleFile {
|
func NewCommitSubmoduleFile(repoLink, fullPath, refURL, refID string) *CommitSubmoduleFile {
|
||||||
return &CommitSubmoduleFile{refURL: refURL, refID: refID}
|
return &CommitSubmoduleFile{repoLink: repoLink, fullPath: fullPath, refURL: refURL, refID: refID}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sf *CommitSubmoduleFile) RefID() string {
|
func (sf *CommitSubmoduleFile) RefID() string {
|
||||||
return sf.refID // this function is only used in templates
|
return sf.refID
|
||||||
}
|
}
|
||||||
|
|
||||||
// SubmoduleWebLink tries to make some web links for a submodule, it also works on "nil" receiver
|
func (sf *CommitSubmoduleFile) getWebLinkInTargetRepo(ctx context.Context, moreLinkPath string) *SubmoduleWebLink {
|
||||||
func (sf *CommitSubmoduleFile) SubmoduleWebLink(ctx context.Context, optCommitID ...string) *SubmoduleWebLink {
|
|
||||||
if sf == nil {
|
if sf == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if strings.HasPrefix(sf.refURL, "../") {
|
||||||
|
targetLink := path.Join(sf.repoLink, path.Dir(sf.fullPath), sf.refURL)
|
||||||
|
return &SubmoduleWebLink{RepoWebLink: targetLink, CommitWebLink: targetLink + moreLinkPath}
|
||||||
|
}
|
||||||
if !sf.parsed {
|
if !sf.parsed {
|
||||||
sf.parsed = true
|
sf.parsed = true
|
||||||
parsedURL, err := giturl.ParseRepositoryURL(ctx, sf.refURL)
|
parsedURL, err := giturl.ParseRepositoryURL(ctx, sf.refURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
sf.parsedURL = parsedURL
|
sf.parsedTargetLink = giturl.MakeRepositoryWebLink(parsedURL)
|
||||||
sf.repoLink = giturl.MakeRepositoryWebLink(sf.parsedURL)
|
|
||||||
}
|
}
|
||||||
var commitLink string
|
return &SubmoduleWebLink{RepoWebLink: sf.parsedTargetLink, CommitWebLink: sf.parsedTargetLink + moreLinkPath}
|
||||||
if len(optCommitID) == 2 {
|
}
|
||||||
commitLink = sf.repoLink + "/compare/" + optCommitID[0] + "..." + optCommitID[1]
|
|
||||||
} else if len(optCommitID) == 1 {
|
// SubmoduleWebLinkTree tries to make the submodule's tree link in its own repo, it also works on "nil" receiver
|
||||||
commitLink = sf.repoLink + "/tree/" + optCommitID[0]
|
func (sf *CommitSubmoduleFile) SubmoduleWebLinkTree(ctx context.Context, optCommitID ...string) *SubmoduleWebLink {
|
||||||
} else {
|
if sf == nil {
|
||||||
commitLink = sf.repoLink + "/tree/" + sf.refID
|
return nil
|
||||||
}
|
}
|
||||||
return &SubmoduleWebLink{RepoWebLink: sf.repoLink, CommitWebLink: commitLink}
|
return sf.getWebLinkInTargetRepo(ctx, "/tree/"+util.OptionalArg(optCommitID, sf.refID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubmoduleWebLinkCompare tries to make the submodule's compare link in its own repo, it also works on "nil" receiver
|
||||||
|
func (sf *CommitSubmoduleFile) SubmoduleWebLinkCompare(ctx context.Context, commitID1, commitID2 string) *SubmoduleWebLink {
|
||||||
|
return sf.getWebLinkInTargetRepo(ctx, "/compare/"+commitID1+"..."+commitID2)
|
||||||
}
|
}
|
||||||
|
@ -10,20 +10,29 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestCommitSubmoduleLink(t *testing.T) {
|
func TestCommitSubmoduleLink(t *testing.T) {
|
||||||
sf := NewCommitSubmoduleFile("git@github.com:user/repo.git", "aaaa")
|
assert.Nil(t, (*CommitSubmoduleFile)(nil).SubmoduleWebLinkTree(t.Context()))
|
||||||
|
assert.Nil(t, (*CommitSubmoduleFile)(nil).SubmoduleWebLinkCompare(t.Context(), "", ""))
|
||||||
|
|
||||||
wl := sf.SubmoduleWebLink(t.Context())
|
t.Run("GitHubRepo", func(t *testing.T) {
|
||||||
assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink)
|
sf := NewCommitSubmoduleFile("/any/repo-link", "full-path", "git@github.com:user/repo.git", "aaaa")
|
||||||
assert.Equal(t, "https://github.com/user/repo/tree/aaaa", wl.CommitWebLink)
|
wl := sf.SubmoduleWebLinkTree(t.Context())
|
||||||
|
assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink)
|
||||||
|
assert.Equal(t, "https://github.com/user/repo/tree/aaaa", wl.CommitWebLink)
|
||||||
|
|
||||||
wl = sf.SubmoduleWebLink(t.Context(), "1111")
|
wl = sf.SubmoduleWebLinkCompare(t.Context(), "1111", "2222")
|
||||||
assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink)
|
assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink)
|
||||||
assert.Equal(t, "https://github.com/user/repo/tree/1111", wl.CommitWebLink)
|
assert.Equal(t, "https://github.com/user/repo/compare/1111...2222", wl.CommitWebLink)
|
||||||
|
})
|
||||||
|
|
||||||
wl = sf.SubmoduleWebLink(t.Context(), "1111", "2222")
|
t.Run("RelativePath", func(t *testing.T) {
|
||||||
assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink)
|
sf := NewCommitSubmoduleFile("/subpath/any/repo-home-link", "full-path", "../../user/repo", "aaaa")
|
||||||
assert.Equal(t, "https://github.com/user/repo/compare/1111...2222", wl.CommitWebLink)
|
wl := sf.SubmoduleWebLinkTree(t.Context())
|
||||||
|
assert.Equal(t, "/subpath/user/repo", wl.RepoWebLink)
|
||||||
|
assert.Equal(t, "/subpath/user/repo/tree/aaaa", wl.CommitWebLink)
|
||||||
|
|
||||||
wl = (*CommitSubmoduleFile)(nil).SubmoduleWebLink(t.Context())
|
sf = NewCommitSubmoduleFile("/subpath/any/repo-home-link", "dir/submodule", "../../../user/repo", "aaaa")
|
||||||
assert.Nil(t, wl)
|
wl = sf.SubmoduleWebLinkCompare(t.Context(), "1111", "2222")
|
||||||
|
assert.Equal(t, "/subpath/user/repo", wl.RepoWebLink)
|
||||||
|
assert.Equal(t, "/subpath/user/repo/compare/1111...2222", wl.CommitWebLink)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -59,8 +59,7 @@ func TestGetFullCommitIDError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCommitFromReader(t *testing.T) {
|
func TestCommitFromReader(t *testing.T) {
|
||||||
commitString := `feaf4ba6bc635fec442f46ddd4512416ec43c2c2 commit 1074
|
commitString := `tree f1a6cb52b2d16773290cefe49ad0684b50a4f930
|
||||||
tree f1a6cb52b2d16773290cefe49ad0684b50a4f930
|
|
||||||
parent 37991dec2c8e592043f47155ce4808d4580f9123
|
parent 37991dec2c8e592043f47155ce4808d4580f9123
|
||||||
author silverwind <me@silverwind.io> 1563741793 +0200
|
author silverwind <me@silverwind.io> 1563741793 +0200
|
||||||
committer silverwind <me@silverwind.io> 1563741793 +0200
|
committer silverwind <me@silverwind.io> 1563741793 +0200
|
||||||
@ -108,8 +107,7 @@ sD53z/f0J+We4VZjY+pidvA9BGZPFVdR3wd3xGs8/oH6UWaLJAMGkLG6dDb3qDLm
|
|||||||
mfeFhT57UbE4qukTDIQ0Y0WM40UYRTakRaDY7ubhXgLgx09Cnp9XTVMsHgT6j9/i
|
mfeFhT57UbE4qukTDIQ0Y0WM40UYRTakRaDY7ubhXgLgx09Cnp9XTVMsHgT6j9/i
|
||||||
1pxsB104XLWjQHTjr1JtiaBQEwFh9r2OKTcpvaLcbNtYpo7CzOs=
|
1pxsB104XLWjQHTjr1JtiaBQEwFh9r2OKTcpvaLcbNtYpo7CzOs=
|
||||||
=FRsO
|
=FRsO
|
||||||
-----END PGP SIGNATURE-----
|
-----END PGP SIGNATURE-----`, commitFromReader.Signature.Signature)
|
||||||
`, commitFromReader.Signature.Signature)
|
|
||||||
assert.Equal(t, `tree f1a6cb52b2d16773290cefe49ad0684b50a4f930
|
assert.Equal(t, `tree f1a6cb52b2d16773290cefe49ad0684b50a4f930
|
||||||
parent 37991dec2c8e592043f47155ce4808d4580f9123
|
parent 37991dec2c8e592043f47155ce4808d4580f9123
|
||||||
author silverwind <me@silverwind.io> 1563741793 +0200
|
author silverwind <me@silverwind.io> 1563741793 +0200
|
||||||
@ -126,8 +124,7 @@ empty commit`, commitFromReader.Signature.Payload)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCommitWithEncodingFromReader(t *testing.T) {
|
func TestCommitWithEncodingFromReader(t *testing.T) {
|
||||||
commitString := `feaf4ba6bc635fec442f46ddd4512416ec43c2c2 commit 1074
|
commitString := `tree ca3fad42080dd1a6d291b75acdfc46e5b9b307e5
|
||||||
tree ca3fad42080dd1a6d291b75acdfc46e5b9b307e5
|
|
||||||
parent 47b24e7ab977ed31c5a39989d570847d6d0052af
|
parent 47b24e7ab977ed31c5a39989d570847d6d0052af
|
||||||
author KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100
|
author KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100
|
||||||
committer KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100
|
committer KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100
|
||||||
@ -172,8 +169,7 @@ SONRzusmu5n3DgV956REL7x62h7JuqmBz/12HZkr0z0zgXkcZ04q08pSJATX5N1F
|
|||||||
yN+tWxTsWg+zhDk96d5Esdo9JMjcFvPv0eioo30GAERaz1hoD7zCMT4jgUFTQwgz
|
yN+tWxTsWg+zhDk96d5Esdo9JMjcFvPv0eioo30GAERaz1hoD7zCMT4jgUFTQwgz
|
||||||
jw4YcO5u
|
jw4YcO5u
|
||||||
=r3UU
|
=r3UU
|
||||||
-----END PGP SIGNATURE-----
|
-----END PGP SIGNATURE-----`, commitFromReader.Signature.Signature)
|
||||||
`, commitFromReader.Signature.Signature)
|
|
||||||
assert.Equal(t, `tree ca3fad42080dd1a6d291b75acdfc46e5b9b307e5
|
assert.Equal(t, `tree ca3fad42080dd1a6d291b75acdfc46e5b9b307e5
|
||||||
parent 47b24e7ab977ed31c5a39989d570847d6d0052af
|
parent 47b24e7ab977ed31c5a39989d570847d6d0052af
|
||||||
author KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100
|
author KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100
|
||||||
|
@ -99,9 +99,9 @@ func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diff
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseDiffHunkString parse the diffhunk content and return
|
// ParseDiffHunkString parse the diff hunk content and return
|
||||||
func ParseDiffHunkString(diffhunk string) (leftLine, leftHunk, rightLine, righHunk int) {
|
func ParseDiffHunkString(diffHunk string) (leftLine, leftHunk, rightLine, rightHunk int) {
|
||||||
ss := strings.Split(diffhunk, "@@")
|
ss := strings.Split(diffHunk, "@@")
|
||||||
ranges := strings.Split(ss[1][1:], " ")
|
ranges := strings.Split(ss[1][1:], " ")
|
||||||
leftRange := strings.Split(ranges[0], ",")
|
leftRange := strings.Split(ranges[0], ",")
|
||||||
leftLine, _ = strconv.Atoi(leftRange[0][1:])
|
leftLine, _ = strconv.Atoi(leftRange[0][1:])
|
||||||
@ -112,14 +112,19 @@ func ParseDiffHunkString(diffhunk string) (leftLine, leftHunk, rightLine, righHu
|
|||||||
rightRange := strings.Split(ranges[1], ",")
|
rightRange := strings.Split(ranges[1], ",")
|
||||||
rightLine, _ = strconv.Atoi(rightRange[0])
|
rightLine, _ = strconv.Atoi(rightRange[0])
|
||||||
if len(rightRange) > 1 {
|
if len(rightRange) > 1 {
|
||||||
righHunk, _ = strconv.Atoi(rightRange[1])
|
rightHunk, _ = strconv.Atoi(rightRange[1])
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Debug("Parse line number failed: %v", diffhunk)
|
log.Debug("Parse line number failed: %v", diffHunk)
|
||||||
rightLine = leftLine
|
rightLine = leftLine
|
||||||
righHunk = leftHunk
|
rightHunk = leftHunk
|
||||||
}
|
}
|
||||||
return leftLine, leftHunk, rightLine, righHunk
|
if rightLine == 0 {
|
||||||
|
// FIXME: GIT-DIFF-CUT-BUG search this tag to see details
|
||||||
|
// this is only a hacky patch, the rightLine&rightHunk might still be incorrect in some cases.
|
||||||
|
rightLine++
|
||||||
|
}
|
||||||
|
return leftLine, leftHunk, rightLine, rightHunk
|
||||||
}
|
}
|
||||||
|
|
||||||
// Example: @@ -1,8 +1,9 @@ => [..., 1, 8, 1, 9]
|
// Example: @@ -1,8 +1,9 @@ => [..., 1, 8, 1, 9]
|
||||||
@ -270,6 +275,12 @@ func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLi
|
|||||||
oldNumOfLines++
|
oldNumOfLines++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// "git diff" outputs "@@ -1 +1,3 @@" for "OLD" => "A\nB\nC"
|
||||||
|
// FIXME: GIT-DIFF-CUT-BUG But there is a bug in CutDiffAroundLine, then the "Patch" stored in the comment model becomes "@@ -1,1 +0,4 @@"
|
||||||
|
// It may generate incorrect results for difference cases, for example: delete 2 line add 1 line, delete 2 line add 2 line etc, need to double check.
|
||||||
|
// For example: "L1\nL2" => "A\nB", then the patch shows "L2" as line 1 on the left (deleted part)
|
||||||
|
|
||||||
// construct the new hunk header
|
// construct the new hunk header
|
||||||
newHunk[headerLines] = fmt.Sprintf("@@ -%d,%d +%d,%d @@",
|
newHunk[headerLines] = fmt.Sprintf("@@ -%d,%d +%d,%d @@",
|
||||||
oldBegin, oldNumOfLines, newBegin, newNumOfLines)
|
oldBegin, oldNumOfLines, newBegin, newNumOfLines)
|
||||||
|
@ -97,17 +97,17 @@ func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64,
|
|||||||
}
|
}
|
||||||
|
|
||||||
isVendored := optional.None[bool]()
|
isVendored := optional.None[bool]()
|
||||||
isGenerated := optional.None[bool]()
|
|
||||||
isDocumentation := optional.None[bool]()
|
isDocumentation := optional.None[bool]()
|
||||||
isDetectable := optional.None[bool]()
|
isDetectable := optional.None[bool]()
|
||||||
|
|
||||||
attrs, err := checker.CheckPath(f.Name())
|
attrs, err := checker.CheckPath(f.Name())
|
||||||
|
attrLinguistGenerated := optional.None[bool]()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if isVendored = attrs.GetVendored(); isVendored.ValueOrDefault(false) {
|
if isVendored = attrs.GetVendored(); isVendored.ValueOrDefault(false) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if isGenerated = attrs.GetGenerated(); isGenerated.ValueOrDefault(false) {
|
if attrLinguistGenerated = attrs.GetGenerated(); attrLinguistGenerated.ValueOrDefault(false) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,7 +169,15 @@ func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !isGenerated.Has() && enry.IsGenerated(f.Name(), content) {
|
|
||||||
|
// if "generated" attribute is set, use it, otherwise use enry.IsGenerated to guess
|
||||||
|
var isGenerated bool
|
||||||
|
if attrLinguistGenerated.Has() {
|
||||||
|
isGenerated = attrLinguistGenerated.Value()
|
||||||
|
} else {
|
||||||
|
isGenerated = enry.IsGenerated(f.Name(), content)
|
||||||
|
}
|
||||||
|
if isGenerated {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,6 +30,31 @@ func (e EntryMode) String() string {
|
|||||||
return strconv.FormatInt(int64(e), 8)
|
return strconv.FormatInt(int64(e), 8)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsSubModule if the entry is a sub module
|
||||||
|
func (e EntryMode) IsSubModule() bool {
|
||||||
|
return e == EntryModeCommit
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDir if the entry is a sub dir
|
||||||
|
func (e EntryMode) IsDir() bool {
|
||||||
|
return e == EntryModeTree
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsLink if the entry is a symlink
|
||||||
|
func (e EntryMode) IsLink() bool {
|
||||||
|
return e == EntryModeSymlink
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRegular if the entry is a regular file
|
||||||
|
func (e EntryMode) IsRegular() bool {
|
||||||
|
return e == EntryModeBlob
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsExecutable if the entry is an executable file (not necessarily binary)
|
||||||
|
func (e EntryMode) IsExecutable() bool {
|
||||||
|
return e == EntryModeExec
|
||||||
|
}
|
||||||
|
|
||||||
func ParseEntryMode(mode string) (EntryMode, error) {
|
func ParseEntryMode(mode string) (EntryMode, error) {
|
||||||
switch mode {
|
switch mode {
|
||||||
case "000000":
|
case "000000":
|
||||||
|
@ -59,27 +59,27 @@ func (te *TreeEntry) Size() int64 {
|
|||||||
|
|
||||||
// IsSubModule if the entry is a sub module
|
// IsSubModule if the entry is a sub module
|
||||||
func (te *TreeEntry) IsSubModule() bool {
|
func (te *TreeEntry) IsSubModule() bool {
|
||||||
return te.entryMode == EntryModeCommit
|
return te.entryMode.IsSubModule()
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsDir if the entry is a sub dir
|
// IsDir if the entry is a sub dir
|
||||||
func (te *TreeEntry) IsDir() bool {
|
func (te *TreeEntry) IsDir() bool {
|
||||||
return te.entryMode == EntryModeTree
|
return te.entryMode.IsDir()
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsLink if the entry is a symlink
|
// IsLink if the entry is a symlink
|
||||||
func (te *TreeEntry) IsLink() bool {
|
func (te *TreeEntry) IsLink() bool {
|
||||||
return te.entryMode == EntryModeSymlink
|
return te.entryMode.IsLink()
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsRegular if the entry is a regular file
|
// IsRegular if the entry is a regular file
|
||||||
func (te *TreeEntry) IsRegular() bool {
|
func (te *TreeEntry) IsRegular() bool {
|
||||||
return te.entryMode == EntryModeBlob
|
return te.entryMode.IsRegular()
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsExecutable if the entry is an executable file (not necessarily binary)
|
// IsExecutable if the entry is an executable file (not necessarily binary)
|
||||||
func (te *TreeEntry) IsExecutable() bool {
|
func (te *TreeEntry) IsExecutable() bool {
|
||||||
return te.entryMode == EntryModeExec
|
return te.entryMode.IsExecutable()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blob returns the blob object the entry
|
// Blob returns the blob object the entry
|
||||||
|
@ -132,6 +132,7 @@ func newInternalRequestLFS(ctx context.Context, internalURL, method string, head
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
req := private.NewInternalRequest(ctx, internalURL, method)
|
req := private.NewInternalRequest(ctx, internalURL, method)
|
||||||
|
req.SetReadWriteTimeout(0)
|
||||||
for k, v := range headers {
|
for k, v := range headers {
|
||||||
req.Header(k, v)
|
req.Header(k, v)
|
||||||
}
|
}
|
||||||
|
@ -409,9 +409,9 @@ func (r *FootnoteHTMLRenderer) renderFootnoteLink(w util.BufWriter, source []byt
|
|||||||
_, _ = w.Write(n.Name)
|
_, _ = w.Write(n.Name)
|
||||||
_, _ = w.WriteString(`"><a href="#fn:`)
|
_, _ = w.WriteString(`"><a href="#fn:`)
|
||||||
_, _ = w.Write(n.Name)
|
_, _ = w.Write(n.Name)
|
||||||
_, _ = w.WriteString(`" class="footnote-ref" role="doc-noteref">`)
|
_, _ = w.WriteString(`" class="footnote-ref" role="doc-noteref">`) // FIXME: here and below, need to keep the classes
|
||||||
_, _ = w.WriteString(is)
|
_, _ = w.WriteString(is)
|
||||||
_, _ = w.WriteString(`</a></sup>`)
|
_, _ = w.WriteString(` </a></sup>`) // the style doesn't work at the moment, so add a space to separate the names
|
||||||
}
|
}
|
||||||
return ast.WalkContinue, nil
|
return ast.WalkContinue, nil
|
||||||
}
|
}
|
||||||
|
@ -86,8 +86,8 @@ var globalVars = sync.OnceValue(func() *globalVarsType {
|
|||||||
// codePreviewPattern matches "http://domain/.../{owner}/{repo}/src/commit/{commit}/{filepath}#L10-L20"
|
// codePreviewPattern matches "http://domain/.../{owner}/{repo}/src/commit/{commit}/{filepath}#L10-L20"
|
||||||
v.codePreviewPattern = regexp.MustCompile(`https?://\S+/([^\s/]+)/([^\s/]+)/src/commit/([0-9a-f]{7,64})(/\S+)#(L\d+(-L\d+)?)`)
|
v.codePreviewPattern = regexp.MustCompile(`https?://\S+/([^\s/]+)/([^\s/]+)/src/commit/([0-9a-f]{7,64})(/\S+)#(L\d+(-L\d+)?)`)
|
||||||
|
|
||||||
// cleans: "<foo/bar", "<any words/", ("<html", "<head", "<script", "<style")
|
// cleans: "<foo/bar", "<any words/", ("<html", "<head", "<script", "<style", "<?", "<%")
|
||||||
v.tagCleaner = regexp.MustCompile(`(?i)<(/?\w+/\w+|/[\w ]+/|/?(html|head|script|style\b))`)
|
v.tagCleaner = regexp.MustCompile(`(?i)<(/?\w+/\w+|/[\w ]+/|/?(html|head|script|style|%|\?)\b)`)
|
||||||
v.nulCleaner = strings.NewReplacer("\000", "")
|
v.nulCleaner = strings.NewReplacer("\000", "")
|
||||||
return v
|
return v
|
||||||
})
|
})
|
||||||
@ -253,7 +253,7 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output
|
|||||||
node, err := html.Parse(io.MultiReader(
|
node, err := html.Parse(io.MultiReader(
|
||||||
// prepend "<html><body>"
|
// prepend "<html><body>"
|
||||||
strings.NewReader("<html><body>"),
|
strings.NewReader("<html><body>"),
|
||||||
// Strip out nuls - they're always invalid
|
// strip out NULLs (they're always invalid), and escape known tags
|
||||||
bytes.NewReader(globalVars().tagCleaner.ReplaceAll([]byte(globalVars().nulCleaner.Replace(string(rawHTML))), []byte("<$1"))),
|
bytes.NewReader(globalVars().tagCleaner.ReplaceAll([]byte(globalVars().nulCleaner.Replace(string(rawHTML))), []byte("<$1"))),
|
||||||
// close the tags
|
// close the tags
|
||||||
strings.NewReader("</body></html>"),
|
strings.NewReader("</body></html>"),
|
||||||
@ -320,6 +320,7 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Nod
|
|||||||
}
|
}
|
||||||
|
|
||||||
processNodeAttrID(node)
|
processNodeAttrID(node)
|
||||||
|
processFootnoteNode(ctx, node) // FIXME: the footnote processing should be done in the "footnote.go" renderer directly
|
||||||
|
|
||||||
if isEmojiNode(node) {
|
if isEmojiNode(node) {
|
||||||
// TextNode emoji will be converted to `<span class="emoji">`, then the next iteration will visit the "span"
|
// TextNode emoji will be converted to `<span class="emoji">`, then the next iteration will visit the "span"
|
||||||
|
@ -30,6 +30,7 @@ func TestRender_IssueList(t *testing.T) {
|
|||||||
rctx := markup.NewTestRenderContext(markup.TestAppURL, map[string]string{
|
rctx := markup.NewTestRenderContext(markup.TestAppURL, map[string]string{
|
||||||
"user": "test-user", "repo": "test-repo",
|
"user": "test-user", "repo": "test-repo",
|
||||||
"markupAllowShortIssuePattern": "true",
|
"markupAllowShortIssuePattern": "true",
|
||||||
|
"footnoteContextId": "12345",
|
||||||
})
|
})
|
||||||
out, err := markdown.RenderString(rctx, input)
|
out, err := markdown.RenderString(rctx, input)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -69,4 +70,22 @@ func TestRender_IssueList(t *testing.T) {
|
|||||||
</ul>`,
|
</ul>`,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("IssueFootnote", func(t *testing.T) {
|
||||||
|
test(
|
||||||
|
"foo[^1][^2]\n\n[^1]: bar\n[^2]: baz",
|
||||||
|
`<p>foo<sup id="fnref:user-content-1-12345"><a href="#fn:user-content-1-12345" rel="nofollow">1 </a></sup><sup id="fnref:user-content-2-12345"><a href="#fn:user-content-2-12345" rel="nofollow">2 </a></sup></p>
|
||||||
|
<div>
|
||||||
|
<hr/>
|
||||||
|
<ol>
|
||||||
|
<li id="fn:user-content-1-12345">
|
||||||
|
<p>bar <a href="#fnref:user-content-1-12345" rel="nofollow">↩︎</a></p>
|
||||||
|
</li>
|
||||||
|
<li id="fn:user-content-2-12345">
|
||||||
|
<p>baz <a href="#fnref:user-content-2-12345" rel="nofollow">↩︎</a></p>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>`,
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,14 @@ func isAnchorIDUserContent(s string) bool {
|
|||||||
return strings.HasPrefix(s, "user-content-") || strings.Contains(s, ":user-content-")
|
return strings.HasPrefix(s, "user-content-") || strings.Contains(s, ":user-content-")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isAnchorIDFootnote(s string) bool {
|
||||||
|
return strings.HasPrefix(s, "fnref:user-content-") || strings.HasPrefix(s, "fn:user-content-")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAnchorHrefFootnote(s string) bool {
|
||||||
|
return strings.HasPrefix(s, "#fnref:user-content-") || strings.HasPrefix(s, "#fn:user-content-")
|
||||||
|
}
|
||||||
|
|
||||||
func processNodeAttrID(node *html.Node) {
|
func processNodeAttrID(node *html.Node) {
|
||||||
// Add user-content- to IDs and "#" links if they don't already have them,
|
// Add user-content- to IDs and "#" links if they don't already have them,
|
||||||
// and convert the link href to a relative link to the host root
|
// and convert the link href to a relative link to the host root
|
||||||
@ -27,6 +35,18 @@ func processNodeAttrID(node *html.Node) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func processFootnoteNode(ctx *RenderContext, node *html.Node) {
|
||||||
|
for idx, attr := range node.Attr {
|
||||||
|
if (attr.Key == "id" && isAnchorIDFootnote(attr.Val)) ||
|
||||||
|
(attr.Key == "href" && isAnchorHrefFootnote(attr.Val)) {
|
||||||
|
if footnoteContextID := ctx.RenderOptions.Metas["footnoteContextId"]; footnoteContextID != "" {
|
||||||
|
node.Attr[idx].Val = attr.Val + "-" + footnoteContextID
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func processNodeA(ctx *RenderContext, node *html.Node) {
|
func processNodeA(ctx *RenderContext, node *html.Node) {
|
||||||
for idx, attr := range node.Attr {
|
for idx, attr := range node.Attr {
|
||||||
if attr.Key == "href" {
|
if attr.Key == "href" {
|
||||||
|
@ -525,6 +525,10 @@ func TestPostProcess(t *testing.T) {
|
|||||||
test("<script>a</script>", `<script>a</script>`)
|
test("<script>a</script>", `<script>a</script>`)
|
||||||
test("<STYLE>a", `<STYLE>a`)
|
test("<STYLE>a", `<STYLE>a`)
|
||||||
test("<style>a</STYLE>", `<style>a</STYLE>`)
|
test("<style>a</STYLE>", `<style>a</STYLE>`)
|
||||||
|
|
||||||
|
// other special tags, our special behavior
|
||||||
|
test("<?php\nfoo", "<?php\nfoo")
|
||||||
|
test("<%asp\nfoo", "<%asp\nfoo")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIssue16020(t *testing.T) {
|
func TestIssue16020(t *testing.T) {
|
||||||
|
@ -223,7 +223,7 @@ This PR has been generated by [Renovate Bot](https://github.com/renovatebot/reno
|
|||||||
<dd>This is another definition of the second term.</dd>
|
<dd>This is another definition of the second term.</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<h3 id="user-content-footnotes">Footnotes</h3>
|
<h3 id="user-content-footnotes">Footnotes</h3>
|
||||||
<p>Here is a simple footnote,<sup id="fnref:user-content-1"><a href="#fn:user-content-1" rel="nofollow">1</a></sup> and here is a longer one.<sup id="fnref:user-content-bignote"><a href="#fn:user-content-bignote" rel="nofollow">2</a></sup></p>
|
<p>Here is a simple footnote,<sup id="fnref:user-content-1"><a href="#fn:user-content-1" rel="nofollow">1 </a></sup> and here is a longer one.<sup id="fnref:user-content-bignote"><a href="#fn:user-content-bignote" rel="nofollow">2 </a></sup></p>
|
||||||
<div>
|
<div>
|
||||||
<hr/>
|
<hr/>
|
||||||
<ol>
|
<ol>
|
||||||
|
@ -22,6 +22,13 @@ func FromPtr[T any](v *T) Option[T] {
|
|||||||
return Some(*v)
|
return Some(*v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FromMapLookup[K comparable, V any](m map[K]V, k K) Option[V] {
|
||||||
|
if v, ok := m[k]; ok {
|
||||||
|
return Some(v)
|
||||||
|
}
|
||||||
|
return None[V]()
|
||||||
|
}
|
||||||
|
|
||||||
func FromNonDefault[T comparable](v T) Option[T] {
|
func FromNonDefault[T comparable](v T) Option[T] {
|
||||||
var zero T
|
var zero T
|
||||||
if v == zero {
|
if v == zero {
|
||||||
|
@ -56,6 +56,12 @@ func TestOption(t *testing.T) {
|
|||||||
opt3 := optional.FromNonDefault(1)
|
opt3 := optional.FromNonDefault(1)
|
||||||
assert.True(t, opt3.Has())
|
assert.True(t, opt3.Has())
|
||||||
assert.Equal(t, int(1), opt3.Value())
|
assert.Equal(t, int(1), opt3.Value())
|
||||||
|
|
||||||
|
opt4 := optional.FromMapLookup(map[string]int{"a": 1}, "a")
|
||||||
|
assert.True(t, opt4.Has())
|
||||||
|
assert.Equal(t, 1, opt4.Value())
|
||||||
|
opt4 = optional.FromMapLookup(map[string]int{"a": 1}, "b")
|
||||||
|
assert.False(t, opt4.Has())
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_ParseBool(t *testing.T) {
|
func Test_ParseBool(t *testing.T) {
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
@ -83,7 +84,8 @@ func ParseImageConfig(mt string, r io.Reader) (*Metadata, error) {
|
|||||||
|
|
||||||
func parseOCIImageConfig(r io.Reader) (*Metadata, error) {
|
func parseOCIImageConfig(r io.Reader) (*Metadata, error) {
|
||||||
var image oci.Image
|
var image oci.Image
|
||||||
if err := json.NewDecoder(r).Decode(&image); err != nil {
|
// EOF means empty input, still use the default data
|
||||||
|
if err := json.NewDecoder(r).Decode(&image); err != nil && !errors.Is(err, io.EOF) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
oci "github.com/opencontainers/image-spec/specs-go/v1"
|
oci "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseImageConfig(t *testing.T) {
|
func TestParseImageConfig(t *testing.T) {
|
||||||
@ -59,3 +60,9 @@ func TestParseImageConfig(t *testing.T) {
|
|||||||
assert.Equal(t, projectURL, metadata.ProjectURL)
|
assert.Equal(t, projectURL, metadata.ProjectURL)
|
||||||
assert.Equal(t, repositoryURL, metadata.RepositoryURL)
|
assert.Equal(t, repositoryURL, metadata.RepositoryURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseOCIImageConfig(t *testing.T) {
|
||||||
|
metadata, err := parseOCIImageConfig(strings.NewReader(""))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, &Metadata{Type: TypeOCI, Platform: DefaultPlatform, ImageLayers: []string{}}, metadata)
|
||||||
|
}
|
||||||
|
@ -46,18 +46,16 @@ type ServCommandResults struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ServCommand preps for a serv call
|
// ServCommand preps for a serv call
|
||||||
func ServCommand(ctx context.Context, keyID int64, ownerName, repoName string, mode perm.AccessMode, verbs ...string) (*ServCommandResults, ResponseExtra) {
|
func ServCommand(ctx context.Context, keyID int64, ownerName, repoName string, mode perm.AccessMode, verb, lfsVerb string) (*ServCommandResults, ResponseExtra) {
|
||||||
reqURL := setting.LocalURL + fmt.Sprintf("api/internal/serv/command/%d/%s/%s?mode=%d",
|
reqURL := setting.LocalURL + fmt.Sprintf("api/internal/serv/command/%d/%s/%s?mode=%d",
|
||||||
keyID,
|
keyID,
|
||||||
url.PathEscape(ownerName),
|
url.PathEscape(ownerName),
|
||||||
url.PathEscape(repoName),
|
url.PathEscape(repoName),
|
||||||
mode,
|
mode,
|
||||||
)
|
)
|
||||||
for _, verb := range verbs {
|
reqURL += "&verb=" + url.QueryEscape(verb)
|
||||||
if verb != "" {
|
// reqURL += "&lfs_verb=" + url.QueryEscape(lfsVerb) // TODO: actually there is no use of this parameter. In the future, the URL construction should be more flexible
|
||||||
reqURL += "&verb=" + url.QueryEscape(verb)
|
_ = lfsVerb
|
||||||
}
|
|
||||||
}
|
|
||||||
req := newInternalRequestAPI(ctx, reqURL, "GET")
|
req := newInternalRequestAPI(ctx, reqURL, "GET")
|
||||||
return requestJSONResp(req, &ServCommandResults{})
|
return requestJSONResp(req, &ServCommandResults{})
|
||||||
}
|
}
|
||||||
|
@ -41,11 +41,14 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("GetObjectFormat: %w", err)
|
return 0, fmt.Errorf("GetObjectFormat: %w", err)
|
||||||
}
|
}
|
||||||
_, err = db.GetEngine(ctx).ID(repo.ID).Update(&repo_model.Repository{ObjectFormatName: objFmt.Name()})
|
|
||||||
if err != nil {
|
if repo.ObjectFormatName != objFmt.Name() {
|
||||||
return 0, fmt.Errorf("UpdateRepository: %w", err)
|
repo.ObjectFormatName = objFmt.Name()
|
||||||
|
_, err = db.GetEngine(ctx).ID(repo.ID).NoAutoTime().Update(&repo_model.Repository{ObjectFormatName: objFmt.Name()})
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("UpdateRepository: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
repo.ObjectFormatName = objFmt.Name() // keep consistent with db
|
|
||||||
|
|
||||||
allBranches := container.Set[string]{}
|
allBranches := container.Set[string]{}
|
||||||
{
|
{
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user