{{ctx.Locale.Tr "admin.auths.delete_auth_desc"}}
-diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index ab30e1789dd..104f1cfeeda 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -4,10 +4,10 @@
"features": {
// installs nodejs into container
"ghcr.io/devcontainers/features/node:1": {
- "version": "20"
+ "version": "lts"
},
"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": {
"version": "3.12"
},
diff --git a/.dockerignore b/.dockerignore
index 94aca6b8d30..843f12a7be8 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -36,15 +36,6 @@ _testmain.go
coverage.all
cpu.out
-/modules/migration/bindata.go
-/modules/migration/bindata.go.hash
-/modules/options/bindata.go
-/modules/options/bindata.go.hash
-/modules/public/bindata.go
-/modules/public/bindata.go.hash
-/modules/templates/bindata.go
-/modules/templates/bindata.go.hash
-
*.db
*.log
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index f9e10502408..57c6b19600b 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -91,6 +91,7 @@ module.exports = {
plugins: ['@vitest/eslint-plugin'],
globals: vitestPlugin.environments.env.globals,
rules: {
+ 'github/unescaped-html-literal': [0],
'@vitest/consistent-test-filename': [0],
'@vitest/consistent-test-it': [0],
'@vitest/expect-expect': [0],
@@ -423,7 +424,7 @@ module.exports = {
'github/no-useless-passive': [2],
'github/prefer-observers': [2],
'github/require-passive-events': [2],
- 'github/unescaped-html-literal': [0],
+ 'github/unescaped-html-literal': [2],
'grouped-accessor-pairs': [2],
'guard-for-in': [0],
'id-blacklist': [0],
diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml
index 64090d64905..f6720bf2f6d 100644
--- a/.github/workflows/pull-compliance.yml
+++ b/.github/workflows/pull-compliance.yml
@@ -37,7 +37,7 @@ jobs:
python-version: "3.12"
- uses: actions/setup-node@v4
with:
- node-version: 22
+ node-version: 24
cache: npm
cache-dependency-path: package-lock.json
- run: pip install poetry
@@ -66,7 +66,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
- node-version: 22
+ node-version: 24
cache: npm
cache-dependency-path: package-lock.json
- run: make deps-frontend
@@ -137,7 +137,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
- node-version: 22
+ node-version: 24
cache: npm
cache-dependency-path: package-lock.json
- run: make deps-frontend
@@ -186,7 +186,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
- node-version: 22
+ node-version: 24
cache: npm
cache-dependency-path: package-lock.json
- run: make deps-frontend
diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml
index a3fd8ca621e..55c2d2bf5e3 100644
--- a/.github/workflows/pull-db-tests.yml
+++ b/.github/workflows/pull-db-tests.yml
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
services:
pgsql:
- image: postgres:12
+ image: postgres:14
env:
POSTGRES_DB: test
POSTGRES_PASSWORD: postgres
diff --git a/.github/workflows/pull-e2e-tests.yml b/.github/workflows/pull-e2e-tests.yml
index 87e931117c1..cc3fbd9c341 100644
--- a/.github/workflows/pull-e2e-tests.yml
+++ b/.github/workflows/pull-e2e-tests.yml
@@ -25,7 +25,7 @@ jobs:
check-latest: true
- uses: actions/setup-node@v4
with:
- node-version: 22
+ node-version: 24
cache: npm
cache-dependency-path: package-lock.json
- run: make deps-frontend frontend deps-backend
diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml
index 2558a16a716..c2cc14f771c 100644
--- a/.github/workflows/release-nightly.yml
+++ b/.github/workflows/release-nightly.yml
@@ -22,7 +22,7 @@ jobs:
check-latest: true
- uses: actions/setup-node@v4
with:
- node-version: 22
+ node-version: 24
cache: npm
cache-dependency-path: package-lock.json
- run: make deps-frontend deps-backend
diff --git a/.github/workflows/release-tag-rc.yml b/.github/workflows/release-tag-rc.yml
index 37b3ff57d2f..c9c15c31a0d 100644
--- a/.github/workflows/release-tag-rc.yml
+++ b/.github/workflows/release-tag-rc.yml
@@ -23,7 +23,7 @@ jobs:
check-latest: true
- uses: actions/setup-node@v4
with:
- node-version: 22
+ node-version: 24
cache: npm
cache-dependency-path: package-lock.json
- run: make deps-frontend deps-backend
diff --git a/.github/workflows/release-tag-version.yml b/.github/workflows/release-tag-version.yml
index 4250623da0f..ae717c7cecc 100644
--- a/.github/workflows/release-tag-version.yml
+++ b/.github/workflows/release-tag-version.yml
@@ -27,7 +27,7 @@ jobs:
check-latest: true
- uses: actions/setup-node@v4
with:
- node-version: 22
+ node-version: 24
cache: npm
cache-dependency-path: package-lock.json
- run: make deps-frontend deps-backend
diff --git a/.gitignore b/.gitignore
index 272ea2b5ed7..0791a17c710 100644
--- a/.gitignore
+++ b/.gitignore
@@ -42,14 +42,10 @@ _testmain.go
coverage.all
cpu.out
-/modules/migration/bindata.go
-/modules/migration/bindata.go.hash
-/modules/options/bindata.go
-/modules/options/bindata.go.hash
-/modules/public/bindata.go
-/modules/public/bindata.go.hash
-/modules/templates/bindata.go
-/modules/templates/bindata.go.hash
+/modules/migration/bindata.*
+/modules/options/bindata.*
+/modules/public/bindata.*
+/modules/templates/bindata.*
*.db
*.log
diff --git a/.golangci.yml b/.golangci.yml
index c176d2115cc..2ad39fbae2c 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -45,7 +45,13 @@ linters:
desc: do not use the ini package, use gitea's config system instead
- pkg: gitea.com/go-chi/cache
desc: do not use the go-chi cache package, use gitea's cache system
+ nolintlint:
+ allow-unused: false
+ require-explanation: true
+ require-specific: true
gocritic:
+ enabled-checks:
+ - equalFold
disabled-checks:
- ifElseChain
- singleCaseSwitch # Every time this occurred in the code, there was no other way.
@@ -83,6 +89,10 @@ linters:
- name: unreachable-code
- name: var-declaration
- name: var-naming
+ arguments:
+ - [] # AllowList - do not remove as args for the rule are positional and won't work without lists first
+ - [] # DenyList
+ - - skip-package-name-checks: true # supress errors from underscore in migration packages
staticcheck:
checks:
- all
diff --git a/.ignore b/.ignore
index 5b96dabd38a..29912ad5c31 100644
--- a/.ignore
+++ b/.ignore
@@ -1,9 +1,6 @@
*.min.css
*.min.js
/assets/*.json
-/modules/options/bindata.go
-/modules/public/bindata.go
-/modules/templates/bindata.go
/options/gitignore
/options/license
/public/assets
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ca2e67929c1..b72ac4849aa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,429 @@ 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
been added to each release, please refer to the [blog](https://blog.gitea.com).
+## [1.24.0](https://github.com/go-gitea/gitea/releases/tag/1.24.0) - 2025-05-26
+
+* 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
* SECURITY
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 979831eb9b8..6a7126388ef 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -30,7 +30,7 @@ These are the values to which people in the Gitea community should aspire.
- **Be constructive.**
- Avoid derailing: stay on topic; if you want to talk about something else, start a new conversation.
- Avoid unconstructive criticism: don't merely decry the current state of affairs; offer—or at least solicit—suggestions as to how things may be improved.
- - Avoid snarking (pithy, unproductive, sniping comments)
+ - Avoid snarking (pithy, unproductive, sniping comments).
- Avoid discussing potentially offensive or sensitive issues; this all too often leads to unnecessary conflict.
- Avoid microaggressions (brief and commonplace verbal, behavioral and environmental indignities that communicate hostile, derogatory or negative slights and insults to a person or group).
- **Be responsible.**
@@ -42,7 +42,7 @@ People are complicated. You should expect to be misunderstood and to misundersta
### Our Pledge
-In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
+In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
### Our Standards
diff --git a/Dockerfile b/Dockerfile
index fa2ae9913cc..c9e6a2d3db8 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
# 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
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
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"
EXPOSE 22 3000
diff --git a/Dockerfile.rootless b/Dockerfile.rootless
index b74dfa58e00..558e6cf73bc 100644
--- a/Dockerfile.rootless
+++ b/Dockerfile.rootless
@@ -1,5 +1,5 @@
# 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
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
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"
EXPOSE 2222 3000
@@ -52,6 +52,7 @@ RUN apk --no-cache add \
git \
curl \
gnupg \
+ openssh-keygen \
&& rm -rf /var/cache/apk/*
RUN addgroup \
diff --git a/Makefile b/Makefile
index d10250bbc7a..6a3fa60e495 100644
--- a/Makefile
+++ b/Makefile
@@ -26,17 +26,18 @@ COMMA := ,
XGO_VERSION := go-1.24.x
AIR_PACKAGE ?= github.com/air-verse/air@v1
-EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.2.1
-GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.7.0
-GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.0.2
+EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3
+GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.8.0
+GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.1.6
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.12
-MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.6.0
-SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.31.0
+MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.7.0
+SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.32.3
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1
-GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.17.1
+GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.19.1
+GOPLS_MODERNIZE_PACKAGE ?= golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@v0.19.1
DOCKER_IMAGE ?= gitea/gitea
DOCKER_TAG ?= latest
@@ -80,7 +81,6 @@ ifeq ($(RACE_ENABLED),true)
endif
STORED_VERSION_FILE := VERSION
-HUGO_VERSION ?= 0.111.3
GITHUB_REF_TYPE ?= branch
GITHUB_REF_NAME ?= $(shell git rev-parse --abbrev-ref HEAD)
@@ -120,8 +120,7 @@ WEBPACK_CONFIGS := webpack.config.js tailwind.config.js
WEBPACK_DEST := public/assets/js/index.js public/assets/css/index.css
WEBPACK_DEST_ENTRIES := public/assets/js public/assets/css public/assets/fonts
-BINDATA_DEST := modules/public/bindata.go modules/options/bindata.go modules/templates/bindata.go
-BINDATA_HASH := $(addsuffix .hash,$(BINDATA_DEST))
+BINDATA_DEST_WILDCARD := modules/migration/bindata.* modules/public/bindata.* modules/options/bindata.* modules/templates/bindata.*
GENERATED_GO_DEST := modules/charset/invisible_gen.go modules/charset/ambiguous_gen.go
@@ -149,14 +148,8 @@ SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) templates options/locale/locale_en-US
EDITORCONFIG_FILES := templates .github/workflows options/locale/locale_en-US.ini
GO_SOURCES := $(wildcard *.go)
-GO_SOURCES += $(shell find $(GO_DIRS) -type f -name "*.go" ! -path modules/options/bindata.go ! -path modules/public/bindata.go ! -path modules/templates/bindata.go)
+GO_SOURCES += $(shell find $(GO_DIRS) -type f -name "*.go")
GO_SOURCES += $(GENERATED_GO_DEST)
-GO_SOURCES_NO_BINDATA := $(GO_SOURCES)
-
-ifeq ($(filter $(TAGS_SPLIT),bindata),bindata)
- GO_SOURCES += $(BINDATA_DEST)
- GENERATED_GO_DEST += $(BINDATA_DEST)
-endif
# Force installation of playwright dependencies by setting this flag
ifdef DEPS_PLAYWRIGHT
@@ -226,7 +219,7 @@ clean-all: clean ## delete backend, frontend and integration files
.PHONY: clean
clean: ## delete backend and integration files
- rm -rf $(EXECUTABLE) $(DIST) $(BINDATA_DEST) $(BINDATA_HASH) \
+ rm -rf $(EXECUTABLE) $(DIST) $(BINDATA_DEST_WILDCARD) \
integrations*.test \
e2e*.test \
tests/integration/gitea-integration-* \
@@ -237,7 +230,7 @@ clean: ## delete backend and integration files
tests/e2e/reports/ tests/e2e/test-artifacts/ tests/e2e/test-snapshots/
.PHONY: fmt
-fmt: ## format the Go code
+fmt: ## format the Go and template code
@GOFUMPT_PACKAGE=$(GOFUMPT_PACKAGE) $(GO) run build/code-batch-process.go gitea-fmt -w '{file-list}'
$(eval TEMPLATES := $(shell find templates -type f -name '*.tmpl'))
@# strip whitespace after '{{' or '(' and before '}}' or ')' unless there is only
@@ -256,6 +249,19 @@ fmt-check: fmt
exit 1; \
fi
+.PHONY: fix
+fix: ## apply automated fixes to Go code
+ $(GO) run $(GOPLS_MODERNIZE_PACKAGE) -fix ./...
+
+.PHONY: fix-check
+fix-check: fix
+ @diff=$$(git diff --color=always $(GO_SOURCES)); \
+ if [ -n "$$diff" ]; then \
+ echo "Please run 'make fix' and commit the result:"; \
+ printf "%s" "$${diff}"; \
+ exit 1; \
+ fi
+
.PHONY: $(TAGS_EVIDENCE)
$(TAGS_EVIDENCE):
@mkdir -p $(MAKE_EVIDENCE_DIR)
@@ -268,7 +274,7 @@ endif
.PHONY: generate-swagger
generate-swagger: $(SWAGGER_SPEC) ## generate the swagger spec from code comments
-$(SWAGGER_SPEC): $(GO_SOURCES_NO_BINDATA) $(SWAGGER_SPEC_INPUT)
+$(SWAGGER_SPEC): $(GO_SOURCES) $(SWAGGER_SPEC_INPUT)
$(GO) run $(SWAGGER_PACKAGE) generate spec --exclude "$(SWAGGER_EXCLUDE)" --input "$(SWAGGER_SPEC_INPUT)" --output './$(SWAGGER_SPEC)'
.PHONY: swagger-check
@@ -295,7 +301,7 @@ checks: checks-frontend checks-backend ## run various consistency checks
checks-frontend: lockfile-check svg-check ## check frontend files
.PHONY: checks-backend
-checks-backend: tidy-check swagger-check fmt-check swagger-validate security-check ## check backend files
+checks-backend: tidy-check swagger-check fmt-check fix-check swagger-validate security-check ## check backend files
.PHONY: lint
lint: lint-frontend lint-backend lint-spell ## lint everything
@@ -373,7 +379,7 @@ lint-go-gitea-vet: ## lint go files with gitea-vet
.PHONY: lint-go-gopls
lint-go-gopls: ## lint go files with gopls
@echo "Running gopls check..."
- @GO=$(GO) GOPLS_PACKAGE=$(GOPLS_PACKAGE) tools/lint-go-gopls.sh $(GO_SOURCES_NO_BINDATA)
+ @GO=$(GO) GOPLS_PACKAGE=$(GOPLS_PACKAGE) tools/lint-go-gopls.sh $(GO_SOURCES)
.PHONY: lint-editorconfig
lint-editorconfig:
@@ -816,6 +822,7 @@ deps-tools: ## install tool dependencies
$(GO) install $(GOVULNCHECK_PACKAGE) & \
$(GO) install $(ACTIONLINT_PACKAGE) & \
$(GO) install $(GOPLS_PACKAGE) & \
+ $(GO) install $(GOPLS_MODERNIZE_PACKAGE) & \
wait
node_modules: package-lock.json
diff --git a/README.md b/README.md
index 017ca629d01..aa2c6010fa3 100644
--- a/README.md
+++ b/README.md
@@ -80,9 +80,9 @@ Expected workflow is: Fork -> Patch -> Push -> Pull Request
[](https://translate.gitea.com)
-Translations are done through [Crowdin](https://translate.gitea.com). If you want to translate to a new language ask one of the managers in the Crowdin project to add a new language there.
+Translations are done through [Crowdin](https://translate.gitea.com). If you want to translate to a new language, ask one of the managers in the Crowdin project to add a new language there.
-You can also just create an issue for adding a language or ask on discord on the #translation channel. If you need context or find some translation issues, you can leave a comment on the string or ask on Discord. For general translation questions there is a section in the docs. Currently a bit empty but we hope to fill it as questions pop up.
+You can also just create an issue for adding a language or ask on Discord on the #translation channel. If you need context or find some translation issues, you can leave a comment on the string or ask on Discord. For general translation questions there is a section in the docs. Currently a bit empty, but we hope to fill it as questions pop up.
Get more information from [documentation](https://docs.gitea.com/contributing/localization).
diff --git a/SECURITY.md b/SECURITY.md
index c9dbf859f58..d7c27ea6136 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -14,12 +14,12 @@ Please **DO NOT** file a public issue, instead send your report privately to `se
Due to the sensitive nature of security information, you can use the below GPG public key to encrypt your mail body.
-The PGP key is valid until July 9, 2025.
+The PGP key is valid until July 4, 2026.
```
Key ID: 6FCD2D5B
Key Type: RSA
-Expires: 7/9/2025
+Expires: 7/4/2026
Key Size: 4096/4096
Fingerprint: 3DE0 3D1E 144A 7F06 9359 99DC AAFD 2381 6FCD 2D5B
```
@@ -42,18 +42,18 @@ lzpAjnN9/KLtQroutrm+Ft0mdjDiJUeFVl1cOHDhoyfCsQh62HumoyZoZvqzQd6e
AbN11nq6aViMe2Q3je1AbiBnRnQSHxt1Tc8X4IshO3MQK1Sk7oPI6LA5oQARAQAB
tCJHaXRlYSBTZWN1cml0eSA8c2VjdXJpdHlAZ2l0ZWEuaW8+iQJXBBMBCABBAhsD
BQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAFiEEPeA9HhRKfwaTWZncqv0jgW/N
-LVsFAmaMse0FCQW4fW8ACgkQqv0jgW/NLVtXLg/+PF4G9Jhlui15BTNlEBJAV2P/
-1QlAV2krk0fP7tykn0FR9RfGIfVV/kwC1f+ouosYPQDDevl9LWdUIM+g94DtNo2o
-7ACpcL3morvt5lVGpIZHL8TbX0qmFRXL/pB/cB+K6IwYvh2mrbp2zH+r4SCRyFYq
-BjgXYFTI1MylJ1ShAjU6Z+m3oJ+2xs5LzHS0X6zkTjzA2Zl4zQzciQ9T+wJcE7Zi
-HXdM1+YMF8KGNP8J9Rpug5oNDJ98lgZirRY7c3A/1xmYBiPnULwuuymdqEZO7l70
-SeAlE1RWYX8kbOBnBb/KY4XwE3Vic1oEzc9DiPWVH1ElX86WNNsFzuyULiwoBoWg
-pqZGhL9x1p5+46RGQSDczsHM7YGVtfYOiDo2PAVrmwsT0BnXnK8Oe3YIkvmUPEJu
-OkLt0Z6A5n8pz8zhQzuApwBsK4ncJ8zTCpvz/pfKKqZC/Vnoh3gKGhDGvOZ+b5IJ
-0kUTe2JsbnwFixDUMDtacQ1op8XOyLoLVmgqLn0+Pws4XPBlMof2bioFir3yHKnP
-gNchsF1agrlSIo5GA8u4ga+IlCSfvFIKrl7+cxacKcJYt/vbOU5KcvVJI5EtHKCG
-xfHjHY2ah1Qww7SxW6IXiRZZzPpsL2mBM2CD7N3qh9bV2s27wxYCdUodsIZbiyHe
-oWPzfBnkmiAN8KlZxHm5Ag0EYrVn/gEQALrFLQjCR3GjuHSindz0rd3Fnx/t7Sen
+LVsFAmhoHmkFCQeT6esACgkQqv0jgW/NLVuFLRAAmjBQSKRAgs2bFIEj7HLAbDp4
+f+XkdH+GsT3jRPOZ9QZgmtM+TfoE4yNgIVfOl+s4RdjM/W4QzqZuPQ55hbEHd056
+cJmm7B+6GsHFcdrPmh65sOCEIyh4+t45dUfeWpFsDPqm9j1UHXAJQIpB8vDEVAPH
+t+3wLCk8GMPJs1o5tIyMmaO23ngvkwn8eG7KgY+rp2PzObrb5g7ppci0ILzILkrp
+HVjZsEfUWRgSVF7LuU5ppqDKrlcqwUpQq6n3kGMZcLrCp6ACKP04TBmTfUxNwdL7
+I0N7apI2Pbct9T1Gv/lYAUFWyU2c3gh/EBLbO6BukaLOFRQHrtNfdJV/YnMPlcXr
+LUJjK9K4eAH9DsrZqrisz/LthsC2BaNIN3KRMTk5YTYgmIh8GXzSgihORmtDFELC
+RroID3pTuS0zjXh+wpY9GuPTh7UW23p42Daxca4fAT4k5EclvDRUrL21xMopPMiL
+HuNdELz4FVchRTy05PjzKVyjVInDNojE2KUxnjxZDzYJ6aT/g+coD5yfntYm8BEj
++ZzL0ndZES54hzKLpv7zwBQwFzam68clZYmDPILOPTflQDfpGEWmJK4undFU5obz
+ZsQRz0R3ulspChATbZxO0d5LX2obLpKO9X3b5VoO1KF+R8Vjw1Y0KxrNZ6rIcfqH
+Z50QVQKSe9dm08K0ON+5Ag0EYrVn/gEQALrFLQjCR3GjuHSindz0rd3Fnx/t7Sen
T+p07yCSSoSlmnJHCQmwh4vfg1blyz0zZ4vkIhtpHsEgc+ZAG+WQXSsJ2iRz+eSN
GwoOQl4XC3n+QWkc1ws+btr48+6UqXIQU+F8TPQyx/PIgi2nZXJB7f5+mjCqsk46
XvH4nTr4kJjuqMSR/++wvre2qNQRa/q/dTsK0OaN/mJsdX6Oi+aGNaQJUhIG7F+E
@@ -65,19 +65,19 @@ s+GsP9I3cmWWQcKYxWHtE8xTXnNCVPFZQj2nwhJzae8ypfOtulBRA3dUKWGKuDH/
axFENhUsT397aOU3qkP/od4a64JyNIEo4CTTSPVeWd7njsGqli2U3A4xL2CcyYvt
D/MWcMBGEoLSNTswwKdom4FaJpn5KThnK/T0bQcmJblJhoCtppXisbexZnCpuS0x
Zdlm2T14KJ3LABEBAAGJAjwEGAEIACYCGwwWIQQ94D0eFEp/BpNZmdyq/SOBb80t
-WwUCZoyyjQUJBbh+DwAKCRCq/SOBb80tW18XD/9MXztmf01MT+1kZdBouZ/7Rp/7
-9kuqo//B1G+RXau4oFtPqb67kNe2WaIc3u5B73PUHsMf3i6z4ib2KbMhZZerLn0O
-dRglcuPeNWmsASY3dH/XVG0cT0zvvWegagd12TJEl3Vs+7XNrOw4cwDj9L1+GH9m
-kSt4uaANWn/6a3RvMRhiVEYuNwhAzcKaactPmYqrLJgoVLbRSDkgyHaMQ2jKgLxk
-ifS/fvluGV0ub2Po6DJiqfRpd1tDvPhe9y1+r1WFDZsOcvTcZUfSt/7dXMGfqGu0
-2daVFlfeSXSALrDE5uc0UxodHCpP3sqRYDZevGLBRaaTkIjYXG/+N898+7K5WJF4
-xXOLWxM2cwGkG7eC9pugcDnBp9XlF7O+GBiZ05JUe5flXDQFZ+h3exjopu6KHF1B
-RnzNy8LC0UKb+AuvRIOLV92a9Q9wGWU/jaVDu6nZ0umAeuSzxiHoDsonm0Fl9QAz
-2/xCokebuoeLrEK7R2af3X86mqq3sVO4ax+HPYChzOaVQBiHUW/TAldWcldYYphR
-/e2WsbmQfvCRtz/bZfo+aUVnrHNjzVMtF2SszdVmA/04Y8pS28MqtuRqhm5DPOOd
-g1YeUywK5jRZ1twyo1kzJEFPLaoeaXaycsR1PMVBW0Urik5mrR/pOWq7PPoZoKb2
-lXYLE8bwkuQTmsyL1g==
-=9i7d
+WwUCaGgeJAUJB5PppgAKCRCq/SOBb80tW/NWEACB6Jrf0gWlk7e+hNCdnbM0ZVWU
+f2sHNFfXxxsdhpcDgKbNHtkZb8nZgv8AX+5fTtUwMVa3vKcdw30xFiIM5N7cCIPV
+vg/5z5BtfEaitnabEUG2iiVDIy8IHXIcK10rX+7BosA3QDl2PsiBHwyi5G13lRk8
+zGTSNDuOalug33h5/lr2dPigamkq74Aoy29q8Rjad6GfWHipL2bFimgtY+Zdi0BH
+NLk4EJXxj1SgVx5dtkQzWJReBA5M+FQ4QYQZBO+f4TDoOLmjui152uhkoLBQbGAa
+WWJFTVxm0bG5MXloEL3gA8DfU7XDwuW/sHJC5pBko8RpQViooOhckMepZV3Y83DK
+bwLYa3JmPgj2rEv4993dvrJbQhpGd082HOxOsllCs8pgNq1SnXpWYfcGTgGKC3ts
+U8YZUUJUQ7mi2L8Tv3ix20c9EiGmA30JAmA8eZTC3cWup91ZkkVBFRml2czTXajd
+RWZ6GbHV5503ueDQcB8yBVgF3CSixs67+dGSbD3p86OqGrjAcJzM5TFbNKcnGLdE
+kGbZpNwAISy750lXzXKmyrh5RTCeTOQerbwCMBvHZO+HAevA/LXDTw2OAiSIQlP5
+sYA4sFYLQ30OAkgJcmdp/pSgVj/erNtSN07ClrOpDb/uFpQymO6K2h0Pst3feNVK
+9M2VbqL9C51z/wyHLg==
+=SfZA
-----END PGP PUBLIC KEY BLOCK-----
```
diff --git a/assets/go-licenses.json b/assets/go-licenses.json
index 3827a092f14..d9614442396 100644
--- a/assets/go-licenses.json
+++ b/assets/go-licenses.json
@@ -1080,9 +1080,14 @@
"licenseText": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n\"License\" shall mean the terms and conditions for use, reproduction, and\ndistribution as defined by Sections 1 through 9 of this document.\n\n\"Licensor\" shall mean the copyright owner or entity authorized by the copyright\nowner that is granting the License.\n\n\"Legal Entity\" shall mean the union of the acting entity and all other entities\nthat control, are controlled by, or are under common control with that entity.\nFor the purposes of this definition, \"control\" means (i) the power, direct or\nindirect, to cause the direction or management of such entity, whether by\ncontract or otherwise, or (ii) ownership of fifty percent (50%) or more of the\noutstanding shares, or (iii) beneficial ownership of such entity.\n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising\npermissions granted by this License.\n\n\"Source\" form shall mean the preferred form for making modifications, including\nbut not limited to software source code, documentation source, and configuration\nfiles.\n\n\"Object\" form shall mean any form resulting from mechanical transformation or\ntranslation of a Source form, including but not limited to compiled object code,\ngenerated documentation, and conversions to other media types.\n\n\"Work\" shall mean the work of authorship, whether in Source or Object form, made\navailable under the License, as indicated by a copyright notice that is included\nin or attached to the work (an example is provided in the Appendix below).\n\n\"Derivative Works\" shall mean any work, whether in Source or Object form, that\nis based on (or derived from) the Work and for which the editorial revisions,\nannotations, elaborations, or other modifications represent, as a whole, an\noriginal work of authorship. For the purposes of this License, Derivative Works\nshall not include works that remain separable from, or merely link (or bind by\nname) to the interfaces of, the Work and Derivative Works thereof.\n\n\"Contribution\" shall mean any work of authorship, including the original version\nof the Work and any modifications or additions to that Work or Derivative Works\nthereof, that is intentionally submitted to Licensor for inclusion in the Work\nby the copyright owner or by an individual or Legal Entity authorized to submit\non behalf of the copyright owner. For the purposes of this definition,\n\"submitted\" means any form of electronic, verbal, or written communication sent\nto the Licensor or its representatives, including but not limited to\ncommunication on electronic mailing lists, source code control systems, and\nissue tracking systems that are managed by, or on behalf of, the Licensor for\nthe purpose of discussing and improving the Work, but excluding communication\nthat is conspicuously marked or otherwise designated in writing by the copyright\nowner as \"Not a Contribution.\"\n\n\"Contributor\" shall mean Licensor and any individual or Legal Entity on behalf\nof whom a Contribution has been received by Licensor and subsequently\nincorporated within the Work.\n\n2. Grant of Copyright License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable copyright license to reproduce, prepare Derivative Works of,\npublicly display, publicly perform, sublicense, and distribute the Work and such\nDerivative Works in Source or Object form.\n\n3. Grant of Patent License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable (except as stated in this section) patent license to make, have\nmade, use, offer to sell, sell, import, and otherwise transfer the Work, where\nsuch license applies only to those patent claims licensable by such Contributor\nthat are necessarily infringed by their Contribution(s) alone or by combination\nof their Contribution(s) with the Work to which such Contribution(s) was\nsubmitted. If You institute patent litigation against any entity (including a\ncross-claim or counterclaim in a lawsuit) alleging that the Work or a\nContribution incorporated within the Work constitutes direct or contributory\npatent infringement, then any patent licenses granted to You under this License\nfor that Work shall terminate as of the date such litigation is filed.\n\n4. Redistribution.\n\nYou may reproduce and distribute copies of the Work or Derivative Works thereof\nin any medium, with or without modifications, and in Source or Object form,\nprovided that You meet the following conditions:\n\nYou must give any other recipients of the Work or Derivative Works a copy of\nthis License; and\nYou must cause any modified files to carry prominent notices stating that You\nchanged the files; and\nYou must retain, in the Source form of any Derivative Works that You distribute,\nall copyright, patent, trademark, and attribution notices from the Source form\nof the Work, excluding those notices that do not pertain to any part of the\nDerivative Works; and\nIf the Work includes a \"NOTICE\" text file as part of its distribution, then any\nDerivative Works that You distribute must include a readable copy of the\nattribution notices contained within such NOTICE file, excluding those notices\nthat do not pertain to any part of the Derivative Works, in at least one of the\nfollowing places: within a NOTICE text file distributed as part of the\nDerivative Works; within the Source form or documentation, if provided along\nwith the Derivative Works; or, within a display generated by the Derivative\nWorks, if and wherever such third-party notices normally appear. The contents of\nthe NOTICE file are for informational purposes only and do not modify the\nLicense. You may add Your own attribution notices within Derivative Works that\nYou distribute, alongside or as an addendum to the NOTICE text from the Work,\nprovided that such additional attribution notices cannot be construed as\nmodifying the License.\nYou may add Your own copyright statement to Your modifications and may provide\nadditional or different license terms and conditions for use, reproduction, or\ndistribution of Your modifications, or for any such Derivative Works as a whole,\nprovided Your use, reproduction, and distribution of the Work otherwise complies\nwith the conditions stated in this License.\n\n5. Submission of Contributions.\n\nUnless You explicitly state otherwise, any Contribution intentionally submitted\nfor inclusion in the Work by You to the Licensor shall be under the terms and\nconditions of this License, without any additional terms or conditions.\nNotwithstanding the above, nothing herein shall supersede or modify the terms of\nany separate license agreement you may have executed with Licensor regarding\nsuch Contributions.\n\n6. Trademarks.\n\nThis License does not grant permission to use the trade names, trademarks,\nservice marks, or product names of the Licensor, except as required for\nreasonable and customary use in describing the origin of the Work and\nreproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty.\n\nUnless required by applicable law or agreed to in writing, Licensor provides the\nWork (and each Contributor provides its Contributions) on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,\nincluding, without limitation, any warranties or conditions of TITLE,\nNON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are\nsolely responsible for determining the appropriateness of using or\nredistributing the Work and assume any risks associated with Your exercise of\npermissions under this License.\n\n8. Limitation of Liability.\n\nIn no event and under no legal theory, whether in tort (including negligence),\ncontract, or otherwise, unless required by applicable law (such as deliberate\nand grossly negligent acts) or agreed to in writing, shall any Contributor be\nliable to You for damages, including any direct, indirect, special, incidental,\nor consequential damages of any character arising as a result of this License or\nout of the use or inability to use the Work (including but not limited to\ndamages for loss of goodwill, work stoppage, computer failure or malfunction, or\nany and all other commercial damages or losses), even if such Contributor has\nbeen advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability.\n\nWhile redistributing the Work or Derivative Works thereof, You may choose to\noffer, and charge a fee for, acceptance of support, warranty, indemnity, or\nother liability obligations and/or rights consistent with this License. However,\nin accepting such obligations, You may act only on Your own behalf and on Your\nsole responsibility, not on behalf of any other Contributor, and only if You\nagree to indemnify, defend, and hold each Contributor harmless for any liability\nincurred by, or claims asserted against, such Contributor by reason of your\naccepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work\n\nTo apply the Apache License to your work, attach the following boilerplate\nnotice, with the fields enclosed by brackets \"[]\" replaced with your own\nidentifying information. (Don't include the brackets!) The text should be\nenclosed in the appropriate comment syntax for the file format. We also\nrecommend that a file or class name and description of purpose be included on\nthe same \"printed page\" as the copyright notice for easier identification within\nthird-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License."
},
{
- "name": "github.com/urfave/cli/v2",
- "path": "github.com/urfave/cli/v2/LICENSE",
- "licenseText": "MIT License\n\nCopyright (c) 2022 urfave/cli maintainers\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ "name": "github.com/urfave/cli-docs/v3",
+ "path": "github.com/urfave/cli-docs/v3/LICENSE",
+ "licenseText": "MIT License\n\nCopyright (c) 2023 urfave/cli maintainers\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
+ },
+ {
+ "name": "github.com/urfave/cli/v3",
+ "path": "github.com/urfave/cli/v3/LICENSE",
+ "licenseText": "MIT License\n\nCopyright (c) 2023 urfave/cli maintainers\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
},
{
"name": "github.com/valyala/fastjson",
@@ -1109,11 +1114,6 @@
"path": "github.com/xanzy/ssh-agent/LICENSE",
"licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"{}\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright {yyyy} {name of copyright owner}\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n\n"
},
- {
- "name": "github.com/xrash/smetrics",
- "path": "github.com/xrash/smetrics/LICENSE",
- "licenseText": "Copyright (C) 2016 Felipe da Cunha Gonçalves\nAll Rights Reserved.\n\nMIT LICENSE\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
- },
{
"name": "github.com/yohcop/openid-go",
"path": "github.com/yohcop/openid-go/LICENSE",
diff --git a/build.go b/build.go
index 234579b5149..e81ba54690b 100644
--- a/build.go
+++ b/build.go
@@ -5,19 +5,10 @@
package main
-// Libraries that are included to vendor utilities used during build.
+// Libraries that are included to vendor utilities used during Makefile build.
// These libraries will not be included in a normal compilation.
import (
- // for embed
- _ "github.com/shurcooL/vfsgen"
-
- // for cover merge
- _ "golang.org/x/tools/cover"
-
// for vet
_ "code.gitea.io/gitea-vet"
-
- // for swagger
- _ "github.com/go-swagger/go-swagger/cmd/swagger"
)
diff --git a/build/generate-bindata.go b/build/generate-bindata.go
index 2fcb7c2f2a0..2553770762f 100644
--- a/build/generate-bindata.go
+++ b/build/generate-bindata.go
@@ -6,87 +6,22 @@
package main
import (
- "bytes"
- "crypto/sha1"
"fmt"
- "log"
- "net/http"
"os"
- "path/filepath"
- "strconv"
- "github.com/shurcooL/vfsgen"
+ "code.gitea.io/gitea/modules/assetfs"
)
-func needsUpdate(dir, filename string) (bool, []byte) {
- needRegen := false
- _, err := os.Stat(filename)
- if err != nil {
- needRegen = true
- }
-
- oldHash, err := os.ReadFile(filename + ".hash")
- if err != nil {
- oldHash = []byte{}
- }
-
- hasher := sha1.New()
-
- err = filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
- if err != nil {
- return err
- }
- info, err := d.Info()
- if err != nil {
- return err
- }
- _, _ = hasher.Write([]byte(d.Name()))
- _, _ = hasher.Write([]byte(info.ModTime().String()))
- _, _ = hasher.Write([]byte(strconv.FormatInt(info.Size(), 16)))
- return nil
- })
- if err != nil {
- return true, oldHash
- }
-
- newHash := hasher.Sum([]byte{})
-
- if bytes.Compare(oldHash, newHash) != 0 {
- return true, newHash
- }
-
- return needRegen, newHash
-}
-
func main() {
- if len(os.Args) < 4 {
- log.Fatal("Insufficient number of arguments. Need: directory packageName filename")
+ if len(os.Args) != 3 {
+ fmt.Println("usage: ./generate-bindata {local-directory} {bindata-filename}")
+ os.Exit(1)
}
- dir, packageName, filename := os.Args[1], os.Args[2], os.Args[3]
- var useGlobalModTime bool
- if len(os.Args) == 5 {
- useGlobalModTime, _ = strconv.ParseBool(os.Args[4])
+ dir, filename := os.Args[1], os.Args[2]
+ fmt.Printf("generating bindata for %s to %s\n", dir, filename)
+ if err := assetfs.GenerateEmbedBindata(dir, filename); err != nil {
+ fmt.Printf("failed: %s\n", err.Error())
+ os.Exit(1)
}
-
- update, newHash := needsUpdate(dir, filename)
-
- if !update {
- fmt.Printf("bindata for %s already up-to-date\n", packageName)
- return
- }
-
- fmt.Printf("generating bindata for %s\n", packageName)
- var fsTemplates http.FileSystem = http.Dir(dir)
- err := vfsgen.Generate(fsTemplates, vfsgen.Options{
- PackageName: packageName,
- BuildTags: "bindata",
- VariableName: "Assets",
- Filename: filename,
- UseGlobalModTime: useGlobalModTime,
- })
- if err != nil {
- log.Fatalf("%v\n", err)
- }
- _ = os.WriteFile(filename+".hash", newHash, 0o666)
}
diff --git a/cmd/actions.go b/cmd/actions.go
index f582c16c81c..2c51c6a1bcc 100644
--- a/cmd/actions.go
+++ b/cmd/actions.go
@@ -4,12 +4,13 @@
package cmd
import (
+ "context"
"fmt"
"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/setting"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
var (
@@ -17,7 +18,7 @@ var (
CmdActions = &cli.Command{
Name: "actions",
Usage: "Manage Gitea Actions",
- Subcommands: []*cli.Command{
+ Commands: []*cli.Command{
subcmdActionsGenRunnerToken,
},
}
@@ -38,10 +39,7 @@ var (
}
)
-func runGenerateActionsRunnerToken(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runGenerateActionsRunnerToken(ctx context.Context, c *cli.Command) error {
setting.MustInstalled()
scope := c.String("scope")
diff --git a/cmd/admin.go b/cmd/admin.go
index 6c9480e76eb..559544edd33 100644
--- a/cmd/admin.go
+++ b/cmd/admin.go
@@ -15,7 +15,7 @@ import (
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
var (
@@ -23,7 +23,7 @@ var (
CmdAdmin = &cli.Command{
Name: "admin",
Usage: "Perform common administrative operations",
- Subcommands: []*cli.Command{
+ Commands: []*cli.Command{
subcmdUser,
subcmdRepoSyncReleases,
subcmdRegenerate,
@@ -41,7 +41,7 @@ var (
subcmdRegenerate = &cli.Command{
Name: "regenerate",
Usage: "Regenerate specific files",
- Subcommands: []*cli.Command{
+ Commands: []*cli.Command{
microcmdRegenHooks,
microcmdRegenKeys,
},
@@ -50,15 +50,15 @@ var (
subcmdAuth = &cli.Command{
Name: "auth",
Usage: "Modify external auth providers",
- Subcommands: []*cli.Command{
- microcmdAuthAddOauth,
- microcmdAuthUpdateOauth,
- microcmdAuthAddLdapBindDn,
- microcmdAuthUpdateLdapBindDn,
- microcmdAuthAddLdapSimpleAuth,
- microcmdAuthUpdateLdapSimpleAuth,
- microcmdAuthAddSMTP,
- microcmdAuthUpdateSMTP,
+ Commands: []*cli.Command{
+ microcmdAuthAddOauth(),
+ microcmdAuthUpdateOauth(),
+ microcmdAuthAddLdapBindDn(),
+ microcmdAuthUpdateLdapBindDn(),
+ microcmdAuthAddLdapSimpleAuth(),
+ microcmdAuthUpdateLdapSimpleAuth(),
+ microcmdAuthAddSMTP(),
+ microcmdAuthUpdateSMTP(),
microcmdAuthList,
microcmdAuthDelete,
},
@@ -70,9 +70,9 @@ var (
Action: runSendMail,
Flags: []cli.Flag{
&cli.StringFlag{
- Name: "title",
- Usage: `a title of a message`,
- Value: "",
+ Name: "title",
+ Usage: "a title of a message",
+ Required: true,
},
&cli.StringFlag{
Name: "content",
@@ -86,17 +86,16 @@ var (
},
},
}
+)
- idFlag = &cli.Int64Flag{
+func idFlag() *cli.Int64Flag {
+ return &cli.Int64Flag{
Name: "id",
Usage: "ID of authentication source",
}
-)
-
-func runRepoSyncReleases(_ *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
+}
+func runRepoSyncReleases(ctx context.Context, _ *cli.Command) error {
if err := initDB(ctx); err != nil {
return err
}
@@ -107,7 +106,7 @@ func runRepoSyncReleases(_ *cli.Context) error {
log.Trace("Synchronizing repository releases (this may take a while)")
for page := 1; ; page++ {
- repos, count, err := repo_model.SearchRepositoryByName(ctx, &repo_model.SearchRepoOptions{
+ repos, count, err := repo_model.SearchRepositoryByName(ctx, repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
PageSize: repo_model.RepositoryListDefaultPageSize,
Page: page,
diff --git a/cmd/admin_auth.go b/cmd/admin_auth.go
index 4777a929086..1a093667229 100644
--- a/cmd/admin_auth.go
+++ b/cmd/admin_auth.go
@@ -4,6 +4,7 @@
package cmd
import (
+ "context"
"errors"
"fmt"
"os"
@@ -13,14 +14,14 @@ import (
"code.gitea.io/gitea/models/db"
auth_service "code.gitea.io/gitea/services/auth"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
var (
microcmdAuthDelete = &cli.Command{
Name: "delete",
Usage: "Delete specific auth source",
- Flags: []cli.Flag{idFlag},
+ Flags: []cli.Flag{idFlag()},
Action: runDeleteAuth,
}
microcmdAuthList = &cli.Command{
@@ -56,10 +57,7 @@ var (
}
)
-func runListAuth(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runListAuth(ctx context.Context, c *cli.Command) error {
if err := initDB(ctx); err != nil {
return err
}
@@ -90,14 +88,11 @@ func runListAuth(c *cli.Context) error {
return nil
}
-func runDeleteAuth(c *cli.Context) error {
+func runDeleteAuth(ctx context.Context, c *cli.Command) error {
if !c.IsSet("id") {
return errors.New("--id flag is missing")
}
- ctx, cancel := installSignals()
- defer cancel()
-
if err := initDB(ctx); err != nil {
return err
}
diff --git a/cmd/admin_auth_ldap.go b/cmd/admin_auth_ldap.go
index d2eeb7c0d6d..069ad6600c7 100644
--- a/cmd/admin_auth_ldap.go
+++ b/cmd/admin_auth_ldap.go
@@ -12,7 +12,7 @@ import (
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/auth/source/ldap"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
type (
@@ -24,8 +24,8 @@ type (
}
)
-var (
- commonLdapCLIFlags = []cli.Flag{
+func commonLdapCLIFlags() []cli.Flag {
+ return []cli.Flag{
&cli.StringFlag{
Name: "name",
Usage: "Authentication name.",
@@ -103,8 +103,10 @@ var (
Usage: "The attribute of the user’s LDAP record containing the user’s avatar.",
},
}
+}
- ldapBindDnCLIFlags = append(commonLdapCLIFlags,
+func ldapBindDnCLIFlags() []cli.Flag {
+ return append(commonLdapCLIFlags(),
&cli.StringFlag{
Name: "bind-dn",
Usage: "The DN to bind to the LDAP server with when searching for the user.",
@@ -157,49 +159,59 @@ var (
Name: "group-team-map-removal",
Usage: "Remove users from synchronized teams if user does not belong to corresponding LDAP group",
})
+}
- ldapSimpleAuthCLIFlags = append(commonLdapCLIFlags,
+func ldapSimpleAuthCLIFlags() []cli.Flag {
+ return append(commonLdapCLIFlags(),
&cli.StringFlag{
Name: "user-dn",
Usage: "The user's DN.",
})
+}
- microcmdAuthAddLdapBindDn = &cli.Command{
+func microcmdAuthAddLdapBindDn() *cli.Command {
+ return &cli.Command{
Name: "add-ldap",
Usage: "Add new LDAP (via Bind DN) authentication source",
- Action: func(c *cli.Context) error {
- return newAuthService().addLdapBindDn(c)
+ Action: func(ctx context.Context, cmd *cli.Command) error {
+ return newAuthService().addLdapBindDn(ctx, cmd)
},
- Flags: ldapBindDnCLIFlags,
+ Flags: ldapBindDnCLIFlags(),
}
+}
- microcmdAuthUpdateLdapBindDn = &cli.Command{
+func microcmdAuthUpdateLdapBindDn() *cli.Command {
+ return &cli.Command{
Name: "update-ldap",
Usage: "Update existing LDAP (via Bind DN) authentication source",
- Action: func(c *cli.Context) error {
- return newAuthService().updateLdapBindDn(c)
+ Action: func(ctx context.Context, cmd *cli.Command) error {
+ return newAuthService().updateLdapBindDn(ctx, cmd)
},
- Flags: append([]cli.Flag{idFlag}, ldapBindDnCLIFlags...),
+ Flags: append([]cli.Flag{idFlag()}, ldapBindDnCLIFlags()...),
}
+}
- microcmdAuthAddLdapSimpleAuth = &cli.Command{
+func microcmdAuthAddLdapSimpleAuth() *cli.Command {
+ return &cli.Command{
Name: "add-ldap-simple",
Usage: "Add new LDAP (simple auth) authentication source",
- Action: func(c *cli.Context) error {
- return newAuthService().addLdapSimpleAuth(c)
+ Action: func(ctx context.Context, cmd *cli.Command) error {
+ return newAuthService().addLdapSimpleAuth(ctx, cmd)
},
- Flags: ldapSimpleAuthCLIFlags,
+ Flags: ldapSimpleAuthCLIFlags(),
}
+}
- microcmdAuthUpdateLdapSimpleAuth = &cli.Command{
+func microcmdAuthUpdateLdapSimpleAuth() *cli.Command {
+ return &cli.Command{
Name: "update-ldap-simple",
Usage: "Update existing LDAP (simple auth) authentication source",
- Action: func(c *cli.Context) error {
- return newAuthService().updateLdapSimpleAuth(c)
+ Action: func(ctx context.Context, cmd *cli.Command) error {
+ return newAuthService().updateLdapSimpleAuth(ctx, cmd)
},
- Flags: append([]cli.Flag{idFlag}, ldapSimpleAuthCLIFlags...),
+ Flags: append([]cli.Flag{idFlag()}, ldapSimpleAuthCLIFlags()...),
}
-)
+}
// newAuthService creates a service with default functions.
func newAuthService() *authService {
@@ -212,7 +224,7 @@ func newAuthService() *authService {
}
// parseAuthSourceLdap assigns values on authSource according to command line flags.
-func parseAuthSourceLdap(c *cli.Context, authSource *auth.Source) {
+func parseAuthSourceLdap(c *cli.Command, authSource *auth.Source) {
if c.IsSet("name") {
authSource.Name = c.String("name")
}
@@ -232,7 +244,7 @@ func parseAuthSourceLdap(c *cli.Context, authSource *auth.Source) {
}
// parseLdapConfig assigns values on config according to command line flags.
-func parseLdapConfig(c *cli.Context, config *ldap.Source) error {
+func parseLdapConfig(c *cli.Command, config *ldap.Source) error {
if c.IsSet("name") {
config.Name = c.String("name")
}
@@ -245,7 +257,7 @@ func parseLdapConfig(c *cli.Context, config *ldap.Source) error {
if c.IsSet("security-protocol") {
p, ok := findLdapSecurityProtocolByName(c.String("security-protocol"))
if !ok {
- return fmt.Errorf("Unknown security protocol name: %s", c.String("security-protocol"))
+ return fmt.Errorf("unknown security protocol name: %s", c.String("security-protocol"))
}
config.SecurityProtocol = p
}
@@ -337,32 +349,27 @@ func findLdapSecurityProtocolByName(name string) (ldap.SecurityProtocol, bool) {
// getAuthSource gets the login source by its id defined in the command line flags.
// It returns an error if the id is not set, does not match any source or if the source is not of expected type.
-func (a *authService) getAuthSource(ctx context.Context, c *cli.Context, authType auth.Type) (*auth.Source, error) {
+func (a *authService) getAuthSource(ctx context.Context, c *cli.Command, authType auth.Type) (*auth.Source, error) {
if err := argsSet(c, "id"); err != nil {
return nil, err
}
-
authSource, err := a.getAuthSourceByID(ctx, c.Int64("id"))
if err != nil {
return nil, err
}
if authSource.Type != authType {
- return nil, fmt.Errorf("Invalid authentication type. expected: %s, actual: %s", authType.String(), authSource.Type.String())
+ return nil, fmt.Errorf("invalid authentication type. expected: %s, actual: %s", authType.String(), authSource.Type.String())
}
return authSource, nil
}
// addLdapBindDn adds a new LDAP via Bind DN authentication source.
-func (a *authService) addLdapBindDn(c *cli.Context) error {
+func (a *authService) addLdapBindDn(ctx context.Context, c *cli.Command) error {
if err := argsSet(c, "name", "security-protocol", "host", "port", "user-search-base", "user-filter", "email-attribute"); err != nil {
return err
}
-
- ctx, cancel := installSignals()
- defer cancel()
-
if err := a.initDB(ctx); err != nil {
return err
}
@@ -384,10 +391,7 @@ func (a *authService) addLdapBindDn(c *cli.Context) error {
}
// updateLdapBindDn updates a new LDAP via Bind DN authentication source.
-func (a *authService) updateLdapBindDn(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func (a *authService) updateLdapBindDn(ctx context.Context, c *cli.Command) error {
if err := a.initDB(ctx); err != nil {
return err
}
@@ -406,14 +410,11 @@ func (a *authService) updateLdapBindDn(c *cli.Context) error {
}
// addLdapSimpleAuth adds a new LDAP (simple auth) authentication source.
-func (a *authService) addLdapSimpleAuth(c *cli.Context) error {
+func (a *authService) addLdapSimpleAuth(ctx context.Context, c *cli.Command) error {
if err := argsSet(c, "name", "security-protocol", "host", "port", "user-dn", "user-filter", "email-attribute"); err != nil {
return err
}
- ctx, cancel := installSignals()
- defer cancel()
-
if err := a.initDB(ctx); err != nil {
return err
}
@@ -435,10 +436,7 @@ func (a *authService) addLdapSimpleAuth(c *cli.Context) error {
}
// updateLdapSimpleAuth updates a new LDAP (simple auth) authentication source.
-func (a *authService) updateLdapSimpleAuth(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func (a *authService) updateLdapSimpleAuth(ctx context.Context, c *cli.Command) error {
if err := a.initDB(ctx); err != nil {
return err
}
diff --git a/cmd/admin_auth_ldap_test.go b/cmd/admin_auth_ldap_test.go
index ea9a83ef76d..2da7ebc573b 100644
--- a/cmd/admin_auth_ldap_test.go
+++ b/cmd/admin_auth_ldap_test.go
@@ -8,17 +8,16 @@ import (
"testing"
"code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/services/auth/source/ldap"
"github.com/stretchr/testify/assert"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
func TestAddLdapBindDn(t *testing.T) {
// Mock cli functions to do not exit on error
- osExiter := cli.OsExiter
- defer func() { cli.OsExiter = osExiter }()
- cli.OsExiter = func(code int) {}
+ defer test.MockVariableValue(&cli.OsExiter, func(code int) {})()
// Test cases
cases := []struct {
@@ -135,7 +134,7 @@ func TestAddLdapBindDn(t *testing.T) {
"--user-filter", "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)",
"--email-attribute", "mail",
},
- errMsg: "Unknown security protocol name: zzzzz",
+ errMsg: "unknown security protocol name: zzzzz",
},
// case 3
{
@@ -239,12 +238,13 @@ func TestAddLdapBindDn(t *testing.T) {
}
// Create a copy of command to test
- app := cli.NewApp()
- app.Flags = microcmdAuthAddLdapBindDn.Flags
- app.Action = service.addLdapBindDn
+ app := cli.Command{
+ Flags: microcmdAuthAddLdapBindDn().Flags,
+ Action: service.addLdapBindDn,
+ }
// Run it
- err := app.Run(c.args)
+ err := app.Run(t.Context(), c.args)
if c.errMsg != "" {
assert.EqualError(t, err, c.errMsg, "case %d: error should match", n)
} else {
@@ -256,9 +256,7 @@ func TestAddLdapBindDn(t *testing.T) {
func TestAddLdapSimpleAuth(t *testing.T) {
// Mock cli functions to do not exit on error
- osExiter := cli.OsExiter
- defer func() { cli.OsExiter = osExiter }()
- cli.OsExiter = func(code int) {}
+ defer test.MockVariableValue(&cli.OsExiter, func(code int) {})()
// Test cases
cases := []struct {
@@ -348,12 +346,12 @@ func TestAddLdapSimpleAuth(t *testing.T) {
"--name", "ldap (simple auth) source",
"--security-protocol", "zzzzz",
"--host", "ldap-server",
- "--port", "123",
+ "--port", "1234",
"--user-filter", "(&(objectClass=posixAccount)(cn=%s))",
"--email-attribute", "mail",
"--user-dn", "cn=%s,ou=Users,dc=domain,dc=org",
},
- errMsg: "Unknown security protocol name: zzzzz",
+ errMsg: "unknown security protocol name: zzzzz",
},
// case 3
{
@@ -470,12 +468,13 @@ func TestAddLdapSimpleAuth(t *testing.T) {
}
// Create a copy of command to test
- app := cli.NewApp()
- app.Flags = microcmdAuthAddLdapSimpleAuth.Flags
- app.Action = service.addLdapSimpleAuth
+ app := &cli.Command{
+ Flags: microcmdAuthAddLdapSimpleAuth().Flags,
+ Action: service.addLdapSimpleAuth,
+ }
// Run it
- err := app.Run(c.args)
+ err := app.Run(t.Context(), c.args)
if c.errMsg != "" {
assert.EqualError(t, err, c.errMsg, "case %d: error should match", n)
} else {
@@ -487,9 +486,7 @@ func TestAddLdapSimpleAuth(t *testing.T) {
func TestUpdateLdapBindDn(t *testing.T) {
// Mock cli functions to do not exit on error
- osExiter := cli.OsExiter
- defer func() { cli.OsExiter = osExiter }()
- cli.OsExiter = func(code int) {}
+ defer test.MockVariableValue(&cli.OsExiter, func(code int) {})()
// Test cases
cases := []struct {
@@ -864,7 +861,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
"--id", "1",
"--security-protocol", "xxxxx",
},
- errMsg: "Unknown security protocol name: xxxxx",
+ errMsg: "unknown security protocol name: xxxxx",
},
// case 22
{
@@ -883,7 +880,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
Type: auth.OAuth2,
Cfg: &ldap.Source{},
},
- errMsg: "Invalid authentication type. expected: LDAP (via BindDN), actual: OAuth2",
+ errMsg: "invalid authentication type. expected: LDAP (via BindDN), actual: OAuth2",
},
// case 24
{
@@ -947,12 +944,12 @@ func TestUpdateLdapBindDn(t *testing.T) {
}
// Create a copy of command to test
- app := cli.NewApp()
- app.Flags = microcmdAuthUpdateLdapBindDn.Flags
- app.Action = service.updateLdapBindDn
-
+ app := cli.Command{
+ Flags: microcmdAuthUpdateLdapBindDn().Flags,
+ Action: service.updateLdapBindDn,
+ }
// Run it
- err := app.Run(c.args)
+ err := app.Run(t.Context(), c.args)
if c.errMsg != "" {
assert.EqualError(t, err, c.errMsg, "case %d: error should match", n)
} else {
@@ -964,9 +961,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
func TestUpdateLdapSimpleAuth(t *testing.T) {
// Mock cli functions to do not exit on error
- osExiter := cli.OsExiter
- defer func() { cli.OsExiter = osExiter }()
- cli.OsExiter = func(code int) {}
+ defer test.MockVariableValue(&cli.OsExiter, func(code int) {})()
// Test cases
cases := []struct {
@@ -1257,7 +1252,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
"--id", "1",
"--security-protocol", "xxxxx",
},
- errMsg: "Unknown security protocol name: xxxxx",
+ errMsg: "unknown security protocol name: xxxxx",
},
// case 18
{
@@ -1276,7 +1271,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
Type: auth.PAM,
Cfg: &ldap.Source{},
},
- errMsg: "Invalid authentication type. expected: LDAP (simple auth), actual: PAM",
+ errMsg: "invalid authentication type. expected: LDAP (simple auth), actual: PAM",
},
// case 20
{
@@ -1337,12 +1332,12 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
}
// Create a copy of command to test
- app := cli.NewApp()
- app.Flags = microcmdAuthUpdateLdapSimpleAuth.Flags
- app.Action = service.updateLdapSimpleAuth
-
+ app := cli.Command{
+ Flags: microcmdAuthUpdateLdapSimpleAuth().Flags,
+ Action: service.updateLdapSimpleAuth,
+ }
// Run it
- err := app.Run(c.args)
+ err := app.Run(t.Context(), c.args)
if c.errMsg != "" {
assert.EqualError(t, err, c.errMsg, "case %d: error should match", n)
} else {
diff --git a/cmd/admin_auth_oauth.go b/cmd/admin_auth_oauth.go
index be5345d9928..d1aa753500a 100644
--- a/cmd/admin_auth_oauth.go
+++ b/cmd/admin_auth_oauth.go
@@ -4,6 +4,7 @@
package cmd
import (
+ "context"
"errors"
"fmt"
"net/url"
@@ -12,11 +13,11 @@ import (
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/auth/source/oauth2"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
-var (
- oauthCLIFlags = []cli.Flag{
+func oauthCLIFlags() []cli.Flag {
+ return []cli.Flag{
&cli.StringFlag{
Name: "name",
Value: "",
@@ -121,23 +122,34 @@ var (
Usage: "Activate automatic team membership removal depending on groups",
},
}
+}
- microcmdAuthAddOauth = &cli.Command{
- Name: "add-oauth",
- Usage: "Add new Oauth authentication source",
- Action: runAddOauth,
- Flags: oauthCLIFlags,
+func microcmdAuthAddOauth() *cli.Command {
+ return &cli.Command{
+ Name: "add-oauth",
+ Usage: "Add new Oauth authentication source",
+ Action: func(ctx context.Context, cmd *cli.Command) error {
+ return newAuthService().runAddOauth(ctx, cmd)
+ },
+ Flags: oauthCLIFlags(),
}
+}
- microcmdAuthUpdateOauth = &cli.Command{
- Name: "update-oauth",
- Usage: "Update existing Oauth authentication source",
- Action: runUpdateOauth,
- Flags: append(oauthCLIFlags[:1], append([]cli.Flag{idFlag}, oauthCLIFlags[1:]...)...),
+func microcmdAuthUpdateOauth() *cli.Command {
+ return &cli.Command{
+ Name: "update-oauth",
+ Usage: "Update existing Oauth authentication source",
+ Action: func(ctx context.Context, cmd *cli.Command) error {
+ return newAuthService().runUpdateOauth(ctx, cmd)
+ },
+ Flags: append(oauthCLIFlags()[:1], append([]cli.Flag{&cli.Int64Flag{
+ Name: "id",
+ Usage: "ID of authentication source",
+ }}, oauthCLIFlags()[1:]...)...),
}
-)
+}
-func parseOAuth2Config(c *cli.Context) *oauth2.Source {
+func parseOAuth2Config(c *cli.Command) *oauth2.Source {
var customURLMapping *oauth2.CustomURLMapping
if c.IsSet("use-custom-urls") {
customURLMapping = &oauth2.CustomURLMapping{
@@ -168,11 +180,8 @@ func parseOAuth2Config(c *cli.Context) *oauth2.Source {
}
}
-func runAddOauth(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
- if err := initDB(ctx); err != nil {
+func (a *authService) runAddOauth(ctx context.Context, c *cli.Command) error {
+ if err := a.initDB(ctx); err != nil {
return err
}
@@ -184,7 +193,7 @@ func runAddOauth(c *cli.Context) error {
}
}
- return auth_model.CreateSource(ctx, &auth_model.Source{
+ return a.createAuthSource(ctx, &auth_model.Source{
Type: auth_model.OAuth2,
Name: c.String("name"),
IsActive: true,
@@ -193,19 +202,16 @@ func runAddOauth(c *cli.Context) error {
})
}
-func runUpdateOauth(c *cli.Context) error {
+func (a *authService) runUpdateOauth(ctx context.Context, c *cli.Command) error {
if !c.IsSet("id") {
return errors.New("--id flag is missing")
}
- ctx, cancel := installSignals()
- defer cancel()
-
- if err := initDB(ctx); err != nil {
+ if err := a.initDB(ctx); err != nil {
return err
}
- source, err := auth_model.GetSourceByID(ctx, c.Int64("id"))
+ source, err := a.getAuthSourceByID(ctx, c.Int64("id"))
if err != nil {
return err
}
@@ -296,5 +302,5 @@ func runUpdateOauth(c *cli.Context) error {
oAuth2Config.CustomURLMapping = customURLMapping
source.Cfg = oAuth2Config
source.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "")
- return auth_model.UpdateSource(ctx, source)
+ return a.updateAuthSource(ctx, source)
}
diff --git a/cmd/admin_auth_oauth_test.go b/cmd/admin_auth_oauth_test.go
new file mode 100644
index 00000000000..df1bd9c1a6d
--- /dev/null
+++ b/cmd/admin_auth_oauth_test.go
@@ -0,0 +1,333 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package cmd
+
+import (
+ "context"
+ "testing"
+
+ auth_model "code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/services/auth/source/oauth2"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/urfave/cli/v3"
+)
+
+func TestAddOauth(t *testing.T) {
+ testCases := []struct {
+ name string
+ args []string
+ source *auth_model.Source
+ errMsg string
+ }{
+ {
+ name: "valid config",
+ args: []string{
+ "--name", "test",
+ "--provider", "github",
+ "--key", "some_key",
+ "--secret", "some_secret",
+ },
+ source: &auth_model.Source{
+ Type: auth_model.OAuth2,
+ Name: "test",
+ IsActive: true,
+ Cfg: &oauth2.Source{
+ Scopes: []string{},
+ Provider: "github",
+ ClientID: "some_key",
+ ClientSecret: "some_secret",
+ },
+ TwoFactorPolicy: "",
+ },
+ },
+ {
+ name: "valid config with openid connect",
+ args: []string{
+ "--name", "test",
+ "--provider", "openidConnect",
+ "--key", "some_key",
+ "--secret", "some_secret",
+ "--auto-discover-url", "https://example.com",
+ },
+ source: &auth_model.Source{
+ Type: auth_model.OAuth2,
+ Name: "test",
+ IsActive: true,
+ Cfg: &oauth2.Source{
+ Scopes: []string{},
+ Provider: "openidConnect",
+ ClientID: "some_key",
+ ClientSecret: "some_secret",
+ OpenIDConnectAutoDiscoveryURL: "https://example.com",
+ },
+ TwoFactorPolicy: "",
+ },
+ },
+ {
+ name: "valid config with options",
+ args: []string{
+ "--name", "test",
+ "--provider", "gitlab",
+ "--key", "some_key",
+ "--secret", "some_secret",
+ "--use-custom-urls", "true",
+ "--custom-token-url", "https://example.com/token",
+ "--custom-auth-url", "https://example.com/auth",
+ "--custom-profile-url", "https://example.com/profile",
+ "--custom-email-url", "https://example.com/email",
+ "--custom-tenant-id", "some_tenant",
+ "--icon-url", "https://example.com/icon",
+ "--scopes", "scope1,scope2",
+ "--skip-local-2fa", "true",
+ "--required-claim-name", "claim_name",
+ "--required-claim-value", "claim_value",
+ "--group-claim-name", "group_name",
+ "--admin-group", "admin",
+ "--restricted-group", "restricted",
+ "--group-team-map", `{"group1": [1,2]}`,
+ "--group-team-map-removal=true",
+ },
+ source: &auth_model.Source{
+ Type: auth_model.OAuth2,
+ Name: "test",
+ IsActive: true,
+ Cfg: &oauth2.Source{
+ Provider: "gitlab",
+ ClientID: "some_key",
+ ClientSecret: "some_secret",
+ CustomURLMapping: &oauth2.CustomURLMapping{
+ TokenURL: "https://example.com/token",
+ AuthURL: "https://example.com/auth",
+ ProfileURL: "https://example.com/profile",
+ EmailURL: "https://example.com/email",
+ Tenant: "some_tenant",
+ },
+ IconURL: "https://example.com/icon",
+ Scopes: []string{"scope1", "scope2"},
+ RequiredClaimName: "claim_name",
+ RequiredClaimValue: "claim_value",
+ GroupClaimName: "group_name",
+ AdminGroup: "admin",
+ RestrictedGroup: "restricted",
+ GroupTeamMap: `{"group1": [1,2]}`,
+ GroupTeamMapRemoval: true,
+ },
+ TwoFactorPolicy: "skip",
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ var createdSource *auth_model.Source
+ a := &authService{
+ initDB: func(ctx context.Context) error {
+ return nil
+ },
+ createAuthSource: func(ctx context.Context, source *auth_model.Source) error {
+ createdSource = source
+ return nil
+ },
+ }
+
+ app := &cli.Command{
+ Flags: microcmdAuthAddOauth().Flags,
+ Action: a.runAddOauth,
+ }
+
+ args := []string{"oauth-test"}
+ args = append(args, tc.args...)
+
+ err := app.Run(t.Context(), args)
+
+ if tc.errMsg != "" {
+ assert.EqualError(t, err, tc.errMsg)
+ } else {
+ assert.NoError(t, err)
+ assert.Equal(t, tc.source, createdSource)
+ }
+ })
+ }
+}
+
+func TestUpdateOauth(t *testing.T) {
+ testCases := []struct {
+ name string
+ args []string
+ id int64
+ existingAuthSource *auth_model.Source
+ authSource *auth_model.Source
+ errMsg string
+ }{
+ {
+ name: "missing id",
+ args: []string{
+ "--name", "test",
+ },
+ errMsg: "--id flag is missing",
+ },
+ {
+ name: "valid config",
+ id: 1,
+ existingAuthSource: &auth_model.Source{
+ ID: 1,
+ Type: auth_model.OAuth2,
+ Name: "old name",
+ IsActive: true,
+ Cfg: &oauth2.Source{
+ Provider: "github",
+ ClientID: "old_key",
+ ClientSecret: "old_secret",
+ },
+ TwoFactorPolicy: "",
+ },
+ args: []string{
+ "--id", "1",
+ "--name", "test",
+ "--provider", "gitlab",
+ "--key", "new_key",
+ "--secret", "new_secret",
+ },
+ authSource: &auth_model.Source{
+ ID: 1,
+ Type: auth_model.OAuth2,
+ Name: "test",
+ IsActive: true,
+ Cfg: &oauth2.Source{
+ Provider: "gitlab",
+ ClientID: "new_key",
+ ClientSecret: "new_secret",
+ CustomURLMapping: &oauth2.CustomURLMapping{},
+ },
+ TwoFactorPolicy: "",
+ },
+ },
+ {
+ name: "valid config with options",
+ id: 1,
+ existingAuthSource: &auth_model.Source{
+ ID: 1,
+ Type: auth_model.OAuth2,
+ Name: "old name",
+ IsActive: true,
+ Cfg: &oauth2.Source{
+ Provider: "gitlab",
+ ClientID: "old_key",
+ ClientSecret: "old_secret",
+ CustomURLMapping: &oauth2.CustomURLMapping{
+ TokenURL: "https://old.example.com/token",
+ AuthURL: "https://old.example.com/auth",
+ ProfileURL: "https://old.example.com/profile",
+ EmailURL: "https://old.example.com/email",
+ Tenant: "old_tenant",
+ },
+ IconURL: "https://old.example.com/icon",
+ Scopes: []string{"old_scope1", "old_scope2"},
+ RequiredClaimName: "old_claim_name",
+ RequiredClaimValue: "old_claim_value",
+ GroupClaimName: "old_group_name",
+ AdminGroup: "old_admin",
+ RestrictedGroup: "old_restricted",
+ GroupTeamMap: `{"old_group1": [1,2]}`,
+ GroupTeamMapRemoval: true,
+ },
+ TwoFactorPolicy: "",
+ },
+ args: []string{
+ "--id", "1",
+ "--name", "test",
+ "--provider", "github",
+ "--key", "new_key",
+ "--secret", "new_secret",
+ "--use-custom-urls", "true",
+ "--custom-token-url", "https://example.com/token",
+ "--custom-auth-url", "https://example.com/auth",
+ "--custom-profile-url", "https://example.com/profile",
+ "--custom-email-url", "https://example.com/email",
+ "--custom-tenant-id", "new_tenant",
+ "--icon-url", "https://example.com/icon",
+ "--scopes", "scope1,scope2",
+ "--skip-local-2fa=true",
+ "--required-claim-name", "claim_name",
+ "--required-claim-value", "claim_value",
+ "--group-claim-name", "group_name",
+ "--admin-group", "admin",
+ "--restricted-group", "restricted",
+ "--group-team-map", `{"group1": [1,2]}`,
+ "--group-team-map-removal=false",
+ },
+ authSource: &auth_model.Source{
+ ID: 1,
+ Type: auth_model.OAuth2,
+ Name: "test",
+ IsActive: true,
+ Cfg: &oauth2.Source{
+ Provider: "github",
+ ClientID: "new_key",
+ ClientSecret: "new_secret",
+ CustomURLMapping: &oauth2.CustomURLMapping{
+ TokenURL: "https://example.com/token",
+ AuthURL: "https://example.com/auth",
+ ProfileURL: "https://example.com/profile",
+ EmailURL: "https://example.com/email",
+ Tenant: "new_tenant",
+ },
+ IconURL: "https://example.com/icon",
+ Scopes: []string{"scope1", "scope2"},
+ RequiredClaimName: "claim_name",
+ RequiredClaimValue: "claim_value",
+ GroupClaimName: "group_name",
+ AdminGroup: "admin",
+ RestrictedGroup: "restricted",
+ GroupTeamMap: `{"group1": [1,2]}`,
+ GroupTeamMapRemoval: false,
+ },
+ TwoFactorPolicy: "skip",
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ a := &authService{
+ initDB: func(ctx context.Context) error {
+ return nil
+ },
+ getAuthSourceByID: func(ctx context.Context, id int64) (*auth_model.Source, error) {
+ return &auth_model.Source{
+ ID: 1,
+ Type: auth_model.OAuth2,
+ Name: "test",
+ IsActive: true,
+ Cfg: &oauth2.Source{
+ CustomURLMapping: &oauth2.CustomURLMapping{},
+ },
+ TwoFactorPolicy: "skip",
+ }, nil
+ },
+ updateAuthSource: func(ctx context.Context, source *auth_model.Source) error {
+ assert.Equal(t, tc.authSource, source)
+ return nil
+ },
+ }
+
+ app := &cli.Command{
+ Flags: microcmdAuthUpdateOauth().Flags,
+ Action: a.runUpdateOauth,
+ }
+
+ args := []string{"oauth-test"}
+ args = append(args, tc.args...)
+
+ err := app.Run(t.Context(), args)
+
+ if tc.errMsg != "" {
+ assert.EqualError(t, err, tc.errMsg)
+ } else {
+ assert.NoError(t, err)
+ }
+ })
+ }
+}
diff --git a/cmd/admin_auth_stmp.go b/cmd/admin_auth_smtp.go
similarity index 73%
rename from cmd/admin_auth_stmp.go
rename to cmd/admin_auth_smtp.go
index babcf78ceae..93e0587fc3c 100644
--- a/cmd/admin_auth_stmp.go
+++ b/cmd/admin_auth_smtp.go
@@ -4,6 +4,7 @@
package cmd
import (
+ "context"
"errors"
"strings"
@@ -11,11 +12,11 @@ import (
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/auth/source/smtp"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
-var (
- smtpCLIFlags = []cli.Flag{
+func smtpCLIFlags() []cli.Flag {
+ return []cli.Flag{
&cli.StringFlag{
Name: "name",
Value: "",
@@ -38,12 +39,10 @@ var (
&cli.BoolFlag{
Name: "force-smtps",
Usage: "SMTPS is always used on port 465. Set this to force SMTPS on other ports.",
- Value: true,
},
&cli.BoolFlag{
Name: "skip-verify",
Usage: "Skip TLS verify.",
- Value: true,
},
&cli.StringFlag{
Name: "helo-hostname",
@@ -53,7 +52,6 @@ var (
&cli.BoolFlag{
Name: "disable-helo",
Usage: "Disable SMTP helo.",
- Value: true,
},
&cli.StringFlag{
Name: "allowed-domains",
@@ -63,7 +61,6 @@ var (
&cli.BoolFlag{
Name: "skip-local-2fa",
Usage: "Skip 2FA to log on.",
- Value: true,
},
&cli.BoolFlag{
Name: "active",
@@ -71,23 +68,34 @@ var (
Value: true,
},
}
+}
- microcmdAuthAddSMTP = &cli.Command{
- Name: "add-smtp",
- Usage: "Add new SMTP authentication source",
- Action: runAddSMTP,
- Flags: smtpCLIFlags,
+func microcmdAuthUpdateSMTP() *cli.Command {
+ return &cli.Command{
+ Name: "update-smtp",
+ Usage: "Update existing SMTP authentication source",
+ Action: func(ctx context.Context, cmd *cli.Command) error {
+ return newAuthService().runUpdateSMTP(ctx, cmd)
+ },
+ Flags: append(smtpCLIFlags()[:1], append([]cli.Flag{&cli.Int64Flag{
+ Name: "id",
+ Usage: "ID of authentication source",
+ }}, smtpCLIFlags()[1:]...)...),
}
+}
- microcmdAuthUpdateSMTP = &cli.Command{
- Name: "update-smtp",
- Usage: "Update existing SMTP authentication source",
- Action: runUpdateSMTP,
- Flags: append(smtpCLIFlags[:1], append([]cli.Flag{idFlag}, smtpCLIFlags[1:]...)...),
+func microcmdAuthAddSMTP() *cli.Command {
+ return &cli.Command{
+ Name: "add-smtp",
+ Usage: "Add new SMTP authentication source",
+ Action: func(ctx context.Context, cmd *cli.Command) error {
+ return newAuthService().runAddSMTP(ctx, cmd)
+ },
+ Flags: smtpCLIFlags(),
}
-)
+}
-func parseSMTPConfig(c *cli.Context, conf *smtp.Source) error {
+func parseSMTPConfig(c *cli.Command, conf *smtp.Source) error {
if c.IsSet("auth-type") {
conf.Auth = c.String("auth-type")
validAuthTypes := []string{"PLAIN", "LOGIN", "CRAM-MD5"}
@@ -120,11 +128,8 @@ func parseSMTPConfig(c *cli.Context, conf *smtp.Source) error {
return nil
}
-func runAddSMTP(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
- if err := initDB(ctx); err != nil {
+func (a *authService) runAddSMTP(ctx context.Context, c *cli.Command) error {
+ if err := a.initDB(ctx); err != nil {
return err
}
@@ -152,7 +157,7 @@ func runAddSMTP(c *cli.Context) error {
smtpConfig.Auth = "PLAIN"
}
- return auth_model.CreateSource(ctx, &auth_model.Source{
+ return a.createAuthSource(ctx, &auth_model.Source{
Type: auth_model.SMTP,
Name: c.String("name"),
IsActive: active,
@@ -161,19 +166,16 @@ func runAddSMTP(c *cli.Context) error {
})
}
-func runUpdateSMTP(c *cli.Context) error {
+func (a *authService) runUpdateSMTP(ctx context.Context, c *cli.Command) error {
if !c.IsSet("id") {
return errors.New("--id flag is missing")
}
- ctx, cancel := installSignals()
- defer cancel()
-
- if err := initDB(ctx); err != nil {
+ if err := a.initDB(ctx); err != nil {
return err
}
- source, err := auth_model.GetSourceByID(ctx, c.Int64("id"))
+ source, err := a.getAuthSourceByID(ctx, c.Int64("id"))
if err != nil {
return err
}
@@ -194,5 +196,5 @@ func runUpdateSMTP(c *cli.Context) error {
source.Cfg = smtpConfig
source.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "")
- return auth_model.UpdateSource(ctx, source)
+ return a.updateAuthSource(ctx, source)
}
diff --git a/cmd/admin_auth_smtp_test.go b/cmd/admin_auth_smtp_test.go
new file mode 100644
index 00000000000..e54e01830c7
--- /dev/null
+++ b/cmd/admin_auth_smtp_test.go
@@ -0,0 +1,271 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package cmd
+
+import (
+ "context"
+ "testing"
+
+ auth_model "code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/services/auth/source/smtp"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/urfave/cli/v3"
+)
+
+func TestAddSMTP(t *testing.T) {
+ testCases := []struct {
+ name string
+ args []string
+ source *auth_model.Source
+ errMsg string
+ }{
+ {
+ name: "missing name",
+ args: []string{
+ "--host", "localhost",
+ "--port", "25",
+ },
+ errMsg: "name must be set",
+ },
+ {
+ name: "missing host",
+ args: []string{
+ "--name", "test",
+ "--port", "25",
+ },
+ errMsg: "host must be set",
+ },
+ {
+ name: "missing port",
+ args: []string{
+ "--name", "test",
+ "--host", "localhost",
+ },
+ errMsg: "port must be set",
+ },
+ {
+ name: "valid config",
+ args: []string{
+ "--name", "test",
+ "--host", "localhost",
+ "--port", "25",
+ },
+ source: &auth_model.Source{
+ Type: auth_model.SMTP,
+ Name: "test",
+ IsActive: true,
+ Cfg: &smtp.Source{
+ Auth: "PLAIN",
+ Host: "localhost",
+ Port: 25,
+ },
+ TwoFactorPolicy: "",
+ },
+ },
+ {
+ name: "valid config with options",
+ args: []string{
+ "--name", "test",
+ "--host", "localhost",
+ "--port", "25",
+ "--auth-type", "LOGIN",
+ "--force-smtps",
+ "--skip-verify",
+ "--helo-hostname", "example.com",
+ "--disable-helo=true",
+ "--allowed-domains", "example.com,example.org",
+ "--skip-local-2fa",
+ "--active=false",
+ },
+ source: &auth_model.Source{
+ Type: auth_model.SMTP,
+ Name: "test",
+ IsActive: false,
+ Cfg: &smtp.Source{
+ Auth: "LOGIN",
+ Host: "localhost",
+ Port: 25,
+ ForceSMTPS: true,
+ SkipVerify: true,
+ HeloHostname: "example.com",
+ DisableHelo: true,
+ AllowedDomains: "example.com,example.org",
+ },
+ TwoFactorPolicy: "skip",
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ a := &authService{
+ initDB: func(ctx context.Context) error {
+ return nil
+ },
+ createAuthSource: func(ctx context.Context, source *auth_model.Source) error {
+ assert.Equal(t, tc.source, source)
+ return nil
+ },
+ }
+
+ cmd := &cli.Command{
+ Flags: microcmdAuthAddSMTP().Flags,
+ Action: a.runAddSMTP,
+ }
+
+ args := []string{"smtp-test"}
+ args = append(args, tc.args...)
+
+ t.Log(args)
+ err := cmd.Run(t.Context(), args)
+
+ if tc.errMsg != "" {
+ assert.EqualError(t, err, tc.errMsg)
+ } else {
+ assert.NoError(t, err)
+ }
+ })
+ }
+}
+
+func TestUpdateSMTP(t *testing.T) {
+ testCases := []struct {
+ name string
+ args []string
+ existingAuthSource *auth_model.Source
+ authSource *auth_model.Source
+ errMsg string
+ }{
+ {
+ name: "missing id",
+ args: []string{
+ "--name", "test",
+ "--host", "localhost",
+ "--port", "25",
+ },
+ errMsg: "--id flag is missing",
+ },
+ {
+ name: "valid config",
+ existingAuthSource: &auth_model.Source{
+ ID: 1,
+ Type: auth_model.SMTP,
+ Name: "old name",
+ IsActive: true,
+ Cfg: &smtp.Source{
+ Auth: "PLAIN",
+ Host: "old host",
+ Port: 26,
+ },
+ },
+ args: []string{
+ "--id", "1",
+ "--name", "test",
+ "--host", "localhost",
+ "--port", "25",
+ },
+ authSource: &auth_model.Source{
+ ID: 1,
+ Type: auth_model.SMTP,
+ Name: "test",
+ IsActive: true,
+ Cfg: &smtp.Source{
+ Auth: "PLAIN",
+ Host: "localhost",
+ Port: 25,
+ },
+ },
+ },
+ {
+ name: "valid config with options",
+ existingAuthSource: &auth_model.Source{
+ ID: 1,
+ Type: auth_model.SMTP,
+ Name: "old name",
+ IsActive: true,
+ Cfg: &smtp.Source{
+ Auth: "PLAIN",
+ Host: "old host",
+ Port: 26,
+ HeloHostname: "old.example.com",
+ AllowedDomains: "old.example.com",
+ },
+ TwoFactorPolicy: "",
+ },
+ args: []string{
+ "--id", "1",
+ "--name", "test",
+ "--host", "localhost",
+ "--port", "25",
+ "--auth-type", "LOGIN",
+ "--force-smtps",
+ "--skip-verify",
+ "--helo-hostname", "example.com",
+ "--disable-helo",
+ "--allowed-domains", "example.com,example.org",
+ "--skip-local-2fa",
+ "--active=false",
+ },
+ authSource: &auth_model.Source{
+ ID: 1,
+ Type: auth_model.SMTP,
+ Name: "test",
+ IsActive: false,
+ Cfg: &smtp.Source{
+ Auth: "LOGIN",
+ Host: "localhost",
+ Port: 25,
+ ForceSMTPS: true,
+ SkipVerify: true,
+ HeloHostname: "example.com",
+ DisableHelo: true,
+ AllowedDomains: "example.com,example.org",
+ },
+ TwoFactorPolicy: "skip",
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ a := &authService{
+ initDB: func(ctx context.Context) error {
+ return nil
+ },
+ getAuthSourceByID: func(ctx context.Context, id int64) (*auth_model.Source, error) {
+ return &auth_model.Source{
+ ID: 1,
+ Type: auth_model.SMTP,
+ Name: "test",
+ IsActive: true,
+ Cfg: &smtp.Source{
+ Auth: "PLAIN",
+ },
+ }, nil
+ },
+
+ updateAuthSource: func(ctx context.Context, source *auth_model.Source) error {
+ assert.Equal(t, tc.authSource, source)
+ return nil
+ },
+ }
+
+ app := &cli.Command{
+ Flags: microcmdAuthUpdateSMTP().Flags,
+ Action: a.runUpdateSMTP,
+ }
+ args := []string{"smtp-tests"}
+ args = append(args, tc.args...)
+
+ err := app.Run(t.Context(), args)
+
+ if tc.errMsg != "" {
+ assert.EqualError(t, err, tc.errMsg)
+ } else {
+ assert.NoError(t, err)
+ }
+ })
+ }
+}
diff --git a/cmd/admin_regenerate.go b/cmd/admin_regenerate.go
index ab769f6d0c6..a5f1bd51059 100644
--- a/cmd/admin_regenerate.go
+++ b/cmd/admin_regenerate.go
@@ -4,11 +4,13 @@
package cmd
import (
+ "context"
+
"code.gitea.io/gitea/modules/graceful"
asymkey_service "code.gitea.io/gitea/services/asymkey"
repo_service "code.gitea.io/gitea/services/repository"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
var (
@@ -25,20 +27,14 @@ var (
}
)
-func runRegenerateHooks(_ *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runRegenerateHooks(ctx context.Context, _ *cli.Command) error {
if err := initDB(ctx); err != nil {
return err
}
return repo_service.SyncRepositoryHooks(graceful.GetManager().ShutdownContext())
}
-func runRegenerateKeys(_ *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runRegenerateKeys(ctx context.Context, _ *cli.Command) error {
if err := initDB(ctx); err != nil {
return err
}
diff --git a/cmd/admin_user.go b/cmd/admin_user.go
index 967a6ed88a2..3a24c3e56f1 100644
--- a/cmd/admin_user.go
+++ b/cmd/admin_user.go
@@ -4,18 +4,18 @@
package cmd
import (
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
var subcmdUser = &cli.Command{
Name: "user",
Usage: "Modify users",
- Subcommands: []*cli.Command{
- microcmdUserCreate,
+ Commands: []*cli.Command{
+ microcmdUserCreate(),
microcmdUserList,
- microcmdUserChangePassword,
- microcmdUserDelete,
+ microcmdUserChangePassword(),
+ microcmdUserDelete(),
microcmdUserGenerateAccessToken,
- microcmdUserMustChangePassword,
+ microcmdUserMustChangePassword(),
},
}
diff --git a/cmd/admin_user_change_password.go b/cmd/admin_user_change_password.go
index f1ed46e70b0..c27905b4db5 100644
--- a/cmd/admin_user_change_password.go
+++ b/cmd/admin_user_change_password.go
@@ -4,6 +4,7 @@
package cmd
import (
+ "context"
"errors"
"fmt"
@@ -13,44 +14,41 @@ import (
"code.gitea.io/gitea/modules/setting"
user_service "code.gitea.io/gitea/services/user"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
-var microcmdUserChangePassword = &cli.Command{
- Name: "change-password",
- Usage: "Change a user's password",
- Action: runChangePassword,
- Flags: []cli.Flag{
- &cli.StringFlag{
- Name: "username",
- Aliases: []string{"u"},
- Value: "",
- Usage: "The user to change password for",
+func microcmdUserChangePassword() *cli.Command {
+ return &cli.Command{
+ Name: "change-password",
+ Usage: "Change a user's password",
+ Action: runChangePassword,
+ Flags: []cli.Flag{
+ &cli.StringFlag{
+ Name: "username",
+ Aliases: []string{"u"},
+ Usage: "The user to change password for",
+ Required: true,
+ },
+ &cli.StringFlag{
+ Name: "password",
+ Aliases: []string{"p"},
+ Usage: "New password to set for user",
+ Required: true,
+ },
+ &cli.BoolFlag{
+ Name: "must-change-password",
+ Usage: "User must change password (can be disabled by --must-change-password=false)",
+ Value: true,
+ },
},
- &cli.StringFlag{
- Name: "password",
- Aliases: []string{"p"},
- Value: "",
- Usage: "New password to set for user",
- },
- &cli.BoolFlag{
- Name: "must-change-password",
- Usage: "User must change password (can be disabled by --must-change-password=false)",
- Value: true,
- },
- },
+ }
}
-func runChangePassword(c *cli.Context) error {
- if err := argsSet(c, "username", "password"); err != nil {
- return err
- }
-
- ctx, cancel := installSignals()
- defer cancel()
-
- if err := initDB(ctx); err != nil {
- return err
+func runChangePassword(ctx context.Context, c *cli.Command) error {
+ if !setting.IsInTesting {
+ if err := initDB(ctx); err != nil {
+ return err
+ }
}
user, err := user_model.GetUserByName(ctx, c.String("username"))
diff --git a/cmd/admin_user_change_password_test.go b/cmd/admin_user_change_password_test.go
new file mode 100644
index 00000000000..17d0382af74
--- /dev/null
+++ b/cmd/admin_user_change_password_test.go
@@ -0,0 +1,91 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package cmd
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestChangePasswordCommand(t *testing.T) {
+ ctx := t.Context()
+
+ defer func() {
+ require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
+ }()
+
+ t.Run("change password successfully", func(t *testing.T) {
+ // defer func() {
+ // require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
+ // }()
+ // Prepare test user
+ unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
+ err := microcmdUserCreate().Run(ctx, []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
+ require.NoError(t, err)
+
+ // load test user
+ userBase := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
+
+ // Change the password
+ err = microcmdUserChangePassword().Run(ctx, []string{"change-password", "--username", "testuser", "--password", "newpassword"})
+ require.NoError(t, err)
+
+ // Verify the password has been changed
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
+ assert.NotEqual(t, userBase.Passwd, user.Passwd)
+ assert.NotEqual(t, userBase.Salt, user.Salt)
+
+ // Additional check for must-change-password flag
+ require.NoError(t, microcmdUserChangePassword().Run(ctx, []string{"change-password", "--username", "testuser", "--password", "anotherpassword", "--must-change-password=false"}))
+ user = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
+ assert.False(t, user.MustChangePassword)
+
+ require.NoError(t, microcmdUserChangePassword().Run(ctx, []string{"change-password", "--username", "testuser", "--password", "yetanotherpassword", "--must-change-password"}))
+ user = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
+ assert.True(t, user.MustChangePassword)
+ })
+
+ t.Run("failure cases", func(t *testing.T) {
+ testCases := []struct {
+ name string
+ args []string
+ expectedErr string
+ }{
+ {
+ name: "user does not exist",
+ args: []string{"change-password", "--username", "nonexistentuser", "--password", "newpassword"},
+ expectedErr: "user does not exist",
+ },
+ {
+ name: "missing username",
+ args: []string{"change-password", "--password", "newpassword"},
+ expectedErr: `"username" not set`,
+ },
+ {
+ name: "missing password",
+ args: []string{"change-password", "--username", "testuser"},
+ expectedErr: `"password" not set`,
+ },
+ {
+ name: "too short password",
+ args: []string{"change-password", "--username", "testuser", "--password", "1"},
+ expectedErr: "password is not long enough",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ err := microcmdUserChangePassword().Run(ctx, tc.args)
+ require.Error(t, err)
+ require.Contains(t, err.Error(), tc.expectedErr)
+ })
+ }
+ })
+}
diff --git a/cmd/admin_user_create.go b/cmd/admin_user_create.go
index 97f9bb7f06a..cbdb5f90e2e 100644
--- a/cmd/admin_user_create.go
+++ b/cmd/admin_user_create.go
@@ -16,87 +16,95 @@ import (
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
-var microcmdUserCreate = &cli.Command{
- Name: "create",
- Usage: "Create a new user in database",
- Action: runCreateUser,
- Flags: []cli.Flag{
- &cli.StringFlag{
- Name: "name",
- Usage: "Username. DEPRECATED: use username instead",
+func microcmdUserCreate() *cli.Command {
+ return &cli.Command{
+ Name: "create",
+ Usage: "Create a new user in database",
+ Action: runCreateUser,
+ MutuallyExclusiveFlags: []cli.MutuallyExclusiveFlags{
+ {
+ Flags: [][]cli.Flag{
+ {
+ &cli.StringFlag{
+ Name: "name",
+ Usage: "Username. DEPRECATED: use username instead",
+ },
+ &cli.StringFlag{
+ Name: "username",
+ Usage: "Username",
+ },
+ },
+ },
+ Required: true,
+ },
},
- &cli.StringFlag{
- Name: "username",
- Usage: "Username",
+ Flags: []cli.Flag{
+ &cli.StringFlag{
+ Name: "user-type",
+ Usage: "Set user's type: individual or bot",
+ Value: "individual",
+ },
+ &cli.StringFlag{
+ Name: "password",
+ Usage: "User password",
+ },
+ &cli.StringFlag{
+ Name: "email",
+ Usage: "User email address",
+ Required: true,
+ },
+ &cli.BoolFlag{
+ Name: "admin",
+ Usage: "User is an admin",
+ },
+ &cli.BoolFlag{
+ Name: "random-password",
+ Usage: "Generate a random password for the user",
+ },
+ &cli.BoolFlag{
+ Name: "must-change-password",
+ Usage: "User must change password after initial login, defaults to true for all users except the first one (can be disabled by --must-change-password=false)",
+ HideDefault: true,
+ },
+ &cli.IntFlag{
+ Name: "random-password-length",
+ Usage: "Length of the random password to be generated",
+ Value: 12,
+ },
+ &cli.BoolFlag{
+ Name: "access-token",
+ Usage: "Generate access token for the user",
+ },
+ &cli.StringFlag{
+ Name: "access-token-name",
+ Usage: `Name of the generated access token`,
+ Value: "gitea-admin",
+ },
+ &cli.StringFlag{
+ Name: "access-token-scopes",
+ Usage: `Scopes of the generated access token, comma separated. Examples: "all", "public-only,read:issue", "write:repository,write:user"`,
+ Value: "all",
+ },
+ &cli.BoolFlag{
+ Name: "restricted",
+ Usage: "Make a restricted user account",
+ },
+ &cli.StringFlag{
+ Name: "fullname",
+ Usage: `The full, human-readable name of the user`,
+ },
},
- &cli.StringFlag{
- Name: "user-type",
- Usage: "Set user's type: individual or bot",
- Value: "individual",
- },
- &cli.StringFlag{
- Name: "password",
- Usage: "User password",
- },
- &cli.StringFlag{
- Name: "email",
- Usage: "User email address",
- },
- &cli.BoolFlag{
- Name: "admin",
- Usage: "User is an admin",
- },
- &cli.BoolFlag{
- Name: "random-password",
- Usage: "Generate a random password for the user",
- },
- &cli.BoolFlag{
- Name: "must-change-password",
- Usage: "User must change password after initial login, defaults to true for all users except the first one (can be disabled by --must-change-password=false)",
- DisableDefaultText: true,
- },
- &cli.IntFlag{
- Name: "random-password-length",
- Usage: "Length of the random password to be generated",
- Value: 12,
- },
- &cli.BoolFlag{
- Name: "access-token",
- Usage: "Generate access token for the user",
- },
- &cli.StringFlag{
- Name: "access-token-name",
- Usage: `Name of the generated access token`,
- Value: "gitea-admin",
- },
- &cli.StringFlag{
- Name: "access-token-scopes",
- Usage: `Scopes of the generated access token, comma separated. Examples: "all", "public-only,read:issue", "write:repository,write:user"`,
- Value: "all",
- },
- &cli.BoolFlag{
- Name: "restricted",
- Usage: "Make a restricted user account",
- },
- &cli.StringFlag{
- Name: "fullname",
- Usage: `The full, human-readable name of the user`,
- },
- },
+ }
}
-func runCreateUser(c *cli.Context) error {
+func runCreateUser(ctx context.Context, c *cli.Command) error {
// this command highly depends on the many setting options (create org, visibility, etc.), so it must have a full setting load first
// duplicate setting loading should be safe at the moment, but it should be refactored & improved in the future.
setting.LoadSettings()
- if err := argsSet(c, "email"); err != nil {
- return err
- }
-
userTypes := map[string]user_model.UserType{
"individual": user_model.UserTypeIndividual,
"bot": user_model.UserTypeBot,
@@ -113,12 +121,6 @@ func runCreateUser(c *cli.Context) error {
return errors.New("password can only be set for individual users")
}
}
- if c.IsSet("name") && c.IsSet("username") {
- return errors.New("cannot set both --name and --username flags")
- }
- if !c.IsSet("name") && !c.IsSet("username") {
- return errors.New("one of --name or --username flags must be set")
- }
if c.IsSet("password") && c.IsSet("random-password") {
return errors.New("cannot set both -random-password and -password flags")
@@ -129,16 +131,12 @@ func runCreateUser(c *cli.Context) error {
username = c.String("username")
} else {
username = c.String("name")
- _, _ = fmt.Fprintf(c.App.ErrWriter, "--name flag is deprecated. Use --username instead.\n")
+ _, _ = fmt.Fprintf(c.ErrWriter, "--name flag is deprecated. Use --username instead.\n")
}
- ctx := c.Context
if !setting.IsInTesting {
- // FIXME: need to refactor the "installSignals/initDB" related code later
+ // FIXME: need to refactor the "initDB" related code later
// it doesn't make sense to call it in (almost) every command action function
- var cancel context.CancelFunc
- ctx, cancel = installSignals()
- defer cancel()
if err := initDB(ctx); err != nil {
return err
}
diff --git a/cmd/admin_user_create_test.go b/cmd/admin_user_create_test.go
index d5952412c30..437e07d9a28 100644
--- a/cmd/admin_user_create_test.go
+++ b/cmd/admin_user_create_test.go
@@ -18,8 +18,6 @@ import (
)
func TestAdminUserCreate(t *testing.T) {
- app := NewMainApp(AppVersion{})
-
reset := func() {
require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
@@ -31,8 +29,9 @@ func TestAdminUserCreate(t *testing.T) {
IsAdmin bool
MustChangePassword bool
}
+
createCheck := func(name, args string) check {
- require.NoError(t, app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s --password foobar", name, name, args))))
+ require.NoError(t, microcmdUserCreate().Run(t.Context(), strings.Fields(fmt.Sprintf("create --username %s --email %s@gitea.local %s --password foobar", name, name, args))))
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name})
return check{IsAdmin: u.IsAdmin, MustChangePassword: u.MustChangePassword}
}
@@ -51,7 +50,7 @@ func TestAdminUserCreate(t *testing.T) {
})
createUser := func(name string, args ...string) error {
- return app.Run(append([]string{"./gitea", "admin", "user", "create", "--username", name, "--email", name + "@gitea.local"}, args...))
+ return microcmdUserCreate().Run(t.Context(), append([]string{"create", "--username", name, "--email", name + "@gitea.local"}, args...))
}
t.Run("UserType", func(t *testing.T) {
diff --git a/cmd/admin_user_delete.go b/cmd/admin_user_delete.go
index 520557554a2..f91041577c3 100644
--- a/cmd/admin_user_delete.go
+++ b/cmd/admin_user_delete.go
@@ -4,53 +4,56 @@
package cmd
import (
+ "context"
"errors"
"fmt"
"strings"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
user_service "code.gitea.io/gitea/services/user"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
-var microcmdUserDelete = &cli.Command{
- Name: "delete",
- Usage: "Delete specific user by id, name or email",
- Flags: []cli.Flag{
- &cli.Int64Flag{
- Name: "id",
- Usage: "ID of user of the user to delete",
+func microcmdUserDelete() *cli.Command {
+ return &cli.Command{
+ Name: "delete",
+ Usage: "Delete specific user by id, name or email",
+ Flags: []cli.Flag{
+ &cli.Int64Flag{
+ Name: "id",
+ Usage: "ID of user of the user to delete",
+ },
+ &cli.StringFlag{
+ Name: "username",
+ Aliases: []string{"u"},
+ Usage: "Username of the user to delete",
+ },
+ &cli.StringFlag{
+ Name: "email",
+ Aliases: []string{"e"},
+ Usage: "Email of the user to delete",
+ },
+ &cli.BoolFlag{
+ Name: "purge",
+ Usage: "Purge user, all their repositories, organizations and comments",
+ },
},
- &cli.StringFlag{
- Name: "username",
- Aliases: []string{"u"},
- Usage: "Username of the user to delete",
- },
- &cli.StringFlag{
- Name: "email",
- Aliases: []string{"e"},
- Usage: "Email of the user to delete",
- },
- &cli.BoolFlag{
- Name: "purge",
- Usage: "Purge user, all their repositories, organizations and comments",
- },
- },
- Action: runDeleteUser,
+ Action: runDeleteUser,
+ }
}
-func runDeleteUser(c *cli.Context) error {
+func runDeleteUser(ctx context.Context, c *cli.Command) error {
if !c.IsSet("id") && !c.IsSet("username") && !c.IsSet("email") {
return errors.New("You must provide the id, username or email of a user to delete")
}
- ctx, cancel := installSignals()
- defer cancel()
-
- if err := initDB(ctx); err != nil {
- return err
+ if !setting.IsInTesting {
+ if err := initDB(ctx); err != nil {
+ return err
+ }
}
if err := storage.Init(); err != nil {
@@ -70,11 +73,11 @@ func runDeleteUser(c *cli.Context) error {
return err
}
if c.IsSet("username") && user.LowerName != strings.ToLower(strings.TrimSpace(c.String("username"))) {
- return fmt.Errorf("The user %s who has email %s does not match the provided username %s", user.Name, c.String("email"), c.String("username"))
+ return fmt.Errorf("the user %s who has email %s does not match the provided username %s", user.Name, c.String("email"), c.String("username"))
}
if c.IsSet("id") && user.ID != c.Int64("id") {
- return fmt.Errorf("The user %s does not match the provided id %d", user.Name, c.Int64("id"))
+ return fmt.Errorf("the user %s does not match the provided id %d", user.Name, c.Int64("id"))
}
return user_service.DeleteUser(ctx, user, c.Bool("purge"))
diff --git a/cmd/admin_user_delete_test.go b/cmd/admin_user_delete_test.go
new file mode 100644
index 00000000000..d0330582d77
--- /dev/null
+++ b/cmd/admin_user_delete_test.go
@@ -0,0 +1,111 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package cmd
+
+import (
+ "strconv"
+ "strings"
+ "testing"
+
+ auth_model "code.gitea.io/gitea/models/auth"
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestAdminUserDelete(t *testing.T) {
+ ctx := t.Context()
+ defer func() {
+ require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
+ require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
+ require.NoError(t, db.TruncateBeans(db.DefaultContext, &auth_model.AccessToken{}))
+ }()
+
+ setupTestUser := func(t *testing.T) {
+ unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
+ err := microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
+ require.NoError(t, err)
+ }
+
+ t.Run("delete user by id", func(t *testing.T) {
+ setupTestUser(t)
+
+ u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
+ err := microcmdUserDelete().Run(ctx, []string{"delete-test", "--id", strconv.FormatInt(u.ID, 10)})
+ require.NoError(t, err)
+ unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
+ })
+ t.Run("delete user by username", func(t *testing.T) {
+ setupTestUser(t)
+
+ err := microcmdUserDelete().Run(ctx, []string{"delete-test", "--username", "testuser"})
+ require.NoError(t, err)
+ unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
+ })
+ t.Run("delete user by email", func(t *testing.T) {
+ setupTestUser(t)
+
+ err := microcmdUserDelete().Run(ctx, []string{"delete-test", "--email", "testuser@gitea.local"})
+ require.NoError(t, err)
+ unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
+ })
+ t.Run("delete user by all 3 attributes", func(t *testing.T) {
+ setupTestUser(t)
+
+ u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
+ err := microcmdUserDelete().Run(ctx, []string{"delete", "--id", strconv.FormatInt(u.ID, 10), "--username", "testuser", "--email", "testuser@gitea.local"})
+ require.NoError(t, err)
+ unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
+ })
+}
+
+func TestAdminUserDeleteFailure(t *testing.T) {
+ testCases := []struct {
+ name string
+ args []string
+ expectedErr string
+ }{
+ {
+ name: "no user to delete",
+ args: []string{"delete", "--username", "nonexistentuser"},
+ expectedErr: "user does not exist",
+ },
+ {
+ name: "user exists but provided username does not match",
+ args: []string{"delete", "--email", "testuser@gitea.local", "--username", "wrongusername"},
+ expectedErr: "the user testuser who has email testuser@gitea.local does not match the provided username wrongusername",
+ },
+ {
+ name: "user exists but provided id does not match",
+ args: []string{"delete", "--username", "testuser", "--id", "999"},
+ expectedErr: "the user testuser does not match the provided id 999",
+ },
+ {
+ name: "no required flags are provided",
+ args: []string{"delete"},
+ expectedErr: "You must provide the id, username or email of a user to delete",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ ctx := t.Context()
+ if strings.Contains(tc.name, "user exists") {
+ unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
+ err := microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
+ require.NoError(t, err)
+ }
+
+ err := microcmdUserDelete().Run(ctx, tc.args)
+ require.Error(t, err)
+ require.Contains(t, err.Error(), tc.expectedErr)
+ })
+
+ require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
+ require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
+ require.NoError(t, db.TruncateBeans(db.DefaultContext, &auth_model.AccessToken{}))
+ }
+}
diff --git a/cmd/admin_user_generate_access_token.go b/cmd/admin_user_generate_access_token.go
index f6db7a74bd1..61064fdef4b 100644
--- a/cmd/admin_user_generate_access_token.go
+++ b/cmd/admin_user_generate_access_token.go
@@ -4,13 +4,14 @@
package cmd
import (
+ "context"
"errors"
"fmt"
auth_model "code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
var microcmdUserGenerateAccessToken = &cli.Command{
@@ -41,14 +42,11 @@ var microcmdUserGenerateAccessToken = &cli.Command{
Action: runGenerateAccessToken,
}
-func runGenerateAccessToken(c *cli.Context) error {
+func runGenerateAccessToken(ctx context.Context, c *cli.Command) error {
if !c.IsSet("username") {
return errors.New("you must provide a username to generate a token for")
}
- ctx, cancel := installSignals()
- defer cancel()
-
if err := initDB(ctx); err != nil {
return err
}
diff --git a/cmd/admin_user_list.go b/cmd/admin_user_list.go
index 4c2b26d1dfd..e3d345e2f24 100644
--- a/cmd/admin_user_list.go
+++ b/cmd/admin_user_list.go
@@ -4,13 +4,14 @@
package cmd
import (
+ "context"
"fmt"
"os"
"text/tabwriter"
user_model "code.gitea.io/gitea/models/user"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
var microcmdUserList = &cli.Command{
@@ -25,10 +26,7 @@ var microcmdUserList = &cli.Command{
},
}
-func runListUsers(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runListUsers(ctx context.Context, c *cli.Command) error {
if err := initDB(ctx); err != nil {
return err
}
diff --git a/cmd/admin_user_must_change_password.go b/cmd/admin_user_must_change_password.go
index 2794414259a..8521853dc19 100644
--- a/cmd/admin_user_must_change_password.go
+++ b/cmd/admin_user_must_change_password.go
@@ -4,40 +4,41 @@
package cmd
import (
+ "context"
"errors"
"fmt"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/setting"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
-var microcmdUserMustChangePassword = &cli.Command{
- Name: "must-change-password",
- Usage: "Set the must change password flag for the provided users or all users",
- Action: runMustChangePassword,
- Flags: []cli.Flag{
- &cli.BoolFlag{
- Name: "all",
- Aliases: []string{"A"},
- Usage: "All users must change password, except those explicitly excluded with --exclude",
+func microcmdUserMustChangePassword() *cli.Command {
+ return &cli.Command{
+ Name: "must-change-password",
+ Usage: "Set the must change password flag for the provided users or all users",
+ Action: runMustChangePassword,
+ Flags: []cli.Flag{
+ &cli.BoolFlag{
+ Name: "all",
+ Aliases: []string{"A"},
+ Usage: "All users must change password, except those explicitly excluded with --exclude",
+ },
+ &cli.StringSliceFlag{
+ Name: "exclude",
+ Aliases: []string{"e"},
+ Usage: "Do not change the must-change-password flag for these users",
+ },
+ &cli.BoolFlag{
+ Name: "unset",
+ Usage: "Instead of setting the must-change-password flag, unset it",
+ },
},
- &cli.StringSliceFlag{
- Name: "exclude",
- Aliases: []string{"e"},
- Usage: "Do not change the must-change-password flag for these users",
- },
- &cli.BoolFlag{
- Name: "unset",
- Usage: "Instead of setting the must-change-password flag, unset it",
- },
- },
+ }
}
-func runMustChangePassword(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runMustChangePassword(ctx context.Context, c *cli.Command) error {
if c.NArg() == 0 && !c.IsSet("all") {
return errors.New("either usernames or --all must be provided")
}
@@ -46,8 +47,10 @@ func runMustChangePassword(c *cli.Context) error {
all := c.Bool("all")
exclude := c.StringSlice("exclude")
- if err := initDB(ctx); err != nil {
- return err
+ if !setting.IsInTesting {
+ if err := initDB(ctx); err != nil {
+ return err
+ }
}
n, err := user_model.SetMustChangePassword(ctx, all, mustChangePassword, c.Args().Slice(), exclude)
diff --git a/cmd/admin_user_must_change_password_test.go b/cmd/admin_user_must_change_password_test.go
new file mode 100644
index 00000000000..a6611fdc041
--- /dev/null
+++ b/cmd/admin_user_must_change_password_test.go
@@ -0,0 +1,78 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package cmd
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestMustChangePassword(t *testing.T) {
+ defer func() {
+ require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
+ }()
+ err := microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
+ require.NoError(t, err)
+ err = microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuserexclude", "--email", "testuserexclude@gitea.local", "--random-password"})
+ require.NoError(t, err)
+ // Reset password change flag
+ err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all", "--unset"})
+ require.NoError(t, err)
+
+ testUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
+ assert.False(t, testUser.MustChangePassword)
+ testUserExclude := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
+ assert.False(t, testUserExclude.MustChangePassword)
+
+ // Make all users change password
+ err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all"})
+ require.NoError(t, err)
+
+ testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
+ assert.True(t, testUser.MustChangePassword)
+ testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
+ assert.True(t, testUserExclude.MustChangePassword)
+
+ // Reset password change flag but exclude all tested users
+ err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all", "--unset", "--exclude", "testuser,testuserexclude"})
+ require.NoError(t, err)
+
+ testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
+ assert.True(t, testUser.MustChangePassword)
+ testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
+ assert.True(t, testUserExclude.MustChangePassword)
+
+ // Reset password change flag by listing multiple users
+ err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--unset", "testuser", "testuserexclude"})
+ require.NoError(t, err)
+
+ testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
+ assert.False(t, testUser.MustChangePassword)
+ testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
+ assert.False(t, testUserExclude.MustChangePassword)
+
+ // Exclude a user from all user
+ err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all", "--exclude", "testuserexclude"})
+ require.NoError(t, err)
+
+ testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
+ assert.True(t, testUser.MustChangePassword)
+ testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
+ assert.False(t, testUserExclude.MustChangePassword)
+
+ // Unset a flag for single user
+ err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--unset", "testuser"})
+ require.NoError(t, err)
+
+ testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
+ assert.False(t, testUser.MustChangePassword)
+ testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
+ assert.False(t, testUserExclude.MustChangePassword)
+}
diff --git a/cmd/cert.go b/cmd/cert.go
index 38241d71a33..53b4f9dcb4c 100644
--- a/cmd/cert.go
+++ b/cmd/cert.go
@@ -6,6 +6,7 @@
package cmd
import (
+ "context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
@@ -13,6 +14,7 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
+ "fmt"
"log"
"math/big"
"net"
@@ -20,47 +22,59 @@ import (
"strings"
"time"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
-// CmdCert represents the available cert sub-command.
-var CmdCert = &cli.Command{
- Name: "cert",
- Usage: "Generate self-signed certificate",
- Description: `Generate a self-signed X.509 certificate for a TLS server.
+// cmdCert represents the available cert sub-command.
+func cmdCert() *cli.Command {
+ return &cli.Command{
+ Name: "cert",
+ Usage: "Generate self-signed certificate",
+ Description: `Generate a self-signed X.509 certificate for a TLS server.
Outputs to 'cert.pem' and 'key.pem' and will overwrite existing files.`,
- Action: runCert,
- Flags: []cli.Flag{
- &cli.StringFlag{
- Name: "host",
- Value: "",
- Usage: "Comma-separated hostnames and IPs to generate a certificate for",
+ Action: runCert,
+ Flags: []cli.Flag{
+ &cli.StringFlag{
+ Name: "host",
+ Usage: "Comma-separated hostnames and IPs to generate a certificate for",
+ Required: true,
+ },
+ &cli.StringFlag{
+ Name: "ecdsa-curve",
+ Value: "",
+ Usage: "ECDSA curve to use to generate a key. Valid values are P224, P256, P384, P521",
+ },
+ &cli.IntFlag{
+ Name: "rsa-bits",
+ Value: 3072,
+ Usage: "Size of RSA key to generate. Ignored if --ecdsa-curve is set",
+ },
+ &cli.StringFlag{
+ Name: "start-date",
+ Value: "",
+ Usage: "Creation date formatted as Jan 1 15:04:05 2011",
+ },
+ &cli.DurationFlag{
+ Name: "duration",
+ Value: 365 * 24 * time.Hour,
+ Usage: "Duration that certificate is valid for",
+ },
+ &cli.BoolFlag{
+ Name: "ca",
+ Usage: "whether this cert should be its own Certificate Authority",
+ },
+ &cli.StringFlag{
+ Name: "out",
+ Value: "cert.pem",
+ Usage: "Path to the file where there certificate will be saved",
+ },
+ &cli.StringFlag{
+ Name: "keyout",
+ Value: "key.pem",
+ Usage: "Path to the file where there certificate key will be saved",
+ },
},
- &cli.StringFlag{
- Name: "ecdsa-curve",
- Value: "",
- Usage: "ECDSA curve to use to generate a key. Valid values are P224, P256, P384, P521",
- },
- &cli.IntFlag{
- Name: "rsa-bits",
- Value: 3072,
- Usage: "Size of RSA key to generate. Ignored if --ecdsa-curve is set",
- },
- &cli.StringFlag{
- Name: "start-date",
- Value: "",
- Usage: "Creation date formatted as Jan 1 15:04:05 2011",
- },
- &cli.DurationFlag{
- Name: "duration",
- Value: 365 * 24 * time.Hour,
- Usage: "Duration that certificate is valid for",
- },
- &cli.BoolFlag{
- Name: "ca",
- Usage: "whether this cert should be its own Certificate Authority",
- },
- },
+ }
}
func publicKey(priv any) any {
@@ -89,11 +103,7 @@ func pemBlockForKey(priv any) *pem.Block {
}
}
-func runCert(c *cli.Context) error {
- if err := argsSet(c, "host"); err != nil {
- return err
- }
-
+func runCert(_ context.Context, c *cli.Command) error {
var priv any
var err error
switch c.String("ecdsa-curve") {
@@ -108,17 +118,17 @@ func runCert(c *cli.Context) error {
case "P521":
priv, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
default:
- log.Fatalf("Unrecognized elliptic curve: %q", c.String("ecdsa-curve"))
+ err = fmt.Errorf("unrecognized elliptic curve: %q", c.String("ecdsa-curve"))
}
if err != nil {
- log.Fatalf("Failed to generate private key: %v", err)
+ return fmt.Errorf("failed to generate private key: %w", err)
}
var notBefore time.Time
if startDate := c.String("start-date"); startDate != "" {
notBefore, err = time.Parse("Jan 2 15:04:05 2006", startDate)
if err != nil {
- log.Fatalf("Failed to parse creation date: %v", err)
+ return fmt.Errorf("failed to parse creation date %w", err)
}
} else {
notBefore = time.Now()
@@ -129,7 +139,7 @@ func runCert(c *cli.Context) error {
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
- log.Fatalf("Failed to generate serial number: %v", err)
+ return fmt.Errorf("failed to generate serial number: %w", err)
}
template := x509.Certificate{
@@ -146,8 +156,8 @@ func runCert(c *cli.Context) error {
BasicConstraintsValid: true,
}
- hosts := strings.Split(c.String("host"), ",")
- for _, h := range hosts {
+ hosts := strings.SplitSeq(c.String("host"), ",")
+ for h := range hosts {
if ip := net.ParseIP(h); ip != nil {
template.IPAddresses = append(template.IPAddresses, ip)
} else {
@@ -162,35 +172,35 @@ func runCert(c *cli.Context) error {
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv)
if err != nil {
- log.Fatalf("Failed to create certificate: %v", err)
+ return fmt.Errorf("failed to create certificate: %w", err)
}
- certOut, err := os.Create("cert.pem")
+ certOut, err := os.Create(c.String("out"))
if err != nil {
- log.Fatalf("Failed to open cert.pem for writing: %v", err)
+ return fmt.Errorf("failed to open %s for writing: %w", c.String("keyout"), err)
}
err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
if err != nil {
- log.Fatalf("Failed to encode certificate: %v", err)
+ return fmt.Errorf("failed to encode certificate: %w", err)
}
err = certOut.Close()
if err != nil {
- log.Fatalf("Failed to write cert: %v", err)
+ return fmt.Errorf("failed to write cert: %w", err)
}
- log.Println("Written cert.pem")
+ fmt.Fprintf(c.Writer, "Written cert to %s\n", c.String("out"))
- keyOut, err := os.OpenFile("key.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
+ keyOut, err := os.OpenFile(c.String("keyout"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil {
- log.Fatalf("Failed to open key.pem for writing: %v", err)
+ return fmt.Errorf("failed to open %s for writing: %w", c.String("keyout"), err)
}
err = pem.Encode(keyOut, pemBlockForKey(priv))
if err != nil {
- log.Fatalf("Failed to encode key: %v", err)
+ return fmt.Errorf("failed to encode key: %w", err)
}
err = keyOut.Close()
if err != nil {
- log.Fatalf("Failed to write key: %v", err)
+ return fmt.Errorf("failed to write key: %w", err)
}
- log.Println("Written key.pem")
+ fmt.Fprintf(c.Writer, "Written key to %s\n", c.String("keyout"))
return nil
}
diff --git a/cmd/cert_test.go b/cmd/cert_test.go
new file mode 100644
index 00000000000..4242d8915b3
--- /dev/null
+++ b/cmd/cert_test.go
@@ -0,0 +1,123 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package cmd
+
+import (
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestCertCommand(t *testing.T) {
+ cases := []struct {
+ name string
+ args []string
+ }{
+ {
+ name: "RSA cert generation",
+ args: []string{
+ "cert-test",
+ "--host", "localhost",
+ "--rsa-bits", "2048",
+ "--duration", "1h",
+ "--start-date", "Jan 1 00:00:00 2024",
+ },
+ },
+ {
+ name: "ECDSA cert generation",
+ args: []string{
+ "cert-test",
+ "--host", "localhost",
+ "--ecdsa-curve", "P256",
+ "--duration", "1h",
+ "--start-date", "Jan 1 00:00:00 2024",
+ },
+ },
+ {
+ name: "mixed host, certificate authority",
+ args: []string{
+ "cert-test",
+ "--host", "localhost,127.0.0.1",
+ "--duration", "1h",
+ "--start-date", "Jan 1 00:00:00 2024",
+ },
+ },
+ }
+
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ app := cmdCert()
+ tempDir := t.TempDir()
+
+ certFile := filepath.Join(tempDir, "cert.pem")
+ keyFile := filepath.Join(tempDir, "key.pem")
+
+ err := app.Run(t.Context(), append(c.args, "--out", certFile, "--keyout", keyFile))
+ require.NoError(t, err)
+
+ assert.FileExists(t, certFile)
+ assert.FileExists(t, keyFile)
+ })
+ }
+}
+
+func TestCertCommandFailures(t *testing.T) {
+ cases := []struct {
+ name string
+ args []string
+ errMsg string
+ }{
+ {
+ name: "Start Date Parsing failure",
+ args: []string{
+ "cert-test",
+ "--host", "localhost",
+ "--start-date", "invalid-date",
+ },
+ errMsg: "parsing time",
+ },
+ {
+ name: "Unknown curve",
+ args: []string{
+ "cert-test",
+ "--host", "localhost",
+ "--ecdsa-curve", "invalid-curve",
+ },
+ errMsg: "unrecognized elliptic curve",
+ },
+ {
+ name: "Key generation failure",
+ args: []string{
+ "cert-test",
+ "--host", "localhost",
+ "--rsa-bits", "invalid-bits",
+ },
+ },
+ {
+ name: "Missing parameters",
+ args: []string{
+ "cert-test",
+ },
+ errMsg: `"host" not set`,
+ },
+ }
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ app := cmdCert()
+ tempDir := t.TempDir()
+
+ certFile := filepath.Join(tempDir, "cert.pem")
+ keyFile := filepath.Join(tempDir, "key.pem")
+ err := app.Run(t.Context(), append(c.args, "--out", certFile, "--keyout", keyFile))
+ require.Error(t, err)
+ if c.errMsg != "" {
+ assert.ErrorContains(t, err, c.errMsg)
+ }
+ assert.NoFileExists(t, certFile)
+ assert.NoFileExists(t, keyFile)
+ })
+ }
+}
diff --git a/cmd/cmd.go b/cmd/cmd.go
index 423dce26748..5b96bcbf9a9 100644
--- a/cmd/cmd.go
+++ b/cmd/cmd.go
@@ -18,20 +18,19 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/util"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
// argsSet checks that all the required arguments are set. args is a list of
// arguments that must be set in the passed Context.
-func argsSet(c *cli.Context, args ...string) error {
+func argsSet(c *cli.Command, args ...string) error {
for _, a := range args {
if !c.IsSet(a) {
return errors.New(a + " is not set")
}
- if util.IsEmptyString(c.String(a)) {
+ if c.Value(a) == nil {
return errors.New(a + " is required")
}
}
@@ -109,7 +108,7 @@ func setupConsoleLogger(level log.Level, colorize bool, out io.Writer) {
log.GetManager().GetLogger(log.DEFAULT).ReplaceAllWriters(writer)
}
-func globalBool(c *cli.Context, name string) bool {
+func globalBool(c *cli.Command, name string) bool {
for _, ctx := range c.Lineage() {
if ctx.Bool(name) {
return true
@@ -120,8 +119,8 @@ func globalBool(c *cli.Context, name string) bool {
// PrepareConsoleLoggerLevel by default, use INFO level for console logger, but some sub-commands (for git/ssh protocol) shouldn't output any log to stdout.
// Any log appears in git stdout pipe will break the git protocol, eg: client can't push and hangs forever.
-func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(*cli.Context) error {
- return func(c *cli.Context) error {
+func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(context.Context, *cli.Command) (context.Context, error) {
+ return func(ctx context.Context, c *cli.Command) (context.Context, error) {
level := defaultLevel
if globalBool(c, "quiet") {
level = log.FATAL
@@ -130,6 +129,16 @@ func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(*cli.Context) error
level = log.TRACE
}
log.SetConsoleLogger(log.DEFAULT, "console-default", level)
- return nil
+ return ctx, nil
}
}
+
+func isValidDefaultSubCommand(cmd *cli.Command) (string, bool) {
+ // Dirty patch for urfave/cli's strange design.
+ // "./gitea bad-cmd" should not start the web server.
+ rootArgs := cmd.Root().Args().Slice()
+ if len(rootArgs) != 0 && rootArgs[0] != cmd.Name {
+ return rootArgs[0], false
+ }
+ return "", true
+}
diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go
new file mode 100644
index 00000000000..a36d05c76e0
--- /dev/null
+++ b/cmd/cmd_test.go
@@ -0,0 +1,38 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package cmd
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/urfave/cli/v3"
+)
+
+func TestDefaultCommand(t *testing.T) {
+ test := func(t *testing.T, args []string, expectedRetName string, expectedRetValid bool) {
+ called := false
+ cmd := &cli.Command{
+ DefaultCommand: "test",
+ Commands: []*cli.Command{
+ {
+ Name: "test",
+ Action: func(ctx context.Context, command *cli.Command) error {
+ retName, retValid := isValidDefaultSubCommand(command)
+ assert.Equal(t, expectedRetName, retName)
+ assert.Equal(t, expectedRetValid, retValid)
+ called = true
+ return nil
+ },
+ },
+ },
+ }
+ assert.NoError(t, cmd.Run(t.Context(), args))
+ assert.True(t, called)
+ }
+ test(t, []string{"./gitea"}, "", true)
+ test(t, []string{"./gitea", "test"}, "", true)
+ test(t, []string{"./gitea", "other"}, "other", false)
+}
diff --git a/cmd/docs.go b/cmd/docs.go
index 605d02e3efe..098c0e9a8a1 100644
--- a/cmd/docs.go
+++ b/cmd/docs.go
@@ -4,11 +4,13 @@
package cmd
import (
+ "context"
"fmt"
"os"
"strings"
- "github.com/urfave/cli/v2"
+ cli_docs "github.com/urfave/cli-docs/v3"
+ "github.com/urfave/cli/v3"
)
// CmdDocs represents the available docs sub-command.
@@ -30,16 +32,16 @@ var CmdDocs = &cli.Command{
},
}
-func runDocs(ctx *cli.Context) error {
- docs, err := ctx.App.ToMarkdown()
- if ctx.Bool("man") {
- docs, err = ctx.App.ToMan()
+func runDocs(_ context.Context, cmd *cli.Command) error {
+ docs, err := cli_docs.ToMarkdown(cmd.Root())
+ if cmd.Bool("man") {
+ docs, err = cli_docs.ToMan(cmd.Root())
}
if err != nil {
return err
}
- if !ctx.Bool("man") {
+ if !cmd.Bool("man") {
// Clean up markdown. The following bug was fixed in v2, but is present in v1.
// It affects markdown output (even though the issue is referring to man pages)
// https://github.com/urfave/cli/issues/1040
@@ -51,8 +53,8 @@ func runDocs(ctx *cli.Context) error {
}
out := os.Stdout
- if ctx.String("output") != "" {
- fi, err := os.Create(ctx.String("output"))
+ if cmd.String("output") != "" {
+ fi, err := os.Create(cmd.String("output"))
if err != nil {
return err
}
diff --git a/cmd/doctor.go b/cmd/doctor.go
index 4a12b957f50..9e0fcbf8779 100644
--- a/cmd/doctor.go
+++ b/cmd/doctor.go
@@ -20,7 +20,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/doctor"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
"xorm.io/xorm"
)
@@ -30,7 +30,7 @@ var CmdDoctor = &cli.Command{
Usage: "Diagnose and optionally fix problems, convert or re-create database tables",
Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.",
- Subcommands: []*cli.Command{
+ Commands: []*cli.Command{
cmdDoctorCheck,
cmdRecreateTable,
cmdDoctorConvert,
@@ -93,16 +93,13 @@ You should back-up your database before doing this and ensure that your database
Action: runRecreateTable,
}
-func runRecreateTable(ctx *cli.Context) error {
- stdCtx, cancel := installSignals()
- defer cancel()
-
+func runRecreateTable(ctx context.Context, cmd *cli.Command) error {
// Redirect the default golog to here
golog.SetFlags(0)
golog.SetPrefix("")
golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info))
- debug := ctx.Bool("debug")
+ debug := cmd.Bool("debug")
setting.MustInstalled()
setting.LoadDBSetting()
@@ -113,15 +110,15 @@ func runRecreateTable(ctx *cli.Context) error {
}
setting.Database.LogSQL = debug
- if err := db.InitEngine(stdCtx); err != nil {
+ if err := db.InitEngine(ctx); err != nil {
fmt.Println(err)
fmt.Println("Check if you are using the right config file. You can use a --config directive to specify one.")
return nil
}
- args := ctx.Args()
- names := make([]string, 0, ctx.NArg())
- for i := 0; i < ctx.NArg(); i++ {
+ args := cmd.Args()
+ names := make([]string, 0, cmd.NArg())
+ for i := 0; i < cmd.NArg(); i++ {
names = append(names, args.Get(i))
}
@@ -131,7 +128,7 @@ func runRecreateTable(ctx *cli.Context) error {
}
recreateTables := migrate_base.RecreateTables(beans...)
- return db.InitEngineWithMigration(stdCtx, func(ctx context.Context, x *xorm.Engine) error {
+ return db.InitEngineWithMigration(ctx, func(ctx context.Context, x *xorm.Engine) error {
if err := migrations.EnsureUpToDate(ctx, x); err != nil {
return err
}
@@ -139,11 +136,11 @@ func runRecreateTable(ctx *cli.Context) error {
})
}
-func setupDoctorDefaultLogger(ctx *cli.Context, colorize bool) {
+func setupDoctorDefaultLogger(cmd *cli.Command, colorize bool) {
// Silence the default loggers
setupConsoleLogger(log.FATAL, log.CanColorStderr, os.Stderr)
- logFile := ctx.String("log-file")
+ logFile := cmd.String("log-file")
switch logFile {
case "":
return // if no doctor log-file is set, do not show any log from default logger
@@ -161,23 +158,20 @@ func setupDoctorDefaultLogger(ctx *cli.Context, colorize bool) {
}
}
-func runDoctorCheck(ctx *cli.Context) error {
- stdCtx, cancel := installSignals()
- defer cancel()
-
+func runDoctorCheck(ctx context.Context, cmd *cli.Command) error {
colorize := log.CanColorStdout
- if ctx.IsSet("color") {
- colorize = ctx.Bool("color")
+ if cmd.IsSet("color") {
+ colorize = cmd.Bool("color")
}
- setupDoctorDefaultLogger(ctx, colorize)
+ setupDoctorDefaultLogger(cmd, colorize)
// Finally redirect the default golang's log to here
golog.SetFlags(0)
golog.SetPrefix("")
golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info))
- if ctx.IsSet("list") {
+ if cmd.IsSet("list") {
w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0)
_, _ = w.Write([]byte("Default\tName\tTitle\n"))
doctor.SortChecks(doctor.Checks)
@@ -195,12 +189,12 @@ func runDoctorCheck(ctx *cli.Context) error {
}
var checks []*doctor.Check
- if ctx.Bool("all") {
+ if cmd.Bool("all") {
checks = make([]*doctor.Check, len(doctor.Checks))
copy(checks, doctor.Checks)
- } else if ctx.IsSet("run") {
- addDefault := ctx.Bool("default")
- runNamesSet := container.SetOf(ctx.StringSlice("run")...)
+ } else if cmd.IsSet("run") {
+ addDefault := cmd.Bool("default")
+ runNamesSet := container.SetOf(cmd.StringSlice("run")...)
for _, check := range doctor.Checks {
if (addDefault && check.IsDefault) || runNamesSet.Contains(check.Name) {
checks = append(checks, check)
@@ -217,5 +211,5 @@ func runDoctorCheck(ctx *cli.Context) error {
}
}
}
- return doctor.RunChecks(stdCtx, colorize, ctx.Bool("fix"), checks)
+ return doctor.RunChecks(ctx, colorize, cmd.Bool("fix"), checks)
}
diff --git a/cmd/doctor_convert.go b/cmd/doctor_convert.go
index 48c835ad0e2..8cb718d3839 100644
--- a/cmd/doctor_convert.go
+++ b/cmd/doctor_convert.go
@@ -4,13 +4,14 @@
package cmd
import (
+ "context"
"fmt"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
// cmdDoctorConvert represents the available convert sub-command.
@@ -21,11 +22,8 @@ var cmdDoctorConvert = &cli.Command{
Action: runDoctorConvert,
}
-func runDoctorConvert(ctx *cli.Context) error {
- stdCtx, cancel := installSignals()
- defer cancel()
-
- if err := initDB(stdCtx); err != nil {
+func runDoctorConvert(ctx context.Context, cmd *cli.Command) error {
+ if err := initDB(ctx); err != nil {
return err
}
diff --git a/cmd/doctor_test.go b/cmd/doctor_test.go
index 3e1ff299c5a..da942b38b60 100644
--- a/cmd/doctor_test.go
+++ b/cmd/doctor_test.go
@@ -11,7 +11,7 @@ import (
"code.gitea.io/gitea/services/doctor"
"github.com/stretchr/testify/assert"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
func TestDoctorRun(t *testing.T) {
@@ -22,12 +22,13 @@ func TestDoctorRun(t *testing.T) {
SkipDatabaseInitialization: true,
})
- app := cli.NewApp()
- app.Commands = []*cli.Command{cmdDoctorCheck}
- err := app.Run([]string{"./gitea", "check", "--run", "test-check"})
+ app := &cli.Command{
+ Commands: []*cli.Command{cmdDoctorCheck},
+ }
+ err := app.Run(t.Context(), []string{"./gitea", "check", "--run", "test-check"})
assert.NoError(t, err)
- err = app.Run([]string{"./gitea", "check", "--run", "no-such"})
+ err = app.Run(t.Context(), []string{"./gitea", "check", "--run", "no-such"})
assert.ErrorContains(t, err, `unknown checks: "no-such"`)
- err = app.Run([]string{"./gitea", "check", "--run", "test-check,no-such"})
+ err = app.Run(t.Context(), []string{"./gitea", "check", "--run", "test-check,no-such"})
assert.ErrorContains(t, err, `unknown checks: "no-such"`)
}
diff --git a/cmd/dump.go b/cmd/dump.go
index 7d640b78fdf..ed19e3d4bf0 100644
--- a/cmd/dump.go
+++ b/cmd/dump.go
@@ -5,6 +5,7 @@
package cmd
import (
+ "context"
"os"
"path"
"path/filepath"
@@ -20,7 +21,7 @@ import (
"gitea.com/go-chi/session"
"github.com/mholt/archiver/v3"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
// CmdDump represents the available dump sub-command.
@@ -101,17 +102,17 @@ func fatal(format string, args ...any) {
log.Fatal(format, args...)
}
-func runDump(ctx *cli.Context) error {
+func runDump(ctx context.Context, cmd *cli.Command) error {
setting.MustInstalled()
- quite := ctx.Bool("quiet")
- verbose := ctx.Bool("verbose")
+ quite := cmd.Bool("quiet")
+ verbose := cmd.Bool("verbose")
if verbose && quite {
fatal("Option --quiet and --verbose cannot both be set")
}
// outFileName is either "-" or a file name (will be made absolute)
- outFileName, outType := dump.PrepareFileNameAndType(ctx.String("file"), ctx.String("type"))
+ outFileName, outType := dump.PrepareFileNameAndType(cmd.String("file"), cmd.String("type"))
if outType == "" {
fatal("Invalid output type")
}
@@ -136,10 +137,7 @@ func runDump(ctx *cli.Context) error {
setting.DisableLoggerInit()
setting.LoadSettings() // cannot access session settings otherwise
- stdCtx, cancel := installSignals()
- defer cancel()
-
- err := db.InitEngine(stdCtx)
+ err := db.InitEngine(ctx)
if err != nil {
return err
}
@@ -165,7 +163,7 @@ func runDump(ctx *cli.Context) error {
}
dumper.GlobalExcludeAbsPath(outFileName)
- if ctx.IsSet("skip-repository") && ctx.Bool("skip-repository") {
+ if cmd.IsSet("skip-repository") && cmd.Bool("skip-repository") {
log.Info("Skip dumping local repositories")
} else {
log.Info("Dumping local repositories... %s", setting.RepoRootPath)
@@ -173,7 +171,7 @@ func runDump(ctx *cli.Context) error {
fatal("Failed to include repositories: %v", err)
}
- if ctx.IsSet("skip-lfs-data") && ctx.Bool("skip-lfs-data") {
+ if cmd.IsSet("skip-lfs-data") && cmd.Bool("skip-lfs-data") {
log.Info("Skip dumping LFS data")
} else if !setting.LFS.StartServer {
log.Info("LFS isn't enabled. Skip dumping LFS data")
@@ -188,12 +186,12 @@ func runDump(ctx *cli.Context) error {
}
}
- if ctx.Bool("skip-db") {
+ if cmd.Bool("skip-db") {
// Ensure that we don't dump the database file that may reside in setting.AppDataPath or elsewhere.
dumper.GlobalExcludeAbsPath(setting.Database.Path)
log.Info("Skipping database")
} else {
- tmpDir := ctx.String("tempdir")
+ tmpDir := cmd.String("tempdir")
if _, err := os.Stat(tmpDir); os.IsNotExist(err) {
fatal("Path does not exist: %s", tmpDir)
}
@@ -209,7 +207,7 @@ func runDump(ctx *cli.Context) error {
}
}()
- targetDBType := ctx.String("database")
+ targetDBType := cmd.String("database")
if len(targetDBType) > 0 && targetDBType != setting.Database.Type.String() {
log.Info("Dumping database %s => %s...", setting.Database.Type, targetDBType)
} else {
@@ -230,7 +228,7 @@ func runDump(ctx *cli.Context) error {
fatal("Failed to include specified app.ini: %v", err)
}
- if ctx.IsSet("skip-custom-dir") && ctx.Bool("skip-custom-dir") {
+ if cmd.IsSet("skip-custom-dir") && cmd.Bool("skip-custom-dir") {
log.Info("Skipping custom directory")
} else {
customDir, err := os.Stat(setting.CustomPath)
@@ -263,7 +261,7 @@ func runDump(ctx *cli.Context) error {
excludes = append(excludes, opts.ProviderConfig)
}
- if ctx.IsSet("skip-index") && ctx.Bool("skip-index") {
+ if cmd.IsSet("skip-index") && cmd.Bool("skip-index") {
excludes = append(excludes, setting.Indexer.RepoPath)
excludes = append(excludes, setting.Indexer.IssuePath)
}
@@ -278,7 +276,7 @@ func runDump(ctx *cli.Context) error {
}
}
- if ctx.IsSet("skip-attachment-data") && ctx.Bool("skip-attachment-data") {
+ if cmd.IsSet("skip-attachment-data") && cmd.Bool("skip-attachment-data") {
log.Info("Skip dumping attachment data")
} else if err := storage.Attachments.IterateObjects("", func(objPath string, object storage.Object) error {
info, err := object.Stat()
@@ -290,7 +288,7 @@ func runDump(ctx *cli.Context) error {
fatal("Failed to dump attachments: %v", err)
}
- if ctx.IsSet("skip-package-data") && ctx.Bool("skip-package-data") {
+ if cmd.IsSet("skip-package-data") && cmd.Bool("skip-package-data") {
log.Info("Skip dumping package data")
} else if !setting.Packages.Enabled {
log.Info("Packages isn't enabled. Skip dumping package data")
@@ -307,7 +305,7 @@ func runDump(ctx *cli.Context) error {
// Doesn't check if LogRootPath exists before processing --skip-log intentionally,
// ensuring that it's clear the dump is skipped whether the directory's initialized
// yet or not.
- if ctx.IsSet("skip-log") && ctx.Bool("skip-log") {
+ if cmd.IsSet("skip-log") && cmd.Bool("skip-log") {
log.Info("Skip dumping log files")
} else {
isExist, err := util.IsExist(setting.Log.RootPath)
diff --git a/cmd/dump_repo.go b/cmd/dump_repo.go
index 11d0270404a..a75b2d1b94f 100644
--- a/cmd/dump_repo.go
+++ b/cmd/dump_repo.go
@@ -19,7 +19,7 @@ import (
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/migrations"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
// CmdDumpRepository represents the available dump repository sub-command.
@@ -79,16 +79,13 @@ wiki, issues, labels, releases, release_assets, milestones, pull_requests, comme
},
}
-func runDumpRepository(ctx *cli.Context) error {
+func runDumpRepository(ctx context.Context, cmd *cli.Command) error {
setupConsoleLogger(log.INFO, log.CanColorStderr, os.Stderr)
setting.DisableLoggerInit()
setting.LoadSettings() // cannot access skip_tls_verify settings otherwise
- stdCtx, cancel := installSignals()
- defer cancel()
-
- if err := initDB(stdCtx); err != nil {
+ if err := initDB(ctx); err != nil {
return err
}
@@ -105,8 +102,8 @@ func runDumpRepository(ctx *cli.Context) error {
var (
serviceType structs.GitServiceType
- cloneAddr = ctx.String("clone_addr")
- serviceStr = ctx.String("git_service")
+ cloneAddr = cmd.String("clone_addr")
+ serviceStr = cmd.String("git_service")
)
if strings.HasPrefix(strings.ToLower(cloneAddr), "https://github.com/") {
@@ -124,13 +121,13 @@ func runDumpRepository(ctx *cli.Context) error {
opts := base.MigrateOptions{
GitServiceType: serviceType,
CloneAddr: cloneAddr,
- AuthUsername: ctx.String("auth_username"),
- AuthPassword: ctx.String("auth_password"),
- AuthToken: ctx.String("auth_token"),
- RepoName: ctx.String("repo_name"),
+ AuthUsername: cmd.String("auth_username"),
+ AuthPassword: cmd.String("auth_password"),
+ AuthToken: cmd.String("auth_token"),
+ RepoName: cmd.String("repo_name"),
}
- if len(ctx.String("units")) == 0 {
+ if len(cmd.String("units")) == 0 {
opts.Wiki = true
opts.Issues = true
opts.Milestones = true
@@ -140,8 +137,8 @@ func runDumpRepository(ctx *cli.Context) error {
opts.PullRequests = true
opts.ReleaseAssets = true
} else {
- units := strings.Split(ctx.String("units"), ",")
- for _, unit := range units {
+ units := strings.SplitSeq(cmd.String("units"), ",")
+ for unit := range units {
switch strings.ToLower(strings.TrimSpace(unit)) {
case "":
continue
@@ -169,7 +166,7 @@ func runDumpRepository(ctx *cli.Context) error {
// the repo_dir will be removed if error occurs in DumpRepository
// make sure the directory doesn't exist or is empty, prevent from deleting user files
- repoDir := ctx.String("repo_dir")
+ repoDir := cmd.String("repo_dir")
if exists, err := util.IsExist(repoDir); err != nil {
return fmt.Errorf("unable to stat repo_dir %q: %w", repoDir, err)
} else if exists {
@@ -184,7 +181,7 @@ func runDumpRepository(ctx *cli.Context) error {
if err := migrations.DumpRepository(
context.Background(),
repoDir,
- ctx.String("owner_name"),
+ cmd.String("owner_name"),
opts,
); err != nil {
log.Fatal("Failed to dump repository: %v", err)
diff --git a/cmd/embedded.go b/cmd/embedded.go
index 9f03f7be7c4..1908352453f 100644
--- a/cmd/embedded.go
+++ b/cmd/embedded.go
@@ -4,6 +4,7 @@
package cmd
import (
+ "context"
"errors"
"fmt"
"os"
@@ -19,7 +20,7 @@ import (
"code.gitea.io/gitea/modules/util"
"github.com/gobwas/glob"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
// CmdEmbedded represents the available extract sub-command.
@@ -28,7 +29,7 @@ var (
Name: "embedded",
Usage: "Extract embedded resources",
Description: "A command for extracting embedded resources, like templates and images",
- Subcommands: []*cli.Command{
+ Commands: []*cli.Command{
subcmdList,
subcmdView,
subcmdExtract,
@@ -100,7 +101,7 @@ type assetFile struct {
path string
}
-func initEmbeddedExtractor(c *cli.Context) error {
+func initEmbeddedExtractor(c *cli.Command) error {
setupConsoleLogger(log.ERROR, log.CanColorStderr, os.Stderr)
patterns, err := compileCollectPatterns(c.Args().Slice())
@@ -115,31 +116,31 @@ func initEmbeddedExtractor(c *cli.Context) error {
return nil
}
-func runList(c *cli.Context) error {
+func runList(_ context.Context, c *cli.Command) error {
if err := runListDo(c); err != nil {
- fmt.Fprintf(os.Stderr, "%v\n", err)
+ _, _ = fmt.Fprintf(os.Stderr, "%v\n", err)
return err
}
return nil
}
-func runView(c *cli.Context) error {
+func runView(_ context.Context, c *cli.Command) error {
if err := runViewDo(c); err != nil {
- fmt.Fprintf(os.Stderr, "%v\n", err)
+ _, _ = fmt.Fprintf(os.Stderr, "%v\n", err)
return err
}
return nil
}
-func runExtract(c *cli.Context) error {
+func runExtract(_ context.Context, c *cli.Command) error {
if err := runExtractDo(c); err != nil {
- fmt.Fprintf(os.Stderr, "%v\n", err)
+ _, _ = fmt.Fprintf(os.Stderr, "%v\n", err)
return err
}
return nil
}
-func runListDo(c *cli.Context) error {
+func runListDo(c *cli.Command) error {
if err := initEmbeddedExtractor(c); err != nil {
return err
}
@@ -151,7 +152,7 @@ func runListDo(c *cli.Context) error {
return nil
}
-func runViewDo(c *cli.Context) error {
+func runViewDo(c *cli.Command) error {
if err := initEmbeddedExtractor(c); err != nil {
return err
}
@@ -174,7 +175,7 @@ func runViewDo(c *cli.Context) error {
return nil
}
-func runExtractDo(c *cli.Context) error {
+func runExtractDo(c *cli.Command) error {
if err := initEmbeddedExtractor(c); err != nil {
return err
}
@@ -216,7 +217,7 @@ func runExtractDo(c *cli.Context) error {
for _, a := range matchedAssetFiles {
if err := extractAsset(destdir, a, overwrite, rename); err != nil {
// Non-fatal error
- fmt.Fprintf(os.Stderr, "%s: %v", a.path, err)
+ _, _ = fmt.Fprintf(os.Stderr, "%s: %v\n", a.path, err)
}
}
@@ -271,7 +272,7 @@ func extractAsset(d string, a assetFile, overwrite, rename bool) error {
return nil
}
-func collectAssetFilesByPattern(c *cli.Context, globs []glob.Glob, path string, layer *assetfs.Layer) {
+func collectAssetFilesByPattern(c *cli.Command, globs []glob.Glob, path string, layer *assetfs.Layer) {
fs := assetfs.Layered(layer)
files, err := fs.ListAllFiles(".", true)
if err != nil {
@@ -294,16 +295,14 @@ func collectAssetFilesByPattern(c *cli.Context, globs []glob.Glob, path string,
}
}
-func compileCollectPatterns(args []string) ([]glob.Glob, error) {
+func compileCollectPatterns(args []string) (_ []glob.Glob, err error) {
if len(args) == 0 {
args = []string{"**"}
}
pat := make([]glob.Glob, len(args))
for i := range args {
- if g, err := glob.Compile(args[i], '/'); err != nil {
- return nil, fmt.Errorf("'%s': Invalid glob pattern: %w", args[i], err)
- } else { //nolint:revive
- pat[i] = g
+ if pat[i], err = glob.Compile(args[i], '/'); err != nil {
+ return nil, fmt.Errorf("invalid glob patterh %q: %w", args[i], err)
}
}
return pat, nil
diff --git a/cmd/generate.go b/cmd/generate.go
index 90b32ecaf0e..cf491604efa 100644
--- a/cmd/generate.go
+++ b/cmd/generate.go
@@ -5,13 +5,14 @@
package cmd
import (
+ "context"
"fmt"
"os"
"code.gitea.io/gitea/modules/generate"
"github.com/mattn/go-isatty"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
var (
@@ -19,7 +20,7 @@ var (
CmdGenerate = &cli.Command{
Name: "generate",
Usage: "Generate Gitea's secrets/keys/tokens",
- Subcommands: []*cli.Command{
+ Commands: []*cli.Command{
subcmdSecret,
},
}
@@ -27,7 +28,7 @@ var (
subcmdSecret = &cli.Command{
Name: "secret",
Usage: "Generate a secret token",
- Subcommands: []*cli.Command{
+ Commands: []*cli.Command{
microcmdGenerateInternalToken,
microcmdGenerateLfsJwtSecret,
microcmdGenerateSecretKey,
@@ -54,7 +55,7 @@ var (
}
)
-func runGenerateInternalToken(c *cli.Context) error {
+func runGenerateInternalToken(_ context.Context, c *cli.Command) error {
internalToken, err := generate.NewInternalToken()
if err != nil {
return err
@@ -69,7 +70,7 @@ func runGenerateInternalToken(c *cli.Context) error {
return nil
}
-func runGenerateLfsJwtSecret(c *cli.Context) error {
+func runGenerateLfsJwtSecret(_ context.Context, c *cli.Command) error {
_, jwtSecretBase64, err := generate.NewJwtSecretWithBase64()
if err != nil {
return err
@@ -84,7 +85,7 @@ func runGenerateLfsJwtSecret(c *cli.Context) error {
return nil
}
-func runGenerateSecretKey(c *cli.Context) error {
+func runGenerateSecretKey(_ context.Context, c *cli.Command) error {
secretKey, err := generate.NewSecretKey()
if err != nil {
return err
diff --git a/cmd/hook.go b/cmd/hook.go
index 41e3c3ce340..2ce272b411e 100644
--- a/cmd/hook.go
+++ b/cmd/hook.go
@@ -20,11 +20,11 @@ import (
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
const (
- hookBatchSize = 30
+ hookBatchSize = 500
)
var (
@@ -34,7 +34,7 @@ var (
Usage: "(internal) Should only be called by Git",
Description: "Delegate commands to corresponding Git hooks",
Before: PrepareConsoleLoggerLevel(log.FATAL),
- Subcommands: []*cli.Command{
+ Commands: []*cli.Command{
subcmdHookPreReceive,
subcmdHookUpdate,
subcmdHookPostReceive,
@@ -161,12 +161,10 @@ func (n *nilWriter) WriteString(s string) (int, error) {
return len(s), nil
}
-func runHookPreReceive(c *cli.Context) error {
+func runHookPreReceive(ctx context.Context, c *cli.Command) error {
if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
return nil
}
- ctx, cancel := installSignals()
- defer cancel()
setup(ctx, c.Bool("debug"))
@@ -292,7 +290,7 @@ Gitea or set your environment appropriately.`, "")
// runHookUpdate avoid to do heavy operations on update hook because it will be
// invoked for every ref update which does not like pre-receive and post-receive
-func runHookUpdate(c *cli.Context) error {
+func runHookUpdate(_ context.Context, c *cli.Command) error {
if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
return nil
}
@@ -309,15 +307,12 @@ func runHookUpdate(c *cli.Context) error {
return nil
}
-func runHookPostReceive(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runHookPostReceive(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
// First of all run update-server-info no matter what
if _, _, err := git.NewCommand("update-server-info").RunStdString(ctx, nil); err != nil {
- return fmt.Errorf("Failed to call 'git update-server-info': %w", err)
+ return fmt.Errorf("failed to call 'git update-server-info': %w", err)
}
// Now if we're an internal don't do anything else
@@ -485,7 +480,7 @@ func hookPrintResult(output, isCreate bool, branch, url string) {
func pushOptions() map[string]string {
opts := make(map[string]string)
if pushCount, err := strconv.Atoi(os.Getenv(private.GitPushOptionCount)); err == nil {
- for idx := 0; idx < pushCount; idx++ {
+ for idx := range pushCount {
opt := os.Getenv(fmt.Sprintf("GIT_PUSH_OPTION_%d", idx))
kv := strings.SplitN(opt, "=", 2)
if len(kv) == 2 {
@@ -496,10 +491,7 @@ func pushOptions() map[string]string {
return opts
}
-func runHookProcReceive(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runHookProcReceive(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
@@ -740,7 +732,7 @@ func readPktLine(ctx context.Context, in *bufio.Reader, requestType pktLineType)
// read prefix
lengthBytes := make([]byte, 4)
- for i := 0; i < 4; i++ {
+ for i := range 4 {
lengthBytes[i], err = in.ReadByte()
if err != nil {
return nil, fail(ctx, "Protocol: stdin error", "Pkt-Line: read stdin failed : %v", err)
diff --git a/cmd/keys.go b/cmd/keys.go
index 7fdbe16119f..8710756a813 100644
--- a/cmd/keys.go
+++ b/cmd/keys.go
@@ -4,6 +4,7 @@
package cmd
import (
+ "context"
"errors"
"fmt"
"strings"
@@ -11,7 +12,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
// CmdKeys represents the available keys sub-command
@@ -49,7 +50,7 @@ var CmdKeys = &cli.Command{
},
}
-func runKeys(c *cli.Context) error {
+func runKeys(ctx context.Context, c *cli.Command) error {
if !c.IsSet("username") {
return errors.New("No username provided")
}
@@ -68,9 +69,6 @@ func runKeys(c *cli.Context) error {
return errors.New("No key type and content provided")
}
- ctx, cancel := installSignals()
- defer cancel()
-
setup(ctx, c.Bool("debug"))
authorizedString, extra := private.AuthorizedPublicKeyByContent(ctx, content)
@@ -78,6 +76,6 @@ func runKeys(c *cli.Context) error {
if extra.Error != nil {
return extra.Error
}
- _, _ = fmt.Fprintln(c.App.Writer, strings.TrimSpace(authorizedString.Text))
+ _, _ = fmt.Fprintln(c.Root().Writer, strings.TrimSpace(authorizedString.Text))
return nil
}
diff --git a/cmd/mailer.go b/cmd/mailer.go
index 0c5f2c8c8d4..72bd8e56012 100644
--- a/cmd/mailer.go
+++ b/cmd/mailer.go
@@ -4,24 +4,18 @@
package cmd
import (
+ "context"
"fmt"
"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/setting"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
-func runSendMail(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runSendMail(ctx context.Context, c *cli.Command) error {
setting.MustInstalled()
- if err := argsSet(c, "title"); err != nil {
- return err
- }
-
subject := c.String("title")
confirmSkiped := c.Bool("force")
body := c.String("content")
diff --git a/cmd/main.go b/cmd/main.go
index 7251bd09a3f..3b8a8a93116 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -4,6 +4,7 @@
package cmd
import (
+ "context"
"fmt"
"os"
"strings"
@@ -11,7 +12,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
// cmdHelp is our own help subcommand with more information
@@ -22,18 +23,18 @@ func cmdHelp() *cli.Command {
Aliases: []string{"h"},
Usage: "Shows a list of commands or help for one command",
ArgsUsage: "[command]",
- Action: func(c *cli.Context) (err error) {
- lineage := c.Lineage() // The order is from child to parent: help, doctor, Gitea, {Command:nil}
+ Action: func(ctx context.Context, c *cli.Command) (err error) {
+ lineage := c.Lineage() // The order is from child to parent: help, doctor, Gitea
targetCmdIdx := 0
- if c.Command.Name == "help" {
+ if c.Name == "help" {
targetCmdIdx = 1
}
- if lineage[targetCmdIdx+1].Command != nil {
- err = cli.ShowCommandHelp(lineage[targetCmdIdx+1], lineage[targetCmdIdx].Command.Name)
+ if lineage[targetCmdIdx] != lineage[targetCmdIdx].Root() {
+ err = cli.ShowCommandHelp(ctx, lineage[targetCmdIdx+1] /* parent cmd */, lineage[targetCmdIdx].Name /* sub cmd */)
} else {
err = cli.ShowAppHelp(c)
}
- _, _ = fmt.Fprintf(c.App.Writer, `
+ _, _ = fmt.Fprintf(c.Root().Writer, `
DEFAULT CONFIGURATION:
AppPath: %s
WorkPath: %s
@@ -74,25 +75,25 @@ func appGlobalFlags() []cli.Flag {
}
}
-func prepareSubcommandWithConfig(command *cli.Command, globalFlags []cli.Flag) {
- command.Flags = append(append([]cli.Flag{}, globalFlags...), command.Flags...)
+func prepareSubcommandWithGlobalFlags(command *cli.Command) {
+ command.Flags = append(append([]cli.Flag{}, appGlobalFlags()...), command.Flags...)
command.Action = prepareWorkPathAndCustomConf(command.Action)
command.HideHelp = true
if command.Name != "help" {
- command.Subcommands = append(command.Subcommands, cmdHelp())
+ command.Commands = append(command.Commands, cmdHelp())
}
- for i := range command.Subcommands {
- prepareSubcommandWithConfig(command.Subcommands[i], globalFlags)
+ for i := range command.Commands {
+ prepareSubcommandWithGlobalFlags(command.Commands[i])
}
}
// prepareWorkPathAndCustomConf wraps the Action to prepare the work path and custom config
// It can't use "Before", because each level's sub-command's Before will be called one by one, so the "init" would be done multiple times
-func prepareWorkPathAndCustomConf(action cli.ActionFunc) func(ctx *cli.Context) error {
- return func(ctx *cli.Context) error {
+func prepareWorkPathAndCustomConf(action cli.ActionFunc) func(context.Context, *cli.Command) error {
+ return func(ctx context.Context, cmd *cli.Command) error {
var args setting.ArgWorkPathAndCustomConf
// from children to parent, check the global flags
- for _, curCtx := range ctx.Lineage() {
+ for _, curCtx := range cmd.Lineage() {
if curCtx.IsSet("work-path") && args.WorkPath == "" {
args.WorkPath = curCtx.String("work-path")
}
@@ -104,11 +105,11 @@ func prepareWorkPathAndCustomConf(action cli.ActionFunc) func(ctx *cli.Context)
}
}
setting.InitWorkPathAndCommonConfig(os.Getenv, args)
- if ctx.Bool("help") || action == nil {
+ if cmd.Bool("help") || action == nil {
// the default behavior of "urfave/cli": "nil action" means "show help"
- return cmdHelp().Action(ctx)
+ return cmdHelp().Action(ctx, cmd)
}
- return action(ctx)
+ return action(ctx, cmd)
}
}
@@ -117,14 +118,13 @@ type AppVersion struct {
Extra string
}
-func NewMainApp(appVer AppVersion) *cli.App {
- app := cli.NewApp()
- app.Name = "Gitea"
- app.HelpName = "gitea"
+func NewMainApp(appVer AppVersion) *cli.Command {
+ app := &cli.Command{}
+ app.Name = "gitea" // must be lower-cased because it appears in the "USAGE" section like "gitea doctor [command [command options]]"
app.Usage = "A painless self-hosted Git service"
app.Description = `Gitea program contains "web" and other subcommands. If no subcommand is given, it starts the web server by default. Use "web" subcommand for more web server arguments, use other subcommands for other purposes.`
app.Version = appVer.Version + appVer.Extra
- app.EnableBashCompletion = true
+ app.EnableShellCompletion = true
// these sub-commands need to use config file
subCmdWithConfig := []*cli.Command{
@@ -147,20 +147,21 @@ func NewMainApp(appVer AppVersion) *cli.App {
// these sub-commands do not need the config file, and they do not depend on any path or environment variable.
subCmdStandalone := []*cli.Command{
- CmdCert,
+ cmdCert(),
CmdGenerate,
CmdDocs,
}
+ // TODO: we should eventually drop the default command,
+ // but not sure whether it would break Windows users who used to double-click the EXE to run.
app.DefaultCommand = CmdWeb.Name
- globalFlags := appGlobalFlags()
app.Flags = append(app.Flags, cli.VersionFlag)
- app.Flags = append(app.Flags, globalFlags...)
+ app.Flags = append(app.Flags, appGlobalFlags()...)
app.HideHelp = true // use our own help action to show helps (with more information like default config)
app.Before = PrepareConsoleLoggerLevel(log.INFO)
for i := range subCmdWithConfig {
- prepareSubcommandWithConfig(subCmdWithConfig[i], globalFlags)
+ prepareSubcommandWithGlobalFlags(subCmdWithConfig[i])
}
app.Commands = append(app.Commands, subCmdWithConfig...)
app.Commands = append(app.Commands, subCmdStandalone...)
@@ -169,8 +170,10 @@ func NewMainApp(appVer AppVersion) *cli.App {
return app
}
-func RunMainApp(app *cli.App, args ...string) error {
- err := app.Run(args)
+func RunMainApp(app *cli.Command, args ...string) error {
+ ctx, cancel := installSignals()
+ defer cancel()
+ err := app.Run(ctx, args)
if err == nil {
return nil
}
diff --git a/cmd/main_test.go b/cmd/main_test.go
index 9573cacbd4d..7dfa87a0ef0 100644
--- a/cmd/main_test.go
+++ b/cmd/main_test.go
@@ -4,6 +4,7 @@
package cmd
import (
+ "context"
"errors"
"fmt"
"io"
@@ -16,7 +17,7 @@ import (
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
func TestMain(m *testing.M) {
@@ -27,10 +28,10 @@ func makePathOutput(workPath, customPath, customConf string) string {
return fmt.Sprintf("WorkPath=%s\nCustomPath=%s\nCustomConf=%s", workPath, customPath, customConf)
}
-func newTestApp(testCmdAction func(ctx *cli.Context) error) *cli.App {
+func newTestApp(testCmdAction cli.ActionFunc) *cli.Command {
app := NewMainApp(AppVersion{})
testCmd := &cli.Command{Name: "test-cmd", Action: testCmdAction}
- prepareSubcommandWithConfig(testCmd, appGlobalFlags())
+ prepareSubcommandWithGlobalFlags(testCmd)
app.Commands = append(app.Commands, testCmd)
app.DefaultCommand = testCmd.Name
return app
@@ -42,7 +43,7 @@ type runResult struct {
ExitCode int
}
-func runTestApp(app *cli.App, args ...string) (runResult, error) {
+func runTestApp(app *cli.Command, args ...string) (runResult, error) {
outBuf := new(strings.Builder)
errBuf := new(strings.Builder)
app.Writer = outBuf
@@ -65,7 +66,7 @@ func TestCliCmd(t *testing.T) {
defaultCustomConf := filepath.Join(defaultCustomPath, "conf/app.ini")
cli.CommandHelpTemplate = "(command help template)"
- cli.AppHelpTemplate = "(app help template)"
+ cli.RootCommandHelpTemplate = "(app help template)"
cli.SubcommandHelpTemplate = "(subcommand help template)"
cases := []struct {
@@ -109,12 +110,12 @@ func TestCliCmd(t *testing.T) {
},
}
- app := newTestApp(func(ctx *cli.Context) error {
- _, _ = fmt.Fprint(ctx.App.Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf))
- return nil
- })
for _, c := range cases {
t.Run(c.cmd, func(t *testing.T) {
+ app := newTestApp(func(ctx context.Context, cmd *cli.Command) error {
+ _, _ = fmt.Fprint(cmd.Root().Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf))
+ return nil
+ })
for k, v := range c.env {
t.Setenv(k, v)
}
@@ -128,28 +129,28 @@ func TestCliCmd(t *testing.T) {
}
func TestCliCmdError(t *testing.T) {
- app := newTestApp(func(ctx *cli.Context) error { return errors.New("normal error") })
+ app := newTestApp(func(ctx context.Context, cmd *cli.Command) error { return errors.New("normal error") })
r, err := runTestApp(app, "./gitea", "test-cmd")
assert.Error(t, err)
assert.Equal(t, 1, r.ExitCode)
assert.Empty(t, r.Stdout)
assert.Equal(t, "Command error: normal error\n", r.Stderr)
- app = newTestApp(func(ctx *cli.Context) error { return cli.Exit("exit error", 2) })
+ app = newTestApp(func(ctx context.Context, cmd *cli.Command) error { return cli.Exit("exit error", 2) })
r, err = runTestApp(app, "./gitea", "test-cmd")
assert.Error(t, err)
assert.Equal(t, 2, r.ExitCode)
assert.Empty(t, r.Stdout)
assert.Equal(t, "exit error\n", r.Stderr)
- app = newTestApp(func(ctx *cli.Context) error { return nil })
+ app = newTestApp(func(ctx context.Context, cmd *cli.Command) error { return nil })
r, err = runTestApp(app, "./gitea", "test-cmd", "--no-such")
assert.Error(t, err)
assert.Equal(t, 1, r.ExitCode)
- assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n\n", r.Stdout)
- assert.Empty(t, r.Stderr) // the cli package's strange behavior, the error message is not in stderr ....
+ assert.Empty(t, r.Stdout)
+ assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n\n", r.Stderr)
- app = newTestApp(func(ctx *cli.Context) error { return nil })
+ app = newTestApp(func(ctx context.Context, cmd *cli.Command) error { return nil })
r, err = runTestApp(app, "./gitea", "test-cmd")
assert.NoError(t, err)
assert.Equal(t, -1, r.ExitCode) // the cli.OsExiter is not called
diff --git a/cmd/manager.go b/cmd/manager.go
index bd2da8edc78..f0935ea0657 100644
--- a/cmd/manager.go
+++ b/cmd/manager.go
@@ -4,12 +4,13 @@
package cmd
import (
+ "context"
"os"
"time"
"code.gitea.io/gitea/modules/private"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
var (
@@ -18,7 +19,7 @@ var (
Name: "manager",
Usage: "Manage the running gitea process",
Description: "This is a command for managing the running gitea process",
- Subcommands: []*cli.Command{
+ Commands: []*cli.Command{
subcmdShutdown,
subcmdRestart,
subcmdReloadTemplates,
@@ -108,46 +109,31 @@ var (
}
)
-func runShutdown(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runShutdown(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
extra := private.Shutdown(ctx)
return handleCliResponseExtra(extra)
}
-func runRestart(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runRestart(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
extra := private.Restart(ctx)
return handleCliResponseExtra(extra)
}
-func runReloadTemplates(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runReloadTemplates(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
extra := private.ReloadTemplates(ctx)
return handleCliResponseExtra(extra)
}
-func runFlushQueues(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runFlushQueues(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
extra := private.FlushQueues(ctx, c.Duration("timeout"), c.Bool("non-blocking"))
return handleCliResponseExtra(extra)
}
-func runProcesses(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runProcesses(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
extra := private.Processes(ctx, os.Stdout, c.Bool("flat"), c.Bool("no-system"), c.Bool("stacktraces"), c.Bool("json"), c.String("cancel"))
return handleCliResponseExtra(extra)
diff --git a/cmd/manager_logging.go b/cmd/manager_logging.go
index c2ae25ec572..ac29e7d3e50 100644
--- a/cmd/manager_logging.go
+++ b/cmd/manager_logging.go
@@ -4,6 +4,7 @@
package cmd
import (
+ "context"
"errors"
"fmt"
"os"
@@ -11,7 +12,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
var (
@@ -60,7 +61,7 @@ var (
subcmdLogging = &cli.Command{
Name: "logging",
Usage: "Adjust logging commands",
- Subcommands: []*cli.Command{
+ Commands: []*cli.Command{
{
Name: "pause",
Usage: "Pause logging (Gitea will buffer logs up to a certain point and will drop them after that point)",
@@ -104,7 +105,7 @@ var (
}, {
Name: "add",
Usage: "Add a logger",
- Subcommands: []*cli.Command{
+ Commands: []*cli.Command{
{
Name: "file",
Usage: "Add a file logger",
@@ -118,7 +119,6 @@ var (
Name: "rotate",
Aliases: []string{"r"},
Usage: "Rotate logs",
- Value: true,
},
&cli.Int64Flag{
Name: "max-size",
@@ -129,7 +129,6 @@ var (
Name: "daily",
Aliases: []string{"d"},
Usage: "Rotate logs daily",
- Value: true,
},
&cli.IntFlag{
Name: "max-days",
@@ -140,7 +139,6 @@ var (
Name: "compress",
Aliases: []string{"z"},
Usage: "Compress rotated logs",
- Value: true,
},
&cli.IntFlag{
Name: "compression-level",
@@ -195,10 +193,7 @@ var (
}
)
-func runRemoveLogger(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runRemoveLogger(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
logger := c.String("logger")
if len(logger) == 0 {
@@ -210,10 +205,7 @@ func runRemoveLogger(c *cli.Context) error {
return handleCliResponseExtra(extra)
}
-func runAddConnLogger(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runAddConnLogger(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
vals := map[string]any{}
mode := "conn"
@@ -237,13 +229,10 @@ func runAddConnLogger(c *cli.Context) error {
if c.IsSet("reconnect-on-message") {
vals["reconnectOnMsg"] = c.Bool("reconnect-on-message")
}
- return commonAddLogger(c, mode, vals)
+ return commonAddLogger(ctx, c, mode, vals)
}
-func runAddFileLogger(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runAddFileLogger(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
vals := map[string]any{}
mode := "file"
@@ -270,10 +259,10 @@ func runAddFileLogger(c *cli.Context) error {
if c.IsSet("compression-level") {
vals["compressionLevel"] = c.Int("compression-level")
}
- return commonAddLogger(c, mode, vals)
+ return commonAddLogger(ctx, c, mode, vals)
}
-func commonAddLogger(c *cli.Context, mode string, vals map[string]any) error {
+func commonAddLogger(ctx context.Context, c *cli.Command, mode string, vals map[string]any) error {
if len(c.String("level")) > 0 {
vals["level"] = log.LevelFromString(c.String("level")).String()
}
@@ -300,46 +289,33 @@ func commonAddLogger(c *cli.Context, mode string, vals map[string]any) error {
if c.IsSet("writer") {
writer = c.String("writer")
}
- ctx, cancel := installSignals()
- defer cancel()
extra := private.AddLogger(ctx, logger, writer, mode, vals)
return handleCliResponseExtra(extra)
}
-func runPauseLogging(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runPauseLogging(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
userMsg := private.PauseLogging(ctx)
_, _ = fmt.Fprintln(os.Stdout, userMsg)
return nil
}
-func runResumeLogging(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runResumeLogging(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
userMsg := private.ResumeLogging(ctx)
_, _ = fmt.Fprintln(os.Stdout, userMsg)
return nil
}
-func runReleaseReopenLogging(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runReleaseReopenLogging(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
userMsg := private.ReleaseReopenLogging(ctx)
_, _ = fmt.Fprintln(os.Stdout, userMsg)
return nil
}
-func runSetLogSQL(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
+func runSetLogSQL(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))
extra := private.SetLogSQL(ctx, !c.Bool("off"))
diff --git a/cmd/migrate.go b/cmd/migrate.go
index 25d8b50c45c..e24dc9e5720 100644
--- a/cmd/migrate.go
+++ b/cmd/migrate.go
@@ -11,7 +11,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/versioned_migration"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
// CmdMigrate represents the available migrate sub-command.
@@ -22,11 +22,8 @@ var CmdMigrate = &cli.Command{
Action: runMigrate,
}
-func runMigrate(ctx *cli.Context) error {
- stdCtx, cancel := installSignals()
- defer cancel()
-
- if err := initDB(stdCtx); err != nil {
+func runMigrate(ctx context.Context, c *cli.Command) error {
+ if err := initDB(ctx); err != nil {
return err
}
diff --git a/cmd/migrate_storage.go b/cmd/migrate_storage.go
index f9ed140395f..2c63e15f509 100644
--- a/cmd/migrate_storage.go
+++ b/cmd/migrate_storage.go
@@ -22,7 +22,7 @@ import (
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/services/versioned_migration"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
// CmdMigrateStorage represents the available migrate storage sub-command.
@@ -213,11 +213,8 @@ func migrateActionsArtifacts(ctx context.Context, dstStorage storage.ObjectStora
})
}
-func runMigrateStorage(ctx *cli.Context) error {
- stdCtx, cancel := installSignals()
- defer cancel()
-
- if err := initDB(stdCtx); err != nil {
+func runMigrateStorage(ctx context.Context, cmd *cli.Command) error {
+ if err := initDB(ctx); err != nil {
return err
}
@@ -238,51 +235,51 @@ func runMigrateStorage(ctx *cli.Context) error {
var dstStorage storage.ObjectStorage
var err error
- switch strings.ToLower(ctx.String("storage")) {
+ switch strings.ToLower(cmd.String("storage")) {
case "":
fallthrough
case string(setting.LocalStorageType):
- p := ctx.String("path")
+ p := cmd.String("path")
if p == "" {
log.Fatal("Path must be given when storage is local")
return nil
}
dstStorage, err = storage.NewLocalStorage(
- stdCtx,
+ ctx,
&setting.Storage{
Path: p,
})
case string(setting.MinioStorageType):
dstStorage, err = storage.NewMinioStorage(
- stdCtx,
+ ctx,
&setting.Storage{
MinioConfig: setting.MinioStorageConfig{
- Endpoint: ctx.String("minio-endpoint"),
- AccessKeyID: ctx.String("minio-access-key-id"),
- SecretAccessKey: ctx.String("minio-secret-access-key"),
- Bucket: ctx.String("minio-bucket"),
- Location: ctx.String("minio-location"),
- BasePath: ctx.String("minio-base-path"),
- UseSSL: ctx.Bool("minio-use-ssl"),
- InsecureSkipVerify: ctx.Bool("minio-insecure-skip-verify"),
- ChecksumAlgorithm: ctx.String("minio-checksum-algorithm"),
- BucketLookUpType: ctx.String("minio-bucket-lookup-type"),
+ Endpoint: cmd.String("minio-endpoint"),
+ AccessKeyID: cmd.String("minio-access-key-id"),
+ SecretAccessKey: cmd.String("minio-secret-access-key"),
+ Bucket: cmd.String("minio-bucket"),
+ Location: cmd.String("minio-location"),
+ BasePath: cmd.String("minio-base-path"),
+ UseSSL: cmd.Bool("minio-use-ssl"),
+ InsecureSkipVerify: cmd.Bool("minio-insecure-skip-verify"),
+ ChecksumAlgorithm: cmd.String("minio-checksum-algorithm"),
+ BucketLookUpType: cmd.String("minio-bucket-lookup-type"),
},
})
case string(setting.AzureBlobStorageType):
dstStorage, err = storage.NewAzureBlobStorage(
- stdCtx,
+ ctx,
&setting.Storage{
AzureBlobConfig: setting.AzureBlobStorageConfig{
- Endpoint: ctx.String("azureblob-endpoint"),
- AccountName: ctx.String("azureblob-account-name"),
- AccountKey: ctx.String("azureblob-account-key"),
- Container: ctx.String("azureblob-container"),
- BasePath: ctx.String("azureblob-base-path"),
+ Endpoint: cmd.String("azureblob-endpoint"),
+ AccountName: cmd.String("azureblob-account-name"),
+ AccountKey: cmd.String("azureblob-account-key"),
+ Container: cmd.String("azureblob-container"),
+ BasePath: cmd.String("azureblob-base-path"),
},
})
default:
- return fmt.Errorf("unsupported storage type: %s", ctx.String("storage"))
+ return fmt.Errorf("unsupported storage type: %s", cmd.String("storage"))
}
if err != nil {
return err
@@ -299,14 +296,14 @@ func runMigrateStorage(ctx *cli.Context) error {
"actions-artifacts": migrateActionsArtifacts,
}
- tp := strings.ToLower(ctx.String("type"))
+ tp := strings.ToLower(cmd.String("type"))
if m, ok := migratedMethods[tp]; ok {
- if err := m(stdCtx, dstStorage); err != nil {
+ if err := m(ctx, dstStorage); err != nil {
return err
}
log.Info("%s files have successfully been copied to the new storage.", tp)
return nil
}
- return fmt.Errorf("unsupported storage: %s", ctx.String("type"))
+ return fmt.Errorf("unsupported storage: %s", cmd.String("type"))
}
diff --git a/cmd/restore_repo.go b/cmd/restore_repo.go
index 37b32aa3045..c61f5a582ef 100644
--- a/cmd/restore_repo.go
+++ b/cmd/restore_repo.go
@@ -4,12 +4,13 @@
package cmd
import (
+ "context"
"strings"
"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/setting"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
// CmdRestoreRepository represents the available restore a repository sub-command.
@@ -48,10 +49,7 @@ wiki, issues, labels, releases, release_assets, milestones, pull_requests, comme
},
}
-func runRestoreRepository(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runRestoreRepository(ctx context.Context, c *cli.Command) error {
setting.MustInstalled()
var units []string
if s := c.String("units"); s != "" {
diff --git a/cmd/serv.go b/cmd/serv.go
index 26a3af50f32..8c6001e7274 100644
--- a/cmd/serv.go
+++ b/cmd/serv.go
@@ -33,7 +33,7 @@ import (
"github.com/golang-jwt/jwt/v5"
"github.com/kballard/go-shellquote"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
// CmdServ represents the available serv sub-command.
@@ -152,10 +152,7 @@ func getLFSAuthToken(ctx context.Context, lfsVerb string, results *private.ServC
return "Bearer " + tokenString, nil
}
-func runServ(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runServ(ctx context.Context, c *cli.Command) error {
// FIXME: This needs to internationalised
setup(ctx, c.Bool("debug"))
@@ -215,7 +212,7 @@ func runServ(c *cli.Context) error {
if git.DefaultFeatures().SupportProcReceive {
// for AGit Flow
if cmd == "ssh_info" {
- fmt.Print(`{"type":"gitea","version":1}`)
+ fmt.Print(`{"type":"agit","version":1}`)
return nil
}
}
diff --git a/cmd/web.go b/cmd/web.go
index e47b171455c..61ee3cbc203 100644
--- a/cmd/web.go
+++ b/cmd/web.go
@@ -28,7 +28,7 @@ import (
"code.gitea.io/gitea/routers/install"
"github.com/felixge/fgprof"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
// PIDFile could be set from build tag
@@ -130,19 +130,19 @@ func showWebStartupMessage(msg string) {
}
}
-func serveInstall(ctx *cli.Context) error {
+func serveInstall(cmd *cli.Command) error {
showWebStartupMessage("Prepare to run install page")
routers.InitWebInstallPage(graceful.GetManager().HammerContext())
// Flag for port number in case first time run conflict
- if ctx.IsSet("port") {
- if err := setPort(ctx.String("port")); err != nil {
+ if cmd.IsSet("port") {
+ if err := setPort(cmd.String("port")); err != nil {
return err
}
}
- if ctx.IsSet("install-port") {
- if err := setPort(ctx.String("install-port")); err != nil {
+ if cmd.IsSet("install-port") {
+ if err := setPort(cmd.String("install-port")); err != nil {
return err
}
}
@@ -163,7 +163,7 @@ func serveInstall(ctx *cli.Context) error {
return nil
}
-func serveInstalled(ctx *cli.Context) error {
+func serveInstalled(c *cli.Command) error {
setting.InitCfgProvider(setting.CustomConf)
setting.LoadCommonSettings()
setting.MustInstalled()
@@ -218,8 +218,8 @@ func serveInstalled(ctx *cli.Context) error {
setting.AppDataTempDir("").RemoveOutdated(3 * 24 * time.Hour)
// Override the provided port number within the configuration
- if ctx.IsSet("port") {
- if err := setPort(ctx.String("port")); err != nil {
+ if c.IsSet("port") {
+ if err := setPort(c.String("port")); err != nil {
return err
}
}
@@ -244,13 +244,17 @@ func servePprof() {
finished()
}
-func runWeb(ctx *cli.Context) error {
+func runWeb(_ context.Context, cmd *cli.Command) error {
defer func() {
if panicked := recover(); panicked != nil {
log.Fatal("PANIC: %v\n%s", panicked, log.Stack(2))
}
}()
+ if subCmdName, valid := isValidDefaultSubCommand(cmd); !valid {
+ return fmt.Errorf("unknown command: %s", subCmdName)
+ }
+
managerCtx, cancel := context.WithCancel(context.Background())
graceful.InitManager(managerCtx)
defer cancel()
@@ -262,12 +266,12 @@ func runWeb(ctx *cli.Context) error {
}
// Set pid file setting
- if ctx.IsSet("pid") {
- createPIDFile(ctx.String("pid"))
+ if cmd.IsSet("pid") {
+ createPIDFile(cmd.String("pid"))
}
if !setting.InstallLock {
- if err := serveInstall(ctx); err != nil {
+ if err := serveInstall(cmd); err != nil {
return err
}
} else {
@@ -278,7 +282,7 @@ func runWeb(ctx *cli.Context) error {
go servePprof()
}
- return serveInstalled(ctx)
+ return serveInstalled(cmd)
}
func setPort(port string) error {
diff --git a/cmd/web_graceful.go b/cmd/web_graceful.go
index 996537be3b5..5e06d2c2164 100644
--- a/cmd/web_graceful.go
+++ b/cmd/web_graceful.go
@@ -23,12 +23,6 @@ func NoHTTPRedirector() {
graceful.GetManager().InformCleanup()
}
-// NoMainListener tells our cleanup routine that we will not be using a possibly provided listener
-// for our main HTTP/HTTPS service
-func NoMainListener() {
- graceful.GetManager().InformCleanup()
-}
-
// NoInstallListener tells our cleanup routine that we will not be using a possibly provided listener
// for our install HTTP/HTTPS service
func NoInstallListener() {
diff --git a/contrib/backport/backport.go b/contrib/backport/backport.go
index 44e4eacf906..2052295fb14 100644
--- a/contrib/backport/backport.go
+++ b/contrib/backport/backport.go
@@ -1,7 +1,7 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
-//nolint:forbidigo
+//nolint:forbidigo // use of print functions is allowed in cli
package main
import (
@@ -12,21 +12,19 @@ import (
"net/http"
"os"
"os/exec"
- "os/signal"
"path"
"strconv"
"strings"
- "syscall"
"github.com/google/go-github/v71/github"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
"gopkg.in/yaml.v3"
)
const defaultVersion = "v1.18" // to backport to
func main() {
- app := cli.NewApp()
+ app := &cli.Command{}
app.Name = "backport"
app.Usage = "Backport provided PR-number on to the current or previous released version"
app.Description = `Backport will look-up the PR in Gitea's git log and attempt to cherry-pick it on the current version`
@@ -91,7 +89,7 @@ func main() {
Usage: "Set this flag to continue from a git cherry-pick that has broken",
},
}
- cli.AppHelpTemplate = `NAME:
+ cli.RootCommandHelpTemplate = `NAME:
{{.Name}} - {{.Usage}}
USAGE:
{{.HelpName}} {{if .VisibleFlags}}[options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}
@@ -105,16 +103,12 @@ OPTIONS:
`
app.Action = runBackport
-
- if err := app.Run(os.Args); err != nil {
+ if err := app.Run(context.Background(), os.Args); err != nil {
fmt.Fprintf(os.Stderr, "Unable to backport: %v\n", err)
}
}
-func runBackport(c *cli.Context) error {
- ctx, cancel := installSignals()
- defer cancel()
-
+func runBackport(ctx context.Context, c *cli.Command) error {
continuing := c.Bool("continue")
var pr string
@@ -343,8 +337,8 @@ func determineRemote(ctx context.Context, forkUser string) (string, string, erro
fmt.Fprintf(os.Stderr, "Unable to list git remotes:\n%s\n", string(out))
return "", "", fmt.Errorf("unable to determine forked remote: %w", err)
}
- lines := strings.Split(string(out), "\n")
- for _, line := range lines {
+ lines := strings.SplitSeq(string(out), "\n")
+ for line := range lines {
fields := strings.Split(line, "\t")
name, remote := fields[0], fields[1]
// only look at pushers
@@ -362,12 +356,12 @@ func determineRemote(ctx context.Context, forkUser string) (string, string, erro
if !strings.Contains(remote, forkUser) {
continue
}
- if strings.HasPrefix(remote, "git@github.com:") {
- forkUser = strings.TrimPrefix(remote, "git@github.com:")
- } else if strings.HasPrefix(remote, "https://github.com/") {
- forkUser = strings.TrimPrefix(remote, "https://github.com/")
- } else if strings.HasPrefix(remote, "https://www.github.com/") {
- forkUser = strings.TrimPrefix(remote, "https://www.github.com/")
+ if after, ok := strings.CutPrefix(remote, "git@github.com:"); ok {
+ forkUser = after
+ } else if after, ok := strings.CutPrefix(remote, "https://github.com/"); ok {
+ forkUser = after
+ } else if after, ok := strings.CutPrefix(remote, "https://www.github.com/"); ok {
+ forkUser = after
} else if forkUser == "" {
return "", "", fmt.Errorf("unable to extract forkUser from remote %s: %s", name, remote)
}
@@ -460,25 +454,3 @@ func determineSHAforPR(ctx context.Context, prStr, accessToken string) (string,
return "", nil
}
-
-func installSignals() (context.Context, context.CancelFunc) {
- ctx, cancel := context.WithCancel(context.Background())
- go func() {
- // install notify
- signalChannel := make(chan os.Signal, 1)
-
- signal.Notify(
- signalChannel,
- syscall.SIGINT,
- syscall.SIGTERM,
- )
- select {
- case <-signalChannel:
- case <-ctx.Done():
- }
- cancel()
- signal.Reset()
- }()
-
- return ctx, cancel
-}
diff --git a/contrib/environment-to-ini/environment-to-ini.go b/contrib/environment-to-ini/environment-to-ini.go
index a7d7a6d293d..5eb576c6fea 100644
--- a/contrib/environment-to-ini/environment-to-ini.go
+++ b/contrib/environment-to-ini/environment-to-ini.go
@@ -4,16 +4,17 @@
package main
import (
+ "context"
"os"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
- "github.com/urfave/cli/v2"
+ "github.com/urfave/cli/v3"
)
func main() {
- app := cli.NewApp()
+ app := cli.Command{}
app.Name = "environment-to-ini"
app.Usage = "Use provided environment to update configuration ini"
app.Description = `As a helper to allow docker users to update the gitea configuration
@@ -72,13 +73,13 @@ func main() {
},
}
app.Action = runEnvironmentToIni
- err := app.Run(os.Args)
+ err := app.Run(context.Background(), os.Args)
if err != nil {
log.Fatal("Failed to run app with %s: %v", os.Args, err)
}
}
-func runEnvironmentToIni(c *cli.Context) error {
+func runEnvironmentToIni(_ context.Context, c *cli.Command) error {
// the config system may change the environment variables, so get a copy first, to be used later
env := append([]string{}, os.Environ()...)
setting.InitWorkPathAndCfgProvider(os.Getenv, setting.ArgWorkPathAndCustomConf{
diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index a7476ad1be8..aa2fcee7655 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -186,17 +186,13 @@ RUN_USER = ; git
;; If you intend to use the AuthorizedPrincipalsCommand functionality then you should turn this off.
;SSH_CREATE_AUTHORIZED_PRINCIPALS_FILE = true
;;
-;; For the built-in SSH server, choose the ciphers to support for SSH connections,
-;; for system SSH this setting has no effect
-;SSH_SERVER_CIPHERS = chacha20-poly1305@openssh.com, aes128-ctr, aes192-ctr, aes256-ctr, aes128-gcm@openssh.com, aes256-gcm@openssh.com
-;;
-;; For the built-in SSH server, choose the key exchange algorithms to support for SSH connections,
-;; for system SSH this setting has no effect
-;SSH_SERVER_KEY_EXCHANGES = curve25519-sha256, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, diffie-hellman-group14-sha256, diffie-hellman-group14-sha1
-;;
-;; For the built-in SSH server, choose the MACs to support for SSH connections,
-;; for system SSH this setting has no effect
-;SSH_SERVER_MACS = hmac-sha2-256-etm@openssh.com, hmac-sha2-256, hmac-sha1
+;; For the builtin SSH server, choose the supported ciphers/key-exchange-algorithms/MACs for SSH connections.
+;; The supported names are listed in https://github.com/golang/crypto/blob/master/ssh/common.go.
+;; Leave them empty to use the Golang crypto's recommended default values.
+;; For system SSH (non-builtin SSH server), this setting has no effect.
+;SSH_SERVER_CIPHERS =
+;SSH_SERVER_KEY_EXCHANGES =
+;SSH_SERVER_MACS =
;;
;; For the built-in SSH server, choose the keypair to offer as the host key
;; The private key should be at SSH_SERVER_HOST_KEY and the public SSH_SERVER_HOST_KEY.pub
@@ -1190,17 +1186,24 @@ LEVEL = Info
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
-;; GPG key to use to sign commits, Defaults to the default - that is the value of git config --get user.signingkey
+;; GPG or SSH key to use to sign commits, Defaults to the default - that is the value of git config --get user.signingkey
+;; Depending on the value of SIGNING_FORMAT this is either:
+;; - openpgp: the GPG key ID
+;; - ssh: the path to the ssh public key "/path/to/key.pub": where "/path/to/key" is the private key, use ssh-keygen -t ed25519 to generate a new key pair without password
;; run in the context of the RUN_USER
;; Switch to none to stop signing completely
;SIGNING_KEY = default
;;
-;; If a SIGNING_KEY ID is provided and is not set to default, use the provided Name and Email address as the signer.
+;; If a SIGNING_KEY ID is provided and is not set to default, use the provided Name and Email address as the signer and the signing format.
;; These should match a publicized name and email address for the key. (When SIGNING_KEY is default these are set to
-;; the results of git config --get user.name and git config --get user.email respectively and can only be overridden
+;; the results of git config --get user.name, git config --get user.email and git config --default openpgp --get gpg.format respectively and can only be overridden
;; by setting the SIGNING_KEY ID to the correct ID.)
;SIGNING_NAME =
;SIGNING_EMAIL =
+;; SIGNING_FORMAT can be one of:
+;; - openpgp (default): use GPG to sign commits
+;; - ssh: use SSH to sign commits
+;SIGNING_FORMAT = openpgp
;;
;; Sets the default trust model for repositories. Options are: collaborator, committer, collaboratorcommitter
;DEFAULT_TRUST_MODEL = collaborator
@@ -1227,6 +1230,13 @@ LEVEL = Info
;; - commitssigned: require that all the commits in the head branch are signed.
;; - approved: only sign when merging an approved pr to a protected branch
;MERGES = pubkey, twofa, basesigned, commitssigned
+;;
+;; Determines which additional ssh keys are trusted for all signed commits regardless of the user
+;; This is useful for ssh signing key rotation.
+;; Exposes the provided SIGNING_NAME and SIGNING_EMAIL as the signer, regardless of the SIGNING_FORMAT value.
+;; Multiple keys should be comma separated.
+;; E.g."ssh-
any
\n", rendered) + }) } diff --git a/models/repo/language_stats.go b/models/repo/language_stats.go index 0bc0f1fb402..7db8cd4dd20 100644 --- a/models/repo/language_stats.go +++ b/models/repo/language_stats.go @@ -157,18 +157,17 @@ func UpdateLanguageStats(ctx context.Context, repo *Repository, commitID string, for lang, size := range stats { if size > s { s = size - topLang = strings.ToLower(lang) + topLang = lang } } for lang, size := range stats { upd := false - llang := strings.ToLower(lang) for _, s := range oldstats { // Update already existing language - if strings.ToLower(s.Language) == llang { + if strings.EqualFold(s.Language, lang) { s.CommitID = commitID - s.IsPrimary = llang == topLang + s.IsPrimary = lang == topLang s.Size = size if _, err := sess.ID(s.ID).Cols("`commit_id`", "`size`", "`is_primary`").Update(s); err != nil { return err @@ -182,7 +181,7 @@ func UpdateLanguageStats(ctx context.Context, repo *Repository, commitID string, if err := db.Insert(ctx, &LanguageStat{ RepoID: repo.ID, CommitID: commitID, - IsPrimary: llang == topLang, + IsPrimary: lang == topLang, Language: lang, Size: size, }); err != nil { diff --git a/models/repo/pushmirror_test.go b/models/repo/pushmirror_test.go index e19749d93a2..9fb74711477 100644 --- a/models/repo/pushmirror_test.go +++ b/models/repo/pushmirror_test.go @@ -39,8 +39,6 @@ func TestPushMirrorsIterate(t *testing.T) { Interval: 0, }) - time.Sleep(1 * time.Millisecond) - repo_model.PushMirrorsIterate(db.DefaultContext, 1, func(idx int, bean any) error { m, ok := bean.(*repo_model.PushMirror) assert.True(t, ok) diff --git a/models/repo/release.go b/models/repo/release.go index 663d310bc02..59f4caf5aa9 100644 --- a/models/repo/release.go +++ b/models/repo/release.go @@ -161,6 +161,11 @@ func UpdateRelease(ctx context.Context, rel *Release) error { 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 func AddReleaseAttachments(ctx context.Context, releaseID int64, attachmentUUIDs []string) (err error) { // Check attachments @@ -175,7 +180,7 @@ func AddReleaseAttachments(ctx context.Context, releaseID int64, attachmentUUIDs } attachments[i].ReleaseID = releaseID // No assign value could be 0, so ignore AllCols(). - if _, err = db.GetEngine(ctx).ID(attachments[i].ID).Update(attachments[i]); err != nil { + if _, err = db.GetEngine(ctx).ID(attachments[i].ID).Cols("release_id").Update(attachments[i]); err != nil { return fmt.Errorf("update attachment [%d]: %w", attachments[i].ID, err) } } @@ -418,8 +423,8 @@ func UpdateReleasesMigrationsByType(ctx context.Context, gitServiceType structs. return err } -// PushUpdateDeleteTagsContext updates a number of delete tags with context -func PushUpdateDeleteTagsContext(ctx context.Context, repo *Repository, tags []string) error { +// PushUpdateDeleteTags updates a number of delete tags with context +func PushUpdateDeleteTags(ctx context.Context, repo *Repository, tags []string) error { if len(tags) == 0 { return nil } @@ -448,58 +453,6 @@ func PushUpdateDeleteTagsContext(ctx context.Context, repo *Repository, tags []s 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 func (r *Release) RemapExternalUser(externalName string, externalID, userID int64) error { r.OriginalAuthor = externalName diff --git a/models/repo/repo.go b/models/repo/repo.go index 5aae02c6d80..34d1bf55f65 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -653,7 +653,7 @@ func (repo *Repository) AllowsPulls(ctx context.Context) bool { // CanEnableEditor returns true if repository meets the requirements of web editor. func (repo *Repository) CanEnableEditor() bool { - return !repo.IsMirror + return !repo.IsMirror && !repo.IsArchived } // DescriptionHTML does special handles to description and return HTML string. diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go index 02c228e8a0e..f2cdd2f2846 100644 --- a/models/repo/repo_list.go +++ b/models/repo/repo_list.go @@ -359,7 +359,7 @@ func UserOrgPublicUnitRepoCond(userID, orgID int64) builder.Cond { } // SearchRepositoryCondition creates a query condition according search repository options -func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { +func SearchRepositoryCondition(opts SearchRepoOptions) builder.Cond { cond := builder.NewCond() if opts.Private { @@ -449,7 +449,7 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { if opts.Keyword != "" { // separate keyword subQueryCond := builder.NewCond() - for _, v := range strings.Split(opts.Keyword, ",") { + for v := range strings.SplitSeq(opts.Keyword, ",") { if opts.TopicOnly { subQueryCond = subQueryCond.Or(builder.Eq{"topic.name": strings.ToLower(v)}) } else { @@ -464,7 +464,7 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { keywordCond := builder.In("id", subQuery) if !opts.TopicOnly { likes := builder.NewCond() - for _, v := range strings.Split(opts.Keyword, ",") { + for v := range strings.SplitSeq(opts.Keyword, ",") { likes = likes.Or(builder.Like{"lower_name", strings.ToLower(v)}) // If the string looks like "org/repo", match against that pattern too @@ -551,18 +551,18 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { // SearchRepository returns repositories based on search options, // it returns results in given range and number of total results. -func SearchRepository(ctx context.Context, opts *SearchRepoOptions) (RepositoryList, int64, error) { +func SearchRepository(ctx context.Context, opts SearchRepoOptions) (RepositoryList, int64, error) { cond := SearchRepositoryCondition(opts) return SearchRepositoryByCondition(ctx, opts, cond, true) } // CountRepository counts repositories based on search options, -func CountRepository(ctx context.Context, opts *SearchRepoOptions) (int64, error) { +func CountRepository(ctx context.Context, opts SearchRepoOptions) (int64, error) { return db.GetEngine(ctx).Where(SearchRepositoryCondition(opts)).Count(new(Repository)) } // SearchRepositoryByCondition search repositories by condition -func SearchRepositoryByCondition(ctx context.Context, opts *SearchRepoOptions, cond builder.Cond, loadAttributes bool) (RepositoryList, int64, error) { +func SearchRepositoryByCondition(ctx context.Context, opts SearchRepoOptions, cond builder.Cond, loadAttributes bool) (RepositoryList, int64, error) { sess, count, err := searchRepositoryByCondition(ctx, opts, cond) if err != nil { return nil, 0, err @@ -590,23 +590,25 @@ func SearchRepositoryByCondition(ctx context.Context, opts *SearchRepoOptions, c return repos, count, nil } -func searchRepositoryByCondition(ctx context.Context, opts *SearchRepoOptions, cond builder.Cond) (db.Engine, int64, error) { - if opts.Page <= 0 { - opts.Page = 1 +func searchRepositoryByCondition(ctx context.Context, opts SearchRepoOptions, cond builder.Cond) (db.Engine, int64, error) { + page := opts.Page + if page <= 0 { + page = 1 } - if len(opts.OrderBy) == 0 { - opts.OrderBy = db.SearchOrderByAlphabetically + orderBy := opts.OrderBy + if len(orderBy) == 0 { + orderBy = db.SearchOrderByAlphabetically } args := make([]any, 0) if opts.PriorityOwnerID > 0 { - opts.OrderBy = db.SearchOrderBy(fmt.Sprintf("CASE WHEN owner_id = ? THEN 0 ELSE owner_id END, %s", opts.OrderBy)) + orderBy = db.SearchOrderBy(fmt.Sprintf("CASE WHEN owner_id = ? THEN 0 ELSE owner_id END, %s", orderBy)) args = append(args, opts.PriorityOwnerID) } else if strings.Count(opts.Keyword, "/") == 1 { // With "owner/repo" search times, prioritise results which match the owner field orgName := strings.Split(opts.Keyword, "/")[0] - opts.OrderBy = db.SearchOrderBy(fmt.Sprintf("CASE WHEN owner_name LIKE ? THEN 0 ELSE 1 END, %s", opts.OrderBy)) + orderBy = db.SearchOrderBy(fmt.Sprintf("CASE WHEN owner_name LIKE ? THEN 0 ELSE 1 END, %s", orderBy)) args = append(args, orgName) } @@ -623,9 +625,9 @@ func searchRepositoryByCondition(ctx context.Context, opts *SearchRepoOptions, c } } - sess = sess.Where(cond).OrderBy(opts.OrderBy.String(), args...) + sess = sess.Where(cond).OrderBy(orderBy.String(), args...) if opts.PageSize > 0 { - sess = sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize) + sess = sess.Limit(opts.PageSize, (page-1)*opts.PageSize) } return sess, count, nil } @@ -689,14 +691,14 @@ func AccessibleRepositoryCondition(user *user_model.User, unitType unit.Type) bu // SearchRepositoryByName takes keyword and part of repository name to search, // it returns results in given range and number of total results. -func SearchRepositoryByName(ctx context.Context, opts *SearchRepoOptions) (RepositoryList, int64, error) { +func SearchRepositoryByName(ctx context.Context, opts SearchRepoOptions) (RepositoryList, int64, error) { opts.IncludeDescription = false return SearchRepository(ctx, opts) } // SearchRepositoryIDs takes keyword and part of repository name to search, // it returns results in given range and number of total results. -func SearchRepositoryIDs(ctx context.Context, opts *SearchRepoOptions) ([]int64, int64, error) { +func SearchRepositoryIDs(ctx context.Context, opts SearchRepoOptions) ([]int64, int64, error) { opts.IncludeDescription = false cond := SearchRepositoryCondition(opts) @@ -740,7 +742,7 @@ func FindUserCodeAccessibleOwnerRepoIDs(ctx context.Context, ownerID int64, user } // GetUserRepositories returns a list of repositories of given user. -func GetUserRepositories(ctx context.Context, opts *SearchRepoOptions) (RepositoryList, int64, error) { +func GetUserRepositories(ctx context.Context, opts SearchRepoOptions) (RepositoryList, int64, error) { if len(opts.OrderBy) == 0 { opts.OrderBy = "updated_unix DESC" } @@ -767,5 +769,5 @@ func GetUserRepositories(ctx context.Context, opts *SearchRepoOptions) (Reposito sess = sess.Where(cond).OrderBy(opts.OrderBy.String()) repos := make(RepositoryList, 0, opts.PageSize) - return repos, count, db.SetSessionPagination(sess, opts).Find(&repos) + return repos, count, db.SetSessionPagination(sess, &opts).Find(&repos) } diff --git a/models/repo/repo_list_test.go b/models/repo/repo_list_test.go index ca6007f6c78..7eb76416c20 100644 --- a/models/repo/repo_list_test.go +++ b/models/repo/repo_list_test.go @@ -17,162 +17,162 @@ import ( func getTestCases() []struct { name string - opts *repo_model.SearchRepoOptions + opts repo_model.SearchRepoOptions count int } { testCases := []struct { name string - opts *repo_model.SearchRepoOptions + opts repo_model.SearchRepoOptions count int }{ { name: "PublicRepositoriesByName", - opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, Collaborate: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, Collaborate: optional.Some(false)}, count: 7, }, { name: "PublicAndPrivateRepositoriesByName", - opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, Collaborate: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, Collaborate: optional.Some(false)}, count: 14, }, { name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitFirstPage", - opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 5}, Private: true, Collaborate: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 5}, Private: true, Collaborate: optional.Some(false)}, count: 14, }, { name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitSecondPage", - opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 2, PageSize: 5}, Private: true, Collaborate: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 2, PageSize: 5}, Private: true, Collaborate: optional.Some(false)}, count: 14, }, { name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitThirdPage", - opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: optional.Some(false)}, count: 14, }, { name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitFourthPage", - opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: optional.Some(false)}, count: 14, }, { name: "PublicRepositoriesOfUser", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Collaborate: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Collaborate: optional.Some(false)}, count: 2, }, { name: "PublicRepositoriesOfUser2", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Collaborate: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Collaborate: optional.Some(false)}, count: 0, }, { name: "PublicRepositoriesOfOrg3", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Collaborate: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Collaborate: optional.Some(false)}, count: 2, }, { name: "PublicAndPrivateRepositoriesOfUser", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, Collaborate: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, Collaborate: optional.Some(false)}, count: 4, }, { name: "PublicAndPrivateRepositoriesOfUser2", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true, Collaborate: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true, Collaborate: optional.Some(false)}, count: 0, }, { name: "PublicAndPrivateRepositoriesOfOrg3", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Private: true, Collaborate: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Private: true, Collaborate: optional.Some(false)}, count: 4, }, { name: "PublicRepositoriesOfUserIncludingCollaborative", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15}, count: 5, }, { name: "PublicRepositoriesOfUser2IncludingCollaborative", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18}, count: 1, }, { name: "PublicRepositoriesOfOrg3IncludingCollaborative", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20}, count: 3, }, { name: "PublicAndPrivateRepositoriesOfUserIncludingCollaborative", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true}, count: 9, }, { name: "PublicAndPrivateRepositoriesOfUser2IncludingCollaborative", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true}, count: 4, }, { name: "PublicAndPrivateRepositoriesOfOrg3IncludingCollaborative", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Private: true}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Private: true}, count: 7, }, { name: "PublicRepositoriesOfOrganization", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Collaborate: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Collaborate: optional.Some(false)}, count: 1, }, { name: "PublicAndPrivateRepositoriesOfOrganization", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Private: true, Collaborate: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Private: true, Collaborate: optional.Some(false)}, count: 2, }, { name: "AllPublic/PublicRepositoriesByName", - opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, AllPublic: true, Collaborate: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, AllPublic: true, Collaborate: optional.Some(false)}, count: 7, }, { name: "AllPublic/PublicAndPrivateRepositoriesByName", - opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, AllPublic: true, Collaborate: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, AllPublic: true, Collaborate: optional.Some(false)}, count: 14, }, { name: "AllPublic/PublicRepositoriesOfUserIncludingCollaborative", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, AllPublic: true, Template: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, AllPublic: true, Template: optional.Some(false)}, count: 34, }, { name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true, AllLimited: true, Template: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true, AllLimited: true, Template: optional.Some(false)}, count: 39, }, { name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborativeByName", - opts: &repo_model.SearchRepoOptions{Keyword: "test", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true}, + opts: repo_model.SearchRepoOptions{Keyword: "test", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true}, count: 15, }, { name: "AllPublic/PublicAndPrivateRepositoriesOfUser2IncludingCollaborativeByName", - opts: &repo_model.SearchRepoOptions{Keyword: "test", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true, AllPublic: true}, + opts: repo_model.SearchRepoOptions{Keyword: "test", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true, AllPublic: true}, count: 13, }, { name: "AllPublic/PublicRepositoriesOfOrganization", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, AllPublic: true, Collaborate: optional.Some(false), Template: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, AllPublic: true, Collaborate: optional.Some(false), Template: optional.Some(false)}, count: 34, }, { name: "AllTemplates", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Template: optional.Some(true)}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Template: optional.Some(true)}, count: 2, }, { name: "OwnerSlashRepoSearch", - opts: &repo_model.SearchRepoOptions{Keyword: "user/repo2", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, OwnerID: 0}, + opts: repo_model.SearchRepoOptions{Keyword: "user/repo2", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, OwnerID: 0}, count: 2, }, { name: "OwnerSlashSearch", - opts: &repo_model.SearchRepoOptions{Keyword: "user20/", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, OwnerID: 0}, + opts: repo_model.SearchRepoOptions{Keyword: "user20/", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, OwnerID: 0}, count: 4, }, } @@ -184,7 +184,7 @@ func TestSearchRepository(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) // test search public repository on explore page - repos, count, err := repo_model.SearchRepositoryByName(db.DefaultContext, &repo_model.SearchRepoOptions{ + repos, count, err := repo_model.SearchRepositoryByName(db.DefaultContext, repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ Page: 1, PageSize: 10, @@ -199,7 +199,7 @@ func TestSearchRepository(t *testing.T) { } assert.Equal(t, int64(1), count) - repos, count, err = repo_model.SearchRepositoryByName(db.DefaultContext, &repo_model.SearchRepoOptions{ + repos, count, err = repo_model.SearchRepositoryByName(db.DefaultContext, repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ Page: 1, PageSize: 10, @@ -213,7 +213,7 @@ func TestSearchRepository(t *testing.T) { assert.Len(t, repos, 2) // test search private repository on explore page - repos, count, err = repo_model.SearchRepositoryByName(db.DefaultContext, &repo_model.SearchRepoOptions{ + repos, count, err = repo_model.SearchRepositoryByName(db.DefaultContext, repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ Page: 1, PageSize: 10, @@ -229,7 +229,7 @@ func TestSearchRepository(t *testing.T) { } assert.Equal(t, int64(1), count) - repos, count, err = repo_model.SearchRepositoryByName(db.DefaultContext, &repo_model.SearchRepoOptions{ + repos, count, err = repo_model.SearchRepositoryByName(db.DefaultContext, repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ Page: 1, PageSize: 10, @@ -244,14 +244,14 @@ func TestSearchRepository(t *testing.T) { assert.Len(t, repos, 3) // Test non existing owner - repos, count, err = repo_model.SearchRepositoryByName(db.DefaultContext, &repo_model.SearchRepoOptions{OwnerID: unittest.NonexistentID}) + repos, count, err = repo_model.SearchRepositoryByName(db.DefaultContext, repo_model.SearchRepoOptions{OwnerID: unittest.NonexistentID}) assert.NoError(t, err) assert.Empty(t, repos) assert.Equal(t, int64(0), count) // Test search within description - repos, count, err = repo_model.SearchRepository(db.DefaultContext, &repo_model.SearchRepoOptions{ + repos, count, err = repo_model.SearchRepository(db.DefaultContext, repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ Page: 1, PageSize: 10, @@ -268,7 +268,7 @@ func TestSearchRepository(t *testing.T) { assert.Equal(t, int64(1), count) // Test NOT search within description - repos, count, err = repo_model.SearchRepository(db.DefaultContext, &repo_model.SearchRepoOptions{ + repos, count, err = repo_model.SearchRepository(db.DefaultContext, repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ Page: 1, PageSize: 10, @@ -374,22 +374,22 @@ func TestSearchRepositoryByTopicName(t *testing.T) { testCases := []struct { name string - opts *repo_model.SearchRepoOptions + opts repo_model.SearchRepoOptions count int }{ { name: "AllPublic/SearchPublicRepositoriesFromTopicAndName", - opts: &repo_model.SearchRepoOptions{OwnerID: 21, AllPublic: true, Keyword: "graphql"}, + opts: repo_model.SearchRepoOptions{OwnerID: 21, AllPublic: true, Keyword: "graphql"}, count: 2, }, { name: "AllPublic/OnlySearchPublicRepositoriesFromTopic", - opts: &repo_model.SearchRepoOptions{OwnerID: 21, AllPublic: true, Keyword: "graphql", TopicOnly: true}, + opts: repo_model.SearchRepoOptions{OwnerID: 21, AllPublic: true, Keyword: "graphql", TopicOnly: true}, count: 1, }, { name: "AllPublic/OnlySearchMultipleKeywordPublicRepositoriesFromTopic", - opts: &repo_model.SearchRepoOptions{OwnerID: 21, AllPublic: true, Keyword: "graphql,golang", TopicOnly: true}, + opts: repo_model.SearchRepoOptions{OwnerID: 21, AllPublic: true, Keyword: "graphql,golang", TopicOnly: true}, count: 2, }, } diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index 8a7dbfe3408..a5207bc22a8 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -185,10 +185,8 @@ func (cfg *ActionsConfig) IsWorkflowDisabled(file string) bool { } func (cfg *ActionsConfig) DisableWorkflow(file string) { - for _, workflow := range cfg.DisabledWorkflows { - if file == workflow { - return - } + if slices.Contains(cfg.DisabledWorkflows, file) { + return } cfg.DisabledWorkflows = append(cfg.DisabledWorkflows, file) diff --git a/models/repo/transfer.go b/models/repo/transfer.go index b4a3592cbcf..3fb8cb69abd 100644 --- a/models/repo/transfer.go +++ b/models/repo/transfer.go @@ -61,7 +61,7 @@ func (err ErrRepoTransferInProgress) Unwrap() error { } // RepoTransfer is used to manage repository transfers -type RepoTransfer struct { //nolint +type RepoTransfer struct { //nolint:revive // export stutter ID int64 `xorm:"pk autoincr"` DoerID int64 Doer *user_model.User `xorm:"-"` diff --git a/models/repo/update.go b/models/repo/update.go index 8a15477a802..64065f11c46 100644 --- a/models/repo/update.go +++ b/models/repo/update.go @@ -40,15 +40,15 @@ func UpdateRepositoryUpdatedTime(ctx context.Context, repoID int64, updateTime t return err } -// UpdateRepositoryColsWithAutoTime updates repository's columns -func UpdateRepositoryColsWithAutoTime(ctx context.Context, repo *Repository, cols ...string) error { - _, err := db.GetEngine(ctx).ID(repo.ID).Cols(cols...).Update(repo) +// UpdateRepositoryColsWithAutoTime updates repository's columns and the timestamp fields automatically +func UpdateRepositoryColsWithAutoTime(ctx context.Context, repo *Repository, colName string, moreColNames ...string) error { + _, err := db.GetEngine(ctx).ID(repo.ID).Cols(append([]string{colName}, moreColNames...)...).Update(repo) return err } -// UpdateRepositoryColsNoAutoTime updates repository's columns and but applies time change automatically -func UpdateRepositoryColsNoAutoTime(ctx context.Context, repo *Repository, cols ...string) error { - _, err := db.GetEngine(ctx).ID(repo.ID).Cols(cols...).NoAutoTime().Update(repo) +// UpdateRepositoryColsNoAutoTime updates repository's columns, doesn't change timestamp field automatically +func UpdateRepositoryColsNoAutoTime(ctx context.Context, repo *Repository, colName string, moreColNames ...string) error { + _, err := db.GetEngine(ctx).ID(repo.ID).Cols(append([]string{colName}, moreColNames...)...).NoAutoTime().Update(repo) return err } diff --git a/models/repo/upload.go b/models/repo/upload.go index fb57fb6c513..20a8fa26fe4 100644 --- a/models/repo/upload.go +++ b/models/repo/upload.go @@ -124,7 +124,7 @@ func DeleteUploads(ctx context.Context, uploads ...*Upload) (err error) { defer committer.Close() ids := make([]int64, len(uploads)) - for i := 0; i < len(uploads); i++ { + for i := range uploads { ids[i] = uploads[i].ID } if err = db.DeleteByIDs[Upload](ctx, ids...); err != nil { diff --git a/models/unit/unit.go b/models/unit/unit.go index 4ca676802ff..c0560678ca9 100644 --- a/models/unit/unit.go +++ b/models/unit/unit.go @@ -6,6 +6,7 @@ package unit import ( "errors" "fmt" + "slices" "strings" "sync/atomic" @@ -204,22 +205,12 @@ func LoadUnitConfig() error { // UnitGlobalDisabled checks if unit type is global disabled func (u Type) UnitGlobalDisabled() bool { - for _, ud := range DisabledRepoUnitsGet() { - if u == ud { - return true - } - } - return false + return slices.Contains(DisabledRepoUnitsGet(), u) } // CanBeDefault checks if the unit type can be a default repo unit func (u *Type) CanBeDefault() bool { - for _, nadU := range NotAllowedDefaultRepoUnits { - if *u == nadU { - return false - } - } - return true + return !slices.Contains(NotAllowedDefaultRepoUnits, *u) } // Unit is a section of one repository diff --git a/models/user/avatar.go b/models/user/avatar.go index 3d9fc4452f8..542bd93b982 100644 --- a/models/user/avatar.go +++ b/models/user/avatar.go @@ -5,7 +5,6 @@ package user import ( "context" - "crypto/md5" "fmt" "image/png" "io" @@ -106,7 +105,7 @@ func (u *User) IsUploadAvatarChanged(data []byte) bool { if !u.UseCustomAvatar || len(u.Avatar) == 0 { 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 } diff --git a/models/user/badge.go b/models/user/badge.go index 3ff3530a369..e475ceb7489 100644 --- a/models/user/badge.go +++ b/models/user/badge.go @@ -19,7 +19,7 @@ type Badge struct { } // UserBadge represents a user badge -type UserBadge struct { //nolint:revive +type UserBadge struct { //nolint:revive // export stutter ID int64 `xorm:"pk autoincr"` BadgeID int64 UserID int64 `xorm:"INDEX"` diff --git a/models/user/email_address_test.go b/models/user/email_address_test.go index 0e52950cfd3..c0666246b04 100644 --- a/models/user/email_address_test.go +++ b/models/user/email_address_test.go @@ -4,6 +4,7 @@ package user_test import ( + "slices" "testing" "code.gitea.io/gitea/models/db" @@ -100,12 +101,7 @@ func TestListEmails(t *testing.T) { assert.Greater(t, count, int64(5)) contains := func(match func(s *user_model.SearchEmailResult) bool) bool { - for _, v := range emails { - if match(v) { - return true - } - } - return false + return slices.ContainsFunc(emails, match) } assert.True(t, contains(func(s *user_model.SearchEmailResult) bool { return s.UID == 18 })) diff --git a/models/user/search.go b/models/user/search.go index f4436be09aa..cfd0d011bcb 100644 --- a/models/user/search.go +++ b/models/user/search.go @@ -137,7 +137,7 @@ func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Sess // SearchUsers takes options i.e. keyword and part of user name to search, // it returns results in given range and number of total results. -func SearchUsers(ctx context.Context, opts *SearchUserOptions) (users []*User, _ int64, _ error) { +func SearchUsers(ctx context.Context, opts SearchUserOptions) (users []*User, _ int64, _ error) { sessCount := opts.toSearchQueryBase(ctx) defer sessCount.Close() count, err := sessCount.Count(new(User)) @@ -152,7 +152,7 @@ func SearchUsers(ctx context.Context, opts *SearchUserOptions) (users []*User, _ sessQuery := opts.toSearchQueryBase(ctx).OrderBy(opts.OrderBy.String()) defer sessQuery.Close() if opts.Page > 0 { - sessQuery = db.SetSessionPagination(sessQuery, opts) + sessQuery = db.SetSessionPagination(sessQuery, &opts) } // the sql may contain JOIN, so we must only select User related columns diff --git a/models/user/user.go b/models/user/user.go index d7331d79f0b..7c871bf5751 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -831,6 +831,20 @@ type CountUserFilter struct { IsActive optional.Option[bool] } +// HasUsers checks whether there are any users in the database, or only one user exists. +func HasUsers(ctx context.Context) (ret struct { + HasAnyUser, HasOnlyOneUser bool +}, err error, +) { + res, err := db.GetEngine(ctx).Table(&User{}).Cols("id").Limit(2).Query() + if err != nil { + return ret, fmt.Errorf("error checking user existence: %w", err) + } + ret.HasAnyUser = len(res) != 0 + ret.HasOnlyOneUser = len(res) == 1 + return ret, nil +} + // CountUsers returns number of users. func CountUsers(ctx context.Context, opts *CountUserFilter) int64 { return countUsers(ctx, opts) @@ -1151,8 +1165,8 @@ func ValidateCommitsWithEmails(ctx context.Context, oldCommits []*git.Commit) ([ } for _, c := range oldCommits { - user, ok := emailUserMap[c.Author.Email] - if !ok { + user := emailUserMap.GetByEmail(c.Author.Email) // FIXME: why ValidateCommitsWithEmails uses "Author", but ParseCommitsWithSignature uses "Committer"? + if user == nil { user = &User{ Name: c.Author.Name, Email: c.Author.Email, @@ -1166,7 +1180,15 @@ func ValidateCommitsWithEmails(ctx context.Context, oldCommits []*git.Commit) ([ 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 { return nil, nil } @@ -1176,7 +1198,7 @@ func GetUsersByEmails(ctx context.Context, emails []string) (map[string]*User, e for _, email := range emails { if strings.HasSuffix(email, "@"+setting.Service.NoReplyAddress) { username := strings.TrimSuffix(email, "@"+setting.Service.NoReplyAddress) - needCheckUserNames.Add(username) + needCheckUserNames.Add(strings.ToLower(username)) } else { needCheckEmails.Add(strings.ToLower(email)) } @@ -1203,8 +1225,7 @@ func GetUsersByEmails(ctx context.Context, emails []string) (map[string]*User, e for _, email := range emailAddresses { user := users[email.UID] if user != nil { - results[user.Email] = user - results[user.GetPlaceholderEmail()] = user + results[email.LowerEmail] = user } } } @@ -1214,10 +1235,9 @@ func GetUsersByEmails(ctx context.Context, emails []string) (map[string]*User, e return nil, err } for _, user := range users { - results[user.Email] = user - 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. diff --git a/models/user/user_list.go b/models/user/user_list.go index 42419050584..1b6a27dd862 100644 --- a/models/user/user_list.go +++ b/models/user/user_list.go @@ -17,10 +17,7 @@ func GetUsersMapByIDs(ctx context.Context, userIDs []int64) (map[int64]*User, er left := len(userIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) err := db.GetEngine(ctx). In("id", userIDs[:limit]). Find(&userMaps) diff --git a/models/user/user_test.go b/models/user/user_test.go index dd232abe2e2..a2597ba3f5e 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -58,13 +58,33 @@ func TestUserEmails(t *testing.T) { assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "org7"})) }) t.Run("GetUsersByEmails", func(t *testing.T) { - m, err := user_model.GetUsersByEmails(db.DefaultContext, []string{"user1@example.com", "user2@" + setting.Service.NoReplyAddress}) - require.NoError(t, err) - require.Len(t, m, 4) - assert.EqualValues(t, 1, m["user1@example.com"].ID) - assert.EqualValues(t, 1, m["user1@"+setting.Service.NoReplyAddress].ID) - assert.EqualValues(t, 2, m["user2@example.com"].ID) - assert.EqualValues(t, 2, m["user2@"+setting.Service.NoReplyAddress].ID) + 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) + }) + } }) } @@ -88,7 +108,7 @@ func TestCanCreateOrganization(t *testing.T) { func TestSearchUsers(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - testSuccess := func(opts *user_model.SearchUserOptions, expectedUserOrOrgIDs []int64) { + testSuccess := func(opts user_model.SearchUserOptions, expectedUserOrOrgIDs []int64) { users, _, err := user_model.SearchUsers(db.DefaultContext, opts) assert.NoError(t, err) cassText := fmt.Sprintf("ids: %v, opts: %v", expectedUserOrOrgIDs, opts) @@ -100,61 +120,61 @@ func TestSearchUsers(t *testing.T) { } // test orgs - testOrgSuccess := func(opts *user_model.SearchUserOptions, expectedOrgIDs []int64) { + testOrgSuccess := func(opts user_model.SearchUserOptions, expectedOrgIDs []int64) { opts.Type = user_model.UserTypeOrganization testSuccess(opts, expectedOrgIDs) } - testOrgSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1, PageSize: 2}}, + testOrgSuccess(user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1, PageSize: 2}}, []int64{3, 6}) - testOrgSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 2, PageSize: 2}}, + testOrgSuccess(user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 2, PageSize: 2}}, []int64{7, 17}) - testOrgSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 3, PageSize: 2}}, + testOrgSuccess(user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 3, PageSize: 2}}, []int64{19, 25}) - testOrgSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 4, PageSize: 2}}, + testOrgSuccess(user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 4, PageSize: 2}}, []int64{26, 41}) - testOrgSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 5, PageSize: 2}}, + testOrgSuccess(user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 5, PageSize: 2}}, []int64{42}) - testOrgSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 6, PageSize: 2}}, + testOrgSuccess(user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 6, PageSize: 2}}, []int64{}) // test users - testUserSuccess := func(opts *user_model.SearchUserOptions, expectedUserIDs []int64) { + testUserSuccess := func(opts user_model.SearchUserOptions, expectedUserIDs []int64) { opts.Type = user_model.UserTypeIndividual testSuccess(opts, expectedUserIDs) } - testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}}, + testUserSuccess(user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}}, []int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37, 38, 39, 40}) - testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(false)}, + testUserSuccess(user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(false)}, []int64{9}) - testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)}, + testUserSuccess(user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)}, []int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37, 38, 39, 40}) - testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)}, + testUserSuccess(user_model.SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)}, []int64{1, 10, 11, 12, 13, 14, 15, 16, 18}) // order by name asc default - testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)}, + testUserSuccess(user_model.SearchUserOptions{Keyword: "user1", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)}, []int64{1, 10, 11, 12, 13, 14, 15, 16, 18}) - testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsAdmin: optional.Some(true)}, + testUserSuccess(user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsAdmin: optional.Some(true)}, []int64{1}) - testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsRestricted: optional.Some(true)}, + testUserSuccess(user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsRestricted: optional.Some(true)}, []int64{29}) - testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsProhibitLogin: optional.Some(true)}, + testUserSuccess(user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsProhibitLogin: optional.Some(true)}, []int64{37}) - testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsTwoFactorEnabled: optional.Some(true)}, + testUserSuccess(user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsTwoFactorEnabled: optional.Some(true)}, []int64{24}) } @@ -184,9 +204,9 @@ func TestHashPasswordDeterministic(t *testing.T) { b := make([]byte, 16) u := &user_model.User{} algos := hash.RecommendedHashAlgorithms - for j := 0; j < len(algos); j++ { + for j := range algos { u.PasswdHashAlgo = algos[j] - for i := 0; i < 50; i++ { + for range 50 { // generate a random password rand.Read(b) pass := string(b) @@ -513,11 +533,8 @@ func TestIsUserVisibleToViewer(t *testing.T) { } func Test_ValidateUser(t *testing.T) { - oldSetting := setting.Service.AllowedUserVisibilityModesSlice - defer func() { - setting.Service.AllowedUserVisibilityModesSlice = oldSetting - }() - setting.Service.AllowedUserVisibilityModesSlice = []bool{true, false, true} + defer test.MockVariableValue(&setting.Service.AllowedUserVisibilityModesSlice, []bool{true, false, true})() + kases := map[*user_model.User]bool{ {ID: 1, Visibility: structs.VisibleTypePublic}: true, {ID: 2, Visibility: structs.VisibleTypeLimited}: false, @@ -586,12 +603,7 @@ func TestDisabledUserFeatures(t *testing.T) { testValues := container.SetOf(setting.UserFeatureDeletion, setting.UserFeatureManageSSHKeys, setting.UserFeatureManageGPGKeys) - - oldSetting := setting.Admin.ExternalUserDisableFeatures - defer func() { - setting.Admin.ExternalUserDisableFeatures = oldSetting - }() - setting.Admin.ExternalUserDisableFeatures = testValues + defer test.MockVariableValue(&setting.Admin.ExternalUserDisableFeatures, testValues)() user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) diff --git a/models/webhook/webhook.go b/models/webhook/webhook.go index 97ad3730277..b234d9ffee5 100644 --- a/models/webhook/webhook.go +++ b/models/webhook/webhook.go @@ -240,7 +240,7 @@ func CreateWebhooks(ctx context.Context, ws []*Webhook) error { if len(ws) == 0 { return nil } - for i := 0; i < len(ws); i++ { + for i := range ws { ws[i].Type = strings.TrimSpace(ws[i].Type) } return db.Insert(ctx, ws) diff --git a/models/webhook/webhook_test.go b/models/webhook/webhook_test.go index e8a2547c65a..edad8fc996c 100644 --- a/models/webhook/webhook_test.go +++ b/models/webhook/webhook_test.go @@ -73,7 +73,7 @@ func TestWebhook_EventsArray(t *testing.T) { "pull_request", "pull_request_assign", "pull_request_label", "pull_request_milestone", "pull_request_comment", "pull_request_review_approved", "pull_request_review_rejected", "pull_request_review_comment", "pull_request_sync", "pull_request_review_request", "wiki", "repository", "release", - "package", "status", "workflow_job", + "package", "status", "workflow_run", "workflow_job", }, (&Webhook{ HookEvent: &webhook_module.HookEvent{SendEverything: true}, diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index a538b6e290b..27bcafa6497 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -6,6 +6,7 @@ package actions import ( "bytes" "io" + "slices" "strings" "code.gitea.io/gitea/modules/git" @@ -43,21 +44,23 @@ func IsWorkflow(path string) bool { return strings.HasPrefix(path, ".gitea/workflows") || strings.HasPrefix(path, ".github/workflows") } -func ListWorkflows(commit *git.Commit) (git.Entries, error) { - tree, err := commit.SubTree(".gitea/workflows") +func ListWorkflows(commit *git.Commit) (string, git.Entries, error) { + rpath := ".gitea/workflows" + tree, err := commit.SubTree(rpath) if _, ok := err.(git.ErrNotExist); ok { - tree, err = commit.SubTree(".github/workflows") + rpath = ".github/workflows" + tree, err = commit.SubTree(rpath) } if _, ok := err.(git.ErrNotExist); ok { - return nil, nil + return "", nil, nil } if err != nil { - return nil, err + return "", nil, err } entries, err := tree.ListEntriesRecursiveFast() if err != nil { - return nil, err + return "", nil, err } ret := make(git.Entries, 0, len(entries)) @@ -66,7 +69,7 @@ func ListWorkflows(commit *git.Commit) (git.Entries, error) { ret = append(ret, entry) } } - return ret, nil + return rpath, ret, nil } func GetContentFromEntry(entry *git.TreeEntry) ([]byte, error) { @@ -102,7 +105,7 @@ func DetectWorkflows( payload api.Payloader, detectSchedule bool, ) ([]*DetectedWorkflow, []*DetectedWorkflow, error) { - entries, err := ListWorkflows(commit) + _, entries, err := ListWorkflows(commit) if err != nil { return nil, nil, err } @@ -147,7 +150,7 @@ func DetectWorkflows( } func DetectScheduledWorkflows(gitRepo *git.Repository, commit *git.Commit) ([]*DetectedWorkflow, error) { - entries, err := ListWorkflows(commit) + _, entries, err := ListWorkflows(commit) if err != nil { return nil, err } @@ -243,6 +246,10 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web webhook_module.HookEventPackage: return matchPackageEvent(payload.(*api.PackagePayload), evt) + case // workflow_run + webhook_module.HookEventWorkflowRun: + return matchWorkflowRunEvent(payload.(*api.WorkflowRunPayload), evt) + default: log.Warn("unsupported event %q", triggedEvent) return false @@ -311,6 +318,10 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa matchTimes++ } case "paths": + if refName.IsTag() { + matchTimes++ + break + } filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before) if err != nil { log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err) @@ -324,6 +335,10 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa } } case "paths-ignore": + if refName.IsTag() { + matchTimes++ + break + } filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before) if err != nil { log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err) @@ -554,21 +569,12 @@ func matchPullRequestReviewEvent(prPayload *api.PullRequestPayload, evt *jobpars actions = append(actions, "submitted", "edited") } - matched := false for _, val := range vals { - for _, action := range actions { - if glob.MustCompile(val, '/').Match(action) { - matched = true - break - } - } - if matched { + if slices.ContainsFunc(actions, glob.MustCompile(val, '/').Match) { + matchTimes++ break } } - if matched { - matchTimes++ - } default: log.Warn("pull request review event unsupported condition %q", cond) } @@ -603,21 +609,12 @@ func matchPullRequestReviewCommentEvent(prPayload *api.PullRequestPayload, evt * actions = append(actions, "created", "edited") } - matched := false for _, val := range vals { - for _, action := range actions { - if glob.MustCompile(val, '/').Match(action) { - matched = true - break - } - } - if matched { + if slices.ContainsFunc(actions, glob.MustCompile(val, '/').Match) { + matchTimes++ break } } - if matched { - matchTimes++ - } default: log.Warn("pull request review comment event unsupported condition %q", cond) } @@ -698,3 +695,53 @@ func matchPackageEvent(payload *api.PackagePayload, evt *jobparser.Event) bool { } return matchTimes == len(evt.Acts()) } + +func matchWorkflowRunEvent(payload *api.WorkflowRunPayload, evt *jobparser.Event) bool { + // with no special filter parameters + if len(evt.Acts()) == 0 { + return true + } + + matchTimes := 0 + // all acts conditions should be satisfied + for cond, vals := range evt.Acts() { + switch cond { + case "types": + action := payload.Action + for _, val := range vals { + if glob.MustCompile(val, '/').Match(action) { + matchTimes++ + break + } + } + case "workflows": + workflow := payload.Workflow + patterns, err := workflowpattern.CompilePatterns(vals...) + if err != nil { + break + } + if !workflowpattern.Skip(patterns, []string{workflow.Name}, &workflowpattern.EmptyTraceWriter{}) { + matchTimes++ + } + case "branches": + patterns, err := workflowpattern.CompilePatterns(vals...) + if err != nil { + break + } + if !workflowpattern.Skip(patterns, []string{payload.WorkflowRun.HeadBranch}, &workflowpattern.EmptyTraceWriter{}) { + matchTimes++ + } + case "branches-ignore": + patterns, err := workflowpattern.CompilePatterns(vals...) + if err != nil { + break + } + if !workflowpattern.Filter(patterns, []string{payload.WorkflowRun.HeadBranch}, &workflowpattern.EmptyTraceWriter{}) { + matchTimes++ + } + default: + log.Warn("workflow run event unsupported condition %q", cond) + } + } + return matchTimes == len(evt.Acts()) +} diff --git a/modules/actions/workflows_test.go b/modules/actions/workflows_test.go index c8e1e553fe9..e23431651da 100644 --- a/modules/actions/workflows_test.go +++ b/modules/actions/workflows_test.go @@ -125,6 +125,24 @@ func TestDetectMatched(t *testing.T) { yamlOn: "on: schedule", 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 { diff --git a/modules/assetfs/embed.go b/modules/assetfs/embed.go new file mode 100644 index 00000000000..95176372d10 --- /dev/null +++ b/modules/assetfs/embed.go @@ -0,0 +1,375 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package assetfs + +import ( + "bytes" + "compress/gzip" + "io" + "io/fs" + "os" + "path" + "path/filepath" + "strings" + "sync" + "time" + + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/util" +) + +type EmbeddedFile interface { + io.ReadSeeker + fs.ReadDirFile + ReadDir(n int) ([]fs.DirEntry, error) +} + +type EmbeddedFileInfo interface { + fs.FileInfo + fs.DirEntry + GetGzipContent() ([]byte, bool) +} + +type decompressor interface { + io.Reader + Close() error + Reset(io.Reader) error +} + +type embeddedFileInfo struct { + fs *embeddedFS + fullName string + data []byte + + BaseName string `json:"n"` + OriginSize int64 `json:"s,omitempty"` + DataBegin int64 `json:"b,omitempty"` + DataLen int64 `json:"l,omitempty"` + Children []*embeddedFileInfo `json:"c,omitempty"` +} + +func (fi *embeddedFileInfo) GetGzipContent() ([]byte, bool) { + // when generating the bindata, if the compressed data equals or is larger than the original data, we store the original data + if fi.DataLen == fi.OriginSize { + return nil, false + } + return fi.data, true +} + +type EmbeddedFileBase struct { + info *embeddedFileInfo + dataReader io.ReadSeeker + seekPos int64 +} + +func (f *EmbeddedFileBase) ReadDir(n int) ([]fs.DirEntry, error) { + // this method is used to satisfy the "func (f ioFile) ReadDir(...)" in httpfs + l, err := f.info.fs.ReadDir(f.info.fullName) + if err != nil { + return nil, err + } + if n < 0 || n > len(l) { + return l, nil + } + return l[:n], nil +} + +type EmbeddedOriginFile struct { + EmbeddedFileBase +} + +type EmbeddedCompressedFile struct { + EmbeddedFileBase + decompressor decompressor + decompressorPos int64 +} + +type embeddedFS struct { + meta func() *EmbeddedMeta + + files map[string]*embeddedFileInfo + filesMu sync.RWMutex + + data []byte +} + +type EmbeddedMeta struct { + Root *embeddedFileInfo +} + +func NewEmbeddedFS(data []byte) fs.ReadDirFS { + efs := &embeddedFS{data: data, files: make(map[string]*embeddedFileInfo)} + efs.meta = sync.OnceValue(func() *EmbeddedMeta { + var meta EmbeddedMeta + p := bytes.LastIndexByte(data, '\n') + if p < 0 { + return &meta + } + if err := json.Unmarshal(data[p+1:], &meta); err != nil { + panic("embedded file is not valid") + } + return &meta + }) + return efs +} + +var _ fs.ReadDirFS = (*embeddedFS)(nil) + +func (e *embeddedFS) ReadDir(name string) (l []fs.DirEntry, err error) { + fi, err := e.getFileInfo(name) + if err != nil { + return nil, err + } + if !fi.IsDir() { + return nil, fs.ErrNotExist + } + l = make([]fs.DirEntry, len(fi.Children)) + for i, child := range fi.Children { + l[i], err = e.getFileInfo(name + "/" + child.BaseName) + if err != nil { + return nil, err + } + } + return l, nil +} + +func (e *embeddedFS) getFileInfo(fullName string) (*embeddedFileInfo, error) { + // no need to do heavy "path.Clean()" because we don't want to support "foo/../bar" or absolute paths + fullName = strings.TrimPrefix(fullName, "./") + if fullName == "" { + fullName = "." + } + + e.filesMu.RLock() + fi := e.files[fullName] + e.filesMu.RUnlock() + if fi != nil { + return fi, nil + } + + fields := strings.Split(fullName, "/") + fi = e.meta().Root + if fullName != "." { + found := true + for _, field := range fields { + for _, child := range fi.Children { + if found = child.BaseName == field; found { + fi = child + break + } + } + if !found { + return nil, fs.ErrNotExist + } + } + } + + e.filesMu.Lock() + defer e.filesMu.Unlock() + if fi != nil { + fi.fs = e + fi.fullName = fullName + fi.data = e.data[fi.DataBegin : fi.DataBegin+fi.DataLen] + e.files[fullName] = fi // do not cache nil, otherwise keeping accessing random non-existing file will cause OOM + return fi, nil + } + return nil, fs.ErrNotExist +} + +func (e *embeddedFS) Open(name string) (fs.File, error) { + info, err := e.getFileInfo(name) + if err != nil { + return nil, err + } + base := EmbeddedFileBase{info: info} + base.dataReader = bytes.NewReader(base.info.data) + if info.DataLen != info.OriginSize { + decomp, err := gzip.NewReader(base.dataReader) + if err != nil { + return nil, err + } + return &EmbeddedCompressedFile{EmbeddedFileBase: base, decompressor: decomp}, nil + } + return &EmbeddedOriginFile{base}, nil +} + +var ( + _ EmbeddedFileInfo = (*embeddedFileInfo)(nil) + _ EmbeddedFile = (*EmbeddedOriginFile)(nil) + _ EmbeddedFile = (*EmbeddedCompressedFile)(nil) +) + +func (f *EmbeddedOriginFile) Read(p []byte) (n int, err error) { + return f.dataReader.Read(p) +} + +func (f *EmbeddedCompressedFile) Read(p []byte) (n int, err error) { + if f.decompressorPos > f.seekPos { + if err = f.decompressor.Reset(bytes.NewReader(f.info.data)); err != nil { + return 0, err + } + f.decompressorPos = 0 + } + if f.decompressorPos < f.seekPos { + if _, err = io.CopyN(io.Discard, f.decompressor, f.seekPos-f.decompressorPos); err != nil { + return 0, err + } + f.decompressorPos = f.seekPos + } + n, err = f.decompressor.Read(p) + f.decompressorPos += int64(n) + f.seekPos = f.decompressorPos + return n, err +} + +func (f *EmbeddedFileBase) Seek(offset int64, whence int) (int64, error) { + switch whence { + case io.SeekStart: + f.seekPos = offset + case io.SeekCurrent: + f.seekPos += offset + case io.SeekEnd: + f.seekPos = f.info.OriginSize + offset + } + return f.seekPos, nil +} + +func (f *EmbeddedFileBase) Stat() (fs.FileInfo, error) { + return f.info, nil +} + +func (f *EmbeddedOriginFile) Close() error { + return nil +} + +func (f *EmbeddedCompressedFile) Close() error { + return f.decompressor.Close() +} + +func (fi *embeddedFileInfo) Name() string { + return fi.BaseName +} + +func (fi *embeddedFileInfo) Size() int64 { + return fi.OriginSize +} + +func (fi *embeddedFileInfo) Mode() fs.FileMode { + return util.Iif(fi.IsDir(), fs.ModeDir|0o555, 0o444) +} + +func (fi *embeddedFileInfo) ModTime() time.Time { + return getExecutableModTime() +} + +func (fi *embeddedFileInfo) IsDir() bool { + return fi.Children != nil +} + +func (fi *embeddedFileInfo) Sys() any { + return nil +} + +func (fi *embeddedFileInfo) Type() fs.FileMode { + return util.Iif(fi.IsDir(), fs.ModeDir, 0) +} + +func (fi *embeddedFileInfo) Info() (fs.FileInfo, error) { + return fi, nil +} + +// getExecutableModTime returns the modification time of the executable file. +// In bindata, we can't use the ModTime of the files because we need to make the build reproducible +var getExecutableModTime = sync.OnceValue(func() (modTime time.Time) { + exePath, err := os.Executable() + if err != nil { + return modTime + } + exePath, err = filepath.Abs(exePath) + if err != nil { + return modTime + } + exePath, err = filepath.EvalSymlinks(exePath) + if err != nil { + return modTime + } + st, err := os.Stat(exePath) + if err != nil { + return modTime + } + return st.ModTime() +}) + +func GenerateEmbedBindata(fsRootPath, outputFile string) error { + output, err := os.OpenFile(outputFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.ModePerm) + if err != nil { + return err + } + defer output.Close() + + meta := &EmbeddedMeta{} + meta.Root = &embeddedFileInfo{} + var outputOffset int64 + var embedFiles func(parent *embeddedFileInfo, fsPath, embedPath string) error + embedFiles = func(parent *embeddedFileInfo, fsPath, embedPath string) error { + dirEntries, err := os.ReadDir(fsPath) + if err != nil { + return err + } + for _, dirEntry := range dirEntries { + if err != nil { + return err + } + if dirEntry.IsDir() { + child := &embeddedFileInfo{ + BaseName: dirEntry.Name(), + Children: []*embeddedFileInfo{}, // non-nil means it's a directory + } + parent.Children = append(parent.Children, child) + if err = embedFiles(child, filepath.Join(fsPath, dirEntry.Name()), path.Join(embedPath, dirEntry.Name())); err != nil { + return err + } + } else { + data, err := os.ReadFile(filepath.Join(fsPath, dirEntry.Name())) + if err != nil { + return err + } + var compressed bytes.Buffer + gz, _ := gzip.NewWriterLevel(&compressed, gzip.BestCompression) + if _, err = gz.Write(data); err != nil { + return err + } + if err = gz.Close(); err != nil { + return err + } + + // only use the compressed data if it is smaller than the original data + outputBytes := util.Iif(len(compressed.Bytes()) < len(data), compressed.Bytes(), data) + child := &embeddedFileInfo{ + BaseName: dirEntry.Name(), + OriginSize: int64(len(data)), + DataBegin: outputOffset, + DataLen: int64(len(outputBytes)), + } + if _, err = output.Write(outputBytes); err != nil { + return err + } + outputOffset += child.DataLen + parent.Children = append(parent.Children, child) + } + } + return nil + } + + if err = embedFiles(meta.Root, fsRootPath, ""); err != nil { + return err + } + jsonBuf, err := json.Marshal(meta) // can't use json.NewEncoder here because it writes extra EOL + if err != nil { + return err + } + _, _ = output.Write([]byte{'\n'}) + _, err = output.Write(jsonBuf) + return err +} diff --git a/modules/assetfs/embed_test.go b/modules/assetfs/embed_test.go new file mode 100644 index 00000000000..06598da4c4e --- /dev/null +++ b/modules/assetfs/embed_test.go @@ -0,0 +1,98 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package assetfs + +import ( + "bytes" + "io/fs" + "net/http" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEmbed(t *testing.T) { + tmpDir := t.TempDir() + tmpDataDir := tmpDir + "/data" + _ = os.MkdirAll(tmpDataDir+"/foo/bar", 0o755) + _ = os.WriteFile(tmpDataDir+"/a.txt", []byte("a"), 0o644) + _ = os.WriteFile(tmpDataDir+"/foo/bar/b.txt", bytes.Repeat([]byte("a"), 1000), 0o644) + _ = os.WriteFile(tmpDataDir+"/foo/c.txt", []byte("c"), 0o644) + require.NoError(t, GenerateEmbedBindata(tmpDataDir, tmpDir+"/out.dat")) + + data, err := os.ReadFile(tmpDir + "/out.dat") + require.NoError(t, err) + efs := NewEmbeddedFS(data) + + // test a non-existing file + _, err = fs.ReadFile(efs, "not exist") + assert.ErrorIs(t, err, fs.ErrNotExist) + + // test a normal file (no compression) + content, err := fs.ReadFile(efs, "a.txt") + require.NoError(t, err) + assert.Equal(t, "a", string(content)) + fi, err := fs.Stat(efs, "a.txt") + require.NoError(t, err) + _, ok := fi.(EmbeddedFileInfo).GetGzipContent() + assert.False(t, ok) + + // test a compressed file + content, err = fs.ReadFile(efs, "foo/bar/b.txt") + require.NoError(t, err) + assert.Equal(t, bytes.Repeat([]byte("a"), 1000), content) + fi, err = fs.Stat(efs, "foo/bar/b.txt") + require.NoError(t, err) + assert.False(t, fi.Mode().IsDir()) + assert.True(t, fi.Mode().IsRegular()) + gzipContent, ok := fi.(EmbeddedFileInfo).GetGzipContent() + assert.True(t, ok) + assert.Greater(t, len(gzipContent), 1) + assert.Less(t, len(gzipContent), 1000) + + // test list root directory + entries, err := fs.ReadDir(efs, ".") + require.NoError(t, err) + assert.Len(t, entries, 2) + assert.Equal(t, "a.txt", entries[0].Name()) + assert.False(t, entries[0].IsDir()) + + // test list subdirectory + entries, err = fs.ReadDir(efs, "foo") + require.NoError(t, err) + require.Len(t, entries, 2) + assert.Equal(t, "bar", entries[0].Name()) + assert.True(t, entries[0].IsDir()) + assert.Equal(t, "c.txt", entries[1].Name()) + assert.False(t, entries[1].IsDir()) + + // test directory mode + fi, err = fs.Stat(efs, "foo") + require.NoError(t, err) + assert.True(t, fi.IsDir()) + assert.True(t, fi.Mode().IsDir()) + assert.False(t, fi.Mode().IsRegular()) + + // test httpfs + hfs := http.FS(efs) + hf, err := hfs.Open("foo/bar/b.txt") + require.NoError(t, err) + hi, err := hf.Stat() + require.NoError(t, err) + fiEmbedded, ok := hi.(EmbeddedFileInfo) + require.True(t, ok) + gzipContent, ok = fiEmbedded.GetGzipContent() + assert.True(t, ok) + assert.Greater(t, len(gzipContent), 1) + assert.Less(t, len(gzipContent), 1000) + + // test httpfs directory listing + hf, err = hfs.Open("foo") + require.NoError(t, err) + dirs, err := hf.Readdir(1) + require.NoError(t, err) + assert.Len(t, dirs, 1) +} diff --git a/modules/assetfs/layered.go b/modules/assetfs/layered.go index 4f3811ba2b2..ce55475bd98 100644 --- a/modules/assetfs/layered.go +++ b/modules/assetfs/layered.go @@ -52,8 +52,8 @@ func Local(name, base string, sub ...string) *Layer { } // Bindata returns a new Layer with the given name, it serves files from the given bindata asset. -func Bindata(name string, fs http.FileSystem) *Layer { - return &Layer{name: name, fs: fs} +func Bindata(name string, fs fs.FS) *Layer { + return &Layer{name: name, fs: http.FS(fs)} } // LayeredFS is a layered asset file-system. It works like http.FileSystem, but it can have multiple layers. diff --git a/modules/auth/httpauth/httpauth.go b/modules/auth/httpauth/httpauth.go new file mode 100644 index 00000000000..7f1f1ee152c --- /dev/null +++ b/modules/auth/httpauth/httpauth.go @@ -0,0 +1,47 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package httpauth + +import ( + "encoding/base64" + "strings" + + "code.gitea.io/gitea/modules/util" +) + +type BasicAuth struct { + Username, Password string +} + +type BearerToken struct { + Token string +} + +type ParsedAuthorizationHeader struct { + BasicAuth *BasicAuth + BearerToken *BearerToken +} + +func ParseAuthorizationHeader(header string) (ret ParsedAuthorizationHeader, _ bool) { + parts := strings.Fields(header) + if len(parts) != 2 { + return ret, false + } + if util.AsciiEqualFold(parts[0], "basic") { + s, err := base64.StdEncoding.DecodeString(parts[1]) + if err != nil { + return ret, false + } + u, p, ok := strings.Cut(string(s), ":") + if !ok { + return ret, false + } + ret.BasicAuth = &BasicAuth{Username: u, Password: p} + return ret, true + } else if util.AsciiEqualFold(parts[0], "token") || util.AsciiEqualFold(parts[0], "bearer") { + ret.BearerToken = &BearerToken{Token: parts[1]} + return ret, true + } + return ret, false +} diff --git a/modules/auth/httpauth/httpauth_test.go b/modules/auth/httpauth/httpauth_test.go new file mode 100644 index 00000000000..087b86917f0 --- /dev/null +++ b/modules/auth/httpauth/httpauth_test.go @@ -0,0 +1,43 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package httpauth + +import ( + "encoding/base64" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseAuthorizationHeader(t *testing.T) { + type parsed = ParsedAuthorizationHeader + type basic = BasicAuth + type bearer = BearerToken + cases := []struct { + headerValue string + expected parsed + ok bool + }{ + {"", parsed{}, false}, + {"?", parsed{}, false}, + {"foo", parsed{}, false}, + {"any value", parsed{}, false}, + + {"Basic ?", parsed{}, false}, + {"Basic " + base64.StdEncoding.EncodeToString([]byte("foo")), parsed{}, false}, + {"Basic " + base64.StdEncoding.EncodeToString([]byte("foo:bar")), parsed{BasicAuth: &basic{"foo", "bar"}}, true}, + {"basic " + base64.StdEncoding.EncodeToString([]byte("foo:bar")), parsed{BasicAuth: &basic{"foo", "bar"}}, true}, + + {"token value", parsed{BearerToken: &bearer{"value"}}, true}, + {"Token value", parsed{BearerToken: &bearer{"value"}}, true}, + {"bearer value", parsed{BearerToken: &bearer{"value"}}, true}, + {"Bearer value", parsed{BearerToken: &bearer{"value"}}, true}, + {"Bearer wrong value", parsed{}, false}, + } + for _, c := range cases { + ret, ok := ParseAuthorizationHeader(c.headerValue) + assert.Equal(t, c.ok, ok, "header %q", c.headerValue) + assert.Equal(t, c.expected, ret, "header %q", c.headerValue) + } +} diff --git a/modules/auth/openid/discovery_cache_test.go b/modules/auth/openid/discovery_cache_test.go index 7d4b27c5dfe..f3d7dd226ef 100644 --- a/modules/auth/openid/discovery_cache_test.go +++ b/modules/auth/openid/discovery_cache_test.go @@ -26,7 +26,8 @@ func (s *testDiscoveredInfo) OpLocalID() string { } func TestTimedDiscoveryCache(t *testing.T) { - dc := newTimedDiscoveryCache(1 * time.Second) + ttl := 50 * time.Millisecond + dc := newTimedDiscoveryCache(ttl) // Put some initial values dc.Put("foo", &testDiscoveredInfo{}) // openid.opEndpoint: "a", openid.opLocalID: "b", openid.claimedID: "c"}) @@ -41,8 +42,8 @@ func TestTimedDiscoveryCache(t *testing.T) { // Attempt to get a non-existent value assert.Nil(t, dc.Get("bar")) - // Sleep one second and try retrieve again - time.Sleep(1 * time.Second) + // Sleep for a while and try to retrieve again + time.Sleep(ttl * 3 / 2) assert.Nil(t, dc.Get("foo")) } diff --git a/modules/auth/password/hash/common.go b/modules/auth/password/hash/common.go index 487c0738f42..d5e2c34314e 100644 --- a/modules/auth/password/hash/common.go +++ b/modules/auth/password/hash/common.go @@ -18,7 +18,7 @@ func parseIntParam(value, param, algorithmName, config string, previousErr error return parsed, previousErr // <- Keep the previous error as this function should still return an error once everything has been checked if any call failed } -func parseUIntParam(value, param, algorithmName, config string, previousErr error) (uint64, error) { //nolint:unparam +func parseUIntParam(value, param, algorithmName, config string, previousErr error) (uint64, error) { //nolint:unparam // algorithmName is always argon2 parsed, err := strconv.ParseUint(value, 10, 64) if err != nil { log.Error("invalid integer for %s representation in %s hash spec %s", param, algorithmName, config) diff --git a/modules/auth/password/password.go b/modules/auth/password/password.go index c66b62937fd..a1e101dd621 100644 --- a/modules/auth/password/password.go +++ b/modules/auth/password/password.go @@ -101,7 +101,7 @@ func Generate(n int) (string, error) { buffer := make([]byte, n) maxInt := big.NewInt(int64(len(validChars))) for { - for j := 0; j < n; j++ { + for j := range n { rnd, err := rand.Int(rand.Reader, maxInt) if err != nil { return "", err diff --git a/modules/auth/password/password_test.go b/modules/auth/password/password_test.go index 6c35dc86bd8..0fea593c859 100644 --- a/modules/auth/password/password_test.go +++ b/modules/auth/password/password_test.go @@ -50,7 +50,7 @@ func TestComplexity_Generate(t *testing.T) { test := func(t *testing.T, modes []string) { testComplextity(modes) - for i := 0; i < maxCount; i++ { + for range maxCount { pwd, err := Generate(pwdLen) assert.NoError(t, err) assert.Len(t, pwd, pwdLen) diff --git a/modules/auth/password/pwn/pwn.go b/modules/auth/password/pwn/pwn.go index f77ce9f40b2..99a6ca6ceac 100644 --- a/modules/auth/password/pwn/pwn.go +++ b/modules/auth/password/pwn/pwn.go @@ -101,7 +101,7 @@ func (c *Client) CheckPassword(pw string, padding bool) (int, error) { } defer resp.Body.Close() - for _, pair := range strings.Split(string(body), "\n") { + for pair := range strings.SplitSeq(string(body), "\n") { parts := strings.Split(pair, ":") if len(parts) != 2 { continue diff --git a/modules/avatar/identicon/block.go b/modules/avatar/identicon/block.go index cb1803a231a..fc8ce902128 100644 --- a/modules/avatar/identicon/block.go +++ b/modules/avatar/identicon/block.go @@ -24,8 +24,8 @@ func drawBlock(img *image.Paletted, x, y, size, angle int, points []int) { rotate(points, m, m, angle) } - for i := 0; i < size; i++ { - for j := 0; j < size; j++ { + for i := range size { + for j := range size { if pointInPolygon(i, j, points) { img.SetColorIndex(x+i, y+j, 1) } diff --git a/modules/avatar/identicon/identicon.go b/modules/avatar/identicon/identicon.go index 87bd87796ec..ee92416a536 100644 --- a/modules/avatar/identicon/identicon.go +++ b/modules/avatar/identicon/identicon.go @@ -134,7 +134,7 @@ func drawBlocks(p *image.Paletted, size int, c, b1, b2 blockFunc, b1Angle, b2Ang // then we make it left-right mirror, so we didn't draw 3/6/9 before for x := 0; x < size/2; x++ { - for y := 0; y < size; y++ { + for y := range size { p.SetColorIndex(size-x, y, p.ColorIndexAt(x, y)) } } diff --git a/modules/base/tool.go b/modules/base/tool.go index 02ca85569e1..ed94575e741 100644 --- a/modules/base/tool.go +++ b/modules/base/tool.go @@ -8,13 +8,10 @@ import ( "crypto/sha1" "crypto/sha256" "crypto/subtle" - "encoding/base64" "encoding/hex" - "errors" "fmt" "hash" "strconv" - "strings" "time" "code.gitea.io/gitea/modules/setting" @@ -36,19 +33,6 @@ func ShortSha(sha1 string) string { return util.TruncateRunes(sha1, 10) } -// BasicAuthDecode decode basic auth string -func BasicAuthDecode(encoded string) (string, string, error) { - s, err := base64.StdEncoding.DecodeString(encoded) - if err != nil { - return "", "", err - } - - if username, password, ok := strings.Cut(string(s), ":"); ok { - return username, password, nil - } - return "", "", errors.New("invalid basic authentication") -} - // VerifyTimeLimitCode verify time limit code func VerifyTimeLimitCode(now time.Time, data string, minutes int, code string) bool { if len(code) <= 18 { diff --git a/modules/base/tool_test.go b/modules/base/tool_test.go index 7cebedb073c..b7365e40c48 100644 --- a/modules/base/tool_test.go +++ b/modules/base/tool_test.go @@ -26,25 +26,6 @@ func TestShortSha(t *testing.T) { assert.Equal(t, "veryverylo", ShortSha("veryverylong")) } -func TestBasicAuthDecode(t *testing.T) { - _, _, err := BasicAuthDecode("?") - assert.Equal(t, "illegal base64 data at input byte 0", err.Error()) - - user, pass, err := BasicAuthDecode("Zm9vOmJhcg==") - assert.NoError(t, err) - assert.Equal(t, "foo", user) - assert.Equal(t, "bar", pass) - - _, _, err = BasicAuthDecode("aW52YWxpZA==") - assert.Error(t, err) - - _, _, err = BasicAuthDecode("invalid") - assert.Error(t, err) - - _, _, err = BasicAuthDecode("YWxpY2U=") // "alice", no colon - assert.Error(t, err) -} - func TestVerifyTimeLimitCode(t *testing.T) { defer test.MockVariableValue(&setting.InstallLock, true)() initGeneralSecret := func(secret string) { diff --git a/modules/cache/cache.go b/modules/cache/cache.go index a434c13b67d..039caa9fbc8 100644 --- a/modules/cache/cache.go +++ b/modules/cache/cache.go @@ -24,7 +24,7 @@ func Init() error { if err != nil { return err } - for i := 0; i < 10; i++ { + for range 10 { if err = c.Ping(); err == nil { break } diff --git a/modules/cache/cache_redis.go b/modules/cache/cache_redis.go index c5b52a2086c..7473c938af7 100644 --- a/modules/cache/cache_redis.go +++ b/modules/cache/cache_redis.go @@ -11,7 +11,7 @@ import ( "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/nosql" - "gitea.com/go-chi/cache" //nolint:depguard + "gitea.com/go-chi/cache" //nolint:depguard // we wrap this package here "github.com/redis/go-redis/v9" ) diff --git a/modules/cache/cache_twoqueue.go b/modules/cache/cache_twoqueue.go index 1eda2debc43..c8db686e576 100644 --- a/modules/cache/cache_twoqueue.go +++ b/modules/cache/cache_twoqueue.go @@ -10,7 +10,7 @@ import ( "code.gitea.io/gitea/modules/json" - mc "gitea.com/go-chi/cache" //nolint:depguard + mc "gitea.com/go-chi/cache" //nolint:depguard // we wrap this package here lru "github.com/hashicorp/golang-lru/v2" ) diff --git a/modules/cache/string_cache.go b/modules/cache/string_cache.go index 4f659616f50..3562b7a926c 100644 --- a/modules/cache/string_cache.go +++ b/modules/cache/string_cache.go @@ -11,7 +11,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" - chi_cache "gitea.com/go-chi/cache" //nolint:depguard + chi_cache "gitea.com/go-chi/cache" //nolint:depguard // we wrap this package here ) type GetJSONError struct { diff --git a/modules/charset/charset.go b/modules/charset/charset.go index 1855446a984..597ce5120c6 100644 --- a/modules/charset/charset.go +++ b/modules/charset/charset.go @@ -164,7 +164,7 @@ func DetectEncoding(content []byte) (string, error) { } times := 1024 / len(content) detectContent = make([]byte, 0, times*len(content)) - for i := 0; i < times; i++ { + for range times { detectContent = append(detectContent, content...) } } else { diff --git a/modules/charset/charset_test.go b/modules/charset/charset_test.go index 1fb362654d1..cd2e3b9aaa4 100644 --- a/modules/charset/charset_test.go +++ b/modules/charset/charset_test.go @@ -242,7 +242,7 @@ func stringMustEndWith(t *testing.T, expected, value string) { func TestToUTF8WithFallbackReader(t *testing.T) { resetDefaultCharsetsOrder() - for testLen := 0; testLen < 2048; testLen++ { + for testLen := range 2048 { pattern := " test { () }\n" input := "" for len(input) < testLen { diff --git a/modules/structs/commit_status.go b/modules/commitstatus/commit_status.go similarity index 54% rename from modules/structs/commit_status.go rename to modules/commitstatus/commit_status.go index dc880ef5eb9..a0ab4e71862 100644 --- a/modules/structs/commit_status.go +++ b/modules/commitstatus/commit_status.go @@ -1,11 +1,11 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package structs +package commitstatus // CommitStatusState holds the state of a CommitStatus -// It can be "pending", "success", "error" and "failure" -type CommitStatusState string +// swagger:enum CommitStatusState +type CommitStatusState string //nolint:revive // export stutter const ( // CommitStatusPending is for when the CommitStatus is Pending @@ -18,35 +18,14 @@ const ( CommitStatusFailure CommitStatusState = "failure" // CommitStatusWarning is for when the CommitStatus is Warning CommitStatusWarning CommitStatusState = "warning" + // CommitStatusSkipped is for when CommitStatus is Skipped + CommitStatusSkipped CommitStatusState = "skipped" ) -var commitStatusPriorities = map[CommitStatusState]int{ - CommitStatusError: 0, - CommitStatusFailure: 1, - CommitStatusWarning: 2, - CommitStatusPending: 3, - CommitStatusSuccess: 4, -} - func (css CommitStatusState) String() string { return string(css) } -// NoBetterThan returns true if this State is no better than the given State -// This function only handles the states defined in CommitStatusPriorities -func (css CommitStatusState) NoBetterThan(css2 CommitStatusState) bool { - // NoBetterThan only handles the 5 states above - if _, exist := commitStatusPriorities[css]; !exist { - return false - } - - if _, exist := commitStatusPriorities[css2]; !exist { - return false - } - - return commitStatusPriorities[css] <= commitStatusPriorities[css2] -} - // IsPending represents if commit status state is pending func (css CommitStatusState) IsPending() bool { return css == CommitStatusPending @@ -71,3 +50,32 @@ func (css CommitStatusState) IsFailure() bool { func (css CommitStatusState) IsWarning() bool { return css == CommitStatusWarning } + +// IsSkipped represents if commit status state is skipped +func (css CommitStatusState) IsSkipped() bool { + return css == CommitStatusSkipped +} + +type CommitStatusStates []CommitStatusState //nolint:revive // export stutter + +// According to https://docs.github.com/en/rest/commits/statuses?apiVersion=2022-11-28#get-the-combined-status-for-a-specific-reference +// > Additionally, a combined state is returned. The state is one of: +// > failure if any of the contexts report as error or failure +// > pending if there are no statuses or a context is pending +// > success if the latest status for all contexts is success +func (css CommitStatusStates) Combine() CommitStatusState { + successCnt := 0 + for _, state := range css { + switch { + case state.IsError() || state.IsFailure(): + return CommitStatusFailure + case state.IsPending(): + case state.IsSuccess() || state.IsWarning() || state.IsSkipped(): + successCnt++ + } + } + if successCnt > 0 && successCnt == len(css) { + return CommitStatusSuccess + } + return CommitStatusPending +} diff --git a/modules/commitstatus/commit_status_test.go b/modules/commitstatus/commit_status_test.go new file mode 100644 index 00000000000..10d8f20aa48 --- /dev/null +++ b/modules/commitstatus/commit_status_test.go @@ -0,0 +1,201 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package commitstatus + +import "testing" + +func TestCombine(t *testing.T) { + tests := []struct { + name string + states CommitStatusStates + expected CommitStatusState + }{ + // 0 states + { + name: "empty", + states: CommitStatusStates{}, + expected: CommitStatusPending, + }, + // 1 state + { + name: "pending", + states: CommitStatusStates{CommitStatusPending}, + expected: CommitStatusPending, + }, + { + name: "success", + states: CommitStatusStates{CommitStatusSuccess}, + expected: CommitStatusSuccess, + }, + { + name: "error", + states: CommitStatusStates{CommitStatusError}, + expected: CommitStatusFailure, + }, + { + name: "failure", + states: CommitStatusStates{CommitStatusFailure}, + expected: CommitStatusFailure, + }, + { + name: "warning", + states: CommitStatusStates{CommitStatusWarning}, + expected: CommitStatusSuccess, + }, + // 2 states + { + name: "pending and success", + states: CommitStatusStates{CommitStatusPending, CommitStatusSuccess}, + expected: CommitStatusPending, + }, + { + name: "pending and error", + states: CommitStatusStates{CommitStatusPending, CommitStatusError}, + expected: CommitStatusFailure, + }, + { + name: "pending and failure", + states: CommitStatusStates{CommitStatusPending, CommitStatusFailure}, + expected: CommitStatusFailure, + }, + { + name: "pending and warning", + states: CommitStatusStates{CommitStatusPending, CommitStatusWarning}, + expected: CommitStatusPending, + }, + { + name: "success and error", + states: CommitStatusStates{CommitStatusSuccess, CommitStatusError}, + expected: CommitStatusFailure, + }, + { + name: "success and failure", + states: CommitStatusStates{CommitStatusSuccess, CommitStatusFailure}, + expected: CommitStatusFailure, + }, + { + name: "success and warning", + states: CommitStatusStates{CommitStatusSuccess, CommitStatusWarning}, + expected: CommitStatusSuccess, + }, + { + name: "error and failure", + states: CommitStatusStates{CommitStatusError, CommitStatusFailure}, + expected: CommitStatusFailure, + }, + { + name: "error and warning", + states: CommitStatusStates{CommitStatusError, CommitStatusWarning}, + expected: CommitStatusFailure, + }, + { + name: "failure and warning", + states: CommitStatusStates{CommitStatusFailure, CommitStatusWarning}, + expected: CommitStatusFailure, + }, + // 3 states + { + name: "pending, success and warning", + states: CommitStatusStates{CommitStatusPending, CommitStatusSuccess, CommitStatusWarning}, + expected: CommitStatusPending, + }, + { + name: "pending, success and error", + states: CommitStatusStates{CommitStatusPending, CommitStatusSuccess, CommitStatusError}, + expected: CommitStatusFailure, + }, + { + name: "pending, success and failure", + states: CommitStatusStates{CommitStatusPending, CommitStatusSuccess, CommitStatusFailure}, + expected: CommitStatusFailure, + }, + { + name: "pending, error and failure", + states: CommitStatusStates{CommitStatusPending, CommitStatusError, CommitStatusFailure}, + expected: CommitStatusFailure, + }, + { + name: "success, error and warning", + states: CommitStatusStates{CommitStatusSuccess, CommitStatusError, CommitStatusWarning}, + expected: CommitStatusFailure, + }, + { + name: "success, failure and warning", + states: CommitStatusStates{CommitStatusSuccess, CommitStatusFailure, CommitStatusWarning}, + expected: CommitStatusFailure, + }, + { + name: "error, failure and warning", + states: CommitStatusStates{CommitStatusError, CommitStatusFailure, CommitStatusWarning}, + expected: CommitStatusFailure, + }, + { + name: "success, warning and skipped", + states: CommitStatusStates{CommitStatusSuccess, CommitStatusWarning, CommitStatusSkipped}, + expected: CommitStatusSuccess, + }, + // All success + { + name: "all success", + states: CommitStatusStates{CommitStatusSuccess, CommitStatusSuccess, CommitStatusSuccess}, + expected: CommitStatusSuccess, + }, + // All pending + { + name: "all pending", + states: CommitStatusStates{CommitStatusPending, CommitStatusPending, CommitStatusPending}, + expected: CommitStatusPending, + }, + { + name: "all skipped", + states: CommitStatusStates{CommitStatusSkipped, CommitStatusSkipped, CommitStatusSkipped}, + expected: CommitStatusSuccess, + }, + // 4 states + { + name: "pending, success, error and warning", + states: CommitStatusStates{CommitStatusPending, CommitStatusSuccess, CommitStatusError, CommitStatusWarning}, + expected: CommitStatusFailure, + }, + { + name: "pending, success, failure and warning", + states: CommitStatusStates{CommitStatusPending, CommitStatusSuccess, CommitStatusFailure, CommitStatusWarning}, + expected: CommitStatusFailure, + }, + { + name: "pending, error, failure and warning", + states: CommitStatusStates{CommitStatusPending, CommitStatusError, CommitStatusFailure, CommitStatusWarning}, + expected: CommitStatusFailure, + }, + { + name: "success, error, failure and warning", + states: CommitStatusStates{CommitStatusSuccess, CommitStatusError, CommitStatusFailure, CommitStatusWarning}, + expected: CommitStatusFailure, + }, + { + name: "mixed states", + states: CommitStatusStates{CommitStatusPending, CommitStatusSuccess, CommitStatusError, CommitStatusWarning}, + expected: CommitStatusFailure, + }, + { + name: "mixed states with all success", + states: CommitStatusStates{CommitStatusSuccess, CommitStatusSuccess, CommitStatusPending, CommitStatusWarning}, + expected: CommitStatusPending, + }, + { + name: "all success with warning", + states: CommitStatusStates{CommitStatusSuccess, CommitStatusSuccess, CommitStatusSuccess, CommitStatusWarning}, + expected: CommitStatusSuccess, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.states.Combine() + if result != tt.expected { + t.Errorf("expected %v, got %v", tt.expected, result) + } + }) + } +} diff --git a/modules/fileicon/entry.go b/modules/fileicon/entry.go index e4ded363e58..0326c2bfa8a 100644 --- a/modules/fileicon/entry.go +++ b/modules/fileicon/entry.go @@ -6,17 +6,17 @@ package fileicon import "code.gitea.io/gitea/modules/git" type EntryInfo struct { - FullName string + BaseName string EntryMode git.EntryMode SymlinkToMode git.EntryMode IsOpen bool } -func EntryInfoFromGitTreeEntry(gitEntry *git.TreeEntry) *EntryInfo { - ret := &EntryInfo{FullName: gitEntry.Name(), EntryMode: gitEntry.Mode()} +func EntryInfoFromGitTreeEntry(commit *git.Commit, fullPath string, gitEntry *git.TreeEntry) *EntryInfo { + ret := &EntryInfo{BaseName: gitEntry.Name(), EntryMode: gitEntry.Mode()} if gitEntry.IsLink() { - if te, err := gitEntry.FollowLink(); err == nil && te.IsDir() { - ret.SymlinkToMode = te.Mode() + if res, err := git.EntryFollowLink(commit, fullPath, gitEntry); err == nil && res.TargetEntry.IsDir() { + ret.SymlinkToMode = res.TargetEntry.Mode() } } return ret diff --git a/modules/fileicon/material.go b/modules/fileicon/material.go index 449f527ee80..5361592d8a3 100644 --- a/modules/fileicon/material.go +++ b/modules/fileicon/material.go @@ -5,7 +5,6 @@ package fileicon import ( "html/template" - "path" "strings" "sync" @@ -134,7 +133,7 @@ func (m *MaterialIconProvider) FindIconName(entry *EntryInfo) string { return "folder-git" } - fileNameLower := strings.ToLower(path.Base(entry.FullName)) + fileNameLower := strings.ToLower(entry.BaseName) if entry.EntryMode.IsDir() { if s, ok := m.rules.FolderNames[fileNameLower]; ok { return s diff --git a/modules/fileicon/material_test.go b/modules/fileicon/material_test.go index 68353d21899..d2a769eaac0 100644 --- a/modules/fileicon/material_test.go +++ b/modules/fileicon/material_test.go @@ -20,8 +20,8 @@ func TestMain(m *testing.M) { func TestFindIconName(t *testing.T) { unittest.PrepareTestEnv(t) p := fileicon.DefaultMaterialIconProvider() - assert.Equal(t, "php", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.php", EntryMode: git.EntryModeBlob})) - assert.Equal(t, "php", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.PHP", EntryMode: git.EntryModeBlob})) - assert.Equal(t, "javascript", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.js", EntryMode: git.EntryModeBlob})) - assert.Equal(t, "visualstudio", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.vba", EntryMode: git.EntryModeBlob})) + assert.Equal(t, "php", p.FindIconName(&fileicon.EntryInfo{BaseName: "foo.php", EntryMode: git.EntryModeBlob})) + assert.Equal(t, "php", p.FindIconName(&fileicon.EntryInfo{BaseName: "foo.PHP", EntryMode: git.EntryModeBlob})) + assert.Equal(t, "javascript", p.FindIconName(&fileicon.EntryInfo{BaseName: "foo.js", EntryMode: git.EntryModeBlob})) + assert.Equal(t, "visualstudio", p.FindIconName(&fileicon.EntryInfo{BaseName: "foo.vba", EntryMode: git.EntryModeBlob})) } diff --git a/modules/git/blame.go b/modules/git/blame.go index 6eb583a6b9c..659dec34a1e 100644 --- a/modules/git/blame.go +++ b/modules/git/blame.go @@ -132,18 +132,22 @@ func (r *BlameReader) Close() error { } // 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) { - reader, stdout, err := os.Pipe() - if err != nil { - return nil, err - } +func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath string, commit *Commit, file string, bypassBlameIgnore bool) (rd *BlameReader, err error) { + var ignoreRevsFileName string + var ignoreRevsFileCleanup func() + defer func() { + if err != nil && ignoreRevsFileCleanup != nil { + ignoreRevsFileCleanup() + } + }() 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 { - ignoreRevsFileName, ignoreRevsFileCleanup = tryCreateBlameIgnoreRevsFile(commit) + ignoreRevsFileName, ignoreRevsFileCleanup, err = tryCreateBlameIgnoreRevsFile(commit) + if err != nil && !IsErrNotExist(err) { + return nil, err + } if ignoreRevsFileName != "" { // 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. @@ -154,6 +158,10 @@ func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath cmd.AddDynamicArguments(commit.ID.String()).AddDashesAndList(file) done := make(chan error, 1) + reader, stdout, err := os.Pipe() + if err != nil { + return nil, err + } go func() { 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" @@ -182,33 +190,29 @@ func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath }, nil } -func tryCreateBlameIgnoreRevsFile(commit *Commit) (string, func()) { +func tryCreateBlameIgnoreRevsFile(commit *Commit) (string, func(), error) { entry, err := commit.GetTreeEntryByPath(".git-blame-ignore-revs") if err != nil { - log.Error("Unable to get .git-blame-ignore-revs file: GetTreeEntryByPath: %v", err) - return "", nil + return "", nil, err } r, err := entry.Blob().DataAsync() if err != nil { - log.Error("Unable to get .git-blame-ignore-revs file data: DataAsync: %v", err) - return "", nil + return "", nil, err } defer r.Close() f, cleanup, err := setting.AppDataTempDir("git-repo-content").CreateTempFileRandom("git-blame-ignore-revs") if err != nil { - log.Error("Unable to get .git-blame-ignore-revs file data: CreateTempFileRandom: %v", err) - return "", nil + return "", nil, err } filename := f.Name() _, err = io.Copy(f, r) _ = f.Close() if err != nil { cleanup() - log.Error("Unable to get .git-blame-ignore-revs file data: Copy: %v", err) - return "", nil + return "", nil, err } - return filename, cleanup + return filename, cleanup, nil } diff --git a/modules/git/blob.go b/modules/git/blob.go index b7857dbbc61..40d8f44e799 100644 --- a/modules/git/blob.go +++ b/modules/git/blob.go @@ -9,6 +9,7 @@ import ( "encoding/base64" "errors" "io" + "strings" "code.gitea.io/gitea/modules/typesniffer" "code.gitea.io/gitea/modules/util" @@ -21,17 +22,22 @@ func (b *Blob) Name() string { return b.name } -// GetBlobContent Gets the limited content of the blob as raw text -func (b *Blob) GetBlobContent(limit int64) (string, error) { +// GetBlobBytes Gets the limited content of the blob +func (b *Blob) GetBlobBytes(limit int64) ([]byte, error) { if limit <= 0 { - return "", nil + return nil, nil } dataRc, err := b.DataAsync() if err != nil { - return "", err + return nil, err } defer dataRc.Close() - buf, err := util.ReadWithLimit(dataRc, int(limit)) + return util.ReadWithLimit(dataRc, int(limit)) +} + +// GetBlobContent Gets the limited content of the blob as raw text +func (b *Blob) GetBlobContent(limit int64) (string, error) { + buf, err := b.GetBlobBytes(limit) return string(buf), err } @@ -63,42 +69,44 @@ func (b *Blob) GetBlobLineCount(w io.Writer) (int, error) { } } -// GetBlobContentBase64 Reads the content of the blob with a base64 encode and returns the encoded string -func (b *Blob) GetBlobContentBase64() (string, error) { +// GetBlobContentBase64 Reads the content of the blob with a base64 encoding and returns the encoded string +func (b *Blob) GetBlobContentBase64(originContent *strings.Builder) (string, error) { dataRc, err := b.DataAsync() if err != nil { return "", err } defer dataRc.Close() - pr, pw := io.Pipe() - encoder := base64.NewEncoder(base64.StdEncoding, pw) - - go func() { - _, err := io.Copy(encoder, dataRc) - _ = encoder.Close() - - if err != nil { - _ = pw.CloseWithError(err) - } else { - _ = pw.Close() + base64buf := &strings.Builder{} + encoder := base64.NewEncoder(base64.StdEncoding, base64buf) + buf := make([]byte, 32*1024) +loop: + for { + n, err := dataRc.Read(buf) + if n > 0 { + if originContent != nil { + _, _ = originContent.Write(buf[:n]) + } + if _, err := encoder.Write(buf[:n]); err != nil { + return "", err + } + } + switch { + case errors.Is(err, io.EOF): + break loop + case err != nil: + return "", err } - }() - - out, err := io.ReadAll(pr) - if err != nil { - return "", err } - return string(out), nil + _ = encoder.Close() + return base64buf.String(), nil } // GuessContentType guesses the content type of the blob. func (b *Blob) GuessContentType() (typesniffer.SniffedType, error) { - r, err := b.DataAsync() + buf, err := b.GetBlobBytes(typesniffer.SniffContentSize) if err != nil { return typesniffer.SniffedType{}, err } - defer r.Close() - - return typesniffer.DetectContentTypeFromReader(r) + return typesniffer.DetectContentType(buf), nil } diff --git a/modules/git/command.go b/modules/git/command.go index eaaa4969d0b..22f1d023391 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -47,6 +47,7 @@ type Command struct { globalArgsLength int brokenArgs []string cmd *exec.Cmd // for debug purpose only + configArgs []string } func logArgSanitize(arg string) string { @@ -196,6 +197,16 @@ func (c *Command) AddDashesAndList(list ...string) *Command { return c } +func (c *Command) AddConfig(key, value string) *Command { + kv := key + "=" + value + if !isSafeArgumentValue(kv) { + c.brokenArgs = append(c.brokenArgs, key) + } else { + c.configArgs = append(c.configArgs, "-c", kv) + } + return c +} + // ToTrustedCmdArgs converts a list of strings (trusted as argument) to TrustedCmdArgs // In most cases, it shouldn't be used. Use NewCommand().AddXxx() function instead func ToTrustedCmdArgs(args []string) TrustedCmdArgs { @@ -321,7 +332,7 @@ func (c *Command) run(ctx context.Context, skip int, opts *RunOpts) error { startTime := time.Now() - cmd := exec.CommandContext(ctx, c.prog, c.args...) + cmd := exec.CommandContext(ctx, c.prog, append(c.configArgs, c.args...)...) c.cmd = cmd // for debug purpose only if opts.Env == nil { cmd.Env = os.Environ() diff --git a/modules/git/commit.go b/modules/git/commit.go index cd50c511514..ed4876e7b3e 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -20,7 +20,8 @@ import ( // Commit represents a git commit. type Commit struct { - Tree + Tree // FIXME: bad design, this field can be nil if the commit is from "last commit cache" + ID ObjectID // The ID of this commit object Author *Signature Committer *Signature @@ -34,7 +35,7 @@ type Commit struct { // CommitSignature represents a git commit signature part. type CommitSignature struct { 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. @@ -277,8 +278,8 @@ func NewSearchCommitsOptions(searchString string, forAllRefs bool) SearchCommits var keywords, authors, committers []string var after, before string - fields := strings.Fields(searchString) - for _, k := range fields { + fields := strings.FieldsSeq(searchString) + for k := range fields { switch { case strings.HasPrefix(k, "author:"): authors = append(authors, strings.TrimPrefix(k, "author:")) diff --git a/modules/git/commit_info_nogogit.go b/modules/git/commit_info_nogogit.go index 7a6af0410bb..1b45fc8a6c3 100644 --- a/modules/git/commit_info_nogogit.go +++ b/modules/git/commit_info_nogogit.go @@ -7,8 +7,7 @@ package git import ( "context" - "fmt" - "io" + "maps" "path" "sort" @@ -40,9 +39,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath return nil, nil, err } - for pth, found := range commits { - revs[pth] = found - } + maps.Copy(revs, commits) } } else { sort.Strings(entryPaths) @@ -124,48 +121,25 @@ func GetLastCommitForPaths(ctx context.Context, commit *Commit, treePath string, return nil, err } - batchStdinWriter, batchReader, cancel, err := commit.repo.CatFileBatch(ctx) - if err != nil { - return nil, err - } - defer cancel() - commitsMap := map[string]*Commit{} commitsMap[commit.ID.String()] = commit commitCommits := map[string]*Commit{} for path, commitID := range revs { + if len(commitID) == 0 { + continue + } + c, ok := commitsMap[commitID] if ok { commitCommits[path] = c continue } - if len(commitID) == 0 { - continue - } - - _, err := batchStdinWriter.Write([]byte(commitID + "\n")) + c, err := commit.repo.GetCommit(commitID) // Ensure the commit exists in the repository if err != nil { return nil, err } - _, typ, size, err := ReadBatchLine(batchReader) - if err != nil { - return nil, err - } - if typ != "commit" { - if err := DiscardFull(batchReader, size+1); err != nil { - return nil, err - } - return nil, fmt.Errorf("unexpected type: %s for commit id: %s", typ, commitID) - } - c, err = CommitFromReader(commit.repo, MustIDFromString(commitID), io.LimitReader(batchReader, size)) - if err != nil { - return nil, err - } - if _, err := batchReader.Discard(1); err != nil { - return nil, err - } commitCommits[path] = c } diff --git a/modules/git/commit_reader.go b/modules/git/commit_reader.go index 228bbaf314d..eb8f4c63227 100644 --- a/modules/git/commit_reader.go +++ b/modules/git/commit_reader.go @@ -6,10 +6,44 @@ package git import ( "bufio" "bytes" + "fmt" "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 // 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{}, } - payloadSB := new(strings.Builder) - signatureSB := new(strings.Builder) - messageSB := new(strings.Builder) - message := false - pgpsig := false - - bufReader, ok := reader.(*bufio.Reader) - if !ok { - bufReader = bufio.NewReader(reader) - } - -readLoop: + bufReader := bufio.NewReader(reader) + inHeader := true + var payloadSB, messageSB bytes.Buffer + var headerKey string + var headerValue []byte for { line, err := bufReader.ReadBytes('\n') - if err != nil { - if err == io.EOF { - if message { - _, _ = messageSB.Write(line) + if err != nil && err != io.EOF { + return nil, fmt.Errorf("unable to read commit %q: %w", objectID.String(), err) + } + 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) - break readLoop + headerKey = string(k) // it also resets the headerValue to empty string if not inHeader + headerValue = v + } else { + headerValue = append(headerValue, v...) } - return nil, err - } - 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 + if headerKey != commitHeaderGpgsig && headerKey != commitHeaderGpgsigSha256 { _, _ = 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 { _, _ = messageSB.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 } diff --git a/modules/git/commit_sha256_test.go b/modules/git/commit_sha256_test.go index 64a0f539088..97ccecdaccf 100644 --- a/modules/git/commit_sha256_test.go +++ b/modules/git/commit_sha256_test.go @@ -60,8 +60,7 @@ func TestGetFullCommitIDErrorSha256(t *testing.T) { } func TestCommitFromReaderSha256(t *testing.T) { - commitString := `9433b2a62b964c17a4485ae180f45f595d3e69d31b786087775e28c6b6399df0 commit 1114 -tree e7f9e96dd79c09b078cac8b303a7d3b9d65ff9b734e86060a4d20409fd379f9e + commitString := `tree e7f9e96dd79c09b078cac8b303a7d3b9d65ff9b734e86060a4d20409fd379f9e parent 26e9ccc29fad747e9c5d9f4c9ddeb7eff61cc45ef6a8dc258cbeb181afc055e8 author Adam Majer
", // by the way, empty "a" tag will be removed
+ `
Here is a simple footnote,1 and here is a longer one.2
+Here is a simple footnote,1 and here is a longer one.2
`) + ``
- _ = r.renderInternal.FormatWithSafeAttrs(w, template.HTML(code))
+ codeHTML := giteaUtil.Iif[template.HTML](n.Inline, "", ``) + ``
+ _, _ = w.WriteString(string(r.renderInternal.ProtectSafeAttrs(codeHTML)))
r.writeLines(w, source, n)
} else {
_, _ = w.WriteString(`` + giteaUtil.Iif(n.Inline, "", `
`) + "\n")
diff --git a/modules/markup/markdown/math/inline_renderer.go b/modules/markup/markdown/math/inline_renderer.go
index d000a7b317a..eeeb60cc7ee 100644
--- a/modules/markup/markdown/math/inline_renderer.go
+++ b/modules/markup/markdown/math/inline_renderer.go
@@ -28,7 +28,7 @@ func NewInlineRenderer(renderInternal *internal.RenderInternal) renderer.NodeRen
func (r *InlineRenderer) renderInline(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
if entering {
- _ = r.renderInternal.FormatWithSafeAttrs(w, ``)
+ _, _ = w.WriteString(string(r.renderInternal.ProtectSafeAttrs(``)))
for c := n.FirstChild(); c != nil; c = c.NextSibling() {
segment := c.(*ast.Text).Segment
value := util.EscapeHTML(segment.Value(source))
diff --git a/modules/markup/markdown/meta_test.go b/modules/markup/markdown/meta_test.go
index 3f74adeaef4..283d289d483 100644
--- a/modules/markup/markdown/meta_test.go
+++ b/modules/markup/markdown/meta_test.go
@@ -60,7 +60,7 @@ func TestExtractMetadata(t *testing.T) {
func TestExtractMetadataBytes(t *testing.T) {
t.Run("ValidFrontAndBody", func(t *testing.T) {
var meta IssueTemplate
- body, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s\n%s", sepTest, frontTest, sepTest, bodyTest)), &meta)
+ body, err := ExtractMetadataBytes(fmt.Appendf(nil, "%s\n%s\n%s\n%s", sepTest, frontTest, sepTest, bodyTest), &meta)
assert.NoError(t, err)
assert.Equal(t, bodyTest, string(body))
assert.Equal(t, metaTest, meta)
@@ -69,19 +69,19 @@ func TestExtractMetadataBytes(t *testing.T) {
t.Run("NoFirstSeparator", func(t *testing.T) {
var meta IssueTemplate
- _, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s", frontTest, sepTest, bodyTest)), &meta)
+ _, err := ExtractMetadataBytes(fmt.Appendf(nil, "%s\n%s\n%s", frontTest, sepTest, bodyTest), &meta)
assert.Error(t, err)
})
t.Run("NoLastSeparator", func(t *testing.T) {
var meta IssueTemplate
- _, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s", sepTest, frontTest, bodyTest)), &meta)
+ _, err := ExtractMetadataBytes(fmt.Appendf(nil, "%s\n%s\n%s", sepTest, frontTest, bodyTest), &meta)
assert.Error(t, err)
})
t.Run("NoBody", func(t *testing.T) {
var meta IssueTemplate
- body, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s", sepTest, frontTest, sepTest)), &meta)
+ body, err := ExtractMetadataBytes(fmt.Appendf(nil, "%s\n%s\n%s", sepTest, frontTest, sepTest), &meta)
assert.NoError(t, err)
assert.Empty(t, string(body))
assert.Equal(t, metaTest, meta)
diff --git a/modules/markup/markdown/transform_blockquote.go b/modules/markup/markdown/transform_blockquote.go
index 3a8c6fa0186..bf17f01681c 100644
--- a/modules/markup/markdown/transform_blockquote.go
+++ b/modules/markup/markdown/transform_blockquote.go
@@ -46,7 +46,7 @@ func (g *ASTTransformer) extractBlockquoteAttentionEmphasis(firstParagraph ast.N
if !ok {
return "", nil
}
- val1 := string(node1.Text(reader.Source())) //nolint:staticcheck
+ val1 := string(node1.Text(reader.Source())) //nolint:staticcheck // Text is deprecated
attentionType := strings.ToLower(val1)
if g.attentionTypes.Contains(attentionType) {
return attentionType, []ast.Node{node1}
diff --git a/modules/markup/markdown/transform_codespan.go b/modules/markup/markdown/transform_codespan.go
index bccc43aad25..c2e4295bc2b 100644
--- a/modules/markup/markdown/transform_codespan.go
+++ b/modules/markup/markdown/transform_codespan.go
@@ -68,7 +68,7 @@ func cssColorHandler(value string) bool {
}
func (g *ASTTransformer) transformCodeSpan(_ *markup.RenderContext, v *ast.CodeSpan, reader text.Reader) {
- colorContent := v.Text(reader.Source()) //nolint:staticcheck
+ colorContent := v.Text(reader.Source()) //nolint:staticcheck // Text is deprecated
if cssColorHandler(string(colorContent)) {
v.AppendChild(v, NewColorPreview(colorContent))
}
diff --git a/modules/markup/markdown/transform_heading.go b/modules/markup/markdown/transform_heading.go
index 5f8a12794da..a229a7b1a4d 100644
--- a/modules/markup/markdown/transform_heading.go
+++ b/modules/markup/markdown/transform_heading.go
@@ -16,10 +16,10 @@ import (
func (g *ASTTransformer) transformHeading(_ *markup.RenderContext, v *ast.Heading, reader text.Reader, tocList *[]Header) {
for _, attr := range v.Attributes() {
if _, ok := attr.Value.([]byte); !ok {
- v.SetAttribute(attr.Name, []byte(fmt.Sprintf("%v", attr.Value)))
+ v.SetAttribute(attr.Name, fmt.Appendf(nil, "%v", attr.Value))
}
}
- txt := v.Text(reader.Source()) //nolint:staticcheck
+ txt := v.Text(reader.Source()) //nolint:staticcheck // Text is deprecated
header := Header{
Text: util.UnsafeBytesToString(txt),
Level: v.Level,
diff --git a/modules/markup/mdstripper/mdstripper.go b/modules/markup/mdstripper/mdstripper.go
index c589926b5e7..5a6504416ab 100644
--- a/modules/markup/mdstripper/mdstripper.go
+++ b/modules/markup/mdstripper/mdstripper.go
@@ -46,7 +46,7 @@ func (r *stripRenderer) Render(w io.Writer, source []byte, doc ast.Node) error {
coalesce := prevSibIsText
r.processString(
w,
- v.Text(source), //nolint:staticcheck
+ v.Text(source), //nolint:staticcheck // Text is deprecated
coalesce)
if v.SoftLineBreak() {
r.doubleSpace(w)
@@ -91,8 +91,7 @@ func (r *stripRenderer) processAutoLink(w io.Writer, link []byte) {
}
// Note: we're not attempting to match the URL scheme (http/https)
- host := strings.ToLower(u.Host)
- if host != "" && host != strings.ToLower(r.localhost.Host) {
+ if u.Host != "" && !strings.EqualFold(u.Host, r.localhost.Host) {
// Process out of band
r.links = append(r.links, linkStr)
return
diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go
index 35f90eb46cb..b6e9c348b73 100644
--- a/modules/markup/renderer.go
+++ b/modules/markup/renderer.go
@@ -4,12 +4,12 @@
package markup
import (
- "bytes"
"io"
"path"
"strings"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/typesniffer"
)
// Renderer defines an interface for rendering markup file to HTML
@@ -37,7 +37,7 @@ type ExternalRenderer interface {
// RendererContentDetector detects if the content can be rendered
// by specified renderer
type RendererContentDetector interface {
- CanRender(filename string, input io.Reader) bool
+ CanRender(filename string, sniffedType typesniffer.SniffedType, prefetchBuf []byte) bool
}
var (
@@ -60,13 +60,9 @@ func GetRendererByFileName(filename string) Renderer {
}
// DetectRendererType detects the markup type of the content
-func DetectRendererType(filename string, input io.Reader) string {
- buf, err := io.ReadAll(input)
- if err != nil {
- return ""
- }
+func DetectRendererType(filename string, sniffedType typesniffer.SniffedType, prefetchBuf []byte) string {
for _, renderer := range renderers {
- if detector, ok := renderer.(RendererContentDetector); ok && detector.CanRender(filename, bytes.NewReader(buf)) {
+ if detector, ok := renderer.(RendererContentDetector); ok && detector.CanRender(filename, sniffedType, prefetchBuf) {
return renderer.Name()
}
}
diff --git a/modules/markup/sanitizer_default.go b/modules/markup/sanitizer_default.go
index 14161eb5337..0fbf0f0b24c 100644
--- a/modules/markup/sanitizer_default.go
+++ b/modules/markup/sanitizer_default.go
@@ -4,6 +4,7 @@
package markup
import (
+ "html/template"
"io"
"net/url"
"regexp"
@@ -52,6 +53,8 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy {
policy.AllowAttrs("src", "autoplay", "controls").OnElements("video")
+ policy.AllowAttrs("loading").OnElements("img")
+
// Allow generally safe attributes (reference: https://github.com/jch/html-pipeline)
generalSafeAttrs := []string{
"abbr", "accept", "accept-charset",
@@ -90,9 +93,9 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy {
return policy
}
-// Sanitize takes a string that contains a HTML fragment or document and applies policy whitelist.
-func Sanitize(s string) string {
- return GetDefaultSanitizer().defaultPolicy.Sanitize(s)
+// Sanitize use default sanitizer policy to sanitize a string
+func Sanitize(s string) template.HTML {
+ return template.HTML(GetDefaultSanitizer().defaultPolicy.Sanitize(s))
}
// SanitizeReader sanitizes a Reader
diff --git a/modules/markup/sanitizer_default_test.go b/modules/markup/sanitizer_default_test.go
index 5282916944d..e5ba018e1be 100644
--- a/modules/markup/sanitizer_default_test.go
+++ b/modules/markup/sanitizer_default_test.go
@@ -69,6 +69,6 @@ func TestSanitizer(t *testing.T) {
}
for i := 0; i < len(testCases); i += 2 {
- assert.Equal(t, testCases[i+1], Sanitize(testCases[i]))
+ assert.Equal(t, testCases[i+1], string(Sanitize(testCases[i])))
}
}
diff --git a/modules/migration/schemas_bindata.go b/modules/migration/schemas_bindata.go
index c5db3b34615..695c2c11352 100644
--- a/modules/migration/schemas_bindata.go
+++ b/modules/migration/schemas_bindata.go
@@ -3,6 +3,28 @@
//go:build bindata
+//go:generate go run ../../build/generate-bindata.go ../../modules/migration/schemas bindata.dat
+
package migration
-//go:generate go run ../../build/generate-bindata.go ../../modules/migration/schemas migration bindata.go
+import (
+ "io"
+ "io/fs"
+ "path"
+ "sync"
+
+ _ "embed"
+
+ "code.gitea.io/gitea/modules/assetfs"
+)
+
+//go:embed bindata.dat
+var bindata []byte
+
+var BuiltinAssets = sync.OnceValue(func() fs.FS {
+ return assetfs.NewEmbeddedFS(bindata)
+})
+
+func openSchema(filename string) (io.ReadCloser, error) {
+ return BuiltinAssets().Open(path.Base(filename))
+}
diff --git a/modules/migration/schemas_static.go b/modules/migration/schemas_static.go
deleted file mode 100644
index 8a0c340a655..00000000000
--- a/modules/migration/schemas_static.go
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright 2022 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-//go:build bindata
-
-package migration
-
-import (
- "io"
- "path"
-)
-
-func openSchema(filename string) (io.ReadCloser, error) {
- return Assets.Open(path.Base(filename))
-}
diff --git a/modules/optional/option.go b/modules/optional/option.go
index ccbad259c21..6075c6347e0 100644
--- a/modules/optional/option.go
+++ b/modules/optional/option.go
@@ -5,6 +5,12 @@ package optional
import "strconv"
+// Option is a generic type that can hold a value of type T or be empty (None).
+//
+// It must use the slice type to work with "chi" form values binding:
+// * non-existing value are represented as an empty slice (None)
+// * existing value is represented as a slice with one element (Some)
+// * multiple values are represented as a slice with multiple elements (Some), the Value is the first element (not well-defined in this case)
type Option[T any] []T
func None[T any]() Option[T] {
diff --git a/modules/optional/serialization_test.go b/modules/optional/serialization_test.go
index 21d3ad84700..cf81a94cfce 100644
--- a/modules/optional/serialization_test.go
+++ b/modules/optional/serialization_test.go
@@ -4,7 +4,7 @@
package optional_test
import (
- std_json "encoding/json" //nolint:depguard
+ std_json "encoding/json" //nolint:depguard // for testing purpose
"testing"
"code.gitea.io/gitea/modules/json"
diff --git a/modules/options/options_bindata.go b/modules/options/options_bindata.go
index 29151cb3cbc..b2321d7eb50 100644
--- a/modules/options/options_bindata.go
+++ b/modules/options/options_bindata.go
@@ -3,6 +3,21 @@
//go:build bindata
+//go:generate go run ../../build/generate-bindata.go ../../options bindata.dat
+
package options
-//go:generate go run ../../build/generate-bindata.go ../../options options bindata.go
+import (
+ "sync"
+
+ _ "embed"
+
+ "code.gitea.io/gitea/modules/assetfs"
+)
+
+//go:embed bindata.dat
+var bindata []byte
+
+var BuiltinAssets = sync.OnceValue(func() *assetfs.Layer {
+ return assetfs.Bindata("builtin(bindata)", assetfs.NewEmbeddedFS(bindata))
+})
diff --git a/modules/options/dynamic.go b/modules/options/options_dynamic.go
similarity index 100%
rename from modules/options/dynamic.go
rename to modules/options/options_dynamic.go
diff --git a/modules/options/static.go b/modules/options/static.go
deleted file mode 100644
index 72b28e990e7..00000000000
--- a/modules/options/static.go
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright 2022 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-//go:build bindata
-
-package options
-
-import (
- "code.gitea.io/gitea/modules/assetfs"
-)
-
-func BuiltinAssets() *assetfs.Layer {
- return assetfs.Bindata("builtin(bindata)", Assets)
-}
diff --git a/models/packages/container/const.go b/modules/packages/container/const.go
similarity index 65%
rename from models/packages/container/const.go
rename to modules/packages/container/const.go
index 0dfbda051db..6c7c9b46d13 100644
--- a/models/packages/container/const.go
+++ b/modules/packages/container/const.go
@@ -4,6 +4,8 @@
package container
const (
+ ContentTypeDockerDistributionManifestV2 = "application/vnd.docker.distribution.manifest.v2+json"
+
ManifestFilename = "manifest.json"
UploadVersion = "_upload"
)
diff --git a/modules/packages/container/metadata.go b/modules/packages/container/metadata.go
index 2a41fb9105e..3ef0684d13f 100644
--- a/modules/packages/container/metadata.go
+++ b/modules/packages/container/metadata.go
@@ -71,14 +71,34 @@ type Manifest struct {
Size int64 `json:"size"`
}
+func IsMediaTypeValid(mt string) bool {
+ return strings.HasPrefix(mt, "application/vnd.docker.") || strings.HasPrefix(mt, "application/vnd.oci.")
+}
+
+func IsMediaTypeImageManifest(mt string) bool {
+ return strings.EqualFold(mt, oci.MediaTypeImageManifest) || strings.EqualFold(mt, "application/vnd.docker.distribution.manifest.v2+json")
+}
+
+func IsMediaTypeImageIndex(mt string) bool {
+ return strings.EqualFold(mt, oci.MediaTypeImageIndex) || strings.EqualFold(mt, "application/vnd.docker.distribution.manifest.list.v2+json")
+}
+
// ParseImageConfig parses the metadata of an image config
-func ParseImageConfig(mt string, r io.Reader) (*Metadata, error) {
- if strings.EqualFold(mt, helm.ConfigMediaType) {
+func ParseImageConfig(mediaType string, r io.Reader) (*Metadata, error) {
+ if strings.EqualFold(mediaType, helm.ConfigMediaType) {
return parseHelmConfig(r)
}
// fallback to OCI Image Config
- return parseOCIImageConfig(r)
+ // FIXME: this fallback is not right, we should strictly check the media type in the future
+ metadata, err := parseOCIImageConfig(r)
+ if err != nil {
+ if !IsMediaTypeImageManifest(mediaType) {
+ return &Metadata{Platform: "unknown/unknown"}, nil
+ }
+ return nil, err
+ }
+ return metadata, nil
}
func parseOCIImageConfig(r io.Reader) (*Metadata, error) {
diff --git a/modules/packages/container/metadata_test.go b/modules/packages/container/metadata_test.go
index 665499b2e66..0f2d702925c 100644
--- a/modules/packages/container/metadata_test.go
+++ b/modules/packages/container/metadata_test.go
@@ -11,6 +11,7 @@ import (
oci "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func TestParseImageConfig(t *testing.T) {
@@ -58,4 +59,8 @@ func TestParseImageConfig(t *testing.T) {
assert.ElementsMatch(t, []string{author}, metadata.Authors)
assert.Equal(t, projectURL, metadata.ProjectURL)
assert.Equal(t, repositoryURL, metadata.RepositoryURL)
+
+ metadata, err = ParseImageConfig("anything-unknown", strings.NewReader(""))
+ require.NoError(t, err)
+ assert.Equal(t, &Metadata{Platform: "unknown/unknown"}, metadata)
}
diff --git a/modules/packages/content_store.go b/modules/packages/content_store.go
index 37612556d7f..dadb7eaefcb 100644
--- a/modules/packages/content_store.go
+++ b/modules/packages/content_store.go
@@ -28,8 +28,7 @@ func NewContentStore() *ContentStore {
return contentStore
}
-// Get gets a package blob
-func (s *ContentStore) Get(key BlobHash256Key) (storage.Object, error) {
+func (s *ContentStore) OpenBlob(key BlobHash256Key) (storage.Object, error) {
return s.store.Open(KeyToRelativePath(key))
}
diff --git a/modules/packages/npm/creator.go b/modules/packages/npm/creator.go
index 8ba4dbfba71..11b5123c278 100644
--- a/modules/packages/npm/creator.go
+++ b/modules/packages/npm/creator.go
@@ -58,7 +58,7 @@ type PackageMetadata struct {
Time map[string]time.Time `json:"time,omitempty"`
Homepage string `json:"homepage,omitempty"`
Keywords []string `json:"keywords,omitempty"`
- Repository Repository `json:"repository,omitempty"`
+ Repository Repository `json:"repository"`
Author User `json:"author"`
ReadmeFilename string `json:"readmeFilename,omitempty"`
Users map[string]bool `json:"users,omitempty"`
@@ -75,7 +75,7 @@ type PackageMetadataVersion struct {
Author User `json:"author"`
Homepage string `json:"homepage,omitempty"`
License string `json:"license,omitempty"`
- Repository Repository `json:"repository,omitempty"`
+ Repository Repository `json:"repository"`
Keywords []string `json:"keywords,omitempty"`
Dependencies map[string]string `json:"dependencies,omitempty"`
BundleDependencies []string `json:"bundleDependencies,omitempty"`
diff --git a/modules/packages/npm/metadata.go b/modules/packages/npm/metadata.go
index d1d02633878..362d0470d55 100644
--- a/modules/packages/npm/metadata.go
+++ b/modules/packages/npm/metadata.go
@@ -23,5 +23,5 @@ type Metadata struct {
OptionalDependencies map[string]string `json:"optional_dependencies,omitempty"`
Bin map[string]string `json:"bin,omitempty"`
Readme string `json:"readme,omitempty"`
- Repository Repository `json:"repository,omitempty"`
+ Repository Repository `json:"repository"`
}
diff --git a/modules/packages/nuget/metadata.go b/modules/packages/nuget/metadata.go
index 1e98ddffde4..513b4dd2b91 100644
--- a/modules/packages/nuget/metadata.go
+++ b/modules/packages/nuget/metadata.go
@@ -57,14 +57,25 @@ type Package struct {
// Metadata represents the metadata of a Nuget package
type Metadata struct {
- Description string `json:"description,omitempty"`
- ReleaseNotes string `json:"release_notes,omitempty"`
- Readme string `json:"readme,omitempty"`
- Authors string `json:"authors,omitempty"`
- ProjectURL string `json:"project_url,omitempty"`
- RepositoryURL string `json:"repository_url,omitempty"`
- RequireLicenseAcceptance bool `json:"require_license_acceptance"`
- Dependencies map[string][]Dependency `json:"dependencies,omitempty"`
+ Authors string `json:"authors,omitempty"`
+ Copyright string `json:"copyright,omitempty"`
+ Description string `json:"description,omitempty"`
+ DevelopmentDependency bool `json:"development_dependency,omitempty"`
+ IconURL string `json:"icon_url,omitempty"`
+ Language string `json:"language,omitempty"`
+ LicenseURL string `json:"license_url,omitempty"`
+ MinClientVersion string `json:"min_client_version,omitempty"`
+ Owners string `json:"owners,omitempty"`
+ ProjectURL string `json:"project_url,omitempty"`
+ Readme string `json:"readme,omitempty"`
+ ReleaseNotes string `json:"release_notes,omitempty"`
+ RepositoryURL string `json:"repository_url,omitempty"`
+ RequireLicenseAcceptance bool `json:"require_license_acceptance"`
+ Summary string `json:"summary,omitempty"`
+ Tags string `json:"tags,omitempty"`
+ Title string `json:"title,omitempty"`
+
+ Dependencies map[string][]Dependency `json:"dependencies,omitempty"`
}
// Dependency represents a dependency of a Nuget package
@@ -74,24 +85,31 @@ type Dependency struct {
}
// https://learn.microsoft.com/en-us/nuget/reference/nuspec
+// https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Packaging/compiler/resources/nuspec.xsd
type nuspecPackage struct {
Metadata struct {
- ID string `xml:"id"`
- Version string `xml:"version"`
- Authors string `xml:"authors"`
- RequireLicenseAcceptance bool `xml:"requireLicenseAcceptance"`
+ // required fields
+ Authors string `xml:"authors"`
+ Description string `xml:"description"`
+ ID string `xml:"id"`
+ Version string `xml:"version"`
+
+ // optional fields
+ Copyright string `xml:"copyright"`
+ DevelopmentDependency bool `xml:"developmentDependency"`
+ IconURL string `xml:"iconUrl"`
+ Language string `xml:"language"`
+ LicenseURL string `xml:"licenseUrl"`
+ MinClientVersion string `xml:"minClientVersion,attr"`
+ Owners string `xml:"owners"`
ProjectURL string `xml:"projectUrl"`
- Description string `xml:"description"`
- ReleaseNotes string `xml:"releaseNotes"`
Readme string `xml:"readme"`
- PackageTypes struct {
- PackageType []struct {
- Name string `xml:"name,attr"`
- } `xml:"packageType"`
- } `xml:"packageTypes"`
- Repository struct {
- URL string `xml:"url,attr"`
- } `xml:"repository"`
+ ReleaseNotes string `xml:"releaseNotes"`
+ RequireLicenseAcceptance bool `xml:"requireLicenseAcceptance"`
+ Summary string `xml:"summary"`
+ Tags string `xml:"tags"`
+ Title string `xml:"title"`
+
Dependencies struct {
Dependency []struct {
ID string `xml:"id,attr"`
@@ -107,6 +125,14 @@ type nuspecPackage struct {
} `xml:"dependency"`
} `xml:"group"`
} `xml:"dependencies"`
+ PackageTypes struct {
+ PackageType []struct {
+ Name string `xml:"name,attr"`
+ } `xml:"packageType"`
+ } `xml:"packageTypes"`
+ Repository struct {
+ URL string `xml:"url,attr"`
+ } `xml:"repository"`
} `xml:"metadata"`
}
@@ -167,13 +193,24 @@ func ParseNuspecMetaData(archive *zip.Reader, r io.Reader) (*Package, error) {
}
m := &Metadata{
- Description: p.Metadata.Description,
- ReleaseNotes: p.Metadata.ReleaseNotes,
Authors: p.Metadata.Authors,
+ Copyright: p.Metadata.Copyright,
+ Description: p.Metadata.Description,
+ DevelopmentDependency: p.Metadata.DevelopmentDependency,
+ IconURL: p.Metadata.IconURL,
+ Language: p.Metadata.Language,
+ LicenseURL: p.Metadata.LicenseURL,
+ MinClientVersion: p.Metadata.MinClientVersion,
+ Owners: p.Metadata.Owners,
ProjectURL: p.Metadata.ProjectURL,
+ ReleaseNotes: p.Metadata.ReleaseNotes,
RepositoryURL: p.Metadata.Repository.URL,
RequireLicenseAcceptance: p.Metadata.RequireLicenseAcceptance,
- Dependencies: make(map[string][]Dependency),
+ Summary: p.Metadata.Summary,
+ Tags: p.Metadata.Tags,
+ Title: p.Metadata.Title,
+
+ Dependencies: make(map[string][]Dependency),
}
if p.Metadata.Readme != "" {
@@ -227,13 +264,13 @@ func ParseNuspecMetaData(archive *zip.Reader, r io.Reader) (*Package, error) {
func toNormalizedVersion(v *version.Version) string {
var buf bytes.Buffer
segments := v.Segments64()
- fmt.Fprintf(&buf, "%d.%d.%d", segments[0], segments[1], segments[2])
+ _, _ = fmt.Fprintf(&buf, "%d.%d.%d", segments[0], segments[1], segments[2])
if len(segments) > 3 && segments[3] > 0 {
- fmt.Fprintf(&buf, ".%d", segments[3])
+ _, _ = fmt.Fprintf(&buf, ".%d", segments[3])
}
pre := v.Prerelease()
if pre != "" {
- fmt.Fprint(&buf, "-", pre)
+ _, _ = fmt.Fprint(&buf, "-", pre)
}
return buf.String()
}
diff --git a/modules/packages/nuget/metadata_test.go b/modules/packages/nuget/metadata_test.go
index f466492f8a8..90c3e8dfeb6 100644
--- a/modules/packages/nuget/metadata_test.go
+++ b/modules/packages/nuget/metadata_test.go
@@ -12,44 +12,62 @@ import (
)
const (
- id = "System.Gitea"
- semver = "1.0.1"
- authors = "Gitea Authors"
- projectURL = "https://gitea.io"
- description = "Package Description"
- releaseNotes = "Package Release Notes"
- readme = "Readme"
- repositoryURL = "https://gitea.io/gitea/gitea"
- targetFramework = ".NETStandard2.1"
- dependencyID = "System.Text.Json"
- dependencyVersion = "5.0.0"
+ authors = "Gitea Authors"
+ copyright = "Package Copyright"
+ dependencyID = "System.Text.Json"
+ dependencyVersion = "5.0.0"
+ developmentDependency = true
+ description = "Package Description"
+ iconURL = "https://gitea.io/favicon.png"
+ id = "System.Gitea"
+ language = "Package Language"
+ licenseURL = "https://gitea.io/license"
+ minClientVersion = "1.0.0.0"
+ owners = "Package Owners"
+ projectURL = "https://gitea.io"
+ readme = "Readme"
+ releaseNotes = "Package Release Notes"
+ repositoryURL = "https://gitea.io/gitea/gitea"
+ requireLicenseAcceptance = true
+ tags = "tag_1 tag_2 tag_3"
+ targetFramework = ".NETStandard2.1"
+ title = "Package Title"
+ versionStr = "1.0.1"
)
const nuspecContent = `
-
- ` + id + `
- ` + semver + `
- ` + authors + `
- true
- ` + projectURL + `
- ` + description + `
- ` + releaseNotes + `
-
- README.md
-
-
-
-
-
-
+
+ ` + authors + `
+ ` + copyright + `
+ ` + description + `
+ true
+ ` + iconURL + `
+ ` + id + `
+ ` + language + `
+ ` + licenseURL + `
+ ` + owners + `
+ ` + projectURL + `
+ README.md
+ ` + releaseNotes + `
+
+ true
+ ` + tags + `
+ ` + title + `
+ ` + versionStr + `
+
+
+
+
+
+
`
const symbolsNuspecContent = `
` + id + `
- ` + semver + `
+ ` + versionStr + `
` + description + `
@@ -140,14 +158,26 @@ func TestParsePackageMetaData(t *testing.T) {
assert.NotNil(t, np)
assert.Equal(t, DependencyPackage, np.PackageType)
- assert.Equal(t, id, np.ID)
- assert.Equal(t, semver, np.Version)
assert.Equal(t, authors, np.Metadata.Authors)
- assert.Equal(t, projectURL, np.Metadata.ProjectURL)
assert.Equal(t, description, np.Metadata.Description)
- assert.Equal(t, releaseNotes, np.Metadata.ReleaseNotes)
+ assert.Equal(t, id, np.ID)
+ assert.Equal(t, versionStr, np.Version)
+
+ assert.Equal(t, copyright, np.Metadata.Copyright)
+ assert.Equal(t, developmentDependency, np.Metadata.DevelopmentDependency)
+ assert.Equal(t, iconURL, np.Metadata.IconURL)
+ assert.Equal(t, language, np.Metadata.Language)
+ assert.Equal(t, licenseURL, np.Metadata.LicenseURL)
+ assert.Equal(t, minClientVersion, np.Metadata.MinClientVersion)
+ assert.Equal(t, owners, np.Metadata.Owners)
+ assert.Equal(t, projectURL, np.Metadata.ProjectURL)
assert.Equal(t, readme, np.Metadata.Readme)
+ assert.Equal(t, releaseNotes, np.Metadata.ReleaseNotes)
assert.Equal(t, repositoryURL, np.Metadata.RepositoryURL)
+ assert.Equal(t, requireLicenseAcceptance, np.Metadata.RequireLicenseAcceptance)
+ assert.Equal(t, tags, np.Metadata.Tags)
+ assert.Equal(t, title, np.Metadata.Title)
+
assert.Len(t, np.Metadata.Dependencies, 1)
assert.Contains(t, np.Metadata.Dependencies, targetFramework)
deps := np.Metadata.Dependencies[targetFramework]
@@ -180,7 +210,7 @@ func TestParsePackageMetaData(t *testing.T) {
assert.Equal(t, SymbolsPackage, np.PackageType)
assert.Equal(t, id, np.ID)
- assert.Equal(t, semver, np.Version)
+ assert.Equal(t, versionStr, np.Version)
assert.Equal(t, description, np.Metadata.Description)
assert.Empty(t, np.Metadata.Dependencies)
})
diff --git a/modules/packages/nuget/symbol_extractor.go b/modules/packages/nuget/symbol_extractor.go
index 81bf0371a0b..9c952e1f109 100644
--- a/modules/packages/nuget/symbol_extractor.go
+++ b/modules/packages/nuget/symbol_extractor.go
@@ -34,7 +34,7 @@ type PortablePdbList []*PortablePdb
func (l PortablePdbList) Close() {
for _, pdb := range l {
- pdb.Content.Close()
+ _ = pdb.Content.Close()
}
}
@@ -65,7 +65,7 @@ func ExtractPortablePdb(r io.ReaderAt, size int64) (PortablePdbList, error) {
buf, err := packages.CreateHashedBufferFromReader(f)
- f.Close()
+ _ = f.Close()
if err != nil {
return err
@@ -73,12 +73,12 @@ func ExtractPortablePdb(r io.ReaderAt, size int64) (PortablePdbList, error) {
id, err := ParseDebugHeaderID(buf)
if err != nil {
- buf.Close()
+ _ = buf.Close()
return fmt.Errorf("Invalid PDB file: %w", err)
}
if _, err := buf.Seek(0, io.SeekStart); err != nil {
- buf.Close()
+ _ = buf.Close()
return err
}
diff --git a/modules/packages/nuget/symbol_extractor_test.go b/modules/packages/nuget/symbol_extractor_test.go
index 711ad6d096a..e841e377d98 100644
--- a/modules/packages/nuget/symbol_extractor_test.go
+++ b/modules/packages/nuget/symbol_extractor_test.go
@@ -24,14 +24,14 @@ func TestExtractPortablePdb(t *testing.T) {
var buf bytes.Buffer
archive := zip.NewWriter(&buf)
w, _ := archive.Create(name)
- w.Write(content)
- archive.Close()
+ _, _ = w.Write(content)
+ _ = archive.Close()
return buf.Bytes()
}
t.Run("MissingPdbFiles", func(t *testing.T) {
var buf bytes.Buffer
- zip.NewWriter(&buf).Close()
+ _ = zip.NewWriter(&buf).Close()
pdbs, err := ExtractPortablePdb(bytes.NewReader(buf.Bytes()), int64(buf.Len()))
assert.ErrorIs(t, err, ErrMissingPdbFiles)
diff --git a/modules/packages/pub/metadata.go b/modules/packages/pub/metadata.go
index afb464e4620..9b00472eb27 100644
--- a/modules/packages/pub/metadata.go
+++ b/modules/packages/pub/metadata.go
@@ -88,7 +88,7 @@ func ParsePackage(r io.Reader) (*Package, error) {
if err != nil {
return nil, err
}
- } else if strings.ToLower(hd.Name) == "readme.md" {
+ } else if strings.EqualFold(hd.Name, "readme.md") {
data, err := io.ReadAll(tr)
if err != nil {
return nil, err
diff --git a/modules/packages/rubygems/marshal.go b/modules/packages/rubygems/marshal.go
index 4e6a5fc5f8b..1505221acc7 100644
--- a/modules/packages/rubygems/marshal.go
+++ b/modules/packages/rubygems/marshal.go
@@ -250,7 +250,7 @@ func (e *MarshalEncoder) marshalArray(arr reflect.Value) error {
return err
}
- for i := 0; i < length; i++ {
+ for i := range length {
if err := e.marshal(arr.Index(i).Interface()); err != nil {
return err
}
diff --git a/modules/packages/swift/metadata.go b/modules/packages/swift/metadata.go
index 24c4262ab72..85beb57607a 100644
--- a/modules/packages/swift/metadata.go
+++ b/modules/packages/swift/metadata.go
@@ -47,7 +47,7 @@ type Metadata struct {
Keywords []string `json:"keywords,omitempty"`
RepositoryURL string `json:"repository_url,omitempty"`
License string `json:"license,omitempty"`
- Author Person `json:"author,omitempty"`
+ Author Person `json:"author"`
Manifests map[string]*Manifest `json:"manifests,omitempty"`
}
diff --git a/modules/public/public.go b/modules/public/public.go
index 7f8ce290562..a7eace1538b 100644
--- a/modules/public/public.go
+++ b/modules/public/public.go
@@ -44,7 +44,7 @@ func FileHandlerFunc() http.HandlerFunc {
func parseAcceptEncoding(val string) container.Set[string] {
parts := strings.Split(val, ";")
types := make(container.Set[string])
- for _, v := range strings.Split(parts[0], ",") {
+ for v := range strings.SplitSeq(parts[0], ",") {
types.Add(strings.TrimSpace(v))
}
return types
@@ -89,19 +89,16 @@ func handleRequest(w http.ResponseWriter, req *http.Request, fs http.FileSystem,
servePublicAsset(w, req, fi, fi.ModTime(), f)
}
-type GzipBytesProvider interface {
- GzipBytes() []byte
-}
-
// servePublicAsset serve http content
func servePublicAsset(w http.ResponseWriter, req *http.Request, fi os.FileInfo, modtime time.Time, content io.ReadSeeker) {
setWellKnownContentType(w, fi.Name())
httpcache.SetCacheControlInHeader(w.Header(), httpcache.CacheControlForPublicStatic())
encodings := parseAcceptEncoding(req.Header.Get("Accept-Encoding"))
- if encodings.Contains("gzip") {
- // try to provide gzip content directly from bindata (provided by vfsgen۰CompressedFileInfo)
- if compressed, ok := fi.(GzipBytesProvider); ok {
- rdGzip := bytes.NewReader(compressed.GzipBytes())
+ fiEmbedded, _ := fi.(assetfs.EmbeddedFileInfo)
+ if encodings.Contains("gzip") && fiEmbedded != nil {
+ // try to provide gzip content directly from bindata
+ if gzipBytes, ok := fiEmbedded.GetGzipContent(); ok {
+ rdGzip := bytes.NewReader(gzipBytes)
// all gzipped static files (from bindata) are managed by Gitea, so we can make sure every file has the correct ext name
// then we can get the correct Content-Type, we do not need to do http.DetectContentType on the decompressed data
if w.Header().Get("Content-Type") == "" {
@@ -113,5 +110,4 @@ func servePublicAsset(w http.ResponseWriter, req *http.Request, fi os.FileInfo,
}
}
http.ServeContent(w, req, fi.Name(), modtime, content)
- return
}
diff --git a/modules/public/public_bindata.go b/modules/public/public_bindata.go
index 4878f88ad1d..2dcf3e72e4e 100644
--- a/modules/public/public_bindata.go
+++ b/modules/public/public_bindata.go
@@ -5,4 +5,19 @@
package public
-//go:generate go run ../../build/generate-bindata.go ../../public public bindata.go true
+//go:generate go run ../../build/generate-bindata.go ../../public bindata.dat
+
+import (
+ "sync"
+
+ _ "embed"
+
+ "code.gitea.io/gitea/modules/assetfs"
+)
+
+//go:embed bindata.dat
+var bindata []byte
+
+var BuiltinAssets = sync.OnceValue(func() *assetfs.Layer {
+ return assetfs.Bindata("builtin(bindata)", assetfs.NewEmbeddedFS(bindata))
+})
diff --git a/modules/public/serve_dynamic.go b/modules/public/public_dynamic.go
similarity index 100%
rename from modules/public/serve_dynamic.go
rename to modules/public/public_dynamic.go
diff --git a/modules/public/serve_static.go b/modules/public/serve_static.go
deleted file mode 100644
index e79085021ea..00000000000
--- a/modules/public/serve_static.go
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright 2016 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-//go:build bindata
-
-package public
-
-import (
- "time"
-
- "code.gitea.io/gitea/modules/assetfs"
- "code.gitea.io/gitea/modules/timeutil"
-)
-
-var _ GzipBytesProvider = (*vfsgen۰CompressedFileInfo)(nil)
-
-// GlobalModTime provide a global mod time for embedded asset files
-func GlobalModTime(filename string) time.Time {
- return timeutil.GetExecutableModTime()
-}
-
-func BuiltinAssets() *assetfs.Layer {
- return assetfs.Bindata("builtin(bindata)", Assets)
-}
diff --git a/modules/queue/base_levelqueue_common.go b/modules/queue/base_levelqueue_common.go
index 78d3b85a8a2..d37093b84dc 100644
--- a/modules/queue/base_levelqueue_common.go
+++ b/modules/queue/base_levelqueue_common.go
@@ -83,7 +83,7 @@ func prepareLevelDB(cfg *BaseConfig) (conn string, db *leveldb.DB, err error) {
}
conn = cfg.ConnStr
}
- for i := 0; i < 10; i++ {
+ for range 10 {
if db, err = nosql.GetManager().GetLevelDB(conn); err == nil {
break
}
diff --git a/modules/queue/base_redis.go b/modules/queue/base_redis.go
index a1e234943d2..bea0fd7a985 100644
--- a/modules/queue/base_redis.go
+++ b/modules/queue/base_redis.go
@@ -29,7 +29,7 @@ func newBaseRedisGeneric(cfg *BaseConfig, unique bool) (baseQueue, error) {
client := nosql.GetManager().GetRedisClient(cfg.ConnStr)
var err error
- for i := 0; i < 10; i++ {
+ for range 10 {
err = client.Ping(graceful.GetManager().ShutdownContext()).Err()
if err == nil {
break
diff --git a/modules/queue/base_test.go b/modules/queue/base_test.go
index 1a96ac1e1d3..8e7c18d7407 100644
--- a/modules/queue/base_test.go
+++ b/modules/queue/base_test.go
@@ -87,7 +87,7 @@ func testQueueBasic(t *testing.T, newFn func(cfg *BaseConfig) (baseQueue, error)
// test blocking push if queue is full
for i := 0; i < cfg.Length; i++ {
- err = q.PushItem(ctx, []byte(fmt.Sprintf("item-%d", i)))
+ err = q.PushItem(ctx, fmt.Appendf(nil, "item-%d", i))
assert.NoError(t, err)
}
ctxTimed, cancel = context.WithTimeout(ctx, 10*time.Millisecond)
diff --git a/modules/queue/manager.go b/modules/queue/manager.go
index 079e2bee7a7..ae6c51872dd 100644
--- a/modules/queue/manager.go
+++ b/modules/queue/manager.go
@@ -6,6 +6,7 @@ package queue
import (
"context"
"errors"
+ "maps"
"sync"
"time"
@@ -70,9 +71,7 @@ func (m *Manager) ManagedQueues() map[int64]ManagedWorkerPoolQueue {
defer m.mu.Unlock()
queues := make(map[int64]ManagedWorkerPoolQueue, len(m.Queues))
- for k, v := range m.Queues {
- queues[k] = v
- }
+ maps.Copy(queues, m.Queues)
return queues
}
diff --git a/modules/queue/workerqueue_test.go b/modules/queue/workerqueue_test.go
index 487c2f1a92e..a6c369d5f9b 100644
--- a/modules/queue/workerqueue_test.go
+++ b/modules/queue/workerqueue_test.go
@@ -77,17 +77,17 @@ func TestWorkerPoolQueueUnhandled(t *testing.T) {
runCount := 2 // we can run these tests even hundreds times to see its stability
t.Run("1/1", func(t *testing.T) {
- for i := 0; i < runCount; i++ {
+ for range runCount {
test(t, setting.QueueSettings{BatchLength: 1, MaxWorkers: 1})
}
})
t.Run("3/1", func(t *testing.T) {
- for i := 0; i < runCount; i++ {
+ for range runCount {
test(t, setting.QueueSettings{BatchLength: 3, MaxWorkers: 1})
}
})
t.Run("4/5", func(t *testing.T) {
- for i := 0; i < runCount; i++ {
+ for range runCount {
test(t, setting.QueueSettings{BatchLength: 4, MaxWorkers: 5})
}
})
@@ -96,17 +96,17 @@ func TestWorkerPoolQueueUnhandled(t *testing.T) {
func TestWorkerPoolQueuePersistence(t *testing.T) {
runCount := 2 // we can run these tests even hundreds times to see its stability
t.Run("1/1", func(t *testing.T) {
- for i := 0; i < runCount; i++ {
+ for range runCount {
testWorkerPoolQueuePersistence(t, setting.QueueSettings{BatchLength: 1, MaxWorkers: 1, Length: 100})
}
})
t.Run("3/1", func(t *testing.T) {
- for i := 0; i < runCount; i++ {
+ for range runCount {
testWorkerPoolQueuePersistence(t, setting.QueueSettings{BatchLength: 3, MaxWorkers: 1, Length: 100})
}
})
t.Run("4/5", func(t *testing.T) {
- for i := 0; i < runCount; i++ {
+ for range runCount {
testWorkerPoolQueuePersistence(t, setting.QueueSettings{BatchLength: 4, MaxWorkers: 5, Length: 100})
}
})
@@ -141,7 +141,7 @@ func testWorkerPoolQueuePersistence(t *testing.T, queueSetting setting.QueueSett
q, _ := newWorkerPoolQueueForTest("pr_patch_checker_test", queueSetting, testHandler, true)
stop := runWorkerPoolQueue(q)
- for i := 0; i < testCount; i++ {
+ for i := range testCount {
_ = q.Push("task-" + strconv.Itoa(i))
}
close(startWhenAllReady)
@@ -186,7 +186,7 @@ func TestWorkerPoolQueueActiveWorkers(t *testing.T) {
q, _ := newWorkerPoolQueueForTest("test-workpoolqueue", setting.QueueSettings{Type: "channel", BatchLength: 1, MaxWorkers: 1, Length: 100}, handler, false)
stop := runWorkerPoolQueue(q)
- for i := 0; i < 5; i++ {
+ for i := range 5 {
assert.NoError(t, q.Push(i))
}
@@ -202,7 +202,7 @@ func TestWorkerPoolQueueActiveWorkers(t *testing.T) {
q, _ = newWorkerPoolQueueForTest("test-workpoolqueue", setting.QueueSettings{Type: "channel", BatchLength: 1, MaxWorkers: 3, Length: 100}, handler, false)
stop = runWorkerPoolQueue(q)
- for i := 0; i < 15; i++ {
+ for i := range 15 {
assert.NoError(t, q.Push(i))
}
@@ -274,7 +274,7 @@ func TestWorkerPoolQueueWorkerIdleReset(t *testing.T) {
}
q, _ = newWorkerPoolQueueForTest("test-workpoolqueue", setting.QueueSettings{Type: "channel", BatchLength: 1, MaxWorkers: 2, Length: 100}, handler, false)
stop := runWorkerPoolQueue(q)
- for i := 0; i < 100; i++ {
+ for i := range 100 {
assert.NoError(t, q.Push(i))
}
time.Sleep(500 * time.Millisecond)
diff --git a/modules/repository/branch.go b/modules/repository/branch.go
index 2bf9930f19f..30aa0a6e85e 100644
--- a/modules/repository/branch.go
+++ b/modules/repository/branch.go
@@ -41,11 +41,12 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository,
if err != nil {
return 0, fmt.Errorf("GetObjectFormat: %w", err)
}
- _, err = db.GetEngine(ctx).ID(repo.ID).Update(&repo_model.Repository{ObjectFormatName: objFmt.Name()})
- if err != nil {
- return 0, fmt.Errorf("UpdateRepository: %w", err)
+ if objFmt.Name() != repo.ObjectFormatName {
+ repo.ObjectFormatName = objFmt.Name()
+ if err = repo_model.UpdateRepositoryColsWithAutoTime(ctx, repo, "object_format_name"); err != nil {
+ return 0, fmt.Errorf("UpdateRepositoryColsWithAutoTime: %w", err)
+ }
}
- repo.ObjectFormatName = objFmt.Name() // keep consistent with db
allBranches := container.Set[string]{}
{
diff --git a/modules/repository/init.go b/modules/repository/init.go
index 91d48897827..12e9606c740 100644
--- a/modules/repository/init.go
+++ b/modules/repository/init.go
@@ -125,7 +125,7 @@ func InitializeLabels(ctx context.Context, id int64, labelTemplate string, isOrg
}
labels := make([]*issues_model.Label, len(list))
- for i := 0; i < len(list); i++ {
+ for i := range list {
labels[i] = &issues_model.Label{
Name: list[i].Name,
Exclusive: list[i].Exclusive,
diff --git a/modules/repository/repo.go b/modules/repository/repo.go
index bc147a4dd55..ad4a53b858c 100644
--- a/modules/repository/repo.go
+++ b/modules/repository/repo.go
@@ -9,13 +9,10 @@ import (
"fmt"
"io"
"strings"
- "time"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/lfs"
@@ -59,118 +56,6 @@ func SyncRepoTags(ctx context.Context, repoID int64) error {
return SyncReleasesWithTags(ctx, repo, gitRepo)
}
-// SyncReleasesWithTags synchronizes release table with repository tags
-func SyncReleasesWithTags(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository) error {
- log.Debug("SyncReleasesWithTags: in Repo[%d:%s/%s]", repo.ID, repo.OwnerName, repo.Name)
-
- // optimized procedure for pull-mirrors which saves a lot of time (in
- // particular for repos with many tags).
- if repo.IsMirror {
- return pullMirrorReleaseSync(ctx, repo, gitRepo)
- }
-
- existingRelTags := make(container.Set[string])
- opts := repo_model.FindReleasesOptions{
- IncludeDrafts: true,
- IncludeTags: true,
- ListOptions: db.ListOptions{PageSize: 50},
- RepoID: repo.ID,
- }
- for page := 1; ; page++ {
- opts.Page = page
- rels, err := db.Find[repo_model.Release](gitRepo.Ctx, opts)
- if err != nil {
- return fmt.Errorf("unable to GetReleasesByRepoID in Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err)
- }
- if len(rels) == 0 {
- break
- }
- for _, rel := range rels {
- if rel.IsDraft {
- continue
- }
- commitID, err := gitRepo.GetTagCommitID(rel.TagName)
- if err != nil && !git.IsErrNotExist(err) {
- return fmt.Errorf("unable to GetTagCommitID for %q in Repo[%d:%s/%s]: %w", rel.TagName, repo.ID, repo.OwnerName, repo.Name, err)
- }
- if git.IsErrNotExist(err) || commitID != rel.Sha1 {
- if err := repo_model.PushUpdateDeleteTag(ctx, repo, rel.TagName); err != nil {
- return fmt.Errorf("unable to PushUpdateDeleteTag: %q in Repo[%d:%s/%s]: %w", rel.TagName, repo.ID, repo.OwnerName, repo.Name, err)
- }
- } else {
- existingRelTags.Add(strings.ToLower(rel.TagName))
- }
- }
- }
-
- _, err := gitRepo.WalkReferences(git.ObjectTag, 0, 0, func(sha1, refname string) error {
- tagName := strings.TrimPrefix(refname, git.TagPrefix)
- if existingRelTags.Contains(strings.ToLower(tagName)) {
- return nil
- }
-
- if err := PushUpdateAddTag(ctx, repo, gitRepo, tagName, sha1, refname); err != nil {
- // sometimes, some tags will be sync failed. i.e. https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tag/?h=v2.6.11
- // this is a tree object, not a tag object which created before git
- log.Error("unable to PushUpdateAddTag: %q to Repo[%d:%s/%s]: %v", tagName, repo.ID, repo.OwnerName, repo.Name, err)
- }
-
- return nil
- })
- return err
-}
-
-// PushUpdateAddTag must be called for any push actions to add tag
-func PushUpdateAddTag(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, tagName, sha1, refname string) error {
- tag, err := gitRepo.GetTagWithID(sha1, tagName)
- if err != nil {
- return fmt.Errorf("unable to GetTag: %w", err)
- }
- commit, err := gitRepo.GetTagCommit(tag.Name)
- if err != nil {
- return fmt.Errorf("unable to get tag Commit: %w", err)
- }
-
- sig := tag.Tagger
- if sig == nil {
- sig = commit.Author
- }
- if sig == nil {
- sig = commit.Committer
- }
-
- var author *user_model.User
- createdAt := time.Unix(1, 0)
-
- if sig != nil {
- author, err = user_model.GetUserByEmail(ctx, sig.Email)
- if err != nil && !user_model.IsErrUserNotExist(err) {
- return fmt.Errorf("unable to GetUserByEmail for %q: %w", sig.Email, err)
- }
- createdAt = sig.When
- }
-
- commitsCount, err := commit.CommitsCount()
- if err != nil {
- return fmt.Errorf("unable to get CommitsCount: %w", err)
- }
-
- rel := repo_model.Release{
- RepoID: repo.ID,
- TagName: tagName,
- LowerTagName: strings.ToLower(tagName),
- Sha1: commit.ID.String(),
- NumCommits: commitsCount,
- CreatedUnix: timeutil.TimeStamp(createdAt.Unix()),
- IsTag: true,
- }
- if author != nil {
- rel.PublisherID = author.ID
- }
-
- return repo_model.SaveOrUpdateTag(ctx, repo, &rel)
-}
-
// StoreMissingLfsObjectsInRepository downloads missing LFS objects
func StoreMissingLfsObjectsInRepository(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, lfsClient lfs.Client) error {
contentStore := lfs.NewContentStore()
@@ -286,18 +171,19 @@ func (shortRelease) TableName() string {
return "release"
}
-// pullMirrorReleaseSync is a pull-mirror specific tag<->release table
+// SyncReleasesWithTags is a tag<->release table
// synchronization which overwrites all Releases from the repository tags. This
// can be relied on since a pull-mirror is always identical to its
-// upstream. Hence, after each sync we want the pull-mirror release set to be
+// upstream. Hence, after each sync we want the release set to be
// identical to the upstream tag set. This is much more efficient for
// repositories like https://github.com/vim/vim (with over 13000 tags).
-func pullMirrorReleaseSync(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository) error {
- log.Trace("pullMirrorReleaseSync: rebuilding releases for pull-mirror Repo[%d:%s/%s]", repo.ID, repo.OwnerName, repo.Name)
- tags, numTags, err := gitRepo.GetTagInfos(0, 0)
+func SyncReleasesWithTags(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository) error {
+ log.Debug("SyncReleasesWithTags: in Repo[%d:%s/%s]", repo.ID, repo.OwnerName, repo.Name)
+ tags, _, err := gitRepo.GetTagInfos(0, 0)
if err != nil {
return fmt.Errorf("unable to GetTagInfos in pull-mirror Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err)
}
+ var added, deleted, updated int
err = db.WithTx(ctx, func(ctx context.Context) error {
dbReleases, err := db.Find[shortRelease](ctx, repo_model.FindReleasesOptions{
RepoID: repo.ID,
@@ -318,9 +204,7 @@ func pullMirrorReleaseSync(ctx context.Context, repo *repo_model.Repository, git
TagName: tag.Name,
LowerTagName: strings.ToLower(tag.Name),
Sha1: tag.Object.String(),
- // NOTE: ignored, since NumCommits are unused
- // for pull-mirrors (only relevant when
- // displaying releases, IsTag: false)
+ // NOTE: ignored, The NumCommits value is calculated and cached on demand when the UI requires it.
NumCommits: -1,
CreatedUnix: timeutil.TimeStamp(tag.Tagger.When.Unix()),
IsTag: true,
@@ -349,13 +233,14 @@ func pullMirrorReleaseSync(ctx context.Context, repo *repo_model.Repository, git
return fmt.Errorf("unable to update tag %s for pull-mirror Repo[%d:%s/%s]: %w", tag.Name, repo.ID, repo.OwnerName, repo.Name, err)
}
}
+ added, deleted, updated = len(deletes), len(updates), len(inserts)
return nil
})
if err != nil {
return fmt.Errorf("unable to rebuild release table for pull-mirror Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err)
}
- log.Trace("pullMirrorReleaseSync: done rebuilding %d releases", numTags)
+ log.Trace("SyncReleasesWithTags: %d tags added, %d tags deleted, %d tags updated", added, deleted, updated)
return nil
}
diff --git a/modules/reqctx/datastore.go b/modules/reqctx/datastore.go
index d025dad7f34..1d4bee613f8 100644
--- a/modules/reqctx/datastore.go
+++ b/modules/reqctx/datastore.go
@@ -6,6 +6,7 @@ package reqctx
import (
"context"
"io"
+ "maps"
"sync"
"code.gitea.io/gitea/modules/process"
@@ -22,9 +23,7 @@ func (ds ContextData) GetData() ContextData {
}
func (ds ContextData) MergeFrom(other ContextData) ContextData {
- for k, v := range other {
- ds[k] = v
- }
+ maps.Copy(ds, other)
return ds
}
diff --git a/modules/setting/actions.go b/modules/setting/actions.go
index 913872eaf23..8bace1f7504 100644
--- a/modules/setting/actions.go
+++ b/modules/setting/actions.go
@@ -62,11 +62,11 @@ func (c logCompression) IsValid() bool {
}
func (c logCompression) IsNone() bool {
- return strings.ToLower(string(c)) == "none"
+ return string(c) == "none"
}
func (c logCompression) IsZstd() bool {
- return c == "" || strings.ToLower(string(c)) == "zstd"
+ return c == "" || string(c) == "zstd"
}
func loadActionsFrom(rootCfg ConfigProvider) error {
diff --git a/modules/setting/config_env.go b/modules/setting/config_env.go
index 5d94a9641f7..409588dc441 100644
--- a/modules/setting/config_env.go
+++ b/modules/setting/config_env.go
@@ -97,7 +97,7 @@ func decodeEnvSectionKey(encoded string) (ok bool, section, key string) {
// decodeEnvironmentKey decode the environment key to section and key
// The environment key is in the form of GITEA__SECTION__KEY or GITEA__SECTION__KEY__FILE
-func decodeEnvironmentKey(prefixGitea, suffixFile, envKey string) (ok bool, section, key string, useFileValue bool) { //nolint:unparam
+func decodeEnvironmentKey(prefixGitea, suffixFile, envKey string) (ok bool, section, key string, useFileValue bool) {
if !strings.HasPrefix(envKey, prefixGitea) {
return false, "", "", false
}
diff --git a/modules/setting/config_env_test.go b/modules/setting/config_env_test.go
index 217ea538603..7d270ac21ad 100644
--- a/modules/setting/config_env_test.go
+++ b/modules/setting/config_env_test.go
@@ -73,6 +73,9 @@ func TestDecodeEnvironmentKey(t *testing.T) {
assert.Equal(t, "sec", section)
assert.Equal(t, "KEY", key)
assert.True(t, file)
+
+ ok, _, _, _ = decodeEnvironmentKey("PREFIX__", "", "PREFIX__SEC__KEY")
+ assert.True(t, ok)
}
func TestEnvironmentToConfig(t *testing.T) {
diff --git a/modules/setting/config_provider.go b/modules/setting/config_provider.go
index a0c53a10325..09eaaefdaff 100644
--- a/modules/setting/config_provider.go
+++ b/modules/setting/config_provider.go
@@ -15,7 +15,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
- "gopkg.in/ini.v1" //nolint:depguard
+ "gopkg.in/ini.v1" //nolint:depguard // wrapper for this package
)
type ConfigKey interface {
diff --git a/modules/setting/git_test.go b/modules/setting/git_test.go
index 818bcf9df62..0d7f634abfa 100644
--- a/modules/setting/git_test.go
+++ b/modules/setting/git_test.go
@@ -6,6 +6,8 @@ package setting
import (
"testing"
+ "code.gitea.io/gitea/modules/test"
+
"github.com/stretchr/testify/assert"
)
@@ -36,12 +38,8 @@ diff.algorithm = other
}
func TestGitReflog(t *testing.T) {
- oldGit := Git
- oldGitConfig := GitConfig
- defer func() {
- Git = oldGit
- GitConfig = oldGitConfig
- }()
+ defer test.MockVariableValue(&Git)
+ defer test.MockVariableValue(&GitConfig)
// default reflog config without legacy options
cfg, err := NewConfigProviderFromData(``)
diff --git a/modules/setting/indexer.go b/modules/setting/indexer.go
index e34baae012b..ace7eec70eb 100644
--- a/modules/setting/indexer.go
+++ b/modules/setting/indexer.go
@@ -96,7 +96,7 @@ func loadIndexerFrom(rootCfg ConfigProvider) {
// IndexerGlobFromString parses a comma separated list of patterns and returns a glob.Glob slice suited for repo indexing
func IndexerGlobFromString(globstr string) []*GlobMatcher {
extarr := make([]*GlobMatcher, 0, 10)
- for _, expr := range strings.Split(strings.ToLower(globstr), ",") {
+ for expr := range strings.SplitSeq(strings.ToLower(globstr), ",") {
expr = strings.TrimSpace(expr)
if expr != "" {
if g, err := GlobMatcherCompile(expr, '.', '/'); err != nil {
diff --git a/modules/setting/log.go b/modules/setting/log.go
index 614d9ee75a8..59866c76055 100644
--- a/modules/setting/log.go
+++ b/modules/setting/log.go
@@ -227,8 +227,8 @@ func initLoggerByName(manager *log.LoggerManager, rootCfg ConfigProvider, logger
}
var eventWriters []log.EventWriter
- modes := strings.Split(modeVal, ",")
- for _, modeName := range modes {
+ modes := strings.SplitSeq(modeVal, ",")
+ for modeName := range modes {
modeName = strings.TrimSpace(modeName)
if modeName == "" {
continue
diff --git a/modules/setting/markup.go b/modules/setting/markup.go
index 365af05fcfa..057b0650c30 100644
--- a/modules/setting/markup.go
+++ b/modules/setting/markup.go
@@ -149,8 +149,8 @@ func loadMarkupFrom(rootCfg ConfigProvider) {
func newMarkupSanitizer(name string, sec ConfigSection) {
rule, ok := createMarkupSanitizerRule(name, sec)
if ok {
- if strings.HasPrefix(name, "sanitizer.") {
- names := strings.SplitN(strings.TrimPrefix(name, "sanitizer."), ".", 2)
+ if after, found := strings.CutPrefix(name, "sanitizer."); found {
+ names := strings.SplitN(after, ".", 2)
name = names[0]
}
for _, renderer := range ExternalMarkupRenderers {
diff --git a/modules/setting/mirror.go b/modules/setting/mirror.go
index 3aa530a1f48..300711789db 100644
--- a/modules/setting/mirror.go
+++ b/modules/setting/mirror.go
@@ -48,11 +48,7 @@ func loadMirrorFrom(rootCfg ConfigProvider) {
Mirror.MinInterval = 1 * time.Minute
}
if Mirror.DefaultInterval < Mirror.MinInterval {
- if time.Hour*8 < Mirror.MinInterval {
- Mirror.DefaultInterval = Mirror.MinInterval
- } else {
- Mirror.DefaultInterval = time.Hour * 8
- }
+ Mirror.DefaultInterval = max(time.Hour*8, Mirror.MinInterval)
log.Warn("Mirror.DefaultInterval is less than Mirror.MinInterval, set to %s", Mirror.DefaultInterval.String())
}
}
diff --git a/modules/setting/repository.go b/modules/setting/repository.go
index c6bdc65b321..318cf411085 100644
--- a/modules/setting/repository.go
+++ b/modules/setting/repository.go
@@ -100,11 +100,13 @@ var (
SigningKey string
SigningName string
SigningEmail string
+ SigningFormat string
InitialCommit []string
CRUDActions []string `ini:"CRUD_ACTIONS"`
Merges []string
Wiki []string
DefaultTrustModel string
+ TrustedSSHKeys []string `ini:"TRUSTED_SSH_KEYS"`
} `ini:"repository.signing"`
}{
DetectedCharsetsOrder: []string{
@@ -242,20 +244,24 @@ var (
SigningKey string
SigningName string
SigningEmail string
+ SigningFormat string
InitialCommit []string
CRUDActions []string `ini:"CRUD_ACTIONS"`
Merges []string
Wiki []string
DefaultTrustModel string
+ TrustedSSHKeys []string `ini:"TRUSTED_SSH_KEYS"`
}{
SigningKey: "default",
SigningName: "",
SigningEmail: "",
+ SigningFormat: "openpgp", // git.SigningKeyFormatOpenPGP
InitialCommit: []string{"always"},
CRUDActions: []string{"pubkey", "twofa", "parentsigned"},
Merges: []string{"pubkey", "twofa", "basesigned", "commitssigned"},
Wiki: []string{"never"},
DefaultTrustModel: "collaborator",
+ TrustedSSHKeys: []string{},
},
}
RepoRootPath string
diff --git a/modules/setting/security.go b/modules/setting/security.go
index 3ae4c005c7b..153b6bc944f 100644
--- a/modules/setting/security.go
+++ b/modules/setting/security.go
@@ -111,7 +111,7 @@ func loadSecurityFrom(rootCfg ConfigProvider) {
if SecretKey == "" {
// FIXME: https://github.com/go-gitea/gitea/issues/16832
// Until it supports rotating an existing secret key, we shouldn't move users off of the widely used default value
- SecretKey = "!#@FDEWREWR&*(" //nolint:gosec
+ SecretKey = "!#@FDEWREWR&*("
}
CookieRememberName = sec.Key("COOKIE_REMEMBER_NAME").MustString("gitea_incredible")
diff --git a/modules/setting/ssh.go b/modules/setting/ssh.go
index da8cdf58d25..900fc6ade26 100644
--- a/modules/setting/ssh.go
+++ b/modules/setting/ssh.go
@@ -51,9 +51,6 @@ var SSH = struct {
StartBuiltinServer: false,
Domain: "",
Port: 22,
- ServerCiphers: []string{"chacha20-poly1305@openssh.com", "aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-gcm@openssh.com", "aes256-gcm@openssh.com"},
- ServerKeyExchanges: []string{"curve25519-sha256", "ecdh-sha2-nistp256", "ecdh-sha2-nistp384", "ecdh-sha2-nistp521", "diffie-hellman-group14-sha256", "diffie-hellman-group14-sha1"},
- ServerMACs: []string{"hmac-sha2-256-etm@openssh.com", "hmac-sha2-256", "hmac-sha1"},
MinimumKeySizeCheck: true,
MinimumKeySizes: map[string]int{"ed25519": 256, "ed25519-sk": 256, "ecdsa": 256, "ecdsa-sk": 256, "rsa": 3071},
ServerHostKeys: []string{"ssh/gitea.rsa", "ssh/gogs.rsa"},
@@ -107,21 +104,20 @@ func loadSSHFrom(rootCfg ConfigProvider) {
homeDir = strings.ReplaceAll(homeDir, "\\", "/")
SSH.RootPath = filepath.Join(homeDir, ".ssh")
- serverCiphers := sec.Key("SSH_SERVER_CIPHERS").Strings(",")
- if len(serverCiphers) > 0 {
- SSH.ServerCiphers = serverCiphers
- }
- serverKeyExchanges := sec.Key("SSH_SERVER_KEY_EXCHANGES").Strings(",")
- if len(serverKeyExchanges) > 0 {
- SSH.ServerKeyExchanges = serverKeyExchanges
- }
- serverMACs := sec.Key("SSH_SERVER_MACS").Strings(",")
- if len(serverMACs) > 0 {
- SSH.ServerMACs = serverMACs
- }
+
if err = sec.MapTo(&SSH); err != nil {
log.Fatal("Failed to map SSH settings: %v", err)
}
+
+ serverCiphers := sec.Key("SSH_SERVER_CIPHERS").Strings(",")
+ SSH.ServerCiphers = util.Iif(len(serverCiphers) > 0, serverCiphers, nil)
+
+ serverKeyExchanges := sec.Key("SSH_SERVER_KEY_EXCHANGES").Strings(",")
+ SSH.ServerKeyExchanges = util.Iif(len(serverKeyExchanges) > 0, serverKeyExchanges, nil)
+
+ serverMACs := sec.Key("SSH_SERVER_MACS").Strings(",")
+ SSH.ServerMACs = util.Iif(len(serverMACs) > 0, serverMACs, nil)
+
for i, key := range SSH.ServerHostKeys {
if !filepath.IsAbs(key) {
SSH.ServerHostKeys[i] = filepath.Join(AppDataPath, key)
diff --git a/modules/setting/storage.go b/modules/setting/storage.go
index e1d9b1fa7aa..ee246158d94 100644
--- a/modules/setting/storage.go
+++ b/modules/setting/storage.go
@@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"path/filepath"
+ "slices"
"strings"
)
@@ -30,12 +31,7 @@ var storageTypes = []StorageType{
// IsValidStorageType returns true if the given storage type is valid
func IsValidStorageType(storageType StorageType) bool {
- for _, t := range storageTypes {
- if t == storageType {
- return true
- }
- }
- return false
+ return slices.Contains(storageTypes, storageType)
}
// MinioStorageConfig represents the configuration for a minio storage
@@ -162,7 +158,7 @@ const (
targetSecIsSec // target section is from the name seciont [name]
)
-func getStorageSectionByType(rootCfg ConfigProvider, typ string) (ConfigSection, targetSecType, error) { //nolint:unparam
+func getStorageSectionByType(rootCfg ConfigProvider, typ string) (ConfigSection, targetSecType, error) { //nolint:unparam // FIXME: targetSecType is always 0, wrong design?
targetSec, err := rootCfg.GetSection(storageSectionName + "." + typ)
if err != nil {
if !IsValidStorageType(StorageType(typ)) {
@@ -287,7 +283,7 @@ func getStorageForLocal(targetSec, overrideSec ConfigSection, tp targetSecType,
return &storage, nil
}
-func getStorageForMinio(targetSec, overrideSec ConfigSection, tp targetSecType, name string) (*Storage, error) { //nolint:dupl
+func getStorageForMinio(targetSec, overrideSec ConfigSection, tp targetSecType, name string) (*Storage, error) { //nolint:dupl // duplicates azure setup
var storage Storage
storage.Type = StorageType(targetSec.Key("STORAGE_TYPE").String())
if err := targetSec.MapTo(&storage.MinioConfig); err != nil {
@@ -316,7 +312,7 @@ func getStorageForMinio(targetSec, overrideSec ConfigSection, tp targetSecType,
return &storage, nil
}
-func getStorageForAzureBlob(targetSec, overrideSec ConfigSection, tp targetSecType, name string) (*Storage, error) { //nolint:dupl
+func getStorageForAzureBlob(targetSec, overrideSec ConfigSection, tp targetSecType, name string) (*Storage, error) { //nolint:dupl // duplicates minio setup
var storage Storage
storage.Type = StorageType(targetSec.Key("STORAGE_TYPE").String())
if err := targetSec.MapTo(&storage.AzureBlobConfig); err != nil {
diff --git a/modules/ssh/init.go b/modules/ssh/init.go
index fdc11632e23..cfb0d5693a8 100644
--- a/modules/ssh/init.go
+++ b/modules/ssh/init.go
@@ -13,6 +13,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
)
func Init() error {
@@ -23,9 +24,11 @@ func Init() error {
if setting.SSH.StartBuiltinServer {
Listen(setting.SSH.ListenHost, setting.SSH.ListenPort, setting.SSH.ServerCiphers, setting.SSH.ServerKeyExchanges, setting.SSH.ServerMACs)
- log.Info("SSH server started on %s. Cipher list (%v), key exchange algorithms (%v), MACs (%v)",
+ log.Info("SSH server started on %q. Ciphers: %v, key exchange algorithms: %v, MACs: %v",
net.JoinHostPort(setting.SSH.ListenHost, strconv.Itoa(setting.SSH.ListenPort)),
- setting.SSH.ServerCiphers, setting.SSH.ServerKeyExchanges, setting.SSH.ServerMACs,
+ util.Iif[any](setting.SSH.ServerCiphers == nil, "default", setting.SSH.ServerCiphers),
+ util.Iif[any](setting.SSH.ServerKeyExchanges == nil, "default", setting.SSH.ServerKeyExchanges),
+ util.Iif[any](setting.SSH.ServerMACs == nil, "default", setting.SSH.ServerMACs),
)
return nil
}
diff --git a/modules/ssh/ssh.go b/modules/ssh/ssh.go
index ff0ad34a0d1..3fea4851c7c 100644
--- a/modules/ssh/ssh.go
+++ b/modules/ssh/ssh.go
@@ -333,7 +333,7 @@ func sshConnectionFailed(conn net.Conn, err error) {
log.Warn("Failed authentication attempt from %s", conn.RemoteAddr())
}
-// Listen starts a SSH server listens on given port.
+// Listen starts an SSH server listening on given port.
func Listen(host string, port int, ciphers, keyExchanges, macs []string) {
srv := ssh.Server{
Addr: net.JoinHostPort(host, strconv.Itoa(port)),
diff --git a/modules/structs/admin_user.go b/modules/structs/admin_user.go
index f7c6d10ba0f..c68b59a8978 100644
--- a/modules/structs/admin_user.go
+++ b/modules/structs/admin_user.go
@@ -8,8 +8,11 @@ import "time"
// CreateUserOption create user options
type CreateUserOption struct {
- SourceID int64 `json:"source_id"`
+ SourceID int64 `json:"source_id"`
+ // identifier of the user, provided by the external authenticator (if configured)
+ // default: empty
LoginName string `json:"login_name"`
+ // username of the user
// required: true
Username string `json:"username" binding:"Required;Username;MaxSize(40)"`
FullName string `json:"full_name" binding:"MaxSize(100)"`
@@ -32,6 +35,8 @@ type CreateUserOption struct {
type EditUserOption struct {
// required: true
SourceID int64 `json:"source_id"`
+ // identifier of the user, provided by the external authenticator (if configured)
+ // default: empty
// required: true
LoginName string `json:"login_name" binding:"Required"`
// swagger:strfmt email
diff --git a/modules/structs/commit_status_test.go b/modules/structs/commit_status_test.go
deleted file mode 100644
index 88e09aadc15..00000000000
--- a/modules/structs/commit_status_test.go
+++ /dev/null
@@ -1,174 +0,0 @@
-// Copyright 2023 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package structs
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestNoBetterThan(t *testing.T) {
- type args struct {
- css CommitStatusState
- css2 CommitStatusState
- }
- var unExpectedState CommitStatusState
- tests := []struct {
- name string
- args args
- want bool
- }{
- {
- name: "success is no better than success",
- args: args{
- css: CommitStatusSuccess,
- css2: CommitStatusSuccess,
- },
- want: true,
- },
- {
- name: "success is no better than pending",
- args: args{
- css: CommitStatusSuccess,
- css2: CommitStatusPending,
- },
- want: false,
- },
- {
- name: "success is no better than failure",
- args: args{
- css: CommitStatusSuccess,
- css2: CommitStatusFailure,
- },
- want: false,
- },
- {
- name: "success is no better than error",
- args: args{
- css: CommitStatusSuccess,
- css2: CommitStatusError,
- },
- want: false,
- },
- {
- name: "pending is no better than success",
- args: args{
- css: CommitStatusPending,
- css2: CommitStatusSuccess,
- },
- want: true,
- },
- {
- name: "pending is no better than pending",
- args: args{
- css: CommitStatusPending,
- css2: CommitStatusPending,
- },
- want: true,
- },
- {
- name: "pending is no better than failure",
- args: args{
- css: CommitStatusPending,
- css2: CommitStatusFailure,
- },
- want: false,
- },
- {
- name: "pending is no better than error",
- args: args{
- css: CommitStatusPending,
- css2: CommitStatusError,
- },
- want: false,
- },
- {
- name: "failure is no better than success",
- args: args{
- css: CommitStatusFailure,
- css2: CommitStatusSuccess,
- },
- want: true,
- },
- {
- name: "failure is no better than pending",
- args: args{
- css: CommitStatusFailure,
- css2: CommitStatusPending,
- },
- want: true,
- },
- {
- name: "failure is no better than failure",
- args: args{
- css: CommitStatusFailure,
- css2: CommitStatusFailure,
- },
- want: true,
- },
- {
- name: "failure is no better than error",
- args: args{
- css: CommitStatusFailure,
- css2: CommitStatusError,
- },
- want: false,
- },
- {
- name: "error is no better than success",
- args: args{
- css: CommitStatusError,
- css2: CommitStatusSuccess,
- },
- want: true,
- },
- {
- name: "error is no better than pending",
- args: args{
- css: CommitStatusError,
- css2: CommitStatusPending,
- },
- want: true,
- },
- {
- name: "error is no better than failure",
- args: args{
- css: CommitStatusError,
- css2: CommitStatusFailure,
- },
- want: true,
- },
- {
- name: "error is no better than error",
- args: args{
- css: CommitStatusError,
- css2: CommitStatusError,
- },
- want: true,
- },
- {
- name: "unExpectedState is no better than success",
- args: args{
- css: unExpectedState,
- css2: CommitStatusSuccess,
- },
- want: false,
- },
- {
- name: "unExpectedState is no better than unExpectedState",
- args: args{
- css: unExpectedState,
- css2: unExpectedState,
- },
- want: false,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- result := tt.args.css.NoBetterThan(tt.args.css2)
- assert.Equal(t, tt.want, result)
- })
- }
-}
diff --git a/modules/structs/git_blob.go b/modules/structs/git_blob.go
index 96770cc62e2..643b69ed372 100644
--- a/modules/structs/git_blob.go
+++ b/modules/structs/git_blob.go
@@ -10,4 +10,7 @@ type GitBlobResponse struct {
URL string `json:"url"`
SHA string `json:"sha"`
Size int64 `json:"size"`
+
+ LfsOid *string `json:"lfs_oid,omitempty"`
+ LfsSize *int64 `json:"lfs_size,omitempty"`
}
diff --git a/modules/structs/hook.go b/modules/structs/hook.go
index aaa9fbc9d36..ac779a57407 100644
--- a/modules/structs/hook.go
+++ b/modules/structs/hook.go
@@ -71,7 +71,8 @@ type PayloadUser struct {
// Full name of the commit author
Name string `json:"name"`
// swagger:strfmt email
- Email string `json:"email"`
+ Email string `json:"email"`
+ // username of the user
UserName string `json:"username"`
}
@@ -286,6 +287,8 @@ const (
HookIssueReOpened HookIssueAction = "reopened"
// HookIssueEdited edited
HookIssueEdited HookIssueAction = "edited"
+ // HookIssueDeleted is an issue action for deleting an issue
+ HookIssueDeleted HookIssueAction = "deleted"
// HookIssueAssigned assigned
HookIssueAssigned HookIssueAction = "assigned"
// HookIssueUnassigned unassigned
@@ -470,6 +473,22 @@ func (p *CommitStatusPayload) JSONPayload() ([]byte, error) {
return json.MarshalIndent(p, "", " ")
}
+// WorkflowRunPayload represents a payload information of workflow run event.
+type WorkflowRunPayload struct {
+ Action string `json:"action"`
+ Workflow *ActionWorkflow `json:"workflow"`
+ WorkflowRun *ActionWorkflowRun `json:"workflow_run"`
+ PullRequest *PullRequest `json:"pull_request,omitempty"`
+ Organization *Organization `json:"organization,omitempty"`
+ Repo *Repository `json:"repository"`
+ Sender *User `json:"sender"`
+}
+
+// JSONPayload implements Payload
+func (p *WorkflowRunPayload) JSONPayload() ([]byte, error) {
+ return json.MarshalIndent(p, "", " ")
+}
+
// WorkflowJobPayload represents a payload information of workflow job event.
type WorkflowJobPayload struct {
Action string `json:"action"`
diff --git a/modules/structs/issue.go b/modules/structs/issue.go
index 6a6b74c34e9..df0be8f9ec7 100644
--- a/modules/structs/issue.go
+++ b/modules/structs/issue.go
@@ -203,7 +203,7 @@ func (l *IssueTemplateStringSlice) UnmarshalYAML(value *yaml.Node) error {
if err != nil {
return err
}
- for _, v := range strings.Split(str, ",") {
+ for v := range strings.SplitSeq(str, ",") {
if v = strings.TrimSpace(v); v == "" {
continue
}
diff --git a/modules/structs/issue_tracked_time.go b/modules/structs/issue_tracked_time.go
index a3904af80ee..befcfb323d0 100644
--- a/modules/structs/issue_tracked_time.go
+++ b/modules/structs/issue_tracked_time.go
@@ -14,7 +14,7 @@ type AddTimeOption struct {
Time int64 `json:"time" binding:"Required"`
// swagger:strfmt date-time
Created time.Time `json:"created"`
- // User who spent the time (optional)
+ // username of the user who spent the time working on the issue (optional)
User string `json:"user_name"`
}
@@ -26,7 +26,8 @@ type TrackedTime struct {
// Time in seconds
Time int64 `json:"time"`
// deprecated (only for backwards compatibility)
- UserID int64 `json:"user_id"`
+ UserID int64 `json:"user_id"`
+ // username of the user
UserName string `json:"user_name"`
// deprecated (only for backwards compatibility)
IssueID int64 `json:"issue_id"`
diff --git a/modules/structs/org.go b/modules/structs/org.go
index f93b3b6493d..33b45c6344e 100644
--- a/modules/structs/org.go
+++ b/modules/structs/org.go
@@ -15,6 +15,7 @@ type Organization struct {
Location string `json:"location"`
Visibility string `json:"visibility"`
RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"`
+ // username of the organization
// deprecated
UserName string `json:"username"`
}
@@ -30,6 +31,7 @@ type OrganizationPermissions struct {
// CreateOrgOption options for creating an organization
type CreateOrgOption struct {
+ // username of the organization
// required: true
UserName string `json:"username" binding:"Required;Username;MaxSize(40)"`
FullName string `json:"full_name" binding:"MaxSize(100)"`
diff --git a/modules/structs/release.go b/modules/structs/release.go
index c7378645c28..fac86ca7a23 100644
--- a/modules/structs/release.go
+++ b/modules/structs/release.go
@@ -33,6 +33,7 @@ type Release struct {
type CreateReleaseOption struct {
// required: true
TagName string `json:"tag_name" binding:"Required"`
+ TagMessage string `json:"tag_message"`
Target string `json:"target_commitish"`
Title string `json:"name"`
Note string `json:"body"`
diff --git a/modules/structs/repo.go b/modules/structs/repo.go
index c48e71bc4e5..abc80763878 100644
--- a/modules/structs/repo.go
+++ b/modules/structs/repo.go
@@ -113,7 +113,7 @@ type Repository struct {
// enum: sha1,sha256
ObjectFormatName string `json:"object_format_name"`
// swagger:strfmt date-time
- MirrorUpdated time.Time `json:"mirror_updated,omitempty"`
+ MirrorUpdated time.Time `json:"mirror_updated"`
RepoTransfer *RepoTransfer `json:"repo_transfer"`
Topics []string `json:"topics"`
Licenses []string `json:"licenses"`
@@ -359,7 +359,7 @@ type MigrateRepoOptions struct {
// required: true
RepoName string `json:"repo_name" binding:"Required;AlphaDashDot;MaxSize(100)"`
- // enum: git,github,gitea,gitlab,gogs,onedev,gitbucket,codebase
+ // enum: git,github,gitea,gitlab,gogs,onedev,gitbucket,codebase,codecommit
Service string `json:"service"`
AuthUsername string `json:"auth_username"`
AuthPassword string `json:"auth_password"`
diff --git a/modules/structs/repo_actions.go b/modules/structs/repo_actions.go
index 75f8e188dda..ac1c288270a 100644
--- a/modules/structs/repo_actions.go
+++ b/modules/structs/repo_actions.go
@@ -57,7 +57,7 @@ type ActionWorkflow struct {
HTMLURL string `json:"html_url"`
BadgeURL string `json:"badge_url"`
// swagger:strfmt date-time
- DeletedAt time.Time `json:"deleted_at,omitempty"`
+ DeletedAt time.Time `json:"deleted_at"`
}
// ActionWorkflowResponse returns a ActionWorkflow
@@ -86,9 +86,39 @@ type ActionArtifact struct {
// ActionWorkflowRun represents a WorkflowRun
type ActionWorkflowRun struct {
- ID int64 `json:"id"`
- RepositoryID int64 `json:"repository_id"`
- HeadSha string `json:"head_sha"`
+ ID int64 `json:"id"`
+ URL string `json:"url"`
+ HTMLURL string `json:"html_url"`
+ DisplayTitle string `json:"display_title"`
+ Path string `json:"path"`
+ Event string `json:"event"`
+ RunAttempt int64 `json:"run_attempt"`
+ RunNumber int64 `json:"run_number"`
+ RepositoryID int64 `json:"repository_id,omitempty"`
+ HeadSha string `json:"head_sha"`
+ HeadBranch string `json:"head_branch,omitempty"`
+ Status string `json:"status"`
+ Actor *User `json:"actor,omitempty"`
+ TriggerActor *User `json:"trigger_actor,omitempty"`
+ Repository *Repository `json:"repository,omitempty"`
+ HeadRepository *Repository `json:"head_repository,omitempty"`
+ Conclusion string `json:"conclusion,omitempty"`
+ // swagger:strfmt date-time
+ StartedAt time.Time `json:"started_at"`
+ // swagger:strfmt date-time
+ CompletedAt time.Time `json:"completed_at"`
+}
+
+// ActionWorkflowRunsResponse returns ActionWorkflowRuns
+type ActionWorkflowRunsResponse struct {
+ Entries []*ActionWorkflowRun `json:"workflow_runs"`
+ TotalCount int64 `json:"total_count"`
+}
+
+// ActionWorkflowJobsResponse returns ActionWorkflowJobs
+type ActionWorkflowJobsResponse struct {
+ Entries []*ActionWorkflowJob `json:"jobs"`
+ TotalCount int64 `json:"total_count"`
}
// ActionArtifactsResponse returns ActionArtifacts
@@ -104,9 +134,9 @@ type ActionWorkflowStep struct {
Status string `json:"status"`
Conclusion string `json:"conclusion,omitempty"`
// swagger:strfmt date-time
- StartedAt time.Time `json:"started_at,omitempty"`
+ StartedAt time.Time `json:"started_at"`
// swagger:strfmt date-time
- CompletedAt time.Time `json:"completed_at,omitempty"`
+ CompletedAt time.Time `json:"completed_at"`
}
// ActionWorkflowJob represents a WorkflowJob
@@ -129,9 +159,9 @@ type ActionWorkflowJob struct {
// swagger:strfmt date-time
CreatedAt time.Time `json:"created_at"`
// swagger:strfmt date-time
- StartedAt time.Time `json:"started_at,omitempty"`
+ StartedAt time.Time `json:"started_at"`
// swagger:strfmt date-time
- CompletedAt time.Time `json:"completed_at,omitempty"`
+ CompletedAt time.Time `json:"completed_at"`
}
// ActionRunnerLabel represents a Runner Label
diff --git a/modules/structs/repo_branch.go b/modules/structs/repo_branch.go
index 55c98d60b91..5416f43b0d1 100644
--- a/modules/structs/repo_branch.go
+++ b/modules/structs/repo_branch.go
@@ -136,6 +136,7 @@ type UpdateBranchProtectionPriories struct {
type MergeUpstreamRequest struct {
Branch string `json:"branch"`
+ FfOnly bool `json:"ff_only"`
}
type MergeUpstreamResponse struct {
diff --git a/modules/structs/repo_file.go b/modules/structs/repo_file.go
index b0e0bd979e1..5a86db868b7 100644
--- a/modules/structs/repo_file.go
+++ b/modules/structs/repo_file.go
@@ -22,6 +22,23 @@ type FileOptions struct {
Signoff bool `json:"signoff"`
}
+type FileOptionsWithSHA struct {
+ FileOptions
+ // the blob ID (SHA) for the file that already exists, it is required for changing existing files
+ // required: true
+ SHA string `json:"sha" binding:"Required"`
+}
+
+func (f *FileOptions) GetFileOptions() *FileOptions {
+ return f
+}
+
+type FileOptionsInterface interface {
+ GetFileOptions() *FileOptions
+}
+
+var _ FileOptionsInterface = (*FileOptions)(nil)
+
// CreateFileOptions options for creating files
// Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)
type CreateFileOptions struct {
@@ -31,29 +48,16 @@ type CreateFileOptions struct {
ContentBase64 string `json:"content"`
}
-// Branch returns branch name
-func (o *CreateFileOptions) Branch() string {
- return o.FileOptions.BranchName
-}
-
// DeleteFileOptions options for deleting files (used for other File structs below)
// Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)
type DeleteFileOptions struct {
- FileOptions
- // sha is the SHA for the file that already exists
- // required: true
- SHA string `json:"sha" binding:"Required"`
-}
-
-// Branch returns branch name
-func (o *DeleteFileOptions) Branch() string {
- return o.FileOptions.BranchName
+ FileOptionsWithSHA
}
// UpdateFileOptions options for updating files
// Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)
type UpdateFileOptions struct {
- DeleteFileOptions
+ FileOptionsWithSHA
// content must be base64 encoded
// required: true
ContentBase64 string `json:"content"`
@@ -61,23 +65,21 @@ type UpdateFileOptions struct {
FromPath string `json:"from_path" binding:"MaxSize(500)"`
}
-// Branch returns branch name
-func (o *UpdateFileOptions) Branch() string {
- return o.FileOptions.BranchName
-}
+// FIXME: there is no LastCommitID in FileOptions, actually it should be an alternative to the SHA in ChangeFileOperation
// ChangeFileOperation for creating, updating or deleting a file
type ChangeFileOperation struct {
- // indicates what to do with the file
+ // indicates what to do with the file: "create" for creating a new file, "update" for updating an existing file,
+ // "upload" for creating or updating a file, "rename" for renaming a file, and "delete" for deleting an existing file.
// required: true
- // enum: create,update,delete
+ // enum: create,update,upload,rename,delete
Operation string `json:"operation" binding:"Required"`
// path to the existing or new file
// required: true
Path string `json:"path" binding:"Required;MaxSize(500)"`
- // new or updated file content, must be base64 encoded
+ // new or updated file content, it must be base64 encoded
ContentBase64 string `json:"content"`
- // sha is the SHA for the file that already exists, required for update or delete
+ // the blob ID (SHA) for the file that already exists, required for changing existing files
SHA string `json:"sha"`
// old path of the file to move
FromPath string `json:"from_path"`
@@ -92,20 +94,10 @@ type ChangeFilesOptions struct {
Files []*ChangeFileOperation `json:"files" binding:"Required"`
}
-// Branch returns branch name
-func (o *ChangeFilesOptions) Branch() string {
- return o.FileOptions.BranchName
-}
-
-// FileOptionInterface provides a unified interface for the different file options
-type FileOptionInterface interface {
- Branch() string
-}
-
// ApplyDiffPatchFileOptions options for applying a diff patch
// Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)
type ApplyDiffPatchFileOptions struct {
- DeleteFileOptions
+ FileOptions
// required: true
Content string `json:"content"`
}
@@ -117,16 +109,24 @@ type FileLinksResponse struct {
HTMLURL *string `json:"html"`
}
+type ContentsExtResponse struct {
+ FileContents *ContentsResponse `json:"file_contents,omitempty"`
+ DirContents []*ContentsResponse `json:"dir_contents,omitempty"`
+}
+
// ContentsResponse contains information about a repo's entry's (dir, file, symlink, submodule) metadata and content
type ContentsResponse struct {
- Name string `json:"name"`
- Path string `json:"path"`
- SHA string `json:"sha"`
- LastCommitSHA string `json:"last_commit_sha"`
+ Name string `json:"name"`
+ Path string `json:"path"`
+ SHA string `json:"sha"`
+
+ LastCommitSHA *string `json:"last_commit_sha,omitempty"`
// swagger:strfmt date-time
- LastCommitterDate time.Time `json:"last_committer_date"`
+ LastCommitterDate *time.Time `json:"last_committer_date,omitempty"`
// swagger:strfmt date-time
- LastAuthorDate time.Time `json:"last_author_date"`
+ LastAuthorDate *time.Time `json:"last_author_date,omitempty"`
+ LastCommitMessage *string `json:"last_commit_message,omitempty"`
+
// `type` will be `file`, `dir`, `symlink`, or `submodule`
Type string `json:"type"`
Size int64 `json:"size"`
@@ -143,6 +143,9 @@ type ContentsResponse struct {
// `submodule_git_url` is populated when `type` is `submodule`, otherwise null
SubmoduleGitURL *string `json:"submodule_git_url"`
Links *FileLinksResponse `json:"_links"`
+
+ LfsOid *string `json:"lfs_oid,omitempty"`
+ LfsSize *int64 `json:"lfs_size,omitempty"`
}
// FileCommitResponse contains information generated from a Git commit for a repo's file.
diff --git a/modules/structs/status.go b/modules/structs/status.go
index c1d8b902ec3..a9779541ff2 100644
--- a/modules/structs/status.go
+++ b/modules/structs/status.go
@@ -5,17 +5,19 @@ package structs
import (
"time"
+
+ "code.gitea.io/gitea/modules/commitstatus"
)
// CommitStatus holds a single status of a single Commit
type CommitStatus struct {
- ID int64 `json:"id"`
- State CommitStatusState `json:"status"`
- TargetURL string `json:"target_url"`
- Description string `json:"description"`
- URL string `json:"url"`
- Context string `json:"context"`
- Creator *User `json:"creator"`
+ ID int64 `json:"id"`
+ State commitstatus.CommitStatusState `json:"status"`
+ TargetURL string `json:"target_url"`
+ Description string `json:"description"`
+ URL string `json:"url"`
+ Context string `json:"context"`
+ Creator *User `json:"creator"`
// swagger:strfmt date-time
Created time.Time `json:"created_at"`
// swagger:strfmt date-time
@@ -24,19 +26,19 @@ type CommitStatus struct {
// CombinedStatus holds the combined state of several statuses for a single commit
type CombinedStatus struct {
- State CommitStatusState `json:"state"`
- SHA string `json:"sha"`
- TotalCount int `json:"total_count"`
- Statuses []*CommitStatus `json:"statuses"`
- Repository *Repository `json:"repository"`
- CommitURL string `json:"commit_url"`
- URL string `json:"url"`
+ State commitstatus.CommitStatusState `json:"state"`
+ SHA string `json:"sha"`
+ TotalCount int `json:"total_count"`
+ Statuses []*CommitStatus `json:"statuses"`
+ Repository *Repository `json:"repository"`
+ CommitURL string `json:"commit_url"`
+ URL string `json:"url"`
}
// CreateStatusOption holds the information needed to create a new CommitStatus for a Commit
type CreateStatusOption struct {
- State CommitStatusState `json:"state"`
- TargetURL string `json:"target_url"`
- Description string `json:"description"`
- Context string `json:"context"`
+ State commitstatus.CommitStatusState `json:"state"`
+ TargetURL string `json:"target_url"`
+ Description string `json:"description"`
+ Context string `json:"context"`
}
diff --git a/modules/structs/user.go b/modules/structs/user.go
index 5ed677f239f..89349cda2c7 100644
--- a/modules/structs/user.go
+++ b/modules/structs/user.go
@@ -15,9 +15,9 @@ import (
type User struct {
// the user's id
ID int64 `json:"id"`
- // the user's username
+ // login of the user, same as `username`
UserName string `json:"login"`
- // the user's authentication sign-in name.
+ // identifier of the user, provided by the external authenticator (if configured)
// default: empty
LoginName string `json:"login_name"`
// The ID of the user's Authentication Source
@@ -35,9 +35,9 @@ type User struct {
// Is the user an administrator
IsAdmin bool `json:"is_admin"`
// swagger:strfmt date-time
- LastLogin time.Time `json:"last_login,omitempty"`
+ LastLogin time.Time `json:"last_login"`
// swagger:strfmt date-time
- Created time.Time `json:"created,omitempty"`
+ Created time.Time `json:"created"`
// Is user restricted
Restricted bool `json:"restricted"`
// Is user active
diff --git a/modules/structs/user_email.go b/modules/structs/user_email.go
index 9319667e8fc..01895a00584 100644
--- a/modules/structs/user_email.go
+++ b/modules/structs/user_email.go
@@ -11,6 +11,7 @@ type Email struct {
Verified bool `json:"verified"`
Primary bool `json:"primary"`
UserID int64 `json:"user_id"`
+ // username of the user
UserName string `json:"username"`
}
diff --git a/modules/structs/user_gpgkey.go b/modules/structs/user_gpgkey.go
index ff9b0aea1d6..deae70de337 100644
--- a/modules/structs/user_gpgkey.go
+++ b/modules/structs/user_gpgkey.go
@@ -21,9 +21,9 @@ type GPGKey struct {
CanCertify bool `json:"can_certify"`
Verified bool `json:"verified"`
// swagger:strfmt date-time
- Created time.Time `json:"created_at,omitempty"`
+ Created time.Time `json:"created_at"`
// swagger:strfmt date-time
- Expires time.Time `json:"expires_at,omitempty"`
+ Expires time.Time `json:"expires_at"`
}
// GPGKeyEmail an email attached to a GPGKey
diff --git a/modules/structs/user_key.go b/modules/structs/user_key.go
index c4c41207e1c..16225a852a5 100644
--- a/modules/structs/user_key.go
+++ b/modules/structs/user_key.go
@@ -15,8 +15,8 @@ type PublicKey struct {
Title string `json:"title,omitempty"`
Fingerprint string `json:"fingerprint,omitempty"`
// swagger:strfmt date-time
- Created time.Time `json:"created_at,omitempty"`
- Updated time.Time `json:"last_used_at,omitempty"`
+ Created time.Time `json:"created_at"`
+ Updated time.Time `json:"last_used_at"`
Owner *User `json:"user,omitempty"`
ReadOnly bool `json:"read_only,omitempty"`
KeyType string `json:"key_type,omitempty"`
diff --git a/modules/templates/eval/eval_test.go b/modules/templates/eval/eval_test.go
index c9e514b5eb9..f956f6cbdf3 100644
--- a/modules/templates/eval/eval_test.go
+++ b/modules/templates/eval/eval_test.go
@@ -12,7 +12,7 @@ import (
)
func tokens(s string) (a []any) {
- for _, v := range strings.Fields(s) {
+ for v := range strings.FieldsSeq(s) {
a = append(a, v)
}
return a
diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index d55d4f87c5f..e454bce4bd3 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -6,7 +6,6 @@ package templates
import (
"fmt"
- "html"
"html/template"
"net/url"
"strconv"
@@ -38,12 +37,9 @@ func NewFuncMap() template.FuncMap {
"dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names.
"Iif": iif,
"Eval": evalTokens,
- "SafeHTML": safeHTML,
"HTMLFormat": htmlFormat,
- "HTMLEscape": htmlEscape,
"QueryEscape": queryEscape,
"QueryBuild": QueryBuild,
- "JSEscape": jsEscapeSafe,
"SanitizeHTML": SanitizeHTML,
"URLJoin": util.URLJoin,
"DotEscape": dotEscape,
@@ -165,30 +161,9 @@ func NewFuncMap() template.FuncMap {
}
}
-// safeHTML render raw as HTML
-func safeHTML(s any) template.HTML {
- switch v := s.(type) {
- case string:
- return template.HTML(v)
- case template.HTML:
- return v
- }
- panic(fmt.Sprintf("unexpected type %T", s))
-}
-
-// SanitizeHTML sanitizes the input by pre-defined markdown rules
+// SanitizeHTML sanitizes the input by default sanitization rules.
func SanitizeHTML(s string) template.HTML {
- return template.HTML(markup.Sanitize(s))
-}
-
-func htmlEscape(s any) template.HTML {
- switch v := s.(type) {
- case string:
- return template.HTML(html.EscapeString(v))
- case template.HTML:
- return v
- }
- panic(fmt.Sprintf("unexpected type %T", s))
+ return markup.Sanitize(s)
}
func htmlFormat(s any, args ...any) template.HTML {
@@ -205,10 +180,6 @@ func htmlFormat(s any, args ...any) template.HTML {
panic(fmt.Sprintf("unexpected type %T", s))
}
-func jsEscapeSafe(s string) template.HTML {
- return template.HTML(template.JSEscapeString(s))
-}
-
func queryEscape(s string) template.URL {
return template.URL(url.QueryEscape(s))
}
diff --git a/modules/templates/helper_test.go b/modules/templates/helper_test.go
index 81f8235bd2b..7e3a952e7b7 100644
--- a/modules/templates/helper_test.go
+++ b/modules/templates/helper_test.go
@@ -57,10 +57,6 @@ func TestSubjectBodySeparator(t *testing.T) {
"Insufficient\n--\nSeparators")
}
-func TestJSEscapeSafe(t *testing.T) {
- assert.EqualValues(t, `\u0026\u003C\u003E\'\"`, jsEscapeSafe(`&<>'"`))
-}
-
func TestSanitizeHTML(t *testing.T) {
assert.Equal(t, template.HTML(`link xss inline`), SanitizeHTML(`link xss inline`))
}
diff --git a/modules/templates/htmlrenderer.go b/modules/templates/htmlrenderer.go
index 529284f7e8e..8073a6e5f5b 100644
--- a/modules/templates/htmlrenderer.go
+++ b/modules/templates/htmlrenderer.go
@@ -42,7 +42,7 @@ var (
var ErrTemplateNotInitialized = errors.New("template system is not initialized, check your log for errors")
-func (h *HTMLRender) HTML(w io.Writer, status int, tplName TplName, data any, ctx context.Context) error { //nolint:revive
+func (h *HTMLRender) HTML(w io.Writer, status int, tplName TplName, data any, ctx context.Context) error { //nolint:revive // we don't use ctx, only pass it to the template executor
name := string(tplName)
if respWriter, ok := w.(http.ResponseWriter); ok {
if respWriter.Header().Get("Content-Type") == "" {
@@ -57,7 +57,7 @@ func (h *HTMLRender) HTML(w io.Writer, status int, tplName TplName, data any, ct
return t.Execute(w, data)
}
-func (h *HTMLRender) TemplateLookup(name string, ctx context.Context) (TemplateExecutor, error) { //nolint:revive
+func (h *HTMLRender) TemplateLookup(name string, ctx context.Context) (TemplateExecutor, error) { //nolint:revive // we don't use ctx, only pass it to the template executor
tmpls := h.templates.Load()
if tmpls == nil {
return nil, ErrTemplateNotInitialized
@@ -251,7 +251,7 @@ func extractErrorLine(code []byte, lineNum, posNum int, target string) string {
b := bufio.NewReader(bytes.NewReader(code))
var line []byte
var err error
- for i := 0; i < lineNum; i++ {
+ for i := range lineNum {
if line, err = b.ReadBytes('\n'); err != nil {
if i == lineNum-1 && errors.Is(err, io.EOF) {
err = nil
diff --git a/modules/templates/scopedtmpl/scopedtmpl.go b/modules/templates/scopedtmpl/scopedtmpl.go
index 2722ba97a28..34e8b9ad701 100644
--- a/modules/templates/scopedtmpl/scopedtmpl.go
+++ b/modules/templates/scopedtmpl/scopedtmpl.go
@@ -7,6 +7,7 @@ import (
"fmt"
"html/template"
"io"
+ "maps"
"reflect"
"sync"
texttemplate "text/template"
@@ -40,9 +41,7 @@ func (t *ScopedTemplate) Funcs(funcMap template.FuncMap) {
panic("cannot add new functions to frozen template set")
}
t.all.Funcs(funcMap)
- for k, v := range funcMap {
- t.parseFuncs[k] = v
- }
+ maps.Copy(t.parseFuncs, funcMap)
}
func (t *ScopedTemplate) New(name string) *template.Template {
@@ -103,31 +102,28 @@ func escapeTemplate(t *template.Template) error {
return nil
}
-//nolint:unused
type htmlTemplate struct {
- escapeErr error
- text *texttemplate.Template
+ _/*escapeErr*/ error
+ text *texttemplate.Template
}
-//nolint:unused
type textTemplateCommon struct {
- tmpl map[string]*template.Template // Map from name to defined templates.
- muTmpl sync.RWMutex // protects tmpl
- option struct {
+ _/*tmpl*/ map[string]*template.Template
+ _/*muTmpl*/ sync.RWMutex
+ _/*option*/ struct {
missingKey int
}
- muFuncs sync.RWMutex // protects parseFuncs and execFuncs
- parseFuncs texttemplate.FuncMap
- execFuncs map[string]reflect.Value
+ muFuncs sync.RWMutex
+ _/*parseFuncs*/ texttemplate.FuncMap
+ execFuncs map[string]reflect.Value
}
-//nolint:unused
type textTemplate struct {
- name string
+ _/*name*/ string
*parse.Tree
*textTemplateCommon
- leftDelim string
- rightDelim string
+ _/*leftDelim*/ string
+ _/*rightDelim*/ string
}
func ptr[T, P any](ptr *P) *T {
@@ -159,9 +155,7 @@ func newScopedTemplateSet(all *template.Template, name string) (*scopedTemplateS
textTmplPtr.muFuncs.Lock()
ts.execFuncs = map[string]reflect.Value{}
- for k, v := range textTmplPtr.execFuncs {
- ts.execFuncs[k] = v
- }
+ maps.Copy(ts.execFuncs, textTmplPtr.execFuncs)
textTmplPtr.muFuncs.Unlock()
var collectTemplates func(nodes []parse.Node)
@@ -220,9 +214,7 @@ func (ts *scopedTemplateSet) newExecutor(funcMap map[string]any) TemplateExecuto
tmpl := texttemplate.New("")
tmplPtr := ptr[textTemplate](tmpl)
tmplPtr.execFuncs = map[string]reflect.Value{}
- for k, v := range ts.execFuncs {
- tmplPtr.execFuncs[k] = v
- }
+ maps.Copy(tmplPtr.execFuncs, ts.execFuncs)
if funcMap != nil {
tmpl.Funcs(funcMap)
}
diff --git a/modules/templates/static.go b/modules/templates/static.go
deleted file mode 100644
index b5a7e561ec0..00000000000
--- a/modules/templates/static.go
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright 2016 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-//go:build bindata
-
-package templates
-
-import (
- "time"
-
- "code.gitea.io/gitea/modules/assetfs"
- "code.gitea.io/gitea/modules/timeutil"
-)
-
-// GlobalModTime provide a global mod time for embedded asset files
-func GlobalModTime(filename string) time.Time {
- return timeutil.GetExecutableModTime()
-}
-
-func BuiltinAssets() *assetfs.Layer {
- return assetfs.Bindata("builtin(bindata)", Assets)
-}
diff --git a/modules/templates/templates_bindata.go b/modules/templates/templates_bindata.go
index 6f1d3cf5395..a919591ecfd 100644
--- a/modules/templates/templates_bindata.go
+++ b/modules/templates/templates_bindata.go
@@ -3,6 +3,21 @@
//go:build bindata
+//go:generate go run ../../build/generate-bindata.go ../../templates bindata.dat
+
package templates
-//go:generate go run ../../build/generate-bindata.go ../../templates templates bindata.go true
+import (
+ "sync"
+
+ _ "embed"
+
+ "code.gitea.io/gitea/modules/assetfs"
+)
+
+//go:embed bindata.dat
+var bindata []byte
+
+var BuiltinAssets = sync.OnceValue(func() *assetfs.Layer {
+ return assetfs.Bindata("builtin(bindata)", assetfs.NewEmbeddedFS(bindata))
+})
diff --git a/modules/templates/dynamic.go b/modules/templates/templates_dynamic.go
similarity index 100%
rename from modules/templates/dynamic.go
rename to modules/templates/templates_dynamic.go
diff --git a/modules/templates/util_json.go b/modules/templates/util_json.go
index 71a4e23d364..29a04290fa0 100644
--- a/modules/templates/util_json.go
+++ b/modules/templates/util_json.go
@@ -9,11 +9,11 @@ import (
"code.gitea.io/gitea/modules/json"
)
-type JsonUtils struct{} //nolint:revive
+type JsonUtils struct{} //nolint:revive // variable naming triggers on Json, wants JSON
var jsonUtils = JsonUtils{}
-func NewJsonUtils() *JsonUtils { //nolint:revive
+func NewJsonUtils() *JsonUtils { //nolint:revive // variable naming triggers on Json, wants JSON
return &jsonUtils
}
diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go
index 8d9ba1000c8..1056c426433 100644
--- a/modules/templates/util_render.go
+++ b/modules/templates/util_render.go
@@ -38,8 +38,8 @@ func NewRenderUtils(ctx reqctx.RequestContext) *RenderUtils {
// RenderCommitMessage renders commit message with XSS-safe and special links.
func (ut *RenderUtils) RenderCommitMessage(msg string, repo *repo.Repository) template.HTML {
cleanMsg := template.HTMLEscapeString(msg)
- // we can safely assume that it will not return any error, since there
- // shouldn't be any special HTML.
+ // we can safely assume that it will not return any error, since there shouldn't be any special HTML.
+ // "repo" can be nil when rendering commit messages for deleted repositories in a user's dashboard feed.
fullMessage, err := markup.PostProcessCommitMessage(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), cleanMsg)
if err != nil {
log.Error("PostProcessCommitMessage: %v", err)
@@ -47,7 +47,7 @@ func (ut *RenderUtils) RenderCommitMessage(msg string, repo *repo.Repository) te
}
msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n")
if len(msgLines) == 0 {
- return template.HTML("")
+ return ""
}
return renderCodeBlock(template.HTML(msgLines[0]))
}
@@ -122,8 +122,23 @@ func (ut *RenderUtils) RenderIssueSimpleTitle(text string) template.HTML {
return ret
}
-// RenderLabel renders a label
+func (ut *RenderUtils) RenderLabelWithLink(label *issues_model.Label, link any) template.HTML {
+ var attrHref template.HTML
+ switch link.(type) {
+ case template.URL, string:
+ attrHref = htmlutil.HTMLFormat(`href="%s"`, link)
+ default:
+ panic(fmt.Sprintf("unexpected type %T for link", link))
+ }
+ return ut.renderLabelWithTag(label, "a", attrHref)
+}
+
func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML {
+ return ut.renderLabelWithTag(label, "span", "")
+}
+
+// RenderLabel renders a label
+func (ut *RenderUtils) renderLabelWithTag(label *issues_model.Label, tagName, tagAttrs template.HTML) template.HTML {
locale := ut.ctx.Value(translation.ContextKey).(translation.Locale)
var extraCSSClasses string
textColor := util.ContrastColor(label.Color)
@@ -137,8 +152,8 @@ func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML {
if labelScope == "" {
// Regular label
- return htmlutil.HTMLFormat(`%s`,
- extraCSSClasses, textColor, label.Color, descriptionText, ut.RenderEmoji(label.Name))
+ return htmlutil.HTMLFormat(`<%s %s class="ui label %s" style="color: %s !important; background-color: %s !important;" data-tooltip-content title="%s">%s%s>`,
+ tagName, tagAttrs, extraCSSClasses, textColor, label.Color, descriptionText, ut.RenderEmoji(label.Name), tagName)
}
// Scoped label
@@ -152,7 +167,7 @@ func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML {
// Ensure we add the same amount of contrast also near 0 and 1.
darken := contrast + math.Max(luminance+contrast-1.0, 0.0)
lighten := contrast + math.Max(contrast-luminance, 0.0)
- // Compute factor to keep RGB values proportional.
+ // Compute the factor to keep RGB values proportional.
darkenFactor := math.Max(luminance-darken, 0.0) / math.Max(luminance, 1.0/255.0)
lightenFactor := math.Min(luminance+lighten, 1.0) / math.Max(luminance, 1.0/255.0)
@@ -173,26 +188,29 @@ func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML {
if label.ExclusiveOrder > 0 {
// | {{ctx.Locale.Tr "admin.auths.tips.oauth2.general.tip"}}
- -{{ctx.Locale.Tr "admin.auths.delete_auth_desc"}}
-| {{FileSize .LFSSize}} | {{DateUtils.AbsoluteShort .UpdatedUnix}} | {{DateUtils.AbsoluteShort .CreatedUnix}} | -{{svg "octicon-trash"}} | ++ {{svg "octicon-trash"}} + | {{else}}|||||||
| {{ctx.Locale.Tr "no_results_found"}} |