aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.changelog.yml17
-rw-r--r--.github/workflows/cron-licenses.yml4
-rw-r--r--MAINTAINERS1
-rw-r--r--Makefile2
-rw-r--r--cmd/admin_user_create.go31
-rw-r--r--cmd/admin_user_create_test.go62
-rw-r--r--main_timezones.go16
-rw-r--r--models/actions/runner.go9
-rw-r--r--models/actions/variable.go22
-rw-r--r--models/issues/issue.go40
-rw-r--r--models/issues/issue_project.go10
-rw-r--r--models/issues/issue_stats.go2
-rw-r--r--models/issues/review.go12
-rw-r--r--models/organization/org_worktime.go103
-rw-r--r--models/project/project.go4
-rw-r--r--models/system/notice_test.go2
-rw-r--r--models/user/user.go16
-rw-r--r--models/user/user_system.go2
-rw-r--r--modules/base/tool.go9
-rw-r--r--modules/base/tool_test.go11
-rw-r--r--modules/git/parse.go16
-rw-r--r--modules/git/tree_entry_mode.go27
-rw-r--r--modules/httplib/request.go13
-rw-r--r--modules/indexer/issues/util.go7
-rw-r--r--modules/lfs/http_client.go8
-rw-r--r--modules/lfstransfer/backend/backend.go40
-rw-r--r--modules/lfstransfer/backend/lock.go6
-rw-r--r--modules/lfstransfer/backend/util.go63
-rw-r--r--modules/markup/html.go2
-rw-r--r--modules/migration/downloader.go21
-rw-r--r--modules/migration/null_downloader.go23
-rw-r--r--modules/migration/retry_downloader.go43
-rw-r--r--modules/migration/uploader.go24
-rw-r--r--modules/private/actions.go2
-rw-r--r--modules/private/hook.go10
-rw-r--r--modules/private/internal.go8
-rw-r--r--modules/private/key.go4
-rw-r--r--modules/private/mail.go2
-rw-r--r--modules/private/manager.go22
-rw-r--r--modules/private/restore_repo.go2
-rw-r--r--modules/private/serv.go4
-rw-r--r--modules/structs/org.go9
-rw-r--r--modules/structs/repo_actions.go33
-rw-r--r--modules/templates/helper.go2
-rw-r--r--modules/util/error.go29
-rw-r--r--modules/util/sec_to_time.go10
-rw-r--r--modules/util/sec_to_time_test.go3
-rw-r--r--modules/web/middleware/binding.go2
-rw-r--r--options/gitignore/Flutter119
-rw-r--r--options/gitignore/Nix3
-rw-r--r--options/gitignore/NotesAndCoreConfiguration16
-rw-r--r--options/gitignore/NotesAndExtendedConfiguration38
-rw-r--r--options/gitignore/NotesOnly4
-rw-r--r--options/locale/locale_cs-CZ.ini4
-rw-r--r--options/locale/locale_de-DE.ini4
-rw-r--r--options/locale/locale_el-GR.ini4
-rw-r--r--options/locale/locale_en-US.ini20
-rw-r--r--options/locale/locale_es-ES.ini4
-rw-r--r--options/locale/locale_fa-IR.ini2
-rw-r--r--options/locale/locale_fi-FI.ini4
-rw-r--r--options/locale/locale_fr-FR.ini24
-rw-r--r--options/locale/locale_ga-IE.ini16
-rw-r--r--options/locale/locale_hu-HU.ini2
-rw-r--r--options/locale/locale_id-ID.ini2
-rw-r--r--options/locale/locale_is-IS.ini4
-rw-r--r--options/locale/locale_it-IT.ini4
-rw-r--r--options/locale/locale_ja-JP.ini4
-rw-r--r--options/locale/locale_ko-KR.ini2
-rw-r--r--options/locale/locale_lv-LV.ini4
-rw-r--r--options/locale/locale_nl-NL.ini4
-rw-r--r--options/locale/locale_pl-PL.ini2
-rw-r--r--options/locale/locale_pt-BR.ini4
-rw-r--r--options/locale/locale_pt-PT.ini16
-rw-r--r--options/locale/locale_ru-RU.ini4
-rw-r--r--options/locale/locale_si-LK.ini2
-rw-r--r--options/locale/locale_sk-SK.ini4
-rw-r--r--options/locale/locale_sv-SE.ini2
-rw-r--r--options/locale/locale_tr-TR.ini8
-rw-r--r--options/locale/locale_uk-UA.ini4
-rw-r--r--options/locale/locale_zh-CN.ini4
-rw-r--r--options/locale/locale_zh-HK.ini2
-rw-r--r--options/locale/locale_zh-TW.ini4
-rw-r--r--package-lock.json8
-rw-r--r--package.json2
-rw-r--r--public/assets/img/feishu.pngbin1982 -> 0 bytes
-rw-r--r--public/assets/img/svg/gitea-feishu.svg1
-rw-r--r--routers/api/actions/runner/runner.go2
-rw-r--r--routers/api/actions/runner/utils.go95
-rw-r--r--routers/api/v1/admin/user.go18
-rw-r--r--routers/api/v1/api.go37
-rw-r--r--routers/api/v1/org/action.go6
-rw-r--r--routers/api/v1/org/org.go38
-rw-r--r--routers/api/v1/repo/action.go276
-rw-r--r--routers/api/v1/repo/star.go2
-rw-r--r--routers/api/v1/swagger/action.go14
-rw-r--r--routers/api/v1/swagger/options.go6
-rw-r--r--routers/api/v1/user/action.go6
-rw-r--r--routers/api/v1/user/star.go8
-rw-r--r--routers/install/install.go1
-rw-r--r--routers/private/internal.go4
-rw-r--r--routers/web/org/home.go2
-rw-r--r--routers/web/org/worktime.go74
-rw-r--r--routers/web/repo/actions/view.go294
-rw-r--r--routers/web/repo/commit.go2
-rw-r--r--routers/web/repo/issue_suggestions.go52
-rw-r--r--routers/web/repo/issue_view.go81
-rw-r--r--routers/web/repo/repo.go91
-rw-r--r--routers/web/repo/setting/runners.go187
-rw-r--r--routers/web/repo/setting/variables.go140
-rw-r--r--routers/web/repo/setting/webhook.go1
-rw-r--r--routers/web/repo/star.go31
-rw-r--r--routers/web/repo/transfer.go38
-rw-r--r--routers/web/repo/watch.go31
-rw-r--r--routers/web/shared/actions/runners.go235
-rw-r--r--routers/web/shared/actions/variables.go182
-rw-r--r--routers/web/user/profile.go6
-rw-r--r--routers/web/web.go59
-rw-r--r--services/actions/cleanup.go11
-rw-r--r--services/actions/task.go107
-rw-r--r--services/actions/variables.go15
-rw-r--r--services/actions/workflow.go281
-rw-r--r--services/auth/auth.go2
-rw-r--r--services/auth/sspi.go2
-rw-r--r--services/context/api.go3
-rw-r--r--services/context/base.go4
-rw-r--r--services/context/context.go13
-rw-r--r--services/context/org.go330
-rw-r--r--services/context/package.go4
-rw-r--r--services/contexttest/context_tests.go1
-rw-r--r--services/forms/repo_form.go1
-rw-r--r--services/gitdiff/git_diff_tree.go249
-rw-r--r--services/gitdiff/git_diff_tree_test.go427
-rw-r--r--services/gitdiff/gitdiff_test.go17
-rw-r--r--services/gitdiff/testdata/academic-module/HEAD1
-rw-r--r--services/gitdiff/testdata/academic-module/config10
-rw-r--r--services/gitdiff/testdata/academic-module/indexbin46960 -> 0 bytes
-rw-r--r--services/gitdiff/testdata/academic-module/logs/HEAD1
-rw-r--r--services/gitdiff/testdata/academic-module/logs/refs/heads/master1
-rw-r--r--services/gitdiff/testdata/academic-module/logs/refs/remotes/origin/HEAD1
-rw-r--r--services/gitdiff/testdata/academic-module/objects/pack/pack-597efbc3613c7ba790e33b178fd9fc1fe17b4245.idxbin65332 -> 0 bytes
-rw-r--r--services/gitdiff/testdata/academic-module/objects/pack/pack-597efbc3613c7ba790e33b178fd9fc1fe17b4245.packbin1167905 -> 0 bytes
-rw-r--r--services/gitdiff/testdata/academic-module/packed-refs2
-rw-r--r--services/gitdiff/testdata/academic-module/refs/heads/master1
-rw-r--r--services/gitdiff/testdata/academic-module/refs/remotes/origin/HEAD1
-rw-r--r--services/issue/suggestion.go73
-rw-r--r--services/issue/suggestion_test.go57
-rw-r--r--services/lfs/server.go4
-rw-r--r--services/mailer/mail_test.go16
-rw-r--r--services/markup/renderhelper.go4
-rw-r--r--services/markup/renderhelper_codepreview.go4
-rw-r--r--services/markup/renderhelper_issueicontitle.go4
-rw-r--r--services/migrations/codebase.go50
-rw-r--r--services/migrations/codebase_test.go18
-rw-r--r--services/migrations/codecommit.go27
-rw-r--r--services/migrations/dump.go48
-rw-r--r--services/migrations/git.go8
-rw-r--r--services/migrations/gitea_downloader.go35
-rw-r--r--services/migrations/gitea_downloader_test.go26
-rw-r--r--services/migrations/gitea_uploader.go116
-rw-r--r--services/migrations/gitea_uploader_test.go24
-rw-r--r--services/migrations/github.go133
-rw-r--r--services/migrations/github_test.go25
-rw-r--r--services/migrations/gitlab.go61
-rw-r--r--services/migrations/gitlab_test.go29
-rw-r--r--services/migrations/gogs.go83
-rw-r--r--services/migrations/gogs_test.go16
-rw-r--r--services/migrations/migrate.go56
-rw-r--r--services/migrations/onedev.go52
-rw-r--r--services/migrations/onedev_test.go17
-rw-r--r--services/migrations/restore.go27
-rw-r--r--services/projects/issue.go31
-rw-r--r--services/repository/gitgraph/graph.go (renamed from modules/gitgraph/graph.go)0
-rw-r--r--services/repository/gitgraph/graph_models.go (renamed from modules/gitgraph/graph_models.go)0
-rw-r--r--services/repository/gitgraph/graph_test.go (renamed from modules/gitgraph/graph_test.go)0
-rw-r--r--services/repository/gitgraph/parser.go (renamed from modules/gitgraph/parser.go)0
-rw-r--r--services/webhook/dingtalk.go6
-rw-r--r--services/webhook/discord.go6
-rw-r--r--services/webhook/feishu.go6
-rw-r--r--services/webhook/general.go12
-rw-r--r--services/webhook/matrix.go7
-rw-r--r--services/webhook/msteams.go14
-rw-r--r--services/webhook/packagist.go4
-rw-r--r--services/webhook/payloader.go3
-rw-r--r--services/webhook/slack.go6
-rw-r--r--services/webhook/telegram.go6
-rw-r--r--services/webhook/wechatwork.go6
-rw-r--r--templates/admin/auth/list.tmpl2
-rw-r--r--templates/admin/emails/list.tmpl2
-rw-r--r--templates/admin/notice.tmpl2
-rw-r--r--templates/admin/org/list.tmpl2
-rw-r--r--templates/admin/packages/list.tmpl2
-rw-r--r--templates/admin/repo/list.tmpl2
-rw-r--r--templates/admin/user/edit.tmpl3
-rw-r--r--templates/admin/user/list.tmpl2
-rw-r--r--templates/org/home.tmpl1
-rw-r--r--templates/org/menu.tmpl5
-rw-r--r--templates/org/settings/options.tmpl4
-rw-r--r--templates/org/worktime.tmpl40
-rw-r--r--templates/org/worktime/table_members.tmpl16
-rw-r--r--templates/org/worktime/table_milestones.tmpl28
-rw-r--r--templates/org/worktime/table_repos.tmpl16
-rw-r--r--templates/projects/list.tmpl2
-rw-r--r--templates/repo/diff/box.tmpl6
-rw-r--r--templates/repo/diff/comments.tmpl4
-rw-r--r--templates/repo/diff/new_review.tmpl95
-rw-r--r--templates/repo/issue/filters.tmpl2
-rw-r--r--templates/repo/issue/list.tmpl2
-rw-r--r--templates/repo/issue/milestone_issues.tmpl2
-rw-r--r--templates/repo/issue/milestones.tmpl2
-rw-r--r--templates/repo/issue/sidebar/stopwatch_timetracker.tmpl4
-rw-r--r--templates/repo/issue/view_content/comments.tmpl6
-rw-r--r--templates/repo/settings/options.tmpl3
-rw-r--r--templates/repo/settings/webhook/settings.tmpl11
-rw-r--r--templates/shared/avatar_upload_crop.tmpl8
-rw-r--r--templates/shared/issuelist.tmpl2
-rw-r--r--templates/shared/user/authorlink.tmpl2
-rw-r--r--templates/shared/webhook/icon.tmpl2
-rw-r--r--templates/swagger/v1_json.tmpl427
-rw-r--r--templates/user/dashboard/dashboard.tmpl6
-rw-r--r--templates/user/dashboard/guide.tmpl10
-rw-r--r--templates/user/dashboard/milestones.tmpl2
-rw-r--r--templates/user/dashboard/repolist.tmpl4
-rw-r--r--templates/user/settings/keys_ssh.tmpl11
-rw-r--r--templates/user/settings/profile.tmpl8
-rw-r--r--tests/integration/actions_runner_modify_test.go151
-rw-r--r--tests/integration/actions_trigger_test.go741
-rw-r--r--tests/integration/actions_variables_test.go149
-rw-r--r--tests/integration/api_org_test.go276
-rw-r--r--tests/integration/api_user_star_test.go64
-rw-r--r--tests/integration/auth_ldap_test.go2
-rw-r--r--tests/integration/benchmarks_test.go69
-rw-r--r--tests/integration/org_worktime_test.go293
-rw-r--r--tests/integration/repo_webhook_test.go39
-rw-r--r--web_src/css/features/cropper.css2
-rw-r--r--web_src/js/components/ActionRunStatus.vue12
-rw-r--r--web_src/js/components/DashboardRepoList.vue53
-rw-r--r--web_src/js/components/RepoBranchTagSelector.vue6
-rw-r--r--web_src/js/features/admin/common.ts5
-rw-r--r--web_src/js/features/common-organization.ts5
-rw-r--r--web_src/js/features/comp/Cropper.ts9
-rw-r--r--web_src/js/features/comp/TextExpander.ts2
-rw-r--r--web_src/js/features/repo-issue.ts18
-rw-r--r--web_src/js/features/repo-settings.ts3
-rw-r--r--web_src/js/features/user-settings.ts13
-rw-r--r--web_src/js/svg.test.ts3
-rw-r--r--web_src/js/svg.ts11
-rw-r--r--web_src/svg/gitea-feishu.svg1
247 files changed, 6234 insertions, 2230 deletions
diff --git a/.changelog.yml b/.changelog.yml
index bfdee0c0ca..a7df8779de 100644
--- a/.changelog.yml
+++ b/.changelog.yml
@@ -23,20 +23,25 @@ groups:
labels:
- type/feature
-
- name: API
- labels:
- - modifies/api
- -
name: ENHANCEMENTS
labels:
- type/enhancement
- - type/refactoring
- - topic/ui
+ -
+ name: PERFORMANCE
+ labels:
+ - performance/memory
+ - performance/speed
+ - performance/bigrepo
+ - performance/cpu
-
name: BUGFIXES
labels:
- type/bug
-
+ name: API
+ labels:
+ - modifies/api
+ -
name: TESTING
labels:
- type/testing
diff --git a/.github/workflows/cron-licenses.yml b/.github/workflows/cron-licenses.yml
index cd8386ecc5..33cbc507d9 100644
--- a/.github/workflows/cron-licenses.yml
+++ b/.github/workflows/cron-licenses.yml
@@ -1,8 +1,8 @@
name: cron-licenses
on:
- schedule:
- - cron: "7 0 * * 1" # every Monday at 00:07 UTC
+ # schedule:
+ # - cron: "7 0 * * 1" # every Monday at 00:07 UTC
workflow_dispatch:
jobs:
diff --git a/MAINTAINERS b/MAINTAINERS
index f0caae4d22..7d21f449fe 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -63,3 +63,4 @@ Kemal Zebari <kemalzebra@gmail.com> (@kemzeb)
Rowan Bohde <rowan.bohde@gmail.com> (@bohde)
hiifong <i@hiif.ong> (@hiifong)
metiftikci <metiftikci@hotmail.com> (@metiftikci)
+Christopher Homberger <christopher.homberger@web.de> (@ChristopherHX)
diff --git a/Makefile b/Makefile
index 8a7855bd5a..8864a18c1a 100644
--- a/Makefile
+++ b/Makefile
@@ -393,7 +393,7 @@ lint-templates: .venv node_modules ## lint template files
.PHONY: lint-yaml
lint-yaml: .venv ## lint yaml files
- @poetry run yamllint .
+ @poetry run yamllint -s .
.PHONY: watch
watch: ## watch everything and continuously rebuild
diff --git a/cmd/admin_user_create.go b/cmd/admin_user_create.go
index bf8cbc7c4c..5e03d6ca3f 100644
--- a/cmd/admin_user_create.go
+++ b/cmd/admin_user_create.go
@@ -32,6 +32,11 @@ var microcmdUserCreate = &cli.Command{
Usage: "Username",
},
&cli.StringFlag{
+ Name: "user-type",
+ Usage: "Set user's type: individual or bot",
+ Value: "individual",
+ },
+ &cli.StringFlag{
Name: "password",
Usage: "User password",
},
@@ -77,6 +82,22 @@ func runCreateUser(c *cli.Context) error {
return err
}
+ userTypes := map[string]user_model.UserType{
+ "individual": user_model.UserTypeIndividual,
+ "bot": user_model.UserTypeBot,
+ }
+ userType, ok := userTypes[c.String("user-type")]
+ if !ok {
+ return fmt.Errorf("invalid user type: %s", c.String("user-type"))
+ }
+ if userType != user_model.UserTypeIndividual {
+ // Some other commands like "change-password" also only support individual users.
+ // It needs to clarify the "password" behavior for bot users in the future.
+ // At the moment, we do not allow setting password for bot users.
+ if c.IsSet("password") || c.IsSet("random-password") {
+ return errors.New("password can only be set for individual users")
+ }
+ }
if c.IsSet("name") && c.IsSet("username") {
return errors.New("cannot set both --name and --username flags")
}
@@ -118,16 +139,19 @@ func runCreateUser(c *cli.Context) error {
return err
}
fmt.Printf("generated random password is '%s'\n", password)
- } else {
+ } else if userType == user_model.UserTypeIndividual {
return errors.New("must set either password or random-password flag")
}
isAdmin := c.Bool("admin")
mustChangePassword := true // always default to true
if c.IsSet("must-change-password") {
+ if userType != user_model.UserTypeIndividual {
+ return errors.New("must-change-password flag can only be set for individual users")
+ }
// if the flag is set, use the value provided by the user
mustChangePassword = c.Bool("must-change-password")
- } else {
+ } else if userType == user_model.UserTypeIndividual {
// check whether there are users in the database
hasUserRecord, err := db.IsTableNotEmpty(&user_model.User{})
if err != nil {
@@ -151,8 +175,9 @@ func runCreateUser(c *cli.Context) error {
u := &user_model.User{
Name: username,
Email: c.String("email"),
- Passwd: password,
IsAdmin: isAdmin,
+ Type: userType,
+ Passwd: password,
MustChangePassword: mustChangePassword,
Visibility: visibility,
}
diff --git a/cmd/admin_user_create_test.go b/cmd/admin_user_create_test.go
index 83754e97b1..d8044e8de7 100644
--- a/cmd/admin_user_create_test.go
+++ b/cmd/admin_user_create_test.go
@@ -13,32 +13,54 @@ import (
user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func TestAdminUserCreate(t *testing.T) {
app := NewMainApp(AppVersion{})
reset := func() {
- assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
- assert.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
+ require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
+ require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
}
- type createCheck struct{ IsAdmin, MustChangePassword bool }
- createUser := func(name, args string) createCheck {
- assert.NoError(t, app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s --password foobar", name, name, args))))
- u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name})
- return createCheck{u.IsAdmin, u.MustChangePassword}
- }
- reset()
- assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u", ""), "first non-admin user doesn't need to change password")
-
- reset()
- assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u", "--admin"), "first admin user doesn't need to change password")
-
- reset()
- assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u", "--admin --must-change-password"))
- assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: true}, createUser("u2", "--admin"))
- assert.Equal(t, createCheck{IsAdmin: true, MustChangePassword: false}, createUser("u3", "--admin --must-change-password=false"))
- assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: true}, createUser("u4", ""))
- assert.Equal(t, createCheck{IsAdmin: false, MustChangePassword: false}, createUser("u5", "--must-change-password=false"))
+ t.Run("MustChangePassword", func(t *testing.T) {
+ type check struct {
+ IsAdmin bool
+ MustChangePassword bool
+ }
+ createCheck := func(name, args string) check {
+ require.NoError(t, app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s --password foobar", name, name, args))))
+ u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name})
+ return check{IsAdmin: u.IsAdmin, MustChangePassword: u.MustChangePassword}
+ }
+ reset()
+ assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u", ""), "first non-admin user doesn't need to change password")
+
+ reset()
+ assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u", "--admin"), "first admin user doesn't need to change password")
+
+ reset()
+ assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u", "--admin --must-change-password"))
+ assert.Equal(t, check{IsAdmin: true, MustChangePassword: true}, createCheck("u2", "--admin"))
+ assert.Equal(t, check{IsAdmin: true, MustChangePassword: false}, createCheck("u3", "--admin --must-change-password=false"))
+ assert.Equal(t, check{IsAdmin: false, MustChangePassword: true}, createCheck("u4", ""))
+ assert.Equal(t, check{IsAdmin: false, MustChangePassword: false}, createCheck("u5", "--must-change-password=false"))
+ })
+
+ t.Run("UserType", func(t *testing.T) {
+ createUser := func(name, args string) error {
+ return app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s", name, name, args)))
+ }
+
+ reset()
+ assert.ErrorContains(t, createUser("u", "--user-type invalid"), "invalid user type")
+ assert.ErrorContains(t, createUser("u", "--user-type bot --password 123"), "can only be set for individual users")
+ assert.ErrorContains(t, createUser("u", "--user-type bot --must-change-password"), "can only be set for individual users")
+
+ assert.NoError(t, createUser("u", "--user-type bot"))
+ u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "u"})
+ assert.Equal(t, user_model.UserTypeBot, u.Type)
+ assert.Equal(t, "", u.Passwd)
+ })
}
diff --git a/main_timezones.go b/main_timezones.go
new file mode 100644
index 0000000000..e1233007c6
--- /dev/null
+++ b/main_timezones.go
@@ -0,0 +1,16 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+//go:build windows
+
+package main
+
+// Golang has the ability to load OS's timezone data from most UNIX systems (https://github.com/golang/go/blob/master/src/time/zoneinfo_unix.go)
+// Even if the timezone data is missing, users could install the related packages to get it.
+// But on Windows, although `zoneinfo_windows.go` tries to load the timezone data from Windows registry,
+// some users still suffer from the issue that the timezone data is missing: https://github.com/go-gitea/gitea/issues/33235
+// So we import the tzdata package to make sure the timezone data is included in the binary.
+//
+// For non-Windows package builders, they could still use the "TAGS=timetzdata" to include the tzdata package in the binary.
+// If we decided to add the tzdata for other platforms, modify the "go:build" directive above.
+import _ "time/tzdata"
diff --git a/models/actions/runner.go b/models/actions/runner.go
index 0d5464a5be..798a647180 100644
--- a/models/actions/runner.go
+++ b/models/actions/runner.go
@@ -167,6 +167,7 @@ func init() {
type FindRunnerOptions struct {
db.ListOptions
+ IDs []int64
RepoID int64
OwnerID int64 // it will be ignored if RepoID is set
Sort string
@@ -178,6 +179,14 @@ type FindRunnerOptions struct {
func (opts FindRunnerOptions) ToConds() builder.Cond {
cond := builder.NewCond()
+ if len(opts.IDs) > 0 {
+ if len(opts.IDs) == 1 {
+ cond = cond.And(builder.Eq{"id": opts.IDs[0]})
+ } else {
+ cond = cond.And(builder.In("id", opts.IDs))
+ }
+ }
+
if opts.RepoID > 0 {
c := builder.NewCond().And(builder.Eq{"repo_id": opts.RepoID})
if opts.WithAvailable {
diff --git a/models/actions/variable.go b/models/actions/variable.go
index d0f917d923..163bb12c93 100644
--- a/models/actions/variable.go
+++ b/models/actions/variable.go
@@ -58,6 +58,7 @@ func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data strin
type FindVariablesOpts struct {
db.ListOptions
+ IDs []int64
RepoID int64
OwnerID int64 // it will be ignored if RepoID is set
Name string
@@ -65,6 +66,15 @@ type FindVariablesOpts struct {
func (opts FindVariablesOpts) ToConds() builder.Cond {
cond := builder.NewCond()
+
+ if len(opts.IDs) > 0 {
+ if len(opts.IDs) == 1 {
+ cond = cond.And(builder.Eq{"id": opts.IDs[0]})
+ } else {
+ cond = cond.And(builder.In("id", opts.IDs))
+ }
+ }
+
// Since we now support instance-level variables,
// there is no need to check for null values for `owner_id` and `repo_id`
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
@@ -85,12 +95,12 @@ func FindVariables(ctx context.Context, opts FindVariablesOpts) ([]*ActionVariab
return db.Find[ActionVariable](ctx, opts)
}
-func UpdateVariable(ctx context.Context, variable *ActionVariable) (bool, error) {
- count, err := db.GetEngine(ctx).ID(variable.ID).Cols("name", "data").
- Update(&ActionVariable{
- Name: variable.Name,
- Data: variable.Data,
- })
+func UpdateVariableCols(ctx context.Context, variable *ActionVariable, cols ...string) (bool, error) {
+ variable.Name = strings.ToUpper(variable.Name)
+ count, err := db.GetEngine(ctx).
+ ID(variable.ID).
+ Cols(cols...).
+ Update(variable)
return count != 0, err
}
diff --git a/models/issues/issue.go b/models/issues/issue.go
index 564a9fb835..5d52f0dd5d 100644
--- a/models/issues/issue.go
+++ b/models/issues/issue.go
@@ -17,6 +17,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
@@ -501,6 +502,45 @@ func GetIssueByIndex(ctx context.Context, repoID, index int64) (*Issue, error) {
return issue, nil
}
+func isPullToCond(isPull optional.Option[bool]) builder.Cond {
+ if isPull.Has() {
+ return builder.Eq{"is_pull": isPull.Value()}
+ }
+ return builder.NewCond()
+}
+
+func FindLatestUpdatedIssues(ctx context.Context, repoID int64, isPull optional.Option[bool], pageSize int) (IssueList, error) {
+ issues := make([]*Issue, 0, pageSize)
+ err := db.GetEngine(ctx).Where("repo_id = ?", repoID).
+ And(isPullToCond(isPull)).
+ OrderBy("updated_unix DESC").
+ Limit(pageSize).
+ Find(&issues)
+ return issues, err
+}
+
+func FindIssuesSuggestionByKeyword(ctx context.Context, repoID int64, keyword string, isPull optional.Option[bool], excludedID int64, pageSize int) (IssueList, error) {
+ cond := builder.NewCond()
+ if excludedID > 0 {
+ cond = cond.And(builder.Neq{"`id`": excludedID})
+ }
+
+ // It seems that GitHub searches both title and content (maybe sorting by the search engine's ranking system?)
+ // The first PR (https://github.com/go-gitea/gitea/pull/32327) uses "search indexer" to search "name(title) + content"
+ // But it seems that searching "content" (especially LIKE by DB engine) generates worse (unusable) results.
+ // So now (https://github.com/go-gitea/gitea/pull/33538) it only searches "name(title)", leave the improvements to the future.
+ cond = cond.And(db.BuildCaseInsensitiveLike("`name`", keyword))
+
+ issues := make([]*Issue, 0, pageSize)
+ err := db.GetEngine(ctx).Where("repo_id = ?", repoID).
+ And(isPullToCond(isPull)).
+ And(cond).
+ OrderBy("updated_unix DESC, `index` DESC").
+ Limit(pageSize).
+ Find(&issues)
+ return issues, err
+}
+
// GetIssueWithAttrsByIndex returns issue by index in a repository.
func GetIssueWithAttrsByIndex(ctx context.Context, repoID, index int64) (*Issue, error) {
issue, err := GetIssueByIndex(ctx, repoID, index)
diff --git a/models/issues/issue_project.go b/models/issues/issue_project.go
index c4515fd898..f520604321 100644
--- a/models/issues/issue_project.go
+++ b/models/issues/issue_project.go
@@ -38,13 +38,15 @@ func (issue *Issue) projectID(ctx context.Context) int64 {
}
// ProjectColumnID return project column id if issue was assigned to one
-func (issue *Issue) ProjectColumnID(ctx context.Context) int64 {
+func (issue *Issue) ProjectColumnID(ctx context.Context) (int64, error) {
var ip project_model.ProjectIssue
has, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Get(&ip)
- if err != nil || !has {
- return 0
+ if err != nil {
+ return 0, err
+ } else if !has {
+ return 0, nil
}
- return ip.ProjectColumnID
+ return ip.ProjectColumnID, nil
}
// LoadIssuesFromColumn load issues assigned to this column
diff --git a/models/issues/issue_stats.go b/models/issues/issue_stats.go
index 9ef9347a16..50409fbbd8 100644
--- a/models/issues/issue_stats.go
+++ b/models/issues/issue_stats.go
@@ -107,7 +107,7 @@ func GetIssueStats(ctx context.Context, opts *IssuesOptions) (*IssueStats, error
accum.YourRepositoriesCount += stats.YourRepositoriesCount
accum.AssignCount += stats.AssignCount
accum.CreateCount += stats.CreateCount
- accum.OpenCount += stats.MentionCount
+ accum.MentionCount += stats.MentionCount
accum.ReviewRequestedCount += stats.ReviewRequestedCount
accum.ReviewedCount += stats.ReviewedCount
i = chunk
diff --git a/models/issues/review.go b/models/issues/review.go
index 3e787273be..1c5c2ee30a 100644
--- a/models/issues/review.go
+++ b/models/issues/review.go
@@ -930,17 +930,19 @@ func MarkConversation(ctx context.Context, comment *Comment, doer *user_model.Us
}
// CanMarkConversation Add or remove Conversation mark for a code comment permission check
-// the PR writer , offfcial reviewer and poster can do it
+// the PR writer , official reviewer and poster can do it
func CanMarkConversation(ctx context.Context, issue *Issue, doer *user_model.User) (permResult bool, err error) {
if doer == nil || issue == nil {
return false, fmt.Errorf("issue or doer is nil")
}
+ if err = issue.LoadRepo(ctx); err != nil {
+ return false, err
+ }
+ if issue.Repo.IsArchived {
+ return false, nil
+ }
if doer.ID != issue.PosterID {
- if err = issue.LoadRepo(ctx); err != nil {
- return false, err
- }
-
p, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
if err != nil {
return false, err
diff --git a/models/organization/org_worktime.go b/models/organization/org_worktime.go
new file mode 100644
index 0000000000..7b57182a8a
--- /dev/null
+++ b/models/organization/org_worktime.go
@@ -0,0 +1,103 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package organization
+
+import (
+ "sort"
+
+ "code.gitea.io/gitea/models/db"
+
+ "xorm.io/builder"
+)
+
+type WorktimeSumByRepos struct {
+ RepoName string
+ SumTime int64
+}
+
+func GetWorktimeByRepos(org *Organization, unitFrom, unixTo int64) (results []WorktimeSumByRepos, err error) {
+ err = db.GetEngine(db.DefaultContext).
+ Select("repository.name AS repo_name, SUM(tracked_time.time) AS sum_time").
+ Table("tracked_time").
+ Join("INNER", "issue", "tracked_time.issue_id = issue.id").
+ Join("INNER", "repository", "issue.repo_id = repository.id").
+ Where(builder.Eq{"repository.owner_id": org.ID}).
+ And(builder.Eq{"tracked_time.deleted": false}).
+ And(builder.Gte{"tracked_time.created_unix": unitFrom}).
+ And(builder.Lte{"tracked_time.created_unix": unixTo}).
+ GroupBy("repository.name").
+ OrderBy("repository.name").
+ Find(&results)
+ return results, err
+}
+
+type WorktimeSumByMilestones struct {
+ RepoName string
+ MilestoneName string
+ MilestoneID int64
+ MilestoneDeadline int64
+ SumTime int64
+ HideRepoName bool
+}
+
+func GetWorktimeByMilestones(org *Organization, unitFrom, unixTo int64) (results []WorktimeSumByMilestones, err error) {
+ err = db.GetEngine(db.DefaultContext).
+ Select("repository.name AS repo_name, milestone.name AS milestone_name, milestone.id AS milestone_id, milestone.deadline_unix as milestone_deadline, SUM(tracked_time.time) AS sum_time").
+ Table("tracked_time").
+ Join("INNER", "issue", "tracked_time.issue_id = issue.id").
+ Join("INNER", "repository", "issue.repo_id = repository.id").
+ Join("LEFT", "milestone", "issue.milestone_id = milestone.id").
+ Where(builder.Eq{"repository.owner_id": org.ID}).
+ And(builder.Eq{"tracked_time.deleted": false}).
+ And(builder.Gte{"tracked_time.created_unix": unitFrom}).
+ And(builder.Lte{"tracked_time.created_unix": unixTo}).
+ GroupBy("repository.name, milestone.name, milestone.deadline_unix, milestone.id").
+ OrderBy("repository.name, milestone.deadline_unix, milestone.id").
+ Find(&results)
+
+ // TODO: pgsql: NULL values are sorted last in default ascending order, so we need to sort them manually again.
+ sort.Slice(results, func(i, j int) bool {
+ if results[i].RepoName != results[j].RepoName {
+ return results[i].RepoName < results[j].RepoName
+ }
+ if results[i].MilestoneDeadline != results[j].MilestoneDeadline {
+ return results[i].MilestoneDeadline < results[j].MilestoneDeadline
+ }
+ return results[i].MilestoneID < results[j].MilestoneID
+ })
+
+ // Show only the first RepoName, for nicer output.
+ prevRepoName := ""
+ for i := 0; i < len(results); i++ {
+ res := &results[i]
+ res.MilestoneDeadline = 0 // clear the deadline because we do not really need it
+ if prevRepoName == res.RepoName {
+ res.HideRepoName = true
+ }
+ prevRepoName = res.RepoName
+ }
+ return results, err
+}
+
+type WorktimeSumByMembers struct {
+ UserName string
+ SumTime int64
+}
+
+func GetWorktimeByMembers(org *Organization, unitFrom, unixTo int64) (results []WorktimeSumByMembers, err error) {
+ err = db.GetEngine(db.DefaultContext).
+ Select("`user`.name AS user_name, SUM(tracked_time.time) AS sum_time").
+ Table("tracked_time").
+ Join("INNER", "issue", "tracked_time.issue_id = issue.id").
+ Join("INNER", "repository", "issue.repo_id = repository.id").
+ Join("INNER", "`user`", "tracked_time.user_id = `user`.id").
+ Where(builder.Eq{"repository.owner_id": org.ID}).
+ And(builder.Eq{"tracked_time.deleted": false}).
+ And(builder.Gte{"tracked_time.created_unix": unitFrom}).
+ And(builder.Lte{"tracked_time.created_unix": unixTo}).
+ GroupBy("`user`.name").
+ OrderBy("sum_time DESC").
+ Find(&results)
+ return results, err
+}
diff --git a/models/project/project.go b/models/project/project.go
index edeb0b4742..20b5df0b6e 100644
--- a/models/project/project.go
+++ b/models/project/project.go
@@ -244,6 +244,10 @@ func GetSearchOrderByBySortType(sortType string) db.SearchOrderBy {
return db.SearchOrderByRecentUpdated
case "leastupdate":
return db.SearchOrderByLeastUpdated
+ case "alphabetically":
+ return "title ASC"
+ case "reversealphabetically":
+ return "title DESC"
default:
return db.SearchOrderByNewest
}
diff --git a/models/system/notice_test.go b/models/system/notice_test.go
index 599b2fb65c..9fc9e6cce1 100644
--- a/models/system/notice_test.go
+++ b/models/system/notice_test.go
@@ -45,8 +45,6 @@ func TestCreateRepositoryNotice(t *testing.T) {
unittest.AssertExistsAndLoadBean(t, noticeBean)
}
-// TODO TestRemoveAllWithNotice
-
func TestCountNotices(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
assert.Equal(t, int64(3), system.CountNotices(db.DefaultContext))
diff --git a/models/user/user.go b/models/user/user.go
index e13fb6ab3c..293c876957 100644
--- a/models/user/user.go
+++ b/models/user/user.go
@@ -385,11 +385,12 @@ func (u *User) ValidatePassword(passwd string) bool {
}
// IsPasswordSet checks if the password is set or left empty
+// TODO: It's better to clarify the "password" behavior for different types (individual, bot)
func (u *User) IsPasswordSet() bool {
- return len(u.Passwd) != 0
+ return u.Passwd != ""
}
-// IsOrganization returns true if user is actually a organization.
+// IsOrganization returns true if user is actually an organization.
func (u *User) IsOrganization() bool {
return u.Type == UserTypeOrganization
}
@@ -399,13 +400,14 @@ func (u *User) IsIndividual() bool {
return u.Type == UserTypeIndividual
}
-func (u *User) IsUser() bool {
- return u.Type == UserTypeIndividual || u.Type == UserTypeBot
+// IsTypeBot returns whether the user is of type bot
+func (u *User) IsTypeBot() bool {
+ return u.Type == UserTypeBot
}
-// IsBot returns whether or not the user is of type bot
-func (u *User) IsBot() bool {
- return u.Type == UserTypeBot
+// IsTokenAccessAllowed returns whether the user is an individual or a bot (which allows for token access)
+func (u *User) IsTokenAccessAllowed() bool {
+ return u.Type == UserTypeIndividual || u.Type == UserTypeBot
}
// DisplayName returns full name if it's not empty,
diff --git a/models/user/user_system.go b/models/user/user_system.go
index e54973dc8e..6fbfd9e69e 100644
--- a/models/user/user_system.go
+++ b/models/user/user_system.go
@@ -56,7 +56,7 @@ func NewActionsUser() *User {
Email: ActionsUserEmail,
KeepEmailPrivate: true,
LoginName: ActionsUserName,
- Type: UserTypeIndividual,
+ Type: UserTypeBot,
AllowCreateOrganization: true,
Visibility: structs.VisibleTypePublic,
}
diff --git a/modules/base/tool.go b/modules/base/tool.go
index 1d16186bc5..b6ed8cbf9a 100644
--- a/modules/base/tool.go
+++ b/modules/base/tool.go
@@ -18,7 +18,6 @@ import (
"time"
"code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
@@ -64,10 +63,7 @@ func VerifyTimeLimitCode(now time.Time, data string, minutes int, code string) b
// check code
retCode := CreateTimeLimitCode(data, aliveTime, startTimeStr, nil)
if subtle.ConstantTimeCompare([]byte(retCode), []byte(code)) != 1 {
- retCode = CreateTimeLimitCode(data, aliveTime, startTimeStr, sha1.New()) // TODO: this is only for the support of legacy codes, remove this in/after 1.23
- if subtle.ConstantTimeCompare([]byte(retCode), []byte(code)) != 1 {
- return false
- }
+ return false
}
// check time is expired or not: startTime <= now && now < startTime + minutes
@@ -144,13 +140,12 @@ func Int64sToStrings(ints []int64) []string {
return strs
}
-// EntryIcon returns the octicon class for displaying files/directories
+// EntryIcon returns the octicon name for displaying files/directories
func EntryIcon(entry *git.TreeEntry) string {
switch {
case entry.IsLink():
te, err := entry.FollowLink()
if err != nil {
- log.Debug(err.Error())
return "file-symlink-file"
}
if te.IsDir() {
diff --git a/modules/base/tool_test.go b/modules/base/tool_test.go
index c821a55c19..7cebedb073 100644
--- a/modules/base/tool_test.go
+++ b/modules/base/tool_test.go
@@ -86,13 +86,10 @@ JWT_SECRET = %s
verifyDataCode := func(c string) bool {
return VerifyTimeLimitCode(now, "data", 2, c)
}
- code1 := CreateTimeLimitCode("data", 2, now, sha1.New())
- code2 := CreateTimeLimitCode("data", 2, now, nil)
- assert.True(t, verifyDataCode(code1))
- assert.True(t, verifyDataCode(code2))
+ code := CreateTimeLimitCode("data", 2, now, nil)
+ assert.True(t, verifyDataCode(code))
initGeneralSecret("000_QLUd4fYVyxetjxC4eZkrBgWM2SndOOWDNtgUUko")
- assert.False(t, verifyDataCode(code1))
- assert.False(t, verifyDataCode(code2))
+ assert.False(t, verifyDataCode(code))
})
}
@@ -137,5 +134,3 @@ func TestInt64sToStrings(t *testing.T) {
Int64sToStrings([]int64{1, 4, 16, 64, 256}),
)
}
-
-// TODO: Test EntryIcon
diff --git a/modules/git/parse.go b/modules/git/parse.go
index eb26632cc0..a7f5c58e89 100644
--- a/modules/git/parse.go
+++ b/modules/git/parse.go
@@ -46,19 +46,9 @@ func parseLsTreeLine(line []byte) (*LsTreeEntry, error) {
entry.Size = optional.Some(size)
}
- switch string(entryMode) {
- case "100644":
- entry.EntryMode = EntryModeBlob
- case "100755":
- entry.EntryMode = EntryModeExec
- case "120000":
- entry.EntryMode = EntryModeSymlink
- case "160000":
- entry.EntryMode = EntryModeCommit
- case "040000", "040755": // git uses 040000 for tree object, but some users may get 040755 for unknown reasons
- entry.EntryMode = EntryModeTree
- default:
- return nil, fmt.Errorf("unknown type: %v", string(entryMode))
+ entry.EntryMode, err = ParseEntryMode(string(entryMode))
+ if err != nil || entry.EntryMode == EntryModeNoEntry {
+ return nil, fmt.Errorf("invalid ls-tree output (invalid mode): %q, err: %w", line, err)
}
entry.ID, err = NewIDFromString(string(entryObjectID))
diff --git a/modules/git/tree_entry_mode.go b/modules/git/tree_entry_mode.go
index a399118cf8..ec4487549d 100644
--- a/modules/git/tree_entry_mode.go
+++ b/modules/git/tree_entry_mode.go
@@ -3,7 +3,10 @@
package git
-import "strconv"
+import (
+ "fmt"
+ "strconv"
+)
// EntryMode the type of the object in the git tree
type EntryMode int
@@ -11,6 +14,9 @@ type EntryMode int
// There are only a few file modes in Git. They look like unix file modes, but they can only be
// one of these.
const (
+ // EntryModeNoEntry is possible if the file was added or removed in a commit. In the case of
+ // added the base commit will not have the file in its tree so a mode of 0o000000 is used.
+ EntryModeNoEntry EntryMode = 0o000000
// EntryModeBlob
EntryModeBlob EntryMode = 0o100644
// EntryModeExec
@@ -33,3 +39,22 @@ func ToEntryMode(value string) EntryMode {
v, _ := strconv.ParseInt(value, 8, 32)
return EntryMode(v)
}
+
+func ParseEntryMode(mode string) (EntryMode, error) {
+ switch mode {
+ case "000000":
+ return EntryModeNoEntry, nil
+ case "100644":
+ return EntryModeBlob, nil
+ case "100755":
+ return EntryModeExec, nil
+ case "120000":
+ return EntryModeSymlink, nil
+ case "160000":
+ return EntryModeCommit, nil
+ case "040000", "040755": // git uses 040000 for tree object, but some users may get 040755 for unknown reasons
+ return EntryModeTree, nil
+ default:
+ return 0, fmt.Errorf("unparsable entry mode: %s", mode)
+ }
+}
diff --git a/modules/httplib/request.go b/modules/httplib/request.go
index 880d7ad3cb..267e276df3 100644
--- a/modules/httplib/request.go
+++ b/modules/httplib/request.go
@@ -99,10 +99,10 @@ func (r *Request) Param(key, value string) *Request {
return r
}
-// Body adds request raw body.
-// it supports string and []byte.
+// Body adds request raw body. It supports string, []byte and io.Reader as body.
func (r *Request) Body(data any) *Request {
switch t := data.(type) {
+ case nil: // do nothing
case string:
bf := bytes.NewBufferString(t)
r.req.Body = io.NopCloser(bf)
@@ -111,6 +111,12 @@ func (r *Request) Body(data any) *Request {
bf := bytes.NewBuffer(t)
r.req.Body = io.NopCloser(bf)
r.req.ContentLength = int64(len(t))
+ case io.ReadCloser:
+ r.req.Body = t
+ case io.Reader:
+ r.req.Body = io.NopCloser(t)
+ default:
+ panic(fmt.Sprintf("unsupported request body type %T", t))
}
return r
}
@@ -141,7 +147,7 @@ func (r *Request) getResponse() (*http.Response, error) {
}
} else if r.req.Method == "POST" && r.req.Body == nil && len(paramBody) > 0 {
r.Header("Content-Type", "application/x-www-form-urlencoded")
- r.Body(paramBody)
+ r.Body(paramBody) // string
}
var err error
@@ -185,6 +191,7 @@ func (r *Request) getResponse() (*http.Response, error) {
}
// Response executes request client gets response manually.
+// Caller MUST close the response body if no error occurs
func (r *Request) Response() (*http.Response, error) {
return r.getResponse()
}
diff --git a/modules/indexer/issues/util.go b/modules/indexer/issues/util.go
index deb19adc49..19d835a1d8 100644
--- a/modules/indexer/issues/util.go
+++ b/modules/indexer/issues/util.go
@@ -92,6 +92,11 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD
projectID = issue.Project.ID
}
+ projectColumnID, err := issue.ProjectColumnID(ctx)
+ if err != nil {
+ return nil, false, err
+ }
+
return &internal.IndexerData{
ID: issue.ID,
RepoID: issue.RepoID,
@@ -106,7 +111,7 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD
NoLabel: len(labels) == 0,
MilestoneID: issue.MilestoneID,
ProjectID: projectID,
- ProjectColumnID: issue.ProjectColumnID(ctx),
+ ProjectColumnID: projectColumnID,
PosterID: issue.PosterID,
AssigneeID: issue.AssigneeID,
MentionIDs: mentionIDs,
diff --git a/modules/lfs/http_client.go b/modules/lfs/http_client.go
index 3acd23b8f7..0a27fb0c86 100644
--- a/modules/lfs/http_client.go
+++ b/modules/lfs/http_client.go
@@ -72,10 +72,14 @@ func (c *HTTPClient) batch(ctx context.Context, operation string, objects []Poin
url := fmt.Sprintf("%s/objects/batch", c.endpoint)
+ // Original: In some lfs server implementations, they require the ref attribute. #32838
// `ref` is an "optional object describing the server ref that the objects belong to"
- // but some (incorrect) lfs servers require it, so maybe adding an empty ref here doesn't break the correct ones.
+ // but some (incorrect) lfs servers like aliyun require it, so maybe adding an empty ref here doesn't break the correct ones.
// https://github.com/git-lfs/git-lfs/blob/a32a02b44bf8a511aa14f047627c49e1a7fd5021/docs/api/batch.md?plain=1#L37
- request := &BatchRequest{operation, c.transferNames(), &Reference{}, objects}
+ //
+ // UPDATE: it can't use "empty ref" here because it breaks others like https://github.com/go-gitea/gitea/issues/33453
+ request := &BatchRequest{operation, c.transferNames(), nil, objects}
+
payload := new(bytes.Buffer)
err := json.NewEncoder(payload).Encode(request)
if err != nil {
diff --git a/modules/lfstransfer/backend/backend.go b/modules/lfstransfer/backend/backend.go
index 2b1fe49fda..540932b930 100644
--- a/modules/lfstransfer/backend/backend.go
+++ b/modules/lfstransfer/backend/backend.go
@@ -4,7 +4,6 @@
package backend
import (
- "bytes"
"context"
"encoding/base64"
"fmt"
@@ -29,7 +28,7 @@ var Capabilities = []string{
"locking",
}
-var _ transfer.Backend = &GiteaBackend{}
+var _ transfer.Backend = (*GiteaBackend)(nil)
// GiteaBackend is an adapter between git-lfs-transfer library and Gitea's internal LFS API
type GiteaBackend struct {
@@ -78,17 +77,17 @@ func (g *GiteaBackend) Batch(_ string, pointers []transfer.BatchItem, args trans
headerAccept: mimeGitLFS,
headerContentType: mimeGitLFS,
}
- req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes)
+ req := newInternalRequestLFS(g.ctx, url, http.MethodPost, headers, bodyBytes)
resp, err := req.Response()
if err != nil {
g.logger.Log("http request error", err)
return nil, err
}
+ defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
g.logger.Log("http statuscode error", resp.StatusCode, statusCodeToErr(resp.StatusCode))
return nil, statusCodeToErr(resp.StatusCode)
}
- defer resp.Body.Close()
respBytes, err := io.ReadAll(resp.Body)
if err != nil {
g.logger.Log("http read error", err)
@@ -158,8 +157,7 @@ func (g *GiteaBackend) Batch(_ string, pointers []transfer.BatchItem, args trans
return pointers, nil
}
-// Download implements transfer.Backend. The returned reader must be closed by the
-// caller.
+// Download implements transfer.Backend. The returned reader must be closed by the caller.
func (g *GiteaBackend) Download(oid string, args transfer.Args) (io.ReadCloser, int64, error) {
idMapStr, exists := args[argID]
if !exists {
@@ -187,25 +185,25 @@ func (g *GiteaBackend) Download(oid string, args transfer.Args) (io.ReadCloser,
headerGiteaInternalAuth: g.internalAuth,
headerAccept: mimeOctetStream,
}
- req := newInternalRequest(g.ctx, url, http.MethodGet, headers, nil)
+ req := newInternalRequestLFS(g.ctx, url, http.MethodGet, headers, nil)
resp, err := req.Response()
if err != nil {
- return nil, 0, err
+ return nil, 0, fmt.Errorf("failed to get response: %w", err)
}
+ // no need to close the body here by "defer resp.Body.Close()", see below
if resp.StatusCode != http.StatusOK {
return nil, 0, statusCodeToErr(resp.StatusCode)
}
- defer resp.Body.Close()
- respBytes, err := io.ReadAll(resp.Body)
+
+ respSize, err := strconv.ParseInt(resp.Header.Get("X-Gitea-LFS-Content-Length"), 10, 64)
if err != nil {
- return nil, 0, err
+ return nil, 0, fmt.Errorf("failed to parse content length: %w", err)
}
- respSize := int64(len(respBytes))
- respBuf := io.NopCloser(bytes.NewBuffer(respBytes))
- return respBuf, respSize, nil
+ // transfer.Backend will check io.Closer interface and close this Body reader
+ return resp.Body, respSize, nil
}
-// StartUpload implements transfer.Backend.
+// Upload implements transfer.Backend.
func (g *GiteaBackend) Upload(oid string, size int64, r io.Reader, args transfer.Args) error {
idMapStr, exists := args[argID]
if !exists {
@@ -234,15 +232,14 @@ func (g *GiteaBackend) Upload(oid string, size int64, r io.Reader, args transfer
headerContentType: mimeOctetStream,
headerContentLength: strconv.FormatInt(size, 10),
}
- reqBytes, err := io.ReadAll(r)
- if err != nil {
- return err
- }
- req := newInternalRequest(g.ctx, url, http.MethodPut, headers, reqBytes)
+
+ req := newInternalRequestLFS(g.ctx, url, http.MethodPut, headers, nil)
+ req.Body(r)
resp, err := req.Response()
if err != nil {
return err
}
+ defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return statusCodeToErr(resp.StatusCode)
}
@@ -284,11 +281,12 @@ func (g *GiteaBackend) Verify(oid string, size int64, args transfer.Args) (trans
headerAccept: mimeGitLFS,
headerContentType: mimeGitLFS,
}
- req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes)
+ req := newInternalRequestLFS(g.ctx, url, http.MethodPost, headers, bodyBytes)
resp, err := req.Response()
if err != nil {
return transfer.NewStatus(transfer.StatusInternalServerError), err
}
+ defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return transfer.NewStatus(uint32(resp.StatusCode), http.StatusText(resp.StatusCode)), statusCodeToErr(resp.StatusCode)
}
diff --git a/modules/lfstransfer/backend/lock.go b/modules/lfstransfer/backend/lock.go
index f094cce1db..4b45658611 100644
--- a/modules/lfstransfer/backend/lock.go
+++ b/modules/lfstransfer/backend/lock.go
@@ -50,7 +50,7 @@ func (g *giteaLockBackend) Create(path, refname string) (transfer.Lock, error) {
headerAccept: mimeGitLFS,
headerContentType: mimeGitLFS,
}
- req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes)
+ req := newInternalRequestLFS(g.ctx, url, http.MethodPost, headers, bodyBytes)
resp, err := req.Response()
if err != nil {
g.logger.Log("http request error", err)
@@ -102,7 +102,7 @@ func (g *giteaLockBackend) Unlock(lock transfer.Lock) error {
headerAccept: mimeGitLFS,
headerContentType: mimeGitLFS,
}
- req := newInternalRequest(g.ctx, url, http.MethodPost, headers, bodyBytes)
+ req := newInternalRequestLFS(g.ctx, url, http.MethodPost, headers, bodyBytes)
resp, err := req.Response()
if err != nil {
g.logger.Log("http request error", err)
@@ -185,7 +185,7 @@ func (g *giteaLockBackend) queryLocks(v url.Values) ([]transfer.Lock, string, er
headerAccept: mimeGitLFS,
headerContentType: mimeGitLFS,
}
- req := newInternalRequest(g.ctx, url, http.MethodGet, headers, nil)
+ req := newInternalRequestLFS(g.ctx, url, http.MethodGet, headers, nil)
resp, err := req.Response()
if err != nil {
g.logger.Log("http request error", err)
diff --git a/modules/lfstransfer/backend/util.go b/modules/lfstransfer/backend/util.go
index cffefef375..f322d54257 100644
--- a/modules/lfstransfer/backend/util.go
+++ b/modules/lfstransfer/backend/util.go
@@ -5,15 +5,12 @@ package backend
import (
"context"
- "crypto/tls"
"fmt"
- "net"
+ "io"
"net/http"
- "time"
"code.gitea.io/gitea/modules/httplib"
- "code.gitea.io/gitea/modules/proxyprotocol"
- "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/private"
"github.com/charmbracelet/git-lfs-transfer/transfer"
)
@@ -89,53 +86,19 @@ func statusCodeToErr(code int) error {
}
}
-func newInternalRequest(ctx context.Context, url, method string, headers map[string]string, body []byte) *httplib.Request {
- req := httplib.NewRequest(url, method).
- SetContext(ctx).
- SetTimeout(10*time.Second, 60*time.Second).
- SetTLSClientConfig(&tls.Config{
- InsecureSkipVerify: true,
- })
-
- if setting.Protocol == setting.HTTPUnix {
- req.SetTransport(&http.Transport{
- DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
- var d net.Dialer
- conn, err := d.DialContext(ctx, "unix", setting.HTTPAddr)
- if err != nil {
- return conn, err
- }
- if setting.LocalUseProxyProtocol {
- if err = proxyprotocol.WriteLocalHeader(conn); err != nil {
- _ = conn.Close()
- return nil, err
- }
- }
- return conn, err
- },
- })
- } else if setting.LocalUseProxyProtocol {
- req.SetTransport(&http.Transport{
- DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
- var d net.Dialer
- conn, err := d.DialContext(ctx, network, address)
- if err != nil {
- return conn, err
- }
- if err = proxyprotocol.WriteLocalHeader(conn); err != nil {
- _ = conn.Close()
- return nil, err
- }
- return conn, err
- },
- })
- }
-
+func newInternalRequestLFS(ctx context.Context, url, method string, headers map[string]string, body any) *httplib.Request {
+ req := private.NewInternalRequest(ctx, url, method)
for k, v := range headers {
req.Header(k, v)
}
-
- req.Body(body)
-
+ switch body := body.(type) {
+ case nil: // do nothing
+ case []byte:
+ req.Body(body) // []byte
+ case io.Reader:
+ req.Body(body) // io.Reader or io.ReadCloser
+ default:
+ panic(fmt.Sprintf("unsupported request body type %T", body))
+ }
return req
}
diff --git a/modules/markup/html.go b/modules/markup/html.go
index bb12febf27..3aaf669c63 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -47,7 +47,7 @@ var globalVars = sync.OnceValue(func() *globalVarsType {
// NOTE: All below regex matching do not perform any extra validation.
// Thus a link is produced even if the linked entity does not exist.
// While fast, this is also incorrect and lead to false positives.
- // TODO: fix invalid linking issue
+ // TODO: fix invalid linking issue (update: stale TODO, what issues? maybe no TODO anymore)
// valid chars in encoded path and parameter: [-+~_%.a-zA-Z0-9/]
diff --git a/modules/migration/downloader.go b/modules/migration/downloader.go
index 08dbbc29a9..669222dea2 100644
--- a/modules/migration/downloader.go
+++ b/modules/migration/downloader.go
@@ -12,18 +12,17 @@ import (
// Downloader downloads the site repo information
type Downloader interface {
- SetContext(context.Context)
- GetRepoInfo() (*Repository, error)
- GetTopics() ([]string, error)
- GetMilestones() ([]*Milestone, error)
- GetReleases() ([]*Release, error)
- GetLabels() ([]*Label, error)
- GetIssues(page, perPage int) ([]*Issue, bool, error)
- GetComments(commentable Commentable) ([]*Comment, bool, error)
- GetAllComments(page, perPage int) ([]*Comment, bool, error)
+ GetRepoInfo(ctx context.Context) (*Repository, error)
+ GetTopics(ctx context.Context) ([]string, error)
+ GetMilestones(ctx context.Context) ([]*Milestone, error)
+ GetReleases(ctx context.Context) ([]*Release, error)
+ GetLabels(ctx context.Context) ([]*Label, error)
+ GetIssues(ctx context.Context, page, perPage int) ([]*Issue, bool, error)
+ GetComments(ctx context.Context, commentable Commentable) ([]*Comment, bool, error)
+ GetAllComments(ctx context.Context, page, perPage int) ([]*Comment, bool, error)
SupportGetRepoComments() bool
- GetPullRequests(page, perPage int) ([]*PullRequest, bool, error)
- GetReviews(reviewable Reviewable) ([]*Review, error)
+ GetPullRequests(ctx context.Context, page, perPage int) ([]*PullRequest, bool, error)
+ GetReviews(ctx context.Context, reviewable Reviewable) ([]*Review, error)
FormatCloneURL(opts MigrateOptions, remoteAddr string) (string, error)
}
diff --git a/modules/migration/null_downloader.go b/modules/migration/null_downloader.go
index e5b69331df..e488f6914f 100644
--- a/modules/migration/null_downloader.go
+++ b/modules/migration/null_downloader.go
@@ -13,56 +13,53 @@ type NullDownloader struct{}
var _ Downloader = &NullDownloader{}
-// SetContext set context
-func (n NullDownloader) SetContext(_ context.Context) {}
-
// GetRepoInfo returns a repository information
-func (n NullDownloader) GetRepoInfo() (*Repository, error) {
+func (n NullDownloader) GetRepoInfo(_ context.Context) (*Repository, error) {
return nil, ErrNotSupported{Entity: "RepoInfo"}
}
// GetTopics return repository topics
-func (n NullDownloader) GetTopics() ([]string, error) {
+func (n NullDownloader) GetTopics(_ context.Context) ([]string, error) {
return nil, ErrNotSupported{Entity: "Topics"}
}
// GetMilestones returns milestones
-func (n NullDownloader) GetMilestones() ([]*Milestone, error) {
+func (n NullDownloader) GetMilestones(_ context.Context) ([]*Milestone, error) {
return nil, ErrNotSupported{Entity: "Milestones"}
}
// GetReleases returns releases
-func (n NullDownloader) GetReleases() ([]*Release, error) {
+func (n NullDownloader) GetReleases(_ context.Context) ([]*Release, error) {
return nil, ErrNotSupported{Entity: "Releases"}
}
// GetLabels returns labels
-func (n NullDownloader) GetLabels() ([]*Label, error) {
+func (n NullDownloader) GetLabels(_ context.Context) ([]*Label, error) {
return nil, ErrNotSupported{Entity: "Labels"}
}
// GetIssues returns issues according start and limit
-func (n NullDownloader) GetIssues(page, perPage int) ([]*Issue, bool, error) {
+func (n NullDownloader) GetIssues(_ context.Context, page, perPage int) ([]*Issue, bool, error) {
return nil, false, ErrNotSupported{Entity: "Issues"}
}
// GetComments returns comments of an issue or PR
-func (n NullDownloader) GetComments(commentable Commentable) ([]*Comment, bool, error) {
+func (n NullDownloader) GetComments(_ context.Context, commentable Commentable) ([]*Comment, bool, error) {
return nil, false, ErrNotSupported{Entity: "Comments"}
}
// GetAllComments returns paginated comments
-func (n NullDownloader) GetAllComments(page, perPage int) ([]*Comment, bool, error) {
+func (n NullDownloader) GetAllComments(_ context.Context, page, perPage int) ([]*Comment, bool, error) {
return nil, false, ErrNotSupported{Entity: "AllComments"}
}
// GetPullRequests returns pull requests according page and perPage
-func (n NullDownloader) GetPullRequests(page, perPage int) ([]*PullRequest, bool, error) {
+func (n NullDownloader) GetPullRequests(_ context.Context, page, perPage int) ([]*PullRequest, bool, error) {
return nil, false, ErrNotSupported{Entity: "PullRequests"}
}
// GetReviews returns pull requests review
-func (n NullDownloader) GetReviews(reviewable Reviewable) ([]*Review, error) {
+func (n NullDownloader) GetReviews(_ context.Context, reviewable Reviewable) ([]*Review, error) {
return nil, ErrNotSupported{Entity: "Reviews"}
}
diff --git a/modules/migration/retry_downloader.go b/modules/migration/retry_downloader.go
index 1cacf5f375..2926c40df7 100644
--- a/modules/migration/retry_downloader.go
+++ b/modules/migration/retry_downloader.go
@@ -49,21 +49,15 @@ func (d *RetryDownloader) retry(work func() error) error {
return err
}
-// SetContext set context
-func (d *RetryDownloader) SetContext(ctx context.Context) {
- d.ctx = ctx
- d.Downloader.SetContext(ctx)
-}
-
// GetRepoInfo returns a repository information with retry
-func (d *RetryDownloader) GetRepoInfo() (*Repository, error) {
+func (d *RetryDownloader) GetRepoInfo(ctx context.Context) (*Repository, error) {
var (
repo *Repository
err error
)
err = d.retry(func() error {
- repo, err = d.Downloader.GetRepoInfo()
+ repo, err = d.Downloader.GetRepoInfo(ctx)
return err
})
@@ -71,14 +65,14 @@ func (d *RetryDownloader) GetRepoInfo() (*Repository, error) {
}
// GetTopics returns a repository's topics with retry
-func (d *RetryDownloader) GetTopics() ([]string, error) {
+func (d *RetryDownloader) GetTopics(ctx context.Context) ([]string, error) {
var (
topics []string
err error
)
err = d.retry(func() error {
- topics, err = d.Downloader.GetTopics()
+ topics, err = d.Downloader.GetTopics(ctx)
return err
})
@@ -86,14 +80,14 @@ func (d *RetryDownloader) GetTopics() ([]string, error) {
}
// GetMilestones returns a repository's milestones with retry
-func (d *RetryDownloader) GetMilestones() ([]*Milestone, error) {
+func (d *RetryDownloader) GetMilestones(ctx context.Context) ([]*Milestone, error) {
var (
milestones []*Milestone
err error
)
err = d.retry(func() error {
- milestones, err = d.Downloader.GetMilestones()
+ milestones, err = d.Downloader.GetMilestones(ctx)
return err
})
@@ -101,14 +95,14 @@ func (d *RetryDownloader) GetMilestones() ([]*Milestone, error) {
}
// GetReleases returns a repository's releases with retry
-func (d *RetryDownloader) GetReleases() ([]*Release, error) {
+func (d *RetryDownloader) GetReleases(ctx context.Context) ([]*Release, error) {
var (
releases []*Release
err error
)
err = d.retry(func() error {
- releases, err = d.Downloader.GetReleases()
+ releases, err = d.Downloader.GetReleases(ctx)
return err
})
@@ -116,14 +110,14 @@ func (d *RetryDownloader) GetReleases() ([]*Release, error) {
}
// GetLabels returns a repository's labels with retry
-func (d *RetryDownloader) GetLabels() ([]*Label, error) {
+func (d *RetryDownloader) GetLabels(ctx context.Context) ([]*Label, error) {
var (
labels []*Label
err error
)
err = d.retry(func() error {
- labels, err = d.Downloader.GetLabels()
+ labels, err = d.Downloader.GetLabels(ctx)
return err
})
@@ -131,7 +125,7 @@ func (d *RetryDownloader) GetLabels() ([]*Label, error) {
}
// GetIssues returns a repository's issues with retry
-func (d *RetryDownloader) GetIssues(page, perPage int) ([]*Issue, bool, error) {
+func (d *RetryDownloader) GetIssues(ctx context.Context, page, perPage int) ([]*Issue, bool, error) {
var (
issues []*Issue
isEnd bool
@@ -139,7 +133,7 @@ func (d *RetryDownloader) GetIssues(page, perPage int) ([]*Issue, bool, error) {
)
err = d.retry(func() error {
- issues, isEnd, err = d.Downloader.GetIssues(page, perPage)
+ issues, isEnd, err = d.Downloader.GetIssues(ctx, page, perPage)
return err
})
@@ -147,7 +141,7 @@ func (d *RetryDownloader) GetIssues(page, perPage int) ([]*Issue, bool, error) {
}
// GetComments returns a repository's comments with retry
-func (d *RetryDownloader) GetComments(commentable Commentable) ([]*Comment, bool, error) {
+func (d *RetryDownloader) GetComments(ctx context.Context, commentable Commentable) ([]*Comment, bool, error) {
var (
comments []*Comment
isEnd bool
@@ -155,7 +149,7 @@ func (d *RetryDownloader) GetComments(commentable Commentable) ([]*Comment, bool
)
err = d.retry(func() error {
- comments, isEnd, err = d.Downloader.GetComments(commentable)
+ comments, isEnd, err = d.Downloader.GetComments(ctx, commentable)
return err
})
@@ -163,7 +157,7 @@ func (d *RetryDownloader) GetComments(commentable Commentable) ([]*Comment, bool
}
// GetPullRequests returns a repository's pull requests with retry
-func (d *RetryDownloader) GetPullRequests(page, perPage int) ([]*PullRequest, bool, error) {
+func (d *RetryDownloader) GetPullRequests(ctx context.Context, page, perPage int) ([]*PullRequest, bool, error) {
var (
prs []*PullRequest
err error
@@ -171,7 +165,7 @@ func (d *RetryDownloader) GetPullRequests(page, perPage int) ([]*PullRequest, bo
)
err = d.retry(func() error {
- prs, isEnd, err = d.Downloader.GetPullRequests(page, perPage)
+ prs, isEnd, err = d.Downloader.GetPullRequests(ctx, page, perPage)
return err
})
@@ -179,14 +173,13 @@ func (d *RetryDownloader) GetPullRequests(page, perPage int) ([]*PullRequest, bo
}
// GetReviews returns pull requests reviews
-func (d *RetryDownloader) GetReviews(reviewable Reviewable) ([]*Review, error) {
+func (d *RetryDownloader) GetReviews(ctx context.Context, reviewable Reviewable) ([]*Review, error) {
var (
reviews []*Review
err error
)
-
err = d.retry(func() error {
- reviews, err = d.Downloader.GetReviews(reviewable)
+ reviews, err = d.Downloader.GetReviews(ctx, reviewable)
return err
})
diff --git a/modules/migration/uploader.go b/modules/migration/uploader.go
index ff642aa4fa..65752e248e 100644
--- a/modules/migration/uploader.go
+++ b/modules/migration/uploader.go
@@ -4,20 +4,22 @@
package migration
+import "context"
+
// Uploader uploads all the information of one repository
type Uploader interface {
MaxBatchInsertSize(tp string) int
- CreateRepo(repo *Repository, opts MigrateOptions) error
- CreateTopics(topic ...string) error
- CreateMilestones(milestones ...*Milestone) error
- CreateReleases(releases ...*Release) error
- SyncTags() error
- CreateLabels(labels ...*Label) error
- CreateIssues(issues ...*Issue) error
- CreateComments(comments ...*Comment) error
- CreatePullRequests(prs ...*PullRequest) error
- CreateReviews(reviews ...*Review) error
+ CreateRepo(ctx context.Context, repo *Repository, opts MigrateOptions) error
+ CreateTopics(ctx context.Context, topic ...string) error
+ CreateMilestones(ctx context.Context, milestones ...*Milestone) error
+ CreateReleases(ctx context.Context, releases ...*Release) error
+ SyncTags(ctx context.Context) error
+ CreateLabels(ctx context.Context, labels ...*Label) error
+ CreateIssues(ctx context.Context, issues ...*Issue) error
+ CreateComments(ctx context.Context, comments ...*Comment) error
+ CreatePullRequests(ctx context.Context, prs ...*PullRequest) error
+ CreateReviews(ctx context.Context, reviews ...*Review) error
Rollback() error
- Finish() error
+ Finish(ctx context.Context) error
Close()
}
diff --git a/modules/private/actions.go b/modules/private/actions.go
index 311a283650..e68f2f85b0 100644
--- a/modules/private/actions.go
+++ b/modules/private/actions.go
@@ -17,7 +17,7 @@ type GenerateTokenRequest struct {
func GenerateActionsRunnerToken(ctx context.Context, scope string) (*ResponseText, ResponseExtra) {
reqURL := setting.LocalURL + "api/internal/actions/generate_actions_runner_token"
- req := newInternalRequest(ctx, reqURL, "POST", GenerateTokenRequest{
+ req := newInternalRequestAPI(ctx, reqURL, "POST", GenerateTokenRequest{
Scope: scope,
})
diff --git a/modules/private/hook.go b/modules/private/hook.go
index 745c200619..87d6549f9c 100644
--- a/modules/private/hook.go
+++ b/modules/private/hook.go
@@ -85,7 +85,7 @@ type HookProcReceiveRefResult struct {
// HookPreReceive check whether the provided commits are allowed
func HookPreReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) ResponseExtra {
reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/pre-receive/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName))
- req := newInternalRequest(ctx, reqURL, "POST", opts)
+ req := newInternalRequestAPI(ctx, reqURL, "POST", opts)
req.SetReadWriteTimeout(time.Duration(60+len(opts.OldCommitIDs)) * time.Second)
_, extra := requestJSONResp(req, &ResponseText{})
return extra
@@ -94,7 +94,7 @@ func HookPreReceive(ctx context.Context, ownerName, repoName string, opts HookOp
// HookPostReceive updates services and users
func HookPostReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HookPostReceiveResult, ResponseExtra) {
reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/post-receive/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName))
- req := newInternalRequest(ctx, reqURL, "POST", opts)
+ req := newInternalRequestAPI(ctx, reqURL, "POST", opts)
req.SetReadWriteTimeout(time.Duration(60+len(opts.OldCommitIDs)) * time.Second)
return requestJSONResp(req, &HookPostReceiveResult{})
}
@@ -103,7 +103,7 @@ func HookPostReceive(ctx context.Context, ownerName, repoName string, opts HookO
func HookProcReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HookProcReceiveResult, ResponseExtra) {
reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/proc-receive/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName))
- req := newInternalRequest(ctx, reqURL, "POST", opts)
+ req := newInternalRequestAPI(ctx, reqURL, "POST", opts)
req.SetReadWriteTimeout(time.Duration(60+len(opts.OldCommitIDs)) * time.Second)
return requestJSONResp(req, &HookProcReceiveResult{})
}
@@ -115,7 +115,7 @@ func SetDefaultBranch(ctx context.Context, ownerName, repoName, branch string) R
url.PathEscape(repoName),
url.PathEscape(branch),
)
- req := newInternalRequest(ctx, reqURL, "POST")
+ req := newInternalRequestAPI(ctx, reqURL, "POST")
_, extra := requestJSONResp(req, &ResponseText{})
return extra
}
@@ -123,7 +123,7 @@ func SetDefaultBranch(ctx context.Context, ownerName, repoName, branch string) R
// SSHLog sends ssh error log response
func SSHLog(ctx context.Context, isErr bool, msg string) error {
reqURL := setting.LocalURL + "api/internal/ssh/log"
- req := newInternalRequest(ctx, reqURL, "POST", &SSHLogOption{IsError: isErr, Message: msg})
+ req := newInternalRequestAPI(ctx, reqURL, "POST", &SSHLogOption{IsError: isErr, Message: msg})
_, extra := requestJSONResp(req, &ResponseText{})
return extra.Error
}
diff --git a/modules/private/internal.go b/modules/private/internal.go
index c7e7773524..3bd4eb06b1 100644
--- a/modules/private/internal.go
+++ b/modules/private/internal.go
@@ -34,7 +34,7 @@ func getClientIP() string {
return strings.Fields(sshConnEnv)[0]
}
-func newInternalRequest(ctx context.Context, url, method string, body ...any) *httplib.Request {
+func NewInternalRequest(ctx context.Context, url, method string) *httplib.Request {
if setting.InternalToken == "" {
log.Fatal(`The INTERNAL_TOKEN setting is missing from the configuration file: %q.
Ensure you are running in the correct environment or set the correct configuration file with -c.`, setting.CustomConf)
@@ -82,13 +82,17 @@ Ensure you are running in the correct environment or set the correct configurati
},
})
}
+ return req
+}
+func newInternalRequestAPI(ctx context.Context, url, method string, body ...any) *httplib.Request {
+ req := NewInternalRequest(ctx, url, method)
if len(body) == 1 {
req.Header("Content-Type", "application/json")
jsonBytes, _ := json.Marshal(body[0])
req.Body(jsonBytes)
} else if len(body) > 1 {
- log.Fatal("Too many arguments for newInternalRequest")
+ log.Fatal("Too many arguments for newInternalRequestAPI")
}
req.SetTimeout(10*time.Second, 60*time.Second)
diff --git a/modules/private/key.go b/modules/private/key.go
index dcd1714856..114683b343 100644
--- a/modules/private/key.go
+++ b/modules/private/key.go
@@ -14,7 +14,7 @@ import (
func UpdatePublicKeyInRepo(ctx context.Context, keyID, repoID int64) error {
// Ask for running deliver hook and test pull request tasks.
reqURL := setting.LocalURL + fmt.Sprintf("api/internal/ssh/%d/update/%d", keyID, repoID)
- req := newInternalRequest(ctx, reqURL, "POST")
+ req := newInternalRequestAPI(ctx, reqURL, "POST")
_, extra := requestJSONResp(req, &ResponseText{})
return extra.Error
}
@@ -24,7 +24,7 @@ func UpdatePublicKeyInRepo(ctx context.Context, keyID, repoID int64) error {
func AuthorizedPublicKeyByContent(ctx context.Context, content string) (*ResponseText, ResponseExtra) {
// Ask for running deliver hook and test pull request tasks.
reqURL := setting.LocalURL + "api/internal/ssh/authorized_keys"
- req := newInternalRequest(ctx, reqURL, "POST")
+ req := newInternalRequestAPI(ctx, reqURL, "POST")
req.Param("content", content)
return requestJSONResp(req, &ResponseText{})
}
diff --git a/modules/private/mail.go b/modules/private/mail.go
index 08de5b7e28..3904e37bea 100644
--- a/modules/private/mail.go
+++ b/modules/private/mail.go
@@ -23,7 +23,7 @@ type Email struct {
func SendEmail(ctx context.Context, subject, message string, to []string) (*ResponseText, ResponseExtra) {
reqURL := setting.LocalURL + "api/internal/mail/send"
- req := newInternalRequest(ctx, reqURL, "POST", Email{
+ req := newInternalRequestAPI(ctx, reqURL, "POST", Email{
Subject: subject,
Message: message,
To: to,
diff --git a/modules/private/manager.go b/modules/private/manager.go
index 6055e553bd..e3d5ad57e0 100644
--- a/modules/private/manager.go
+++ b/modules/private/manager.go
@@ -18,21 +18,21 @@ import (
// Shutdown calls the internal shutdown function
func Shutdown(ctx context.Context) ResponseExtra {
reqURL := setting.LocalURL + "api/internal/manager/shutdown"
- req := newInternalRequest(ctx, reqURL, "POST")
+ req := newInternalRequestAPI(ctx, reqURL, "POST")
return requestJSONClientMsg(req, "Shutting down")
}
// Restart calls the internal restart function
func Restart(ctx context.Context) ResponseExtra {
reqURL := setting.LocalURL + "api/internal/manager/restart"
- req := newInternalRequest(ctx, reqURL, "POST")
+ req := newInternalRequestAPI(ctx, reqURL, "POST")
return requestJSONClientMsg(req, "Restarting")
}
// ReloadTemplates calls the internal reload-templates function
func ReloadTemplates(ctx context.Context) ResponseExtra {
reqURL := setting.LocalURL + "api/internal/manager/reload-templates"
- req := newInternalRequest(ctx, reqURL, "POST")
+ req := newInternalRequestAPI(ctx, reqURL, "POST")
return requestJSONClientMsg(req, "Reloaded")
}
@@ -45,7 +45,7 @@ type FlushOptions struct {
// FlushQueues calls the internal flush-queues function
func FlushQueues(ctx context.Context, timeout time.Duration, nonBlocking bool) ResponseExtra {
reqURL := setting.LocalURL + "api/internal/manager/flush-queues"
- req := newInternalRequest(ctx, reqURL, "POST", FlushOptions{Timeout: timeout, NonBlocking: nonBlocking})
+ req := newInternalRequestAPI(ctx, reqURL, "POST", FlushOptions{Timeout: timeout, NonBlocking: nonBlocking})
if timeout > 0 {
req.SetReadWriteTimeout(timeout + 10*time.Second)
}
@@ -55,28 +55,28 @@ func FlushQueues(ctx context.Context, timeout time.Duration, nonBlocking bool) R
// PauseLogging pauses logging
func PauseLogging(ctx context.Context) ResponseExtra {
reqURL := setting.LocalURL + "api/internal/manager/pause-logging"
- req := newInternalRequest(ctx, reqURL, "POST")
+ req := newInternalRequestAPI(ctx, reqURL, "POST")
return requestJSONClientMsg(req, "Logging Paused")
}
// ResumeLogging resumes logging
func ResumeLogging(ctx context.Context) ResponseExtra {
reqURL := setting.LocalURL + "api/internal/manager/resume-logging"
- req := newInternalRequest(ctx, reqURL, "POST")
+ req := newInternalRequestAPI(ctx, reqURL, "POST")
return requestJSONClientMsg(req, "Logging Restarted")
}
// ReleaseReopenLogging releases and reopens logging files
func ReleaseReopenLogging(ctx context.Context) ResponseExtra {
reqURL := setting.LocalURL + "api/internal/manager/release-and-reopen-logging"
- req := newInternalRequest(ctx, reqURL, "POST")
+ req := newInternalRequestAPI(ctx, reqURL, "POST")
return requestJSONClientMsg(req, "Logging Restarted")
}
// SetLogSQL sets database logging
func SetLogSQL(ctx context.Context, on bool) ResponseExtra {
reqURL := setting.LocalURL + "api/internal/manager/set-log-sql?on=" + strconv.FormatBool(on)
- req := newInternalRequest(ctx, reqURL, "POST")
+ req := newInternalRequestAPI(ctx, reqURL, "POST")
return requestJSONClientMsg(req, "Log SQL setting set")
}
@@ -91,7 +91,7 @@ type LoggerOptions struct {
// AddLogger adds a logger
func AddLogger(ctx context.Context, logger, writer, mode string, config map[string]any) ResponseExtra {
reqURL := setting.LocalURL + "api/internal/manager/add-logger"
- req := newInternalRequest(ctx, reqURL, "POST", LoggerOptions{
+ req := newInternalRequestAPI(ctx, reqURL, "POST", LoggerOptions{
Logger: logger,
Writer: writer,
Mode: mode,
@@ -103,7 +103,7 @@ func AddLogger(ctx context.Context, logger, writer, mode string, config map[stri
// RemoveLogger removes a logger
func RemoveLogger(ctx context.Context, logger, writer string) ResponseExtra {
reqURL := setting.LocalURL + fmt.Sprintf("api/internal/manager/remove-logger/%s/%s", url.PathEscape(logger), url.PathEscape(writer))
- req := newInternalRequest(ctx, reqURL, "POST")
+ req := newInternalRequestAPI(ctx, reqURL, "POST")
return requestJSONClientMsg(req, "Removed")
}
@@ -111,7 +111,7 @@ func RemoveLogger(ctx context.Context, logger, writer string) ResponseExtra {
func Processes(ctx context.Context, out io.Writer, flat, noSystem, stacktraces, json bool, cancel string) ResponseExtra {
reqURL := setting.LocalURL + fmt.Sprintf("api/internal/manager/processes?flat=%t&no-system=%t&stacktraces=%t&json=%t&cancel-pid=%s", flat, noSystem, stacktraces, json, url.QueryEscape(cancel))
- req := newInternalRequest(ctx, reqURL, "GET")
+ req := newInternalRequestAPI(ctx, reqURL, "GET")
callback := func(resp *http.Response, extra *ResponseExtra) {
_, extra.Error = io.Copy(out, resp.Body)
}
diff --git a/modules/private/restore_repo.go b/modules/private/restore_repo.go
index 496209d3cb..9c3a008142 100644
--- a/modules/private/restore_repo.go
+++ b/modules/private/restore_repo.go
@@ -24,7 +24,7 @@ type RestoreParams struct {
func RestoreRepo(ctx context.Context, repoDir, ownerName, repoName string, units []string, validation bool) ResponseExtra {
reqURL := setting.LocalURL + "api/internal/restore_repo"
- req := newInternalRequest(ctx, reqURL, "POST", RestoreParams{
+ req := newInternalRequestAPI(ctx, reqURL, "POST", RestoreParams{
RepoDir: repoDir,
OwnerName: ownerName,
RepoName: repoName,
diff --git a/modules/private/serv.go b/modules/private/serv.go
index 480a446954..2ccc6c1129 100644
--- a/modules/private/serv.go
+++ b/modules/private/serv.go
@@ -23,7 +23,7 @@ type KeyAndOwner struct {
// ServNoCommand returns information about the provided key
func ServNoCommand(ctx context.Context, keyID int64) (*asymkey_model.PublicKey, *user_model.User, error) {
reqURL := setting.LocalURL + fmt.Sprintf("api/internal/serv/none/%d", keyID)
- req := newInternalRequest(ctx, reqURL, "GET")
+ req := newInternalRequestAPI(ctx, reqURL, "GET")
keyAndOwner, extra := requestJSONResp(req, &KeyAndOwner{})
if extra.HasError() {
return nil, nil, extra.Error
@@ -58,6 +58,6 @@ func ServCommand(ctx context.Context, keyID int64, ownerName, repoName string, m
reqURL += fmt.Sprintf("&verb=%s", url.QueryEscape(verb))
}
}
- req := newInternalRequest(ctx, reqURL, "GET")
+ req := newInternalRequestAPI(ctx, reqURL, "GET")
return requestJSONResp(req, &ServCommandResults{})
}
diff --git a/modules/structs/org.go b/modules/structs/org.go
index c0a545ac1c..f93b3b6493 100644
--- a/modules/structs/org.go
+++ b/modules/structs/org.go
@@ -57,3 +57,12 @@ type EditOrgOption struct {
Visibility string `json:"visibility" binding:"In(,public,limited,private)"`
RepoAdminChangeTeamAccess *bool `json:"repo_admin_change_team_access"`
}
+
+// RenameOrgOption options when renaming an organization
+type RenameOrgOption struct {
+ // New username for this org. This name cannot be in use yet by any other user.
+ //
+ // required: true
+ // unique: true
+ NewName string `json:"new_name" binding:"Required"`
+}
diff --git a/modules/structs/repo_actions.go b/modules/structs/repo_actions.go
index b13f344738..e6d11a8acb 100644
--- a/modules/structs/repo_actions.go
+++ b/modules/structs/repo_actions.go
@@ -32,3 +32,36 @@ type ActionTaskResponse struct {
Entries []*ActionTask `json:"workflow_runs"`
TotalCount int64 `json:"total_count"`
}
+
+// CreateActionWorkflowDispatch represents the payload for triggering a workflow dispatch event
+// swagger:model
+type CreateActionWorkflowDispatch struct {
+ // required: true
+ // example: refs/heads/main
+ Ref string `json:"ref" binding:"Required"`
+ // required: false
+ Inputs map[string]string `json:"inputs,omitempty"`
+}
+
+// ActionWorkflow represents a ActionWorkflow
+type ActionWorkflow struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Path string `json:"path"`
+ State string `json:"state"`
+ // swagger:strfmt date-time
+ CreatedAt time.Time `json:"created_at"`
+ // swagger:strfmt date-time
+ UpdatedAt time.Time `json:"updated_at"`
+ URL string `json:"url"`
+ HTMLURL string `json:"html_url"`
+ BadgeURL string `json:"badge_url"`
+ // swagger:strfmt date-time
+ DeletedAt time.Time `json:"deleted_at,omitempty"`
+}
+
+// ActionWorkflowResponse returns a ActionWorkflow
+type ActionWorkflowResponse struct {
+ Workflows []*ActionWorkflow `json:"workflows"`
+ TotalCount int64 `json:"total_count"`
+}
diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index a2cc166de9..c0b0ddc97d 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -69,7 +69,7 @@ func NewFuncMap() template.FuncMap {
// time / number / format
"FileSize": base.FileSize,
"CountFmt": countFmt,
- "Sec2Time": util.SecToHours,
+ "Sec2Hour": util.SecToHours,
"TimeEstimateString": timeEstimateString,
diff --git a/modules/util/error.go b/modules/util/error.go
index 0f3597147c..07fadf3cab 100644
--- a/modules/util/error.go
+++ b/modules/util/error.go
@@ -36,6 +36,22 @@ func (w SilentWrap) Unwrap() error {
return w.Err
}
+type LocaleWrap struct {
+ err error
+ TrKey string
+ TrArgs []any
+}
+
+// Error returns the message
+func (w LocaleWrap) Error() string {
+ return w.err.Error()
+}
+
+// Unwrap returns the underlying error
+func (w LocaleWrap) Unwrap() error {
+ return w.err
+}
+
// NewSilentWrapErrorf returns an error that formats as the given text but unwraps as the provided error
func NewSilentWrapErrorf(unwrap error, message string, args ...any) error {
if len(args) == 0 {
@@ -63,3 +79,16 @@ func NewAlreadyExistErrorf(message string, args ...any) error {
func NewNotExistErrorf(message string, args ...any) error {
return NewSilentWrapErrorf(ErrNotExist, message, args...)
}
+
+// ErrWrapLocale wraps an err with a translation key and arguments
+func ErrWrapLocale(err error, trKey string, trArgs ...any) error {
+ return LocaleWrap{err: err, TrKey: trKey, TrArgs: trArgs}
+}
+
+func ErrAsLocale(err error) *LocaleWrap {
+ var e LocaleWrap
+ if errors.As(err, &e) {
+ return &e
+ }
+ return nil
+}
diff --git a/modules/util/sec_to_time.go b/modules/util/sec_to_time.go
index 73667d723e..646f33c82a 100644
--- a/modules/util/sec_to_time.go
+++ b/modules/util/sec_to_time.go
@@ -11,16 +11,20 @@ import (
// SecToHours converts an amount of seconds to a human-readable hours string.
// This is stable for planning and managing timesheets.
// Here it only supports hours and minutes, because a work day could contain 6 or 7 or 8 hours.
+// If the duration is less than 1 minute, it will be shown as seconds.
func SecToHours(durationVal any) string {
- duration, _ := ToInt64(durationVal)
- hours := duration / 3600
- minutes := (duration / 60) % 60
+ seconds, _ := ToInt64(durationVal)
+ hours := seconds / 3600
+ minutes := (seconds / 60) % 60
formattedTime := ""
formattedTime = formatTime(hours, "hour", formattedTime)
formattedTime = formatTime(minutes, "minute", formattedTime)
// The formatTime() function always appends a space at the end. This will be trimmed
+ if formattedTime == "" && seconds > 0 {
+ formattedTime = formatTime(seconds, "second", "")
+ }
return strings.TrimRight(formattedTime, " ")
}
diff --git a/modules/util/sec_to_time_test.go b/modules/util/sec_to_time_test.go
index 71a8801d4f..b67926bbcf 100644
--- a/modules/util/sec_to_time_test.go
+++ b/modules/util/sec_to_time_test.go
@@ -22,4 +22,7 @@ func TestSecToHours(t *testing.T) {
assert.Equal(t, "156 hours 30 minutes", SecToHours(6*day+12*hour+30*minute+18*second))
assert.Equal(t, "98 hours 16 minutes", SecToHours(4*day+2*hour+16*minute+58*second))
assert.Equal(t, "672 hours", SecToHours(4*7*day))
+ assert.Equal(t, "1 second", SecToHours(1))
+ assert.Equal(t, "2 seconds", SecToHours(2))
+ assert.Equal(t, "", SecToHours(nil)) // old behavior, empty means no output
}
diff --git a/modules/web/middleware/binding.go b/modules/web/middleware/binding.go
index 43e1bbc70e..03e188f509 100644
--- a/modules/web/middleware/binding.go
+++ b/modules/web/middleware/binding.go
@@ -78,7 +78,7 @@ func GetInclude(field reflect.StructField) string {
return getRuleBody(field, "Include(")
}
-// Validate validate TODO:
+// Validate validate
func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Locale) binding.Errors {
if errs.Len() == 0 {
return errs
diff --git a/options/gitignore/Flutter b/options/gitignore/Flutter
new file mode 100644
index 0000000000..39b8814aec
--- /dev/null
+++ b/options/gitignore/Flutter
@@ -0,0 +1,119 @@
+# Miscellaneous
+*.class
+*.lock
+*.log
+*.pyc
+*.swp
+.buildlog/
+.history
+
+
+
+# Flutter repo-specific
+/bin/cache/
+/bin/internal/bootstrap.bat
+/bin/internal/bootstrap.sh
+/bin/mingit/
+/dev/benchmarks/mega_gallery/
+/dev/bots/.recipe_deps
+/dev/bots/android_tools/
+/dev/devicelab/ABresults*.json
+/dev/docs/doc/
+/dev/docs/flutter.docs.zip
+/dev/docs/lib/
+/dev/docs/pubspec.yaml
+/dev/integration_tests/**/xcuserdata
+/dev/integration_tests/**/Pods
+/packages/flutter/coverage/
+version
+analysis_benchmark.json
+
+# packages file containing multi-root paths
+.packages.generated
+
+# Flutter/Dart/Pub related
+**/doc/api/
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+**/generated_plugin_registrant.dart
+.packages
+.pub-preload-cache/
+.pub/
+build/
+flutter_*.png
+linked_*.ds
+unlinked.ds
+unlinked_spec.ds
+
+# Android related
+**/android/**/gradle-wrapper.jar
+.gradle/
+**/android/captures/
+**/android/gradlew
+**/android/gradlew.bat
+**/android/local.properties
+**/android/**/GeneratedPluginRegistrant.java
+**/android/key.properties
+*.jks
+
+# iOS/XCode related
+**/ios/**/*.mode1v3
+**/ios/**/*.mode2v3
+**/ios/**/*.moved-aside
+**/ios/**/*.pbxuser
+**/ios/**/*.perspectivev3
+**/ios/**/*sync/
+**/ios/**/.sconsign.dblite
+**/ios/**/.tags*
+**/ios/**/.vagrant/
+**/ios/**/DerivedData/
+**/ios/**/Icon?
+**/ios/**/Pods/
+**/ios/**/.symlinks/
+**/ios/**/profile
+**/ios/**/xcuserdata
+**/ios/.generated/
+**/ios/Flutter/.last_build_id
+**/ios/Flutter/App.framework
+**/ios/Flutter/Flutter.framework
+**/ios/Flutter/Flutter.podspec
+**/ios/Flutter/Generated.xcconfig
+**/ios/Flutter/ephemeral
+**/ios/Flutter/app.flx
+**/ios/Flutter/app.zip
+**/ios/Flutter/flutter_assets/
+**/ios/Flutter/flutter_export_environment.sh
+**/ios/ServiceDefinitions.json
+**/ios/Runner/GeneratedPluginRegistrant.*
+
+# macOS
+**/Flutter/ephemeral/
+**/Pods/
+**/macos/Flutter/GeneratedPluginRegistrant.swift
+**/macos/Flutter/ephemeral
+**/xcuserdata/
+
+# Windows
+**/windows/flutter/generated_plugin_registrant.cc
+**/windows/flutter/generated_plugin_registrant.h
+**/windows/flutter/generated_plugins.cmake
+
+# Linux
+**/linux/flutter/generated_plugin_registrant.cc
+**/linux/flutter/generated_plugin_registrant.h
+**/linux/flutter/generated_plugins.cmake
+
+# Coverage
+coverage/
+
+# Symbols
+app.*.symbols
+
+# Exceptions to above rules.
+!**/ios/**/default.mode1v3
+!**/ios/**/default.mode2v3
+!**/ios/**/default.pbxuser
+!**/ios/**/default.perspectivev3
+!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
+!/dev/ci/**/Gemfile.lock \ No newline at end of file
diff --git a/options/gitignore/Nix b/options/gitignore/Nix
index 1fd04ef1f6..912e6700f4 100644
--- a/options/gitignore/Nix
+++ b/options/gitignore/Nix
@@ -1,3 +1,6 @@
# Ignore build outputs from performing a nix-build or `nix build` command
result
result-*
+
+# Ignore automatically generated direnv output
+.direnv
diff --git a/options/gitignore/NotesAndCoreConfiguration b/options/gitignore/NotesAndCoreConfiguration
new file mode 100644
index 0000000000..4eff01dae1
--- /dev/null
+++ b/options/gitignore/NotesAndCoreConfiguration
@@ -0,0 +1,16 @@
+# Excludes Obsidian workspace cache and plugins. All notes and core obsidian
+# configuration files are tracked by Git.
+
+# The current application UI state (DOM layout, recently-opened files, etc.) is
+# stored in these files (separate for desktop and mobile) so you can resume
+# your session seamlessly after a restart. If you want to track UI state, use
+# the Workspaces core plugin instead of relying on these files.
+.obsidian/workspace.json
+.obsidian/workspace-mobile.json
+
+# Obsidian plugins are stored under .obsidian/plugins/$plugin_name. They
+# contain metadata (manifest.json), application code (main.js), stylesheets
+# (styles.css), and user-configuration data (data.json).
+# We want to exclude all plugin-related files, so we can exclude everything
+# under this directory.
+.obsidian/plugins/**/*
diff --git a/options/gitignore/NotesAndExtendedConfiguration b/options/gitignore/NotesAndExtendedConfiguration
new file mode 100644
index 0000000000..3e0804f299
--- /dev/null
+++ b/options/gitignore/NotesAndExtendedConfiguration
@@ -0,0 +1,38 @@
+# Excludes Obsidian workspace cache and plugin code, but retains plugin
+# configuration. All notes and user-controlled configuration files are tracked
+# by Git.
+#
+# !!! WARNING !!!
+#
+# Community plugins may store sensitive secrets in their data.json files. By
+# including these files, those secrets may be tracked in your Git repository.
+#
+# To ignore configurations for specific plugins, add a line like this after the
+# contents of this file (order is important):
+# .obsidian/plugins/{{plugin_name}}/data.json
+#
+# Alternatively, ensure that you are treating your entire Git repository as
+# sensitive data, since it may contain secrets, or may have contained them in
+# past commits. Understand your threat profile, and make the decision
+# appropriate for yourself. If in doubt, err on the side of not including
+# plugin configuration. Use one of the alternative gitignore files instead:
+# * NotesOnly.gitignore
+# * NotesAndCoreConfiguration.gitignore
+
+# The current application UI state (DOM layout, recently-opened files, etc.) is
+# stored in these files (separate for desktop and mobile) so you can resume
+# your session seamlessly after a restart. If you want to track UI state, use
+# the Workspaces core plugin instead of relying on these files.
+.obsidian/workspace.json
+.obsidian/workspace-mobile.json
+
+# Obsidian plugins are stored under .obsidian/plugins/$plugin_name. They
+# contain metadata (manifest.json), application code (main.js), stylesheets
+# (styles.css), and user-configuration data (data.json).
+# We only want to track data.json, so we:
+# 1. exclude everything under the plugins directory recursively,
+# 2. unignore the plugin directories themselves, which then allows us to
+# 3. unignore the data.json files
+.obsidian/plugins/**/*
+!.obsidian/plugins/*/
+!.obsidian/plugins/*/data.json
diff --git a/options/gitignore/NotesOnly b/options/gitignore/NotesOnly
new file mode 100644
index 0000000000..2b3b76ee0e
--- /dev/null
+++ b/options/gitignore/NotesOnly
@@ -0,0 +1,4 @@
+# Excludes all Obsidian-related configuration. All notes are tracked by Git.
+
+# All Obsidian configuration and runtime state is stored here
+.obsidian/**/*
diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini
index 91605a1f31..3f2ac68802 100644
--- a/options/locale/locale_cs-CZ.ini
+++ b/options/locale/locale_cs-CZ.ini
@@ -54,6 +54,7 @@ webauthn_reload=Znovu naÄíst
repository=Repozitář
organization=Organizace
mirror=Zrcadlo
+issue_milestone=Milník
new_repo=Nový repozitář
new_migrate=Nová migrace
new_mirror=Nové zrcadlo
@@ -384,6 +385,7 @@ show_only_public=Zobrazeny pouze veřejné
issues.in_your_repos=Ve vašich repozitářích
+
[explore]
repos=Repozitáře
users=Uživatelé
@@ -1253,6 +1255,7 @@ labels=Štítky
org_labels_desc=Štítky na úrovni organizace, které mohou být použity se <strong>všemi repozitáři</strong> v rámci této organizace
org_labels_desc_manage=spravovat
+milestone=Milník
milestones=Milníky
commits=Commity
commit=Commit
@@ -2873,6 +2876,7 @@ view_as_role=Zobrazit jako: %s
view_as_public_hint=Prohlížíte README jako veřejný uživatel.
view_as_member_hint=Prohlížíte README jako Älen této organizace.
+
[admin]
maintenance=Údržba
dashboard=Přehled
diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini
index 29ef51bfc4..f1eada3990 100644
--- a/options/locale/locale_de-DE.ini
+++ b/options/locale/locale_de-DE.ini
@@ -54,6 +54,7 @@ webauthn_reload=Neu laden
repository=Repository
organization=Organisation
mirror=Mirror
+issue_milestone=Meilenstein
new_repo=Neues Repository
new_migrate=Neue Migration
new_mirror=Neuer Mirror
@@ -383,6 +384,7 @@ show_only_public=Nur öffentliche anzeigen
issues.in_your_repos=Eigene Repositories
+
[explore]
repos=Repositories
users=Benutzer
@@ -1247,6 +1249,7 @@ labels=Label
org_labels_desc=Labels der Organisationsebene, die mit <strong>allen Repositories</strong> in dieser Organisation verwendet werden können
org_labels_desc_manage=verwalten
+milestone=Meilenstein
milestones=Meilensteine
commits=Commits
commit=Commit
@@ -2854,6 +2857,7 @@ teams.invite.by=Von %s eingeladen
teams.invite.description=Bitte klicke auf die folgende Schaltfläche, um dem Team beizutreten.
+
[admin]
maintenance=Wartung
dashboard=Dashboard
diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini
index e989819c5e..7fb4151f17 100644
--- a/options/locale/locale_el-GR.ini
+++ b/options/locale/locale_el-GR.ini
@@ -53,6 +53,7 @@ webauthn_reload=Ανανέωση
repository=ΑποθετήÏιο
organization=ΟÏγανισμός
mirror=ΑντίγÏαφο
+issue_milestone=ΟÏόσημο
new_repo=Îέο ΑποθετήÏιο
new_migrate=Îέα ΜεταφοÏά
new_mirror=Îέο Είδωλο
@@ -334,6 +335,7 @@ show_only_public=Εμφανίζονται μόνο δημόσια
issues.in_your_repos=Στα αποθετήÏια σας
+
[explore]
repos=ΑποθετήÏια
users=ΧÏήστες
@@ -1119,6 +1121,7 @@ labels=Σήματα
org_labels_desc=Τα σήματα στο επίπεδο οÏγανισμοÏ, που μποÏοÏν να χÏησιμοποιηθοÏν με <strong>όλα τα αποθετήÏια</strong> κάτω από αυτόν τον οÏγανισμό
org_labels_desc_manage=διαχείÏιση
+milestone=ΟÏόσημο
milestones=ΟÏόσημα
commits=Υποβολές
commit=Υποβολή
@@ -2590,6 +2593,7 @@ teams.invite.by=ΠÏοσκλήθηκε από %s
teams.invite.description=ΠαÏακαλώ κάντε κλικ στον παÏακάτω σÏνδεσμο για συμμετοχή στην ομάδα.
+
[admin]
dashboard=Πίνακας Ελέγχου
identity_access=Ταυτότητα & ΠÏόσβαση
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 68b7fa2f9f..bce64a81a3 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -54,6 +54,7 @@ webauthn_reload = Reload
repository = Repository
organization = Organization
mirror = Mirror
+issue_milestone = Milestone
new_repo = New Repository
new_migrate = New Migration
new_mirror = New Mirror
@@ -384,6 +385,13 @@ show_only_public = Showing only public
issues.in_your_repos = In your repositories
+guide_title = No Activity
+guide_desc = You are currently not following any repositories or users, so there is no content to display. You can explore repositories or users of interest from the links below.
+explore_repos = Explore repositories
+explore_users = Explore users
+empty_org = There are no organizations yet.
+empty_repo = There are no repositories yet.
+
[explore]
repos = Repositories
users = Users
@@ -1253,6 +1261,7 @@ labels = Labels
org_labels_desc = Organization level labels that can be used with <strong>all repositories</strong> under this organization
org_labels_desc_manage = manage
+milestone = Milestone
milestones = Milestones
commits = Commits
commit = Commit
@@ -2329,6 +2338,8 @@ settings.event_fork = Fork
settings.event_fork_desc = Repository forked.
settings.event_wiki = Wiki
settings.event_wiki_desc = Wiki page created, renamed, edited or deleted.
+settings.event_statuses = Statuses
+settings.event_statuses_desc = Commit Status updated from the API.
settings.event_release = Release
settings.event_release_desc = Release published, updated or deleted in a repository.
settings.event_push = Push
@@ -2876,6 +2887,15 @@ view_as_role = View as: %s
view_as_public_hint = You are viewing the README as a public user.
view_as_member_hint = You are viewing the README as a member of this organization.
+worktime = Worktime
+worktime.date_range_start = Start date
+worktime.date_range_end = End date
+worktime.query = Query
+worktime.time = Time
+worktime.by_repositories = By repositories
+worktime.by_milestones = By milestones
+worktime.by_members = By members
+
[admin]
maintenance = Maintenance
dashboard = Dashboard
diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini
index 049fb9196d..c399b1209c 100644
--- a/options/locale/locale_es-ES.ini
+++ b/options/locale/locale_es-ES.ini
@@ -52,6 +52,7 @@ webauthn_reload=Recargar
repository=Repositorio
organization=Organización
mirror=Réplica
+issue_milestone=Hito
new_repo=Nuevo repositorio
new_migrate=Nueva migración
new_mirror=Nueva réplica
@@ -332,6 +333,7 @@ show_only_public=Mostrar sólo repositorios públicos
issues.in_your_repos=En tus repositorios
+
[explore]
repos=Repositorios
users=Usuarios
@@ -1109,6 +1111,7 @@ labels=Etiquetas
org_labels_desc=Etiquetas de nivel de la organización que pueden ser utilizadas con <strong>todos los repositorios</strong> bajo esta organización
org_labels_desc_manage=gestionar
+milestone=Hito
milestones=Hitos
commits=Commits
commit=Commit
@@ -2571,6 +2574,7 @@ teams.invite.by=Invitado por %s
teams.invite.description=Por favor, haga clic en el botón de abajo para unirse al equipo.
+
[admin]
dashboard=Panel de control
identity_access=Identidad y acceso
diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini
index 4d90cf9876..3d34e01722 100644
--- a/options/locale/locale_fa-IR.ini
+++ b/options/locale/locale_fa-IR.ini
@@ -256,6 +256,7 @@ show_only_public=نمایش دادن موارد عمومی
issues.in_your_repos=در مخازن شما
+
[explore]
repos=مخازن
users=کاربران
@@ -1993,6 +1994,7 @@ teams.all_repositories_write_permission_desc=این تیم دسترسی<strong>
teams.all_repositories_admin_permission_desc=این تیم دسترسی<strong> مدیر </strong> به <strong> مخازن همه</strong> را Ù…ÛŒ بخشد: اعضا Ù…ÛŒ توانند مخازن را بخواند، همکار Ùˆ مخزن اضاÙÙ‡ کنند.
+
[admin]
dashboard=پیشخوان
users=حساب کاربران
diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini
index b5fa5c8afc..d78a06ae20 100644
--- a/options/locale/locale_fi-FI.ini
+++ b/options/locale/locale_fi-FI.ini
@@ -49,6 +49,7 @@ webauthn_reload=Päivitä
repository=Repo
organization=Organisaatio
mirror=Peili
+issue_milestone=Merkkipaalu
new_repo=Uusi repo
new_migrate=Uusi migraatio
new_mirror=Uusi peilaus
@@ -265,6 +266,7 @@ show_only_public=Näytetään vain julkiset
issues.in_your_repos=Repoissasi
+
[explore]
repos=Repot
users=Käyttäjät
@@ -720,6 +722,7 @@ projects=Projektit
packages=Paketit
labels=Tunnisteet
+milestone=Merkkipaalu
milestones=Merkkipaalut
commits=Commitit
commit=Commit
@@ -1361,6 +1364,7 @@ teams.members.none=Ei jäseniä tässä tiimissä.
teams.all_repositories=Kaikki repot
+
[admin]
dashboard=Kojelauta
users=Käyttäjätilit
diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini
index a5558eebb0..df141aabc8 100644
--- a/options/locale/locale_fr-FR.ini
+++ b/options/locale/locale_fr-FR.ini
@@ -54,6 +54,7 @@ webauthn_reload=Recharger
repository=Dépôt
organization=Organisation
mirror=Miroir
+issue_milestone=Jalon
new_repo=Nouveau dépôt
new_migrate=Nouvelle migration
new_mirror=Nouveau miroir
@@ -384,6 +385,7 @@ show_only_public=Afficher uniquement les dépôts publics
issues.in_your_repos=Dans vos dépôts
+
[explore]
repos=Dépôts
users=Utilisateurs
@@ -1172,7 +1174,7 @@ migrate_items_releases=Publications
migrate_repo=Migrer le dépôt
migrate.clone_address=Migrer/Cloner depuis une URL
migrate.clone_address_desc=L'URL HTTP(S) ou Git "clone" d'un dépôt existant
-migrate.github_token_desc=Vous pouvez mettre un ou plusieurs jetons séparés par des virgules ici pour rendre la migration plus rapide en raison de la limite de débit de l'API GitHub. ATTENTION : Abuser de cette fonctionnalité peut enfreindre la politique du fournisseur de services et entraîner un blocage de compte.
+migrate.github_token_desc=Vous pouvez mettre un ou plusieurs jetons séparés par des virgules ici pour rendre la migration plus rapide et contourner la limite de débit de l’API GitHub. ATTENTION : Abuser de cette fonctionnalité peut enfreindre la politique du fournisseur de service et entraîner un blocage de votre compte.
migrate.clone_local_path=ou un chemin serveur local
migrate.permission_denied=Vous n'êtes pas autorisé à importer des dépôts locaux.
migrate.permission_denied_blocked=Vous ne pouvez pas importer depuis des hôtes interdits, veuillez demander à l'administrateur de vérifier les paramètres ALLOWED_DOMAINS/ALLOW_LOCALNETWORKS/BLOCKED_DOMAINS.
@@ -1253,6 +1255,7 @@ labels=Labels
org_labels_desc=Les labels d'une organisation peuvent être utilisés avec <strong>tous les dépôts</strong> de cette organisation.
org_labels_desc_manage=gérer
+milestone=Jalon
milestones=Jalons
commits=Révisions
commit=Révision
@@ -1345,6 +1348,8 @@ editor.new_branch_name_desc=Nouveau nom de la branche…
editor.cancel=Annuler
editor.filename_cannot_be_empty=Le nom de fichier ne peut être vide.
editor.filename_is_invalid=Le nom du fichier est invalide : "%s".
+editor.commit_email=Courriel de la révision
+editor.invalid_commit_email=Le courriel pour la révision n’est pas valide.
editor.branch_does_not_exist=La branche "%s" n'existe pas dans ce dépôt.
editor.branch_already_exists=La branche "%s" existe déjà dans ce dépôt.
editor.directory_is_a_file=Le nom de dossier "%s" est déjà utilisé comme nom de fichier dans ce dépôt.
@@ -1562,12 +1567,12 @@ issues.action_assignee=Assigné à
issues.action_assignee_no_select=Pas d'assignataire
issues.action_check=Cocher/Décocher
issues.action_check_all=Cocher/Décocher tous les éléments
-issues.opened_by=créé %[1]s par <a href="%[2]s">%[3]s</a>
-pulls.merged_by=par <a href="%[2]s">%[3]s</a> fusionné %[1]s.
-pulls.merged_by_fake=par %[2]s fusionné %[1]s.
-issues.closed_by=de <a href="%[2]s">%[3]s</a>, clôt %[1]s
-issues.opened_by_fake=%[1]s ouvert par %[2]s
-issues.closed_by_fake=de %[2]s, clôt %[1]s
+issues.opened_by=ouvert(e) par <a href="%[2]s">%[3]s</a> %[1]s
+pulls.merged_by=par <a href="%[2]s">%[3]s</a> a été fusionnée %[1]s
+pulls.merged_by_fake=par %[2]s a été fusionnée %[1]s
+issues.closed_by=par <a href="%[2]s">%[3]s</a> a été fermé(e) %[1]s
+issues.opened_by_fake=ouvert(e) par %[2]s %[1]s
+issues.closed_by_fake=par %[2]s a été fermé(e) %[1]s
issues.previous=Précédent
issues.next=Suivant
issues.open_title=Ouvert
@@ -1735,8 +1740,8 @@ issues.dependency.added_dependency=`a créé une dépendance %s.`
issues.dependency.removed_dependency=`a supprimé une dépendance %s.`
issues.dependency.pr_closing_blockedby=La fermeture de cette demande d’ajout est bloquée par les tickets suivants
issues.dependency.issue_closing_blockedby=La fermeture de ce ticket est bloquée par les tickets suivants
-issues.dependency.issue_close_blocks=Cette demande d'ajout empêche la clôture des tickets suivants
-issues.dependency.pr_close_blocks=Cette demande d'ajout empêche la clôture des tickets suivants
+issues.dependency.issue_close_blocks=Ce ticket empêche la clôture des tickets suivants
+issues.dependency.pr_close_blocks=Cette demande d’ajout empêche la clôture des tickets suivants
issues.dependency.issue_close_blocked=Vous devez fermer tous les tickets qui bloquent ce ticket avant de pouvoir le fermer.
issues.dependency.issue_batch_close_blocked=Impossible de fermer tous les tickets que vous avez choisis, car le ticket #%d a toujours des dépendances ouvertes.
issues.dependency.pr_close_blocked=Vous devez fermer tous les tickets qui bloquent cette demande d'ajout avant de pouvoir la fusionner.
@@ -2873,6 +2878,7 @@ view_as_role=Voir en tant que %s
view_as_public_hint=Vous visualisez le README en tant qu’utilisateur public.
view_as_member_hint=Vous visualisez le README en tant que membre de cette organisation.
+
[admin]
maintenance=Maintenance
dashboard=Tableau de bord
diff --git a/options/locale/locale_ga-IE.ini b/options/locale/locale_ga-IE.ini
index 805deb618d..6cacc4a6d9 100644
--- a/options/locale/locale_ga-IE.ini
+++ b/options/locale/locale_ga-IE.ini
@@ -54,6 +54,7 @@ webauthn_reload=Athlódáil
repository=Stór
organization=Eagraíocht
mirror=Scáthán
+issue_milestone=Cloch Mhíle
new_repo=Stór Nua
new_migrate=Imirce Nua
new_mirror=Scáthán Nua
@@ -384,6 +385,7 @@ show_only_public=Ag taispeáint poiblí amháin
issues.in_your_repos=I do stórais
+
[explore]
repos=Stórais
users=Úsáideoirí
@@ -1253,6 +1255,7 @@ labels=Lipéid
org_labels_desc=Lipéid ar leibhéal eagraíochta is féidir a úsáid le <strong>gach stóras</strong> faoin eagraíocht seo
org_labels_desc_manage=bainistigh
+milestone=Cloch Mhíle
milestones=Clocha míle
commits=Tiomáintí
commit=Tiomantas
@@ -1345,6 +1348,8 @@ editor.new_branch_name_desc=Ainm brainse nua…
editor.cancel=Cealaigh
editor.filename_cannot_be_empty=Ní féidir ainm an chomhaid a bheith folamh.
editor.filename_is_invalid=Tá ainm an chomhaid neamhbhailí: "%s".
+editor.commit_email=Tiomantas ríomhphost
+editor.invalid_commit_email=Tá an ríomhphost don ghealltanas neamhbhailí.
editor.branch_does_not_exist=Níl brainse "%s" ann sa stóras seo.
editor.branch_already_exists=Tá brainse "%s" ann cheana féin sa stóras seo.
editor.directory_is_a_file=Úsáidtear ainm eolaire "%s" cheana féin mar ainm comhaid sa stóras seo.
@@ -2326,6 +2331,8 @@ settings.event_fork=Forc
settings.event_fork_desc=Forcadh stóras.
settings.event_wiki=Vicí
settings.event_wiki_desc=Leathanach Vicí cruthaithe, athainmnithe, curtha in eagar nó scriosta.
+settings.event_statuses=Stádais
+settings.event_statuses_desc=Nuashonraíodh Stádas Commit ón API.
settings.event_release=Scaoileadh
settings.event_release_desc=Scaoileadh foilsithe, nuashonraithe nó scriosta i stóras.
settings.event_push=Brúigh
@@ -2873,6 +2880,15 @@ view_as_role=Féach mar: %s
view_as_public_hint=Tá tú ag féachaint ar an README mar úsáideoir poiblí.
view_as_member_hint=Tá tú ag féachaint ar an README mar bhall den eagraíocht seo.
+worktime=Am oibre
+worktime.date_range_start=Dáta tosaithe
+worktime.date_range_end=Dáta deiridh
+worktime.query=Ceist
+worktime.time=Am
+worktime.by_repositories=De réir stórtha
+worktime.by_milestones=De réir clocha míle
+worktime.by_members=Ag baill
+
[admin]
maintenance=Cothabháil
dashboard=Deais
diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini
index f0935a2916..4767a48547 100644
--- a/options/locale/locale_hu-HU.ini
+++ b/options/locale/locale_hu-HU.ini
@@ -225,6 +225,7 @@ show_only_public=Csak publikus mutatása
issues.in_your_repos=A tárolóidban
+
[explore]
repos=Tárolók
users=Felhasználók
@@ -1229,6 +1230,7 @@ teams.specific_repositories=Meghatározott tárolók
teams.all_repositories=Minden tároló
+
[admin]
dashboard=Műszerfal
users=Felhasználói fiókok
diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini
index 391691ebf5..2beade34ad 100644
--- a/options/locale/locale_id-ID.ini
+++ b/options/locale/locale_id-ID.ini
@@ -243,6 +243,7 @@ show_private=Pribadi
issues.in_your_repos=Dalam repositori anda
+
[explore]
repos=Repositori
users=Pengguna
@@ -1084,6 +1085,7 @@ teams.delete_team_success=Tim sudah di hapus.
teams.repositories=Tim repositori
+
[admin]
dashboard=Dasbor
organizations=Organisasi
diff --git a/options/locale/locale_is-IS.ini b/options/locale/locale_is-IS.ini
index 1eab4d58be..98a8615130 100644
--- a/options/locale/locale_is-IS.ini
+++ b/options/locale/locale_is-IS.ini
@@ -49,6 +49,7 @@ webauthn_reload=Endurhlaða
repository=Hugbúnaðarsafn
organization=Stofnun
mirror=Speglun
+issue_milestone=Tímamót
new_repo=Nýtt Hugbúnaðarsafn
new_migrate=Nýr Flutningur
new_mirror=Ný Speglun
@@ -239,6 +240,7 @@ show_only_public=Að sýna aðeins opinber
issues.in_your_repos=à hugbúnaðarsöfnum þínum
+
[explore]
repos=Hugbúnaðarsöfn
users=Notendur
@@ -652,6 +654,7 @@ projects=Verkefni
packages=Pakkar
labels=Skýringar
+milestone=Tímamót
milestones=Tímamót
commits=Framlög
commit=Framlag
@@ -1137,6 +1140,7 @@ teams.update_settings=Uppfæra Stillingar
teams.all_repositories=Öll hugbúnaðarsöfn
+
[admin]
repositories=Hugbúnaðarsöfn
config=Stilling
diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini
index 17f0aa83d2..29512f47f3 100644
--- a/options/locale/locale_it-IT.ini
+++ b/options/locale/locale_it-IT.ini
@@ -50,6 +50,7 @@ webauthn_reload=Ricarica
repository=Repository
organization=Organizzazione
mirror=Mirror
+issue_milestone=Traguardo
new_repo=Nuovo Repository
new_migrate=Nuova Migrazione
new_mirror=Nuovo Mirror
@@ -276,6 +277,7 @@ show_only_public=Mostrando solo pubblici
issues.in_your_repos=Nei tuoi repository
+
[explore]
repos=Repository
users=Utenti
@@ -942,6 +944,7 @@ labels=Etichette
org_labels_desc=Etichette a livello di organizzazione che possono essere utilizzate con <strong>tutti i repository</strong> sotto questa organizzazione
org_labels_desc_manage=gestisci
+milestone=Traguardo
milestones=Traguardi
commits=Commit
commit=Commit
@@ -2154,6 +2157,7 @@ teams.all_repositories_write_permission_desc=Questo team concede <strong>permess
teams.all_repositories_admin_permission_desc=Questo team concede a <strong>Amministratore</strong> l'accesso a <strong>tutte le repository</strong>: i membri possono leggere, pushare e aggiungere collaboratori alle repository.
+
[admin]
dashboard=Pannello di Controllo
users=Account utenti
diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini
index 925d0249b7..bc29d530b4 100644
--- a/options/locale/locale_ja-JP.ini
+++ b/options/locale/locale_ja-JP.ini
@@ -54,6 +54,7 @@ webauthn_reload=リロード
repository=リãƒã‚¸ãƒˆãƒª
organization=組織
mirror=ミラー
+issue_milestone=マイルストーン
new_repo=æ–°ã—ã„リãƒã‚¸ãƒˆãƒª
new_migrate=æ–°ã—ã„移行
new_mirror=æ–°ã—ã„ミラー
@@ -384,6 +385,7 @@ show_only_public=公開ã®ã¿è¡¨ç¤º
issues.in_your_repos=ã‚ãªãŸã®ãƒªãƒã‚¸ãƒˆãƒª
+
[explore]
repos=リãƒã‚¸ãƒˆãƒª
users=ユーザー
@@ -1253,6 +1255,7 @@ labels=ラベル
org_labels_desc=組織ã§å®šç¾©ã•ã‚Œã¦ã„るラベル (組織ã®<strong>ã™ã¹ã¦ã®ãƒªãƒã‚¸ãƒˆãƒª</strong>ã§ä½¿ç”¨å¯èƒ½ãªã‚‚ã®)
org_labels_desc_manage=編集
+milestone=マイルストーン
milestones=マイルストーン
commits=コミット
commit=コミット
@@ -2873,6 +2876,7 @@ view_as_role=表示: %s
view_as_public_hint=READMEを公開ユーザーã¨ã—ã¦è¦‹ã¦ã„ã¾ã™ã€‚
view_as_member_hint=READMEã‚’ã“ã®çµ„ç¹”ã®ãƒ¡ãƒ³ãƒãƒ¼ã¨ã—ã¦è¦‹ã¦ã„ã¾ã™ã€‚
+
[admin]
maintenance=メンテナンス
dashboard=ダッシュボード
diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini
index 5485a53c81..a570a05274 100644
--- a/options/locale/locale_ko-KR.ini
+++ b/options/locale/locale_ko-KR.ini
@@ -212,6 +212,7 @@ show_private=비공개
issues.in_your_repos=ë‹¹ì‹ ì˜ ì €ìž¥ì†Œì—
+
[explore]
repos=저장소
users=유저
@@ -1191,6 +1192,7 @@ teams.add_duplicate_users=사용ìžê°€ ì´ë¯¸ 팀 멤버입니다.
teams.members.none=ì´ íŒ€ì— ë©¤ë²„ê°€ 없습니다.
+
[admin]
dashboard=대시보드
users=ì‚¬ìš©ìž ê³„ì •
diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini
index cc2dcd1180..d2df0813ae 100644
--- a/options/locale/locale_lv-LV.ini
+++ b/options/locale/locale_lv-LV.ini
@@ -54,6 +54,7 @@ webauthn_reload=PÄrlÄdÄ“t
repository=Repozitorijs
organization=OrganizÄcija
mirror=Spogulis
+issue_milestone=Atskaites punktus
new_repo=Jauns repozitorijs
new_migrate=Jauna migrÄcija
new_mirror=Jauns spogulis
@@ -337,6 +338,7 @@ show_only_public=Attēlot tikai publiskos
issues.in_your_repos=JÅ«su repozitorijos
+
[explore]
repos=Repozitoriji
users=LietotÄji
@@ -1125,6 +1127,7 @@ labels=Iezīmes
org_labels_desc=OrganizÄcijas lÄ«meņa iezÄ«mes var tikt izmantotas <strong>visiem repozitorijiem</strong> Å¡ajÄ organizÄcijÄ
org_labels_desc_manage=pÄrvaldÄ«t
+milestone=Atskaites punktus
milestones=Atskaites punkti
commits=Revīzijas
commit=Revīzija
@@ -2593,6 +2596,7 @@ teams.invite.by=UzaicinÄja %s
teams.invite.description=Nospiediet pogu zemÄk, lai pievienotos komandai.
+
[admin]
dashboard=Infopanelis
self_check=PaÅ¡pÄrbaude
diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini
index 8a6dabbceb..c23df29e99 100644
--- a/options/locale/locale_nl-NL.ini
+++ b/options/locale/locale_nl-NL.ini
@@ -50,6 +50,7 @@ webauthn_reload=Vernieuwen
repository=Repository
organization=Organisatie
mirror=Kopie
+issue_milestone=Mijlpaal
new_repo=Nieuwe repository
new_migrate=Nieuwe migratie
new_mirror=Nieuwe kopie
@@ -275,6 +276,7 @@ show_only_public=Toon alleen opbenbaar
issues.in_your_repos=In uw repositories
+
[explore]
repos=Repositories
users=Gebruikers
@@ -940,6 +942,7 @@ labels=Labels
org_labels_desc=Organisatielabel dat gebruikt kan worden met <strong>alle repositories</strong> onder deze organisatie
org_labels_desc_manage=beheren
+milestone=Mijlpaal
milestones=Mijlpalen
commits=Commits
commit=Commit
@@ -2055,6 +2058,7 @@ teams.all_repositories_helper=Team heeft toegang tot alle repositories. Door dit
teams.all_repositories_read_permission_desc=Dit team heeft <strong>Lees</strong> toegang tot <strong>alle repositories</strong>: leden kunnen repositories bekijken en klonen.
+
[admin]
dashboard=Overzicht
users=Gebruikersacount
diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini
index 4dfae86bb6..d03018c0d9 100644
--- a/options/locale/locale_pl-PL.ini
+++ b/options/locale/locale_pl-PL.ini
@@ -272,6 +272,7 @@ show_only_public=Wyświetlanie tylko publicznych
issues.in_your_repos=W Twoich repozytoriach
+
[explore]
repos=Repozytoria
users=Użytkownicy
@@ -1934,6 +1935,7 @@ teams.all_repositories_write_permission_desc=Ten zespół nadaje uprawnienie <st
teams.all_repositories_admin_permission_desc=Ten zespół nadaje uprawnienia <strong>Administratora</strong> do <strong>wszystkich repozytoriów</strong>: jego członkowie mogą odczytywać, przesyłać oraz dodawać innych współtwórców do repozytoriów.
+
[admin]
dashboard=Pulpit
users=Konta użytkownika
diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini
index cc21c5abea..33aad76023 100644
--- a/options/locale/locale_pt-BR.ini
+++ b/options/locale/locale_pt-BR.ini
@@ -52,6 +52,7 @@ webauthn_reload=Recarregar
repository=Repositório
organization=Organização
mirror=Espelhamento
+issue_milestone=Marco
new_repo=Novo repositório
new_migrate=Nova migração
new_mirror=Novo espelhamento
@@ -334,6 +335,7 @@ show_only_public=Mostrando somente públicos
issues.in_your_repos=Em seus repositórios
+
[explore]
repos=Repositórios
users=Usuários
@@ -1119,6 +1121,7 @@ labels=Etiquetas
org_labels_desc=Rótulos de nível de organização que podem ser usados em <strong>todos os repositórios</strong> sob esta organização
org_labels_desc_manage=gerenciar
+milestone=Marco
milestones=Marcos
commits=Commits
commit=Commit
@@ -2551,6 +2554,7 @@ teams.invite.by=Convidado por %s
teams.invite.description=Por favor, clique no botão abaixo para se juntar à equipe.
+
[admin]
dashboard=Painel
identity_access=Identidade e acesso
diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini
index 88308271a7..d914b1c810 100644
--- a/options/locale/locale_pt-PT.ini
+++ b/options/locale/locale_pt-PT.ini
@@ -54,6 +54,7 @@ webauthn_reload=Recarregar
repository=Repositório
organization=Organização
mirror=Réplica
+issue_milestone=Etapa
new_repo=Novo repositório
new_migrate=Nova migração
new_mirror=Nova réplica
@@ -384,6 +385,7 @@ show_only_public=Apresentando somente os públicos
issues.in_your_repos=Nos seus repositórios
+
[explore]
repos=Repositórios
users=Utilizadores
@@ -1253,6 +1255,7 @@ labels=Rótulos
org_labels_desc=Rótulos ao nível da organização que podem ser usados em <strong>todos os repositórios</strong> desta organização
org_labels_desc_manage=gerir
+milestone=Etapa
milestones=Etapas
commits=Cometimentos
commit=Cometimento
@@ -1345,6 +1348,8 @@ editor.new_branch_name_desc=Nome do novo ramo…
editor.cancel=Cancelar
editor.filename_cannot_be_empty=O nome do ficheiro não pode estar em branco.
editor.filename_is_invalid=O nome do ficheiro é inválido: "%s".
+editor.commit_email=Email do cometimento
+editor.invalid_commit_email=O email do comentimento é inválido.
editor.branch_does_not_exist=O ramo "%s" não existe neste repositório.
editor.branch_already_exists=O ramo "%s" já existe neste repositório.
editor.directory_is_a_file=O nome da pasta "%s" já é usado como um nome de ficheiro neste repositório.
@@ -2326,6 +2331,8 @@ settings.event_fork=Derivar
settings.event_fork_desc=Feita a derivação do repositório.
settings.event_wiki=Wiki
settings.event_wiki_desc=Página do wiki criada, renomeada, editada ou eliminada.
+settings.event_statuses=Estados
+settings.event_statuses_desc=Estado do cometimento modificado através da API.
settings.event_release=Lançamento
settings.event_release_desc=Lançamento publicado, modificado ou eliminado num repositório.
settings.event_push=Enviar
@@ -2873,6 +2880,15 @@ view_as_role=Ver como: %s
view_as_public_hint=Está a ver o README como um utilizador público.
view_as_member_hint=Está a ver o README como um membro desta organização.
+worktime=Tempo de trabalho
+worktime.date_range_start=Data do início
+worktime.date_range_end=Data do fim
+worktime.query=Consulta
+worktime.time=Tempo
+worktime.by_repositories=Por repositórios
+worktime.by_milestones=Por etapas
+worktime.by_members=Por membros
+
[admin]
maintenance=Manutenção
dashboard=Painel de controlo
diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini
index d3b673bd18..0aa776b78a 100644
--- a/options/locale/locale_ru-RU.ini
+++ b/options/locale/locale_ru-RU.ini
@@ -52,6 +52,7 @@ webauthn_reload=Обновить
repository=Репозиторий
organization=ОрганизациÑ
mirror=Зеркало
+issue_milestone=Этап
new_repo=Ðовый репозиторий
new_migrate=ÐÐ¾Ð²Ð°Ñ Ð¼Ð¸Ð³Ñ€Ð°Ñ†Ð¸Ñ
new_mirror=Ðовое зеркало
@@ -332,6 +333,7 @@ show_only_public=Показаны только публичные
issues.in_your_repos=Ð’ ваших репозиториÑÑ…
+
[explore]
repos=Репозитории
users=Пользователи
@@ -1100,6 +1102,7 @@ labels=Метки
org_labels_desc=Метки ÑƒÑ€Ð¾Ð²Ð½Ñ Ð¾Ñ€Ð³Ð°Ð½Ð¸Ð·Ð°Ñ†Ð¸Ð¸, которые можно иÑпользовать Ñ <strong>вÑеми репозиториÑми</strong> в Ñтой организации
org_labels_desc_manage=управлÑÑ‚ÑŒ
+milestone=Этап
milestones=Этапы
commits=коммитов
commit=коммит
@@ -2540,6 +2543,7 @@ teams.invite.by=Приглашен(а) %s
teams.invite.description=Ðажмите на кнопку ниже, чтобы приÑоединитьÑÑ Ðº команде.
+
[admin]
dashboard=Панель
identity_access=Ð˜Ð´ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ Ð¸ доÑтуп
diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini
index 167ecaf24a..80db8862fe 100644
--- a/options/locale/locale_si-LK.ini
+++ b/options/locale/locale_si-LK.ini
@@ -246,6 +246,7 @@ show_only_public=ප්â€à¶»à·ƒà·’ද්ධ පමණක් පෙන්වයà
issues.in_your_repos=ඔබගේ කà·à·‚්ඨවල
+
[explore]
repos=කà·à·‚්ඨ
users=පරිà·à·“ලකයින්
@@ -1955,6 +1956,7 @@ teams.all_repositories_write_permission_desc=මෙම කණ්ඩà·à¶ºà¶¸ ප
teams.all_repositories_admin_permission_desc=මෙම කණ්ඩà·à¶ºà¶¸ ප්රදà·à¶±à¶º කරයි <strong>පරිපà·à¶½à¶š</strong> වෙත ප්රවේà·à¶º <strong>සියලු ගබඩà·à·€à¶±à·Šà¶§</strong>: à·ƒà·à¶¸à·à¶¢à·’කයින්ට කියවීමට, තල්ලු කිරීමට සහ ගබඩà·à·€à¶±à·Šà¶§ සහයà·à¶œà·“කයින් එකතු කිරීමට.
+
[admin]
dashboard=උපකරණ පුවරුව
users=පරිà·à·“ලක ගිණුම්
diff --git a/options/locale/locale_sk-SK.ini b/options/locale/locale_sk-SK.ini
index cd2f915755..53ea17b43e 100644
--- a/options/locale/locale_sk-SK.ini
+++ b/options/locale/locale_sk-SK.ini
@@ -53,6 +53,7 @@ webauthn_reload=Znovu naÄítaÅ¥
repository=Repozitár
organization=Organizácia
mirror=Zrkadlo
+issue_milestone=Míľnik
new_repo=Nový repozitár
new_migrate=Nová migrácia
new_mirror=Nové zrkadlo
@@ -327,6 +328,7 @@ show_only_public=Zobrazuje sa iba verejné
issues.in_your_repos=Vo vašich repozitároch
+
[explore]
repos=Repozitáre
users=Používatelia
@@ -967,6 +969,7 @@ labels=Štítky
org_labels_desc=Štítky na úrovni organizácie, ktoré možno použiť so <strong>všetkými repozitármi</strong> v rámci tejto organizácie
org_labels_desc_manage=spravovať
+milestone=Míľnik
milestones=Míľniky
commits=Commitov
release=Vydanie
@@ -1236,6 +1239,7 @@ teams.all_repositories_write_permission_desc=Tomuto tímu je pridelený prístup
teams.all_repositories_admin_permission_desc=Tomuto tímu je pridelený <strong>Admin</strong> prístup ku <strong>vÅ¡etkým repozitárom</strong>: Älenovia môžu prezeraÅ¥, nahrávaÅ¥ do repozitárov a pridávaÅ¥ do nich spolupracovníkov.
+
[admin]
repositories=Repozitáre
hooks=Webhooky
diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini
index 0315ebe9a1..0d3d0f5fc4 100644
--- a/options/locale/locale_sv-SE.ini
+++ b/options/locale/locale_sv-SE.ini
@@ -233,6 +233,7 @@ show_only_public=Visar endast publika
issues.in_your_repos=I dina utvecklingskataloger
+
[explore]
repos=Utvecklingskataloger
users=Användare
@@ -1592,6 +1593,7 @@ teams.all_repositories_write_permission_desc=Detta team beviljar <strong>Skriv</
teams.all_repositories_admin_permission_desc=Detta team beviljar <strong>Admin</strong>-rättigheter till <strong>alla utvecklingskataloger</strong>: medlemmar kan läsa från, pusha till och lägga till kollaboratörer för utvecklingskatalogerna.
+
[admin]
dashboard=Instrumentpanel
users=Användarkonto
diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini
index 6d14f512ff..0454512402 100644
--- a/options/locale/locale_tr-TR.ini
+++ b/options/locale/locale_tr-TR.ini
@@ -54,6 +54,7 @@ webauthn_reload=Yeniden yükle
repository=Depo
organization=Organizasyon
mirror=Yansı
+issue_milestone=Dönüm noktası
new_repo=Yeni Depo
new_migrate=Yeni Göç
new_mirror=Yeni Yansı
@@ -78,7 +79,7 @@ forks=Çatallar
activities=Etkinlikler
pull_requests=DeÄŸiÅŸiklik Ä°stekleri
issues=Konular
-milestones=Kilometre Taşları
+milestones=Dönüm noktaları
ok=Tamam
cancel=Ä°ptal
@@ -379,6 +380,7 @@ show_only_public=Yalnızca açık olanlar gösteriliyor
issues.in_your_repos=Depolarınızda
+
[explore]
repos=Depolar
users=Kullanıcılar
@@ -1128,7 +1130,7 @@ migrate_options_lfs_endpoint.description.local=Yerel bir sunucu yolu da destekle
migrate_options_lfs_endpoint.placeholder=Boş bırakılırsa, uç nokta klon URL'sinden türetilecektir
migrate_items=Göç Öğeleri
migrate_items_wiki=Wiki
-migrate_items_milestones=Kilometre Taşları
+migrate_items_milestones=Dönüm noktaları
migrate_items_labels=Etiketler
migrate_items_issues=Konular
migrate_items_pullrequests=DeÄŸiÅŸiklik Ä°stekleri
@@ -1212,6 +1214,7 @@ labels=Etiketler
org_labels_desc=Bu organizasyon altında <strong>tüm depolarla</strong> kullanılabilen organizasyon düzeyinde etiketler
org_labels_desc_manage=yönet
+milestone=Dönüm noktası
milestones=Kilometre Taşları
commits=Ä°ÅŸleme
commit=Ä°ÅŸle
@@ -2752,6 +2755,7 @@ teams.invite.by=%s tarafından davet edildi
teams.invite.description=Takıma katılmak için aşağıdaki düğmeye tıklayın.
+
[admin]
maintenance=Bakım
dashboard=Pano
diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini
index 2b0e57c8e0..25ebb843a9 100644
--- a/options/locale/locale_uk-UA.ini
+++ b/options/locale/locale_uk-UA.ini
@@ -37,6 +37,7 @@ webauthn_reload=Оновити
repository=Репозиторій
organization=ОрганізаціÑ
mirror=Дзеркало
+issue_milestone=Етап
new_repo=Ðовий репозиторій
new_migrate=Ðова міграціÑ
new_mirror=Ðове дзеркало
@@ -259,6 +260,7 @@ show_only_public=Показано тільки публічні
issues.in_your_repos=Ð’ ваших репозиторіÑÑ…
+
[explore]
repos=Репозиторії
users=КориÑтувачі
@@ -889,6 +891,7 @@ labels=Мітки
org_labels_desc=Мітки Ñ€Ñ–Ð²Ð½Ñ Ð¾Ñ€Ð³Ð°Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñ— можуть викориÑтовуватиÑÑ <strong>в уÑÑ–Ñ… репозиторіÑÑ…</strong> цієї організації
org_labels_desc_manage=керувати
+milestone=Етап
milestones=Етап
commits=Коміти
commit=Коміт
@@ -2003,6 +2006,7 @@ teams.all_repositories_write_permission_desc=Ð¦Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð° надає до
teams.all_repositories_admin_permission_desc=Ð¦Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð° надає дозвіл <strong>ÐдмініÑтруваннÑ</strong> Ð´Ð»Ñ <strong>вÑÑ–Ñ… репозиторіїв</strong>: учаÑники можуть переглÑдати, виконувати push та додавати Ñпівробітників.
+
[admin]
dashboard=Панель управліннÑ
users=Облікові запиÑи кориÑтувачів
diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini
index 92de8a1280..3b6aca4e92 100644
--- a/options/locale/locale_zh-CN.ini
+++ b/options/locale/locale_zh-CN.ini
@@ -54,6 +54,7 @@ webauthn_reload=é‡æ–°åŠ è½½
repository=仓库
organization=组织
mirror=é•œåƒ
+issue_milestone=里程碑
new_repo=创建仓库
new_migrate=è¿ç§»å¤–部仓库
new_mirror=创建新的镜åƒ
@@ -383,6 +384,7 @@ show_only_public=åªæ˜¾ç¤ºå…¬å¼€çš„
issues.in_your_repos=在您的仓库中
+
[explore]
repos=仓库
users=用户
@@ -1247,6 +1249,7 @@ labels=标签
org_labels_desc=组织级别的标签,å¯ä»¥è¢«æœ¬ç»„织下的 <strong>所有仓库</strong> 使用
org_labels_desc_manage=管ç†
+milestone=里程碑
milestones=里程碑
commits=æ交
commit=æ交
@@ -2854,6 +2857,7 @@ teams.invite.by=邀请人 %s
teams.invite.description=请点击下é¢çš„按钮加入团队。
+
[admin]
maintenance=维护
dashboard=管ç†é¢æ¿
diff --git a/options/locale/locale_zh-HK.ini b/options/locale/locale_zh-HK.ini
index 77f8d8a25d..3733d95ec8 100644
--- a/options/locale/locale_zh-HK.ini
+++ b/options/locale/locale_zh-HK.ini
@@ -118,6 +118,7 @@ show_private=ç§æœ‰åº«
issues.in_your_repos=屬於該用戶儲存庫的
+
[explore]
repos=儲存庫
users=使用者
@@ -685,6 +686,7 @@ teams.delete_team_success=該團隊已被刪除。
teams.repositories=團隊儲存庫
+
[admin]
dashboard=控制é¢ç‰ˆ
organizations=組織管ç†
diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini
index d03d9cf1fa..737f183f73 100644
--- a/options/locale/locale_zh-TW.ini
+++ b/options/locale/locale_zh-TW.ini
@@ -54,6 +54,7 @@ webauthn_reload=é‡æ–°è¼‰å…¥
repository=儲存庫
organization=組織
mirror=é¡åƒ
+issue_milestone=里程碑
new_repo=新增儲存庫
new_migrate=é·ç§»å¤–部儲存庫
new_mirror=æ–°é¡åƒ
@@ -382,6 +383,7 @@ show_only_public=åªé¡¯ç¤ºå…¬é–‹
issues.in_your_repos=在您的儲存庫中
+
[explore]
repos=儲存庫
users=使用者
@@ -1241,6 +1243,7 @@ labels=標籤
org_labels_desc=組織層級標籤å¯ç”¨æ–¼æ­¤çµ„織下的<strong>所有存儲庫</strong>。
org_labels_desc_manage=管ç†
+milestone=里程碑
milestones=里程碑
commits=æ交歷å²
commit=æ交
@@ -2845,6 +2848,7 @@ teams.invite.by=邀請人 %s
teams.invite.description=請點擊下方按鈕加入團隊。
+
[admin]
maintenance=維護
dashboard=資訊主é 
diff --git a/package-lock.json b/package-lock.json
index 8afe2b533f..ac25c50dde 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,7 +11,7 @@
"@citation-js/plugin-software-formats": "0.6.1",
"@github/markdown-toolbar-element": "2.2.3",
"@github/relative-time-element": "4.4.5",
- "@github/text-expander-element": "2.9.0",
+ "@github/text-expander-element": "2.9.1",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@primer/octicons": "19.14.0",
"@silverwind/vue3-calendar-heatmap": "2.0.6",
@@ -2850,9 +2850,9 @@
"license": "MIT"
},
"node_modules/@github/text-expander-element": {
- "version": "2.9.0",
- "resolved": "https://registry.npmjs.org/@github/text-expander-element/-/text-expander-element-2.9.0.tgz",
- "integrity": "sha512-NjoFiQ/3955XyefrkmtUpZvrgDl0MGyncv2QJBrUZ1+oOFOu+UmCR/ybkcuTgNg0O6AGcl8rUEXStUfrRPUCVQ==",
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/@github/text-expander-element/-/text-expander-element-2.9.1.tgz",
+ "integrity": "sha512-T/pCDjB/diMaarmcdc01hP026v0b9lidluyZD5z/EPOExXRdNDqb11kOXevoMZY42WiI3Yhoqsj3nbM+HthLgQ==",
"license": "MIT",
"dependencies": {
"@github/combobox-nav": "^2.0.2",
diff --git a/package.json b/package.json
index 997941f0b4..7eb9000bb5 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
"@citation-js/plugin-software-formats": "0.6.1",
"@github/markdown-toolbar-element": "2.2.3",
"@github/relative-time-element": "4.4.5",
- "@github/text-expander-element": "2.9.0",
+ "@github/text-expander-element": "2.9.1",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@primer/octicons": "19.14.0",
"@silverwind/vue3-calendar-heatmap": "2.0.6",
diff --git a/public/assets/img/feishu.png b/public/assets/img/feishu.png
deleted file mode 100644
index 2c3ab74413..0000000000
--- a/public/assets/img/feishu.png
+++ /dev/null
Binary files differ
diff --git a/public/assets/img/svg/gitea-feishu.svg b/public/assets/img/svg/gitea-feishu.svg
new file mode 100644
index 0000000000..d7a5ead499
--- /dev/null
+++ b/public/assets/img/svg/gitea-feishu.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="7 7 26 26" class="svg gitea-feishu" width="16" height="16" aria-hidden="true"><path fill="#00d6b9" d="m21.069 20.504.063-.06.125-.122.085-.084.256-.254.348-.344.299-.296.281-.278.293-.289.269-.266.374-.37.218-.206.419-.359.404-.306.598-.386.617-.33.606-.265.348-.127.177-.058a14.8 14.8 0 0 0-2.793-5.603 1.34 1.34 0 0 0-1.047-.502H12.221a.201.201 0 0 0-.119.364 31.5 31.5 0 0 1 8.943 10.162l.025-.023z"/><path fill="#3370ff" d="M16.791 30c5.57 0 10.423-3.074 12.955-7.618q.133-.239.258-.484a6 6 0 0 1-.425.699 6 6 0 0 1-.17.23 6 6 0 0 1-.225.274q-.092.105-.188.206a6 6 0 0 1-.407.384 6 6 0 0 1-.24.195 7 7 0 0 1-.292.21q-.094.065-.191.122c-.097.057-.134.081-.204.119q-.21.116-.428.215a6 6 0 0 1-.385.157 6 6 0 0 1-.43.138 6 6 0 0 1-.661.143 6 6 0 0 1-.491.055 6.125 6.125 0 0 1-1.543-.085 7 7 0 0 1-.38-.079l-.2-.051-.555-.155-.275-.081-.41-.125-.334-.107-.317-.104-.215-.073-.26-.091-.186-.066-.367-.134-.212-.081-.284-.11-.299-.119-.193-.079-.24-.1-.185-.078-.192-.084-.166-.073-.152-.067-.153-.07-.159-.073-.2-.093-.208-.099-.222-.108-.189-.093a31.2 31.2 0 0 1-8.822-6.583.202.202 0 0 0-.349.138l.005 9.52v.773c0 .448.222.87.595 1.118A14.75 14.75 0 0 0 16.791 30"/><path fill="#133c92" d="m29.746 22.382.051-.093zm.231-.435.014-.025.007-.012z"/><path fill="#133c9a" d="M33.151 16.582a8.45 8.45 0 0 0-3.744-.869 8.5 8.5 0 0 0-2.303.317l-.252.075-.177.058-.348.127-.606.265-.617.33-.598.386-.404.306-.419.359-.218.206-.374.37-.269.266-.293.289-.281.278-.299.296-.348.344-.256.254-.085.084-.125.122-.063.06-.095.09-.105.099a15 15 0 0 1-3.072 2.175l.2.093.159.073.153.07.152.067.166.073.192.084.185.078.24.1.193.079.299.119.284.11.212.081.367.134.186.066.26.09.215.073.317.104.334.107.41.125.275.081.555.155.2.051.379.079.433.062.585.037.525-.014.491-.055a6 6 0 0 0 .66-.143l.43-.138.385-.158.427-.215.204-.119.191-.122.292-.21.24-.195.407-.384.188-.206.225-.274.17-.23a6 6 0 0 0 .421-.693l.144-.288 1.305-2.599-.003.006a8.1 8.1 0 0 1 1.697-2.439z"/></svg> \ No newline at end of file
diff --git a/routers/api/actions/runner/runner.go b/routers/api/actions/runner/runner.go
index c55b30f7eb..f34dfb443b 100644
--- a/routers/api/actions/runner/runner.go
+++ b/routers/api/actions/runner/runner.go
@@ -156,7 +156,7 @@ func (s *Service) FetchTask(
// if the task version in request is not equal to the version in db,
// it means there may still be some tasks not be assgined.
// try to pick a task for the runner that send the request.
- if t, ok, err := pickTask(ctx, runner); err != nil {
+ if t, ok, err := actions_service.PickTask(ctx, runner); err != nil {
log.Error("pick task failed: %v", err)
return nil, status.Errorf(codes.Internal, "pick task: %v", err)
} else if ok {
diff --git a/routers/api/actions/runner/utils.go b/routers/api/actions/runner/utils.go
deleted file mode 100644
index 0fd7ca5c44..0000000000
--- a/routers/api/actions/runner/utils.go
+++ /dev/null
@@ -1,95 +0,0 @@
-// Copyright 2022 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package runner
-
-import (
- "context"
- "fmt"
-
- actions_model "code.gitea.io/gitea/models/actions"
- secret_model "code.gitea.io/gitea/models/secret"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/services/actions"
-
- runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
- "google.golang.org/protobuf/types/known/structpb"
-)
-
-func pickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv1.Task, bool, error) {
- t, ok, err := actions_model.CreateTaskForRunner(ctx, runner)
- if err != nil {
- return nil, false, fmt.Errorf("CreateTaskForRunner: %w", err)
- }
- if !ok {
- return nil, false, nil
- }
-
- secrets, err := secret_model.GetSecretsOfTask(ctx, t)
- if err != nil {
- return nil, false, fmt.Errorf("GetSecretsOfTask: %w", err)
- }
-
- vars, err := actions_model.GetVariablesOfRun(ctx, t.Job.Run)
- if err != nil {
- return nil, false, fmt.Errorf("GetVariablesOfRun: %w", err)
- }
-
- actions.CreateCommitStatus(ctx, t.Job)
-
- task := &runnerv1.Task{
- Id: t.ID,
- WorkflowPayload: t.Job.WorkflowPayload,
- Context: generateTaskContext(t),
- Secrets: secrets,
- Vars: vars,
- }
-
- if needs, err := findTaskNeeds(ctx, t); err != nil {
- log.Error("Cannot find needs for task %v: %v", t.ID, err)
- // Go on with empty needs.
- // If return error, the task will be wild, which means the runner will never get it when it has been assigned to the runner.
- // In contrast, missing needs is less serious.
- // And the task will fail and the runner will report the error in the logs.
- } else {
- task.Needs = needs
- }
-
- return task, true, nil
-}
-
-func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct {
- giteaRuntimeToken, err := actions.CreateAuthorizationToken(t.ID, t.Job.RunID, t.JobID)
- if err != nil {
- log.Error("actions.CreateAuthorizationToken failed: %v", err)
- }
-
- gitCtx := actions.GenerateGiteaContext(t.Job.Run, t.Job)
- gitCtx["token"] = t.Token
- gitCtx["gitea_runtime_token"] = giteaRuntimeToken
-
- taskContext, err := structpb.NewStruct(gitCtx)
- if err != nil {
- log.Error("structpb.NewStruct failed: %v", err)
- }
-
- return taskContext
-}
-
-func findTaskNeeds(ctx context.Context, task *actions_model.ActionTask) (map[string]*runnerv1.TaskNeed, error) {
- if err := task.LoadAttributes(ctx); err != nil {
- return nil, fmt.Errorf("task LoadAttributes: %w", err)
- }
- taskNeeds, err := actions.FindTaskNeeds(ctx, task.Job)
- if err != nil {
- return nil, err
- }
- ret := make(map[string]*runnerv1.TaskNeed, len(taskNeeds))
- for jobID, taskNeed := range taskNeeds {
- ret[jobID] = &runnerv1.TaskNeed{
- Outputs: taskNeed.Outputs,
- Result: runnerv1.Result(taskNeed.Result),
- }
- }
- return ret, nil
-}
diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go
index 21cb2f9ccd..53eee72631 100644
--- a/routers/api/v1/admin/user.go
+++ b/routers/api/v1/admin/user.go
@@ -477,26 +477,16 @@ func RenameUser(ctx *context.APIContext) {
return
}
- oldName := ctx.ContextUser.Name
newName := web.GetForm(ctx).(*api.RenameUserOption).NewName
- // Check if user name has been changed
+ // Check if username has been changed
if err := user_service.RenameUser(ctx, ctx.ContextUser, newName); err != nil {
- switch {
- case user_model.IsErrUserAlreadyExist(err):
- ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("form.username_been_taken"))
- case db.IsErrNameReserved(err):
- ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("user.form.name_reserved", newName))
- case db.IsErrNamePatternNotAllowed(err):
- ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("user.form.name_pattern_not_allowed", newName))
- case db.IsErrNameCharsNotAllowed(err):
- ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("user.form.name_chars_not_allowed", newName))
- default:
+ if user_model.IsErrUserAlreadyExist(err) || db.IsErrNameReserved(err) || db.IsErrNamePatternNotAllowed(err) || db.IsErrNameCharsNotAllowed(err) {
+ ctx.Error(http.StatusUnprocessableEntity, "", err)
+ } else {
ctx.ServerError("ChangeUserName", err)
}
return
}
-
- log.Trace("User name changed: %s -> %s", oldName, newName)
ctx.Status(http.StatusNoContent)
}
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index b1a42a85e6..8d9e4bfd6c 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -268,12 +268,12 @@ func checkTokenPublicOnly() func(ctx *context.APIContext) {
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryUser):
- if ctx.ContextUser != nil && ctx.ContextUser.IsUser() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
+ if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public users")
return
}
case auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryActivityPub):
- if ctx.ContextUser != nil && ctx.ContextUser.IsUser() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
+ if ctx.ContextUser != nil && ctx.ContextUser.IsTokenAccessAllowed() && ctx.ContextUser.Visibility != api.VisibleTypePublic {
ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public activitypub")
return
}
@@ -580,6 +580,16 @@ func reqWebhooksEnabled() func(ctx *context.APIContext) {
}
}
+// reqStarsEnabled requires Starring to be enabled in the config.
+func reqStarsEnabled() func(ctx *context.APIContext) {
+ return func(ctx *context.APIContext) {
+ if setting.Repository.DisableStars {
+ ctx.Error(http.StatusForbidden, "", "stars disabled by administrator")
+ return
+ }
+ }
+}
+
func orgAssignment(args ...bool) func(ctx *context.APIContext) {
var (
assignOrg bool
@@ -995,7 +1005,7 @@ func Routes() *web.Router {
m.Get("/{target}", user.CheckFollowing)
})
- m.Get("/starred", user.GetStarredRepos)
+ m.Get("/starred", reqStarsEnabled(), user.GetStarredRepos)
m.Get("/subscriptions", user.GetWatchedRepos)
}, context.UserAssignmentAPI(), checkTokenPublicOnly())
@@ -1086,7 +1096,7 @@ func Routes() *web.Router {
m.Put("", user.Star)
m.Delete("", user.Unstar)
}, repoAssignment(), checkTokenPublicOnly())
- }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
+ }, reqStarsEnabled(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
m.Get("/times", repo.ListMyTrackedTimes)
m.Get("/stopwatches", repo.GetStopwatches)
m.Get("/subscriptions", user.GetMyWatchedRepos)
@@ -1145,11 +1155,17 @@ func Routes() *web.Router {
m.Post("/accept", repo.AcceptTransfer)
m.Post("/reject", repo.RejectTransfer)
}, reqToken())
- addActionsRoutes(
- m,
- reqOwner(),
- repo.NewAction(),
- )
+
+ addActionsRoutes(m, reqOwner(), repo.NewAction()) // it adds the routes for secrets/variables and runner management
+
+ m.Group("/actions/workflows", func() {
+ m.Get("", repo.ActionsListRepositoryWorkflows)
+ m.Get("/{workflow_id}", repo.ActionsGetWorkflow)
+ m.Put("/{workflow_id}/disable", reqRepoWriter(unit.TypeActions), repo.ActionsDisableWorkflow)
+ m.Put("/{workflow_id}/enable", reqRepoWriter(unit.TypeActions), repo.ActionsEnableWorkflow)
+ m.Post("/{workflow_id}/dispatches", reqRepoWriter(unit.TypeActions), bind(api.CreateActionWorkflowDispatch{}), repo.ActionsDispatchWorkflow)
+ }, context.ReferencesGitRepo(), reqToken(), reqRepoReader(unit.TypeActions))
+
m.Group("/hooks/git", func() {
m.Combo("").Get(repo.ListGitHooks)
m.Group("/{id}", func() {
@@ -1248,7 +1264,7 @@ func Routes() *web.Router {
m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup)
m.Post("/markdown", reqToken(), bind(api.MarkdownOption{}), misc.Markdown)
m.Post("/markdown/raw", reqToken(), misc.MarkdownRaw)
- m.Get("/stargazers", repo.ListStargazers)
+ m.Get("/stargazers", reqStarsEnabled(), repo.ListStargazers)
m.Get("/subscribers", repo.ListSubscribers)
m.Group("/subscription", func() {
m.Get("", user.IsWatching)
@@ -1530,6 +1546,7 @@ func Routes() *web.Router {
m.Combo("").Get(org.Get).
Patch(reqToken(), reqOrgOwnership(), bind(api.EditOrgOption{}), org.Edit).
Delete(reqToken(), reqOrgOwnership(), org.Delete)
+ m.Post("/rename", reqToken(), reqOrgOwnership(), bind(api.RenameOrgOption{}), org.Rename)
m.Combo("/repos").Get(user.ListOrgRepos).
Post(reqToken(), bind(api.CreateRepoOption{}), repo.CreateOrgRepo)
m.Group("/members", func() {
diff --git a/routers/api/v1/org/action.go b/routers/api/v1/org/action.go
index 199ee7d777..05919c5234 100644
--- a/routers/api/v1/org/action.go
+++ b/routers/api/v1/org/action.go
@@ -450,7 +450,11 @@ func (Action) UpdateVariable(ctx *context.APIContext) {
if opt.Name == "" {
opt.Name = ctx.PathParam("variablename")
}
- if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil {
+
+ v.Name = opt.Name
+ v.Data = opt.Value
+
+ if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "UpdateVariable", err)
} else {
diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go
index d65f922434..2fcba0bf1a 100644
--- a/routers/api/v1/org/org.go
+++ b/routers/api/v1/org/org.go
@@ -315,6 +315,44 @@ func Get(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, org)
}
+func Rename(ctx *context.APIContext) {
+ // swagger:operation POST /orgs/{org}/rename organization renameOrg
+ // ---
+ // summary: Rename an organization
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: org
+ // in: path
+ // description: existing org name
+ // type: string
+ // required: true
+ // - name: body
+ // in: body
+ // required: true
+ // schema:
+ // "$ref": "#/definitions/RenameOrgOption"
+ // responses:
+ // "204":
+ // "$ref": "#/responses/empty"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "422":
+ // "$ref": "#/responses/validationError"
+
+ form := web.GetForm(ctx).(*api.RenameOrgOption)
+ orgUser := ctx.Org.Organization.AsUser()
+ if err := user_service.RenameUser(ctx, orgUser, form.NewName); err != nil {
+ if user_model.IsErrUserAlreadyExist(err) || db.IsErrNameReserved(err) || db.IsErrNamePatternNotAllowed(err) || db.IsErrNameCharsNotAllowed(err) {
+ ctx.Error(http.StatusUnprocessableEntity, "RenameOrg", err)
+ } else {
+ ctx.ServerError("RenameOrg", err)
+ }
+ return
+ }
+ ctx.Status(http.StatusNoContent)
+}
+
// Edit change an organization's information
func Edit(ctx *context.APIContext) {
// swagger:operation PATCH /orgs/{org} organization orgEdit
diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go
index d27e8d2427..850384e778 100644
--- a/routers/api/v1/repo/action.go
+++ b/routers/api/v1/repo/action.go
@@ -6,6 +6,7 @@ package repo
import (
"errors"
"net/http"
+ "strings"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
@@ -19,6 +20,8 @@ import (
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
secret_service "code.gitea.io/gitea/services/secrets"
+
+ "github.com/nektos/act/pkg/model"
)
// ListActionsSecrets list an repo's actions secrets
@@ -414,7 +417,11 @@ func (Action) UpdateVariable(ctx *context.APIContext) {
if opt.Name == "" {
opt.Name = ctx.PathParam("variablename")
}
- if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil {
+
+ v.Name = opt.Name
+ v.Data = opt.Value
+
+ if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "UpdateVariable", err)
} else {
@@ -581,3 +588,270 @@ func ListActionTasks(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, &res)
}
+
+func ActionsListRepositoryWorkflows(ctx *context.APIContext) {
+ // swagger:operation GET /repos/{owner}/{repo}/actions/workflows repository ActionsListRepositoryWorkflows
+ // ---
+ // summary: List repository workflows
+ // 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/ActionWorkflowList"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ // "422":
+ // "$ref": "#/responses/validationError"
+ // "500":
+ // "$ref": "#/responses/error"
+
+ workflows, err := actions_service.ListActionWorkflows(ctx)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "ListActionWorkflows", err)
+ return
+ }
+
+ ctx.JSON(http.StatusOK, &api.ActionWorkflowResponse{Workflows: workflows, TotalCount: int64(len(workflows))})
+}
+
+func ActionsGetWorkflow(ctx *context.APIContext) {
+ // swagger:operation GET /repos/{owner}/{repo}/actions/workflows/{workflow_id} repository ActionsGetWorkflow
+ // ---
+ // summary: Get a workflow
+ // 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
+ // - name: workflow_id
+ // in: path
+ // description: id of the workflow
+ // type: string
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/ActionWorkflow"
+ // "400":
+ // "$ref": "#/responses/error"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ // "422":
+ // "$ref": "#/responses/validationError"
+ // "500":
+ // "$ref": "#/responses/error"
+
+ workflowID := ctx.PathParam("workflow_id")
+ workflow, err := actions_service.GetActionWorkflow(ctx, workflowID)
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.Error(http.StatusNotFound, "GetActionWorkflow", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "GetActionWorkflow", err)
+ }
+ return
+ }
+
+ ctx.JSON(http.StatusOK, workflow)
+}
+
+func ActionsDisableWorkflow(ctx *context.APIContext) {
+ // swagger:operation PUT /repos/{owner}/{repo}/actions/workflows/{workflow_id}/disable repository ActionsDisableWorkflow
+ // ---
+ // summary: Disable a workflow
+ // 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
+ // - name: workflow_id
+ // in: path
+ // description: id of the workflow
+ // type: string
+ // required: true
+ // responses:
+ // "204":
+ // description: No Content
+ // "400":
+ // "$ref": "#/responses/error"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ // "422":
+ // "$ref": "#/responses/validationError"
+
+ workflowID := ctx.PathParam("workflow_id")
+ err := actions_service.EnableOrDisableWorkflow(ctx, workflowID, false)
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.Error(http.StatusNotFound, "DisableActionWorkflow", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "DisableActionWorkflow", err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
+
+func ActionsDispatchWorkflow(ctx *context.APIContext) {
+ // swagger:operation POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches repository ActionsDispatchWorkflow
+ // ---
+ // summary: Create a workflow dispatch event
+ // 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
+ // - name: workflow_id
+ // in: path
+ // description: id of the workflow
+ // type: string
+ // required: true
+ // - name: body
+ // in: body
+ // schema:
+ // "$ref": "#/definitions/CreateActionWorkflowDispatch"
+ // responses:
+ // "204":
+ // description: No Content
+ // "400":
+ // "$ref": "#/responses/error"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ // "422":
+ // "$ref": "#/responses/validationError"
+
+ workflowID := ctx.PathParam("workflow_id")
+ opt := web.GetForm(ctx).(*api.CreateActionWorkflowDispatch)
+ if opt.Ref == "" {
+ ctx.Error(http.StatusUnprocessableEntity, "MissingWorkflowParameter", util.NewInvalidArgumentErrorf("ref is required parameter"))
+ return
+ }
+
+ err := actions_service.DispatchActionWorkflow(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, workflowID, opt.Ref, func(workflowDispatch *model.WorkflowDispatch, inputs map[string]any) error {
+ if strings.Contains(ctx.Req.Header.Get("Content-Type"), "form-urlencoded") {
+ // The chi framework's "Binding" doesn't support to bind the form map values into a map[string]string
+ // So we have to manually read the `inputs[key]` from the form
+ for name, config := range workflowDispatch.Inputs {
+ value := ctx.FormString("inputs["+name+"]", config.Default)
+ inputs[name] = value
+ }
+ } else {
+ for name, config := range workflowDispatch.Inputs {
+ value, ok := opt.Inputs[name]
+ if ok {
+ inputs[name] = value
+ } else {
+ inputs[name] = config.Default
+ }
+ }
+ }
+ return nil
+ })
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.Error(http.StatusNotFound, "DispatchActionWorkflow", err)
+ } else if errors.Is(err, util.ErrPermissionDenied) {
+ ctx.Error(http.StatusForbidden, "DispatchActionWorkflow", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "DispatchActionWorkflow", err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
+
+func ActionsEnableWorkflow(ctx *context.APIContext) {
+ // swagger:operation PUT /repos/{owner}/{repo}/actions/workflows/{workflow_id}/enable repository ActionsEnableWorkflow
+ // ---
+ // summary: Enable a workflow
+ // 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
+ // - name: workflow_id
+ // in: path
+ // description: id of the workflow
+ // type: string
+ // required: true
+ // responses:
+ // "204":
+ // description: No Content
+ // "400":
+ // "$ref": "#/responses/error"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "404":
+ // "$ref": "#/responses/notFound"
+ // "409":
+ // "$ref": "#/responses/conflict"
+ // "422":
+ // "$ref": "#/responses/validationError"
+
+ workflowID := ctx.PathParam("workflow_id")
+ err := actions_service.EnableOrDisableWorkflow(ctx, workflowID, true)
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.Error(http.StatusNotFound, "EnableActionWorkflow", err)
+ } else {
+ ctx.Error(http.StatusInternalServerError, "EnableActionWorkflow", err)
+ }
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
diff --git a/routers/api/v1/repo/star.go b/routers/api/v1/repo/star.go
index 99676de119..46ed17ad91 100644
--- a/routers/api/v1/repo/star.go
+++ b/routers/api/v1/repo/star.go
@@ -44,6 +44,8 @@ func ListStargazers(ctx *context.APIContext) {
// "$ref": "#/responses/UserList"
// "404":
// "$ref": "#/responses/notFound"
+ // "403":
+ // "$ref": "#/responses/forbidden"
stargazers, err := repo_model.GetStargazers(ctx, ctx.Repo.Repository, utils.GetListOptions(ctx))
if err != nil {
diff --git a/routers/api/v1/swagger/action.go b/routers/api/v1/swagger/action.go
index 665f4d0b85..16a250184a 100644
--- a/routers/api/v1/swagger/action.go
+++ b/routers/api/v1/swagger/action.go
@@ -32,3 +32,17 @@ type swaggerResponseVariableList struct {
// in:body
Body []api.ActionVariable `json:"body"`
}
+
+// ActionWorkflow
+// swagger:response ActionWorkflow
+type swaggerResponseActionWorkflow struct {
+ // in:body
+ Body api.ActionWorkflow `json:"body"`
+}
+
+// ActionWorkflowList
+// swagger:response ActionWorkflowList
+type swaggerResponseActionWorkflowList struct {
+ // in:body
+ Body []api.ActionWorkflow `json:"body"`
+}
diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go
index 125605d98f..aa5990eb38 100644
--- a/routers/api/v1/swagger/options.go
+++ b/routers/api/v1/swagger/options.go
@@ -209,5 +209,11 @@ type swaggerParameterBodies struct {
CreateVariableOption api.CreateVariableOption
// in:body
+ RenameOrgOption api.RenameOrgOption
+
+ // in:body
+ CreateActionWorkflowDispatch api.CreateActionWorkflowDispatch
+
+ // in:body
UpdateVariableOption api.UpdateVariableOption
}
diff --git a/routers/api/v1/user/action.go b/routers/api/v1/user/action.go
index 22707196f4..baa4b3b81e 100644
--- a/routers/api/v1/user/action.go
+++ b/routers/api/v1/user/action.go
@@ -212,7 +212,11 @@ func UpdateVariable(ctx *context.APIContext) {
if opt.Name == "" {
opt.Name = ctx.PathParam("variablename")
}
- if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil {
+
+ v.Name = opt.Name
+ v.Data = opt.Value
+
+ if _, err := actions_service.UpdateVariableNameData(ctx, v); err != nil {
if errors.Is(err, util.ErrInvalidArgument) {
ctx.Error(http.StatusBadRequest, "UpdateVariable", err)
} else {
diff --git a/routers/api/v1/user/star.go b/routers/api/v1/user/star.go
index ad9ed9548d..70e54bc1ae 100644
--- a/routers/api/v1/user/star.go
+++ b/routers/api/v1/user/star.go
@@ -66,6 +66,8 @@ func GetStarredRepos(ctx *context.APIContext) {
// "$ref": "#/responses/RepositoryList"
// "404":
// "$ref": "#/responses/notFound"
+ // "403":
+ // "$ref": "#/responses/forbidden"
private := ctx.ContextUser.ID == ctx.Doer.ID
repos, err := getStarredRepos(ctx, ctx.ContextUser, private)
@@ -97,6 +99,8 @@ func GetMyStarredRepos(ctx *context.APIContext) {
// responses:
// "200":
// "$ref": "#/responses/RepositoryList"
+ // "403":
+ // "$ref": "#/responses/forbidden"
repos, err := getStarredRepos(ctx, ctx.Doer, true)
if err != nil {
@@ -128,6 +132,8 @@ func IsStarring(ctx *context.APIContext) {
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
+ // "403":
+ // "$ref": "#/responses/forbidden"
if repo_model.IsStaring(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) {
ctx.Status(http.StatusNoContent)
@@ -193,6 +199,8 @@ func Unstar(ctx *context.APIContext) {
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
+ // "403":
+ // "$ref": "#/responses/forbidden"
err := repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
if err != nil {
diff --git a/routers/install/install.go b/routers/install/install.go
index 8a1d57aa0b..8544717f65 100644
--- a/routers/install/install.go
+++ b/routers/install/install.go
@@ -64,7 +64,6 @@ func Contexter() func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
base := context.NewBaseContext(resp, req)
ctx := context.NewWebContext(base, rnd, session.GetSession(req))
- ctx.SetContextValue(context.WebContextKey, ctx)
ctx.Data.MergeFrom(middleware.CommonTemplateContextData())
ctx.Data.MergeFrom(reqctx.ContextData{
"Title": ctx.Locale.Tr("install.install"),
diff --git a/routers/private/internal.go b/routers/private/internal.go
index a78c76f897..55a11aa3dd 100644
--- a/routers/private/internal.go
+++ b/routers/private/internal.go
@@ -87,8 +87,8 @@ func Routes() *web.Router {
// FIXME: it is not right to use context.Contexter here because all routes here should use PrivateContext
// Fortunately, the LFS handlers are able to handle requests without a complete web context
common.AddOwnerRepoGitLFSRoutes(r, func(ctx *context.PrivateContext) {
- webContext := &context.Context{Base: ctx.Base}
- ctx.SetContextValue(context.WebContextKey, webContext)
+ webContext := &context.Context{Base: ctx.Base} // see above, it shouldn't manually construct the web context
+ ctx.SetContextValue(context.WebContextKey, webContext) // FIXME: this is not ideal but no other way at the moment
})
})
diff --git a/routers/web/org/home.go b/routers/web/org/home.go
index 277adb60ca..27d1e14d85 100644
--- a/routers/web/org/home.go
+++ b/routers/web/org/home.go
@@ -34,7 +34,7 @@ func Home(ctx *context.Context) {
}
ctx.SetPathParam("org", uname)
- context.HandleOrgAssignment(ctx)
+ context.OrgAssignment(context.OrgAssignmentOptions{})(ctx)
if ctx.Written() {
return
}
diff --git a/routers/web/org/worktime.go b/routers/web/org/worktime.go
new file mode 100644
index 0000000000..2336984825
--- /dev/null
+++ b/routers/web/org/worktime.go
@@ -0,0 +1,74 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package org
+
+import (
+ "net/http"
+ "time"
+
+ "code.gitea.io/gitea/models/organization"
+ "code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/services/context"
+)
+
+const tplByRepos templates.TplName = "org/worktime"
+
+// parseOrgTimes contains functionality that is required in all these functions,
+// like parsing the date from the request, setting default dates, etc.
+func parseOrgTimes(ctx *context.Context) (unixFrom, unixTo int64) {
+ rangeFrom := ctx.FormString("from")
+ rangeTo := ctx.FormString("to")
+ if rangeFrom == "" {
+ rangeFrom = time.Now().Format("2006-01") + "-01" // defaults to start of current month
+ }
+ if rangeTo == "" {
+ rangeTo = time.Now().Format("2006-01-02") // defaults to today
+ }
+
+ ctx.Data["RangeFrom"] = rangeFrom
+ ctx.Data["RangeTo"] = rangeTo
+
+ timeFrom, err := time.Parse("2006-01-02", rangeFrom)
+ if err != nil {
+ ctx.ServerError("time.Parse", err)
+ }
+ timeTo, err := time.Parse("2006-01-02", rangeTo)
+ if err != nil {
+ ctx.ServerError("time.Parse", err)
+ }
+ unixFrom = timeFrom.Unix()
+ unixTo = timeTo.Add(1440*time.Minute - 1*time.Second).Unix() // humans expect that we include the ending day too
+ return unixFrom, unixTo
+}
+
+func Worktime(ctx *context.Context) {
+ ctx.Data["PageIsOrgTimes"] = true
+
+ unixFrom, unixTo := parseOrgTimes(ctx)
+ if ctx.Written() {
+ return
+ }
+
+ worktimeBy := ctx.FormString("by")
+ ctx.Data["WorktimeBy"] = worktimeBy
+
+ var worktimeSumResult any
+ var err error
+ if worktimeBy == "milestones" {
+ worktimeSumResult, err = organization.GetWorktimeByMilestones(ctx.Org.Organization, unixFrom, unixTo)
+ ctx.Data["WorktimeByMilestones"] = true
+ } else if worktimeBy == "members" {
+ worktimeSumResult, err = organization.GetWorktimeByMembers(ctx.Org.Organization, unixFrom, unixTo)
+ ctx.Data["WorktimeByMembers"] = true
+ } else /* by repos */ {
+ worktimeSumResult, err = organization.GetWorktimeByRepos(ctx.Org.Organization, unixFrom, unixTo)
+ ctx.Data["WorktimeByRepos"] = true
+ }
+ if err != nil {
+ ctx.ServerError("GetWorktime", err)
+ return
+ }
+ ctx.Data["WorktimeSumResult"] = worktimeSumResult
+ ctx.HTML(http.StatusOK, tplByRepos)
+}
diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go
index e5d83960b8..7099582c1b 100644
--- a/routers/web/repo/actions/view.go
+++ b/routers/web/repo/actions/view.go
@@ -20,8 +20,6 @@ import (
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
- "code.gitea.io/gitea/models/perm"
- access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/actions"
@@ -30,16 +28,13 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
- api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
actions_service "code.gitea.io/gitea/services/actions"
context_module "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/convert"
- "github.com/nektos/act/pkg/jobparser"
"github.com/nektos/act/pkg/model"
"xorm.io/builder"
)
@@ -281,84 +276,98 @@ func ViewPost(ctx *context_module.Context) {
resp.State.CurrentJob.Steps = make([]*ViewJobStep, 0) // marshal to '[]' instead fo 'null' in json
resp.Logs.StepsLog = make([]*ViewStepLog, 0) // marshal to '[]' instead fo 'null' in json
if task != nil {
- steps := actions.FullSteps(task)
-
- for _, v := range steps {
- resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, &ViewJobStep{
- Summary: v.Name,
- Duration: v.Duration().String(),
- Status: v.Status.String(),
- })
+ steps, logs, err := convertToViewModel(ctx, req.LogCursors, task)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, err.Error())
+ return
}
+ resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, steps...)
+ resp.Logs.StepsLog = append(resp.Logs.StepsLog, logs...)
+ }
- for _, cursor := range req.LogCursors {
- if !cursor.Expanded {
- continue
- }
+ ctx.JSON(http.StatusOK, resp)
+}
+
+func convertToViewModel(ctx *context_module.Context, cursors []LogCursor, task *actions_model.ActionTask) ([]*ViewJobStep, []*ViewStepLog, error) {
+ var viewJobs []*ViewJobStep
+ var logs []*ViewStepLog
+
+ steps := actions.FullSteps(task)
- step := steps[cursor.Step]
-
- // if task log is expired, return a consistent log line
- if task.LogExpired {
- if cursor.Cursor == 0 {
- resp.Logs.StepsLog = append(resp.Logs.StepsLog, &ViewStepLog{
- Step: cursor.Step,
- Cursor: 1,
- Lines: []*ViewStepLogLine{
- {
- Index: 1,
- Message: ctx.Locale.TrString("actions.runs.expire_log_message"),
- // Timestamp doesn't mean anything when the log is expired.
- // Set it to the task's updated time since it's probably the time when the log has expired.
- Timestamp: float64(task.Updated.AsTime().UnixNano()) / float64(time.Second),
- },
+ for _, v := range steps {
+ viewJobs = append(viewJobs, &ViewJobStep{
+ Summary: v.Name,
+ Duration: v.Duration().String(),
+ Status: v.Status.String(),
+ })
+ }
+
+ for _, cursor := range cursors {
+ if !cursor.Expanded {
+ continue
+ }
+
+ step := steps[cursor.Step]
+
+ // if task log is expired, return a consistent log line
+ if task.LogExpired {
+ if cursor.Cursor == 0 {
+ logs = append(logs, &ViewStepLog{
+ Step: cursor.Step,
+ Cursor: 1,
+ Lines: []*ViewStepLogLine{
+ {
+ Index: 1,
+ Message: ctx.Locale.TrString("actions.runs.expire_log_message"),
+ // Timestamp doesn't mean anything when the log is expired.
+ // Set it to the task's updated time since it's probably the time when the log has expired.
+ Timestamp: float64(task.Updated.AsTime().UnixNano()) / float64(time.Second),
},
- Started: int64(step.Started),
- })
- }
- continue
+ },
+ Started: int64(step.Started),
+ })
}
+ continue
+ }
- logLines := make([]*ViewStepLogLine, 0) // marshal to '[]' instead fo 'null' in json
-
- index := step.LogIndex + cursor.Cursor
- validCursor := cursor.Cursor >= 0 &&
- // !(cursor.Cursor < step.LogLength) when the frontend tries to fetch next line before it's ready.
- // So return the same cursor and empty lines to let the frontend retry.
- cursor.Cursor < step.LogLength &&
- // !(index < task.LogIndexes[index]) when task data is older than step data.
- // It can be fixed by making sure write/read tasks and steps in the same transaction,
- // but it's easier to just treat it as fetching the next line before it's ready.
- index < int64(len(task.LogIndexes))
-
- if validCursor {
- length := step.LogLength - cursor.Cursor
- offset := task.LogIndexes[index]
- logRows, err := actions.ReadLogs(ctx, task.LogInStorage, task.LogFilename, offset, length)
- if err != nil {
- ctx.ServerError("actions.ReadLogs", err)
- return
- }
-
- for i, row := range logRows {
- logLines = append(logLines, &ViewStepLogLine{
- Index: cursor.Cursor + int64(i) + 1, // start at 1
- Message: row.Content,
- Timestamp: float64(row.Time.AsTime().UnixNano()) / float64(time.Second),
- })
- }
+ logLines := make([]*ViewStepLogLine, 0) // marshal to '[]' instead fo 'null' in json
+
+ index := step.LogIndex + cursor.Cursor
+ validCursor := cursor.Cursor >= 0 &&
+ // !(cursor.Cursor < step.LogLength) when the frontend tries to fetch next line before it's ready.
+ // So return the same cursor and empty lines to let the frontend retry.
+ cursor.Cursor < step.LogLength &&
+ // !(index < task.LogIndexes[index]) when task data is older than step data.
+ // It can be fixed by making sure write/read tasks and steps in the same transaction,
+ // but it's easier to just treat it as fetching the next line before it's ready.
+ index < int64(len(task.LogIndexes))
+
+ if validCursor {
+ length := step.LogLength - cursor.Cursor
+ offset := task.LogIndexes[index]
+ logRows, err := actions.ReadLogs(ctx, task.LogInStorage, task.LogFilename, offset, length)
+ if err != nil {
+ return nil, nil, fmt.Errorf("actions.ReadLogs: %w", err)
}
- resp.Logs.StepsLog = append(resp.Logs.StepsLog, &ViewStepLog{
- Step: cursor.Step,
- Cursor: cursor.Cursor + int64(len(logLines)),
- Lines: logLines,
- Started: int64(step.Started),
- })
+ for i, row := range logRows {
+ logLines = append(logLines, &ViewStepLogLine{
+ Index: cursor.Cursor + int64(i) + 1, // start at 1
+ Message: row.Content,
+ Timestamp: float64(row.Time.AsTime().UnixNano()) / float64(time.Second),
+ })
+ }
}
+
+ logs = append(logs, &ViewStepLog{
+ Step: cursor.Step,
+ Cursor: cursor.Cursor + int64(len(logLines)),
+ Lines: logLines,
+ Started: int64(step.Started),
+ })
}
- ctx.JSON(http.StatusOK, resp)
+ return viewJobs, logs, nil
}
// Rerun will rerun jobs in the given run
@@ -614,11 +623,6 @@ func getRunJobs(ctx *context_module.Context, runIndex, jobIndex int64) (*actions
}
func ArtifactsDeleteView(ctx *context_module.Context) {
- if !ctx.Repo.CanWrite(unit.TypeActions) {
- ctx.Error(http.StatusForbidden, "no permission")
- return
- }
-
runIndex := getRunIndex(ctx)
artifactName := ctx.PathParam("artifact_name")
@@ -783,142 +787,28 @@ func Run(ctx *context_module.Context) {
ctx.ServerError("ref", nil)
return
}
-
- // can not rerun job when workflow is disabled
- cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions)
- cfg := cfgUnit.ActionsConfig()
- if cfg.IsWorkflowDisabled(workflowID) {
- ctx.Flash.Error(ctx.Tr("actions.workflow.disabled"))
- ctx.Redirect(redirectURL)
- return
- }
-
- // get target commit of run from specified ref
- refName := git.RefName(ref)
- var runTargetCommit *git.Commit
- var err error
- if refName.IsTag() {
- runTargetCommit, err = ctx.Repo.GitRepo.GetTagCommit(refName.TagName())
- } else if refName.IsBranch() {
- runTargetCommit, err = ctx.Repo.GitRepo.GetBranchCommit(refName.BranchName())
- } else {
- ctx.Flash.Error(ctx.Tr("form.git_ref_name_error", ref))
- ctx.Redirect(redirectURL)
- return
- }
- if err != nil {
- ctx.Flash.Error(ctx.Tr("form.target_ref_not_exist", ref))
- ctx.Redirect(redirectURL)
- return
- }
-
- // get workflow entry from runTargetCommit
- entries, err := actions.ListWorkflows(runTargetCommit)
- if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
- return
- }
-
- // find workflow from commit
- var workflows []*jobparser.SingleWorkflow
- for _, entry := range entries {
- if entry.Name() == workflowID {
- content, err := actions.GetContentFromEntry(entry)
- if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
- return
- }
- workflows, err = jobparser.Parse(content)
- if err != nil {
- ctx.ServerError("workflow", err)
- return
- }
- break
- }
- }
-
- if len(workflows) == 0 {
- ctx.Flash.Error(ctx.Tr("actions.workflow.not_found", workflowID))
- ctx.Redirect(redirectURL)
- return
- }
-
- // get inputs from post
- workflow := &model.Workflow{
- RawOn: workflows[0].RawOn,
- }
- inputs := make(map[string]any)
- if workflowDispatch := workflow.WorkflowDispatchConfig(); workflowDispatch != nil {
+ err := actions_service.DispatchActionWorkflow(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, workflowID, ref, func(workflowDispatch *model.WorkflowDispatch, inputs map[string]any) error {
for name, config := range workflowDispatch.Inputs {
value := ctx.Req.PostFormValue(name)
if config.Type == "boolean" {
- // https://www.w3.org/TR/html401/interact/forms.html
- // https://stackoverflow.com/questions/11424037/do-checkbox-inputs-only-post-data-if-theyre-checked
- // Checkboxes (and radio buttons) are on/off switches that may be toggled by the user.
- // A switch is "on" when the control element's checked attribute is set.
- // When a form is submitted, only "on" checkbox controls can become successful.
- inputs[name] = strconv.FormatBool(value == "on")
+ inputs[name] = strconv.FormatBool(ctx.FormBool(name))
} else if value != "" {
inputs[name] = value
} else {
inputs[name] = config.Default
}
}
- }
-
- // ctx.Req.PostForm -> WorkflowDispatchPayload.Inputs -> ActionRun.EventPayload -> runner: ghc.Event
- // https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
- // https://docs.github.com/en/webhooks/webhook-events-and-payloads#workflow_dispatch
- workflowDispatchPayload := &api.WorkflowDispatchPayload{
- Workflow: workflowID,
- Ref: ref,
- Repository: convert.ToRepo(ctx, ctx.Repo.Repository, access_model.Permission{AccessMode: perm.AccessModeNone}),
- Inputs: inputs,
- Sender: convert.ToUserWithAccessMode(ctx, ctx.Doer, perm.AccessModeNone),
- }
- var eventPayload []byte
- if eventPayload, err = workflowDispatchPayload.JSONPayload(); err != nil {
- ctx.ServerError("JSONPayload", err)
- return
- }
-
- run := &actions_model.ActionRun{
- Title: strings.SplitN(runTargetCommit.CommitMessage, "\n", 2)[0],
- RepoID: ctx.Repo.Repository.ID,
- OwnerID: ctx.Repo.Repository.OwnerID,
- WorkflowID: workflowID,
- TriggerUserID: ctx.Doer.ID,
- Ref: ref,
- CommitSHA: runTargetCommit.ID.String(),
- IsForkPullRequest: false,
- Event: "workflow_dispatch",
- TriggerEvent: "workflow_dispatch",
- EventPayload: string(eventPayload),
- Status: actions_model.StatusWaiting,
- }
-
- // cancel running jobs of the same workflow
- if err := actions_model.CancelPreviousJobs(
- ctx,
- run.RepoID,
- run.Ref,
- run.WorkflowID,
- run.Event,
- ); err != nil {
- log.Error("CancelRunningJobs: %v", err)
- }
-
- // Insert the action run and its associated jobs into the database
- if err := actions_model.InsertRun(ctx, run, workflows); err != nil {
- ctx.ServerError("workflow", err)
- return
- }
-
- alljobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: run.ID})
+ return nil
+ })
if err != nil {
- log.Error("FindRunJobs: %v", err)
+ if errLocale := util.ErrAsLocale(err); errLocale != nil {
+ ctx.Flash.Error(ctx.Tr(errLocale.TrKey, errLocale.TrArgs...))
+ ctx.Redirect(redirectURL)
+ } else {
+ ctx.ServerError("DispatchActionWorkflow", err)
+ }
+ return
}
- actions_service.CreateCommitStatus(ctx, alljobs...)
ctx.Flash.Success(ctx.Tr("actions.workflow.run_success", workflowID))
ctx.Redirect(redirectURL)
diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go
index 8ffda8ae0a..c8291d98c6 100644
--- a/routers/web/repo/commit.go
+++ b/routers/web/repo/commit.go
@@ -22,7 +22,6 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/gitgraph"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
@@ -32,6 +31,7 @@ import (
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/gitdiff"
repo_service "code.gitea.io/gitea/services/repository"
+ "code.gitea.io/gitea/services/repository/gitgraph"
)
const (
diff --git a/routers/web/repo/issue_suggestions.go b/routers/web/repo/issue_suggestions.go
index 46e9f339a5..9ef3942504 100644
--- a/routers/web/repo/issue_suggestions.go
+++ b/routers/web/repo/issue_suggestions.go
@@ -6,13 +6,10 @@ package repo
import (
"net/http"
- "code.gitea.io/gitea/models/db"
- issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unit"
- issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
"code.gitea.io/gitea/modules/optional"
- "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/context"
+ issue_service "code.gitea.io/gitea/services/issue"
)
// IssueSuggestions returns a list of issue suggestions
@@ -29,54 +26,11 @@ func IssueSuggestions(ctx *context.Context) {
isPull = optional.Some(false)
}
- searchOpt := &issue_indexer.SearchOptions{
- Paginator: &db.ListOptions{
- Page: 0,
- PageSize: 5,
- },
- Keyword: keyword,
- RepoIDs: []int64{ctx.Repo.Repository.ID},
- IsPull: isPull,
- IsClosed: nil,
- SortBy: issue_indexer.SortByUpdatedDesc,
- }
-
- ids, _, err := issue_indexer.SearchIssues(ctx, searchOpt)
- if err != nil {
- ctx.ServerError("SearchIssues", err)
- return
- }
- issues, err := issues_model.GetIssuesByIDs(ctx, ids, true)
+ suggestions, err := issue_service.GetSuggestion(ctx, ctx.Repo.Repository, isPull, keyword)
if err != nil {
- ctx.ServerError("FindIssuesByIDs", err)
+ ctx.ServerError("GetSuggestion", err)
return
}
- suggestions := make([]*structs.Issue, 0, len(issues))
-
- for _, issue := range issues {
- suggestion := &structs.Issue{
- ID: issue.ID,
- Index: issue.Index,
- Title: issue.Title,
- State: issue.State(),
- }
-
- if issue.IsPull {
- if err := issue.LoadPullRequest(ctx); err != nil {
- ctx.ServerError("LoadPullRequest", err)
- return
- }
- if issue.PullRequest != nil {
- suggestion.PullRequest = &structs.PullRequestMeta{
- HasMerged: issue.PullRequest.HasMerged,
- IsWorkInProgress: issue.PullRequest.IsWorkInProgress(ctx),
- }
- }
- }
-
- suggestions = append(suggestions, suggestion)
- }
-
ctx.JSON(http.StatusOK, suggestions)
}
diff --git a/routers/web/repo/issue_view.go b/routers/web/repo/issue_view.go
index aa49d2e1e8..aeb2fa52b6 100644
--- a/routers/web/repo/issue_view.go
+++ b/routers/web/repo/issue_view.go
@@ -4,7 +4,6 @@
package repo
import (
- stdCtx "context"
"fmt"
"math/big"
"net/http"
@@ -40,86 +39,80 @@ import (
)
// roleDescriptor returns the role descriptor for a comment in/with the given repo, poster and issue
-func roleDescriptor(ctx stdCtx.Context, repo *repo_model.Repository, poster *user_model.User, permsCache map[int64]access_model.Permission, issue *issues_model.Issue, hasOriginalAuthor bool) (issues_model.RoleDescriptor, error) {
- roleDescriptor := issues_model.RoleDescriptor{}
-
+func roleDescriptor(ctx *context.Context, repo *repo_model.Repository, poster *user_model.User, permsCache map[int64]access_model.Permission, issue *issues_model.Issue, hasOriginalAuthor bool) (roleDesc issues_model.RoleDescriptor, err error) {
if hasOriginalAuthor {
- return roleDescriptor, nil
+ // the poster is a migrated user, so no need to detect the role
+ return roleDesc, nil
}
- var perm access_model.Permission
- var err error
- if permsCache != nil {
- var ok bool
- perm, ok = permsCache[poster.ID]
- if !ok {
- perm, err = access_model.GetUserRepoPermission(ctx, repo, poster)
- if err != nil {
- return roleDescriptor, err
- }
- }
- permsCache[poster.ID] = perm
- } else {
+ if poster.IsGhost() || !poster.IsIndividual() {
+ return roleDesc, nil
+ }
+
+ roleDesc.IsPoster = issue.IsPoster(poster.ID) // check whether the comment's poster is the issue's poster
+
+ // Guess the role of the poster in the repo by permission
+ perm, hasPermCache := permsCache[poster.ID]
+ if !hasPermCache {
perm, err = access_model.GetUserRepoPermission(ctx, repo, poster)
if err != nil {
- return roleDescriptor, err
+ return roleDesc, err
}
}
-
- // If the poster is the actual poster of the issue, enable Poster role.
- roleDescriptor.IsPoster = issue.IsPoster(poster.ID)
+ if permsCache != nil {
+ permsCache[poster.ID] = perm
+ }
// Check if the poster is owner of the repo.
if perm.IsOwner() {
- // If the poster isn't an admin, enable the owner role.
+ // If the poster isn't a site admin, then is must be the repo's owner
if !poster.IsAdmin {
- roleDescriptor.RoleInRepo = issues_model.RoleRepoOwner
- return roleDescriptor, nil
+ roleDesc.RoleInRepo = issues_model.RoleRepoOwner
+ return roleDesc, nil
}
-
- // Otherwise check if poster is the real repo admin.
- ok, err := access_model.IsUserRealRepoAdmin(ctx, repo, poster)
+ // Otherwise (poster is site admin), check if poster is the real repo admin.
+ isRealRepoAdmin, err := access_model.IsUserRealRepoAdmin(ctx, repo, poster)
if err != nil {
- return roleDescriptor, err
+ return roleDesc, err
}
- if ok {
- roleDescriptor.RoleInRepo = issues_model.RoleRepoOwner
- return roleDescriptor, nil
+ if isRealRepoAdmin {
+ roleDesc.RoleInRepo = issues_model.RoleRepoOwner
+ return roleDesc, nil
}
}
// If repo is organization, check Member role
- if err := repo.LoadOwner(ctx); err != nil {
- return roleDescriptor, err
+ if err = repo.LoadOwner(ctx); err != nil {
+ return roleDesc, err
}
if repo.Owner.IsOrganization() {
if isMember, err := organization.IsOrganizationMember(ctx, repo.Owner.ID, poster.ID); err != nil {
- return roleDescriptor, err
+ return roleDesc, err
} else if isMember {
- roleDescriptor.RoleInRepo = issues_model.RoleRepoMember
- return roleDescriptor, nil
+ roleDesc.RoleInRepo = issues_model.RoleRepoMember
+ return roleDesc, nil
}
}
// If the poster is the collaborator of the repo
if isCollaborator, err := repo_model.IsCollaborator(ctx, repo.ID, poster.ID); err != nil {
- return roleDescriptor, err
+ return roleDesc, err
} else if isCollaborator {
- roleDescriptor.RoleInRepo = issues_model.RoleRepoCollaborator
- return roleDescriptor, nil
+ roleDesc.RoleInRepo = issues_model.RoleRepoCollaborator
+ return roleDesc, nil
}
hasMergedPR, err := issues_model.HasMergedPullRequestInRepo(ctx, repo.ID, poster.ID)
if err != nil {
- return roleDescriptor, err
+ return roleDesc, err
} else if hasMergedPR {
- roleDescriptor.RoleInRepo = issues_model.RoleRepoContributor
+ roleDesc.RoleInRepo = issues_model.RoleRepoContributor
} else if issue.IsPull {
// only display first time contributor in the first opening pull request
- roleDescriptor.RoleInRepo = issues_model.RoleRepoFirstTimeContributor
+ roleDesc.RoleInRepo = issues_model.RoleRepoFirstTimeContributor
}
- return roleDescriptor, nil
+ return roleDesc, nil
}
func getBranchData(ctx *context.Context, issue *issues_model.Issue) {
diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go
index 8ebf5bcf39..0d4513ec67 100644
--- a/routers/web/repo/repo.go
+++ b/routers/web/repo/repo.go
@@ -304,31 +304,6 @@ func CreatePost(ctx *context.Context) {
handleCreateError(ctx, ctxUser, err, "CreatePost", tplCreate, &form)
}
-const (
- tplWatchUnwatch templates.TplName = "repo/watch_unwatch"
- tplStarUnstar templates.TplName = "repo/star_unstar"
-)
-
-func acceptTransfer(ctx *context.Context) {
- err := repo_service.AcceptTransferOwnership(ctx, ctx.Repo.Repository, ctx.Doer)
- if err == nil {
- ctx.Flash.Success(ctx.Tr("repo.settings.transfer.success"))
- ctx.Redirect(ctx.Repo.Repository.Link())
- return
- }
- handleActionError(ctx, err)
-}
-
-func rejectTransfer(ctx *context.Context) {
- err := repo_service.RejectRepositoryTransfer(ctx, ctx.Repo.Repository, ctx.Doer)
- if err == nil {
- ctx.Flash.Success(ctx.Tr("repo.settings.transfer.rejected"))
- ctx.Redirect(ctx.Repo.Repository.Link())
- return
- }
- handleActionError(ctx, err)
-}
-
func handleActionError(ctx *context.Context, err error) {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.Flash.Error(ctx.Tr("repo.action.blocked_user"))
@@ -339,72 +314,6 @@ func handleActionError(ctx *context.Context, err error) {
}
}
-// Action response for actions to a repository
-func Action(ctx *context.Context) {
- var err error
- switch ctx.PathParam("action") {
- case "watch":
- err = repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
- case "unwatch":
- err = repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
- case "star":
- err = repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
- case "unstar":
- err = repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
- case "accept_transfer":
- acceptTransfer(ctx)
- return
- case "reject_transfer":
- rejectTransfer(ctx)
- return
- case "desc": // FIXME: this is not used
- if !ctx.Repo.IsOwner() {
- ctx.Error(http.StatusNotFound)
- return
- }
-
- ctx.Repo.Repository.Description = ctx.FormString("desc")
- ctx.Repo.Repository.Website = ctx.FormString("site")
- err = repo_service.UpdateRepository(ctx, ctx.Repo.Repository, false)
- }
-
- if err != nil {
- handleActionError(ctx, err)
- return
- }
-
- switch ctx.PathParam("action") {
- case "watch", "unwatch":
- ctx.Data["IsWatchingRepo"] = repo_model.IsWatching(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
- case "star", "unstar":
- ctx.Data["IsStaringRepo"] = repo_model.IsStaring(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
- }
-
- // see the `hx-trigger="refreshUserCards ..."` comments in tmpl
- ctx.RespHeader().Add("hx-trigger", "refreshUserCards")
-
- switch ctx.PathParam("action") {
- case "watch", "unwatch", "star", "unstar":
- // we have to reload the repository because NumStars or NumWatching (used in the templates) has just changed
- ctx.Data["Repository"], err = repo_model.GetRepositoryByName(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.Name)
- if err != nil {
- ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.PathParam("action")), err)
- return
- }
- }
-
- switch ctx.PathParam("action") {
- case "watch", "unwatch":
- ctx.HTML(http.StatusOK, tplWatchUnwatch)
- return
- case "star", "unstar":
- ctx.HTML(http.StatusOK, tplStarUnstar)
- return
- }
-
- ctx.RedirectToCurrentSite(ctx.FormString("redirect_to"), ctx.Repo.RepoLink)
-}
-
// RedirectDownload return a file based on the following infos:
func RedirectDownload(ctx *context.Context) {
var (
diff --git a/routers/web/repo/setting/runners.go b/routers/web/repo/setting/runners.go
deleted file mode 100644
index 94f2ae7a0c..0000000000
--- a/routers/web/repo/setting/runners.go
+++ /dev/null
@@ -1,187 +0,0 @@
-// Copyright 2022 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package setting
-
-import (
- "errors"
- "net/http"
- "net/url"
-
- actions_model "code.gitea.io/gitea/models/actions"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/templates"
- actions_shared "code.gitea.io/gitea/routers/web/shared/actions"
- shared_user "code.gitea.io/gitea/routers/web/shared/user"
- "code.gitea.io/gitea/services/context"
-)
-
-const (
- // TODO: Separate secrets from runners when layout is ready
- tplRepoRunners templates.TplName = "repo/settings/actions"
- tplOrgRunners templates.TplName = "org/settings/actions"
- tplAdminRunners templates.TplName = "admin/actions"
- tplUserRunners templates.TplName = "user/settings/actions"
- tplRepoRunnerEdit templates.TplName = "repo/settings/runner_edit"
- tplOrgRunnerEdit templates.TplName = "org/settings/runners_edit"
- tplAdminRunnerEdit templates.TplName = "admin/runners/edit"
- tplUserRunnerEdit templates.TplName = "user/settings/runner_edit"
-)
-
-type runnersCtx struct {
- OwnerID int64
- RepoID int64
- IsRepo bool
- IsOrg bool
- IsAdmin bool
- IsUser bool
- RunnersTemplate templates.TplName
- RunnerEditTemplate templates.TplName
- RedirectLink string
-}
-
-func getRunnersCtx(ctx *context.Context) (*runnersCtx, error) {
- if ctx.Data["PageIsRepoSettings"] == true {
- return &runnersCtx{
- RepoID: ctx.Repo.Repository.ID,
- OwnerID: 0,
- IsRepo: true,
- RunnersTemplate: tplRepoRunners,
- RunnerEditTemplate: tplRepoRunnerEdit,
- RedirectLink: ctx.Repo.RepoLink + "/settings/actions/runners/",
- }, nil
- }
-
- if ctx.Data["PageIsOrgSettings"] == true {
- err := shared_user.LoadHeaderCount(ctx)
- if err != nil {
- ctx.ServerError("LoadHeaderCount", err)
- return nil, nil
- }
- return &runnersCtx{
- RepoID: 0,
- OwnerID: ctx.Org.Organization.ID,
- IsOrg: true,
- RunnersTemplate: tplOrgRunners,
- RunnerEditTemplate: tplOrgRunnerEdit,
- RedirectLink: ctx.Org.OrgLink + "/settings/actions/runners/",
- }, nil
- }
-
- if ctx.Data["PageIsAdmin"] == true {
- return &runnersCtx{
- RepoID: 0,
- OwnerID: 0,
- IsAdmin: true,
- RunnersTemplate: tplAdminRunners,
- RunnerEditTemplate: tplAdminRunnerEdit,
- RedirectLink: setting.AppSubURL + "/-/admin/actions/runners/",
- }, nil
- }
-
- if ctx.Data["PageIsUserSettings"] == true {
- return &runnersCtx{
- OwnerID: ctx.Doer.ID,
- RepoID: 0,
- IsUser: true,
- RunnersTemplate: tplUserRunners,
- RunnerEditTemplate: tplUserRunnerEdit,
- RedirectLink: setting.AppSubURL + "/user/settings/actions/runners/",
- }, nil
- }
-
- return nil, errors.New("unable to set Runners context")
-}
-
-// Runners render settings/actions/runners page for repo level
-func Runners(ctx *context.Context) {
- ctx.Data["PageIsSharedSettingsRunners"] = true
- ctx.Data["Title"] = ctx.Tr("actions.actions")
- ctx.Data["PageType"] = "runners"
-
- rCtx, err := getRunnersCtx(ctx)
- if err != nil {
- ctx.ServerError("getRunnersCtx", err)
- return
- }
-
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
-
- opts := actions_model.FindRunnerOptions{
- ListOptions: db.ListOptions{
- Page: page,
- PageSize: 100,
- },
- Sort: ctx.Req.URL.Query().Get("sort"),
- Filter: ctx.Req.URL.Query().Get("q"),
- }
- if rCtx.IsRepo {
- opts.RepoID = rCtx.RepoID
- opts.WithAvailable = true
- } else if rCtx.IsOrg || rCtx.IsUser {
- opts.OwnerID = rCtx.OwnerID
- opts.WithAvailable = true
- }
- actions_shared.RunnersList(ctx, opts)
-
- ctx.HTML(http.StatusOK, rCtx.RunnersTemplate)
-}
-
-// RunnersEdit renders runner edit page for repository level
-func RunnersEdit(ctx *context.Context) {
- ctx.Data["PageIsSharedSettingsRunners"] = true
- ctx.Data["Title"] = ctx.Tr("actions.runners.edit_runner")
- rCtx, err := getRunnersCtx(ctx)
- if err != nil {
- ctx.ServerError("getRunnersCtx", err)
- return
- }
-
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
-
- actions_shared.RunnerDetails(ctx, page,
- ctx.PathParamInt64("runnerid"), rCtx.OwnerID, rCtx.RepoID,
- )
- ctx.HTML(http.StatusOK, rCtx.RunnerEditTemplate)
-}
-
-func RunnersEditPost(ctx *context.Context) {
- rCtx, err := getRunnersCtx(ctx)
- if err != nil {
- ctx.ServerError("getRunnersCtx", err)
- return
- }
- actions_shared.RunnerDetailsEditPost(ctx, ctx.PathParamInt64("runnerid"),
- rCtx.OwnerID, rCtx.RepoID,
- rCtx.RedirectLink+url.PathEscape(ctx.PathParam("runnerid")))
-}
-
-func ResetRunnerRegistrationToken(ctx *context.Context) {
- rCtx, err := getRunnersCtx(ctx)
- if err != nil {
- ctx.ServerError("getRunnersCtx", err)
- return
- }
- actions_shared.RunnerResetRegistrationToken(ctx, rCtx.OwnerID, rCtx.RepoID, rCtx.RedirectLink)
-}
-
-// RunnerDeletePost response for deleting runner
-func RunnerDeletePost(ctx *context.Context) {
- rCtx, err := getRunnersCtx(ctx)
- if err != nil {
- ctx.ServerError("getRunnersCtx", err)
- return
- }
- actions_shared.RunnerDeletePost(ctx, ctx.PathParamInt64("runnerid"), rCtx.RedirectLink, rCtx.RedirectLink+url.PathEscape(ctx.PathParam("runnerid")))
-}
-
-func RedirectToDefaultSetting(ctx *context.Context) {
- ctx.Redirect(ctx.Repo.RepoLink + "/settings/actions/runners")
-}
diff --git a/routers/web/repo/setting/variables.go b/routers/web/repo/setting/variables.go
deleted file mode 100644
index 9b5453f043..0000000000
--- a/routers/web/repo/setting/variables.go
+++ /dev/null
@@ -1,140 +0,0 @@
-// Copyright 2023 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package setting
-
-import (
- "errors"
- "net/http"
-
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/templates"
- shared "code.gitea.io/gitea/routers/web/shared/actions"
- shared_user "code.gitea.io/gitea/routers/web/shared/user"
- "code.gitea.io/gitea/services/context"
-)
-
-const (
- tplRepoVariables templates.TplName = "repo/settings/actions"
- tplOrgVariables templates.TplName = "org/settings/actions"
- tplUserVariables templates.TplName = "user/settings/actions"
- tplAdminVariables templates.TplName = "admin/actions"
-)
-
-type variablesCtx struct {
- OwnerID int64
- RepoID int64
- IsRepo bool
- IsOrg bool
- IsUser bool
- IsGlobal bool
- VariablesTemplate templates.TplName
- RedirectLink string
-}
-
-func getVariablesCtx(ctx *context.Context) (*variablesCtx, error) {
- if ctx.Data["PageIsRepoSettings"] == true {
- return &variablesCtx{
- OwnerID: 0,
- RepoID: ctx.Repo.Repository.ID,
- IsRepo: true,
- VariablesTemplate: tplRepoVariables,
- RedirectLink: ctx.Repo.RepoLink + "/settings/actions/variables",
- }, nil
- }
-
- if ctx.Data["PageIsOrgSettings"] == true {
- err := shared_user.LoadHeaderCount(ctx)
- if err != nil {
- ctx.ServerError("LoadHeaderCount", err)
- return nil, nil
- }
- return &variablesCtx{
- OwnerID: ctx.ContextUser.ID,
- RepoID: 0,
- IsOrg: true,
- VariablesTemplate: tplOrgVariables,
- RedirectLink: ctx.Org.OrgLink + "/settings/actions/variables",
- }, nil
- }
-
- if ctx.Data["PageIsUserSettings"] == true {
- return &variablesCtx{
- OwnerID: ctx.Doer.ID,
- RepoID: 0,
- IsUser: true,
- VariablesTemplate: tplUserVariables,
- RedirectLink: setting.AppSubURL + "/user/settings/actions/variables",
- }, nil
- }
-
- if ctx.Data["PageIsAdmin"] == true {
- return &variablesCtx{
- OwnerID: 0,
- RepoID: 0,
- IsGlobal: true,
- VariablesTemplate: tplAdminVariables,
- RedirectLink: setting.AppSubURL + "/-/admin/actions/variables",
- }, nil
- }
-
- return nil, errors.New("unable to set Variables context")
-}
-
-func Variables(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("actions.variables")
- ctx.Data["PageType"] = "variables"
- ctx.Data["PageIsSharedSettingsVariables"] = true
-
- vCtx, err := getVariablesCtx(ctx)
- if err != nil {
- ctx.ServerError("getVariablesCtx", err)
- return
- }
-
- shared.SetVariablesContext(ctx, vCtx.OwnerID, vCtx.RepoID)
- if ctx.Written() {
- return
- }
-
- ctx.HTML(http.StatusOK, vCtx.VariablesTemplate)
-}
-
-func VariableCreate(ctx *context.Context) {
- vCtx, err := getVariablesCtx(ctx)
- if err != nil {
- ctx.ServerError("getVariablesCtx", err)
- return
- }
-
- if ctx.HasError() { // form binding validation error
- ctx.JSONError(ctx.GetErrMsg())
- return
- }
-
- shared.CreateVariable(ctx, vCtx.OwnerID, vCtx.RepoID, vCtx.RedirectLink)
-}
-
-func VariableUpdate(ctx *context.Context) {
- vCtx, err := getVariablesCtx(ctx)
- if err != nil {
- ctx.ServerError("getVariablesCtx", err)
- return
- }
-
- if ctx.HasError() { // form binding validation error
- ctx.JSONError(ctx.GetErrMsg())
- return
- }
-
- shared.UpdateVariable(ctx, vCtx.RedirectLink)
-}
-
-func VariableDelete(ctx *context.Context) {
- vCtx, err := getVariablesCtx(ctx)
- if err != nil {
- ctx.ServerError("getVariablesCtx", err)
- return
- }
- shared.DeleteVariable(ctx, vCtx.RedirectLink)
-}
diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go
index 997145b507..4ff2467041 100644
--- a/routers/web/repo/setting/webhook.go
+++ b/routers/web/repo/setting/webhook.go
@@ -184,6 +184,7 @@ func ParseHookEvent(form forms.WebhookForm) *webhook_module.HookEvent {
webhook_module.HookEventWiki: form.Wiki,
webhook_module.HookEventRepository: form.Repository,
webhook_module.HookEventPackage: form.Package,
+ webhook_module.HookEventStatus: form.Status,
},
BranchFilter: form.BranchFilter,
}
diff --git a/routers/web/repo/star.go b/routers/web/repo/star.go
new file mode 100644
index 0000000000..00c06b7d02
--- /dev/null
+++ b/routers/web/repo/star.go
@@ -0,0 +1,31 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "net/http"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/services/context"
+)
+
+const tplStarUnstar templates.TplName = "repo/star_unstar"
+
+func ActionStar(ctx *context.Context) {
+ err := repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, ctx.PathParam("action") == "star")
+ if err != nil {
+ handleActionError(ctx, err)
+ return
+ }
+
+ ctx.Data["IsStaringRepo"] = repo_model.IsStaring(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
+ ctx.Data["Repository"], err = repo_model.GetRepositoryByName(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.Name)
+ if err != nil {
+ ctx.ServerError("GetRepositoryByName", err)
+ return
+ }
+ ctx.RespHeader().Add("hx-trigger", "refreshUserCards") // see the `hx-trigger="refreshUserCards ..."` comments in tmpl
+ ctx.HTML(http.StatusOK, tplStarUnstar)
+}
diff --git a/routers/web/repo/transfer.go b/routers/web/repo/transfer.go
new file mode 100644
index 0000000000..5553eee674
--- /dev/null
+++ b/routers/web/repo/transfer.go
@@ -0,0 +1,38 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "code.gitea.io/gitea/services/context"
+ repo_service "code.gitea.io/gitea/services/repository"
+)
+
+func acceptTransfer(ctx *context.Context) {
+ err := repo_service.AcceptTransferOwnership(ctx, ctx.Repo.Repository, ctx.Doer)
+ if err == nil {
+ ctx.Flash.Success(ctx.Tr("repo.settings.transfer.success"))
+ ctx.Redirect(ctx.Repo.Repository.Link())
+ return
+ }
+ handleActionError(ctx, err)
+}
+
+func rejectTransfer(ctx *context.Context) {
+ err := repo_service.RejectRepositoryTransfer(ctx, ctx.Repo.Repository, ctx.Doer)
+ if err == nil {
+ ctx.Flash.Success(ctx.Tr("repo.settings.transfer.rejected"))
+ ctx.Redirect(ctx.Repo.Repository.Link())
+ return
+ }
+ handleActionError(ctx, err)
+}
+
+func ActionTransfer(ctx *context.Context) {
+ switch ctx.PathParam("action") {
+ case "accept_transfer":
+ acceptTransfer(ctx)
+ case "reject_transfer":
+ rejectTransfer(ctx)
+ }
+}
diff --git a/routers/web/repo/watch.go b/routers/web/repo/watch.go
new file mode 100644
index 0000000000..70c548b8ce
--- /dev/null
+++ b/routers/web/repo/watch.go
@@ -0,0 +1,31 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "net/http"
+
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/services/context"
+)
+
+const tplWatchUnwatch templates.TplName = "repo/watch_unwatch"
+
+func ActionWatch(ctx *context.Context) {
+ err := repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, ctx.PathParam("action") == "watch")
+ if err != nil {
+ handleActionError(ctx, err)
+ return
+ }
+
+ ctx.Data["IsWatchingRepo"] = repo_model.IsWatching(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
+ ctx.Data["Repository"], err = repo_model.GetRepositoryByName(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.Name)
+ if err != nil {
+ ctx.ServerError("GetRepositoryByName", err)
+ return
+ }
+ ctx.RespHeader().Add("hx-trigger", "refreshUserCards") // see the `hx-trigger="refreshUserCards ..."` comments in tmpl
+ ctx.HTML(http.StatusOK, tplWatchUnwatch)
+}
diff --git a/routers/web/shared/actions/runners.go b/routers/web/shared/actions/runners.go
index 6d77bdd2fa..41aac4976b 100644
--- a/routers/web/shared/actions/runners.go
+++ b/routers/web/shared/actions/runners.go
@@ -5,18 +5,131 @@ package actions
import (
"errors"
+ "net/http"
+ "net/url"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ shared_user "code.gitea.io/gitea/routers/web/shared/user"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
-// RunnersList prepares data for runners list
-func RunnersList(ctx *context.Context, opts actions_model.FindRunnerOptions) {
+const (
+ // TODO: Separate secrets from runners when layout is ready
+ tplRepoRunners templates.TplName = "repo/settings/actions"
+ tplOrgRunners templates.TplName = "org/settings/actions"
+ tplAdminRunners templates.TplName = "admin/actions"
+ tplUserRunners templates.TplName = "user/settings/actions"
+ tplRepoRunnerEdit templates.TplName = "repo/settings/runner_edit"
+ tplOrgRunnerEdit templates.TplName = "org/settings/runners_edit"
+ tplAdminRunnerEdit templates.TplName = "admin/runners/edit"
+ tplUserRunnerEdit templates.TplName = "user/settings/runner_edit"
+)
+
+type runnersCtx struct {
+ OwnerID int64
+ RepoID int64
+ IsRepo bool
+ IsOrg bool
+ IsAdmin bool
+ IsUser bool
+ RunnersTemplate templates.TplName
+ RunnerEditTemplate templates.TplName
+ RedirectLink string
+}
+
+func getRunnersCtx(ctx *context.Context) (*runnersCtx, error) {
+ if ctx.Data["PageIsRepoSettings"] == true {
+ return &runnersCtx{
+ RepoID: ctx.Repo.Repository.ID,
+ OwnerID: 0,
+ IsRepo: true,
+ RunnersTemplate: tplRepoRunners,
+ RunnerEditTemplate: tplRepoRunnerEdit,
+ RedirectLink: ctx.Repo.RepoLink + "/settings/actions/runners/",
+ }, nil
+ }
+
+ if ctx.Data["PageIsOrgSettings"] == true {
+ err := shared_user.LoadHeaderCount(ctx)
+ if err != nil {
+ ctx.ServerError("LoadHeaderCount", err)
+ return nil, nil
+ }
+ return &runnersCtx{
+ RepoID: 0,
+ OwnerID: ctx.Org.Organization.ID,
+ IsOrg: true,
+ RunnersTemplate: tplOrgRunners,
+ RunnerEditTemplate: tplOrgRunnerEdit,
+ RedirectLink: ctx.Org.OrgLink + "/settings/actions/runners/",
+ }, nil
+ }
+
+ if ctx.Data["PageIsAdmin"] == true {
+ return &runnersCtx{
+ RepoID: 0,
+ OwnerID: 0,
+ IsAdmin: true,
+ RunnersTemplate: tplAdminRunners,
+ RunnerEditTemplate: tplAdminRunnerEdit,
+ RedirectLink: setting.AppSubURL + "/-/admin/actions/runners/",
+ }, nil
+ }
+
+ if ctx.Data["PageIsUserSettings"] == true {
+ return &runnersCtx{
+ OwnerID: ctx.Doer.ID,
+ RepoID: 0,
+ IsUser: true,
+ RunnersTemplate: tplUserRunners,
+ RunnerEditTemplate: tplUserRunnerEdit,
+ RedirectLink: setting.AppSubURL + "/user/settings/actions/runners/",
+ }, nil
+ }
+
+ return nil, errors.New("unable to set Runners context")
+}
+
+// Runners render settings/actions/runners page for repo level
+func Runners(ctx *context.Context) {
+ ctx.Data["PageIsSharedSettingsRunners"] = true
+ ctx.Data["Title"] = ctx.Tr("actions.actions")
+ ctx.Data["PageType"] = "runners"
+
+ rCtx, err := getRunnersCtx(ctx)
+ if err != nil {
+ ctx.ServerError("getRunnersCtx", err)
+ return
+ }
+
+ page := ctx.FormInt("page")
+ if page <= 1 {
+ page = 1
+ }
+
+ opts := actions_model.FindRunnerOptions{
+ ListOptions: db.ListOptions{
+ Page: page,
+ PageSize: 100,
+ },
+ Sort: ctx.Req.URL.Query().Get("sort"),
+ Filter: ctx.Req.URL.Query().Get("q"),
+ }
+ if rCtx.IsRepo {
+ opts.RepoID = rCtx.RepoID
+ opts.WithAvailable = true
+ } else if rCtx.IsOrg || rCtx.IsUser {
+ opts.OwnerID = rCtx.OwnerID
+ opts.WithAvailable = true
+ }
+
runners, count, err := db.FindAndCount[actions_model.ActionRunner](ctx, opts)
if err != nil {
ctx.ServerError("CountRunners", err)
@@ -53,10 +166,29 @@ func RunnersList(ctx *context.Context, opts actions_model.FindRunnerOptions) {
pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
ctx.Data["Page"] = pager
+
+ ctx.HTML(http.StatusOK, rCtx.RunnersTemplate)
}
-// RunnerDetails prepares data for runners edit page
-func RunnerDetails(ctx *context.Context, page int, runnerID, ownerID, repoID int64) {
+// RunnersEdit renders runner edit page for repository level
+func RunnersEdit(ctx *context.Context) {
+ ctx.Data["PageIsSharedSettingsRunners"] = true
+ ctx.Data["Title"] = ctx.Tr("actions.runners.edit_runner")
+ rCtx, err := getRunnersCtx(ctx)
+ if err != nil {
+ ctx.ServerError("getRunnersCtx", err)
+ return
+ }
+
+ page := ctx.FormInt("page")
+ if page <= 1 {
+ page = 1
+ }
+
+ runnerID := ctx.PathParamInt64("runnerid")
+ ownerID := rCtx.OwnerID
+ repoID := rCtx.RepoID
+
runner, err := actions_model.GetRunnerByID(ctx, runnerID)
if err != nil {
ctx.ServerError("GetRunnerByID", err)
@@ -97,10 +229,22 @@ func RunnerDetails(ctx *context.Context, page int, runnerID, ownerID, repoID int
ctx.Data["Tasks"] = tasks
pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
ctx.Data["Page"] = pager
+
+ ctx.HTML(http.StatusOK, rCtx.RunnerEditTemplate)
}
-// RunnerDetailsEditPost response for edit runner details
-func RunnerDetailsEditPost(ctx *context.Context, runnerID, ownerID, repoID int64, redirectTo string) {
+func RunnersEditPost(ctx *context.Context) {
+ rCtx, err := getRunnersCtx(ctx)
+ if err != nil {
+ ctx.ServerError("getRunnersCtx", err)
+ return
+ }
+
+ runnerID := ctx.PathParamInt64("runnerid")
+ ownerID := rCtx.OwnerID
+ repoID := rCtx.RepoID
+ redirectTo := rCtx.RedirectLink
+
runner, err := actions_model.GetRunnerByID(ctx, runnerID)
if err != nil {
log.Warn("RunnerDetailsEditPost.GetRunnerByID failed: %v, url: %s", err, ctx.Req.URL)
@@ -129,10 +273,18 @@ func RunnerDetailsEditPost(ctx *context.Context, runnerID, ownerID, repoID int64
ctx.Redirect(redirectTo)
}
-// RunnerResetRegistrationToken reset registration token
-func RunnerResetRegistrationToken(ctx *context.Context, ownerID, repoID int64, redirectTo string) {
- _, err := actions_model.NewRunnerToken(ctx, ownerID, repoID)
+func ResetRunnerRegistrationToken(ctx *context.Context) {
+ rCtx, err := getRunnersCtx(ctx)
if err != nil {
+ ctx.ServerError("getRunnersCtx", err)
+ return
+ }
+
+ ownerID := rCtx.OwnerID
+ repoID := rCtx.RepoID
+ redirectTo := rCtx.RedirectLink
+
+ if _, err := actions_model.NewRunnerToken(ctx, ownerID, repoID); err != nil {
ctx.ServerError("ResetRunnerRegistrationToken", err)
return
}
@@ -140,11 +292,28 @@ func RunnerResetRegistrationToken(ctx *context.Context, ownerID, repoID int64, r
ctx.JSONRedirect(redirectTo)
}
-// RunnerDeletePost response for deleting a runner
-func RunnerDeletePost(ctx *context.Context, runnerID int64,
- successRedirectTo, failedRedirectTo string,
-) {
- if err := actions_model.DeleteRunner(ctx, runnerID); err != nil {
+// RunnerDeletePost response for deleting runner
+func RunnerDeletePost(ctx *context.Context) {
+ rCtx, err := getRunnersCtx(ctx)
+ if err != nil {
+ ctx.ServerError("getRunnersCtx", err)
+ return
+ }
+
+ runner := findActionsRunner(ctx, rCtx)
+ if ctx.Written() {
+ return
+ }
+
+ if !runner.Editable(rCtx.OwnerID, rCtx.RepoID) {
+ ctx.NotFound("RunnerDeletePost", util.NewPermissionDeniedErrorf("no permission to delete this runner"))
+ return
+ }
+
+ successRedirectTo := rCtx.RedirectLink
+ failedRedirectTo := rCtx.RedirectLink + url.PathEscape(ctx.PathParam("runnerid"))
+
+ if err := actions_model.DeleteRunner(ctx, runner.ID); err != nil {
log.Warn("DeleteRunnerPost.UpdateRunner failed: %v, url: %s", err, ctx.Req.URL)
ctx.Flash.Warning(ctx.Tr("actions.runners.delete_runner_failed"))
@@ -158,3 +327,41 @@ func RunnerDeletePost(ctx *context.Context, runnerID int64,
ctx.JSONRedirect(successRedirectTo)
}
+
+func RedirectToDefaultSetting(ctx *context.Context) {
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings/actions/runners")
+}
+
+func findActionsRunner(ctx *context.Context, rCtx *runnersCtx) *actions_model.ActionRunner {
+ runnerID := ctx.PathParamInt64("runnerid")
+ opts := &actions_model.FindRunnerOptions{
+ IDs: []int64{runnerID},
+ }
+ switch {
+ case rCtx.IsRepo:
+ opts.RepoID = rCtx.RepoID
+ if opts.RepoID == 0 {
+ panic("repoID is 0")
+ }
+ case rCtx.IsOrg, rCtx.IsUser:
+ opts.OwnerID = rCtx.OwnerID
+ if opts.OwnerID == 0 {
+ panic("ownerID is 0")
+ }
+ case rCtx.IsAdmin:
+ // do nothing
+ default:
+ panic("invalid actions runner context")
+ }
+
+ got, err := db.Find[actions_model.ActionRunner](ctx, opts)
+ if err != nil {
+ ctx.ServerError("FindRunner", err)
+ return nil
+ } else if len(got) == 0 {
+ ctx.NotFound("FindRunner", errors.New("runner not found"))
+ return nil
+ }
+
+ return got[0]
+}
diff --git a/routers/web/shared/actions/variables.go b/routers/web/shared/actions/variables.go
index f895475748..052a8fdd18 100644
--- a/routers/web/shared/actions/variables.go
+++ b/routers/web/shared/actions/variables.go
@@ -4,31 +4,127 @@
package actions
import (
+ "errors"
+ "net/http"
+
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web"
+ shared_user "code.gitea.io/gitea/routers/web/shared/user"
actions_service "code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
-func SetVariablesContext(ctx *context.Context, ownerID, repoID int64) {
+const (
+ tplRepoVariables templates.TplName = "repo/settings/actions"
+ tplOrgVariables templates.TplName = "org/settings/actions"
+ tplUserVariables templates.TplName = "user/settings/actions"
+ tplAdminVariables templates.TplName = "admin/actions"
+)
+
+type variablesCtx struct {
+ OwnerID int64
+ RepoID int64
+ IsRepo bool
+ IsOrg bool
+ IsUser bool
+ IsGlobal bool
+ VariablesTemplate templates.TplName
+ RedirectLink string
+}
+
+func getVariablesCtx(ctx *context.Context) (*variablesCtx, error) {
+ if ctx.Data["PageIsRepoSettings"] == true {
+ return &variablesCtx{
+ OwnerID: 0,
+ RepoID: ctx.Repo.Repository.ID,
+ IsRepo: true,
+ VariablesTemplate: tplRepoVariables,
+ RedirectLink: ctx.Repo.RepoLink + "/settings/actions/variables",
+ }, nil
+ }
+
+ if ctx.Data["PageIsOrgSettings"] == true {
+ err := shared_user.LoadHeaderCount(ctx)
+ if err != nil {
+ ctx.ServerError("LoadHeaderCount", err)
+ return nil, nil
+ }
+ return &variablesCtx{
+ OwnerID: ctx.ContextUser.ID,
+ RepoID: 0,
+ IsOrg: true,
+ VariablesTemplate: tplOrgVariables,
+ RedirectLink: ctx.Org.OrgLink + "/settings/actions/variables",
+ }, nil
+ }
+
+ if ctx.Data["PageIsUserSettings"] == true {
+ return &variablesCtx{
+ OwnerID: ctx.Doer.ID,
+ RepoID: 0,
+ IsUser: true,
+ VariablesTemplate: tplUserVariables,
+ RedirectLink: setting.AppSubURL + "/user/settings/actions/variables",
+ }, nil
+ }
+
+ if ctx.Data["PageIsAdmin"] == true {
+ return &variablesCtx{
+ OwnerID: 0,
+ RepoID: 0,
+ IsGlobal: true,
+ VariablesTemplate: tplAdminVariables,
+ RedirectLink: setting.AppSubURL + "/-/admin/actions/variables",
+ }, nil
+ }
+
+ return nil, errors.New("unable to set Variables context")
+}
+
+func Variables(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("actions.variables")
+ ctx.Data["PageType"] = "variables"
+ ctx.Data["PageIsSharedSettingsVariables"] = true
+
+ vCtx, err := getVariablesCtx(ctx)
+ if err != nil {
+ ctx.ServerError("getVariablesCtx", err)
+ return
+ }
+
variables, err := db.Find[actions_model.ActionVariable](ctx, actions_model.FindVariablesOpts{
- OwnerID: ownerID,
- RepoID: repoID,
+ OwnerID: vCtx.OwnerID,
+ RepoID: vCtx.RepoID,
})
if err != nil {
ctx.ServerError("FindVariables", err)
return
}
ctx.Data["Variables"] = variables
+
+ ctx.HTML(http.StatusOK, vCtx.VariablesTemplate)
}
-func CreateVariable(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
+func VariableCreate(ctx *context.Context) {
+ vCtx, err := getVariablesCtx(ctx)
+ if err != nil {
+ ctx.ServerError("getVariablesCtx", err)
+ return
+ }
+
+ if ctx.HasError() { // form binding validation error
+ ctx.JSONError(ctx.GetErrMsg())
+ return
+ }
+
form := web.GetForm(ctx).(*forms.EditVariableForm)
- v, err := actions_service.CreateVariable(ctx, ownerID, repoID, form.Name, form.Data)
+ v, err := actions_service.CreateVariable(ctx, vCtx.OwnerID, vCtx.RepoID, form.Name, form.Data)
if err != nil {
log.Error("CreateVariable: %v", err)
ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
@@ -36,30 +132,92 @@ func CreateVariable(ctx *context.Context, ownerID, repoID int64, redirectURL str
}
ctx.Flash.Success(ctx.Tr("actions.variables.creation.success", v.Name))
- ctx.JSONRedirect(redirectURL)
+ ctx.JSONRedirect(vCtx.RedirectLink)
}
-func UpdateVariable(ctx *context.Context, redirectURL string) {
+func VariableUpdate(ctx *context.Context) {
+ vCtx, err := getVariablesCtx(ctx)
+ if err != nil {
+ ctx.ServerError("getVariablesCtx", err)
+ return
+ }
+
+ if ctx.HasError() { // form binding validation error
+ ctx.JSONError(ctx.GetErrMsg())
+ return
+ }
+
id := ctx.PathParamInt64("variable_id")
+
+ variable := findActionsVariable(ctx, id, vCtx)
+ if ctx.Written() {
+ return
+ }
+
form := web.GetForm(ctx).(*forms.EditVariableForm)
+ variable.Name = form.Name
+ variable.Data = form.Data
- if ok, err := actions_service.UpdateVariable(ctx, id, form.Name, form.Data); err != nil || !ok {
+ if ok, err := actions_service.UpdateVariableNameData(ctx, variable); err != nil || !ok {
log.Error("UpdateVariable: %v", err)
ctx.JSONError(ctx.Tr("actions.variables.update.failed"))
return
}
ctx.Flash.Success(ctx.Tr("actions.variables.update.success"))
- ctx.JSONRedirect(redirectURL)
+ ctx.JSONRedirect(vCtx.RedirectLink)
}
-func DeleteVariable(ctx *context.Context, redirectURL string) {
+func findActionsVariable(ctx *context.Context, id int64, vCtx *variablesCtx) *actions_model.ActionVariable {
+ opts := actions_model.FindVariablesOpts{
+ IDs: []int64{id},
+ }
+ switch {
+ case vCtx.IsRepo:
+ opts.RepoID = vCtx.RepoID
+ if opts.RepoID == 0 {
+ panic("RepoID is 0")
+ }
+ case vCtx.IsOrg, vCtx.IsUser:
+ opts.OwnerID = vCtx.OwnerID
+ if opts.OwnerID == 0 {
+ panic("OwnerID is 0")
+ }
+ case vCtx.IsGlobal:
+ // do nothing
+ default:
+ panic("invalid actions variable")
+ }
+
+ got, err := actions_model.FindVariables(ctx, opts)
+ if err != nil {
+ ctx.ServerError("FindVariables", err)
+ return nil
+ } else if len(got) == 0 {
+ ctx.NotFound("FindVariables", nil)
+ return nil
+ }
+ return got[0]
+}
+
+func VariableDelete(ctx *context.Context) {
+ vCtx, err := getVariablesCtx(ctx)
+ if err != nil {
+ ctx.ServerError("getVariablesCtx", err)
+ return
+ }
+
id := ctx.PathParamInt64("variable_id")
- if err := actions_service.DeleteVariableByID(ctx, id); err != nil {
+ variable := findActionsVariable(ctx, id, vCtx)
+ if ctx.Written() {
+ return
+ }
+
+ if err := actions_service.DeleteVariableByID(ctx, variable.ID); err != nil {
log.Error("Delete variable [%d] failed: %v", id, err)
ctx.JSONError(ctx.Tr("actions.variables.deletion.failed"))
return
}
ctx.Flash.Success(ctx.Tr("actions.variables.deletion.success"))
- ctx.JSONRedirect(redirectURL)
+ ctx.JSONRedirect(vCtx.RedirectLink)
}
diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go
index 006ffdcf7e..7cda3c038c 100644
--- a/routers/web/user/profile.go
+++ b/routers/web/user/profile.go
@@ -313,8 +313,8 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
ctx.Data["Page"] = pager
}
-// Action response for follow/unfollow user request
-func Action(ctx *context.Context) {
+// ActionUserFollow is for follow/unfollow user request
+func ActionUserFollow(ctx *context.Context) {
var err error
switch ctx.FormString("action") {
case "follow":
@@ -339,6 +339,6 @@ func Action(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplFollowUnfollow)
return
}
- log.Error("Failed to apply action %q: unsupport context user type: %s", ctx.FormString("action"), ctx.ContextUser.Type)
+ log.Error("Failed to apply action %q: unsupported context user type: %s", ctx.FormString("action"), ctx.ContextUser.Type)
ctx.Error(http.StatusBadRequest, fmt.Sprintf("Action %q failed", ctx.FormString("action")))
}
diff --git a/routers/web/web.go b/routers/web/web.go
index bbf257a493..f5bd6a9297 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -37,6 +37,7 @@ import (
"code.gitea.io/gitea/routers/web/repo"
"code.gitea.io/gitea/routers/web/repo/actions"
repo_setting "code.gitea.io/gitea/routers/web/repo/setting"
+ shared_actions "code.gitea.io/gitea/routers/web/shared/actions"
"code.gitea.io/gitea/routers/web/shared/project"
"code.gitea.io/gitea/routers/web/user"
user_setting "code.gitea.io/gitea/routers/web/user/setting"
@@ -118,7 +119,7 @@ func webAuth(authMethod auth_service.Method) func(*context.Context) {
ar, err := common.AuthShared(ctx.Base, ctx.Session, authMethod)
if err != nil {
log.Error("Failed to verify user: %v", err)
- ctx.Error(http.StatusUnauthorized, "Verify")
+ ctx.Error(http.StatusUnauthorized, "Failed to authenticate user")
return
}
ctx.Doer = ar.Doer
@@ -347,6 +348,13 @@ func registerRoutes(m *web.Router) {
}
}
+ starsEnabled := func(ctx *context.Context) {
+ if setting.Repository.DisableStars {
+ ctx.Error(http.StatusForbidden)
+ return
+ }
+ }
+
lfsServerEnabled := func(ctx *context.Context) {
if !setting.LFS.StartServer {
ctx.Error(http.StatusNotFound)
@@ -442,10 +450,10 @@ func registerRoutes(m *web.Router) {
addSettingsVariablesRoutes := func() {
m.Group("/variables", func() {
- m.Get("", repo_setting.Variables)
- m.Post("/new", web.Bind(forms.EditVariableForm{}), repo_setting.VariableCreate)
- m.Post("/{variable_id}/edit", web.Bind(forms.EditVariableForm{}), repo_setting.VariableUpdate)
- m.Post("/{variable_id}/delete", repo_setting.VariableDelete)
+ m.Get("", shared_actions.Variables)
+ m.Post("/new", web.Bind(forms.EditVariableForm{}), shared_actions.VariableCreate)
+ m.Post("/{variable_id}/edit", web.Bind(forms.EditVariableForm{}), shared_actions.VariableUpdate)
+ m.Post("/{variable_id}/delete", shared_actions.VariableDelete)
})
}
@@ -459,11 +467,11 @@ func registerRoutes(m *web.Router) {
addSettingsRunnersRoutes := func() {
m.Group("/runners", func() {
- m.Get("", repo_setting.Runners)
- m.Combo("/{runnerid}").Get(repo_setting.RunnersEdit).
- Post(web.Bind(forms.EditRunnerForm{}), repo_setting.RunnersEditPost)
- m.Post("/{runnerid}/delete", repo_setting.RunnerDeletePost)
- m.Post("/reset_registration_token", repo_setting.ResetRunnerRegistrationToken)
+ m.Get("", shared_actions.Runners)
+ m.Combo("/{runnerid}").Get(shared_actions.RunnersEdit).
+ Post(web.Bind(forms.EditRunnerForm{}), shared_actions.RunnersEditPost)
+ m.Post("/{runnerid}/delete", shared_actions.RunnerDeletePost)
+ m.Post("/reset_registration_token", shared_actions.ResetRunnerRegistrationToken)
})
}
@@ -815,7 +823,7 @@ func registerRoutes(m *web.Router) {
m.Methods("GET, OPTIONS", "/attachments/{uuid}", optionsCorsHandler(), repo.GetAttachment)
}, optSignIn)
- m.Post("/{username}", reqSignIn, context.UserAssignmentWeb(), user.Action)
+ m.Post("/{username}", reqSignIn, context.UserAssignmentWeb(), user.ActionUserFollow)
reqRepoAdmin := context.RequireRepoAdmin()
reqRepoCodeWriter := context.RequireUnitWriter(unit.TypeCode)
@@ -865,7 +873,7 @@ func registerRoutes(m *web.Router) {
m.Group("/org", func() {
m.Group("/{org}", func() {
m.Get("/members", org.Members)
- }, context.OrgAssignment())
+ }, context.OrgAssignment(context.OrgAssignmentOptions{}))
}, optSignIn)
// end "/org": members
@@ -891,19 +899,20 @@ func registerRoutes(m *web.Router) {
m.Get("/milestones/{team}", reqMilestonesDashboardPageEnabled, user.Milestones)
m.Post("/members/action/{action}", org.MembersAction)
m.Get("/teams", org.Teams)
- }, context.OrgAssignment(true, false, true))
+ }, context.OrgAssignment(context.OrgAssignmentOptions{RequireMember: true, RequireTeamMember: true}))
m.Group("/{org}", func() {
m.Get("/teams/{team}", org.TeamMembers)
m.Get("/teams/{team}/repositories", org.TeamRepositories)
m.Post("/teams/{team}/action/{action}", org.TeamsAction)
m.Post("/teams/{team}/action/repo/{action}", org.TeamsRepoAction)
- }, context.OrgAssignment(true, false, true))
+ }, context.OrgAssignment(context.OrgAssignmentOptions{RequireMember: true, RequireTeamMember: true}))
- // require admin permission
+ // require member/team-admin permission (old logic is: requireMember=true, requireTeamAdmin=true)
+ // but it doesn't seem right: requireTeamAdmin does nothing
m.Group("/{org}", func() {
m.Get("/teams/-/search", org.SearchTeam)
- }, context.OrgAssignment(true, false, false, true))
+ }, context.OrgAssignment(context.OrgAssignmentOptions{RequireMember: true, RequireTeamAdmin: true}))
// require owner permission
m.Group("/{org}", func() {
@@ -913,6 +922,8 @@ func registerRoutes(m *web.Router) {
m.Post("/teams/{team}/edit", web.Bind(forms.CreateTeamForm{}), org.EditTeamPost)
m.Post("/teams/{team}/delete", org.DeleteTeam)
+ m.Get("/worktime", context.OrgAssignment(context.OrgAssignmentOptions{RequireOwner: true}), org.Worktime)
+
m.Group("/settings", func() {
m.Combo("").Get(org.Settings).
Post(web.Bind(forms.UpdateOrgSettingForm{}), org.SettingsPost)
@@ -980,7 +991,7 @@ func registerRoutes(m *web.Router) {
m.Post("", web.Bind(forms.BlockUserForm{}), org.BlockedUsersPost)
})
}, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled, "PageIsOrgSettings", true))
- }, context.OrgAssignment(true, true))
+ }, context.OrgAssignment(context.OrgAssignmentOptions{RequireOwner: true}))
}, reqSignIn)
// end "/org": most org routes
@@ -1050,7 +1061,7 @@ func registerRoutes(m *web.Router) {
m.Group("", func() {
m.Get("/code", user.CodeSearch)
}, reqUnitAccess(unit.TypeCode, perm.AccessModeRead, false), individualPermsChecker)
- }, optSignIn, context.UserAssignmentWeb(), context.OrgAssignment())
+ }, optSignIn, context.UserAssignmentWeb(), context.OrgAssignment(context.OrgAssignmentOptions{}))
// end "/{username}/-": packages, projects, code
m.Group("/{username}/{reponame}/-", func() {
@@ -1136,7 +1147,7 @@ func registerRoutes(m *web.Router) {
})
})
m.Group("/actions", func() {
- m.Get("", repo_setting.RedirectToDefaultSetting)
+ m.Get("", shared_actions.RedirectToDefaultSetting)
addSettingsRunnersRoutes()
addSettingsSecretsRoutes()
addSettingsVariablesRoutes()
@@ -1428,7 +1439,7 @@ func registerRoutes(m *web.Router) {
m.Post("/cancel", reqRepoActionsWriter, actions.Cancel)
m.Post("/approve", reqRepoActionsWriter, actions.Approve)
m.Get("/artifacts/{artifact_name}", actions.ArtifactsDownloadView)
- m.Delete("/artifacts/{artifact_name}", actions.ArtifactsDeleteView)
+ m.Delete("/artifacts/{artifact_name}", reqRepoActionsWriter, actions.ArtifactsDeleteView)
m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
})
m.Group("/workflows/{workflow_name}", func() {
@@ -1591,10 +1602,12 @@ func registerRoutes(m *web.Router) {
// end "/{username}/{reponame}": repo code
m.Group("/{username}/{reponame}", func() {
- m.Get("/stars", repo.Stars)
+ m.Get("/stars", starsEnabled, repo.Stars)
m.Get("/watchers", repo.Watchers)
m.Get("/search", reqUnitCodeReader, repo.Search)
- m.Post("/action/{action}", reqSignIn, repo.Action)
+ m.Post("/action/{action:star|unstar}", reqSignIn, starsEnabled, repo.ActionStar)
+ m.Post("/action/{action:watch|unwatch}", reqSignIn, repo.ActionWatch)
+ m.Post("/action/{action:accept_transfer|reject_transfer}", reqSignIn, repo.ActionTransfer)
}, optSignIn, context.RepoAssignment)
common.AddOwnerRepoGitLFSRoutes(m, optSignInIgnoreCsrf, lfsServerEnabled) // "/{username}/{reponame}/{lfs-paths}": git-lfs support
@@ -1624,7 +1637,7 @@ func registerRoutes(m *web.Router) {
}
m.NotFound(func(w http.ResponseWriter, req *http.Request) {
- ctx := context.GetWebContext(req)
+ ctx := context.GetWebContext(req.Context())
defer routing.RecordFuncInfo(ctx, routing.GetFuncInfo(ctx.NotFound, "WebNotFound"))()
ctx.NotFound("", nil)
})
diff --git a/services/actions/cleanup.go b/services/actions/cleanup.go
index 1223ebcab6..ee1d167713 100644
--- a/services/actions/cleanup.go
+++ b/services/actions/cleanup.go
@@ -52,9 +52,9 @@ func cleanExpiredArtifacts(taskCtx context.Context) error {
}
if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil {
log.Error("Cannot delete artifact %d: %v", artifact.ID, err)
- continue
+ // go on
}
- log.Info("Artifact %d set expired", artifact.ID)
+ log.Info("Artifact %d is deleted (due to expiration)", artifact.ID)
}
return nil
}
@@ -76,9 +76,9 @@ func cleanNeedDeleteArtifacts(taskCtx context.Context) error {
}
if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil {
log.Error("Cannot delete artifact %d: %v", artifact.ID, err)
- continue
+ // go on
}
- log.Info("Artifact %d set deleted", artifact.ID)
+ log.Info("Artifact %d is deleted (due to pending deletion)", artifact.ID)
}
if len(artifacts) < deleteArtifactBatchSize {
log.Debug("No more artifacts pending deletion")
@@ -103,8 +103,7 @@ func CleanupLogs(ctx context.Context) error {
for _, task := range tasks {
if err := actions_module.RemoveLogs(ctx, task.LogInStorage, task.LogFilename); err != nil {
log.Error("Failed to remove log %s (in storage %v) of task %v: %v", task.LogFilename, task.LogInStorage, task.ID, err)
- // do not return error here, continue to next task
- continue
+ // do not return error here, go on
}
task.LogIndexes = nil // clear log indexes since it's a heavy field
task.LogExpired = true
diff --git a/services/actions/task.go b/services/actions/task.go
new file mode 100644
index 0000000000..bc54ade347
--- /dev/null
+++ b/services/actions/task.go
@@ -0,0 +1,107 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package actions
+
+import (
+ "context"
+ "fmt"
+
+ actions_model "code.gitea.io/gitea/models/actions"
+ "code.gitea.io/gitea/models/db"
+ secret_model "code.gitea.io/gitea/models/secret"
+
+ runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
+ "google.golang.org/protobuf/types/known/structpb"
+)
+
+func PickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv1.Task, bool, error) {
+ var (
+ task *runnerv1.Task
+ job *actions_model.ActionRunJob
+ )
+
+ if err := db.WithTx(ctx, func(ctx context.Context) error {
+ t, ok, err := actions_model.CreateTaskForRunner(ctx, runner)
+ if err != nil {
+ return fmt.Errorf("CreateTaskForRunner: %w", err)
+ }
+ if !ok {
+ return nil
+ }
+
+ if err := t.LoadAttributes(ctx); err != nil {
+ return fmt.Errorf("task LoadAttributes: %w", err)
+ }
+ job = t.Job
+
+ secrets, err := secret_model.GetSecretsOfTask(ctx, t)
+ if err != nil {
+ return fmt.Errorf("GetSecretsOfTask: %w", err)
+ }
+
+ vars, err := actions_model.GetVariablesOfRun(ctx, t.Job.Run)
+ if err != nil {
+ return fmt.Errorf("GetVariablesOfRun: %w", err)
+ }
+
+ needs, err := findTaskNeeds(ctx, job)
+ if err != nil {
+ return fmt.Errorf("findTaskNeeds: %w", err)
+ }
+
+ taskContext, err := generateTaskContext(t)
+ if err != nil {
+ return fmt.Errorf("generateTaskContext: %w", err)
+ }
+
+ task = &runnerv1.Task{
+ Id: t.ID,
+ WorkflowPayload: t.Job.WorkflowPayload,
+ Context: taskContext,
+ Secrets: secrets,
+ Vars: vars,
+ Needs: needs,
+ }
+
+ return nil
+ }); err != nil {
+ return nil, false, err
+ }
+
+ if task == nil {
+ return nil, false, nil
+ }
+
+ CreateCommitStatus(ctx, job)
+
+ return task, true, nil
+}
+
+func generateTaskContext(t *actions_model.ActionTask) (*structpb.Struct, error) {
+ giteaRuntimeToken, err := CreateAuthorizationToken(t.ID, t.Job.RunID, t.JobID)
+ if err != nil {
+ return nil, err
+ }
+
+ gitCtx := GenerateGiteaContext(t.Job.Run, t.Job)
+ gitCtx["token"] = t.Token
+ gitCtx["gitea_runtime_token"] = giteaRuntimeToken
+
+ return structpb.NewStruct(gitCtx)
+}
+
+func findTaskNeeds(ctx context.Context, taskJob *actions_model.ActionRunJob) (map[string]*runnerv1.TaskNeed, error) {
+ taskNeeds, err := FindTaskNeeds(ctx, taskJob)
+ if err != nil {
+ return nil, err
+ }
+ ret := make(map[string]*runnerv1.TaskNeed, len(taskNeeds))
+ for jobID, taskNeed := range taskNeeds {
+ ret[jobID] = &runnerv1.TaskNeed{
+ Outputs: taskNeed.Outputs,
+ Result: runnerv1.Result(taskNeed.Result),
+ }
+ }
+ return ret, nil
+}
diff --git a/services/actions/variables.go b/services/actions/variables.go
index 8dde9c4af5..95f088dbd3 100644
--- a/services/actions/variables.go
+++ b/services/actions/variables.go
@@ -6,7 +6,6 @@ package actions
import (
"context"
"regexp"
- "strings"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/modules/log"
@@ -31,20 +30,18 @@ func CreateVariable(ctx context.Context, ownerID, repoID int64, name, data strin
return v, nil
}
-func UpdateVariable(ctx context.Context, variableID int64, name, data string) (bool, error) {
- if err := secret_service.ValidateName(name); err != nil {
+func UpdateVariableNameData(ctx context.Context, variable *actions_model.ActionVariable) (bool, error) {
+ if err := secret_service.ValidateName(variable.Name); err != nil {
return false, err
}
- if err := envNameCIRegexMatch(name); err != nil {
+ if err := envNameCIRegexMatch(variable.Name); err != nil {
return false, err
}
- return actions_model.UpdateVariable(ctx, &actions_model.ActionVariable{
- ID: variableID,
- Name: strings.ToUpper(name),
- Data: util.ReserveLineBreakForTextarea(data),
- })
+ variable.Data = util.ReserveLineBreakForTextarea(variable.Data)
+
+ return actions_model.UpdateVariableCols(ctx, variable, "name", "data")
}
func DeleteVariableByID(ctx context.Context, variableID int64) error {
diff --git a/services/actions/workflow.go b/services/actions/workflow.go
new file mode 100644
index 0000000000..4470b60c64
--- /dev/null
+++ b/services/actions/workflow.go
@@ -0,0 +1,281 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package actions
+
+import (
+ "fmt"
+ "net/http"
+ "net/url"
+ "path"
+ "strings"
+
+ actions_model "code.gitea.io/gitea/models/actions"
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/perm"
+ access_model "code.gitea.io/gitea/models/perm/access"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unit"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/actions"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/reqctx"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/convert"
+
+ "github.com/nektos/act/pkg/jobparser"
+ "github.com/nektos/act/pkg/model"
+)
+
+func getActionWorkflowPath(commit *git.Commit) string {
+ paths := []string{".gitea/workflows", ".github/workflows"}
+ for _, treePath := range paths {
+ if _, err := commit.SubTree(treePath); err == nil {
+ return treePath
+ }
+ }
+ return ""
+}
+
+func getActionWorkflowEntry(ctx *context.APIContext, commit *git.Commit, folder string, entry *git.TreeEntry) *api.ActionWorkflow {
+ cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions)
+ cfg := cfgUnit.ActionsConfig()
+
+ defaultBranch, _ := commit.GetBranchName()
+
+ workflowURL := fmt.Sprintf("%s/actions/workflows/%s", ctx.Repo.Repository.APIURL(), url.PathEscape(entry.Name()))
+ workflowRepoURL := fmt.Sprintf("%s/src/branch/%s/%s/%s", ctx.Repo.Repository.HTMLURL(ctx), util.PathEscapeSegments(defaultBranch), util.PathEscapeSegments(folder), url.PathEscape(entry.Name()))
+ badgeURL := fmt.Sprintf("%s/actions/workflows/%s/badge.svg?branch=%s", ctx.Repo.Repository.HTMLURL(ctx), url.PathEscape(entry.Name()), url.QueryEscape(ctx.Repo.Repository.DefaultBranch))
+
+ // See https://docs.github.com/en/rest/actions/workflows?apiVersion=2022-11-28#get-a-workflow
+ // State types:
+ // - active
+ // - deleted
+ // - disabled_fork
+ // - disabled_inactivity
+ // - disabled_manually
+ state := "active"
+ if cfg.IsWorkflowDisabled(entry.Name()) {
+ state = "disabled_manually"
+ }
+
+ // The CreatedAt and UpdatedAt fields currently reflect the timestamp of the latest commit, which can later be refined
+ // by retrieving the first and last commits for the file history. The first commit would indicate the creation date,
+ // while the last commit would represent the modification date. The DeletedAt could be determined by identifying
+ // the last commit where the file existed. However, this implementation has not been done here yet, as it would likely
+ // cause a significant performance degradation.
+ createdAt := commit.Author.When
+ updatedAt := commit.Author.When
+
+ return &api.ActionWorkflow{
+ ID: entry.Name(),
+ Name: entry.Name(),
+ Path: path.Join(folder, entry.Name()),
+ State: state,
+ CreatedAt: createdAt,
+ UpdatedAt: updatedAt,
+ URL: workflowURL,
+ HTMLURL: workflowRepoURL,
+ BadgeURL: badgeURL,
+ }
+}
+
+func EnableOrDisableWorkflow(ctx *context.APIContext, workflowID string, isEnable bool) error {
+ workflow, err := GetActionWorkflow(ctx, workflowID)
+ if err != nil {
+ return err
+ }
+
+ cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions)
+ cfg := cfgUnit.ActionsConfig()
+
+ if isEnable {
+ cfg.EnableWorkflow(workflow.ID)
+ } else {
+ cfg.DisableWorkflow(workflow.ID)
+ }
+
+ return repo_model.UpdateRepoUnit(ctx, cfgUnit)
+}
+
+func ListActionWorkflows(ctx *context.APIContext) ([]*api.ActionWorkflow, error) {
+ defaultBranchCommit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "WorkflowDefaultBranchError", err.Error())
+ return nil, err
+ }
+
+ entries, err := actions.ListWorkflows(defaultBranchCommit)
+ if err != nil {
+ ctx.Error(http.StatusNotFound, "WorkflowListNotFound", err.Error())
+ return nil, err
+ }
+
+ folder := getActionWorkflowPath(defaultBranchCommit)
+
+ workflows := make([]*api.ActionWorkflow, len(entries))
+ for i, entry := range entries {
+ workflows[i] = getActionWorkflowEntry(ctx, defaultBranchCommit, folder, entry)
+ }
+
+ return workflows, nil
+}
+
+func GetActionWorkflow(ctx *context.APIContext, workflowID string) (*api.ActionWorkflow, error) {
+ entries, err := ListActionWorkflows(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, entry := range entries {
+ if entry.Name == workflowID {
+ return entry, nil
+ }
+ }
+
+ return nil, util.NewNotExistErrorf("workflow %q not found", workflowID)
+}
+
+func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_model.Repository, gitRepo *git.Repository, workflowID, ref string, processInputs func(model *model.WorkflowDispatch, inputs map[string]any) error) error {
+ if workflowID == "" {
+ return util.ErrWrapLocale(
+ util.NewNotExistErrorf("workflowID is empty"),
+ "actions.workflow.not_found", workflowID,
+ )
+ }
+
+ if ref == "" {
+ return util.ErrWrapLocale(
+ util.NewNotExistErrorf("ref is empty"),
+ "form.target_ref_not_exist", ref,
+ )
+ }
+
+ // can not rerun job when workflow is disabled
+ cfgUnit := repo.MustGetUnit(ctx, unit.TypeActions)
+ cfg := cfgUnit.ActionsConfig()
+ if cfg.IsWorkflowDisabled(workflowID) {
+ return util.ErrWrapLocale(
+ util.NewPermissionDeniedErrorf("workflow is disabled"),
+ "actions.workflow.disabled",
+ )
+ }
+
+ // get target commit of run from specified ref
+ refName := git.RefName(ref)
+ var runTargetCommit *git.Commit
+ var err error
+ if refName.IsTag() {
+ runTargetCommit, err = gitRepo.GetTagCommit(refName.TagName())
+ } else if refName.IsBranch() {
+ runTargetCommit, err = gitRepo.GetBranchCommit(refName.BranchName())
+ } else {
+ refName = git.RefNameFromBranch(ref)
+ runTargetCommit, err = gitRepo.GetBranchCommit(ref)
+ }
+ if err != nil {
+ return util.ErrWrapLocale(
+ util.NewNotExistErrorf("ref %q doesn't exist", ref),
+ "form.target_ref_not_exist", ref,
+ )
+ }
+
+ // get workflow entry from runTargetCommit
+ entries, err := actions.ListWorkflows(runTargetCommit)
+ if err != nil {
+ return err
+ }
+
+ // find workflow from commit
+ var workflows []*jobparser.SingleWorkflow
+ for _, entry := range entries {
+ if entry.Name() != workflowID {
+ continue
+ }
+
+ content, err := actions.GetContentFromEntry(entry)
+ if err != nil {
+ return err
+ }
+ workflows, err = jobparser.Parse(content)
+ if err != nil {
+ return err
+ }
+ break
+ }
+
+ if len(workflows) == 0 {
+ return util.ErrWrapLocale(
+ util.NewNotExistErrorf("workflow %q doesn't exist", workflowID),
+ "actions.workflow.not_found", workflowID,
+ )
+ }
+
+ // get inputs from post
+ workflow := &model.Workflow{
+ RawOn: workflows[0].RawOn,
+ }
+ inputsWithDefaults := make(map[string]any)
+ if workflowDispatch := workflow.WorkflowDispatchConfig(); workflowDispatch != nil {
+ if err = processInputs(workflowDispatch, inputsWithDefaults); err != nil {
+ return err
+ }
+ }
+
+ // ctx.Req.PostForm -> WorkflowDispatchPayload.Inputs -> ActionRun.EventPayload -> runner: ghc.Event
+ // https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
+ // https://docs.github.com/en/webhooks/webhook-events-and-payloads#workflow_dispatch
+ workflowDispatchPayload := &api.WorkflowDispatchPayload{
+ Workflow: workflowID,
+ Ref: ref,
+ Repository: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeNone}),
+ Inputs: inputsWithDefaults,
+ Sender: convert.ToUserWithAccessMode(ctx, doer, perm.AccessModeNone),
+ }
+ var eventPayload []byte
+ if eventPayload, err = workflowDispatchPayload.JSONPayload(); err != nil {
+ return fmt.Errorf("JSONPayload: %w", err)
+ }
+
+ run := &actions_model.ActionRun{
+ Title: strings.SplitN(runTargetCommit.CommitMessage, "\n", 2)[0],
+ RepoID: repo.ID,
+ OwnerID: repo.OwnerID,
+ WorkflowID: workflowID,
+ TriggerUserID: doer.ID,
+ Ref: string(refName),
+ CommitSHA: runTargetCommit.ID.String(),
+ IsForkPullRequest: false,
+ Event: "workflow_dispatch",
+ TriggerEvent: "workflow_dispatch",
+ EventPayload: string(eventPayload),
+ Status: actions_model.StatusWaiting,
+ }
+
+ // cancel running jobs of the same workflow
+ if err := actions_model.CancelPreviousJobs(
+ ctx,
+ run.RepoID,
+ run.Ref,
+ run.WorkflowID,
+ run.Event,
+ ); err != nil {
+ log.Error("CancelRunningJobs: %v", err)
+ }
+
+ // Insert the action run and its associated jobs into the database
+ if err := actions_model.InsertRun(ctx, run, workflows); err != nil {
+ return fmt.Errorf("InsertRun: %w", err)
+ }
+
+ allJobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: run.ID})
+ if err != nil {
+ log.Error("FindRunJobs: %v", err)
+ }
+ CreateCommitStatus(ctx, allJobs...)
+
+ return nil
+}
diff --git a/services/auth/auth.go b/services/auth/auth.go
index 7deca9bc3d..f7deeb4c50 100644
--- a/services/auth/auth.go
+++ b/services/auth/auth.go
@@ -149,7 +149,7 @@ func handleSignIn(resp http.ResponseWriter, req *http.Request, sess SessionStore
middleware.SetLocaleCookie(resp, user.Language, 0)
// force to generate a new CSRF token
- if ctx := gitea_context.GetWebContext(req); ctx != nil {
+ if ctx := gitea_context.GetWebContext(req.Context()); ctx != nil {
ctx.Csrf.PrepareForSessionUser(ctx)
}
}
diff --git a/services/auth/sspi.go b/services/auth/sspi.go
index 3882740ae3..8cb39886c4 100644
--- a/services/auth/sspi.go
+++ b/services/auth/sspi.go
@@ -88,7 +88,7 @@ func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore,
store.GetData()["EnableSSPI"] = true
// in this case, the Verify function is called in Gitea's web context
// FIXME: it doesn't look good to render the page here, why not redirect?
- gitea_context.GetWebContext(req).HTML(http.StatusUnauthorized, tplSignIn)
+ gitea_context.GetWebContext(req.Context()).HTML(http.StatusUnauthorized, tplSignIn)
return nil, err
}
if outToken != "" {
diff --git a/services/context/api.go b/services/context/api.go
index bdeff0af63..baf4131edc 100644
--- a/services/context/api.go
+++ b/services/context/api.go
@@ -22,6 +22,9 @@ import (
)
// APIContext is a specific context for API service
+// ATTENTION: This struct should never be manually constructed in routes/services,
+// it has many internal details which should be carefully prepared by the framework.
+// If it is abused, it would cause strange bugs like panic/resource-leak.
type APIContext struct {
*Base
diff --git a/services/context/base.go b/services/context/base.go
index 5db84f42a5..4d1c3659a2 100644
--- a/services/context/base.go
+++ b/services/context/base.go
@@ -23,6 +23,10 @@ type BaseContextKeyType struct{}
var BaseContextKey BaseContextKeyType
+// Base is the base context for all web handlers
+// ATTENTION: This struct should never be manually constructed in routes/services,
+// it has many internal details which should be carefully prepared by the framework.
+// If it is abused, it would cause strange bugs like panic/resource-leak.
type Base struct {
reqctx.RequestContext
diff --git a/services/context/context.go b/services/context/context.go
index 5b16f9be98..5e08fba442 100644
--- a/services/context/context.go
+++ b/services/context/context.go
@@ -34,7 +34,10 @@ type Render interface {
HTML(w io.Writer, status int, name templates.TplName, data any, templateCtx context.Context) error
}
-// Context represents context of a request.
+// Context represents context of a web request.
+// ATTENTION: This struct should never be manually constructed in routes/services,
+// it has many internal details which should be carefully prepared by the framework.
+// If it is abused, it would cause strange bugs like panic/resource-leak.
type Context struct {
*Base
@@ -76,9 +79,9 @@ type webContextKeyType struct{}
var WebContextKey = webContextKeyType{}
-func GetWebContext(req *http.Request) *Context {
- ctx, _ := req.Context().Value(WebContextKey).(*Context)
- return ctx
+func GetWebContext(ctx context.Context) *Context {
+ webCtx, _ := ctx.Value(WebContextKey).(*Context)
+ return webCtx
}
// ValidateContext is a special context for form validation middleware. It may be different from other contexts.
@@ -132,6 +135,7 @@ func NewWebContext(base *Base, render Render, session session.Store) *Context {
}
ctx.TemplateContext = NewTemplateContextForWeb(ctx)
ctx.Flash = &middleware.Flash{DataStore: ctx, Values: url.Values{}}
+ ctx.SetContextValue(WebContextKey, ctx)
return ctx
}
@@ -162,7 +166,6 @@ func Contexter() func(next http.Handler) http.Handler {
ctx.PageData = map[string]any{}
ctx.Data["PageData"] = ctx.PageData
- ctx.Base.SetContextValue(WebContextKey, ctx)
ctx.Csrf = NewCSRFProtector(csrfOpts)
// get the last flash message from cookie
diff --git a/services/context/org.go b/services/context/org.go
index be87cef7a3..3f73165076 100644
--- a/services/context/org.go
+++ b/services/context/org.go
@@ -62,215 +62,193 @@ func GetOrganizationByParams(ctx *Context) {
}
}
-// HandleOrgAssignment handles organization assignment
-func HandleOrgAssignment(ctx *Context, args ...bool) {
- var (
- requireMember bool
- requireOwner bool
- requireTeamMember bool
- requireTeamAdmin bool
- )
- if len(args) >= 1 {
- requireMember = args[0]
- }
- if len(args) >= 2 {
- requireOwner = args[1]
- }
- if len(args) >= 3 {
- requireTeamMember = args[2]
- }
- if len(args) >= 4 {
- requireTeamAdmin = args[3]
- }
-
- var err error
+type OrgAssignmentOptions struct {
+ RequireMember bool
+ RequireOwner bool
+ RequireTeamMember bool
+ RequireTeamAdmin bool
+}
- if ctx.ContextUser == nil {
- // if Organization is not defined, get it from params
- if ctx.Org.Organization == nil {
- GetOrganizationByParams(ctx)
- if ctx.Written() {
- return
+// OrgAssignment returns a middleware to handle organization assignment
+func OrgAssignment(opts OrgAssignmentOptions) func(ctx *Context) {
+ return func(ctx *Context) {
+ var err error
+ if ctx.ContextUser == nil {
+ // if Organization is not defined, get it from params
+ if ctx.Org.Organization == nil {
+ GetOrganizationByParams(ctx)
+ if ctx.Written() {
+ return
+ }
}
+ } else if ctx.ContextUser.IsOrganization() {
+ ctx.Org.Organization = (*organization.Organization)(ctx.ContextUser)
+ } else {
+ // ContextUser is an individual User
+ return
}
- } else if ctx.ContextUser.IsOrganization() {
- if ctx.Org == nil {
- ctx.Org = &Organization{}
- }
- ctx.Org.Organization = (*organization.Organization)(ctx.ContextUser)
- } else {
- // ContextUser is an individual User
- return
- }
-
- org := ctx.Org.Organization
-
- // Handle Visibility
- if org.Visibility != structs.VisibleTypePublic && !ctx.IsSigned {
- // We must be signed in to see limited or private organizations
- ctx.NotFound("OrgAssignment", err)
- return
- }
-
- if org.Visibility == structs.VisibleTypePrivate {
- requireMember = true
- } else if ctx.IsSigned && ctx.Doer.IsRestricted {
- requireMember = true
- }
- ctx.ContextUser = org.AsUser()
- ctx.Data["Org"] = org
+ org := ctx.Org.Organization
- // Admin has super access.
- if ctx.IsSigned && ctx.Doer.IsAdmin {
- ctx.Org.IsOwner = true
- ctx.Org.IsMember = true
- ctx.Org.IsTeamMember = true
- ctx.Org.IsTeamAdmin = true
- ctx.Org.CanCreateOrgRepo = true
- } else if ctx.IsSigned {
- ctx.Org.IsOwner, err = org.IsOwnedBy(ctx, ctx.Doer.ID)
- if err != nil {
- ctx.ServerError("IsOwnedBy", err)
+ // Handle Visibility
+ if org.Visibility != structs.VisibleTypePublic && !ctx.IsSigned {
+ // We must be signed in to see limited or private organizations
+ ctx.NotFound("OrgAssignment", err)
return
}
- if ctx.Org.IsOwner {
+ if org.Visibility == structs.VisibleTypePrivate {
+ opts.RequireMember = true
+ } else if ctx.IsSigned && ctx.Doer.IsRestricted {
+ opts.RequireMember = true
+ }
+
+ ctx.ContextUser = org.AsUser()
+ ctx.Data["Org"] = org
+
+ // Admin has super access.
+ if ctx.IsSigned && ctx.Doer.IsAdmin {
+ ctx.Org.IsOwner = true
ctx.Org.IsMember = true
ctx.Org.IsTeamMember = true
ctx.Org.IsTeamAdmin = true
ctx.Org.CanCreateOrgRepo = true
- } else {
- ctx.Org.IsMember, err = org.IsOrgMember(ctx, ctx.Doer.ID)
+ } else if ctx.IsSigned {
+ ctx.Org.IsOwner, err = org.IsOwnedBy(ctx, ctx.Doer.ID)
if err != nil {
- ctx.ServerError("IsOrgMember", err)
+ ctx.ServerError("IsOwnedBy", err)
return
}
- ctx.Org.CanCreateOrgRepo, err = org.CanCreateOrgRepo(ctx, ctx.Doer.ID)
- if err != nil {
- ctx.ServerError("CanCreateOrgRepo", err)
- return
+
+ if ctx.Org.IsOwner {
+ ctx.Org.IsMember = true
+ ctx.Org.IsTeamMember = true
+ ctx.Org.IsTeamAdmin = true
+ ctx.Org.CanCreateOrgRepo = true
+ } else {
+ ctx.Org.IsMember, err = org.IsOrgMember(ctx, ctx.Doer.ID)
+ if err != nil {
+ ctx.ServerError("IsOrgMember", err)
+ return
+ }
+ ctx.Org.CanCreateOrgRepo, err = org.CanCreateOrgRepo(ctx, ctx.Doer.ID)
+ if err != nil {
+ ctx.ServerError("CanCreateOrgRepo", err)
+ return
+ }
}
+ } else {
+ // Fake data.
+ ctx.Data["SignedUser"] = &user_model.User{}
}
- } else {
- // Fake data.
- ctx.Data["SignedUser"] = &user_model.User{}
- }
- if (requireMember && !ctx.Org.IsMember) ||
- (requireOwner && !ctx.Org.IsOwner) {
- ctx.NotFound("OrgAssignment", err)
- return
- }
- ctx.Data["IsOrganizationOwner"] = ctx.Org.IsOwner
- ctx.Data["IsOrganizationMember"] = ctx.Org.IsMember
- ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled
- ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
- ctx.Data["IsPublicMember"] = func(uid int64) bool {
- is, _ := organization.IsPublicMembership(ctx, ctx.Org.Organization.ID, uid)
- return is
- }
- ctx.Data["CanCreateOrgRepo"] = ctx.Org.CanCreateOrgRepo
+ if (opts.RequireMember && !ctx.Org.IsMember) || (opts.RequireOwner && !ctx.Org.IsOwner) {
+ ctx.NotFound("OrgAssignment", err)
+ return
+ }
+ ctx.Data["IsOrganizationOwner"] = ctx.Org.IsOwner
+ ctx.Data["IsOrganizationMember"] = ctx.Org.IsMember
+ ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled
+ ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
+ ctx.Data["IsPublicMember"] = func(uid int64) bool {
+ is, _ := organization.IsPublicMembership(ctx, ctx.Org.Organization.ID, uid)
+ return is
+ }
+ ctx.Data["CanCreateOrgRepo"] = ctx.Org.CanCreateOrgRepo
- ctx.Org.OrgLink = org.AsUser().OrganisationLink()
- ctx.Data["OrgLink"] = ctx.Org.OrgLink
+ ctx.Org.OrgLink = org.AsUser().OrganisationLink()
+ ctx.Data["OrgLink"] = ctx.Org.OrgLink
- // Member
- opts := &organization.FindOrgMembersOpts{
- Doer: ctx.Doer,
- OrgID: org.ID,
- IsDoerMember: ctx.Org.IsMember,
- }
- ctx.Data["NumMembers"], err = organization.CountOrgMembers(ctx, opts)
- if err != nil {
- ctx.ServerError("CountOrgMembers", err)
- return
- }
+ // Member
+ findMembersOpts := &organization.FindOrgMembersOpts{
+ Doer: ctx.Doer,
+ OrgID: org.ID,
+ IsDoerMember: ctx.Org.IsMember,
+ }
+ ctx.Data["NumMembers"], err = organization.CountOrgMembers(ctx, findMembersOpts)
+ if err != nil {
+ ctx.ServerError("CountOrgMembers", err)
+ return
+ }
- // Team.
- if ctx.Org.IsMember {
- shouldSeeAllTeams := false
- if ctx.Org.IsOwner {
- shouldSeeAllTeams = true
- } else {
- teams, err := org.GetUserTeams(ctx, ctx.Doer.ID)
- if err != nil {
- ctx.ServerError("GetUserTeams", err)
- return
+ // Team.
+ if ctx.Org.IsMember {
+ shouldSeeAllTeams := false
+ if ctx.Org.IsOwner {
+ shouldSeeAllTeams = true
+ } else {
+ teams, err := org.GetUserTeams(ctx, ctx.Doer.ID)
+ if err != nil {
+ ctx.ServerError("GetUserTeams", err)
+ return
+ }
+ for _, team := range teams {
+ if team.IncludesAllRepositories && team.AccessMode >= perm.AccessModeAdmin {
+ shouldSeeAllTeams = true
+ break
+ }
+ }
}
- for _, team := range teams {
- if team.IncludesAllRepositories && team.AccessMode >= perm.AccessModeAdmin {
- shouldSeeAllTeams = true
- break
+ if shouldSeeAllTeams {
+ ctx.Org.Teams, err = org.LoadTeams(ctx)
+ if err != nil {
+ ctx.ServerError("LoadTeams", err)
+ return
+ }
+ } else {
+ ctx.Org.Teams, err = org.GetUserTeams(ctx, ctx.Doer.ID)
+ if err != nil {
+ ctx.ServerError("GetUserTeams", err)
+ return
}
}
+ ctx.Data["NumTeams"] = len(ctx.Org.Teams)
}
- if shouldSeeAllTeams {
- ctx.Org.Teams, err = org.LoadTeams(ctx)
- if err != nil {
- ctx.ServerError("LoadTeams", err)
- return
+
+ teamName := ctx.PathParam("team")
+ if len(teamName) > 0 {
+ teamExists := false
+ for _, team := range ctx.Org.Teams {
+ if team.LowerName == strings.ToLower(teamName) {
+ teamExists = true
+ ctx.Org.Team = team
+ ctx.Org.IsTeamMember = true
+ ctx.Data["Team"] = ctx.Org.Team
+ break
+ }
}
- } else {
- ctx.Org.Teams, err = org.GetUserTeams(ctx, ctx.Doer.ID)
- if err != nil {
- ctx.ServerError("GetUserTeams", err)
+
+ if !teamExists {
+ ctx.NotFound("OrgAssignment", err)
return
}
- }
- ctx.Data["NumTeams"] = len(ctx.Org.Teams)
- }
- teamName := ctx.PathParam("team")
- if len(teamName) > 0 {
- teamExists := false
- for _, team := range ctx.Org.Teams {
- if team.LowerName == strings.ToLower(teamName) {
- teamExists = true
- ctx.Org.Team = team
- ctx.Org.IsTeamMember = true
- ctx.Data["Team"] = ctx.Org.Team
- break
+ ctx.Data["IsTeamMember"] = ctx.Org.IsTeamMember
+ if opts.RequireTeamMember && !ctx.Org.IsTeamMember {
+ ctx.NotFound("OrgAssignment", err)
+ return
}
- }
-
- if !teamExists {
- ctx.NotFound("OrgAssignment", err)
- return
- }
-
- ctx.Data["IsTeamMember"] = ctx.Org.IsTeamMember
- if requireTeamMember && !ctx.Org.IsTeamMember {
- ctx.NotFound("OrgAssignment", err)
- return
- }
- ctx.Org.IsTeamAdmin = ctx.Org.Team.IsOwnerTeam() || ctx.Org.Team.AccessMode >= perm.AccessModeAdmin
- ctx.Data["IsTeamAdmin"] = ctx.Org.IsTeamAdmin
- if requireTeamAdmin && !ctx.Org.IsTeamAdmin {
- ctx.NotFound("OrgAssignment", err)
- return
+ ctx.Org.IsTeamAdmin = ctx.Org.Team.IsOwnerTeam() || ctx.Org.Team.AccessMode >= perm.AccessModeAdmin
+ ctx.Data["IsTeamAdmin"] = ctx.Org.IsTeamAdmin
+ if opts.RequireTeamAdmin && !ctx.Org.IsTeamAdmin {
+ ctx.NotFound("OrgAssignment", err)
+ return
+ }
}
- }
- ctx.Data["ContextUser"] = ctx.ContextUser
+ ctx.Data["ContextUser"] = ctx.ContextUser
- ctx.Data["CanReadProjects"] = ctx.Org.CanReadUnit(ctx, unit.TypeProjects)
- ctx.Data["CanReadPackages"] = ctx.Org.CanReadUnit(ctx, unit.TypePackages)
- ctx.Data["CanReadCode"] = ctx.Org.CanReadUnit(ctx, unit.TypeCode)
+ ctx.Data["CanReadProjects"] = ctx.Org.CanReadUnit(ctx, unit.TypeProjects)
+ ctx.Data["CanReadPackages"] = ctx.Org.CanReadUnit(ctx, unit.TypePackages)
+ ctx.Data["CanReadCode"] = ctx.Org.CanReadUnit(ctx, unit.TypeCode)
- ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
- if len(ctx.ContextUser.Description) != 0 {
- content, err := markdown.RenderString(markup.NewRenderContext(ctx), ctx.ContextUser.Description)
- if err != nil {
- ctx.ServerError("RenderString", err)
- return
+ ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
+ if len(ctx.ContextUser.Description) != 0 {
+ content, err := markdown.RenderString(markup.NewRenderContext(ctx), ctx.ContextUser.Description)
+ if err != nil {
+ ctx.ServerError("RenderString", err)
+ return
+ }
+ ctx.Data["RenderedDescription"] = content
}
- ctx.Data["RenderedDescription"] = content
- }
-}
-
-// OrgAssignment returns a middleware to handle organization assignment
-func OrgAssignment(args ...bool) func(ctx *Context) {
- return func(ctx *Context) {
- HandleOrgAssignment(ctx, args...)
}
}
diff --git a/services/context/package.go b/services/context/package.go
index e98e01acbb..e32ba3b481 100644
--- a/services/context/package.go
+++ b/services/context/package.go
@@ -154,9 +154,9 @@ func PackageContexter() func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
base := NewBaseContext(resp, req)
- // it is still needed when rendering 500 page in a package handler
+ // FIXME: web Context is still needed when rendering 500 page in a package handler
+ // It should be refactored to use new error handling mechanisms
ctx := NewWebContext(base, renderer, nil)
- ctx.SetContextValue(WebContextKey, ctx)
next.ServeHTTP(ctx.Resp, ctx.Req)
})
}
diff --git a/services/contexttest/context_tests.go b/services/contexttest/context_tests.go
index b0f71cad20..98b8bdd63e 100644
--- a/services/contexttest/context_tests.go
+++ b/services/contexttest/context_tests.go
@@ -67,7 +67,6 @@ func MockContext(t *testing.T, reqPath string, opts ...MockContextOption) (*cont
chiCtx := chi.NewRouteContext()
ctx := context.NewWebContext(base, opt.Render, nil)
- ctx.SetContextValue(context.WebContextKey, ctx)
ctx.SetContextValue(chi.RouteCtxKey, chiCtx)
if opt.SessionStore != nil {
ctx.SetContextValue(session.MockStoreContextKey, opt.SessionStore)
diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go
index 2c6373e03c..70019f3fa9 100644
--- a/services/forms/repo_form.go
+++ b/services/forms/repo_form.go
@@ -243,6 +243,7 @@ type WebhookForm struct {
Repository bool
Release bool
Package bool
+ Status bool
Active bool
BranchFilter string `binding:"GlobPattern"`
AuthorizationHeader string
diff --git a/services/gitdiff/git_diff_tree.go b/services/gitdiff/git_diff_tree.go
new file mode 100644
index 0000000000..8039de145d
--- /dev/null
+++ b/services/gitdiff/git_diff_tree.go
@@ -0,0 +1,249 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package gitdiff
+
+import (
+ "bufio"
+ "context"
+ "fmt"
+ "io"
+ "strconv"
+ "strings"
+
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/log"
+)
+
+type DiffTree struct {
+ Files []*DiffTreeRecord
+}
+
+type DiffTreeRecord struct {
+ // Status is one of 'added', 'deleted', 'modified', 'renamed', 'copied', 'typechanged', 'unmerged', 'unknown'
+ Status string
+
+ // For renames and copies, the percentage of similarity between the source and target of the move/rename.
+ Score uint8
+
+ HeadPath string
+ BasePath string
+ HeadMode git.EntryMode
+ BaseMode git.EntryMode
+ HeadBlobID string
+ BaseBlobID string
+}
+
+// GetDiffTree returns the list of path of the files that have changed between the two commits.
+// If useMergeBase is true, the diff will be calculated using the merge base of the two commits.
+// This is the same behavior as using a three-dot diff in git diff.
+func GetDiffTree(ctx context.Context, gitRepo *git.Repository, useMergeBase bool, baseSha, headSha string) (*DiffTree, error) {
+ gitDiffTreeRecords, err := runGitDiffTree(ctx, gitRepo, useMergeBase, baseSha, headSha)
+ if err != nil {
+ return nil, err
+ }
+
+ return &DiffTree{
+ Files: gitDiffTreeRecords,
+ }, nil
+}
+
+func runGitDiffTree(ctx context.Context, gitRepo *git.Repository, useMergeBase bool, baseSha, headSha string) ([]*DiffTreeRecord, error) {
+ useMergeBase, baseCommitID, headCommitID, err := validateGitDiffTreeArguments(gitRepo, useMergeBase, baseSha, headSha)
+ if err != nil {
+ return nil, err
+ }
+
+ cmd := git.NewCommand(ctx, "diff-tree", "--raw", "-r", "--find-renames", "--root")
+ if useMergeBase {
+ cmd.AddArguments("--merge-base")
+ }
+ cmd.AddDynamicArguments(baseCommitID, headCommitID)
+ stdout, _, runErr := cmd.RunStdString(&git.RunOpts{Dir: gitRepo.Path})
+ if runErr != nil {
+ log.Warn("git diff-tree: %v", runErr)
+ return nil, runErr
+ }
+
+ return parseGitDiffTree(strings.NewReader(stdout))
+}
+
+func validateGitDiffTreeArguments(gitRepo *git.Repository, useMergeBase bool, baseSha, headSha string) (shouldUseMergeBase bool, resolvedBaseSha, resolvedHeadSha string, err error) {
+ // if the head is empty its an error
+ if headSha == "" {
+ return false, "", "", fmt.Errorf("headSha is empty")
+ }
+
+ // if the head commit doesn't exist its and error
+ headCommit, err := gitRepo.GetCommit(headSha)
+ if err != nil {
+ return false, "", "", fmt.Errorf("failed to get commit headSha: %v", err)
+ }
+ headCommitID := headCommit.ID.String()
+
+ // if the base is empty we should use the parent of the head commit
+ if baseSha == "" {
+ // if the headCommit has no parent we should use an empty commit
+ // this can happen when we are generating a diff against an orphaned commit
+ if headCommit.ParentCount() == 0 {
+ objectFormat, err := gitRepo.GetObjectFormat()
+ if err != nil {
+ return false, "", "", err
+ }
+
+ // We set use merge base to false because we have no base commit
+ return false, objectFormat.EmptyTree().String(), headCommitID, nil
+ }
+
+ baseCommit, err := headCommit.Parent(0)
+ if err != nil {
+ return false, "", "", fmt.Errorf("baseSha is '', attempted to use parent of commit %s, got error: %v", headCommit.ID.String(), err)
+ }
+ return useMergeBase, baseCommit.ID.String(), headCommitID, nil
+ }
+
+ // try and get the base commit
+ baseCommit, err := gitRepo.GetCommit(baseSha)
+ // propagate the error if we couldn't get the base commit
+ if err != nil {
+ return useMergeBase, "", "", fmt.Errorf("failed to get base commit %s: %v", baseSha, err)
+ }
+
+ return useMergeBase, baseCommit.ID.String(), headCommit.ID.String(), nil
+}
+
+func parseGitDiffTree(gitOutput io.Reader) ([]*DiffTreeRecord, error) {
+ /*
+ The output of `git diff-tree --raw -r --find-renames` is of the form:
+
+ :<old_mode> <new_mode> <old_sha> <new_sha> <status>\t<path>
+
+ or for renames:
+
+ :<old_mode> <new_mode> <old_sha> <new_sha> <status>\t<old_path>\t<new_path>
+
+ See: <https://git-scm.com/docs/git-diff-tree#_raw_output_format> for more details
+ */
+ results := make([]*DiffTreeRecord, 0)
+
+ lines := bufio.NewScanner(gitOutput)
+ for lines.Scan() {
+ line := lines.Text()
+
+ if len(line) == 0 {
+ continue
+ }
+
+ record, err := parseGitDiffTreeLine(line)
+ if err != nil {
+ return nil, err
+ }
+
+ results = append(results, record)
+ }
+
+ if err := lines.Err(); err != nil {
+ return nil, err
+ }
+
+ return results, nil
+}
+
+func parseGitDiffTreeLine(line string) (*DiffTreeRecord, error) {
+ line = strings.TrimPrefix(line, ":")
+ splitSections := strings.SplitN(line, "\t", 2)
+ if len(splitSections) < 2 {
+ return nil, fmt.Errorf("unparsable output for diff-tree --raw: `%s`)", line)
+ }
+
+ fields := strings.Fields(splitSections[0])
+ if len(fields) < 5 {
+ return nil, fmt.Errorf("unparsable output for diff-tree --raw: `%s`, expected 5 space delimited values got %d)", line, len(fields))
+ }
+
+ baseMode, err := git.ParseEntryMode(fields[0])
+ if err != nil {
+ return nil, err
+ }
+
+ headMode, err := git.ParseEntryMode(fields[1])
+ if err != nil {
+ return nil, err
+ }
+
+ baseBlobID := fields[2]
+ headBlobID := fields[3]
+
+ status, score, err := statusFromLetter(fields[4])
+ if err != nil {
+ return nil, fmt.Errorf("unparsable output for diff-tree --raw: %s, error: %s", line, err)
+ }
+
+ filePaths := strings.Split(splitSections[1], "\t")
+
+ var headPath, basePath string
+ if status == "renamed" {
+ if len(filePaths) != 2 {
+ return nil, fmt.Errorf("unparsable output for diff-tree --raw: `%s`, expected 2 paths found %d", line, len(filePaths))
+ }
+ basePath = filePaths[0]
+ headPath = filePaths[1]
+ } else {
+ basePath = filePaths[0]
+ headPath = filePaths[0]
+ }
+
+ return &DiffTreeRecord{
+ Status: status,
+ Score: score,
+ BaseMode: baseMode,
+ HeadMode: headMode,
+ BaseBlobID: baseBlobID,
+ HeadBlobID: headBlobID,
+ BasePath: basePath,
+ HeadPath: headPath,
+ }, nil
+}
+
+func statusFromLetter(rawStatus string) (status string, score uint8, err error) {
+ if len(rawStatus) < 1 {
+ return "", 0, fmt.Errorf("empty status letter")
+ }
+ switch rawStatus[0] {
+ case 'A':
+ return "added", 0, nil
+ case 'D':
+ return "deleted", 0, nil
+ case 'M':
+ return "modified", 0, nil
+ case 'R':
+ score, err = tryParseStatusScore(rawStatus)
+ return "renamed", score, err
+ case 'C':
+ score, err = tryParseStatusScore(rawStatus)
+ return "copied", score, err
+ case 'T':
+ return "typechanged", 0, nil
+ case 'U':
+ return "unmerged", 0, nil
+ case 'X':
+ return "unknown", 0, nil
+ default:
+ return "", 0, fmt.Errorf("unknown status letter: '%s'", rawStatus)
+ }
+}
+
+func tryParseStatusScore(rawStatus string) (uint8, error) {
+ if len(rawStatus) < 2 {
+ return 0, fmt.Errorf("status score missing")
+ }
+
+ score, err := strconv.ParseUint(rawStatus[1:], 10, 8)
+ if err != nil {
+ return 0, fmt.Errorf("failed to parse status score: %w", err)
+ } else if score > 100 {
+ return 0, fmt.Errorf("status score out of range: %d", score)
+ }
+
+ return uint8(score), nil
+}
diff --git a/services/gitdiff/git_diff_tree_test.go b/services/gitdiff/git_diff_tree_test.go
new file mode 100644
index 0000000000..313d279e95
--- /dev/null
+++ b/services/gitdiff/git_diff_tree_test.go
@@ -0,0 +1,427 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package gitdiff
+
+import (
+ "strings"
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/modules/git"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestGitDiffTree(t *testing.T) {
+ test := []struct {
+ Name string
+ RepoPath string
+ BaseSha string
+ HeadSha string
+ useMergeBase bool
+ Expected *DiffTree
+ }{
+ {
+ Name: "happy path",
+ RepoPath: "../../modules/git/tests/repos/repo5_pulls",
+ BaseSha: "72866af952e98d02a73003501836074b286a78f6",
+ HeadSha: "d8e0bbb45f200e67d9a784ce55bd90821af45ebd",
+ Expected: &DiffTree{
+ Files: []*DiffTreeRecord{
+ {
+ Status: "modified",
+ HeadPath: "LICENSE",
+ BasePath: "LICENSE",
+ HeadMode: git.EntryModeBlob,
+ BaseMode: git.EntryModeBlob,
+ HeadBlobID: "ee469963e76ae1bb7ee83d7510df2864e6c8c640",
+ BaseBlobID: "c996f4725be8fc8c1d1c776e58c97ddc5d03b336",
+ },
+ {
+ Status: "modified",
+ HeadPath: "README.md",
+ BasePath: "README.md",
+ HeadMode: git.EntryModeBlob,
+ BaseMode: git.EntryModeBlob,
+ HeadBlobID: "9dfc0a6257d8eff526f0cfaf6a8ea950f55a9dba",
+ BaseBlobID: "074e590b8e64898b02beef03ece83f962c94f54c",
+ },
+ },
+ },
+ },
+ {
+ Name: "first commit (no parent)",
+ RepoPath: "../../modules/git/tests/repos/repo5_pulls",
+ HeadSha: "72866af952e98d02a73003501836074b286a78f6",
+ Expected: &DiffTree{
+ Files: []*DiffTreeRecord{
+ {
+ Status: "added",
+ HeadPath: ".gitignore",
+ BasePath: ".gitignore",
+ HeadMode: git.EntryModeBlob,
+ BaseMode: git.EntryModeNoEntry,
+ HeadBlobID: "f1c181ec9c5c921245027c6b452ecfc1d3626364",
+ BaseBlobID: "0000000000000000000000000000000000000000",
+ },
+ {
+ Status: "added",
+ HeadPath: "LICENSE",
+ BasePath: "LICENSE",
+ HeadMode: git.EntryModeBlob,
+ BaseMode: git.EntryModeNoEntry,
+ HeadBlobID: "c996f4725be8fc8c1d1c776e58c97ddc5d03b336",
+ BaseBlobID: "0000000000000000000000000000000000000000",
+ },
+ {
+ Status: "added",
+ HeadPath: "README.md",
+ BasePath: "README.md",
+ HeadMode: git.EntryModeBlob,
+ BaseMode: git.EntryModeNoEntry,
+ HeadBlobID: "074e590b8e64898b02beef03ece83f962c94f54c",
+ BaseBlobID: "0000000000000000000000000000000000000000",
+ },
+ },
+ },
+ },
+ {
+ Name: "first commit (no parent), merge base = true",
+ RepoPath: "../../modules/git/tests/repos/repo5_pulls",
+ HeadSha: "72866af952e98d02a73003501836074b286a78f6",
+ useMergeBase: true,
+ Expected: &DiffTree{
+ Files: []*DiffTreeRecord{
+ {
+ Status: "added",
+ HeadPath: ".gitignore",
+ BasePath: ".gitignore",
+ HeadMode: git.EntryModeBlob,
+ BaseMode: git.EntryModeNoEntry,
+ HeadBlobID: "f1c181ec9c5c921245027c6b452ecfc1d3626364",
+ BaseBlobID: "0000000000000000000000000000000000000000",
+ },
+ {
+ Status: "added",
+ HeadPath: "LICENSE",
+ BasePath: "LICENSE",
+ HeadMode: git.EntryModeBlob,
+ BaseMode: git.EntryModeNoEntry,
+ HeadBlobID: "c996f4725be8fc8c1d1c776e58c97ddc5d03b336",
+ BaseBlobID: "0000000000000000000000000000000000000000",
+ },
+ {
+ Status: "added",
+ HeadPath: "README.md",
+ BasePath: "README.md",
+ HeadMode: git.EntryModeBlob,
+ BaseMode: git.EntryModeNoEntry,
+ HeadBlobID: "074e590b8e64898b02beef03ece83f962c94f54c",
+ BaseBlobID: "0000000000000000000000000000000000000000",
+ },
+ },
+ },
+ },
+ {
+ Name: "base and head same",
+ RepoPath: "../../modules/git/tests/repos/repo5_pulls",
+ BaseSha: "ed8f4d2fa5b2420706580d191f5dd50c4e491f3f",
+ HeadSha: "ed8f4d2fa5b2420706580d191f5dd50c4e491f3f",
+ Expected: &DiffTree{
+ Files: []*DiffTreeRecord{},
+ },
+ },
+ {
+ Name: "useMergeBase false",
+ RepoPath: "../../modules/git/tests/repos/repo5_pulls",
+ BaseSha: "ed8f4d2fa5b2420706580d191f5dd50c4e491f3f",
+ HeadSha: "111cac04bd7d20301964e27a93698aabb5781b80", // this commit can be found on the update-readme branch
+ useMergeBase: false,
+ Expected: &DiffTree{
+ Files: []*DiffTreeRecord{
+ {
+ Status: "modified",
+ HeadPath: "LICENSE",
+ BasePath: "LICENSE",
+ HeadMode: git.EntryModeBlob,
+ BaseMode: git.EntryModeBlob,
+ HeadBlobID: "c996f4725be8fc8c1d1c776e58c97ddc5d03b336",
+ BaseBlobID: "ed5119b3c1f45547b6785bc03eac7f87570fa17f",
+ },
+
+ {
+ Status: "modified",
+ HeadPath: "README.md",
+ BasePath: "README.md",
+ HeadMode: git.EntryModeBlob,
+ BaseMode: git.EntryModeBlob,
+ HeadBlobID: "fb39771a8865c9a67f2ab9b616c854805664553c",
+ BaseBlobID: "9dfc0a6257d8eff526f0cfaf6a8ea950f55a9dba",
+ },
+ },
+ },
+ },
+ {
+ Name: "useMergeBase true",
+ RepoPath: "../../modules/git/tests/repos/repo5_pulls",
+ BaseSha: "ed8f4d2fa5b2420706580d191f5dd50c4e491f3f",
+ HeadSha: "111cac04bd7d20301964e27a93698aabb5781b80", // this commit can be found on the update-readme branch
+ useMergeBase: true,
+ Expected: &DiffTree{
+ Files: []*DiffTreeRecord{
+ {
+ Status: "modified",
+ HeadPath: "README.md",
+ BasePath: "README.md",
+ HeadMode: git.EntryModeBlob,
+ BaseMode: git.EntryModeBlob,
+ HeadBlobID: "fb39771a8865c9a67f2ab9b616c854805664553c",
+ BaseBlobID: "9dfc0a6257d8eff526f0cfaf6a8ea950f55a9dba",
+ },
+ },
+ },
+ },
+ {
+ Name: "no base set",
+ RepoPath: "../../modules/git/tests/repos/repo5_pulls",
+ HeadSha: "d8e0bbb45f200e67d9a784ce55bd90821af45ebd", // this commit can be found on the update-readme branch
+ useMergeBase: false,
+ Expected: &DiffTree{
+ Files: []*DiffTreeRecord{
+ {
+ Status: "modified",
+ HeadPath: "LICENSE",
+ BasePath: "LICENSE",
+ HeadMode: git.EntryModeBlob,
+ BaseMode: git.EntryModeBlob,
+ HeadBlobID: "ee469963e76ae1bb7ee83d7510df2864e6c8c640",
+ BaseBlobID: "ed5119b3c1f45547b6785bc03eac7f87570fa17f",
+ },
+ },
+ },
+ },
+ }
+
+ for _, tt := range test {
+ t.Run(tt.Name, func(t *testing.T) {
+ gitRepo, err := git.OpenRepository(git.DefaultContext, tt.RepoPath)
+ assert.NoError(t, err)
+ defer gitRepo.Close()
+
+ diffPaths, err := GetDiffTree(db.DefaultContext, gitRepo, tt.useMergeBase, tt.BaseSha, tt.HeadSha)
+ require.NoError(t, err)
+
+ assert.Equal(t, tt.Expected, diffPaths)
+ })
+ }
+}
+
+func TestParseGitDiffTree(t *testing.T) {
+ test := []struct {
+ Name string
+ GitOutput string
+ Expected []*DiffTreeRecord
+ }{
+ {
+ Name: "file change",
+ GitOutput: ":100644 100644 64e43d23bcd08db12563a0a4d84309cadb437e1a 5dbc7792b5bb228647cfcc8dfe65fc649119dedc M\tResources/views/curriculum/edit.blade.php",
+ Expected: []*DiffTreeRecord{
+ {
+ Status: "modified",
+ HeadPath: "Resources/views/curriculum/edit.blade.php",
+ BasePath: "Resources/views/curriculum/edit.blade.php",
+ HeadMode: git.EntryModeBlob,
+ BaseMode: git.EntryModeBlob,
+ HeadBlobID: "5dbc7792b5bb228647cfcc8dfe65fc649119dedc",
+ BaseBlobID: "64e43d23bcd08db12563a0a4d84309cadb437e1a",
+ },
+ },
+ },
+ {
+ Name: "file added",
+ GitOutput: ":000000 100644 0000000000000000000000000000000000000000 0063162fb403db15ceb0517b34ab782e4e58b619 A\tResources/views/class/index.blade.php",
+ Expected: []*DiffTreeRecord{
+ {
+ Status: "added",
+ HeadPath: "Resources/views/class/index.blade.php",
+ BasePath: "Resources/views/class/index.blade.php",
+ HeadMode: git.EntryModeBlob,
+ BaseMode: git.EntryModeNoEntry,
+ HeadBlobID: "0063162fb403db15ceb0517b34ab782e4e58b619",
+ BaseBlobID: "0000000000000000000000000000000000000000",
+ },
+ },
+ },
+ {
+ Name: "file deleted",
+ GitOutput: ":100644 000000 bac4286303c8c0017ea2f0a48c561ddcc0330a14 0000000000000000000000000000000000000000 D\tResources/views/classes/index.blade.php",
+ Expected: []*DiffTreeRecord{
+ {
+ Status: "deleted",
+ HeadPath: "Resources/views/classes/index.blade.php",
+ BasePath: "Resources/views/classes/index.blade.php",
+ HeadMode: git.EntryModeNoEntry,
+ BaseMode: git.EntryModeBlob,
+ HeadBlobID: "0000000000000000000000000000000000000000",
+ BaseBlobID: "bac4286303c8c0017ea2f0a48c561ddcc0330a14",
+ },
+ },
+ },
+ {
+ Name: "file renamed",
+ GitOutput: ":100644 100644 c8a055cfb45cd39747292983ad1797ceab40f5b1 97248f79a90aaf81fe7fd74b33c1cb182dd41783 R087\tDatabase/Seeders/AdminDatabaseSeeder.php\tDatabase/Seeders/AcademicDatabaseSeeder.php",
+ Expected: []*DiffTreeRecord{
+ {
+ Status: "renamed",
+ Score: 87,
+ HeadPath: "Database/Seeders/AcademicDatabaseSeeder.php",
+ BasePath: "Database/Seeders/AdminDatabaseSeeder.php",
+ HeadMode: git.EntryModeBlob,
+ BaseMode: git.EntryModeBlob,
+ HeadBlobID: "97248f79a90aaf81fe7fd74b33c1cb182dd41783",
+ BaseBlobID: "c8a055cfb45cd39747292983ad1797ceab40f5b1",
+ },
+ },
+ },
+ {
+ Name: "no changes",
+ GitOutput: ``,
+ Expected: []*DiffTreeRecord{},
+ },
+ {
+ Name: "multiple changes",
+ GitOutput: ":000000 100644 0000000000000000000000000000000000000000 db736b44533a840981f1f17b7029d0f612b69550 A\tHttp/Controllers/ClassController.php\n" +
+ ":100644 000000 9a4d2344d4d0145db7c91b3f3e123c74367d4ef4 0000000000000000000000000000000000000000 D\tHttp/Controllers/ClassesController.php\n" +
+ ":100644 100644 f060d6aede65d423f49e7dc248dfa0d8835ef920 b82c8e39a3602dedadb44669956d6eb5b6a7cc86 M\tHttp/Controllers/ProgramDirectorController.php\n",
+ Expected: []*DiffTreeRecord{
+ {
+ Status: "added",
+ HeadPath: "Http/Controllers/ClassController.php",
+ BasePath: "Http/Controllers/ClassController.php",
+ HeadMode: git.EntryModeBlob,
+ BaseMode: git.EntryModeNoEntry,
+ HeadBlobID: "db736b44533a840981f1f17b7029d0f612b69550",
+ BaseBlobID: "0000000000000000000000000000000000000000",
+ },
+ {
+ Status: "deleted",
+ HeadPath: "Http/Controllers/ClassesController.php",
+ BasePath: "Http/Controllers/ClassesController.php",
+ HeadMode: git.EntryModeNoEntry,
+ BaseMode: git.EntryModeBlob,
+ HeadBlobID: "0000000000000000000000000000000000000000",
+ BaseBlobID: "9a4d2344d4d0145db7c91b3f3e123c74367d4ef4",
+ },
+ {
+ Status: "modified",
+ HeadPath: "Http/Controllers/ProgramDirectorController.php",
+ BasePath: "Http/Controllers/ProgramDirectorController.php",
+ HeadMode: git.EntryModeBlob,
+ BaseMode: git.EntryModeBlob,
+ HeadBlobID: "b82c8e39a3602dedadb44669956d6eb5b6a7cc86",
+ BaseBlobID: "f060d6aede65d423f49e7dc248dfa0d8835ef920",
+ },
+ },
+ },
+ {
+ Name: "spaces in file path",
+ GitOutput: ":000000 100644 0000000000000000000000000000000000000000 db736b44533a840981f1f17b7029d0f612b69550 A\tHttp /Controllers/Class Controller.php\n" +
+ ":100644 000000 9a4d2344d4d0145db7c91b3f3e123c74367d4ef4 0000000000000000000000000000000000000000 D\tHttp/Cont rollers/Classes Controller.php\n" +
+ ":100644 100644 f060d6aede65d423f49e7dc248dfa0d8835ef920 b82c8e39a3602dedadb44669956d6eb5b6a7cc86 R010\tHttp/Controllers/Program Director Controller.php\tHttp/Cont rollers/ProgramDirectorController.php\n",
+ Expected: []*DiffTreeRecord{
+ {
+ Status: "added",
+ HeadPath: "Http /Controllers/Class Controller.php",
+ BasePath: "Http /Controllers/Class Controller.php",
+ HeadMode: git.EntryModeBlob,
+ BaseMode: git.EntryModeNoEntry,
+ HeadBlobID: "db736b44533a840981f1f17b7029d0f612b69550",
+ BaseBlobID: "0000000000000000000000000000000000000000",
+ },
+ {
+ Status: "deleted",
+ HeadPath: "Http/Cont rollers/Classes Controller.php",
+ BasePath: "Http/Cont rollers/Classes Controller.php",
+ HeadMode: git.EntryModeNoEntry,
+ BaseMode: git.EntryModeBlob,
+ HeadBlobID: "0000000000000000000000000000000000000000",
+ BaseBlobID: "9a4d2344d4d0145db7c91b3f3e123c74367d4ef4",
+ },
+ {
+ Status: "renamed",
+ Score: 10,
+ HeadPath: "Http/Cont rollers/ProgramDirectorController.php",
+ BasePath: "Http/Controllers/Program Director Controller.php",
+ HeadMode: git.EntryModeBlob,
+ BaseMode: git.EntryModeBlob,
+ HeadBlobID: "b82c8e39a3602dedadb44669956d6eb5b6a7cc86",
+ BaseBlobID: "f060d6aede65d423f49e7dc248dfa0d8835ef920",
+ },
+ },
+ },
+ {
+ Name: "file type changed",
+ GitOutput: ":100644 120000 344e0ca8aa791cc4164fb0ea645f334fd40d00f0 a7c2973de00bfdc6ca51d315f401b5199fe01dc3 T\twebpack.mix.js",
+ Expected: []*DiffTreeRecord{
+ {
+ Status: "typechanged",
+ HeadPath: "webpack.mix.js",
+ BasePath: "webpack.mix.js",
+ HeadMode: git.EntryModeSymlink,
+ BaseMode: git.EntryModeBlob,
+ HeadBlobID: "a7c2973de00bfdc6ca51d315f401b5199fe01dc3",
+ BaseBlobID: "344e0ca8aa791cc4164fb0ea645f334fd40d00f0",
+ },
+ },
+ },
+ }
+
+ for _, tt := range test {
+ t.Run(tt.Name, func(t *testing.T) {
+ entries, err := parseGitDiffTree(strings.NewReader(tt.GitOutput))
+ assert.NoError(t, err)
+ assert.Equal(t, tt.Expected, entries)
+ })
+ }
+}
+
+func TestGitDiffTreeErrors(t *testing.T) {
+ test := []struct {
+ Name string
+ RepoPath string
+ BaseSha string
+ HeadSha string
+ }{
+ {
+ Name: "head doesn't exist",
+ RepoPath: "../../modules/git/tests/repos/repo5_pulls",
+ BaseSha: "f32b0a9dfd09a60f616f29158f772cedd89942d2",
+ HeadSha: "asdfasdfasdf",
+ },
+ {
+ Name: "base doesn't exist",
+ RepoPath: "../../modules/git/tests/repos/repo5_pulls",
+ BaseSha: "asdfasdfasdf",
+ HeadSha: "f32b0a9dfd09a60f616f29158f772cedd89942d2",
+ },
+ {
+ Name: "head not set",
+ RepoPath: "../../modules/git/tests/repos/repo5_pulls",
+ BaseSha: "f32b0a9dfd09a60f616f29158f772cedd89942d2",
+ },
+ }
+
+ for _, tt := range test {
+ t.Run(tt.Name, func(t *testing.T) {
+ gitRepo, err := git.OpenRepository(git.DefaultContext, tt.RepoPath)
+ assert.NoError(t, err)
+ defer gitRepo.Close()
+
+ diffPaths, err := GetDiffTree(db.DefaultContext, gitRepo, true, tt.BaseSha, tt.HeadSha)
+ assert.Error(t, err)
+ assert.Nil(t, diffPaths)
+ })
+ }
+}
diff --git a/services/gitdiff/gitdiff_test.go b/services/gitdiff/gitdiff_test.go
index 1017d188dd..ca9b5a6f4e 100644
--- a/services/gitdiff/gitdiff_test.go
+++ b/services/gitdiff/gitdiff_test.go
@@ -5,6 +5,7 @@
package gitdiff
import (
+ "context"
"strconv"
"strings"
"testing"
@@ -628,23 +629,25 @@ func TestDiffLine_GetCommentSide(t *testing.T) {
}
func TestGetDiffRangeWithWhitespaceBehavior(t *testing.T) {
- gitRepo, err := git.OpenRepository(git.DefaultContext, "./testdata/academic-module")
+ gitRepo, err := git.OpenRepository(context.Background(), "../../modules/git/tests/repos/repo5_pulls")
require.NoError(t, err)
defer gitRepo.Close()
for _, behavior := range []git.TrustedCmdArgs{{"-w"}, {"--ignore-space-at-eol"}, {"-b"}, nil} {
- diffs, err := GetDiff(db.DefaultContext, gitRepo,
+ diffs, err := GetDiff(context.Background(), gitRepo,
&DiffOptions{
- AfterCommitID: "bd7063cc7c04689c4d082183d32a604ed27a24f9",
- BeforeCommitID: "559c156f8e0178b71cb44355428f24001b08fc68",
+ AfterCommitID: "d8e0bbb45f200e67d9a784ce55bd90821af45ebd",
+ BeforeCommitID: "72866af952e98d02a73003501836074b286a78f6",
MaxLines: setting.Git.MaxGitDiffLines,
MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters,
- MaxFiles: setting.Git.MaxGitDiffFiles,
+ MaxFiles: 1,
WhitespaceBehavior: behavior,
})
- assert.NoError(t, err, "Error when diff with %s", behavior)
+ require.NoError(t, err, "Error when diff with WhitespaceBehavior=%s", behavior)
+ assert.True(t, diffs.IsIncomplete)
+ assert.Len(t, diffs.Files, 1)
for _, f := range diffs.Files {
- assert.NotEmpty(t, f.Sections, "%s should have sections", f.Name)
+ assert.NotEmpty(t, f.Sections, "Diff file %q should have sections", f.Name)
}
}
}
diff --git a/services/gitdiff/testdata/academic-module/HEAD b/services/gitdiff/testdata/academic-module/HEAD
deleted file mode 100644
index cb089cd89a..0000000000
--- a/services/gitdiff/testdata/academic-module/HEAD
+++ /dev/null
@@ -1 +0,0 @@
-ref: refs/heads/master
diff --git a/services/gitdiff/testdata/academic-module/config b/services/gitdiff/testdata/academic-module/config
deleted file mode 100644
index 1bc26be514..0000000000
--- a/services/gitdiff/testdata/academic-module/config
+++ /dev/null
@@ -1,10 +0,0 @@
-[core]
- repositoryformatversion = 0
- filemode = true
- bare = false
- logallrefupdates = true
- ignorecase = true
- precomposeunicode = true
-[branch "master"]
- remote = origin
- merge = refs/heads/master
diff --git a/services/gitdiff/testdata/academic-module/index b/services/gitdiff/testdata/academic-module/index
deleted file mode 100644
index e712c906e3..0000000000
--- a/services/gitdiff/testdata/academic-module/index
+++ /dev/null
Binary files differ
diff --git a/services/gitdiff/testdata/academic-module/logs/HEAD b/services/gitdiff/testdata/academic-module/logs/HEAD
deleted file mode 100644
index 16b2e1c0f6..0000000000
--- a/services/gitdiff/testdata/academic-module/logs/HEAD
+++ /dev/null
@@ -1 +0,0 @@
-0000000000000000000000000000000000000000 bd7063cc7c04689c4d082183d32a604ed27a24f9 Lunny Xiao <xiaolunwen@gmail.com> 1574829684 +0800 clone: from https://try.gitea.io/shemgp-aiias/academic-module
diff --git a/services/gitdiff/testdata/academic-module/logs/refs/heads/master b/services/gitdiff/testdata/academic-module/logs/refs/heads/master
deleted file mode 100644
index 16b2e1c0f6..0000000000
--- a/services/gitdiff/testdata/academic-module/logs/refs/heads/master
+++ /dev/null
@@ -1 +0,0 @@
-0000000000000000000000000000000000000000 bd7063cc7c04689c4d082183d32a604ed27a24f9 Lunny Xiao <xiaolunwen@gmail.com> 1574829684 +0800 clone: from https://try.gitea.io/shemgp-aiias/academic-module
diff --git a/services/gitdiff/testdata/academic-module/logs/refs/remotes/origin/HEAD b/services/gitdiff/testdata/academic-module/logs/refs/remotes/origin/HEAD
deleted file mode 100644
index 16b2e1c0f6..0000000000
--- a/services/gitdiff/testdata/academic-module/logs/refs/remotes/origin/HEAD
+++ /dev/null
@@ -1 +0,0 @@
-0000000000000000000000000000000000000000 bd7063cc7c04689c4d082183d32a604ed27a24f9 Lunny Xiao <xiaolunwen@gmail.com> 1574829684 +0800 clone: from https://try.gitea.io/shemgp-aiias/academic-module
diff --git a/services/gitdiff/testdata/academic-module/objects/pack/pack-597efbc3613c7ba790e33b178fd9fc1fe17b4245.idx b/services/gitdiff/testdata/academic-module/objects/pack/pack-597efbc3613c7ba790e33b178fd9fc1fe17b4245.idx
deleted file mode 100644
index 4d759aa504..0000000000
--- a/services/gitdiff/testdata/academic-module/objects/pack/pack-597efbc3613c7ba790e33b178fd9fc1fe17b4245.idx
+++ /dev/null
Binary files differ
diff --git a/services/gitdiff/testdata/academic-module/objects/pack/pack-597efbc3613c7ba790e33b178fd9fc1fe17b4245.pack b/services/gitdiff/testdata/academic-module/objects/pack/pack-597efbc3613c7ba790e33b178fd9fc1fe17b4245.pack
deleted file mode 100644
index 2dc49cfded..0000000000
--- a/services/gitdiff/testdata/academic-module/objects/pack/pack-597efbc3613c7ba790e33b178fd9fc1fe17b4245.pack
+++ /dev/null
Binary files differ
diff --git a/services/gitdiff/testdata/academic-module/packed-refs b/services/gitdiff/testdata/academic-module/packed-refs
deleted file mode 100644
index 13b5611650..0000000000
--- a/services/gitdiff/testdata/academic-module/packed-refs
+++ /dev/null
@@ -1,2 +0,0 @@
-# pack-refs with: peeled fully-peeled sorted
-bd7063cc7c04689c4d082183d32a604ed27a24f9 refs/remotes/origin/master
diff --git a/services/gitdiff/testdata/academic-module/refs/heads/master b/services/gitdiff/testdata/academic-module/refs/heads/master
deleted file mode 100644
index bd2b56eaf4..0000000000
--- a/services/gitdiff/testdata/academic-module/refs/heads/master
+++ /dev/null
@@ -1 +0,0 @@
-bd7063cc7c04689c4d082183d32a604ed27a24f9
diff --git a/services/gitdiff/testdata/academic-module/refs/remotes/origin/HEAD b/services/gitdiff/testdata/academic-module/refs/remotes/origin/HEAD
deleted file mode 100644
index 6efe28fff8..0000000000
--- a/services/gitdiff/testdata/academic-module/refs/remotes/origin/HEAD
+++ /dev/null
@@ -1 +0,0 @@
-ref: refs/remotes/origin/master
diff --git a/services/issue/suggestion.go b/services/issue/suggestion.go
new file mode 100644
index 0000000000..22eddb1904
--- /dev/null
+++ b/services/issue/suggestion.go
@@ -0,0 +1,73 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package issue
+
+import (
+ "context"
+ "strconv"
+
+ issues_model "code.gitea.io/gitea/models/issues"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/modules/optional"
+ "code.gitea.io/gitea/modules/structs"
+)
+
+func GetSuggestion(ctx context.Context, repo *repo_model.Repository, isPull optional.Option[bool], keyword string) ([]*structs.Issue, error) {
+ var issues issues_model.IssueList
+ var err error
+ pageSize := 5
+ if keyword == "" {
+ issues, err = issues_model.FindLatestUpdatedIssues(ctx, repo.ID, isPull, pageSize)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ indexKeyword, _ := strconv.ParseInt(keyword, 10, 64)
+ var issueByIndex *issues_model.Issue
+ var excludedID int64
+ if indexKeyword > 0 {
+ issueByIndex, err = issues_model.GetIssueByIndex(ctx, repo.ID, indexKeyword)
+ if err != nil && !issues_model.IsErrIssueNotExist(err) {
+ return nil, err
+ }
+ if issueByIndex != nil {
+ excludedID = issueByIndex.ID
+ pageSize--
+ }
+ }
+
+ issues, err = issues_model.FindIssuesSuggestionByKeyword(ctx, repo.ID, keyword, isPull, excludedID, pageSize)
+ if err != nil {
+ return nil, err
+ }
+
+ if issueByIndex != nil {
+ issues = append([]*issues_model.Issue{issueByIndex}, issues...)
+ }
+ }
+
+ if err := issues.LoadPullRequests(ctx); err != nil {
+ return nil, err
+ }
+
+ suggestions := make([]*structs.Issue, 0, len(issues))
+ for _, issue := range issues {
+ suggestion := &structs.Issue{
+ ID: issue.ID,
+ Index: issue.Index,
+ Title: issue.Title,
+ State: issue.State(),
+ }
+
+ if issue.IsPull && issue.PullRequest != nil {
+ suggestion.PullRequest = &structs.PullRequestMeta{
+ HasMerged: issue.PullRequest.HasMerged,
+ IsWorkInProgress: issue.PullRequest.IsWorkInProgress(ctx),
+ }
+ }
+ suggestions = append(suggestions, suggestion)
+ }
+
+ return suggestions, nil
+}
diff --git a/services/issue/suggestion_test.go b/services/issue/suggestion_test.go
new file mode 100644
index 0000000000..84cfd520ac
--- /dev/null
+++ b/services/issue/suggestion_test.go
@@ -0,0 +1,57 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package issue
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/optional"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func Test_Suggestion(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+
+ testCases := []struct {
+ keyword string
+ isPull optional.Option[bool]
+ expectedIndexes []int64
+ }{
+ {
+ keyword: "",
+ expectedIndexes: []int64{5, 1, 4, 2, 3},
+ },
+ {
+ keyword: "1",
+ expectedIndexes: []int64{1},
+ },
+ {
+ keyword: "issue",
+ expectedIndexes: []int64{4, 1, 2, 3},
+ },
+ {
+ keyword: "pull",
+ expectedIndexes: []int64{5},
+ },
+ }
+
+ for _, testCase := range testCases {
+ t.Run(testCase.keyword, func(t *testing.T) {
+ issues, err := GetSuggestion(db.DefaultContext, repo1, testCase.isPull, testCase.keyword)
+ assert.NoError(t, err)
+
+ issueIndexes := make([]int64, 0, len(issues))
+ for _, issue := range issues {
+ issueIndexes = append(issueIndexes, issue.Index)
+ }
+ assert.EqualValues(t, testCase.expectedIndexes, issueIndexes)
+ })
+ }
+}
diff --git a/services/lfs/server.go b/services/lfs/server.go
index a77623fdc1..c4866edaab 100644
--- a/services/lfs/server.go
+++ b/services/lfs/server.go
@@ -134,7 +134,9 @@ func DownloadHandler(ctx *context.Context) {
}
contentLength := toByte + 1 - fromByte
- ctx.Resp.Header().Set("Content-Length", strconv.FormatInt(contentLength, 10))
+ contentLengthStr := strconv.FormatInt(contentLength, 10)
+ ctx.Resp.Header().Set("Content-Length", contentLengthStr)
+ ctx.Resp.Header().Set("X-Gitea-LFS-Content-Length", contentLengthStr) // we need this header to make sure it won't be affected by reverse proxy or compression
ctx.Resp.Header().Set("Content-Type", "application/octet-stream")
filename := ctx.PathParam("filename")
diff --git a/services/mailer/mail_test.go b/services/mailer/mail_test.go
index 36cef486c9..8298ac4a34 100644
--- a/services/mailer/mail_test.go
+++ b/services/mailer/mail_test.go
@@ -85,7 +85,7 @@ func TestComposeIssueCommentMessage(t *testing.T) {
recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}, {Name: "Test2", Email: "test2@gitea.com"}}
msgs, err := composeIssueCommentMessages(&mailCommentContext{
- Context: context.TODO(), // TODO: use a correct context
+ Context: context.TODO(),
Issue: issue, Doer: doer, ActionType: activities_model.ActionCommentIssue,
Content: fmt.Sprintf("test @%s %s#%d body", doer.Name, issue.Repo.FullName(), issue.Index),
Comment: comment,
@@ -131,7 +131,7 @@ func TestComposeIssueMessage(t *testing.T) {
recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}, {Name: "Test2", Email: "test2@gitea.com"}}
msgs, err := composeIssueCommentMessages(&mailCommentContext{
- Context: context.TODO(), // TODO: use a correct context
+ Context: context.TODO(),
Issue: issue, Doer: doer, ActionType: activities_model.ActionCreateIssue,
Content: "test body",
}, "en-US", recipients, false, "issue create")
@@ -178,14 +178,14 @@ func TestTemplateSelection(t *testing.T) {
}
msg := testComposeIssueCommentMessage(t, &mailCommentContext{
- Context: context.TODO(), // TODO: use a correct context
+ Context: context.TODO(),
Issue: issue, Doer: doer, ActionType: activities_model.ActionCreateIssue,
Content: "test body",
}, recipients, false, "TestTemplateSelection")
expect(t, msg, "issue/new/subject", "issue/new/body")
msg = testComposeIssueCommentMessage(t, &mailCommentContext{
- Context: context.TODO(), // TODO: use a correct context
+ Context: context.TODO(),
Issue: issue, Doer: doer, ActionType: activities_model.ActionCommentIssue,
Content: "test body", Comment: comment,
}, recipients, false, "TestTemplateSelection")
@@ -194,14 +194,14 @@ func TestTemplateSelection(t *testing.T) {
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2, Repo: repo, Poster: doer})
comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 4, Issue: pull})
msg = testComposeIssueCommentMessage(t, &mailCommentContext{
- Context: context.TODO(), // TODO: use a correct context
+ Context: context.TODO(),
Issue: pull, Doer: doer, ActionType: activities_model.ActionCommentPull,
Content: "test body", Comment: comment,
}, recipients, false, "TestTemplateSelection")
expect(t, msg, "pull/comment/subject", "pull/comment/body")
msg = testComposeIssueCommentMessage(t, &mailCommentContext{
- Context: context.TODO(), // TODO: use a correct context
+ Context: context.TODO(),
Issue: issue, Doer: doer, ActionType: activities_model.ActionCloseIssue,
Content: "test body", Comment: comment,
}, recipients, false, "TestTemplateSelection")
@@ -220,7 +220,7 @@ func TestTemplateServices(t *testing.T) {
recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}}
msg := testComposeIssueCommentMessage(t, &mailCommentContext{
- Context: context.TODO(), // TODO: use a correct context
+ Context: context.TODO(),
Issue: issue, Doer: doer, ActionType: actionType,
Content: "test body", Comment: comment,
}, recipients, fromMention, "TestTemplateServices")
@@ -263,7 +263,7 @@ func testComposeIssueCommentMessage(t *testing.T, ctx *mailCommentContext, recip
func TestGenerateAdditionalHeaders(t *testing.T) {
doer, _, issue, _ := prepareMailerTest(t)
- ctx := &mailCommentContext{Context: context.TODO() /* TODO: use a correct context */, Issue: issue, Doer: doer}
+ ctx := &mailCommentContext{Context: context.TODO(), Issue: issue, Doer: doer}
recipient := &user_model.User{Name: "test", Email: "test@gitea.com"}
headers := generateAdditionalHeaders(ctx, "dummy-reason", recipient)
diff --git a/services/markup/renderhelper.go b/services/markup/renderhelper.go
index 4b9852b48b..ea494146a7 100644
--- a/services/markup/renderhelper.go
+++ b/services/markup/renderhelper.go
@@ -21,8 +21,8 @@ func FormalRenderHelperFuncs() *markup.RenderHelperFuncs {
return false
}
- giteaCtx, ok := ctx.(*gitea_context.Context)
- if !ok {
+ giteaCtx := gitea_context.GetWebContext(ctx)
+ if giteaCtx == nil {
// when using general context, use user's visibility to check
return mentionedUser.Visibility.IsPublic()
}
diff --git a/services/markup/renderhelper_codepreview.go b/services/markup/renderhelper_codepreview.go
index 170c70c409..d638af7ff0 100644
--- a/services/markup/renderhelper_codepreview.go
+++ b/services/markup/renderhelper_codepreview.go
@@ -36,8 +36,8 @@ func renderRepoFileCodePreview(ctx context.Context, opts markup.RenderCodePrevie
return "", err
}
- webCtx, ok := ctx.Value(gitea_context.WebContextKey).(*gitea_context.Context)
- if !ok {
+ webCtx := gitea_context.GetWebContext(ctx)
+ if webCtx == nil {
return "", fmt.Errorf("context is not a web context")
}
doer := webCtx.Doer
diff --git a/services/markup/renderhelper_issueicontitle.go b/services/markup/renderhelper_issueicontitle.go
index 53a508e908..fd8f9d43fa 100644
--- a/services/markup/renderhelper_issueicontitle.go
+++ b/services/markup/renderhelper_issueicontitle.go
@@ -18,8 +18,8 @@ import (
)
func renderRepoIssueIconTitle(ctx context.Context, opts markup.RenderIssueIconTitleOptions) (_ template.HTML, err error) {
- webCtx, ok := ctx.Value(gitea_context.WebContextKey).(*gitea_context.Context)
- if !ok {
+ webCtx := gitea_context.GetWebContext(ctx)
+ if webCtx == nil {
return "", fmt.Errorf("context is not a web context")
}
diff --git a/services/migrations/codebase.go b/services/migrations/codebase.go
index 492fc908e9..880dd21497 100644
--- a/services/migrations/codebase.go
+++ b/services/migrations/codebase.go
@@ -66,7 +66,6 @@ type codebaseUser struct {
// from Codebase
type CodebaseDownloader struct {
base.NullDownloader
- ctx context.Context
client *http.Client
baseURL *url.URL
projectURL *url.URL
@@ -77,17 +76,11 @@ type CodebaseDownloader struct {
commitMap map[string]string
}
-// SetContext set context
-func (d *CodebaseDownloader) SetContext(ctx context.Context) {
- d.ctx = ctx
-}
-
// NewCodebaseDownloader creates a new downloader
-func NewCodebaseDownloader(ctx context.Context, projectURL *url.URL, project, repoName, username, password string) *CodebaseDownloader {
+func NewCodebaseDownloader(_ context.Context, projectURL *url.URL, project, repoName, username, password string) *CodebaseDownloader {
baseURL, _ := url.Parse("https://api3.codebasehq.com")
downloader := &CodebaseDownloader{
- ctx: ctx,
baseURL: baseURL,
projectURL: projectURL,
project: project,
@@ -127,7 +120,7 @@ func (d *CodebaseDownloader) FormatCloneURL(opts base.MigrateOptions, remoteAddr
return opts.CloneAddr, nil
}
-func (d *CodebaseDownloader) callAPI(endpoint string, parameter map[string]string, result any) error {
+func (d *CodebaseDownloader) callAPI(ctx context.Context, endpoint string, parameter map[string]string, result any) error {
u, err := d.baseURL.Parse(endpoint)
if err != nil {
return err
@@ -141,7 +134,7 @@ func (d *CodebaseDownloader) callAPI(endpoint string, parameter map[string]strin
u.RawQuery = query.Encode()
}
- req, err := http.NewRequestWithContext(d.ctx, "GET", u.String(), nil)
+ req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil)
if err != nil {
return err
}
@@ -158,7 +151,7 @@ func (d *CodebaseDownloader) callAPI(endpoint string, parameter map[string]strin
// GetRepoInfo returns repository information
// https://support.codebasehq.com/kb/projects
-func (d *CodebaseDownloader) GetRepoInfo() (*base.Repository, error) {
+func (d *CodebaseDownloader) GetRepoInfo(ctx context.Context) (*base.Repository, error) {
var rawRepository struct {
XMLName xml.Name `xml:"repository"`
Name string `xml:"name"`
@@ -169,6 +162,7 @@ func (d *CodebaseDownloader) GetRepoInfo() (*base.Repository, error) {
}
err := d.callAPI(
+ ctx,
fmt.Sprintf("/%s/%s", d.project, d.repoName),
nil,
&rawRepository,
@@ -187,7 +181,7 @@ func (d *CodebaseDownloader) GetRepoInfo() (*base.Repository, error) {
// GetMilestones returns milestones
// https://support.codebasehq.com/kb/tickets-and-milestones/milestones
-func (d *CodebaseDownloader) GetMilestones() ([]*base.Milestone, error) {
+func (d *CodebaseDownloader) GetMilestones(ctx context.Context) ([]*base.Milestone, error) {
var rawMilestones struct {
XMLName xml.Name `xml:"ticketing-milestone"`
Type string `xml:"type,attr"`
@@ -209,6 +203,7 @@ func (d *CodebaseDownloader) GetMilestones() ([]*base.Milestone, error) {
}
err := d.callAPI(
+ ctx,
fmt.Sprintf("/%s/milestones", d.project),
nil,
&rawMilestones,
@@ -245,7 +240,7 @@ func (d *CodebaseDownloader) GetMilestones() ([]*base.Milestone, error) {
// GetLabels returns labels
// https://support.codebasehq.com/kb/tickets-and-milestones/statuses-priorities-and-categories
-func (d *CodebaseDownloader) GetLabels() ([]*base.Label, error) {
+func (d *CodebaseDownloader) GetLabels(ctx context.Context) ([]*base.Label, error) {
var rawTypes struct {
XMLName xml.Name `xml:"ticketing-types"`
Type string `xml:"type,attr"`
@@ -259,6 +254,7 @@ func (d *CodebaseDownloader) GetLabels() ([]*base.Label, error) {
}
err := d.callAPI(
+ ctx,
fmt.Sprintf("/%s/tickets/types", d.project),
nil,
&rawTypes,
@@ -284,7 +280,7 @@ type codebaseIssueContext struct {
// GetIssues returns issues, limits are not supported
// https://support.codebasehq.com/kb/tickets-and-milestones
// https://support.codebasehq.com/kb/tickets-and-milestones/updating-tickets
-func (d *CodebaseDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, error) {
+func (d *CodebaseDownloader) GetIssues(ctx context.Context, _, _ int) ([]*base.Issue, bool, error) {
var rawIssues struct {
XMLName xml.Name `xml:"tickets"`
Type string `xml:"type,attr"`
@@ -324,6 +320,7 @@ func (d *CodebaseDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool,
}
err := d.callAPI(
+ ctx,
fmt.Sprintf("/%s/tickets", d.project),
nil,
&rawIssues,
@@ -358,6 +355,7 @@ func (d *CodebaseDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool,
} `xml:"ticket-note"`
}
err := d.callAPI(
+ ctx,
fmt.Sprintf("/%s/tickets/%d/notes", d.project, issue.TicketID.Value),
nil,
&notes,
@@ -370,7 +368,7 @@ func (d *CodebaseDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool,
if len(note.Content) == 0 {
continue
}
- poster := d.tryGetUser(note.UserID.Value)
+ poster := d.tryGetUser(ctx, note.UserID.Value)
comments = append(comments, &base.Comment{
IssueIndex: issue.TicketID.Value,
Index: note.ID.Value,
@@ -390,7 +388,7 @@ func (d *CodebaseDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool,
if issue.Status.TreatAsClosed.Value {
state = "closed"
}
- poster := d.tryGetUser(issue.ReporterID.Value)
+ poster := d.tryGetUser(ctx, issue.ReporterID.Value)
issues = append(issues, &base.Issue{
Title: issue.Summary,
Number: issue.TicketID.Value,
@@ -419,7 +417,7 @@ func (d *CodebaseDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool,
}
// GetComments returns comments
-func (d *CodebaseDownloader) GetComments(commentable base.Commentable) ([]*base.Comment, bool, error) {
+func (d *CodebaseDownloader) GetComments(_ context.Context, commentable base.Commentable) ([]*base.Comment, bool, error) {
context, ok := commentable.GetContext().(codebaseIssueContext)
if !ok {
return nil, false, fmt.Errorf("unexpected context: %+v", commentable.GetContext())
@@ -430,7 +428,7 @@ func (d *CodebaseDownloader) GetComments(commentable base.Commentable) ([]*base.
// GetPullRequests returns pull requests
// https://support.codebasehq.com/kb/repositories/merge-requests
-func (d *CodebaseDownloader) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) {
+func (d *CodebaseDownloader) GetPullRequests(ctx context.Context, page, perPage int) ([]*base.PullRequest, bool, error) {
var rawMergeRequests struct {
XMLName xml.Name `xml:"merge-requests"`
Type string `xml:"type,attr"`
@@ -443,6 +441,7 @@ func (d *CodebaseDownloader) GetPullRequests(page, perPage int) ([]*base.PullReq
}
err := d.callAPI(
+ ctx,
fmt.Sprintf("/%s/%s/merge_requests", d.project, d.repoName),
map[string]string{
"query": `"Target Project" is "` + d.repoName + `"`,
@@ -503,6 +502,7 @@ func (d *CodebaseDownloader) GetPullRequests(page, perPage int) ([]*base.PullReq
} `xml:"comments"`
}
err := d.callAPI(
+ ctx,
fmt.Sprintf("/%s/%s/merge_requests/%d", d.project, d.repoName, mr.ID.Value),
nil,
&rawMergeRequest,
@@ -531,7 +531,7 @@ func (d *CodebaseDownloader) GetPullRequests(page, perPage int) ([]*base.PullReq
}
continue
}
- poster := d.tryGetUser(comment.UserID.Value)
+ poster := d.tryGetUser(ctx, comment.UserID.Value)
comments = append(comments, &base.Comment{
IssueIndex: number,
Index: comment.ID.Value,
@@ -547,7 +547,7 @@ func (d *CodebaseDownloader) GetPullRequests(page, perPage int) ([]*base.PullReq
comments = append(comments, &base.Comment{})
}
- poster := d.tryGetUser(rawMergeRequest.UserID.Value)
+ poster := d.tryGetUser(ctx, rawMergeRequest.UserID.Value)
pullRequests = append(pullRequests, &base.PullRequest{
Title: rawMergeRequest.Subject,
@@ -563,12 +563,12 @@ func (d *CodebaseDownloader) GetPullRequests(page, perPage int) ([]*base.PullReq
MergedTime: mergedTime,
Head: base.PullRequestBranch{
Ref: rawMergeRequest.SourceRef,
- SHA: d.getHeadCommit(rawMergeRequest.SourceRef),
+ SHA: d.getHeadCommit(ctx, rawMergeRequest.SourceRef),
RepoName: d.repoName,
},
Base: base.PullRequestBranch{
Ref: rawMergeRequest.TargetRef,
- SHA: d.getHeadCommit(rawMergeRequest.TargetRef),
+ SHA: d.getHeadCommit(ctx, rawMergeRequest.TargetRef),
RepoName: d.repoName,
},
ForeignIndex: rawMergeRequest.ID.Value,
@@ -584,7 +584,7 @@ func (d *CodebaseDownloader) GetPullRequests(page, perPage int) ([]*base.PullReq
return pullRequests, true, nil
}
-func (d *CodebaseDownloader) tryGetUser(userID int64) *codebaseUser {
+func (d *CodebaseDownloader) tryGetUser(ctx context.Context, userID int64) *codebaseUser {
if len(d.userMap) == 0 {
var rawUsers struct {
XMLName xml.Name `xml:"users"`
@@ -602,6 +602,7 @@ func (d *CodebaseDownloader) tryGetUser(userID int64) *codebaseUser {
}
err := d.callAPI(
+ ctx,
"/users",
nil,
&rawUsers,
@@ -627,7 +628,7 @@ func (d *CodebaseDownloader) tryGetUser(userID int64) *codebaseUser {
return user
}
-func (d *CodebaseDownloader) getHeadCommit(ref string) string {
+func (d *CodebaseDownloader) getHeadCommit(ctx context.Context, ref string) string {
commitRef, ok := d.commitMap[ref]
if !ok {
var rawCommits struct {
@@ -638,6 +639,7 @@ func (d *CodebaseDownloader) getHeadCommit(ref string) string {
} `xml:"commit"`
}
err := d.callAPI(
+ ctx,
fmt.Sprintf("/%s/%s/commits/%s", d.project, d.repoName, ref),
nil,
&rawCommits,
diff --git a/services/migrations/codebase_test.go b/services/migrations/codebase_test.go
index 68721e0641..ec4da1bff5 100644
--- a/services/migrations/codebase_test.go
+++ b/services/migrations/codebase_test.go
@@ -30,9 +30,9 @@ func TestCodebaseDownloadRepo(t *testing.T) {
if cloneUser != "" {
u.User = url.UserPassword(cloneUser, clonePassword)
}
-
+ ctx := context.Background()
factory := &CodebaseDownloaderFactory{}
- downloader, err := factory.New(context.Background(), base.MigrateOptions{
+ downloader, err := factory.New(ctx, base.MigrateOptions{
CloneAddr: u.String(),
AuthUsername: apiUser,
AuthPassword: apiPassword,
@@ -40,7 +40,7 @@ func TestCodebaseDownloadRepo(t *testing.T) {
if err != nil {
t.Fatalf("Error creating Codebase downloader: %v", err)
}
- repo, err := downloader.GetRepoInfo()
+ repo, err := downloader.GetRepoInfo(ctx)
assert.NoError(t, err)
assertRepositoryEqual(t, &base.Repository{
Name: "test",
@@ -50,7 +50,7 @@ func TestCodebaseDownloadRepo(t *testing.T) {
OriginalURL: cloneAddr,
}, repo)
- milestones, err := downloader.GetMilestones()
+ milestones, err := downloader.GetMilestones(ctx)
assert.NoError(t, err)
assertMilestonesEqual(t, []*base.Milestone{
{
@@ -65,11 +65,11 @@ func TestCodebaseDownloadRepo(t *testing.T) {
},
}, milestones)
- labels, err := downloader.GetLabels()
+ labels, err := downloader.GetLabels(ctx)
assert.NoError(t, err)
assert.Len(t, labels, 4)
- issues, isEnd, err := downloader.GetIssues(1, 2)
+ issues, isEnd, err := downloader.GetIssues(ctx, 1, 2)
assert.NoError(t, err)
assert.True(t, isEnd)
assertIssuesEqual(t, []*base.Issue{
@@ -106,7 +106,7 @@ func TestCodebaseDownloadRepo(t *testing.T) {
},
}, issues)
- comments, _, err := downloader.GetComments(issues[0])
+ comments, _, err := downloader.GetComments(ctx, issues[0])
assert.NoError(t, err)
assertCommentsEqual(t, []*base.Comment{
{
@@ -119,7 +119,7 @@ func TestCodebaseDownloadRepo(t *testing.T) {
},
}, comments)
- prs, _, err := downloader.GetPullRequests(1, 1)
+ prs, _, err := downloader.GetPullRequests(ctx, 1, 1)
assert.NoError(t, err)
assertPullRequestsEqual(t, []*base.PullRequest{
{
@@ -144,7 +144,7 @@ func TestCodebaseDownloadRepo(t *testing.T) {
},
}, prs)
- rvs, err := downloader.GetReviews(prs[0])
+ rvs, err := downloader.GetReviews(ctx, prs[0])
assert.NoError(t, err)
assert.Empty(t, rvs)
}
diff --git a/services/migrations/codecommit.go b/services/migrations/codecommit.go
index fead527f5b..c45f9e5943 100644
--- a/services/migrations/codecommit.go
+++ b/services/migrations/codecommit.go
@@ -62,9 +62,8 @@ func (c *CodeCommitDownloaderFactory) GitServiceType() structs.GitServiceType {
return structs.CodeCommitService
}
-func NewCodeCommitDownloader(ctx context.Context, repoName, baseURL, accessKeyID, secretAccessKey, region string) *CodeCommitDownloader {
+func NewCodeCommitDownloader(_ context.Context, repoName, baseURL, accessKeyID, secretAccessKey, region string) *CodeCommitDownloader {
downloader := CodeCommitDownloader{
- ctx: ctx,
repoName: repoName,
baseURL: baseURL,
codeCommitClient: codecommit.New(codecommit.Options{
@@ -79,21 +78,15 @@ func NewCodeCommitDownloader(ctx context.Context, repoName, baseURL, accessKeyID
// CodeCommitDownloader implements a downloader for AWS CodeCommit
type CodeCommitDownloader struct {
base.NullDownloader
- ctx context.Context
codeCommitClient *codecommit.Client
repoName string
baseURL string
allPullRequestIDs []string
}
-// SetContext set context
-func (c *CodeCommitDownloader) SetContext(ctx context.Context) {
- c.ctx = ctx
-}
-
// GetRepoInfo returns a repository information
-func (c *CodeCommitDownloader) GetRepoInfo() (*base.Repository, error) {
- output, err := c.codeCommitClient.GetRepository(c.ctx, &codecommit.GetRepositoryInput{
+func (c *CodeCommitDownloader) GetRepoInfo(ctx context.Context) (*base.Repository, error) {
+ output, err := c.codeCommitClient.GetRepository(ctx, &codecommit.GetRepositoryInput{
RepositoryName: util.ToPointer(c.repoName),
})
if err != nil {
@@ -117,14 +110,14 @@ func (c *CodeCommitDownloader) GetRepoInfo() (*base.Repository, error) {
}
// GetComments returns comments of an issue or PR
-func (c *CodeCommitDownloader) GetComments(commentable base.Commentable) ([]*base.Comment, bool, error) {
+func (c *CodeCommitDownloader) GetComments(ctx context.Context, commentable base.Commentable) ([]*base.Comment, bool, error) {
var (
nextToken *string
comments []*base.Comment
)
for {
- resp, err := c.codeCommitClient.GetCommentsForPullRequest(c.ctx, &codecommit.GetCommentsForPullRequestInput{
+ resp, err := c.codeCommitClient.GetCommentsForPullRequest(ctx, &codecommit.GetCommentsForPullRequestInput{
NextToken: nextToken,
PullRequestId: util.ToPointer(strconv.FormatInt(commentable.GetForeignIndex(), 10)),
})
@@ -155,8 +148,8 @@ func (c *CodeCommitDownloader) GetComments(commentable base.Commentable) ([]*bas
}
// GetPullRequests returns pull requests according page and perPage
-func (c *CodeCommitDownloader) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) {
- allPullRequestIDs, err := c.getAllPullRequestIDs()
+func (c *CodeCommitDownloader) GetPullRequests(ctx context.Context, page, perPage int) ([]*base.PullRequest, bool, error) {
+ allPullRequestIDs, err := c.getAllPullRequestIDs(ctx)
if err != nil {
return nil, false, err
}
@@ -170,7 +163,7 @@ func (c *CodeCommitDownloader) GetPullRequests(page, perPage int) ([]*base.PullR
prs := make([]*base.PullRequest, 0, len(batch))
for _, id := range batch {
- output, err := c.codeCommitClient.GetPullRequest(c.ctx, &codecommit.GetPullRequestInput{
+ output, err := c.codeCommitClient.GetPullRequest(ctx, &codecommit.GetPullRequestInput{
PullRequestId: util.ToPointer(id),
})
if err != nil {
@@ -231,7 +224,7 @@ func (c *CodeCommitDownloader) FormatCloneURL(opts MigrateOptions, remoteAddr st
return u.String(), nil
}
-func (c *CodeCommitDownloader) getAllPullRequestIDs() ([]string, error) {
+func (c *CodeCommitDownloader) getAllPullRequestIDs(ctx context.Context) ([]string, error) {
if len(c.allPullRequestIDs) > 0 {
return c.allPullRequestIDs, nil
}
@@ -242,7 +235,7 @@ func (c *CodeCommitDownloader) getAllPullRequestIDs() ([]string, error) {
)
for {
- output, err := c.codeCommitClient.ListPullRequests(c.ctx, &codecommit.ListPullRequestsInput{
+ output, err := c.codeCommitClient.ListPullRequests(ctx, &codecommit.ListPullRequestsInput{
RepositoryName: util.ToPointer(c.repoName),
NextToken: nextToken,
})
diff --git a/services/migrations/dump.go b/services/migrations/dump.go
index 07812002af..11efc18163 100644
--- a/services/migrations/dump.go
+++ b/services/migrations/dump.go
@@ -32,7 +32,6 @@ var _ base.Uploader = &RepositoryDumper{}
// RepositoryDumper implements an Uploader to the local directory
type RepositoryDumper struct {
- ctx context.Context
baseDir string
repoOwner string
repoName string
@@ -56,7 +55,6 @@ func NewRepositoryDumper(ctx context.Context, baseDir, repoOwner, repoName strin
return nil, err
}
return &RepositoryDumper{
- ctx: ctx,
opts: opts,
baseDir: baseDir,
repoOwner: repoOwner,
@@ -105,7 +103,7 @@ func (g *RepositoryDumper) setURLToken(remoteAddr string) (string, error) {
}
// CreateRepo creates a repository
-func (g *RepositoryDumper) CreateRepo(repo *base.Repository, opts base.MigrateOptions) error {
+func (g *RepositoryDumper) CreateRepo(ctx context.Context, repo *base.Repository, opts base.MigrateOptions) error {
f, err := os.Create(filepath.Join(g.baseDir, "repo.yml"))
if err != nil {
return err
@@ -149,7 +147,7 @@ func (g *RepositoryDumper) CreateRepo(repo *base.Repository, opts base.MigrateOp
return err
}
- err = git.Clone(g.ctx, remoteAddr, repoPath, git.CloneRepoOptions{
+ err = git.Clone(ctx, remoteAddr, repoPath, git.CloneRepoOptions{
Mirror: true,
Quiet: true,
Timeout: migrateTimeout,
@@ -158,19 +156,19 @@ func (g *RepositoryDumper) CreateRepo(repo *base.Repository, opts base.MigrateOp
if err != nil {
return fmt.Errorf("Clone: %w", err)
}
- if err := git.WriteCommitGraph(g.ctx, repoPath); err != nil {
+ if err := git.WriteCommitGraph(ctx, repoPath); err != nil {
return err
}
if opts.Wiki {
wikiPath := g.wikiPath()
- wikiRemotePath := repository.WikiRemoteURL(g.ctx, remoteAddr)
+ wikiRemotePath := repository.WikiRemoteURL(ctx, remoteAddr)
if len(wikiRemotePath) > 0 {
if err := os.MkdirAll(wikiPath, os.ModePerm); err != nil {
return fmt.Errorf("Failed to remove %s: %w", wikiPath, err)
}
- if err := git.Clone(g.ctx, wikiRemotePath, wikiPath, git.CloneRepoOptions{
+ if err := git.Clone(ctx, wikiRemotePath, wikiPath, git.CloneRepoOptions{
Mirror: true,
Quiet: true,
Timeout: migrateTimeout,
@@ -181,13 +179,13 @@ func (g *RepositoryDumper) CreateRepo(repo *base.Repository, opts base.MigrateOp
if err := os.RemoveAll(wikiPath); err != nil {
return fmt.Errorf("Failed to remove %s: %w", wikiPath, err)
}
- } else if err := git.WriteCommitGraph(g.ctx, wikiPath); err != nil {
+ } else if err := git.WriteCommitGraph(ctx, wikiPath); err != nil {
return err
}
}
}
- g.gitRepo, err = git.OpenRepository(g.ctx, g.gitPath())
+ g.gitRepo, err = git.OpenRepository(ctx, g.gitPath())
return err
}
@@ -220,7 +218,7 @@ func (g *RepositoryDumper) Close() {
}
// CreateTopics creates topics
-func (g *RepositoryDumper) CreateTopics(topics ...string) error {
+func (g *RepositoryDumper) CreateTopics(_ context.Context, topics ...string) error {
f, err := os.Create(filepath.Join(g.baseDir, "topic.yml"))
if err != nil {
return err
@@ -242,7 +240,7 @@ func (g *RepositoryDumper) CreateTopics(topics ...string) error {
}
// CreateMilestones creates milestones
-func (g *RepositoryDumper) CreateMilestones(milestones ...*base.Milestone) error {
+func (g *RepositoryDumper) CreateMilestones(_ context.Context, milestones ...*base.Milestone) error {
var err error
if g.milestoneFile == nil {
g.milestoneFile, err = os.Create(filepath.Join(g.baseDir, "milestone.yml"))
@@ -264,7 +262,7 @@ func (g *RepositoryDumper) CreateMilestones(milestones ...*base.Milestone) error
}
// CreateLabels creates labels
-func (g *RepositoryDumper) CreateLabels(labels ...*base.Label) error {
+func (g *RepositoryDumper) CreateLabels(_ context.Context, labels ...*base.Label) error {
var err error
if g.labelFile == nil {
g.labelFile, err = os.Create(filepath.Join(g.baseDir, "label.yml"))
@@ -286,7 +284,7 @@ func (g *RepositoryDumper) CreateLabels(labels ...*base.Label) error {
}
// CreateReleases creates releases
-func (g *RepositoryDumper) CreateReleases(releases ...*base.Release) error {
+func (g *RepositoryDumper) CreateReleases(_ context.Context, releases ...*base.Release) error {
if g.opts.ReleaseAssets {
for _, release := range releases {
attachDir := filepath.Join("release_assets", release.TagName)
@@ -354,12 +352,12 @@ func (g *RepositoryDumper) CreateReleases(releases ...*base.Release) error {
}
// SyncTags syncs releases with tags in the database
-func (g *RepositoryDumper) SyncTags() error {
+func (g *RepositoryDumper) SyncTags(ctx context.Context) error {
return nil
}
// CreateIssues creates issues
-func (g *RepositoryDumper) CreateIssues(issues ...*base.Issue) error {
+func (g *RepositoryDumper) CreateIssues(_ context.Context, issues ...*base.Issue) error {
var err error
if g.issueFile == nil {
g.issueFile, err = os.Create(filepath.Join(g.baseDir, "issue.yml"))
@@ -412,7 +410,7 @@ func (g *RepositoryDumper) encodeItems(number int64, items []any, dir string, it
}
// CreateComments creates comments of issues
-func (g *RepositoryDumper) CreateComments(comments ...*base.Comment) error {
+func (g *RepositoryDumper) CreateComments(_ context.Context, comments ...*base.Comment) error {
commentsMap := make(map[int64][]any, len(comments))
for _, comment := range comments {
commentsMap[comment.IssueIndex] = append(commentsMap[comment.IssueIndex], comment)
@@ -421,7 +419,7 @@ func (g *RepositoryDumper) CreateComments(comments ...*base.Comment) error {
return g.createItems(g.commentDir(), g.commentFiles, commentsMap)
}
-func (g *RepositoryDumper) handlePullRequest(pr *base.PullRequest) error {
+func (g *RepositoryDumper) handlePullRequest(ctx context.Context, pr *base.PullRequest) error {
// SECURITY: this pr must have been ensured safe
if !pr.EnsuredSafe {
log.Error("PR #%d in %s/%s has not been checked for safety ... We will ignore this.", pr.Number, g.repoOwner, g.repoName)
@@ -490,7 +488,7 @@ func (g *RepositoryDumper) handlePullRequest(pr *base.PullRequest) error {
if pr.Head.CloneURL == "" || pr.Head.Ref == "" {
// Set head information if pr.Head.SHA is available
if pr.Head.SHA != "" {
- _, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.gitPath()})
+ _, _, err = git.NewCommand(ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.gitPath()})
if err != nil {
log.Error("PR #%d in %s/%s unable to update-ref for pr HEAD: %v", pr.Number, g.repoOwner, g.repoName, err)
}
@@ -520,7 +518,7 @@ func (g *RepositoryDumper) handlePullRequest(pr *base.PullRequest) error {
if !ok {
// Set head information if pr.Head.SHA is available
if pr.Head.SHA != "" {
- _, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.gitPath()})
+ _, _, err = git.NewCommand(ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.gitPath()})
if err != nil {
log.Error("PR #%d in %s/%s unable to update-ref for pr HEAD: %v", pr.Number, g.repoOwner, g.repoName, err)
}
@@ -555,7 +553,7 @@ func (g *RepositoryDumper) handlePullRequest(pr *base.PullRequest) error {
fetchArg = git.BranchPrefix + fetchArg
}
- _, _, err = git.NewCommand(g.ctx, "fetch", "--no-tags").AddDashesAndList(remote, fetchArg).RunStdString(&git.RunOpts{Dir: g.gitPath()})
+ _, _, err = git.NewCommand(ctx, "fetch", "--no-tags").AddDashesAndList(remote, fetchArg).RunStdString(&git.RunOpts{Dir: g.gitPath()})
if err != nil {
log.Error("Fetch branch from %s failed: %v", pr.Head.CloneURL, err)
// We need to continue here so that the Head.Ref is reset and we attempt to set the gitref for the PR
@@ -579,7 +577,7 @@ func (g *RepositoryDumper) handlePullRequest(pr *base.PullRequest) error {
pr.Head.SHA = headSha
}
if pr.Head.SHA != "" {
- _, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.gitPath()})
+ _, _, err = git.NewCommand(ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.gitPath()})
if err != nil {
log.Error("unable to set %s as the local head for PR #%d from %s in %s/%s. Error: %v", pr.Head.SHA, pr.Number, pr.Head.Ref, g.repoOwner, g.repoName, err)
}
@@ -589,7 +587,7 @@ func (g *RepositoryDumper) handlePullRequest(pr *base.PullRequest) error {
}
// CreatePullRequests creates pull requests
-func (g *RepositoryDumper) CreatePullRequests(prs ...*base.PullRequest) error {
+func (g *RepositoryDumper) CreatePullRequests(ctx context.Context, prs ...*base.PullRequest) error {
var err error
if g.pullrequestFile == nil {
if err := os.MkdirAll(g.baseDir, os.ModePerm); err != nil {
@@ -607,7 +605,7 @@ func (g *RepositoryDumper) CreatePullRequests(prs ...*base.PullRequest) error {
count := 0
for i := 0; i < len(prs); i++ {
pr := prs[i]
- if err := g.handlePullRequest(pr); err != nil {
+ if err := g.handlePullRequest(ctx, pr); err != nil {
log.Error("PR #%d in %s/%s failed - skipping", pr.Number, g.repoOwner, g.repoName, err)
continue
}
@@ -620,7 +618,7 @@ func (g *RepositoryDumper) CreatePullRequests(prs ...*base.PullRequest) error {
}
// CreateReviews create pull request reviews
-func (g *RepositoryDumper) CreateReviews(reviews ...*base.Review) error {
+func (g *RepositoryDumper) CreateReviews(_ context.Context, reviews ...*base.Review) error {
reviewsMap := make(map[int64][]any, len(reviews))
for _, review := range reviews {
reviewsMap[review.IssueIndex] = append(reviewsMap[review.IssueIndex], review)
@@ -636,7 +634,7 @@ func (g *RepositoryDumper) Rollback() error {
}
// Finish when migrating succeed, this will update something.
-func (g *RepositoryDumper) Finish() error {
+func (g *RepositoryDumper) Finish(_ context.Context) error {
return nil
}
diff --git a/services/migrations/git.go b/services/migrations/git.go
index 22ffd5e765..1ed99499a1 100644
--- a/services/migrations/git.go
+++ b/services/migrations/git.go
@@ -28,12 +28,8 @@ func NewPlainGitDownloader(ownerName, repoName, remoteURL string) *PlainGitDownl
}
}
-// SetContext set context
-func (g *PlainGitDownloader) SetContext(ctx context.Context) {
-}
-
// GetRepoInfo returns a repository information
-func (g *PlainGitDownloader) GetRepoInfo() (*base.Repository, error) {
+func (g *PlainGitDownloader) GetRepoInfo(_ context.Context) (*base.Repository, error) {
// convert github repo to stand Repo
return &base.Repository{
Owner: g.ownerName,
@@ -43,6 +39,6 @@ func (g *PlainGitDownloader) GetRepoInfo() (*base.Repository, error) {
}
// GetTopics return empty string slice
-func (g PlainGitDownloader) GetTopics() ([]string, error) {
+func (g PlainGitDownloader) GetTopics(_ context.Context) ([]string, error) {
return []string{}, nil
}
diff --git a/services/migrations/gitea_downloader.go b/services/migrations/gitea_downloader.go
index 272bf02e11..f92f318293 100644
--- a/services/migrations/gitea_downloader.go
+++ b/services/migrations/gitea_downloader.go
@@ -67,7 +67,6 @@ func (f *GiteaDownloaderFactory) GitServiceType() structs.GitServiceType {
// GiteaDownloader implements a Downloader interface to get repository information's
type GiteaDownloader struct {
base.NullDownloader
- ctx context.Context
client *gitea_sdk.Client
baseURL string
repoOwner string
@@ -114,7 +113,6 @@ func NewGiteaDownloader(ctx context.Context, baseURL, repoPath, username, passwo
}
return &GiteaDownloader{
- ctx: ctx,
client: giteaClient,
baseURL: baseURL,
repoOwner: path[0],
@@ -124,11 +122,6 @@ func NewGiteaDownloader(ctx context.Context, baseURL, repoPath, username, passwo
}, nil
}
-// SetContext set context
-func (g *GiteaDownloader) SetContext(ctx context.Context) {
- g.ctx = ctx
-}
-
// String implements Stringer
func (g *GiteaDownloader) String() string {
return fmt.Sprintf("migration from gitea server %s %s/%s", g.baseURL, g.repoOwner, g.repoName)
@@ -142,7 +135,7 @@ func (g *GiteaDownloader) LogString() string {
}
// GetRepoInfo returns a repository information
-func (g *GiteaDownloader) GetRepoInfo() (*base.Repository, error) {
+func (g *GiteaDownloader) GetRepoInfo(_ context.Context) (*base.Repository, error) {
if g == nil {
return nil, errors.New("error: GiteaDownloader is nil")
}
@@ -164,19 +157,19 @@ func (g *GiteaDownloader) GetRepoInfo() (*base.Repository, error) {
}
// GetTopics return gitea topics
-func (g *GiteaDownloader) GetTopics() ([]string, error) {
+func (g *GiteaDownloader) GetTopics(_ context.Context) ([]string, error) {
topics, _, err := g.client.ListRepoTopics(g.repoOwner, g.repoName, gitea_sdk.ListRepoTopicsOptions{})
return topics, err
}
// GetMilestones returns milestones
-func (g *GiteaDownloader) GetMilestones() ([]*base.Milestone, error) {
+func (g *GiteaDownloader) GetMilestones(ctx context.Context) ([]*base.Milestone, error) {
milestones := make([]*base.Milestone, 0, g.maxPerPage)
for i := 1; ; i++ {
// make sure gitea can shutdown gracefully
select {
- case <-g.ctx.Done():
+ case <-ctx.Done():
return nil, nil
default:
}
@@ -235,13 +228,13 @@ func (g *GiteaDownloader) convertGiteaLabel(label *gitea_sdk.Label) *base.Label
}
// GetLabels returns labels
-func (g *GiteaDownloader) GetLabels() ([]*base.Label, error) {
+func (g *GiteaDownloader) GetLabels(ctx context.Context) ([]*base.Label, error) {
labels := make([]*base.Label, 0, g.maxPerPage)
for i := 1; ; i++ {
// make sure gitea can shutdown gracefully
select {
- case <-g.ctx.Done():
+ case <-ctx.Done():
return nil, nil
default:
}
@@ -323,13 +316,13 @@ func (g *GiteaDownloader) convertGiteaRelease(rel *gitea_sdk.Release) *base.Rele
}
// GetReleases returns releases
-func (g *GiteaDownloader) GetReleases() ([]*base.Release, error) {
+func (g *GiteaDownloader) GetReleases(ctx context.Context) ([]*base.Release, error) {
releases := make([]*base.Release, 0, g.maxPerPage)
for i := 1; ; i++ {
// make sure gitea can shutdown gracefully
select {
- case <-g.ctx.Done():
+ case <-ctx.Done():
return nil, nil
default:
}
@@ -395,7 +388,7 @@ func (g *GiteaDownloader) getCommentReactions(commentID int64) ([]*base.Reaction
}
// GetIssues returns issues according start and limit
-func (g *GiteaDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, error) {
+func (g *GiteaDownloader) GetIssues(_ context.Context, page, perPage int) ([]*base.Issue, bool, error) {
if perPage > g.maxPerPage {
perPage = g.maxPerPage
}
@@ -458,13 +451,13 @@ func (g *GiteaDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, err
}
// GetComments returns comments according issueNumber
-func (g *GiteaDownloader) GetComments(commentable base.Commentable) ([]*base.Comment, bool, error) {
+func (g *GiteaDownloader) GetComments(ctx context.Context, commentable base.Commentable) ([]*base.Comment, bool, error) {
allComments := make([]*base.Comment, 0, g.maxPerPage)
for i := 1; ; i++ {
// make sure gitea can shutdown gracefully
select {
- case <-g.ctx.Done():
+ case <-ctx.Done():
return nil, false, nil
default:
}
@@ -504,7 +497,7 @@ func (g *GiteaDownloader) GetComments(commentable base.Commentable) ([]*base.Com
}
// GetPullRequests returns pull requests according page and perPage
-func (g *GiteaDownloader) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) {
+func (g *GiteaDownloader) GetPullRequests(_ context.Context, page, perPage int) ([]*base.PullRequest, bool, error) {
if perPage > g.maxPerPage {
perPage = g.maxPerPage
}
@@ -624,7 +617,7 @@ func (g *GiteaDownloader) GetPullRequests(page, perPage int) ([]*base.PullReques
}
// GetReviews returns pull requests review
-func (g *GiteaDownloader) GetReviews(reviewable base.Reviewable) ([]*base.Review, error) {
+func (g *GiteaDownloader) GetReviews(ctx context.Context, reviewable base.Reviewable) ([]*base.Review, error) {
if err := g.client.CheckServerVersionConstraint(">=1.12"); err != nil {
log.Info("GiteaDownloader: instance to old, skip GetReviews")
return nil, nil
@@ -635,7 +628,7 @@ func (g *GiteaDownloader) GetReviews(reviewable base.Reviewable) ([]*base.Review
for i := 1; ; i++ {
// make sure gitea can shutdown gracefully
select {
- case <-g.ctx.Done():
+ case <-ctx.Done():
return nil, nil
default:
}
diff --git a/services/migrations/gitea_downloader_test.go b/services/migrations/gitea_downloader_test.go
index 6f6ef99d96..3dccc4017e 100644
--- a/services/migrations/gitea_downloader_test.go
+++ b/services/migrations/gitea_downloader_test.go
@@ -28,12 +28,12 @@ func TestGiteaDownloadRepo(t *testing.T) {
if err != nil || resp.StatusCode != http.StatusOK {
t.Skipf("Can't reach https://gitea.com, skipping %s", t.Name())
}
-
- downloader, err := NewGiteaDownloader(context.Background(), "https://gitea.com", "gitea/test_repo", "", "", giteaToken)
+ ctx := context.Background()
+ downloader, err := NewGiteaDownloader(ctx, "https://gitea.com", "gitea/test_repo", "", "", giteaToken)
require.NoError(t, err, "NewGiteaDownloader error occur")
require.NotNil(t, downloader, "NewGiteaDownloader is nil")
- repo, err := downloader.GetRepoInfo()
+ repo, err := downloader.GetRepoInfo(ctx)
assert.NoError(t, err)
assertRepositoryEqual(t, &base.Repository{
Name: "test_repo",
@@ -45,12 +45,12 @@ func TestGiteaDownloadRepo(t *testing.T) {
DefaultBranch: "master",
}, repo)
- topics, err := downloader.GetTopics()
+ topics, err := downloader.GetTopics(ctx)
assert.NoError(t, err)
sort.Strings(topics)
assert.EqualValues(t, []string{"ci", "gitea", "migration", "test"}, topics)
- labels, err := downloader.GetLabels()
+ labels, err := downloader.GetLabels(ctx)
assert.NoError(t, err)
assertLabelsEqual(t, []*base.Label{
{
@@ -80,7 +80,7 @@ func TestGiteaDownloadRepo(t *testing.T) {
},
}, labels)
- milestones, err := downloader.GetMilestones()
+ milestones, err := downloader.GetMilestones(ctx)
assert.NoError(t, err)
assertMilestonesEqual(t, []*base.Milestone{
{
@@ -100,7 +100,7 @@ func TestGiteaDownloadRepo(t *testing.T) {
},
}, milestones)
- releases, err := downloader.GetReleases()
+ releases, err := downloader.GetReleases(ctx)
assert.NoError(t, err)
assertReleasesEqual(t, []*base.Release{
{
@@ -131,13 +131,13 @@ func TestGiteaDownloadRepo(t *testing.T) {
},
}, releases)
- issues, isEnd, err := downloader.GetIssues(1, 50)
+ issues, isEnd, err := downloader.GetIssues(ctx, 1, 50)
assert.NoError(t, err)
assert.True(t, isEnd)
assert.Len(t, issues, 7)
assert.EqualValues(t, "open", issues[0].State)
- issues, isEnd, err = downloader.GetIssues(3, 2)
+ issues, isEnd, err = downloader.GetIssues(ctx, 3, 2)
assert.NoError(t, err)
assert.False(t, isEnd)
@@ -194,7 +194,7 @@ func TestGiteaDownloadRepo(t *testing.T) {
},
}, issues)
- comments, _, err := downloader.GetComments(&base.Issue{Number: 4, ForeignIndex: 4})
+ comments, _, err := downloader.GetComments(ctx, &base.Issue{Number: 4, ForeignIndex: 4})
assert.NoError(t, err)
assertCommentsEqual(t, []*base.Comment{
{
@@ -217,11 +217,11 @@ func TestGiteaDownloadRepo(t *testing.T) {
},
}, comments)
- prs, isEnd, err := downloader.GetPullRequests(1, 50)
+ prs, isEnd, err := downloader.GetPullRequests(ctx, 1, 50)
assert.NoError(t, err)
assert.True(t, isEnd)
assert.Len(t, prs, 6)
- prs, isEnd, err = downloader.GetPullRequests(1, 3)
+ prs, isEnd, err = downloader.GetPullRequests(ctx, 1, 3)
assert.NoError(t, err)
assert.False(t, isEnd)
assert.Len(t, prs, 3)
@@ -259,7 +259,7 @@ func TestGiteaDownloadRepo(t *testing.T) {
PatchURL: "https://gitea.com/gitea/test_repo/pulls/12.patch",
}, prs[1])
- reviews, err := downloader.GetReviews(&base.Issue{Number: 7, ForeignIndex: 7})
+ reviews, err := downloader.GetReviews(ctx, &base.Issue{Number: 7, ForeignIndex: 7})
assert.NoError(t, err)
assertReviewsEqual(t, []*base.Review{
{
diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go
index 9e06b77b66..eb16d6cb42 100644
--- a/services/migrations/gitea_uploader.go
+++ b/services/migrations/gitea_uploader.go
@@ -41,7 +41,6 @@ var _ base.Uploader = &GiteaLocalUploader{}
// GiteaLocalUploader implements an Uploader to gitea sites
type GiteaLocalUploader struct {
- ctx context.Context
doer *user_model.User
repoOwner string
repoName string
@@ -58,9 +57,8 @@ type GiteaLocalUploader struct {
}
// NewGiteaLocalUploader creates an gitea Uploader via gitea API v1
-func NewGiteaLocalUploader(ctx context.Context, doer *user_model.User, repoOwner, repoName string) *GiteaLocalUploader {
+func NewGiteaLocalUploader(_ context.Context, doer *user_model.User, repoOwner, repoName string) *GiteaLocalUploader {
return &GiteaLocalUploader{
- ctx: ctx,
doer: doer,
repoOwner: repoOwner,
repoName: repoName,
@@ -93,15 +91,15 @@ func (g *GiteaLocalUploader) MaxBatchInsertSize(tp string) int {
}
// CreateRepo creates a repository
-func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.MigrateOptions) error {
- owner, err := user_model.GetUserByName(g.ctx, g.repoOwner)
+func (g *GiteaLocalUploader) CreateRepo(ctx context.Context, repo *base.Repository, opts base.MigrateOptions) error {
+ owner, err := user_model.GetUserByName(ctx, g.repoOwner)
if err != nil {
return err
}
var r *repo_model.Repository
if opts.MigrateToRepoID <= 0 {
- r, err = repo_service.CreateRepositoryDirectly(g.ctx, g.doer, owner, repo_service.CreateRepoOptions{
+ r, err = repo_service.CreateRepositoryDirectly(ctx, g.doer, owner, repo_service.CreateRepoOptions{
Name: g.repoName,
Description: repo.Description,
OriginalURL: repo.OriginalURL,
@@ -111,7 +109,7 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate
Status: repo_model.RepositoryBeingMigrated,
})
} else {
- r, err = repo_model.GetRepositoryByID(g.ctx, opts.MigrateToRepoID)
+ r, err = repo_model.GetRepositoryByID(ctx, opts.MigrateToRepoID)
}
if err != nil {
return err
@@ -119,7 +117,7 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate
r.DefaultBranch = repo.DefaultBranch
r.Description = repo.Description
- r, err = repo_service.MigrateRepositoryGitData(g.ctx, owner, r, base.MigrateOptions{
+ r, err = repo_service.MigrateRepositoryGitData(ctx, owner, r, base.MigrateOptions{
RepoName: g.repoName,
Description: repo.Description,
OriginalURL: repo.OriginalURL,
@@ -139,7 +137,7 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate
if err != nil {
return err
}
- g.gitRepo, err = gitrepo.OpenRepository(g.ctx, g.repo)
+ g.gitRepo, err = gitrepo.OpenRepository(ctx, g.repo)
if err != nil {
return err
}
@@ -150,7 +148,7 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate
return err
}
g.repo.ObjectFormatName = objectFormat.Name()
- return repo_model.UpdateRepositoryCols(g.ctx, g.repo, "object_format_name")
+ return repo_model.UpdateRepositoryCols(ctx, g.repo, "object_format_name")
}
// Close closes this uploader
@@ -161,7 +159,7 @@ func (g *GiteaLocalUploader) Close() {
}
// CreateTopics creates topics
-func (g *GiteaLocalUploader) CreateTopics(topics ...string) error {
+func (g *GiteaLocalUploader) CreateTopics(ctx context.Context, topics ...string) error {
// Ignore topics too long for the db
c := 0
for _, topic := range topics {
@@ -173,11 +171,11 @@ func (g *GiteaLocalUploader) CreateTopics(topics ...string) error {
c++
}
topics = topics[:c]
- return repo_model.SaveTopics(g.ctx, g.repo.ID, topics...)
+ return repo_model.SaveTopics(ctx, g.repo.ID, topics...)
}
// CreateMilestones creates milestones
-func (g *GiteaLocalUploader) CreateMilestones(milestones ...*base.Milestone) error {
+func (g *GiteaLocalUploader) CreateMilestones(ctx context.Context, milestones ...*base.Milestone) error {
mss := make([]*issues_model.Milestone, 0, len(milestones))
for _, milestone := range milestones {
var deadline timeutil.TimeStamp
@@ -216,7 +214,7 @@ func (g *GiteaLocalUploader) CreateMilestones(milestones ...*base.Milestone) err
mss = append(mss, &ms)
}
- err := issues_model.InsertMilestones(g.ctx, mss...)
+ err := issues_model.InsertMilestones(ctx, mss...)
if err != nil {
return err
}
@@ -228,7 +226,7 @@ func (g *GiteaLocalUploader) CreateMilestones(milestones ...*base.Milestone) err
}
// CreateLabels creates labels
-func (g *GiteaLocalUploader) CreateLabels(labels ...*base.Label) error {
+func (g *GiteaLocalUploader) CreateLabels(ctx context.Context, labels ...*base.Label) error {
lbs := make([]*issues_model.Label, 0, len(labels))
for _, l := range labels {
if color, err := label.NormalizeColor(l.Color); err != nil {
@@ -247,7 +245,7 @@ func (g *GiteaLocalUploader) CreateLabels(labels ...*base.Label) error {
})
}
- err := issues_model.NewLabels(g.ctx, lbs...)
+ err := issues_model.NewLabels(ctx, lbs...)
if err != nil {
return err
}
@@ -258,7 +256,7 @@ func (g *GiteaLocalUploader) CreateLabels(labels ...*base.Label) error {
}
// CreateReleases creates releases
-func (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error {
+func (g *GiteaLocalUploader) CreateReleases(ctx context.Context, releases ...*base.Release) error {
rels := make([]*repo_model.Release, 0, len(releases))
for _, release := range releases {
if release.Created.IsZero() {
@@ -292,7 +290,7 @@ func (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error {
CreatedUnix: timeutil.TimeStamp(release.Created.Unix()),
}
- if err := g.remapUser(release, &rel); err != nil {
+ if err := g.remapUser(ctx, release, &rel); err != nil {
return err
}
@@ -361,16 +359,16 @@ func (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error {
rels = append(rels, &rel)
}
- return repo_model.InsertReleases(g.ctx, rels...)
+ return repo_model.InsertReleases(ctx, rels...)
}
// SyncTags syncs releases with tags in the database
-func (g *GiteaLocalUploader) SyncTags() error {
- return repo_module.SyncReleasesWithTags(g.ctx, g.repo, g.gitRepo)
+func (g *GiteaLocalUploader) SyncTags(ctx context.Context) error {
+ return repo_module.SyncReleasesWithTags(ctx, g.repo, g.gitRepo)
}
// CreateIssues creates issues
-func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error {
+func (g *GiteaLocalUploader) CreateIssues(ctx context.Context, issues ...*base.Issue) error {
iss := make([]*issues_model.Issue, 0, len(issues))
for _, issue := range issues {
var labels []*issues_model.Label
@@ -419,7 +417,7 @@ func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error {
UpdatedUnix: timeutil.TimeStamp(issue.Updated.Unix()),
}
- if err := g.remapUser(issue, &is); err != nil {
+ if err := g.remapUser(ctx, issue, &is); err != nil {
return err
}
@@ -432,7 +430,7 @@ func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error {
Type: reaction.Content,
CreatedUnix: timeutil.TimeStampNow(),
}
- if err := g.remapUser(reaction, &res); err != nil {
+ if err := g.remapUser(ctx, reaction, &res); err != nil {
return err
}
is.Reactions = append(is.Reactions, &res)
@@ -441,7 +439,7 @@ func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error {
}
if len(iss) > 0 {
- if err := issues_model.InsertIssues(g.ctx, iss...); err != nil {
+ if err := issues_model.InsertIssues(ctx, iss...); err != nil {
return err
}
@@ -454,7 +452,7 @@ func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error {
}
// CreateComments creates comments of issues
-func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error {
+func (g *GiteaLocalUploader) CreateComments(ctx context.Context, comments ...*base.Comment) error {
cms := make([]*issues_model.Comment, 0, len(comments))
for _, comment := range comments {
var issue *issues_model.Issue
@@ -513,7 +511,7 @@ func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error {
default:
}
- if err := g.remapUser(comment, &cm); err != nil {
+ if err := g.remapUser(ctx, comment, &cm); err != nil {
return err
}
@@ -523,7 +521,7 @@ func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error {
Type: reaction.Content,
CreatedUnix: timeutil.TimeStampNow(),
}
- if err := g.remapUser(reaction, &res); err != nil {
+ if err := g.remapUser(ctx, reaction, &res); err != nil {
return err
}
cm.Reactions = append(cm.Reactions, &res)
@@ -535,35 +533,35 @@ func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error {
if len(cms) == 0 {
return nil
}
- return issues_model.InsertIssueComments(g.ctx, cms)
+ return issues_model.InsertIssueComments(ctx, cms)
}
// CreatePullRequests creates pull requests
-func (g *GiteaLocalUploader) CreatePullRequests(prs ...*base.PullRequest) error {
+func (g *GiteaLocalUploader) CreatePullRequests(ctx context.Context, prs ...*base.PullRequest) error {
gprs := make([]*issues_model.PullRequest, 0, len(prs))
for _, pr := range prs {
- gpr, err := g.newPullRequest(pr)
+ gpr, err := g.newPullRequest(ctx, pr)
if err != nil {
return err
}
- if err := g.remapUser(pr, gpr.Issue); err != nil {
+ if err := g.remapUser(ctx, pr, gpr.Issue); err != nil {
return err
}
gprs = append(gprs, gpr)
}
- if err := issues_model.InsertPullRequests(g.ctx, gprs...); err != nil {
+ if err := issues_model.InsertPullRequests(ctx, gprs...); err != nil {
return err
}
for _, pr := range gprs {
g.issues[pr.Issue.Index] = pr.Issue
- pull.AddToTaskQueue(g.ctx, pr)
+ pull.AddToTaskQueue(ctx, pr)
}
return nil
}
-func (g *GiteaLocalUploader) updateGitForPullRequest(pr *base.PullRequest) (head string, err error) {
+func (g *GiteaLocalUploader) updateGitForPullRequest(ctx context.Context, pr *base.PullRequest) (head string, err error) {
// SECURITY: this pr must have been must have been ensured safe
if !pr.EnsuredSafe {
log.Error("PR #%d in %s/%s has not been checked for safety.", pr.Number, g.repoOwner, g.repoName)
@@ -664,7 +662,7 @@ func (g *GiteaLocalUploader) updateGitForPullRequest(pr *base.PullRequest) (head
fetchArg = git.BranchPrefix + fetchArg
}
- _, _, err = git.NewCommand(g.ctx, "fetch", "--no-tags").AddDashesAndList(remote, fetchArg).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()})
+ _, _, err = git.NewCommand(ctx, "fetch", "--no-tags").AddDashesAndList(remote, fetchArg).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()})
if err != nil {
log.Error("Fetch branch from %s failed: %v", pr.Head.CloneURL, err)
return head, nil
@@ -683,7 +681,7 @@ func (g *GiteaLocalUploader) updateGitForPullRequest(pr *base.PullRequest) (head
pr.Head.SHA = headSha
}
- _, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()})
+ _, _, err = git.NewCommand(ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()})
if err != nil {
return "", err
}
@@ -700,13 +698,13 @@ func (g *GiteaLocalUploader) updateGitForPullRequest(pr *base.PullRequest) (head
// The SHA is empty
log.Warn("Empty reference, no pull head for PR #%d in %s/%s", pr.Number, g.repoOwner, g.repoName)
} else {
- _, _, err = git.NewCommand(g.ctx, "rev-list", "--quiet", "-1").AddDynamicArguments(pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()})
+ _, _, err = git.NewCommand(ctx, "rev-list", "--quiet", "-1").AddDynamicArguments(pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()})
if err != nil {
// Git update-ref remove bad references with a relative path
log.Warn("Deprecated local head %s for PR #%d in %s/%s, removing %s", pr.Head.SHA, pr.Number, g.repoOwner, g.repoName, pr.GetGitRefName())
} else {
// set head information
- _, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()})
+ _, _, err = git.NewCommand(ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()})
if err != nil {
log.Error("unable to set %s as the local head for PR #%d from %s in %s/%s. Error: %v", pr.Head.SHA, pr.Number, pr.Head.Ref, g.repoOwner, g.repoName, err)
}
@@ -716,7 +714,7 @@ func (g *GiteaLocalUploader) updateGitForPullRequest(pr *base.PullRequest) (head
return head, nil
}
-func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*issues_model.PullRequest, error) {
+func (g *GiteaLocalUploader) newPullRequest(ctx context.Context, pr *base.PullRequest) (*issues_model.PullRequest, error) {
var labels []*issues_model.Label
for _, label := range pr.Labels {
lb, ok := g.labels[label.Name]
@@ -727,7 +725,7 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*issues_model
milestoneID := g.milestones[pr.Milestone]
- head, err := g.updateGitForPullRequest(pr)
+ head, err := g.updateGitForPullRequest(ctx, pr)
if err != nil {
return nil, fmt.Errorf("updateGitForPullRequest: %w", err)
}
@@ -779,7 +777,7 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*issues_model
UpdatedUnix: timeutil.TimeStamp(pr.Updated.Unix()),
}
- if err := g.remapUser(pr, &issue); err != nil {
+ if err := g.remapUser(ctx, pr, &issue); err != nil {
return nil, err
}
@@ -789,7 +787,7 @@ func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*issues_model
Type: reaction.Content,
CreatedUnix: timeutil.TimeStampNow(),
}
- if err := g.remapUser(reaction, &res); err != nil {
+ if err := g.remapUser(ctx, reaction, &res); err != nil {
return nil, err
}
issue.Reactions = append(issue.Reactions, &res)
@@ -839,7 +837,7 @@ func convertReviewState(state string) issues_model.ReviewType {
}
// CreateReviews create pull request reviews of currently migrated issues
-func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
+func (g *GiteaLocalUploader) CreateReviews(ctx context.Context, reviews ...*base.Review) error {
cms := make([]*issues_model.Review, 0, len(reviews))
for _, review := range reviews {
var issue *issues_model.Issue
@@ -860,7 +858,7 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
UpdatedUnix: timeutil.TimeStamp(review.CreatedAt.Unix()),
}
- if err := g.remapUser(review, &cm); err != nil {
+ if err := g.remapUser(ctx, review, &cm); err != nil {
return err
}
@@ -870,7 +868,7 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
pr, ok := g.prCache[issue.ID]
if !ok {
var err error
- pr, err = issues_model.GetPullRequestByIssueIDWithNoAttributes(g.ctx, issue.ID)
+ pr, err = issues_model.GetPullRequestByIssueIDWithNoAttributes(ctx, issue.ID)
if err != nil {
return err
}
@@ -940,7 +938,7 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
UpdatedUnix: timeutil.TimeStamp(comment.UpdatedAt.Unix()),
}
- if err := g.remapUser(review, &c); err != nil {
+ if err := g.remapUser(ctx, review, &c); err != nil {
return err
}
@@ -948,7 +946,7 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
}
}
- return issues_model.InsertReviews(g.ctx, cms)
+ return issues_model.InsertReviews(ctx, cms)
}
// Rollback when migrating failed, this will rollback all the changes.
@@ -962,31 +960,31 @@ func (g *GiteaLocalUploader) Rollback() error {
}
// Finish when migrating success, this will do some status update things.
-func (g *GiteaLocalUploader) Finish() error {
+func (g *GiteaLocalUploader) Finish(ctx context.Context) error {
if g.repo == nil || g.repo.ID <= 0 {
return ErrRepoNotCreated
}
// update issue_index
- if err := issues_model.RecalculateIssueIndexForRepo(g.ctx, g.repo.ID); err != nil {
+ if err := issues_model.RecalculateIssueIndexForRepo(ctx, g.repo.ID); err != nil {
return err
}
- if err := models.UpdateRepoStats(g.ctx, g.repo.ID); err != nil {
+ if err := models.UpdateRepoStats(ctx, g.repo.ID); err != nil {
return err
}
g.repo.Status = repo_model.RepositoryReady
- return repo_model.UpdateRepositoryCols(g.ctx, g.repo, "status")
+ return repo_model.UpdateRepositoryCols(ctx, g.repo, "status")
}
-func (g *GiteaLocalUploader) remapUser(source user_model.ExternalUserMigrated, target user_model.ExternalUserRemappable) error {
+func (g *GiteaLocalUploader) remapUser(ctx context.Context, source user_model.ExternalUserMigrated, target user_model.ExternalUserRemappable) error {
var userID int64
var err error
if g.sameApp {
- userID, err = g.remapLocalUser(source)
+ userID, err = g.remapLocalUser(ctx, source)
} else {
- userID, err = g.remapExternalUser(source)
+ userID, err = g.remapExternalUser(ctx, source)
}
if err != nil {
return err
@@ -998,10 +996,10 @@ func (g *GiteaLocalUploader) remapUser(source user_model.ExternalUserMigrated, t
return target.RemapExternalUser(source.GetExternalName(), source.GetExternalID(), g.doer.ID)
}
-func (g *GiteaLocalUploader) remapLocalUser(source user_model.ExternalUserMigrated) (int64, error) {
+func (g *GiteaLocalUploader) remapLocalUser(ctx context.Context, source user_model.ExternalUserMigrated) (int64, error) {
userid, ok := g.userMap[source.GetExternalID()]
if !ok {
- name, err := user_model.GetUserNameByID(g.ctx, source.GetExternalID())
+ name, err := user_model.GetUserNameByID(ctx, source.GetExternalID())
if err != nil {
return 0, err
}
@@ -1016,10 +1014,10 @@ func (g *GiteaLocalUploader) remapLocalUser(source user_model.ExternalUserMigrat
return userid, nil
}
-func (g *GiteaLocalUploader) remapExternalUser(source user_model.ExternalUserMigrated) (userid int64, err error) {
+func (g *GiteaLocalUploader) remapExternalUser(ctx context.Context, source user_model.ExternalUserMigrated) (userid int64, err error) {
userid, ok := g.userMap[source.GetExternalID()]
if !ok {
- userid, err = user_model.GetUserIDByExternalUserID(g.ctx, g.gitServiceType.Name(), fmt.Sprintf("%d", source.GetExternalID()))
+ userid, err = user_model.GetUserIDByExternalUserID(ctx, g.gitServiceType.Name(), fmt.Sprintf("%d", source.GetExternalID()))
if err != nil {
log.Error("GetUserIDByExternalUserID: %v", err)
return 0, err
diff --git a/services/migrations/gitea_uploader_test.go b/services/migrations/gitea_uploader_test.go
index f2379dadf8..18d1171597 100644
--- a/services/migrations/gitea_uploader_test.go
+++ b/services/migrations/gitea_uploader_test.go
@@ -132,8 +132,9 @@ func TestGiteaUploadRemapLocalUser(t *testing.T) {
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ ctx := context.Background()
repoName := "migrated"
- uploader := NewGiteaLocalUploader(context.Background(), doer, doer.Name, repoName)
+ uploader := NewGiteaLocalUploader(ctx, doer, doer.Name, repoName)
// call remapLocalUser
uploader.sameApp = true
@@ -150,7 +151,7 @@ func TestGiteaUploadRemapLocalUser(t *testing.T) {
//
target := repo_model.Release{}
uploader.userMap = make(map[int64]int64)
- err := uploader.remapUser(&source, &target)
+ err := uploader.remapUser(ctx, &source, &target)
assert.NoError(t, err)
assert.EqualValues(t, doer.ID, target.GetUserID())
@@ -161,7 +162,7 @@ func TestGiteaUploadRemapLocalUser(t *testing.T) {
source.PublisherID = user.ID
target = repo_model.Release{}
uploader.userMap = make(map[int64]int64)
- err = uploader.remapUser(&source, &target)
+ err = uploader.remapUser(ctx, &source, &target)
assert.NoError(t, err)
assert.EqualValues(t, doer.ID, target.GetUserID())
@@ -172,7 +173,7 @@ func TestGiteaUploadRemapLocalUser(t *testing.T) {
source.PublisherName = user.Name
target = repo_model.Release{}
uploader.userMap = make(map[int64]int64)
- err = uploader.remapUser(&source, &target)
+ err = uploader.remapUser(ctx, &source, &target)
assert.NoError(t, err)
assert.EqualValues(t, user.ID, target.GetUserID())
}
@@ -180,9 +181,9 @@ func TestGiteaUploadRemapLocalUser(t *testing.T) {
func TestGiteaUploadRemapExternalUser(t *testing.T) {
unittest.PrepareTestEnv(t)
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
-
+ ctx := context.Background()
repoName := "migrated"
- uploader := NewGiteaLocalUploader(context.Background(), doer, doer.Name, repoName)
+ uploader := NewGiteaLocalUploader(ctx, doer, doer.Name, repoName)
uploader.gitServiceType = structs.GiteaService
// call remapExternalUser
uploader.sameApp = false
@@ -200,7 +201,7 @@ func TestGiteaUploadRemapExternalUser(t *testing.T) {
//
uploader.userMap = make(map[int64]int64)
target := repo_model.Release{}
- err := uploader.remapUser(&source, &target)
+ err := uploader.remapUser(ctx, &source, &target)
assert.NoError(t, err)
assert.EqualValues(t, doer.ID, target.GetUserID())
@@ -223,7 +224,7 @@ func TestGiteaUploadRemapExternalUser(t *testing.T) {
//
uploader.userMap = make(map[int64]int64)
target = repo_model.Release{}
- err = uploader.remapUser(&source, &target)
+ err = uploader.remapUser(ctx, &source, &target)
assert.NoError(t, err)
assert.EqualValues(t, linkedUser.ID, target.GetUserID())
}
@@ -301,11 +302,12 @@ func TestGiteaUploadUpdateGitForPullRequest(t *testing.T) {
assert.NoError(t, err)
toRepoName := "migrated"
- uploader := NewGiteaLocalUploader(context.Background(), fromRepoOwner, fromRepoOwner.Name, toRepoName)
+ ctx := context.Background()
+ uploader := NewGiteaLocalUploader(ctx, fromRepoOwner, fromRepoOwner.Name, toRepoName)
uploader.gitServiceType = structs.GiteaService
assert.NoError(t, repo_service.Init(context.Background()))
- assert.NoError(t, uploader.CreateRepo(&base.Repository{
+ assert.NoError(t, uploader.CreateRepo(ctx, &base.Repository{
Description: "description",
OriginalURL: fromRepo.RepoPath(),
CloneURL: fromRepo.RepoPath(),
@@ -505,7 +507,7 @@ func TestGiteaUploadUpdateGitForPullRequest(t *testing.T) {
testCase.pr.EnsuredSafe = true
- head, err := uploader.updateGitForPullRequest(&testCase.pr)
+ head, err := uploader.updateGitForPullRequest(ctx, &testCase.pr)
assert.NoError(t, err)
assert.EqualValues(t, testCase.head, head)
diff --git a/services/migrations/github.go b/services/migrations/github.go
index 604ab84b39..b00d6ed27f 100644
--- a/services/migrations/github.go
+++ b/services/migrations/github.go
@@ -64,7 +64,6 @@ func (f *GithubDownloaderV3Factory) GitServiceType() structs.GitServiceType {
// from github via APIv3
type GithubDownloaderV3 struct {
base.NullDownloader
- ctx context.Context
clients []*github.Client
baseURL string
repoOwner string
@@ -79,12 +78,11 @@ type GithubDownloaderV3 struct {
}
// NewGithubDownloaderV3 creates a github Downloader via github v3 API
-func NewGithubDownloaderV3(ctx context.Context, baseURL, userName, password, token, repoOwner, repoName string) *GithubDownloaderV3 {
+func NewGithubDownloaderV3(_ context.Context, baseURL, userName, password, token, repoOwner, repoName string) *GithubDownloaderV3 {
downloader := GithubDownloaderV3{
userName: userName,
baseURL: baseURL,
password: password,
- ctx: ctx,
repoOwner: repoOwner,
repoName: repoName,
maxPerPage: 100,
@@ -141,12 +139,7 @@ func (g *GithubDownloaderV3) addClient(client *http.Client, baseURL string) {
g.rates = append(g.rates, nil)
}
-// SetContext set context
-func (g *GithubDownloaderV3) SetContext(ctx context.Context) {
- g.ctx = ctx
-}
-
-func (g *GithubDownloaderV3) waitAndPickClient() {
+func (g *GithubDownloaderV3) waitAndPickClient(ctx context.Context) {
var recentIdx int
var maxRemaining int
for i := 0; i < len(g.clients); i++ {
@@ -160,13 +153,13 @@ func (g *GithubDownloaderV3) waitAndPickClient() {
for g.rates[g.curClientIdx] != nil && g.rates[g.curClientIdx].Remaining <= GithubLimitRateRemaining {
timer := time.NewTimer(time.Until(g.rates[g.curClientIdx].Reset.Time))
select {
- case <-g.ctx.Done():
+ case <-ctx.Done():
timer.Stop()
return
case <-timer.C:
}
- err := g.RefreshRate()
+ err := g.RefreshRate(ctx)
if err != nil {
log.Error("g.getClient().RateLimit.Get: %s", err)
}
@@ -174,8 +167,8 @@ func (g *GithubDownloaderV3) waitAndPickClient() {
}
// RefreshRate update the current rate (doesn't count in rate limit)
-func (g *GithubDownloaderV3) RefreshRate() error {
- rates, _, err := g.getClient().RateLimit.Get(g.ctx)
+func (g *GithubDownloaderV3) RefreshRate(ctx context.Context) error {
+ rates, _, err := g.getClient().RateLimit.Get(ctx)
if err != nil {
// if rate limit is not enabled, ignore it
if strings.Contains(err.Error(), "404") {
@@ -198,9 +191,9 @@ func (g *GithubDownloaderV3) setRate(rate *github.Rate) {
}
// GetRepoInfo returns a repository information
-func (g *GithubDownloaderV3) GetRepoInfo() (*base.Repository, error) {
- g.waitAndPickClient()
- gr, resp, err := g.getClient().Repositories.Get(g.ctx, g.repoOwner, g.repoName)
+func (g *GithubDownloaderV3) GetRepoInfo(ctx context.Context) (*base.Repository, error) {
+ g.waitAndPickClient(ctx)
+ gr, resp, err := g.getClient().Repositories.Get(ctx, g.repoOwner, g.repoName)
if err != nil {
return nil, err
}
@@ -219,9 +212,9 @@ func (g *GithubDownloaderV3) GetRepoInfo() (*base.Repository, error) {
}
// GetTopics return github topics
-func (g *GithubDownloaderV3) GetTopics() ([]string, error) {
- g.waitAndPickClient()
- r, resp, err := g.getClient().Repositories.Get(g.ctx, g.repoOwner, g.repoName)
+func (g *GithubDownloaderV3) GetTopics(ctx context.Context) ([]string, error) {
+ g.waitAndPickClient(ctx)
+ r, resp, err := g.getClient().Repositories.Get(ctx, g.repoOwner, g.repoName)
if err != nil {
return nil, err
}
@@ -230,12 +223,12 @@ func (g *GithubDownloaderV3) GetTopics() ([]string, error) {
}
// GetMilestones returns milestones
-func (g *GithubDownloaderV3) GetMilestones() ([]*base.Milestone, error) {
+func (g *GithubDownloaderV3) GetMilestones(ctx context.Context) ([]*base.Milestone, error) {
perPage := g.maxPerPage
milestones := make([]*base.Milestone, 0, perPage)
for i := 1; ; i++ {
- g.waitAndPickClient()
- ms, resp, err := g.getClient().Issues.ListMilestones(g.ctx, g.repoOwner, g.repoName,
+ g.waitAndPickClient(ctx)
+ ms, resp, err := g.getClient().Issues.ListMilestones(ctx, g.repoOwner, g.repoName,
&github.MilestoneListOptions{
State: "all",
ListOptions: github.ListOptions{
@@ -279,12 +272,12 @@ func convertGithubLabel(label *github.Label) *base.Label {
}
// GetLabels returns labels
-func (g *GithubDownloaderV3) GetLabels() ([]*base.Label, error) {
+func (g *GithubDownloaderV3) GetLabels(ctx context.Context) ([]*base.Label, error) {
perPage := g.maxPerPage
labels := make([]*base.Label, 0, perPage)
for i := 1; ; i++ {
- g.waitAndPickClient()
- ls, resp, err := g.getClient().Issues.ListLabels(g.ctx, g.repoOwner, g.repoName,
+ g.waitAndPickClient(ctx)
+ ls, resp, err := g.getClient().Issues.ListLabels(ctx, g.repoOwner, g.repoName,
&github.ListOptions{
Page: i,
PerPage: perPage,
@@ -304,7 +297,7 @@ func (g *GithubDownloaderV3) GetLabels() ([]*base.Label, error) {
return labels, nil
}
-func (g *GithubDownloaderV3) convertGithubRelease(rel *github.RepositoryRelease) *base.Release {
+func (g *GithubDownloaderV3) convertGithubRelease(ctx context.Context, rel *github.RepositoryRelease) *base.Release {
// GitHub allows commitish to be a reference.
// In this case, we need to remove the prefix, i.e. convert "refs/heads/main" to "main".
targetCommitish := strings.TrimPrefix(rel.GetTargetCommitish(), git.BranchPrefix)
@@ -339,12 +332,12 @@ func (g *GithubDownloaderV3) convertGithubRelease(rel *github.RepositoryRelease)
Created: asset.CreatedAt.Time,
Updated: asset.UpdatedAt.Time,
DownloadFunc: func() (io.ReadCloser, error) {
- g.waitAndPickClient()
- readCloser, redirectURL, err := g.getClient().Repositories.DownloadReleaseAsset(g.ctx, g.repoOwner, g.repoName, assetID, nil)
+ g.waitAndPickClient(ctx)
+ readCloser, redirectURL, err := g.getClient().Repositories.DownloadReleaseAsset(ctx, g.repoOwner, g.repoName, assetID, nil)
if err != nil {
return nil, err
}
- if err := g.RefreshRate(); err != nil {
+ if err := g.RefreshRate(ctx); err != nil {
log.Error("g.getClient().RateLimits: %s", err)
}
@@ -364,13 +357,13 @@ func (g *GithubDownloaderV3) convertGithubRelease(rel *github.RepositoryRelease)
return io.NopCloser(strings.NewReader(redirectURL)), nil
}
- g.waitAndPickClient()
- req, err := http.NewRequestWithContext(g.ctx, "GET", redirectURL, nil)
+ g.waitAndPickClient(ctx)
+ req, err := http.NewRequestWithContext(ctx, "GET", redirectURL, nil)
if err != nil {
return nil, err
}
resp, err := httpClient.Do(req)
- err1 := g.RefreshRate()
+ err1 := g.RefreshRate(ctx)
if err1 != nil {
log.Error("g.RefreshRate(): %s", err1)
}
@@ -385,12 +378,12 @@ func (g *GithubDownloaderV3) convertGithubRelease(rel *github.RepositoryRelease)
}
// GetReleases returns releases
-func (g *GithubDownloaderV3) GetReleases() ([]*base.Release, error) {
+func (g *GithubDownloaderV3) GetReleases(ctx context.Context) ([]*base.Release, error) {
perPage := g.maxPerPage
releases := make([]*base.Release, 0, perPage)
for i := 1; ; i++ {
- g.waitAndPickClient()
- ls, resp, err := g.getClient().Repositories.ListReleases(g.ctx, g.repoOwner, g.repoName,
+ g.waitAndPickClient(ctx)
+ ls, resp, err := g.getClient().Repositories.ListReleases(ctx, g.repoOwner, g.repoName,
&github.ListOptions{
Page: i,
PerPage: perPage,
@@ -401,7 +394,7 @@ func (g *GithubDownloaderV3) GetReleases() ([]*base.Release, error) {
g.setRate(&resp.Rate)
for _, release := range ls {
- releases = append(releases, g.convertGithubRelease(release))
+ releases = append(releases, g.convertGithubRelease(ctx, release))
}
if len(ls) < perPage {
break
@@ -411,7 +404,7 @@ func (g *GithubDownloaderV3) GetReleases() ([]*base.Release, error) {
}
// GetIssues returns issues according start and limit
-func (g *GithubDownloaderV3) GetIssues(page, perPage int) ([]*base.Issue, bool, error) {
+func (g *GithubDownloaderV3) GetIssues(ctx context.Context, page, perPage int) ([]*base.Issue, bool, error) {
if perPage > g.maxPerPage {
perPage = g.maxPerPage
}
@@ -426,8 +419,8 @@ func (g *GithubDownloaderV3) GetIssues(page, perPage int) ([]*base.Issue, bool,
}
allIssues := make([]*base.Issue, 0, perPage)
- g.waitAndPickClient()
- issues, resp, err := g.getClient().Issues.ListByRepo(g.ctx, g.repoOwner, g.repoName, opt)
+ g.waitAndPickClient(ctx)
+ issues, resp, err := g.getClient().Issues.ListByRepo(ctx, g.repoOwner, g.repoName, opt)
if err != nil {
return nil, false, fmt.Errorf("error while listing repos: %w", err)
}
@@ -447,8 +440,8 @@ func (g *GithubDownloaderV3) GetIssues(page, perPage int) ([]*base.Issue, bool,
var reactions []*base.Reaction
if !g.SkipReactions {
for i := 1; ; i++ {
- g.waitAndPickClient()
- res, resp, err := g.getClient().Reactions.ListIssueReactions(g.ctx, g.repoOwner, g.repoName, issue.GetNumber(), &github.ListOptions{
+ g.waitAndPickClient(ctx)
+ res, resp, err := g.getClient().Reactions.ListIssueReactions(ctx, g.repoOwner, g.repoName, issue.GetNumber(), &github.ListOptions{
Page: i,
PerPage: perPage,
})
@@ -503,12 +496,12 @@ func (g *GithubDownloaderV3) SupportGetRepoComments() bool {
}
// GetComments returns comments according issueNumber
-func (g *GithubDownloaderV3) GetComments(commentable base.Commentable) ([]*base.Comment, bool, error) {
- comments, err := g.getComments(commentable)
+func (g *GithubDownloaderV3) GetComments(ctx context.Context, commentable base.Commentable) ([]*base.Comment, bool, error) {
+ comments, err := g.getComments(ctx, commentable)
return comments, false, err
}
-func (g *GithubDownloaderV3) getComments(commentable base.Commentable) ([]*base.Comment, error) {
+func (g *GithubDownloaderV3) getComments(ctx context.Context, commentable base.Commentable) ([]*base.Comment, error) {
var (
allComments = make([]*base.Comment, 0, g.maxPerPage)
created = "created"
@@ -522,8 +515,8 @@ func (g *GithubDownloaderV3) getComments(commentable base.Commentable) ([]*base.
},
}
for {
- g.waitAndPickClient()
- comments, resp, err := g.getClient().Issues.ListComments(g.ctx, g.repoOwner, g.repoName, int(commentable.GetForeignIndex()), opt)
+ g.waitAndPickClient(ctx)
+ comments, resp, err := g.getClient().Issues.ListComments(ctx, g.repoOwner, g.repoName, int(commentable.GetForeignIndex()), opt)
if err != nil {
return nil, fmt.Errorf("error while listing repos: %w", err)
}
@@ -533,8 +526,8 @@ func (g *GithubDownloaderV3) getComments(commentable base.Commentable) ([]*base.
var reactions []*base.Reaction
if !g.SkipReactions {
for i := 1; ; i++ {
- g.waitAndPickClient()
- res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(g.ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListOptions{
+ g.waitAndPickClient(ctx)
+ res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListOptions{
Page: i,
PerPage: g.maxPerPage,
})
@@ -576,7 +569,7 @@ func (g *GithubDownloaderV3) getComments(commentable base.Commentable) ([]*base.
}
// GetAllComments returns repository comments according page and perPageSize
-func (g *GithubDownloaderV3) GetAllComments(page, perPage int) ([]*base.Comment, bool, error) {
+func (g *GithubDownloaderV3) GetAllComments(ctx context.Context, page, perPage int) ([]*base.Comment, bool, error) {
var (
allComments = make([]*base.Comment, 0, perPage)
created = "created"
@@ -594,8 +587,8 @@ func (g *GithubDownloaderV3) GetAllComments(page, perPage int) ([]*base.Comment,
},
}
- g.waitAndPickClient()
- comments, resp, err := g.getClient().Issues.ListComments(g.ctx, g.repoOwner, g.repoName, 0, opt)
+ g.waitAndPickClient(ctx)
+ comments, resp, err := g.getClient().Issues.ListComments(ctx, g.repoOwner, g.repoName, 0, opt)
if err != nil {
return nil, false, fmt.Errorf("error while listing repos: %w", err)
}
@@ -608,8 +601,8 @@ func (g *GithubDownloaderV3) GetAllComments(page, perPage int) ([]*base.Comment,
var reactions []*base.Reaction
if !g.SkipReactions {
for i := 1; ; i++ {
- g.waitAndPickClient()
- res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(g.ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListOptions{
+ g.waitAndPickClient(ctx)
+ res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListOptions{
Page: i,
PerPage: g.maxPerPage,
})
@@ -648,7 +641,7 @@ func (g *GithubDownloaderV3) GetAllComments(page, perPage int) ([]*base.Comment,
}
// GetPullRequests returns pull requests according page and perPage
-func (g *GithubDownloaderV3) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) {
+func (g *GithubDownloaderV3) GetPullRequests(ctx context.Context, page, perPage int) ([]*base.PullRequest, bool, error) {
if perPage > g.maxPerPage {
perPage = g.maxPerPage
}
@@ -662,8 +655,8 @@ func (g *GithubDownloaderV3) GetPullRequests(page, perPage int) ([]*base.PullReq
},
}
allPRs := make([]*base.PullRequest, 0, perPage)
- g.waitAndPickClient()
- prs, resp, err := g.getClient().PullRequests.List(g.ctx, g.repoOwner, g.repoName, opt)
+ g.waitAndPickClient(ctx)
+ prs, resp, err := g.getClient().PullRequests.List(ctx, g.repoOwner, g.repoName, opt)
if err != nil {
return nil, false, fmt.Errorf("error while listing repos: %w", err)
}
@@ -679,8 +672,8 @@ func (g *GithubDownloaderV3) GetPullRequests(page, perPage int) ([]*base.PullReq
var reactions []*base.Reaction
if !g.SkipReactions {
for i := 1; ; i++ {
- g.waitAndPickClient()
- res, resp, err := g.getClient().Reactions.ListIssueReactions(g.ctx, g.repoOwner, g.repoName, pr.GetNumber(), &github.ListOptions{
+ g.waitAndPickClient(ctx)
+ res, resp, err := g.getClient().Reactions.ListIssueReactions(ctx, g.repoOwner, g.repoName, pr.GetNumber(), &github.ListOptions{
Page: i,
PerPage: perPage,
})
@@ -702,7 +695,7 @@ func (g *GithubDownloaderV3) GetPullRequests(page, perPage int) ([]*base.PullReq
}
// download patch and saved as tmp file
- g.waitAndPickClient()
+ g.waitAndPickClient(ctx)
allPRs = append(allPRs, &base.PullRequest{
Title: pr.GetTitle(),
@@ -759,15 +752,15 @@ func convertGithubReview(r *github.PullRequestReview) *base.Review {
}
}
-func (g *GithubDownloaderV3) convertGithubReviewComments(cs []*github.PullRequestComment) ([]*base.ReviewComment, error) {
+func (g *GithubDownloaderV3) convertGithubReviewComments(ctx context.Context, cs []*github.PullRequestComment) ([]*base.ReviewComment, error) {
rcs := make([]*base.ReviewComment, 0, len(cs))
for _, c := range cs {
// get reactions
var reactions []*base.Reaction
if !g.SkipReactions {
for i := 1; ; i++ {
- g.waitAndPickClient()
- res, resp, err := g.getClient().Reactions.ListPullRequestCommentReactions(g.ctx, g.repoOwner, g.repoName, c.GetID(), &github.ListOptions{
+ g.waitAndPickClient(ctx)
+ res, resp, err := g.getClient().Reactions.ListPullRequestCommentReactions(ctx, g.repoOwner, g.repoName, c.GetID(), &github.ListOptions{
Page: i,
PerPage: g.maxPerPage,
})
@@ -806,7 +799,7 @@ func (g *GithubDownloaderV3) convertGithubReviewComments(cs []*github.PullReques
}
// GetReviews returns pull requests review
-func (g *GithubDownloaderV3) GetReviews(reviewable base.Reviewable) ([]*base.Review, error) {
+func (g *GithubDownloaderV3) GetReviews(ctx context.Context, reviewable base.Reviewable) ([]*base.Review, error) {
allReviews := make([]*base.Review, 0, g.maxPerPage)
if g.SkipReviews {
return allReviews, nil
@@ -816,8 +809,8 @@ func (g *GithubDownloaderV3) GetReviews(reviewable base.Reviewable) ([]*base.Rev
}
// Get approve/request change reviews
for {
- g.waitAndPickClient()
- reviews, resp, err := g.getClient().PullRequests.ListReviews(g.ctx, g.repoOwner, g.repoName, int(reviewable.GetForeignIndex()), opt)
+ g.waitAndPickClient(ctx)
+ reviews, resp, err := g.getClient().PullRequests.ListReviews(ctx, g.repoOwner, g.repoName, int(reviewable.GetForeignIndex()), opt)
if err != nil {
return nil, fmt.Errorf("error while listing repos: %w", err)
}
@@ -830,14 +823,14 @@ func (g *GithubDownloaderV3) GetReviews(reviewable base.Reviewable) ([]*base.Rev
PerPage: g.maxPerPage,
}
for {
- g.waitAndPickClient()
- reviewComments, resp, err := g.getClient().PullRequests.ListReviewComments(g.ctx, g.repoOwner, g.repoName, int(reviewable.GetForeignIndex()), review.GetID(), opt2)
+ g.waitAndPickClient(ctx)
+ reviewComments, resp, err := g.getClient().PullRequests.ListReviewComments(ctx, g.repoOwner, g.repoName, int(reviewable.GetForeignIndex()), review.GetID(), opt2)
if err != nil {
return nil, fmt.Errorf("error while listing repos: %w", err)
}
g.setRate(&resp.Rate)
- cs, err := g.convertGithubReviewComments(reviewComments)
+ cs, err := g.convertGithubReviewComments(ctx, reviewComments)
if err != nil {
return nil, err
}
@@ -856,8 +849,8 @@ func (g *GithubDownloaderV3) GetReviews(reviewable base.Reviewable) ([]*base.Rev
}
// Get requested reviews
for {
- g.waitAndPickClient()
- reviewers, resp, err := g.getClient().PullRequests.ListReviewers(g.ctx, g.repoOwner, g.repoName, int(reviewable.GetForeignIndex()), opt)
+ g.waitAndPickClient(ctx)
+ reviewers, resp, err := g.getClient().PullRequests.ListReviewers(ctx, g.repoOwner, g.repoName, int(reviewable.GetForeignIndex()), opt)
if err != nil {
return nil, fmt.Errorf("error while listing repos: %w", err)
}
diff --git a/services/migrations/github_test.go b/services/migrations/github_test.go
index 2b89e6dc0f..899f9fe52c 100644
--- a/services/migrations/github_test.go
+++ b/services/migrations/github_test.go
@@ -21,11 +21,12 @@ func TestGitHubDownloadRepo(t *testing.T) {
if token == "" {
t.Skip("Skipping GitHub migration test because GITHUB_READ_TOKEN is empty")
}
- downloader := NewGithubDownloaderV3(context.Background(), "https://github.com", "", "", token, "go-gitea", "test_repo")
- err := downloader.RefreshRate()
+ ctx := context.Background()
+ downloader := NewGithubDownloaderV3(ctx, "https://github.com", "", "", token, "go-gitea", "test_repo")
+ err := downloader.RefreshRate(ctx)
assert.NoError(t, err)
- repo, err := downloader.GetRepoInfo()
+ repo, err := downloader.GetRepoInfo(ctx)
assert.NoError(t, err)
assertRepositoryEqual(t, &base.Repository{
Name: "test_repo",
@@ -36,11 +37,11 @@ func TestGitHubDownloadRepo(t *testing.T) {
DefaultBranch: "master",
}, repo)
- topics, err := downloader.GetTopics()
+ topics, err := downloader.GetTopics(ctx)
assert.NoError(t, err)
assert.Contains(t, topics, "gitea")
- milestones, err := downloader.GetMilestones()
+ milestones, err := downloader.GetMilestones(ctx)
assert.NoError(t, err)
assertMilestonesEqual(t, []*base.Milestone{
{
@@ -63,7 +64,7 @@ func TestGitHubDownloadRepo(t *testing.T) {
},
}, milestones)
- labels, err := downloader.GetLabels()
+ labels, err := downloader.GetLabels(ctx)
assert.NoError(t, err)
assertLabelsEqual(t, []*base.Label{
{
@@ -113,7 +114,7 @@ func TestGitHubDownloadRepo(t *testing.T) {
},
}, labels)
- releases, err := downloader.GetReleases()
+ releases, err := downloader.GetReleases(ctx)
assert.NoError(t, err)
assertReleasesEqual(t, []*base.Release{
{
@@ -129,7 +130,7 @@ func TestGitHubDownloadRepo(t *testing.T) {
}, releases)
// downloader.GetIssues()
- issues, isEnd, err := downloader.GetIssues(1, 2)
+ issues, isEnd, err := downloader.GetIssues(ctx, 1, 2)
assert.NoError(t, err)
assert.False(t, isEnd)
assertIssuesEqual(t, []*base.Issue{
@@ -218,7 +219,7 @@ func TestGitHubDownloadRepo(t *testing.T) {
}, issues)
// downloader.GetComments()
- comments, _, err := downloader.GetComments(&base.Issue{Number: 2, ForeignIndex: 2})
+ comments, _, err := downloader.GetComments(ctx, &base.Issue{Number: 2, ForeignIndex: 2})
assert.NoError(t, err)
assertCommentsEqual(t, []*base.Comment{
{
@@ -248,7 +249,7 @@ func TestGitHubDownloadRepo(t *testing.T) {
}, comments)
// downloader.GetPullRequests()
- prs, _, err := downloader.GetPullRequests(1, 2)
+ prs, _, err := downloader.GetPullRequests(ctx, 1, 2)
assert.NoError(t, err)
assertPullRequestsEqual(t, []*base.PullRequest{
{
@@ -338,7 +339,7 @@ func TestGitHubDownloadRepo(t *testing.T) {
},
}, prs)
- reviews, err := downloader.GetReviews(&base.PullRequest{Number: 3, ForeignIndex: 3})
+ reviews, err := downloader.GetReviews(ctx, &base.PullRequest{Number: 3, ForeignIndex: 3})
assert.NoError(t, err)
assertReviewsEqual(t, []*base.Review{
{
@@ -370,7 +371,7 @@ func TestGitHubDownloadRepo(t *testing.T) {
},
}, reviews)
- reviews, err = downloader.GetReviews(&base.PullRequest{Number: 4, ForeignIndex: 4})
+ reviews, err = downloader.GetReviews(ctx, &base.PullRequest{Number: 4, ForeignIndex: 4})
assert.NoError(t, err)
assertReviewsEqual(t, []*base.Review{
{
diff --git a/services/migrations/gitlab.go b/services/migrations/gitlab.go
index 07d5040b5b..efc5b960cf 100644
--- a/services/migrations/gitlab.go
+++ b/services/migrations/gitlab.go
@@ -80,7 +80,6 @@ func (r *gitlabIIDResolver) generatePullRequestNumber(mrIID int) int64 {
// because Gitlab has individual Issue and Pull Request numbers.
type GitlabDownloader struct {
base.NullDownloader
- ctx context.Context
client *gitlab.Client
baseURL string
repoID int
@@ -143,7 +142,6 @@ func NewGitlabDownloader(ctx context.Context, baseURL, repoPath, username, passw
}
return &GitlabDownloader{
- ctx: ctx,
client: gitlabClient,
baseURL: baseURL,
repoID: gr.ID,
@@ -164,14 +162,9 @@ func (g *GitlabDownloader) LogString() string {
return fmt.Sprintf("<GitlabDownloader %s [%d]/%s>", g.baseURL, g.repoID, g.repoName)
}
-// SetContext set context
-func (g *GitlabDownloader) SetContext(ctx context.Context) {
- g.ctx = ctx
-}
-
// GetRepoInfo returns a repository information
-func (g *GitlabDownloader) GetRepoInfo() (*base.Repository, error) {
- gr, _, err := g.client.Projects.GetProject(g.repoID, nil, nil, gitlab.WithContext(g.ctx))
+func (g *GitlabDownloader) GetRepoInfo(ctx context.Context) (*base.Repository, error) {
+ gr, _, err := g.client.Projects.GetProject(g.repoID, nil, nil, gitlab.WithContext(ctx))
if err != nil {
return nil, err
}
@@ -207,8 +200,8 @@ func (g *GitlabDownloader) GetRepoInfo() (*base.Repository, error) {
}
// GetTopics return gitlab topics
-func (g *GitlabDownloader) GetTopics() ([]string, error) {
- gr, _, err := g.client.Projects.GetProject(g.repoID, nil, nil, gitlab.WithContext(g.ctx))
+func (g *GitlabDownloader) GetTopics(ctx context.Context) ([]string, error) {
+ gr, _, err := g.client.Projects.GetProject(g.repoID, nil, nil, gitlab.WithContext(ctx))
if err != nil {
return nil, err
}
@@ -216,7 +209,7 @@ func (g *GitlabDownloader) GetTopics() ([]string, error) {
}
// GetMilestones returns milestones
-func (g *GitlabDownloader) GetMilestones() ([]*base.Milestone, error) {
+func (g *GitlabDownloader) GetMilestones(ctx context.Context) ([]*base.Milestone, error) {
perPage := g.maxPerPage
state := "all"
milestones := make([]*base.Milestone, 0, perPage)
@@ -227,7 +220,7 @@ func (g *GitlabDownloader) GetMilestones() ([]*base.Milestone, error) {
Page: i,
PerPage: perPage,
},
- }, nil, gitlab.WithContext(g.ctx))
+ }, nil, gitlab.WithContext(ctx))
if err != nil {
return nil, err
}
@@ -288,14 +281,14 @@ func (g *GitlabDownloader) normalizeColor(val string) string {
}
// GetLabels returns labels
-func (g *GitlabDownloader) GetLabels() ([]*base.Label, error) {
+func (g *GitlabDownloader) GetLabels(ctx context.Context) ([]*base.Label, error) {
perPage := g.maxPerPage
labels := make([]*base.Label, 0, perPage)
for i := 1; ; i++ {
ls, _, err := g.client.Labels.ListLabels(g.repoID, &gitlab.ListLabelsOptions{ListOptions: gitlab.ListOptions{
Page: i,
PerPage: perPage,
- }}, nil, gitlab.WithContext(g.ctx))
+ }}, nil, gitlab.WithContext(ctx))
if err != nil {
return nil, err
}
@@ -314,7 +307,7 @@ func (g *GitlabDownloader) GetLabels() ([]*base.Label, error) {
return labels, nil
}
-func (g *GitlabDownloader) convertGitlabRelease(rel *gitlab.Release) *base.Release {
+func (g *GitlabDownloader) convertGitlabRelease(ctx context.Context, rel *gitlab.Release) *base.Release {
var zero int
r := &base.Release{
TagName: rel.TagName,
@@ -337,7 +330,7 @@ func (g *GitlabDownloader) convertGitlabRelease(rel *gitlab.Release) *base.Relea
Size: &zero,
DownloadCount: &zero,
DownloadFunc: func() (io.ReadCloser, error) {
- link, _, err := g.client.ReleaseLinks.GetReleaseLink(g.repoID, rel.TagName, assetID, gitlab.WithContext(g.ctx))
+ link, _, err := g.client.ReleaseLinks.GetReleaseLink(g.repoID, rel.TagName, assetID, gitlab.WithContext(ctx))
if err != nil {
return nil, err
}
@@ -351,7 +344,7 @@ func (g *GitlabDownloader) convertGitlabRelease(rel *gitlab.Release) *base.Relea
if err != nil {
return nil, err
}
- req = req.WithContext(g.ctx)
+ req = req.WithContext(ctx)
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
@@ -366,7 +359,7 @@ func (g *GitlabDownloader) convertGitlabRelease(rel *gitlab.Release) *base.Relea
}
// GetReleases returns releases
-func (g *GitlabDownloader) GetReleases() ([]*base.Release, error) {
+func (g *GitlabDownloader) GetReleases(ctx context.Context) ([]*base.Release, error) {
perPage := g.maxPerPage
releases := make([]*base.Release, 0, perPage)
for i := 1; ; i++ {
@@ -375,13 +368,13 @@ func (g *GitlabDownloader) GetReleases() ([]*base.Release, error) {
Page: i,
PerPage: perPage,
},
- }, nil, gitlab.WithContext(g.ctx))
+ }, nil, gitlab.WithContext(ctx))
if err != nil {
return nil, err
}
for _, release := range ls {
- releases = append(releases, g.convertGitlabRelease(release))
+ releases = append(releases, g.convertGitlabRelease(ctx, release))
}
if len(ls) < perPage {
break
@@ -397,7 +390,7 @@ type gitlabIssueContext struct {
// GetIssues returns issues according start and limit
//
// Note: issue label description and colors are not supported by the go-gitlab library at this time
-func (g *GitlabDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, error) {
+func (g *GitlabDownloader) GetIssues(ctx context.Context, page, perPage int) ([]*base.Issue, bool, error) {
state := "all"
sort := "asc"
@@ -416,7 +409,7 @@ func (g *GitlabDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, er
allIssues := make([]*base.Issue, 0, perPage)
- issues, _, err := g.client.Issues.ListProjectIssues(g.repoID, opt, nil, gitlab.WithContext(g.ctx))
+ issues, _, err := g.client.Issues.ListProjectIssues(g.repoID, opt, nil, gitlab.WithContext(ctx))
if err != nil {
return nil, false, fmt.Errorf("error while listing issues: %w", err)
}
@@ -436,7 +429,7 @@ func (g *GitlabDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, er
var reactions []*gitlab.AwardEmoji
awardPage := 1
for {
- awards, _, err := g.client.AwardEmoji.ListIssueAwardEmoji(g.repoID, issue.IID, &gitlab.ListAwardEmojiOptions{Page: awardPage, PerPage: perPage}, gitlab.WithContext(g.ctx))
+ awards, _, err := g.client.AwardEmoji.ListIssueAwardEmoji(g.repoID, issue.IID, &gitlab.ListAwardEmojiOptions{Page: awardPage, PerPage: perPage}, gitlab.WithContext(ctx))
if err != nil {
return nil, false, fmt.Errorf("error while listing issue awards: %w", err)
}
@@ -477,7 +470,7 @@ func (g *GitlabDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, er
// GetComments returns comments according issueNumber
// TODO: figure out how to transfer comment reactions
-func (g *GitlabDownloader) GetComments(commentable base.Commentable) ([]*base.Comment, bool, error) {
+func (g *GitlabDownloader) GetComments(ctx context.Context, commentable base.Commentable) ([]*base.Comment, bool, error) {
context, ok := commentable.GetContext().(gitlabIssueContext)
if !ok {
return nil, false, fmt.Errorf("unexpected context: %+v", commentable.GetContext())
@@ -495,12 +488,12 @@ func (g *GitlabDownloader) GetComments(commentable base.Commentable) ([]*base.Co
comments, resp, err = g.client.Discussions.ListIssueDiscussions(g.repoID, int(commentable.GetForeignIndex()), &gitlab.ListIssueDiscussionsOptions{
Page: page,
PerPage: g.maxPerPage,
- }, nil, gitlab.WithContext(g.ctx))
+ }, nil, gitlab.WithContext(ctx))
} else {
comments, resp, err = g.client.Discussions.ListMergeRequestDiscussions(g.repoID, int(commentable.GetForeignIndex()), &gitlab.ListMergeRequestDiscussionsOptions{
Page: page,
PerPage: g.maxPerPage,
- }, nil, gitlab.WithContext(g.ctx))
+ }, nil, gitlab.WithContext(ctx))
}
if err != nil {
@@ -528,14 +521,14 @@ func (g *GitlabDownloader) GetComments(commentable base.Commentable) ([]*base.Co
Page: page,
PerPage: g.maxPerPage,
},
- }, nil, gitlab.WithContext(g.ctx))
+ }, nil, gitlab.WithContext(ctx))
} else {
stateEvents, resp, err = g.client.ResourceStateEvents.ListIssueStateEvents(g.repoID, int(commentable.GetForeignIndex()), &gitlab.ListStateEventsOptions{
ListOptions: gitlab.ListOptions{
Page: page,
PerPage: g.maxPerPage,
},
- }, nil, gitlab.WithContext(g.ctx))
+ }, nil, gitlab.WithContext(ctx))
}
if err != nil {
return nil, false, fmt.Errorf("error while listing state events: %v %w", g.repoID, err)
@@ -604,7 +597,7 @@ func (g *GitlabDownloader) convertNoteToComment(localIndex int64, note *gitlab.N
}
// GetPullRequests returns pull requests according page and perPage
-func (g *GitlabDownloader) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) {
+func (g *GitlabDownloader) GetPullRequests(ctx context.Context, page, perPage int) ([]*base.PullRequest, bool, error) {
if perPage > g.maxPerPage {
perPage = g.maxPerPage
}
@@ -620,7 +613,7 @@ func (g *GitlabDownloader) GetPullRequests(page, perPage int) ([]*base.PullReque
allPRs := make([]*base.PullRequest, 0, perPage)
- prs, _, err := g.client.MergeRequests.ListProjectMergeRequests(g.repoID, opt, nil, gitlab.WithContext(g.ctx))
+ prs, _, err := g.client.MergeRequests.ListProjectMergeRequests(g.repoID, opt, nil, gitlab.WithContext(ctx))
if err != nil {
return nil, false, fmt.Errorf("error while listing merge requests: %w", err)
}
@@ -673,7 +666,7 @@ func (g *GitlabDownloader) GetPullRequests(page, perPage int) ([]*base.PullReque
var reactions []*gitlab.AwardEmoji
awardPage := 1
for {
- awards, _, err := g.client.AwardEmoji.ListMergeRequestAwardEmoji(g.repoID, pr.IID, &gitlab.ListAwardEmojiOptions{Page: awardPage, PerPage: perPage}, gitlab.WithContext(g.ctx))
+ awards, _, err := g.client.AwardEmoji.ListMergeRequestAwardEmoji(g.repoID, pr.IID, &gitlab.ListAwardEmojiOptions{Page: awardPage, PerPage: perPage}, gitlab.WithContext(ctx))
if err != nil {
return nil, false, fmt.Errorf("error while listing merge requests awards: %w", err)
}
@@ -733,8 +726,8 @@ func (g *GitlabDownloader) GetPullRequests(page, perPage int) ([]*base.PullReque
}
// GetReviews returns pull requests review
-func (g *GitlabDownloader) GetReviews(reviewable base.Reviewable) ([]*base.Review, error) {
- approvals, resp, err := g.client.MergeRequestApprovals.GetConfiguration(g.repoID, int(reviewable.GetForeignIndex()), gitlab.WithContext(g.ctx))
+func (g *GitlabDownloader) GetReviews(ctx context.Context, reviewable base.Reviewable) ([]*base.Review, error) {
+ approvals, resp, err := g.client.MergeRequestApprovals.GetConfiguration(g.repoID, int(reviewable.GetForeignIndex()), gitlab.WithContext(ctx))
if err != nil {
if resp != nil && resp.StatusCode == http.StatusNotFound {
log.Error(fmt.Sprintf("GitlabDownloader: while migrating a error occurred: '%s'", err.Error()))
diff --git a/services/migrations/gitlab_test.go b/services/migrations/gitlab_test.go
index 556fe771c5..223a3b86d7 100644
--- a/services/migrations/gitlab_test.go
+++ b/services/migrations/gitlab_test.go
@@ -31,12 +31,12 @@ func TestGitlabDownloadRepo(t *testing.T) {
if err != nil || resp.StatusCode != http.StatusOK {
t.Skipf("Can't access test repo, skipping %s", t.Name())
}
-
- downloader, err := NewGitlabDownloader(context.Background(), "https://gitlab.com", "gitea/test_repo", "", "", gitlabPersonalAccessToken)
+ ctx := context.Background()
+ downloader, err := NewGitlabDownloader(ctx, "https://gitlab.com", "gitea/test_repo", "", "", gitlabPersonalAccessToken)
if err != nil {
t.Fatalf("NewGitlabDownloader is nil: %v", err)
}
- repo, err := downloader.GetRepoInfo()
+ repo, err := downloader.GetRepoInfo(ctx)
assert.NoError(t, err)
// Repo Owner is blank in Gitlab Group repos
assertRepositoryEqual(t, &base.Repository{
@@ -48,12 +48,12 @@ func TestGitlabDownloadRepo(t *testing.T) {
DefaultBranch: "master",
}, repo)
- topics, err := downloader.GetTopics()
+ topics, err := downloader.GetTopics(ctx)
assert.NoError(t, err)
assert.Len(t, topics, 2)
assert.EqualValues(t, []string{"migration", "test"}, topics)
- milestones, err := downloader.GetMilestones()
+ milestones, err := downloader.GetMilestones(ctx)
assert.NoError(t, err)
assertMilestonesEqual(t, []*base.Milestone{
{
@@ -71,7 +71,7 @@ func TestGitlabDownloadRepo(t *testing.T) {
},
}, milestones)
- labels, err := downloader.GetLabels()
+ labels, err := downloader.GetLabels(ctx)
assert.NoError(t, err)
assertLabelsEqual(t, []*base.Label{
{
@@ -112,7 +112,7 @@ func TestGitlabDownloadRepo(t *testing.T) {
},
}, labels)
- releases, err := downloader.GetReleases()
+ releases, err := downloader.GetReleases(ctx)
assert.NoError(t, err)
assertReleasesEqual(t, []*base.Release{
{
@@ -126,7 +126,7 @@ func TestGitlabDownloadRepo(t *testing.T) {
},
}, releases)
- issues, isEnd, err := downloader.GetIssues(1, 2)
+ issues, isEnd, err := downloader.GetIssues(ctx, 1, 2)
assert.NoError(t, err)
assert.False(t, isEnd)
@@ -214,7 +214,7 @@ func TestGitlabDownloadRepo(t *testing.T) {
},
}, issues)
- comments, _, err := downloader.GetComments(&base.Issue{
+ comments, _, err := downloader.GetComments(ctx, &base.Issue{
Number: 2,
ForeignIndex: 2,
Context: gitlabIssueContext{IsMergeRequest: false},
@@ -255,7 +255,7 @@ func TestGitlabDownloadRepo(t *testing.T) {
},
}, comments)
- prs, _, err := downloader.GetPullRequests(1, 1)
+ prs, _, err := downloader.GetPullRequests(ctx, 1, 1)
assert.NoError(t, err)
assertPullRequestsEqual(t, []*base.PullRequest{
{
@@ -304,7 +304,7 @@ func TestGitlabDownloadRepo(t *testing.T) {
},
}, prs)
- rvs, err := downloader.GetReviews(&base.PullRequest{Number: 1, ForeignIndex: 1})
+ rvs, err := downloader.GetReviews(ctx, &base.PullRequest{Number: 1, ForeignIndex: 1})
assert.NoError(t, err)
assertReviewsEqual(t, []*base.Review{
{
@@ -323,7 +323,7 @@ func TestGitlabDownloadRepo(t *testing.T) {
},
}, rvs)
- rvs, err = downloader.GetReviews(&base.PullRequest{Number: 2, ForeignIndex: 2})
+ rvs, err = downloader.GetReviews(ctx, &base.PullRequest{Number: 2, ForeignIndex: 2})
assert.NoError(t, err)
assertReviewsEqual(t, []*base.Review{
{
@@ -423,9 +423,8 @@ func TestGitlabGetReviews(t *testing.T) {
defer gitlabClientMockTeardown(server)
repoID := 1324
-
+ ctx := context.Background()
downloader := &GitlabDownloader{
- ctx: context.Background(),
client: client,
repoID: repoID,
}
@@ -465,7 +464,7 @@ func TestGitlabGetReviews(t *testing.T) {
mux.HandleFunc(fmt.Sprintf("/api/v4/projects/%d/merge_requests/%d/approvals", testCase.repoID, testCase.prID), mock)
id := int64(testCase.prID)
- rvs, err := downloader.GetReviews(&base.Issue{Number: id, ForeignIndex: id})
+ rvs, err := downloader.GetReviews(ctx, &base.Issue{Number: id, ForeignIndex: id})
assert.NoError(t, err)
assertReviewsEqual(t, []*base.Review{&review}, rvs)
}
diff --git a/services/migrations/gogs.go b/services/migrations/gogs.go
index 72c52d180b..a4f84dbf72 100644
--- a/services/migrations/gogs.go
+++ b/services/migrations/gogs.go
@@ -13,7 +13,6 @@ import (
"code.gitea.io/gitea/modules/log"
base "code.gitea.io/gitea/modules/migration"
- "code.gitea.io/gitea/modules/proxy"
"code.gitea.io/gitea/modules/structs"
"github.com/gogs/go-gogs-client"
@@ -60,16 +59,14 @@ func (f *GogsDownloaderFactory) GitServiceType() structs.GitServiceType {
// from gogs via API
type GogsDownloader struct {
base.NullDownloader
- ctx context.Context
- client *gogs.Client
baseURL string
repoOwner string
repoName string
userName string
password string
+ token string
openIssuesFinished bool
openIssuesPages int
- transport http.RoundTripper
}
// String implements Stringer
@@ -84,53 +81,45 @@ func (g *GogsDownloader) LogString() string {
return fmt.Sprintf("<GogsDownloader %s %s/%s>", g.baseURL, g.repoOwner, g.repoName)
}
-// SetContext set context
-func (g *GogsDownloader) SetContext(ctx context.Context) {
- g.ctx = ctx
-}
-
// NewGogsDownloader creates a gogs Downloader via gogs API
-func NewGogsDownloader(ctx context.Context, baseURL, userName, password, token, repoOwner, repoName string) *GogsDownloader {
+func NewGogsDownloader(_ context.Context, baseURL, userName, password, token, repoOwner, repoName string) *GogsDownloader {
downloader := GogsDownloader{
- ctx: ctx,
baseURL: baseURL,
userName: userName,
password: password,
+ token: token,
repoOwner: repoOwner,
repoName: repoName,
}
+ return &downloader
+}
- var client *gogs.Client
- if len(token) != 0 {
- client = gogs.NewClient(baseURL, token)
- downloader.userName = token
- } else {
- transport := NewMigrationHTTPTransport()
- transport.Proxy = func(req *http.Request) (*url.URL, error) {
- req.SetBasicAuth(userName, password)
- return proxy.Proxy()(req)
- }
- downloader.transport = transport
-
- client = gogs.NewClient(baseURL, "")
- client.SetHTTPClient(&http.Client{
- Transport: &downloader,
- })
- }
+type roundTripperFunc func(req *http.Request) (*http.Response, error)
- downloader.client = client
- return &downloader
+func (rt roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) {
+ return rt(r)
}
-// RoundTrip wraps the provided request within this downloader's context and passes it to our internal http.Transport.
-// This implements http.RoundTripper and makes the gogs client requests cancellable even though it is not cancellable itself
-func (g *GogsDownloader) RoundTrip(req *http.Request) (*http.Response, error) {
- return g.transport.RoundTrip(req.WithContext(g.ctx))
+func (g *GogsDownloader) client(ctx context.Context) *gogs.Client {
+ // Gogs client lacks the context support, so we use a custom transport
+ // Then each request uses a dedicated client with its own context
+ httpTransport := NewMigrationHTTPTransport()
+ gogsClient := gogs.NewClient(g.baseURL, g.token)
+ gogsClient.SetHTTPClient(&http.Client{
+ Transport: roundTripperFunc(func(req *http.Request) (*http.Response, error) {
+ if g.password != "" {
+ // Gogs client lacks the support for basic auth, this is the only way to set it
+ req.SetBasicAuth(g.userName, g.password)
+ }
+ return httpTransport.RoundTrip(req.WithContext(ctx))
+ }),
+ })
+ return gogsClient
}
// GetRepoInfo returns a repository information
-func (g *GogsDownloader) GetRepoInfo() (*base.Repository, error) {
- gr, err := g.client.GetRepo(g.repoOwner, g.repoName)
+func (g *GogsDownloader) GetRepoInfo(ctx context.Context) (*base.Repository, error) {
+ gr, err := g.client(ctx).GetRepo(g.repoOwner, g.repoName)
if err != nil {
return nil, err
}
@@ -148,11 +137,11 @@ func (g *GogsDownloader) GetRepoInfo() (*base.Repository, error) {
}
// GetMilestones returns milestones
-func (g *GogsDownloader) GetMilestones() ([]*base.Milestone, error) {
+func (g *GogsDownloader) GetMilestones(ctx context.Context) ([]*base.Milestone, error) {
perPage := 100
milestones := make([]*base.Milestone, 0, perPage)
- ms, err := g.client.ListRepoMilestones(g.repoOwner, g.repoName)
+ ms, err := g.client(ctx).ListRepoMilestones(g.repoOwner, g.repoName)
if err != nil {
return nil, err
}
@@ -171,10 +160,10 @@ func (g *GogsDownloader) GetMilestones() ([]*base.Milestone, error) {
}
// GetLabels returns labels
-func (g *GogsDownloader) GetLabels() ([]*base.Label, error) {
+func (g *GogsDownloader) GetLabels(ctx context.Context) ([]*base.Label, error) {
perPage := 100
labels := make([]*base.Label, 0, perPage)
- ls, err := g.client.ListRepoLabels(g.repoOwner, g.repoName)
+ ls, err := g.client(ctx).ListRepoLabels(g.repoOwner, g.repoName)
if err != nil {
return nil, err
}
@@ -187,7 +176,7 @@ func (g *GogsDownloader) GetLabels() ([]*base.Label, error) {
}
// GetIssues returns issues according start and limit, perPage is not supported
-func (g *GogsDownloader) GetIssues(page, _ int) ([]*base.Issue, bool, error) {
+func (g *GogsDownloader) GetIssues(ctx context.Context, page, _ int) ([]*base.Issue, bool, error) {
var state string
if g.openIssuesFinished {
state = string(gogs.STATE_CLOSED)
@@ -197,7 +186,7 @@ func (g *GogsDownloader) GetIssues(page, _ int) ([]*base.Issue, bool, error) {
g.openIssuesPages = page
}
- issues, isEnd, err := g.getIssues(page, state)
+ issues, isEnd, err := g.getIssues(ctx, page, state)
if err != nil {
return nil, false, err
}
@@ -212,10 +201,10 @@ func (g *GogsDownloader) GetIssues(page, _ int) ([]*base.Issue, bool, error) {
return issues, false, nil
}
-func (g *GogsDownloader) getIssues(page int, state string) ([]*base.Issue, bool, error) {
+func (g *GogsDownloader) getIssues(ctx context.Context, page int, state string) ([]*base.Issue, bool, error) {
allIssues := make([]*base.Issue, 0, 10)
- issues, err := g.client.ListRepoIssues(g.repoOwner, g.repoName, gogs.ListIssueOption{
+ issues, err := g.client(ctx).ListRepoIssues(g.repoOwner, g.repoName, gogs.ListIssueOption{
Page: page,
State: state,
})
@@ -234,10 +223,10 @@ func (g *GogsDownloader) getIssues(page int, state string) ([]*base.Issue, bool,
}
// GetComments returns comments according issueNumber
-func (g *GogsDownloader) GetComments(commentable base.Commentable) ([]*base.Comment, bool, error) {
+func (g *GogsDownloader) GetComments(ctx context.Context, commentable base.Commentable) ([]*base.Comment, bool, error) {
allComments := make([]*base.Comment, 0, 100)
- comments, err := g.client.ListIssueComments(g.repoOwner, g.repoName, commentable.GetForeignIndex())
+ comments, err := g.client(ctx).ListIssueComments(g.repoOwner, g.repoName, commentable.GetForeignIndex())
if err != nil {
return nil, false, fmt.Errorf("error while listing repos: %w", err)
}
@@ -261,7 +250,7 @@ func (g *GogsDownloader) GetComments(commentable base.Commentable) ([]*base.Comm
}
// GetTopics return repository topics
-func (g *GogsDownloader) GetTopics() ([]string, error) {
+func (g *GogsDownloader) GetTopics(_ context.Context) ([]string, error) {
return []string{}, nil
}
diff --git a/services/migrations/gogs_test.go b/services/migrations/gogs_test.go
index 610af183de..91c36bdcc6 100644
--- a/services/migrations/gogs_test.go
+++ b/services/migrations/gogs_test.go
@@ -28,9 +28,9 @@ func TestGogsDownloadRepo(t *testing.T) {
t.Skipf("visit test repo failed, ignored")
return
}
-
- downloader := NewGogsDownloader(context.Background(), "https://try.gogs.io", "", "", gogsPersonalAccessToken, "lunnytest", "TESTREPO")
- repo, err := downloader.GetRepoInfo()
+ ctx := context.Background()
+ downloader := NewGogsDownloader(ctx, "https://try.gogs.io", "", "", gogsPersonalAccessToken, "lunnytest", "TESTREPO")
+ repo, err := downloader.GetRepoInfo(ctx)
assert.NoError(t, err)
assertRepositoryEqual(t, &base.Repository{
@@ -42,7 +42,7 @@ func TestGogsDownloadRepo(t *testing.T) {
DefaultBranch: "master",
}, repo)
- milestones, err := downloader.GetMilestones()
+ milestones, err := downloader.GetMilestones(ctx)
assert.NoError(t, err)
assertMilestonesEqual(t, []*base.Milestone{
{
@@ -51,7 +51,7 @@ func TestGogsDownloadRepo(t *testing.T) {
},
}, milestones)
- labels, err := downloader.GetLabels()
+ labels, err := downloader.GetLabels(ctx)
assert.NoError(t, err)
assertLabelsEqual(t, []*base.Label{
{
@@ -85,7 +85,7 @@ func TestGogsDownloadRepo(t *testing.T) {
}, labels)
// downloader.GetIssues()
- issues, isEnd, err := downloader.GetIssues(1, 8)
+ issues, isEnd, err := downloader.GetIssues(ctx, 1, 8)
assert.NoError(t, err)
assert.False(t, isEnd)
assertIssuesEqual(t, []*base.Issue{
@@ -110,7 +110,7 @@ func TestGogsDownloadRepo(t *testing.T) {
}, issues)
// downloader.GetComments()
- comments, _, err := downloader.GetComments(&base.Issue{Number: 1, ForeignIndex: 1})
+ comments, _, err := downloader.GetComments(ctx, &base.Issue{Number: 1, ForeignIndex: 1})
assert.NoError(t, err)
assertCommentsEqual(t, []*base.Comment{
{
@@ -134,6 +134,6 @@ func TestGogsDownloadRepo(t *testing.T) {
}, comments)
// downloader.GetPullRequests()
- _, _, err = downloader.GetPullRequests(1, 3)
+ _, _, err = downloader.GetPullRequests(ctx, 1, 3)
assert.Error(t, err)
}
diff --git a/services/migrations/migrate.go b/services/migrations/migrate.go
index 51b22d6111..8319fd541b 100644
--- a/services/migrations/migrate.go
+++ b/services/migrations/migrate.go
@@ -176,12 +176,12 @@ func newDownloader(ctx context.Context, ownerName string, opts base.MigrateOptio
// migrateRepository will download information and then upload it to Uploader, this is a simple
// process for small repository. For a big repository, save all the data to disk
// before upload is better
-func migrateRepository(_ context.Context, doer *user_model.User, downloader base.Downloader, uploader base.Uploader, opts base.MigrateOptions, messenger base.Messenger) error {
+func migrateRepository(ctx context.Context, doer *user_model.User, downloader base.Downloader, uploader base.Uploader, opts base.MigrateOptions, messenger base.Messenger) error {
if messenger == nil {
messenger = base.NilMessenger
}
- repo, err := downloader.GetRepoInfo()
+ repo, err := downloader.GetRepoInfo(ctx)
if err != nil {
if !base.IsErrNotSupported(err) {
return err
@@ -220,14 +220,14 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base
log.Trace("migrating git data from %s", repo.CloneURL)
messenger("repo.migrate.migrating_git")
- if err = uploader.CreateRepo(repo, opts); err != nil {
+ if err = uploader.CreateRepo(ctx, repo, opts); err != nil {
return err
}
defer uploader.Close()
log.Trace("migrating topics")
messenger("repo.migrate.migrating_topics")
- topics, err := downloader.GetTopics()
+ topics, err := downloader.GetTopics(ctx)
if err != nil {
if !base.IsErrNotSupported(err) {
return err
@@ -235,7 +235,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base
log.Warn("migrating topics is not supported, ignored")
}
if len(topics) != 0 {
- if err = uploader.CreateTopics(topics...); err != nil {
+ if err = uploader.CreateTopics(ctx, topics...); err != nil {
return err
}
}
@@ -243,7 +243,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base
if opts.Milestones {
log.Trace("migrating milestones")
messenger("repo.migrate.migrating_milestones")
- milestones, err := downloader.GetMilestones()
+ milestones, err := downloader.GetMilestones(ctx)
if err != nil {
if !base.IsErrNotSupported(err) {
return err
@@ -256,7 +256,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base
msBatchSize = len(milestones)
}
- if err := uploader.CreateMilestones(milestones[:msBatchSize]...); err != nil {
+ if err := uploader.CreateMilestones(ctx, milestones[:msBatchSize]...); err != nil {
return err
}
milestones = milestones[msBatchSize:]
@@ -266,7 +266,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base
if opts.Labels {
log.Trace("migrating labels")
messenger("repo.migrate.migrating_labels")
- labels, err := downloader.GetLabels()
+ labels, err := downloader.GetLabels(ctx)
if err != nil {
if !base.IsErrNotSupported(err) {
return err
@@ -280,7 +280,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base
lbBatchSize = len(labels)
}
- if err := uploader.CreateLabels(labels[:lbBatchSize]...); err != nil {
+ if err := uploader.CreateLabels(ctx, labels[:lbBatchSize]...); err != nil {
return err
}
labels = labels[lbBatchSize:]
@@ -290,7 +290,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base
if opts.Releases {
log.Trace("migrating releases")
messenger("repo.migrate.migrating_releases")
- releases, err := downloader.GetReleases()
+ releases, err := downloader.GetReleases(ctx)
if err != nil {
if !base.IsErrNotSupported(err) {
return err
@@ -304,14 +304,14 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base
relBatchSize = len(releases)
}
- if err = uploader.CreateReleases(releases[:relBatchSize]...); err != nil {
+ if err = uploader.CreateReleases(ctx, releases[:relBatchSize]...); err != nil {
return err
}
releases = releases[relBatchSize:]
}
// Once all releases (if any) are inserted, sync any remaining non-release tags
- if err = uploader.SyncTags(); err != nil {
+ if err = uploader.SyncTags(ctx); err != nil {
return err
}
}
@@ -329,7 +329,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base
issueBatchSize := uploader.MaxBatchInsertSize("issue")
for i := 1; ; i++ {
- issues, isEnd, err := downloader.GetIssues(i, issueBatchSize)
+ issues, isEnd, err := downloader.GetIssues(ctx, i, issueBatchSize)
if err != nil {
if !base.IsErrNotSupported(err) {
return err
@@ -338,7 +338,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base
break
}
- if err := uploader.CreateIssues(issues...); err != nil {
+ if err := uploader.CreateIssues(ctx, issues...); err != nil {
return err
}
@@ -346,7 +346,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base
allComments := make([]*base.Comment, 0, commentBatchSize)
for _, issue := range issues {
log.Trace("migrating issue %d's comments", issue.Number)
- comments, _, err := downloader.GetComments(issue)
+ comments, _, err := downloader.GetComments(ctx, issue)
if err != nil {
if !base.IsErrNotSupported(err) {
return err
@@ -357,7 +357,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base
allComments = append(allComments, comments...)
if len(allComments) >= commentBatchSize {
- if err = uploader.CreateComments(allComments[:commentBatchSize]...); err != nil {
+ if err = uploader.CreateComments(ctx, allComments[:commentBatchSize]...); err != nil {
return err
}
@@ -366,7 +366,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base
}
if len(allComments) > 0 {
- if err = uploader.CreateComments(allComments...); err != nil {
+ if err = uploader.CreateComments(ctx, allComments...); err != nil {
return err
}
}
@@ -383,7 +383,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base
messenger("repo.migrate.migrating_pulls")
prBatchSize := uploader.MaxBatchInsertSize("pullrequest")
for i := 1; ; i++ {
- prs, isEnd, err := downloader.GetPullRequests(i, prBatchSize)
+ prs, isEnd, err := downloader.GetPullRequests(ctx, i, prBatchSize)
if err != nil {
if !base.IsErrNotSupported(err) {
return err
@@ -392,7 +392,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base
break
}
- if err := uploader.CreatePullRequests(prs...); err != nil {
+ if err := uploader.CreatePullRequests(ctx, prs...); err != nil {
return err
}
@@ -402,7 +402,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base
allComments := make([]*base.Comment, 0, commentBatchSize)
for _, pr := range prs {
log.Trace("migrating pull request %d's comments", pr.Number)
- comments, _, err := downloader.GetComments(pr)
+ comments, _, err := downloader.GetComments(ctx, pr)
if err != nil {
if !base.IsErrNotSupported(err) {
return err
@@ -413,14 +413,14 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base
allComments = append(allComments, comments...)
if len(allComments) >= commentBatchSize {
- if err = uploader.CreateComments(allComments[:commentBatchSize]...); err != nil {
+ if err = uploader.CreateComments(ctx, allComments[:commentBatchSize]...); err != nil {
return err
}
allComments = allComments[commentBatchSize:]
}
}
if len(allComments) > 0 {
- if err = uploader.CreateComments(allComments...); err != nil {
+ if err = uploader.CreateComments(ctx, allComments...); err != nil {
return err
}
}
@@ -429,7 +429,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base
// migrate reviews
allReviews := make([]*base.Review, 0, reviewBatchSize)
for _, pr := range prs {
- reviews, err := downloader.GetReviews(pr)
+ reviews, err := downloader.GetReviews(ctx, pr)
if err != nil {
if !base.IsErrNotSupported(err) {
return err
@@ -441,14 +441,14 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base
allReviews = append(allReviews, reviews...)
if len(allReviews) >= reviewBatchSize {
- if err = uploader.CreateReviews(allReviews[:reviewBatchSize]...); err != nil {
+ if err = uploader.CreateReviews(ctx, allReviews[:reviewBatchSize]...); err != nil {
return err
}
allReviews = allReviews[reviewBatchSize:]
}
}
if len(allReviews) > 0 {
- if err = uploader.CreateReviews(allReviews...); err != nil {
+ if err = uploader.CreateReviews(ctx, allReviews...); err != nil {
return err
}
}
@@ -463,12 +463,12 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base
if opts.Comments && supportAllComments {
log.Trace("migrating comments")
for i := 1; ; i++ {
- comments, isEnd, err := downloader.GetAllComments(i, commentBatchSize)
+ comments, isEnd, err := downloader.GetAllComments(ctx, i, commentBatchSize)
if err != nil {
return err
}
- if err := uploader.CreateComments(comments...); err != nil {
+ if err := uploader.CreateComments(ctx, comments...); err != nil {
return err
}
@@ -478,7 +478,7 @@ func migrateRepository(_ context.Context, doer *user_model.User, downloader base
}
}
- return uploader.Finish()
+ return uploader.Finish(ctx)
}
// Init migrations service
diff --git a/services/migrations/onedev.go b/services/migrations/onedev.go
index e2f7b771f3..4ce35dd12e 100644
--- a/services/migrations/onedev.go
+++ b/services/migrations/onedev.go
@@ -71,7 +71,6 @@ type onedevUser struct {
// from OneDev
type OneDevDownloader struct {
base.NullDownloader
- ctx context.Context
client *http.Client
baseURL *url.URL
repoName string
@@ -81,15 +80,9 @@ type OneDevDownloader struct {
milestoneMap map[int64]string
}
-// SetContext set context
-func (d *OneDevDownloader) SetContext(ctx context.Context) {
- d.ctx = ctx
-}
-
// NewOneDevDownloader creates a new downloader
-func NewOneDevDownloader(ctx context.Context, baseURL *url.URL, username, password, repoName string) *OneDevDownloader {
+func NewOneDevDownloader(_ context.Context, baseURL *url.URL, username, password, repoName string) *OneDevDownloader {
downloader := &OneDevDownloader{
- ctx: ctx,
baseURL: baseURL,
repoName: repoName,
client: &http.Client{
@@ -121,7 +114,7 @@ func (d *OneDevDownloader) LogString() string {
return fmt.Sprintf("<OneDevDownloader %s [%d]/%s>", d.baseURL, d.repoID, d.repoName)
}
-func (d *OneDevDownloader) callAPI(endpoint string, parameter map[string]string, result any) error {
+func (d *OneDevDownloader) callAPI(ctx context.Context, endpoint string, parameter map[string]string, result any) error {
u, err := d.baseURL.Parse(endpoint)
if err != nil {
return err
@@ -135,7 +128,7 @@ func (d *OneDevDownloader) callAPI(endpoint string, parameter map[string]string,
u.RawQuery = query.Encode()
}
- req, err := http.NewRequestWithContext(d.ctx, "GET", u.String(), nil)
+ req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil)
if err != nil {
return err
}
@@ -151,7 +144,7 @@ func (d *OneDevDownloader) callAPI(endpoint string, parameter map[string]string,
}
// GetRepoInfo returns repository information
-func (d *OneDevDownloader) GetRepoInfo() (*base.Repository, error) {
+func (d *OneDevDownloader) GetRepoInfo(ctx context.Context) (*base.Repository, error) {
info := make([]struct {
ID int64 `json:"id"`
Name string `json:"name"`
@@ -159,6 +152,7 @@ func (d *OneDevDownloader) GetRepoInfo() (*base.Repository, error) {
}, 0, 1)
err := d.callAPI(
+ ctx,
"/api/projects",
map[string]string{
"query": `"Name" is "` + d.repoName + `"`,
@@ -194,7 +188,7 @@ func (d *OneDevDownloader) GetRepoInfo() (*base.Repository, error) {
}
// GetMilestones returns milestones
-func (d *OneDevDownloader) GetMilestones() ([]*base.Milestone, error) {
+func (d *OneDevDownloader) GetMilestones(ctx context.Context) ([]*base.Milestone, error) {
rawMilestones := make([]struct {
ID int64 `json:"id"`
Name string `json:"name"`
@@ -209,6 +203,7 @@ func (d *OneDevDownloader) GetMilestones() ([]*base.Milestone, error) {
offset := 0
for {
err := d.callAPI(
+ ctx,
endpoint,
map[string]string{
"offset": strconv.Itoa(offset),
@@ -243,7 +238,7 @@ func (d *OneDevDownloader) GetMilestones() ([]*base.Milestone, error) {
}
// GetLabels returns labels
-func (d *OneDevDownloader) GetLabels() ([]*base.Label, error) {
+func (d *OneDevDownloader) GetLabels(_ context.Context) ([]*base.Label, error) {
return []*base.Label{
{
Name: "Bug",
@@ -277,7 +272,7 @@ type onedevIssueContext struct {
}
// GetIssues returns issues
-func (d *OneDevDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, error) {
+func (d *OneDevDownloader) GetIssues(ctx context.Context, page, perPage int) ([]*base.Issue, bool, error) {
rawIssues := make([]struct {
ID int64 `json:"id"`
Number int64 `json:"number"`
@@ -289,6 +284,7 @@ func (d *OneDevDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, er
}, 0, perPage)
err := d.callAPI(
+ ctx,
"/api/issues",
map[string]string{
"query": `"Project" is "` + d.repoName + `"`,
@@ -308,6 +304,7 @@ func (d *OneDevDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, er
Value string `json:"value"`
}, 0, 10)
err := d.callAPI(
+ ctx,
fmt.Sprintf("/api/issues/%d/fields", issue.ID),
nil,
&fields,
@@ -329,6 +326,7 @@ func (d *OneDevDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, er
Name string `json:"name"`
}, 0, 10)
err = d.callAPI(
+ ctx,
fmt.Sprintf("/api/issues/%d/milestones", issue.ID),
nil,
&milestones,
@@ -345,7 +343,7 @@ func (d *OneDevDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, er
if state == "released" {
state = "closed"
}
- poster := d.tryGetUser(issue.SubmitterID)
+ poster := d.tryGetUser(ctx, issue.SubmitterID)
issues = append(issues, &base.Issue{
Title: issue.Title,
Number: issue.Number,
@@ -370,7 +368,7 @@ func (d *OneDevDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, er
}
// GetComments returns comments
-func (d *OneDevDownloader) GetComments(commentable base.Commentable) ([]*base.Comment, bool, error) {
+func (d *OneDevDownloader) GetComments(ctx context.Context, commentable base.Commentable) ([]*base.Comment, bool, error) {
context, ok := commentable.GetContext().(onedevIssueContext)
if !ok {
return nil, false, fmt.Errorf("unexpected context: %+v", commentable.GetContext())
@@ -391,6 +389,7 @@ func (d *OneDevDownloader) GetComments(commentable base.Commentable) ([]*base.Co
}
err := d.callAPI(
+ ctx,
endpoint,
nil,
&rawComments,
@@ -412,6 +411,7 @@ func (d *OneDevDownloader) GetComments(commentable base.Commentable) ([]*base.Co
}
err = d.callAPI(
+ ctx,
endpoint,
nil,
&rawChanges,
@@ -425,7 +425,7 @@ func (d *OneDevDownloader) GetComments(commentable base.Commentable) ([]*base.Co
if len(comment.Content) == 0 {
continue
}
- poster := d.tryGetUser(comment.UserID)
+ poster := d.tryGetUser(ctx, comment.UserID)
comments = append(comments, &base.Comment{
IssueIndex: commentable.GetLocalIndex(),
Index: comment.ID,
@@ -450,7 +450,7 @@ func (d *OneDevDownloader) GetComments(commentable base.Commentable) ([]*base.Co
continue
}
- poster := d.tryGetUser(change.UserID)
+ poster := d.tryGetUser(ctx, change.UserID)
comments = append(comments, &base.Comment{
IssueIndex: commentable.GetLocalIndex(),
PosterID: poster.ID,
@@ -466,7 +466,7 @@ func (d *OneDevDownloader) GetComments(commentable base.Commentable) ([]*base.Co
}
// GetPullRequests returns pull requests
-func (d *OneDevDownloader) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) {
+func (d *OneDevDownloader) GetPullRequests(ctx context.Context, page, perPage int) ([]*base.PullRequest, bool, error) {
rawPullRequests := make([]struct {
ID int64 `json:"id"`
Number int64 `json:"number"`
@@ -484,6 +484,7 @@ func (d *OneDevDownloader) GetPullRequests(page, perPage int) ([]*base.PullReque
}, 0, perPage)
err := d.callAPI(
+ ctx,
"/api/pull-requests",
map[string]string{
"query": `"Target Project" is "` + d.repoName + `"`,
@@ -505,6 +506,7 @@ func (d *OneDevDownloader) GetPullRequests(page, perPage int) ([]*base.PullReque
MergeCommitHash string `json:"mergeCommitHash"`
}
err := d.callAPI(
+ ctx,
fmt.Sprintf("/api/pull-requests/%d/merge-preview", pr.ID),
nil,
&mergePreview,
@@ -525,7 +527,7 @@ func (d *OneDevDownloader) GetPullRequests(page, perPage int) ([]*base.PullReque
mergedTime = pr.CloseInfo.Date
}
}
- poster := d.tryGetUser(pr.SubmitterID)
+ poster := d.tryGetUser(ctx, pr.SubmitterID)
number := pr.Number + d.maxIssueIndex
pullRequests = append(pullRequests, &base.PullRequest{
@@ -562,7 +564,7 @@ func (d *OneDevDownloader) GetPullRequests(page, perPage int) ([]*base.PullReque
}
// GetReviews returns pull requests reviews
-func (d *OneDevDownloader) GetReviews(reviewable base.Reviewable) ([]*base.Review, error) {
+func (d *OneDevDownloader) GetReviews(ctx context.Context, reviewable base.Reviewable) ([]*base.Review, error) {
rawReviews := make([]struct {
ID int64 `json:"id"`
UserID int64 `json:"userId"`
@@ -574,6 +576,7 @@ func (d *OneDevDownloader) GetReviews(reviewable base.Reviewable) ([]*base.Revie
}, 0, 100)
err := d.callAPI(
+ ctx,
fmt.Sprintf("/api/pull-requests/%d/reviews", reviewable.GetForeignIndex()),
nil,
&rawReviews,
@@ -596,7 +599,7 @@ func (d *OneDevDownloader) GetReviews(reviewable base.Reviewable) ([]*base.Revie
}
}
- poster := d.tryGetUser(review.UserID)
+ poster := d.tryGetUser(ctx, review.UserID)
reviews = append(reviews, &base.Review{
IssueIndex: reviewable.GetLocalIndex(),
ReviewerID: poster.ID,
@@ -610,14 +613,15 @@ func (d *OneDevDownloader) GetReviews(reviewable base.Reviewable) ([]*base.Revie
}
// GetTopics return repository topics
-func (d *OneDevDownloader) GetTopics() ([]string, error) {
+func (d *OneDevDownloader) GetTopics(_ context.Context) ([]string, error) {
return []string{}, nil
}
-func (d *OneDevDownloader) tryGetUser(userID int64) *onedevUser {
+func (d *OneDevDownloader) tryGetUser(ctx context.Context, userID int64) *onedevUser {
user, ok := d.userMap[userID]
if !ok {
err := d.callAPI(
+ ctx,
fmt.Sprintf("/api/users/%d", userID),
nil,
&user,
diff --git a/services/migrations/onedev_test.go b/services/migrations/onedev_test.go
index 48412fec64..0a4b05446d 100644
--- a/services/migrations/onedev_test.go
+++ b/services/migrations/onedev_test.go
@@ -22,11 +22,12 @@ func TestOneDevDownloadRepo(t *testing.T) {
}
u, _ := url.Parse("https://code.onedev.io")
- downloader := NewOneDevDownloader(context.Background(), u, "", "", "go-gitea-test_repo")
+ ctx := context.Background()
+ downloader := NewOneDevDownloader(ctx, u, "", "", "go-gitea-test_repo")
if err != nil {
t.Fatalf("NewOneDevDownloader is nil: %v", err)
}
- repo, err := downloader.GetRepoInfo()
+ repo, err := downloader.GetRepoInfo(ctx)
assert.NoError(t, err)
assertRepositoryEqual(t, &base.Repository{
Name: "go-gitea-test_repo",
@@ -36,7 +37,7 @@ func TestOneDevDownloadRepo(t *testing.T) {
OriginalURL: "https://code.onedev.io/projects/go-gitea-test_repo",
}, repo)
- milestones, err := downloader.GetMilestones()
+ milestones, err := downloader.GetMilestones(ctx)
assert.NoError(t, err)
deadline := time.Unix(1620086400, 0)
assertMilestonesEqual(t, []*base.Milestone{
@@ -51,11 +52,11 @@ func TestOneDevDownloadRepo(t *testing.T) {
},
}, milestones)
- labels, err := downloader.GetLabels()
+ labels, err := downloader.GetLabels(ctx)
assert.NoError(t, err)
assert.Len(t, labels, 6)
- issues, isEnd, err := downloader.GetIssues(1, 2)
+ issues, isEnd, err := downloader.GetIssues(ctx, 1, 2)
assert.NoError(t, err)
assert.False(t, isEnd)
assertIssuesEqual(t, []*base.Issue{
@@ -94,7 +95,7 @@ func TestOneDevDownloadRepo(t *testing.T) {
},
}, issues)
- comments, _, err := downloader.GetComments(&base.Issue{
+ comments, _, err := downloader.GetComments(ctx, &base.Issue{
Number: 4,
ForeignIndex: 398,
Context: onedevIssueContext{IsPullRequest: false},
@@ -110,7 +111,7 @@ func TestOneDevDownloadRepo(t *testing.T) {
},
}, comments)
- prs, _, err := downloader.GetPullRequests(1, 1)
+ prs, _, err := downloader.GetPullRequests(ctx, 1, 1)
assert.NoError(t, err)
assertPullRequestsEqual(t, []*base.PullRequest{
{
@@ -136,7 +137,7 @@ func TestOneDevDownloadRepo(t *testing.T) {
},
}, prs)
- rvs, err := downloader.GetReviews(&base.PullRequest{Number: 5, ForeignIndex: 186})
+ rvs, err := downloader.GetReviews(ctx, &base.PullRequest{Number: 5, ForeignIndex: 186})
assert.NoError(t, err)
assertReviewsEqual(t, []*base.Review{
{
diff --git a/services/migrations/restore.go b/services/migrations/restore.go
index fd337b22c7..5686285935 100644
--- a/services/migrations/restore.go
+++ b/services/migrations/restore.go
@@ -18,7 +18,6 @@ import (
// RepositoryRestorer implements an Downloader from the local directory
type RepositoryRestorer struct {
base.NullDownloader
- ctx context.Context
baseDir string
repoOwner string
repoName string
@@ -26,13 +25,12 @@ type RepositoryRestorer struct {
}
// NewRepositoryRestorer creates a repository restorer which could restore repository from a dumped folder
-func NewRepositoryRestorer(ctx context.Context, baseDir, owner, repoName string, validation bool) (*RepositoryRestorer, error) {
+func NewRepositoryRestorer(_ context.Context, baseDir, owner, repoName string, validation bool) (*RepositoryRestorer, error) {
baseDir, err := filepath.Abs(baseDir)
if err != nil {
return nil, err
}
return &RepositoryRestorer{
- ctx: ctx,
baseDir: baseDir,
repoOwner: owner,
repoName: repoName,
@@ -48,11 +46,6 @@ func (r *RepositoryRestorer) reviewDir() string {
return filepath.Join(r.baseDir, "reviews")
}
-// SetContext set context
-func (r *RepositoryRestorer) SetContext(ctx context.Context) {
- r.ctx = ctx
-}
-
func (r *RepositoryRestorer) getRepoOptions() (map[string]string, error) {
p := filepath.Join(r.baseDir, "repo.yml")
bs, err := os.ReadFile(p)
@@ -69,7 +62,7 @@ func (r *RepositoryRestorer) getRepoOptions() (map[string]string, error) {
}
// GetRepoInfo returns a repository information
-func (r *RepositoryRestorer) GetRepoInfo() (*base.Repository, error) {
+func (r *RepositoryRestorer) GetRepoInfo(_ context.Context) (*base.Repository, error) {
opts, err := r.getRepoOptions()
if err != nil {
return nil, err
@@ -89,7 +82,7 @@ func (r *RepositoryRestorer) GetRepoInfo() (*base.Repository, error) {
}
// GetTopics return github topics
-func (r *RepositoryRestorer) GetTopics() ([]string, error) {
+func (r *RepositoryRestorer) GetTopics(_ context.Context) ([]string, error) {
p := filepath.Join(r.baseDir, "topic.yml")
topics := struct {
@@ -112,7 +105,7 @@ func (r *RepositoryRestorer) GetTopics() ([]string, error) {
}
// GetMilestones returns milestones
-func (r *RepositoryRestorer) GetMilestones() ([]*base.Milestone, error) {
+func (r *RepositoryRestorer) GetMilestones(_ context.Context) ([]*base.Milestone, error) {
milestones := make([]*base.Milestone, 0, 10)
p := filepath.Join(r.baseDir, "milestone.yml")
err := base.Load(p, &milestones, r.validation)
@@ -127,7 +120,7 @@ func (r *RepositoryRestorer) GetMilestones() ([]*base.Milestone, error) {
}
// GetReleases returns releases
-func (r *RepositoryRestorer) GetReleases() ([]*base.Release, error) {
+func (r *RepositoryRestorer) GetReleases(_ context.Context) ([]*base.Release, error) {
releases := make([]*base.Release, 0, 10)
p := filepath.Join(r.baseDir, "release.yml")
_, err := os.Stat(p)
@@ -158,7 +151,7 @@ func (r *RepositoryRestorer) GetReleases() ([]*base.Release, error) {
}
// GetLabels returns labels
-func (r *RepositoryRestorer) GetLabels() ([]*base.Label, error) {
+func (r *RepositoryRestorer) GetLabels(_ context.Context) ([]*base.Label, error) {
labels := make([]*base.Label, 0, 10)
p := filepath.Join(r.baseDir, "label.yml")
_, err := os.Stat(p)
@@ -182,7 +175,7 @@ func (r *RepositoryRestorer) GetLabels() ([]*base.Label, error) {
}
// GetIssues returns issues according start and limit
-func (r *RepositoryRestorer) GetIssues(page, perPage int) ([]*base.Issue, bool, error) {
+func (r *RepositoryRestorer) GetIssues(_ context.Context, _, _ int) ([]*base.Issue, bool, error) {
issues := make([]*base.Issue, 0, 10)
p := filepath.Join(r.baseDir, "issue.yml")
err := base.Load(p, &issues, r.validation)
@@ -196,7 +189,7 @@ func (r *RepositoryRestorer) GetIssues(page, perPage int) ([]*base.Issue, bool,
}
// GetComments returns comments according issueNumber
-func (r *RepositoryRestorer) GetComments(commentable base.Commentable) ([]*base.Comment, bool, error) {
+func (r *RepositoryRestorer) GetComments(_ context.Context, commentable base.Commentable) ([]*base.Comment, bool, error) {
comments := make([]*base.Comment, 0, 10)
p := filepath.Join(r.commentDir(), fmt.Sprintf("%d.yml", commentable.GetForeignIndex()))
_, err := os.Stat(p)
@@ -220,7 +213,7 @@ func (r *RepositoryRestorer) GetComments(commentable base.Commentable) ([]*base.
}
// GetPullRequests returns pull requests according page and perPage
-func (r *RepositoryRestorer) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) {
+func (r *RepositoryRestorer) GetPullRequests(_ context.Context, page, perPage int) ([]*base.PullRequest, bool, error) {
pulls := make([]*base.PullRequest, 0, 10)
p := filepath.Join(r.baseDir, "pull_request.yml")
_, err := os.Stat(p)
@@ -248,7 +241,7 @@ func (r *RepositoryRestorer) GetPullRequests(page, perPage int) ([]*base.PullReq
}
// GetReviews returns pull requests review
-func (r *RepositoryRestorer) GetReviews(reviewable base.Reviewable) ([]*base.Review, error) {
+func (r *RepositoryRestorer) GetReviews(ctx context.Context, reviewable base.Reviewable) ([]*base.Review, error) {
reviews := make([]*base.Review, 0, 10)
p := filepath.Join(r.reviewDir(), fmt.Sprintf("%d.yml", reviewable.GetForeignIndex()))
_, err := os.Stat(p)
diff --git a/services/projects/issue.go b/services/projects/issue.go
index db1621a39f..6ca0f16806 100644
--- a/services/projects/issue.go
+++ b/services/projects/issue.go
@@ -55,22 +55,29 @@ func MoveIssuesOnProjectColumn(ctx context.Context, doer *user_model.User, colum
continue
}
- _, err = db.Exec(ctx, "UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", column.ID, sorting, issueID)
+ projectColumnID, err := curIssue.ProjectColumnID(ctx)
if err != nil {
return err
}
- // add timeline to issue
- if _, err := issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{
- Type: issues_model.CommentTypeProjectColumn,
- Doer: doer,
- Repo: curIssue.Repo,
- Issue: curIssue,
- ProjectID: column.ProjectID,
- ProjectTitle: project.Title,
- ProjectColumnID: column.ID,
- ProjectColumnTitle: column.Title,
- }); err != nil {
+ if projectColumnID != column.ID {
+ // add timeline to issue
+ if _, err := issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{
+ Type: issues_model.CommentTypeProjectColumn,
+ Doer: doer,
+ Repo: curIssue.Repo,
+ Issue: curIssue,
+ ProjectID: column.ProjectID,
+ ProjectTitle: project.Title,
+ ProjectColumnID: column.ID,
+ ProjectColumnTitle: column.Title,
+ }); err != nil {
+ return err
+ }
+ }
+
+ _, err = db.Exec(ctx, "UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", column.ID, sorting, issueID)
+ if err != nil {
return err
}
}
diff --git a/modules/gitgraph/graph.go b/services/repository/gitgraph/graph.go
index 7e12be030f..7e12be030f 100644
--- a/modules/gitgraph/graph.go
+++ b/services/repository/gitgraph/graph.go
diff --git a/modules/gitgraph/graph_models.go b/services/repository/gitgraph/graph_models.go
index 191b0b3afc..191b0b3afc 100644
--- a/modules/gitgraph/graph_models.go
+++ b/services/repository/gitgraph/graph_models.go
diff --git a/modules/gitgraph/graph_test.go b/services/repository/gitgraph/graph_test.go
index 2f647aaf83..2f647aaf83 100644
--- a/modules/gitgraph/graph_test.go
+++ b/services/repository/gitgraph/graph_test.go
diff --git a/modules/gitgraph/parser.go b/services/repository/gitgraph/parser.go
index f6bf9b0b90..f6bf9b0b90 100644
--- a/modules/gitgraph/parser.go
+++ b/services/repository/gitgraph/parser.go
diff --git a/services/webhook/dingtalk.go b/services/webhook/dingtalk.go
index 992b8c566f..3ea8f50764 100644
--- a/services/webhook/dingtalk.go
+++ b/services/webhook/dingtalk.go
@@ -170,6 +170,12 @@ func (dc dingtalkConvertor) Package(p *api.PackagePayload) (DingtalkPayload, err
return createDingtalkPayload(text, text, "view package", p.Package.HTMLURL), nil
}
+func (dc dingtalkConvertor) Status(p *api.CommitStatusPayload) (DingtalkPayload, error) {
+ text, _ := getStatusPayloadInfo(p, noneLinkFormatter, true)
+
+ return createDingtalkPayload(text, text, "Status Changed", p.TargetURL), nil
+}
+
func createDingtalkPayload(title, text, singleTitle, singleURL string) DingtalkPayload {
return DingtalkPayload{
MsgType: "actionCard",
diff --git a/services/webhook/discord.go b/services/webhook/discord.go
index 30d930062e..43e5e533bf 100644
--- a/services/webhook/discord.go
+++ b/services/webhook/discord.go
@@ -265,6 +265,12 @@ func (d discordConvertor) Package(p *api.PackagePayload) (DiscordPayload, error)
return d.createPayload(p.Sender, text, "", p.Package.HTMLURL, color), nil
}
+func (d discordConvertor) Status(p *api.CommitStatusPayload) (DiscordPayload, error) {
+ text, color := getStatusPayloadInfo(p, noneLinkFormatter, false)
+
+ return d.createPayload(p.Sender, text, "", p.TargetURL, color), nil
+}
+
func newDiscordRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
meta := &DiscordMeta{}
if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
diff --git a/services/webhook/feishu.go b/services/webhook/feishu.go
index 4e6aebc39d..639118d2a5 100644
--- a/services/webhook/feishu.go
+++ b/services/webhook/feishu.go
@@ -166,6 +166,12 @@ func (fc feishuConvertor) Package(p *api.PackagePayload) (FeishuPayload, error)
return newFeishuTextPayload(text), nil
}
+func (fc feishuConvertor) Status(p *api.CommitStatusPayload) (FeishuPayload, error) {
+ text, _ := getStatusPayloadInfo(p, noneLinkFormatter, true)
+
+ return newFeishuTextPayload(text), nil
+}
+
func newFeishuRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
var pc payloadConvertor[FeishuPayload] = feishuConvertor{}
return newJSONRequest(pc, w, t, true)
diff --git a/services/webhook/general.go b/services/webhook/general.go
index dde43bb349..91bf68600f 100644
--- a/services/webhook/general.go
+++ b/services/webhook/general.go
@@ -307,6 +307,18 @@ func getPackagePayloadInfo(p *api.PackagePayload, linkFormatter linkFormatter, w
return text, color
}
+func getStatusPayloadInfo(p *api.CommitStatusPayload, linkFormatter linkFormatter, withSender bool) (text string, color int) {
+ refLink := linkFormatter(p.TargetURL, p.Context+"["+p.SHA+"]:"+p.Description)
+
+ text = fmt.Sprintf("Commit Status changed: %s", refLink)
+ color = greenColor
+ if withSender {
+ text += fmt.Sprintf(" by %s", linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName))
+ }
+
+ return text, color
+}
+
// ToHook convert models.Webhook to api.Hook
// This function is not part of the convert package to prevent an import cycle
func ToHook(repoLink string, w *webhook_model.Webhook) (*api.Hook, error) {
diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go
index 96dfa139ac..ec21712837 100644
--- a/services/webhook/matrix.go
+++ b/services/webhook/matrix.go
@@ -244,6 +244,13 @@ func (m matrixConvertor) Package(p *api.PackagePayload) (MatrixPayload, error) {
return m.newPayload(text)
}
+func (m matrixConvertor) Status(p *api.CommitStatusPayload) (MatrixPayload, error) {
+ refLink := htmlLinkFormatter(p.TargetURL, p.Context+"["+p.SHA+"]:"+p.Description)
+ text := fmt.Sprintf("Commit Status changed: %s", refLink)
+
+ return m.newPayload(text)
+}
+
var urlRegex = regexp.MustCompile(`<a [^>]*?href="([^">]*?)">(.*?)</a>`)
func getMessageBody(htmlText string) string {
diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go
index 1ae7c4f931..485f695be2 100644
--- a/services/webhook/msteams.go
+++ b/services/webhook/msteams.go
@@ -303,6 +303,20 @@ func (m msteamsConvertor) Package(p *api.PackagePayload) (MSTeamsPayload, error)
), nil
}
+func (m msteamsConvertor) Status(p *api.CommitStatusPayload) (MSTeamsPayload, error) {
+ title, color := getStatusPayloadInfo(p, noneLinkFormatter, false)
+
+ return createMSTeamsPayload(
+ p.Repo,
+ p.Sender,
+ title,
+ "",
+ p.TargetURL,
+ color,
+ &MSTeamsFact{"CommitStatus:", p.Context},
+ ), nil
+}
+
func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTarget string, color int, fact *MSTeamsFact) MSTeamsPayload {
facts := make([]MSTeamsFact, 0, 2)
if r != nil {
diff --git a/services/webhook/packagist.go b/services/webhook/packagist.go
index e66895832b..6864fc822a 100644
--- a/services/webhook/packagist.go
+++ b/services/webhook/packagist.go
@@ -110,6 +110,10 @@ func (pc packagistConvertor) Package(_ *api.PackagePayload) (PackagistPayload, e
return PackagistPayload{}, nil
}
+func (pc packagistConvertor) Status(_ *api.CommitStatusPayload) (PackagistPayload, error) {
+ return PackagistPayload{}, nil
+}
+
func newPackagistRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
meta := &PackagistMeta{}
if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
diff --git a/services/webhook/payloader.go b/services/webhook/payloader.go
index ab280a25b6..c29ad8ac92 100644
--- a/services/webhook/payloader.go
+++ b/services/webhook/payloader.go
@@ -28,6 +28,7 @@ type payloadConvertor[T any] interface {
Release(*api.ReleasePayload) (T, error)
Wiki(*api.WikiPayload) (T, error)
Package(*api.PackagePayload) (T, error)
+ Status(*api.CommitStatusPayload) (T, error)
}
func convertUnmarshalledJSON[T, P any](convert func(P) (T, error), data []byte) (t T, err error) {
@@ -77,6 +78,8 @@ func newPayload[T any](rc payloadConvertor[T], data []byte, event webhook_module
return convertUnmarshalledJSON(rc.Wiki, data)
case webhook_module.HookEventPackage:
return convertUnmarshalledJSON(rc.Package, data)
+ case webhook_module.HookEventStatus:
+ return convertUnmarshalledJSON(rc.Status, data)
}
return t, fmt.Errorf("newPayload unsupported event: %s", event)
}
diff --git a/services/webhook/slack.go b/services/webhook/slack.go
index 0371ee23e6..80ed747fd1 100644
--- a/services/webhook/slack.go
+++ b/services/webhook/slack.go
@@ -167,6 +167,12 @@ func (s slackConvertor) Package(p *api.PackagePayload) (SlackPayload, error) {
return s.createPayload(text, nil), nil
}
+func (s slackConvertor) Status(p *api.CommitStatusPayload) (SlackPayload, error) {
+ text, _ := getStatusPayloadInfo(p, SlackLinkFormatter, true)
+
+ return s.createPayload(text, nil), nil
+}
+
// Push implements payloadConvertor Push method
func (s slackConvertor) Push(p *api.PushPayload) (SlackPayload, error) {
// n new commits
diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go
index 6fbf995801..485e2d990b 100644
--- a/services/webhook/telegram.go
+++ b/services/webhook/telegram.go
@@ -174,6 +174,12 @@ func (t telegramConvertor) Package(p *api.PackagePayload) (TelegramPayload, erro
return createTelegramPayloadHTML(text), nil
}
+func (t telegramConvertor) Status(p *api.CommitStatusPayload) (TelegramPayload, error) {
+ text, _ := getStatusPayloadInfo(p, htmlLinkFormatter, true)
+
+ return createTelegramPayloadHTML(text), nil
+}
+
func createTelegramPayloadHTML(msgHTML string) TelegramPayload {
// https://core.telegram.org/bots/api#formatting-options
return TelegramPayload{
diff --git a/services/webhook/wechatwork.go b/services/webhook/wechatwork.go
index 44e0ff7de5..1c834b4020 100644
--- a/services/webhook/wechatwork.go
+++ b/services/webhook/wechatwork.go
@@ -175,6 +175,12 @@ func (wc wechatworkConvertor) Package(p *api.PackagePayload) (WechatworkPayload,
return newWechatworkMarkdownPayload(text), nil
}
+func (wc wechatworkConvertor) Status(p *api.CommitStatusPayload) (WechatworkPayload, error) {
+ text, _ := getStatusPayloadInfo(p, noneLinkFormatter, true)
+
+ return newWechatworkMarkdownPayload(text), nil
+}
+
func newWechatworkRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
var pc payloadConvertor[WechatworkPayload] = wechatworkConvertor{}
return newJSONRequest(pc, w, t, true)
diff --git a/templates/admin/auth/list.tmpl b/templates/admin/auth/list.tmpl
index 7931014b1a..a1e72b742f 100644
--- a/templates/admin/auth/list.tmpl
+++ b/templates/admin/auth/list.tmpl
@@ -30,6 +30,8 @@
<td>{{DateUtils.AbsoluteShort .CreatedUnix}}</td>
<td><a href="{{AppSubUrl}}/-/admin/auths/{{.ID}}">{{svg "octicon-pencil"}}</a></td>
</tr>
+ {{else}}
+ <tr><td class="tw-text-center" colspan="7">{{ctx.Locale.Tr "no_results_found"}}</td></tr>
{{end}}
</tbody>
</table>
diff --git a/templates/admin/emails/list.tmpl b/templates/admin/emails/list.tmpl
index 0dc1fb9d03..b4335aeeec 100644
--- a/templates/admin/emails/list.tmpl
+++ b/templates/admin/emails/list.tmpl
@@ -67,6 +67,8 @@
>{{svg "octicon-trash"}}</a>
</td>
</tr>
+ {{else}}
+ <tr><td class="tw-text-center" colspan="6">{{ctx.Locale.Tr "no_results_found"}}</td></tr>
{{end}}
</tbody>
</table>
diff --git a/templates/admin/notice.tmpl b/templates/admin/notice.tmpl
index fd475d7157..a4c9dc53fb 100644
--- a/templates/admin/notice.tmpl
+++ b/templates/admin/notice.tmpl
@@ -24,6 +24,8 @@
<td nowrap>{{DateUtils.AbsoluteShort .CreatedUnix}}</td>
<td class="view-detail"><a href="#">{{svg "octicon-note" 16}}</a></td>
</tr>
+ {{else}}
+ <tr><td class="tw-text-center" colspan="6">{{ctx.Locale.Tr "no_results_found"}}</td></tr>
{{end}}
</tbody>
{{if .Notices}}
diff --git a/templates/admin/org/list.tmpl b/templates/admin/org/list.tmpl
index d5e09939c5..137c42b45d 100644
--- a/templates/admin/org/list.tmpl
+++ b/templates/admin/org/list.tmpl
@@ -66,6 +66,8 @@
<td>{{DateUtils.AbsoluteShort .CreatedUnix}}</td>
<td><a href="{{.OrganisationLink}}/settings" data-tooltip-content="{{ctx.Locale.Tr "edit"}}">{{svg "octicon-pencil"}}</a></td>
</tr>
+ {{else}}
+ <tr><td class="tw-text-center" colspan="7">{{ctx.Locale.Tr "no_results_found"}}</td></tr>
{{end}}
</tbody>
</table>
diff --git a/templates/admin/packages/list.tmpl b/templates/admin/packages/list.tmpl
index 08c11442bc..0c6889b599 100644
--- a/templates/admin/packages/list.tmpl
+++ b/templates/admin/packages/list.tmpl
@@ -74,6 +74,8 @@
<td>{{DateUtils.AbsoluteShort .Version.CreatedUnix}}</td>
<td><a class="delete-button" href="" data-url="{{$.Link}}/delete?page={{$.Page.Paginater.Current}}&sort={{$.SortType}}" data-id="{{.Version.ID}}" data-name="{{.Package.Name}}" data-data-version="{{.Version.Version}}">{{svg "octicon-trash"}}</a></td>
</tr>
+ {{else}}
+ <tr><td class="tw-text-center" colspan="10">{{ctx.Locale.Tr "no_results_found"}}</td></tr>
{{end}}
</tbody>
</table>
diff --git a/templates/admin/repo/list.tmpl b/templates/admin/repo/list.tmpl
index 08fd893e76..762013af47 100644
--- a/templates/admin/repo/list.tmpl
+++ b/templates/admin/repo/list.tmpl
@@ -86,6 +86,8 @@
<td>{{DateUtils.AbsoluteShort .CreatedUnix}}</td>
<td><a class="delete-button" href="" data-url="{{$.Link}}/delete?page={{$.Page.Paginater.Current}}&sort={{$.SortType}}" data-id="{{.ID}}" data-name="{{.Name}}">{{svg "octicon-trash"}}</a></td>
</tr>
+ {{else}}
+ <tr><td class="tw-text-center" colspan="12">{{ctx.Locale.Tr "no_results_found"}}</td></tr>
{{end}}
</tbody>
</table>
diff --git a/templates/admin/user/edit.tmpl b/templates/admin/user/edit.tmpl
index d591a645d8..c04d332660 100644
--- a/templates/admin/user/edit.tmpl
+++ b/templates/admin/user/edit.tmpl
@@ -195,8 +195,7 @@
</div>
<div class="inline field tw-pl-4">
- <label for="avatar">{{ctx.Locale.Tr "settings.choose_new_avatar"}}</label>
- <input name="avatar" type="file" accept="image/png,image/jpeg,image/gif,image/webp">
+ {{template "shared/avatar_upload_crop" dict "LabelText" (ctx.Locale.Tr "settings.choose_new_avatar")}}
</div>
<div class="field">
diff --git a/templates/admin/user/list.tmpl b/templates/admin/user/list.tmpl
index 7e4c8854f5..eb3f6cd720 100644
--- a/templates/admin/user/list.tmpl
+++ b/templates/admin/user/list.tmpl
@@ -109,6 +109,8 @@
</div>
</td>
</tr>
+ {{else}}
+ <tr class="no-results-row"><td class="tw-text-center" colspan="9">{{ctx.Locale.Tr "no_results_found"}}</td></tr>
{{end}}
</tbody>
</table>
diff --git a/templates/org/home.tmpl b/templates/org/home.tmpl
index db750692bf..bae5db00be 100644
--- a/templates/org/home.tmpl
+++ b/templates/org/home.tmpl
@@ -32,7 +32,6 @@
<span class="text">{{svg "octicon-eye"}} {{ctx.Locale.Tr "org.view_as_role" $viewAsRole}}</span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">
- {{/* TODO: does it really need to use CurrentURL with query parameters? Why not construct a new link with clear parameters */}}
<a href="?view_as=public" class="item {{if not .IsViewingOrgAsMember}}selected{{end}}">
{{svg "octicon-check" 14 (Iif (not .IsViewingOrgAsMember) "" "tw-invisible")}} {{ctx.Locale.Tr "settings.visibility.public"}}
</a>
diff --git a/templates/org/menu.tmpl b/templates/org/menu.tmpl
index 4a8aee68a7..2d3af2d559 100644
--- a/templates/org/menu.tmpl
+++ b/templates/org/menu.tmpl
@@ -45,6 +45,11 @@
</a>
{{end}}
{{if .IsOrganizationOwner}}
+ <a class="{{if $.PageIsOrgTimes}}active{{end}} item" href="{{$.OrgLink}}/worktime">
+ {{svg "octicon-clock"}} {{ctx.Locale.Tr "org.worktime"}}
+ </a>
+ {{end}}
+ {{if .IsOrganizationOwner}}
<span class="item-flex-space"></span>
<a class="{{if .PageIsOrgSettings}}active {{end}}item" href="{{.OrgLink}}/settings">
{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}}
diff --git a/templates/org/settings/options.tmpl b/templates/org/settings/options.tmpl
index 3b817d068b..76315f3eac 100644
--- a/templates/org/settings/options.tmpl
+++ b/templates/org/settings/options.tmpl
@@ -89,10 +89,8 @@
<form class="ui form" action="{{.Link}}/avatar" method="post" enctype="multipart/form-data">
{{.CsrfTokenHtml}}
<div class="inline field">
- <label for="avatar">{{ctx.Locale.Tr "settings.choose_new_avatar"}}</label>
- <input name="avatar" type="file" accept="image/png,image/jpeg,image/gif,image/webp">
+ {{template "shared/avatar_upload_crop" dict "LabelText" (ctx.Locale.Tr "settings.choose_new_avatar")}}
</div>
-
<div class="field">
<button class="ui primary button">{{ctx.Locale.Tr "settings.update_avatar"}}</button>
<button class="ui red button link-action" data-url="{{.Link}}/avatar/delete">{{ctx.Locale.Tr "settings.delete_current_avatar"}}</button>
diff --git a/templates/org/worktime.tmpl b/templates/org/worktime.tmpl
new file mode 100644
index 0000000000..5d99998129
--- /dev/null
+++ b/templates/org/worktime.tmpl
@@ -0,0 +1,40 @@
+{{template "base/head" .}}
+<div class="page-content organization times">
+ {{template "org/header" .}}
+ <div class="ui container">
+ <div class="ui grid">
+ <div class="three wide column">
+ <form class="ui form" method="get">
+ <input type="hidden" name="by" value="{{$.WorktimeBy}}">
+ <div class="field">
+ <label>{{ctx.Locale.Tr "org.worktime.date_range_start"}}</label>
+ <input type="date" name="from" value="{{.RangeFrom}}">
+ </div>
+ <div class="field">
+ <label>{{ctx.Locale.Tr "org.worktime.date_range_end"}}</label>
+ <input type="date" name="to" value="{{.RangeTo}}">
+ </div>
+ <button class="ui primary button">{{ctx.Locale.Tr "org.worktime.query"}}</button>
+ </form>
+ </div>
+ <div class="thirteen wide column">
+ <div class="ui column">
+ <div class="ui compact small menu">
+ {{$queryParams := QueryBuild "from" .RangeFrom "to" .RangeTo}}
+ <a class="{{Iif .WorktimeByRepos "active"}} item" href="{{$.Org.OrganisationLink}}/worktime?by=repos&{{$queryParams}}">{{svg "octicon-repo"}} {{ctx.Locale.Tr "org.worktime.by_repositories"}}</a>
+ <a class="{{Iif .WorktimeByMilestones "active"}} item" href="{{$.Org.OrganisationLink}}/worktime?by=milestones&{{$queryParams}}">{{svg "octicon-milestone"}} {{ctx.Locale.Tr "org.worktime.by_milestones"}}</a>
+ <a class="{{Iif .WorktimeByMembers "active"}} item" href="{{$.Org.OrganisationLink}}/worktime?by=members&{{$queryParams}}">{{svg "octicon-people"}} {{ctx.Locale.Tr "org.worktime.by_members"}}</a>
+ </div>
+ </div>
+ {{if .WorktimeByRepos}}
+ {{template "org/worktime/table_repos" dict "Org" .Org "WorktimeSumResult" .WorktimeSumResult}}
+ {{else if .WorktimeByMilestones}}
+ {{template "org/worktime/table_milestones" dict "Org" .Org "WorktimeSumResult" .WorktimeSumResult}}
+ {{else if .WorktimeByMembers}}
+ {{template "org/worktime/table_members" dict "Org" .Org "WorktimeSumResult" .WorktimeSumResult}}
+ {{end}}
+ </div>
+ </div>
+ </div>
+</div>
+{{template "base/footer" .}}
diff --git a/templates/org/worktime/table_members.tmpl b/templates/org/worktime/table_members.tmpl
new file mode 100644
index 0000000000..a59d1941d8
--- /dev/null
+++ b/templates/org/worktime/table_members.tmpl
@@ -0,0 +1,16 @@
+<table class="ui table">
+ <thead>
+ <tr>
+ <th>{{ctx.Locale.Tr "org.members.member"}}</th>
+ <th>{{ctx.Locale.Tr "org.worktime.time"}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{range $.WorktimeSumResult}}
+ <tr>
+ <td>{{svg "octicon-person"}} <a href="{{AppSubUrl}}/{{PathEscape .UserName}}">{{.UserName}}</a></td>
+ <td>{{svg "octicon-clock"}} {{.SumTime | Sec2Hour}}</td>
+ </tr>
+ {{end}}
+ </tbody>
+</table>
diff --git a/templates/org/worktime/table_milestones.tmpl b/templates/org/worktime/table_milestones.tmpl
new file mode 100644
index 0000000000..6ef9289e56
--- /dev/null
+++ b/templates/org/worktime/table_milestones.tmpl
@@ -0,0 +1,28 @@
+<table class="ui table">
+ <thead>
+ <tr>
+ <th>{{ctx.Locale.Tr "repository"}}</th>
+ <th>{{ctx.Locale.Tr "repo.milestone"}}</th>
+ <th>{{ctx.Locale.Tr "org.worktime.time"}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{range $.WorktimeSumResult}}
+ <tr>
+ <td>
+ {{if not .HideRepoName}}
+ {{svg "octicon-repo"}} <a href="{{$.Org.HomeLink}}/{{PathEscape .RepoName}}/issues">{{.RepoName}}</a>
+ {{end}}
+ </td>
+ <td>
+ {{if .MilestoneName}}
+ {{svg "octicon-milestone"}} <a href="{{$.Org.HomeLink}}/{{PathEscape .RepoName}}/milestone/{{.MilestoneID}}">{{.MilestoneName}}</a>
+ {{else}}
+ -
+ {{end}}
+ </td>
+ <td>{{svg "octicon-clock"}} {{.SumTime | Sec2Hour}}</td>
+ </tr>
+ {{end}}
+ </tbody>
+</table>
diff --git a/templates/org/worktime/table_repos.tmpl b/templates/org/worktime/table_repos.tmpl
new file mode 100644
index 0000000000..eaa085df0c
--- /dev/null
+++ b/templates/org/worktime/table_repos.tmpl
@@ -0,0 +1,16 @@
+<table class="ui table">
+ <thead>
+ <tr>
+ <th>{{ctx.Locale.Tr "repository"}}</th>
+ <th>{{ctx.Locale.Tr "org.worktime.time"}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{range $.WorktimeSumResult}}
+ <tr>
+ <td>{{svg "octicon-repo"}} <a href="{{$.Org.HomeLink}}/{{PathEscape .RepoName}}/issues">{{.RepoName}}</a></td>
+ <td>{{svg "octicon-clock"}} {{.SumTime | Sec2Hour}}</td>
+ </tr>
+ {{end}}
+ </tbody>
+</table>
diff --git a/templates/projects/list.tmpl b/templates/projects/list.tmpl
index f5a48f7241..7c75585bf7 100644
--- a/templates/projects/list.tmpl
+++ b/templates/projects/list.tmpl
@@ -34,6 +34,8 @@
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">
<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="?q={{$.Keyword}}&sort=oldest&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
+ <a class="{{if eq .SortType "alphabetically"}}active {{end}}item" href="?q={{$.Keyword}}&sort=alphabetically&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</a>
+ <a class="{{if eq .SortType "reversealphabetically"}}active {{end}}item" href="?q={{$.Keyword}}&sort=reversealphabetically&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</a>
<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="?q={{$.Keyword}}&sort=recentupdate&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="?q={{$.Keyword}}&sort=leastupdate&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
</div>
diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl
index ea01d96928..a3b64b8a11 100644
--- a/templates/repo/diff/box.tmpl
+++ b/templates/repo/diff/box.tmpl
@@ -24,7 +24,7 @@
{{end}}
</div>
<div class="diff-detail-actions">
- {{if and .PageIsPullFiles $.SignedUserID (not .IsArchived) (not .DiffNotAvailable)}}
+ {{if and .PageIsPullFiles $.SignedUserID (not .DiffNotAvailable)}}
<div class="not-mobile tw-flex tw-items-center tw-flex-col tw-whitespace-nowrap tw-mr-1">
<label for="viewed-files-summary" id="viewed-files-summary-label" data-text-changed-template="{{ctx.Locale.Tr "repo.pulls.viewed_files_label"}}">
{{ctx.Locale.Tr "repo.pulls.viewed_files_label" .Diff.NumViewedFiles .Diff.NumFiles}}
@@ -42,7 +42,7 @@
</div>
</div>
{{end}}
- {{if and .PageIsPullFiles $.SignedUserID (not .IsArchived)}}
+ {{if and .PageIsPullFiles $.SignedUserID}}
{{template "repo/diff/new_review" .}}
{{end}}
</div>
@@ -105,7 +105,7 @@
{{$isCsv := (call $.IsCsvFile $file)}}
{{$showFileViewToggle := or $isImage (and (not $file.IsIncomplete) $isCsv)}}
{{$isExpandable := or (gt $file.Addition 0) (gt $file.Deletion 0) $file.IsBin}}
- {{$isReviewFile := and $.IsSigned $.PageIsPullFiles (not $.IsArchived) $.IsShowingAllCommits}}
+ {{$isReviewFile := and $.IsSigned $.PageIsPullFiles (not $.Repository.IsArchived) $.IsShowingAllCommits}}
<div class="diff-file-box diff-box file-content {{TabSizeClass $.Editorconfig $file.Name}} tw-mt-0" id="diff-{{$file.NameHash}}" data-old-filename="{{$file.OldName}}" data-new-filename="{{$file.Name}}" {{if or ($file.ShouldBeHidden) (not $isExpandable)}}data-folded="true"{{end}}>
<h4 class="diff-file-header sticky-2nd-row ui top attached header">
<div class="diff-file-name tw-flex tw-flex-1 tw-items-center tw-gap-1 tw-flex-wrap">
diff --git a/templates/repo/diff/comments.tmpl b/templates/repo/diff/comments.tmpl
index ec52934a9d..2e8261e479 100644
--- a/templates/repo/diff/comments.tmpl
+++ b/templates/repo/diff/comments.tmpl
@@ -48,7 +48,9 @@
</div>
{{end}}
{{end}}
- {{template "repo/issue/view_content/add_reaction" dict "ActionURL" (printf "%s/comments/%d/reactions" $.root.RepoLink .ID)}}
+ {{if not $.root.Repository.IsArchived}}
+ {{template "repo/issue/view_content/add_reaction" dict "ActionURL" (printf "%s/comments/%d/reactions" $.root.RepoLink .ID)}}
+ {{end}}
{{template "repo/issue/view_content/context_menu" dict "item" . "delete" true "issue" false "diff" true "IsCommentPoster" (and $.root.IsSigned (eq $.root.SignedUserID .PosterID))}}
</div>
</div>
diff --git a/templates/repo/diff/new_review.tmpl b/templates/repo/diff/new_review.tmpl
index 2febc6303a..3bb01a139a 100644
--- a/templates/repo/diff/new_review.tmpl
+++ b/templates/repo/diff/new_review.tmpl
@@ -1,56 +1,59 @@
-<div id="review-box">
- <button class="ui tiny primary button tw-pr-1 tw-flex js-btn-review {{if not $.IsShowingAllCommits}}disabled{{end}}" {{if not $.IsShowingAllCommits}}data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.review_only_possible_for_full_diff"}}"{{end}}>
+<div id="review-box" {{if $.Repository.IsArchived}}data-tooltip-content="{{ctx.Locale.Tr "repo.archive.pull.nocomment"}}"{{end}}>
+ <button class="ui tiny primary button tw-pr-1 js-btn-review {{if not $.IsShowingAllCommits}}disabled{{end}}"
+ {{if not $.IsShowingAllCommits}}data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.review_only_possible_for_full_diff"}}"{{end}}
+ {{if $.Repository.IsArchived}}disabled{{end}}
+ >
{{ctx.Locale.Tr "repo.diff.review"}}
<span class="ui small label review-comments-counter" data-pending-comment-number="{{.PendingCodeCommentNumber}}">{{.PendingCodeCommentNumber}}</span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
</button>
- {{if $.IsShowingAllCommits}}
- <div class="review-box-panel tippy-target">
- <div class="ui segment">
- <form class="ui form form-fetch-action" action="{{.Link}}/reviews/submit" method="post">
- {{.CsrfTokenHtml}}
- <input type="hidden" name="commit_id" value="{{.AfterCommitID}}">
- <div class="field tw-flex tw-items-center">
- <div class="tw-flex-1">{{ctx.Locale.Tr "repo.diff.review.header"}}</div>
- <a class="muted close">{{svg "octicon-x" 16}}</a>
- </div>
+</div>
+{{if $.IsShowingAllCommits}}
+<div class="review-box-panel tippy-target">
+ <div class="ui segment">
+ <form class="ui form form-fetch-action" action="{{.Link}}/reviews/submit" method="post">
+ {{.CsrfTokenHtml}}
+ <input type="hidden" name="commit_id" value="{{.AfterCommitID}}">
+ <div class="field tw-flex tw-items-center">
+ <div class="tw-flex-1">{{ctx.Locale.Tr "repo.diff.review.header"}}</div>
+ <a class="muted close">{{svg "octicon-x" 16}}</a>
+ </div>
+ <div class="field">
+ {{template "shared/combomarkdowneditor" (dict
+ "MarkdownPreviewInRepo" $.Repository
+ "MarkdownPreviewMode" "comment"
+ "TextareaName" "content"
+ "TextareaPlaceholder" (ctx.Locale.Tr "repo.diff.review.placeholder")
+ "DropzoneParentContainer" "form"
+ )}}
+ </div>
+ {{if .IsAttachmentEnabled}}
<div class="field">
- {{template "shared/combomarkdowneditor" (dict
- "MarkdownPreviewInRepo" $.Repository
- "MarkdownPreviewMode" "comment"
- "TextareaName" "content"
- "TextareaPlaceholder" (ctx.Locale.Tr "repo.diff.review.placeholder")
- "DropzoneParentContainer" "form"
- )}}
+ {{template "repo/upload" .}}
</div>
- {{if .IsAttachmentEnabled}}
- <div class="field">
- {{template "repo/upload" .}}
- </div>
- {{end}}
- <div class="divider"></div>
- {{$showSelfTooltip := (and $.IsSigned ($.Issue.IsPoster $.SignedUser.ID))}}
- {{if not $.Issue.IsClosed}}
- {{if $showSelfTooltip}}
- <span class="tw-inline-block" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.review.self_approve"}}">
- <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>
- {{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}}
+ <div class="divider"></div>
+ {{$showSelfTooltip := (and $.IsSigned ($.Issue.IsPoster $.SignedUser.ID))}}
+ {{if not $.Issue.IsClosed}}
+ {{if $showSelfTooltip}}
+ <span class="tw-inline-block" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.review.self_approve"}}">
+ <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>
+ {{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}}
- <button type="submit" name="type" value="comment" class="ui submit tiny basic button btn-submit">{{ctx.Locale.Tr "repo.diff.review.comment"}}</button>
- {{if not $.Issue.IsClosed}}
- {{if $showSelfTooltip}}
- <span class="tw-inline-block" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.review.self_reject"}}">
- <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>
- {{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}}
+ <button type="submit" name="type" value="comment" class="ui submit tiny basic button btn-submit">{{ctx.Locale.Tr "repo.diff.review.comment"}}</button>
+ {{if not $.Issue.IsClosed}}
+ {{if $showSelfTooltip}}
+ <span class="tw-inline-block" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.review.self_reject"}}">
+ <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>
+ {{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}}
- </form>
- </div>
+ {{end}}
+ </form>
</div>
- {{end}}
</div>
+{{end}}
diff --git a/templates/repo/issue/filters.tmpl b/templates/repo/issue/filters.tmpl
index 06e7c1aa6c..409ec876e6 100644
--- a/templates/repo/issue/filters.tmpl
+++ b/templates/repo/issue/filters.tmpl
@@ -9,7 +9,7 @@
<div class="ui compact tiny secondary menu">
<span class="item" data-tooltip-content='{{ctx.Locale.Tr "tracked_time_summary"}}'>
{{svg "octicon-clock"}}
- {{.TotalTrackedTime | Sec2Time}}
+ {{.TotalTrackedTime | Sec2Hour}}
</span>
</div>
{{end}}
diff --git a/templates/repo/issue/list.tmpl b/templates/repo/issue/list.tmpl
index 01b610b39d..53d0eca171 100644
--- a/templates/repo/issue/list.tmpl
+++ b/templates/repo/issue/list.tmpl
@@ -40,7 +40,7 @@
<div class="ui compact tiny secondary menu">
<span class="item" data-tooltip-content='{{ctx.Locale.Tr "tracked_time_summary"}}'>
{{svg "octicon-clock"}}
- {{.TotalTrackedTime | Sec2Time}}
+ {{.TotalTrackedTime | Sec2Hour}}
</span>
</div>
{{end}}
diff --git a/templates/repo/issue/milestone_issues.tmpl b/templates/repo/issue/milestone_issues.tmpl
index 4fc6057117..abb4e3290d 100644
--- a/templates/repo/issue/milestone_issues.tmpl
+++ b/templates/repo/issue/milestone_issues.tmpl
@@ -50,7 +50,7 @@
{{if .TotalTrackedTime}}
<div data-tooltip-content='{{ctx.Locale.Tr "tracked_time_summary"}}'>
{{svg "octicon-clock"}}
- {{.TotalTrackedTime | Sec2Time}}
+ {{.TotalTrackedTime | Sec2Hour}}
</div>
{{end}}
</div>
diff --git a/templates/repo/issue/milestones.tmpl b/templates/repo/issue/milestones.tmpl
index 9515acfb8e..e7dfe08ee0 100644
--- a/templates/repo/issue/milestones.tmpl
+++ b/templates/repo/issue/milestones.tmpl
@@ -41,7 +41,7 @@
{{if .TotalTrackedTime}}
<div class="flex-text-block">
{{svg "octicon-clock"}}
- {{.TotalTrackedTime|Sec2Time}}
+ {{.TotalTrackedTime|Sec2Hour}}
</div>
{{end}}
{{if .UpdatedUnix}}
diff --git a/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl b/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl
index f107dc5ef5..d5ac6827ba 100644
--- a/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl
+++ b/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl
@@ -72,7 +72,7 @@
{{end}}
{{if .WorkingUsers}}
<div class="ui comments tw-mt-2">
- {{ctx.Locale.Tr "repo.issues.time_spent_from_all_authors" ($.Issue.TotalTrackedTime | Sec2Time)}}
+ {{ctx.Locale.Tr "repo.issues.time_spent_from_all_authors" ($.Issue.TotalTrackedTime | Sec2Hour)}}
<div>
{{range $user, $trackedtime := .WorkingUsers}}
<div class="comment tw-mt-2">
@@ -82,7 +82,7 @@
<div class="content">
{{template "shared/user/authorlink" $user}}
<div class="text">
- {{$trackedtime|Sec2Time}}
+ {{$trackedtime|Sec2Hour}}
</div>
</div>
</div>
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl
index c1ad64a118..f2f3d1c9cc 100644
--- a/templates/repo/issue/view_content/comments.tmpl
+++ b/templates/repo/issue/view_content/comments.tmpl
@@ -252,7 +252,7 @@
<span class="text grey muted-links">
{{template "shared/user/authorlink" .Poster}}
{{$timeStr := .RenderedContent}} {{/* compatibility with time comments made before v1.21 */}}
- {{if not $timeStr}}{{$timeStr = .Content|Sec2Time}}{{end}}
+ {{if not $timeStr}}{{$timeStr = .Content|Sec2Hour}}{{end}}
{{ctx.Locale.Tr "repo.issues.stop_tracking_history" $timeStr $createdStr}}
</span>
{{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}}
@@ -264,7 +264,7 @@
<span class="text grey muted-links">
{{template "shared/user/authorlink" .Poster}}
{{$timeStr := .RenderedContent}} {{/* compatibility with time comments made before v1.21 */}}
- {{if not $timeStr}}{{$timeStr = .Content|Sec2Time}}{{end}}
+ {{if not $timeStr}}{{$timeStr = .Content|Sec2Hour}}{{end}}
{{ctx.Locale.Tr "repo.issues.add_time_history" $timeStr $createdStr}}
</span>
{{template "repo/issue/view_content/comments_delete_time" dict "ctxData" $ "comment" .}}
@@ -506,7 +506,7 @@
{{/* compatibility with time comments made before v1.21 */}}
<span class="text grey muted-links">{{.RenderedContent}}</span>
{{else}}
- <span class="text grey muted-links">- {{.Content|Sec2Time}}</span>
+ <span class="text grey muted-links">- {{.Content|Sec2Hour}}</span>
{{end}}
</div>
</div>
diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl
index cb596f013b..0520c87cc1 100644
--- a/templates/repo/settings/options.tmpl
+++ b/templates/repo/settings/options.tmpl
@@ -40,8 +40,7 @@
<form class="ui form" action="{{.Link}}/avatar" method="post" enctype="multipart/form-data">
{{.CsrfTokenHtml}}
<div class="inline field">
- <label for="avatar">{{ctx.Locale.Tr "settings.choose_new_avatar"}}</label>
- <input name="avatar" type="file" accept="image/png,image/jpeg,image/gif,image/webp">
+ {{template "shared/avatar_upload_crop" dict "LabelText" (ctx.Locale.Tr "settings.choose_new_avatar")}}
</div>
<div class="field">
<button class="ui primary button">{{ctx.Locale.Tr "settings.update_avatar"}}</button>
diff --git a/templates/repo/settings/webhook/settings.tmpl b/templates/repo/settings/webhook/settings.tmpl
index 1a01a6aea8..3b28a4c6c0 100644
--- a/templates/repo/settings/webhook/settings.tmpl
+++ b/templates/repo/settings/webhook/settings.tmpl
@@ -109,6 +109,17 @@
</div>
</div>
+ <!-- Status -->
+ <div class="seven wide column">
+ <div class="field">
+ <div class="ui checkbox">
+ <input name="status" type="checkbox" {{if .Webhook.HookEvents.Get "status"}}checked{{end}}>
+ <label>{{ctx.Locale.Tr "repo.settings.event_statuses"}}</label>
+ <span class="help">{{ctx.Locale.Tr "repo.settings.event_statuses_desc"}}</span>
+ </div>
+ </div>
+ </div>
+
<!-- Issue Events -->
<div class="fourteen wide column">
<label>{{ctx.Locale.Tr "repo.settings.event_header_issue"}}</label>
diff --git a/templates/shared/avatar_upload_crop.tmpl b/templates/shared/avatar_upload_crop.tmpl
new file mode 100644
index 0000000000..2c4166fa9c
--- /dev/null
+++ b/templates/shared/avatar_upload_crop.tmpl
@@ -0,0 +1,8 @@
+{{- /* we do not need to set for/id here, global aria init code will add them automatically */ -}}
+<label>{{.LabelText}}</label>
+<input class="avatar-file-with-cropper" name="avatar" type="file" accept="image/png,image/jpeg,image/gif,image/webp">
+{{- /* the cropper-panel must be next sibling of the input "avatar" */ -}}
+<div class="cropper-panel tw-hidden">
+ <div class="tw-my-2">{{ctx.Locale.Tr "settings.cropper_prompt"}}</div>
+ <div class="cropper-wrapper"><img class="cropper-source" src alt></div>
+</div>
diff --git a/templates/shared/issuelist.tmpl b/templates/shared/issuelist.tmpl
index e8015b40ea..fe7f2fd8bf 100644
--- a/templates/shared/issuelist.tmpl
+++ b/templates/shared/issuelist.tmpl
@@ -28,7 +28,7 @@
{{if .TotalTrackedTime}}
<div class="text grey flex-text-block">
{{svg "octicon-clock" 16}}
- {{.TotalTrackedTime | Sec2Time}}
+ {{.TotalTrackedTime | Sec2Hour}}
</div>
{{end}}
</div>
diff --git a/templates/shared/user/authorlink.tmpl b/templates/shared/user/authorlink.tmpl
index d57a635b4b..abfee6aae3 100644
--- a/templates/shared/user/authorlink.tmpl
+++ b/templates/shared/user/authorlink.tmpl
@@ -1 +1 @@
-<a class="author text black tw-font-semibold muted"{{if gt .ID 0}} href="{{.HomeLink}}"{{end}}>{{.GetDisplayName}}</a>{{if .IsBot}}<span class="ui basic label tw-p-1">bot</span>{{end}}
+<a class="author text black tw-font-semibold muted"{{if gt .ID 0}} href="{{.HomeLink}}"{{end}}>{{.GetDisplayName}}</a>{{if .IsTypeBot}}<span class="ui basic label tw-p-1 tw-align-baseline">bot</span>{{end}}
diff --git a/templates/shared/webhook/icon.tmpl b/templates/shared/webhook/icon.tmpl
index 0f80787c57..245ed16505 100644
--- a/templates/shared/webhook/icon.tmpl
+++ b/templates/shared/webhook/icon.tmpl
@@ -17,7 +17,7 @@
{{else if eq .HookType "msteams"}}
<img width="{{$size}}" height="{{$size}}" src="{{AssetUrlPrefix}}/img/msteams.png">
{{else if eq .HookType "feishu"}}
- <img width="{{$size}}" height="{{$size}}" src="{{AssetUrlPrefix}}/img/feishu.png">
+ {{svg "gitea-feishu" $size "img"}}
{{else if eq .HookType "matrix"}}
{{svg "gitea-matrix" $size "img"}}
{{else if eq .HookType "wechatwork"}}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 8082fc594a..80cf1b5623 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -2991,6 +2991,46 @@
}
}
},
+ "/orgs/{org}/rename": {
+ "post": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "organization"
+ ],
+ "summary": "Rename an organization",
+ "operationId": "renameOrg",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "existing org name",
+ "name": "org",
+ "in": "path",
+ "required": true
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/RenameOrgOption"
+ }
+ }
+ ],
+ "responses": {
+ "204": {
+ "$ref": "#/responses/empty"
+ },
+ "403": {
+ "$ref": "#/responses/forbidden"
+ },
+ "422": {
+ "$ref": "#/responses/validationError"
+ }
+ }
+ }
+ },
"/orgs/{org}/repos": {
"get": {
"produces": [
@@ -4381,6 +4421,275 @@
}
}
},
+ "/repos/{owner}/{repo}/actions/workflows": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "repository"
+ ],
+ "summary": "List repository workflows",
+ "operationId": "ActionsListRepositoryWorkflows",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "owner of the repo",
+ "name": "owner",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the repo",
+ "name": "repo",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/ActionWorkflowList"
+ },
+ "400": {
+ "$ref": "#/responses/error"
+ },
+ "403": {
+ "$ref": "#/responses/forbidden"
+ },
+ "404": {
+ "$ref": "#/responses/notFound"
+ },
+ "422": {
+ "$ref": "#/responses/validationError"
+ },
+ "500": {
+ "$ref": "#/responses/error"
+ }
+ }
+ }
+ },
+ "/repos/{owner}/{repo}/actions/workflows/{workflow_id}": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "repository"
+ ],
+ "summary": "Get a workflow",
+ "operationId": "ActionsGetWorkflow",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "owner of the repo",
+ "name": "owner",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the repo",
+ "name": "repo",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "id of the workflow",
+ "name": "workflow_id",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/ActionWorkflow"
+ },
+ "400": {
+ "$ref": "#/responses/error"
+ },
+ "403": {
+ "$ref": "#/responses/forbidden"
+ },
+ "404": {
+ "$ref": "#/responses/notFound"
+ },
+ "422": {
+ "$ref": "#/responses/validationError"
+ },
+ "500": {
+ "$ref": "#/responses/error"
+ }
+ }
+ }
+ },
+ "/repos/{owner}/{repo}/actions/workflows/{workflow_id}/disable": {
+ "put": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "repository"
+ ],
+ "summary": "Disable a workflow",
+ "operationId": "ActionsDisableWorkflow",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "owner of the repo",
+ "name": "owner",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the repo",
+ "name": "repo",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "id of the workflow",
+ "name": "workflow_id",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": "No Content"
+ },
+ "400": {
+ "$ref": "#/responses/error"
+ },
+ "403": {
+ "$ref": "#/responses/forbidden"
+ },
+ "404": {
+ "$ref": "#/responses/notFound"
+ },
+ "422": {
+ "$ref": "#/responses/validationError"
+ }
+ }
+ }
+ },
+ "/repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches": {
+ "post": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "repository"
+ ],
+ "summary": "Create a workflow dispatch event",
+ "operationId": "ActionsDispatchWorkflow",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "owner of the repo",
+ "name": "owner",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the repo",
+ "name": "repo",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "id of the workflow",
+ "name": "workflow_id",
+ "in": "path",
+ "required": true
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "schema": {
+ "$ref": "#/definitions/CreateActionWorkflowDispatch"
+ }
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": "No Content"
+ },
+ "400": {
+ "$ref": "#/responses/error"
+ },
+ "403": {
+ "$ref": "#/responses/forbidden"
+ },
+ "404": {
+ "$ref": "#/responses/notFound"
+ },
+ "422": {
+ "$ref": "#/responses/validationError"
+ }
+ }
+ }
+ },
+ "/repos/{owner}/{repo}/actions/workflows/{workflow_id}/enable": {
+ "put": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "repository"
+ ],
+ "summary": "Enable a workflow",
+ "operationId": "ActionsEnableWorkflow",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "owner of the repo",
+ "name": "owner",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "name of the repo",
+ "name": "repo",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "id of the workflow",
+ "name": "workflow_id",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": "No Content"
+ },
+ "400": {
+ "$ref": "#/responses/error"
+ },
+ "403": {
+ "$ref": "#/responses/forbidden"
+ },
+ "404": {
+ "$ref": "#/responses/notFound"
+ },
+ "409": {
+ "$ref": "#/responses/conflict"
+ },
+ "422": {
+ "$ref": "#/responses/validationError"
+ }
+ }
+ }
+ },
"/repos/{owner}/{repo}/activities/feeds": {
"get": {
"produces": [
@@ -13768,6 +14077,9 @@
"200": {
"$ref": "#/responses/UserList"
},
+ "403": {
+ "$ref": "#/responses/forbidden"
+ },
"404": {
"$ref": "#/responses/notFound"
}
@@ -17506,6 +17818,9 @@
"responses": {
"200": {
"$ref": "#/responses/RepositoryList"
+ },
+ "403": {
+ "$ref": "#/responses/forbidden"
}
}
}
@@ -17537,6 +17852,9 @@
"204": {
"$ref": "#/responses/empty"
},
+ "403": {
+ "$ref": "#/responses/forbidden"
+ },
"404": {
"$ref": "#/responses/notFound"
}
@@ -17602,6 +17920,9 @@
"204": {
"$ref": "#/responses/empty"
},
+ "403": {
+ "$ref": "#/responses/forbidden"
+ },
"404": {
"$ref": "#/responses/notFound"
}
@@ -18278,6 +18599,9 @@
"200": {
"$ref": "#/responses/RepositoryList"
},
+ "403": {
+ "$ref": "#/responses/forbidden"
+ },
"404": {
"$ref": "#/responses/notFound"
}
@@ -18625,6 +18949,56 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
+ "ActionWorkflow": {
+ "description": "ActionWorkflow represents a ActionWorkflow",
+ "type": "object",
+ "properties": {
+ "badge_url": {
+ "type": "string",
+ "x-go-name": "BadgeURL"
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time",
+ "x-go-name": "CreatedAt"
+ },
+ "deleted_at": {
+ "type": "string",
+ "format": "date-time",
+ "x-go-name": "DeletedAt"
+ },
+ "html_url": {
+ "type": "string",
+ "x-go-name": "HTMLURL"
+ },
+ "id": {
+ "type": "string",
+ "x-go-name": "ID"
+ },
+ "name": {
+ "type": "string",
+ "x-go-name": "Name"
+ },
+ "path": {
+ "type": "string",
+ "x-go-name": "Path"
+ },
+ "state": {
+ "type": "string",
+ "x-go-name": "State"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time",
+ "x-go-name": "UpdatedAt"
+ },
+ "url": {
+ "type": "string",
+ "x-go-name": "URL"
+ }
+ },
+ "x-go-package": "code.gitea.io/gitea/modules/structs"
+ },
"Activity": {
"type": "object",
"properties": {
@@ -19633,6 +20007,28 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
+ "CreateActionWorkflowDispatch": {
+ "description": "CreateActionWorkflowDispatch represents the payload for triggering a workflow dispatch event",
+ "type": "object",
+ "required": [
+ "ref"
+ ],
+ "properties": {
+ "inputs": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ },
+ "x-go-name": "Inputs"
+ },
+ "ref": {
+ "type": "string",
+ "x-go-name": "Ref",
+ "example": "refs/heads/main"
+ }
+ },
+ "x-go-package": "code.gitea.io/gitea/modules/structs"
+ },
"CreateBranchProtectionOption": {
"description": "CreateBranchProtectionOption options for creating a branch protection",
"type": "object",
@@ -24207,6 +24603,22 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
+ "RenameOrgOption": {
+ "description": "RenameOrgOption options when renaming an organization",
+ "type": "object",
+ "required": [
+ "new_name"
+ ],
+ "properties": {
+ "new_name": {
+ "description": "New username for this org. This name cannot be in use yet by any other user.",
+ "type": "string",
+ "uniqueItems": true,
+ "x-go-name": "NewName"
+ }
+ },
+ "x-go-package": "code.gitea.io/gitea/modules/structs"
+ },
"RenameUserOption": {
"description": "RenameUserOption options when renaming a user",
"type": "object",
@@ -25616,6 +26028,21 @@
"$ref": "#/definitions/ActionVariable"
}
},
+ "ActionWorkflow": {
+ "description": "ActionWorkflow",
+ "schema": {
+ "$ref": "#/definitions/ActionWorkflow"
+ }
+ },
+ "ActionWorkflowList": {
+ "description": "ActionWorkflowList",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/ActionWorkflow"
+ }
+ }
+ },
"ActivityFeedsList": {
"description": "ActivityFeedsList",
"schema": {
diff --git a/templates/user/dashboard/dashboard.tmpl b/templates/user/dashboard/dashboard.tmpl
index 5dc46dc0a5..3ce3c1eb73 100644
--- a/templates/user/dashboard/dashboard.tmpl
+++ b/templates/user/dashboard/dashboard.tmpl
@@ -5,7 +5,11 @@
<div class="flex-container-main">
{{template "base/alert" .}}
{{template "user/heatmap" .}}
- {{template "user/dashboard/feeds" .}}
+ {{if .Feeds}}
+ {{template "user/dashboard/feeds" .}}
+ {{else}}
+ {{template "user/dashboard/guide" .}}
+ {{end}}
</div>
{{template "user/dashboard/repolist" .}}
</div>
diff --git a/templates/user/dashboard/guide.tmpl b/templates/user/dashboard/guide.tmpl
new file mode 100644
index 0000000000..bdbe81ece0
--- /dev/null
+++ b/templates/user/dashboard/guide.tmpl
@@ -0,0 +1,10 @@
+<div class="tw-text-center tw-p-8">
+ {{svg "octicon-package" 24 "tw-text-placeholder-text"}}
+ <h3 class="tw-my-4">{{ctx.Locale.Tr "home.guide_title"}}</h3>
+ <p class="tw-text-placeholder-text">{{ctx.Locale.Tr "home.guide_desc"}}</p>
+ <div>
+ <a href="{{AppSubUrl}}/explore/repos">{{ctx.Locale.Tr "home.explore_repos"}}</a>
+ <span>·</span>
+ <a href="{{AppSubUrl}}/explore/users">{{ctx.Locale.Tr "home.explore_users"}}</a>
+ </div>
+</div>
diff --git a/templates/user/dashboard/milestones.tmpl b/templates/user/dashboard/milestones.tmpl
index c0059d3cd4..7c1a69a6f5 100644
--- a/templates/user/dashboard/milestones.tmpl
+++ b/templates/user/dashboard/milestones.tmpl
@@ -100,7 +100,7 @@
{{if .TotalTrackedTime}}
<div class="flex-text-block">
{{svg "octicon-clock"}}
- {{.TotalTrackedTime|Sec2Time}}
+ {{.TotalTrackedTime|Sec2Hour}}
</div>
{{end}}
{{if .UpdatedUnix}}
diff --git a/templates/user/dashboard/repolist.tmpl b/templates/user/dashboard/repolist.tmpl
index a2764ba608..8b0fcbb401 100644
--- a/templates/user/dashboard/repolist.tmpl
+++ b/templates/user/dashboard/repolist.tmpl
@@ -5,6 +5,10 @@ const data = {
isMirrorsEnabled: {{.MirrorsEnabled}},
isStarsEnabled: {{not .IsDisableStars}},
+ canCreateMigrations: {{not .DisableMigrations}},
+
+ textNoOrg: {{ctx.Locale.Tr "home.empty_org"}},
+ textNoRepo: {{ctx.Locale.Tr "home.empty_repo"}},
textRepository: {{ctx.Locale.Tr "repository"}},
textOrganization: {{ctx.Locale.Tr "organization"}},
textMyRepos: {{ctx.Locale.Tr "home.my_repos"}},
diff --git a/templates/user/settings/keys_ssh.tmpl b/templates/user/settings/keys_ssh.tmpl
index b894ccdfbd..9d62d4ab08 100644
--- a/templates/user/settings/keys_ssh.tmpl
+++ b/templates/user/settings/keys_ssh.tmpl
@@ -78,7 +78,16 @@
<input readonly="" value="{{$.TokenToSign}}">
<div class="help">
<p>{{ctx.Locale.Tr "settings.ssh_token_help"}}</p>
- <p><code>{{printf "echo -n '%s' | ssh-keygen -Y sign -n gitea -f /path_to_PrivateKey_or_RelatedPublicKey" $.TokenToSign}}</code></p>
+ <p><code>echo -n '{{$.TokenToSign}}' | ssh-keygen -Y sign -n gitea -f /path_to_PrivateKey_or_RelatedPublicKey</code></p>
+ <details>
+ <summary>Windows PowerShell</summary>
+ <p><code>cmd /c "&lt;NUL set /p=`"{{$.TokenToSign}}`"| ssh-keygen -Y sign -n gitea -f /path_to_PrivateKey_or_RelatedPublicKey"</code></p>
+ </details>
+ <br>
+ <details>
+ <summary>Windows CMD</summary>
+ <p><code>set /p={{$.TokenToSign}}| ssh-keygen -Y sign -n gitea -f /path_to_PrivateKey_or_RelatedPublicKey</code></p>
+ </details>
</div>
<br>
</div>
diff --git a/templates/user/settings/profile.tmpl b/templates/user/settings/profile.tmpl
index 197763425c..03c3c18f28 100644
--- a/templates/user/settings/profile.tmpl
+++ b/templates/user/settings/profile.tmpl
@@ -124,13 +124,7 @@
</div>
<div class="inline field tw-pl-4">
- <label for="new-avatar">{{ctx.Locale.Tr "settings.choose_new_avatar"}}</label>
- <input id="new-avatar" name="avatar" type="file" accept="image/png,image/jpeg,image/gif,image/webp">
- </div>
-
- <div class="field tw-pl-4 cropper-panel tw-hidden">
- <div>{{ctx.Locale.Tr "settings.cropper_prompt"}}</div>
- <div class="cropper-wrapper"><img class="cropper-source" src alt></div>
+ {{template "shared/avatar_upload_crop" dict "LabelText" (ctx.Locale.Tr "settings.choose_new_avatar")}}
</div>
<div class="field">
diff --git a/tests/integration/actions_runner_modify_test.go b/tests/integration/actions_runner_modify_test.go
new file mode 100644
index 0000000000..feb3bc0893
--- /dev/null
+++ b/tests/integration/actions_runner_modify_test.go
@@ -0,0 +1,151 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "testing"
+
+ actions_model "code.gitea.io/gitea/models/actions"
+ "code.gitea.io/gitea/models/db"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestActionsRunnerModify(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ ctx := context.Background()
+
+ require.NoError(t, db.DeleteAllRecords("action_runner"))
+
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ _ = actions_model.CreateRunner(ctx, &actions_model.ActionRunner{OwnerID: user2.ID, Name: "user2-runner", TokenHash: "a", UUID: "a"})
+ user2Runner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{OwnerID: user2.ID, Name: "user2-runner"})
+ userWebURL := "/user/settings/actions/runners"
+
+ org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization})
+ require.NoError(t, actions_model.CreateRunner(ctx, &actions_model.ActionRunner{OwnerID: org3.ID, Name: "org3-runner", TokenHash: "b", UUID: "b"}))
+ org3Runner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{OwnerID: org3.ID, Name: "org3-runner"})
+ orgWebURL := "/org/org3/settings/actions/runners"
+
+ repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ _ = actions_model.CreateRunner(ctx, &actions_model.ActionRunner{RepoID: repo1.ID, Name: "repo1-runner", TokenHash: "c", UUID: "c"})
+ repo1Runner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{RepoID: repo1.ID, Name: "repo1-runner"})
+ repoWebURL := "/user2/repo1/settings/actions/runners"
+
+ _ = actions_model.CreateRunner(ctx, &actions_model.ActionRunner{Name: "global-runner", TokenHash: "d", UUID: "d"})
+ globalRunner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{Name: "global-runner"})
+ adminWebURL := "/-/admin/actions/runners"
+
+ sessionAdmin := loginUser(t, "user1")
+ sessionUser2 := loginUser(t, user2.Name)
+
+ doUpdate := func(t *testing.T, sess *TestSession, baseURL string, id int64, description string, expectedStatus int) {
+ req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/%d", baseURL, id), map[string]string{
+ "_csrf": GetUserCSRFToken(t, sess),
+ "description": description,
+ })
+ sess.MakeRequest(t, req, expectedStatus)
+ }
+
+ doDelete := func(t *testing.T, sess *TestSession, baseURL string, id int64, expectedStatus int) {
+ req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/%d/delete", baseURL, id), map[string]string{
+ "_csrf": GetUserCSRFToken(t, sess),
+ })
+ sess.MakeRequest(t, req, expectedStatus)
+ }
+
+ assertDenied := func(t *testing.T, sess *TestSession, baseURL string, id int64) {
+ doUpdate(t, sess, baseURL, id, "ChangedDescription", http.StatusNotFound)
+ doDelete(t, sess, baseURL, id, http.StatusNotFound)
+ v := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: id})
+ assert.Empty(t, v.Description)
+ }
+
+ assertSuccess := func(t *testing.T, sess *TestSession, baseURL string, id int64) {
+ doUpdate(t, sess, baseURL, id, "ChangedDescription", http.StatusSeeOther)
+ v := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: id})
+ assert.Equal(t, "ChangedDescription", v.Description)
+ doDelete(t, sess, baseURL, id, http.StatusOK)
+ unittest.AssertNotExistsBean(t, &actions_model.ActionRunner{ID: id})
+ }
+
+ t.Run("UpdateUserRunner", func(t *testing.T) {
+ theRunner := user2Runner
+ t.Run("FromOrg", func(t *testing.T) {
+ assertDenied(t, sessionAdmin, orgWebURL, theRunner.ID)
+ })
+ t.Run("FromRepo", func(t *testing.T) {
+ assertDenied(t, sessionAdmin, repoWebURL, theRunner.ID)
+ })
+ t.Run("FromAdmin", func(t *testing.T) {
+ t.Skip("Admin can update any runner (not right but not too bad)")
+ assertDenied(t, sessionAdmin, adminWebURL, theRunner.ID)
+ })
+ })
+
+ t.Run("UpdateOrgRunner", func(t *testing.T) {
+ theRunner := org3Runner
+ t.Run("FromRepo", func(t *testing.T) {
+ assertDenied(t, sessionAdmin, repoWebURL, theRunner.ID)
+ })
+ t.Run("FromUser", func(t *testing.T) {
+ assertDenied(t, sessionAdmin, userWebURL, theRunner.ID)
+ })
+ t.Run("FromAdmin", func(t *testing.T) {
+ t.Skip("Admin can update any runner (not right but not too bad)")
+ assertDenied(t, sessionAdmin, adminWebURL, theRunner.ID)
+ })
+ })
+
+ t.Run("UpdateRepoRunner", func(t *testing.T) {
+ theRunner := repo1Runner
+ t.Run("FromOrg", func(t *testing.T) {
+ assertDenied(t, sessionAdmin, orgWebURL, theRunner.ID)
+ })
+ t.Run("FromUser", func(t *testing.T) {
+ assertDenied(t, sessionAdmin, userWebURL, theRunner.ID)
+ })
+ t.Run("FromAdmin", func(t *testing.T) {
+ t.Skip("Admin can update any runner (not right but not too bad)")
+ assertDenied(t, sessionAdmin, adminWebURL, theRunner.ID)
+ })
+ })
+
+ t.Run("UpdateGlobalRunner", func(t *testing.T) {
+ theRunner := globalRunner
+ t.Run("FromOrg", func(t *testing.T) {
+ assertDenied(t, sessionAdmin, orgWebURL, theRunner.ID)
+ })
+ t.Run("FromUser", func(t *testing.T) {
+ assertDenied(t, sessionAdmin, userWebURL, theRunner.ID)
+ })
+ t.Run("FromRepo", func(t *testing.T) {
+ assertDenied(t, sessionAdmin, repoWebURL, theRunner.ID)
+ })
+ })
+
+ t.Run("UpdateSuccess", func(t *testing.T) {
+ t.Run("User", func(t *testing.T) {
+ assertSuccess(t, sessionUser2, userWebURL, user2Runner.ID)
+ })
+ t.Run("Org", func(t *testing.T) {
+ assertSuccess(t, sessionAdmin, orgWebURL, org3Runner.ID)
+ })
+ t.Run("Repo", func(t *testing.T) {
+ assertSuccess(t, sessionUser2, repoWebURL, repo1Runner.ID)
+ })
+ t.Run("Admin", func(t *testing.T) {
+ assertSuccess(t, sessionAdmin, adminWebURL, globalRunner.ID)
+ })
+ })
+}
diff --git a/tests/integration/actions_trigger_test.go b/tests/integration/actions_trigger_test.go
index 8ea9b34efe..096f51dfc0 100644
--- a/tests/integration/actions_trigger_test.go
+++ b/tests/integration/actions_trigger_test.go
@@ -5,6 +5,7 @@ package integration
import (
"fmt"
+ "net/http"
"net/url"
"strings"
"testing"
@@ -22,6 +23,7 @@ import (
actions_module "code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
+ "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
@@ -72,9 +74,19 @@ func TestPullRequestTargetEvent(t *testing.T) {
addWorkflowToBaseResp, err := files_service.ChangeRepoFiles(git.DefaultContext, baseRepo, user2, &files_service.ChangeRepoFilesOptions{
Files: []*files_service.ChangeRepoFile{
{
- Operation: "create",
- TreePath: ".gitea/workflows/pr.yml",
- ContentReader: strings.NewReader("name: test\non:\n pull_request_target:\n paths:\n - 'file_*.txt'\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - run: echo helloworld\n"),
+ Operation: "create",
+ TreePath: ".gitea/workflows/pr.yml",
+ ContentReader: strings.NewReader(`name: test
+on:
+ pull_request_target:
+ paths:
+ - 'file_*.txt'
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo helloworld
+`),
},
},
Message: "add workflow",
@@ -228,9 +240,19 @@ func TestSkipCI(t *testing.T) {
addWorkflowToBaseResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
Files: []*files_service.ChangeRepoFile{
{
- Operation: "create",
- TreePath: ".gitea/workflows/pr.yml",
- ContentReader: strings.NewReader("name: test\non:\n push:\n branches: [master]\n pull_request:\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - run: echo helloworld\n"),
+ Operation: "create",
+ TreePath: ".gitea/workflows/pr.yml",
+ ContentReader: strings.NewReader(`name: test
+on:
+ push:
+ branches: [master]
+ pull_request:
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo helloworld
+`),
},
},
Message: "add workflow",
@@ -347,9 +369,17 @@ func TestCreateDeleteRefEvent(t *testing.T) {
addWorkflowToBaseResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
Files: []*files_service.ChangeRepoFile{
{
- Operation: "create",
- TreePath: ".gitea/workflows/createdelete.yml",
- ContentReader: strings.NewReader("name: test\non:\n [create,delete]\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - run: echo helloworld\n"),
+ Operation: "create",
+ TreePath: ".gitea/workflows/createdelete.yml",
+ ContentReader: strings.NewReader(`name: test
+on:
+ [create,delete]
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo helloworld
+`),
},
},
Message: "add workflow",
@@ -461,9 +491,18 @@ func TestPullRequestCommitStatusEvent(t *testing.T) {
addWorkflow, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
Files: []*files_service.ChangeRepoFile{
{
- Operation: "create",
- TreePath: ".gitea/workflows/pr.yml",
- ContentReader: strings.NewReader("name: test\non:\n pull_request:\n types: [assigned, unassigned, labeled, unlabeled, opened, edited, closed, reopened, synchronize, milestoned, demilestoned, review_requested, review_request_removed]\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - run: echo helloworld\n"),
+ Operation: "create",
+ TreePath: ".gitea/workflows/pr.yml",
+ ContentReader: strings.NewReader(`name: test
+on:
+ pull_request:
+ types: [assigned, unassigned, labeled, unlabeled, opened, edited, closed, reopened, synchronize, milestoned, demilestoned, review_requested, review_request_removed]
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo helloworld
+`),
},
},
Message: "add workflow",
@@ -651,3 +690,681 @@ func insertFakeStatus(t *testing.T, repo *repo_model.Repository, sha, targetURL,
})
assert.NoError(t, err)
}
+
+func TestWorkflowDispatchPublicApi(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ session := loginUser(t, user2.Name)
+ token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
+
+ // create the repo
+ repo, err := repo_service.CreateRepository(db.DefaultContext, user2, user2, repo_service.CreateRepoOptions{
+ Name: "workflow-dispatch-event",
+ Description: "test workflow-dispatch ci event",
+ AutoInit: true,
+ Gitignores: "Go",
+ License: "MIT",
+ Readme: "Default",
+ DefaultBranch: "main",
+ IsPrivate: false,
+ })
+ assert.NoError(t, err)
+ assert.NotEmpty(t, repo)
+
+ // add workflow file to the repo
+ addWorkflowToBaseResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
+ Files: []*files_service.ChangeRepoFile{
+ {
+ Operation: "create",
+ TreePath: ".gitea/workflows/dispatch.yml",
+ ContentReader: strings.NewReader(`name: test
+on:
+ workflow_dispatch
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo helloworld
+`),
+ },
+ },
+ Message: "add workflow",
+ OldBranch: "main",
+ NewBranch: "main",
+ Author: &files_service.IdentityOptions{
+ GitUserName: user2.Name,
+ GitUserEmail: user2.Email,
+ },
+ Committer: &files_service.IdentityOptions{
+ GitUserName: user2.Name,
+ GitUserEmail: user2.Email,
+ },
+ Dates: &files_service.CommitDateOptions{
+ Author: time.Now(),
+ Committer: time.Now(),
+ },
+ })
+ assert.NoError(t, err)
+ assert.NotEmpty(t, addWorkflowToBaseResp)
+
+ // Get the commit ID of the default branch
+ gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo)
+ assert.NoError(t, err)
+ defer gitRepo.Close()
+ branch, err := git_model.GetBranch(db.DefaultContext, repo.ID, repo.DefaultBranch)
+ assert.NoError(t, err)
+ values := url.Values{}
+ values.Set("ref", "main")
+ req := NewRequestWithURLValues(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/workflows/dispatch.yml/dispatches", repo.FullName()), values).
+ AddTokenAuth(token)
+ _ = MakeRequest(t, req, http.StatusNoContent)
+
+ run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
+ Title: "add workflow",
+ RepoID: repo.ID,
+ Event: "workflow_dispatch",
+ Ref: "refs/heads/main",
+ WorkflowID: "dispatch.yml",
+ CommitSHA: branch.CommitID,
+ })
+ assert.NotNil(t, run)
+ })
+}
+
+func TestWorkflowDispatchPublicApiWithInputs(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ session := loginUser(t, user2.Name)
+ token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
+
+ // create the repo
+ repo, err := repo_service.CreateRepository(db.DefaultContext, user2, user2, repo_service.CreateRepoOptions{
+ Name: "workflow-dispatch-event",
+ Description: "test workflow-dispatch ci event",
+ AutoInit: true,
+ Gitignores: "Go",
+ License: "MIT",
+ Readme: "Default",
+ DefaultBranch: "main",
+ IsPrivate: false,
+ })
+ assert.NoError(t, err)
+ assert.NotEmpty(t, repo)
+
+ // add workflow file to the repo
+ addWorkflowToBaseResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
+ Files: []*files_service.ChangeRepoFile{
+ {
+ Operation: "create",
+ TreePath: ".gitea/workflows/dispatch.yml",
+ ContentReader: strings.NewReader(`name: test
+on:
+ workflow_dispatch: { inputs: { myinput: { default: def }, myinput2: { default: def2 }, myinput3: { type: boolean, default: false } } }
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo helloworld
+`),
+ },
+ },
+ Message: "add workflow",
+ OldBranch: "main",
+ NewBranch: "main",
+ Author: &files_service.IdentityOptions{
+ GitUserName: user2.Name,
+ GitUserEmail: user2.Email,
+ },
+ Committer: &files_service.IdentityOptions{
+ GitUserName: user2.Name,
+ GitUserEmail: user2.Email,
+ },
+ Dates: &files_service.CommitDateOptions{
+ Author: time.Now(),
+ Committer: time.Now(),
+ },
+ })
+ assert.NoError(t, err)
+ assert.NotEmpty(t, addWorkflowToBaseResp)
+
+ // Get the commit ID of the default branch
+ gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo)
+ assert.NoError(t, err)
+ defer gitRepo.Close()
+ branch, err := git_model.GetBranch(db.DefaultContext, repo.ID, repo.DefaultBranch)
+ assert.NoError(t, err)
+ values := url.Values{}
+ values.Set("ref", "main")
+ values.Set("inputs[myinput]", "val0")
+ values.Set("inputs[myinput3]", "true")
+ req := NewRequestWithURLValues(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/workflows/dispatch.yml/dispatches", repo.FullName()), values).
+ AddTokenAuth(token)
+ _ = MakeRequest(t, req, http.StatusNoContent)
+
+ run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
+ Title: "add workflow",
+ RepoID: repo.ID,
+ Event: "workflow_dispatch",
+ Ref: "refs/heads/main",
+ WorkflowID: "dispatch.yml",
+ CommitSHA: branch.CommitID,
+ })
+ assert.NotNil(t, run)
+ dispatchPayload := &api.WorkflowDispatchPayload{}
+ err = json.Unmarshal([]byte(run.EventPayload), dispatchPayload)
+ assert.NoError(t, err)
+ assert.Contains(t, dispatchPayload.Inputs, "myinput")
+ assert.Contains(t, dispatchPayload.Inputs, "myinput2")
+ assert.Contains(t, dispatchPayload.Inputs, "myinput3")
+ assert.Equal(t, "val0", dispatchPayload.Inputs["myinput"])
+ assert.Equal(t, "def2", dispatchPayload.Inputs["myinput2"])
+ assert.Equal(t, "true", dispatchPayload.Inputs["myinput3"])
+ })
+}
+
+func TestWorkflowDispatchPublicApiJSON(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ session := loginUser(t, user2.Name)
+ token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
+
+ // create the repo
+ repo, err := repo_service.CreateRepository(db.DefaultContext, user2, user2, repo_service.CreateRepoOptions{
+ Name: "workflow-dispatch-event",
+ Description: "test workflow-dispatch ci event",
+ AutoInit: true,
+ Gitignores: "Go",
+ License: "MIT",
+ Readme: "Default",
+ DefaultBranch: "main",
+ IsPrivate: false,
+ })
+ assert.NoError(t, err)
+ assert.NotEmpty(t, repo)
+
+ // add workflow file to the repo
+ addWorkflowToBaseResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
+ Files: []*files_service.ChangeRepoFile{
+ {
+ Operation: "create",
+ TreePath: ".gitea/workflows/dispatch.yml",
+ ContentReader: strings.NewReader(`name: test
+on:
+ workflow_dispatch: { inputs: { myinput: { default: def }, myinput2: { default: def2 }, myinput3: { type: boolean, default: false } } }
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo helloworld
+`),
+ },
+ },
+ Message: "add workflow",
+ OldBranch: "main",
+ NewBranch: "main",
+ Author: &files_service.IdentityOptions{
+ GitUserName: user2.Name,
+ GitUserEmail: user2.Email,
+ },
+ Committer: &files_service.IdentityOptions{
+ GitUserName: user2.Name,
+ GitUserEmail: user2.Email,
+ },
+ Dates: &files_service.CommitDateOptions{
+ Author: time.Now(),
+ Committer: time.Now(),
+ },
+ })
+ assert.NoError(t, err)
+ assert.NotEmpty(t, addWorkflowToBaseResp)
+
+ // Get the commit ID of the default branch
+ gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo)
+ assert.NoError(t, err)
+ defer gitRepo.Close()
+ branch, err := git_model.GetBranch(db.DefaultContext, repo.ID, repo.DefaultBranch)
+ assert.NoError(t, err)
+ inputs := &api.CreateActionWorkflowDispatch{
+ Ref: "main",
+ Inputs: map[string]string{
+ "myinput": "val0",
+ "myinput3": "true",
+ },
+ }
+
+ req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/workflows/dispatch.yml/dispatches", repo.FullName()), inputs).
+ AddTokenAuth(token)
+ _ = MakeRequest(t, req, http.StatusNoContent)
+
+ run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
+ Title: "add workflow",
+ RepoID: repo.ID,
+ Event: "workflow_dispatch",
+ Ref: "refs/heads/main",
+ WorkflowID: "dispatch.yml",
+ CommitSHA: branch.CommitID,
+ })
+ assert.NotNil(t, run)
+ })
+}
+
+func TestWorkflowDispatchPublicApiWithInputsJSON(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ session := loginUser(t, user2.Name)
+ token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
+
+ // create the repo
+ repo, err := repo_service.CreateRepository(db.DefaultContext, user2, user2, repo_service.CreateRepoOptions{
+ Name: "workflow-dispatch-event",
+ Description: "test workflow-dispatch ci event",
+ AutoInit: true,
+ Gitignores: "Go",
+ License: "MIT",
+ Readme: "Default",
+ DefaultBranch: "main",
+ IsPrivate: false,
+ })
+ assert.NoError(t, err)
+ assert.NotEmpty(t, repo)
+
+ // add workflow file to the repo
+ addWorkflowToBaseResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
+ Files: []*files_service.ChangeRepoFile{
+ {
+ Operation: "create",
+ TreePath: ".gitea/workflows/dispatch.yml",
+ ContentReader: strings.NewReader(`name: test
+on:
+ workflow_dispatch: { inputs: { myinput: { default: def }, myinput2: { default: def2 }, myinput3: { type: boolean, default: false } } }
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo helloworld
+`),
+ },
+ },
+ Message: "add workflow",
+ OldBranch: "main",
+ NewBranch: "main",
+ Author: &files_service.IdentityOptions{
+ GitUserName: user2.Name,
+ GitUserEmail: user2.Email,
+ },
+ Committer: &files_service.IdentityOptions{
+ GitUserName: user2.Name,
+ GitUserEmail: user2.Email,
+ },
+ Dates: &files_service.CommitDateOptions{
+ Author: time.Now(),
+ Committer: time.Now(),
+ },
+ })
+ assert.NoError(t, err)
+ assert.NotEmpty(t, addWorkflowToBaseResp)
+
+ // Get the commit ID of the default branch
+ gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo)
+ assert.NoError(t, err)
+ defer gitRepo.Close()
+ branch, err := git_model.GetBranch(db.DefaultContext, repo.ID, repo.DefaultBranch)
+ assert.NoError(t, err)
+ inputs := &api.CreateActionWorkflowDispatch{
+ Ref: "main",
+ Inputs: map[string]string{
+ "myinput": "val0",
+ "myinput3": "true",
+ },
+ }
+ req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/workflows/dispatch.yml/dispatches", repo.FullName()), inputs).
+ AddTokenAuth(token)
+ _ = MakeRequest(t, req, http.StatusNoContent)
+
+ run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
+ Title: "add workflow",
+ RepoID: repo.ID,
+ Event: "workflow_dispatch",
+ Ref: "refs/heads/main",
+ WorkflowID: "dispatch.yml",
+ CommitSHA: branch.CommitID,
+ })
+ assert.NotNil(t, run)
+ dispatchPayload := &api.WorkflowDispatchPayload{}
+ err = json.Unmarshal([]byte(run.EventPayload), dispatchPayload)
+ assert.NoError(t, err)
+ assert.Contains(t, dispatchPayload.Inputs, "myinput")
+ assert.Contains(t, dispatchPayload.Inputs, "myinput2")
+ assert.Contains(t, dispatchPayload.Inputs, "myinput3")
+ assert.Equal(t, "val0", dispatchPayload.Inputs["myinput"])
+ assert.Equal(t, "def2", dispatchPayload.Inputs["myinput2"])
+ assert.Equal(t, "true", dispatchPayload.Inputs["myinput3"])
+ })
+}
+
+func TestWorkflowDispatchPublicApiWithInputsNonDefaultBranchJSON(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ session := loginUser(t, user2.Name)
+ token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
+
+ // create the repo
+ repo, err := repo_service.CreateRepository(db.DefaultContext, user2, user2, repo_service.CreateRepoOptions{
+ Name: "workflow-dispatch-event",
+ Description: "test workflow-dispatch ci event",
+ AutoInit: true,
+ Gitignores: "Go",
+ License: "MIT",
+ Readme: "Default",
+ DefaultBranch: "main",
+ IsPrivate: false,
+ })
+ assert.NoError(t, err)
+ assert.NotEmpty(t, repo)
+
+ // add workflow file to the repo
+ addWorkflowToBaseResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
+ Files: []*files_service.ChangeRepoFile{
+ {
+ Operation: "create",
+ TreePath: ".gitea/workflows/dispatch.yml",
+ ContentReader: strings.NewReader(`name: test
+on:
+ workflow_dispatch
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo helloworld
+`),
+ },
+ },
+ Message: "add workflow",
+ OldBranch: "main",
+ NewBranch: "main",
+ Author: &files_service.IdentityOptions{
+ GitUserName: user2.Name,
+ GitUserEmail: user2.Email,
+ },
+ Committer: &files_service.IdentityOptions{
+ GitUserName: user2.Name,
+ GitUserEmail: user2.Email,
+ },
+ Dates: &files_service.CommitDateOptions{
+ Author: time.Now(),
+ Committer: time.Now(),
+ },
+ })
+ assert.NoError(t, err)
+ assert.NotEmpty(t, addWorkflowToBaseResp)
+
+ // add workflow file to the repo
+ addWorkflowToBaseResp, err = files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
+ Files: []*files_service.ChangeRepoFile{
+ {
+ Operation: "update",
+ TreePath: ".gitea/workflows/dispatch.yml",
+ ContentReader: strings.NewReader(`name: test
+on:
+ workflow_dispatch: { inputs: { myinput: { default: def }, myinput2: { default: def2 }, myinput3: { type: boolean, default: false } } }
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo helloworld
+`),
+ },
+ },
+ Message: "add workflow",
+ OldBranch: "main",
+ NewBranch: "dispatch",
+ Author: &files_service.IdentityOptions{
+ GitUserName: user2.Name,
+ GitUserEmail: user2.Email,
+ },
+ Committer: &files_service.IdentityOptions{
+ GitUserName: user2.Name,
+ GitUserEmail: user2.Email,
+ },
+ Dates: &files_service.CommitDateOptions{
+ Author: time.Now(),
+ Committer: time.Now(),
+ },
+ })
+ assert.NoError(t, err)
+ assert.NotEmpty(t, addWorkflowToBaseResp)
+
+ // Get the commit ID of the dispatch branch
+ gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo)
+ assert.NoError(t, err)
+ defer gitRepo.Close()
+ commit, err := gitRepo.GetBranchCommit("dispatch")
+ assert.NoError(t, err)
+ inputs := &api.CreateActionWorkflowDispatch{
+ Ref: "refs/heads/dispatch",
+ Inputs: map[string]string{
+ "myinput": "val0",
+ "myinput3": "true",
+ },
+ }
+ req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/workflows/dispatch.yml/dispatches", repo.FullName()), inputs).
+ AddTokenAuth(token)
+ _ = MakeRequest(t, req, http.StatusNoContent)
+
+ run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
+ Title: "add workflow",
+ RepoID: repo.ID,
+ Event: "workflow_dispatch",
+ Ref: "refs/heads/dispatch",
+ WorkflowID: "dispatch.yml",
+ CommitSHA: commit.ID.String(),
+ })
+ assert.NotNil(t, run)
+ dispatchPayload := &api.WorkflowDispatchPayload{}
+ err = json.Unmarshal([]byte(run.EventPayload), dispatchPayload)
+ assert.NoError(t, err)
+ assert.Contains(t, dispatchPayload.Inputs, "myinput")
+ assert.Contains(t, dispatchPayload.Inputs, "myinput2")
+ assert.Contains(t, dispatchPayload.Inputs, "myinput3")
+ assert.Equal(t, "val0", dispatchPayload.Inputs["myinput"])
+ assert.Equal(t, "def2", dispatchPayload.Inputs["myinput2"])
+ assert.Equal(t, "true", dispatchPayload.Inputs["myinput3"])
+ })
+}
+
+func TestWorkflowApi(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ session := loginUser(t, user2.Name)
+ token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
+
+ // create the repo
+ repo, err := repo_service.CreateRepository(db.DefaultContext, user2, user2, repo_service.CreateRepoOptions{
+ Name: "workflow-api",
+ Description: "test workflow apis",
+ AutoInit: true,
+ Gitignores: "Go",
+ License: "MIT",
+ Readme: "Default",
+ DefaultBranch: "main",
+ IsPrivate: false,
+ })
+ assert.NoError(t, err)
+ assert.NotEmpty(t, repo)
+
+ req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/workflows", repo.FullName())).
+ AddTokenAuth(token)
+ resp := MakeRequest(t, req, http.StatusOK)
+ workflows := &api.ActionWorkflowResponse{}
+ json.NewDecoder(resp.Body).Decode(workflows)
+ assert.Empty(t, workflows.Workflows)
+
+ // add workflow file to the repo
+ addWorkflowToBaseResp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, user2, &files_service.ChangeRepoFilesOptions{
+ Files: []*files_service.ChangeRepoFile{
+ {
+ Operation: "create",
+ TreePath: ".gitea/workflows/dispatch.yml",
+ ContentReader: strings.NewReader(`name: test
+on:
+ workflow_dispatch: { inputs: { myinput: { default: def }, myinput2: { default: def2 }, myinput3: { type: boolean, default: false } } }
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - run: echo helloworld
+`),
+ },
+ },
+ Message: "add workflow",
+ OldBranch: "main",
+ NewBranch: "main",
+ Author: &files_service.IdentityOptions{
+ GitUserName: user2.Name,
+ GitUserEmail: user2.Email,
+ },
+ Committer: &files_service.IdentityOptions{
+ GitUserName: user2.Name,
+ GitUserEmail: user2.Email,
+ },
+ Dates: &files_service.CommitDateOptions{
+ Author: time.Now(),
+ Committer: time.Now(),
+ },
+ })
+ assert.NoError(t, err)
+ assert.NotEmpty(t, addWorkflowToBaseResp)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/workflows", repo.FullName())).
+ AddTokenAuth(token)
+ resp = MakeRequest(t, req, http.StatusOK)
+ json.NewDecoder(resp.Body).Decode(workflows)
+ assert.Len(t, workflows.Workflows, 1)
+ assert.Equal(t, "dispatch.yml", workflows.Workflows[0].Name)
+ assert.Equal(t, ".gitea/workflows/dispatch.yml", workflows.Workflows[0].Path)
+ assert.Equal(t, ".gitea/workflows/dispatch.yml", workflows.Workflows[0].Path)
+ assert.Equal(t, "active", workflows.Workflows[0].State)
+
+ // Use a hardcoded api path
+ req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/workflows/%s", repo.FullName(), workflows.Workflows[0].ID)).
+ AddTokenAuth(token)
+ resp = MakeRequest(t, req, http.StatusOK)
+ workflow := &api.ActionWorkflow{}
+ json.NewDecoder(resp.Body).Decode(workflow)
+ assert.Equal(t, workflows.Workflows[0].ID, workflow.ID)
+ assert.Equal(t, workflows.Workflows[0].Path, workflow.Path)
+ assert.Equal(t, workflows.Workflows[0].URL, workflow.URL)
+ assert.Equal(t, workflows.Workflows[0].HTMLURL, workflow.HTMLURL)
+ assert.Equal(t, workflows.Workflows[0].Name, workflow.Name)
+ assert.Equal(t, workflows.Workflows[0].State, workflow.State)
+
+ // Use the provided url instead of the hardcoded one
+ req = NewRequest(t, "GET", workflows.Workflows[0].URL).
+ AddTokenAuth(token)
+ resp = MakeRequest(t, req, http.StatusOK)
+ workflow = &api.ActionWorkflow{}
+ json.NewDecoder(resp.Body).Decode(workflow)
+ assert.Equal(t, workflows.Workflows[0].ID, workflow.ID)
+ assert.Equal(t, workflows.Workflows[0].Path, workflow.Path)
+ assert.Equal(t, workflows.Workflows[0].URL, workflow.URL)
+ assert.Equal(t, workflows.Workflows[0].HTMLURL, workflow.HTMLURL)
+ assert.Equal(t, workflows.Workflows[0].Name, workflow.Name)
+ assert.Equal(t, workflows.Workflows[0].State, workflow.State)
+
+ // Disable the workflow
+ req = NewRequest(t, "PUT", workflows.Workflows[0].URL+"/disable").
+ AddTokenAuth(token)
+ _ = MakeRequest(t, req, http.StatusNoContent)
+
+ // Use the provided url instead of the hardcoded one
+ req = NewRequest(t, "GET", workflows.Workflows[0].URL).
+ AddTokenAuth(token)
+ resp = MakeRequest(t, req, http.StatusOK)
+ workflow = &api.ActionWorkflow{}
+ json.NewDecoder(resp.Body).Decode(workflow)
+ assert.Equal(t, workflows.Workflows[0].ID, workflow.ID)
+ assert.Equal(t, workflows.Workflows[0].Path, workflow.Path)
+ assert.Equal(t, workflows.Workflows[0].URL, workflow.URL)
+ assert.Equal(t, workflows.Workflows[0].HTMLURL, workflow.HTMLURL)
+ assert.Equal(t, workflows.Workflows[0].Name, workflow.Name)
+ assert.Equal(t, "disabled_manually", workflow.State)
+
+ inputs := &api.CreateActionWorkflowDispatch{
+ Ref: "main",
+ Inputs: map[string]string{
+ "myinput": "val0",
+ "myinput3": "true",
+ },
+ }
+ // Since the workflow is disabled, so the response code is 403 forbidden
+ req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/workflows/dispatch.yml/dispatches", repo.FullName()), inputs).
+ AddTokenAuth(token)
+ _ = MakeRequest(t, req, http.StatusForbidden)
+
+ // Enable the workflow again
+ req = NewRequest(t, "PUT", workflows.Workflows[0].URL+"/enable").
+ AddTokenAuth(token)
+ _ = MakeRequest(t, req, http.StatusNoContent)
+
+ // Use the provided url instead of the hardcoded one
+ req = NewRequest(t, "GET", workflows.Workflows[0].URL).
+ AddTokenAuth(token)
+ resp = MakeRequest(t, req, http.StatusOK)
+ workflow = &api.ActionWorkflow{}
+ json.NewDecoder(resp.Body).Decode(workflow)
+ assert.Equal(t, workflows.Workflows[0].ID, workflow.ID)
+ assert.Equal(t, workflows.Workflows[0].Path, workflow.Path)
+ assert.Equal(t, workflows.Workflows[0].URL, workflow.URL)
+ assert.Equal(t, workflows.Workflows[0].HTMLURL, workflow.HTMLURL)
+ assert.Equal(t, workflows.Workflows[0].Name, workflow.Name)
+ assert.Equal(t, workflows.Workflows[0].State, workflow.State)
+
+ req = NewRequest(t, "GET", workflows.Workflows[0].URL).
+ AddTokenAuth(token)
+ resp = MakeRequest(t, req, http.StatusOK)
+ workflow = &api.ActionWorkflow{}
+ json.NewDecoder(resp.Body).Decode(workflow)
+ assert.Equal(t, workflows.Workflows[0].ID, workflow.ID)
+ assert.Equal(t, workflows.Workflows[0].Path, workflow.Path)
+ assert.Equal(t, workflows.Workflows[0].URL, workflow.URL)
+ assert.Equal(t, workflows.Workflows[0].HTMLURL, workflow.HTMLURL)
+ assert.Equal(t, workflows.Workflows[0].Name, workflow.Name)
+ assert.Equal(t, workflows.Workflows[0].State, workflow.State)
+
+ // Get the commit ID of the default branch
+ gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo)
+ assert.NoError(t, err)
+ defer gitRepo.Close()
+ branch, err := git_model.GetBranch(db.DefaultContext, repo.ID, repo.DefaultBranch)
+ assert.NoError(t, err)
+ inputs = &api.CreateActionWorkflowDispatch{
+ Ref: "main",
+ Inputs: map[string]string{
+ "myinput": "val0",
+ "myinput3": "true",
+ },
+ }
+ req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/workflows/dispatch.yml/dispatches", repo.FullName()), inputs).
+ AddTokenAuth(token)
+ _ = MakeRequest(t, req, http.StatusNoContent)
+
+ run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
+ Title: "add workflow",
+ RepoID: repo.ID,
+ Event: "workflow_dispatch",
+ Ref: "refs/heads/main",
+ WorkflowID: "dispatch.yml",
+ CommitSHA: branch.CommitID,
+ })
+ assert.NotNil(t, run)
+ dispatchPayload := &api.WorkflowDispatchPayload{}
+ err = json.Unmarshal([]byte(run.EventPayload), dispatchPayload)
+ assert.NoError(t, err)
+ assert.Contains(t, dispatchPayload.Inputs, "myinput")
+ assert.Contains(t, dispatchPayload.Inputs, "myinput2")
+ assert.Contains(t, dispatchPayload.Inputs, "myinput3")
+ assert.Equal(t, "val0", dispatchPayload.Inputs["myinput"])
+ assert.Equal(t, "def2", dispatchPayload.Inputs["myinput2"])
+ assert.Equal(t, "true", dispatchPayload.Inputs["myinput3"])
+ })
+}
diff --git a/tests/integration/actions_variables_test.go b/tests/integration/actions_variables_test.go
new file mode 100644
index 0000000000..12c1c3f628
--- /dev/null
+++ b/tests/integration/actions_variables_test.go
@@ -0,0 +1,149 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "testing"
+
+ actions_model "code.gitea.io/gitea/models/actions"
+ "code.gitea.io/gitea/models/db"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/tests"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestActionsVariables(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ ctx := context.Background()
+
+ require.NoError(t, db.DeleteAllRecords("action_variable"))
+
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ _, _ = actions_model.InsertVariable(ctx, user2.ID, 0, "VAR", "user2-var")
+ user2Var := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{OwnerID: user2.ID, Name: "VAR"})
+ userWebURL := "/user/settings/actions/variables"
+
+ org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization})
+ _, _ = actions_model.InsertVariable(ctx, org3.ID, 0, "VAR", "org3-var")
+ org3Var := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{OwnerID: org3.ID, Name: "VAR"})
+ orgWebURL := "/org/org3/settings/actions/variables"
+
+ repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ _, _ = actions_model.InsertVariable(ctx, 0, repo1.ID, "VAR", "repo1-var")
+ repo1Var := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{RepoID: repo1.ID, Name: "VAR"})
+ repoWebURL := "/user2/repo1/settings/actions/variables"
+
+ _, _ = actions_model.InsertVariable(ctx, 0, 0, "VAR", "global-var")
+ globalVar := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{Name: "VAR", Data: "global-var"})
+ adminWebURL := "/-/admin/actions/variables"
+
+ sessionAdmin := loginUser(t, "user1")
+ sessionUser2 := loginUser(t, user2.Name)
+
+ doUpdate := func(t *testing.T, sess *TestSession, baseURL string, id int64, data string, expectedStatus int) {
+ req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/%d/edit", baseURL, id), map[string]string{
+ "_csrf": GetUserCSRFToken(t, sess),
+ "name": "VAR",
+ "data": data,
+ })
+ sess.MakeRequest(t, req, expectedStatus)
+ }
+
+ doDelete := func(t *testing.T, sess *TestSession, baseURL string, id int64, expectedStatus int) {
+ req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/%d/delete", baseURL, id), map[string]string{
+ "_csrf": GetUserCSRFToken(t, sess),
+ })
+ sess.MakeRequest(t, req, expectedStatus)
+ }
+
+ assertDenied := func(t *testing.T, sess *TestSession, baseURL string, id int64) {
+ doUpdate(t, sess, baseURL, id, "ChangedData", http.StatusNotFound)
+ doDelete(t, sess, baseURL, id, http.StatusNotFound)
+ v := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{ID: id})
+ assert.Contains(t, v.Data, "-var")
+ }
+
+ assertSuccess := func(t *testing.T, sess *TestSession, baseURL string, id int64) {
+ doUpdate(t, sess, baseURL, id, "ChangedData", http.StatusOK)
+ v := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{ID: id})
+ assert.Equal(t, "ChangedData", v.Data)
+ doDelete(t, sess, baseURL, id, http.StatusOK)
+ unittest.AssertNotExistsBean(t, &actions_model.ActionVariable{ID: id})
+ }
+
+ t.Run("UpdateUserVar", func(t *testing.T) {
+ theVar := user2Var
+ t.Run("FromOrg", func(t *testing.T) {
+ assertDenied(t, sessionAdmin, orgWebURL, theVar.ID)
+ })
+ t.Run("FromRepo", func(t *testing.T) {
+ assertDenied(t, sessionAdmin, repoWebURL, theVar.ID)
+ })
+ t.Run("FromAdmin", func(t *testing.T) {
+ assertDenied(t, sessionAdmin, adminWebURL, theVar.ID)
+ })
+ })
+
+ t.Run("UpdateOrgVar", func(t *testing.T) {
+ theVar := org3Var
+ t.Run("FromRepo", func(t *testing.T) {
+ assertDenied(t, sessionAdmin, repoWebURL, theVar.ID)
+ })
+ t.Run("FromUser", func(t *testing.T) {
+ assertDenied(t, sessionAdmin, userWebURL, theVar.ID)
+ })
+ t.Run("FromAdmin", func(t *testing.T) {
+ assertDenied(t, sessionAdmin, adminWebURL, theVar.ID)
+ })
+ })
+
+ t.Run("UpdateRepoVar", func(t *testing.T) {
+ theVar := repo1Var
+ t.Run("FromOrg", func(t *testing.T) {
+ assertDenied(t, sessionAdmin, orgWebURL, theVar.ID)
+ })
+ t.Run("FromUser", func(t *testing.T) {
+ assertDenied(t, sessionAdmin, userWebURL, theVar.ID)
+ })
+ t.Run("FromAdmin", func(t *testing.T) {
+ assertDenied(t, sessionAdmin, adminWebURL, theVar.ID)
+ })
+ })
+
+ t.Run("UpdateGlobalVar", func(t *testing.T) {
+ theVar := globalVar
+ t.Run("FromOrg", func(t *testing.T) {
+ assertDenied(t, sessionAdmin, orgWebURL, theVar.ID)
+ })
+ t.Run("FromUser", func(t *testing.T) {
+ assertDenied(t, sessionAdmin, userWebURL, theVar.ID)
+ })
+ t.Run("FromRepo", func(t *testing.T) {
+ assertDenied(t, sessionAdmin, repoWebURL, theVar.ID)
+ })
+ })
+
+ t.Run("UpdateSuccess", func(t *testing.T) {
+ t.Run("User", func(t *testing.T) {
+ assertSuccess(t, sessionUser2, userWebURL, user2Var.ID)
+ })
+ t.Run("Org", func(t *testing.T) {
+ assertSuccess(t, sessionAdmin, orgWebURL, org3Var.ID)
+ })
+ t.Run("Repo", func(t *testing.T) {
+ assertSuccess(t, sessionUser2, repoWebURL, repo1Var.ID)
+ })
+ t.Run("Admin", func(t *testing.T) {
+ assertSuccess(t, sessionAdmin, adminWebURL, globalVar.ID)
+ })
+ })
+}
diff --git a/tests/integration/api_org_test.go b/tests/integration/api_org_test.go
index fff121490c..d766b1e8be 100644
--- a/tests/integration/api_org_test.go
+++ b/tests/integration/api_org_test.go
@@ -6,7 +6,6 @@ package integration
import (
"fmt"
"net/http"
- "net/url"
"strings"
"testing"
@@ -19,46 +18,52 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
-func TestAPIOrgCreate(t *testing.T) {
- onGiteaRun(t, func(*testing.T, *url.URL) {
- token := getUserToken(t, "user1", auth_model.AccessTokenScopeWriteOrganization)
-
- org := api.CreateOrgOption{
- UserName: "user1_org",
- FullName: "User1's organization",
- Description: "This organization created by user1",
- Website: "https://try.gitea.io",
- Location: "Shanghai",
- Visibility: "limited",
- }
- req := NewRequestWithJSON(t, "POST", "/api/v1/orgs", &org).
- AddTokenAuth(token)
- resp := MakeRequest(t, req, http.StatusCreated)
-
- var apiOrg api.Organization
- DecodeJSON(t, resp, &apiOrg)
-
- assert.Equal(t, org.UserName, apiOrg.Name)
- assert.Equal(t, org.FullName, apiOrg.FullName)
- assert.Equal(t, org.Description, apiOrg.Description)
- assert.Equal(t, org.Website, apiOrg.Website)
- assert.Equal(t, org.Location, apiOrg.Location)
- assert.Equal(t, org.Visibility, apiOrg.Visibility)
-
- unittest.AssertExistsAndLoadBean(t, &user_model.User{
- Name: org.UserName,
- LowerName: strings.ToLower(org.UserName),
- FullName: org.FullName,
- })
+func TestAPIOrgCreateRename(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ token := getUserToken(t, "user1", auth_model.AccessTokenScopeWriteOrganization)
+
+ org := api.CreateOrgOption{
+ UserName: "user1_org",
+ FullName: "User1's organization",
+ Description: "This organization created by user1",
+ Website: "https://try.gitea.io",
+ Location: "Shanghai",
+ Visibility: "limited",
+ }
+ req := NewRequestWithJSON(t, "POST", "/api/v1/orgs", &org).AddTokenAuth(token)
+ resp := MakeRequest(t, req, http.StatusCreated)
+
+ var apiOrg api.Organization
+ DecodeJSON(t, resp, &apiOrg)
+
+ assert.Equal(t, org.UserName, apiOrg.Name)
+ assert.Equal(t, org.FullName, apiOrg.FullName)
+ assert.Equal(t, org.Description, apiOrg.Description)
+ assert.Equal(t, org.Website, apiOrg.Website)
+ assert.Equal(t, org.Location, apiOrg.Location)
+ assert.Equal(t, org.Visibility, apiOrg.Visibility)
+
+ unittest.AssertExistsAndLoadBean(t, &user_model.User{
+ Name: org.UserName,
+ LowerName: strings.ToLower(org.UserName),
+ FullName: org.FullName,
+ })
+ // check org name
+ req = NewRequestf(t, "GET", "/api/v1/orgs/%s", org.UserName).AddTokenAuth(token)
+ resp = MakeRequest(t, req, http.StatusOK)
+ DecodeJSON(t, resp, &apiOrg)
+ assert.EqualValues(t, org.UserName, apiOrg.Name)
+
+ t.Run("CheckPermission", func(t *testing.T) {
// Check owner team permission
ownerTeam, _ := org_model.GetOwnerTeam(db.DefaultContext, apiOrg.ID)
-
for _, ut := range unit_model.AllRepoUnitTypes {
up := perm.AccessModeOwner
if ut == unit_model.TypeExternalTracker || ut == unit_model.TypeExternalWiki {
@@ -71,103 +76,101 @@ func TestAPIOrgCreate(t *testing.T) {
AccessMode: up,
})
}
+ })
- req = NewRequestf(t, "GET", "/api/v1/orgs/%s", org.UserName).
- AddTokenAuth(token)
+ t.Run("CheckMembers", func(t *testing.T) {
+ req = NewRequestf(t, "GET", "/api/v1/orgs/%s/members", org.UserName).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
- DecodeJSON(t, resp, &apiOrg)
- assert.EqualValues(t, org.UserName, apiOrg.Name)
- req = NewRequestf(t, "GET", "/api/v1/orgs/%s/repos", org.UserName).
- AddTokenAuth(token)
- resp = MakeRequest(t, req, http.StatusOK)
+ // user1 on this org is public
+ var users []*api.User
+ DecodeJSON(t, resp, &users)
+ assert.Len(t, users, 1)
+ assert.EqualValues(t, "user1", users[0].UserName)
+ })
+
+ t.Run("RenameOrg", func(t *testing.T) {
+ req = NewRequestWithJSON(t, "POST", "/api/v1/orgs/user1_org/rename", &api.RenameOrgOption{
+ NewName: "renamed_org",
+ }).AddTokenAuth(token)
+ MakeRequest(t, req, http.StatusNoContent)
+ unittest.AssertExistsAndLoadBean(t, &org_model.Organization{Name: "renamed_org"})
+ org.UserName = "renamed_org" // update the variable so the following tests could still use it
+ })
+ t.Run("ListRepos", func(t *testing.T) {
+ // FIXME: this test is wrong, there is no repository at all, so the for-loop is empty
+ req = NewRequestf(t, "GET", "/api/v1/orgs/%s/repos", org.UserName).AddTokenAuth(token)
+ resp = MakeRequest(t, req, http.StatusOK)
var repos []*api.Repository
DecodeJSON(t, resp, &repos)
for _, repo := range repos {
assert.False(t, repo.Private)
}
-
- req = NewRequestf(t, "GET", "/api/v1/orgs/%s/members", org.UserName).
- AddTokenAuth(token)
- resp = MakeRequest(t, req, http.StatusOK)
-
- // user1 on this org is public
- var users []*api.User
- DecodeJSON(t, resp, &users)
- assert.Len(t, users, 1)
- assert.EqualValues(t, "user1", users[0].UserName)
})
}
func TestAPIOrgEdit(t *testing.T) {
- onGiteaRun(t, func(*testing.T, *url.URL) {
- session := loginUser(t, "user1")
-
- token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization)
- org := api.EditOrgOption{
- FullName: "Org3 organization new full name",
- Description: "A new description",
- Website: "https://try.gitea.io/new",
- Location: "Beijing",
- Visibility: "private",
- }
- req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).
- AddTokenAuth(token)
- resp := MakeRequest(t, req, http.StatusOK)
-
- var apiOrg api.Organization
- DecodeJSON(t, resp, &apiOrg)
-
- assert.Equal(t, "org3", apiOrg.Name)
- assert.Equal(t, org.FullName, apiOrg.FullName)
- assert.Equal(t, org.Description, apiOrg.Description)
- assert.Equal(t, org.Website, apiOrg.Website)
- assert.Equal(t, org.Location, apiOrg.Location)
- assert.Equal(t, org.Visibility, apiOrg.Visibility)
- })
+ defer tests.PrepareTestEnv(t)()
+ session := loginUser(t, "user1")
+
+ token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization)
+ org := api.EditOrgOption{
+ FullName: "Org3 organization new full name",
+ Description: "A new description",
+ Website: "https://try.gitea.io/new",
+ Location: "Beijing",
+ Visibility: "private",
+ }
+ req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).
+ AddTokenAuth(token)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var apiOrg api.Organization
+ DecodeJSON(t, resp, &apiOrg)
+
+ assert.Equal(t, "org3", apiOrg.Name)
+ assert.Equal(t, org.FullName, apiOrg.FullName)
+ assert.Equal(t, org.Description, apiOrg.Description)
+ assert.Equal(t, org.Website, apiOrg.Website)
+ assert.Equal(t, org.Location, apiOrg.Location)
+ assert.Equal(t, org.Visibility, apiOrg.Visibility)
}
func TestAPIOrgEditBadVisibility(t *testing.T) {
- onGiteaRun(t, func(*testing.T, *url.URL) {
- session := loginUser(t, "user1")
-
- token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization)
- org := api.EditOrgOption{
- FullName: "Org3 organization new full name",
- Description: "A new description",
- Website: "https://try.gitea.io/new",
- Location: "Beijing",
- Visibility: "badvisibility",
- }
- req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).
- AddTokenAuth(token)
- MakeRequest(t, req, http.StatusUnprocessableEntity)
- })
+ defer tests.PrepareTestEnv(t)()
+ session := loginUser(t, "user1")
+
+ token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization)
+ org := api.EditOrgOption{
+ FullName: "Org3 organization new full name",
+ Description: "A new description",
+ Website: "https://try.gitea.io/new",
+ Location: "Beijing",
+ Visibility: "badvisibility",
+ }
+ req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).
+ AddTokenAuth(token)
+ MakeRequest(t, req, http.StatusUnprocessableEntity)
}
func TestAPIOrgDeny(t *testing.T) {
- onGiteaRun(t, func(*testing.T, *url.URL) {
- setting.Service.RequireSignInView = true
- defer func() {
- setting.Service.RequireSignInView = false
- }()
+ defer tests.PrepareTestEnv(t)()
+ defer test.MockVariableValue(&setting.Service.RequireSignInView, true)()
- orgName := "user1_org"
- req := NewRequestf(t, "GET", "/api/v1/orgs/%s", orgName)
- MakeRequest(t, req, http.StatusNotFound)
+ orgName := "user1_org"
+ req := NewRequestf(t, "GET", "/api/v1/orgs/%s", orgName)
+ MakeRequest(t, req, http.StatusNotFound)
- req = NewRequestf(t, "GET", "/api/v1/orgs/%s/repos", orgName)
- MakeRequest(t, req, http.StatusNotFound)
+ req = NewRequestf(t, "GET", "/api/v1/orgs/%s/repos", orgName)
+ MakeRequest(t, req, http.StatusNotFound)
- req = NewRequestf(t, "GET", "/api/v1/orgs/%s/members", orgName)
- MakeRequest(t, req, http.StatusNotFound)
- })
+ req = NewRequestf(t, "GET", "/api/v1/orgs/%s/members", orgName)
+ MakeRequest(t, req, http.StatusNotFound)
}
func TestAPIGetAll(t *testing.T) {
defer tests.PrepareTestEnv(t)()
-
token := getUserToken(t, "user1", auth_model.AccessTokenScopeReadOrganization)
// accessing with a token will return all orgs
@@ -192,37 +195,36 @@ func TestAPIGetAll(t *testing.T) {
}
func TestAPIOrgSearchEmptyTeam(t *testing.T) {
- onGiteaRun(t, func(*testing.T, *url.URL) {
- token := getUserToken(t, "user1", auth_model.AccessTokenScopeWriteOrganization)
- orgName := "org_with_empty_team"
-
- // create org
- req := NewRequestWithJSON(t, "POST", "/api/v1/orgs", &api.CreateOrgOption{
- UserName: orgName,
- }).AddTokenAuth(token)
- MakeRequest(t, req, http.StatusCreated)
-
- // create team with no member
- req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/%s/teams", orgName), &api.CreateTeamOption{
- Name: "Empty",
- IncludesAllRepositories: true,
- Permission: "read",
- Units: []string{"repo.code", "repo.issues", "repo.ext_issues", "repo.wiki", "repo.pulls"},
- }).AddTokenAuth(token)
- MakeRequest(t, req, http.StatusCreated)
-
- // case-insensitive search for teams that have no members
- req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/teams/search?q=%s", orgName, "empty")).
- AddTokenAuth(token)
- resp := MakeRequest(t, req, http.StatusOK)
- data := struct {
- Ok bool
- Data []*api.Team
- }{}
- DecodeJSON(t, resp, &data)
- assert.True(t, data.Ok)
- if assert.Len(t, data.Data, 1) {
- assert.EqualValues(t, "Empty", data.Data[0].Name)
- }
- })
+ defer tests.PrepareTestEnv(t)()
+ token := getUserToken(t, "user1", auth_model.AccessTokenScopeWriteOrganization)
+ orgName := "org_with_empty_team"
+
+ // create org
+ req := NewRequestWithJSON(t, "POST", "/api/v1/orgs", &api.CreateOrgOption{
+ UserName: orgName,
+ }).AddTokenAuth(token)
+ MakeRequest(t, req, http.StatusCreated)
+
+ // create team with no member
+ req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/%s/teams", orgName), &api.CreateTeamOption{
+ Name: "Empty",
+ IncludesAllRepositories: true,
+ Permission: "read",
+ Units: []string{"repo.code", "repo.issues", "repo.ext_issues", "repo.wiki", "repo.pulls"},
+ }).AddTokenAuth(token)
+ MakeRequest(t, req, http.StatusCreated)
+
+ // case-insensitive search for teams that have no members
+ req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/teams/search?q=%s", orgName, "empty")).
+ AddTokenAuth(token)
+ resp := MakeRequest(t, req, http.StatusOK)
+ data := struct {
+ Ok bool
+ Data []*api.Team
+ }{}
+ DecodeJSON(t, resp, &data)
+ assert.True(t, data.Ok)
+ if assert.Len(t, data.Data, 1) {
+ assert.EqualValues(t, "Empty", data.Data[0].Name)
+ }
}
diff --git a/tests/integration/api_user_star_test.go b/tests/integration/api_user_star_test.go
index 0062889a92..368756528a 100644
--- a/tests/integration/api_user_star_test.go
+++ b/tests/integration/api_user_star_test.go
@@ -11,7 +11,9 @@ import (
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
@@ -91,3 +93,65 @@ func TestAPIStar(t *testing.T) {
MakeRequest(t, req, http.StatusNoContent)
})
}
+
+func TestAPIStarDisabled(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+
+ user := "user1"
+ repo := "user2/repo1"
+
+ session := loginUser(t, user)
+ token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser)
+ tokenWithUserScope := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteRepository)
+
+ defer test.MockVariableValue(&setting.Repository.DisableStars, true)()
+
+ t.Run("Star", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/starred/%s", repo)).
+ AddTokenAuth(tokenWithUserScope)
+ MakeRequest(t, req, http.StatusForbidden)
+
+ user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
+ req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/starred/%s", repo)).
+ AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteRepository))
+ MakeRequest(t, req, http.StatusForbidden)
+ })
+
+ t.Run("GetStarredRepos", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/users/%s/starred", user)).
+ AddTokenAuth(token)
+ MakeRequest(t, req, http.StatusForbidden)
+ })
+
+ t.Run("GetMyStarredRepos", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", "/api/v1/user/starred").
+ AddTokenAuth(tokenWithUserScope)
+ MakeRequest(t, req, http.StatusForbidden)
+ })
+
+ t.Run("IsStarring", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/starred/%s", repo)).
+ AddTokenAuth(tokenWithUserScope)
+ MakeRequest(t, req, http.StatusForbidden)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/starred/%s", repo+"notexisting")).
+ AddTokenAuth(tokenWithUserScope)
+ MakeRequest(t, req, http.StatusForbidden)
+ })
+
+ t.Run("Unstar", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/starred/%s", repo)).
+ AddTokenAuth(tokenWithUserScope)
+ MakeRequest(t, req, http.StatusForbidden)
+ })
+}
diff --git a/tests/integration/auth_ldap_test.go b/tests/integration/auth_ldap_test.go
index 5c50fd0288..0599c43805 100644
--- a/tests/integration/auth_ldap_test.go
+++ b/tests/integration/auth_ldap_test.go
@@ -279,7 +279,7 @@ func TestLDAPUserSyncWithEmptyUsernameAttribute(t *testing.T) {
htmlDoc := NewHTMLParser(t, resp.Body)
- tr := htmlDoc.doc.Find("table.table tbody tr")
+ tr := htmlDoc.doc.Find("table.table tbody tr:not(.no-results-row)")
assert.Equal(t, 0, tr.Length())
}
diff --git a/tests/integration/benchmarks_test.go b/tests/integration/benchmarks_test.go
deleted file mode 100644
index 62da761d2d..0000000000
--- a/tests/integration/benchmarks_test.go
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright 2017 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package integration
-
-import (
- "math/rand/v2"
- "net/http"
- "net/url"
- "testing"
-
- repo_model "code.gitea.io/gitea/models/repo"
- "code.gitea.io/gitea/models/unittest"
- api "code.gitea.io/gitea/modules/structs"
-)
-
-// StringWithCharset random string (from https://www.calhoun.io/creating-random-strings-in-go/)
-func StringWithCharset(length int, charset string) string {
- b := make([]byte, length)
- for i := range b {
- b[i] = charset[rand.IntN(len(charset))]
- }
- return string(b)
-}
-
-func BenchmarkRepoBranchCommit(b *testing.B) {
- onGiteaRun(b, func(b *testing.B, u *url.URL) {
- samples := []int64{1, 2, 3}
- b.ResetTimer()
-
- for _, repoID := range samples {
- b.StopTimer()
- repo := unittest.AssertExistsAndLoadBean(b, &repo_model.Repository{ID: repoID})
- b.StartTimer()
- b.Run(repo.Name, func(b *testing.B) {
- session := loginUser(b, "user2")
- b.ResetTimer()
- b.Run("CreateBranch", func(b *testing.B) {
- b.StopTimer()
- branchName := StringWithCharset(5+rand.IntN(10), "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
- b.StartTimer()
- for i := 0; i < b.N; i++ {
- b.Run("new_"+branchName, func(b *testing.B) {
- b.Skip("benchmark broken") // TODO fix
- testAPICreateBranch(b, session, repo.OwnerName, repo.Name, repo.DefaultBranch, "new_"+branchName, http.StatusCreated)
- })
- }
- })
- b.Run("GetBranches", func(b *testing.B) {
- req := NewRequestf(b, "GET", "/api/v1/repos/%s/branches", repo.FullName())
- session.MakeRequest(b, req, http.StatusOK)
- })
- b.Run("AccessCommits", func(b *testing.B) {
- var branches []*api.Branch
- req := NewRequestf(b, "GET", "/api/v1/repos/%s/branches", repo.FullName())
- resp := session.MakeRequest(b, req, http.StatusOK)
- DecodeJSON(b, resp, &branches)
- b.ResetTimer() // We measure from here
- if len(branches) != 0 {
- for i := 0; i < b.N; i++ {
- req := NewRequestf(b, "GET", "/api/v1/repos/%s/commits?sha=%s", repo.FullName(), branches[i%len(branches)].Name)
- session.MakeRequest(b, req, http.StatusOK)
- }
- }
- })
- })
- }
- })
-}
diff --git a/tests/integration/org_worktime_test.go b/tests/integration/org_worktime_test.go
new file mode 100644
index 0000000000..fb5216be8d
--- /dev/null
+++ b/tests/integration/org_worktime_test.go
@@ -0,0 +1,293 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package integration_test
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/organization"
+ "code.gitea.io/gitea/models/unittest"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// TestTimesByRepos tests TimesByRepos functionality
+func testTimesByRepos(t *testing.T) {
+ kases := []struct {
+ name string
+ unixfrom int64
+ unixto int64
+ orgname int64
+ expected []organization.WorktimeSumByRepos
+ }{
+ {
+ name: "Full sum for org 1",
+ unixfrom: 0,
+ unixto: 9223372036854775807,
+ orgname: 1,
+ expected: []organization.WorktimeSumByRepos(nil),
+ },
+ {
+ name: "Full sum for org 2",
+ unixfrom: 0,
+ unixto: 9223372036854775807,
+ orgname: 2,
+ expected: []organization.WorktimeSumByRepos{
+ {
+ RepoName: "repo1",
+ SumTime: 4083,
+ },
+ {
+ RepoName: "repo2",
+ SumTime: 75,
+ },
+ },
+ },
+ {
+ name: "Simple time bound",
+ unixfrom: 946684801,
+ unixto: 946684802,
+ orgname: 2,
+ expected: []organization.WorktimeSumByRepos{
+ {
+ RepoName: "repo1",
+ SumTime: 3662,
+ },
+ },
+ },
+ {
+ name: "Both times inclusive",
+ unixfrom: 946684801,
+ unixto: 946684801,
+ orgname: 2,
+ expected: []organization.WorktimeSumByRepos{
+ {
+ RepoName: "repo1",
+ SumTime: 3661,
+ },
+ },
+ },
+ {
+ name: "Should ignore deleted",
+ unixfrom: 947688814,
+ unixto: 947688815,
+ orgname: 2,
+ expected: []organization.WorktimeSumByRepos{
+ {
+ RepoName: "repo2",
+ SumTime: 71,
+ },
+ },
+ },
+ }
+
+ // Run test kases
+ for _, kase := range kases {
+ t.Run(kase.name, func(t *testing.T) {
+ org, err := organization.GetOrgByID(db.DefaultContext, kase.orgname)
+ assert.NoError(t, err)
+ results, err := organization.GetWorktimeByRepos(org, kase.unixfrom, kase.unixto)
+ assert.NoError(t, err)
+ assert.Equal(t, kase.expected, results)
+ })
+ }
+}
+
+// TestTimesByMilestones tests TimesByMilestones functionality
+func testTimesByMilestones(t *testing.T) {
+ kases := []struct {
+ name string
+ unixfrom int64
+ unixto int64
+ orgname int64
+ expected []organization.WorktimeSumByMilestones
+ }{
+ {
+ name: "Full sum for org 1",
+ unixfrom: 0,
+ unixto: 9223372036854775807,
+ orgname: 1,
+ expected: []organization.WorktimeSumByMilestones(nil),
+ },
+ {
+ name: "Full sum for org 2",
+ unixfrom: 0,
+ unixto: 9223372036854775807,
+ orgname: 2,
+ expected: []organization.WorktimeSumByMilestones{
+ {
+ RepoName: "repo1",
+ MilestoneName: "",
+ MilestoneID: 0,
+ SumTime: 401,
+ HideRepoName: false,
+ },
+ {
+ RepoName: "repo1",
+ MilestoneName: "milestone1",
+ MilestoneID: 1,
+ SumTime: 3682,
+ HideRepoName: true,
+ },
+ {
+ RepoName: "repo2",
+ MilestoneName: "",
+ MilestoneID: 0,
+ SumTime: 75,
+ HideRepoName: false,
+ },
+ },
+ },
+ {
+ name: "Simple time bound",
+ unixfrom: 946684801,
+ unixto: 946684802,
+ orgname: 2,
+ expected: []organization.WorktimeSumByMilestones{
+ {
+ RepoName: "repo1",
+ MilestoneName: "milestone1",
+ MilestoneID: 1,
+ SumTime: 3662,
+ HideRepoName: false,
+ },
+ },
+ },
+ {
+ name: "Both times inclusive",
+ unixfrom: 946684801,
+ unixto: 946684801,
+ orgname: 2,
+ expected: []organization.WorktimeSumByMilestones{
+ {
+ RepoName: "repo1",
+ MilestoneName: "milestone1",
+ MilestoneID: 1,
+ SumTime: 3661,
+ HideRepoName: false,
+ },
+ },
+ },
+ {
+ name: "Should ignore deleted",
+ unixfrom: 947688814,
+ unixto: 947688815,
+ orgname: 2,
+ expected: []organization.WorktimeSumByMilestones{
+ {
+ RepoName: "repo2",
+ MilestoneName: "",
+ MilestoneID: 0,
+ SumTime: 71,
+ HideRepoName: false,
+ },
+ },
+ },
+ }
+
+ // Run test kases
+ for _, kase := range kases {
+ t.Run(kase.name, func(t *testing.T) {
+ org, err := organization.GetOrgByID(db.DefaultContext, kase.orgname)
+ require.NoError(t, err)
+ results, err := organization.GetWorktimeByMilestones(org, kase.unixfrom, kase.unixto)
+ if assert.NoError(t, err) {
+ assert.Equal(t, kase.expected, results)
+ }
+ })
+ }
+}
+
+// TestTimesByMembers tests TimesByMembers functionality
+func testTimesByMembers(t *testing.T) {
+ kases := []struct {
+ name string
+ unixfrom int64
+ unixto int64
+ orgname int64
+ expected []organization.WorktimeSumByMembers
+ }{
+ {
+ name: "Full sum for org 1",
+ unixfrom: 0,
+ unixto: 9223372036854775807,
+ orgname: 1,
+ expected: []organization.WorktimeSumByMembers(nil),
+ },
+ {
+ // Test case: Sum of times forever in org no. 2
+ name: "Full sum for org 2",
+ unixfrom: 0,
+ unixto: 9223372036854775807,
+ orgname: 2,
+ expected: []organization.WorktimeSumByMembers{
+ {
+ UserName: "user2",
+ SumTime: 3666,
+ },
+ {
+ UserName: "user1",
+ SumTime: 491,
+ },
+ },
+ },
+ {
+ name: "Simple time bound",
+ unixfrom: 946684801,
+ unixto: 946684802,
+ orgname: 2,
+ expected: []organization.WorktimeSumByMembers{
+ {
+ UserName: "user2",
+ SumTime: 3662,
+ },
+ },
+ },
+ {
+ name: "Both times inclusive",
+ unixfrom: 946684801,
+ unixto: 946684801,
+ orgname: 2,
+ expected: []organization.WorktimeSumByMembers{
+ {
+ UserName: "user2",
+ SumTime: 3661,
+ },
+ },
+ },
+ {
+ name: "Should ignore deleted",
+ unixfrom: 947688814,
+ unixto: 947688815,
+ orgname: 2,
+ expected: []organization.WorktimeSumByMembers{
+ {
+ UserName: "user1",
+ SumTime: 71,
+ },
+ },
+ },
+ }
+
+ // Run test kases
+ for _, kase := range kases {
+ t.Run(kase.name, func(t *testing.T) {
+ org, err := organization.GetOrgByID(db.DefaultContext, kase.orgname)
+ assert.NoError(t, err)
+ results, err := organization.GetWorktimeByMembers(org, kase.unixfrom, kase.unixto)
+ assert.NoError(t, err)
+ assert.Equal(t, kase.expected, results)
+ })
+ }
+}
+
+func TestOrgWorktime(t *testing.T) {
+ // we need to run these tests in integration test because there are complex SQL queries
+ assert.NoError(t, unittest.PrepareTestDatabase())
+ t.Run("ByRepos", testTimesByRepos)
+ t.Run("ByMilestones", testTimesByMilestones)
+ t.Run("ByMembers", testTimesByMembers)
+}
diff --git a/tests/integration/repo_webhook_test.go b/tests/integration/repo_webhook_test.go
index 17905513c3..2f9a815fef 100644
--- a/tests/integration/repo_webhook_test.go
+++ b/tests/integration/repo_webhook_test.go
@@ -16,6 +16,7 @@ import (
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
@@ -66,6 +67,19 @@ func testAPICreateWebhookForRepo(t *testing.T, session *TestSession, userName, r
MakeRequest(t, req, http.StatusCreated)
}
+func testCreateWebhookForRepo(t *testing.T, session *TestSession, webhookType, userName, repoName, url, eventKind string) {
+ csrf := GetUserCSRFToken(t, session)
+ req := NewRequestWithValues(t, "POST", "/"+userName+"/"+repoName+"/settings/hooks/"+webhookType+"/new", map[string]string{
+ "_csrf": csrf,
+ "payload_url": url,
+ "events": eventKind,
+ "active": "true",
+ "content_type": fmt.Sprintf("%d", webhook.ContentTypeJSON),
+ "http_method": "POST",
+ })
+ session.MakeRequest(t, req, http.StatusSeeOther)
+}
+
func testAPICreateWebhookForOrg(t *testing.T, session *TestSession, userName, url, event string) {
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll)
req := NewRequestWithJSON(t, "POST", "/api/v1/orgs/"+userName+"/hooks", api.CreateHookOption{
@@ -562,3 +576,28 @@ func Test_WebhookStatus(t *testing.T) {
assert.EqualValues(t, commitID, payloads[0].SHA)
})
}
+
+func Test_WebhookStatus_NoWrongTrigger(t *testing.T) {
+ var trigger string
+ provider := newMockWebhookProvider(func(r *http.Request) {
+ assert.NotContains(t, r.Header["X-Github-Event-Type"], "status", "X-GitHub-Event-Type should not contain status")
+ assert.NotContains(t, r.Header["X-Gitea-Event-Type"], "status", "X-Gitea-Event-Type should not contain status")
+ assert.NotContains(t, r.Header["X-Gogs-Event-Type"], "status", "X-Gogs-Event-Type should not contain status")
+ trigger = "push"
+ }, http.StatusOK)
+ defer provider.Close()
+
+ onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
+ // 1. create a new webhook with special webhook for repo1
+ session := loginUser(t, "user2")
+
+ // create a push_only webhook from web UI
+ testCreateWebhookForRepo(t, session, "gitea", "user2", "repo1", provider.URL(), "push_only")
+
+ // 2. trigger the webhook with a push action
+ testCreateFile(t, session, "user2", "repo1", "master", "test_webhook_push.md", "# a test file for webhook push")
+
+ // 3. validate the webhook is triggered with right event
+ assert.EqualValues(t, "push", trigger)
+ })
+}
diff --git a/web_src/css/features/cropper.css b/web_src/css/features/cropper.css
index ed7171e770..f7f8168006 100644
--- a/web_src/css/features/cropper.css
+++ b/web_src/css/features/cropper.css
@@ -1,6 +1,6 @@
@import "cropperjs/dist/cropper.css";
-.page-content.user.profile .cropper-panel .cropper-wrapper {
+.avatar-file-with-cropper + .cropper-panel .cropper-wrapper {
max-width: 400px;
max-height: 400px;
}
diff --git a/web_src/js/components/ActionRunStatus.vue b/web_src/js/components/ActionRunStatus.vue
index 96c6c441be..487d2460cc 100644
--- a/web_src/js/components/ActionRunStatus.vue
+++ b/web_src/js/components/ActionRunStatus.vue
@@ -19,12 +19,12 @@ withDefaults(defineProps<{
<template>
<span :data-tooltip-content="localeStatus ?? status" v-if="status">
- <SvgIcon name="octicon-check-circle-fill" class="text green" :size="size" :class-name="className" v-if="status === 'success'"/>
- <SvgIcon name="octicon-skip" class="text grey" :size="size" :class-name="className" v-else-if="status === 'skipped'"/>
- <SvgIcon name="octicon-stop" class="text yellow" :size="size" :class-name="className" v-else-if="status === 'cancelled'"/>
- <SvgIcon name="octicon-clock" class="text yellow" :size="size" :class-name="className" v-else-if="status === 'waiting'"/>
- <SvgIcon name="octicon-blocked" class="text yellow" :size="size" :class-name="className" v-else-if="status === 'blocked'"/>
- <SvgIcon name="octicon-meter" class="text yellow" :size="size" :class-name="'job-status-rotate ' + className" v-else-if="status === 'running'"/>
+ <SvgIcon name="octicon-check-circle-fill" class="text green" :size="size" :class="className" v-if="status === 'success'"/>
+ <SvgIcon name="octicon-skip" class="text grey" :size="size" :class="className" v-else-if="status === 'skipped'"/>
+ <SvgIcon name="octicon-stop" class="text yellow" :size="size" :class="className" v-else-if="status === 'cancelled'"/>
+ <SvgIcon name="octicon-clock" class="text yellow" :size="size" :class="className" v-else-if="status === 'waiting'"/>
+ <SvgIcon name="octicon-blocked" class="text yellow" :size="size" :class="className" v-else-if="status === 'blocked'"/>
+ <SvgIcon name="octicon-meter" class="text yellow" :size="size" :class="'job-status-rotate ' + className" v-else-if="status === 'running'"/>
<SvgIcon name="octicon-x-circle-fill" class="text red" :size="size" v-else/><!-- failure, unknown -->
</span>
</template>
diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue
index 876292fc94..1840e89144 100644
--- a/web_src/js/components/DashboardRepoList.vue
+++ b/web_src/js/components/DashboardRepoList.vue
@@ -113,7 +113,7 @@ export default defineComponent({
this.changeReposFilter(this.reposFilter);
fomanticQuery(el.querySelector('.ui.dropdown')).dropdown();
nextTick(() => {
- this.$refs.search.focus();
+ this.$refs.search?.focus();
});
this.textArchivedFilterTitles = {
@@ -243,7 +243,7 @@ export default defineComponent({
if (!this.reposTotalCount) {
const totalCountSearchURL = `${this.subUrl}/repo/search?count_only=1&uid=${this.uid}&team_id=${this.teamId}&q=&page=1&mode=`;
response = await GET(totalCountSearchURL);
- this.reposTotalCount = response.headers.get('X-Total-Count') ?? '?';
+ this.reposTotalCount = parseInt(response.headers.get('X-Total-Count') ?? '0');
}
response = await GET(searchedURL);
@@ -336,7 +336,6 @@ export default defineComponent({
},
},
});
-
</script>
<template>
<div>
@@ -354,7 +353,15 @@ export default defineComponent({
<svg-icon name="octicon-plus"/>
</a>
</h4>
- <div class="ui attached segment repos-search">
+ <div v-if="!reposTotalCount" class="ui attached segment">
+ <div v-if="!isLoading" class="empty-repo-or-org">
+ <svg-icon name="octicon-git-branch" :size="24"/>
+ <p>{{ textNoRepo }}</p>
+ </div>
+ <!-- using the loading indicator here will cause more (unnecessary) page flickers, so at the moment, not use the loading indicator -->
+ <!-- <div v-else class="is-loading loading-icon-2px tw-min-h-16"/> -->
+ </div>
+ <div v-else class="ui attached segment repos-search">
<div class="ui small fluid action left icon input">
<input type="search" spellcheck="false" maxlength="255" @input="changeReposFilter(reposFilter)" v-model="searchQuery" ref="search" @keydown="reposFilterKeyControl" :placeholder="textSearchRepos">
<i class="icon loading-icon-3px" :class="{'is-loading': isLoading}"><svg-icon name="octicon-search" :size="16"/></i>
@@ -367,7 +374,7 @@ export default defineComponent({
otherwise if the "input" handles click event for intermediate status, it breaks the internal state-->
<input type="checkbox" class="tw-pointer-events-none" v-bind.prop="checkboxArchivedFilterProps">
<label>
- <svg-icon name="octicon-archive" :size="16" class-name="tw-mr-1"/>
+ <svg-icon name="octicon-archive" :size="16" class="tw-mr-1"/>
{{ textShowArchived }}
</label>
</div>
@@ -376,7 +383,7 @@ export default defineComponent({
<div class="ui checkbox" ref="checkboxPrivateFilter" :title="checkboxPrivateFilterTitle">
<input type="checkbox" class="tw-pointer-events-none" v-bind.prop="checkboxPrivateFilterProps">
<label>
- <svg-icon name="octicon-lock" :size="16" class-name="tw-mr-1"/>
+ <svg-icon name="octicon-lock" :size="16" class="tw-mr-1"/>
{{ textShowPrivate }}
</label>
</div>
@@ -413,7 +420,7 @@ export default defineComponent({
<ul class="repo-owner-name-list">
<li class="tw-flex tw-items-center tw-py-2" v-for="repo, index in repos" :class="{'active': index === activeIndex}" :key="repo.id">
<a class="repo-list-link muted" :href="repo.link">
- <svg-icon :name="repoIcon(repo)" :size="16" class-name="repo-list-icon"/>
+ <svg-icon :name="repoIcon(repo)" :size="16" class="repo-list-icon"/>
<div class="text truncate">{{ repo.full_name }}</div>
<div v-if="repo.archived">
<svg-icon name="octicon-archive" :size="16"/>
@@ -421,7 +428,7 @@ export default defineComponent({
</a>
<a class="tw-flex tw-items-center" v-if="repo.latest_commit_status_state" :href="repo.latest_commit_status_state_link" :data-tooltip-content="repo.locale_latest_commit_status_state">
<!-- the commit status icon logic is taken from templates/repo/commit_status.tmpl -->
- <svg-icon :name="statusIcon(repo.latest_commit_status_state)" :class-name="'tw-ml-2 commit-status icon text ' + statusColor(repo.latest_commit_status_state)" :size="16"/>
+ <svg-icon :name="statusIcon(repo.latest_commit_status_state)" :class="'tw-ml-2 commit-status icon text ' + statusColor(repo.latest_commit_status_state)" :size="16"/>
</a>
</li>
</ul>
@@ -432,26 +439,26 @@ export default defineComponent({
class="item navigation tw-py-1" :class="{'disabled': page === 1}"
@click="changePage(1)" :title="textFirstPage"
>
- <svg-icon name="gitea-double-chevron-left" :size="16" class-name="tw-mr-1"/>
+ <svg-icon name="gitea-double-chevron-left" :size="16" class="tw-mr-1"/>
</a>
<a
class="item navigation tw-py-1" :class="{'disabled': page === 1}"
@click="changePage(page - 1)" :title="textPreviousPage"
>
- <svg-icon name="octicon-chevron-left" :size="16" clsas-name="tw-mr-1"/>
+ <svg-icon name="octicon-chevron-left" :size="16" clsas="tw-mr-1"/>
</a>
<a class="active item tw-py-1">{{ page }}</a>
<a
class="item navigation" :class="{'disabled': page === finalPage}"
@click="changePage(page + 1)" :title="textNextPage"
>
- <svg-icon name="octicon-chevron-right" :size="16" class-name="tw-ml-1"/>
+ <svg-icon name="octicon-chevron-right" :size="16" class="tw-ml-1"/>
</a>
<a
class="item navigation tw-py-1" :class="{'disabled': page === finalPage}"
@click="changePage(finalPage)" :title="textLastPage"
>
- <svg-icon name="gitea-double-chevron-right" :size="16" class-name="tw-ml-1"/>
+ <svg-icon name="gitea-double-chevron-right" :size="16" class="tw-ml-1"/>
</a>
</div>
</div>
@@ -467,11 +474,17 @@ export default defineComponent({
<svg-icon name="octicon-plus"/>
</a>
</h4>
- <div v-if="organizations.length" class="ui attached table segment tw-rounded-b">
+ <div v-if="!organizations.length" class="ui attached segment">
+ <div class="empty-repo-or-org">
+ <svg-icon name="octicon-organization" :size="24"/>
+ <p>{{ textNoOrg }}</p>
+ </div>
+ </div>
+ <div v-else class="ui attached table segment tw-rounded-b">
<ul class="repo-owner-name-list">
<li class="tw-flex tw-items-center tw-py-2" v-for="org in organizations" :key="org.name">
<a class="repo-list-link muted" :href="subUrl + '/' + encodeURIComponent(org.name)">
- <svg-icon name="octicon-organization" :size="16" class-name="repo-list-icon"/>
+ <svg-icon name="octicon-organization" :size="16" class="repo-list-icon"/>
<div class="text truncate">{{ org.full_name ? `${org.full_name} (${org.name})` : org.name }}</div>
<div><!-- div to prevent underline of label on hover -->
<span class="ui tiny basic label" v-if="org.org_visibility !== 'public'">
@@ -481,7 +494,7 @@ export default defineComponent({
</a>
<div class="text light grey tw-flex tw-items-center tw-ml-2">
{{ org.num_repos }}
- <svg-icon name="octicon-repo" :size="16" class-name="tw-ml-1 tw-mt-0.5"/>
+ <svg-icon name="octicon-repo" :size="16" class="tw-ml-1 tw-mt-0.5"/>
</div>
</li>
</ul>
@@ -546,4 +559,14 @@ ul li:not(:last-child) {
.repo-owner-name-list li.active {
background: var(--color-hover);
}
+
+.empty-repo-or-org {
+ margin-top: 1em;
+ text-align: center;
+ color: var(--color-placeholder-text);
+}
+
+.empty-repo-or-org p {
+ margin: 1em auto;
+}
</style>
diff --git a/web_src/js/components/RepoBranchTagSelector.vue b/web_src/js/components/RepoBranchTagSelector.vue
index fa5c75af99..820e69d9ab 100644
--- a/web_src/js/components/RepoBranchTagSelector.vue
+++ b/web_src/js/components/RepoBranchTagSelector.vue
@@ -226,7 +226,7 @@ export default defineComponent({
<strong ref="dropdownRefName" class="tw-ml-2 tw-inline-block gt-ellipsis">{{ currentRefShortName }}</strong>
</template>
</span>
- <svg-icon name="octicon-triangle-down" :size="14" class-name="dropdown icon"/>
+ <svg-icon name="octicon-triangle-down" :size="14" class="dropdown icon"/>
</div>
<div class="menu transition" :class="{visible: menuVisible}" v-show="menuVisible" v-cloak>
<div class="ui icon search input">
@@ -235,10 +235,10 @@ export default defineComponent({
</div>
<div v-if="showTabBranches" class="branch-tag-tab">
<a class="branch-tag-item muted" :class="{active: selectedTab === 'branches'}" href="#" @click="handleTabSwitch('branches')">
- <svg-icon name="octicon-git-branch" :size="16" class-name="tw-mr-1"/>{{ textBranches }}
+ <svg-icon name="octicon-git-branch" :size="16" class="tw-mr-1"/>{{ textBranches }}
</a>
<a v-if="showTabTags" class="branch-tag-item muted" :class="{active: selectedTab === 'tags'}" href="#" @click="handleTabSwitch('tags')">
- <svg-icon name="octicon-tag" :size="16" class-name="tw-mr-1"/>{{ textTags }}
+ <svg-icon name="octicon-tag" :size="16" class="tw-mr-1"/>{{ textTags }}
</a>
</div>
<div class="branch-tag-divider"/>
diff --git a/web_src/js/features/admin/common.ts b/web_src/js/features/admin/common.ts
index b991749d81..14a49af81e 100644
--- a/web_src/js/features/admin/common.ts
+++ b/web_src/js/features/admin/common.ts
@@ -1,7 +1,8 @@
import $ from 'jquery';
import {checkAppUrl} from '../common-page.ts';
-import {hideElem, showElem, toggleElem} from '../../utils/dom.ts';
+import {hideElem, queryElems, showElem, toggleElem} from '../../utils/dom.ts';
import {POST} from '../../modules/fetch.ts';
+import {initAvatarUploaderWithCropper} from '../comp/Cropper.ts';
const {appSubUrl} = window.config;
@@ -258,4 +259,6 @@ export function initAdminCommon(): void {
window.location.href = this.getAttribute('data-redirect');
});
}
+
+ queryElems(document, '.avatar-file-with-cropper', initAvatarUploaderWithCropper);
}
diff --git a/web_src/js/features/common-organization.ts b/web_src/js/features/common-organization.ts
index a1f19bedea..9d5964c4c7 100644
--- a/web_src/js/features/common-organization.ts
+++ b/web_src/js/features/common-organization.ts
@@ -1,5 +1,6 @@
import {initCompLabelEdit} from './comp/LabelEdit.ts';
-import {toggleElem} from '../utils/dom.ts';
+import {queryElems, toggleElem} from '../utils/dom.ts';
+import {initAvatarUploaderWithCropper} from './comp/Cropper.ts';
export function initCommonOrganization() {
if (!document.querySelectorAll('.organization').length) {
@@ -13,4 +14,6 @@ export function initCommonOrganization() {
// Labels
initCompLabelEdit('.page-content.organization.settings.labels');
+
+ queryElems(document, '.avatar-file-with-cropper', initAvatarUploaderWithCropper);
}
diff --git a/web_src/js/features/comp/Cropper.ts b/web_src/js/features/comp/Cropper.ts
index e65dcfbe13..aaa1691152 100644
--- a/web_src/js/features/comp/Cropper.ts
+++ b/web_src/js/features/comp/Cropper.ts
@@ -6,7 +6,7 @@ type CropperOpts = {
fileInput: HTMLInputElement,
}
-export async function initCompCropper({container, fileInput, imageSource}: CropperOpts) {
+async function initCompCropper({container, fileInput, imageSource}: CropperOpts) {
const {default: Cropper} = await import(/* webpackChunkName: "cropperjs" */'cropperjs');
let currentFileName = '';
let currentFileLastModified = 0;
@@ -38,3 +38,10 @@ export async function initCompCropper({container, fileInput, imageSource}: Cropp
}
});
}
+
+export async function initAvatarUploaderWithCropper(fileInput: HTMLInputElement) {
+ const panel = fileInput.nextElementSibling as HTMLElement;
+ if (!panel?.matches('.cropper-panel')) throw new Error('Missing cropper panel for avatar uploader');
+ const imageSource = panel.querySelector<HTMLImageElement>('.cropper-source');
+ await initCompCropper({container: panel, fileInput, imageSource});
+}
diff --git a/web_src/js/features/comp/TextExpander.ts b/web_src/js/features/comp/TextExpander.ts
index 87d2b3a7a4..5be234629d 100644
--- a/web_src/js/features/comp/TextExpander.ts
+++ b/web_src/js/features/comp/TextExpander.ts
@@ -6,7 +6,7 @@ import {createElementFromAttrs, createElementFromHTML} from '../../utils/dom.ts'
import {getIssueColor, getIssueIcon} from '../issue.ts';
import {debounce} from 'perfect-debounce';
import type TextExpanderElement from '@github/text-expander-element';
-import type {TextExpanderChangeEvent, TextExpanderResult} from '@github/text-expander-element/dist/text-expander-element.d.ts';
+import type {TextExpanderChangeEvent, TextExpanderResult} from '@github/text-expander-element';
async function fetchIssueSuggestions(key: string, text: string): Promise<TextExpanderResult> {
const issuePathInfo = parseIssueHref(window.location.href);
diff --git a/web_src/js/features/repo-issue.ts b/web_src/js/features/repo-issue.ts
index a0cb875a87..f5455393b2 100644
--- a/web_src/js/features/repo-issue.ts
+++ b/web_src/js/features/repo-issue.ts
@@ -421,13 +421,11 @@ export function initRepoPullRequestReview() {
// The following part is only for diff views
if (!$('.repository.pull.diff').length) return;
- const $reviewBtn = $('.js-btn-review');
- const $panel = $reviewBtn.parent().find('.review-box-panel');
- const $closeBtn = $panel.find('.close');
-
- if ($reviewBtn.length && $panel.length) {
- const tippy = createTippy($reviewBtn[0], {
- content: $panel[0],
+ const elReviewBtn = document.querySelector('.js-btn-review');
+ const elReviewPanel = document.querySelector('.review-box-panel.tippy-target');
+ if (elReviewBtn && elReviewPanel) {
+ const tippy = createTippy(elReviewBtn, {
+ content: elReviewPanel,
theme: 'default',
placement: 'bottom',
trigger: 'click',
@@ -435,11 +433,7 @@ export function initRepoPullRequestReview() {
interactive: true,
hideOnClick: true,
});
-
- $closeBtn.on('click', (e) => {
- e.preventDefault();
- tippy.hide();
- });
+ elReviewPanel.querySelector('.close').addEventListener('click', () => tippy.hide());
}
addDelegatedEventListener(document, 'click', '.add-code-comment', async (el, e) => {
diff --git a/web_src/js/features/repo-settings.ts b/web_src/js/features/repo-settings.ts
index b61ef9a153..7e890a43e0 100644
--- a/web_src/js/features/repo-settings.ts
+++ b/web_src/js/features/repo-settings.ts
@@ -3,6 +3,7 @@ import {minimatch} from 'minimatch';
import {createMonaco} from './codeeditor.ts';
import {onInputDebounce, queryElems, toggleElem} from '../utils/dom.ts';
import {POST} from '../modules/fetch.ts';
+import {initAvatarUploaderWithCropper} from './comp/Cropper.ts';
import {initRepoSettingsBranchesDrag} from './repo-settings-branches.ts';
const {appSubUrl, csrfToken} = window.config;
@@ -156,4 +157,6 @@ export function initRepoSettings() {
initRepoSettingsSearchTeamBox();
initRepoSettingsGitHook();
initRepoSettingsBranchesDrag();
+
+ queryElems(document, '.avatar-file-with-cropper', initAvatarUploaderWithCropper);
}
diff --git a/web_src/js/features/user-settings.ts b/web_src/js/features/user-settings.ts
index 6312a8b682..21d20e676f 100644
--- a/web_src/js/features/user-settings.ts
+++ b/web_src/js/features/user-settings.ts
@@ -1,17 +1,10 @@
-import {hideElem, showElem} from '../utils/dom.ts';
-import {initCompCropper} from './comp/Cropper.ts';
-
-function initUserSettingsAvatarCropper() {
- const fileInput = document.querySelector<HTMLInputElement>('#new-avatar');
- const container = document.querySelector<HTMLElement>('.user.settings.profile .cropper-panel');
- const imageSource = container.querySelector<HTMLImageElement>('.cropper-source');
- initCompCropper({container, fileInput, imageSource});
-}
+import {hideElem, queryElems, showElem} from '../utils/dom.ts';
+import {initAvatarUploaderWithCropper} from './comp/Cropper.ts';
export function initUserSettings() {
if (!document.querySelector('.user.settings.profile')) return;
- initUserSettingsAvatarCropper();
+ queryElems(document, '.avatar-file-with-cropper', initAvatarUploaderWithCropper);
const usernameInput = document.querySelector<HTMLInputElement>('#username');
if (!usernameInput) return;
diff --git a/web_src/js/svg.test.ts b/web_src/js/svg.test.ts
index 7f3e0496ec..715b739a82 100644
--- a/web_src/js/svg.test.ts
+++ b/web_src/js/svg.test.ts
@@ -16,12 +16,11 @@ test('svgParseOuterInner', () => {
test('SvgIcon', () => {
const root = document.createElement('div');
- createApp({render: () => h(SvgIcon, {name: 'octicon-link', size: 24, class: 'base', className: 'extra'})}).mount(root);
+ createApp({render: () => h(SvgIcon, {name: 'octicon-link', size: 24, class: 'base'})}).mount(root);
const node = root.firstChild as Element;
expect(node.nodeName).toEqual('svg');
expect(node.getAttribute('width')).toEqual('24');
expect(node.getAttribute('height')).toEqual('24');
expect(node.classList.contains('octicon-link')).toBeTruthy();
expect(node.classList.contains('base')).toBeTruthy();
- expect(node.classList.contains('extra')).toBeTruthy();
});
diff --git a/web_src/js/svg.ts b/web_src/js/svg.ts
index b193afb255..8316cbcf85 100644
--- a/web_src/js/svg.ts
+++ b/web_src/js/svg.ts
@@ -201,7 +201,6 @@ export const SvgIcon = defineComponent({
props: {
name: {type: String as PropType<SvgName>, required: true},
size: {type: Number, default: 16},
- className: {type: String, default: ''},
symbolId: {type: String},
},
render() {
@@ -216,15 +215,7 @@ export const SvgIcon = defineComponent({
attrs[`^width`] = this.size;
attrs[`^height`] = this.size;
- // make the <SvgIcon class="foo" class-name="bar"> classes work together
- const classes: Array<string> = [];
- for (const cls of svgOuter.classList) {
- classes.push(cls);
- }
- // TODO: drop the `className/class-name` prop in the future, only use "class" prop
- if (this.className) {
- classes.push(...this.className.split(/\s+/).filter(Boolean));
- }
+ const classes = Array.from(svgOuter.classList);
if (this.symbolId) {
classes.push('tw-hidden', 'svg-symbol-container');
svgInnerHtml = `<symbol id="${this.symbolId}" viewBox="${attrs['^viewBox']}">${svgInnerHtml}</symbol>`;
diff --git a/web_src/svg/gitea-feishu.svg b/web_src/svg/gitea-feishu.svg
new file mode 100644
index 0000000000..57941978d1
--- /dev/null
+++ b/web_src/svg/gitea-feishu.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="7 7 26 26" width="20" height="20"><path d="M21.069 20.504l.063-.06.125-.122.085-.084.256-.254.348-.344.299-.296.281-.278.293-.289.269-.266.374-.37.218-.206.419-.359.404-.306.598-.386.617-.33.606-.265.348-.127.177-.058a14.78 14.78 0 0 0-2.793-5.603c-.252-.318-.639-.502-1.047-.502H12.221c-.196 0-.277.249-.119.364a31.49 31.49 0 0 1 8.943 10.162c.008-.007.016-.015.025-.023z" fill="#00d6b9"/><path d="M16.791 30c5.57 0 10.423-3.074 12.955-7.618.089-.159.175-.321.258-.484a6.12 6.12 0 0 1-.425.699c-.055.078-.111.155-.17.23a6.29 6.29 0 0 1-.225.274c-.062.07-.123.138-.188.206a5.61 5.61 0 0 1-.407.384 5.53 5.53 0 0 1-.24.195 7.12 7.12 0 0 1-.292.21c-.063.043-.126.084-.191.122s-.134.081-.204.119c-.14.078-.282.149-.428.215a5.53 5.53 0 0 1-.385.157 5.81 5.81 0 0 1-.43.138 5.91 5.91 0 0 1-.661.143c-.162.025-.325.044-.491.055-.173.012-.348.016-.525.014-.193-.003-.388-.015-.585-.037-.144-.015-.289-.037-.433-.062-.126-.022-.252-.049-.38-.079l-.2-.051-.555-.155-.275-.081-.41-.125-.334-.107-.317-.104-.215-.073-.26-.091-.186-.066-.367-.134-.212-.081-.284-.11-.299-.119-.193-.079-.24-.1-.185-.078-.192-.084-.166-.073-.152-.067-.153-.07-.159-.073-.2-.093-.208-.099-.222-.108-.189-.093c-3.335-1.668-6.295-3.89-8.822-6.583-.126-.134-.349-.045-.349.138l.005 9.52v.773c0 .448.222.87.595 1.118C10.946 29.092 13.762 30 16.791 30z" fill="#3370ff"/><path d="M29.746 22.382h0l.051-.093-.051.093zm.231-.435l.014-.025.007-.012-.021.037z" fill="#133c92"/><path d="M33.151 16.582c-1.129-.556-2.399-.869-3.744-.869a8.45 8.45 0 0 0-2.303.317l-.252.075-.177.058-.348.127-.606.265-.617.33-.598.386-.404.306-.419.359-.218.206-.374.37-.269.266-.293.289-.281.278-.299.296-.348.344-.256.254-.085.084-.125.122-.063.06-.095.09-.105.099c-.924.848-1.956 1.581-3.072 2.175l.2.093.159.073.153.07.152.067.166.073.192.084.185.078.24.1.193.079.299.119.284.11.212.081.367.134.186.066.26.09.215.073.317.104.334.107.41.125.275.081.555.155.2.051.379.079.433.062.585.037.525-.014.491-.055a5.61 5.61 0 0 0 .66-.143l.43-.138.385-.158.427-.215.204-.119.191-.122.292-.21.24-.195.407-.384.188-.206.225-.274.17-.23a6.13 6.13 0 0 0 .421-.693l.144-.288 1.305-2.599-.003.006a8.07 8.07 0 0 1 1.697-2.439z" fill="#133c9a"/></svg>