Compare commits

...

27 Commits

Author SHA1 Message Date
Giteabot 6fbc0f4972
Merge branch 'main' into test-must-change-password 2024-04-27 19:55:15 +08:00
Kemal Zebari dd301cae1c
Prevent allow/reject reviews on merged/closed PRs (#30686)
Resolves #30675.
2024-04-27 11:55:03 +00:00
Giteabot 05c88ca6d0
Merge branch 'main' into test-must-change-password 2024-04-27 19:28:42 +08:00
silverwind 238eb3ff9f
Update JS dependencies (#30713)
- Update all JS dependencies
- Remove
[now-unnecessary](https://github.com/microsoft/monaco-editor/issues/4325)
monaco workaround
- Update stylelint config for new rule
- Tested Monaco, Swagger UI, Mermaid
2024-04-27 11:28:28 +00:00
wxiaoguang 7415e0e307
Merge branch 'main' into test-must-change-password 2024-04-27 19:24:23 +08:00
silverwind b2abac5e5f
Improve diff stats bar (#30669)
Minor tweaks:

- Remove unnecessary `item` class which was causing unwanted padding to
be added.
- Add some padding and prevent wrapping so it looks better on mobile.
- Increase width by 4px.

<img width="116" alt="Screenshot 2024-04-24 at 00 15 07"
src="https://github.com/go-gitea/gitea/assets/115237/1f1cf54c-8053-4297-b309-71d9c2ceb9ee">
<img width="441" alt="Screenshot 2024-04-24 at 00 14 57"
src="https://github.com/go-gitea/gitea/assets/115237/2f3a33dc-edad-4b97-b64c-6812aae513cb">
2024-04-27 11:22:55 +00:00
wxiaoguang 7d69cde0dc revert to old behavior 2024-04-27 19:20:59 +08:00
wxiaoguang 849a130142 Merge branch 'main' into test-must-change-password 2024-04-27 19:02:32 +08:00
Chongyi Zheng 4ae6b1a553
Remove unused parameter for some functions in `services/mirror` (#30724)
Suggested by gopls `unusedparams`
2024-04-27 10:44:49 +00:00
silverwind 9b2536b78f
Update misspell to 0.5.1 and add `misspellings.csv` (#30573)
Misspell 0.5.0 supports passing a csv file to extend the list of
misspellings, so I added some common ones from the codebase. There is at
least one typo in a API response so we need to decided whether to revert
that and then likely remove the dict entry.
2024-04-27 08:03:49 +00:00
silverwind dcc3c17e5c
Suppress browserslist warning in webpack target (#30571)
1. Set
[`BROWSERSLIST_IGNORE_OLD_DATA`](c6ddf7b387/node.js (L400))
to avoid warning on outdated browserslist data which the end user can
likely not do anything about and which is currently visible in the v1.21
branch.
2. Suppress all command echoing and add a "Running webpack..." message
in place.

Warning in question was this:

```
Browserslist: caniuse-lite is outdated. Please run:
  npx update-browserslist-db@latest
  Why you should do it regularly: https://github.com/browserslist/update-db#readme
```
2024-04-27 07:21:07 +00:00
GiteaBot 27861d711b [skip ci] Updated translations via Crowdin 2024-04-27 00:24:31 +00:00
silverwind c93eefb42b
Diff color enhancements, add line number background (#30670)
1. Bring back the background on line numbers. This feature was lost a
long time ago.

<img width="457" alt="Screenshot 2024-04-24 at 01 36 09"
src="https://github.com/go-gitea/gitea/assets/115237/76a7f5a9-c22a-4c72-9f0a-ebf16a66513e">
<img width="473" alt="Screenshot 2024-04-24 at 01 22 47"
src="https://github.com/go-gitea/gitea/assets/115237/eef06cf2-f1b9-40e3-947d-dd5852ec12a3">
<img width="457" alt="Screenshot 2024-04-24 at 02 13 18"
src="https://github.com/go-gitea/gitea/assets/115237/59e317d4-76a7-468c-8a19-10d88c675cc3">
<img width="459" alt="Screenshot 2024-04-24 at 01 23 21"
src="https://github.com/go-gitea/gitea/assets/115237/f1a46f8d-8846-4d78-a9d7-8b7dc18ac6e4">

2. Expanded lines background is now full-line, including line numbers:

<img width="1303" alt="Screenshot 2024-04-24 at 01 37 12"
src="https://github.com/go-gitea/gitea/assets/115237/271eefe2-0869-424e-93fb-ccd8adc87806">

3. Sort affected colors alphabetically in the CSS

Fixes #14603
2024-04-26 19:37:21 +00:00
Bo-Yi Wu 852547d0dc
feat(api): enhance Actions Secrets Management API for repository (#30656)
- Add endpoint to list repository action secrets in API routes
- Implement `ListActionsSecrets` function to retrieve action secrets
from the database
- Update Swagger documentation to include the new
`/repos/{owner}/{repo}/actions/secrets` endpoint
- Add `actions` package import and define new routes for actions,
secrets, variables, and runners in `api.go`.
- Refactor action-related API functions into `Action` struct methods in
`org/action.go` and `repo/action.go`.
- Remove `actionAPI` struct and related functions, replacing them with
`NewAction()` calls.
- Rename `variables.go` to `action.go` in `org` directory.
- Delete `runners.go` and `secrets.go` in both `org` and `repo`
directories, consolidating their content into `action.go`.
- Update copyright year and add new imports in `org/action.go`.
- Implement `API` interface in `services/actions/interface.go` for
action-related methods.
- Remove individual action-related functions and replace them with
methods on the `Action` struct in `repo/action.go`.

---------

Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Signed-off-by: appleboy <appleboy.tw@gmail.com>
2024-04-26 21:11:49 +08:00
wxiaoguang 993736d838
Fix code search input for different views (#30678)
Now only show the "code search" on the repo home page, because it only
does global search.
So do not show it when viewing file or directory to avoid misleading
users (it doesn't search in a directory)
2024-04-26 11:21:04 +00:00
wxiaoguang cd70ab31cd
Fix incorrect object id hash function (#30708)
Great thanks to @oliverpool for figuring out the problem and proposing a
fix.

Regression of #28138

Incorrect hash causes the user's LFS files get all deleted when running
`doctor fix all`

(by the way, remove unused/non-standard comments)

Co-authored-by: Giteabot <teabot@gitea.io>
2024-04-26 09:49:48 +00:00
wxiaoguang 1e749b80d7
Add route handler info for debugging purpose (#30705)
Follow #30519
2024-04-26 09:09:49 +00:00
Yarden Shoham 68a3e6b5e6
Bump htmx version to 1.9.12 (#30711)
There are no breaking changes. I tested and everything works as before.

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
2024-04-26 09:27:34 +02:00
wxiaoguang ed8c63cea3
Deduplicate lfs common code (#30704) 2024-04-26 02:53:30 +00:00
yp05327 2a3906d755
Improve job commit description (#30579)
Fix https://github.com/go-gitea/gitea/issues/30567

When job is a schedule:

![image](https://github.com/go-gitea/gitea/assets/18380374/b07e9d43-e8b7-4ee2-87b3-a7050c3a8ca5)
When it is a normal one:

![image](https://github.com/go-gitea/gitea/assets/18380374/0d58dab9-74bb-421b-8952-0578cdf21a52)

also add a 'space' behind  `:`

![image](https://github.com/go-gitea/gitea/assets/18380374/4cebece0-bfe6-4ad9-b806-e5c49bb9be43)


![image](https://github.com/go-gitea/gitea/assets/18380374/02da7681-474b-4c0f-9dad-b6558f6cb484)

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2024-04-26 02:22:45 +00:00
Lunny Xiao 2a6418abb1
Improve test for TestPullCompare (#30699) 2024-04-26 01:52:28 +00:00
wxiaoguang 6a0750177f
Allow to save empty comment (#30706)
Fix #29986
2024-04-26 01:17:43 +00:00
GiteaBot 935330b1b9 [skip ci] Updated translations via Crowdin 2024-04-26 00:26:00 +00:00
wxiaoguang fd63b96f6a
Refactor imagediff and fix regression bug (#30694)
Fix #30683
2024-04-25 21:01:38 +08:00
wxiaoguang bffbbf5470
Improve oauth2 client "preferred username field" logic and the error handling (#30622)
Follow #30454
And fix #24957

When using "preferred_username", if no such field,
`extractUserNameFromOAuth2` (old `getUserName`) shouldn't return an
error. All other USERNAME options do not return such error.

And fine tune some logic and error messages, make code more stable and
more friendly to end users.
2024-04-25 11:22:32 +00:00
silverwind d0bfc978de
Fix active item in tab menu (#30690)
Before, item would also resize on hover because of font weight:
<img width="381" alt="Screenshot 2024-04-25 at 01 28 53"
src="https://github.com/go-gitea/gitea/assets/115237/4f3291fc-90be-4d66-ae8b-3c2f763cb956">

After:
<img width="381" alt="Screenshot 2024-04-25 at 01 28 40"
src="https://github.com/go-gitea/gitea/assets/115237/06145bf2-1ddd-4171-9217-d92c100ea405">

Co-authored-by: Giteabot <teabot@gitea.io>
2024-04-25 12:53:39 +02:00
Lunny Xiao c685eefe4a
If a repository return no commitstatus, then still cache it but not query it from database (#30700)
The previous repository default branch commit status cache will only
store if the commit status has value. So the repository which have no
any commit status will always be fetched from database.

This PR will store the empty state of commit status of a repository into
cache because the cache will be updated once there is a commit status
stored.
2024-04-25 17:14:23 +08:00
96 changed files with 1504 additions and 1101 deletions

View File

@ -30,7 +30,7 @@ EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-che
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.6.0 GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.6.0
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.57.2 GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.57.2
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11 GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.4.1 MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.5.1
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@db51e79a0e37c572d8b59ae0c58bf2bbbbe53285 SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@db51e79a0e37c572d8b59ae0c58bf2bbbbe53285
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1 GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1
@ -397,11 +397,11 @@ lint-md: node_modules
.PHONY: lint-spell .PHONY: lint-spell
lint-spell: lint-spell:
@go run $(MISSPELL_PACKAGE) -error $(SPELLCHECK_FILES) @go run $(MISSPELL_PACKAGE) -dict tools/misspellings.csv -error $(SPELLCHECK_FILES)
.PHONY: lint-spell-fix .PHONY: lint-spell-fix
lint-spell-fix: lint-spell-fix:
@go run $(MISSPELL_PACKAGE) -w $(SPELLCHECK_FILES) @go run $(MISSPELL_PACKAGE) -dict tools/misspellings.csv -w $(SPELLCHECK_FILES)
.PHONY: lint-go .PHONY: lint-go
lint-go: lint-go:
@ -908,8 +908,9 @@ webpack: $(WEBPACK_DEST)
$(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) package-lock.json $(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) package-lock.json
@$(MAKE) -s node-check node_modules @$(MAKE) -s node-check node_modules
rm -rf $(WEBPACK_DEST_ENTRIES) @rm -rf $(WEBPACK_DEST_ENTRIES)
npx webpack @echo "Running webpack..."
@BROWSERSLIST_IGNORE_OLD_DATA=true npx webpack
@touch $(WEBPACK_DEST) @touch $(WEBPACK_DEST)
.PHONY: svg .PHONY: svg

View File

@ -49,7 +49,7 @@ var microcmdUserCreate = &cli.Command{
}, },
&cli.BoolFlag{ &cli.BoolFlag{
Name: "must-change-password", Name: "must-change-password",
Usage: "User must change password after initial login, defaults to true for all users except the first admin user (can be disabled by --must-change-password=false)", 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, DisableDefaultText: true,
}, },
&cli.IntFlag{ &cli.IntFlag{
@ -129,8 +129,8 @@ func runCreateUser(c *cli.Context) error {
if err != nil { if err != nil {
return fmt.Errorf("IsTableNotEmpty: %w", err) return fmt.Errorf("IsTableNotEmpty: %w", err)
} }
if !hasUserRecord && isAdmin { if !hasUserRecord {
// if this is the first admin being created, don't force to change password (keep the old behavior) // if this is the first one being created, don't force to change password (keep the old behavior)
mustChangePassword = false mustChangePassword = false
} }
} }

View File

@ -30,7 +30,7 @@ func TestAdminUserCreate(t *testing.T) {
return createCheck{u.IsAdmin, u.MustChangePassword} return createCheck{u.IsAdmin, u.MustChangePassword}
} }
reset() reset()
assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: true}, createUser("u", ""), "first non-admin user must change password") assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u", ""), "first non-admin user doesn't need to change password")
reset() reset()
assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u", "--admin"), "first admin user doesn't need to change password") assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u", "--admin"), "first admin user doesn't need to change password")

View File

@ -1558,8 +1558,8 @@ LEVEL = Info
;; email = use the username part of the email attribute ;; email = use the username part of the email attribute
;; Note: `nickname`, `preferred_username` and `email` options will normalize input strings using the following criteria: ;; Note: `nickname`, `preferred_username` and `email` options will normalize input strings using the following criteria:
;; - diacritics are removed ;; - diacritics are removed
;; - the characters in the set `['´\x60]` are removed ;; - the characters in the set ['´`] are removed
;; - the characters in the set `[\s~+]` are replaced with `-` ;; - the characters in the set [\s~+] are replaced with "-"
;USERNAME = nickname ;USERNAME = nickname
;; ;;
;; Update avatar if available from oauth2 provider. ;; Update avatar if available from oauth2 provider.

View File

@ -612,7 +612,7 @@ And the following unique queues:
- `email` - use the username part of the email attribute - `email` - use the username part of the email attribute
- Note: `nickname`, `preferred_username` and `email` options will normalize input strings using the following criteria: - Note: `nickname`, `preferred_username` and `email` options will normalize input strings using the following criteria:
- diacritics are removed - diacritics are removed
- the characters in the set `['´\x60]` are removed - the characters in the set ```['´`]``` are removed
- the characters in the set `[\s~+]` are replaced with `-` - the characters in the set `[\s~+]` are replaced with `-`
- `UPDATE_AVATAR`: **false**: Update avatar if available from oauth2 provider. Update will be performed on each login. - `UPDATE_AVATAR`: **false**: Update avatar if available from oauth2 provider. Update will be performed on each login.
- `ACCOUNT_LINKING`: **login**: How to handle if an account / email already exists: - `ACCOUNT_LINKING`: **login**: How to handle if an account / email already exists:
@ -1322,7 +1322,7 @@ Defaultly every storage has their default base path like below
| actions_log | actions_log/ | | actions_log | actions_log/ |
| actions_artifacts | actions_artifacts/ | | actions_artifacts | actions_artifacts/ |
And bucket, basepath or `SERVE_DIRECT` could be special or overrided, if you want to use a different you can: And bucket, basepath or `SERVE_DIRECT` could be special or overridden, if you want to use a different you can:
```ini ```ini
[storage.actions_log] [storage.actions_log]

View File

@ -74,6 +74,13 @@ func (run *ActionRun) Link() string {
return fmt.Sprintf("%s/actions/runs/%d", run.Repo.Link(), run.Index) return fmt.Sprintf("%s/actions/runs/%d", run.Repo.Link(), run.Index)
} }
func (run *ActionRun) WorkflowLink() string {
if run.Repo == nil {
return ""
}
return fmt.Sprintf("%s/actions/?workflow=%s", run.Repo.Link(), run.WorkflowID)
}
// RefLink return the url of run's ref // RefLink return the url of run's ref
func (run *ActionRun) RefLink() string { func (run *ActionRun) RefLink() string {
refName := git.RefName(run.Ref) refName := git.RefName(run.Ref)
@ -156,6 +163,10 @@ func (run *ActionRun) GetPullRequestEventPayload() (*api.PullRequestPayload, err
return nil, fmt.Errorf("event %s is not a pull request event", run.Event) return nil, fmt.Errorf("event %s is not a pull request event", run.Event)
} }
func (run *ActionRun) IsSchedule() bool {
return run.ScheduleID > 0
}
func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) error { func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) error {
_, err := db.GetEngine(ctx).ID(repo.ID). _, err := db.GetEngine(ctx).ID(repo.ID).
SetExpr("num_action_runs", SetExpr("num_action_runs",
@ -251,11 +262,11 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin
// InsertRun inserts a run // InsertRun inserts a run
func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWorkflow) error { func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWorkflow) error {
ctx, commiter, err := db.TxContext(ctx) ctx, committer, err := db.TxContext(ctx)
if err != nil { if err != nil {
return err return err
} }
defer commiter.Close() defer committer.Close()
index, err := db.GetNextResourceIndex(ctx, "action_run_index", run.RepoID) index, err := db.GetNextResourceIndex(ctx, "action_run_index", run.RepoID)
if err != nil { if err != nil {
@ -320,7 +331,7 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork
} }
} }
return commiter.Commit() return committer.Commit()
} }
func GetRunByID(ctx context.Context, id int64) (*ActionRun, error) { func GetRunByID(ctx context.Context, id int64) (*ActionRun, error) {

View File

@ -216,11 +216,11 @@ func GetRunningTaskByToken(ctx context.Context, token string) (*ActionTask, erro
} }
func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask, bool, error) { func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask, bool, error) {
ctx, commiter, err := db.TxContext(ctx) ctx, committer, err := db.TxContext(ctx)
if err != nil { if err != nil {
return nil, false, err return nil, false, err
} }
defer commiter.Close() defer committer.Close()
e := db.GetEngine(ctx) e := db.GetEngine(ctx)
@ -322,7 +322,7 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
task.Job = job task.Job = job
if err := commiter.Commit(); err != nil { if err := committer.Commit(); err != nil {
return nil, false, err return nil, false, err
} }
@ -347,11 +347,11 @@ func UpdateTaskByState(ctx context.Context, state *runnerv1.TaskState) (*ActionT
stepStates[v.Id] = v stepStates[v.Id] = v
} }
ctx, commiter, err := db.TxContext(ctx) ctx, committer, err := db.TxContext(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer commiter.Close() defer committer.Close()
e := db.GetEngine(ctx) e := db.GetEngine(ctx)
@ -412,7 +412,7 @@ func UpdateTaskByState(ctx context.Context, state *runnerv1.TaskState) (*ActionT
} }
} }
if err := commiter.Commit(); err != nil { if err := committer.Commit(); err != nil {
return nil, err return nil, err
} }

View File

@ -13,7 +13,7 @@ import (
// ActionTasksVersion // ActionTasksVersion
// If both ownerID and repoID is zero, its scope is global. // If both ownerID and repoID is zero, its scope is global.
// If ownerID is not zero and repoID is zero, its scope is org (there is no user-level runner currrently). // If ownerID is not zero and repoID is zero, its scope is org (there is no user-level runner currently).
// If ownerID is zero and repoID is not zero, its scope is repo. // If ownerID is zero and repoID is not zero, its scope is repo.
type ActionTasksVersion struct { type ActionTasksVersion struct {
ID int64 `xorm:"pk autoincr"` ID int64 `xorm:"pk autoincr"`
@ -73,11 +73,11 @@ func increaseTasksVersionByScope(ctx context.Context, ownerID, repoID int64) err
} }
func IncreaseTaskVersion(ctx context.Context, ownerID, repoID int64) error { func IncreaseTaskVersion(ctx context.Context, ownerID, repoID int64) error {
ctx, commiter, err := db.TxContext(ctx) ctx, committer, err := db.TxContext(ctx)
if err != nil { if err != nil {
return err return err
} }
defer commiter.Close() defer committer.Close()
// 1. increase global // 1. increase global
if err := increaseTasksVersionByScope(ctx, 0, 0); err != nil { if err := increaseTasksVersionByScope(ctx, 0, 0); err != nil {
@ -101,5 +101,5 @@ func IncreaseTaskVersion(ctx context.Context, ownerID, repoID int64) error {
} }
} }
return commiter.Commit() return committer.Commit()
} }

View File

@ -1,7 +1,7 @@
- -
id: 1 id: 1
type: 0 # gitea pull request type: 0 # gitea pull request
status: 2 # mergable status: 2 # mergeable
issue_id: 2 issue_id: 2
index: 2 index: 2
head_repo_id: 1 head_repo_id: 1
@ -16,7 +16,7 @@
- -
id: 2 id: 2
type: 0 # gitea pull request type: 0 # gitea pull request
status: 2 # mergable status: 2 # mergeable
issue_id: 3 issue_id: 3
index: 3 index: 3
head_repo_id: 1 head_repo_id: 1
@ -29,7 +29,7 @@
- -
id: 3 id: 3
type: 0 # gitea pull request type: 0 # gitea pull request
status: 2 # mergable status: 2 # mergeable
issue_id: 8 issue_id: 8
index: 1 index: 1
head_repo_id: 11 head_repo_id: 11
@ -42,7 +42,7 @@
- -
id: 4 id: 4
type: 0 # gitea pull request type: 0 # gitea pull request
status: 2 # mergable status: 2 # mergeable
issue_id: 9 issue_id: 9
index: 1 index: 1
head_repo_id: 48 head_repo_id: 48
@ -55,7 +55,7 @@
- -
id: 5 # this PR is outdated (one commit behind branch1 ) id: 5 # this PR is outdated (one commit behind branch1 )
type: 0 # gitea pull request type: 0 # gitea pull request
status: 2 # mergable status: 2 # mergeable
issue_id: 11 issue_id: 11
index: 5 index: 5
head_repo_id: 1 head_repo_id: 1
@ -68,7 +68,7 @@
- -
id: 6 id: 6
type: 0 # gitea pull request type: 0 # gitea pull request
status: 2 # mergable status: 2 # mergeable
issue_id: 12 issue_id: 12
index: 2 index: 2
head_repo_id: 3 head_repo_id: 3
@ -81,7 +81,7 @@
- -
id: 7 id: 7
type: 0 # gitea pull request type: 0 # gitea pull request
status: 2 # mergable status: 2 # mergeable
issue_id: 19 issue_id: 19
index: 1 index: 1
head_repo_id: 58 head_repo_id: 58
@ -94,7 +94,7 @@
- -
id: 8 id: 8
type: 0 # gitea pull request type: 0 # gitea pull request
status: 2 # mergable status: 2 # mergeable
issue_id: 20 issue_id: 20
index: 1 index: 1
head_repo_id: 23 head_repo_id: 23
@ -103,7 +103,7 @@
- -
id: 9 id: 9
type: 0 # gitea pull request type: 0 # gitea pull request
status: 2 # mergable status: 2 # mergeable
issue_id: 21 issue_id: 21
index: 1 index: 1
head_repo_id: 60 head_repo_id: 60
@ -112,7 +112,7 @@
- -
id: 10 id: 10
type: 0 # gitea pull request type: 0 # gitea pull request
status: 2 # mergable status: 2 # mergeable
issue_id: 22 issue_id: 22
index: 1 index: 1
head_repo_id: 61 head_repo_id: 61

View File

@ -807,7 +807,7 @@ func UpdateAllowEdits(ctx context.Context, pr *PullRequest) error {
// Mergeable returns if the pullrequest is mergeable. // Mergeable returns if the pullrequest is mergeable.
func (pr *PullRequest) Mergeable(ctx context.Context) bool { func (pr *PullRequest) Mergeable(ctx context.Context) bool {
// If a pull request isn't mergable if it's: // If a pull request isn't mergeable if it's:
// - Being conflict checked. // - Being conflict checked.
// - Has a conflict. // - Has a conflict.
// - Received a error while being conflict checked. // - Received a error while being conflict checked.

View File

@ -187,8 +187,8 @@ func AddTime(ctx context.Context, user *user_model.User, issue *Issue, amount in
Issue: issue, Issue: issue,
Repo: issue.Repo, Repo: issue.Repo,
Doer: user, Doer: user,
// Content before v1.21 did store the formated string instead of seconds, // Content before v1.21 did store the formatted string instead of seconds,
// so use "|" as delimeter to mark the new format // so use "|" as delimiter to mark the new format
Content: fmt.Sprintf("|%d", amount), Content: fmt.Sprintf("|%d", amount),
Type: CommentTypeAddTimeManual, Type: CommentTypeAddTimeManual,
TimeID: t.ID, TimeID: t.ID,
@ -267,8 +267,8 @@ func DeleteIssueUserTimes(ctx context.Context, issue *Issue, user *user_model.Us
Issue: issue, Issue: issue,
Repo: issue.Repo, Repo: issue.Repo,
Doer: user, Doer: user,
// Content before v1.21 did store the formated string instead of seconds, // Content before v1.21 did store the formatted string instead of seconds,
// so use "|" as delimeter to mark the new format // so use "|" as delimiter to mark the new format
Content: fmt.Sprintf("|%d", removedTime), Content: fmt.Sprintf("|%d", removedTime),
Type: CommentTypeDeleteTimeManual, Type: CommentTypeDeleteTimeManual,
}); err != nil { }); err != nil {
@ -298,8 +298,8 @@ func DeleteTime(ctx context.Context, t *TrackedTime) error {
Issue: t.Issue, Issue: t.Issue,
Repo: t.Issue.Repo, Repo: t.Issue.Repo,
Doer: t.User, Doer: t.User,
// Content before v1.21 did store the formated string instead of seconds, // Content before v1.21 did store the formatted string instead of seconds,
// so use "|" as delimeter to mark the new format // so use "|" as delimiter to mark the new format
Content: fmt.Sprintf("|%d", t.Time), Content: fmt.Sprintf("|%d", t.Time),
Type: CommentTypeDeleteTimeManual, Type: CommentTypeDeleteTimeManual,
}); err != nil { }); err != nil {

View File

@ -4,4 +4,4 @@
package v1_17 //nolint package v1_17 //nolint
// This migration added non-ideal indices to the action table which on larger datasets slowed things down // This migration added non-ideal indices to the action table which on larger datasets slowed things down
// it has been superceded by v218.go // it has been superseded by v218.go

View File

@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/models/system" "code.gitea.io/gitea/models/system"
"code.gitea.io/gitea/modules/auth/password/hash" "code.gitea.io/gitea/modules/auth/password/hash"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -114,6 +115,7 @@ func MainTest(m *testing.M, testOpts ...*TestOptions) {
fatalTestError("Error creating test engine: %v\n", err) fatalTestError("Error creating test engine: %v\n", err)
} }
setting.IsInTesting = true
setting.AppURL = "https://try.gitea.io/" setting.AppURL = "https://try.gitea.io/"
setting.RunUser = "runuser" setting.RunUser = "runuser"
setting.SSH.User = "sshuser" setting.SSH.User = "sshuser"
@ -156,6 +158,9 @@ func MainTest(m *testing.M, testOpts ...*TestOptions) {
config.SetDynGetter(system.NewDatabaseDynKeyGetter()) config.SetDynGetter(system.NewDatabaseDynKeyGetter())
if err = cache.Init(); err != nil {
fatalTestError("cache.Init: %v\n", err)
}
if err = storage.Init(); err != nil { if err = storage.Init(); err != nil {
fatalTestError("storage.Init: %v\n", err) fatalTestError("storage.Init: %v\n", err)
} }

View File

@ -501,19 +501,19 @@ func GetUserSalt() (string, error) {
// Note: The set of characters here can safely expand without a breaking change, // Note: The set of characters here can safely expand without a breaking change,
// but characters removed from this set can cause user account linking to break // but characters removed from this set can cause user account linking to break
var ( var (
customCharsReplacement = strings.NewReplacer("Æ", "AE") customCharsReplacement = strings.NewReplacer("Æ", "AE")
removeCharsRE = regexp.MustCompile(`['´\x60]`) removeCharsRE = regexp.MustCompile("['`´]")
removeDiacriticsTransform = transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC) transformDiacritics = transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
replaceCharsHyphenRE = regexp.MustCompile(`[\s~+]`) replaceCharsHyphenRE = regexp.MustCompile(`[\s~+]`)
) )
// normalizeUserName returns a string with single-quotes and diacritics // NormalizeUserName only takes the name part if it is an email address, transforms it diacritics to ASCII characters.
// removed, and any other non-supported username characters replaced with // It returns a string with the single-quotes removed, and any other non-supported username characters are replaced with a `-` character
// a `-` character
func NormalizeUserName(s string) (string, error) { func NormalizeUserName(s string) (string, error) {
strDiacriticsRemoved, n, err := transform.String(removeDiacriticsTransform, customCharsReplacement.Replace(s)) s, _, _ = strings.Cut(s, "@")
strDiacriticsRemoved, n, err := transform.String(transformDiacritics, customCharsReplacement.Replace(s))
if err != nil { if err != nil {
return "", fmt.Errorf("Failed to normalize character `%v` in provided username `%v`", s[n], s) return "", fmt.Errorf("failed to normalize the string of provided username %q at position %d", s, n)
} }
return replaceCharsHyphenRE.ReplaceAllLiteralString(removeCharsRE.ReplaceAllLiteralString(strDiacriticsRemoved, ""), "-"), nil return replaceCharsHyphenRE.ReplaceAllLiteralString(removeCharsRE.ReplaceAllLiteralString(strDiacriticsRemoved, ""), "-"), nil
} }

View File

@ -506,15 +506,16 @@ func Test_NormalizeUserFromEmail(t *testing.T) {
Expected string Expected string
IsNormalizedValid bool IsNormalizedValid bool
}{ }{
{"test", "test", true}, {"name@example.com", "name", true},
{"test'`´name", "testname", true},
{"Sinéad.O'Connor", "Sinead.OConnor", true}, {"Sinéad.O'Connor", "Sinead.OConnor", true},
{"Æsir", "AEsir", true}, {"Æsir", "AEsir", true},
// \u00e9\u0065\u0301 {"éé", "ee", true}, // \u00e9\u0065\u0301
{"éé", "ee", true},
{"Awareness Hub", "Awareness-Hub", true}, {"Awareness Hub", "Awareness-Hub", true},
{"double__underscore", "double__underscore", false}, // We should consider squashing double non-alpha characters {"double__underscore", "double__underscore", false}, // We should consider squashing double non-alpha characters
{".bad.", ".bad.", false}, {".bad.", ".bad.", false},
{"new😀user", "new😀user", false}, // No plans to support {"new😀user", "new😀user", false}, // No plans to support
{`"quoted"`, `"quoted"`, false}, // No plans to support
} }
for _, testCase := range testCases { for _, testCase := range testCases {
normalizedName, err := user_model.NormalizeUserName(testCase.Input) normalizedName, err := user_model.NormalizeUserName(testCase.Input)

View File

@ -33,7 +33,6 @@ type ObjectFormat interface {
ComputeHash(t ObjectType, content []byte) ObjectID ComputeHash(t ObjectType, content []byte) ObjectID
} }
/* SHA1 Type */
type Sha1ObjectFormatImpl struct{} type Sha1ObjectFormatImpl struct{}
var ( var (
@ -70,14 +69,10 @@ func (h Sha1ObjectFormatImpl) ComputeHash(t ObjectType, content []byte) ObjectID
_, _ = hasher.Write([]byte(" ")) _, _ = hasher.Write([]byte(" "))
_, _ = hasher.Write([]byte(strconv.FormatInt(int64(len(content)), 10))) _, _ = hasher.Write([]byte(strconv.FormatInt(int64(len(content)), 10)))
_, _ = hasher.Write([]byte{0}) _, _ = hasher.Write([]byte{0})
_, _ = hasher.Write(content)
// HashSum generates a SHA1 for the provided hash return h.MustID(hasher.Sum(nil))
var sha1 Sha1Hash
copy(sha1[:], hasher.Sum(nil))
return &sha1
} }
/* SHA256 Type */
type Sha256ObjectFormatImpl struct{} type Sha256ObjectFormatImpl struct{}
var ( var (
@ -116,11 +111,8 @@ func (h Sha256ObjectFormatImpl) ComputeHash(t ObjectType, content []byte) Object
_, _ = hasher.Write([]byte(" ")) _, _ = hasher.Write([]byte(" "))
_, _ = hasher.Write([]byte(strconv.FormatInt(int64(len(content)), 10))) _, _ = hasher.Write([]byte(strconv.FormatInt(int64(len(content)), 10)))
_, _ = hasher.Write([]byte{0}) _, _ = hasher.Write([]byte{0})
_, _ = hasher.Write(content)
// HashSum generates a SHA256 for the provided hash return h.MustID(hasher.Sum(nil))
var sha256 Sha1Hash
copy(sha256[:], hasher.Sum(nil))
return &sha256
} }
var ( var (

View File

@ -16,7 +16,6 @@ type ObjectID interface {
Type() ObjectFormat Type() ObjectFormat
} }
/* SHA1 */
type Sha1Hash [20]byte type Sha1Hash [20]byte
func (h *Sha1Hash) String() string { func (h *Sha1Hash) String() string {
@ -40,7 +39,6 @@ func MustIDFromString(hexHash string) ObjectID {
return id return id
} }
/* SHA256 */
type Sha256Hash [32]byte type Sha256Hash [32]byte
func (h *Sha256Hash) String() string { func (h *Sha256Hash) String() string {
@ -54,7 +52,6 @@ func (h *Sha256Hash) IsZero() bool {
func (h *Sha256Hash) RawValue() []byte { return h[:] } func (h *Sha256Hash) RawValue() []byte { return h[:] }
func (*Sha256Hash) Type() ObjectFormat { return Sha256ObjectFormat } func (*Sha256Hash) Type() ObjectFormat { return Sha256ObjectFormat }
/* utility */
func NewIDFromString(hexHash string) (ObjectID, error) { func NewIDFromString(hexHash string) (ObjectID, error) {
var theObjectFormat ObjectFormat var theObjectFormat ObjectFormat
for _, objectFormat := range SupportedObjectFormats { for _, objectFormat := range SupportedObjectFormats {

View File

@ -18,4 +18,8 @@ func TestIsValidSHAPattern(t *testing.T) {
assert.False(t, h.IsValid("abc")) assert.False(t, h.IsValid("abc"))
assert.False(t, h.IsValid("123g")) assert.False(t, h.IsValid("123g"))
assert.False(t, h.IsValid("some random text")) assert.False(t, h.IsValid("some random text"))
assert.Equal(t, "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", ComputeBlobHash(Sha1ObjectFormat, nil).String())
assert.Equal(t, "2e65efe2a145dda7ee51d1741299f848e5bf752e", ComputeBlobHash(Sha1ObjectFormat, []byte("a")).String())
assert.Equal(t, "473a0f4c3be8a93681a267e3b1e9a7dcda1185436fe141f7749120a303721813", ComputeBlobHash(Sha256ObjectFormat, nil).String())
assert.Equal(t, "eb337bcee2061c5313c9a1392116b6c76039e9e30d71467ae359b36277e17dc7", ComputeBlobHash(Sha256ObjectFormat, []byte("a")).String())
} }

View File

@ -0,0 +1,32 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pipeline
import (
"fmt"
"time"
"code.gitea.io/gitea/modules/git"
)
// LFSResult represents commits found using a provided pointer file hash
type LFSResult struct {
Name string
SHA string
Summary string
When time.Time
ParentHashes []git.ObjectID
BranchName string
FullCommitName string
}
type lfsResultSlice []*LFSResult
func (a lfsResultSlice) Len() int { return len(a) }
func (a lfsResultSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a lfsResultSlice) Less(i, j int) bool { return a[j].When.After(a[i].When) }
func lfsError(msg string, err error) error {
return fmt.Errorf("LFS error occurred, %s: err: %w", msg, err)
}

View File

@ -7,12 +7,10 @@ package pipeline
import ( import (
"bufio" "bufio"
"fmt"
"io" "io"
"sort" "sort"
"strings" "strings"
"sync" "sync"
"time"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
@ -21,23 +19,6 @@ import (
"github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/plumbing/object"
) )
// LFSResult represents commits found using a provided pointer file hash
type LFSResult struct {
Name string
SHA string
Summary string
When time.Time
ParentHashes []git.ObjectID
BranchName string
FullCommitName string
}
type lfsResultSlice []*LFSResult
func (a lfsResultSlice) Len() int { return len(a) }
func (a lfsResultSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a lfsResultSlice) Less(i, j int) bool { return a[j].When.After(a[i].When) }
// FindLFSFile finds commits that contain a provided pointer file hash // FindLFSFile finds commits that contain a provided pointer file hash
func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, error) { func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, error) {
resultsMap := map[string]*LFSResult{} resultsMap := map[string]*LFSResult{}
@ -51,7 +32,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
All: true, All: true,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("Failed to get GoGit CommitsIter. Error: %w", err) return nil, lfsError("failed to get GoGit CommitsIter", err)
} }
err = commitsIter.ForEach(func(gitCommit *object.Commit) error { err = commitsIter.ForEach(func(gitCommit *object.Commit) error {
@ -85,7 +66,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
return nil return nil
}) })
if err != nil && err != io.EOF { if err != nil && err != io.EOF {
return nil, fmt.Errorf("Failure in CommitIter.ForEach: %w", err) return nil, lfsError("failure in CommitIter.ForEach", err)
} }
for _, result := range resultsMap { for _, result := range resultsMap {
@ -156,7 +137,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
select { select {
case err, has := <-errChan: case err, has := <-errChan:
if has { if has {
return nil, fmt.Errorf("Unable to obtain name for LFS files. Error: %w", err) return nil, lfsError("unable to obtain name for LFS files", err)
} }
default: default:
} }

View File

@ -8,33 +8,14 @@ package pipeline
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"fmt"
"io" "io"
"sort" "sort"
"strings" "strings"
"sync" "sync"
"time"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
) )
// LFSResult represents commits found using a provided pointer file hash
type LFSResult struct {
Name string
SHA string
Summary string
When time.Time
ParentIDs []git.ObjectID
BranchName string
FullCommitName string
}
type lfsResultSlice []*LFSResult
func (a lfsResultSlice) Len() int { return len(a) }
func (a lfsResultSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a lfsResultSlice) Less(i, j int) bool { return a[j].When.After(a[i].When) }
// FindLFSFile finds commits that contain a provided pointer file hash // FindLFSFile finds commits that contain a provided pointer file hash
func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, error) { func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, error) {
resultsMap := map[string]*LFSResult{} resultsMap := map[string]*LFSResult{}
@ -137,11 +118,11 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
n += int64(count) n += int64(count)
if bytes.Equal(binObjectID, objectID.RawValue()) { if bytes.Equal(binObjectID, objectID.RawValue()) {
result := LFSResult{ result := LFSResult{
Name: curPath + string(fname), Name: curPath + string(fname),
SHA: curCommit.ID.String(), SHA: curCommit.ID.String(),
Summary: strings.Split(strings.TrimSpace(curCommit.CommitMessage), "\n")[0], Summary: strings.Split(strings.TrimSpace(curCommit.CommitMessage), "\n")[0],
When: curCommit.Author.When, When: curCommit.Author.When,
ParentIDs: curCommit.Parents, ParentHashes: curCommit.Parents,
} }
resultsMap[curCommit.ID.String()+":"+curPath+string(fname)] = &result resultsMap[curCommit.ID.String()+":"+curPath+string(fname)] = &result
} else if string(mode) == git.EntryModeTree.String() { } else if string(mode) == git.EntryModeTree.String() {
@ -183,7 +164,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
for _, result := range resultsMap { for _, result := range resultsMap {
hasParent := false hasParent := false
for _, parentID := range result.ParentIDs { for _, parentID := range result.ParentHashes {
if _, hasParent = resultsMap[parentID.String()+":"+result.Name]; hasParent { if _, hasParent = resultsMap[parentID.String()+":"+result.Name]; hasParent {
break break
} }
@ -240,7 +221,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err
select { select {
case err, has := <-errChan: case err, has := <-errChan:
if has { if has {
return nil, fmt.Errorf("Unable to obtain name for LFS files. Error: %w", err) return nil, lfsError("unable to obtain name for LFS files", err)
} }
default: default:
} }

View File

@ -184,7 +184,7 @@ func (ref RefName) RefGroup() string {
} }
// RefType returns the simple ref type of the reference, e.g. branch, tag // RefType returns the simple ref type of the reference, e.g. branch, tag
// It's differrent from RefGroup, which is using the name of the directory under .git/refs // It's different from RefGroup, which is using the name of the directory under .git/refs
// Here we using branch but not heads, using tag but not tags // Here we using branch but not heads, using tag but not tags
func (ref RefName) RefType() string { func (ref RefName) RefType() string {
var refType string var refType string

View File

@ -134,7 +134,7 @@ func (pm *Manager) AddTypedContext(parent context.Context, description, processT
// //
// Most processes will not need to use the cancel function but there will be cases whereby you want to cancel the process but not immediately remove it from the // Most processes will not need to use the cancel function but there will be cases whereby you want to cancel the process but not immediately remove it from the
// process table. // process table.
func (pm *Manager) AddContextTimeout(parent context.Context, timeout time.Duration, description string) (ctx context.Context, cancel context.CancelFunc, finshed FinishedFunc) { func (pm *Manager) AddContextTimeout(parent context.Context, timeout time.Duration, description string) (ctx context.Context, cancel context.CancelFunc, finished FinishedFunc) {
if timeout <= 0 { if timeout <= 0 {
// it's meaningless to use timeout <= 0, and it must be a bug! so we must panic here to tell developers to make the timeout correct // it's meaningless to use timeout <= 0, and it must be a bug! so we must panic here to tell developers to make the timeout correct
panic("the timeout must be greater than zero, otherwise the context will be cancelled immediately") panic("the timeout must be greater than zero, otherwise the context will be cancelled immediately")
@ -142,9 +142,9 @@ func (pm *Manager) AddContextTimeout(parent context.Context, timeout time.Durati
ctx, cancel = context.WithTimeout(parent, timeout) ctx, cancel = context.WithTimeout(parent, timeout)
ctx, _, finshed = pm.Add(ctx, description, cancel, NormalProcessType, true) ctx, _, finished = pm.Add(ctx, description, cancel, NormalProcessType, true)
return ctx, cancel, finshed return ctx, cancel, finished
} }
// Add create a new process // Add create a new process

26
modules/session/mock.go Normal file
View File

@ -0,0 +1,26 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package session
import (
"net/http"
"gitea.com/go-chi/session"
)
type MockStore struct {
*session.MemStore
}
func (m *MockStore) Destroy(writer http.ResponseWriter, request *http.Request) error {
return nil
}
type mockStoreContextKeyStruct struct{}
var MockStoreContextKey = mockStoreContextKeyStruct{}
func NewMockStore(sid string) *MockStore {
return &MockStore{session.NewMemStore(sid)}
}

View File

@ -6,6 +6,8 @@ package session
import ( import (
"net/http" "net/http"
"code.gitea.io/gitea/modules/setting"
"gitea.com/go-chi/session" "gitea.com/go-chi/session"
) )
@ -14,6 +16,10 @@ type Store interface {
Get(any) any Get(any) any
Set(any, any) error Set(any, any) error
Delete(any) error Delete(any) error
ID() string
Release() error
Flush() error
Destroy(http.ResponseWriter, *http.Request) error
} }
// RegenerateSession regenerates the underlying session and returns the new store // RegenerateSession regenerates the underlying session and returns the new store
@ -21,8 +27,21 @@ func RegenerateSession(resp http.ResponseWriter, req *http.Request) (Store, erro
for _, f := range BeforeRegenerateSession { for _, f := range BeforeRegenerateSession {
f(resp, req) f(resp, req)
} }
s, err := session.RegenerateSession(resp, req) if setting.IsInTesting {
return s, err if store, ok := req.Context().Value(MockStoreContextKey).(*MockStore); ok {
return store, nil
}
}
return session.RegenerateSession(resp, req)
}
func GetContextSession(req *http.Request) Store {
if setting.IsInTesting {
if store, ok := req.Context().Value(MockStoreContextKey).(*MockStore); ok {
return store
}
}
return session.GetSession(req)
} }
// BeforeRegenerateSession is a list of functions that are called before a session is regenerated. // BeforeRegenerateSession is a list of functions that are called before a session is regenerated.

View File

@ -16,14 +16,10 @@ import (
type OAuth2UsernameType string type OAuth2UsernameType string
const ( const (
// OAuth2UsernameUserid oauth2 userid field will be used as gitea name OAuth2UsernameUserid OAuth2UsernameType = "userid" // use user id (sub) field as gitea's username
OAuth2UsernameUserid OAuth2UsernameType = "userid" OAuth2UsernameNickname OAuth2UsernameType = "nickname" // use nickname field
// OAuth2UsernameNickname oauth2 nickname field will be used as gitea name OAuth2UsernameEmail OAuth2UsernameType = "email" // use email field
OAuth2UsernameNickname OAuth2UsernameType = "nickname" OAuth2UsernamePreferredUsername OAuth2UsernameType = "preferred_username" // use preferred_username field
// OAuth2UsernameEmail username of oauth2 email field will be used as gitea name
OAuth2UsernameEmail OAuth2UsernameType = "email"
// OAuth2UsernameEmail username of oauth2 preferred_username field will be used as gitea name
OAuth2UsernamePreferredUsername OAuth2UsernameType = "preferred_username"
) )
func (username OAuth2UsernameType) isValid() bool { func (username OAuth2UsernameType) isValid() bool {
@ -71,8 +67,8 @@ func loadOAuth2ClientFrom(rootCfg ConfigProvider) {
OAuth2Client.EnableAutoRegistration = sec.Key("ENABLE_AUTO_REGISTRATION").MustBool() OAuth2Client.EnableAutoRegistration = sec.Key("ENABLE_AUTO_REGISTRATION").MustBool()
OAuth2Client.Username = OAuth2UsernameType(sec.Key("USERNAME").MustString(string(OAuth2UsernameNickname))) OAuth2Client.Username = OAuth2UsernameType(sec.Key("USERNAME").MustString(string(OAuth2UsernameNickname)))
if !OAuth2Client.Username.isValid() { if !OAuth2Client.Username.isValid() {
log.Warn("Username setting is not valid: '%s', will fallback to '%s'", OAuth2Client.Username, OAuth2UsernameNickname)
OAuth2Client.Username = OAuth2UsernameNickname OAuth2Client.Username = OAuth2UsernameNickname
log.Warn("[oauth2_client].USERNAME setting is invalid, falls back to %q", OAuth2Client.Username)
} }
OAuth2Client.UpdateAvatar = sec.Key("UPDATE_AVATAR").MustBool() OAuth2Client.UpdateAvatar = sec.Key("UPDATE_AVATAR").MustBool()
OAuth2Client.AccountLinking = OAuth2AccountLinkingType(sec.Key("ACCOUNT_LINKING").MustString(string(OAuth2AccountLinkingLogin))) OAuth2Client.AccountLinking = OAuth2AccountLinkingType(sec.Key("ACCOUNT_LINKING").MustString(string(OAuth2AccountLinkingLogin)))

View File

@ -49,9 +49,9 @@ func TestSubjectBodySeparator(t *testing.T) {
test("Multiple\n---\n-------\n---\nSeparators", test("Multiple\n---\n-------\n---\nSeparators",
"Multiple\n", "Multiple\n",
"\n-------\n---\nSeparators") "\n-------\n---\nSeparators")
test("Insuficient\n--\nSeparators", test("Insufficient\n--\nSeparators",
"", "",
"Insuficient\n--\nSeparators") "Insufficient\n--\nSeparators")
} }
func TestJSEscapeSafe(t *testing.T) { func TestJSEscapeSafe(t *testing.T) {

View File

@ -436,6 +436,7 @@ oauth_signin_submit = Link Account
oauth.signin.error = There was an error processing the authorization request. If this error persists, please contact the site administrator. oauth.signin.error = There was an error processing the authorization request. If this error persists, please contact the site administrator.
oauth.signin.error.access_denied = The authorization request was denied. oauth.signin.error.access_denied = The authorization request was denied.
oauth.signin.error.temporarily_unavailable = Authorization failed because the authentication server is temporarily unavailable. Please try again later. oauth.signin.error.temporarily_unavailable = Authorization failed because the authentication server is temporarily unavailable. Please try again later.
oauth_callback_unable_auto_reg = Auto Registration is enabled, but OAuth2 Provider %[1]s returned missing fields: %[2]s, unable to create an account automatically, please create or link to an account, or contact the site administrator.
openid_connect_submit = Connect openid_connect_submit = Connect
openid_connect_title = Connect to an existing account openid_connect_title = Connect to an existing account
openid_connect_desc = The chosen OpenID URI is unknown. Associate it with a new account here. openid_connect_desc = The chosen OpenID URI is unknown. Associate it with a new account here.

View File

@ -436,6 +436,7 @@ oauth_signin_submit=Vincular conta
oauth.signin.error=Ocorreu um erro durante o processamento do pedido de autorização. Se este erro persistir, contacte o administrador. oauth.signin.error=Ocorreu um erro durante o processamento do pedido de autorização. Se este erro persistir, contacte o administrador.
oauth.signin.error.access_denied=O pedido de autorização foi negado. oauth.signin.error.access_denied=O pedido de autorização foi negado.
oauth.signin.error.temporarily_unavailable=A autorização falhou porque o servidor de autenticação está temporariamente indisponível. Tente mais tarde. oauth.signin.error.temporarily_unavailable=A autorização falhou porque o servidor de autenticação está temporariamente indisponível. Tente mais tarde.
oauth_callback_unable_auto_reg=O registo automático está habilitado, mas o fornecedor OAuth2 %[1]s sinalizou campos em falta: %[2]s, por isso não foi possível criar uma conta automaticamente. Crie ou vincule uma conta ou contacte o administrador do sítio.
openid_connect_submit=Estabelecer ligação openid_connect_submit=Estabelecer ligação
openid_connect_title=Estabelecer ligação a uma conta existente openid_connect_title=Estabelecer ligação a uma conta existente
openid_connect_desc=O URI do OpenID escolhido é desconhecido. Associe-o a uma nova conta aqui. openid_connect_desc=O URI do OpenID escolhido é desconhecido. Associe-o a uma nova conta aqui.
@ -2358,7 +2359,7 @@ settings.protected_branch.delete_rule=Eliminar regra
settings.protected_branch_can_push=Permitir envios? settings.protected_branch_can_push=Permitir envios?
settings.protected_branch_can_push_yes=Pode enviar settings.protected_branch_can_push_yes=Pode enviar
settings.protected_branch_can_push_no=Não pode enviar settings.protected_branch_can_push_no=Não pode enviar
settings.branch_protection=Salvaguarda do ramo '<b>%s</b>' settings.branch_protection=Regras de salvaguarda do ramo '<b>%s</b>'
settings.protect_this_branch=Habilitar salvaguarda do ramo settings.protect_this_branch=Habilitar salvaguarda do ramo
settings.protect_this_branch_desc=Impede a eliminação e restringe envios e integrações do Git no ramo. settings.protect_this_branch_desc=Impede a eliminação e restringe envios e integrações do Git no ramo.
settings.protect_disable_push=Desabilitar envios settings.protect_disable_push=Desabilitar envios
@ -2402,7 +2403,7 @@ settings.protect_patterns=Padrões
settings.protect_protected_file_patterns=Padrões de ficheiros protegidos (separados com ponto e vírgula ';'): settings.protect_protected_file_patterns=Padrões de ficheiros protegidos (separados com ponto e vírgula ';'):
settings.protect_protected_file_patterns_desc=Ficheiros protegidos não podem ser modificados imediatamente, mesmo que o utilizador tenha direitos para adicionar, editar ou eliminar ficheiros neste ramo. Múltiplos padrões podem ser separados com ponto e vírgula (';'). Veja a documentação em <a href='https://pkg.go.dev/github.com/gobwas/glob#Compile'>github.com/gobwas/glob</a> para ver a sintaxe. Exemplos: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>. settings.protect_protected_file_patterns_desc=Ficheiros protegidos não podem ser modificados imediatamente, mesmo que o utilizador tenha direitos para adicionar, editar ou eliminar ficheiros neste ramo. Múltiplos padrões podem ser separados com ponto e vírgula (';'). Veja a documentação em <a href='https://pkg.go.dev/github.com/gobwas/glob#Compile'>github.com/gobwas/glob</a> para ver a sintaxe. Exemplos: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>.
settings.protect_unprotected_file_patterns=Padrões de ficheiros desprotegidos (separados com ponto e vírgula ';'): settings.protect_unprotected_file_patterns=Padrões de ficheiros desprotegidos (separados com ponto e vírgula ';'):
settings.protect_unprotected_file_patterns_desc=Ficheiros desprotegidos que podem ser modificados imediatamente se o utilizador tiver direitos de escrita, contornando a restrição no envio. Múltiplos padrões podem ser separados com ponto e vírgula (';'). Veja a documentação em <a href='https://pkg.go.dev/github.com/gobwas/glob#Compile'>github.com/gobwas/glob</a> para ver a sintaxe. Exemplos: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>. settings.protect_unprotected_file_patterns_desc=Ficheiros desprotegidos que podem ser modificados imediatamente se o utilizador tiver direitos de escrita, contornando a restrição no envio. Padrões múltiplos podem ser separados com ponto e vírgula (';'). Veja a documentação em <a href='https://pkg.go.dev/github.com/gobwas/glob#Compile'>github.com/gobwas/glob</a> para ver a sintaxe. Exemplos: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>.
settings.add_protected_branch=Habilitar salvaguarda settings.add_protected_branch=Habilitar salvaguarda
settings.delete_protected_branch=Desabilitar salvaguarda settings.delete_protected_branch=Desabilitar salvaguarda
settings.update_protect_branch_success=A salvaguarda do ramo "%s" foi modificada. settings.update_protect_branch_success=A salvaguarda do ramo "%s" foi modificada.
@ -2418,7 +2419,7 @@ settings.block_outdated_branch=Bloquear integração se o pedido de integração
settings.block_outdated_branch_desc=A integração não será possível quando o ramo de topo estiver abaixo do ramo base. settings.block_outdated_branch_desc=A integração não será possível quando o ramo de topo estiver abaixo do ramo base.
settings.default_branch_desc=Escolha um ramo do repositório como sendo o predefinido para pedidos de integração e cometimentos: settings.default_branch_desc=Escolha um ramo do repositório como sendo o predefinido para pedidos de integração e cometimentos:
settings.merge_style_desc=Estilos de integração settings.merge_style_desc=Estilos de integração
settings.default_merge_style_desc=Tipo de integração predefinido para pedidos de integração: settings.default_merge_style_desc=Tipo de integração predefinido
settings.choose_branch=Escolha um ramo… settings.choose_branch=Escolha um ramo…
settings.no_protected_branch=Não existem ramos protegidos. settings.no_protected_branch=Não existem ramos protegidos.
settings.edit_protected_branch=Editar settings.edit_protected_branch=Editar
@ -2788,7 +2789,7 @@ self_check=Auto-verificação
identity_access=Identidade e acesso identity_access=Identidade e acesso
users=Contas de utilizador users=Contas de utilizador
organizations=Organizações organizations=Organizações
assets=Recursos de código assets=Recursos do código-fonte
repositories=Repositórios repositories=Repositórios
hooks=Automatismos web hooks=Automatismos web
integrations=Integrações integrations=Integrações
@ -2869,14 +2870,14 @@ dashboard.mspan_structures_obtained=Estruturas MSpan obtidas
dashboard.mcache_structures_usage=Uso das estruturas MCache dashboard.mcache_structures_usage=Uso das estruturas MCache
dashboard.mcache_structures_obtained=Estruturas MCache obtidas dashboard.mcache_structures_obtained=Estruturas MCache obtidas
dashboard.profiling_bucket_hash_table_obtained=Perfil obtido da tabela de hash do balde dashboard.profiling_bucket_hash_table_obtained=Perfil obtido da tabela de hash do balde
dashboard.gc_metadata_obtained=Metadados da recolha de lixo obtidos dashboard.gc_metadata_obtained=Metadados obtidos da recolha de lixo
dashboard.other_system_allocation_obtained=Outras alocações de sistema obtidas dashboard.other_system_allocation_obtained=Outras alocações de sistema obtidas
dashboard.next_gc_recycle=Próxima reciclagem da recolha de lixo dashboard.next_gc_recycle=Próxima reciclagem da recolha de lixo
dashboard.last_gc_time=Tempo decorrido desde a última recolha de lixo dashboard.last_gc_time=Tempo decorrido desde a última recolha de lixo
dashboard.total_gc_time=Pausa total da recolha de lixo dashboard.total_gc_time=Pausa total da recolha de lixo
dashboard.total_gc_pause=Pausa total da recolha de lixo dashboard.total_gc_pause=Pausa total da recolha de lixo
dashboard.last_gc_pause=Última pausa da recolha de lixo dashboard.last_gc_pause=Última pausa da recolha de lixo
dashboard.gc_times=Tempos da recolha de lixo dashboard.gc_times=N.º de recolhas de lixo
dashboard.delete_old_actions=Eliminar todas as operações antigas da base de dados dashboard.delete_old_actions=Eliminar todas as operações antigas da base de dados
dashboard.delete_old_actions.started=Foi iniciado o processo de eliminação de todas as operações antigas da base de dados. dashboard.delete_old_actions.started=Foi iniciado o processo de eliminação de todas as operações antigas da base de dados.
dashboard.update_checker=Verificador de novas versões dashboard.update_checker=Verificador de novas versões
@ -3025,7 +3026,7 @@ auths.attribute_surname=Atributo do Sobrenome
auths.attribute_mail=Atributo do email auths.attribute_mail=Atributo do email
auths.attribute_ssh_public_key=Atributo da chave pública SSH auths.attribute_ssh_public_key=Atributo da chave pública SSH
auths.attribute_avatar=Atributo do avatar auths.attribute_avatar=Atributo do avatar
auths.attributes_in_bind=Buscar os atributos no contexto de Bind DN auths.attributes_in_bind=Buscar atributos no contexto do Bind DN
auths.allow_deactivate_all=Permitir que um resultado de pesquisa vazio desabilite todos os utilizadores auths.allow_deactivate_all=Permitir que um resultado de pesquisa vazio desabilite todos os utilizadores
auths.use_paged_search=Usar pesquisa paginada auths.use_paged_search=Usar pesquisa paginada
auths.search_page_size=Tamanho da página auths.search_page_size=Tamanho da página
@ -3224,7 +3225,7 @@ config.session_config=Configuração de sessão
config.session_provider=Fornecedor da sessão config.session_provider=Fornecedor da sessão
config.provider_config=Configuração do fornecedor config.provider_config=Configuração do fornecedor
config.cookie_name=Nome do cookie config.cookie_name=Nome do cookie
config.gc_interval_time=Intervalo da recolha do lixo config.gc_interval_time=Intervalo de tempo entre recolhas do lixo
config.session_life_time=Tempo de vida da sessão config.session_life_time=Tempo de vida da sessão
config.https_only=Apenas HTTPS config.https_only=Apenas HTTPS
config.cookie_life_time=Tempo de vida do cookie config.cookie_life_time=Tempo de vida do cookie

673
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,9 +4,9 @@
"node": ">= 18.0.0" "node": ">= 18.0.0"
}, },
"dependencies": { "dependencies": {
"@citation-js/core": "0.7.9", "@citation-js/core": "0.7.11",
"@citation-js/plugin-bibtex": "0.7.9", "@citation-js/plugin-bibtex": "0.7.11",
"@citation-js/plugin-csl": "0.7.9", "@citation-js/plugin-csl": "0.7.11",
"@citation-js/plugin-software-formats": "0.6.1", "@citation-js/plugin-software-formats": "0.6.1",
"@github/markdown-toolbar-element": "2.2.3", "@github/markdown-toolbar-element": "2.2.3",
"@github/relative-time-element": "4.4.0", "@github/relative-time-element": "4.4.0",
@ -27,23 +27,23 @@
"esbuild-loader": "4.1.0", "esbuild-loader": "4.1.0",
"escape-goat": "4.0.0", "escape-goat": "4.0.0",
"fast-glob": "3.3.2", "fast-glob": "3.3.2",
"htmx.org": "1.9.11", "htmx.org": "1.9.12",
"idiomorph": "0.3.0", "idiomorph": "0.3.0",
"jquery": "3.7.1", "jquery": "3.7.1",
"katex": "0.16.10", "katex": "0.16.10",
"license-checker-webpack-plugin": "0.2.1", "license-checker-webpack-plugin": "0.2.1",
"mermaid": "10.9.0", "mermaid": "10.9.0",
"mini-css-extract-plugin": "2.8.1", "mini-css-extract-plugin": "2.9.0",
"minimatch": "9.0.4", "minimatch": "9.0.4",
"monaco-editor": "0.47.0", "monaco-editor": "0.48.0",
"monaco-editor-webpack-plugin": "7.1.0", "monaco-editor-webpack-plugin": "7.1.0",
"pdfobject": "2.3.0", "pdfobject": "2.3.0",
"postcss": "8.4.38", "postcss": "8.4.38",
"postcss-loader": "8.1.1", "postcss-loader": "8.1.1",
"postcss-nesting": "12.1.1", "postcss-nesting": "12.1.2",
"pretty-ms": "9.0.0", "pretty-ms": "9.0.0",
"sortablejs": "1.15.2", "sortablejs": "1.15.2",
"swagger-ui-dist": "5.15.1", "swagger-ui-dist": "5.17.2",
"tailwindcss": "3.4.3", "tailwindcss": "3.4.3",
"temporal-polyfill": "0.2.4", "temporal-polyfill": "0.2.4",
"throttle-debounce": "5.0.0", "throttle-debounce": "5.0.0",
@ -53,7 +53,7 @@
"tributejs": "5.1.3", "tributejs": "5.1.3",
"uint8-to-base64": "0.2.0", "uint8-to-base64": "0.2.0",
"vanilla-colorful": "0.7.2", "vanilla-colorful": "0.7.2",
"vue": "3.4.21", "vue": "3.4.25",
"vue-bar-graph": "2.0.0", "vue-bar-graph": "2.0.0",
"vue-chartjs": "5.3.1", "vue-chartjs": "5.3.1",
"vue-loader": "17.4.2", "vue-loader": "17.4.2",
@ -66,7 +66,7 @@
"@eslint-community/eslint-plugin-eslint-comments": "4.3.0", "@eslint-community/eslint-plugin-eslint-comments": "4.3.0",
"@playwright/test": "1.43.1", "@playwright/test": "1.43.1",
"@stoplight/spectral-cli": "6.11.1", "@stoplight/spectral-cli": "6.11.1",
"@stylistic/eslint-plugin-js": "1.7.0", "@stylistic/eslint-plugin-js": "1.7.2",
"@stylistic/stylelint-plugin": "2.1.1", "@stylistic/stylelint-plugin": "2.1.1",
"@vitejs/plugin-vue": "5.0.4", "@vitejs/plugin-vue": "5.0.4",
"eslint": "8.57.0", "eslint": "8.57.0",
@ -81,20 +81,20 @@
"eslint-plugin-unicorn": "52.0.0", "eslint-plugin-unicorn": "52.0.0",
"eslint-plugin-vitest": "0.4.1", "eslint-plugin-vitest": "0.4.1",
"eslint-plugin-vitest-globals": "1.5.0", "eslint-plugin-vitest-globals": "1.5.0",
"eslint-plugin-vue": "9.24.1", "eslint-plugin-vue": "9.25.0",
"eslint-plugin-vue-scoped-css": "2.8.0", "eslint-plugin-vue-scoped-css": "2.8.0",
"eslint-plugin-wc": "2.1.0", "eslint-plugin-wc": "2.1.0",
"happy-dom": "14.7.1", "happy-dom": "14.7.1",
"markdownlint-cli": "0.39.0", "markdownlint-cli": "0.39.0",
"postcss-html": "1.6.0", "postcss-html": "1.6.0",
"stylelint": "16.3.1", "stylelint": "16.4.0",
"stylelint-declaration-block-no-ignored-properties": "2.8.0", "stylelint-declaration-block-no-ignored-properties": "2.8.0",
"stylelint-declaration-strict-value": "1.10.4", "stylelint-declaration-strict-value": "1.10.4",
"stylelint-value-no-unknown-custom-properties": "6.0.1", "stylelint-value-no-unknown-custom-properties": "6.0.1",
"svgo": "3.2.0", "svgo": "3.2.0",
"updates": "16.0.1", "updates": "16.0.1",
"vite-string-plugin": "1.1.5", "vite-string-plugin": "1.2.0",
"vitest": "1.5.0" "vitest": "1.5.2"
}, },
"browserslist": [ "browserslist": [
"defaults" "defaults"

View File

@ -301,7 +301,7 @@ func (ar artifactRoutes) uploadArtifact(ctx *ArtifactContext) {
}) })
} }
// comfirmUploadArtifact comfirm upload artifact. // comfirmUploadArtifact confirm upload artifact.
// if all chunks are uploaded, merge them to one file. // if all chunks are uploaded, merge them to one file.
func (ar artifactRoutes) comfirmUploadArtifact(ctx *ArtifactContext) { func (ar artifactRoutes) comfirmUploadArtifact(ctx *ArtifactContext) {
_, runID, ok := validateRunID(ctx) _, runID, ok := validateRunID(ctx)

View File

@ -36,7 +36,7 @@ var withRunner = connect.WithInterceptors(connect.UnaryInterceptorFunc(func(unar
uuid := request.Header().Get(uuidHeaderKey) uuid := request.Header().Get(uuidHeaderKey)
token := request.Header().Get(tokenHeaderKey) token := request.Header().Get(tokenHeaderKey)
// TODO: version will be removed from request header after Gitea 1.20 released. // TODO: version will be removed from request header after Gitea 1.20 released.
// And Gitea will not try to read version from reuqest header // And Gitea will not try to read version from request header
version := request.Header().Get(versionHeaderKey) version := request.Header().Get(versionHeaderKey)
runner, err := actions_model.GetRunnerByUUID(ctx, uuid) runner, err := actions_model.GetRunnerByUUID(ctx, uuid)
@ -53,7 +53,7 @@ var withRunner = connect.WithInterceptors(connect.UnaryInterceptorFunc(func(unar
cols := []string{"last_online"} cols := []string{"last_online"}
// TODO: version will be removed from request header after Gitea 1.20 released. // TODO: version will be removed from request header after Gitea 1.20 released.
// And Gitea will not try to read version from reuqest header // And Gitea will not try to read version from request header
version, _ = util.SplitStringAtByteN(version, 64) version, _ = util.SplitStringAtByteN(version, 64)
if !util.IsEmptyString(version) && runner.Version != version { if !util.IsEmptyString(version) && runner.Version != version {
runner.Version = version runner.Version = version

View File

@ -19,7 +19,7 @@ The package registry code is divided into multiple modules to split the function
## Models ## Models
Every package registry implementation uses the same underlaying models: Every package registry implementation uses the same underlying models:
| Model | Description | | Model | Description |
| - | - | | - | - |

View File

@ -93,6 +93,7 @@ import (
"code.gitea.io/gitea/routers/api/v1/settings" "code.gitea.io/gitea/routers/api/v1/settings"
"code.gitea.io/gitea/routers/api/v1/user" "code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/auth"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
@ -835,6 +836,34 @@ func Routes() *web.Route {
SignInRequired: setting.Service.RequireSignInView, SignInRequired: setting.Service.RequireSignInView,
})) }))
addActionsRoutes := func(
m *web.Route,
reqChecker func(ctx *context.APIContext),
act actions.API,
) {
m.Group("/actions", func() {
m.Group("/secrets", func() {
m.Get("", reqToken(), reqChecker, act.ListActionsSecrets)
m.Combo("/{secretname}").
Put(reqToken(), reqChecker, bind(api.CreateOrUpdateSecretOption{}), act.CreateOrUpdateSecret).
Delete(reqToken(), reqChecker, act.DeleteSecret)
})
m.Group("/variables", func() {
m.Get("", reqToken(), reqChecker, act.ListVariables)
m.Combo("/{variablename}").
Get(reqToken(), reqChecker, act.GetVariable).
Delete(reqToken(), reqChecker, act.DeleteVariable).
Post(reqToken(), reqChecker, bind(api.CreateVariableOption{}), act.CreateVariable).
Put(reqToken(), reqChecker, bind(api.UpdateVariableOption{}), act.UpdateVariable)
})
m.Group("/runners", func() {
m.Get("/registration-token", reqToken(), reqChecker, act.GetRegistrationToken)
})
})
}
m.Group("", func() { m.Group("", func() {
// Miscellaneous (no scope required) // Miscellaneous (no scope required)
if setting.API.EnableSwagger { if setting.API.EnableSwagger {
@ -1073,26 +1102,11 @@ func Routes() *web.Route {
m.Post("/accept", repo.AcceptTransfer) m.Post("/accept", repo.AcceptTransfer)
m.Post("/reject", repo.RejectTransfer) m.Post("/reject", repo.RejectTransfer)
}, reqToken()) }, reqToken())
m.Group("/actions", func() { addActionsRoutes(
m.Group("/secrets", func() { m,
m.Combo("/{secretname}"). reqOwner(),
Put(reqToken(), reqOwner(), bind(api.CreateOrUpdateSecretOption{}), repo.CreateOrUpdateSecret). repo.NewAction(),
Delete(reqToken(), reqOwner(), repo.DeleteSecret) )
})
m.Group("/variables", func() {
m.Get("", reqToken(), reqOwner(), repo.ListVariables)
m.Combo("/{variablename}").
Get(reqToken(), reqOwner(), repo.GetVariable).
Delete(reqToken(), reqOwner(), repo.DeleteVariable).
Post(reqToken(), reqOwner(), bind(api.CreateVariableOption{}), repo.CreateVariable).
Put(reqToken(), reqOwner(), bind(api.UpdateVariableOption{}), repo.UpdateVariable)
})
m.Group("/runners", func() {
m.Get("/registration-token", reqToken(), reqOwner(), repo.GetRegistrationToken)
})
})
m.Group("/hooks/git", func() { m.Group("/hooks/git", func() {
m.Combo("").Get(repo.ListGitHooks) m.Combo("").Get(repo.ListGitHooks)
m.Group("/{id}", func() { m.Group("/{id}", func() {
@ -1460,27 +1474,11 @@ func Routes() *web.Route {
m.Combo("/{username}").Get(reqToken(), org.IsMember). m.Combo("/{username}").Get(reqToken(), org.IsMember).
Delete(reqToken(), reqOrgOwnership(), org.DeleteMember) Delete(reqToken(), reqOrgOwnership(), org.DeleteMember)
}) })
m.Group("/actions", func() { addActionsRoutes(
m.Group("/secrets", func() { m,
m.Get("", reqToken(), reqOrgOwnership(), org.ListActionsSecrets) reqOrgOwnership(),
m.Combo("/{secretname}"). org.NewAction(),
Put(reqToken(), reqOrgOwnership(), bind(api.CreateOrUpdateSecretOption{}), org.CreateOrUpdateSecret). )
Delete(reqToken(), reqOrgOwnership(), org.DeleteSecret)
})
m.Group("/variables", func() {
m.Get("", reqToken(), reqOrgOwnership(), org.ListVariables)
m.Combo("/{variablename}").
Get(reqToken(), reqOrgOwnership(), org.GetVariable).
Delete(reqToken(), reqOrgOwnership(), org.DeleteVariable).
Post(reqToken(), reqOrgOwnership(), bind(api.CreateVariableOption{}), org.CreateVariable).
Put(reqToken(), reqOrgOwnership(), bind(api.UpdateVariableOption{}), org.UpdateVariable)
})
m.Group("/runners", func() {
m.Get("/registration-token", reqToken(), reqOrgOwnership(), org.GetRegistrationToken)
})
})
m.Group("/public_members", func() { m.Group("/public_members", func() {
m.Get("", org.ListPublicMembers) m.Get("", org.ListPublicMembers)
m.Combo("/{username}").Get(org.IsPublicMember). m.Combo("/{username}").Get(org.IsPublicMember).

View File

@ -9,16 +9,188 @@ import (
actions_model "code.gitea.io/gitea/models/actions" actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
secret_model "code.gitea.io/gitea/models/secret"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/shared"
"code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/routers/api/v1/utils"
actions_service "code.gitea.io/gitea/services/actions" actions_service "code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
secret_service "code.gitea.io/gitea/services/secrets"
) )
// ListActionsSecrets list an organization's actions secrets
func (Action) ListActionsSecrets(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/actions/secrets organization orgListActionsSecrets
// ---
// summary: List an organization's actions secrets
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/SecretList"
// "404":
// "$ref": "#/responses/notFound"
opts := &secret_model.FindSecretsOptions{
OwnerID: ctx.Org.Organization.ID,
ListOptions: utils.GetListOptions(ctx),
}
secrets, count, err := db.FindAndCount[secret_model.Secret](ctx, opts)
if err != nil {
ctx.InternalServerError(err)
return
}
apiSecrets := make([]*api.Secret, len(secrets))
for k, v := range secrets {
apiSecrets[k] = &api.Secret{
Name: v.Name,
Created: v.CreatedUnix.AsTime(),
}
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiSecrets)
}
// create or update one secret of the organization
func (Action) CreateOrUpdateSecret(ctx *context.APIContext) {
// swagger:operation PUT /orgs/{org}/actions/secrets/{secretname} organization updateOrgSecret
// ---
// summary: Create or Update a secret value in an organization
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of organization
// type: string
// required: true
// - name: secretname
// in: path
// description: name of the secret
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateOrUpdateSecretOption"
// responses:
// "201":
// description: response when creating a secret
// "204":
// description: response when updating a secret
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption)
_, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Org.Organization.ID, 0, ctx.Params("secretname"), opt.Data)
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "CreateOrUpdateSecret", err)
} else if errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusNotFound, "CreateOrUpdateSecret", err)
} else {
ctx.Error(http.StatusInternalServerError, "CreateOrUpdateSecret", err)
}
return
}
if created {
ctx.Status(http.StatusCreated)
} else {
ctx.Status(http.StatusNoContent)
}
}
// DeleteSecret delete one secret of the organization
func (Action) DeleteSecret(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/actions/secrets/{secretname} organization deleteOrgSecret
// ---
// summary: Delete a secret in an organization
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of organization
// type: string
// required: true
// - name: secretname
// in: path
// description: name of the secret
// type: string
// required: true
// responses:
// "204":
// description: delete one secret of the organization
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
err := secret_service.DeleteSecretByName(ctx, ctx.Org.Organization.ID, 0, ctx.Params("secretname"))
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "DeleteSecret", err)
} else if errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusNotFound, "DeleteSecret", err)
} else {
ctx.Error(http.StatusInternalServerError, "DeleteSecret", err)
}
return
}
ctx.Status(http.StatusNoContent)
}
// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization
// GetRegistrationToken returns the token to register org runners
func (Action) GetRegistrationToken(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/actions/runners/registration-token organization orgGetRunnerRegistrationToken
// ---
// summary: Get an organization's actions runner registration token
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/RegistrationToken"
shared.GetRegistrationToken(ctx, ctx.Org.Organization.ID, 0)
}
// ListVariables list org-level variables // ListVariables list org-level variables
func ListVariables(ctx *context.APIContext) { func (Action) ListVariables(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/actions/variables organization getOrgVariablesList // swagger:operation GET /orgs/{org}/actions/variables organization getOrgVariablesList
// --- // ---
// summary: Get an org-level variables list // summary: Get an org-level variables list
@ -70,7 +242,7 @@ func ListVariables(ctx *context.APIContext) {
} }
// GetVariable get an org-level variable // GetVariable get an org-level variable
func GetVariable(ctx *context.APIContext) { func (Action) GetVariable(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/actions/variables/{variablename} organization getOrgVariable // swagger:operation GET /orgs/{org}/actions/variables/{variablename} organization getOrgVariable
// --- // ---
// summary: Get an org-level variable // summary: Get an org-level variable
@ -119,7 +291,7 @@ func GetVariable(ctx *context.APIContext) {
} }
// DeleteVariable delete an org-level variable // DeleteVariable delete an org-level variable
func DeleteVariable(ctx *context.APIContext) { func (Action) DeleteVariable(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/actions/variables/{variablename} organization deleteOrgVariable // swagger:operation DELETE /orgs/{org}/actions/variables/{variablename} organization deleteOrgVariable
// --- // ---
// summary: Delete an org-level variable // summary: Delete an org-level variable
@ -163,7 +335,7 @@ func DeleteVariable(ctx *context.APIContext) {
} }
// CreateVariable create an org-level variable // CreateVariable create an org-level variable
func CreateVariable(ctx *context.APIContext) { func (Action) CreateVariable(ctx *context.APIContext) {
// swagger:operation POST /orgs/{org}/actions/variables/{variablename} organization createOrgVariable // swagger:operation POST /orgs/{org}/actions/variables/{variablename} organization createOrgVariable
// --- // ---
// summary: Create an org-level variable // summary: Create an org-level variable
@ -227,7 +399,7 @@ func CreateVariable(ctx *context.APIContext) {
} }
// UpdateVariable update an org-level variable // UpdateVariable update an org-level variable
func UpdateVariable(ctx *context.APIContext) { func (Action) UpdateVariable(ctx *context.APIContext) {
// swagger:operation PUT /orgs/{org}/actions/variables/{variablename} organization updateOrgVariable // swagger:operation PUT /orgs/{org}/actions/variables/{variablename} organization updateOrgVariable
// --- // ---
// summary: Update an org-level variable // summary: Update an org-level variable
@ -289,3 +461,13 @@ func UpdateVariable(ctx *context.APIContext) {
ctx.Status(http.StatusNoContent) ctx.Status(http.StatusNoContent)
} }
var _ actions_service.API = new(Action)
// Action implements actions_service.API
type Action struct{}
// NewAction creates a new Action service
func NewAction() actions_service.API {
return Action{}
}

View File

@ -1,31 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package org
import (
"code.gitea.io/gitea/routers/api/v1/shared"
"code.gitea.io/gitea/services/context"
)
// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization
// GetRegistrationToken returns the token to register org runners
func GetRegistrationToken(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/actions/runners/registration-token organization orgGetRunnerRegistrationToken
// ---
// summary: Get an organization's actions runner registration token
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/RegistrationToken"
shared.GetRegistrationToken(ctx, ctx.Org.Organization.ID, 0)
}

View File

@ -1,166 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package org
import (
"errors"
"net/http"
"code.gitea.io/gitea/models/db"
secret_model "code.gitea.io/gitea/models/secret"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
secret_service "code.gitea.io/gitea/services/secrets"
)
// ListActionsSecrets list an organization's actions secrets
func ListActionsSecrets(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/actions/secrets organization orgListActionsSecrets
// ---
// summary: List an organization's actions secrets
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/SecretList"
// "404":
// "$ref": "#/responses/notFound"
opts := &secret_model.FindSecretsOptions{
OwnerID: ctx.Org.Organization.ID,
ListOptions: utils.GetListOptions(ctx),
}
secrets, count, err := db.FindAndCount[secret_model.Secret](ctx, opts)
if err != nil {
ctx.InternalServerError(err)
return
}
apiSecrets := make([]*api.Secret, len(secrets))
for k, v := range secrets {
apiSecrets[k] = &api.Secret{
Name: v.Name,
Created: v.CreatedUnix.AsTime(),
}
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiSecrets)
}
// create or update one secret of the organization
func CreateOrUpdateSecret(ctx *context.APIContext) {
// swagger:operation PUT /orgs/{org}/actions/secrets/{secretname} organization updateOrgSecret
// ---
// summary: Create or Update a secret value in an organization
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of organization
// type: string
// required: true
// - name: secretname
// in: path
// description: name of the secret
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateOrUpdateSecretOption"
// responses:
// "201":
// description: response when creating a secret
// "204":
// description: response when updating a secret
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
opt := web.GetForm(ctx).(*api.CreateOrUpdateSecretOption)
_, created, err := secret_service.CreateOrUpdateSecret(ctx, ctx.Org.Organization.ID, 0, ctx.Params("secretname"), opt.Data)
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "CreateOrUpdateSecret", err)
} else if errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusNotFound, "CreateOrUpdateSecret", err)
} else {
ctx.Error(http.StatusInternalServerError, "CreateOrUpdateSecret", err)
}
return
}
if created {
ctx.Status(http.StatusCreated)
} else {
ctx.Status(http.StatusNoContent)
}
}
// DeleteSecret delete one secret of the organization
func DeleteSecret(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/actions/secrets/{secretname} organization deleteOrgSecret
// ---
// summary: Delete a secret in an organization
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of organization
// type: string
// required: true
// - name: secretname
// in: path
// description: name of the secret
// type: string
// required: true
// responses:
// "204":
// description: delete one secret of the organization
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
err := secret_service.DeleteSecretByName(ctx, ctx.Org.Organization.ID, 0, ctx.Params("secretname"))
if err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "DeleteSecret", err)
} else if errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusNotFound, "DeleteSecret", err)
} else {
ctx.Error(http.StatusInternalServerError, "DeleteSecret", err)
}
return
}
ctx.Status(http.StatusNoContent)
}

View File

@ -9,17 +9,76 @@ import (
actions_model "code.gitea.io/gitea/models/actions" actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
secret_model "code.gitea.io/gitea/models/secret"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/shared"
"code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/routers/api/v1/utils"
actions_service "code.gitea.io/gitea/services/actions" actions_service "code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
secret_service "code.gitea.io/gitea/services/secrets" secret_service "code.gitea.io/gitea/services/secrets"
) )
// ListActionsSecrets list an repo's actions secrets
func (Action) ListActionsSecrets(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/secrets repository repoListActionsSecrets
// ---
// summary: List an repo's actions secrets
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repository
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repository
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/SecretList"
// "404":
// "$ref": "#/responses/notFound"
repo := ctx.Repo.Repository
opts := &secret_model.FindSecretsOptions{
RepoID: repo.ID,
ListOptions: utils.GetListOptions(ctx),
}
secrets, count, err := db.FindAndCount[secret_model.Secret](ctx, opts)
if err != nil {
ctx.InternalServerError(err)
return
}
apiSecrets := make([]*api.Secret, len(secrets))
for k, v := range secrets {
apiSecrets[k] = &api.Secret{
Name: v.Name,
Created: v.CreatedUnix.AsTime(),
}
}
ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, apiSecrets)
}
// create or update one secret of the repository // create or update one secret of the repository
func CreateOrUpdateSecret(ctx *context.APIContext) { func (Action) CreateOrUpdateSecret(ctx *context.APIContext) {
// swagger:operation PUT /repos/{owner}/{repo}/actions/secrets/{secretname} repository updateRepoSecret // swagger:operation PUT /repos/{owner}/{repo}/actions/secrets/{secretname} repository updateRepoSecret
// --- // ---
// summary: Create or Update a secret value in a repository // summary: Create or Update a secret value in a repository
@ -82,7 +141,7 @@ func CreateOrUpdateSecret(ctx *context.APIContext) {
} }
// DeleteSecret delete one secret of the repository // DeleteSecret delete one secret of the repository
func DeleteSecret(ctx *context.APIContext) { func (Action) DeleteSecret(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/actions/secrets/{secretname} repository deleteRepoSecret // swagger:operation DELETE /repos/{owner}/{repo}/actions/secrets/{secretname} repository deleteRepoSecret
// --- // ---
// summary: Delete a secret in a repository // summary: Delete a secret in a repository
@ -133,7 +192,7 @@ func DeleteSecret(ctx *context.APIContext) {
} }
// GetVariable get a repo-level variable // GetVariable get a repo-level variable
func GetVariable(ctx *context.APIContext) { func (Action) GetVariable(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/variables/{variablename} repository getRepoVariable // swagger:operation GET /repos/{owner}/{repo}/actions/variables/{variablename} repository getRepoVariable
// --- // ---
// summary: Get a repo-level variable // summary: Get a repo-level variable
@ -186,7 +245,7 @@ func GetVariable(ctx *context.APIContext) {
} }
// DeleteVariable delete a repo-level variable // DeleteVariable delete a repo-level variable
func DeleteVariable(ctx *context.APIContext) { func (Action) DeleteVariable(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/actions/variables/{variablename} repository deleteRepoVariable // swagger:operation DELETE /repos/{owner}/{repo}/actions/variables/{variablename} repository deleteRepoVariable
// --- // ---
// summary: Delete a repo-level variable // summary: Delete a repo-level variable
@ -235,7 +294,7 @@ func DeleteVariable(ctx *context.APIContext) {
} }
// CreateVariable create a repo-level variable // CreateVariable create a repo-level variable
func CreateVariable(ctx *context.APIContext) { func (Action) CreateVariable(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/actions/variables/{variablename} repository createRepoVariable // swagger:operation POST /repos/{owner}/{repo}/actions/variables/{variablename} repository createRepoVariable
// --- // ---
// summary: Create a repo-level variable // summary: Create a repo-level variable
@ -302,7 +361,7 @@ func CreateVariable(ctx *context.APIContext) {
} }
// UpdateVariable update a repo-level variable // UpdateVariable update a repo-level variable
func UpdateVariable(ctx *context.APIContext) { func (Action) UpdateVariable(ctx *context.APIContext) {
// swagger:operation PUT /repos/{owner}/{repo}/actions/variables/{variablename} repository updateRepoVariable // swagger:operation PUT /repos/{owner}/{repo}/actions/variables/{variablename} repository updateRepoVariable
// --- // ---
// summary: Update a repo-level variable // summary: Update a repo-level variable
@ -369,7 +428,7 @@ func UpdateVariable(ctx *context.APIContext) {
} }
// ListVariables list repo-level variables // ListVariables list repo-level variables
func ListVariables(ctx *context.APIContext) { func (Action) ListVariables(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/actions/variables repository getRepoVariablesList // swagger:operation GET /repos/{owner}/{repo}/actions/variables repository getRepoVariablesList
// --- // ---
// summary: Get repo-level variables list // summary: Get repo-level variables list
@ -423,3 +482,38 @@ func ListVariables(ctx *context.APIContext) {
ctx.SetTotalCountHeader(count) ctx.SetTotalCountHeader(count)
ctx.JSON(http.StatusOK, variables) ctx.JSON(http.StatusOK, variables)
} }
// GetRegistrationToken returns the token to register repo runners
func (Action) GetRegistrationToken(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/runners/registration-token repository repoGetRunnerRegistrationToken
// ---
// summary: Get a repository's actions runner registration token
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/RegistrationToken"
shared.GetRegistrationToken(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID)
}
var _ actions_service.API = new(Action)
// Action implements actions_service.API
type Action struct{}
// NewAction creates a new Action service
func NewAction() actions_service.API {
return Action{}
}

View File

@ -4,6 +4,7 @@
package repo package repo
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
@ -372,7 +373,11 @@ func CreatePullReview(ctx *context.APIContext) {
// create review and associate all pending review comments // create review and associate all pending review comments
review, _, err := pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, opts.CommitID, nil) review, _, err := pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, opts.CommitID, nil)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "SubmitReview", err) if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) {
ctx.Error(http.StatusUnprocessableEntity, "", err)
} else {
ctx.Error(http.StatusInternalServerError, "SubmitReview", err)
}
return return
} }
@ -460,7 +465,11 @@ func SubmitPullReview(ctx *context.APIContext) {
// create review and associate all pending review comments // create review and associate all pending review comments
review, _, err = pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, headCommitID, nil) review, _, err = pull_service.SubmitReview(ctx, ctx.Doer, ctx.Repo.GitRepo, pr.Issue, reviewType, opts.Body, headCommitID, nil)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "SubmitReview", err) if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) {
ctx.Error(http.StatusUnprocessableEntity, "", err)
} else {
ctx.Error(http.StatusInternalServerError, "SubmitReview", err)
}
return return
} }

View File

@ -1,34 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"code.gitea.io/gitea/routers/api/v1/shared"
"code.gitea.io/gitea/services/context"
)
// GetRegistrationToken returns the token to register repo runners
func GetRegistrationToken(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/runners/registration-token repository repoGetRunnerRegistrationToken
// ---
// summary: Get a repository's actions runner registration token
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/RegistrationToken"
shared.GetRegistrationToken(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID)
}

View File

@ -12,7 +12,7 @@ import (
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
) )
// RegistrationToken is response related to registeration token // RegistrationToken is response related to registration token
// swagger:response RegistrationToken // swagger:response RegistrationToken
type RegistrationToken struct { type RegistrationToken struct {
Token string `json:"token"` Token string `json:"token"`

View File

@ -5,6 +5,7 @@ package routers
import ( import (
"context" "context"
"net/http"
"reflect" "reflect"
"runtime" "runtime"
@ -25,6 +26,7 @@ import (
"code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/routing"
actions_router "code.gitea.io/gitea/routers/api/actions" actions_router "code.gitea.io/gitea/routers/api/actions"
packages_router "code.gitea.io/gitea/routers/api/packages" packages_router "code.gitea.io/gitea/routers/api/packages"
apiv1 "code.gitea.io/gitea/routers/api/v1" apiv1 "code.gitea.io/gitea/routers/api/v1"
@ -202,5 +204,9 @@ func NormalRoutes() *web.Route {
r.Mount(prefix, actions_router.ArtifactsV4Routes(prefix)) r.Mount(prefix, actions_router.ArtifactsV4Routes(prefix))
} }
r.NotFound(func(w http.ResponseWriter, req *http.Request) {
routing.UpdateFuncInfo(req.Context(), routing.GetFuncInfo(http.NotFound, "GlobalNotFound"))
http.NotFound(w, req)
})
return r return r
} }

View File

@ -359,7 +359,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
}) })
return return
} }
log.Error("Unable to check if mergable: protected branch %s in %-v and pr #%d. Error: %v", ctx.opts.UserID, branchName, repo, pr.Index, err) log.Error("Unable to check if mergeable: protected branch %s in %-v and pr #%d. Error: %v", ctx.opts.UserID, branchName, repo, pr.Index, err)
ctx.JSON(http.StatusInternalServerError, private.Response{ ctx.JSON(http.StatusInternalServerError, private.Response{
Err: fmt.Sprintf("Unable to get status of pull request %d. Error: %v", ctx.opts.PullRequestID, err), Err: fmt.Sprintf("Unable to get status of pull request %d. Error: %v", ctx.opts.PullRequestID, err),
}) })

View File

@ -30,7 +30,7 @@ func Organizations(ctx *context.Context) {
explore.RenderUserSearch(ctx, &user_model.SearchUserOptions{ explore.RenderUserSearch(ctx, &user_model.SearchUserOptions{
Actor: ctx.Doer, Actor: ctx.Doer,
Type: user_model.UserTypeOrganization, Type: user_model.UserTypeOrganization,
IncludeReserved: true, // administrator needs to list all acounts include reserved IncludeReserved: true, // administrator needs to list all accounts include reserved
ListOptions: db.ListOptions{ ListOptions: db.ListOptions{
PageSize: setting.UI.Admin.OrgPagingNum, PageSize: setting.UI.Admin.OrgPagingNum,
}, },

View File

@ -81,7 +81,7 @@ func Users(ctx *context.Context) {
IsRestricted: util.OptionalBoolParse(statusFilterMap["is_restricted"]), IsRestricted: util.OptionalBoolParse(statusFilterMap["is_restricted"]),
IsTwoFactorEnabled: util.OptionalBoolParse(statusFilterMap["is_2fa_enabled"]), IsTwoFactorEnabled: util.OptionalBoolParse(statusFilterMap["is_2fa_enabled"]),
IsProhibitLogin: util.OptionalBoolParse(statusFilterMap["is_prohibit_login"]), IsProhibitLogin: util.OptionalBoolParse(statusFilterMap["is_prohibit_login"]),
IncludeReserved: true, // administrator needs to list all acounts include reserved, bot, remote ones IncludeReserved: true, // administrator needs to list all accounts include reserved, bot, remote ones
ExtraParamStrings: extraParamStrings, ExtraParamStrings: extraParamStrings,
}, tplUsers) }, tplUsers)
} }

View File

@ -382,17 +382,17 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
return setting.AppSubURL + "/" return setting.AppSubURL + "/"
} }
func getUserName(gothUser *goth.User) (string, error) { // extractUserNameFromOAuth2 tries to extract a normalized username from the given OAuth2 user.
// It returns ("", nil) if the required field doesn't exist.
func extractUserNameFromOAuth2(gothUser *goth.User) (string, error) {
switch setting.OAuth2Client.Username { switch setting.OAuth2Client.Username {
case setting.OAuth2UsernameEmail: case setting.OAuth2UsernameEmail:
return user_model.NormalizeUserName(strings.Split(gothUser.Email, "@")[0]) return user_model.NormalizeUserName(gothUser.Email)
case setting.OAuth2UsernamePreferredUsername: case setting.OAuth2UsernamePreferredUsername:
preferredUsername, exists := gothUser.RawData["preferred_username"] if preferredUsername, ok := gothUser.RawData["preferred_username"].(string); ok {
if exists { return user_model.NormalizeUserName(preferredUsername)
return user_model.NormalizeUserName(preferredUsername.(string))
} else {
return "", fmt.Errorf("preferred_username is missing in received user data but configured as username source for user_id %q. Check if OPENID_CONNECT_SCOPES contains profile", gothUser.UserID)
} }
return "", nil
case setting.OAuth2UsernameNickname: case setting.OAuth2UsernameNickname:
return user_model.NormalizeUserName(gothUser.NickName) return user_model.NormalizeUserName(gothUser.NickName)
default: // OAuth2UsernameUserid default: // OAuth2UsernameUserid

View File

@ -8,12 +8,31 @@ import (
"net/url" "net/url"
"testing" "testing"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/auth/source/oauth2"
"code.gitea.io/gitea/services/contexttest" "code.gitea.io/gitea/services/contexttest"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func addOAuth2Source(t *testing.T, authName string, cfg oauth2.Source) {
cfg.Provider = util.IfZero(cfg.Provider, "gitea")
err := auth_model.CreateSource(db.DefaultContext, &auth_model.Source{
Type: auth_model.OAuth2,
Name: authName,
IsActive: true,
Cfg: &cfg,
})
assert.NoError(t, err)
}
func TestUserLogin(t *testing.T) { func TestUserLogin(t *testing.T) {
ctx, resp := contexttest.MockContext(t, "/user/login") ctx, resp := contexttest.MockContext(t, "/user/login")
SignIn(ctx) SignIn(ctx)
@ -41,3 +60,24 @@ func TestUserLogin(t *testing.T) {
SignIn(ctx) SignIn(ctx)
assert.Equal(t, "/", test.RedirectURL(resp)) assert.Equal(t, "/", test.RedirectURL(resp))
} }
func TestSignUpOAuth2ButMissingFields(t *testing.T) {
defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)()
defer test.MockVariableValue(&gothic.CompleteUserAuth, func(res http.ResponseWriter, req *http.Request) (goth.User, error) {
return goth.User{Provider: "dummy-auth-source", UserID: "dummy-user"}, nil
})()
addOAuth2Source(t, "dummy-auth-source", oauth2.Source{})
mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockStore("dummy-sid")}
ctx, resp := contexttest.MockContext(t, "/user/oauth2/dummy-auth-source/callback?code=dummy-code", mockOpt)
ctx.SetParams("provider", "dummy-auth-source")
SignInOAuthCallback(ctx)
assert.Equal(t, http.StatusSeeOther, resp.Code)
assert.Equal(t, "/user/link_account", test.RedirectURL(resp))
// then the user will be redirected to the link account page, and see a message about the missing fields
ctx, _ = contexttest.MockContext(t, "/user/link_account", mockOpt)
LinkAccount(ctx)
assert.EqualValues(t, "auth.oauth_callback_unable_auto_reg:dummy-auth-source,email", ctx.Data["AutoRegistrationFailedPrompt"])
}

View File

@ -48,23 +48,27 @@ func LinkAccount(ctx *context.Context) {
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin" ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup" ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
gothUser := ctx.Session.Get("linkAccountGothUser") gothUser, ok := ctx.Session.Get("linkAccountGothUser").(goth.User)
if gothUser == nil { if !ok {
ctx.ServerError("UserSignIn", errors.New("not in LinkAccount session")) // no account in session, so just redirect to the login page, then the user could restart the process
ctx.Redirect(setting.AppSubURL + "/user/login")
return return
} }
gu, _ := gothUser.(goth.User) if missingFields, ok := gothUser.RawData["__giteaAutoRegMissingFields"].([]string); ok {
uname, err := getUserName(&gu) ctx.Data["AutoRegistrationFailedPrompt"] = ctx.Tr("auth.oauth_callback_unable_auto_reg", gothUser.Provider, strings.Join(missingFields, ","))
}
uname, err := extractUserNameFromOAuth2(&gothUser)
if err != nil { if err != nil {
ctx.ServerError("UserSignIn", err) ctx.ServerError("UserSignIn", err)
return return
} }
email := gu.Email email := gothUser.Email
ctx.Data["user_name"] = uname ctx.Data["user_name"] = uname
ctx.Data["email"] = email ctx.Data["email"] = email
if len(email) != 0 { if email != "" {
u, err := user_model.GetUserByEmail(ctx, email) u, err := user_model.GetUserByEmail(ctx, email)
if err != nil && !user_model.IsErrUserNotExist(err) { if err != nil && !user_model.IsErrUserNotExist(err) {
ctx.ServerError("UserSignIn", err) ctx.ServerError("UserSignIn", err)
@ -73,7 +77,7 @@ func LinkAccount(ctx *context.Context) {
if u != nil { if u != nil {
ctx.Data["user_exists"] = true ctx.Data["user_exists"] = true
} }
} else if len(uname) != 0 { } else if uname != "" {
u, err := user_model.GetUserByName(ctx, uname) u, err := user_model.GetUserByName(ctx, uname)
if err != nil && !user_model.IsErrUserNotExist(err) { if err != nil && !user_model.IsErrUserNotExist(err) {
ctx.ServerError("UserSignIn", err) ctx.ServerError("UserSignIn", err)

View File

@ -934,7 +934,7 @@ func SignInOAuthCallback(ctx *context.Context) {
if u == nil { if u == nil {
if ctx.Doer != nil { if ctx.Doer != nil {
// attach user to already logged in user // attach user to the current signed-in user
err = externalaccount.LinkAccountToUser(ctx, ctx.Doer, gothUser) err = externalaccount.LinkAccountToUser(ctx, ctx.Doer, gothUser)
if err != nil { if err != nil {
ctx.ServerError("UserLinkAccount", err) ctx.ServerError("UserLinkAccount", err)
@ -952,23 +952,32 @@ func SignInOAuthCallback(ctx *context.Context) {
if gothUser.Email == "" { if gothUser.Email == "" {
missingFields = append(missingFields, "email") missingFields = append(missingFields, "email")
} }
if setting.OAuth2Client.Username == setting.OAuth2UsernameNickname && gothUser.NickName == "" { uname, err := extractUserNameFromOAuth2(&gothUser)
missingFields = append(missingFields, "nickname")
}
if len(missingFields) > 0 {
log.Error("OAuth2 Provider %s returned empty or missing fields: %s", authSource.Name, missingFields)
if authSource.IsOAuth2() && authSource.Cfg.(*oauth2.Source).Provider == "openidConnect" {
log.Error("You may need to change the 'OPENID_CONNECT_SCOPES' setting to request all required fields")
}
err = fmt.Errorf("OAuth2 Provider %s returned empty or missing fields: %s", authSource.Name, missingFields)
ctx.ServerError("CreateUser", err)
return
}
uname, err := getUserName(&gothUser)
if err != nil { if err != nil {
ctx.ServerError("UserSignIn", err) ctx.ServerError("UserSignIn", err)
return return
} }
if uname == "" {
if setting.OAuth2Client.Username == setting.OAuth2UsernameNickname {
missingFields = append(missingFields, "nickname")
} else if setting.OAuth2Client.Username == setting.OAuth2UsernamePreferredUsername {
missingFields = append(missingFields, "preferred_username")
} // else: "UserID" and "Email" have been handled above separately
}
if len(missingFields) > 0 {
log.Error(`OAuth2 auto registration (ENABLE_AUTO_REGISTRATION) is enabled but OAuth2 provider %q doesn't return required fields: %s. `+
`Suggest to: disable auto registration, or make OPENID_CONNECT_SCOPES (for OpenIDConnect) / Authentication Source Scopes (for Admin panel) to request all required fields, and the fields shouldn't be empty.`,
authSource.Name, strings.Join(missingFields, ","))
// The RawData is the only way to pass the missing fields to the another page at the moment, other ways all have various problems:
// by session or cookie: difficult to clean or reset; by URL: could be injected with uncontrollable content; by ctx.Flash: the link_account page is a mess ...
// Since the RawData is for the provider's data, so we need to use our own prefix here to avoid conflict.
if gothUser.RawData == nil {
gothUser.RawData = make(map[string]any)
}
gothUser.RawData["__giteaAutoRegMissingFields"] = missingFields
showLinkingLogin(ctx, gothUser)
return
}
u = &user_model.User{ u = &user_model.User{
Name: uname, Name: uname,
FullName: gothUser.Name, FullName: gothUser.Name,

View File

@ -67,6 +67,9 @@ type ViewResponse struct {
CanRerun bool `json:"canRerun"` CanRerun bool `json:"canRerun"`
CanDeleteArtifact bool `json:"canDeleteArtifact"` CanDeleteArtifact bool `json:"canDeleteArtifact"`
Done bool `json:"done"` Done bool `json:"done"`
WorkflowID string `json:"workflowID"`
WorkflowLink string `json:"workflowLink"`
IsSchedule bool `json:"isSchedule"`
Jobs []*ViewJob `json:"jobs"` Jobs []*ViewJob `json:"jobs"`
Commit ViewCommit `json:"commit"` Commit ViewCommit `json:"commit"`
} `json:"run"` } `json:"run"`
@ -90,12 +93,10 @@ type ViewJob struct {
} }
type ViewCommit struct { type ViewCommit struct {
LocaleCommit string `json:"localeCommit"` ShortSha string `json:"shortSHA"`
LocalePushedBy string `json:"localePushedBy"` Link string `json:"link"`
ShortSha string `json:"shortSHA"` Pusher ViewUser `json:"pusher"`
Link string `json:"link"` Branch ViewBranch `json:"branch"`
Pusher ViewUser `json:"pusher"`
Branch ViewBranch `json:"branch"`
} }
type ViewUser struct { type ViewUser struct {
@ -151,6 +152,9 @@ func ViewPost(ctx *context_module.Context) {
resp.State.Run.CanRerun = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions) resp.State.Run.CanRerun = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
resp.State.Run.CanDeleteArtifact = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions) resp.State.Run.CanDeleteArtifact = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
resp.State.Run.Done = run.Status.IsDone() resp.State.Run.Done = run.Status.IsDone()
resp.State.Run.WorkflowID = run.WorkflowID
resp.State.Run.WorkflowLink = run.WorkflowLink()
resp.State.Run.IsSchedule = run.IsSchedule()
resp.State.Run.Jobs = make([]*ViewJob, 0, len(jobs)) // marshal to '[]' instead fo 'null' in json resp.State.Run.Jobs = make([]*ViewJob, 0, len(jobs)) // marshal to '[]' instead fo 'null' in json
resp.State.Run.Status = run.Status.String() resp.State.Run.Status = run.Status.String()
for _, v := range jobs { for _, v := range jobs {
@ -172,12 +176,10 @@ func ViewPost(ctx *context_module.Context) {
Link: run.RefLink(), Link: run.RefLink(),
} }
resp.State.Run.Commit = ViewCommit{ resp.State.Run.Commit = ViewCommit{
LocaleCommit: ctx.Locale.TrString("actions.runs.commit"), ShortSha: base.ShortSha(run.CommitSHA),
LocalePushedBy: ctx.Locale.TrString("actions.runs.pushed_by"), Link: fmt.Sprintf("%s/commit/%s", run.Repo.Link(), run.CommitSHA),
ShortSha: base.ShortSha(run.CommitSHA), Pusher: pusher,
Link: fmt.Sprintf("%s/commit/%s", run.Repo.Link(), run.CommitSHA), Branch: branch,
Pusher: pusher,
Branch: branch,
} }
var task *actions_model.ActionTask var task *actions_model.ActionTask

View File

@ -212,8 +212,6 @@ func SearchCommits(ctx *context.Context) {
// FileHistory show a file's reversions // FileHistory show a file's reversions
func FileHistory(ctx *context.Context) { func FileHistory(ctx *context.Context) {
ctx.Data["IsRepoToolbarCommits"] = true
fileName := ctx.Repo.TreePath fileName := ctx.Repo.TreePath
if len(fileName) == 0 { if len(fileName) == 0 {
Commits(ctx) Commits(ctx)

View File

@ -800,7 +800,6 @@ func CompareDiff(ctx *context.Context) {
} }
ctx.Data["Title"] = "Comparing " + base.ShortSha(beforeCommitID) + separator + base.ShortSha(afterCommitID) ctx.Data["Title"] = "Comparing " + base.ShortSha(beforeCommitID) + separator + base.ShortSha(afterCommitID)
ctx.Data["IsRepoToolbarCommits"] = true
ctx.Data["IsDiffCompare"] = true ctx.Data["IsDiffCompare"] = true
_, templateErrs := setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates) _, templateErrs := setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates)
@ -813,7 +812,7 @@ func CompareDiff(ctx *context.Context) {
// applicable if you have one commit to compare and that commit has a message. // applicable if you have one commit to compare and that commit has a message.
// In that case the commit message will be prepend to the template body. // In that case the commit message will be prepend to the template body.
if templateContent, ok := ctx.Data[pullRequestTemplateKey].(string); ok && templateContent != "" { if templateContent, ok := ctx.Data[pullRequestTemplateKey].(string); ok && templateContent != "" {
// Re-use the same key as that's priortized over the "content" key. // Re-use the same key as that's prioritized over the "content" key.
// Add two new lines between the content to ensure there's always at least // Add two new lines between the content to ensure there's always at least
// one empty line between them. // one empty line between them.
ctx.Data[pullRequestTemplateKey] = content + "\n\n" + templateContent ctx.Data[pullRequestTemplateKey] = content + "\n\n" + templateContent

View File

@ -1760,8 +1760,8 @@ func ViewIssue(ctx *context.Context) {
// drop error since times could be pruned from DB.. // drop error since times could be pruned from DB..
_ = comment.LoadTime(ctx) _ = comment.LoadTime(ctx)
if comment.Content != "" { if comment.Content != "" {
// Content before v1.21 did store the formated string instead of seconds, // Content before v1.21 did store the formatted string instead of seconds,
// so "|" is used as delimeter to mark the new format // so "|" is used as delimiter to mark the new format
if comment.Content[0] != '|' { if comment.Content[0] != '|' {
// handle old time comments that have formatted text stored // handle old time comments that have formatted text stored
comment.RenderedContent = templates.SanitizeHTML(comment.Content) comment.RenderedContent = templates.SanitizeHTML(comment.Content)
@ -3149,13 +3149,10 @@ func UpdateCommentContent(ctx *context.Context) {
} }
oldContent := comment.Content oldContent := comment.Content
comment.Content = ctx.FormString("content") newContent := ctx.FormString("content")
if len(comment.Content) == 0 {
ctx.JSON(http.StatusOK, map[string]any{ // allow to save empty content
"content": "", comment.Content = newContent
})
return
}
if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil { if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {
if errors.Is(err, user_model.ErrBlockedUser) { if errors.Is(err, user_model.ErrBlockedUser) {
ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user")) ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
@ -3178,21 +3175,27 @@ func UpdateCommentContent(ctx *context.Context) {
} }
} }
content, err := markdown.RenderString(&markup.RenderContext{ var renderedContent template.HTML
Links: markup.Links{ if comment.Content != "" {
Base: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ? renderedContent, err = markdown.RenderString(&markup.RenderContext{
}, Links: markup.Links{
Metas: ctx.Repo.Repository.ComposeMetas(ctx), Base: ctx.FormString("context"), // FIXME: <- IS THIS SAFE ?
GitRepo: ctx.Repo.GitRepo, },
Ctx: ctx, Metas: ctx.Repo.Repository.ComposeMetas(ctx),
}, comment.Content) GitRepo: ctx.Repo.GitRepo,
if err != nil { Ctx: ctx,
ctx.ServerError("RenderString", err) }, comment.Content)
return if err != nil {
ctx.ServerError("RenderString", err)
return
}
} else {
contentEmpty := fmt.Sprintf(`<span class="no-content">%s</span>`, ctx.Tr("repo.issues.no_content"))
renderedContent = template.HTML(contentEmpty)
} }
ctx.JSON(http.StatusOK, map[string]any{ ctx.JSON(http.StatusOK, map[string]any{
"content": content, "content": renderedContent,
"attachments": attachmentsHTML(ctx, comment.Attachments, comment.Content), "attachments": attachmentsHTML(ctx, comment.Attachments, comment.Content),
}) })
} }

View File

@ -1225,7 +1225,6 @@ func CompareAndPullRequestPost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.pulls.compare_changes") ctx.Data["Title"] = ctx.Tr("repo.pulls.compare_changes")
ctx.Data["PageIsComparePull"] = true ctx.Data["PageIsComparePull"] = true
ctx.Data["IsDiffCompare"] = true ctx.Data["IsDiffCompare"] = true
ctx.Data["IsRepoToolbarCommits"] = true
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
upload.AddUploadContext(ctx, "comment") upload.AddUploadContext(ctx, "comment")

View File

@ -264,6 +264,8 @@ func SubmitReview(ctx *context.Context) {
if issues_model.IsContentEmptyErr(err) { if issues_model.IsContentEmptyErr(err) {
ctx.Flash.Error(ctx.Tr("repo.issues.review.content.empty")) ctx.Flash.Error(ctx.Tr("repo.issues.review.content.empty"))
ctx.JSONRedirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index)) ctx.JSONRedirect(fmt.Sprintf("%s/pulls/%d/files", ctx.Repo.RepoLink, issue.Index))
} else if errors.Is(err, pull_service.ErrSubmitReviewOnClosedPR) {
ctx.Status(http.StatusUnprocessableEntity)
} else { } else {
ctx.ServerError("SubmitReview", err) ctx.ServerError("SubmitReview", err)
} }

View File

@ -1612,7 +1612,7 @@ func registerRoutes(m *web.Route) {
m.NotFound(func(w http.ResponseWriter, req *http.Request) { m.NotFound(func(w http.ResponseWriter, req *http.Request) {
ctx := context.GetWebContext(req) ctx := context.GetWebContext(req)
routing.UpdateFuncInfo(ctx, routing.GetFuncInfo(ctx.NotFound, "GlobalNotFound")) routing.UpdateFuncInfo(ctx, routing.GetFuncInfo(ctx.NotFound, "WebNotFound"))
ctx.NotFound("", nil) ctx.NotFound("", nil)
}) })
} }

View File

@ -0,0 +1,28 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import "code.gitea.io/gitea/services/context"
// API for actions of a repository or organization
type API interface {
// ListActionsSecrets list secrets
ListActionsSecrets(*context.APIContext)
// CreateOrUpdateSecret create or update a secret
CreateOrUpdateSecret(*context.APIContext)
// DeleteSecret delete a secret
DeleteSecret(*context.APIContext)
// ListVariables list variables
ListVariables(*context.APIContext)
// GetVariable get a variable
GetVariable(*context.APIContext)
// DeleteVariable delete a variable
DeleteVariable(*context.APIContext)
// CreateVariable create a variable
CreateVariable(*context.APIContext)
// UpdateVariable update a variable
UpdateVariable(*context.APIContext)
// GetRegistrationToken get registration token
GetRegistrationToken(*context.APIContext)
}

View File

@ -20,14 +20,13 @@ import (
"code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/httpcache" "code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
web_types "code.gitea.io/gitea/modules/web/types" web_types "code.gitea.io/gitea/modules/web/types"
"gitea.com/go-chi/session"
) )
// Render represents a template render // Render represents a template render
@ -154,7 +153,7 @@ func Contexter() func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
base, baseCleanUp := NewBaseContext(resp, req) base, baseCleanUp := NewBaseContext(resp, req)
defer baseCleanUp() defer baseCleanUp()
ctx := NewWebContext(base, rnd, session.GetSession(req)) ctx := NewWebContext(base, rnd, session.GetContextSession(req))
ctx.Data.MergeFrom(middleware.CommonTemplateContextData()) ctx.Data.MergeFrom(middleware.CommonTemplateContextData())
ctx.Data["Context"] = ctx // TODO: use "ctx" in template and remove this ctx.Data["Context"] = ctx // TODO: use "ctx" in template and remove this

View File

@ -19,7 +19,9 @@ import (
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
@ -43,7 +45,8 @@ func mockRequest(t *testing.T, reqPath string) *http.Request {
} }
type MockContextOption struct { type MockContextOption struct {
Render context.Render Render context.Render
SessionStore *session.MockStore
} }
// MockContext mock context for unit tests // MockContext mock context for unit tests
@ -62,12 +65,17 @@ func MockContext(t *testing.T, reqPath string, opts ...MockContextOption) (*cont
base.Data = middleware.GetContextData(req.Context()) base.Data = middleware.GetContextData(req.Context())
base.Locale = &translation.MockLocale{} base.Locale = &translation.MockLocale{}
chiCtx := chi.NewRouteContext()
ctx := context.NewWebContext(base, opt.Render, nil) ctx := context.NewWebContext(base, opt.Render, nil)
ctx.AppendContextValue(context.WebContextKey, ctx) ctx.AppendContextValue(context.WebContextKey, ctx)
ctx.AppendContextValue(chi.RouteCtxKey, chiCtx)
if opt.SessionStore != nil {
ctx.AppendContextValue(session.MockStoreContextKey, opt.SessionStore)
ctx.Session = opt.SessionStore
}
ctx.Cache = cache.GetCache()
ctx.PageData = map[string]any{} ctx.PageData = map[string]any{}
ctx.Data["PageStartTime"] = time.Now() ctx.Data["PageStartTime"] = time.Now()
chiCtx := chi.NewRouteContext()
ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx)
return ctx, resp return ctx, resp
} }

View File

@ -72,8 +72,8 @@ func ToTimelineComment(ctx context.Context, repo *repo_model.Repository, c *issu
c.Type == issues_model.CommentTypeStopTracking || c.Type == issues_model.CommentTypeStopTracking ||
c.Type == issues_model.CommentTypeDeleteTimeManual) && c.Type == issues_model.CommentTypeDeleteTimeManual) &&
c.Content[0] == '|' { c.Content[0] == '|' {
// TimeTracking Comments from v1.21 on store the seconds instead of an formated string // TimeTracking Comments from v1.21 on store the seconds instead of an formatted string
// so we check for the "|" delimeter and convert new to legacy format on demand // so we check for the "|" delimiter and convert new to legacy format on demand
c.Content = util.SecToTime(c.Content[1:]) c.Content = util.SecToTime(c.Content[1:])
} }
} }

View File

@ -229,12 +229,12 @@ func TeamReviewRequest(ctx context.Context, issue *issues_model.Issue, doer *use
return comment, teamReviewRequestNotify(ctx, issue, doer, reviewer, isAdd, comment) return comment, teamReviewRequestNotify(ctx, issue, doer, reviewer, isAdd, comment)
} }
func ReviewRequestNotify(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, reviewNotifers []*ReviewRequestNotifier) { func ReviewRequestNotify(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, reviewNotifiers []*ReviewRequestNotifier) {
for _, reviewNotifer := range reviewNotifers { for _, reviewNotifier := range reviewNotifiers {
if reviewNotifer.Reviwer != nil { if reviewNotifier.Reviewer != nil {
notify_service.PullRequestReviewRequest(ctx, issue.Poster, issue, reviewNotifer.Reviwer, reviewNotifer.IsAdd, reviewNotifer.Comment) notify_service.PullRequestReviewRequest(ctx, issue.Poster, issue, reviewNotifier.Reviewer, reviewNotifier.IsAdd, reviewNotifier.Comment)
} else if reviewNotifer.ReviewTeam != nil { } else if reviewNotifier.ReviewTeam != nil {
if err := teamReviewRequestNotify(ctx, issue, issue.Poster, reviewNotifer.ReviewTeam, reviewNotifer.IsAdd, reviewNotifer.Comment); err != nil { if err := teamReviewRequestNotify(ctx, issue, issue.Poster, reviewNotifier.ReviewTeam, reviewNotifier.IsAdd, reviewNotifier.Comment); err != nil {
log.Error("teamReviewRequestNotify: %v", err) log.Error("teamReviewRequestNotify: %v", err)
} }
} }

View File

@ -90,17 +90,17 @@ func ChangeTitle(ctx context.Context, issue *issues_model.Issue, doer *user_mode
return err return err
} }
var reviewNotifers []*ReviewRequestNotifier var reviewNotifiers []*ReviewRequestNotifier
if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issues_model.HasWorkInProgressPrefix(title) { if issue.IsPull && issues_model.HasWorkInProgressPrefix(oldTitle) && !issues_model.HasWorkInProgressPrefix(title) {
var err error var err error
reviewNotifers, err = PullRequestCodeOwnersReview(ctx, issue, issue.PullRequest) reviewNotifiers, err = PullRequestCodeOwnersReview(ctx, issue, issue.PullRequest)
if err != nil { if err != nil {
log.Error("PullRequestCodeOwnersReview: %v", err) log.Error("PullRequestCodeOwnersReview: %v", err)
} }
} }
notify_service.IssueChangeTitle(ctx, doer, issue, oldTitle) notify_service.IssueChangeTitle(ctx, doer, issue, oldTitle)
ReviewRequestNotify(ctx, issue, issue.Poster, reviewNotifers) ReviewRequestNotify(ctx, issue, issue.Poster, reviewNotifiers)
return nil return nil
} }

View File

@ -36,7 +36,7 @@ func getMergeBase(repo *git.Repository, pr *issues_model.PullRequest, baseBranch
type ReviewRequestNotifier struct { type ReviewRequestNotifier struct {
Comment *issues_model.Comment Comment *issues_model.Comment
IsAdd bool IsAdd bool
Reviwer *user_model.User Reviewer *user_model.User
ReviewTeam *org_model.Team ReviewTeam *org_model.Team
} }
@ -124,9 +124,9 @@ func PullRequestCodeOwnersReview(ctx context.Context, issue *issues_model.Issue,
return nil, err return nil, err
} }
notifiers = append(notifiers, &ReviewRequestNotifier{ notifiers = append(notifiers, &ReviewRequestNotifier{
Comment: comment, Comment: comment,
IsAdd: true, IsAdd: true,
Reviwer: u, Reviewer: u,
}) })
} }
} }

View File

@ -40,7 +40,7 @@ func Update(ctx context.Context, pullLimit, pushLimit int) error {
} }
log.Trace("Doing: Update") log.Trace("Doing: Update")
handler := func(idx int, bean any) error { handler := func(bean any) error {
var repo *repo_model.Repository var repo *repo_model.Repository
var mirrorType SyncType var mirrorType SyncType
var referenceID int64 var referenceID int64
@ -91,7 +91,7 @@ func Update(ctx context.Context, pullLimit, pushLimit int) error {
pullMirrorsRequested := 0 pullMirrorsRequested := 0
if pullLimit != 0 { if pullLimit != 0 {
if err := repo_model.MirrorsIterate(ctx, pullLimit, func(idx int, bean any) error { if err := repo_model.MirrorsIterate(ctx, pullLimit, func(idx int, bean any) error {
if err := handler(idx, bean); err != nil { if err := handler(bean); err != nil {
return err return err
} }
pullMirrorsRequested++ pullMirrorsRequested++
@ -105,7 +105,7 @@ func Update(ctx context.Context, pullLimit, pushLimit int) error {
pushMirrorsRequested := 0 pushMirrorsRequested := 0
if pushLimit != 0 { if pushLimit != 0 {
if err := repo_model.PushMirrorsIterate(ctx, pushLimit, func(idx int, bean any) error { if err := repo_model.PushMirrorsIterate(ctx, pushLimit, func(idx int, bean any) error {
if err := handler(idx, bean); err != nil { if err := handler(bean); err != nil {
return err return err
} }
pushMirrorsRequested++ pushMirrorsRequested++

View File

@ -466,7 +466,7 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool {
log.Trace("SyncMirrors [repo: %-v]: %d branches updated", m.Repo, len(results)) log.Trace("SyncMirrors [repo: %-v]: %d branches updated", m.Repo, len(results))
if len(results) > 0 { if len(results) > 0 {
if ok := checkAndUpdateEmptyRepository(ctx, m, gitRepo, results); !ok { if ok := checkAndUpdateEmptyRepository(ctx, m, results); !ok {
log.Error("SyncMirrors [repo: %-v]: checkAndUpdateEmptyRepository: %v", m.Repo, err) log.Error("SyncMirrors [repo: %-v]: checkAndUpdateEmptyRepository: %v", m.Repo, err)
return false return false
} }
@ -564,7 +564,7 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool {
return true return true
} }
func checkAndUpdateEmptyRepository(ctx context.Context, m *repo_model.Mirror, gitRepo *git.Repository, results []*mirrorSyncResult) bool { func checkAndUpdateEmptyRepository(ctx context.Context, m *repo_model.Mirror, results []*mirrorSyncResult) bool {
if !m.Repo.IsEmpty { if !m.Repo.IsEmpty {
return true return true
} }

View File

@ -20,11 +20,11 @@ import (
// DeleteOrganization completely and permanently deletes everything of organization. // DeleteOrganization completely and permanently deletes everything of organization.
func DeleteOrganization(ctx context.Context, org *org_model.Organization, purge bool) error { func DeleteOrganization(ctx context.Context, org *org_model.Organization, purge bool) error {
ctx, commiter, err := db.TxContext(ctx) ctx, committer, err := db.TxContext(ctx)
if err != nil { if err != nil {
return err return err
} }
defer commiter.Close() defer committer.Close()
if purge { if purge {
err := repo_service.DeleteOwnerRepositoriesDirectly(ctx, org.AsUser()) err := repo_service.DeleteOwnerRepositoriesDirectly(ctx, org.AsUser())
@ -52,7 +52,7 @@ func DeleteOrganization(ctx context.Context, org *org_model.Organization, purge
return fmt.Errorf("DeleteOrganization: %w", err) return fmt.Errorf("DeleteOrganization: %w", err)
} }
if err := commiter.Commit(); err != nil { if err := committer.Commit(); err != nil {
return err return err
} }

View File

@ -66,7 +66,7 @@ const (
MergeCheckTypeAuto // Auto Merge (Scheduled Merge) After Checks Succeed MergeCheckTypeAuto // Auto Merge (Scheduled Merge) After Checks Succeed
) )
// CheckPullMergable check if the pull mergable based on all conditions (branch protection, merge options, ...) // CheckPullMergable check if the pull mergeable based on all conditions (branch protection, merge options, ...)
func CheckPullMergable(stdCtx context.Context, doer *user_model.User, perm *access_model.Permission, pr *issues_model.PullRequest, mergeCheckType MergeCheckType, adminSkipProtectionCheck bool) error { func CheckPullMergable(stdCtx context.Context, doer *user_model.User, perm *access_model.Permission, pr *issues_model.PullRequest, mergeCheckType MergeCheckType, adminSkipProtectionCheck bool) error {
return db.WithTx(stdCtx, func(ctx context.Context) error { return db.WithTx(stdCtx, func(ctx context.Context) error {
if pr.HasMerged { if pr.HasMerged {

View File

@ -46,7 +46,7 @@ func getCommitIDsFromRepo(ctx context.Context, repo *repo_model.Repository, oldC
return commitIDs, isForcePush, err return commitIDs, isForcePush, err
} }
// Find commits between new and old commit exclusing base branch commits // Find commits between new and old commit excluding base branch commits
commits, err := gitRepo.CommitsBetweenNotBase(newCommit, oldCommit, baseBranch) commits, err := gitRepo.CommitsBetweenNotBase(newCommit, oldCommit, baseBranch)
if err != nil { if err != nil {
return nil, false, err return nil, false, err

View File

@ -77,7 +77,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *iss
} }
defer baseGitRepo.Close() defer baseGitRepo.Close()
var reviewNotifers []*issue_service.ReviewRequestNotifier var reviewNotifiers []*issue_service.ReviewRequestNotifier
if err := db.WithTx(ctx, func(ctx context.Context) error { if err := db.WithTx(ctx, func(ctx context.Context) error {
if err := issues_model.NewPullRequest(ctx, repo, issue, labelIDs, uuids, pr); err != nil { if err := issues_model.NewPullRequest(ctx, repo, issue, labelIDs, uuids, pr); err != nil {
return err return err
@ -137,7 +137,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *iss
} }
if !pr.IsWorkInProgress(ctx) { if !pr.IsWorkInProgress(ctx) {
reviewNotifers, err = issue_service.PullRequestCodeOwnersReview(ctx, issue, pr) reviewNotifiers, err = issue_service.PullRequestCodeOwnersReview(ctx, issue, pr)
if err != nil { if err != nil {
return err return err
} }
@ -152,7 +152,7 @@ func NewPullRequest(ctx context.Context, repo *repo_model.Repository, issue *iss
} }
baseGitRepo.Close() // close immediately to avoid notifications will open the repository again baseGitRepo.Close() // close immediately to avoid notifications will open the repository again
issue_service.ReviewRequestNotify(ctx, issue, issue.Poster, reviewNotifers) issue_service.ReviewRequestNotify(ctx, issue, issue.Poster, reviewNotifiers)
mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, issue, issue.Poster, issue.Content) mentions, err := issues_model.FindAndUpdateIssueMentions(ctx, issue, issue.Poster, issue.Content)
if err != nil { if err != nil {

View File

@ -6,6 +6,7 @@ package pull
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"regexp" "regexp"
@ -43,6 +44,9 @@ func (err ErrDismissRequestOnClosedPR) Unwrap() error {
return util.ErrPermissionDenied return util.ErrPermissionDenied
} }
// ErrSubmitReviewOnClosedPR represents an error when an user tries to submit an approve or reject review associated to a closed or merged PR.
var ErrSubmitReviewOnClosedPR = errors.New("can't submit review for a closed or merged PR")
// checkInvalidation checks if the line of code comment got changed by another commit. // checkInvalidation checks if the line of code comment got changed by another commit.
// If the line got changed the comment is going to be invalidated. // If the line got changed the comment is going to be invalidated.
func checkInvalidation(ctx context.Context, c *issues_model.Comment, doer *user_model.User, repo *git.Repository, branch string) error { func checkInvalidation(ctx context.Context, c *issues_model.Comment, doer *user_model.User, repo *git.Repository, branch string) error {
@ -293,6 +297,10 @@ func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repos
if reviewType != issues_model.ReviewTypeApprove && reviewType != issues_model.ReviewTypeReject { if reviewType != issues_model.ReviewTypeApprove && reviewType != issues_model.ReviewTypeReject {
stale = false stale = false
} else { } else {
if issue.IsClosed {
return nil, nil, ErrSubmitReviewOnClosedPR
}
headCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName()) headCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName())
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err

View File

@ -38,12 +38,10 @@ func getCommitStatusCache(repoID int64, branchName string) *commitStatusCacheVal
if ok && statusStr != "" { if ok && statusStr != "" {
var cv commitStatusCacheValue var cv commitStatusCacheValue
err := json.Unmarshal([]byte(statusStr), &cv) err := json.Unmarshal([]byte(statusStr), &cv)
if err == nil && cv.State != "" { if err == nil {
return &cv return &cv
} }
if err != nil { log.Warn("getCommitStatusCache: json.Unmarshal failed: %v", err)
log.Warn("getCommitStatusCache: json.Unmarshal failed: %v", err)
}
} }
return nil return nil
} }
@ -128,15 +126,22 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, creato
// FindReposLastestCommitStatuses loading repository default branch latest combinded commit status with cache // FindReposLastestCommitStatuses loading repository default branch latest combinded commit status with cache
func FindReposLastestCommitStatuses(ctx context.Context, repos []*repo_model.Repository) ([]*git_model.CommitStatus, error) { func FindReposLastestCommitStatuses(ctx context.Context, repos []*repo_model.Repository) ([]*git_model.CommitStatus, error) {
results := make([]*git_model.CommitStatus, len(repos)) results := make([]*git_model.CommitStatus, len(repos))
allCached := true
for i, repo := range repos { for i, repo := range repos {
if cv := getCommitStatusCache(repo.ID, repo.DefaultBranch); cv != nil { if cv := getCommitStatusCache(repo.ID, repo.DefaultBranch); cv != nil {
results[i] = &git_model.CommitStatus{ results[i] = &git_model.CommitStatus{
State: api.CommitStatusState(cv.State), State: api.CommitStatusState(cv.State),
TargetURL: cv.TargetURL, TargetURL: cv.TargetURL,
} }
} else {
allCached = false
} }
} }
if allCached {
return results, nil
}
// collect the latest commit of each repo // collect the latest commit of each repo
// at most there are dozens of repos (limited by MaxResponseItems), so it's not a big problem at the moment // at most there are dozens of repos (limited by MaxResponseItems), so it's not a big problem at the moment
repoBranchNames := make(map[int64]string, len(repos)) repoBranchNames := make(map[int64]string, len(repos))
@ -165,10 +170,10 @@ func FindReposLastestCommitStatuses(ctx context.Context, repos []*repo_model.Rep
for i, repo := range repos { for i, repo := range repos {
if repo.ID == summary.RepoID { if repo.ID == summary.RepoID {
results[i] = summary results[i] = summary
_ = slices.DeleteFunc(repoSHAs, func(repoSHA git_model.RepoSHA) bool { repoSHAs = slices.DeleteFunc(repoSHAs, func(repoSHA git_model.RepoSHA) bool {
return repoSHA.RepoID == repo.ID return repoSHA.RepoID == repo.ID
}) })
if results[i].State != "" { if results[i] != nil {
if err := updateCommitStatusCache(repo.ID, repo.DefaultBranch, results[i].State, results[i].TargetURL); err != nil { if err := updateCommitStatusCache(repo.ID, repo.DefaultBranch, results[i].State, results[i].TargetURL); err != nil {
log.Error("updateCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err) log.Error("updateCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err)
} }
@ -177,6 +182,9 @@ func FindReposLastestCommitStatuses(ctx context.Context, repos []*repo_model.Rep
} }
} }
} }
if len(repoSHAs) == 0 {
return results, nil
}
// call the database O(1) times to get the commit statuses for all repos // call the database O(1) times to get the commit statuses for all repos
repoToItsLatestCommitStatuses, err := git_model.GetLatestCommitStatusForPairs(ctx, repoSHAs) repoToItsLatestCommitStatuses, err := git_model.GetLatestCommitStatusForPairs(ctx, repoSHAs)
@ -187,7 +195,7 @@ func FindReposLastestCommitStatuses(ctx context.Context, repos []*repo_model.Rep
for i, repo := range repos { for i, repo := range repos {
if results[i] == nil { if results[i] == nil {
results[i] = git_model.CalcCommitStatus(repoToItsLatestCommitStatuses[repo.ID]) results[i] = git_model.CalcCommitStatus(repoToItsLatestCommitStatuses[repo.ID])
if results[i].State != "" { if results[i] != nil {
if err := updateCommitStatusCache(repo.ID, repo.DefaultBranch, results[i].State, results[i].TargetURL); err != nil { if err := updateCommitStatusCache(repo.ID, repo.DefaultBranch, results[i].State, results[i].TargetURL); err != nil {
log.Error("updateCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err) log.Error("updateCommitStatusCache[%d:%s] failed: %v", repo.ID, repo.DefaultBranch, err)
} }

View File

@ -191,8 +191,9 @@ export default {
'no-invalid-double-slash-comments': true, 'no-invalid-double-slash-comments': true,
'no-invalid-position-at-import-rule': [true, {ignoreAtRules: ['tailwind']}], 'no-invalid-position-at-import-rule': [true, {ignoreAtRules: ['tailwind']}],
'no-irregular-whitespace': true, 'no-irregular-whitespace': true,
'no-unknown-animations': null, 'no-unknown-animations': null, // disabled until stylelint supports multi-file linting
'no-unknown-custom-properties': null, 'no-unknown-custom-media': null, // disabled until stylelint supports multi-file linting
'no-unknown-custom-properties': null, // disabled until stylelint supports multi-file linting
'number-max-precision': null, 'number-max-precision': null,
'plugin/declaration-block-no-ignored-properties': true, 'plugin/declaration-block-no-ignored-properties': true,
'property-allowed-list': null, 'property-allowed-list': null,

View File

@ -15,7 +15,7 @@
{{if .Title}}{{.Title}}{{else}}{{ctx.Locale.Tr "actions.runs.empty_commit_message"}}{{end}} {{if .Title}}{{.Title}}{{else}}{{ctx.Locale.Tr "actions.runs.empty_commit_message"}}{{end}}
</a> </a>
<div class="flex-item-body"> <div class="flex-item-body">
<b>{{if not $.CurWorkflow}}{{.WorkflowID}} {{end}}#{{.Index}}</b>: <span><b>{{if not $.CurWorkflow}}{{.WorkflowID}} {{end}}#{{.Index}}</b>:</span>
{{- if .ScheduleID -}} {{- if .ScheduleID -}}
{{ctx.Locale.Tr "actions.runs.scheduled"}} {{ctx.Locale.Tr "actions.runs.scheduled"}}
{{- else -}} {{- else -}}

View File

@ -10,6 +10,9 @@
data-locale-cancel="{{ctx.Locale.Tr "cancel"}}" data-locale-cancel="{{ctx.Locale.Tr "cancel"}}"
data-locale-rerun="{{ctx.Locale.Tr "rerun"}}" data-locale-rerun="{{ctx.Locale.Tr "rerun"}}"
data-locale-rerun-all="{{ctx.Locale.Tr "rerun_all"}}" data-locale-rerun-all="{{ctx.Locale.Tr "rerun_all"}}"
data-locale-runs-scheduled="{{ctx.Locale.Tr "actions.runs.scheduled"}}"
data-locale-runs-commit="{{ctx.Locale.Tr "actions.runs.commit"}}"
data-locale-runs-pushed-by="{{ctx.Locale.Tr "actions.runs.pushed_by"}}"
data-locale-status-unknown="{{ctx.Locale.Tr "actions.status.unknown"}}" data-locale-status-unknown="{{ctx.Locale.Tr "actions.status.unknown"}}"
data-locale-status-waiting="{{ctx.Locale.Tr "actions.status.waiting"}}" data-locale-status-waiting="{{ctx.Locale.Tr "actions.status.waiting"}}"
data-locale-status-running="{{ctx.Locale.Tr "actions.status.running"}}" data-locale-status-running="{{ctx.Locale.Tr "actions.status.running"}}"

View File

@ -1,6 +1,6 @@
{{if $.IsSplitStyle}} {{if $.IsSplitStyle}}
{{range $k, $line := $.section.Lines}} {{range $k, $line := $.section.Lines}}
<tr class="{{.GetHTMLDiffLineType}}-code nl-{{$k}} ol-{{$k}}"> <tr class="{{.GetHTMLDiffLineType}}-code nl-{{$k}} ol-{{$k}} line-expanded">
{{if eq .GetType 4}} {{if eq .GetType 4}}
<td class="lines-num lines-num-old" data-line-num="{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}"> <td class="lines-num lines-num-old" data-line-num="{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}">
<div class="tw-flex"> <div class="tw-flex">
@ -26,17 +26,17 @@
{{else}} {{else}}
{{$inlineDiff := $.section.GetComputedInlineDiffFor $line ctx.Locale}} {{$inlineDiff := $.section.GetComputedInlineDiffFor $line ctx.Locale}}
<td class="lines-num lines-num-old" data-line-num="{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}"><span rel="{{if $line.LeftIdx}}diff-{{$.FileNameHash}}L{{$line.LeftIdx}}{{end}}"></span></td> <td class="lines-num lines-num-old" data-line-num="{{if $line.LeftIdx}}{{$line.LeftIdx}}{{end}}"><span rel="{{if $line.LeftIdx}}diff-{{$.FileNameHash}}L{{$line.LeftIdx}}{{end}}"></span></td>
<td class="blob-excerpt lines-escape lines-escape-old">{{if and $line.LeftIdx $inlineDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>{{end}}</td> <td class="lines-escape lines-escape-old">{{if and $line.LeftIdx $inlineDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>{{end}}</td>
<td class="blob-excerpt lines-type-marker lines-type-marker-old">{{if $line.LeftIdx}}<span class="tw-font-mono" data-type-marker=""></span>{{end}}</td> <td class="lines-type-marker lines-type-marker-old">{{if $line.LeftIdx}}<span class="tw-font-mono" data-type-marker=""></span>{{end}}</td>
<td class="blob-excerpt lines-code lines-code-old">{{/* <td class="lines-code lines-code-old">{{/*
*/}}{{if $line.LeftIdx}}{{template "repo/diff/section_code" dict "diff" $inlineDiff}}{{else}}{{/* */}}{{if $line.LeftIdx}}{{template "repo/diff/section_code" dict "diff" $inlineDiff}}{{else}}{{/*
*/}}<code class="code-inner"></code>{{/* */}}<code class="code-inner"></code>{{/*
*/}}{{end}}{{/* */}}{{end}}{{/*
*/}}</td> */}}</td>
<td class="lines-num lines-num-new" data-line-num="{{if $line.RightIdx}}{{$line.RightIdx}}{{end}}"><span rel="{{if $line.RightIdx}}diff-{{$.FileNameHash}}R{{$line.RightIdx}}{{end}}"></span></td> <td class="lines-num lines-num-new" data-line-num="{{if $line.RightIdx}}{{$line.RightIdx}}{{end}}"><span rel="{{if $line.RightIdx}}diff-{{$.FileNameHash}}R{{$line.RightIdx}}{{end}}"></span></td>
<td class="blob-excerpt lines-escape lines-escape-new">{{if and $line.RightIdx $inlineDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>{{end}}</td> <td class="lines-escape lines-escape-new">{{if and $line.RightIdx $inlineDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>{{end}}</td>
<td class="blob-excerpt lines-type-marker lines-type-marker-new">{{if $line.RightIdx}}<span class="tw-font-mono" data-type-marker=""></span>{{end}}</td> <td class="lines-type-marker lines-type-marker-new">{{if $line.RightIdx}}<span class="tw-font-mono" data-type-marker=""></span>{{end}}</td>
<td class="blob-excerpt lines-code lines-code-new">{{/* <td class="lines-code lines-code-new">{{/*
*/}}{{if $line.RightIdx}}{{template "repo/diff/section_code" dict "diff" $inlineDiff}}{{else}}{{/* */}}{{if $line.RightIdx}}{{template "repo/diff/section_code" dict "diff" $inlineDiff}}{{else}}{{/*
*/}}<code class="code-inner"></code>{{/* */}}<code class="code-inner"></code>{{/*
*/}}{{end}}{{/* */}}{{end}}{{/*
@ -46,7 +46,7 @@
{{end}} {{end}}
{{else}} {{else}}
{{range $k, $line := $.section.Lines}} {{range $k, $line := $.section.Lines}}
<tr class="{{.GetHTMLDiffLineType}}-code nl-{{$k}} ol-{{$k}}"> <tr class="{{.GetHTMLDiffLineType}}-code nl-{{$k}} ol-{{$k}} line-expanded">
{{if eq .GetType 4}} {{if eq .GetType 4}}
<td colspan="2" class="lines-num"> <td colspan="2" class="lines-num">
<div class="tw-flex"> <div class="tw-flex">
@ -72,9 +72,9 @@
<td class="lines-num lines-num-new" data-line-num="{{if $line.RightIdx}}{{$line.RightIdx}}{{end}}"><span rel="{{if $line.RightIdx}}diff-{{$.FileNameHash}}R{{$line.RightIdx}}{{end}}"></span></td> <td class="lines-num lines-num-new" data-line-num="{{if $line.RightIdx}}{{$line.RightIdx}}{{end}}"><span rel="{{if $line.RightIdx}}diff-{{$.FileNameHash}}R{{$line.RightIdx}}{{end}}"></span></td>
{{end}} {{end}}
{{$inlineDiff := $.section.GetComputedInlineDiffFor $line ctx.Locale}} {{$inlineDiff := $.section.GetComputedInlineDiffFor $line ctx.Locale}}
<td class="blob-excerpt lines-escape">{{if $inlineDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>{{end}}</td> <td class="lines-escape">{{if $inlineDiff.EscapeStatus.Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"></button>{{end}}</td>
<td class="blob-excerpt lines-type-marker"><span class="tw-font-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span></td> <td class="lines-type-marker"><span class="tw-font-mono" data-type-marker="{{$line.GetLineTypeMarker}}"></span></td>
<td class="blob-excerpt lines-code{{if (not $line.RightIdx)}} lines-code-old{{end}}"><code {{if $inlineDiff.EscapeStatus.Escaped}}class="code-inner has-escaped" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"{{else}}class="code-inner"{{end}}>{{$inlineDiff.Content}}</code></td> <td class="lines-code{{if (not $line.RightIdx)}} lines-code-old{{end}}"><code {{if $inlineDiff.EscapeStatus.Escaped}}class="code-inner has-escaped" title="{{template "repo/diff/escape_title" dict "diff" $inlineDiff}}"{{else}}class="code-inner"{{end}}>{{$inlineDiff.Content}}</code></td>
</tr> </tr>
{{end}} {{end}}
{{end}} {{end}}

View File

@ -30,20 +30,24 @@
{{end}} {{end}}
<div class="divider"></div> <div class="divider"></div>
{{$showSelfTooltip := (and $.IsSigned ($.Issue.IsPoster $.SignedUser.ID))}} {{$showSelfTooltip := (and $.IsSigned ($.Issue.IsPoster $.SignedUser.ID))}}
{{if $showSelfTooltip}} {{if not $.Issue.IsClosed}}
<span class="tw-inline-block" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.review.self_approve"}}"> {{if $showSelfTooltip}}
<button type="submit" name="type" value="approve" disabled class="ui submit primary tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.approve"}}</button> <span class="tw-inline-block" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.review.self_approve"}}">
</span> <button type="submit" name="type" value="approve" disabled class="ui submit primary tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.approve"}}</button>
{{else}} </span>
<button type="submit" name="type" value="approve" class="ui submit primary tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.approve"}}</button> {{else}}
<button type="submit" name="type" value="approve" class="ui submit primary tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.approve"}}</button>
{{end}}
{{end}} {{end}}
<button type="submit" name="type" value="comment" class="ui submit tiny basic button btn-submit">{{ctx.Locale.Tr "repo.diff.review.comment"}}</button> <button type="submit" name="type" value="comment" class="ui submit tiny basic button btn-submit">{{ctx.Locale.Tr "repo.diff.review.comment"}}</button>
{{if $showSelfTooltip}} {{if not $.Issue.IsClosed}}
<span class="tw-inline-block" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.review.self_reject"}}"> {{if $showSelfTooltip}}
<button type="submit" name="type" value="reject" disabled class="ui submit red tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.reject"}}</button> <span class="tw-inline-block" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.review.self_reject"}}">
</span> <button type="submit" name="type" value="reject" disabled class="ui submit red tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.reject"}}</button>
{{else}} </span>
<button type="submit" name="type" value="reject" class="ui submit red tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.reject"}}</button> {{else}}
<button type="submit" name="type" value="reject" class="ui submit red tiny button btn-submit">{{ctx.Locale.Tr "repo.diff.review.reject"}}</button>
{{end}}
{{end}} {{end}}
</form> </form>
</div> </div>

View File

@ -90,7 +90,16 @@
{{ctx.Locale.Tr "repo.use_template"}} {{ctx.Locale.Tr "repo.use_template"}}
</a> </a>
{{end}} {{end}}
{{if (not $isHomepage)}} {{if $isHomepage}}
{{/* only show the "code search" on the repo home page, it only does global search,
so do not show it when viewing file or directory to avoid misleading users (it doesn't search in a directory) */}}
<form class="ignore-dirty" action="{{.RepoLink}}/search" method="get">
<div class="ui small action input">
<input name="q" placeholder="{{ctx.Locale.Tr "search.code_kind"}}">
{{template "shared/search/button"}}
</div>
</form>
{{else}}
<span class="breadcrumb repo-path tw-ml-1"> <span class="breadcrumb repo-path tw-ml-1">
<a class="section" href="{{.RepoLink}}/src/{{.BranchNameSubURL}}" title="{{.Repository.Name}}">{{StringUtils.EllipsisString .Repository.Name 30}}</a> <a class="section" href="{{.RepoLink}}/src/{{.BranchNameSubURL}}" title="{{.Repository.Name}}">{{StringUtils.EllipsisString .Repository.Name 30}}</a>
{{- range $i, $v := .TreeNames -}} {{- range $i, $v := .TreeNames -}}
@ -103,13 +112,6 @@
{{- end -}} {{- end -}}
</span> </span>
{{end}} {{end}}
<form class="ignore-dirty" action="{{.RepoLink}}/search" method="get">
<div class="ui small action input">
<input name="q" value="{{.Keyword}}" placeholder="{{ctx.Locale.Tr "search.code_kind"}}">
{{template "shared/search/button"}}
</div>
</form>
</div> </div>
<div class="tw-flex tw-items-center"> <div class="tw-flex tw-items-center">
<!-- Only show clone panel in repository home page --> <!-- Only show clone panel in repository home page -->
@ -136,7 +138,7 @@
</div> </div>
{{template "repo/cite/cite_modal" .}} {{template "repo/cite/cite_modal" .}}
{{end}} {{end}}
{{if and (not $isHomepage) (not .IsViewFile) (not .IsBlame)}} {{if and (not $isHomepage) (not .IsViewFile) (not .IsBlame)}}{{/* IsViewDirectory (not home), TODO: split the templates, avoid using "if" tricks */}}
<a class="ui button" href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/{{.TreePath | PathEscapeSegments}}"> <a class="ui button" href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/{{.TreePath | PathEscapeSegments}}">
{{svg "octicon-history" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.file_history"}} {{svg "octicon-history" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.file_history"}}
</a> </a>
@ -147,7 +149,7 @@
{{template "repo/view_file" .}} {{template "repo/view_file" .}}
{{else if .IsBlame}} {{else if .IsBlame}}
{{template "repo/blame" .}} {{template "repo/blame" .}}
{{else}} {{else}}{{/* IsViewDirectory */}}
{{template "repo/view_list" .}} {{template "repo/view_list" .}}
{{end}} {{end}}
</div> </div>

View File

@ -16,7 +16,7 @@
<span class="ui small label">{{if .NumFiles}}{{.NumFiles}}{{else}}-{{end}}</span> <span class="ui small label">{{if .NumFiles}}{{.NumFiles}}{{else}}-{{end}}</span>
</a> </a>
{{if or .Diff.TotalAddition .Diff.TotalDeletion}} {{if or .Diff.TotalAddition .Diff.TotalDeletion}}
<span class="item tw-ml-auto tw-pr-0 tw-font-bold tw-flex tw-items-center tw-gap-2"> <span class="tw-ml-auto tw-pl-3 tw-whitespace-nowrap tw-pr-0 tw-font-bold tw-flex tw-items-center tw-gap-2">
<span><span class="text green">{{if .Diff.TotalAddition}}+{{.Diff.TotalAddition}}{{end}}</span> <span class="text red">{{if .Diff.TotalDeletion}}-{{.Diff.TotalDeletion}}{{end}}</span></span> <span><span class="text green">{{if .Diff.TotalAddition}}+{{.Diff.TotalAddition}}{{end}}</span> <span class="text red">{{if .Diff.TotalDeletion}}-{{.Diff.TotalDeletion}}{{end}}</span></span>
<span class="diff-stats-bar"> <span class="diff-stats-bar">
<div class="diff-stats-add-bar" style="width: {{Eval 100 "*" .Diff.TotalAddition "/" "(" .Diff.TotalAddition "+" .Diff.TotalDeletion "+" 0.0 ")"}}%"></div> <div class="diff-stats-add-bar" style="width: {{Eval 100 "*" .Diff.TotalAddition "/" "(" .Diff.TotalAddition "+" .Diff.TotalDeletion "+" 0.0 ")"}}%"></div>

View File

@ -3843,6 +3843,54 @@
} }
} }
}, },
"/repos/{owner}/{repo}/actions/secrets": {
"get": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "List an repo's actions secrets",
"operationId": "repoListActionsSecrets",
"parameters": [
{
"type": "string",
"description": "owner of the repository",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repository",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "page number of results to return (1-based)",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "page size of results",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"$ref": "#/responses/SecretList"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/repos/{owner}/{repo}/actions/secrets/{secretname}": { "/repos/{owner}/{repo}/actions/secrets/{secretname}": {
"put": { "put": {
"consumes": [ "consumes": [
@ -25234,7 +25282,7 @@
} }
}, },
"RegistrationToken": { "RegistrationToken": {
"description": "RegistrationToken is response related to registeration token", "description": "RegistrationToken is response related to registration token",
"headers": { "headers": {
"token": { "token": {
"type": "string" "type": "string"

View File

@ -17,15 +17,12 @@
</overflow-menu> </overflow-menu>
<div class="ui middle very relaxed page grid"> <div class="ui middle very relaxed page grid">
<div class="column"> <div class="column">
<div class="ui tab {{if not .user_exists}}active{{end}}" <div class="ui tab {{if not .user_exists}}active{{end}}" data-tab="auth-link-signup-tab">
data-tab="auth-link-signup-tab"> {{if .AutoRegistrationFailedPrompt}}<div class="ui message">{{.AutoRegistrationFailedPrompt}}</div>{{end}}
{{template "user/auth/signup_inner" .}} {{template "user/auth/signup_inner" .}}
</div> </div>
<div class="ui tab {{if .user_exists}}active{{end}}" <div class="ui tab {{if .user_exists}}active{{end}}" data-tab="auth-link-signin-tab">
data-tab="auth-link-signin-tab"> {{template "user/auth/signin_inner" .}}
<div class="ui user signin container icon">
{{template "user/auth/signin_inner" .}}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -119,9 +119,9 @@ func TestAPIRepoIssueConfigPaths(t *testing.T) {
".github/issue_template/config", ".github/issue_template/config",
} }
for _, canidate := range templateConfigCandidates { for _, candidate := range templateConfigCandidates {
for _, extension := range []string{".yaml", ".yml"} { for _, extension := range []string{".yaml", ".yml"} {
fullPath := canidate + extension fullPath := candidate + extension
t.Run(fullPath, func(t *testing.T) { t.Run(fullPath, func(t *testing.T) {
configMap := make(map[string]any) configMap := make(map[string]any)
configMap["blank_issues_enabled"] = false configMap["blank_issues_enabled"] = false

View File

@ -24,6 +24,12 @@ func TestAPIRepoSecrets(t *testing.T) {
session := loginUser(t, user.Name) session := loginUser(t, user.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
t.Run("List", func(t *testing.T) {
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/secrets", repo.FullName())).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
})
t.Run("Create", func(t *testing.T) { t.Run("Create", func(t *testing.T) {
cases := []struct { cases := []struct {
Name string Name string
@ -31,7 +37,7 @@ func TestAPIRepoSecrets(t *testing.T) {
}{ }{
{ {
Name: "", Name: "",
ExpectedStatus: http.StatusNotFound, ExpectedStatus: http.StatusMethodNotAllowed,
}, },
{ {
Name: "-", Name: "-",

View File

@ -67,7 +67,7 @@ func TestCompareBranches(t *testing.T) {
session := loginUser(t, "user2") session := loginUser(t, "user2")
// Inderect compare remove-files-b (head) with add-csv (base) branch // Indirect compare remove-files-b (head) with add-csv (base) branch
// //
// 'link_hi' and 'test.csv' are deleted, 'test.txt' is added // 'link_hi' and 'test.csv' are deleted, 'test.txt' is added
req := NewRequest(t, "GET", "/user2/repo20/compare/add-csv...remove-files-b") req := NewRequest(t, "GET", "/user2/repo20/compare/add-csv...remove-files-b")
@ -79,7 +79,7 @@ func TestCompareBranches(t *testing.T) {
inspectCompare(t, htmlDoc, diffCount, diffChanges) inspectCompare(t, htmlDoc, diffCount, diffChanges)
// Inderect compare remove-files-b (head) with remove-files-a (base) branch // Indirect compare remove-files-b (head) with remove-files-a (base) branch
// //
// 'link_hi' and 'test.csv' are deleted, 'test.txt' is added // 'link_hi' and 'test.csv' are deleted, 'test.txt' is added
@ -92,7 +92,7 @@ func TestCompareBranches(t *testing.T) {
inspectCompare(t, htmlDoc, diffCount, diffChanges) inspectCompare(t, htmlDoc, diffCount, diffChanges)
// Inderect compare remove-files-a (head) with remove-files-b (base) branch // Indirect compare remove-files-a (head) with remove-files-b (base) branch
// //
// 'link_hi' and 'test.csv' are deleted // 'link_hi' and 'test.csv' are deleted

View File

@ -4,11 +4,13 @@
package integration package integration
import ( import (
"fmt"
"net/http" "net/http"
"net/url" "net/url"
"testing" "testing"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@ -46,22 +48,25 @@ func TestPullCompare(t *testing.T) {
testRepoFork(t, session, "user2", "repo1", "user1", "repo1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1")
testCreateBranch(t, session, "user1", "repo1", "branch/master", "master1", http.StatusSeeOther) testCreateBranch(t, session, "user1", "repo1", "branch/master", "master1", http.StatusSeeOther)
testEditFile(t, session, "user1", "repo1", "master1", "README.md", "Hello, World (Edited)\n") testEditFile(t, session, "user1", "repo1", "master1", "README.md", "Hello, World (Edited)\n")
resp = testPullCreate(t, session, "user1", "repo1", false, "master", "master1", "This is a pull title") testPullCreate(t, session, "user1", "repo1", false, "master", "master1", "This is a pull title")
// the max value on issue_index.yml for repo_id=1 is 5 repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"})
req = NewRequest(t, "GET", "/user2/repo1/pulls/6/files") issueIndex := unittest.AssertExistsAndLoadBean(t, &issues_model.IssueIndex{GroupID: repo1.ID}, unittest.OrderBy("group_id ASC"))
prFilesURL := fmt.Sprintf("/user2/repo1/pulls/%d/files", issueIndex.MaxIndex)
req = NewRequest(t, "GET", prFilesURL)
resp = session.MakeRequest(t, req, http.StatusOK) resp = session.MakeRequest(t, req, http.StatusOK)
doc := NewHTMLParser(t, resp.Body) doc := NewHTMLParser(t, resp.Body)
editButtonCount := doc.doc.Find(".diff-file-header-actions a[href*='/_edit/']").Length() editButtonCount := doc.doc.Find(".diff-file-header-actions a[href*='/_edit/']").Length()
assert.Greater(t, editButtonCount, 0, "Expected to find a button to edit a file in the PR diff view but there were none") assert.Greater(t, editButtonCount, 0, "Expected to find a button to edit a file in the PR diff view but there were none")
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
repoForked := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: "repo1"}) repoForked := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: "repo1"})
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
// delete the head repository and revisit the PR diff view // delete the head repository and revisit the PR diff view
err := repo_service.DeleteRepositoryDirectly(db.DefaultContext, user2, repoForked.ID) err := repo_service.DeleteRepositoryDirectly(db.DefaultContext, user2, repoForked.ID)
assert.NoError(t, err) assert.NoError(t, err)
req = NewRequest(t, "GET", "/user2/repo1/pulls/6/files") req = NewRequest(t, "GET", prFilesURL)
resp = session.MakeRequest(t, req, http.StatusOK) resp = session.MakeRequest(t, req, http.StatusOK)
doc = NewHTMLParser(t, resp.Body) doc = NewHTMLParser(t, resp.Body)
editButtonCount = doc.doc.Find(".diff-file-header-actions a[href*='/_edit/']").Length() editButtonCount = doc.doc.Find(".diff-file-header-actions a[href*='/_edit/']").Length()

View File

@ -5,12 +5,15 @@ package integration
import ( import (
"net/http" "net/http"
"net/http/httptest"
"net/url" "net/url"
"path"
"strings" "strings"
"testing" "testing"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
@ -176,3 +179,82 @@ func TestPullView_CodeOwner(t *testing.T) {
}) })
}) })
} }
func TestPullView_GivenApproveOrRejectReviewOnClosedPR(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
user1Session := loginUser(t, "user1")
user2Session := loginUser(t, "user2")
// Have user1 create a fork of repo1.
testRepoFork(t, user1Session, "user2", "repo1", "user1", "repo1")
t.Run("Submit approve/reject review on merged PR", func(t *testing.T) {
// Create a merged PR (made by user1) in the upstream repo1.
testEditFile(t, user1Session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
resp := testPullCreate(t, user1Session, "user1", "repo1", false, "master", "master", "This is a pull title")
elem := strings.Split(test.RedirectURL(resp), "/")
assert.EqualValues(t, "pulls", elem[3])
testPullMerge(t, user1Session, elem[1], elem[2], elem[4], repo_model.MergeStyleMerge, false)
// Grab the CSRF token.
req := NewRequest(t, "GET", path.Join(elem[1], elem[2], "pulls", elem[4]))
resp = user2Session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
// Submit an approve review on the PR.
testSubmitReview(t, user2Session, htmlDoc.GetCSRF(), "user2", "repo1", elem[4], "approve", http.StatusUnprocessableEntity)
// Submit a reject review on the PR.
testSubmitReview(t, user2Session, htmlDoc.GetCSRF(), "user2", "repo1", elem[4], "reject", http.StatusUnprocessableEntity)
})
t.Run("Submit approve/reject review on closed PR", func(t *testing.T) {
// Created a closed PR (made by user1) in the upstream repo1.
testEditFileToNewBranch(t, user1Session, "user1", "repo1", "master", "a-test-branch", "README.md", "Hello, World (Editied...again)\n")
resp := testPullCreate(t, user1Session, "user1", "repo1", false, "master", "a-test-branch", "This is a pull title")
elem := strings.Split(test.RedirectURL(resp), "/")
assert.EqualValues(t, "pulls", elem[3])
testIssueClose(t, user1Session, elem[1], elem[2], elem[4])
// Grab the CSRF token.
req := NewRequest(t, "GET", path.Join(elem[1], elem[2], "pulls", elem[4]))
resp = user2Session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
// Submit an approve review on the PR.
testSubmitReview(t, user2Session, htmlDoc.GetCSRF(), "user2", "repo1", elem[4], "approve", http.StatusUnprocessableEntity)
// Submit a reject review on the PR.
testSubmitReview(t, user2Session, htmlDoc.GetCSRF(), "user2", "repo1", elem[4], "reject", http.StatusUnprocessableEntity)
})
})
}
func testSubmitReview(t *testing.T, session *TestSession, csrf, owner, repo, pullNumber, reviewType string, expectedSubmitStatus int) *httptest.ResponseRecorder {
options := map[string]string{
"_csrf": csrf,
"commit_id": "",
"content": "test",
"type": reviewType,
}
submitURL := path.Join(owner, repo, "pulls", pullNumber, "files", "reviews", "submit")
req := NewRequestWithValues(t, "POST", submitURL, options)
return session.MakeRequest(t, req, expectedSubmitStatus)
}
func testIssueClose(t *testing.T, session *TestSession, owner, repo, issueNumber string) *httptest.ResponseRecorder {
req := NewRequest(t, "GET", path.Join(owner, repo, "pulls", issueNumber))
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
closeURL := path.Join(owner, repo, "issues", issueNumber, "comments")
options := map[string]string{
"_csrf": htmlDoc.GetCSRF(),
"status": "close",
}
req = NewRequestWithValues(t, "POST", closeURL, options)
return session.MakeRequest(t, req, http.StatusOK)
}

21
tools/misspellings.csv Normal file
View File

@ -0,0 +1,21 @@
acounts,accounts
canidate,candidate
comfirm,confirm
converage,coverage
currrently,currently
delimeter,delimiter
differrent,different
exclusing,excluding
finshed,finished
formated,formatted
inderect,indirect
insuficient,insufficient
likly,likely
mergable,mergeable
overrided,overridden
priortized,prioritized
registeration,registration
reuqest,request
reviwer,reviewer
superceded,superseded
underlaying,underlying
1 acounts accounts
2 canidate candidate
3 comfirm confirm
4 converage coverage
5 currrently currently
6 delimeter delimiter
7 differrent different
8 exclusing excluding
9 finshed finished
10 formated formatted
11 inderect indirect
12 insuficient insufficient
13 likly likely
14 mergable mergeable
15 overrided overridden
16 priortized prioritized
17 registeration registration
18 reuqest request
19 reviwer reviewer
20 superceded superseded
21 underlaying underlying

View File

@ -403,7 +403,7 @@
background: var(--color-body); background: var(--color-body);
border-top-width: 1px; border-top-width: 1px;
border-color: var(--color-secondary); border-color: var(--color-secondary);
font-weight: var(--font-weight-medium); color: var(--color-text-dark);
margin-bottom: -1px; margin-bottom: -1px;
border-radius: 0.28571429rem 0.28571429rem 0 0 !important; border-radius: 0.28571429rem 0.28571429rem 0 0 !important;
} }

View File

@ -2377,7 +2377,7 @@ tbody.commit-list {
.tag-code, .tag-code,
.tag-code td, .tag-code td,
.tag-code .blob-excerpt { .tag-code.line-expanded {
background-color: var(--color-box-body-highlight); background-color: var(--color-box-body-highlight);
vertical-align: middle; vertical-align: middle;
} }
@ -2393,8 +2393,8 @@ tbody.commit-list {
padding-top: 0 !important; padding-top: 0 !important;
} }
.blob-excerpt { .line-expanded {
background-color: var(--color-secondary-alpha-30); background-color: var(--color-secondary-alpha-20);
} }
.issue-keyword { .issue-keyword {
@ -2520,7 +2520,7 @@ tbody.commit-list {
display: inline-block; display: inline-block;
background-color: var(--color-red); background-color: var(--color-red);
height: 12px; height: 12px;
width: 40px; width: 44px;
} }
.diff-stats-bar .diff-stats-add-bar { .diff-stats-bar .diff-stats-add-bar {
@ -2553,11 +2553,9 @@ tbody.commit-list {
.code-diff-unified .add-code, .code-diff-unified .add-code,
.code-diff-unified .add-code td, .code-diff-unified .add-code td,
.code-diff-split .add-code .lines-num-new,
.code-diff-split .add-code .lines-type-marker-new, .code-diff-split .add-code .lines-type-marker-new,
.code-diff-split .add-code .lines-escape-new, .code-diff-split .add-code .lines-escape-new,
.code-diff-split .add-code .lines-code-new, .code-diff-split .add-code .lines-code-new,
.code-diff-split .del-code .add-code.lines-num-new,
.code-diff-split .del-code .add-code.lines-type-marker-new, .code-diff-split .del-code .add-code.lines-type-marker-new,
.code-diff-split .del-code .add-code.lines-escape-new, .code-diff-split .del-code .add-code.lines-escape-new,
.code-diff-split .del-code .add-code.lines-code-new { .code-diff-split .del-code .add-code.lines-code-new {
@ -2565,17 +2563,33 @@ tbody.commit-list {
border-color: var(--color-diff-added-row-border); border-color: var(--color-diff-added-row-border);
} }
.code-diff-split .del-code .lines-num-new,
.code-diff-split .del-code .lines-type-marker-new, .code-diff-split .del-code .lines-type-marker-new,
.code-diff-split .del-code .lines-code-new, .code-diff-split .del-code .lines-code-new,
.code-diff-split .del-code .lines-escape-new, .code-diff-split .del-code .lines-escape-new,
.code-diff-split .add-code .lines-num-old,
.code-diff-split .add-code .lines-escape-old, .code-diff-split .add-code .lines-escape-old,
.code-diff-split .add-code .lines-type-marker-old, .code-diff-split .add-code .lines-type-marker-old,
.code-diff-split .add-code .lines-code-old { .code-diff-split .add-code .lines-code-old {
background: var(--color-diff-inactive); background: var(--color-diff-inactive);
} }
.code-diff-split .add-code .lines-num.lines-num-old,
.code-diff-split .del-code .lines-num.lines-num-new {
background: var(--color-diff-inactive);
}
.code-diff-unified .del-code .lines-num,
.code-diff-split .del-code .lines-num {
background: var(--color-diff-removed-linenum-bg);
color: var(--color-text);
}
.code-diff-unified .add-code .lines-num,
.code-diff-split .add-code .lines-num,
.code-diff-split .del-code .add-code.lines-num {
background: var(--color-diff-added-linenum-bg);
color: var(--color-text);
}
.code-diff-split tbody tr td:nth-child(5), .code-diff-split tbody tr td:nth-child(5),
.code-diff-split tbody tr td.add-comment-right { .code-diff-split tbody tr td.add-comment-right {
border-left: 1px solid var(--color-secondary); border-left: 1px solid var(--color-secondary);

View File

@ -3,9 +3,10 @@
/* red/green colorblind-friendly colors */ /* red/green colorblind-friendly colors */
/* from GitHub: --diffBlob-addition-*, --diffBlob-deletion-*, etc */ /* from GitHub: --diffBlob-addition-*, --diffBlob-deletion-*, etc */
:root { :root {
--color-diff-added-word-bg: #388bfd66; --color-diff-added-linenum-bg: #1979fd46;
--color-diff-added-row-bg: #388bfd26; --color-diff-added-row-bg: #1979fd20;
--color-diff-added-word-bg: #1979fd66;
--color-diff-removed-word-bg: #db6d2866; --color-diff-removed-linenum-bg: #c8622146;
--color-diff-removed-row-bg: #db6d2826; --color-diff-removed-row-bg: #c8622120;
--color-diff-removed-word-bg: #c8622166;
} }

View File

@ -143,14 +143,16 @@
--color-grey-light: #818f9e; --color-grey-light: #818f9e;
--color-gold: #b1983b; --color-gold: #b1983b;
--color-white: #ffffff; --color-white: #ffffff;
--color-diff-removed-word-bg: #6f3333; --color-diff-added-linenum-bg: #274227;
--color-diff-added-word-bg: #3c653c; --color-diff-added-row-bg: #203224;
--color-diff-removed-row-bg: #3c2626;
--color-diff-moved-row-bg: #818044;
--color-diff-added-row-bg: #283e2d;
--color-diff-removed-row-border: #634343;
--color-diff-moved-row-border: #bcca6f;
--color-diff-added-row-border: #314a37; --color-diff-added-row-border: #314a37;
--color-diff-added-word-bg: #3c653c;
--color-diff-moved-row-bg: #818044;
--color-diff-moved-row-border: #bcca6f;
--color-diff-removed-linenum-bg: #482121;
--color-diff-removed-row-bg: #301e1e;
--color-diff-removed-row-border: #634343;
--color-diff-removed-word-bg: #6f3333;
--color-diff-inactive: #22282d; --color-diff-inactive: #22282d;
--color-error-border: #a04141; --color-error-border: #a04141;
--color-error-bg: #522; --color-error-bg: #522;

View File

@ -3,9 +3,10 @@
/* red/green colorblind-friendly colors */ /* red/green colorblind-friendly colors */
/* from GitHub: --diffBlob-addition-*, --diffBlob-deletion-*, etc */ /* from GitHub: --diffBlob-addition-*, --diffBlob-deletion-*, etc */
:root { :root {
--color-diff-added-word-bg: #54aeff66; --color-diff-added-linenum-bg: #54aeff4d;
--color-diff-added-row-bg: #ddf4ff80; --color-diff-added-row-bg: #ddf4ff80;
--color-diff-added-word-bg: #54aeff66;
--color-diff-removed-word-bg: #ffb77c80; --color-diff-removed-linenum-bg: #ffb77c4d;
--color-diff-removed-row-bg: #fff1e580; --color-diff-removed-row-bg: #fff1e580;
--color-diff-removed-word-bg: #ffb77c80;
} }

View File

@ -143,14 +143,16 @@
--color-grey-light: #7c838a; --color-grey-light: #7c838a;
--color-gold: #a1882b; --color-gold: #a1882b;
--color-white: #ffffff; --color-white: #ffffff;
--color-diff-removed-word-bg: #fdb8c0; --color-diff-added-linenum-bg: #d1f8d9;
--color-diff-added-word-bg: #acf2bd;
--color-diff-removed-row-bg: #ffeef0;
--color-diff-moved-row-bg: #f1f8d1;
--color-diff-added-row-bg: #e6ffed; --color-diff-added-row-bg: #e6ffed;
--color-diff-removed-row-border: #f1c0c0;
--color-diff-moved-row-border: #d0e27f;
--color-diff-added-row-border: #e6ffed; --color-diff-added-row-border: #e6ffed;
--color-diff-added-word-bg: #acf2bd;
--color-diff-moved-row-bg: #f1f8d1;
--color-diff-moved-row-border: #d0e27f;
--color-diff-removed-linenum-bg: #ffcecb;
--color-diff-removed-row-bg: #ffeef0;
--color-diff-removed-row-border: #f1c0c0;
--color-diff-removed-word-bg: #fdb8c0;
--color-diff-inactive: #f0f2f4; --color-diff-inactive: #f0f2f4;
--color-error-border: #e0b4b4; --color-error-border: #e0b4b4;
--color-error-bg: #fff6f6; --color-error-bg: #fff6f6;

View File

@ -6,18 +6,10 @@
// This file must be imported before any lazy-loading is being attempted. // This file must be imported before any lazy-loading is being attempted.
__webpack_public_path__ = `${window.config?.assetUrlPrefix ?? '/assets'}/`; __webpack_public_path__ = `${window.config?.assetUrlPrefix ?? '/assets'}/`;
const filteredErrors = new Set([
'getModifierState is not a function', // https://github.com/microsoft/monaco-editor/issues/4325
]);
export function showGlobalErrorMessage(msg) { export function showGlobalErrorMessage(msg) {
const pageContent = document.querySelector('.page-content'); const pageContent = document.querySelector('.page-content');
if (!pageContent) return; if (!pageContent) return;
for (const filteredError of filteredErrors) {
if (msg.includes(filteredError)) return;
}
// compact the message to a data attribute to avoid too many duplicated messages // compact the message to a data attribute to avoid too many duplicated messages
const msgCompact = msg.replace(/\W/g, '').trim(); const msgCompact = msg.replace(/\W/g, '').trim();
let msgDiv = pageContent.querySelector(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`); let msgDiv = pageContent.querySelector(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`);
@ -50,7 +42,7 @@ function processWindowErrorEvent({error, reason, message, type, filename, lineno
const assetBaseUrl = String(new URL(__webpack_public_path__, window.location.origin)); const assetBaseUrl = String(new URL(__webpack_public_path__, window.location.origin));
const {runModeIsProd} = window.config ?? {}; const {runModeIsProd} = window.config ?? {};
// `error` and `reason` are not guaranteed to be errors. If the value is falsy, it is likly a // `error` and `reason` are not guaranteed to be errors. If the value is falsy, it is likely a
// non-critical event from the browser. We log them but don't show them to users. Examples: // non-critical event from the browser. We log them but don't show them to users. Examples:
// - https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver#observation_errors // - https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver#observation_errors
// - https://github.com/mozilla-mobile/firefox-ios/issues/10817 // - https://github.com/mozilla-mobile/firefox-ios/issues/10817

View File

@ -44,6 +44,9 @@ const sfc = {
canApprove: false, canApprove: false,
canRerun: false, canRerun: false,
done: false, done: false,
workflowID: '',
workflowLink: '',
isSchedule: false,
jobs: [ jobs: [
// { // {
// id: 0, // id: 0,
@ -338,10 +341,13 @@ export function initRepositoryActionView() {
approve: el.getAttribute('data-locale-approve'), approve: el.getAttribute('data-locale-approve'),
cancel: el.getAttribute('data-locale-cancel'), cancel: el.getAttribute('data-locale-cancel'),
rerun: el.getAttribute('data-locale-rerun'), rerun: el.getAttribute('data-locale-rerun'),
rerun_all: el.getAttribute('data-locale-rerun-all'),
scheduled: el.getAttribute('data-locale-runs-scheduled'),
commit: el.getAttribute('data-locale-runs-commit'),
pushedBy: el.getAttribute('data-locale-runs-pushed-by'),
artifactsTitle: el.getAttribute('data-locale-artifacts-title'), artifactsTitle: el.getAttribute('data-locale-artifacts-title'),
areYouSure: el.getAttribute('data-locale-are-you-sure'), areYouSure: el.getAttribute('data-locale-are-you-sure'),
confirmDeleteArtifact: el.getAttribute('data-locale-confirm-delete-artifact'), confirmDeleteArtifact: el.getAttribute('data-locale-confirm-delete-artifact'),
rerun_all: el.getAttribute('data-locale-rerun-all'),
showTimeStamps: el.getAttribute('data-locale-show-timestamps'), showTimeStamps: el.getAttribute('data-locale-show-timestamps'),
showLogSeconds: el.getAttribute('data-locale-show-log-seconds'), showLogSeconds: el.getAttribute('data-locale-show-log-seconds'),
showFullScreen: el.getAttribute('data-locale-show-full-screen'), showFullScreen: el.getAttribute('data-locale-show-full-screen'),
@ -382,10 +388,16 @@ export function initRepositoryActionView() {
</button> </button>
</div> </div>
<div class="action-commit-summary"> <div class="action-commit-summary">
{{ run.commit.localeCommit }} <span><a class="muted" :href="run.workflowLink"><b>{{ run.workflowID }}</b></a>:</span>
<a class="muted" :href="run.commit.link">{{ run.commit.shortSHA }}</a> <template v-if="run.isSchedule">
{{ run.commit.localePushedBy }} {{ locale.scheduled }}
<a class="muted" :href="run.commit.pusher.link">{{ run.commit.pusher.displayName }}</a> </template>
<template v-else>
{{ locale.commit }}
<a class="muted" :href="run.commit.link">{{ run.commit.shortSHA }}</a>
{{ locale.pushedBy }}
<a class="muted" :href="run.commit.pusher.link">{{ run.commit.pusher.displayName }}</a>
</template>
<span class="ui label tw-max-w-full" v-if="run.commit.shortSHA"> <span class="ui label tw-max-w-full" v-if="run.commit.shortSHA">
<a class="gt-ellipsis" :href="run.commit.branch.link">{{ run.commit.branch.name }}</a> <a class="gt-ellipsis" :href="run.commit.branch.link">{{ run.commit.branch.name }}</a>
</span> </span>

View File

@ -1,6 +1,6 @@
import $ from 'jquery'; import $ from 'jquery';
import {GET} from '../modules/fetch.js'; import {GET} from '../modules/fetch.js';
import {hideElem, loadElem} from '../utils/dom.js'; import {hideElem, loadElem, queryElemChildren} from '../utils/dom.js';
import {parseDom} from '../utils.js'; import {parseDom} from '../utils.js';
function getDefaultSvgBoundsIfUndefined(text, src) { function getDefaultSvgBoundsIfUndefined(text, src) {
@ -38,36 +38,36 @@ function getDefaultSvgBoundsIfUndefined(text, src) {
return null; return null;
} }
function createContext(imageAfter, imageBefore) {
const sizeAfter = {
width: imageAfter?.width || 0,
height: imageAfter?.height || 0,
};
const sizeBefore = {
width: imageBefore?.width || 0,
height: imageBefore?.height || 0,
};
const maxSize = {
width: Math.max(sizeBefore.width, sizeAfter.width),
height: Math.max(sizeBefore.height, sizeAfter.height),
};
return {
imageAfter,
imageBefore,
sizeAfter,
sizeBefore,
maxSize,
ratio: [
Math.floor(maxSize.width - sizeAfter.width) / 2,
Math.floor(maxSize.height - sizeAfter.height) / 2,
Math.floor(maxSize.width - sizeBefore.width) / 2,
Math.floor(maxSize.height - sizeBefore.height) / 2,
],
};
}
export function initImageDiff() { export function initImageDiff() {
function createContext(image1, image2) {
const size1 = {
width: image1 && image1.width || 0,
height: image1 && image1.height || 0,
};
const size2 = {
width: image2 && image2.width || 0,
height: image2 && image2.height || 0,
};
const max = {
width: Math.max(size2.width, size1.width),
height: Math.max(size2.height, size1.height),
};
return {
$image1: $(image1),
$image2: $(image2),
size1,
size2,
max,
ratio: [
Math.floor(max.width - size1.width) / 2,
Math.floor(max.height - size1.height) / 2,
Math.floor(max.width - size2.width) / 2,
Math.floor(max.height - size2.height) / 2,
],
};
}
$('.image-diff:not([data-image-diff-loaded])').each(async function() { $('.image-diff:not([data-image-diff-loaded])').each(async function() {
const $container = $(this); const $container = $(this);
this.setAttribute('data-image-diff-loaded', 'true'); this.setAttribute('data-image-diff-loaded', 'true');
@ -116,94 +116,96 @@ export function initImageDiff() {
initOverlay(createContext($imagesAfter[2], $imagesBefore[2])); initOverlay(createContext($imagesAfter[2], $imagesBefore[2]));
} }
this.querySelector(':scope > .image-diff-tabs')?.classList.remove('is-loading'); queryElemChildren(this, '.image-diff-tabs', (el) => el.classList.remove('is-loading'));
function initSideBySide(container, sizes) { function initSideBySide(container, sizes) {
let factor = 1; let factor = 1;
if (sizes.max.width > (diffContainerWidth - 24) / 2) { if (sizes.maxSize.width > (diffContainerWidth - 24) / 2) {
factor = (diffContainerWidth - 24) / 2 / sizes.max.width; factor = (diffContainerWidth - 24) / 2 / sizes.maxSize.width;
} }
const widthChanged = sizes.$image1.length !== 0 && sizes.$image2.length !== 0 && sizes.$image1[0].naturalWidth !== sizes.$image2[0].naturalWidth; const widthChanged = sizes.imageAfter && sizes.imageBefore && sizes.imageAfter.naturalWidth !== sizes.imageBefore.naturalWidth;
const heightChanged = sizes.$image1.length !== 0 && sizes.$image2.length !== 0 && sizes.$image1[0].naturalHeight !== sizes.$image2[0].naturalHeight; const heightChanged = sizes.imageAfter && sizes.imageBefore && sizes.imageAfter.naturalHeight !== sizes.imageBefore.naturalHeight;
if (sizes.$image1?.length) { if (sizes.imageAfter) {
const boundsInfoAfterWidth = container.querySelector('.bounds-info-after .bounds-info-width'); const boundsInfoAfterWidth = container.querySelector('.bounds-info-after .bounds-info-width');
boundsInfoAfterWidth.textContent = `${sizes.$image1[0].naturalWidth}px`; if (boundsInfoAfterWidth) {
if (widthChanged) boundsInfoAfterWidth.classList.add('green'); boundsInfoAfterWidth.textContent = `${sizes.imageAfter.naturalWidth}px`;
boundsInfoAfterWidth.classList.toggle('green', widthChanged);
}
const boundsInfoAfterHeight = container.querySelector('.bounds-info-after .bounds-info-height'); const boundsInfoAfterHeight = container.querySelector('.bounds-info-after .bounds-info-height');
boundsInfoAfterHeight.textContent = `${sizes.$image1[0].naturalHeight}px`; if (boundsInfoAfterHeight) {
if (heightChanged) boundsInfoAfterHeight.classList.add('green'); boundsInfoAfterHeight.textContent = `${sizes.imageAfter.naturalHeight}px`;
boundsInfoAfterHeight.classList.toggle('green', heightChanged);
}
} }
if (sizes.$image2?.length) { if (sizes.imageBefore) {
const boundsInfoBeforeWidth = container.querySelector('.bounds-info-before .bounds-info-width'); const boundsInfoBeforeWidth = container.querySelector('.bounds-info-before .bounds-info-width');
boundsInfoBeforeWidth.textContent = `${sizes.$image2[0].naturalWidth}px`; if (boundsInfoBeforeWidth) {
if (widthChanged) boundsInfoBeforeWidth.classList.add('red'); boundsInfoBeforeWidth.textContent = `${sizes.imageBefore.naturalWidth}px`;
boundsInfoBeforeWidth.classList.toggle('red', widthChanged);
}
const boundsInfoBeforeHeight = container.querySelector('.bounds-info-before .bounds-info-height'); const boundsInfoBeforeHeight = container.querySelector('.bounds-info-before .bounds-info-height');
boundsInfoBeforeHeight.textContent = `${sizes.$image2[0].naturalHeight}px`; if (boundsInfoBeforeHeight) {
if (heightChanged) boundsInfoBeforeHeight.classList.add('red'); boundsInfoBeforeHeight.textContent = `${sizes.imageBefore.naturalHeight}px`;
boundsInfoBeforeHeight.classList.add('red', heightChanged);
}
} }
const image1 = sizes.$image1[0]; if (sizes.imageAfter) {
if (image1) { const container = sizes.imageAfter.parentNode;
const container = image1.parentNode; sizes.imageAfter.style.width = `${sizes.sizeAfter.width * factor}px`;
image1.style.width = `${sizes.size1.width * factor}px`; sizes.imageAfter.style.height = `${sizes.sizeAfter.height * factor}px`;
image1.style.height = `${sizes.size1.height * factor}px`;
container.style.margin = '10px auto'; container.style.margin = '10px auto';
container.style.width = `${sizes.size1.width * factor + 2}px`; container.style.width = `${sizes.sizeAfter.width * factor + 2}px`;
container.style.height = `${sizes.size1.height * factor + 2}px`; container.style.height = `${sizes.sizeAfter.height * factor + 2}px`;
} }
const image2 = sizes.$image2[0]; if (sizes.imageBefore) {
if (image2) { const container = sizes.imageBefore.parentNode;
const container = image2.parentNode; sizes.imageBefore.style.width = `${sizes.sizeBefore.width * factor}px`;
image2.style.width = `${sizes.size2.width * factor}px`; sizes.imageBefore.style.height = `${sizes.sizeBefore.height * factor}px`;
image2.style.height = `${sizes.size2.height * factor}px`;
container.style.margin = '10px auto'; container.style.margin = '10px auto';
container.style.width = `${sizes.size2.width * factor + 2}px`; container.style.width = `${sizes.sizeBefore.width * factor + 2}px`;
container.style.height = `${sizes.size2.height * factor + 2}px`; container.style.height = `${sizes.sizeBefore.height * factor + 2}px`;
} }
} }
function initSwipe(sizes) { function initSwipe(sizes) {
let factor = 1; let factor = 1;
if (sizes.max.width > diffContainerWidth - 12) { if (sizes.maxSize.width > diffContainerWidth - 12) {
factor = (diffContainerWidth - 12) / sizes.max.width; factor = (diffContainerWidth - 12) / sizes.maxSize.width;
} }
const image1 = sizes.$image1[0]; if (sizes.imageAfter) {
if (image1) { const container = sizes.imageAfter.parentNode;
const container = image1.parentNode;
const swipeFrame = container.parentNode; const swipeFrame = container.parentNode;
image1.style.width = `${sizes.size1.width * factor}px`; sizes.imageAfter.style.width = `${sizes.sizeAfter.width * factor}px`;
image1.style.height = `${sizes.size1.height * factor}px`; sizes.imageAfter.style.height = `${sizes.sizeAfter.height * factor}px`;
container.style.margin = `0px ${sizes.ratio[0] * factor}px`; container.style.margin = `0px ${sizes.ratio[0] * factor}px`;
container.style.width = `${sizes.size1.width * factor + 2}px`; container.style.width = `${sizes.sizeAfter.width * factor + 2}px`;
container.style.height = `${sizes.size1.height * factor + 2}px`; container.style.height = `${sizes.sizeAfter.height * factor + 2}px`;
swipeFrame.style.padding = `${sizes.ratio[1] * factor}px 0 0 0`; swipeFrame.style.padding = `${sizes.ratio[1] * factor}px 0 0 0`;
swipeFrame.style.width = `${sizes.max.width * factor + 2}px`; swipeFrame.style.width = `${sizes.maxSize.width * factor + 2}px`;
} }
const image2 = sizes.$image2[0]; if (sizes.imageBefore) {
if (image2) { const container = sizes.imageBefore.parentNode;
const container = image2.parentNode;
const swipeFrame = container.parentNode; const swipeFrame = container.parentNode;
image2.style.width = `${sizes.size2.width * factor}px`; sizes.imageBefore.style.width = `${sizes.sizeBefore.width * factor}px`;
image2.style.height = `${sizes.size2.height * factor}px`; sizes.imageBefore.style.height = `${sizes.sizeBefore.height * factor}px`;
container.style.margin = `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`; container.style.margin = `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`;
container.style.width = `${sizes.size2.width * factor + 2}px`; container.style.width = `${sizes.sizeBefore.width * factor + 2}px`;
container.style.height = `${sizes.size2.height * factor + 2}px`; container.style.height = `${sizes.sizeBefore.height * factor + 2}px`;
swipeFrame.style.width = `${sizes.max.width * factor + 2}px`; swipeFrame.style.width = `${sizes.maxSize.width * factor + 2}px`;
swipeFrame.style.height = `${sizes.max.height * factor + 2}px`; swipeFrame.style.height = `${sizes.maxSize.height * factor + 2}px`;
} }
// extra height for inner "position: absolute" elements // extra height for inner "position: absolute" elements
const swipe = $container.find('.diff-swipe')[0]; const swipe = $container.find('.diff-swipe')[0];
if (swipe) { if (swipe) {
swipe.style.width = `${sizes.max.width * factor + 2}px`; swipe.style.width = `${sizes.maxSize.width * factor + 2}px`;
swipe.style.height = `${sizes.max.height * factor + 30}px`; swipe.style.height = `${sizes.maxSize.height * factor + 30}px`;
} }
$container.find('.swipe-bar').on('mousedown', function(e) { $container.find('.swipe-bar').on('mousedown', function(e) {
@ -229,39 +231,37 @@ export function initImageDiff() {
function initOverlay(sizes) { function initOverlay(sizes) {
let factor = 1; let factor = 1;
if (sizes.max.width > diffContainerWidth - 12) { if (sizes.maxSize.width > diffContainerWidth - 12) {
factor = (diffContainerWidth - 12) / sizes.max.width; factor = (diffContainerWidth - 12) / sizes.maxSize.width;
} }
const image1 = sizes.$image1[0]; if (sizes.imageAfter) {
if (image1) { const container = sizes.imageAfter.parentNode;
const container = image1.parentNode; sizes.imageAfter.style.width = `${sizes.sizeAfter.width * factor}px`;
image1.style.width = `${sizes.size1.width * factor}px`; sizes.imageAfter.style.height = `${sizes.sizeAfter.height * factor}px`;
image1.style.height = `${sizes.size1.height * factor}px`;
container.style.margin = `${sizes.ratio[1] * factor}px ${sizes.ratio[0] * factor}px`; container.style.margin = `${sizes.ratio[1] * factor}px ${sizes.ratio[0] * factor}px`;
container.style.width = `${sizes.size1.width * factor + 2}px`; container.style.width = `${sizes.sizeAfter.width * factor + 2}px`;
container.style.height = `${sizes.size1.height * factor + 2}px`; container.style.height = `${sizes.sizeAfter.height * factor + 2}px`;
} }
const image2 = sizes.$image2[0]; if (sizes.imageBefore) {
if (image2) { const container = sizes.imageBefore.parentNode;
const container = image2.parentNode;
const overlayFrame = container.parentNode; const overlayFrame = container.parentNode;
image2.style.width = `${sizes.size2.width * factor}px`; sizes.imageBefore.style.width = `${sizes.sizeBefore.width * factor}px`;
image2.style.height = `${sizes.size2.height * factor}px`; sizes.imageBefore.style.height = `${sizes.sizeBefore.height * factor}px`;
container.style.margin = `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`; container.style.margin = `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`;
container.style.width = `${sizes.size2.width * factor + 2}px`; container.style.width = `${sizes.sizeBefore.width * factor + 2}px`;
container.style.height = `${sizes.size2.height * factor + 2}px`; container.style.height = `${sizes.sizeBefore.height * factor + 2}px`;
// some inner elements are `position: absolute`, so the container's height must be large enough // some inner elements are `position: absolute`, so the container's height must be large enough
overlayFrame.style.width = `${sizes.max.width * factor + 2}px`; overlayFrame.style.width = `${sizes.maxSize.width * factor + 2}px`;
overlayFrame.style.height = `${sizes.max.height * factor + 2}px`; overlayFrame.style.height = `${sizes.maxSize.height * factor + 2}px`;
} }
const rangeInput = $container[0].querySelector('input[type="range"]'); const rangeInput = $container[0].querySelector('input[type="range"]');
function updateOpacity() { function updateOpacity() {
if (sizes?.$image1?.[0]) { if (sizes.imageAfter) {
sizes.$image1[0].parentNode.style.opacity = `${rangeInput.value / 100}`; sizes.imageAfter.parentNode.style.opacity = `${rangeInput.value / 100}`;
} }
} }
rangeInput?.addEventListener('input', updateOpacity); rangeInput?.addEventListener('input', updateOpacity);