aboutsummaryrefslogtreecommitdiffstats
path: root/routers/web
diff options
context:
space:
mode:
Diffstat (limited to 'routers/web')
-rw-r--r--routers/web/admin/admin.go1
-rw-r--r--routers/web/admin/admin_test.go4
-rw-r--r--routers/web/admin/applications.go5
-rw-r--r--routers/web/admin/auths.go32
-rw-r--r--routers/web/admin/config.go4
-rw-r--r--routers/web/admin/diagnosis.go22
-rw-r--r--routers/web/admin/emails.go2
-rw-r--r--routers/web/admin/notice.go5
-rw-r--r--routers/web/admin/orgs.go2
-rw-r--r--routers/web/admin/packages.go5
-rw-r--r--routers/web/admin/perftrace.go18
-rw-r--r--routers/web/admin/stacktrace.go11
-rw-r--r--routers/web/admin/users.go24
-rw-r--r--routers/web/auth/2fa.go2
-rw-r--r--routers/web/auth/auth.go54
-rw-r--r--routers/web/auth/auth_test.go40
-rw-r--r--routers/web/auth/linkaccount.go16
-rw-r--r--routers/web/auth/oauth.go32
-rw-r--r--routers/web/auth/oauth2_provider.go42
-rw-r--r--routers/web/auth/openid.go15
-rw-r--r--routers/web/auth/password.go11
-rw-r--r--routers/web/auth/webauthn.go10
-rw-r--r--routers/web/base.go20
-rw-r--r--routers/web/devtest/devtest.go201
-rw-r--r--routers/web/devtest/mock_actions.go54
-rw-r--r--routers/web/explore/code.go18
-rw-r--r--routers/web/explore/org.go2
-rw-r--r--routers/web/explore/repo.go3
-rw-r--r--routers/web/explore/topic.go2
-rw-r--r--routers/web/explore/user.go6
-rw-r--r--routers/web/feed/branch.go6
-rw-r--r--routers/web/feed/convert.go14
-rw-r--r--routers/web/feed/file.go4
-rw-r--r--routers/web/feed/profile.go14
-rw-r--r--routers/web/feed/profile_test.go38
-rw-r--r--routers/web/feed/render.go2
-rw-r--r--routers/web/githttp.go16
-rw-r--r--routers/web/goget.go2
-rw-r--r--routers/web/home.go6
-rw-r--r--routers/web/misc/markup.go2
-rw-r--r--routers/web/misc/misc.go4
-rw-r--r--routers/web/nodeinfo.go3
-rw-r--r--routers/web/org/block.go10
-rw-r--r--routers/web/org/home.go18
-rw-r--r--routers/web/org/members.go20
-rw-r--r--routers/web/org/org.go5
-rw-r--r--routers/web/org/org_labels.go48
-rw-r--r--routers/web/org/projects.go95
-rw-r--r--routers/web/org/setting.go123
-rw-r--r--routers/web/org/setting_oauth2.go5
-rw-r--r--routers/web/org/setting_packages.go20
-rw-r--r--routers/web/org/teams.go99
-rw-r--r--routers/web/org/worktime.go82
-rw-r--r--routers/web/repo/actions/actions.go27
-rw-r--r--routers/web/repo/actions/badge.go25
-rw-r--r--routers/web/repo/actions/view.go503
-rw-r--r--routers/web/repo/activity.go19
-rw-r--r--routers/web/repo/attachment.go22
-rw-r--r--routers/web/repo/blame.go94
-rw-r--r--routers/web/repo/branch.go17
-rw-r--r--routers/web/repo/cherry_pick.go184
-rw-r--r--routers/web/repo/code_frequency.go4
-rw-r--r--routers/web/repo/commit.go106
-rw-r--r--routers/web/repo/compare.go111
-rw-r--r--routers/web/repo/contributors.go2
-rw-r--r--routers/web/repo/download.go16
-rw-r--r--routers/web/repo/editor.go1037
-rw-r--r--routers/web/repo/editor_apply_patch.go51
-rw-r--r--routers/web/repo/editor_cherry_pick.go86
-rw-r--r--routers/web/repo/editor_error.go82
-rw-r--r--routers/web/repo/editor_fork.go31
-rw-r--r--routers/web/repo/editor_preview.go41
-rw-r--r--routers/web/repo/editor_test.go79
-rw-r--r--routers/web/repo/editor_uploader.go61
-rw-r--r--routers/web/repo/editor_util.go110
-rw-r--r--routers/web/repo/fork.go63
-rw-r--r--routers/web/repo/githttp.go48
-rw-r--r--routers/web/repo/githttp_test.go2
-rw-r--r--routers/web/repo/helper.go2
-rw-r--r--routers/web/repo/issue.go57
-rw-r--r--routers/web/repo/issue_comment.go65
-rw-r--r--routers/web/repo/issue_content_history.go15
-rw-r--r--routers/web/repo/issue_dependency.go6
-rw-r--r--routers/web/repo/issue_label.go52
-rw-r--r--routers/web/repo/issue_label_test.go104
-rw-r--r--routers/web/repo/issue_list.go157
-rw-r--r--routers/web/repo/issue_lock.go5
-rw-r--r--routers/web/repo/issue_new.go34
-rw-r--r--routers/web/repo/issue_page_meta.go24
-rw-r--r--routers/web/repo/issue_pin.go47
-rw-r--r--routers/web/repo/issue_stopwatch.go80
-rw-r--r--routers/web/repo/issue_suggestions.go52
-rw-r--r--routers/web/repo/issue_timetrack.go12
-rw-r--r--routers/web/repo/issue_view.go186
-rw-r--r--routers/web/repo/issue_watch.go2
-rw-r--r--routers/web/repo/migrate.go8
-rw-r--r--routers/web/repo/milestone.go15
-rw-r--r--routers/web/repo/packages.go5
-rw-r--r--routers/web/repo/patch.go116
-rw-r--r--routers/web/repo/projects.go66
-rw-r--r--routers/web/repo/pull.go180
-rw-r--r--routers/web/repo/pull_review.go17
-rw-r--r--routers/web/repo/recent_commits.go15
-rw-r--r--routers/web/repo/release.go30
-rw-r--r--routers/web/repo/render.go2
-rw-r--r--routers/web/repo/repo.go168
-rw-r--r--routers/web/repo/search.go48
-rw-r--r--routers/web/repo/search_test.go19
-rw-r--r--routers/web/repo/setting/default_branch.go4
-rw-r--r--routers/web/repo/setting/git_hooks.go4
-rw-r--r--routers/web/repo/setting/lfs.go80
-rw-r--r--routers/web/repo/setting/protected_branch.go58
-rw-r--r--routers/web/repo/setting/protected_tag.go5
-rw-r--r--routers/web/repo/setting/public_access.go155
-rw-r--r--routers/web/repo/setting/runners.go187
-rw-r--r--routers/web/repo/setting/secrets.go5
-rw-r--r--routers/web/repo/setting/setting.go1503
-rw-r--r--routers/web/repo/setting/settings_test.go46
-rw-r--r--routers/web/repo/setting/variables.go140
-rw-r--r--routers/web/repo/setting/webhook.go53
-rw-r--r--routers/web/repo/star.go31
-rw-r--r--routers/web/repo/transfer.go38
-rw-r--r--routers/web/repo/treelist.go99
-rw-r--r--routers/web/repo/treelist_test.go68
-rw-r--r--routers/web/repo/view.go95
-rw-r--r--routers/web/repo/view_file.go425
-rw-r--r--routers/web/repo/view_home.go151
-rw-r--r--routers/web/repo/view_home_test.go32
-rw-r--r--routers/web/repo/view_readme.go74
-rw-r--r--routers/web/repo/watch.go31
-rw-r--r--routers/web/repo/webgit.go40
-rw-r--r--routers/web/repo/wiki.go223
-rw-r--r--routers/web/repo/wiki_test.go39
-rw-r--r--routers/web/shared/actions/runners.go236
-rw-r--r--routers/web/shared/actions/variables.go183
-rw-r--r--routers/web/shared/issue/issue_label.go21
-rw-r--r--routers/web/shared/label/label.go26
-rw-r--r--routers/web/shared/packages/packages.go14
-rw-r--r--routers/web/shared/project/column.go2
-rw-r--r--routers/web/shared/secrets/secrets.go8
-rw-r--r--routers/web/shared/user/header.go92
-rw-r--r--routers/web/shared/user/helper.go19
-rw-r--r--routers/web/user/avatar.go28
-rw-r--r--routers/web/user/code.go25
-rw-r--r--routers/web/user/home.go97
-rw-r--r--routers/web/user/home_test.go16
-rw-r--r--routers/web/user/notification.go52
-rw-r--r--routers/web/user/package.go132
-rw-r--r--routers/web/user/profile.go73
-rw-r--r--routers/web/user/search.go2
-rw-r--r--routers/web/user/setting/account.go11
-rw-r--r--routers/web/user/setting/account_test.go2
-rw-r--r--routers/web/user/setting/applications.go35
-rw-r--r--routers/web/user/setting/keys.go14
-rw-r--r--routers/web/user/setting/oauth2_common.go16
-rw-r--r--routers/web/user/setting/profile.go17
-rw-r--r--routers/web/user/setting/security/2fa.go19
-rw-r--r--routers/web/user/setting/security/openid.go6
-rw-r--r--routers/web/user/setting/security/security.go4
-rw-r--r--routers/web/user/setting/security/webauthn.go13
-rw-r--r--routers/web/user/setting/settings.go26
-rw-r--r--routers/web/user/stop_watch.go6
-rw-r--r--routers/web/web.go263
-rw-r--r--routers/web/webfinger.go21
164 files changed, 5669 insertions, 5105 deletions
diff --git a/routers/web/admin/admin.go b/routers/web/admin/admin.go
index 3902a1efb1..0cd13acf60 100644
--- a/routers/web/admin/admin.go
+++ b/routers/web/admin/admin.go
@@ -37,6 +37,7 @@ const (
tplSelfCheck templates.TplName = "admin/self_check"
tplCron templates.TplName = "admin/cron"
tplQueue templates.TplName = "admin/queue"
+ tplPerfTrace templates.TplName = "admin/perftrace"
tplStacktrace templates.TplName = "admin/stacktrace"
tplQueueManage templates.TplName = "admin/queue_manage"
tplStats templates.TplName = "admin/stats"
diff --git a/routers/web/admin/admin_test.go b/routers/web/admin/admin_test.go
index 6c38f0b509..a568c7c5c8 100644
--- a/routers/web/admin/admin_test.go
+++ b/routers/web/admin/admin_test.go
@@ -69,7 +69,7 @@ func TestShadowPassword(t *testing.T) {
}
for _, k := range kases {
- assert.EqualValues(t, k.Result, shadowPassword(k.Provider, k.CfgItem))
+ assert.Equal(t, k.Result, shadowPassword(k.Provider, k.CfgItem))
}
}
@@ -79,7 +79,7 @@ func TestSelfCheckPost(t *testing.T) {
ctx, resp := contexttest.MockContext(t, "GET http://host/sub/admin/self_check?location_origin=http://frontend")
SelfCheckPost(ctx)
- assert.EqualValues(t, http.StatusOK, resp.Code)
+ assert.Equal(t, http.StatusOK, resp.Code)
data := struct {
Problems []string `json:"problems"`
diff --git a/routers/web/admin/applications.go b/routers/web/admin/applications.go
index aec6349f21..79c3a08808 100644
--- a/routers/web/admin/applications.go
+++ b/routers/web/admin/applications.go
@@ -4,7 +4,6 @@
package admin
import (
- "fmt"
"net/http"
"code.gitea.io/gitea/models/auth"
@@ -23,8 +22,8 @@ var (
func newOAuth2CommonHandlers() *user_setting.OAuth2CommonHandlers {
return &user_setting.OAuth2CommonHandlers{
OwnerID: 0,
- BasePathList: fmt.Sprintf("%s/-/admin/applications", setting.AppSubURL),
- BasePathEditPrefix: fmt.Sprintf("%s/-/admin/applications/oauth2", setting.AppSubURL),
+ BasePathList: setting.AppSubURL + "/-/admin/applications",
+ BasePathEditPrefix: setting.AppSubURL + "/-/admin/applications/oauth2",
TplAppEdit: tplSettingsOauth2ApplicationEdit,
}
}
diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go
index 249347e835..0f6f31b884 100644
--- a/routers/web/admin/auths.go
+++ b/routers/web/admin/auths.go
@@ -28,8 +28,6 @@ import (
"code.gitea.io/gitea/services/auth/source/sspi"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
-
- "xorm.io/xorm/convert"
)
const (
@@ -149,7 +147,6 @@ func parseLDAPConfig(form forms.AuthenticationForm) *ldap.Source {
RestrictedFilter: form.RestrictedFilter,
AllowDeactivateAll: form.AllowDeactivateAll,
Enabled: true,
- SkipLocalTwoFA: form.SkipLocalTwoFA,
}
}
@@ -163,7 +160,6 @@ func parseSMTPConfig(form forms.AuthenticationForm) *smtp.Source {
SkipVerify: form.SkipVerify,
HeloHostname: form.HeloHostname,
DisableHelo: form.DisableHelo,
- SkipLocalTwoFA: form.SkipLocalTwoFA,
}
}
@@ -181,7 +177,7 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
customURLMapping = nil
}
var scopes []string
- for _, s := range strings.Split(form.Oauth2Scopes, ",") {
+ for s := range strings.SplitSeq(form.Oauth2Scopes, ",") {
s = strings.TrimSpace(s)
if s != "" {
scopes = append(scopes, s)
@@ -198,7 +194,6 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
Scopes: scopes,
RequiredClaimName: form.Oauth2RequiredClaimName,
RequiredClaimValue: form.Oauth2RequiredClaimValue,
- SkipLocalTwoFA: form.SkipLocalTwoFA,
GroupClaimName: form.Oauth2GroupClaimName,
RestrictedGroup: form.Oauth2RestrictedGroup,
AdminGroup: form.Oauth2AdminGroup,
@@ -252,7 +247,7 @@ func NewAuthSourcePost(ctx *context.Context) {
ctx.Data["SSPIDefaultLanguage"] = ""
hasTLS := false
- var config convert.Conversion
+ var config auth.Config
switch auth.Type(form.Type) {
case auth.LDAP, auth.DLDAP:
config = parseLDAPConfig(form)
@@ -262,9 +257,8 @@ func NewAuthSourcePost(ctx *context.Context) {
hasTLS = true
case auth.PAM:
config = &pam_service.Source{
- ServiceName: form.PAMServiceName,
- EmailDomain: form.PAMEmailDomain,
- SkipLocalTwoFA: form.SkipLocalTwoFA,
+ ServiceName: form.PAMServiceName,
+ EmailDomain: form.PAMEmailDomain,
}
case auth.OAuth2:
config = parseOAuth2Config(form)
@@ -291,7 +285,7 @@ func NewAuthSourcePost(ctx *context.Context) {
return
}
default:
- ctx.Error(http.StatusBadRequest)
+ ctx.HTTPError(http.StatusBadRequest)
return
}
ctx.Data["HasTLS"] = hasTLS
@@ -302,11 +296,12 @@ func NewAuthSourcePost(ctx *context.Context) {
}
if err := auth.CreateSource(ctx, &auth.Source{
- Type: auth.Type(form.Type),
- Name: form.Name,
- IsActive: form.IsActive,
- IsSyncEnabled: form.IsSyncEnabled,
- Cfg: config,
+ Type: auth.Type(form.Type),
+ Name: form.Name,
+ IsActive: form.IsActive,
+ IsSyncEnabled: form.IsSyncEnabled,
+ TwoFactorPolicy: form.TwoFactorPolicy,
+ Cfg: config,
}); err != nil {
if auth.IsErrSourceAlreadyExist(err) {
ctx.Data["Err_Name"] = true
@@ -384,7 +379,7 @@ func EditAuthSourcePost(ctx *context.Context) {
return
}
- var config convert.Conversion
+ var config auth.Config
switch auth.Type(form.Type) {
case auth.LDAP, auth.DLDAP:
config = parseLDAPConfig(form)
@@ -413,7 +408,7 @@ func EditAuthSourcePost(ctx *context.Context) {
return
}
default:
- ctx.Error(http.StatusBadRequest)
+ ctx.HTTPError(http.StatusBadRequest)
return
}
@@ -421,6 +416,7 @@ func EditAuthSourcePost(ctx *context.Context) {
source.IsActive = form.IsActive
source.IsSyncEnabled = form.IsSyncEnabled
source.Cfg = config
+ source.TwoFactorPolicy = form.TwoFactorPolicy
if err := auth.UpdateSource(ctx, source); err != nil {
if auth.IsErrSourceAlreadyExist(err) {
ctx.Data["Err_Name"] = true
diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go
index 520f14e89f..0e5b23db6d 100644
--- a/routers/web/admin/config.go
+++ b/routers/web/admin/config.go
@@ -61,7 +61,7 @@ func TestCache(ctx *context.Context) {
func shadowPasswordKV(cfgItem, splitter string) string {
fields := strings.Split(cfgItem, splitter)
- for i := 0; i < len(fields); i++ {
+ for i := range fields {
if strings.HasPrefix(fields[i], "password=") {
fields[i] = "password=******"
break
@@ -200,7 +200,7 @@ func ChangeConfig(ctx *context.Context) {
value := ctx.FormString("value")
cfg := setting.Config()
- marshalBool := func(v string) (string, error) { //nolint:unparam
+ marshalBool := func(v string) (string, error) { //nolint:unparam // error is always nil
if b, _ := strconv.ParseBool(v); b {
return "true", nil
}
diff --git a/routers/web/admin/diagnosis.go b/routers/web/admin/diagnosis.go
index 020554a35a..5395529d66 100644
--- a/routers/web/admin/diagnosis.go
+++ b/routers/web/admin/diagnosis.go
@@ -10,17 +10,13 @@ import (
"time"
"code.gitea.io/gitea/modules/httplib"
+ "code.gitea.io/gitea/modules/tailmsg"
+ "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
)
func MonitorDiagnosis(ctx *context.Context) {
- seconds := ctx.FormInt64("seconds")
- if seconds <= 5 {
- seconds = 5
- }
- if seconds > 300 {
- seconds = 300
- }
+ seconds := min(max(ctx.FormInt64("seconds"), 1), 300)
httplib.ServeSetHeaders(ctx.Resp, &httplib.ServeHeaderOptions{
ContentType: "application/zip",
@@ -65,4 +61,16 @@ func MonitorDiagnosis(ctx *context.Context) {
return
}
_ = pprof.Lookup("heap").WriteTo(f, 0)
+
+ f, err = zipWriter.CreateHeader(&zip.FileHeader{Name: "perftrace.txt", Method: zip.Deflate, Modified: time.Now()})
+ if err != nil {
+ ctx.ServerError("Failed to create zip file", err)
+ return
+ }
+ for _, record := range tailmsg.GetManager().GetTraceRecorder().GetRecords() {
+ _, _ = f.Write(util.UnsafeStringToBytes(record.Time.Format(time.RFC3339)))
+ _, _ = f.Write([]byte(" "))
+ _, _ = f.Write(util.UnsafeStringToBytes((record.Content)))
+ _, _ = f.Write([]byte("\n\n"))
+ }
}
diff --git a/routers/web/admin/emails.go b/routers/web/admin/emails.go
index 23ddfa583a..51b3d584f4 100644
--- a/routers/web/admin/emails.go
+++ b/routers/web/admin/emails.go
@@ -116,7 +116,7 @@ func ActivateEmail(ctx *context.Context) {
activate, oka := truefalse[ctx.FormString("activate")]
if uid == 0 || len(email) == 0 || !okp || !oka {
- ctx.Error(http.StatusBadRequest)
+ ctx.HTTPError(http.StatusBadRequest)
return
}
diff --git a/routers/web/admin/notice.go b/routers/web/admin/notice.go
index 21a8ab0d17..e9d6abbe92 100644
--- a/routers/web/admin/notice.go
+++ b/routers/web/admin/notice.go
@@ -26,10 +26,7 @@ func Notices(ctx *context.Context) {
ctx.Data["PageIsAdminNotices"] = true
total := system_model.CountNotices(ctx)
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
notices, err := system_model.Notices(ctx, page, setting.UI.Admin.NoticePagingNum)
if err != nil {
diff --git a/routers/web/admin/orgs.go b/routers/web/admin/orgs.go
index 35e61efa17..e34f203aaf 100644
--- a/routers/web/admin/orgs.go
+++ b/routers/web/admin/orgs.go
@@ -27,7 +27,7 @@ func Organizations(ctx *context.Context) {
ctx.SetFormString("sort", UserSearchDefaultAdminSort)
}
- explore.RenderUserSearch(ctx, &user_model.SearchUserOptions{
+ explore.RenderUserSearch(ctx, user_model.SearchUserOptions{
Actor: ctx.Doer,
Type: user_model.UserTypeOrganization,
IncludeReserved: true, // administrator needs to list all accounts include reserved
diff --git a/routers/web/admin/packages.go b/routers/web/admin/packages.go
index 5122342259..1904bfee11 100644
--- a/routers/web/admin/packages.go
+++ b/routers/web/admin/packages.go
@@ -24,10 +24,7 @@ const (
// Packages shows all packages
func Packages(ctx *context.Context) {
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
query := ctx.FormTrim("q")
packageType := ctx.FormTrim("type")
sort := ctx.FormTrim("sort")
diff --git a/routers/web/admin/perftrace.go b/routers/web/admin/perftrace.go
new file mode 100644
index 0000000000..51ee57da10
--- /dev/null
+++ b/routers/web/admin/perftrace.go
@@ -0,0 +1,18 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package admin
+
+import (
+ "net/http"
+
+ "code.gitea.io/gitea/modules/tailmsg"
+ "code.gitea.io/gitea/services/context"
+)
+
+func PerfTrace(ctx *context.Context) {
+ monitorTraceCommon(ctx)
+ ctx.Data["PageIsAdminMonitorPerfTrace"] = true
+ ctx.Data["PerfTraceRecords"] = tailmsg.GetManager().GetTraceRecorder().GetRecords()
+ ctx.HTML(http.StatusOK, tplPerfTrace)
+}
diff --git a/routers/web/admin/stacktrace.go b/routers/web/admin/stacktrace.go
index ff751be621..2b8c2fb4af 100644
--- a/routers/web/admin/stacktrace.go
+++ b/routers/web/admin/stacktrace.go
@@ -12,10 +12,17 @@ import (
"code.gitea.io/gitea/services/context"
)
+func monitorTraceCommon(ctx *context.Context) {
+ ctx.Data["Title"] = ctx.Tr("admin.monitor")
+ ctx.Data["PageIsAdminMonitorTrace"] = true
+ // Hide the performance trace tab in production, because it shows a lot of SQLs and is not that useful for end users.
+ // To avoid confusing end users, do not let them know this tab. End users should "download diagnosis report" instead.
+ ctx.Data["ShowAdminPerformanceTraceTab"] = !setting.IsProd
+}
+
// Stacktrace show admin monitor goroutines page
func Stacktrace(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("admin.monitor")
- ctx.Data["PageIsAdminMonitorStacktrace"] = true
+ monitorTraceCommon(ctx)
ctx.Data["GoroutineCount"] = runtime.NumGoroutine()
diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go
index f6a3af1c86..27577cd35b 100644
--- a/routers/web/admin/users.go
+++ b/routers/web/admin/users.go
@@ -21,8 +21,8 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/templates"
- "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/web/explore"
user_setting "code.gitea.io/gitea/routers/web/user/setting"
@@ -65,18 +65,18 @@ func Users(ctx *context.Context) {
"SortType": sortType,
}
- explore.RenderUserSearch(ctx, &user_model.SearchUserOptions{
+ explore.RenderUserSearch(ctx, user_model.SearchUserOptions{
Actor: ctx.Doer,
Type: user_model.UserTypeIndividual,
ListOptions: db.ListOptions{
PageSize: setting.UI.Admin.UserPagingNum,
},
SearchByEmail: true,
- IsActive: util.OptionalBoolParse(statusFilterMap["is_active"]),
- IsAdmin: util.OptionalBoolParse(statusFilterMap["is_admin"]),
- IsRestricted: util.OptionalBoolParse(statusFilterMap["is_restricted"]),
- IsTwoFactorEnabled: util.OptionalBoolParse(statusFilterMap["is_2fa_enabled"]),
- IsProhibitLogin: util.OptionalBoolParse(statusFilterMap["is_prohibit_login"]),
+ IsActive: optional.ParseBool(statusFilterMap["is_active"]),
+ IsAdmin: optional.ParseBool(statusFilterMap["is_admin"]),
+ IsRestricted: optional.ParseBool(statusFilterMap["is_restricted"]),
+ IsTwoFactorEnabled: optional.ParseBool(statusFilterMap["is_2fa_enabled"]),
+ IsProhibitLogin: optional.ParseBool(statusFilterMap["is_prohibit_login"]),
IncludeReserved: true, // administrator needs to list all accounts include reserved, bot, remote ones
}, tplUsers)
}
@@ -269,7 +269,7 @@ func ViewUser(ctx *context.Context) {
return
}
- repos, count, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
+ repos, count, err := repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{
ListOptions: db.ListOptionsAll,
OwnerID: u.ID,
OrderBy: db.SearchOrderByAlphabetically,
@@ -293,9 +293,9 @@ func ViewUser(ctx *context.Context) {
ctx.Data["EmailsTotal"] = len(emails)
orgs, err := db.Find[org_model.Organization](ctx, org_model.FindOrgOptions{
- ListOptions: db.ListOptionsAll,
- UserID: u.ID,
- IncludePrivate: true,
+ ListOptions: db.ListOptionsAll,
+ UserID: u.ID,
+ IncludeVisibility: structs.VisibleTypePrivate,
})
if err != nil {
ctx.ServerError("FindOrgs", err)
@@ -432,7 +432,7 @@ func EditUserPost(ctx *context.Context) {
Website: optional.Some(form.Website),
Location: optional.Some(form.Location),
IsActive: optional.Some(form.Active),
- IsAdmin: optional.Some(form.Admin),
+ IsAdmin: user_service.UpdateOptionFieldFromValue(form.Admin),
AllowGitHook: optional.Some(form.AllowGitHook),
AllowImportLocal: optional.Some(form.AllowImportLocal),
MaxRepoCreation: optional.Some(form.MaxRepoCreation),
diff --git a/routers/web/auth/2fa.go b/routers/web/auth/2fa.go
index fe363fe90a..d15d33dfd4 100644
--- a/routers/web/auth/2fa.go
+++ b/routers/web/auth/2fa.go
@@ -9,6 +9,7 @@ import (
"code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web"
@@ -87,6 +88,7 @@ func TwoFactorPost(ctx *context.Context) {
return
}
+ _ = ctx.Session.Set(session.KeyUserHasTwoFactorAuth, true)
handleSignIn(ctx, u, remember)
return
}
diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go
index 3fe1d5970e..94f75f69ff 100644
--- a/routers/web/auth/auth.go
+++ b/routers/web/auth/auth.go
@@ -76,6 +76,10 @@ func autoSignIn(ctx *context.Context) (bool, error) {
}
return false, nil
}
+ userHasTwoFactorAuth, err := auth.HasTwoFactorOrWebAuthn(ctx, u.ID)
+ if err != nil {
+ return false, fmt.Errorf("HasTwoFactorOrWebAuthn: %w", err)
+ }
isSucceed = true
@@ -87,9 +91,9 @@ func autoSignIn(ctx *context.Context) (bool, error) {
ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day)
if err := updateSession(ctx, nil, map[string]any{
- // Set session IDs
- "uid": u.ID,
- "uname": u.Name,
+ session.KeyUID: u.ID,
+ session.KeyUname: u.Name,
+ session.KeyUserHasTwoFactorAuth: userHasTwoFactorAuth,
}); err != nil {
return false, fmt.Errorf("unable to updateSession: %w", err)
}
@@ -169,6 +173,7 @@ func prepareSignInPageData(ctx *context.Context) {
ctx.Data["PageIsLogin"] = true
ctx.Data["EnableSSPI"] = auth.IsSSPIEnabled(ctx)
ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
+ ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth
if setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin {
context.SetCaptchaData(ctx)
@@ -191,7 +196,7 @@ func SignIn(ctx *context.Context) {
// SignInPost response for sign in request
func SignInPost(ctx *context.Context) {
if !setting.Service.EnablePasswordSignInForm {
- ctx.Error(http.StatusForbidden)
+ ctx.HTTPError(http.StatusForbidden)
return
}
@@ -238,9 +243,8 @@ func SignInPost(ctx *context.Context) {
}
// Now handle 2FA:
-
// First of all if the source can skip local two fa we're done
- if skipper, ok := source.Cfg.(auth_service.LocalTwoFASkipper); ok && skipper.IsSkipLocalTwoFA() {
+ if source.TwoFactorShouldSkip() {
handleSignIn(ctx, u, form.Remember)
return
}
@@ -261,7 +265,7 @@ func SignInPost(ctx *context.Context) {
}
if !hasTOTPtwofa && !hasWebAuthnTwofa {
- // No two factor auth configured we can sign in the user
+ // No two-factor auth configured we can sign in the user
handleSignIn(ctx, u, form.Remember)
return
}
@@ -310,8 +314,14 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day)
}
+ userHasTwoFactorAuth, err := auth.HasTwoFactorOrWebAuthn(ctx, u.ID)
+ if err != nil {
+ ctx.ServerError("HasTwoFactorOrWebAuthn", err)
+ return setting.AppSubURL + "/"
+ }
+
if err := updateSession(ctx, []string{
- // Delete the openid, 2fa and linkaccount data
+ // Delete the openid, 2fa and link_account data
"openid_verified_uri",
"openid_signin_remember",
"openid_determined_email",
@@ -320,8 +330,9 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
"twofaRemember",
"linkAccount",
}, map[string]any{
- "uid": u.ID,
- "uname": u.Name,
+ session.KeyUID: u.ID,
+ session.KeyUname: u.Name,
+ session.KeyUserHasTwoFactorAuth: userHasTwoFactorAuth,
}); err != nil {
ctx.ServerError("RegenerateSession", err)
return setting.AppSubURL + "/"
@@ -410,9 +421,11 @@ func SignOut(ctx *context.Context) {
// SignUp render the register page
func SignUp(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("sign_up")
-
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
+ hasUsers, _ := user_model.HasUsers(ctx)
+ ctx.Data["IsFirstTimeRegistration"] = !hasUsers.HasAnyUser
+
oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
if err != nil {
ctx.ServerError("UserSignUp", err)
@@ -455,7 +468,7 @@ func SignUpPost(ctx *context.Context) {
// Permission denied if DisableRegistration or AllowOnlyExternalRegistration options are true
if setting.Service.DisableRegistration || setting.Service.AllowOnlyExternalRegistration {
- ctx.Error(http.StatusForbidden)
+ ctx.HTTPError(http.StatusForbidden)
return
}
@@ -533,7 +546,8 @@ func createUserInContext(ctx *context.Context, tpl templates.TplName, form any,
}
if err := user_model.CreateUser(ctx, u, meta, overwrites); err != nil {
if allowLink && (user_model.IsErrUserAlreadyExist(err) || user_model.IsErrEmailAlreadyUsed(err)) {
- if setting.OAuth2Client.AccountLinking == setting.OAuth2AccountLinkingAuto {
+ switch setting.OAuth2Client.AccountLinking {
+ case setting.OAuth2AccountLinkingAuto:
var user *user_model.User
user = &user_model.User{Name: u.Name}
hasUser, err := user_model.GetUser(ctx, user)
@@ -549,7 +563,7 @@ func createUserInContext(ctx *context.Context, tpl templates.TplName, form any,
// TODO: probably we should respect 'remember' user's choice...
linkAccount(ctx, user, *gothUser, true)
return false // user is already created here, all redirects are handled
- } else if setting.OAuth2Client.AccountLinking == setting.OAuth2AccountLinkingLogin {
+ case setting.OAuth2AccountLinkingLogin:
showLinkingLogin(ctx, *gothUser)
return false // user will be created only after linking login
}
@@ -598,10 +612,16 @@ func createUserInContext(ctx *context.Context, tpl templates.TplName, form any,
// sends a confirmation email if required.
func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.User) (ok bool) {
// Auto-set admin for the only user.
- if user_model.CountUsers(ctx, nil) == 1 {
+ hasUsers, err := user_model.HasUsers(ctx)
+ if err != nil {
+ ctx.ServerError("HasUsers", err)
+ return false
+ }
+ if hasUsers.HasOnlyOneUser {
+ // the only user is the one just created, will set it as admin
opts := &user_service.UpdateOptions{
IsActive: optional.Some(true),
- IsAdmin: optional.Some(true),
+ IsAdmin: user_service.UpdateOptionFieldFromValue(true),
SetLastLogin: true,
}
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
@@ -766,7 +786,7 @@ func handleAccountActivation(ctx *context.Context, user *user_model.User) {
}
if err := user_model.UpdateUserCols(ctx, user, "is_active", "rands"); err != nil {
if user_model.IsErrUserNotExist(err) {
- ctx.NotFound("UpdateUserCols", err)
+ ctx.NotFound(err)
} else {
ctx.ServerError("UpdateUser", err)
}
diff --git a/routers/web/auth/auth_test.go b/routers/web/auth/auth_test.go
index cbcb2a5222..e238125407 100644
--- a/routers/web/auth/auth_test.go
+++ b/routers/web/auth/auth_test.go
@@ -61,23 +61,35 @@ func TestUserLogin(t *testing.T) {
assert.Equal(t, "/", test.RedirectURL(resp))
}
-func TestSignUpOAuth2ButMissingFields(t *testing.T) {
+func TestSignUpOAuth2Login(t *testing.T) {
defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)()
- defer test.MockVariableValue(&gothic.CompleteUserAuth, func(res http.ResponseWriter, req *http.Request) (goth.User, error) {
- return goth.User{Provider: "dummy-auth-source", UserID: "dummy-user"}, nil
- })()
addOAuth2Source(t, "dummy-auth-source", oauth2.Source{})
- mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockStore("dummy-sid")}
- ctx, resp := contexttest.MockContext(t, "/user/oauth2/dummy-auth-source/callback?code=dummy-code", mockOpt)
- ctx.SetPathParam("provider", "dummy-auth-source")
- SignInOAuthCallback(ctx)
- assert.Equal(t, http.StatusSeeOther, resp.Code)
- assert.Equal(t, "/user/link_account", test.RedirectURL(resp))
+ t.Run("OAuth2MissingField", func(t *testing.T) {
+ defer test.MockVariableValue(&gothic.CompleteUserAuth, func(res http.ResponseWriter, req *http.Request) (goth.User, error) {
+ return goth.User{Provider: "dummy-auth-source", UserID: "dummy-user"}, nil
+ })()
+ mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockStore("dummy-sid")}
+ ctx, resp := contexttest.MockContext(t, "/user/oauth2/dummy-auth-source/callback?code=dummy-code", mockOpt)
+ ctx.SetPathParam("provider", "dummy-auth-source")
+ SignInOAuthCallback(ctx)
+ assert.Equal(t, http.StatusSeeOther, resp.Code)
+ assert.Equal(t, "/user/link_account", test.RedirectURL(resp))
+
+ // then the user will be redirected to the link account page, and see a message about the missing fields
+ ctx, _ = contexttest.MockContext(t, "/user/link_account", mockOpt)
+ LinkAccount(ctx)
+ assert.EqualValues(t, "auth.oauth_callback_unable_auto_reg:dummy-auth-source,email", ctx.Data["AutoRegistrationFailedPrompt"])
+ })
- // then the user will be redirected to the link account page, and see a message about the missing fields
- ctx, _ = contexttest.MockContext(t, "/user/link_account", mockOpt)
- LinkAccount(ctx)
- assert.EqualValues(t, "auth.oauth_callback_unable_auto_reg:dummy-auth-source,email", ctx.Data["AutoRegistrationFailedPrompt"])
+ t.Run("OAuth2CallbackError", func(t *testing.T) {
+ mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockStore("dummy-sid")}
+ ctx, resp := contexttest.MockContext(t, "/user/oauth2/dummy-auth-source/callback", mockOpt)
+ ctx.SetPathParam("provider", "dummy-auth-source")
+ SignInOAuthCallback(ctx)
+ assert.Equal(t, http.StatusSeeOther, resp.Code)
+ assert.Equal(t, "/user/login", test.RedirectURL(resp))
+ assert.Contains(t, ctx.Flash.ErrorMsg, "auth.oauth.signin.error.general")
+ })
}
diff --git a/routers/web/auth/linkaccount.go b/routers/web/auth/linkaccount.go
index 147d8d3802..b3c61946b9 100644
--- a/routers/web/auth/linkaccount.go
+++ b/routers/web/auth/linkaccount.go
@@ -29,6 +29,7 @@ var tplLinkAccount templates.TplName = "user/auth/link_account"
// LinkAccount shows the page where the user can decide to login or create a new account
func LinkAccount(ctx *context.Context) {
+ // FIXME: these common template variables should be prepared in one common function, but not just copy-paste again and again.
ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration
ctx.Data["Title"] = ctx.Tr("link_account")
ctx.Data["LinkAccountMode"] = true
@@ -43,13 +44,20 @@ func LinkAccount(ctx *context.Context) {
ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey
ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
+ ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
ctx.Data["ShowRegistrationButton"] = false
+ ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth
// use this to set the right link into the signIn and signUp templates in the link_account template
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
gothUser, ok := ctx.Session.Get("linkAccountGothUser").(goth.User)
+
+ // If you'd like to quickly debug the "link account" page layout, just uncomment the blow line
+ // Don't worry, when the below line exists, the lint won't pass: ineffectual assignment to gothUser (ineffassign)
+ // gothUser, ok = goth.User{Email: "invalid-email", Name: "."}, true // intentionally use invalid data to avoid pass the registration check
+
if !ok {
// no account in session, so just redirect to the login page, then the user could restart the process
ctx.Redirect(setting.AppSubURL + "/user/login")
@@ -135,7 +143,10 @@ func LinkAccountPostSignIn(ctx *context.Context) {
ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL
ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey
ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
+ ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
+ ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
ctx.Data["ShowRegistrationButton"] = false
+ ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth
// use this to set the right link into the signIn and signUp templates in the link_account template
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
@@ -223,7 +234,10 @@ func LinkAccountPostRegister(ctx *context.Context) {
ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL
ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey
ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
+ ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
+ ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
ctx.Data["ShowRegistrationButton"] = false
+ ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth
// use this to set the right link into the signIn and signUp templates in the link_account template
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
@@ -246,7 +260,7 @@ func LinkAccountPostRegister(ctx *context.Context) {
}
if setting.Service.DisableRegistration || setting.Service.AllowOnlyInternalRegistration {
- ctx.Error(http.StatusForbidden)
+ ctx.HTTPError(http.StatusForbidden)
return
}
diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go
index 7a9721cf56..a13b987aab 100644
--- a/routers/web/auth/oauth.go
+++ b/routers/web/auth/oauth.go
@@ -18,6 +18,7 @@ import (
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
+ "code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web/middleware"
@@ -115,7 +116,7 @@ func SignInOAuthCallback(ctx *context.Context) {
case "temporarily_unavailable":
ctx.Flash.Error(ctx.Tr("auth.oauth.signin.error.temporarily_unavailable"))
default:
- ctx.Flash.Error(ctx.Tr("auth.oauth.signin.error"))
+ ctx.Flash.Error(ctx.Tr("auth.oauth.signin.error.general", callbackErr.Description))
}
ctx.Redirect(setting.AppSubURL + "/user/login")
return
@@ -155,9 +156,10 @@ func SignInOAuthCallback(ctx *context.Context) {
return
}
if uname == "" {
- if setting.OAuth2Client.Username == setting.OAuth2UsernameNickname {
+ switch setting.OAuth2Client.Username {
+ case setting.OAuth2UsernameNickname:
missingFields = append(missingFields, "nickname")
- } else if setting.OAuth2Client.Username == setting.OAuth2UsernamePreferredUsername {
+ case setting.OAuth2UsernamePreferredUsername:
missingFields = append(missingFields, "preferred_username")
} // else: "UserID" and "Email" have been handled above separately
}
@@ -191,8 +193,8 @@ func SignInOAuthCallback(ctx *context.Context) {
source := authSource.Cfg.(*oauth2.Source)
isAdmin, isRestricted := getUserAdminAndRestrictedFromGroupClaims(source, &gothUser)
- u.IsAdmin = isAdmin.ValueOrDefault(false)
- u.IsRestricted = isRestricted.ValueOrDefault(false)
+ u.IsAdmin = isAdmin.ValueOrDefault(user_service.UpdateOptionField[bool]{FieldValue: false}).FieldValue
+ u.IsRestricted = isRestricted.ValueOrDefault(setting.Service.DefaultUserIsRestricted)
if !createAndHandleCreatedUser(ctx, templates.TplName(""), nil, u, overwriteDefault, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled) {
// error already handled
@@ -256,11 +258,11 @@ func getClaimedGroups(source *oauth2.Source, gothUser *goth.User) container.Set[
return claimValueToStringSet(groupClaims)
}
-func getUserAdminAndRestrictedFromGroupClaims(source *oauth2.Source, gothUser *goth.User) (isAdmin, isRestricted optional.Option[bool]) {
+func getUserAdminAndRestrictedFromGroupClaims(source *oauth2.Source, gothUser *goth.User) (isAdmin optional.Option[user_service.UpdateOptionField[bool]], isRestricted optional.Option[bool]) {
groups := getClaimedGroups(source, gothUser)
if source.AdminGroup != "" {
- isAdmin = optional.Some(groups.Contains(source.AdminGroup))
+ isAdmin = user_service.UpdateOptionFieldFromSync(groups.Contains(source.AdminGroup))
}
if source.RestrictedGroup != "" {
isRestricted = optional.Some(groups.Contains(source.RestrictedGroup))
@@ -301,7 +303,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
updateAvatarIfNeed(ctx, gothUser.AvatarURL, u)
needs2FA := false
- if !source.Cfg.(*oauth2.Source).SkipLocalTwoFA {
+ if !source.TwoFactorShouldSkip() {
_, err := auth.GetTwoFactorByUID(ctx, u.ID)
if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
ctx.ServerError("UserSignIn", err)
@@ -351,10 +353,16 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
ctx.ServerError("UpdateUser", err)
return
}
+ userHasTwoFactorAuth, err := auth.HasTwoFactorOrWebAuthn(ctx, u.ID)
+ if err != nil {
+ ctx.ServerError("UpdateUser", err)
+ return
+ }
if err := updateSession(ctx, nil, map[string]any{
- "uid": u.ID,
- "uname": u.Name,
+ session.KeyUID: u.ID,
+ session.KeyUname: u.Name,
+ session.KeyUserHasTwoFactorAuth: userHasTwoFactorAuth,
}); err != nil {
ctx.ServerError("updateSession", err)
return
@@ -431,8 +439,10 @@ func oAuth2UserLoginCallback(ctx *context.Context, authSource *auth.Source, requ
gothUser, err := oauth2Source.Callback(request, response)
if err != nil {
if err.Error() == "securecookie: the value is too long" || strings.Contains(err.Error(), "Data too long") {
- log.Error("OAuth2 Provider %s returned too long a token. Current max: %d. Either increase the [OAuth2] MAX_TOKEN_LENGTH or reduce the information returned from the OAuth2 provider", authSource.Name, setting.OAuth2.MaxTokenLength)
err = fmt.Errorf("OAuth2 Provider %s returned too long a token. Current max: %d. Either increase the [OAuth2] MAX_TOKEN_LENGTH or reduce the information returned from the OAuth2 provider", authSource.Name, setting.OAuth2.MaxTokenLength)
+ log.Error("oauth2Source.Callback failed: %v", err)
+ } else {
+ err = errCallback{Code: "internal", Description: err.Error()}
}
return nil, goth.User{}, err
}
diff --git a/routers/web/auth/oauth2_provider.go b/routers/web/auth/oauth2_provider.go
index 6262ad8a6d..dc9f34fd44 100644
--- a/routers/web/auth/oauth2_provider.go
+++ b/routers/web/auth/oauth2_provider.go
@@ -4,17 +4,16 @@
package auth
import (
- "errors"
"fmt"
"html"
"html/template"
"net/http"
"net/url"
- "strings"
+ "strconv"
"code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/auth/httpauth"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
@@ -98,7 +97,7 @@ func InfoOAuth(ctx *context.Context) {
}
response := &userInfoResponse{
- Sub: fmt.Sprint(ctx.Doer.ID),
+ Sub: strconv.FormatInt(ctx.Doer.ID, 10),
Name: ctx.Doer.DisplayName(),
PreferredUsername: ctx.Doer.Name,
Email: ctx.Doer.Email,
@@ -107,9 +106,8 @@ func InfoOAuth(ctx *context.Context) {
var accessTokenScope auth.AccessTokenScope
if auHead := ctx.Req.Header.Get("Authorization"); auHead != "" {
- auths := strings.Fields(auHead)
- if len(auths) == 2 && (auths[0] == "token" || strings.ToLower(auths[0]) == "bearer") {
- accessTokenScope, _ = auth_service.GetOAuthAccessTokenScopeAndUserID(ctx, auths[1])
+ if parsed, ok := httpauth.ParseAuthorizationHeader(auHead); ok && parsed.BearerToken != nil {
+ accessTokenScope, _ = auth_service.GetOAuthAccessTokenScopeAndUserID(ctx, parsed.BearerToken.Token)
}
}
@@ -126,23 +124,17 @@ func InfoOAuth(ctx *context.Context) {
ctx.JSON(http.StatusOK, response)
}
-func parseBasicAuth(ctx *context.Context) (username, password string, err error) {
- authHeader := ctx.Req.Header.Get("Authorization")
- if authType, authData, ok := strings.Cut(authHeader, " "); ok && strings.EqualFold(authType, "Basic") {
- return base.BasicAuthDecode(authData)
- }
- return "", "", errors.New("invalid basic authentication")
-}
-
// IntrospectOAuth introspects an oauth token
func IntrospectOAuth(ctx *context.Context) {
clientIDValid := false
- if clientID, clientSecret, err := parseBasicAuth(ctx); err == nil {
+ authHeader := ctx.Req.Header.Get("Authorization")
+ if parsed, ok := httpauth.ParseAuthorizationHeader(authHeader); ok && parsed.BasicAuth != nil {
+ clientID, clientSecret := parsed.BasicAuth.Username, parsed.BasicAuth.Password
app, err := auth.GetOAuth2ApplicationByClientID(ctx, clientID)
if err != nil && !auth.IsErrOauthClientIDInvalid(err) {
// this is likely a database error; log it and respond without details
log.Error("Error retrieving client_id: %v", err)
- ctx.Error(http.StatusInternalServerError)
+ ctx.HTTPError(http.StatusInternalServerError)
return
}
clientIDValid = err == nil && app.ValidateClientSecret([]byte(clientSecret))
@@ -171,7 +163,7 @@ func IntrospectOAuth(ctx *context.Context) {
response.Scope = grant.Scope
response.Issuer = setting.AppURL
response.Audience = []string{app.ClientID}
- response.Subject = fmt.Sprint(grant.UserID)
+ response.Subject = strconv.FormatInt(grant.UserID, 10)
}
if user, err := user_model.GetUserByID(ctx, grant.UserID); err == nil {
response.Username = user.Name
@@ -249,7 +241,7 @@ func AuthorizeOAuth(ctx *context.Context) {
}, form.RedirectURI)
return
}
- if err := ctx.Session.Set("CodeChallengeMethod", form.CodeChallenge); err != nil {
+ if err := ctx.Session.Set("CodeChallenge", form.CodeChallenge); err != nil {
handleAuthorizeError(ctx, AuthorizeError{
ErrorCode: ErrorCodeServerError,
ErrorDescription: "cannot set code challenge",
@@ -363,7 +355,7 @@ func GrantApplicationOAuth(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.GrantApplicationForm)
if ctx.Session.Get("client_id") != form.ClientID || ctx.Session.Get("state") != form.State ||
ctx.Session.Get("redirect_uri") != form.RedirectURI {
- ctx.Error(http.StatusBadRequest)
+ ctx.HTTPError(http.StatusBadRequest)
return
}
@@ -440,7 +432,7 @@ func OIDCKeys(ctx *context.Context) {
jwk, err := oauth2_provider.DefaultSigningKey.ToJWK()
if err != nil {
log.Error("Error converting signing key to JWK: %v", err)
- ctx.Error(http.StatusInternalServerError)
+ ctx.HTTPError(http.StatusInternalServerError)
return
}
@@ -464,16 +456,16 @@ func AccessTokenOAuth(ctx *context.Context) {
form := *web.GetForm(ctx).(*forms.AccessTokenForm)
// if there is no ClientID or ClientSecret in the request body, fill these fields by the Authorization header and ensure the provided field matches the Authorization header
if form.ClientID == "" || form.ClientSecret == "" {
- authHeader := ctx.Req.Header.Get("Authorization")
- if authType, authData, ok := strings.Cut(authHeader, " "); ok && strings.EqualFold(authType, "Basic") {
- clientID, clientSecret, err := base.BasicAuthDecode(authData)
- if err != nil {
+ if authHeader := ctx.Req.Header.Get("Authorization"); authHeader != "" {
+ parsed, ok := httpauth.ParseAuthorizationHeader(authHeader)
+ if !ok || parsed.BasicAuth == nil {
handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidRequest,
ErrorDescription: "cannot parse basic auth header",
})
return
}
+ clientID, clientSecret := parsed.BasicAuth.Username, parsed.BasicAuth.Password
// validate that any fields present in the form match the Basic auth header
if form.ClientID != "" && form.ClientID != clientID {
handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
diff --git a/routers/web/auth/openid.go b/routers/web/auth/openid.go
index 41d37ecb8b..2ef4a86022 100644
--- a/routers/web/auth/openid.go
+++ b/routers/web/auth/openid.go
@@ -4,7 +4,7 @@
package auth
import (
- "fmt"
+ "errors"
"net/http"
"net/url"
@@ -55,13 +55,13 @@ func allowedOpenIDURI(uri string) (err error) {
}
}
// must match one of this or be refused
- return fmt.Errorf("URI not allowed by whitelist")
+ return errors.New("URI not allowed by whitelist")
}
// A blacklist match expliclty forbids
for _, pat := range setting.Service.OpenIDBlacklist {
if pat.MatchString(uri) {
- return fmt.Errorf("URI forbidden by blacklist")
+ return errors.New("URI forbidden by blacklist")
}
}
@@ -99,7 +99,7 @@ func SignInOpenIDPost(ctx *context.Context) {
url, err := openid.RedirectURL(id, redirectTo, setting.AppURL)
if err != nil {
log.Error("Error in OpenID redirect URL: %s, %v", redirectTo, err.Error())
- ctx.RenderWithErr(fmt.Sprintf("Unable to find OpenID provider in %s", redirectTo), tplSignInOpenID, &form)
+ ctx.RenderWithErr("Unable to find OpenID provider in "+redirectTo, tplSignInOpenID, &form)
return
}
@@ -337,7 +337,7 @@ func RegisterOpenIDPost(ctx *context.Context) {
ctx.Data["OpenID"] = oid
if setting.Service.AllowOnlyInternalRegistration {
- ctx.Error(http.StatusForbidden)
+ ctx.HTTPError(http.StatusForbidden)
return
}
@@ -349,10 +349,7 @@ func RegisterOpenIDPost(ctx *context.Context) {
context.VerifyCaptcha(ctx, tplSignUpOID, form)
}
- length := setting.MinPasswordLength
- if length < 256 {
- length = 256
- }
+ length := max(setting.MinPasswordLength, 256)
password, err := util.CryptoRandomString(int64(length))
if err != nil {
ctx.RenderWithErr(err.Error(), tplSignUpOID, form)
diff --git a/routers/web/auth/password.go b/routers/web/auth/password.go
index 614e086f77..537ad4b994 100644
--- a/routers/web/auth/password.go
+++ b/routers/web/auth/password.go
@@ -5,7 +5,6 @@ package auth
import (
"errors"
- "fmt"
"net/http"
"code.gitea.io/gitea/models/auth"
@@ -53,7 +52,7 @@ func ForgotPasswdPost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("auth.forgot_password_title")
if setting.MailService == nil {
- ctx.NotFound("ForgotPasswdPost", nil)
+ ctx.NotFound(nil)
return
}
ctx.Data["IsResetRequest"] = true
@@ -108,21 +107,21 @@ func commonResetPassword(ctx *context.Context) (*user_model.User, *auth.TwoFacto
}
if len(code) == 0 {
- ctx.Flash.Error(ctx.Tr("auth.invalid_code_forgot_password", fmt.Sprintf("%s/user/forgot_password", setting.AppSubURL)), true)
+ ctx.Flash.Error(ctx.Tr("auth.invalid_code_forgot_password", setting.AppSubURL+"/user/forgot_password"), true)
return nil, nil
}
// Fail early, don't frustrate the user
u := user_model.VerifyUserTimeLimitCode(ctx, &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeResetPassword}, code)
if u == nil {
- ctx.Flash.Error(ctx.Tr("auth.invalid_code_forgot_password", fmt.Sprintf("%s/user/forgot_password", setting.AppSubURL)), true)
+ ctx.Flash.Error(ctx.Tr("auth.invalid_code_forgot_password", setting.AppSubURL+"/user/forgot_password"), true)
return nil, nil
}
twofa, err := auth.GetTwoFactorByUID(ctx, u.ID)
if err != nil {
if !auth.IsErrTwoFactorNotEnrolled(err) {
- ctx.Error(http.StatusInternalServerError, "CommonResetPassword", err.Error())
+ ctx.HTTPError(http.StatusInternalServerError, "CommonResetPassword", err.Error())
return nil, nil
}
} else {
@@ -181,7 +180,7 @@ func ResetPasswdPost(ctx *context.Context) {
passcode := ctx.FormString("passcode")
ok, err := twofa.ValidateTOTP(passcode)
if err != nil {
- ctx.Error(http.StatusInternalServerError, "ValidateTOTP", err.Error())
+ ctx.HTTPError(http.StatusInternalServerError, "ValidateTOTP", err.Error())
return
}
if !ok || twofa.LastUsedPasscode == passcode {
diff --git a/routers/web/auth/webauthn.go b/routers/web/auth/webauthn.go
index 69031adeaa..78f6c3b58e 100644
--- a/routers/web/auth/webauthn.go
+++ b/routers/web/auth/webauthn.go
@@ -50,6 +50,11 @@ func WebAuthn(ctx *context.Context) {
// WebAuthnPasskeyAssertion submits a WebAuthn challenge for the passkey login to the browser
func WebAuthnPasskeyAssertion(ctx *context.Context) {
+ if !setting.Service.EnablePasskeyAuth {
+ ctx.HTTPError(http.StatusForbidden)
+ return
+ }
+
assertion, sessionData, err := wa.WebAuthn.BeginDiscoverableLogin()
if err != nil {
ctx.ServerError("webauthn.BeginDiscoverableLogin", err)
@@ -66,6 +71,11 @@ func WebAuthnPasskeyAssertion(ctx *context.Context) {
// WebAuthnPasskeyLogin handles the WebAuthn login process using a Passkey
func WebAuthnPasskeyLogin(ctx *context.Context) {
+ if !setting.Service.EnablePasskeyAuth {
+ ctx.HTTPError(http.StatusForbidden)
+ return
+ }
+
sessionData, okData := ctx.Session.Get("webauthnPasskeyAssertion").(*webauthn.SessionData)
if !okData || sessionData == nil {
ctx.ServerError("ctx.Session.Get", errors.New("not in WebAuthn session"))
diff --git a/routers/web/base.go b/routers/web/base.go
index abe11593f7..e43f36a97b 100644
--- a/routers/web/base.go
+++ b/routers/web/base.go
@@ -19,13 +19,13 @@ import (
"code.gitea.io/gitea/modules/web/routing"
)
-func storageHandler(storageSetting *setting.Storage, prefix string, objStore storage.ObjectStorage) http.HandlerFunc {
+func avatarStorageHandler(storageSetting *setting.Storage, prefix string, objStore storage.ObjectStorage) http.HandlerFunc {
prefix = strings.Trim(prefix, "/")
- funcInfo := routing.GetFuncInfo(storageHandler, prefix)
+ funcInfo := routing.GetFuncInfo(avatarStorageHandler, prefix)
if storageSetting.ServeDirect() {
- return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
- if req.Method != "GET" && req.Method != "HEAD" {
+ return func(w http.ResponseWriter, req *http.Request) {
+ if req.Method != http.MethodGet && req.Method != http.MethodHead {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
@@ -52,11 +52,11 @@ func storageHandler(storageSetting *setting.Storage, prefix string, objStore sto
}
http.Redirect(w, req, u.String(), http.StatusTemporaryRedirect)
- })
+ }
}
- return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
- if req.Method != "GET" && req.Method != "HEAD" {
+ return func(w http.ResponseWriter, req *http.Request) {
+ if req.Method != http.MethodGet && req.Method != http.MethodHead {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
@@ -93,6 +93,8 @@ func storageHandler(storageSetting *setting.Storage, prefix string, objStore sto
return
}
defer fr.Close()
- httpcache.ServeContentWithCacheControl(w, req, path.Base(rPath), fi.ModTime(), fr)
- })
+
+ httpcache.SetCacheControlInHeader(w.Header(), httpcache.CacheControlForPublicStatic())
+ http.ServeContent(w, req, path.Base(rPath), fi.ModTime(), fr)
+ }
}
diff --git a/routers/web/devtest/devtest.go b/routers/web/devtest/devtest.go
index 1ea1398173..a22d376579 100644
--- a/routers/web/devtest/devtest.go
+++ b/routers/web/devtest/devtest.go
@@ -4,16 +4,22 @@
package devtest
import (
+ "fmt"
+ "html/template"
"net/http"
"path"
+ "strconv"
"strings"
"time"
+ "unicode"
"code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/badge"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
)
@@ -45,86 +51,135 @@ func FetchActionTest(ctx *context.Context) {
ctx.JSONRedirect("")
}
-func prepareMockData(ctx *context.Context) {
- if ctx.Req.URL.Path == "/devtest/gitea-ui" {
- now := time.Now()
- ctx.Data["TimeNow"] = now
- ctx.Data["TimePast5s"] = now.Add(-5 * time.Second)
- ctx.Data["TimeFuture5s"] = now.Add(5 * time.Second)
- ctx.Data["TimePast2m"] = now.Add(-2 * time.Minute)
- ctx.Data["TimeFuture2m"] = now.Add(2 * time.Minute)
- ctx.Data["TimePast1y"] = now.Add(-1 * 366 * 86400 * time.Second)
- ctx.Data["TimeFuture1y"] = now.Add(1 * 366 * 86400 * time.Second)
+func prepareMockDataGiteaUI(ctx *context.Context) {
+ now := time.Now()
+ ctx.Data["TimeNow"] = now
+ ctx.Data["TimePast5s"] = now.Add(-5 * time.Second)
+ ctx.Data["TimeFuture5s"] = now.Add(5 * time.Second)
+ ctx.Data["TimePast2m"] = now.Add(-2 * time.Minute)
+ ctx.Data["TimeFuture2m"] = now.Add(2 * time.Minute)
+ ctx.Data["TimePast1y"] = now.Add(-1 * 366 * 86400 * time.Second)
+ ctx.Data["TimeFuture1y"] = now.Add(1 * 366 * 86400 * time.Second)
+}
+
+func prepareMockDataBadgeCommitSign(ctx *context.Context) {
+ var commits []*asymkey.SignCommit
+ mockUsers, _ := db.Find[user_model.User](ctx, user_model.SearchUserOptions{ListOptions: db.ListOptions{PageSize: 1}})
+ mockUser := mockUsers[0]
+ commits = append(commits, &asymkey.SignCommit{
+ Verification: &asymkey.CommitVerification{},
+ UserCommit: &user_model.UserCommit{
+ Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
+ },
+ })
+ commits = append(commits, &asymkey.SignCommit{
+ Verification: &asymkey.CommitVerification{
+ Verified: true,
+ Reason: "name / key-id",
+ SigningUser: mockUser,
+ SigningKey: &asymkey.GPGKey{KeyID: "12345678"},
+ TrustStatus: "trusted",
+ },
+ UserCommit: &user_model.UserCommit{
+ User: mockUser,
+ Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
+ },
+ })
+ commits = append(commits, &asymkey.SignCommit{
+ Verification: &asymkey.CommitVerification{
+ Verified: true,
+ Reason: "name / key-id",
+ SigningUser: mockUser,
+ SigningSSHKey: &asymkey.PublicKey{Fingerprint: "aa:bb:cc:dd:ee"},
+ TrustStatus: "untrusted",
+ },
+ UserCommit: &user_model.UserCommit{
+ User: mockUser,
+ Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
+ },
+ })
+ commits = append(commits, &asymkey.SignCommit{
+ Verification: &asymkey.CommitVerification{
+ Verified: true,
+ Reason: "name / key-id",
+ SigningUser: mockUser,
+ SigningSSHKey: &asymkey.PublicKey{Fingerprint: "aa:bb:cc:dd:ee"},
+ TrustStatus: "other(unmatch)",
+ },
+ UserCommit: &user_model.UserCommit{
+ User: mockUser,
+ Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
+ },
+ })
+ commits = append(commits, &asymkey.SignCommit{
+ Verification: &asymkey.CommitVerification{
+ Warning: true,
+ Reason: "gpg.error",
+ SigningEmail: "test@example.com",
+ },
+ UserCommit: &user_model.UserCommit{
+ User: mockUser,
+ Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
+ },
+ })
+
+ ctx.Data["MockCommits"] = commits
+}
+
+func prepareMockDataBadgeActionsSvg(ctx *context.Context) {
+ fontFamilyNames := strings.Split(badge.DefaultFontFamily, ",")
+ selectedFontFamilyName := ctx.FormString("font", fontFamilyNames[0])
+ selectedStyle := ctx.FormString("style", badge.DefaultStyle)
+ var badges []badge.Badge
+ badges = append(badges, badge.GenerateBadge("啊啊啊啊啊啊啊啊啊啊啊啊", "🌞🌞🌞🌞🌞", "green"))
+ for r := range rune(256) {
+ if unicode.IsPrint(r) {
+ s := strings.Repeat(string(r), 15)
+ badges = append(badges, badge.GenerateBadge(s, util.TruncateRunes(s, 7), "green"))
+ }
}
- if ctx.Req.URL.Path == "/devtest/commit-sign-badge" {
- var commits []*asymkey.SignCommit
- mockUsers, _ := db.Find[user_model.User](ctx, user_model.SearchUserOptions{ListOptions: db.ListOptions{PageSize: 1}})
- mockUser := mockUsers[0]
- commits = append(commits, &asymkey.SignCommit{
- Verification: &asymkey.CommitVerification{},
- UserCommit: &user_model.UserCommit{
- Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
- },
- })
- commits = append(commits, &asymkey.SignCommit{
- Verification: &asymkey.CommitVerification{
- Verified: true,
- Reason: "name / key-id",
- SigningUser: mockUser,
- SigningKey: &asymkey.GPGKey{KeyID: "12345678"},
- TrustStatus: "trusted",
- },
- UserCommit: &user_model.UserCommit{
- User: mockUser,
- Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
- },
- })
- commits = append(commits, &asymkey.SignCommit{
- Verification: &asymkey.CommitVerification{
- Verified: true,
- Reason: "name / key-id",
- SigningUser: mockUser,
- SigningSSHKey: &asymkey.PublicKey{Fingerprint: "aa:bb:cc:dd:ee"},
- TrustStatus: "untrusted",
- },
- UserCommit: &user_model.UserCommit{
- User: mockUser,
- Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
- },
- })
- commits = append(commits, &asymkey.SignCommit{
- Verification: &asymkey.CommitVerification{
- Verified: true,
- Reason: "name / key-id",
- SigningUser: mockUser,
- SigningSSHKey: &asymkey.PublicKey{Fingerprint: "aa:bb:cc:dd:ee"},
- TrustStatus: "other(unmatch)",
- },
- UserCommit: &user_model.UserCommit{
- User: mockUser,
- Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
- },
- })
- commits = append(commits, &asymkey.SignCommit{
- Verification: &asymkey.CommitVerification{
- Warning: true,
- Reason: "gpg.error",
- SigningEmail: "test@example.com",
- },
- UserCommit: &user_model.UserCommit{
- User: mockUser,
- Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
- },
- })
+ var badgeSVGs []template.HTML
+ for i, b := range badges {
+ b.IDPrefix = "devtest-" + strconv.FormatInt(int64(i), 10) + "-"
+ b.FontFamily = selectedFontFamilyName
+ var h template.HTML
+ var err error
+ switch selectedStyle {
+ case badge.StyleFlat:
+ h, err = ctx.RenderToHTML("shared/actions/runner_badge_flat", map[string]any{"Badge": b})
+ case badge.StyleFlatSquare:
+ h, err = ctx.RenderToHTML("shared/actions/runner_badge_flat-square", map[string]any{"Badge": b})
+ default:
+ err = fmt.Errorf("unknown badge style: %s", selectedStyle)
+ }
+ if err != nil {
+ ctx.ServerError("RenderToHTML", err)
+ return
+ }
+ badgeSVGs = append(badgeSVGs, h)
+ }
+ ctx.Data["BadgeSVGs"] = badgeSVGs
+ ctx.Data["BadgeFontFamilyNames"] = fontFamilyNames
+ ctx.Data["SelectedFontFamilyName"] = selectedFontFamilyName
+ ctx.Data["BadgeStyles"] = badge.GlobalVars().AllStyles
+ ctx.Data["SelectedStyle"] = selectedStyle
+}
- ctx.Data["MockCommits"] = commits
+func prepareMockData(ctx *context.Context) {
+ switch ctx.Req.URL.Path {
+ case "/devtest/gitea-ui":
+ prepareMockDataGiteaUI(ctx)
+ case "/devtest/badge-commit-sign":
+ prepareMockDataBadgeCommitSign(ctx)
+ case "/devtest/badge-actions-svg":
+ prepareMockDataBadgeActionsSvg(ctx)
}
}
-func Tmpl(ctx *context.Context) {
+func TmplCommon(ctx *context.Context) {
prepareMockData(ctx)
- if ctx.Req.Method == "POST" {
+ if ctx.Req.Method == http.MethodPost {
_ = ctx.Req.ParseForm()
ctx.Flash.Info("form: "+ctx.Req.Method+" "+ctx.Req.RequestURI+"<br>"+
"Form: "+ctx.Req.Form.Encode()+"<br>"+
diff --git a/routers/web/devtest/mock_actions.go b/routers/web/devtest/mock_actions.go
index f29b8e4046..bc741ecd11 100644
--- a/routers/web/devtest/mock_actions.go
+++ b/routers/web/devtest/mock_actions.go
@@ -4,9 +4,9 @@
package devtest
import (
- "fmt"
mathRand "math/rand/v2"
"net/http"
+ "strconv"
"strings"
"time"
@@ -38,8 +38,8 @@ func generateMockStepsLog(logCur actions.LogCursor) (stepsLog []*actions.ViewSte
for i := 0; i < mockCount; i++ {
logStr := mockedLogs[int(cur)%len(mockedLogs)]
cur++
- logStr = strings.ReplaceAll(logStr, "{step}", fmt.Sprintf("%d", logCur.Step))
- logStr = strings.ReplaceAll(logStr, "{cursor}", fmt.Sprintf("%d", cur))
+ logStr = strings.ReplaceAll(logStr, "{step}", strconv.Itoa(logCur.Step))
+ logStr = strings.ReplaceAll(logStr, "{cursor}", strconv.FormatInt(cur, 10))
stepsLog = append(stepsLog, &actions.ViewStepLog{
Step: logCur.Step,
Cursor: cur,
@@ -52,13 +52,22 @@ func generateMockStepsLog(logCur actions.LogCursor) (stepsLog []*actions.ViewSte
return stepsLog
}
+func MockActionsView(ctx *context.Context) {
+ ctx.Data["RunID"] = ctx.PathParam("run")
+ ctx.Data["JobID"] = ctx.PathParam("job")
+ ctx.HTML(http.StatusOK, "devtest/repo-action-view")
+}
+
func MockActionsRunsJobs(ctx *context.Context) {
- req := web.GetForm(ctx).(*actions.ViewRequest)
+ runID := ctx.PathParamInt64("run")
+ req := web.GetForm(ctx).(*actions.ViewRequest)
resp := &actions.ViewResponse{}
resp.State.Run.TitleHTML = `mock run title <a href="/">link</a>`
resp.State.Run.Status = actions_model.StatusRunning.String()
- resp.State.Run.CanCancel = true
+ resp.State.Run.CanCancel = runID == 10
+ resp.State.Run.CanApprove = runID == 20
+ resp.State.Run.CanRerun = runID == 30
resp.State.Run.CanDeleteArtifact = true
resp.State.Run.WorkflowID = "workflow-id"
resp.State.Run.WorkflowLink = "./workflow-link"
@@ -85,6 +94,39 @@ func MockActionsRunsJobs(ctx *context.Context) {
Size: 1024 * 1024,
Status: "completed",
})
+ resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{
+ Name: "artifact-very-loooooooooooooooooooooooooooooooooooooooooooooooooooooooong",
+ Size: 100 * 1024,
+ Status: "expired",
+ })
+ resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{
+ Name: "artifact-really-loooooooooooooooooooooooooooooooooooooooooooooooooooooooong",
+ Size: 1024 * 1024,
+ Status: "completed",
+ })
+
+ resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{
+ ID: runID * 10,
+ Name: "job 100",
+ Status: actions_model.StatusRunning.String(),
+ CanRerun: true,
+ Duration: "1h",
+ })
+ resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{
+ ID: runID*10 + 1,
+ Name: "job 101",
+ Status: actions_model.StatusWaiting.String(),
+ CanRerun: false,
+ Duration: "2h",
+ })
+ resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{
+ ID: runID*10 + 2,
+ Name: "job 102",
+ Status: actions_model.StatusFailure.String(),
+ CanRerun: false,
+ Duration: "3h",
+ })
+
resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, &actions.ViewJobStep{
Summary: "step 0 (mock slow)",
Duration: time.Hour.String(),
@@ -118,7 +160,7 @@ func MockActionsRunsJobs(ctx *context.Context) {
}
if doErrorResponse {
if mathRand.Float64() > 0.5 {
- ctx.Error(http.StatusInternalServerError, "devtest mock error response")
+ ctx.HTTPError(http.StatusInternalServerError, "devtest mock error response")
return
}
}
diff --git a/routers/web/explore/code.go b/routers/web/explore/code.go
index ae5ff3db76..3bb50ef397 100644
--- a/routers/web/explore/code.go
+++ b/routers/web/explore/code.go
@@ -5,6 +5,7 @@ package explore
import (
"net/http"
+ "slices"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
@@ -72,10 +73,10 @@ func Code(ctx *context.Context) {
if (len(repoIDs) > 0) || isAdmin {
total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{
- RepoIDs: repoIDs,
- Keyword: prepareSearch.Keyword,
- IsKeywordFuzzy: prepareSearch.IsFuzzy,
- Language: prepareSearch.Language,
+ RepoIDs: repoIDs,
+ Keyword: prepareSearch.Keyword,
+ SearchMode: prepareSearch.SearchMode,
+ Language: prepareSearch.Language,
Paginator: &db.ListOptions{
Page: page,
PageSize: setting.UI.RepoSearchPagingNum,
@@ -93,14 +94,7 @@ func Code(ctx *context.Context) {
loadRepoIDs := make([]int64, 0, len(searchResults))
for _, result := range searchResults {
- var find bool
- for _, id := range loadRepoIDs {
- if id == result.RepoID {
- find = true
- break
- }
- }
- if !find {
+ if !slices.Contains(loadRepoIDs, result.RepoID) {
loadRepoIDs = append(loadRepoIDs, result.RepoID)
}
}
diff --git a/routers/web/explore/org.go b/routers/web/explore/org.go
index 7bb71acfd7..f8f7f5c18c 100644
--- a/routers/web/explore/org.go
+++ b/routers/web/explore/org.go
@@ -44,7 +44,7 @@ func Organizations(ctx *context.Context) {
ctx.SetFormString("sort", sortOrder)
}
- RenderUserSearch(ctx, &user_model.SearchUserOptions{
+ RenderUserSearch(ctx, user_model.SearchUserOptions{
Actor: ctx.Doer,
Type: user_model.UserTypeOrganization,
ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum},
diff --git a/routers/web/explore/repo.go b/routers/web/explore/repo.go
index cf3128314b..f0d7d0ce7d 100644
--- a/routers/web/explore/repo.go
+++ b/routers/web/explore/repo.go
@@ -94,7 +94,7 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
private := ctx.FormOptionalBool("private")
ctx.Data["IsPrivate"] = private
- repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
+ repos, count, err = repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
Page: page,
PageSize: opts.PageSize,
@@ -151,6 +151,7 @@ func Repos(ctx *context.Context) {
ctx.Data["CodePageIsDisabled"] = setting.Service.Explore.DisableCodePage
ctx.Data["Title"] = ctx.Tr("explore")
ctx.Data["PageIsExplore"] = true
+ ctx.Data["ShowRepoOwnerOnList"] = true
ctx.Data["PageIsExploreRepositories"] = true
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
diff --git a/routers/web/explore/topic.go b/routers/web/explore/topic.go
index b4507ba28d..be457df587 100644
--- a/routers/web/explore/topic.go
+++ b/routers/web/explore/topic.go
@@ -25,7 +25,7 @@ func TopicSearch(ctx *context.Context) {
topics, total, err := db.FindAndCount[repo_model.Topic](ctx, opts)
if err != nil {
- ctx.Error(http.StatusInternalServerError)
+ ctx.HTTPError(http.StatusInternalServerError)
return
}
diff --git a/routers/web/explore/user.go b/routers/web/explore/user.go
index b14272c76a..af48e6fb79 100644
--- a/routers/web/explore/user.go
+++ b/routers/web/explore/user.go
@@ -32,7 +32,7 @@ func isKeywordValid(keyword string) bool {
}
// RenderUserSearch render user search page
-func RenderUserSearch(ctx *context.Context, opts *user_model.SearchUserOptions, tplName templates.TplName) {
+func RenderUserSearch(ctx *context.Context, opts user_model.SearchUserOptions, tplName templates.TplName) {
// Sitemap index for sitemap paths
opts.Page = int(ctx.PathParamInt64("idx"))
isSitemap := ctx.PathParam("idx") != ""
@@ -87,7 +87,7 @@ func RenderUserSearch(ctx *context.Context, opts *user_model.SearchUserOptions,
}
if opts.SupportedSortOrders != nil && !opts.SupportedSortOrders.Contains(sortOrder) {
- ctx.NotFound("unsupported sort order", nil)
+ ctx.NotFound(nil)
return
}
@@ -151,7 +151,7 @@ func Users(ctx *context.Context) {
ctx.SetFormString("sort", sortOrder)
}
- RenderUserSearch(ctx, &user_model.SearchUserOptions{
+ RenderUserSearch(ctx, user_model.SearchUserOptions{
Actor: ctx.Doer,
Type: user_model.UserTypeIndividual,
ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum},
diff --git a/routers/web/feed/branch.go b/routers/web/feed/branch.go
index 6c4cc11ca0..094fd987ac 100644
--- a/routers/web/feed/branch.go
+++ b/routers/web/feed/branch.go
@@ -4,7 +4,6 @@
package feed
import (
- "fmt"
"strings"
"time"
@@ -16,13 +15,13 @@ import (
// ShowBranchFeed shows tags and/or releases on the repo as RSS / Atom feed
func ShowBranchFeed(ctx *context.Context, repo *repo.Repository, formatType string) {
- commits, err := ctx.Repo.Commit.CommitsByRange(0, 10, "")
+ commits, err := ctx.Repo.Commit.CommitsByRange(0, 10, "", "", "")
if err != nil {
ctx.ServerError("ShowBranchFeed", err)
return
}
- title := fmt.Sprintf("Latest commits for branch %s", ctx.Repo.BranchName)
+ title := "Latest commits for branch " + ctx.Repo.BranchName
link := &feeds.Link{Href: repo.HTMLURL() + "/" + ctx.Repo.RefTypeNameSubURL()}
feed := &feeds.Feed{
@@ -43,6 +42,7 @@ func ShowBranchFeed(ctx *context.Context, repo *repo.Repository, formatType stri
},
Description: commit.Message(),
Content: commit.Message(),
+ Created: commit.Committer.When,
})
}
diff --git a/routers/web/feed/convert.go b/routers/web/feed/convert.go
index b7f849dc65..7c59132841 100644
--- a/routers/web/feed/convert.go
+++ b/routers/web/feed/convert.go
@@ -50,7 +50,7 @@ func toReleaseLink(ctx *context.Context, act *activities_model.Action) string {
// renderCommentMarkdown renders the comment markdown to html
func renderCommentMarkdown(ctx *context.Context, act *activities_model.Action, content string) template.HTML {
- act.LoadRepo(ctx)
+ _ = act.LoadRepo(ctx)
if act.Repo == nil {
return ""
}
@@ -201,7 +201,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
switch act.OpType {
case activities_model.ActionCommitRepo, activities_model.ActionMirrorSyncPush:
push := templates.ActionContent2Commits(act)
-
+ _ = act.LoadRepo(ctx)
for _, commit := range push.Commits {
if len(desc) != 0 {
desc += "\n\n"
@@ -209,7 +209,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
desc += fmt.Sprintf("<a href=\"%s\">%s</a>\n%s",
html.EscapeString(fmt.Sprintf("%s/commit/%s", act.GetRepoAbsoluteLink(ctx), commit.Sha1)),
commit.Sha1,
- renderUtils.RenderCommitMessage(commit.Message, nil),
+ renderUtils.RenderCommitMessage(commit.Message, act.Repo),
)
}
@@ -258,18 +258,18 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
}
// GetFeedType return if it is a feed request and altered name and feed type.
-func GetFeedType(name string, req *http.Request) (bool, string, string) {
+func GetFeedType(name string, req *http.Request) (showFeed bool, feedType string) {
if strings.HasSuffix(name, ".rss") ||
strings.Contains(req.Header.Get("Accept"), "application/rss+xml") {
- return true, strings.TrimSuffix(name, ".rss"), "rss"
+ return true, "rss"
}
if strings.HasSuffix(name, ".atom") ||
strings.Contains(req.Header.Get("Accept"), "application/atom+xml") {
- return true, strings.TrimSuffix(name, ".atom"), "atom"
+ return true, "atom"
}
- return false, name, ""
+ return false, ""
}
// feedActionsToFeedItems convert gitea's Repo's Releases to feeds Item
diff --git a/routers/web/feed/file.go b/routers/web/feed/file.go
index 518d995ccb..026c15c43a 100644
--- a/routers/web/feed/file.go
+++ b/routers/web/feed/file.go
@@ -4,7 +4,6 @@
package feed
import (
- "fmt"
"strings"
"time"
@@ -33,7 +32,7 @@ func ShowFileFeed(ctx *context.Context, repo *repo.Repository, formatType string
return
}
- title := fmt.Sprintf("Latest commits for file %s", ctx.Repo.TreePath)
+ title := "Latest commits for file " + ctx.Repo.TreePath
link := &feeds.Link{Href: repo.HTMLURL() + "/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)}
@@ -55,6 +54,7 @@ func ShowFileFeed(ctx *context.Context, repo *repo.Repository, formatType string
},
Description: commit.Message(),
Content: commit.Message(),
+ Created: commit.Committer.When,
})
}
diff --git a/routers/web/feed/profile.go b/routers/web/feed/profile.go
index 4ec46e302a..cecf9d409a 100644
--- a/routers/web/feed/profile.go
+++ b/routers/web/feed/profile.go
@@ -7,6 +7,7 @@ import (
"time"
activities_model "code.gitea.io/gitea/models/activities"
+ "code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/renderhelper"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/services/context"
@@ -28,12 +29,23 @@ func ShowUserFeedAtom(ctx *context.Context) {
// showUserFeed show user activity as RSS / Atom feed
func showUserFeed(ctx *context.Context, formatType string) {
includePrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
+ isOrganisation := ctx.ContextUser.IsOrganization()
+ if ctx.IsSigned && isOrganisation && !includePrivate {
+ // When feed is requested by a member of the organization,
+ // include the private repo's the member has access to.
+ isOrgMember, err := organization.IsOrganizationMember(ctx, ctx.ContextUser.ID, ctx.Doer.ID)
+ if err != nil {
+ ctx.ServerError("IsOrganizationMember", err)
+ return
+ }
+ includePrivate = isOrgMember
+ }
actions, _, err := feed_service.GetFeeds(ctx, activities_model.GetFeedsOptions{
RequestedUser: ctx.ContextUser,
Actor: ctx.Doer,
IncludePrivate: includePrivate,
- OnlyPerformedBy: !ctx.ContextUser.IsOrganization(),
+ OnlyPerformedBy: !isOrganisation,
IncludeDeleted: false,
Date: ctx.FormString("date"),
})
diff --git a/routers/web/feed/profile_test.go b/routers/web/feed/profile_test.go
new file mode 100644
index 0000000000..a0f1509269
--- /dev/null
+++ b/routers/web/feed/profile_test.go
@@ -0,0 +1,38 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+package feed_test
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/routers/web/feed"
+ "code.gitea.io/gitea/services/contexttest"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestMain(m *testing.M) {
+ unittest.MainTest(m)
+}
+
+func TestCheckGetOrgFeedsAsOrgMember(t *testing.T) {
+ unittest.PrepareTestEnv(t)
+ t.Run("OrgMember", func(t *testing.T) {
+ ctx, resp := contexttest.MockContext(t, "org3.atom")
+ ctx.ContextUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
+ contexttest.LoadUser(t, ctx, 2)
+ ctx.IsSigned = true
+ feed.ShowUserFeedAtom(ctx)
+ assert.Contains(t, resp.Body.String(), "<entry>") // Should contain 1 private entry
+ })
+ t.Run("NonOrgMember", func(t *testing.T) {
+ ctx, resp := contexttest.MockContext(t, "org3.atom")
+ ctx.ContextUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
+ contexttest.LoadUser(t, ctx, 5)
+ ctx.IsSigned = true
+ feed.ShowUserFeedAtom(ctx)
+ assert.NotContains(t, resp.Body.String(), "<entry>") // Should not contain any entries
+ })
+}
diff --git a/routers/web/feed/render.go b/routers/web/feed/render.go
index 462ebb97b5..d1a229e5b2 100644
--- a/routers/web/feed/render.go
+++ b/routers/web/feed/render.go
@@ -9,7 +9,7 @@ import (
// RenderBranchFeed render format for branch or file
func RenderBranchFeed(ctx *context.Context) {
- _, _, showFeedType := GetFeedType(ctx.PathParam("reponame"), ctx.Req)
+ _, showFeedType := GetFeedType(ctx.PathParam("reponame"), ctx.Req)
if ctx.Repo.TreePath == "" {
ShowBranchFeed(ctx, ctx.Repo.Repository, showFeedType)
} else {
diff --git a/routers/web/githttp.go b/routers/web/githttp.go
index 5831e6f523..06de811f16 100644
--- a/routers/web/githttp.go
+++ b/routers/web/githttp.go
@@ -4,26 +4,12 @@
package web
import (
- "net/http"
-
- "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/web/repo"
"code.gitea.io/gitea/services/context"
)
func addOwnerRepoGitHTTPRouters(m *web.Router) {
- reqGitSignIn := func(ctx *context.Context) {
- if !setting.Service.RequireSignInView {
- return
- }
- // rely on the results of Contexter
- if !ctx.IsSigned {
- // TODO: support digit auth - which would be Authorization header with digit
- ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea"`)
- ctx.Error(http.StatusUnauthorized)
- }
- }
m.Group("/{username}/{reponame}", func() {
m.Methods("POST,OPTIONS", "/git-upload-pack", repo.ServiceUploadPack)
m.Methods("POST,OPTIONS", "/git-receive-pack", repo.ServiceReceivePack)
@@ -36,5 +22,5 @@ func addOwnerRepoGitHTTPRouters(m *web.Router) {
m.Methods("GET,OPTIONS", "/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38,62}}", repo.GetLooseObject)
m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.pack", repo.GetPackFile)
m.Methods("GET,OPTIONS", "/objects/pack/pack-{file:[0-9a-f]{40,64}}.idx", repo.GetIdxFile)
- }, optSignInIgnoreCsrf, reqGitSignIn, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context.UserAssignmentWeb())
+ }, optSignInIgnoreCsrf, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context.UserAssignmentWeb())
}
diff --git a/routers/web/goget.go b/routers/web/goget.go
index 79d5c2b207..67e0bee866 100644
--- a/routers/web/goget.go
+++ b/routers/web/goget.go
@@ -18,7 +18,7 @@ import (
)
func goGet(ctx *context.Context) {
- if ctx.Req.Method != "GET" || len(ctx.Req.URL.RawQuery) < 8 || ctx.FormString("go-get") != "1" {
+ if ctx.Req.Method != http.MethodGet || len(ctx.Req.URL.RawQuery) < 8 || ctx.FormString("go-get") != "1" {
return
}
diff --git a/routers/web/home.go b/routers/web/home.go
index 9ad495d54f..4b15ee83c2 100644
--- a/routers/web/home.go
+++ b/routers/web/home.go
@@ -68,7 +68,7 @@ func Home(ctx *context.Context) {
func HomeSitemap(ctx *context.Context) {
m := sitemap.NewSitemapIndex()
if !setting.Service.Explore.DisableUsersPage {
- _, cnt, err := user_model.SearchUsers(ctx, &user_model.SearchUserOptions{
+ _, cnt, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{
Type: user_model.UserTypeIndividual,
ListOptions: db.ListOptions{PageSize: 1},
IsActive: optional.Some(true),
@@ -86,7 +86,7 @@ func HomeSitemap(ctx *context.Context) {
}
}
- _, cnt, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
+ _, cnt, err := repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
PageSize: 1,
},
@@ -113,5 +113,5 @@ func HomeSitemap(ctx *context.Context) {
// NotFound render 404 page
func NotFound(ctx *context.Context) {
ctx.Data["Title"] = "Page Not Found"
- ctx.NotFound("home.NotFound", nil)
+ ctx.NotFound(nil)
}
diff --git a/routers/web/misc/markup.go b/routers/web/misc/markup.go
index 0c7ec6c2eb..f90cf3ffed 100644
--- a/routers/web/misc/markup.go
+++ b/routers/web/misc/markup.go
@@ -15,6 +15,6 @@ import (
// Markup render markup document to HTML
func Markup(ctx *context.Context) {
form := web.GetForm(ctx).(*api.MarkupOption)
- mode := util.Iif(form.Wiki, "wiki", form.Mode) //nolint:staticcheck
+ mode := util.Iif(form.Wiki, "wiki", form.Mode) //nolint:staticcheck // form.Wiki is deprecated
common.RenderMarkup(ctx.Base, ctx.Repo, mode, form.Text, form.Context, form.FilePath)
}
diff --git a/routers/web/misc/misc.go b/routers/web/misc/misc.go
index caaca7f521..59b97c1717 100644
--- a/routers/web/misc/misc.go
+++ b/routers/web/misc/misc.go
@@ -20,7 +20,7 @@ func SSHInfo(rw http.ResponseWriter, req *http.Request) {
return
}
rw.Header().Set("content-type", "text/json;charset=UTF-8")
- _, err := rw.Write([]byte(`{"type":"gitea","version":1}`))
+ _, err := rw.Write([]byte(`{"type":"agit","version":1}`))
if err != nil {
log.Error("fail to write result: err: %v", err)
rw.WriteHeader(http.StatusInternalServerError)
@@ -38,7 +38,7 @@ func RobotsTxt(w http.ResponseWriter, req *http.Request) {
if ok, _ := util.IsExist(robotsTxt); !ok {
robotsTxt = util.FilePathJoinAbs(setting.CustomPath, "robots.txt") // the legacy "robots.txt"
}
- httpcache.SetCacheControlInHeader(w.Header(), setting.StaticCacheTime)
+ httpcache.SetCacheControlInHeader(w.Header(), httpcache.CacheControlForPublicStatic())
http.ServeFile(w, req, robotsTxt)
}
diff --git a/routers/web/nodeinfo.go b/routers/web/nodeinfo.go
index f1cc7bf530..47856bf98b 100644
--- a/routers/web/nodeinfo.go
+++ b/routers/web/nodeinfo.go
@@ -4,7 +4,6 @@
package web
import (
- "fmt"
"net/http"
"code.gitea.io/gitea/modules/setting"
@@ -24,7 +23,7 @@ type nodeInfoLink struct {
func NodeInfoLinks(ctx *context.Context) {
nodeinfolinks := &nodeInfoLinks{
Links: []nodeInfoLink{{
- fmt.Sprintf("%sapi/v1/nodeinfo", setting.AppURL),
+ setting.AppURL + "api/v1/nodeinfo",
"http://nodeinfo.diaspora.software/ns/schema/2.1",
}},
}
diff --git a/routers/web/org/block.go b/routers/web/org/block.go
index aeb4bd51a8..60f722dd39 100644
--- a/routers/web/org/block.go
+++ b/routers/web/org/block.go
@@ -20,6 +20,11 @@ func BlockedUsers(ctx *context.Context) {
ctx.Data["PageIsOrgSettings"] = true
ctx.Data["PageIsSettingsBlockedUsers"] = true
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
+ return
+ }
+
shared_user.BlockedUsers(ctx, ctx.ContextUser)
if ctx.Written() {
return
@@ -29,6 +34,11 @@ func BlockedUsers(ctx *context.Context) {
}
func BlockedUsersPost(ctx *context.Context) {
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
+ return
+ }
+
shared_user.BlockedUsersPost(ctx, ctx.ContextUser)
if ctx.Written() {
return
diff --git a/routers/web/org/home.go b/routers/web/org/home.go
index 277adb60ca..63ae6c683b 100644
--- a/routers/web/org/home.go
+++ b/routers/web/org/home.go
@@ -29,12 +29,12 @@ func Home(ctx *context.Context) {
uname := ctx.PathParam("username")
if strings.HasSuffix(uname, ".keys") || strings.HasSuffix(uname, ".gpg") {
- ctx.NotFound("", nil)
+ ctx.NotFound(nil)
return
}
ctx.SetPathParam("org", uname)
- context.HandleOrgAssignment(ctx)
+ context.OrgAssignment(context.OrgAssignmentOptions{})(ctx)
if ctx.Written() {
return
}
@@ -86,12 +86,6 @@ func home(ctx *context.Context, viewRepositories bool) {
private := ctx.FormOptionalBool("private")
ctx.Data["IsPrivate"] = private
- err := shared_user.LoadHeaderCount(ctx)
- if err != nil {
- ctx.ServerError("LoadHeaderCount", err)
- return
- }
-
opts := &organization.FindOrgMembersOpts{
Doer: ctx.Doer,
OrgID: org.ID,
@@ -109,9 +103,9 @@ func home(ctx *context.Context, viewRepositories bool) {
ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull
ctx.Data["ShowMemberAndTeamTab"] = ctx.Org.IsMember || len(members) > 0
- prepareResult, err := shared_user.PrepareOrgHeader(ctx)
+ prepareResult, err := shared_user.RenderUserOrgHeader(ctx)
if err != nil {
- ctx.ServerError("PrepareOrgHeader", err)
+ ctx.ServerError("RenderUserOrgHeader", err)
return
}
@@ -121,7 +115,7 @@ func home(ctx *context.Context, viewRepositories bool) {
ctx.Data["PageIsViewOverview"] = isViewOverview
ctx.Data["ShowOrgProfileReadmeSelector"] = isViewOverview && prepareResult.ProfilePublicReadmeBlob != nil && prepareResult.ProfilePrivateReadmeBlob != nil
- repos, count, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
+ repos, count, err := repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
PageSize: setting.UI.User.RepoPagingNum,
Page: page,
@@ -154,7 +148,7 @@ func home(ctx *context.Context, viewRepositories bool) {
ctx.HTML(http.StatusOK, tplOrgHome)
}
-func prepareOrgProfileReadme(ctx *context.Context, prepareResult *shared_user.PrepareOrgHeaderResult) bool {
+func prepareOrgProfileReadme(ctx *context.Context, prepareResult *shared_user.PrepareOwnerHeaderResult) bool {
viewAs := ctx.FormString("view_as", util.Iif(ctx.Org.IsMember, "member", "public"))
viewAsMember := viewAs == "member"
diff --git a/routers/web/org/members.go b/routers/web/org/members.go
index 1665a12302..61022d3f09 100644
--- a/routers/web/org/members.go
+++ b/routers/web/org/members.go
@@ -28,10 +28,7 @@ func Members(ctx *context.Context) {
ctx.Data["Title"] = org.FullName
ctx.Data["PageIsOrgMembers"] = true
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
opts := &organization.FindOrgMembersOpts{
Doer: ctx.Doer,
@@ -41,7 +38,7 @@ func Members(ctx *context.Context) {
if ctx.Doer != nil {
isMember, err := ctx.Org.Organization.IsOrgMember(ctx, ctx.Doer.ID)
if err != nil {
- ctx.Error(http.StatusInternalServerError, "IsOrgMember")
+ ctx.HTTPError(http.StatusInternalServerError, "IsOrgMember")
return
}
opts.IsDoerMember = isMember
@@ -50,13 +47,12 @@ func Members(ctx *context.Context) {
total, err := organization.CountOrgMembers(ctx, opts)
if err != nil {
- ctx.Error(http.StatusInternalServerError, "CountOrgMembers")
+ ctx.HTTPError(http.StatusInternalServerError, "CountOrgMembers")
return
}
- _, err = shared_user.PrepareOrgHeader(ctx)
- if err != nil {
- ctx.ServerError("PrepareOrgHeader", err)
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
return
}
@@ -93,19 +89,19 @@ func MembersAction(ctx *context.Context) {
switch ctx.PathParam("action") {
case "private":
if ctx.Doer.ID != member.ID && !ctx.Org.IsOwner {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
err = organization.ChangeOrgUserStatus(ctx, org.ID, member.ID, false)
case "public":
if ctx.Doer.ID != member.ID && !ctx.Org.IsOwner {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
err = organization.ChangeOrgUserStatus(ctx, org.ID, member.ID, true)
case "remove":
if !ctx.Org.IsOwner {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
err = org_service.RemoveOrgUser(ctx, org, member)
diff --git a/routers/web/org/org.go b/routers/web/org/org.go
index 856a605764..0540d5c591 100644
--- a/routers/web/org/org.go
+++ b/routers/web/org/org.go
@@ -27,11 +27,14 @@ const (
// Create render the page for create organization
func Create(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("new_org")
- ctx.Data["DefaultOrgVisibilityMode"] = setting.Service.DefaultOrgVisibilityMode
if !ctx.Doer.CanCreateOrganization() {
ctx.ServerError("Not allowed", errors.New(ctx.Locale.TrString("org.form.create_org_not_allowed")))
return
}
+
+ ctx.Data["visibility"] = setting.Service.DefaultOrgVisibilityMode
+ ctx.Data["repo_admin_change_team_access"] = true
+
ctx.HTML(http.StatusOK, tplCreateOrg)
}
diff --git a/routers/web/org/org_labels.go b/routers/web/org/org_labels.go
index 02eae8052e..2a4aa7f557 100644
--- a/routers/web/org/org_labels.go
+++ b/routers/web/org/org_labels.go
@@ -4,13 +4,15 @@
package org
import (
- "net/http"
+ "errors"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/label"
repo_module "code.gitea.io/gitea/modules/repository"
+ "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ shared_label "code.gitea.io/gitea/routers/web/shared/label"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
@@ -32,47 +34,45 @@ func RetrieveLabels(ctx *context.Context) {
// NewLabel create new label for organization
func NewLabel(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.CreateLabelForm)
- ctx.Data["Title"] = ctx.Tr("repo.labels")
- ctx.Data["PageIsLabels"] = true
- ctx.Data["PageIsOrgSettings"] = true
-
- if ctx.HasError() {
- ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
- ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
+ form := shared_label.GetLabelEditForm(ctx)
+ if ctx.Written() {
return
}
l := &issues_model.Label{
- OrgID: ctx.Org.Organization.ID,
- Name: form.Title,
- Exclusive: form.Exclusive,
- Description: form.Description,
- Color: form.Color,
+ OrgID: ctx.Org.Organization.ID,
+ Name: form.Title,
+ Exclusive: form.Exclusive,
+ Description: form.Description,
+ Color: form.Color,
+ ExclusiveOrder: form.ExclusiveOrder,
}
if err := issues_model.NewLabel(ctx, l); err != nil {
ctx.ServerError("NewLabel", err)
return
}
- ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
+ ctx.JSONRedirect(ctx.Org.OrgLink + "/settings/labels")
}
// UpdateLabel update a label's name and color
func UpdateLabel(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.CreateLabelForm)
+ form := shared_label.GetLabelEditForm(ctx)
+ if ctx.Written() {
+ return
+ }
+
l, err := issues_model.GetLabelInOrgByID(ctx, ctx.Org.Organization.ID, form.ID)
- if err != nil {
- switch {
- case issues_model.IsErrOrgLabelNotExist(err):
- ctx.Error(http.StatusNotFound)
- default:
- ctx.ServerError("UpdateLabel", err)
- }
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.JSONErrorNotFound()
+ return
+ } else if err != nil {
+ ctx.ServerError("GetLabelInOrgByID", err)
return
}
l.Name = form.Title
l.Exclusive = form.Exclusive
+ l.ExclusiveOrder = form.ExclusiveOrder
l.Description = form.Description
l.Color = form.Color
l.SetArchived(form.IsArchived)
@@ -80,7 +80,7 @@ func UpdateLabel(ctx *context.Context) {
ctx.ServerError("UpdateLabel", err)
return
}
- ctx.Redirect(ctx.Org.OrgLink + "/settings/labels")
+ ctx.JSONRedirect(ctx.Org.OrgLink + "/settings/labels")
}
// DeleteLabel delete a label
diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go
index 32da1b41d1..059cce8281 100644
--- a/routers/web/org/projects.go
+++ b/routers/web/org/projects.go
@@ -36,24 +36,24 @@ const (
// MustEnableProjects check if projects are enabled in settings
func MustEnableProjects(ctx *context.Context) {
if unit.TypeProjects.UnitGlobalDisabled() {
- ctx.NotFound("EnableProjects", nil)
+ ctx.NotFound(nil)
return
}
}
// Projects renders the home page of projects
func Projects(ctx *context.Context) {
- shared_user.PrepareContextForProfileBigAvatar(ctx)
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
+ return
+ }
ctx.Data["Title"] = ctx.Tr("repo.projects")
sortType := ctx.FormTrim("sort")
isShowClosed := strings.ToLower(ctx.FormTrim("state")) == "closed"
keyword := ctx.FormTrim("q")
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
var projectType project_model.Type
if ctx.ContextUser.IsOrganization() {
@@ -77,6 +77,11 @@ func Projects(ctx *context.Context) {
return
}
+ if err := project_service.LoadIssueNumbersForProjects(ctx, projects, ctx.Doer); err != nil {
+ ctx.ServerError("LoadIssueNumbersForProjects", err)
+ return
+ }
+
opTotal, err := db.Count[project_model.Project](ctx, project_model.SearchOptions{
OwnerID: ctx.ContextUser.ID,
IsClosed: optional.Some(!isShowClosed),
@@ -96,7 +101,6 @@ func Projects(ctx *context.Context) {
}
ctx.Data["Projects"] = projects
- shared_user.RenderUserHeader(ctx)
if isShowClosed {
ctx.Data["State"] = "closed"
@@ -108,12 +112,6 @@ func Projects(ctx *context.Context) {
project.RenderedContent = renderUtils.MarkdownToHtml(project.Description)
}
- err = shared_user.LoadHeaderCount(ctx)
- if err != nil {
- ctx.ServerError("LoadHeaderCount", err)
- return
- }
-
numPages := 0
if total > 0 {
numPages = (int(total) - 1/setting.UI.IssuePagingNum)
@@ -147,11 +145,8 @@ func RenderNewProject(ctx *context.Context) {
ctx.Data["PageIsViewProjects"] = true
ctx.Data["HomeLink"] = ctx.ContextUser.HomeLink()
ctx.Data["CancelLink"] = ctx.ContextUser.HomeLink() + "/-/projects"
- shared_user.RenderUserHeader(ctx)
-
- err := shared_user.LoadHeaderCount(ctx)
- if err != nil {
- ctx.ServerError("LoadHeaderCount", err)
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
return
}
@@ -162,7 +157,10 @@ func RenderNewProject(ctx *context.Context) {
func NewProjectPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateProjectForm)
ctx.Data["Title"] = ctx.Tr("repo.projects.new")
- shared_user.RenderUserHeader(ctx)
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
+ return
+ }
if ctx.HasError() {
RenderNewProject(ctx)
@@ -222,7 +220,7 @@ func DeleteProject(ctx *context.Context) {
return
}
if p.OwnerID != ctx.ContextUser.ID {
- ctx.NotFound("", nil)
+ ctx.NotFound(nil)
return
}
@@ -243,7 +241,10 @@ func RenderEditProject(ctx *context.Context) {
ctx.Data["CanWriteProjects"] = canWriteProjects(ctx)
ctx.Data["CardTypes"] = project_model.GetCardConfig()
- shared_user.RenderUserHeader(ctx)
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
+ return
+ }
p, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
@@ -251,7 +252,7 @@ func RenderEditProject(ctx *context.Context) {
return
}
if p.OwnerID != ctx.ContextUser.ID {
- ctx.NotFound("", nil)
+ ctx.NotFound(nil)
return
}
@@ -277,11 +278,8 @@ func EditProjectPost(ctx *context.Context) {
ctx.Data["CardTypes"] = project_model.GetCardConfig()
ctx.Data["CancelLink"] = project_model.ProjectLinkForOrg(ctx.ContextUser, projectID)
- shared_user.RenderUserHeader(ctx)
-
- err := shared_user.LoadHeaderCount(ctx)
- if err != nil {
- ctx.ServerError("LoadHeaderCount", err)
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
return
}
@@ -296,7 +294,7 @@ func EditProjectPost(ctx *context.Context) {
return
}
if p.OwnerID != ctx.ContextUser.ID {
- ctx.NotFound("", nil)
+ ctx.NotFound(nil)
return
}
@@ -324,7 +322,11 @@ func ViewProject(ctx *context.Context) {
return
}
if project.OwnerID != ctx.ContextUser.ID {
- ctx.NotFound("", nil)
+ ctx.NotFound(nil)
+ return
+ }
+ if err := project.LoadOwner(ctx); err != nil {
+ ctx.ServerError("LoadOwner", err)
return
}
@@ -334,20 +336,27 @@ func ViewProject(ctx *context.Context) {
return
}
- labelIDs := issue.PrepareFilterIssueLabels(ctx, project.RepoID, project.Owner)
+ preparedLabelFilter := issue.PrepareFilterIssueLabels(ctx, project.RepoID, project.Owner)
if ctx.Written() {
return
}
- assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future
+ assigneeID := ctx.FormString("assignee")
- issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns, &issues_model.IssuesOptions{
- LabelIDs: labelIDs,
- AssigneeID: optional.Some(assigneeID),
- })
+ opts := issues_model.IssuesOptions{
+ LabelIDs: preparedLabelFilter.SelectedLabelIDs,
+ AssigneeID: assigneeID,
+ Owner: project.Owner,
+ Doer: ctx.Doer,
+ }
+
+ issuesMap, err := project_service.LoadIssuesFromProject(ctx, project, &opts)
if err != nil {
ctx.ServerError("LoadIssuesOfColumns", err)
return
}
+ for _, column := range columns {
+ column.NumIssues = int64(len(issuesMap[column.ID]))
+ }
if project.CardType != project_model.CardTypeTextOnly {
issuesAttachmentMap := make(map[int64][]*attachment_model.Attachment)
@@ -390,8 +399,8 @@ func ViewProject(ctx *context.Context) {
}
// Get the exclusive scope for every label ID
- labelExclusiveScopes := make([]string, 0, len(labelIDs))
- for _, labelID := range labelIDs {
+ labelExclusiveScopes := make([]string, 0, len(preparedLabelFilter.SelectedLabelIDs))
+ for _, labelID := range preparedLabelFilter.SelectedLabelIDs {
foundExclusiveScope := false
for _, label := range labels {
if label.ID == labelID || label.ID == -labelID {
@@ -406,7 +415,7 @@ func ViewProject(ctx *context.Context) {
}
for _, l := range labels {
- l.LoadSelectedLabelsAfterClick(labelIDs, labelExclusiveScopes)
+ l.LoadSelectedLabelsAfterClick(preparedLabelFilter.SelectedLabelIDs, labelExclusiveScopes)
}
ctx.Data["Labels"] = labels
ctx.Data["NumLabels"] = len(labels)
@@ -427,11 +436,9 @@ func ViewProject(ctx *context.Context) {
ctx.Data["Project"] = project
ctx.Data["IssuesMap"] = issuesMap
ctx.Data["Columns"] = columns
- shared_user.RenderUserHeader(ctx)
- err = shared_user.LoadHeaderCount(ctx)
- if err != nil {
- ctx.ServerError("LoadHeaderCount", err)
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
return
}
@@ -593,7 +600,7 @@ func MoveIssues(ctx *context.Context) {
return
}
if project.OwnerID != ctx.ContextUser.ID {
- ctx.NotFound("InvalidRepoID", nil)
+ ctx.NotFound(nil)
return
}
@@ -604,7 +611,7 @@ func MoveIssues(ctx *context.Context) {
}
if column.ProjectID != project.ID {
- ctx.NotFound("ColumnNotInProject", nil)
+ ctx.NotFound(nil)
return
}
diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go
index cb1c4213c9..2bc1e8bc43 100644
--- a/routers/web/org/setting.go
+++ b/routers/web/org/setting.go
@@ -18,6 +18,7 @@ import (
repo_module "code.gitea.io/gitea/modules/repository"
"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"
user_setting "code.gitea.io/gitea/routers/web/user/setting"
@@ -31,8 +32,6 @@ import (
const (
// tplSettingsOptions template path for render settings
tplSettingsOptions templates.TplName = "org/settings/options"
- // tplSettingsDelete template path for render delete repository
- tplSettingsDelete templates.TplName = "org/settings/delete"
// tplSettingsHooks template path for render hook settings
tplSettingsHooks templates.TplName = "org/settings/hooks"
// tplSettingsLabels template path for render labels settings
@@ -48,9 +47,8 @@ func Settings(ctx *context.Context) {
ctx.Data["RepoAdminChangeTeamAccess"] = ctx.Org.Organization.RepoAdminChangeTeamAccess
ctx.Data["ContextUser"] = ctx.ContextUser
- err := shared_user.LoadHeaderCount(ctx)
- if err != nil {
- ctx.ServerError("LoadHeaderCount", err)
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
return
}
@@ -72,26 +70,6 @@ func SettingsPost(ctx *context.Context) {
org := ctx.Org.Organization
- if org.Name != form.Name {
- if err := user_service.RenameUser(ctx, org.AsUser(), form.Name); err != nil {
- if user_model.IsErrUserAlreadyExist(err) {
- ctx.Data["Err_Name"] = true
- ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplSettingsOptions, &form)
- } else if db.IsErrNameReserved(err) {
- ctx.Data["Err_Name"] = true
- ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplSettingsOptions, &form)
- } else if db.IsErrNamePatternNotAllowed(err) {
- ctx.Data["Err_Name"] = true
- ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form)
- } else {
- ctx.ServerError("RenameUser", err)
- }
- return
- }
-
- ctx.Org.OrgLink = setting.AppSubURL + "/org/" + url.PathEscape(org.Name)
- }
-
if form.Email != "" {
if err := user_service.ReplacePrimaryEmailAddress(ctx, org.AsUser(), form.Email); err != nil {
ctx.Data["Err_Email"] = true
@@ -121,7 +99,7 @@ func SettingsPost(ctx *context.Context) {
// update forks visibility
if visibilityChanged {
- repos, _, err := repo_model.GetUserRepositories(ctx, &repo_model.SearchRepoOptions{
+ repos, _, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{
Actor: org.AsUser(), Private: true, ListOptions: db.ListOptions{Page: 1, PageSize: org.NumRepos},
})
if err != nil {
@@ -164,43 +142,27 @@ func SettingsDeleteAvatar(ctx *context.Context) {
ctx.JSONRedirect(ctx.Org.OrgLink + "/settings")
}
-// SettingsDelete response for deleting an organization
-func SettingsDelete(ctx *context.Context) {
- ctx.Data["Title"] = ctx.Tr("org.settings")
- ctx.Data["PageIsOrgSettings"] = true
- ctx.Data["PageIsSettingsDelete"] = true
-
- if ctx.Req.Method == "POST" {
- if ctx.Org.Organization.Name != ctx.FormString("org_name") {
- ctx.Data["Err_OrgName"] = true
- ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_org_name"), tplSettingsDelete, nil)
- return
- }
-
- if err := org_service.DeleteOrganization(ctx, ctx.Org.Organization, false); err != nil {
- if repo_model.IsErrUserOwnRepos(err) {
- ctx.Flash.Error(ctx.Tr("form.org_still_own_repo"))
- ctx.Redirect(ctx.Org.OrgLink + "/settings/delete")
- } else if packages_model.IsErrUserOwnPackages(err) {
- ctx.Flash.Error(ctx.Tr("form.org_still_own_packages"))
- ctx.Redirect(ctx.Org.OrgLink + "/settings/delete")
- } else {
- ctx.ServerError("DeleteOrganization", err)
- }
- } else {
- log.Trace("Organization deleted: %s", ctx.Org.Organization.Name)
- ctx.Redirect(setting.AppSubURL + "/")
- }
+// SettingsDeleteOrgPost response for deleting an organization
+func SettingsDeleteOrgPost(ctx *context.Context) {
+ if ctx.Org.Organization.Name != ctx.FormString("org_name") {
+ ctx.JSONError(ctx.Tr("form.enterred_invalid_org_name"))
return
}
- err := shared_user.LoadHeaderCount(ctx)
- if err != nil {
- ctx.ServerError("LoadHeaderCount", err)
+ if err := org_service.DeleteOrganization(ctx, ctx.Org.Organization, false /* no purge */); err != nil {
+ if repo_model.IsErrUserOwnRepos(err) {
+ ctx.JSONError(ctx.Tr("form.org_still_own_repo"))
+ } else if packages_model.IsErrUserOwnPackages(err) {
+ ctx.JSONError(ctx.Tr("form.org_still_own_packages"))
+ } else {
+ log.Error("DeleteOrganization: %v", err)
+ ctx.JSONError(util.Iif(ctx.Doer.IsAdmin, err.Error(), string(ctx.Tr("org.settings.delete_failed"))))
+ }
return
}
- ctx.HTML(http.StatusOK, tplSettingsDelete)
+ ctx.Flash.Success(ctx.Tr("org.settings.delete_successful", ctx.Org.Organization.Name))
+ ctx.JSONRedirect(setting.AppSubURL + "/")
}
// Webhooks render webhook list page
@@ -218,9 +180,8 @@ func Webhooks(ctx *context.Context) {
return
}
- err = shared_user.LoadHeaderCount(ctx)
- if err != nil {
- ctx.ServerError("LoadHeaderCount", err)
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
return
}
@@ -246,11 +207,47 @@ func Labels(ctx *context.Context) {
ctx.Data["PageIsOrgSettingsLabels"] = true
ctx.Data["LabelTemplateFiles"] = repo_module.LabelTemplateFiles
- err := shared_user.LoadHeaderCount(ctx)
- if err != nil {
- ctx.ServerError("LoadHeaderCount", err)
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
return
}
ctx.HTML(http.StatusOK, tplSettingsLabels)
}
+
+// SettingsRenamePost response for renaming organization
+func SettingsRenamePost(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RenameOrgForm)
+ if ctx.HasError() {
+ ctx.JSONError(ctx.GetErrMsg())
+ return
+ }
+
+ oldOrgName, newOrgName := ctx.Org.Organization.Name, form.NewOrgName
+
+ if form.OrgName != oldOrgName {
+ ctx.JSONError(ctx.Tr("form.enterred_invalid_org_name"))
+ return
+ }
+ if newOrgName == oldOrgName {
+ ctx.JSONError(ctx.Tr("org.settings.rename_no_change"))
+ return
+ }
+
+ if err := user_service.RenameUser(ctx, ctx.Org.Organization.AsUser(), newOrgName); err != nil {
+ if user_model.IsErrUserAlreadyExist(err) {
+ ctx.JSONError(ctx.Tr("org.form.name_been_taken", newOrgName))
+ } else if db.IsErrNameReserved(err) {
+ ctx.JSONError(ctx.Tr("org.form.name_reserved", newOrgName))
+ } else if db.IsErrNamePatternNotAllowed(err) {
+ ctx.JSONError(ctx.Tr("org.form.name_pattern_not_allowed", newOrgName))
+ } else {
+ log.Error("RenameOrganization: %v", err)
+ ctx.JSONError(util.Iif(ctx.Doer.IsAdmin, err.Error(), string(ctx.Tr("org.settings.rename_failed"))))
+ }
+ return
+ }
+
+ ctx.Flash.Success(ctx.Tr("org.settings.rename_success", oldOrgName, newOrgName))
+ ctx.JSONRedirect(setting.AppSubURL + "/org/" + url.PathEscape(newOrgName) + "/settings")
+}
diff --git a/routers/web/org/setting_oauth2.go b/routers/web/org/setting_oauth2.go
index c93058477e..47f653bf88 100644
--- a/routers/web/org/setting_oauth2.go
+++ b/routers/web/org/setting_oauth2.go
@@ -45,9 +45,8 @@ func Applications(ctx *context.Context) {
}
ctx.Data["Applications"] = apps
- err = shared_user.LoadHeaderCount(ctx)
- if err != nil {
- ctx.ServerError("LoadHeaderCount", err)
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
return
}
diff --git a/routers/web/org/setting_packages.go b/routers/web/org/setting_packages.go
index 0912a9e0fd..ec80e2867c 100644
--- a/routers/web/org/setting_packages.go
+++ b/routers/web/org/setting_packages.go
@@ -25,9 +25,8 @@ func Packages(ctx *context.Context) {
ctx.Data["PageIsOrgSettings"] = true
ctx.Data["PageIsSettingsPackages"] = true
- err := shared_user.LoadHeaderCount(ctx)
- if err != nil {
- ctx.ServerError("LoadHeaderCount", err)
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
return
}
@@ -41,9 +40,8 @@ func PackagesRuleAdd(ctx *context.Context) {
ctx.Data["PageIsOrgSettings"] = true
ctx.Data["PageIsSettingsPackages"] = true
- err := shared_user.LoadHeaderCount(ctx)
- if err != nil {
- ctx.ServerError("LoadHeaderCount", err)
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
return
}
@@ -57,9 +55,8 @@ func PackagesRuleEdit(ctx *context.Context) {
ctx.Data["PageIsOrgSettings"] = true
ctx.Data["PageIsSettingsPackages"] = true
- err := shared_user.LoadHeaderCount(ctx)
- if err != nil {
- ctx.ServerError("LoadHeaderCount", err)
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
return
}
@@ -99,9 +96,8 @@ func PackagesRulePreview(ctx *context.Context) {
ctx.Data["PageIsOrgSettings"] = true
ctx.Data["PageIsSettingsPackages"] = true
- err := shared_user.LoadHeaderCount(ctx)
- if err != nil {
- ctx.ServerError("LoadHeaderCount", err)
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
return
}
diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go
index 26031029d6..0ec7cfddc5 100644
--- a/routers/web/org/teams.go
+++ b/routers/web/org/teams.go
@@ -46,6 +46,10 @@ const (
// Teams render teams list page
func Teams(ctx *context.Context) {
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
+ return
+ }
org := ctx.Org.Organization
ctx.Data["Title"] = org.FullName
ctx.Data["PageIsOrgTeams"] = true
@@ -58,12 +62,6 @@ func Teams(ctx *context.Context) {
}
ctx.Data["Teams"] = ctx.Org.Teams
- _, err := shared_user.PrepareOrgHeader(ctx)
- if err != nil {
- ctx.ServerError("PrepareOrgHeader", err)
- return
- }
-
ctx.HTML(http.StatusOK, tplTeams)
}
@@ -74,7 +72,7 @@ func TeamsAction(ctx *context.Context) {
switch ctx.PathParam("action") {
case "join":
if !ctx.Org.IsOwner {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
err = org_service.AddTeamMember(ctx, ctx.Org.Team, ctx.Doer)
@@ -96,7 +94,7 @@ func TeamsAction(ctx *context.Context) {
return
case "remove":
if !ctx.Org.IsOwner {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
@@ -123,7 +121,7 @@ func TeamsAction(ctx *context.Context) {
return
case "add":
if !ctx.Org.IsOwner {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
uname := strings.ToLower(ctx.FormString("uname"))
@@ -167,7 +165,7 @@ func TeamsAction(ctx *context.Context) {
page = "team"
case "remove_invite":
if !ctx.Org.IsOwner {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
@@ -228,7 +226,7 @@ func checkIsOrgMemberAndRedirect(ctx *context.Context, defaultRedirect string) {
// TeamsRepoAction operate team's repository
func TeamsRepoAction(ctx *context.Context) {
if !ctx.Org.IsOwner {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
@@ -272,22 +270,35 @@ func TeamsRepoAction(ctx *context.Context) {
// NewTeam render create new team page
func NewTeam(ctx *context.Context) {
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
+ return
+ }
ctx.Data["Title"] = ctx.Org.Organization.FullName
ctx.Data["PageIsOrgTeams"] = true
ctx.Data["PageIsOrgTeamsNew"] = true
ctx.Data["Team"] = &org_model.Team{}
ctx.Data["Units"] = unit_model.Units
- if err := shared_user.LoadHeaderCount(ctx); err != nil {
- ctx.ServerError("LoadHeaderCount", err)
- return
- }
ctx.HTML(http.StatusOK, tplTeamNew)
}
+// FIXME: TEAM-UNIT-PERMISSION: this design is not right, when a new unit is added in the future,
+// The existing teams won't inherit the correct admin permission for the new unit.
+// The full history is like this:
+// 1. There was only "team", no "team unit", so "team.authorize" was used to determine the team permission.
+// 2. Later, "team unit" was introduced, then the usage of "team.authorize" became inconsistent, and causes various bugs.
+// - Sometimes, "team.authorize" is used to determine the team permission, e.g. admin, owner
+// - Sometimes, "team unit" is used not really used and "team unit" is used.
+// - Some functions like `GetTeamsWithAccessToAnyRepoUnit` use both.
+//
+// 3. After introducing "team unit" and more unclear changes, it becomes difficult to maintain team permissions.
+// - Org owner need to click the permission for each unit, but can't just set a common "write" permission for all units.
+//
+// Ideally, "team.authorize=write" should mean the team has write access to all units including newly (future) added ones.
func getUnitPerms(forms url.Values, teamPermission perm.AccessMode) map[unit_model.Type]perm.AccessMode {
unitPerms := make(map[unit_model.Type]perm.AccessMode)
for _, ut := range unit_model.AllRepoUnitTypes {
- // Default accessmode is none
+ // Default access mode is none
unitPerms[ut] = perm.AccessModeNone
v, ok := forms[fmt.Sprintf("unit_%d", ut)]
@@ -314,19 +325,14 @@ func getUnitPerms(forms url.Values, teamPermission perm.AccessMode) map[unit_mod
func NewTeamPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateTeamForm)
includesAllRepositories := form.RepoAccess == "all"
- p := perm.ParseAccessMode(form.Permission)
- unitPerms := getUnitPerms(ctx.Req.Form, p)
- if p < perm.AccessModeAdmin {
- // if p is less than admin accessmode, then it should be general accessmode,
- // so we should calculate the minial accessmode from units accessmodes.
- p = unit_model.MinUnitAccessMode(unitPerms)
- }
+ teamPermission := perm.ParseAccessMode(form.Permission, perm.AccessModeNone, perm.AccessModeAdmin)
+ unitPerms := getUnitPerms(ctx.Req.Form, teamPermission)
t := &org_model.Team{
OrgID: ctx.Org.Organization.ID,
Name: form.TeamName,
Description: form.Description,
- AccessMode: p,
+ AccessMode: teamPermission,
IncludesAllRepositories: includesAllRepositories,
CanCreateOrgRepo: form.CanCreateOrgRepo,
}
@@ -373,15 +379,15 @@ func NewTeamPost(ctx *context.Context) {
// TeamMembers render team members page
func TeamMembers(ctx *context.Context) {
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
+ return
+ }
+
ctx.Data["Title"] = ctx.Org.Team.Name
ctx.Data["PageIsOrgTeams"] = true
ctx.Data["PageIsOrgTeamMembers"] = true
- if err := shared_user.LoadHeaderCount(ctx); err != nil {
- ctx.ServerError("LoadHeaderCount", err)
- return
- }
-
if err := ctx.Org.Team.LoadMembers(ctx); err != nil {
ctx.ServerError("GetMembers", err)
return
@@ -401,15 +407,15 @@ func TeamMembers(ctx *context.Context) {
// TeamRepositories show the repositories of team
func TeamRepositories(ctx *context.Context) {
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
+ return
+ }
+
ctx.Data["Title"] = ctx.Org.Team.Name
ctx.Data["PageIsOrgTeams"] = true
ctx.Data["PageIsOrgTeamRepos"] = true
- if err := shared_user.LoadHeaderCount(ctx); err != nil {
- ctx.ServerError("LoadHeaderCount", err)
- return
- }
-
repos, err := repo_model.GetTeamRepositories(ctx, &repo_model.SearchTeamRepoOptions{
TeamID: ctx.Org.Team.ID,
})
@@ -466,16 +472,16 @@ func SearchTeam(ctx *context.Context) {
// EditTeam render team edit page
func EditTeam(ctx *context.Context) {
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
+ return
+ }
ctx.Data["Title"] = ctx.Org.Organization.FullName
ctx.Data["PageIsOrgTeams"] = true
if err := ctx.Org.Team.LoadUnits(ctx); err != nil {
ctx.ServerError("LoadUnits", err)
return
}
- if err := shared_user.LoadHeaderCount(ctx); err != nil {
- ctx.ServerError("LoadHeaderCount", err)
- return
- }
ctx.Data["Team"] = ctx.Org.Team
ctx.Data["Units"] = unit_model.Units
ctx.HTML(http.StatusOK, tplTeamNew)
@@ -485,13 +491,8 @@ func EditTeam(ctx *context.Context) {
func EditTeamPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateTeamForm)
t := ctx.Org.Team
- newAccessMode := perm.ParseAccessMode(form.Permission)
- unitPerms := getUnitPerms(ctx.Req.Form, newAccessMode)
- if newAccessMode < perm.AccessModeAdmin {
- // if newAccessMode is less than admin accessmode, then it should be general accessmode,
- // so we should calculate the minial accessmode from units accessmodes.
- newAccessMode = unit_model.MinUnitAccessMode(unitPerms)
- }
+ teamPermission := perm.ParseAccessMode(form.Permission, perm.AccessModeNone, perm.AccessModeAdmin)
+ unitPerms := getUnitPerms(ctx.Req.Form, teamPermission)
isAuthChanged := false
isIncludeAllChanged := false
includesAllRepositories := form.RepoAccess == "all"
@@ -503,9 +504,9 @@ func EditTeamPost(ctx *context.Context) {
if !t.IsOwnerTeam() {
t.Name = form.TeamName
- if t.AccessMode != newAccessMode {
+ if t.AccessMode != teamPermission {
isAuthChanged = true
- t.AccessMode = newAccessMode
+ t.AccessMode = teamPermission
}
if t.IncludesAllRepositories != includesAllRepositories {
@@ -568,7 +569,7 @@ func TeamInvite(ctx *context.Context) {
invite, org, team, inviter, err := getTeamInviteFromContext(ctx)
if err != nil {
if org_model.IsErrTeamInviteNotFound(err) {
- ctx.NotFound("ErrTeamInviteNotFound", err)
+ ctx.NotFound(err)
} else {
ctx.ServerError("getTeamInviteFromContext", err)
}
@@ -589,7 +590,7 @@ func TeamInvitePost(ctx *context.Context) {
invite, org, team, _, err := getTeamInviteFromContext(ctx)
if err != nil {
if org_model.IsErrTeamInviteNotFound(err) {
- ctx.NotFound("ErrTeamInviteNotFound", err)
+ ctx.NotFound(err)
} else {
ctx.ServerError("getTeamInviteFromContext", err)
}
diff --git a/routers/web/org/worktime.go b/routers/web/org/worktime.go
new file mode 100644
index 0000000000..c7b44baf7b
--- /dev/null
+++ b/routers/web/org/worktime.go
@@ -0,0 +1,82 @@
+// 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"
+ shared_user "code.gitea.io/gitea/routers/web/shared/user"
+ "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
+ switch worktimeBy {
+ case "milestones":
+ worktimeSumResult, err = organization.GetWorktimeByMilestones(ctx.Org.Organization, unixFrom, unixTo)
+ ctx.Data["WorktimeByMilestones"] = true
+ case "members":
+ worktimeSumResult, err = organization.GetWorktimeByMembers(ctx.Org.Organization, unixFrom, unixTo)
+ ctx.Data["WorktimeByMembers"] = true
+ default: /* by repos */
+ worktimeSumResult, err = organization.GetWorktimeByRepos(ctx.Org.Organization, unixFrom, unixTo)
+ ctx.Data["WorktimeByRepos"] = true
+ }
+ if err != nil {
+ ctx.ServerError("GetWorktime", err)
+ return
+ }
+
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
+ return
+ }
+
+ ctx.Data["WorktimeSumResult"] = worktimeSumResult
+ ctx.HTML(http.StatusOK, tplByRepos)
+}
diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go
index 539c4b6ed0..202da407d2 100644
--- a/routers/web/repo/actions/actions.go
+++ b/routers/web/repo/actions/actions.go
@@ -6,6 +6,7 @@ package actions
import (
"bytes"
stdCtx "context"
+ "errors"
"net/http"
"slices"
"strings"
@@ -45,18 +46,18 @@ type Workflow struct {
// MustEnableActions check if actions are enabled in settings
func MustEnableActions(ctx *context.Context) {
if !setting.Actions.Enabled {
- ctx.NotFound("MustEnableActions", nil)
+ ctx.NotFound(nil)
return
}
if unit.TypeActions.UnitGlobalDisabled() {
- ctx.NotFound("MustEnableActions", nil)
+ ctx.NotFound(nil)
return
}
if ctx.Repo.Repository != nil {
if !ctx.Repo.CanRead(unit.TypeActions) {
- ctx.NotFound("MustEnableActions", nil)
+ ctx.NotFound(nil)
return
}
}
@@ -67,7 +68,11 @@ func List(ctx *context.Context) {
ctx.Data["PageIsActions"] = true
commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
- if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.Data["NotFoundPrompt"] = ctx.Tr("repo.branch.default_branch_not_exist", ctx.Repo.Repository.DefaultBranch)
+ ctx.NotFound(nil)
+ return
+ } else if err != nil {
ctx.ServerError("GetBranchCommit", err)
return
}
@@ -88,7 +93,7 @@ func List(ctx *context.Context) {
func WorkflowDispatchInputs(ctx *context.Context) {
ref := ctx.FormString("ref")
if ref == "" {
- ctx.NotFound("WorkflowDispatchInputs: no ref", nil)
+ ctx.NotFound(nil)
return
}
// get target commit of run from specified ref
@@ -121,7 +126,7 @@ func prepareWorkflowDispatchTemplate(ctx *context.Context, commit *git.Commit) (
var curWorkflow *model.Workflow
- entries, err := actions.ListWorkflows(commit)
+ _, entries, err := actions.ListWorkflows(commit)
if err != nil {
ctx.ServerError("ListWorkflows", err)
return nil
@@ -306,12 +311,14 @@ func prepareWorkflowList(ctx *context.Context, workflows []Workflow) {
}
ctx.Data["Actors"] = shared_user.MakeSelfOnTop(ctx.Doer, actors)
- ctx.Data["StatusInfoList"] = actions_model.GetStatusInfoList(ctx)
+ ctx.Data["StatusInfoList"] = actions_model.GetStatusInfoList(ctx, ctx.Locale)
pager := context.NewPagination(int(total), opts.PageSize, opts.Page, 5)
pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager
ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(runs) > 0
+
+ ctx.Data["CanWriteRepoUnitActions"] = ctx.Repo.CanWrite(unit.TypeActions)
}
// loadIsRefDeleted loads the IsRefDeleted field for each run in the list.
@@ -370,10 +377,8 @@ func workflowDispatchConfig(w *model.Workflow) *WorkflowDispatch {
if !decodeNode(w.RawOn, &val) {
return nil
}
- for _, v := range val {
- if v == "workflow_dispatch" {
- return &WorkflowDispatch{}
- }
+ if slices.Contains(val, "workflow_dispatch") {
+ return &WorkflowDispatch{}
}
case yaml.MappingNode:
var val map[string]yaml.Node
diff --git a/routers/web/repo/actions/badge.go b/routers/web/repo/actions/badge.go
index e920ecaf58..d268a8df8a 100644
--- a/routers/web/repo/actions/badge.go
+++ b/routers/web/repo/actions/badge.go
@@ -5,35 +5,38 @@ package actions
import (
"errors"
- "fmt"
"net/http"
"path/filepath"
"strings"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/modules/badge"
+ "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
)
func GetWorkflowBadge(ctx *context.Context) {
workflowFile := ctx.PathParam("workflow_name")
- branch := ctx.Req.URL.Query().Get("branch")
- if branch == "" {
- branch = ctx.Repo.Repository.DefaultBranch
- }
- branchRef := fmt.Sprintf("refs/heads/%s", branch)
- event := ctx.Req.URL.Query().Get("event")
+ branch := ctx.FormString("branch", ctx.Repo.Repository.DefaultBranch)
+ event := ctx.FormString("event")
+ style := ctx.FormString("style")
- badge, err := getWorkflowBadge(ctx, workflowFile, branchRef, event)
+ branchRef := git.RefNameFromBranch(branch)
+ b, err := getWorkflowBadge(ctx, workflowFile, branchRef.String(), event)
if err != nil {
ctx.ServerError("GetWorkflowBadge", err)
return
}
- ctx.Data["Badge"] = badge
+ ctx.Data["Badge"] = b
ctx.RespHeader().Set("Content-Type", "image/svg+xml")
- ctx.HTML(http.StatusOK, "shared/actions/runner_badge")
+ switch style {
+ case badge.StyleFlatSquare:
+ ctx.HTML(http.StatusOK, "shared/actions/runner_badge_flat-square")
+ default: // defaults to badge.StyleFlat
+ ctx.HTML(http.StatusOK, "shared/actions/runner_badge_flat")
+ }
}
func getWorkflowBadge(ctx *context.Context, workflowFile, branchName, event string) (badge.Badge, error) {
@@ -48,7 +51,7 @@ func getWorkflowBadge(ctx *context.Context, workflowFile, branchName, event stri
return badge.Badge{}, err
}
- color, ok := badge.StatusColorMap[run.Status]
+ color, ok := badge.GlobalVars().StatusColorMap[run.Status]
if !ok {
return badge.GenerateBadge(workflowName, "unknown status", badge.DefaultColor), nil
}
diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go
index e5d83960b8..7e1b923fa4 100644
--- a/routers/web/repo/actions/view.go
+++ b/routers/web/repo/actions/view.go
@@ -14,32 +14,27 @@ import (
"net/http"
"net/url"
"strconv"
- "strings"
"time"
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"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/git"
"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"
+ "code.gitea.io/gitea/routers/common"
actions_service "code.gitea.io/gitea/services/actions"
context_module "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/convert"
+ notify_service "code.gitea.io/gitea/services/notify"
- "github.com/nektos/act/pkg/jobparser"
"github.com/nektos/act/pkg/model"
"xorm.io/builder"
)
@@ -69,6 +64,36 @@ func View(ctx *context_module.Context) {
ctx.HTML(http.StatusOK, tplViewActions)
}
+func ViewWorkflowFile(ctx *context_module.Context) {
+ runIndex := getRunIndex(ctx)
+ run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
+ if err != nil {
+ ctx.NotFoundOrServerError("GetRunByIndex", func(err error) bool {
+ return errors.Is(err, util.ErrNotExist)
+ }, err)
+ return
+ }
+ commit, err := ctx.Repo.GitRepo.GetCommit(run.CommitSHA)
+ if err != nil {
+ ctx.NotFoundOrServerError("GetCommit", func(err error) bool {
+ return errors.Is(err, util.ErrNotExist)
+ }, err)
+ return
+ }
+ rpath, entries, err := actions.ListWorkflows(commit)
+ if err != nil {
+ ctx.ServerError("ListWorkflows", err)
+ return
+ }
+ for _, entry := range entries {
+ if entry.Name() == run.WorkflowID {
+ ctx.Redirect(fmt.Sprintf("%s/src/commit/%s/%s/%s", ctx.Repo.RepoLink, url.PathEscape(run.CommitSHA), util.PathEscapeSegments(rpath), util.PathEscapeSegments(run.WorkflowID)))
+ return
+ }
+ }
+ ctx.NotFound(nil)
+}
+
type LogCursor struct {
Step int `json:"step"`
Cursor int64 `json:"cursor"`
@@ -205,13 +230,9 @@ func ViewPost(ctx *context_module.Context) {
}
}
- // TODO: "ComposeMetas" (usually for comment) is not quite right, but it is still the same as what template "RenderCommitMessage" does.
- // need to be refactored together in the future
- metas := ctx.Repo.Repository.ComposeMetas(ctx)
-
// the title for the "run" is from the commit message
resp.State.Run.Title = run.Title
- resp.State.Run.TitleHTML = templates.NewRenderUtils(ctx).RenderCommitMessage(run.Title, metas)
+ resp.State.Run.TitleHTML = templates.NewRenderUtils(ctx).RenderCommitMessage(run.Title, ctx.Repo.Repository)
resp.State.Run.Link = run.Link()
resp.State.Run.CanCancel = !run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
resp.State.Run.CanApprove = run.NeedApproval && ctx.Repo.CanWrite(unit.TypeActions)
@@ -281,84 +302,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.ServerError("convertToViewModel", err)
+ 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)
+}
- 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),
- },
+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)
+
+ 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
@@ -373,7 +408,7 @@ func Rerun(ctx *context_module.Context) {
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("GetRunByIndex", err)
return
}
@@ -391,7 +426,7 @@ func Rerun(ctx *context_module.Context) {
run.Started = 0
run.Stopped = 0
if err := actions_model.UpdateRun(ctx, run, "started", "stopped", "previous_duration"); err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("UpdateRun", err)
return
}
}
@@ -406,7 +441,7 @@ func Rerun(ctx *context_module.Context) {
// if the job has needs, it should be set to "blocked" status to wait for other jobs
shouldBlock := len(j.Needs) > 0
if err := rerunJob(ctx, j, shouldBlock); err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("RerunJob", err)
return
}
}
@@ -420,7 +455,7 @@ func Rerun(ctx *context_module.Context) {
// jobs other than the specified one should be set to "blocked" status
shouldBlock := j.JobID != job.JobID
if err := rerunJob(ctx, j, shouldBlock); err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("RerunJob", err)
return
}
}
@@ -450,6 +485,9 @@ func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shou
}
actions_service.CreateCommitStatus(ctx, job)
+ actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job)
+ notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
+
return nil
}
@@ -457,49 +495,19 @@ func Logs(ctx *context_module.Context) {
runIndex := getRunIndex(ctx)
jobIndex := ctx.PathParamInt64("job")
- job, _ := getRunJobs(ctx, runIndex, jobIndex)
- if ctx.Written() {
- return
- }
- if job.TaskID == 0 {
- ctx.Error(http.StatusNotFound, "job is not started")
- return
- }
-
- err := job.LoadRun(ctx)
- if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
- return
- }
-
- task, err := actions_model.GetTaskByID(ctx, job.TaskID)
- if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
- return
- }
- if task.LogExpired {
- ctx.Error(http.StatusNotFound, "logs have been cleaned up")
- return
- }
-
- reader, err := actions.OpenLogs(ctx, task.LogInStorage, task.LogFilename)
+ run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.NotFoundOrServerError("GetRunByIndex", func(err error) bool {
+ return errors.Is(err, util.ErrNotExist)
+ }, err)
return
}
- defer reader.Close()
- workflowName := job.Run.WorkflowID
- if p := strings.Index(workflowName, "."); p > 0 {
- workflowName = workflowName[0:p]
+ if err = common.DownloadActionsRunJobLogsWithIndex(ctx.Base, ctx.Repo.Repository, run.ID, jobIndex); err != nil {
+ ctx.NotFoundOrServerError("DownloadActionsRunJobLogsWithIndex", func(err error) bool {
+ return errors.Is(err, util.ErrNotExist)
+ }, err)
}
- ctx.ServeContent(reader, &context_module.ServeHeaderOptions{
- Filename: fmt.Sprintf("%v-%v-%v.log", workflowName, job.Name, task.ID),
- ContentLength: &task.LogSize,
- ContentType: "text/plain",
- ContentTypeCharset: "utf-8",
- Disposition: "attachment",
- })
}
func Cancel(ctx *context_module.Context) {
@@ -510,6 +518,8 @@ func Cancel(ctx *context_module.Context) {
return
}
+ var updatedjobs []*actions_model.ActionRunJob
+
if err := db.WithTx(ctx, func(ctx context.Context) error {
for _, job := range jobs {
status := job.Status
@@ -524,7 +534,10 @@ func Cancel(ctx *context_module.Context) {
return err
}
if n == 0 {
- return fmt.Errorf("job has changed, try again")
+ return errors.New("job has changed, try again")
+ }
+ if n > 0 {
+ updatedjobs = append(updatedjobs, job)
}
continue
}
@@ -534,12 +547,21 @@ func Cancel(ctx *context_module.Context) {
}
return nil
}); err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("StopTask", err)
return
}
actions_service.CreateCommitStatus(ctx, jobs...)
+ for _, job := range updatedjobs {
+ _ = job.LoadAttributes(ctx)
+ notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
+ }
+ if len(updatedjobs) > 0 {
+ job := updatedjobs[0]
+ actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job)
+ notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run)
+ }
ctx.JSON(http.StatusOK, struct{}{})
}
@@ -553,6 +575,8 @@ func Approve(ctx *context_module.Context) {
run := current.Run
doer := ctx.Doer
+ var updatedjobs []*actions_model.ActionRunJob
+
if err := db.WithTx(ctx, func(ctx context.Context) error {
run.NeedApproval = false
run.ApprovedBy = doer.ID
@@ -562,23 +586,64 @@ func Approve(ctx *context_module.Context) {
for _, job := range jobs {
if len(job.Needs) == 0 && job.Status.IsBlocked() {
job.Status = actions_model.StatusWaiting
- _, err := actions_model.UpdateRunJob(ctx, job, nil, "status")
+ n, err := actions_model.UpdateRunJob(ctx, job, nil, "status")
if err != nil {
return err
}
+ if n > 0 {
+ updatedjobs = append(updatedjobs, job)
+ }
}
}
return nil
}); err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("UpdateRunJob", err)
return
}
actions_service.CreateCommitStatus(ctx, jobs...)
+ if len(updatedjobs) > 0 {
+ job := updatedjobs[0]
+ actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job)
+ notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run)
+ }
+
+ for _, job := range updatedjobs {
+ _ = job.LoadAttributes(ctx)
+ notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
+ }
+
ctx.JSON(http.StatusOK, struct{}{})
}
+func Delete(ctx *context_module.Context) {
+ runIndex := getRunIndex(ctx)
+ repoID := ctx.Repo.Repository.ID
+
+ run, err := actions_model.GetRunByIndex(ctx, repoID, runIndex)
+ if err != nil {
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.JSONErrorNotFound()
+ return
+ }
+ ctx.ServerError("GetRunByIndex", err)
+ return
+ }
+
+ if !run.Status.IsDone() {
+ ctx.JSONError(ctx.Tr("actions.runs.not_done"))
+ return
+ }
+
+ if err := actions_service.DeleteRun(ctx, run); err != nil {
+ ctx.ServerError("DeleteRun", err)
+ return
+ }
+
+ ctx.JSONOK()
+}
+
// getRunJobs gets the jobs of runIndex, and returns jobs[jobIndex], jobs.
// Any error will be written to the ctx.
// It never returns a nil job of an empty jobs, if the jobIndex is out of range, it will be treated as 0.
@@ -586,20 +651,20 @@ func getRunJobs(ctx *context_module.Context, runIndex, jobIndex int64) (*actions
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
- ctx.Error(http.StatusNotFound, err.Error())
+ ctx.NotFound(nil)
return nil, nil
}
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("GetRunByIndex", err)
return nil, nil
}
run.Repo = ctx.Repo.Repository
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("GetRunJobsByRunID", err)
return nil, nil
}
if len(jobs) == 0 {
- ctx.Error(http.StatusNotFound)
+ ctx.NotFound(nil)
return nil, nil
}
@@ -614,11 +679,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")
@@ -630,7 +690,7 @@ func ArtifactsDeleteView(ctx *context_module.Context) {
return
}
if err = actions_model.SetArtifactNeedDelete(ctx, run.ID, artifactName); err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("SetArtifactNeedDelete", err)
return
}
ctx.JSON(http.StatusOK, struct{}{})
@@ -643,10 +703,10 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
- ctx.Error(http.StatusNotFound, err.Error())
+ ctx.HTTPError(http.StatusNotFound, err.Error())
return
}
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("GetRunByIndex", err)
return
}
@@ -655,41 +715,30 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
ArtifactName: artifactName,
})
if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("FindArtifacts", err)
return
}
if len(artifacts) == 0 {
- ctx.Error(http.StatusNotFound, "artifact not found")
+ ctx.HTTPError(http.StatusNotFound, "artifact not found")
return
}
// if artifacts status is not uploaded-confirmed, treat it as not found
for _, art := range artifacts {
- if art.Status != int64(actions_model.ArtifactStatusUploadConfirmed) {
- ctx.Error(http.StatusNotFound, "artifact not found")
+ if art.Status != actions_model.ArtifactStatusUploadConfirmed {
+ ctx.HTTPError(http.StatusNotFound, "artifact not found")
return
}
}
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(artifactName), artifactName))
- // Artifacts using the v4 backend are stored as a single combined zip file per artifact on the backend
- // The v4 backend enshures ContentEncoding is set to "application/zip", which is not the case for the old backend
- if len(artifacts) == 1 && artifacts[0].ArtifactName+".zip" == artifacts[0].ArtifactPath && artifacts[0].ContentEncoding == "application/zip" {
- art := artifacts[0]
- if setting.Actions.ArtifactStorage.ServeDirect() {
- u, err := storage.ActionsArtifacts.URL(art.StoragePath, art.ArtifactPath, nil)
- if u != nil && err == nil {
- ctx.Redirect(u.String())
- return
- }
- }
- f, err := storage.ActionsArtifacts.Open(art.StoragePath)
+ if len(artifacts) == 1 && actions.IsArtifactV4(artifacts[0]) {
+ err := actions.DownloadArtifactV4(ctx.Base, artifacts[0])
if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("DownloadArtifactV4", err)
return
}
- _, _ = io.Copy(ctx.Resp, f)
return
}
@@ -700,7 +749,7 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
for _, art := range artifacts {
f, err := storage.ActionsArtifacts.Open(art.StoragePath)
if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("ActionsArtifacts.Open", err)
return
}
@@ -708,7 +757,7 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
if art.ContentEncoding == "gzip" {
r, err = gzip.NewReader(f)
if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("gzip.NewReader", err)
return
}
} else {
@@ -718,11 +767,11 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
w, err := writer.Create(art.ArtifactPath)
if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("writer.Create", err)
return
}
if _, err := io.Copy(w, r); err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.ServerError("io.Copy", err)
return
}
}
@@ -783,142 +832,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.ErrorAsLocale(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/activity.go b/routers/web/repo/activity.go
index 1d809ad8e9..8232f0cc04 100644
--- a/routers/web/repo/activity.go
+++ b/routers/web/repo/activity.go
@@ -8,6 +8,7 @@ import (
"time"
activities_model "code.gitea.io/gitea/models/activities"
+ "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/services/context"
@@ -52,12 +53,26 @@ func Activity(ctx *context.Context) {
ctx.Data["DateUntil"] = timeUntil
ctx.Data["PeriodText"] = ctx.Tr("repo.activity.period." + ctx.Data["Period"].(string))
+ canReadCode := ctx.Repo.CanRead(unit.TypeCode)
+ if canReadCode {
+ // GetActivityStats needs to read the default branch to get some information
+ branchExist, _ := git.IsBranchExist(ctx, ctx.Repo.Repository.ID, ctx.Repo.Repository.DefaultBranch)
+ if !branchExist {
+ ctx.Data["NotFoundPrompt"] = ctx.Tr("repo.branch.default_branch_not_exist", ctx.Repo.Repository.DefaultBranch)
+ ctx.NotFound(nil)
+ return
+ }
+ }
+
var err error
- if ctx.Data["Activity"], err = activities_model.GetActivityStats(ctx, ctx.Repo.Repository, timeFrom,
+ // TODO: refactor these arguments to a struct
+ ctx.Data["Activity"], err = activities_model.GetActivityStats(ctx, ctx.Repo.Repository, timeFrom,
ctx.Repo.CanRead(unit.TypeReleases),
ctx.Repo.CanRead(unit.TypeIssues),
ctx.Repo.CanRead(unit.TypePullRequests),
- ctx.Repo.CanRead(unit.TypeCode)); err != nil {
+ canReadCode,
+ )
+ if err != nil {
ctx.ServerError("GetActivityStats", err)
return
}
diff --git a/routers/web/repo/attachment.go b/routers/web/repo/attachment.go
index f8e51521be..9eda926dad 100644
--- a/routers/web/repo/attachment.go
+++ b/routers/web/repo/attachment.go
@@ -34,13 +34,13 @@ func UploadReleaseAttachment(ctx *context.Context) {
// UploadAttachment response for uploading attachments
func uploadAttachment(ctx *context.Context, repoID int64, allowedTypes string) {
if !setting.Attachment.Enabled {
- ctx.Error(http.StatusNotFound, "attachment is not enabled")
+ ctx.HTTPError(http.StatusNotFound, "attachment is not enabled")
return
}
file, header, err := ctx.Req.FormFile("file")
if err != nil {
- ctx.Error(http.StatusInternalServerError, fmt.Sprintf("FormFile: %v", err))
+ ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("FormFile: %v", err))
return
}
defer file.Close()
@@ -52,10 +52,10 @@ func uploadAttachment(ctx *context.Context, repoID int64, allowedTypes string) {
})
if err != nil {
if upload.IsErrFileTypeForbidden(err) {
- ctx.Error(http.StatusBadRequest, err.Error())
+ ctx.HTTPError(http.StatusBadRequest, err.Error())
return
}
- ctx.Error(http.StatusInternalServerError, fmt.Sprintf("NewAttachment: %v", err))
+ ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("NewAttachment: %v", err))
return
}
@@ -70,16 +70,16 @@ func DeleteAttachment(ctx *context.Context) {
file := ctx.FormString("file")
attach, err := repo_model.GetAttachmentByUUID(ctx, file)
if err != nil {
- ctx.Error(http.StatusBadRequest, err.Error())
+ ctx.HTTPError(http.StatusBadRequest, err.Error())
return
}
if !ctx.IsSigned || (ctx.Doer.ID != attach.UploaderID) {
- ctx.Error(http.StatusForbidden)
+ ctx.HTTPError(http.StatusForbidden)
return
}
err = repo_model.DeleteAttachment(ctx, attach, true)
if err != nil {
- ctx.Error(http.StatusInternalServerError, fmt.Sprintf("DeleteAttachment: %v", err))
+ ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("DeleteAttachment: %v", err))
return
}
ctx.JSON(http.StatusOK, map[string]string{
@@ -92,7 +92,7 @@ func ServeAttachment(ctx *context.Context, uuid string) {
attach, err := repo_model.GetAttachmentByUUID(ctx, uuid)
if err != nil {
if repo_model.IsErrAttachmentNotExist(err) {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
} else {
ctx.ServerError("GetAttachmentByUUID", err)
}
@@ -107,17 +107,17 @@ func ServeAttachment(ctx *context.Context, uuid string) {
if repository == nil { // If not linked
if !(ctx.IsSigned && attach.UploaderID == ctx.Doer.ID) { // We block if not the uploader
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
} else { // If we have the repository we check access
perm, err := access_model.GetUserRepoPermission(ctx, repository, ctx.Doer)
if err != nil {
- ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err.Error())
+ ctx.HTTPError(http.StatusInternalServerError, "GetUserRepoPermission", err.Error())
return
}
if !perm.CanRead(unitType) {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
}
diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go
index 1022c5e6d6..e304633f95 100644
--- a/routers/web/repo/blame.go
+++ b/routers/web/repo/blame.go
@@ -8,19 +8,21 @@ import (
gotemplate "html/template"
"net/http"
"net/url"
+ "path"
"strconv"
"strings"
+ repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/git/languagestats"
"code.gitea.io/gitea/modules/highlight"
"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/services/context"
- files_service "code.gitea.io/gitea/services/repository/files"
)
type blameRow struct {
@@ -40,74 +42,58 @@ type blameRow struct {
// RefBlame render blame page
func RefBlame(ctx *context.Context) {
- fileName := ctx.Repo.TreePath
- if len(fileName) == 0 {
- ctx.NotFound("Blame FileName", nil)
- return
- }
-
- branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
- treeLink := branchLink
- rawLink := ctx.Repo.RepoLink + "/raw/" + ctx.Repo.RefTypeNameSubURL()
-
- if len(ctx.Repo.TreePath) > 0 {
- treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
- }
-
- var treeNames []string
- paths := make([]string, 0, 5)
- if len(ctx.Repo.TreePath) > 0 {
- treeNames = strings.Split(ctx.Repo.TreePath, "/")
- for i := range treeNames {
- paths = append(paths, strings.Join(treeNames[:i+1], "/"))
- }
-
- ctx.Data["HasParentPath"] = true
- if len(paths)-2 >= 0 {
- ctx.Data["ParentPath"] = "/" + paths[len(paths)-1]
- }
- }
+ ctx.Data["PageIsViewCode"] = true
+ ctx.Data["IsBlame"] = true
// Get current entry user currently looking at.
+ if ctx.Repo.TreePath == "" {
+ ctx.NotFound(nil)
+ return
+ }
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
if err != nil {
HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err)
return
}
- blob := entry.Blob()
+ treeNames := strings.Split(ctx.Repo.TreePath, "/")
+ var paths []string
+ for i := range treeNames {
+ paths = append(paths, strings.Join(treeNames[:i+1], "/"))
+ }
ctx.Data["Paths"] = paths
- ctx.Data["TreeLink"] = treeLink
ctx.Data["TreeNames"] = treeNames
- ctx.Data["BranchLink"] = branchLink
-
- ctx.Data["RawFileLink"] = rawLink + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
- ctx.Data["PageIsViewCode"] = true
-
- ctx.Data["IsBlame"] = true
+ ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
+ ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
+ blob := entry.Blob()
fileSize := blob.Size()
ctx.Data["FileSize"] = fileSize
- ctx.Data["FileName"] = blob.Name()
+ ctx.Data["FileTreePath"] = ctx.Repo.TreePath
+
+ tplName := tplRepoViewContent
+ if !ctx.FormBool("only_content") {
+ prepareHomeTreeSideBarSwitch(ctx)
+ tplName = tplRepoView
+ }
if fileSize >= setting.UI.MaxDisplayFileSize {
ctx.Data["IsFileTooLarge"] = true
- ctx.HTML(http.StatusOK, tplRepoHome)
+ ctx.HTML(http.StatusOK, tplName)
return
}
- ctx.Data["NumLines"], err = blob.GetBlobLineCount()
+ ctx.Data["NumLines"], err = blob.GetBlobLineCount(nil)
if err != nil {
- ctx.NotFound("GetBlobLineCount", err)
+ ctx.NotFound(err)
return
}
bypassBlameIgnore, _ := strconv.ParseBool(ctx.FormString("bypass-blame-ignore"))
-
- result, err := performBlame(ctx, ctx.Repo.Repository.RepoPath(), ctx.Repo.Commit, fileName, bypassBlameIgnore)
+ result, err := performBlame(ctx, ctx.Repo.Repository, ctx.Repo.Commit, ctx.Repo.TreePath, bypassBlameIgnore)
if err != nil {
- ctx.NotFound("CreateBlameReader", err)
+ ctx.NotFound(err)
return
}
@@ -121,7 +107,7 @@ func RefBlame(ctx *context.Context) {
renderBlame(ctx, result.Parts, commitNames)
- ctx.HTML(http.StatusOK, tplRepoHome)
+ ctx.HTML(http.StatusOK, tplName)
}
type blameResult struct {
@@ -130,10 +116,10 @@ type blameResult struct {
FaultyIgnoreRevsFile bool
}
-func performBlame(ctx *context.Context, repoPath string, commit *git.Commit, file string, bypassBlameIgnore bool) (*blameResult, error) {
+func performBlame(ctx *context.Context, repo *repo_model.Repository, commit *git.Commit, file string, bypassBlameIgnore bool) (*blameResult, error) {
objectFormat := ctx.Repo.GetObjectFormat()
- blameReader, err := git.CreateBlameReader(ctx, objectFormat, repoPath, commit, file, bypassBlameIgnore)
+ blameReader, err := git.CreateBlameReader(ctx, objectFormat, repo.RepoPath(), commit, file, bypassBlameIgnore)
if err != nil {
return nil, err
}
@@ -149,7 +135,7 @@ func performBlame(ctx *context.Context, repoPath string, commit *git.Commit, fil
if len(r.Parts) == 0 && r.UsesIgnoreRevs {
// try again without ignored revs
- blameReader, err = git.CreateBlameReader(ctx, objectFormat, repoPath, commit, file, true)
+ blameReader, err = git.CreateBlameReader(ctx, objectFormat, repo.RepoPath(), commit, file, true)
if err != nil {
return nil, err
}
@@ -221,7 +207,7 @@ func processBlameParts(ctx *context.Context, blameParts []*git.BlamePart) map[st
commit, err = ctx.Repo.GitRepo.GetCommit(sha)
if err != nil {
if git.IsErrNotExist(err) {
- ctx.NotFound("Repo.GitRepo.GetCommit", err)
+ ctx.NotFound(err)
} else {
ctx.ServerError("Repo.GitRepo.GetCommit", err)
}
@@ -234,7 +220,12 @@ func processBlameParts(ctx *context.Context, blameParts []*git.BlamePart) map[st
}
// populate commit email addresses to later look up avatars.
- for _, c := range user_model.ValidateCommitsWithEmails(ctx, commits) {
+ validatedCommits, err := user_model.ValidateCommitsWithEmails(ctx, commits)
+ if err != nil {
+ ctx.ServerError("ValidateCommitsWithEmails", err)
+ return nil
+ }
+ for _, c := range validatedCommits {
commitNames[c.ID.String()] = c
}
@@ -244,7 +235,7 @@ func processBlameParts(ctx *context.Context, blameParts []*git.BlamePart) map[st
func renderBlame(ctx *context.Context, blameParts []*git.BlamePart, commitNames map[string]*user_model.UserCommit) {
repoLink := ctx.Repo.RepoLink
- language, err := files_service.TryGetContentLanguage(ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath)
+ language, err := languagestats.GetFileLanguage(ctx, ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath)
if err != nil {
log.Error("Unable to get file language for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
}
@@ -295,8 +286,7 @@ func renderBlame(ctx *context.Context, blameParts []*git.BlamePart, commitNames
if i != len(lines)-1 {
line += "\n"
}
- fileName := fmt.Sprintf("%v", ctx.Data["FileName"])
- line, lexerNameForLine := highlight.Code(fileName, language, line)
+ line, lexerNameForLine := highlight.Code(path.Base(ctx.Repo.TreePath), language, line)
// set lexer name to the first detected lexer. this is certainly suboptimal and
// we should instead highlight the whole file at once
diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go
index 8747526f72..96d1d87836 100644
--- a/routers/web/repo/branch.go
+++ b/routers/web/repo/branch.go
@@ -45,10 +45,7 @@ func Branches(ctx *context.Context) {
ctx.Data["PageIsViewCode"] = true
ctx.Data["PageIsBranches"] = true
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
pageSize := setting.Git.BranchesRangeSize
kw := ctx.FormString("q")
@@ -93,7 +90,7 @@ func Branches(ctx *context.Context) {
// DeleteBranchPost responses for delete merged branch
func DeleteBranchPost(ctx *context.Context) {
- defer redirect(ctx)
+ defer jsonRedirectBranches(ctx)
branchName := ctx.FormString("name")
if err := repo_service.DeleteBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName, nil); err != nil {
@@ -120,7 +117,7 @@ func DeleteBranchPost(ctx *context.Context) {
// RestoreBranchPost responses for delete merged branch
func RestoreBranchPost(ctx *context.Context) {
- defer redirect(ctx)
+ defer jsonRedirectBranches(ctx)
branchID := ctx.FormInt64("branch_id")
branchName := ctx.FormString("name")
@@ -170,7 +167,7 @@ func RestoreBranchPost(ctx *context.Context) {
ctx.Flash.Success(ctx.Tr("repo.branch.restore_success", deletedBranch.Name))
}
-func redirect(ctx *context.Context) {
+func jsonRedirectBranches(ctx *context.Context) {
ctx.JSONRedirect(ctx.Repo.RepoLink + "/branches?page=" + url.QueryEscape(ctx.FormString("page")))
}
@@ -178,7 +175,7 @@ func redirect(ctx *context.Context) {
func CreateBranch(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.NewBranchForm)
if !ctx.Repo.CanCreateBranch() {
- ctx.NotFound("CreateBranch", nil)
+ ctx.NotFound(nil)
return
}
@@ -261,10 +258,10 @@ func CreateBranch(ctx *context.Context) {
func MergeUpstream(ctx *context.Context) {
branchName := ctx.FormString("branch")
- _, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, branchName)
+ _, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, branchName, false)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
- ctx.JSONError(ctx.Tr("error.not_found"))
+ ctx.JSONErrorNotFound()
return
} else if pull_service.IsErrMergeConflicts(err) {
ctx.JSONError(ctx.Tr("repo.pulls.merge_conflict"))
diff --git a/routers/web/repo/cherry_pick.go b/routers/web/repo/cherry_pick.go
deleted file mode 100644
index 33d941c9d8..0000000000
--- a/routers/web/repo/cherry_pick.go
+++ /dev/null
@@ -1,184 +0,0 @@
-// Copyright 2021 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package repo
-
-import (
- "bytes"
- "errors"
- "strings"
-
- git_model "code.gitea.io/gitea/models/git"
- "code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/templates"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/repository/files"
-)
-
-var tplCherryPick templates.TplName = "repo/editor/cherry_pick"
-
-// CherryPick handles cherrypick GETs
-func CherryPick(ctx *context.Context) {
- ctx.Data["SHA"] = ctx.PathParam("sha")
- cherryPickCommit, err := ctx.Repo.GitRepo.GetCommit(ctx.PathParam("sha"))
- if err != nil {
- if git.IsErrNotExist(err) {
- ctx.NotFound("Missing Commit", err)
- return
- }
- ctx.ServerError("GetCommit", err)
- return
- }
-
- if ctx.FormString("cherry-pick-type") == "revert" {
- ctx.Data["CherryPickType"] = "revert"
- ctx.Data["commit_summary"] = "revert " + ctx.PathParam("sha")
- ctx.Data["commit_message"] = "revert " + cherryPickCommit.Message()
- } else {
- ctx.Data["CherryPickType"] = "cherry-pick"
- splits := strings.SplitN(cherryPickCommit.Message(), "\n", 2)
- ctx.Data["commit_summary"] = splits[0]
- ctx.Data["commit_message"] = splits[1]
- }
-
- canCommit := renderCommitRights(ctx)
- ctx.Data["TreePath"] = ""
-
- if canCommit {
- ctx.Data["commit_choice"] = frmCommitChoiceDirect
- } else {
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- }
- ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
- ctx.Data["last_commit"] = ctx.Repo.CommitID
- ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
-
- ctx.HTML(200, tplCherryPick)
-}
-
-// CherryPickPost handles cherrypick POSTs
-func CherryPickPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.CherryPickForm)
-
- sha := ctx.PathParam("sha")
- ctx.Data["SHA"] = sha
- if form.Revert {
- ctx.Data["CherryPickType"] = "revert"
- } else {
- ctx.Data["CherryPickType"] = "cherry-pick"
- }
-
- canCommit := renderCommitRights(ctx)
- branchName := ctx.Repo.BranchName
- if form.CommitChoice == frmCommitChoiceNewBranch {
- branchName = form.NewBranchName
- }
- ctx.Data["commit_summary"] = form.CommitSummary
- ctx.Data["commit_message"] = form.CommitMessage
- ctx.Data["commit_choice"] = form.CommitChoice
- ctx.Data["new_branch_name"] = form.NewBranchName
- ctx.Data["last_commit"] = ctx.Repo.CommitID
- ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
-
- if ctx.HasError() {
- ctx.HTML(200, tplCherryPick)
- return
- }
-
- // Cannot commit to a an existing branch if user doesn't have rights
- if branchName == ctx.Repo.BranchName && !canCommit {
- ctx.Data["Err_NewBranchName"] = true
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplCherryPick, &form)
- return
- }
-
- message := strings.TrimSpace(form.CommitSummary)
- if message == "" {
- if form.Revert {
- message = ctx.Locale.TrString("repo.commit.revert-header", sha)
- } else {
- message = ctx.Locale.TrString("repo.commit.cherry-pick-header", sha)
- }
- }
-
- form.CommitMessage = strings.TrimSpace(form.CommitMessage)
- if len(form.CommitMessage) > 0 {
- message += "\n\n" + form.CommitMessage
- }
-
- opts := &files.ApplyDiffPatchOptions{
- LastCommitID: form.LastCommit,
- OldBranch: ctx.Repo.BranchName,
- NewBranch: branchName,
- Message: message,
- }
-
- // First lets try the simple plain read-tree -m approach
- opts.Content = sha
- if _, err := files.CherryPick(ctx, ctx.Repo.Repository, ctx.Doer, form.Revert, opts); err != nil {
- if git_model.IsErrBranchAlreadyExists(err) {
- // User has specified a branch that already exists
- branchErr := err.(git_model.ErrBranchAlreadyExists)
- ctx.Data["Err_NewBranchName"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplCherryPick, &form)
- return
- } else if files.IsErrCommitIDDoesNotMatch(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form)
- return
- }
- // Drop through to the apply technique
-
- buf := &bytes.Buffer{}
- if form.Revert {
- if err := git.GetReverseRawDiff(ctx, ctx.Repo.Repository.RepoPath(), sha, buf); err != nil {
- if git.IsErrNotExist(err) {
- ctx.NotFound("GetRawDiff", errors.New("commit "+ctx.PathParam("sha")+" does not exist."))
- return
- }
- ctx.ServerError("GetRawDiff", err)
- return
- }
- } else {
- if err := git.GetRawDiff(ctx.Repo.GitRepo, sha, git.RawDiffType("patch"), buf); err != nil {
- if git.IsErrNotExist(err) {
- ctx.NotFound("GetRawDiff", errors.New("commit "+ctx.PathParam("sha")+" does not exist."))
- return
- }
- ctx.ServerError("GetRawDiff", err)
- return
- }
- }
-
- opts.Content = buf.String()
- ctx.Data["FileContent"] = opts.Content
-
- if _, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil {
- if git_model.IsErrBranchAlreadyExists(err) {
- // User has specified a branch that already exists
- branchErr := err.(git_model.ErrBranchAlreadyExists)
- ctx.Data["Err_NewBranchName"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplCherryPick, &form)
- return
- } else if files.IsErrCommitIDDoesNotMatch(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form)
- return
- }
- ctx.RenderWithErr(ctx.Tr("repo.editor.fail_to_apply_patch", err), tplPatchFile, &form)
- return
- }
- }
-
- if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) {
- ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName))
- } else {
- ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName))
- }
-}
diff --git a/routers/web/repo/code_frequency.go b/routers/web/repo/code_frequency.go
index 6572adce74..2b2dd5744a 100644
--- a/routers/web/repo/code_frequency.go
+++ b/routers/web/repo/code_frequency.go
@@ -29,12 +29,12 @@ func CodeFrequency(ctx *context.Context) {
// CodeFrequencyData returns JSON of code frequency data
func CodeFrequencyData(ctx *context.Context) {
- if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
+ if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch); err != nil {
if errors.Is(err, contributors_service.ErrAwaitGeneration) {
ctx.Status(http.StatusAccepted)
return
}
- ctx.ServerError("GetCodeFrequencyData", err)
+ ctx.ServerError("GetContributorStats", err)
} else {
ctx.JSON(http.StatusOK, contributorStats["total"].Weeks)
}
diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go
index 8ffda8ae0a..b3af138461 100644
--- a/routers/web/repo/commit.go
+++ b/routers/web/repo/commit.go
@@ -21,17 +21,20 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset"
+ "code.gitea.io/gitea/modules/fileicon"
"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"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/util"
+ asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/context"
+ git_service "code.gitea.io/gitea/services/git"
"code.gitea.io/gitea/services/gitdiff"
repo_service "code.gitea.io/gitea/services/repository"
+ "code.gitea.io/gitea/services/repository/gitgraph"
)
const (
@@ -57,17 +60,14 @@ func RefCommits(ctx *context.Context) {
func Commits(ctx *context.Context) {
ctx.Data["PageIsCommits"] = true
if ctx.Repo.Commit == nil {
- ctx.NotFound("Commit not found", nil)
+ ctx.NotFound(nil)
return
}
ctx.Data["PageIsViewCode"] = true
commitsCount := ctx.Repo.CommitsCount
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
pageSize := ctx.FormInt("limit")
if pageSize <= 0 {
@@ -75,12 +75,16 @@ func Commits(ctx *context.Context) {
}
// Both `git log branchName` and `git log commitId` work.
- commits, err := ctx.Repo.Commit.CommitsByRange(page, pageSize, "")
+ commits, err := ctx.Repo.Commit.CommitsByRange(page, pageSize, "", "", "")
if err != nil {
ctx.ServerError("CommitsByRange", err)
return
}
- ctx.Data["Commits"] = processGitCommits(ctx, commits)
+ ctx.Data["Commits"], err = processGitCommits(ctx, commits)
+ if err != nil {
+ ctx.ServerError("processGitCommits", err)
+ return
+ }
commitIDs := make([]string, 0, len(commits))
for _, c := range commits {
commitIDs = append(commitIDs, c.ID.String())
@@ -192,7 +196,11 @@ func SearchCommits(ctx *context.Context) {
return
}
ctx.Data["CommitCount"] = len(commits)
- ctx.Data["Commits"] = processGitCommits(ctx, commits)
+ ctx.Data["Commits"], err = processGitCommits(ctx, commits)
+ if err != nil {
+ ctx.ServerError("processGitCommits", err)
+ return
+ }
ctx.Data["Keyword"] = query
if all {
@@ -205,41 +213,41 @@ func SearchCommits(ctx *context.Context) {
// FileHistory show a file's reversions
func FileHistory(ctx *context.Context) {
- fileName := ctx.Repo.TreePath
- if len(fileName) == 0 {
+ if ctx.Repo.TreePath == "" {
Commits(ctx)
return
}
- commitsCount, err := ctx.Repo.GitRepo.FileCommitsCount(ctx.Repo.RefFullName.ShortName(), fileName) // FIXME: legacy code used ShortName
+ commitsCount, err := ctx.Repo.GitRepo.FileCommitsCount(ctx.Repo.RefFullName.ShortName(), ctx.Repo.TreePath)
if err != nil {
ctx.ServerError("FileCommitsCount", err)
return
} else if commitsCount == 0 {
- ctx.NotFound("FileCommitsCount", nil)
+ ctx.NotFound(nil)
return
}
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
commits, err := ctx.Repo.GitRepo.CommitsByFileAndRange(
git.CommitsByFileAndRangeOptions{
Revision: ctx.Repo.RefFullName.ShortName(), // FIXME: legacy code used ShortName
- File: fileName,
+ File: ctx.Repo.TreePath,
Page: page,
})
if err != nil {
ctx.ServerError("CommitsByFileAndRange", err)
return
}
- ctx.Data["Commits"] = processGitCommits(ctx, commits)
+ ctx.Data["Commits"], err = processGitCommits(ctx, commits)
+ if err != nil {
+ ctx.ServerError("processGitCommits", err)
+ return
+ }
ctx.Data["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
- ctx.Data["FileName"] = fileName
+ ctx.Data["FileTreePath"] = ctx.Repo.TreePath
ctx.Data["CommitCount"] = commitsCount
pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5)
@@ -270,7 +278,7 @@ func Diff(ctx *context.Context) {
)
if ctx.Data["PageIsWiki"] != nil {
- gitRepo, err = gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository)
+ gitRepo, err = gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo())
if err != nil {
ctx.ServerError("Repo.GitRepo.GetCommit", err)
return
@@ -283,7 +291,7 @@ func Diff(ctx *context.Context) {
commit, err := gitRepo.GetCommit(commitID)
if err != nil {
if git.IsErrNotExist(err) {
- ctx.NotFound("Repo.GitRepo.GetCommit", err)
+ ctx.NotFound(err)
} else {
ctx.ServerError("Repo.GitRepo.GetCommit", err)
}
@@ -300,25 +308,30 @@ func Diff(ctx *context.Context) {
maxLines, maxFiles = -1, -1
}
- diff, err := gitdiff.GetDiff(ctx, gitRepo, &gitdiff.DiffOptions{
+ diff, err := gitdiff.GetDiffForRender(ctx, gitRepo, &gitdiff.DiffOptions{
AfterCommitID: commitID,
SkipTo: ctx.FormString("skip-to"),
MaxLines: maxLines,
MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters,
MaxFiles: maxFiles,
WhitespaceBehavior: gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)),
- FileOnly: fileOnly,
}, files...)
if err != nil {
- ctx.NotFound("GetDiff", err)
+ ctx.NotFound(err)
+ return
+ }
+ diffShortStat, err := gitdiff.GetDiffShortStat(gitRepo, "", commitID)
+ if err != nil {
+ ctx.ServerError("GetDiffShortStat", err)
return
}
+ ctx.Data["DiffShortStat"] = diffShortStat
parents := make([]string, commit.ParentCount())
for i := 0; i < commit.ParentCount(); i++ {
sha, err := commit.ParentID(i)
if err != nil {
- ctx.NotFound("repo.Diff", err)
+ ctx.NotFound(err)
return
}
parents[i] = sha.String()
@@ -330,19 +343,35 @@ func Diff(ctx *context.Context) {
ctx.Data["Reponame"] = repoName
var parentCommit *git.Commit
+ var parentCommitID string
if commit.ParentCount() > 0 {
parentCommit, err = gitRepo.GetCommit(parents[0])
if err != nil {
- ctx.NotFound("GetParentCommit", err)
+ ctx.NotFound(err)
return
}
+ parentCommitID = parentCommit.ID.String()
}
setCompareContext(ctx, parentCommit, commit, userName, repoName)
ctx.Data["Title"] = commit.Summary() + " · " + base.ShortSha(commitID)
ctx.Data["Commit"] = commit
ctx.Data["Diff"] = diff
- statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptionsAll)
+ if !fileOnly {
+ diffTree, err := gitdiff.GetDiffTree(ctx, gitRepo, false, parentCommitID, commitID)
+ if err != nil {
+ ctx.ServerError("GetDiffTree", err)
+ return
+ }
+
+ renderedIconPool := fileicon.NewRenderedIconPool()
+ ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(renderedIconPool, diffTree, nil)
+ ctx.PageData["FolderIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder())
+ ctx.PageData["FolderOpenIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolderOpen())
+ ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
+ }
+
+ statuses, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptionsAll)
if err != nil {
log.Error("GetLatestCommitStatus: %v", err)
}
@@ -353,11 +382,11 @@ func Diff(ctx *context.Context) {
ctx.Data["CommitStatus"] = git_model.CalcCommitStatus(statuses)
ctx.Data["CommitStatuses"] = statuses
- verification := asymkey_model.ParseCommitWithSignature(ctx, commit)
+ verification := asymkey_service.ParseCommitWithSignature(ctx, commit)
ctx.Data["Verification"] = verification
ctx.Data["Author"] = user_model.ValidateCommitWithEmail(ctx, commit)
ctx.Data["Parents"] = parents
- ctx.Data["DiffNotAvailable"] = diff.NumFiles == 0
+ ctx.Data["DiffNotAvailable"] = diffShortStat.NumFiles == 0
if err := asymkey_model.CalculateTrustStatus(verification, ctx.Repo.Repository.GetTrustModel(), func(user *user_model.User) (bool, error) {
return repo_model.IsOwnerMemberCollaborator(ctx, ctx.Repo.Repository, user.ID)
@@ -386,7 +415,7 @@ func Diff(ctx *context.Context) {
func RawDiff(ctx *context.Context) {
var gitRepo *git.Repository
if ctx.Data["PageIsWiki"] != nil {
- wikiRepo, err := gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository)
+ wikiRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo())
if err != nil {
ctx.ServerError("OpenRepository", err)
return
@@ -407,8 +436,7 @@ func RawDiff(ctx *context.Context) {
ctx.Resp,
); err != nil {
if git.IsErrNotExist(err) {
- ctx.NotFound("GetRawDiff",
- errors.New("commit "+ctx.PathParam("sha")+" does not exist."))
+ ctx.NotFound(errors.New("commit " + ctx.PathParam("sha") + " does not exist."))
return
}
ctx.ServerError("GetRawDiff", err)
@@ -416,13 +444,19 @@ func RawDiff(ctx *context.Context) {
}
}
-func processGitCommits(ctx *context.Context, gitCommits []*git.Commit) []*git_model.SignCommitWithStatuses {
- commits := git_model.ConvertFromGitCommit(ctx, gitCommits, ctx.Repo.Repository)
+func processGitCommits(ctx *context.Context, gitCommits []*git.Commit) ([]*git_model.SignCommitWithStatuses, error) {
+ commits, err := git_service.ConvertFromGitCommit(ctx, gitCommits, ctx.Repo.Repository)
+ if err != nil {
+ return nil, err
+ }
if !ctx.Repo.CanRead(unit_model.TypeActions) {
for _, commit := range commits {
+ if commit.Status == nil {
+ continue
+ }
commit.Status.HideActionsURL(ctx)
git_model.CommitStatusesHideActionsURL(ctx, commit.Statuses)
}
}
- return commits
+ return commits, nil
}
diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go
index b3c1eb7cb0..de34a9375c 100644
--- a/routers/web/repo/compare.go
+++ b/routers/web/repo/compare.go
@@ -26,6 +26,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset"
csv_module "code.gitea.io/gitea/modules/csv"
+ "code.gitea.io/gitea/modules/fileicon"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
@@ -258,7 +259,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
ci.HeadUser, err = user_model.GetUserByName(ctx, headInfos[0])
if err != nil {
if user_model.IsErrUserNotExist(err) {
- ctx.NotFound("GetUserByName", nil)
+ ctx.NotFound(nil)
} else {
ctx.ServerError("GetUserByName", err)
}
@@ -273,7 +274,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
ci.HeadRepo, err = repo_model.GetRepositoryByOwnerAndName(ctx, headInfosSplit[0], headInfosSplit[1])
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
- ctx.NotFound("GetRepositoryByOwnerAndName", nil)
+ ctx.NotFound(nil)
} else {
ctx.ServerError("GetRepositoryByOwnerAndName", err)
}
@@ -281,7 +282,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
}
if err := ci.HeadRepo.LoadOwner(ctx); err != nil {
if user_model.IsErrUserNotExist(err) {
- ctx.NotFound("GetUserByName", nil)
+ ctx.NotFound(nil)
} else {
ctx.ServerError("GetUserByName", err)
}
@@ -292,7 +293,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
isSameRepo = ci.HeadRepo.ID == ctx.Repo.Repository.ID
}
} else {
- ctx.NotFound("CompareAndPullRequest", nil)
+ ctx.NotFound(nil)
return nil
}
ctx.Data["HeadUser"] = ci.HeadUser
@@ -301,8 +302,8 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
// Check if base branch is valid.
baseIsCommit := ctx.Repo.GitRepo.IsCommitExist(ci.BaseBranch)
- baseIsBranch := ctx.Repo.GitRepo.IsBranchExist(ci.BaseBranch)
- baseIsTag := ctx.Repo.GitRepo.IsTagExist(ci.BaseBranch)
+ baseIsBranch := gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, ci.BaseBranch)
+ baseIsTag := gitrepo.IsTagExist(ctx, ctx.Repo.Repository, ci.BaseBranch)
if !baseIsCommit && !baseIsBranch && !baseIsTag {
// Check if baseBranch is short sha commit hash
@@ -318,7 +319,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
}
return nil
} else {
- ctx.NotFound("IsRefExist", nil)
+ ctx.NotFound(nil)
return nil
}
}
@@ -401,14 +402,13 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
ci.HeadRepo = ctx.Repo.Repository
ci.HeadGitRepo = ctx.Repo.GitRepo
} else if has {
- ci.HeadGitRepo, err = gitrepo.OpenRepository(ctx, ci.HeadRepo)
+ ci.HeadGitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ci.HeadRepo)
if err != nil {
- ctx.ServerError("OpenRepository", err)
+ ctx.ServerError("RepositoryFromRequestContextOrOpen", err)
return nil
}
- defer ci.HeadGitRepo.Close()
} else {
- ctx.NotFound("ParseCompareInfo", nil)
+ ctx.NotFound(nil)
return nil
}
@@ -430,7 +430,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
baseRepo,
permBase)
}
- ctx.NotFound("ParseCompareInfo", nil)
+ ctx.NotFound(nil)
return nil
}
@@ -449,7 +449,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
ci.HeadRepo,
permHead)
}
- ctx.NotFound("ParseCompareInfo", nil)
+ ctx.NotFound(nil)
return nil
}
ctx.Data["CanWriteToHeadRepo"] = permHead.CanWrite(unit.TypeCode)
@@ -504,8 +504,8 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
// Check if head branch is valid.
headIsCommit := ci.HeadGitRepo.IsCommitExist(ci.HeadBranch)
- headIsBranch := ci.HeadGitRepo.IsBranchExist(ci.HeadBranch)
- headIsTag := ci.HeadGitRepo.IsTagExist(ci.HeadBranch)
+ headIsBranch := gitrepo.IsBranchExist(ctx, ci.HeadRepo, ci.HeadBranch)
+ headIsTag := gitrepo.IsTagExist(ctx, ci.HeadRepo, ci.HeadBranch)
if !headIsCommit && !headIsBranch && !headIsTag {
// Check if headBranch is short sha commit hash
if headCommit, _ := ci.HeadGitRepo.GetCommit(ci.HeadBranch); headCommit != nil {
@@ -513,7 +513,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
ctx.Data["HeadBranch"] = ci.HeadBranch
headIsCommit = true
} else {
- ctx.NotFound("IsRefExist", nil)
+ ctx.NotFound(nil)
return nil
}
}
@@ -533,7 +533,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
baseRepo,
permBase)
}
- ctx.NotFound("ParseCompareInfo", nil)
+ ctx.NotFound(nil)
return nil
}
@@ -569,20 +569,20 @@ func PrepareCompareDiff(
ctx *context.Context,
ci *common.CompareInfo,
whitespaceBehavior git.TrustedCmdArgs,
-) bool {
- var (
- repo = ctx.Repo.Repository
- err error
- title string
- )
-
- // Get diff information.
- ctx.Data["CommitRepoLink"] = ci.HeadRepo.Link()
-
+) (nothingToCompare bool) {
+ repo := ctx.Repo.Repository
headCommitID := ci.CompareInfo.HeadCommitID
+ ctx.Data["CommitRepoLink"] = ci.HeadRepo.Link()
ctx.Data["AfterCommitID"] = headCommitID
+ // follow GitHub's behavior: autofill the form and expand
+ newPrFormTitle := ctx.FormTrim("title")
+ newPrFormBody := ctx.FormTrim("body")
+ ctx.Data["ExpandNewPrForm"] = ctx.FormBool("expand") || ctx.FormBool("quick_pull") || newPrFormTitle != "" || newPrFormBody != ""
+ ctx.Data["TitleQuery"] = newPrFormTitle
+ ctx.Data["BodyQuery"] = newPrFormBody
+
if (headCommitID == ci.CompareInfo.MergeBase && !ci.DirectComparison) ||
headCommitID == ci.CompareInfo.BaseCommitID {
ctx.Data["IsNothingToCompare"] = true
@@ -614,7 +614,7 @@ func PrepareCompareDiff(
fileOnly := ctx.FormBool("file-only")
- diff, err := gitdiff.GetDiff(ctx, ci.HeadGitRepo,
+ diff, err := gitdiff.GetDiffForRender(ctx, ci.HeadGitRepo,
&gitdiff.DiffOptions{
BeforeCommitID: beforeCommitID,
AfterCommitID: headCommitID,
@@ -624,14 +624,33 @@ func PrepareCompareDiff(
MaxFiles: maxFiles,
WhitespaceBehavior: whitespaceBehavior,
DirectComparison: ci.DirectComparison,
- FileOnly: fileOnly,
}, ctx.FormStrings("files")...)
if err != nil {
- ctx.ServerError("GetDiffRangeWithWhitespaceBehavior", err)
+ ctx.ServerError("GetDiff", err)
+ return false
+ }
+ diffShortStat, err := gitdiff.GetDiffShortStat(ci.HeadGitRepo, beforeCommitID, headCommitID)
+ if err != nil {
+ ctx.ServerError("GetDiffShortStat", err)
return false
}
+ ctx.Data["DiffShortStat"] = diffShortStat
ctx.Data["Diff"] = diff
- ctx.Data["DiffNotAvailable"] = diff.NumFiles == 0
+ ctx.Data["DiffNotAvailable"] = diffShortStat.NumFiles == 0
+
+ if !fileOnly {
+ diffTree, err := gitdiff.GetDiffTree(ctx, ci.HeadGitRepo, false, beforeCommitID, headCommitID)
+ if err != nil {
+ ctx.ServerError("GetDiffTree", err)
+ return false
+ }
+
+ renderedIconPool := fileicon.NewRenderedIconPool()
+ ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(renderedIconPool, diffTree, nil)
+ ctx.PageData["FolderIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder())
+ ctx.PageData["FolderOpenIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolderOpen())
+ ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
+ }
headCommit, err := ci.HeadGitRepo.GetCommit(headCommitID)
if err != nil {
@@ -647,10 +666,15 @@ func PrepareCompareDiff(
return false
}
- commits := processGitCommits(ctx, ci.CompareInfo.Commits)
+ commits, err := processGitCommits(ctx, ci.CompareInfo.Commits)
+ if err != nil {
+ ctx.ServerError("processGitCommits", err)
+ return false
+ }
ctx.Data["Commits"] = commits
ctx.Data["CommitCount"] = len(commits)
+ title := ci.HeadBranch
if len(commits) == 1 {
c := commits[0]
title = strings.TrimSpace(c.UserCommit.Summary())
@@ -659,9 +683,8 @@ func PrepareCompareDiff(
if len(body) > 1 {
ctx.Data["content"] = strings.Join(body[1:], "\n")
}
- } else {
- title = ci.HeadBranch
}
+
if len(title) > 255 {
var trailer string
title, trailer = util.EllipsisDisplayStringX(title, 255)
@@ -708,11 +731,6 @@ func getBranchesAndTagsForRepo(ctx gocontext.Context, repo *repo_model.Repositor
// CompareDiff show different from one commit to another commit
func CompareDiff(ctx *context.Context) {
ci := ParseCompareInfo(ctx)
- defer func() {
- if ci != nil && ci.HeadGitRepo != nil {
- ci.HeadGitRepo.Close()
- }
- }()
if ctx.Written() {
return
}
@@ -726,8 +744,7 @@ func CompareDiff(ctx *context.Context) {
ctx.Data["OtherCompareSeparator"] = "..."
}
- nothingToCompare := PrepareCompareDiff(ctx, ci,
- gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)))
+ nothingToCompare := PrepareCompareDiff(ctx, ci, gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)))
if ctx.Written() {
return
}
@@ -866,7 +883,7 @@ func ExcerptBlob(ctx *context.Context) {
gitRepo := ctx.Repo.GitRepo
if ctx.Data["PageIsWiki"] == true {
var err error
- gitRepo, err = gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository)
+ gitRepo, err = gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo())
if err != nil {
ctx.ServerError("OpenRepository", err)
return
@@ -876,12 +893,11 @@ func ExcerptBlob(ctx *context.Context) {
chunkSize := gitdiff.BlobExcerptChunkSize
commit, err := gitRepo.GetCommit(commitID)
if err != nil {
- ctx.Error(http.StatusInternalServerError, "GetCommit")
+ ctx.HTTPError(http.StatusInternalServerError, "GetCommit")
return
}
section := &gitdiff.DiffSection{
FileName: filePath,
- Name: filePath,
}
if direction == "up" && (idxLeft-lastLeft) > chunkSize {
idxLeft -= chunkSize
@@ -905,7 +921,7 @@ func ExcerptBlob(ctx *context.Context) {
idxRight = lastRight
}
if err != nil {
- ctx.Error(http.StatusInternalServerError, "getExcerptLines")
+ ctx.HTTPError(http.StatusInternalServerError, "getExcerptLines")
return
}
if idxRight > lastRight {
@@ -927,9 +943,10 @@ func ExcerptBlob(ctx *context.Context) {
RightHunkSize: rightHunkSize,
},
}
- if direction == "up" {
+ switch direction {
+ case "up":
section.Lines = append([]*gitdiff.DiffLine{lineSection}, section.Lines...)
- } else if direction == "down" {
+ case "down":
section.Lines = append(section.Lines, lineSection)
}
}
diff --git a/routers/web/repo/contributors.go b/routers/web/repo/contributors.go
index e9c0919955..93dec1f350 100644
--- a/routers/web/repo/contributors.go
+++ b/routers/web/repo/contributors.go
@@ -26,7 +26,7 @@ func Contributors(ctx *context.Context) {
// ContributorsData renders JSON of contributors along with their weekly commit statistics
func ContributorsData(ctx *context.Context) {
- if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
+ if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch); err != nil {
if errors.Is(err, contributors_service.ErrAwaitGeneration) {
ctx.Status(http.StatusAccepted)
return
diff --git a/routers/web/repo/download.go b/routers/web/repo/download.go
index cb1163c70b..020cebf196 100644
--- a/routers/web/repo/download.go
+++ b/routers/web/repo/download.go
@@ -46,7 +46,7 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified *time.Tim
log.Error("ServeBlobOrLFS: Close: %v", err)
}
closed = true
- return common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified)
+ return common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, lastModified)
}
if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+pointer.Oid+`"`) {
return nil
@@ -78,14 +78,14 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified *time.Tim
}
closed = true
- return common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified)
+ return common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, lastModified)
}
func getBlobForEntry(ctx *context.Context) (*git.Blob, *time.Time) {
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
if err != nil {
if git.IsErrNotExist(err) {
- ctx.NotFound("GetTreeEntryByPath", err)
+ ctx.NotFound(err)
} else {
ctx.ServerError("GetTreeEntryByPath", err)
}
@@ -93,7 +93,7 @@ func getBlobForEntry(ctx *context.Context) (*git.Blob, *time.Time) {
}
if entry.IsDir() || entry.IsSubModule() {
- ctx.NotFound("getBlobForEntry", nil)
+ ctx.NotFound(nil)
return nil, nil
}
@@ -114,7 +114,7 @@ func SingleDownload(ctx *context.Context) {
return
}
- if err := common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified); err != nil {
+ if err := common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, lastModified); err != nil {
ctx.ServerError("ServeBlob", err)
}
}
@@ -136,13 +136,13 @@ func DownloadByID(ctx *context.Context) {
blob, err := ctx.Repo.GitRepo.GetBlob(ctx.PathParam("sha"))
if err != nil {
if git.IsErrNotExist(err) {
- ctx.NotFound("GetBlob", nil)
+ ctx.NotFound(nil)
} else {
ctx.ServerError("GetBlob", err)
}
return
}
- if err = common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, nil); err != nil {
+ if err = common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, nil); err != nil {
ctx.ServerError("ServeBlob", err)
}
}
@@ -152,7 +152,7 @@ func DownloadByIDOrLFS(ctx *context.Context) {
blob, err := ctx.Repo.GitRepo.GetBlob(ctx.PathParam("sha"))
if err != nil {
if git.IsErrNotExist(err) {
- ctx.NotFound("GetBlob", nil)
+ ctx.NotFound(nil)
} else {
ctx.ServerError("GetBlob", err)
}
diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go
index 85f407ab8d..2a5ac10282 100644
--- a/routers/web/repo/editor.go
+++ b/routers/web/repo/editor.go
@@ -4,6 +4,7 @@
package repo
import (
+ "bytes"
"fmt"
"io"
"net/http"
@@ -11,19 +12,17 @@ import (
"strings"
git_model "code.gitea.io/gitea/models/git"
- repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
- "code.gitea.io/gitea/modules/typesniffer"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/routers/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/context/upload"
"code.gitea.io/gitea/services/forms"
@@ -35,848 +34,422 @@ const (
tplEditDiffPreview templates.TplName = "repo/editor/diff_preview"
tplDeleteFile templates.TplName = "repo/editor/delete"
tplUploadFile templates.TplName = "repo/editor/upload"
+ tplPatchFile templates.TplName = "repo/editor/patch"
+ tplCherryPick templates.TplName = "repo/editor/cherry_pick"
- frmCommitChoiceDirect string = "direct"
- frmCommitChoiceNewBranch string = "commit-to-new-branch"
+ editorCommitChoiceDirect string = "direct"
+ editorCommitChoiceNewBranch string = "commit-to-new-branch"
)
-func canCreateBasePullRequest(ctx *context.Context) bool {
- baseRepo := ctx.Repo.Repository.BaseRepo
- return baseRepo != nil && baseRepo.UnitEnabled(ctx, unit.TypePullRequests)
-}
-
-func renderCommitRights(ctx *context.Context) bool {
- canCommitToBranch, err := ctx.Repo.CanCommitToBranch(ctx, ctx.Doer)
- if err != nil {
- log.Error("CanCommitToBranch: %v", err)
- }
- ctx.Data["CanCommitToBranch"] = canCommitToBranch
- ctx.Data["CanCreatePullRequest"] = ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) || canCreateBasePullRequest(ctx)
-
- return canCommitToBranch.CanCommitToBranch
-}
-
-// redirectForCommitChoice redirects after committing the edit to a branch
-func redirectForCommitChoice(ctx *context.Context, commitChoice, newBranchName, treePath string) {
- if commitChoice == frmCommitChoiceNewBranch {
- // Redirect to a pull request when possible
- redirectToPullRequest := false
- repo := ctx.Repo.Repository
- baseBranch := ctx.Repo.BranchName
- headBranch := newBranchName
- if repo.UnitEnabled(ctx, unit.TypePullRequests) {
- redirectToPullRequest = true
- } else if canCreateBasePullRequest(ctx) {
- redirectToPullRequest = true
- baseBranch = repo.BaseRepo.DefaultBranch
- headBranch = repo.Owner.Name + "/" + repo.Name + ":" + headBranch
- repo = repo.BaseRepo
- }
-
- if redirectToPullRequest {
- ctx.Redirect(repo.Link() + "/compare/" + util.PathEscapeSegments(baseBranch) + "..." + util.PathEscapeSegments(headBranch))
- return
+func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) *context.CommitFormOptions {
+ cleanedTreePath := files_service.CleanGitTreePath(ctx.Repo.TreePath)
+ if cleanedTreePath != ctx.Repo.TreePath {
+ redirectTo := fmt.Sprintf("%s/%s/%s/%s", ctx.Repo.RepoLink, editorAction, util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(cleanedTreePath))
+ if ctx.Req.URL.RawQuery != "" {
+ redirectTo += "?" + ctx.Req.URL.RawQuery
}
+ ctx.Redirect(redirectTo)
+ return nil
}
- returnURI := ctx.FormString("return_uri")
-
- ctx.RedirectToCurrentSite(
- returnURI,
- ctx.Repo.RepoLink+"/src/branch/"+util.PathEscapeSegments(newBranchName)+"/"+util.PathEscapeSegments(treePath),
- )
-}
-
-// getParentTreeFields returns list of parent tree names and corresponding tree paths
-// based on given tree path.
-func getParentTreeFields(treePath string) (treeNames, treePaths []string) {
- if len(treePath) == 0 {
- return treeNames, treePaths
- }
-
- treeNames = strings.Split(treePath, "/")
- treePaths = make([]string, len(treeNames))
- for i := range treeNames {
- treePaths[i] = strings.Join(treeNames[:i+1], "/")
+ commitFormOptions, err := context.PrepareCommitFormOptions(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.Permission, ctx.Repo.RefFullName)
+ if err != nil {
+ ctx.ServerError("PrepareCommitFormOptions", err)
+ return nil
}
- return treeNames, treePaths
-}
-
-func editFile(ctx *context.Context, isNewFile bool) {
- ctx.Data["PageIsViewCode"] = true
- ctx.Data["PageIsEdit"] = true
- ctx.Data["IsNewFile"] = isNewFile
- canCommit := renderCommitRights(ctx)
- treePath := cleanUploadFileName(ctx.Repo.TreePath)
- if treePath != ctx.Repo.TreePath {
- if isNewFile {
- ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_new", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
- } else {
- ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_edit", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
- }
- return
+ if commitFormOptions.NeedFork {
+ ForkToEdit(ctx)
+ return nil
}
- // Check if the filename (and additional path) is specified in the querystring
- // (filename is a misnomer, but kept for compatibility with GitHub)
- filePath, fileName := path.Split(ctx.Req.URL.Query().Get("filename"))
- filePath = strings.Trim(filePath, "/")
- treeNames, treePaths := getParentTreeFields(path.Join(ctx.Repo.TreePath, filePath))
-
- if !isNewFile {
- entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
- if err != nil {
- HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err)
- return
- }
-
- // No way to edit a directory online.
- if entry.IsDir() {
- ctx.NotFound("entry.IsDir", nil)
- return
- }
-
- blob := entry.Blob()
- if blob.Size() >= setting.UI.MaxDisplayFileSize {
- ctx.NotFound("blob.Size", err)
- return
- }
-
- dataRc, err := blob.DataAsync()
- if err != nil {
- ctx.NotFound("blob.Data", err)
- return
- }
-
- defer dataRc.Close()
-
- ctx.Data["FileSize"] = blob.Size()
- ctx.Data["FileName"] = blob.Name()
-
- buf := make([]byte, 1024)
- n, _ := util.ReadAtMost(dataRc, buf)
- buf = buf[:n]
-
- // Only some file types are editable online as text.
- if !typesniffer.DetectContentType(buf).IsRepresentableAsText() {
- ctx.NotFound("typesniffer.IsRepresentableAsText", nil)
- return
- }
-
- d, _ := io.ReadAll(dataRc)
-
- buf = append(buf, d...)
- if content, err := charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true}); err != nil {
- log.Error("ToUTF8: %v", err)
- ctx.Data["FileContent"] = string(buf)
- } else {
- ctx.Data["FileContent"] = content
- }
- } else {
- // Append filename from query, or empty string to allow user name the new file.
- treeNames = append(treeNames, fileName)
+ if commitFormOptions.WillSubmitToFork && !commitFormOptions.TargetRepo.CanEnableEditor() {
+ ctx.Data["NotFoundPrompt"] = ctx.Locale.Tr("repo.editor.fork_not_editable")
+ ctx.NotFound(nil)
}
- ctx.Data["TreeNames"] = treeNames
- ctx.Data["TreePaths"] = treePaths
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
- ctx.Data["commit_summary"] = ""
- ctx.Data["commit_message"] = ""
- if canCommit {
- ctx.Data["commit_choice"] = frmCommitChoiceDirect
- } else {
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- }
- ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
- ctx.Data["last_commit"] = ctx.Repo.CommitID
+ ctx.Data["TreePath"] = ctx.Repo.TreePath
+ ctx.Data["CommitFormOptions"] = commitFormOptions
+
+ // for online editor
ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
- ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, treePath)
-
ctx.Data["IsEditingFileOnly"] = ctx.FormString("return_uri") != ""
ctx.Data["ReturnURI"] = ctx.FormString("return_uri")
- ctx.HTML(http.StatusOK, tplEditFile)
+ // form fields
+ ctx.Data["commit_summary"] = ""
+ ctx.Data["commit_message"] = ""
+ ctx.Data["commit_choice"] = util.Iif(commitFormOptions.CanCommitToBranch, editorCommitChoiceDirect, editorCommitChoiceNewBranch)
+ ctx.Data["new_branch_name"] = getUniquePatchBranchName(ctx, ctx.Doer.LowerName, commitFormOptions.TargetRepo)
+ ctx.Data["last_commit"] = ctx.Repo.CommitID
+ return commitFormOptions
}
-// GetEditorConfig returns a editorconfig JSON string for given treePath or "null"
-func GetEditorConfig(ctx *context.Context, treePath string) string {
- ec, _, err := ctx.Repo.GetEditorconfig()
- if err == nil {
- def, err := ec.GetDefinitionForFilename(treePath)
- if err == nil {
- jsonStr, _ := json.Marshal(def)
- return string(jsonStr)
- }
- }
- return "null"
+func prepareTreePathFieldsAndPaths(ctx *context.Context, treePath string) {
+ // show the tree path fields in the "breadcrumb" and help users to edit the target tree path
+ ctx.Data["TreeNames"], ctx.Data["TreePaths"] = getParentTreeFields(strings.TrimPrefix(treePath, "/"))
}
-// EditFile render edit file page
-func EditFile(ctx *context.Context) {
- editFile(ctx, false)
+type preparedEditorCommitForm[T any] struct {
+ form T
+ commonForm *forms.CommitCommonForm
+ CommitFormOptions *context.CommitFormOptions
+ OldBranchName string
+ NewBranchName string
+ GitCommitter *files_service.IdentityOptions
}
-// NewFile render create file page
-func NewFile(ctx *context.Context) {
- editFile(ctx, true)
+func (f *preparedEditorCommitForm[T]) GetCommitMessage(defaultCommitMessage string) string {
+ commitMessage := util.IfZero(strings.TrimSpace(f.commonForm.CommitSummary), defaultCommitMessage)
+ if body := strings.TrimSpace(f.commonForm.CommitMessage); body != "" {
+ commitMessage += "\n\n" + body
+ }
+ return commitMessage
}
-func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile bool) {
- canCommit := renderCommitRights(ctx)
- treeNames, treePaths := getParentTreeFields(form.TreePath)
- branchName := ctx.Repo.BranchName
- if form.CommitChoice == frmCommitChoiceNewBranch {
- branchName = form.NewBranchName
+func prepareEditorCommitSubmittedForm[T forms.CommitCommonFormInterface](ctx *context.Context) *preparedEditorCommitForm[T] {
+ form := web.GetForm(ctx).(T)
+ if ctx.HasError() {
+ ctx.JSONError(ctx.GetErrMsg())
+ return nil
}
- ctx.Data["PageIsEdit"] = true
- ctx.Data["PageHasPosted"] = true
- ctx.Data["IsNewFile"] = isNewFile
- ctx.Data["TreePath"] = form.TreePath
- ctx.Data["TreeNames"] = treeNames
- ctx.Data["TreePaths"] = treePaths
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(ctx.Repo.BranchName)
- ctx.Data["FileContent"] = form.Content
- ctx.Data["commit_summary"] = form.CommitSummary
- ctx.Data["commit_message"] = form.CommitMessage
- ctx.Data["commit_choice"] = form.CommitChoice
- ctx.Data["new_branch_name"] = form.NewBranchName
- ctx.Data["last_commit"] = ctx.Repo.CommitID
- ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
- ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
- ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, form.TreePath)
+ commonForm := form.GetCommitCommonForm()
+ commonForm.TreePath = files_service.CleanGitTreePath(commonForm.TreePath)
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, tplEditFile)
- return
+ commitFormOptions, err := context.PrepareCommitFormOptions(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.Permission, ctx.Repo.RefFullName)
+ if err != nil {
+ ctx.ServerError("PrepareCommitFormOptions", err)
+ return nil
}
-
- // Cannot commit to a an existing branch if user doesn't have rights
- if branchName == ctx.Repo.BranchName && !canCommit {
- ctx.Data["Err_NewBranchName"] = true
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplEditFile, &form)
- return
+ if commitFormOptions.NeedFork {
+ // It shouldn't happen, because we should have done the checks in the "GET" request. But just in case.
+ ctx.JSONError(ctx.Locale.TrString("error.not_found"))
+ return nil
}
- // CommitSummary is optional in the web form, if empty, give it a default message based on add or update
- // `message` will be both the summary and message combined
- message := strings.TrimSpace(form.CommitSummary)
- if len(message) == 0 {
- if isNewFile {
- message = ctx.Locale.TrString("repo.editor.add", form.TreePath)
- } else {
- message = ctx.Locale.TrString("repo.editor.update", form.TreePath)
- }
+ // check commit behavior
+ fromBaseBranch := ctx.FormString("from_base_branch")
+ commitToNewBranch := commonForm.CommitChoice == editorCommitChoiceNewBranch || fromBaseBranch != ""
+ targetBranchName := util.Iif(commitToNewBranch, commonForm.NewBranchName, ctx.Repo.BranchName)
+ if targetBranchName == ctx.Repo.BranchName && !commitFormOptions.CanCommitToBranch {
+ ctx.JSONError(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", targetBranchName))
+ return nil
}
- form.CommitMessage = strings.TrimSpace(form.CommitMessage)
- if len(form.CommitMessage) > 0 {
- message += "\n\n" + form.CommitMessage
+
+ if !issues.CanMaintainerWriteToBranch(ctx, ctx.Repo.Permission, targetBranchName, ctx.Doer) {
+ ctx.NotFound(nil)
+ return nil
}
- operation := "update"
- if isNewFile {
- operation = "create"
+ // Committer user info
+ gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, commonForm.CommitEmail)
+ if !valid {
+ ctx.JSONError(ctx.Tr("repo.editor.invalid_commit_email"))
+ return nil
}
- if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
- LastCommitID: form.LastCommit,
- OldBranch: ctx.Repo.BranchName,
- NewBranch: branchName,
- Message: message,
- Files: []*files_service.ChangeRepoFile{
- {
- Operation: operation,
- FromTreePath: ctx.Repo.TreePath,
- TreePath: form.TreePath,
- ContentReader: strings.NewReader(strings.ReplaceAll(form.Content, "\r", "")),
- },
- },
- Signoff: form.Signoff,
- }); err != nil {
- // This is where we handle all the errors thrown by files_service.ChangeRepoFiles
- if git.IsErrNotExist(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_editing_no_longer_exists", ctx.Repo.TreePath), tplEditFile, &form)
- } else if git_model.IsErrLFSFileLocked(err) {
- ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(git_model.ErrLFSFileLocked).Path, err.(git_model.ErrLFSFileLocked).UserName), tplEditFile, &form)
- } else if files_service.IsErrFilenameInvalid(err) {
- ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath), tplEditFile, &form)
- } else if files_service.IsErrFilePathInvalid(err) {
- ctx.Data["Err_TreePath"] = true
- if fileErr, ok := err.(files_service.ErrFilePathInvalid); ok {
- switch fileErr.Type {
- case git.EntryModeSymlink:
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplEditFile, &form)
- case git.EntryModeTree:
- ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplEditFile, &form)
- case git.EntryModeBlob:
- ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplEditFile, &form)
- default:
- ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", fileErr.Path), tplEditFile, &form)
- }
- } else {
- ctx.Error(http.StatusInternalServerError, err.Error())
- }
- } else if files_service.IsErrRepoFileAlreadyExists(err) {
- ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplEditFile, &form)
- } else if git.IsErrBranchNotExist(err) {
- // For when a user adds/updates a file to a branch that no longer exists
- if branchErr, ok := err.(git.ErrBranchNotExist); ok {
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplEditFile, &form)
- } else {
- ctx.Error(http.StatusInternalServerError, err.Error())
- }
- } else if git_model.IsErrBranchAlreadyExists(err) {
- // For when a user specifies a new branch that already exists
- ctx.Data["Err_NewBranchName"] = true
- if branchErr, ok := err.(git_model.ErrBranchAlreadyExists); ok {
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form)
- } else {
- ctx.Error(http.StatusInternalServerError, err.Error())
- }
- } else if files_service.IsErrCommitIDDoesNotMatch(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.commit_id_not_matching"), tplEditFile, &form)
- } else if git.IsErrPushOutOfDate(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.push_out_of_date"), tplEditFile, &form)
- } else if git.IsErrPushRejected(err) {
- errPushRej := err.(*git.ErrPushRejected)
- if len(errPushRej.Message) == 0 {
- ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplEditFile, &form)
+ if commitToNewBranch {
+ // if target branch exists, we should stop
+ targetBranchExists, err := git_model.IsBranchExist(ctx, commitFormOptions.TargetRepo.ID, targetBranchName)
+ if err != nil {
+ ctx.ServerError("IsBranchExist", err)
+ return nil
+ } else if targetBranchExists {
+ if fromBaseBranch != "" {
+ ctx.JSONError(ctx.Tr("repo.editor.fork_branch_exists", targetBranchName))
} else {
- flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
- "Message": ctx.Tr("repo.editor.push_rejected"),
- "Summary": ctx.Tr("repo.editor.push_rejected_summary"),
- "Details": utils.SanitizeFlashErrorString(errPushRej.Message),
- })
- if err != nil {
- ctx.ServerError("editFilePost.HTMLString", err)
- return
- }
- ctx.RenderWithErr(flashError, tplEditFile, &form)
- }
- } else {
- flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
- "Message": ctx.Tr("repo.editor.fail_to_update_file", form.TreePath),
- "Summary": ctx.Tr("repo.editor.fail_to_update_file_summary"),
- "Details": utils.SanitizeFlashErrorString(err.Error()),
- })
- if err != nil {
- ctx.ServerError("editFilePost.HTMLString", err)
- return
+ ctx.JSONError(ctx.Tr("repo.editor.branch_already_exists", targetBranchName))
}
- ctx.RenderWithErr(flashError, tplEditFile, &form)
+ return nil
}
}
- if ctx.Repo.Repository.IsEmpty {
- if isEmpty, err := ctx.Repo.GitRepo.IsEmpty(); err == nil && !isEmpty {
- _ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, IsEmpty: false}, "is_empty")
+ oldBranchName := ctx.Repo.BranchName
+ if fromBaseBranch != "" {
+ err = editorPushBranchToForkedRepository(ctx, ctx.Doer, ctx.Repo.Repository.BaseRepo, fromBaseBranch, commitFormOptions.TargetRepo, targetBranchName)
+ if err != nil {
+ log.Error("Unable to editorPushBranchToForkedRepository: %v", err)
+ ctx.JSONError(ctx.Tr("repo.editor.fork_failed_to_push_branch", targetBranchName))
+ return nil
}
+ // we have pushed the base branch as the new branch, now we need to commit the changes directly to the new branch
+ oldBranchName = targetBranchName
}
- redirectForCommitChoice(ctx, form.CommitChoice, branchName, form.TreePath)
-}
-
-// EditFilePost response for editing file
-func EditFilePost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.EditRepoFileForm)
- editFilePost(ctx, *form, false)
-}
-
-// NewFilePost response for creating file
-func NewFilePost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.EditRepoFileForm)
- editFilePost(ctx, *form, true)
-}
-
-// DiffPreviewPost render preview diff page
-func DiffPreviewPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.EditPreviewDiffForm)
- treePath := cleanUploadFileName(ctx.Repo.TreePath)
- if len(treePath) == 0 {
- ctx.Error(http.StatusInternalServerError, "file name to diff is invalid")
- return
- }
-
- entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath)
- if err != nil {
- ctx.Error(http.StatusInternalServerError, "GetTreeEntryByPath: "+err.Error())
- return
- } else if entry.IsDir() {
- ctx.Error(http.StatusUnprocessableEntity)
- return
- }
-
- diff, err := files_service.GetDiffPreview(ctx, ctx.Repo.Repository, ctx.Repo.BranchName, treePath, form.Content)
- if err != nil {
- ctx.Error(http.StatusInternalServerError, "GetDiffPreview: "+err.Error())
- return
- }
-
- if diff.NumFiles != 0 {
- ctx.Data["File"] = diff.Files[0]
+ return &preparedEditorCommitForm[T]{
+ form: form,
+ commonForm: commonForm,
+ CommitFormOptions: commitFormOptions,
+ OldBranchName: oldBranchName,
+ NewBranchName: targetBranchName,
+ GitCommitter: gitCommitter,
}
-
- ctx.HTML(http.StatusOK, tplEditDiffPreview)
}
-// DeleteFile render delete file page
-func DeleteFile(ctx *context.Context) {
- ctx.Data["PageIsDelete"] = true
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
- treePath := cleanUploadFileName(ctx.Repo.TreePath)
-
- if treePath != ctx.Repo.TreePath {
- ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_delete", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
+// redirectForCommitChoice redirects after committing the edit to a branch
+func redirectForCommitChoice[T any](ctx *context.Context, parsed *preparedEditorCommitForm[T], treePath string) {
+ // when editing a file in a PR, it should return to the origin location
+ if returnURI := ctx.FormString("return_uri"); returnURI != "" && httplib.IsCurrentGiteaSiteURL(ctx, returnURI) {
+ ctx.JSONRedirect(returnURI)
return
}
- ctx.Data["TreePath"] = treePath
- canCommit := renderCommitRights(ctx)
-
- ctx.Data["commit_summary"] = ""
- ctx.Data["commit_message"] = ""
- ctx.Data["last_commit"] = ctx.Repo.CommitID
- if canCommit {
- ctx.Data["commit_choice"] = frmCommitChoiceDirect
- } else {
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
+ if parsed.commonForm.CommitChoice == editorCommitChoiceNewBranch {
+ // Redirect to a pull request when possible
+ redirectToPullRequest := false
+ repo, baseBranch, headBranch := ctx.Repo.Repository, parsed.OldBranchName, parsed.NewBranchName
+ if ctx.Repo.Repository.IsFork && parsed.CommitFormOptions.CanCreateBasePullRequest {
+ redirectToPullRequest = true
+ baseBranch = repo.BaseRepo.DefaultBranch
+ headBranch = repo.Owner.Name + "/" + repo.Name + ":" + headBranch
+ repo = repo.BaseRepo
+ } else if repo.UnitEnabled(ctx, unit.TypePullRequests) {
+ redirectToPullRequest = true
+ }
+ if redirectToPullRequest {
+ ctx.JSONRedirect(repo.Link() + "/compare/" + util.PathEscapeSegments(baseBranch) + "..." + util.PathEscapeSegments(headBranch))
+ return
+ }
}
- ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
- ctx.HTML(http.StatusOK, tplDeleteFile)
+ // redirect to the newly updated file
+ redirectTo := util.URLJoin(ctx.Repo.RepoLink, "src/branch", util.PathEscapeSegments(parsed.NewBranchName), util.PathEscapeSegments(treePath))
+ ctx.JSONRedirect(redirectTo)
}
-// DeleteFilePost response for deleting file
-func DeleteFilePost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.DeleteRepoFileForm)
- canCommit := renderCommitRights(ctx)
- branchName := ctx.Repo.BranchName
- if form.CommitChoice == frmCommitChoiceNewBranch {
- branchName = form.NewBranchName
- }
-
- ctx.Data["PageIsDelete"] = true
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
- ctx.Data["TreePath"] = ctx.Repo.TreePath
- ctx.Data["commit_summary"] = form.CommitSummary
- ctx.Data["commit_message"] = form.CommitMessage
- ctx.Data["commit_choice"] = form.CommitChoice
- ctx.Data["new_branch_name"] = form.NewBranchName
- ctx.Data["last_commit"] = ctx.Repo.CommitID
-
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, tplDeleteFile)
- return
- }
-
- if branchName == ctx.Repo.BranchName && !canCommit {
- ctx.Data["Err_NewBranchName"] = true
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplDeleteFile, &form)
- return
+func editFileOpenExisting(ctx *context.Context) (prefetch []byte, dataRc io.ReadCloser, fInfo *fileInfo) {
+ entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
+ if err != nil {
+ HandleGitError(ctx, "GetTreeEntryByPath", err)
+ return nil, nil, nil
}
- message := strings.TrimSpace(form.CommitSummary)
- if len(message) == 0 {
- message = ctx.Locale.TrString("repo.editor.delete", ctx.Repo.TreePath)
- }
- form.CommitMessage = strings.TrimSpace(form.CommitMessage)
- if len(form.CommitMessage) > 0 {
- message += "\n\n" + form.CommitMessage
+ // No way to edit a directory online.
+ if entry.IsDir() {
+ ctx.NotFound(nil)
+ return nil, nil, nil
}
- if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
- LastCommitID: form.LastCommit,
- OldBranch: ctx.Repo.BranchName,
- NewBranch: branchName,
- Files: []*files_service.ChangeRepoFile{
- {
- Operation: "delete",
- TreePath: ctx.Repo.TreePath,
- },
- },
- Message: message,
- Signoff: form.Signoff,
- }); err != nil {
- // This is where we handle all the errors thrown by repofiles.DeleteRepoFile
- if git.IsErrNotExist(err) || files_service.IsErrRepoFileDoesNotExist(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_deleting_no_longer_exists", ctx.Repo.TreePath), tplDeleteFile, &form)
- } else if files_service.IsErrFilenameInvalid(err) {
- ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", ctx.Repo.TreePath), tplDeleteFile, &form)
- } else if files_service.IsErrFilePathInvalid(err) {
- ctx.Data["Err_TreePath"] = true
- if fileErr, ok := err.(files_service.ErrFilePathInvalid); ok {
- switch fileErr.Type {
- case git.EntryModeSymlink:
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplDeleteFile, &form)
- case git.EntryModeTree:
- ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplDeleteFile, &form)
- case git.EntryModeBlob:
- ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplDeleteFile, &form)
- default:
- ctx.ServerError("DeleteRepoFile", err)
- }
- } else {
- ctx.ServerError("DeleteRepoFile", err)
- }
- } else if git.IsErrBranchNotExist(err) {
- // For when a user deletes a file to a branch that no longer exists
- if branchErr, ok := err.(git.ErrBranchNotExist); ok {
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplDeleteFile, &form)
- } else {
- ctx.Error(http.StatusInternalServerError, err.Error())
- }
- } else if git_model.IsErrBranchAlreadyExists(err) {
- // For when a user specifies a new branch that already exists
- if branchErr, ok := err.(git_model.ErrBranchAlreadyExists); ok {
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplDeleteFile, &form)
- } else {
- ctx.Error(http.StatusInternalServerError, err.Error())
- }
- } else if files_service.IsErrCommitIDDoesNotMatch(err) || git.IsErrPushOutOfDate(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_deleting", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(form.LastCommit)+"..."+util.PathEscapeSegments(ctx.Repo.CommitID)), tplDeleteFile, &form)
- } else if git.IsErrPushRejected(err) {
- errPushRej := err.(*git.ErrPushRejected)
- if len(errPushRej.Message) == 0 {
- ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplDeleteFile, &form)
- } else {
- flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
- "Message": ctx.Tr("repo.editor.push_rejected"),
- "Summary": ctx.Tr("repo.editor.push_rejected_summary"),
- "Details": utils.SanitizeFlashErrorString(errPushRej.Message),
- })
- if err != nil {
- ctx.ServerError("DeleteFilePost.HTMLString", err)
- return
- }
- ctx.RenderWithErr(flashError, tplDeleteFile, &form)
- }
+ blob := entry.Blob()
+ buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob)
+ if err != nil {
+ if git.IsErrNotExist(err) {
+ ctx.NotFound(err)
} else {
- ctx.ServerError("DeleteRepoFile", err)
+ ctx.ServerError("getFileReader", err)
}
- return
+ return nil, nil, nil
}
- ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", ctx.Repo.TreePath))
- treePath := path.Dir(ctx.Repo.TreePath)
- if treePath == "." {
- treePath = "" // the file deleted was in the root, so we return the user to the root directory
- }
- if len(treePath) > 0 {
- // Need to get the latest commit since it changed
- commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName)
- if err == nil && commit != nil {
- // We have the comment, now find what directory we can return the user to
- // (must have entries)
- treePath = GetClosestParentWithFiles(treePath, commit)
- } else {
- treePath = "" // otherwise return them to the root of the repo
+ if fInfo.isLFSFile() {
+ lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath)
+ if err != nil {
+ _ = dataRc.Close()
+ ctx.ServerError("GetTreePathLock", err)
+ return nil, nil, nil
+ } else if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID {
+ _ = dataRc.Close()
+ ctx.NotFound(nil)
+ return nil, nil, nil
}
}
- redirectForCommitChoice(ctx, form.CommitChoice, branchName, treePath)
-}
-
-// UploadFile render upload file page
-func UploadFile(ctx *context.Context) {
- ctx.Data["PageIsUpload"] = true
- upload.AddUploadContext(ctx, "repo")
- canCommit := renderCommitRights(ctx)
- treePath := cleanUploadFileName(ctx.Repo.TreePath)
- if treePath != ctx.Repo.TreePath {
- ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_upload", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath)))
- return
- }
- ctx.Repo.TreePath = treePath
-
- treeNames, treePaths := getParentTreeFields(ctx.Repo.TreePath)
- if len(treeNames) == 0 {
- // We must at least have one element for user to input.
- treeNames = []string{""}
- }
-
- ctx.Data["TreeNames"] = treeNames
- ctx.Data["TreePaths"] = treePaths
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
- ctx.Data["commit_summary"] = ""
- ctx.Data["commit_message"] = ""
- if canCommit {
- ctx.Data["commit_choice"] = frmCommitChoiceDirect
- } else {
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- }
- ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
-
- ctx.HTML(http.StatusOK, tplUploadFile)
+ return buf, dataRc, fInfo
}
-// UploadFilePost response for uploading file
-func UploadFilePost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.UploadRepoFileForm)
- ctx.Data["PageIsUpload"] = true
- upload.AddUploadContext(ctx, "repo")
- canCommit := renderCommitRights(ctx)
-
- oldBranchName := ctx.Repo.BranchName
- branchName := oldBranchName
-
- if form.CommitChoice == frmCommitChoiceNewBranch {
- branchName = form.NewBranchName
- }
-
- form.TreePath = cleanUploadFileName(form.TreePath)
+func EditFile(ctx *context.Context) {
+ editorAction := ctx.PathParam("editor_action")
+ isNewFile := editorAction == "_new"
+ ctx.Data["IsNewFile"] = isNewFile
- treeNames, treePaths := getParentTreeFields(form.TreePath)
- if len(treeNames) == 0 {
- // We must at least have one element for user to input.
- treeNames = []string{""}
+ // Check if the filename (and additional path) is specified in the querystring
+ // (filename is a misnomer, but kept for compatibility with GitHub)
+ urlQuery := ctx.Req.URL.Query()
+ queryFilename := urlQuery.Get("filename")
+ if queryFilename != "" {
+ newTreePath := path.Join(ctx.Repo.TreePath, queryFilename)
+ redirectTo := fmt.Sprintf("%s/%s/%s/%s", ctx.Repo.RepoLink, editorAction, util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(newTreePath))
+ urlQuery.Del("filename")
+ if newQueryParams := urlQuery.Encode(); newQueryParams != "" {
+ redirectTo += "?" + newQueryParams
+ }
+ ctx.Redirect(redirectTo)
+ return
}
- ctx.Data["TreePath"] = form.TreePath
- ctx.Data["TreeNames"] = treeNames
- ctx.Data["TreePaths"] = treePaths
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName)
- ctx.Data["commit_summary"] = form.CommitSummary
- ctx.Data["commit_message"] = form.CommitMessage
- ctx.Data["commit_choice"] = form.CommitChoice
- ctx.Data["new_branch_name"] = branchName
+ // on the "New File" page, we should add an empty path field to make end users could input a new name
+ prepareTreePathFieldsAndPaths(ctx, util.Iif(isNewFile, ctx.Repo.TreePath+"/", ctx.Repo.TreePath))
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, tplUploadFile)
+ prepareEditorCommitFormOptions(ctx, editorAction)
+ if ctx.Written() {
return
}
- if oldBranchName != branchName {
- if _, err := ctx.Repo.GitRepo.GetBranch(branchName); err == nil {
- ctx.Data["Err_NewBranchName"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchName), tplUploadFile, &form)
+ if !isNewFile {
+ prefetch, dataRc, fInfo := editFileOpenExisting(ctx)
+ if ctx.Written() {
return
}
- } else if !canCommit {
- ctx.Data["Err_NewBranchName"] = true
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplUploadFile, &form)
- return
- }
+ defer dataRc.Close()
- if !ctx.Repo.Repository.IsEmpty {
- var newTreePath string
- for _, part := range treeNames {
- newTreePath = path.Join(newTreePath, part)
- entry, err := ctx.Repo.Commit.GetTreeEntryByPath(newTreePath)
- if err != nil {
- if git.IsErrNotExist(err) {
- break // Means there is no item with that name, so we're good
- }
- ctx.ServerError("Repo.Commit.GetTreeEntryByPath", err)
- return
- }
+ ctx.Data["FileSize"] = fInfo.fileSize
- // User can only upload files to a directory, the directory name shouldn't be an existing file.
- if !entry.IsDir() {
- ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", part), tplUploadFile, &form)
- return
- }
+ // Only some file types are editable online as text.
+ if fInfo.isLFSFile() {
+ ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_lfs_files")
+ } else if !fInfo.st.IsRepresentableAsText() {
+ ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_non_text_files")
+ } else if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
+ ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_too_large_file")
}
- }
- message := strings.TrimSpace(form.CommitSummary)
- if len(message) == 0 {
- dir := form.TreePath
- if dir == "" {
- dir = "/"
- }
- message = ctx.Locale.TrString("repo.editor.upload_files_to_dir", dir)
- }
-
- form.CommitMessage = strings.TrimSpace(form.CommitMessage)
- if len(form.CommitMessage) > 0 {
- message += "\n\n" + form.CommitMessage
- }
-
- if err := files_service.UploadRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.UploadRepoFileOptions{
- LastCommitID: ctx.Repo.CommitID,
- OldBranch: oldBranchName,
- NewBranch: branchName,
- TreePath: form.TreePath,
- Message: message,
- Files: form.Files,
- Signoff: form.Signoff,
- }); err != nil {
- if git_model.IsErrLFSFileLocked(err) {
- ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(git_model.ErrLFSFileLocked).Path, err.(git_model.ErrLFSFileLocked).UserName), tplUploadFile, &form)
- } else if files_service.IsErrFilenameInvalid(err) {
- ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath), tplUploadFile, &form)
- } else if files_service.IsErrFilePathInvalid(err) {
- ctx.Data["Err_TreePath"] = true
- fileErr := err.(files_service.ErrFilePathInvalid)
- switch fileErr.Type {
- case git.EntryModeSymlink:
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplUploadFile, &form)
- case git.EntryModeTree:
- ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplUploadFile, &form)
- case git.EntryModeBlob:
- ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplUploadFile, &form)
- default:
- ctx.Error(http.StatusInternalServerError, err.Error())
+ if ctx.Data["NotEditableReason"] == nil {
+ buf, err := io.ReadAll(io.MultiReader(bytes.NewReader(prefetch), dataRc))
+ if err != nil {
+ ctx.ServerError("ReadAll", err)
+ return
}
- } else if files_service.IsErrRepoFileAlreadyExists(err) {
- ctx.Data["Err_TreePath"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplUploadFile, &form)
- } else if git.IsErrBranchNotExist(err) {
- branchErr := err.(git.ErrBranchNotExist)
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplUploadFile, &form)
- } else if git_model.IsErrBranchAlreadyExists(err) {
- // For when a user specifies a new branch that already exists
- ctx.Data["Err_NewBranchName"] = true
- branchErr := err.(git_model.ErrBranchAlreadyExists)
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplUploadFile, &form)
- } else if git.IsErrPushOutOfDate(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(ctx.Repo.CommitID)+"..."+util.PathEscapeSegments(form.NewBranchName)), tplUploadFile, &form)
- } else if git.IsErrPushRejected(err) {
- errPushRej := err.(*git.ErrPushRejected)
- if len(errPushRej.Message) == 0 {
- ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplUploadFile, &form)
+ if content, err := charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true}); err != nil {
+ ctx.Data["FileContent"] = string(buf)
} else {
- flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
- "Message": ctx.Tr("repo.editor.push_rejected"),
- "Summary": ctx.Tr("repo.editor.push_rejected_summary"),
- "Details": utils.SanitizeFlashErrorString(errPushRej.Message),
- })
- if err != nil {
- ctx.ServerError("UploadFilePost.HTMLString", err)
- return
- }
- ctx.RenderWithErr(flashError, tplUploadFile, &form)
+ ctx.Data["FileContent"] = content
}
- } else {
- // os.ErrNotExist - upload file missing in the intervening time?!
- log.Error("Error during upload to repo: %-v to filepath: %s on %s from %s: %v", ctx.Repo.Repository, form.TreePath, oldBranchName, form.NewBranchName, err)
- ctx.RenderWithErr(ctx.Tr("repo.editor.unable_to_upload_files", form.TreePath, err), tplUploadFile, &form)
}
- return
}
- if ctx.Repo.Repository.IsEmpty {
- if isEmpty, err := ctx.Repo.GitRepo.IsEmpty(); err == nil && !isEmpty {
- _ = repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, IsEmpty: false}, "is_empty")
- }
+ ctx.Data["EditorconfigJson"] = getContextRepoEditorConfig(ctx, ctx.Repo.TreePath)
+ ctx.HTML(http.StatusOK, tplEditFile)
+}
+
+func EditFilePost(ctx *context.Context) {
+ editorAction := ctx.PathParam("editor_action")
+ isNewFile := editorAction == "_new"
+ parsed := prepareEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx)
+ if ctx.Written() {
+ return
}
- redirectForCommitChoice(ctx, form.CommitChoice, branchName, form.TreePath)
-}
+ defaultCommitMessage := util.Iif(isNewFile, ctx.Locale.TrString("repo.editor.add", parsed.form.TreePath), ctx.Locale.TrString("repo.editor.update", parsed.form.TreePath))
-func cleanUploadFileName(name string) string {
- // Rebase the filename
- name = util.PathJoinRel(name)
- // Git disallows any filenames to have a .git directory in them.
- for _, part := range strings.Split(name, "/") {
- if strings.ToLower(part) == ".git" {
- return ""
- }
+ var operation string
+ if isNewFile {
+ operation = "create"
+ } else if parsed.form.Content.Has() {
+ // The form content only has data if the file is representable as text, is not too large and not in lfs.
+ operation = "update"
+ } else if ctx.Repo.TreePath != parsed.form.TreePath {
+ // If it doesn't have data, the only possible operation is a "rename"
+ operation = "rename"
+ } else {
+ // It should never happen, just in case
+ ctx.JSONError(ctx.Tr("error.occurred"))
+ return
}
- return name
-}
-// UploadFileToServer upload file to server file dir not git
-func UploadFileToServer(ctx *context.Context) {
- file, header, err := ctx.Req.FormFile("file")
+ _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
+ LastCommitID: parsed.form.LastCommit,
+ OldBranch: parsed.OldBranchName,
+ NewBranch: parsed.NewBranchName,
+ Message: parsed.GetCommitMessage(defaultCommitMessage),
+ Files: []*files_service.ChangeRepoFile{
+ {
+ Operation: operation,
+ FromTreePath: ctx.Repo.TreePath,
+ TreePath: parsed.form.TreePath,
+ ContentReader: strings.NewReader(strings.ReplaceAll(parsed.form.Content.Value(), "\r", "")),
+ },
+ },
+ Signoff: parsed.form.Signoff,
+ Author: parsed.GitCommitter,
+ Committer: parsed.GitCommitter,
+ })
if err != nil {
- ctx.Error(http.StatusInternalServerError, fmt.Sprintf("FormFile: %v", err))
+ editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
return
}
- defer file.Close()
- buf := make([]byte, 1024)
- n, _ := util.ReadAtMost(file, buf)
- if n > 0 {
- buf = buf[:n]
- }
+ redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)
+}
- err = upload.Verify(buf, header.Filename, setting.Repository.Upload.AllowedTypes)
- if err != nil {
- ctx.Error(http.StatusBadRequest, err.Error())
+// DeleteFile render delete file page
+func DeleteFile(ctx *context.Context) {
+ prepareEditorCommitFormOptions(ctx, "_delete")
+ if ctx.Written() {
return
}
+ ctx.Data["PageIsDelete"] = true
+ ctx.HTML(http.StatusOK, tplDeleteFile)
+}
- name := cleanUploadFileName(header.Filename)
- if len(name) == 0 {
- ctx.Error(http.StatusInternalServerError, "Upload file name is invalid")
+// DeleteFilePost response for deleting file
+func DeleteFilePost(ctx *context.Context) {
+ parsed := prepareEditorCommitSubmittedForm[*forms.DeleteRepoFileForm](ctx)
+ if ctx.Written() {
return
}
- upload, err := repo_model.NewUpload(ctx, name, buf, file)
+ treePath := ctx.Repo.TreePath
+ _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
+ LastCommitID: parsed.form.LastCommit,
+ OldBranch: parsed.OldBranchName,
+ NewBranch: parsed.NewBranchName,
+ Files: []*files_service.ChangeRepoFile{
+ {
+ Operation: "delete",
+ TreePath: treePath,
+ },
+ },
+ Message: parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete", treePath)),
+ Signoff: parsed.form.Signoff,
+ Author: parsed.GitCommitter,
+ Committer: parsed.GitCommitter,
+ })
if err != nil {
- ctx.Error(http.StatusInternalServerError, fmt.Sprintf("NewUpload: %v", err))
+ editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
return
}
- log.Trace("New file uploaded: %s", upload.UUID)
- ctx.JSON(http.StatusOK, map[string]string{
- "uuid": upload.UUID,
- })
+ ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath))
+ redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, parsed.NewBranchName, treePath)
+ redirectForCommitChoice(ctx, parsed, redirectTreePath)
}
-// RemoveUploadFileFromServer remove file from server file dir
-func RemoveUploadFileFromServer(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.RemoveUploadFileForm)
- if len(form.File) == 0 {
- ctx.Status(http.StatusNoContent)
- return
- }
-
- if err := repo_model.DeleteUploadByUUID(ctx, form.File); err != nil {
- ctx.Error(http.StatusInternalServerError, fmt.Sprintf("DeleteUploadByUUID: %v", err))
+func UploadFile(ctx *context.Context) {
+ ctx.Data["PageIsUpload"] = true
+ prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath)
+ opts := prepareEditorCommitFormOptions(ctx, "_upload")
+ if ctx.Written() {
return
}
+ upload.AddUploadContextForRepo(ctx, opts.TargetRepo)
- log.Trace("Upload file removed: %s", form.File)
- ctx.Status(http.StatusNoContent)
+ ctx.HTML(http.StatusOK, tplUploadFile)
}
-// GetUniquePatchBranchName Gets a unique branch name for a new patch branch
-// It will be in the form of <username>-patch-<num> where <num> is the first branch of this format
-// that doesn't already exist. If we exceed 1000 tries or an error is thrown, we just return "" so the user has to
-// type in the branch name themselves (will be an empty field)
-func GetUniquePatchBranchName(ctx *context.Context) string {
- prefix := ctx.Doer.LowerName + "-patch-"
- for i := 1; i <= 1000; i++ {
- branchName := fmt.Sprintf("%s%d", prefix, i)
- if _, err := ctx.Repo.GitRepo.GetBranch(branchName); err != nil {
- if git.IsErrBranchNotExist(err) {
- return branchName
- }
- log.Error("GetUniquePatchBranchName: %v", err)
- return ""
- }
+func UploadFilePost(ctx *context.Context) {
+ parsed := prepareEditorCommitSubmittedForm[*forms.UploadRepoFileForm](ctx)
+ if ctx.Written() {
+ return
}
- return ""
-}
-// GetClosestParentWithFiles Recursively gets the path of parent in a tree that has files (used when file in a tree is
-// deleted). Returns "" for the root if no parents other than the root have files. If the given treePath isn't a
-// SubTree or it has no entries, we go up one dir and see if we can return the user to that listing.
-func GetClosestParentWithFiles(treePath string, commit *git.Commit) string {
- if len(treePath) == 0 || treePath == "." {
- return ""
- }
- // see if the tree has entries
- if tree, err := commit.SubTree(treePath); err != nil {
- // failed to get tree, going up a dir
- return GetClosestParentWithFiles(path.Dir(treePath), commit)
- } else if entries, err := tree.ListEntries(); err != nil || len(entries) == 0 {
- // no files in this dir, going up a dir
- return GetClosestParentWithFiles(path.Dir(treePath), commit)
- }
- return treePath
+ defaultCommitMessage := ctx.Locale.TrString("repo.editor.upload_files_to_dir", util.IfZero(parsed.form.TreePath, "/"))
+ err := files_service.UploadRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.UploadRepoFileOptions{
+ LastCommitID: parsed.form.LastCommit,
+ OldBranch: parsed.OldBranchName,
+ NewBranch: parsed.NewBranchName,
+ TreePath: parsed.form.TreePath,
+ Message: parsed.GetCommitMessage(defaultCommitMessage),
+ Files: parsed.form.Files,
+ Signoff: parsed.form.Signoff,
+ Author: parsed.GitCommitter,
+ Committer: parsed.GitCommitter,
+ })
+ if err != nil {
+ editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
+ return
+ }
+ redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)
}
diff --git a/routers/web/repo/editor_apply_patch.go b/routers/web/repo/editor_apply_patch.go
new file mode 100644
index 0000000000..bd2811cc5f
--- /dev/null
+++ b/routers/web/repo/editor_apply_patch.go
@@ -0,0 +1,51 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "net/http"
+ "strings"
+
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/forms"
+ "code.gitea.io/gitea/services/repository/files"
+)
+
+func NewDiffPatch(ctx *context.Context) {
+ prepareEditorCommitFormOptions(ctx, "_diffpatch")
+ if ctx.Written() {
+ return
+ }
+
+ ctx.Data["PageIsPatch"] = true
+ ctx.HTML(http.StatusOK, tplPatchFile)
+}
+
+// NewDiffPatchPost response for sending patch page
+func NewDiffPatchPost(ctx *context.Context) {
+ parsed := prepareEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx)
+ if ctx.Written() {
+ return
+ }
+
+ defaultCommitMessage := ctx.Locale.TrString("repo.editor.patch")
+ _, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, &files.ApplyDiffPatchOptions{
+ LastCommitID: parsed.form.LastCommit,
+ OldBranch: parsed.OldBranchName,
+ NewBranch: parsed.NewBranchName,
+ Message: parsed.GetCommitMessage(defaultCommitMessage),
+ Content: strings.ReplaceAll(parsed.form.Content.Value(), "\r\n", "\n"),
+ Author: parsed.GitCommitter,
+ Committer: parsed.GitCommitter,
+ })
+ if err != nil {
+ err = util.ErrorWrapLocale(err, "repo.editor.fail_to_apply_patch")
+ }
+ if err != nil {
+ editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
+ return
+ }
+ redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)
+}
diff --git a/routers/web/repo/editor_cherry_pick.go b/routers/web/repo/editor_cherry_pick.go
new file mode 100644
index 0000000000..10c2741b1c
--- /dev/null
+++ b/routers/web/repo/editor_cherry_pick.go
@@ -0,0 +1,86 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "bytes"
+ "net/http"
+ "strings"
+
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/forms"
+ "code.gitea.io/gitea/services/repository/files"
+)
+
+func CherryPick(ctx *context.Context) {
+ prepareEditorCommitFormOptions(ctx, "_cherrypick")
+ if ctx.Written() {
+ return
+ }
+
+ fromCommitID := ctx.PathParam("sha")
+ ctx.Data["FromCommitID"] = fromCommitID
+ cherryPickCommit, err := ctx.Repo.GitRepo.GetCommit(fromCommitID)
+ if err != nil {
+ HandleGitError(ctx, "GetCommit", err)
+ return
+ }
+
+ if ctx.FormString("cherry-pick-type") == "revert" {
+ ctx.Data["CherryPickType"] = "revert"
+ ctx.Data["commit_summary"] = "revert " + ctx.PathParam("sha")
+ ctx.Data["commit_message"] = "revert " + cherryPickCommit.Message()
+ } else {
+ ctx.Data["CherryPickType"] = "cherry-pick"
+ splits := strings.SplitN(cherryPickCommit.Message(), "\n", 2)
+ ctx.Data["commit_summary"] = splits[0]
+ ctx.Data["commit_message"] = splits[1]
+ }
+
+ ctx.HTML(http.StatusOK, tplCherryPick)
+}
+
+func CherryPickPost(ctx *context.Context) {
+ fromCommitID := ctx.PathParam("sha")
+ parsed := prepareEditorCommitSubmittedForm[*forms.CherryPickForm](ctx)
+ if ctx.Written() {
+ return
+ }
+
+ defaultCommitMessage := util.Iif(parsed.form.Revert, ctx.Locale.TrString("repo.commit.revert-header", fromCommitID), ctx.Locale.TrString("repo.commit.cherry-pick-header", fromCommitID))
+ opts := &files.ApplyDiffPatchOptions{
+ LastCommitID: parsed.form.LastCommit,
+ OldBranch: parsed.OldBranchName,
+ NewBranch: parsed.NewBranchName,
+ Message: parsed.GetCommitMessage(defaultCommitMessage),
+ Author: parsed.GitCommitter,
+ Committer: parsed.GitCommitter,
+ }
+
+ // First try the simple plain read-tree -m approach
+ opts.Content = fromCommitID
+ if _, err := files.CherryPick(ctx, ctx.Repo.Repository, ctx.Doer, parsed.form.Revert, opts); err != nil {
+ // Drop through to the "apply" method
+ buf := &bytes.Buffer{}
+ if parsed.form.Revert {
+ err = git.GetReverseRawDiff(ctx, ctx.Repo.Repository.RepoPath(), fromCommitID, buf)
+ } else {
+ err = git.GetRawDiff(ctx.Repo.GitRepo, fromCommitID, "patch", buf)
+ }
+ if err == nil {
+ opts.Content = buf.String()
+ _, err = files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, opts)
+ if err != nil {
+ err = util.ErrorWrapLocale(err, "repo.editor.fail_to_apply_patch")
+ }
+ }
+ if err != nil {
+ editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
+ return
+ }
+ }
+ redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)
+}
diff --git a/routers/web/repo/editor_error.go b/routers/web/repo/editor_error.go
new file mode 100644
index 0000000000..245226a039
--- /dev/null
+++ b/routers/web/repo/editor_error.go
@@ -0,0 +1,82 @@
+// Copyright 2025 Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "errors"
+
+ git_model "code.gitea.io/gitea/models/git"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/routers/utils"
+ context_service "code.gitea.io/gitea/services/context"
+ files_service "code.gitea.io/gitea/services/repository/files"
+)
+
+func errorAs[T error](v error) (e T, ok bool) {
+ if errors.As(v, &e) {
+ return e, true
+ }
+ return e, false
+}
+
+func editorHandleFileOperationErrorRender(ctx *context_service.Context, message, summary, details string) {
+ flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
+ "Message": message,
+ "Summary": summary,
+ "Details": utils.SanitizeFlashErrorString(details),
+ })
+ if err == nil {
+ ctx.JSONError(flashError)
+ } else {
+ log.Error("RenderToHTML: %v", err)
+ ctx.JSONError(message + "\n" + summary + "\n" + utils.SanitizeFlashErrorString(details))
+ }
+}
+
+func editorHandleFileOperationError(ctx *context_service.Context, targetBranchName string, err error) {
+ if errAs := util.ErrorAsLocale(err); errAs != nil {
+ ctx.JSONError(ctx.Tr(errAs.TrKey, errAs.TrArgs...))
+ } else if errAs, ok := errorAs[git.ErrNotExist](err); ok {
+ ctx.JSONError(ctx.Tr("repo.editor.file_modifying_no_longer_exists", errAs.RelPath))
+ } else if errAs, ok := errorAs[git_model.ErrLFSFileLocked](err); ok {
+ ctx.JSONError(ctx.Tr("repo.editor.upload_file_is_locked", errAs.Path, errAs.UserName))
+ } else if errAs, ok := errorAs[files_service.ErrFilenameInvalid](err); ok {
+ ctx.JSONError(ctx.Tr("repo.editor.filename_is_invalid", errAs.Path))
+ } else if errAs, ok := errorAs[files_service.ErrFilePathInvalid](err); ok {
+ switch errAs.Type {
+ case git.EntryModeSymlink:
+ ctx.JSONError(ctx.Tr("repo.editor.file_is_a_symlink", errAs.Path))
+ case git.EntryModeTree:
+ ctx.JSONError(ctx.Tr("repo.editor.filename_is_a_directory", errAs.Path))
+ case git.EntryModeBlob:
+ ctx.JSONError(ctx.Tr("repo.editor.directory_is_a_file", errAs.Path))
+ default:
+ ctx.JSONError(ctx.Tr("repo.editor.filename_is_invalid", errAs.Path))
+ }
+ } else if errAs, ok := errorAs[files_service.ErrRepoFileAlreadyExists](err); ok {
+ ctx.JSONError(ctx.Tr("repo.editor.file_already_exists", errAs.Path))
+ } else if errAs, ok := errorAs[git.ErrBranchNotExist](err); ok {
+ ctx.JSONError(ctx.Tr("repo.editor.branch_does_not_exist", errAs.Name))
+ } else if errAs, ok := errorAs[git_model.ErrBranchAlreadyExists](err); ok {
+ ctx.JSONError(ctx.Tr("repo.editor.branch_already_exists", errAs.BranchName))
+ } else if files_service.IsErrCommitIDDoesNotMatch(err) {
+ ctx.JSONError(ctx.Tr("repo.editor.commit_id_not_matching"))
+ } else if files_service.IsErrCommitIDDoesNotMatch(err) || git.IsErrPushOutOfDate(err) {
+ ctx.JSONError(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(ctx.Repo.CommitID)+"..."+util.PathEscapeSegments(targetBranchName)))
+ } else if errAs, ok := errorAs[*git.ErrPushRejected](err); ok {
+ if errAs.Message == "" {
+ ctx.JSONError(ctx.Tr("repo.editor.push_rejected_no_message"))
+ } else {
+ editorHandleFileOperationErrorRender(ctx, ctx.Locale.TrString("repo.editor.push_rejected"), ctx.Locale.TrString("repo.editor.push_rejected_summary"), errAs.Message)
+ }
+ } else if errors.Is(err, util.ErrNotExist) {
+ ctx.JSONError(ctx.Tr("error.not_found"))
+ } else {
+ setting.PanicInDevOrTesting("unclear err %T: %v", err, err)
+ editorHandleFileOperationErrorRender(ctx, ctx.Locale.TrString("repo.editor.failed_to_commit"), ctx.Locale.TrString("repo.editor.failed_to_commit_summary"), err.Error())
+ }
+}
diff --git a/routers/web/repo/editor_fork.go b/routers/web/repo/editor_fork.go
new file mode 100644
index 0000000000..b78a634c00
--- /dev/null
+++ b/routers/web/repo/editor_fork.go
@@ -0,0 +1,31 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "net/http"
+
+ "code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/services/context"
+ repo_service "code.gitea.io/gitea/services/repository"
+)
+
+const tplEditorFork templates.TplName = "repo/editor/fork"
+
+func ForkToEdit(ctx *context.Context) {
+ ctx.HTML(http.StatusOK, tplEditorFork)
+}
+
+func ForkToEditPost(ctx *context.Context) {
+ ForkRepoTo(ctx, ctx.Doer, repo_service.ForkRepoOptions{
+ BaseRepo: ctx.Repo.Repository,
+ Name: getUniqueRepositoryName(ctx, ctx.Doer.ID, ctx.Repo.Repository.Name),
+ Description: ctx.Repo.Repository.Description,
+ SingleBranch: ctx.Repo.Repository.DefaultBranch, // maybe we only need the default branch in the fork?
+ })
+ if ctx.Written() {
+ return
+ }
+ ctx.JSONRedirect("") // reload the page, the new fork should be editable now
+}
diff --git a/routers/web/repo/editor_preview.go b/routers/web/repo/editor_preview.go
new file mode 100644
index 0000000000..14be5b72b6
--- /dev/null
+++ b/routers/web/repo/editor_preview.go
@@ -0,0 +1,41 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "net/http"
+
+ "code.gitea.io/gitea/services/context"
+ files_service "code.gitea.io/gitea/services/repository/files"
+)
+
+func DiffPreviewPost(ctx *context.Context) {
+ content := ctx.FormString("content")
+ treePath := files_service.CleanGitTreePath(ctx.Repo.TreePath)
+ if treePath == "" {
+ ctx.HTTPError(http.StatusBadRequest, "file name to diff is invalid")
+ return
+ }
+
+ entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath)
+ if err != nil {
+ ctx.ServerError("GetTreeEntryByPath", err)
+ return
+ } else if entry.IsDir() {
+ ctx.HTTPError(http.StatusUnprocessableEntity)
+ return
+ }
+
+ diff, err := files_service.GetDiffPreview(ctx, ctx.Repo.Repository, ctx.Repo.BranchName, treePath, content)
+ if err != nil {
+ ctx.ServerError("GetDiffPreview", err)
+ return
+ }
+
+ if len(diff.Files) != 0 {
+ ctx.Data["File"] = diff.Files[0]
+ }
+
+ ctx.HTML(http.StatusOK, tplEditDiffPreview)
+}
diff --git a/routers/web/repo/editor_test.go b/routers/web/repo/editor_test.go
index 566db31693..6e2c1d6219 100644
--- a/routers/web/repo/editor_test.go
+++ b/routers/web/repo/editor_test.go
@@ -6,76 +6,27 @@ package repo
import (
"testing"
+ repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
- "code.gitea.io/gitea/services/contexttest"
"github.com/stretchr/testify/assert"
)
-func TestCleanUploadName(t *testing.T) {
+func TestEditorUtils(t *testing.T) {
unittest.PrepareTestEnv(t)
-
- kases := map[string]string{
- ".git/refs/master": "",
- "/root/abc": "root/abc",
- "./../../abc": "abc",
- "a/../.git": "",
- "a/../../../abc": "abc",
- "../../../acd": "acd",
- "../../.git/abc": "",
- "..\\..\\.git/abc": "..\\..\\.git/abc",
- "..\\../.git/abc": "",
- "..\\../.git": "",
- "abc/../def": "def",
- ".drone.yml": ".drone.yml",
- ".abc/def/.drone.yml": ".abc/def/.drone.yml",
- "..drone.yml.": "..drone.yml.",
- "..a.dotty...name...": "..a.dotty...name...",
- "..a.dotty../.folder../.name...": "..a.dotty../.folder../.name...",
- }
- for k, v := range kases {
- assert.EqualValues(t, cleanUploadFileName(k), v)
- }
-}
-
-func TestGetUniquePatchBranchName(t *testing.T) {
- unittest.PrepareTestEnv(t)
- ctx, _ := contexttest.MockContext(t, "user2/repo1")
- ctx.SetPathParam("id", "1")
- contexttest.LoadRepo(t, ctx, 1)
- contexttest.LoadRepoCommit(t, ctx)
- contexttest.LoadUser(t, ctx, 2)
- contexttest.LoadGitRepo(t, ctx)
- defer ctx.Repo.GitRepo.Close()
-
- expectedBranchName := "user2-patch-1"
- branchName := GetUniquePatchBranchName(ctx)
- assert.Equal(t, expectedBranchName, branchName)
-}
-
-func TestGetClosestParentWithFiles(t *testing.T) {
- unittest.PrepareTestEnv(t)
- ctx, _ := contexttest.MockContext(t, "user2/repo1")
- ctx.SetPathParam("id", "1")
- contexttest.LoadRepo(t, ctx, 1)
- contexttest.LoadRepoCommit(t, ctx)
- contexttest.LoadUser(t, ctx, 2)
- contexttest.LoadGitRepo(t, ctx)
- defer ctx.Repo.GitRepo.Close()
-
- repo := ctx.Repo.Repository
- branch := repo.DefaultBranch
- gitRepo, _ := gitrepo.OpenRepository(git.DefaultContext, repo)
- defer gitRepo.Close()
- commit, _ := gitRepo.GetBranchCommit(branch)
- var expectedTreePath string // Should return the root dir, empty string, since there are no subdirs in this repo
- for _, deletedFile := range []string{
- "dir1/dir2/dir3/file.txt",
- "file.txt",
- } {
- treePath := GetClosestParentWithFiles(deletedFile, commit)
- assert.Equal(t, expectedTreePath, treePath)
- }
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ t.Run("getUniquePatchBranchName", func(t *testing.T) {
+ branchName := getUniquePatchBranchName(t.Context(), "user2", repo)
+ assert.Equal(t, "user2-patch-1", branchName)
+ })
+ t.Run("getClosestParentWithFiles", func(t *testing.T) {
+ gitRepo, _ := gitrepo.OpenRepository(git.DefaultContext, repo)
+ defer gitRepo.Close()
+ treePath := getClosestParentWithFiles(gitRepo, "sub-home-md-img-check", "docs/foo/bar")
+ assert.Equal(t, "docs", treePath)
+ treePath = getClosestParentWithFiles(gitRepo, "sub-home-md-img-check", "any/other")
+ assert.Empty(t, treePath)
+ })
}
diff --git a/routers/web/repo/editor_uploader.go b/routers/web/repo/editor_uploader.go
new file mode 100644
index 0000000000..1ce9a1aca4
--- /dev/null
+++ b/routers/web/repo/editor_uploader.go
@@ -0,0 +1,61 @@
+// 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/setting"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/context/upload"
+ files_service "code.gitea.io/gitea/services/repository/files"
+)
+
+// UploadFileToServer upload file to server file dir not git
+func UploadFileToServer(ctx *context.Context) {
+ file, header, err := ctx.Req.FormFile("file")
+ if err != nil {
+ ctx.ServerError("FormFile", err)
+ return
+ }
+ defer file.Close()
+
+ buf := make([]byte, 1024)
+ n, _ := util.ReadAtMost(file, buf)
+ if n > 0 {
+ buf = buf[:n]
+ }
+
+ err = upload.Verify(buf, header.Filename, setting.Repository.Upload.AllowedTypes)
+ if err != nil {
+ ctx.HTTPError(http.StatusBadRequest, err.Error())
+ return
+ }
+
+ name := files_service.CleanGitTreePath(header.Filename)
+ if len(name) == 0 {
+ ctx.HTTPError(http.StatusBadRequest, "Upload file name is invalid")
+ return
+ }
+
+ uploaded, err := repo_model.NewUpload(ctx, name, buf, file)
+ if err != nil {
+ ctx.ServerError("NewUpload", err)
+ return
+ }
+
+ ctx.JSON(http.StatusOK, map[string]string{"uuid": uploaded.UUID})
+}
+
+// RemoveUploadFileFromServer remove file from server file dir
+func RemoveUploadFileFromServer(ctx *context.Context) {
+ fileUUID := ctx.FormString("file")
+ if err := repo_model.DeleteUploadByUUID(ctx, fileUUID); err != nil {
+ ctx.ServerError("DeleteUploadByUUID", err)
+ return
+ }
+ ctx.Status(http.StatusNoContent)
+}
diff --git a/routers/web/repo/editor_util.go b/routers/web/repo/editor_util.go
new file mode 100644
index 0000000000..f910f0bd40
--- /dev/null
+++ b/routers/web/repo/editor_util.go
@@ -0,0 +1,110 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "context"
+ "fmt"
+ "path"
+ "strings"
+
+ git_model "code.gitea.io/gitea/models/git"
+ repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/log"
+ repo_module "code.gitea.io/gitea/modules/repository"
+ context_service "code.gitea.io/gitea/services/context"
+)
+
+// getUniquePatchBranchName Gets a unique branch name for a new patch branch
+// It will be in the form of <username>-patch-<num> where <num> is the first branch of this format
+// that doesn't already exist. If we exceed 1000 tries or an error is thrown, we just return "" so the user has to
+// type in the branch name themselves (will be an empty field)
+func getUniquePatchBranchName(ctx context.Context, prefixName string, repo *repo_model.Repository) string {
+ prefix := prefixName + "-patch-"
+ for i := 1; i <= 1000; i++ {
+ branchName := fmt.Sprintf("%s%d", prefix, i)
+ if exist, err := git_model.IsBranchExist(ctx, repo.ID, branchName); err != nil {
+ log.Error("getUniquePatchBranchName: %v", err)
+ return ""
+ } else if !exist {
+ return branchName
+ }
+ }
+ return ""
+}
+
+// getClosestParentWithFiles Recursively gets the closest path of parent in a tree that has files when a file in a tree is
+// deleted. It returns "" for the tree root if no parents other than the root have files.
+func getClosestParentWithFiles(gitRepo *git.Repository, branchName, originTreePath string) string {
+ var f func(treePath string, commit *git.Commit) string
+ f = func(treePath string, commit *git.Commit) string {
+ if treePath == "" || treePath == "." {
+ return ""
+ }
+ // see if the tree has entries
+ if tree, err := commit.SubTree(treePath); err != nil {
+ return f(path.Dir(treePath), commit) // failed to get the tree, going up a dir
+ } else if entries, err := tree.ListEntries(); err != nil || len(entries) == 0 {
+ return f(path.Dir(treePath), commit) // no files in this dir, going up a dir
+ }
+ return treePath
+ }
+ commit, err := gitRepo.GetBranchCommit(branchName) // must get the commit again to get the latest change
+ if err != nil {
+ log.Error("GetBranchCommit: %v", err)
+ return ""
+ }
+ return f(originTreePath, commit)
+}
+
+// getContextRepoEditorConfig returns the editorconfig JSON string for given treePath or "null"
+func getContextRepoEditorConfig(ctx *context_service.Context, treePath string) string {
+ ec, _, err := ctx.Repo.GetEditorconfig()
+ if err == nil {
+ def, err := ec.GetDefinitionForFilename(treePath)
+ if err == nil {
+ jsonStr, _ := json.Marshal(def)
+ return string(jsonStr)
+ }
+ }
+ return "null"
+}
+
+// getParentTreeFields returns list of parent tree names and corresponding tree paths based on given treePath.
+// eg: []{"a", "b", "c"}, []{"a", "a/b", "a/b/c"}
+// or: []{""}, []{""} for the root treePath
+func getParentTreeFields(treePath string) (treeNames, treePaths []string) {
+ treeNames = strings.Split(treePath, "/")
+ treePaths = make([]string, len(treeNames))
+ for i := range treeNames {
+ treePaths[i] = strings.Join(treeNames[:i+1], "/")
+ }
+ return treeNames, treePaths
+}
+
+// getUniqueRepositoryName Gets a unique repository name for a user
+// It will append a -<num> postfix if the name is already taken
+func getUniqueRepositoryName(ctx context.Context, ownerID int64, name string) string {
+ uniqueName := name
+ for i := 1; i < 1000; i++ {
+ _, err := repo_model.GetRepositoryByName(ctx, ownerID, uniqueName)
+ if err != nil || repo_model.IsErrRepoNotExist(err) {
+ return uniqueName
+ }
+ uniqueName = fmt.Sprintf("%s-%d", name, i)
+ i++
+ }
+ return ""
+}
+
+func editorPushBranchToForkedRepository(ctx context.Context, doer *user_model.User, baseRepo *repo_model.Repository, baseBranchName string, targetRepo *repo_model.Repository, targetBranchName string) error {
+ return git.Push(ctx, baseRepo.RepoPath(), git.PushOptions{
+ Remote: targetRepo.RepoPath(),
+ Branch: baseBranchName + ":" + targetBranchName,
+ Env: repo_module.PushingEnvironment(doer, targetRepo),
+ })
+}
diff --git a/routers/web/repo/fork.go b/routers/web/repo/fork.go
index 786b5d7e43..c2694e540f 100644
--- a/routers/web/repo/fork.go
+++ b/routers/web/repo/fork.go
@@ -37,7 +37,7 @@ func getForkRepository(ctx *context.Context) *repo_model.Repository {
if forkRepo.IsEmpty {
log.Trace("Empty repository %-v", forkRepo)
- ctx.NotFound("getForkRepository", nil)
+ ctx.NotFound(nil)
return nil
}
@@ -91,12 +91,17 @@ func getForkRepository(ctx *context.Context) *repo_model.Repository {
ctx.Data["CanForkToUser"] = canForkToUser
ctx.Data["Orgs"] = orgs
+ // TODO: this message should only be shown for the "current doer" when it is selected, just like the "new repo" page.
+ // msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", ctx.Doer.MaxCreationLimit())
+
if canForkToUser {
ctx.Data["ContextUser"] = ctx.Doer
+ ctx.Data["CanForkRepoInNewOwner"] = true
} else if len(orgs) > 0 {
ctx.Data["ContextUser"] = orgs[0]
+ ctx.Data["CanForkRepoInNewOwner"] = true
} else {
- ctx.Data["CanForkRepo"] = false
+ ctx.Data["CanForkRepoInNewOwner"] = false
ctx.Flash.Error(ctx.Tr("repo.fork_no_valid_owners"), true)
return nil
}
@@ -120,15 +125,6 @@ func getForkRepository(ctx *context.Context) *repo_model.Repository {
// Fork render repository fork page
func Fork(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("new_fork")
-
- if ctx.Doer.CanForkRepo() {
- ctx.Data["CanForkRepo"] = true
- } else {
- maxCreationLimit := ctx.Doer.MaxCreationLimit()
- msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
- ctx.Flash.Error(msg, true)
- }
-
getForkRepository(ctx)
if ctx.Written() {
return
@@ -141,7 +137,6 @@ func Fork(ctx *context.Context) {
func ForkPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateRepoForm)
ctx.Data["Title"] = ctx.Tr("new_fork")
- ctx.Data["CanForkRepo"] = true
ctxUser := checkContextUser(ctx, form.UID)
if ctx.Written() {
@@ -156,7 +151,7 @@ func ForkPost(ctx *context.Context) {
ctx.Data["ContextUser"] = ctxUser
if ctx.HasError() {
- ctx.HTML(http.StatusOK, tplFork)
+ ctx.JSONError(ctx.GetErrMsg())
return
}
@@ -164,12 +159,12 @@ func ForkPost(ctx *context.Context) {
traverseParentRepo := forkRepo
for {
if !repository.CanUserForkBetweenOwners(ctxUser.ID, traverseParentRepo.OwnerID) {
- ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
+ ctx.JSONError(ctx.Tr("repo.settings.new_owner_has_same_repo"))
return
}
repo := repo_model.GetForkedRepo(ctx, ctxUser.ID, traverseParentRepo.ID)
if repo != nil {
- ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
+ ctx.JSONRedirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
return
}
if !traverseParentRepo.IsFork {
@@ -189,49 +184,55 @@ func ForkPost(ctx *context.Context) {
ctx.ServerError("CanCreateOrgRepo", err)
return
} else if !isAllowedToFork {
- ctx.Error(http.StatusForbidden)
+ ctx.HTTPError(http.StatusForbidden)
return
}
}
- repo, err := repo_service.ForkRepository(ctx, ctx.Doer, ctxUser, repo_service.ForkRepoOptions{
+ repo := ForkRepoTo(ctx, ctxUser, repo_service.ForkRepoOptions{
BaseRepo: forkRepo,
Name: form.RepoName,
Description: form.Description,
SingleBranch: form.ForkSingleBranch,
})
+ if ctx.Written() {
+ return
+ }
+ ctx.JSONRedirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
+}
+
+func ForkRepoTo(ctx *context.Context, owner *user_model.User, forkOpts repo_service.ForkRepoOptions) *repo_model.Repository {
+ repo, err := repo_service.ForkRepository(ctx, ctx.Doer, owner, forkOpts)
if err != nil {
ctx.Data["Err_RepoName"] = true
switch {
case repo_model.IsErrReachLimitOfRepo(err):
- maxCreationLimit := ctxUser.MaxCreationLimit()
+ maxCreationLimit := owner.MaxCreationLimit()
msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
- ctx.RenderWithErr(msg, tplFork, &form)
+ ctx.JSONError(msg)
case repo_model.IsErrRepoAlreadyExist(err):
- ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
+ ctx.JSONError(ctx.Tr("repo.settings.new_owner_has_same_repo"))
case repo_model.IsErrRepoFilesAlreadyExist(err):
switch {
case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
- ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplFork, form)
+ ctx.JSONError(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"))
case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
- ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplFork, form)
+ ctx.JSONError(ctx.Tr("form.repository_files_already_exist.adopt"))
case setting.Repository.AllowDeleteOfUnadoptedRepositories:
- ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplFork, form)
+ ctx.JSONError(ctx.Tr("form.repository_files_already_exist.delete"))
default:
- ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplFork, form)
+ ctx.JSONError(ctx.Tr("form.repository_files_already_exist"))
}
case db.IsErrNameReserved(err):
- ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplFork, &form)
+ ctx.JSONError(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name))
case db.IsErrNamePatternNotAllowed(err):
- ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplFork, &form)
+ ctx.JSONError(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern))
case errors.Is(err, user_model.ErrBlockedUser):
- ctx.RenderWithErr(ctx.Tr("repo.fork.blocked_user"), tplFork, form)
+ ctx.JSONError(ctx.Tr("repo.fork.blocked_user"))
default:
ctx.ServerError("ForkPost", err)
}
- return
+ return nil
}
-
- log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name)
- ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
+ return repo
}
diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go
index 6b2a7fd076..deb3ae4f3a 100644
--- a/routers/web/repo/githttp.go
+++ b/routers/web/repo/githttp.go
@@ -13,6 +13,7 @@ import (
"os"
"path/filepath"
"regexp"
+ "slices"
"strconv"
"strings"
"sync"
@@ -29,7 +30,6 @@ import (
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
- "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
repo_service "code.gitea.io/gitea/services/repository"
@@ -78,7 +78,7 @@ func httpBase(ctx *context.Context) *serviceHandler {
strings.HasSuffix(ctx.Req.URL.Path, "git-upload-archive") {
isPull = true
} else {
- isPull = ctx.Req.Method == "GET"
+ isPull = ctx.Req.Method == http.MethodHead || ctx.Req.Method == http.MethodGet
}
var accessMode perm.AccessMode
@@ -127,7 +127,7 @@ func httpBase(ctx *context.Context) *serviceHandler {
// Only public pull don't need auth.
isPublicPull := repoExist && !repo.IsPrivate && isPull
var (
- askAuth = !isPublicPull || setting.Service.RequireSignInView
+ askAuth = !isPublicPull || setting.Service.RequireSignInViewStrict
environ []string
)
@@ -147,7 +147,7 @@ func httpBase(ctx *context.Context) *serviceHandler {
if !ctx.IsSigned {
// TODO: support digit auth - which would be Authorization header with digit
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea"`)
- ctx.Error(http.StatusUnauthorized)
+ ctx.HTTPError(http.StatusUnauthorized)
return nil
}
@@ -303,24 +303,19 @@ var (
func dummyInfoRefs(ctx *context.Context) {
infoRefsOnce.Do(func() {
- tmpDir, err := os.MkdirTemp(os.TempDir(), "gitea-info-refs-cache")
+ tmpDir, cleanup, err := setting.AppDataTempDir("git-repo-content").MkdirTempRandom("gitea-info-refs-cache")
if err != nil {
log.Error("Failed to create temp dir for git-receive-pack cache: %v", err)
return
}
-
- defer func() {
- if err := util.RemoveAll(tmpDir); err != nil {
- log.Error("RemoveAll: %v", err)
- }
- }()
+ defer cleanup()
if err := git.InitRepository(ctx, tmpDir, true, git.Sha1ObjectFormat.Name()); err != nil {
log.Error("Failed to init bare repo for git-receive-pack cache: %v", err)
return
}
- refs, _, err := git.NewCommand(ctx, "receive-pack", "--stateless-rpc", "--advertise-refs", ".").RunStdBytes(&git.RunOpts{Dir: tmpDir})
+ refs, _, err := git.NewCommand("receive-pack", "--stateless-rpc", "--advertise-refs", ".").RunStdBytes(ctx, &git.RunOpts{Dir: tmpDir})
if err != nil {
log.Error(fmt.Sprintf("%v - %s", err, string(refs)))
}
@@ -360,8 +355,8 @@ func setHeaderNoCache(ctx *context.Context) {
func setHeaderCacheForever(ctx *context.Context) {
now := time.Now().Unix()
expires := now + 31536000
- ctx.Resp.Header().Set("Date", fmt.Sprintf("%d", now))
- ctx.Resp.Header().Set("Expires", fmt.Sprintf("%d", expires))
+ ctx.Resp.Header().Set("Date", strconv.FormatInt(now, 10))
+ ctx.Resp.Header().Set("Expires", strconv.FormatInt(expires, 10))
ctx.Resp.Header().Set("Cache-Control", "public, max-age=31536000")
}
@@ -369,12 +364,7 @@ func containsParentDirectorySeparator(v string) bool {
if !strings.Contains(v, "..") {
return false
}
- for _, ent := range strings.FieldsFunc(v, isSlashRune) {
- if ent == ".." {
- return true
- }
- }
- return false
+ return slices.Contains(strings.FieldsFunc(v, isSlashRune), "..")
}
func isSlashRune(r rune) bool { return r == '/' || r == '\\' }
@@ -394,7 +384,7 @@ func (h *serviceHandler) sendFile(ctx *context.Context, contentType, file string
}
ctx.Resp.Header().Set("Content-Type", contentType)
- ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", fi.Size()))
+ ctx.Resp.Header().Set("Content-Length", strconv.FormatInt(fi.Size(), 10))
// http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat
ctx.Resp.Header().Set("Last-Modified", fi.ModTime().UTC().Format(http.TimeFormat))
http.ServeFile(ctx.Resp, ctx.Req, reqFile)
@@ -403,12 +393,12 @@ func (h *serviceHandler) sendFile(ctx *context.Context, contentType, file string
// one or more key=value pairs separated by colons
var safeGitProtocolHeader = regexp.MustCompile(`^[0-9a-zA-Z]+=[0-9a-zA-Z]+(:[0-9a-zA-Z]+=[0-9a-zA-Z]+)*$`)
-func prepareGitCmdWithAllowedService(ctx *context.Context, service string) (*git.Command, error) {
+func prepareGitCmdWithAllowedService(service string) (*git.Command, error) {
if service == "receive-pack" {
- return git.NewCommand(ctx, "receive-pack"), nil
+ return git.NewCommand("receive-pack"), nil
}
if service == "upload-pack" {
- return git.NewCommand(ctx, "upload-pack"), nil
+ return git.NewCommand("upload-pack"), nil
}
return nil, fmt.Errorf("service %q is not allowed", service)
@@ -428,7 +418,7 @@ func serviceRPC(ctx *context.Context, h *serviceHandler, service string) {
return
}
- cmd, err := prepareGitCmdWithAllowedService(ctx, service)
+ cmd, err := prepareGitCmdWithAllowedService(service)
if err != nil {
log.Error("Failed to prepareGitCmdWithService: %v", err)
ctx.Resp.WriteHeader(http.StatusUnauthorized)
@@ -458,7 +448,7 @@ func serviceRPC(ctx *context.Context, h *serviceHandler, service string) {
var stderr bytes.Buffer
cmd.AddArguments("--stateless-rpc").AddDynamicArguments(h.getRepoDir())
- if err := cmd.Run(&git.RunOpts{
+ if err := cmd.Run(ctx, &git.RunOpts{
Dir: h.getRepoDir(),
Env: append(os.Environ(), h.environ...),
Stdout: ctx.Resp,
@@ -498,7 +488,7 @@ func getServiceType(ctx *context.Context) string {
}
func updateServerInfo(ctx gocontext.Context, dir string) []byte {
- out, _, err := git.NewCommand(ctx, "update-server-info").RunStdBytes(&git.RunOpts{Dir: dir})
+ out, _, err := git.NewCommand("update-server-info").RunStdBytes(ctx, &git.RunOpts{Dir: dir})
if err != nil {
log.Error(fmt.Sprintf("%v - %s", err, string(out)))
}
@@ -521,14 +511,14 @@ func GetInfoRefs(ctx *context.Context) {
}
setHeaderNoCache(ctx)
service := getServiceType(ctx)
- cmd, err := prepareGitCmdWithAllowedService(ctx, service)
+ cmd, err := prepareGitCmdWithAllowedService(service)
if err == nil {
if protocol := ctx.Req.Header.Get("Git-Protocol"); protocol != "" && safeGitProtocolHeader.MatchString(protocol) {
h.environ = append(h.environ, "GIT_PROTOCOL="+protocol)
}
h.environ = append(os.Environ(), h.environ...)
- refs, _, err := cmd.AddArguments("--stateless-rpc", "--advertise-refs", ".").RunStdBytes(&git.RunOpts{Env: h.environ, Dir: h.getRepoDir()})
+ refs, _, err := cmd.AddArguments("--stateless-rpc", "--advertise-refs", ".").RunStdBytes(ctx, &git.RunOpts{Env: h.environ, Dir: h.getRepoDir()})
if err != nil {
log.Error(fmt.Sprintf("%v - %s", err, string(refs)))
}
diff --git a/routers/web/repo/githttp_test.go b/routers/web/repo/githttp_test.go
index 5ba8de3d63..0164b11f66 100644
--- a/routers/web/repo/githttp_test.go
+++ b/routers/web/repo/githttp_test.go
@@ -37,6 +37,6 @@ func TestContainsParentDirectorySeparator(t *testing.T) {
}
for i := range tests {
- assert.EqualValues(t, tests[i].b, containsParentDirectorySeparator(tests[i].v))
+ assert.Equal(t, tests[i].b, containsParentDirectorySeparator(tests[i].v))
}
}
diff --git a/routers/web/repo/helper.go b/routers/web/repo/helper.go
index 3bf064c1e3..7b92cba0fd 100644
--- a/routers/web/repo/helper.go
+++ b/routers/web/repo/helper.go
@@ -12,7 +12,7 @@ func HandleGitError(ctx *context.Context, msg string, err error) {
if git.IsErrNotExist(err) {
ctx.Data["NotFoundPrompt"] = ctx.Locale.Tr("repo.tree_path_not_found", ctx.Repo.TreePath, ctx.Repo.RefTypeNameSubURL())
ctx.Data["NotFoundGoBackURL"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
- ctx.NotFound(msg, err)
+ ctx.NotFound(err)
} else {
ctx.ServerError(msg, err)
}
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index f150897a2d..54b7e5df2a 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -43,7 +43,8 @@ const (
tplIssueChoose templates.TplName = "repo/issue/choose"
tplIssueView templates.TplName = "repo/issue/view"
- tplReactions templates.TplName = "repo/issue/view_content/reactions"
+ tplPullMergeBox templates.TplName = "repo/issue/view_content/pull_merge_box"
+ tplReactions templates.TplName = "repo/issue/view_content/reactions"
issueTemplateKey = "IssueTemplate"
issueTemplateTitleKey = "IssueTemplateTitle"
@@ -91,7 +92,7 @@ func MustAllowUserComment(ctx *context.Context) {
func MustEnableIssues(ctx *context.Context) {
if !ctx.Repo.CanRead(unit.TypeIssues) &&
!ctx.Repo.CanRead(unit.TypeExternalTracker) {
- ctx.NotFound("MustEnableIssues", nil)
+ ctx.NotFound(nil)
return
}
@@ -105,7 +106,7 @@ func MustEnableIssues(ctx *context.Context) {
// MustAllowPulls check if repository enable pull requests and user have right to do that
func MustAllowPulls(ctx *context.Context) {
if !ctx.Repo.Repository.CanEnablePulls() || !ctx.Repo.CanRead(unit.TypePullRequests) {
- ctx.NotFound("MustAllowPulls", nil)
+ ctx.NotFound(nil)
return
}
@@ -201,7 +202,7 @@ func GetActionIssue(ctx *context.Context) *issues_model.Issue {
func checkIssueRights(ctx *context.Context, issue *issues_model.Issue) {
if issue.IsPull && !ctx.Repo.CanRead(unit.TypePullRequests) ||
!issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) {
- ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil)
+ ctx.NotFound(nil)
}
}
@@ -211,7 +212,7 @@ func getActionIssues(ctx *context.Context) issues_model.IssueList {
return nil
}
issueIDs := make([]int64, 0, 10)
- for _, stringIssueID := range strings.Split(commaSeparatedIssueIDs, ",") {
+ for stringIssueID := range strings.SplitSeq(commaSeparatedIssueIDs, ",") {
issueID, err := strconv.ParseInt(stringIssueID, 10, 64)
if err != nil {
ctx.ServerError("ParseInt", err)
@@ -229,11 +230,11 @@ func getActionIssues(ctx *context.Context) issues_model.IssueList {
prUnitEnabled := ctx.Repo.CanRead(unit.TypePullRequests)
for _, issue := range issues {
if issue.RepoID != ctx.Repo.Repository.ID {
- ctx.NotFound("some issue's RepoID is incorrect", errors.New("some issue's RepoID is incorrect"))
+ ctx.NotFound(errors.New("some issue's RepoID is incorrect"))
return nil
}
if issue.IsPull && !prUnitEnabled || !issue.IsPull && !issueUnitEnabled {
- ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil)
+ ctx.NotFound(nil)
return nil
}
if err = issue.LoadAttributes(ctx); err != nil {
@@ -249,9 +250,9 @@ func GetIssueInfo(ctx *context.Context) {
issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
} else {
- ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err.Error())
+ ctx.HTTPError(http.StatusInternalServerError, "GetIssueByIndex", err.Error())
}
return
}
@@ -259,13 +260,13 @@ func GetIssueInfo(ctx *context.Context) {
if issue.IsPull {
// Need to check if Pulls are enabled and we can read Pulls
if !ctx.Repo.Repository.CanEnablePulls() || !ctx.Repo.CanRead(unit.TypePullRequests) {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
} else {
// Need to check if Issues are enabled and we can read Issues
if !ctx.Repo.CanRead(unit.TypeIssues) {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
}
@@ -284,13 +285,13 @@ func UpdateIssueTitle(ctx *context.Context) {
}
if !ctx.IsSigned || (!issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) {
- ctx.Error(http.StatusForbidden)
+ ctx.HTTPError(http.StatusForbidden)
return
}
title := ctx.FormTrim("title")
if len(title) == 0 {
- ctx.Error(http.StatusNoContent)
+ ctx.HTTPError(http.StatusNoContent)
return
}
@@ -312,7 +313,7 @@ func UpdateIssueRef(ctx *context.Context) {
}
if !ctx.IsSigned || (!issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) || issue.IsPull {
- ctx.Error(http.StatusForbidden)
+ ctx.HTTPError(http.StatusForbidden)
return
}
@@ -336,7 +337,7 @@ func UpdateIssueContent(ctx *context.Context) {
}
if !ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) {
- ctx.Error(http.StatusForbidden)
+ ctx.HTTPError(http.StatusForbidden)
return
}
@@ -363,7 +364,9 @@ func UpdateIssueContent(ctx *context.Context) {
}
}
- rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository)
+ rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{
+ FootnoteContextID: "0",
+ })
content, err := markdown.RenderString(rctx, issue.Content)
if err != nil {
ctx.ServerError("RenderString", err)
@@ -382,21 +385,21 @@ func UpdateIssueDeadline(ctx *context.Context) {
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
- ctx.NotFound("GetIssueByIndex", err)
+ ctx.NotFound(err)
} else {
- ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err.Error())
+ ctx.HTTPError(http.StatusInternalServerError, "GetIssueByIndex", err.Error())
}
return
}
if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
- ctx.Error(http.StatusForbidden, "", "Not repo writer")
+ ctx.HTTPError(http.StatusForbidden, "", "Not repo writer")
return
}
deadlineUnix, _ := common.ParseDeadlineDateToEndOfDay(ctx.FormString("deadline"))
if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil {
- ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err.Error())
+ ctx.HTTPError(http.StatusInternalServerError, "UpdateIssueDeadline", err.Error())
return
}
@@ -417,6 +420,16 @@ func UpdateIssueMilestone(ctx *context.Context) {
continue
}
issue.MilestoneID = milestoneID
+ if milestoneID > 0 {
+ var err error
+ issue.Milestone, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID)
+ if err != nil {
+ ctx.ServerError("GetMilestoneByRepoID", err)
+ return
+ }
+ } else {
+ issue.Milestone = nil
+ }
if err := issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil {
ctx.ServerError("ChangeMilestoneAssign", err)
return
@@ -497,7 +510,7 @@ func ChangeIssueReaction(ctx *context.Context) {
}
}
- ctx.Error(http.StatusForbidden)
+ ctx.HTTPError(http.StatusForbidden)
return
}
@@ -540,7 +553,7 @@ func ChangeIssueReaction(ctx *context.Context) {
log.Trace("Reaction for issue removed: %d/%d", ctx.Repo.Repository.ID, issue.ID)
default:
- ctx.NotFound(fmt.Sprintf("Unknown action %s", ctx.PathParam("action")), nil)
+ ctx.NotFound(nil)
return
}
diff --git a/routers/web/repo/issue_comment.go b/routers/web/repo/issue_comment.go
index 8564c613de..c2a7f6b682 100644
--- a/routers/web/repo/issue_comment.go
+++ b/routers/web/repo/issue_comment.go
@@ -8,11 +8,13 @@ import (
"fmt"
"html/template"
"net/http"
+ "strconv"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/renderhelper"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup/markdown"
repo_module "code.gitea.io/gitea/modules/repository"
@@ -53,7 +55,7 @@ func NewComment(ctx *context.Context) {
}
}
- ctx.Error(http.StatusForbidden)
+ ctx.HTTPError(http.StatusForbidden)
return
}
@@ -95,7 +97,7 @@ func NewComment(ctx *context.Context) {
// Regenerate patch and test conflict.
if pr == nil {
issue.PullRequest.HeadCommitID = ""
- pull_service.AddToTaskQueue(ctx, issue.PullRequest)
+ pull_service.StartPullRequestCheckImmediately(ctx, issue.PullRequest)
}
// check whether the ref of PR <refs/pulls/pr_index/head> in base repo is consistent with the head commit of head branch in the head repo
@@ -117,7 +119,7 @@ func NewComment(ctx *context.Context) {
ctx.ServerError("Unable to load head repo", err)
return
}
- if ok := git.IsBranchExist(ctx, pull.HeadRepo.RepoPath(), pull.BaseBranch); !ok {
+ if ok := gitrepo.IsBranchExist(ctx, pull.HeadRepo, pull.BaseBranch); !ok {
// todo localize
ctx.JSONError("The origin branch is delete, cannot reopen.")
return
@@ -224,35 +226,42 @@ func UpdateCommentContent(ctx *context.Context) {
}
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
- ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
+ ctx.NotFound(issues_model.ErrCommentNotExist{})
return
}
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
- ctx.Error(http.StatusForbidden)
+ ctx.HTTPError(http.StatusForbidden)
return
}
if !comment.Type.HasContentSupport() {
- ctx.Error(http.StatusNoContent)
+ ctx.HTTPError(http.StatusNoContent)
return
}
- oldContent := comment.Content
newContent := ctx.FormString("content")
contentVersion := ctx.FormInt("content_version")
+ if contentVersion != comment.ContentVersion {
+ ctx.JSONError(ctx.Tr("repo.comments.edit.already_changed"))
+ return
+ }
- // allow to save empty content
- comment.Content = newContent
- if err = issue_service.UpdateComment(ctx, comment, contentVersion, ctx.Doer, oldContent); err != nil {
- if errors.Is(err, user_model.ErrBlockedUser) {
- ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
- } else if errors.Is(err, issues_model.ErrCommentAlreadyChanged) {
- ctx.JSONError(ctx.Tr("repo.comments.edit.already_changed"))
- } else {
- ctx.ServerError("UpdateComment", err)
+ if newContent != comment.Content {
+ // allow to save empty content
+ oldContent := comment.Content
+ comment.Content = newContent
+
+ if err = issue_service.UpdateComment(ctx, comment, contentVersion, ctx.Doer, oldContent); err != nil {
+ if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
+ } else if errors.Is(err, issues_model.ErrCommentAlreadyChanged) {
+ ctx.JSONError(ctx.Tr("repo.comments.edit.already_changed"))
+ } else {
+ ctx.ServerError("UpdateComment", err)
+ }
+ return
}
- return
}
if err := comment.LoadAttachments(ctx); err != nil {
@@ -270,7 +279,9 @@ func UpdateCommentContent(ctx *context.Context) {
var renderedContent template.HTML
if comment.Content != "" {
- rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository)
+ rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{
+ FootnoteContextID: strconv.FormatInt(comment.ID, 10),
+ })
renderedContent, err = markdown.RenderString(rctx, comment.Content)
if err != nil {
ctx.ServerError("RenderString", err)
@@ -302,15 +313,15 @@ func DeleteComment(ctx *context.Context) {
}
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
- ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
+ ctx.NotFound(issues_model.ErrCommentNotExist{})
return
}
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
- ctx.Error(http.StatusForbidden)
+ ctx.HTTPError(http.StatusForbidden)
return
} else if !comment.Type.HasContentSupport() {
- ctx.Error(http.StatusNoContent)
+ ctx.HTTPError(http.StatusNoContent)
return
}
@@ -337,7 +348,7 @@ func ChangeCommentReaction(ctx *context.Context) {
}
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
- ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
+ ctx.NotFound(issues_model.ErrCommentNotExist{})
return
}
@@ -360,12 +371,12 @@ func ChangeCommentReaction(ctx *context.Context) {
}
}
- ctx.Error(http.StatusForbidden)
+ ctx.HTTPError(http.StatusForbidden)
return
}
if !comment.Type.HasContentSupport() {
- ctx.Error(http.StatusNoContent)
+ ctx.HTTPError(http.StatusNoContent)
return
}
@@ -403,7 +414,7 @@ func ChangeCommentReaction(ctx *context.Context) {
log.Trace("Reaction for comment removed: %d/%d/%d", ctx.Repo.Repository.ID, comment.Issue.ID, comment.ID)
default:
- ctx.NotFound(fmt.Sprintf("Unknown action %s", ctx.PathParam("action")), nil)
+ ctx.NotFound(nil)
return
}
@@ -442,12 +453,12 @@ func GetCommentAttachments(ctx *context.Context) {
}
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
- ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
+ ctx.NotFound(issues_model.ErrCommentNotExist{})
return
}
if !ctx.Repo.Permission.CanReadIssuesOrPulls(comment.Issue.IsPull) {
- ctx.NotFound("CanReadIssuesOrPulls", issues_model.ErrCommentNotExist{})
+ ctx.NotFound(issues_model.ErrCommentNotExist{})
return
}
diff --git a/routers/web/repo/issue_content_history.go b/routers/web/repo/issue_content_history.go
index 13c6d89b6e..3602f4ec8a 100644
--- a/routers/web/repo/issue_content_history.go
+++ b/routers/web/repo/issue_content_history.go
@@ -157,15 +157,16 @@ func GetContentHistoryDetail(ctx *context.Context) {
diffHTMLBuf := bytes.Buffer{}
diffHTMLBuf.WriteString("<pre class='chroma'>")
for _, it := range diff {
- if it.Type == diffmatchpatch.DiffInsert {
+ switch it.Type {
+ case diffmatchpatch.DiffInsert:
diffHTMLBuf.WriteString("<span class='gi'>")
diffHTMLBuf.WriteString(html.EscapeString(it.Text))
diffHTMLBuf.WriteString("</span>")
- } else if it.Type == diffmatchpatch.DiffDelete {
+ case diffmatchpatch.DiffDelete:
diffHTMLBuf.WriteString("<span class='gd'>")
diffHTMLBuf.WriteString(html.EscapeString(it.Text))
diffHTMLBuf.WriteString("</span>")
- } else {
+ default:
diffHTMLBuf.WriteString(html.EscapeString(it.Text))
}
}
@@ -186,7 +187,7 @@ func SoftDeleteContentHistory(ctx *context.Context) {
return
}
if ctx.Doer == nil {
- ctx.NotFound("Require SignIn", nil)
+ ctx.NotFound(nil)
return
}
@@ -202,12 +203,12 @@ func SoftDeleteContentHistory(ctx *context.Context) {
return
}
if history.IssueID != issue.ID {
- ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
+ ctx.NotFound(issues_model.ErrCommentNotExist{})
return
}
if commentID != 0 {
if history.CommentID != commentID {
- ctx.NotFound("CompareCommentID", issues_model.ErrCommentNotExist{})
+ ctx.NotFound(issues_model.ErrCommentNotExist{})
return
}
@@ -216,7 +217,7 @@ func SoftDeleteContentHistory(ctx *context.Context) {
return
}
if comment.IssueID != issue.ID {
- ctx.NotFound("CompareIssueID", issues_model.ErrCommentNotExist{})
+ ctx.NotFound(issues_model.ErrCommentNotExist{})
return
}
}
diff --git a/routers/web/repo/issue_dependency.go b/routers/web/repo/issue_dependency.go
index 0f6787386d..73298958c0 100644
--- a/routers/web/repo/issue_dependency.go
+++ b/routers/web/repo/issue_dependency.go
@@ -23,7 +23,7 @@ func AddDependency(ctx *context.Context) {
// Check if the Repo is allowed to have dependencies
if !ctx.Repo.CanCreateIssueDependencies(ctx, ctx.Doer, issue.IsPull) {
- ctx.Error(http.StatusForbidden, "CanCreateIssueDependencies")
+ ctx.HTTPError(http.StatusForbidden, "CanCreateIssueDependencies")
return
}
@@ -97,7 +97,7 @@ func RemoveDependency(ctx *context.Context) {
// Check if the Repo is allowed to have dependencies
if !ctx.Repo.CanCreateIssueDependencies(ctx, ctx.Doer, issue.IsPull) {
- ctx.Error(http.StatusForbidden, "CanCreateIssueDependencies")
+ ctx.HTTPError(http.StatusForbidden, "CanCreateIssueDependencies")
return
}
@@ -119,7 +119,7 @@ func RemoveDependency(ctx *context.Context) {
case "blocking":
depType = issues_model.DependencyTypeBlocking
default:
- ctx.Error(http.StatusBadRequest, "GetDependecyType")
+ ctx.HTTPError(http.StatusBadRequest, "GetDependecyType")
return
}
diff --git a/routers/web/repo/issue_label.go b/routers/web/repo/issue_label.go
index 5ef6b09faa..72a316e98d 100644
--- a/routers/web/repo/issue_label.go
+++ b/routers/web/repo/issue_label.go
@@ -4,6 +4,7 @@
package repo
import (
+ "errors"
"net/http"
"code.gitea.io/gitea/models/db"
@@ -13,7 +14,9 @@ import (
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
+ shared_label "code.gitea.io/gitea/routers/web/shared/label"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
issue_service "code.gitea.io/gitea/services/issue"
@@ -100,54 +103,53 @@ func RetrieveLabelsForList(ctx *context.Context) {
// NewLabel create new label for repository
func NewLabel(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.CreateLabelForm)
- ctx.Data["Title"] = ctx.Tr("repo.labels")
- ctx.Data["PageIsLabels"] = true
-
- if ctx.HasError() {
- ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
- ctx.Redirect(ctx.Repo.RepoLink + "/labels")
+ form := shared_label.GetLabelEditForm(ctx)
+ if ctx.Written() {
return
}
l := &issues_model.Label{
- RepoID: ctx.Repo.Repository.ID,
- Name: form.Title,
- Exclusive: form.Exclusive,
- Description: form.Description,
- Color: form.Color,
+ RepoID: ctx.Repo.Repository.ID,
+ Name: form.Title,
+ Exclusive: form.Exclusive,
+ ExclusiveOrder: form.ExclusiveOrder,
+ Description: form.Description,
+ Color: form.Color,
}
if err := issues_model.NewLabel(ctx, l); err != nil {
ctx.ServerError("NewLabel", err)
return
}
- ctx.Redirect(ctx.Repo.RepoLink + "/labels")
+ ctx.JSONRedirect(ctx.Repo.RepoLink + "/labels")
}
// UpdateLabel update a label's name and color
func UpdateLabel(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.CreateLabelForm)
+ form := shared_label.GetLabelEditForm(ctx)
+ if ctx.Written() {
+ return
+ }
+
l, err := issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, form.ID)
- if err != nil {
- switch {
- case issues_model.IsErrRepoLabelNotExist(err):
- ctx.Error(http.StatusNotFound)
- default:
- ctx.ServerError("UpdateLabel", err)
- }
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.JSONErrorNotFound()
+ return
+ } else if err != nil {
+ ctx.ServerError("GetLabelInRepoByID", err)
return
}
+
l.Name = form.Title
l.Exclusive = form.Exclusive
+ l.ExclusiveOrder = form.ExclusiveOrder
l.Description = form.Description
l.Color = form.Color
-
l.SetArchived(form.IsArchived)
if err := issues_model.UpdateLabel(ctx, l); err != nil {
ctx.ServerError("UpdateLabel", err)
return
}
- ctx.Redirect(ctx.Repo.RepoLink + "/labels")
+ ctx.JSONRedirect(ctx.Repo.RepoLink + "/labels")
}
// DeleteLabel delete a label
@@ -180,7 +182,7 @@ func UpdateIssueLabel(ctx *context.Context) {
label, err := issues_model.GetLabelByID(ctx, ctx.FormInt64("id"))
if err != nil {
if issues_model.IsErrRepoLabelNotExist(err) {
- ctx.Error(http.StatusNotFound, "GetLabelByID")
+ ctx.HTTPError(http.StatusNotFound, "GetLabelByID")
} else {
ctx.ServerError("GetLabelByID", err)
}
@@ -221,7 +223,7 @@ func UpdateIssueLabel(ctx *context.Context) {
}
default:
log.Warn("Unrecognized action: %s", action)
- ctx.Error(http.StatusInternalServerError)
+ ctx.HTTPError(http.StatusInternalServerError)
return
}
diff --git a/routers/web/repo/issue_label_test.go b/routers/web/repo/issue_label_test.go
index 8a613e2c7e..f4eca26f8e 100644
--- a/routers/web/repo/issue_label_test.go
+++ b/routers/web/repo/issue_label_test.go
@@ -6,10 +6,12 @@ package repo
import (
"net/http"
"strconv"
+ "strings"
"testing"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/web"
@@ -19,26 +21,27 @@ import (
"github.com/stretchr/testify/assert"
)
-func int64SliceToCommaSeparated(a []int64) string {
- s := ""
- for i, n := range a {
- if i > 0 {
- s += ","
- }
- s += strconv.Itoa(int(n))
- }
- return s
+func TestIssueLabel(t *testing.T) {
+ unittest.PrepareTestEnv(t)
+ t.Run("RetrieveLabels", testRetrieveLabels)
+ t.Run("NewLabel", testNewLabel)
+ t.Run("NewLabelInvalidColor", testNewLabelInvalidColor)
+ t.Run("UpdateLabel", testUpdateLabel)
+ t.Run("UpdateLabelInvalidColor", testUpdateLabelInvalidColor)
+ t.Run("UpdateIssueLabelClear", testUpdateIssueLabelClear)
+ t.Run("UpdateIssueLabelToggle", testUpdateIssueLabelToggle)
+ t.Run("InitializeLabels", testInitializeLabels)
+ t.Run("DeleteLabel", testDeleteLabel)
}
-func TestInitializeLabels(t *testing.T) {
- unittest.PrepareTestEnv(t)
+func testInitializeLabels(t *testing.T) {
assert.NoError(t, repository.LoadRepoConfig())
ctx, _ := contexttest.MockContext(t, "user2/repo1/labels/initialize")
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 2)
web.SetForm(ctx, &forms.InitializeLabelsForm{TemplateName: "Default"})
InitializeLabels(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &issues_model.Label{
RepoID: 2,
Name: "enhancement",
@@ -47,8 +50,7 @@ func TestInitializeLabels(t *testing.T) {
assert.Equal(t, "/user2/repo2/labels", test.RedirectURL(ctx.Resp))
}
-func TestRetrieveLabels(t *testing.T) {
- unittest.PrepareTestEnv(t)
+func testRetrieveLabels(t *testing.T) {
for _, testCase := range []struct {
RepoID int64
Sort string
@@ -68,15 +70,14 @@ func TestRetrieveLabels(t *testing.T) {
assert.True(t, ok)
if assert.Len(t, labels, len(testCase.ExpectedLabelIDs)) {
for i, label := range labels {
- assert.EqualValues(t, testCase.ExpectedLabelIDs[i], label.ID)
+ assert.Equal(t, testCase.ExpectedLabelIDs[i], label.ID)
}
}
}
}
-func TestNewLabel(t *testing.T) {
- unittest.PrepareTestEnv(t)
- ctx, _ := contexttest.MockContext(t, "user2/repo1/labels/edit")
+func testNewLabel(t *testing.T) {
+ ctx, respWriter := contexttest.MockContext(t, "user2/repo1/labels/edit")
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
web.SetForm(ctx, &forms.CreateLabelForm{
@@ -84,17 +85,32 @@ func TestNewLabel(t *testing.T) {
Color: "#abcdef",
})
NewLabel(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &issues_model.Label{
Name: "newlabel",
Color: "#abcdef",
})
- assert.Equal(t, "/user2/repo1/labels", test.RedirectURL(ctx.Resp))
+ assert.Equal(t, "/user2/repo1/labels", test.RedirectURL(respWriter))
}
-func TestUpdateLabel(t *testing.T) {
- unittest.PrepareTestEnv(t)
- ctx, _ := contexttest.MockContext(t, "user2/repo1/labels/edit")
+func testNewLabelInvalidColor(t *testing.T) {
+ ctx, respWriter := contexttest.MockContext(t, "user2/repo1/labels/edit")
+ contexttest.LoadUser(t, ctx, 2)
+ contexttest.LoadRepo(t, ctx, 1)
+ web.SetForm(ctx, &forms.CreateLabelForm{
+ Title: "newlabel-x",
+ Color: "bad-label-code",
+ })
+ NewLabel(ctx)
+ assert.Equal(t, http.StatusBadRequest, ctx.Resp.WrittenStatus())
+ assert.Equal(t, "repo.issues.label_color_invalid", test.ParseJSONError(respWriter.Body.Bytes()).ErrorMessage)
+ unittest.AssertNotExistsBean(t, &issues_model.Label{
+ Name: "newlabel-x",
+ })
+}
+
+func testUpdateLabel(t *testing.T) {
+ ctx, respWriter := contexttest.MockContext(t, "user2/repo1/labels/edit")
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
web.SetForm(ctx, &forms.CreateLabelForm{
@@ -104,43 +120,62 @@ func TestUpdateLabel(t *testing.T) {
IsArchived: true,
})
UpdateLabel(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &issues_model.Label{
ID: 2,
Name: "newnameforlabel",
Color: "#abcdef",
})
- assert.Equal(t, "/user2/repo1/labels", test.RedirectURL(ctx.Resp))
+ assert.Equal(t, "/user2/repo1/labels", test.RedirectURL(respWriter))
}
-func TestDeleteLabel(t *testing.T) {
- unittest.PrepareTestEnv(t)
+func testUpdateLabelInvalidColor(t *testing.T) {
+ ctx, respWriter := contexttest.MockContext(t, "user2/repo1/labels/edit")
+ contexttest.LoadUser(t, ctx, 2)
+ contexttest.LoadRepo(t, ctx, 1)
+ web.SetForm(ctx, &forms.CreateLabelForm{
+ ID: 1,
+ Title: "label1",
+ Color: "bad-label-code",
+ })
+
+ UpdateLabel(ctx)
+
+ assert.Equal(t, http.StatusBadRequest, ctx.Resp.WrittenStatus())
+ assert.Equal(t, "repo.issues.label_color_invalid", test.ParseJSONError(respWriter.Body.Bytes()).ErrorMessage)
+ unittest.AssertExistsAndLoadBean(t, &issues_model.Label{
+ ID: 1,
+ Name: "label1",
+ Color: "#abcdef",
+ })
+}
+
+func testDeleteLabel(t *testing.T) {
ctx, _ := contexttest.MockContext(t, "user2/repo1/labels/delete")
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
ctx.Req.Form.Set("id", "2")
DeleteLabel(ctx)
- assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+ assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
unittest.AssertNotExistsBean(t, &issues_model.Label{ID: 2})
unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{LabelID: 2})
assert.EqualValues(t, ctx.Tr("repo.issues.label_deletion_success"), ctx.Flash.SuccessMsg)
}
-func TestUpdateIssueLabel_Clear(t *testing.T) {
- unittest.PrepareTestEnv(t)
+func testUpdateIssueLabelClear(t *testing.T) {
ctx, _ := contexttest.MockContext(t, "user2/repo1/issues/labels")
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
ctx.Req.Form.Set("issue_ids", "1,3")
ctx.Req.Form.Set("action", "clear")
UpdateIssueLabel(ctx)
- assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+ assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: 1})
unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: 3})
unittest.CheckConsistencyFor(t, &issues_model.Label{})
}
-func TestUpdateIssueLabel_Toggle(t *testing.T) {
+func testUpdateIssueLabelToggle(t *testing.T) {
for _, testCase := range []struct {
Action string
IssueIDs []int64
@@ -156,11 +191,12 @@ func TestUpdateIssueLabel_Toggle(t *testing.T) {
ctx, _ := contexttest.MockContext(t, "user2/repo1/issues/labels")
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
- ctx.Req.Form.Set("issue_ids", int64SliceToCommaSeparated(testCase.IssueIDs))
+
+ ctx.Req.Form.Set("issue_ids", strings.Join(base.Int64sToStrings(testCase.IssueIDs), ","))
ctx.Req.Form.Set("action", testCase.Action)
ctx.Req.Form.Set("id", strconv.Itoa(int(testCase.LabelID)))
UpdateIssueLabel(ctx)
- assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+ assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
for _, issueID := range testCase.IssueIDs {
if testCase.ExpectedAdd {
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: testCase.LabelID})
diff --git a/routers/web/repo/issue_list.go b/routers/web/repo/issue_list.go
index 2f615a100e..b55f4bcc90 100644
--- a/routers/web/repo/issue_list.go
+++ b/routers/web/repo/issue_list.go
@@ -5,8 +5,10 @@ package repo
import (
"bytes"
- "fmt"
+ "maps"
"net/http"
+ "slices"
+ "sort"
"strconv"
"strings"
@@ -18,6 +20,7 @@ import (
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
+ db_indexer "code.gitea.io/gitea/modules/indexer/issues/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
@@ -30,14 +33,6 @@ import (
pull_service "code.gitea.io/gitea/services/pull"
)
-func issueIDsFromSearch(ctx *context.Context, keyword string, opts *issues_model.IssuesOptions) ([]int64, error) {
- ids, _, err := issue_indexer.SearchIssues(ctx, issue_indexer.ToSearchOptions(keyword, opts))
- if err != nil {
- return nil, fmt.Errorf("SearchIssues: %w", err)
- }
- return ids, nil
-}
-
func retrieveProjectsForIssueList(ctx *context.Context, repo *repo_model.Repository) {
ctx.Data["OpenProjects"], ctx.Data["ClosedProjects"] = retrieveProjectsInternal(ctx, repo)
}
@@ -46,7 +41,7 @@ func retrieveProjectsForIssueList(ctx *context.Context, repo *repo_model.Reposit
func SearchIssues(ctx *context.Context) {
before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
- ctx.Error(http.StatusUnprocessableEntity, err.Error())
+ ctx.HTTPError(http.StatusUnprocessableEntity, err.Error())
return
}
@@ -66,7 +61,7 @@ func SearchIssues(ctx *context.Context) {
)
{
// find repos user can access (for issue search)
- opts := &repo_model.SearchRepoOptions{
+ opts := repo_model.SearchRepoOptions{
Private: false,
AllPublic: true,
TopicOnly: false,
@@ -84,9 +79,9 @@ func SearchIssues(ctx *context.Context) {
owner, err := user_model.GetUserByName(ctx, ctx.FormString("owner"))
if err != nil {
if user_model.IsErrUserNotExist(err) {
- ctx.Error(http.StatusBadRequest, "Owner not found", err.Error())
+ ctx.HTTPError(http.StatusBadRequest, "Owner not found", err.Error())
} else {
- ctx.Error(http.StatusInternalServerError, "GetUserByName", err.Error())
+ ctx.HTTPError(http.StatusInternalServerError, "GetUserByName", err.Error())
}
return
}
@@ -97,15 +92,15 @@ func SearchIssues(ctx *context.Context) {
}
if ctx.FormString("team") != "" {
if ctx.FormString("owner") == "" {
- ctx.Error(http.StatusBadRequest, "", "Owner organisation is required for filtering on team")
+ ctx.HTTPError(http.StatusBadRequest, "", "Owner organisation is required for filtering on team")
return
}
team, err := organization.GetTeam(ctx, opts.OwnerID, ctx.FormString("team"))
if err != nil {
if organization.IsErrTeamNotExist(err) {
- ctx.Error(http.StatusBadRequest, "Team not found", err.Error())
+ ctx.HTTPError(http.StatusBadRequest, "Team not found", err.Error())
} else {
- ctx.Error(http.StatusInternalServerError, "GetUserByName", err.Error())
+ ctx.HTTPError(http.StatusInternalServerError, "GetUserByName", err.Error())
}
return
}
@@ -118,7 +113,7 @@ func SearchIssues(ctx *context.Context) {
}
repoIDs, _, err = repo_model.SearchRepositoryIDs(ctx, opts)
if err != nil {
- ctx.Error(http.StatusInternalServerError, "SearchRepositoryIDs", err.Error())
+ ctx.HTTPError(http.StatusInternalServerError, "SearchRepositoryIDs", err.Error())
return
}
if len(repoIDs) == 0 {
@@ -149,7 +144,7 @@ func SearchIssues(ctx *context.Context) {
}
includedAnyLabels, err = issues_model.GetLabelIDsByNames(ctx, includedLabelNames)
if err != nil {
- ctx.Error(http.StatusInternalServerError, "GetLabelIDsByNames", err.Error())
+ ctx.HTTPError(http.StatusInternalServerError, "GetLabelIDsByNames", err.Error())
return
}
}
@@ -163,7 +158,7 @@ func SearchIssues(ctx *context.Context) {
}
includedMilestones, err = issues_model.GetMilestoneIDsByNames(ctx, includedMilestoneNames)
if err != nil {
- ctx.Error(http.StatusInternalServerError, "GetMilestoneIDsByNames", err.Error())
+ ctx.HTTPError(http.StatusInternalServerError, "GetMilestoneIDsByNames", err.Error())
return
}
}
@@ -208,10 +203,10 @@ func SearchIssues(ctx *context.Context) {
if ctx.IsSigned {
ctxUserID := ctx.Doer.ID
if ctx.FormBool("created") {
- searchOpt.PosterID = optional.Some(ctxUserID)
+ searchOpt.PosterID = strconv.FormatInt(ctxUserID, 10)
}
if ctx.FormBool("assigned") {
- searchOpt.AssigneeID = optional.Some(ctxUserID)
+ searchOpt.AssigneeID = strconv.FormatInt(ctxUserID, 10)
}
if ctx.FormBool("mentioned") {
searchOpt.MentionID = optional.Some(ctxUserID)
@@ -230,12 +225,12 @@ func SearchIssues(ctx *context.Context) {
ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
if err != nil {
- ctx.Error(http.StatusInternalServerError, "SearchIssues", err.Error())
+ ctx.HTTPError(http.StatusInternalServerError, "SearchIssues", err.Error())
return
}
issues, err := issues_model.GetIssuesByIDs(ctx, ids, true)
if err != nil {
- ctx.Error(http.StatusInternalServerError, "FindIssuesByIDs", err.Error())
+ ctx.HTTPError(http.StatusInternalServerError, "FindIssuesByIDs", err.Error())
return
}
@@ -251,12 +246,12 @@ func getUserIDForFilter(ctx *context.Context, queryName string) int64 {
user, err := user_model.GetUserByName(ctx, userName)
if user_model.IsErrUserNotExist(err) {
- ctx.NotFound("", err)
+ ctx.NotFound(err)
return 0
}
if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.HTTPError(http.StatusInternalServerError, err.Error())
return 0
}
@@ -269,7 +264,7 @@ func getUserIDForFilter(ctx *context.Context, queryName string) int64 {
func SearchRepoIssuesJSON(ctx *context.Context) {
before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil {
- ctx.Error(http.StatusUnprocessableEntity, err.Error())
+ ctx.HTTPError(http.StatusUnprocessableEntity, err.Error())
return
}
@@ -299,7 +294,7 @@ func SearchRepoIssuesJSON(ctx *context.Context) {
continue
}
if !issues_model.IsErrMilestoneNotExist(err) {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.HTTPError(http.StatusInternalServerError, err.Error())
return
}
id, err := strconv.ParseInt(part[i], 10, 64)
@@ -314,7 +309,7 @@ func SearchRepoIssuesJSON(ctx *context.Context) {
if issues_model.IsErrMilestoneNotExist(err) {
continue
}
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.HTTPError(http.StatusInternalServerError, err.Error())
}
}
@@ -373,10 +368,10 @@ func SearchRepoIssuesJSON(ctx *context.Context) {
}
if createdByID > 0 {
- searchOpt.PosterID = optional.Some(createdByID)
+ searchOpt.PosterID = strconv.FormatInt(createdByID, 10)
}
if assignedByID > 0 {
- searchOpt.AssigneeID = optional.Some(assignedByID)
+ searchOpt.AssigneeID = strconv.FormatInt(assignedByID, 10)
}
if mentionedByID > 0 {
searchOpt.MentionID = optional.Some(mentionedByID)
@@ -384,12 +379,12 @@ func SearchRepoIssuesJSON(ctx *context.Context) {
ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
if err != nil {
- ctx.Error(http.StatusInternalServerError, "SearchIssues", err.Error())
+ ctx.HTTPError(http.StatusInternalServerError, "SearchIssues", err.Error())
return
}
issues, err := issues_model.GetIssuesByIDs(ctx, ids, true)
if err != nil {
- ctx.Error(http.StatusInternalServerError, "FindIssuesByIDs", err.Error())
+ ctx.HTTPError(http.StatusInternalServerError, "FindIssuesByIDs", err.Error())
return
}
@@ -459,6 +454,19 @@ func UpdateIssueStatus(ctx *context.Context) {
ctx.JSONOK()
}
+func prepareIssueFilterExclusiveOrderScopes(ctx *context.Context, allLabels []*issues_model.Label) {
+ scopeSet := make(map[string]bool)
+ for _, label := range allLabels {
+ scope := label.ExclusiveScope()
+ if len(scope) > 0 && label.ExclusiveOrder > 0 {
+ scopeSet[scope] = true
+ }
+ }
+ scopes := slices.Collect(maps.Keys(scopeSet))
+ sort.Strings(scopes)
+ ctx.Data["ExclusiveLabelScopes"] = scopes
+}
+
func renderMilestones(ctx *context.Context) {
// Get milestones
milestones, err := db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
@@ -481,7 +489,7 @@ func renderMilestones(ctx *context.Context) {
ctx.Data["ClosedMilestones"] = closedMilestones
}
-func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption optional.Option[bool]) {
+func prepareIssueFilterAndList(ctx *context.Context, milestoneID, projectID int64, isPullOption optional.Option[bool]) {
var err error
viewType := ctx.FormString("type")
sortType := ctx.FormString("sort")
@@ -490,7 +498,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
viewType = "all"
}
- assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future
+ assigneeID := ctx.FormString("assignee")
posterUsername := ctx.FormString("poster")
posterUserID := shared_user.GetFilterUserIDByName(ctx, posterUsername)
var mentionedID, reviewRequestedID, reviewedID int64
@@ -498,11 +506,11 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
if ctx.IsSigned {
switch viewType {
case "created_by":
- posterUserID = optional.Some(ctx.Doer.ID)
+ posterUserID = strconv.FormatInt(ctx.Doer.ID, 10)
case "mentioned":
mentionedID = ctx.Doer.ID
case "assigned":
- assigneeID = ctx.Doer.ID
+ assigneeID = strconv.FormatInt(ctx.Doer.ID, 10)
case "review_requested":
reviewRequestedID = ctx.Doer.ID
case "reviewed_by":
@@ -521,18 +529,21 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
mileIDs = []int64{milestoneID}
}
- labelIDs := issue.PrepareFilterIssueLabels(ctx, repo.ID, ctx.Repo.Owner)
+ preparedLabelFilter := issue.PrepareFilterIssueLabels(ctx, repo.ID, ctx.Repo.Owner)
if ctx.Written() {
return
}
+ prepareIssueFilterExclusiveOrderScopes(ctx, preparedLabelFilter.AllLabels)
+
+ var keywordMatchedIssueIDs []int64
var issueStats *issues_model.IssueStats
statsOpts := &issues_model.IssuesOptions{
RepoIDs: []int64{repo.ID},
- LabelIDs: labelIDs,
+ LabelIDs: preparedLabelFilter.SelectedLabelIDs,
MilestoneIDs: mileIDs,
ProjectID: projectID,
- AssigneeID: optional.Some(assigneeID),
+ AssigneeID: assigneeID,
MentionedID: mentionedID,
PosterID: posterUserID,
ReviewRequestedID: reviewRequestedID,
@@ -541,7 +552,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
IssueIDs: nil,
}
if keyword != "" {
- allIssueIDs, err := issueIDsFromSearch(ctx, keyword, statsOpts)
+ keywordMatchedIssueIDs, _, err = issue_indexer.SearchIssues(ctx, issue_indexer.ToSearchOptions(keyword, statsOpts))
if err != nil {
if issue_indexer.IsAvailable(ctx) {
ctx.ServerError("issueIDsFromSearch", err)
@@ -550,14 +561,17 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
ctx.Data["IssueIndexerUnavailable"] = true
return
}
- statsOpts.IssueIDs = allIssueIDs
+ if len(keywordMatchedIssueIDs) == 0 {
+ // It did search with the keyword, but no issue found, just set issueStats to empty, then no need to do query again.
+ issueStats = &issues_model.IssueStats{}
+ // set keywordMatchedIssueIDs to empty slice, so we can distinguish it from "nil"
+ keywordMatchedIssueIDs = []int64{}
+ }
+ statsOpts.IssueIDs = keywordMatchedIssueIDs
}
- if keyword != "" && len(statsOpts.IssueIDs) == 0 {
- // So it did search with the keyword, but no issue found.
- // Just set issueStats to empty.
- issueStats = &issues_model.IssueStats{}
- } else {
- // So it did search with the keyword, and found some issues. It needs to get issueStats of these issues.
+
+ if issueStats == nil {
+ // Either it did search with the keyword, and found some issues, it needs to get issueStats of these issues.
// Or the keyword is empty, so it doesn't need issueIDs as filter, just get issueStats with statsOpts.
issueStats, err = issues_model.GetIssueStats(ctx, statsOpts)
if err != nil {
@@ -589,31 +603,27 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
ctx.Data["TotalTrackedTime"] = totalTrackedTime
}
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
-
- var total int
- switch {
- case isShowClosed.Value():
- total = int(issueStats.ClosedCount)
- case !isShowClosed.Has():
- total = int(issueStats.OpenCount + issueStats.ClosedCount)
- default:
- total = int(issueStats.OpenCount)
+ // prepare pager
+ total := int(issueStats.OpenCount + issueStats.ClosedCount)
+ if isShowClosed.Has() {
+ total = util.Iif(isShowClosed.Value(), int(issueStats.ClosedCount), int(issueStats.OpenCount))
}
+ page := max(ctx.FormInt("page"), 1)
pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5)
+ // prepare real issue list:
var issues issues_model.IssueList
- {
- ids, err := issueIDsFromSearch(ctx, keyword, &issues_model.IssuesOptions{
+ if keywordMatchedIssueIDs == nil || len(keywordMatchedIssueIDs) > 0 {
+ // Either it did search with the keyword, and found some issues, then keywordMatchedIssueIDs is not null, it needs to use db indexer.
+ // Or the keyword is empty, it also needs to usd db indexer.
+ // In either case, no need to use keyword anymore
+ searchResult, err := db_indexer.GetIndexer().FindWithIssueOptions(ctx, &issues_model.IssuesOptions{
Paginator: &db.ListOptions{
Page: pager.Paginater.Current(),
PageSize: setting.UI.IssuePagingNum,
},
RepoIDs: []int64{repo.ID},
- AssigneeID: optional.Some(assigneeID),
+ AssigneeID: assigneeID,
PosterID: posterUserID,
MentionedID: mentionedID,
ReviewRequestedID: reviewRequestedID,
@@ -622,18 +632,16 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
ProjectID: projectID,
IsClosed: isShowClosed,
IsPull: isPullOption,
- LabelIDs: labelIDs,
+ LabelIDs: preparedLabelFilter.SelectedLabelIDs,
SortType: sortType,
+ IssueIDs: keywordMatchedIssueIDs,
})
if err != nil {
- if issue_indexer.IsAvailable(ctx) {
- ctx.ServerError("issueIDsFromSearch", err)
- return
- }
- ctx.Data["IssueIndexerUnavailable"] = true
+ ctx.ServerError("DBIndexer.Search", err)
return
}
- issues, err = issues_model.GetIssuesByIDs(ctx, ids, true)
+ issueIDs := issue_indexer.SearchResultToIDSlice(searchResult)
+ issues, err = issues_model.GetIssuesByIDs(ctx, issueIDs, true)
if err != nil {
ctx.ServerError("GetIssuesByIDs", err)
return
@@ -696,9 +704,10 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
return 0
}
reviewTyp := issues_model.ReviewTypeApprove
- if typ == "reject" {
+ switch typ {
+ case "reject":
reviewTyp = issues_model.ReviewTypeReject
- } else if typ == "waiting" {
+ case "waiting":
reviewTyp = issues_model.ReviewTypeRequest
}
for _, count := range counts {
@@ -727,7 +736,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
ctx.Data["IssueStats"] = issueStats
ctx.Data["OpenCount"] = issueStats.OpenCount
ctx.Data["ClosedCount"] = issueStats.ClosedCount
- ctx.Data["SelLabelIDs"] = labelIDs
+ ctx.Data["SelLabelIDs"] = preparedLabelFilter.SelectedLabelIDs
ctx.Data["ViewType"] = viewType
ctx.Data["SortType"] = sortType
ctx.Data["MilestoneID"] = milestoneID
@@ -768,7 +777,7 @@ func Issues(ctx *context.Context) {
ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo)
}
- issues(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), optional.Some(isPullList))
+ prepareIssueFilterAndList(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), optional.Some(isPullList))
if ctx.Written() {
return
}
diff --git a/routers/web/repo/issue_lock.go b/routers/web/repo/issue_lock.go
index 1d5fc8a5f3..bc8aabd90b 100644
--- a/routers/web/repo/issue_lock.go
+++ b/routers/web/repo/issue_lock.go
@@ -24,11 +24,6 @@ func LockIssue(ctx *context.Context) {
return
}
- if !form.HasValidReason() {
- ctx.JSONError(ctx.Tr("repo.issues.lock.unknown_reason"))
- return
- }
-
if err := issues_model.LockIssue(ctx, &issues_model.IssueLockOptions{
Doer: ctx.Doer,
Issue: issue,
diff --git a/routers/web/repo/issue_new.go b/routers/web/repo/issue_new.go
index 32daa3e48f..887019b146 100644
--- a/routers/web/repo/issue_new.go
+++ b/routers/web/repo/issue_new.go
@@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"html/template"
+ "maps"
"net/http"
"slices"
"sort"
@@ -136,9 +137,7 @@ func NewIssue(ctx *context.Context) {
ret := issue_service.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
templateLoaded, errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates, pageMetaData)
- for k, v := range errs {
- ret.TemplateErrors[k] = v
- }
+ maps.Copy(ret.TemplateErrors, errs)
if ctx.Written() {
return
}
@@ -223,11 +222,11 @@ func DeleteIssue(ctx *context.Context) {
}
if issue.IsPull {
- ctx.Redirect(fmt.Sprintf("%s/pulls", ctx.Repo.Repository.Link()), http.StatusSeeOther)
+ ctx.Redirect(ctx.Repo.Repository.Link()+"/pulls", http.StatusSeeOther)
return
}
- ctx.Redirect(fmt.Sprintf("%s/issues", ctx.Repo.Repository.Link()), http.StatusSeeOther)
+ ctx.Redirect(ctx.Repo.Repository.Link()+"/issues", http.StatusSeeOther)
}
func toSet[ItemType any, KeyType comparable](slice []ItemType, keyFunc func(ItemType) KeyType) container.Set[KeyType] {
@@ -255,7 +254,7 @@ func ValidateRepoMetasForNewIssue(ctx *context.Context, form forms.CreateIssueFo
inputLabelIDs, _ := base.StringsToInt64s(strings.Split(form.LabelIDs, ","))
candidateLabels := toSet(pageMetaData.LabelsData.AllLabels, func(label *issues_model.Label) int64 { return label.ID })
if len(inputLabelIDs) > 0 && !candidateLabels.Contains(inputLabelIDs...) {
- ctx.NotFound("", nil)
+ ctx.NotFound(nil)
return ret
}
pageMetaData.LabelsData.SetSelectedLabelIDs(inputLabelIDs)
@@ -263,7 +262,7 @@ func ValidateRepoMetasForNewIssue(ctx *context.Context, form forms.CreateIssueFo
allMilestones := append(slices.Clone(pageMetaData.MilestonesData.OpenMilestones), pageMetaData.MilestonesData.ClosedMilestones...)
candidateMilestones := toSet(allMilestones, func(milestone *issues_model.Milestone) int64 { return milestone.ID })
if form.MilestoneID > 0 && !candidateMilestones.Contains(form.MilestoneID) {
- ctx.NotFound("", nil)
+ ctx.NotFound(nil)
return ret
}
pageMetaData.MilestonesData.SelectedMilestoneID = form.MilestoneID
@@ -271,18 +270,21 @@ func ValidateRepoMetasForNewIssue(ctx *context.Context, form forms.CreateIssueFo
allProjects := append(slices.Clone(pageMetaData.ProjectsData.OpenProjects), pageMetaData.ProjectsData.ClosedProjects...)
candidateProjects := toSet(allProjects, func(project *project_model.Project) int64 { return project.ID })
if form.ProjectID > 0 && !candidateProjects.Contains(form.ProjectID) {
- ctx.NotFound("", nil)
+ ctx.NotFound(nil)
return ret
}
pageMetaData.ProjectsData.SelectedProjectID = form.ProjectID
+ // prepare assignees
candidateAssignees := toSet(pageMetaData.AssigneesData.CandidateAssignees, func(user *user_model.User) int64 { return user.ID })
inputAssigneeIDs, _ := base.StringsToInt64s(strings.Split(form.AssigneeIDs, ","))
- if len(inputAssigneeIDs) > 0 && !candidateAssignees.Contains(inputAssigneeIDs...) {
- ctx.NotFound("", nil)
- return ret
+ var assigneeIDStrings []string
+ for _, inputAssigneeID := range inputAssigneeIDs {
+ if candidateAssignees.Contains(inputAssigneeID) {
+ assigneeIDStrings = append(assigneeIDStrings, strconv.FormatInt(inputAssigneeID, 10))
+ }
}
- pageMetaData.AssigneesData.SelectedAssigneeIDs = form.AssigneeIDs
+ pageMetaData.AssigneesData.SelectedAssigneeIDs = strings.Join(assigneeIDStrings, ",")
// Check if the passed reviewers (user/team) actually exist
var reviewers []*user_model.User
@@ -301,14 +303,14 @@ func ValidateRepoMetasForNewIssue(ctx *context.Context, form forms.CreateIssueFo
if rID < 0 { // negative reviewIDs represent team requests
team, ok := teamReviewersMap[-rID]
if !ok {
- ctx.NotFound("", nil)
+ ctx.NotFound(nil)
return ret
}
teamReviewers = append(teamReviewers, team)
} else {
user, ok := userReviewersMap[rID]
if !ok {
- ctx.NotFound("", nil)
+ ctx.NotFound(nil)
return ret
}
reviewers = append(reviewers, user)
@@ -346,7 +348,7 @@ func NewIssuePost(ctx *context.Context) {
if projectID > 0 {
if !ctx.Repo.CanRead(unit.TypeProjects) {
// User must also be able to see the project.
- ctx.Error(http.StatusBadRequest, "user hasn't permissions to read projects")
+ ctx.HTTPError(http.StatusBadRequest, "user hasn't permissions to read projects")
return
}
}
@@ -385,7 +387,7 @@ func NewIssuePost(ctx *context.Context) {
if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs, projectID); err != nil {
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
- ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
+ ctx.HTTPError(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
} else if errors.Is(err, user_model.ErrBlockedUser) {
ctx.JSONError(ctx.Tr("repo.issues.new.blocked_user"))
} else {
diff --git a/routers/web/repo/issue_page_meta.go b/routers/web/repo/issue_page_meta.go
index 272343f460..93cc38bffa 100644
--- a/routers/web/repo/issue_page_meta.go
+++ b/routers/web/repo/issue_page_meta.go
@@ -79,27 +79,35 @@ func retrieveRepoIssueMetaData(ctx *context.Context, repo *repo_model.Repository
return data
}
- data.CanModifyIssueOrPull = ctx.Repo.CanWriteIssuesOrPulls(isPull) && !ctx.Repo.Repository.IsArchived
- if !data.CanModifyIssueOrPull {
+ // it sets "Branches" template data,
+ // it is used to render the "edit PR target branches" dropdown, and the "branch selector" in the issue's sidebar.
+ PrepareBranchList(ctx)
+ if ctx.Written() {
return data
}
- data.retrieveAssigneesDataForIssueWriter(ctx)
+ // it sets the "Assignees" template data, and the data is also used to "mention" users.
+ data.retrieveAssigneesData(ctx)
if ctx.Written() {
return data
}
- data.retrieveMilestonesDataForIssueWriter(ctx)
- if ctx.Written() {
+ // TODO: the issue/pull permissions are quite complex and unclear
+ // A reader could create an issue/PR with setting some meta (eg: assignees from issue template, reviewers, target branch)
+ // A reader(creator) could update some meta (eg: target branch), but can't change assignees anymore.
+ // For non-creator users, only writers could update some meta (eg: assignees, milestone, project)
+ // Need to clarify the logic and add some tests in the future
+ data.CanModifyIssueOrPull = ctx.Repo.CanWriteIssuesOrPulls(isPull) && !ctx.Repo.Repository.IsArchived
+ if !data.CanModifyIssueOrPull {
return data
}
- data.retrieveProjectsDataForIssueWriter(ctx)
+ data.retrieveMilestonesDataForIssueWriter(ctx)
if ctx.Written() {
return data
}
- PrepareBranchList(ctx)
+ data.retrieveProjectsDataForIssueWriter(ctx)
if ctx.Written() {
return data
}
@@ -131,7 +139,7 @@ func (d *IssuePageMetaData) retrieveMilestonesDataForIssueWriter(ctx *context.Co
}
}
-func (d *IssuePageMetaData) retrieveAssigneesDataForIssueWriter(ctx *context.Context) {
+func (d *IssuePageMetaData) retrieveAssigneesData(ctx *context.Context) {
var err error
d.AssigneesData.CandidateAssignees, err = repo_model.GetRepoAssignees(ctx, d.Repository)
if err != nil {
diff --git a/routers/web/repo/issue_pin.go b/routers/web/repo/issue_pin.go
index d7d3205c37..8d3de90d25 100644
--- a/routers/web/repo/issue_pin.go
+++ b/routers/web/repo/issue_pin.go
@@ -6,6 +6,7 @@ package repo
import (
"net/http"
+ "code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
@@ -22,15 +23,29 @@ func IssuePinOrUnpin(ctx *context.Context) {
// If we don't do this, it will crash when trying to add the pin event to the comment history
err := issue.LoadRepo(ctx)
if err != nil {
- ctx.Status(http.StatusInternalServerError)
- log.Error(err.Error())
+ ctx.ServerError("LoadRepo", err)
return
}
- err = issue.PinOrUnpin(ctx, ctx.Doer)
+ // PinOrUnpin pins or unpins a Issue
+ _, err = issues_model.GetIssuePin(ctx, issue)
+ if err != nil && !db.IsErrNotExist(err) {
+ ctx.ServerError("GetIssuePin", err)
+ return
+ }
+
+ if db.IsErrNotExist(err) {
+ err = issues_model.PinIssue(ctx, issue, ctx.Doer)
+ } else {
+ err = issues_model.UnpinIssue(ctx, issue, ctx.Doer)
+ }
+
if err != nil {
- ctx.Status(http.StatusInternalServerError)
- log.Error(err.Error())
+ if issues_model.IsErrIssueMaxPinReached(err) {
+ ctx.JSONError(ctx.Tr("repo.issues.max_pinned"))
+ } else {
+ ctx.ServerError("Pin/Unpin failed", err)
+ }
return
}
@@ -41,23 +56,20 @@ func IssuePinOrUnpin(ctx *context.Context) {
func IssueUnpin(ctx *context.Context) {
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
- ctx.Status(http.StatusInternalServerError)
- log.Error(err.Error())
+ ctx.ServerError("GetIssueByIndex", err)
return
}
// If we don't do this, it will crash when trying to add the pin event to the comment history
err = issue.LoadRepo(ctx)
if err != nil {
- ctx.Status(http.StatusInternalServerError)
- log.Error(err.Error())
+ ctx.ServerError("LoadRepo", err)
return
}
- err = issue.Unpin(ctx, ctx.Doer)
+ err = issues_model.UnpinIssue(ctx, issue, ctx.Doer)
if err != nil {
- ctx.Status(http.StatusInternalServerError)
- log.Error(err.Error())
+ ctx.ServerError("UnpinIssue", err)
return
}
@@ -78,15 +90,13 @@ func IssuePinMove(ctx *context.Context) {
form := &movePinIssueForm{}
if err := json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil {
- ctx.Status(http.StatusInternalServerError)
- log.Error(err.Error())
+ ctx.ServerError("Decode", err)
return
}
issue, err := issues_model.GetIssueByID(ctx, form.ID)
if err != nil {
- ctx.Status(http.StatusInternalServerError)
- log.Error(err.Error())
+ ctx.ServerError("GetIssueByID", err)
return
}
@@ -96,10 +106,9 @@ func IssuePinMove(ctx *context.Context) {
return
}
- err = issue.MovePin(ctx, form.Position)
+ err = issues_model.MovePin(ctx, issue, form.Position)
if err != nil {
- ctx.Status(http.StatusInternalServerError)
- log.Error(err.Error())
+ ctx.ServerError("MovePin", err)
return
}
diff --git a/routers/web/repo/issue_stopwatch.go b/routers/web/repo/issue_stopwatch.go
index 5d6e723311..2de3a7cfec 100644
--- a/routers/web/repo/issue_stopwatch.go
+++ b/routers/web/repo/issue_stopwatch.go
@@ -4,41 +4,53 @@
package repo
import (
- "strings"
-
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/modules/eventsource"
"code.gitea.io/gitea/services/context"
)
-// IssueStopwatch creates or stops a stopwatch for the given issue.
-func IssueStopwatch(c *context.Context) {
+// IssueStartStopwatch creates a stopwatch for the given issue.
+func IssueStartStopwatch(c *context.Context) {
issue := GetActionIssue(c)
if c.Written() {
return
}
- var showSuccessMessage bool
-
- if !issues_model.StopwatchExists(c, c.Doer.ID, issue.ID) {
- showSuccessMessage = true
+ if !c.Repo.CanUseTimetracker(c, issue, c.Doer) {
+ c.NotFound(nil)
+ return
}
- if !c.Repo.CanUseTimetracker(c, issue, c.Doer) {
- c.NotFound("CanUseTimetracker", nil)
+ if ok, err := issues_model.CreateIssueStopwatch(c, c.Doer, issue); err != nil {
+ c.ServerError("CreateIssueStopwatch", err)
return
+ } else if !ok {
+ c.Flash.Warning(c.Tr("repo.issues.stopwatch_already_created"))
+ } else {
+ c.Flash.Success(c.Tr("repo.issues.tracker_auto_close"))
}
+ c.JSONRedirect("")
+}
- if err := issues_model.CreateOrStopIssueStopwatch(c, c.Doer, issue); err != nil {
- c.ServerError("CreateOrStopIssueStopwatch", err)
+// IssueStopStopwatch stops a stopwatch for the given issue.
+func IssueStopStopwatch(c *context.Context) {
+ issue := GetActionIssue(c)
+ if c.Written() {
return
}
- if showSuccessMessage {
- c.Flash.Success(c.Tr("repo.issues.tracker_auto_close"))
+ if !c.Repo.CanUseTimetracker(c, issue, c.Doer) {
+ c.NotFound(nil)
+ return
}
+ if ok, err := issues_model.FinishIssueStopwatch(c, c.Doer, issue); err != nil {
+ c.ServerError("FinishIssueStopwatch", err)
+ return
+ } else if !ok {
+ c.Flash.Warning(c.Tr("repo.issues.stopwatch_already_stopped"))
+ }
c.JSONRedirect("")
}
@@ -49,11 +61,11 @@ func CancelStopwatch(c *context.Context) {
return
}
if !c.Repo.CanUseTimetracker(c, issue, c.Doer) {
- c.NotFound("CanUseTimetracker", nil)
+ c.NotFound(nil)
return
}
- if err := issues_model.CancelStopwatch(c, c.Doer, issue); err != nil {
+ if _, err := issues_model.CancelStopwatch(c, c.Doer, issue); err != nil {
c.ServerError("CancelStopwatch", err)
return
}
@@ -72,39 +84,3 @@ func CancelStopwatch(c *context.Context) {
c.JSONRedirect("")
}
-
-// GetActiveStopwatch is the middleware that sets .ActiveStopwatch on context
-func GetActiveStopwatch(ctx *context.Context) {
- if strings.HasPrefix(ctx.Req.URL.Path, "/api") {
- return
- }
-
- if !ctx.IsSigned {
- return
- }
-
- _, sw, issue, err := issues_model.HasUserStopwatch(ctx, ctx.Doer.ID)
- if err != nil {
- ctx.ServerError("HasUserStopwatch", err)
- return
- }
-
- if sw == nil || sw.ID == 0 {
- return
- }
-
- ctx.Data["ActiveStopwatch"] = StopwatchTmplInfo{
- issue.Link(),
- issue.Repo.FullName(),
- issue.Index,
- sw.Seconds() + 1, // ensure time is never zero in ui
- }
-}
-
-// StopwatchTmplInfo is a view on a stopwatch specifically for template rendering
-type StopwatchTmplInfo struct {
- IssueLink string
- RepoSlug string
- IssueIndex int64
- Seconds int64
-}
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_timetrack.go b/routers/web/repo/issue_timetrack.go
index 134ded82d1..985bfd6698 100644
--- a/routers/web/repo/issue_timetrack.go
+++ b/routers/web/repo/issue_timetrack.go
@@ -25,7 +25,7 @@ func AddTimeManually(c *context.Context) {
return
}
if !c.Repo.CanUseTimetracker(c, issue, c.Doer) {
- c.NotFound("CanUseTimetracker", nil)
+ c.NotFound(nil)
return
}
@@ -56,23 +56,23 @@ func DeleteTime(c *context.Context) {
return
}
if !c.Repo.CanUseTimetracker(c, issue, c.Doer) {
- c.NotFound("CanUseTimetracker", nil)
+ c.NotFound(nil)
return
}
t, err := issues_model.GetTrackedTimeByID(c, c.PathParamInt64("timeid"))
if err != nil {
if db.IsErrNotExist(err) {
- c.NotFound("time not found", err)
+ c.NotFound(err)
return
}
- c.Error(http.StatusInternalServerError, "GetTrackedTimeByID", err.Error())
+ c.HTTPError(http.StatusInternalServerError, "GetTrackedTimeByID", err.Error())
return
}
// only OP or admin may delete
if !c.IsSigned || (!c.IsUserSiteAdmin() && c.Doer.ID != t.UserID) {
- c.Error(http.StatusForbidden, "not allowed")
+ c.HTTPError(http.StatusForbidden, "not allowed")
return
}
@@ -92,7 +92,7 @@ func UpdateIssueTimeEstimate(ctx *context.Context) {
}
if !ctx.IsSigned || (!issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) {
- ctx.Error(http.StatusForbidden)
+ ctx.HTTPError(http.StatusForbidden)
return
}
diff --git a/routers/web/repo/issue_view.go b/routers/web/repo/issue_view.go
index aa49d2e1e8..d4458ed19e 100644
--- a/routers/web/repo/issue_view.go
+++ b/routers/web/repo/issue_view.go
@@ -4,12 +4,12 @@
package repo
import (
- stdCtx "context"
"fmt"
"math/big"
"net/http"
"net/url"
"sort"
+ "strconv"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
@@ -25,12 +25,14 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/emoji"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/templates/vars"
+ "code.gitea.io/gitea/modules/util"
asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/context/upload"
@@ -40,86 +42,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) {
@@ -277,14 +273,29 @@ func combineLabelComments(issue *issues_model.Issue) {
}
}
-// ViewIssue render issue view page
-func ViewIssue(ctx *context.Context) {
+func prepareIssueViewLoad(ctx *context.Context) *issues_model.Issue {
+ issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
+ if err != nil {
+ ctx.NotFoundOrServerError("GetIssueByIndex", issues_model.IsErrIssueNotExist, err)
+ return nil
+ }
+ issue.Repo = ctx.Repo.Repository
+ ctx.Data["Issue"] = issue
+
+ if err = issue.LoadPullRequest(ctx); err != nil {
+ ctx.ServerError("LoadPullRequest", err)
+ return nil
+ }
+ return issue
+}
+
+func handleViewIssueRedirectExternal(ctx *context.Context) {
if ctx.PathParam("type") == "issues" {
// If issue was requested we check if repo has external tracker and redirect
extIssueUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalTracker)
if err == nil && extIssueUnit != nil {
if extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == markup.IssueNameStyleNumeric || extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == "" {
- metas := ctx.Repo.Repository.ComposeMetas(ctx)
+ metas := ctx.Repo.Repository.ComposeCommentMetas(ctx)
metas["index"] = ctx.PathParam("index")
res, err := vars.Expand(extIssueUnit.ExternalTrackerConfig().ExternalTrackerFormat, metas)
if err != nil {
@@ -300,18 +311,18 @@ func ViewIssue(ctx *context.Context) {
return
}
}
+}
- issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
- if err != nil {
- if issues_model.IsErrIssueNotExist(err) {
- ctx.NotFound("GetIssueByIndex", err)
- } else {
- ctx.ServerError("GetIssueByIndex", err)
- }
+// ViewIssue render issue view page
+func ViewIssue(ctx *context.Context) {
+ handleViewIssueRedirectExternal(ctx)
+ if ctx.Written() {
return
}
- if issue.Repo == nil {
- issue.Repo = ctx.Repo.Repository
+
+ issue := prepareIssueViewLoad(ctx)
+ if ctx.Written() {
+ return
}
// Make sure type and URL matches.
@@ -343,12 +354,12 @@ func ViewIssue(ctx *context.Context) {
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
upload.AddUploadContext(ctx, "comment")
- if err = issue.LoadAttributes(ctx); err != nil {
+ if err := issue.LoadAttributes(ctx); err != nil {
ctx.ServerError("LoadAttributes", err)
return
}
- if err = filterXRefComments(ctx, issue); err != nil {
+ if err := filterXRefComments(ctx, issue); err != nil {
ctx.ServerError("filterXRefComments", err)
return
}
@@ -357,7 +368,7 @@ func ViewIssue(ctx *context.Context) {
if ctx.IsSigned {
// Update issue-user.
- if err = activities_model.SetIssueReadBy(ctx, issue.ID, ctx.Doer.ID); err != nil {
+ if err := activities_model.SetIssueReadBy(ctx, issue.ID, ctx.Doer.ID); err != nil {
ctx.ServerError("ReadBy", err)
return
}
@@ -371,15 +382,13 @@ func ViewIssue(ctx *context.Context) {
prepareFuncs := []func(*context.Context, *issues_model.Issue){
prepareIssueViewContent,
- func(ctx *context.Context, issue *issues_model.Issue) {
- preparePullViewPullInfo(ctx, issue)
- },
prepareIssueViewCommentsAndSidebarParticipants,
- preparePullViewReviewAndMerge,
prepareIssueViewSidebarWatch,
prepareIssueViewSidebarTimeTracker,
prepareIssueViewSidebarDependency,
prepareIssueViewSidebarPin,
+ func(ctx *context.Context, issue *issues_model.Issue) { preparePullViewPullInfo(ctx, issue) },
+ preparePullViewReviewAndMerge,
}
for _, prepareFunc := range prepareFuncs {
@@ -418,9 +427,29 @@ func ViewIssue(ctx *context.Context) {
return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
}
+ if issue.PullRequest != nil && !issue.PullRequest.IsChecking() && !setting.IsProd {
+ ctx.Data["PullMergeBoxReloadingInterval"] = 1 // in dev env, force using the reloading logic to make sure it won't break
+ }
+
ctx.HTML(http.StatusOK, tplIssueView)
}
+func ViewPullMergeBox(ctx *context.Context) {
+ issue := prepareIssueViewLoad(ctx)
+ if !issue.IsPull {
+ ctx.NotFound(nil)
+ return
+ }
+ preparePullViewPullInfo(ctx, issue)
+ preparePullViewReviewAndMerge(ctx, issue)
+ ctx.Data["PullMergeBoxReloading"] = issue.PullRequest.IsChecking()
+
+ // TODO: it should use a dedicated struct to render the pull merge box, to make sure all data is prepared correctly
+ ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.Doer.ID)
+ ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
+ ctx.HTML(http.StatusOK, tplPullMergeBox)
+}
+
func prepareIssueViewSidebarDependency(ctx *context.Context, issue *issues_model.Issue) {
if issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) {
ctx.Data["IssueDependencySearchType"] = "pulls"
@@ -533,7 +562,7 @@ func preparePullViewDeleteBranch(ctx *context.Context, issue *issues_model.Issue
pull := issue.PullRequest
isPullBranchDeletable := canDelete &&
pull.HeadRepo != nil &&
- git.IsBranchExist(ctx, pull.HeadRepo.RepoPath(), pull.HeadBranch) &&
+ gitrepo.IsBranchExist(ctx, pull.HeadRepo, pull.HeadBranch) &&
(!pull.HasMerged || ctx.Data["HeadBranchCommitID"] == ctx.Data["PullHeadCommitID"])
if isPullBranchDeletable && pull.HasMerged {
@@ -550,7 +579,11 @@ func preparePullViewDeleteBranch(ctx *context.Context, issue *issues_model.Issue
func prepareIssueViewSidebarPin(ctx *context.Context, issue *issues_model.Issue) {
var pinAllowed bool
- if !issue.IsPinned() {
+ if err := issue.LoadPinOrder(ctx); err != nil {
+ ctx.ServerError("LoadPinOrder", err)
+ return
+ }
+ if issue.PinOrder == 0 {
var err error
pinAllowed, err = issues_model.IsNewPinAllowed(ctx, issue.RepoID, issue.IsPull)
if err != nil {
@@ -596,7 +629,9 @@ func prepareIssueViewCommentsAndSidebarParticipants(ctx *context.Context, issue
comment.Issue = issue
if comment.Type == issues_model.CommentTypeComment || comment.Type == issues_model.CommentTypeReview {
- rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo)
+ rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo, renderhelper.RepoCommentOptions{
+ FootnoteContextID: strconv.FormatInt(comment.ID, 10),
+ })
comment.RenderedContent, err = markdown.RenderString(rctx, comment.Content)
if err != nil {
ctx.ServerError("RenderString", err)
@@ -672,7 +707,9 @@ func prepareIssueViewCommentsAndSidebarParticipants(ctx *context.Context, issue
}
}
} else if comment.Type.HasContentSupport() {
- rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo)
+ rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo, renderhelper.RepoCommentOptions{
+ FootnoteContextID: strconv.FormatInt(comment.ID, 10),
+ })
comment.RenderedContent, err = markdown.RenderString(rctx, comment.Content)
if err != nil {
ctx.ServerError("RenderString", err)
@@ -723,12 +760,15 @@ func prepareIssueViewCommentsAndSidebarParticipants(ctx *context.Context, issue
}
} else if comment.Type == issues_model.CommentTypePullRequestPush {
participants = addParticipant(comment.Poster, participants)
- if err = comment.LoadPushCommits(ctx); err != nil {
- ctx.ServerError("LoadPushCommits", err)
+ if err = issue_service.LoadCommentPushCommits(ctx, comment); err != nil {
+ ctx.ServerError("LoadCommentPushCommits", err)
return
}
if !ctx.Repo.CanRead(unit.TypeActions) {
for _, commit := range comment.Commits {
+ if commit.Status == nil {
+ continue
+ }
commit.Status.HideActionsURL(ctx)
git_model.CommitStatusesHideActionsURL(ctx, commit.Statuses)
}
@@ -794,6 +834,8 @@ func preparePullViewReviewAndMerge(ctx *context.Context, issue *issues_model.Iss
allowMerge := false
canWriteToHeadRepo := false
+ pull_service.StartPullRequestCheckOnView(ctx, pull)
+
if ctx.IsSigned {
if err := pull.LoadHeadRepo(ctx); err != nil {
log.Error("LoadHeadRepo: %v", err)
@@ -840,6 +882,7 @@ func preparePullViewReviewAndMerge(ctx *context.Context, issue *issues_model.Iss
}
}
+ ctx.Data["PullMergeBoxReloadingInterval"] = util.Iif(pull != nil && pull.IsChecking(), 2000, 0)
ctx.Data["CanWriteToHeadRepo"] = canWriteToHeadRepo
ctx.Data["ShowMergeInstructions"] = canWriteToHeadRepo
ctx.Data["AllowMerge"] = allowMerge
@@ -950,7 +993,9 @@ func preparePullViewReviewAndMerge(ctx *context.Context, issue *issues_model.Iss
func prepareIssueViewContent(ctx *context.Context, issue *issues_model.Issue) {
var err error
- rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository)
+ rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{
+ FootnoteContextID: "0", // Set footnote context ID to 0 for the issue content
+ })
issue.RenderedContent, err = markdown.RenderString(rctx, issue.Content)
if err != nil {
ctx.ServerError("RenderString", err)
@@ -960,5 +1005,4 @@ func prepareIssueViewContent(ctx *context.Context, issue *issues_model.Issue) {
ctx.ServerError("roleDescriptor", err)
return
}
- ctx.Data["Issue"] = issue
}
diff --git a/routers/web/repo/issue_watch.go b/routers/web/repo/issue_watch.go
index e7d55e5555..dfa3491786 100644
--- a/routers/web/repo/issue_watch.go
+++ b/routers/web/repo/issue_watch.go
@@ -42,7 +42,7 @@ func IssueWatch(ctx *context.Context) {
log.Trace("Permission Denied: Not logged in")
}
}
- ctx.Error(http.StatusForbidden)
+ ctx.HTTPError(http.StatusForbidden)
return
}
diff --git a/routers/web/repo/migrate.go b/routers/web/repo/migrate.go
index 3a7dc29466..ea15e90e5c 100644
--- a/routers/web/repo/migrate.go
+++ b/routers/web/repo/migrate.go
@@ -35,7 +35,7 @@ const (
// Migrate render migration of repository page
func Migrate(ctx *context.Context) {
if setting.Repository.DisableMigrations {
- ctx.Error(http.StatusForbidden, "Migrate: the site administrator has disabled migrations")
+ ctx.HTTPError(http.StatusForbidden, "Migrate: the site administrator has disabled migrations")
return
}
@@ -72,7 +72,7 @@ func Migrate(ctx *context.Context) {
func handleMigrateError(ctx *context.Context, owner *user_model.User, err error, name string, tpl templates.TplName, form *forms.MigrateRepoForm) {
if setting.Repository.DisableMigrations {
- ctx.Error(http.StatusForbidden, "MigrateError: the site administrator has disabled migrations")
+ ctx.HTTPError(http.StatusForbidden, "MigrateError: the site administrator has disabled migrations")
return
}
@@ -152,12 +152,12 @@ func handleMigrateRemoteAddrError(ctx *context.Context, err error, tpl templates
func MigratePost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.MigrateRepoForm)
if setting.Repository.DisableMigrations {
- ctx.Error(http.StatusForbidden, "MigratePost: the site administrator has disabled migrations")
+ ctx.HTTPError(http.StatusForbidden, "MigratePost: the site administrator has disabled migrations")
return
}
if form.Mirror && setting.Mirror.DisableNewPull {
- ctx.Error(http.StatusBadRequest, "MigratePost: the site administrator has disabled creation of new mirrors")
+ ctx.HTTPError(http.StatusBadRequest, "MigratePost: the site administrator has disabled creation of new mirrors")
return
}
diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go
index 6a0e6b25a9..dd53b1d3f1 100644
--- a/routers/web/repo/milestone.go
+++ b/routers/web/repo/milestone.go
@@ -38,10 +38,7 @@ func Milestones(ctx *context.Context) {
isShowClosed := ctx.FormString("state") == "closed"
sortType := ctx.FormString("sort")
keyword := ctx.FormTrim("q")
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
miles, total, err := db.FindAndCount[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
ListOptions: db.ListOptions{
@@ -148,7 +145,7 @@ func EditMilestone(ctx *context.Context) {
m, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
if err != nil {
if issues_model.IsErrMilestoneNotExist(err) {
- ctx.NotFound("", nil)
+ ctx.NotFound(nil)
} else {
ctx.ServerError("GetMilestoneByRepoID", err)
}
@@ -184,7 +181,7 @@ func EditMilestonePost(ctx *context.Context) {
m, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
if err != nil {
if issues_model.IsErrMilestoneNotExist(err) {
- ctx.NotFound("", nil)
+ ctx.NotFound(nil)
} else {
ctx.ServerError("GetMilestoneByRepoID", err)
}
@@ -218,7 +215,7 @@ func ChangeMilestoneStatus(ctx *context.Context) {
if err := issues_model.ChangeMilestoneStatusByRepoIDAndID(ctx, ctx.Repo.Repository.ID, id, toClose); err != nil {
if issues_model.IsErrMilestoneNotExist(err) {
- ctx.NotFound("", err)
+ ctx.NotFound(err)
} else {
ctx.ServerError("ChangeMilestoneStatusByIDAndRepoID", err)
}
@@ -245,7 +242,7 @@ func MilestoneIssuesAndPulls(ctx *context.Context) {
milestone, err := issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, milestoneID)
if err != nil {
if issues_model.IsErrMilestoneNotExist(err) {
- ctx.NotFound("GetMilestoneByID", err)
+ ctx.NotFound(err)
return
}
@@ -263,7 +260,7 @@ func MilestoneIssuesAndPulls(ctx *context.Context) {
ctx.Data["Title"] = milestone.Name
ctx.Data["Milestone"] = milestone
- issues(ctx, milestoneID, projectID, optional.None[bool]())
+ prepareIssueFilterAndList(ctx, milestoneID, projectID, optional.None[bool]())
ret := issue.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
ctx.Data["NewIssueChooseTemplate"] = len(ret.IssueTemplates) > 0
diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go
index 65a340a799..d09a57c03f 100644
--- a/routers/web/repo/packages.go
+++ b/routers/web/repo/packages.go
@@ -21,10 +21,7 @@ const (
// Packages displays a list of all packages in the repository
func Packages(ctx *context.Context) {
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
query := ctx.FormTrim("q")
packageType := ctx.FormTrim("type")
diff --git a/routers/web/repo/patch.go b/routers/web/repo/patch.go
deleted file mode 100644
index 4d47a705d6..0000000000
--- a/routers/web/repo/patch.go
+++ /dev/null
@@ -1,116 +0,0 @@
-// Copyright 2021 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package repo
-
-import (
- "strings"
-
- git_model "code.gitea.io/gitea/models/git"
- "code.gitea.io/gitea/models/unit"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/templates"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/modules/web"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/repository/files"
-)
-
-const (
- tplPatchFile templates.TplName = "repo/editor/patch"
-)
-
-// NewDiffPatch render create patch page
-func NewDiffPatch(ctx *context.Context) {
- canCommit := renderCommitRights(ctx)
-
- ctx.Data["PageIsPatch"] = true
-
- ctx.Data["commit_summary"] = ""
- ctx.Data["commit_message"] = ""
- if canCommit {
- ctx.Data["commit_choice"] = frmCommitChoiceDirect
- } else {
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- }
- ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx)
- ctx.Data["last_commit"] = ctx.Repo.CommitID
- ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
-
- ctx.HTML(200, tplPatchFile)
-}
-
-// NewDiffPatchPost response for sending patch page
-func NewDiffPatchPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.EditRepoFileForm)
-
- canCommit := renderCommitRights(ctx)
- branchName := ctx.Repo.BranchName
- if form.CommitChoice == frmCommitChoiceNewBranch {
- branchName = form.NewBranchName
- }
- ctx.Data["PageIsPatch"] = true
- ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
- ctx.Data["FileContent"] = form.Content
- ctx.Data["commit_summary"] = form.CommitSummary
- ctx.Data["commit_message"] = form.CommitMessage
- ctx.Data["commit_choice"] = form.CommitChoice
- ctx.Data["new_branch_name"] = form.NewBranchName
- ctx.Data["last_commit"] = ctx.Repo.CommitID
- ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
-
- if ctx.HasError() {
- ctx.HTML(200, tplPatchFile)
- return
- }
-
- // Cannot commit to a an existing branch if user doesn't have rights
- if branchName == ctx.Repo.BranchName && !canCommit {
- ctx.Data["Err_NewBranchName"] = true
- ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
- ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplEditFile, &form)
- return
- }
-
- // CommitSummary is optional in the web form, if empty, give it a default message based on add or update
- // `message` will be both the summary and message combined
- message := strings.TrimSpace(form.CommitSummary)
- if len(message) == 0 {
- message = ctx.Locale.TrString("repo.editor.patch")
- }
-
- form.CommitMessage = strings.TrimSpace(form.CommitMessage)
- if len(form.CommitMessage) > 0 {
- message += "\n\n" + form.CommitMessage
- }
-
- fileResponse, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, &files.ApplyDiffPatchOptions{
- LastCommitID: form.LastCommit,
- OldBranch: ctx.Repo.BranchName,
- NewBranch: branchName,
- Message: message,
- Content: strings.ReplaceAll(form.Content, "\r", ""),
- })
- if err != nil {
- if git_model.IsErrBranchAlreadyExists(err) {
- // User has specified a branch that already exists
- branchErr := err.(git_model.ErrBranchAlreadyExists)
- ctx.Data["Err_NewBranchName"] = true
- ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form)
- return
- } else if files.IsErrCommitIDDoesNotMatch(err) {
- ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form)
- return
- }
- ctx.RenderWithErr(ctx.Tr("repo.editor.fail_to_apply_patch", err), tplPatchFile, &form)
- return
- }
-
- if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) {
- ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName))
- } else {
- ctx.Redirect(ctx.Repo.RepoLink + "/commit/" + fileResponse.Commit.SHA)
- }
-}
diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go
index 346132102f..a57976b4ca 100644
--- a/routers/web/repo/projects.go
+++ b/routers/web/repo/projects.go
@@ -39,14 +39,14 @@ const (
// MustEnableRepoProjects check if repo projects are enabled in settings
func MustEnableRepoProjects(ctx *context.Context) {
if unit.TypeProjects.UnitGlobalDisabled() {
- ctx.NotFound("EnableRepoProjects", nil)
+ ctx.NotFound(nil)
return
}
if ctx.Repo.Repository != nil {
projectsUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeProjects)
if !ctx.Repo.CanRead(unit.TypeProjects) || !projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeRepo) {
- ctx.NotFound("MustEnableRepoProjects", nil)
+ ctx.NotFound(nil)
return
}
}
@@ -61,10 +61,7 @@ func Projects(ctx *context.Context) {
isShowClosed := strings.ToLower(ctx.FormTrim("state")) == "closed"
keyword := ctx.FormTrim("q")
repo := ctx.Repo.Repository
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
ctx.Data["OpenCount"] = repo.NumOpenProjects
ctx.Data["ClosedCount"] = repo.NumClosedProjects
@@ -92,6 +89,11 @@ func Projects(ctx *context.Context) {
return
}
+ if err := project_service.LoadIssueNumbersForProjects(ctx, projects, ctx.Doer); err != nil {
+ ctx.ServerError("LoadIssueNumbersForProjects", err)
+ return
+ }
+
for i := range projects {
rctx := renderhelper.NewRenderContextRepoComment(ctx, repo)
projects[i].RenderedContent, err = markdown.RenderString(rctx, projects[i].Description)
@@ -189,14 +191,14 @@ func DeleteProject(ctx *context.Context) {
p, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
if project_model.IsErrProjectNotExist(err) {
- ctx.NotFound("", nil)
+ ctx.NotFound(nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return
}
if p.RepoID != ctx.Repo.Repository.ID {
- ctx.NotFound("", nil)
+ ctx.NotFound(nil)
return
}
@@ -219,14 +221,14 @@ func RenderEditProject(ctx *context.Context) {
p, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
if project_model.IsErrProjectNotExist(err) {
- ctx.NotFound("", nil)
+ ctx.NotFound(nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return
}
if p.RepoID != ctx.Repo.Repository.ID {
- ctx.NotFound("", nil)
+ ctx.NotFound(nil)
return
}
@@ -259,14 +261,14 @@ func EditProjectPost(ctx *context.Context) {
p, err := project_model.GetProjectByID(ctx, projectID)
if err != nil {
if project_model.IsErrProjectNotExist(err) {
- ctx.NotFound("", nil)
+ ctx.NotFound(nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return
}
if p.RepoID != ctx.Repo.Repository.ID {
- ctx.NotFound("", nil)
+ ctx.NotFound(nil)
return
}
@@ -291,14 +293,14 @@ func ViewProject(ctx *context.Context) {
project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
if project_model.IsErrProjectNotExist(err) {
- ctx.NotFound("", nil)
+ ctx.NotFound(nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return
}
if project.RepoID != ctx.Repo.Repository.ID {
- ctx.NotFound("", nil)
+ ctx.NotFound(nil)
return
}
@@ -308,18 +310,22 @@ func ViewProject(ctx *context.Context) {
return
}
- labelIDs := issue.PrepareFilterIssueLabels(ctx, ctx.Repo.Repository.ID, ctx.Repo.Owner)
+ preparedLabelFilter := issue.PrepareFilterIssueLabels(ctx, ctx.Repo.Repository.ID, ctx.Repo.Owner)
- assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future
+ assigneeID := ctx.FormString("assignee")
- issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns, &issues_model.IssuesOptions{
- LabelIDs: labelIDs,
- AssigneeID: optional.Some(assigneeID),
+ issuesMap, err := project_service.LoadIssuesFromProject(ctx, project, &issues_model.IssuesOptions{
+ RepoIDs: []int64{ctx.Repo.Repository.ID},
+ LabelIDs: preparedLabelFilter.SelectedLabelIDs,
+ AssigneeID: assigneeID,
})
if err != nil {
ctx.ServerError("LoadIssuesOfColumns", err)
return
}
+ for _, column := range columns {
+ column.NumIssues = int64(len(issuesMap[column.ID]))
+ }
if project.CardType != project_model.CardTypeTextOnly {
issuesAttachmentMap := make(map[int64][]*repo_model.Attachment)
@@ -372,8 +378,8 @@ func ViewProject(ctx *context.Context) {
}
// Get the exclusive scope for every label ID
- labelExclusiveScopes := make([]string, 0, len(labelIDs))
- for _, labelID := range labelIDs {
+ labelExclusiveScopes := make([]string, 0, len(preparedLabelFilter.SelectedLabelIDs))
+ for _, labelID := range preparedLabelFilter.SelectedLabelIDs {
foundExclusiveScope := false
for _, label := range labels {
if label.ID == labelID || label.ID == -labelID {
@@ -388,7 +394,7 @@ func ViewProject(ctx *context.Context) {
}
for _, l := range labels {
- l.LoadSelectedLabelsAfterClick(labelIDs, labelExclusiveScopes)
+ l.LoadSelectedLabelsAfterClick(preparedLabelFilter.SelectedLabelIDs, labelExclusiveScopes)
}
ctx.Data["Labels"] = labels
ctx.Data["NumLabels"] = len(labels)
@@ -471,7 +477,7 @@ func DeleteProjectColumn(ctx *context.Context) {
project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
if project_model.IsErrProjectNotExist(err) {
- ctx.NotFound("", nil)
+ ctx.NotFound(nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
@@ -518,7 +524,7 @@ func AddColumnToProjectPost(ctx *context.Context) {
project, err := project_model.GetProjectForRepoByID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
if err != nil {
if project_model.IsErrProjectNotExist(err) {
- ctx.NotFound("", nil)
+ ctx.NotFound(nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
@@ -556,7 +562,7 @@ func checkProjectColumnChangePermissions(ctx *context.Context) (*project_model.P
project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
if project_model.IsErrProjectNotExist(err) {
- ctx.NotFound("", nil)
+ ctx.NotFound(nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
@@ -642,21 +648,21 @@ func MoveIssues(ctx *context.Context) {
project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
if project_model.IsErrProjectNotExist(err) {
- ctx.NotFound("ProjectNotExist", nil)
+ ctx.NotFound(nil)
} else {
ctx.ServerError("GetProjectByID", err)
}
return
}
if project.RepoID != ctx.Repo.Repository.ID {
- ctx.NotFound("InvalidRepoID", nil)
+ ctx.NotFound(nil)
return
}
column, err := project_model.GetColumn(ctx, ctx.PathParamInt64("columnID"))
if err != nil {
if project_model.IsErrProjectColumnNotExist(err) {
- ctx.NotFound("ProjectColumnNotExist", nil)
+ ctx.NotFound(nil)
} else {
ctx.ServerError("GetProjectColumn", err)
}
@@ -664,7 +670,7 @@ func MoveIssues(ctx *context.Context) {
}
if column.ProjectID != project.ID {
- ctx.NotFound("ColumnNotInProject", nil)
+ ctx.NotFound(nil)
return
}
@@ -689,7 +695,7 @@ func MoveIssues(ctx *context.Context) {
movedIssues, err := issues_model.GetIssuesByIDs(ctx, issueIDs)
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
- ctx.NotFound("IssueNotExisting", nil)
+ ctx.NotFound(nil)
} else {
ctx.ServerError("GetIssueByID", err)
}
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index e6fb492d6e..f662152e2e 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -24,8 +24,10 @@ import (
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/emoji"
+ "code.gitea.io/gitea/modules/fileicon"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
+ "code.gitea.io/gitea/modules/graceful"
issue_template "code.gitea.io/gitea/modules/issue/template"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
@@ -81,7 +83,7 @@ func getRepository(ctx *context.Context, repoID int64) *repo_model.Repository {
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
- ctx.NotFound("GetRepositoryByID", nil)
+ ctx.NotFound(nil)
} else {
ctx.ServerError("GetRepositoryByID", err)
}
@@ -101,7 +103,7 @@ func getRepository(ctx *context.Context, repoID int64) *repo_model.Repository {
unit.TypeCode,
ctx.Repo,
perm)
- ctx.NotFound("getRepository", nil)
+ ctx.NotFound(nil)
return nil
}
return repo
@@ -111,7 +113,7 @@ func getPullInfo(ctx *context.Context) (issue *issues_model.Issue, ok bool) {
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
- ctx.NotFound("GetIssueByIndex", err)
+ ctx.NotFound(err)
} else {
ctx.ServerError("GetIssueByIndex", err)
}
@@ -129,7 +131,7 @@ func getPullInfo(ctx *context.Context) (issue *issues_model.Issue, ok bool) {
ctx.Data["Issue"] = issue
if !issue.IsPull {
- ctx.NotFound("ViewPullCommits", nil)
+ ctx.NotFound(nil)
return nil, false
}
@@ -181,6 +183,7 @@ func setMergeTarget(ctx *context.Context, pull *issues_model.PullRequest) {
// GetPullDiffStats get Pull Requests diff stats
func GetPullDiffStats(ctx *context.Context) {
+ // FIXME: this getPullInfo seems to be a duplicate call with other route handlers
issue, ok := getPullInfo(ctx)
if !ok {
return
@@ -188,34 +191,23 @@ func GetPullDiffStats(ctx *context.Context) {
pull := issue.PullRequest
mergeBaseCommitID := GetMergedBaseCommitID(ctx, issue)
-
if mergeBaseCommitID == "" {
- ctx.NotFound("PullFiles", nil)
- return
+ return // no merge base, do nothing, do not stop the route handler, see below
}
+ // do not report 500 server error to end users if error occurs, otherwise a PR missing ref won't be able to view.
headCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(pull.GetGitRefName())
if err != nil {
- ctx.ServerError("GetRefCommitID", err)
+ log.Error("Failed to GetRefCommitID: %v, repo: %v", err, ctx.Repo.Repository.FullName())
return
}
-
- diffOptions := &gitdiff.DiffOptions{
- BeforeCommitID: mergeBaseCommitID,
- AfterCommitID: headCommitID,
- MaxLines: setting.Git.MaxGitDiffLines,
- MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters,
- MaxFiles: setting.Git.MaxGitDiffFiles,
- WhitespaceBehavior: gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)),
- }
-
- diff, err := gitdiff.GetPullDiffStats(ctx.Repo.GitRepo, diffOptions)
+ diffShortStat, err := gitdiff.GetDiffShortStat(ctx.Repo.GitRepo, mergeBaseCommitID, headCommitID)
if err != nil {
- ctx.ServerError("GetPullDiffStats", err)
+ log.Error("Failed to GetDiffShortStat: %v, repo: %v", err, ctx.Repo.Repository.FullName())
return
}
- ctx.Data["Diff"] = diff
+ ctx.Data["DiffShortStat"] = diffShortStat
}
func GetMergedBaseCommitID(ctx *context.Context, issue *issues_model.Issue) string {
@@ -242,7 +234,7 @@ func GetMergedBaseCommitID(ctx *context.Context, issue *issues_model.Issue) stri
}
if commitSHA != "" {
// Get immediate parent of the first commit in the patch, grab history back
- parentCommit, _, err = git.NewCommand(ctx, "rev-list", "-1", "--skip=1").AddDynamicArguments(commitSHA).RunStdString(&git.RunOpts{Dir: ctx.Repo.GitRepo.Path})
+ parentCommit, _, err = git.NewCommand("rev-list", "-1", "--skip=1").AddDynamicArguments(commitSHA).RunStdString(ctx, &git.RunOpts{Dir: ctx.Repo.GitRepo.Path})
if err == nil {
parentCommit = strings.TrimSpace(parentCommit)
}
@@ -300,7 +292,7 @@ func prepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue)
if len(compareInfo.Commits) != 0 {
sha := compareInfo.Commits[0].ID.String()
- commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, sha, db.ListOptionsAll)
+ commitStatuses, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, sha, db.ListOptionsAll)
if err != nil {
ctx.ServerError("GetLatestCommitStatus", err)
return nil
@@ -356,7 +348,7 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
defer baseGitRepo.Close()
}
- if !baseGitRepo.IsBranchExist(pull.BaseBranch) {
+ if !gitrepo.IsBranchExist(ctx, pull.BaseRepo, pull.BaseBranch) {
ctx.Data["BaseBranchNotExist"] = true
ctx.Data["IsPullRequestBroken"] = true
ctx.Data["BaseTarget"] = pull.BaseBranch
@@ -367,7 +359,7 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
ctx.ServerError(fmt.Sprintf("GetRefCommitID(%s)", pull.GetGitRefName()), err)
return nil
}
- commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll)
+ commitStatuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll)
if err != nil {
ctx.ServerError("GetLatestCommitStatus", err)
return nil
@@ -413,9 +405,9 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
defer closer.Close()
if pull.Flow == issues_model.PullRequestFlowGithub {
- headBranchExist = headGitRepo.IsBranchExist(pull.HeadBranch)
+ headBranchExist = gitrepo.IsBranchExist(ctx, pull.HeadRepo, pull.HeadBranch)
} else {
- headBranchExist = git.IsReferenceExist(ctx, baseGitRepo.Path, pull.GetGitRefName())
+ headBranchExist = gitrepo.IsReferenceExist(ctx, pull.BaseRepo, pull.GetGitRefName())
}
if headBranchExist {
@@ -463,7 +455,7 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
return nil
}
- commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll)
+ commitStatuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll)
if err != nil {
ctx.ServerError("GetLatestCommitStatus", err)
return nil
@@ -624,14 +616,18 @@ func ViewPullCommits(ctx *context.Context) {
if ctx.Written() {
return
} else if prInfo == nil {
- ctx.NotFound("ViewPullCommits", nil)
+ ctx.NotFound(nil)
return
}
ctx.Data["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
- commits := processGitCommits(ctx, prInfo.Commits)
+ commits, err := processGitCommits(ctx, prInfo.Commits)
+ if err != nil {
+ ctx.ServerError("processGitCommits", err)
+ return
+ }
ctx.Data["Commits"] = commits
ctx.Data["CommitCount"] = len(commits)
@@ -668,7 +664,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
if ctx.Written() {
return
} else if prInfo == nil {
- ctx.NotFound("ViewPullFiles", nil)
+ ctx.NotFound(nil)
return
}
@@ -693,7 +689,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
}
if !(foundStartCommit && foundEndCommit) {
- ctx.NotFound("Given SHA1 not found for this PR", nil)
+ ctx.NotFound(nil)
return
}
}
@@ -748,34 +744,40 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters,
MaxFiles: maxFiles,
WhitespaceBehavior: gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)),
- FileOnly: fileOnly,
}
if !willShowSpecifiedCommit {
diffOptions.BeforeCommitID = startCommitID
}
- var methodWithError string
- var diff *gitdiff.Diff
+ diff, err := gitdiff.GetDiffForRender(ctx, gitRepo, diffOptions, files...)
+ if err != nil {
+ ctx.ServerError("GetDiff", err)
+ return
+ }
// if we're not logged in or only a single commit (or commit range) is shown we
// have to load only the diff and not get the viewed information
// as the viewed information is designed to be loaded only on latest PR
// diff and if you're signed in.
- if !ctx.IsSigned || willShowSpecifiedCommit || willShowSpecifiedCommitRange {
- diff, err = gitdiff.GetDiff(ctx, gitRepo, diffOptions, files...)
- methodWithError = "GetDiff"
- } else {
- diff, err = gitdiff.SyncAndGetUserSpecificDiff(ctx, ctx.Doer.ID, pull, gitRepo, diffOptions, files...)
- methodWithError = "SyncAndGetUserSpecificDiff"
+ var reviewState *pull_model.ReviewState
+ if ctx.IsSigned && !willShowSpecifiedCommit && !willShowSpecifiedCommitRange {
+ reviewState, err = gitdiff.SyncUserSpecificDiff(ctx, ctx.Doer.ID, pull, gitRepo, diff, diffOptions)
+ if err != nil {
+ ctx.ServerError("SyncUserSpecificDiff", err)
+ return
+ }
}
+
+ diffShortStat, err := gitdiff.GetDiffShortStat(ctx.Repo.GitRepo, startCommitID, endCommitID)
if err != nil {
- ctx.ServerError(methodWithError, err)
+ ctx.ServerError("GetDiffShortStat", err)
return
}
+ ctx.Data["DiffShortStat"] = diffShortStat
ctx.PageData["prReview"] = map[string]any{
- "numberOfFiles": diff.NumFiles,
+ "numberOfFiles": diffShortStat.NumFiles,
"numberOfViewedFiles": diff.NumViewedFiles,
}
@@ -784,18 +786,18 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
return
}
+ allComments := issues_model.CommentList{}
for _, file := range diff.Files {
for _, section := range file.Sections {
for _, line := range section.Lines {
- for _, comment := range line.Comments {
- if err := comment.LoadAttachments(ctx); err != nil {
- ctx.ServerError("LoadAttachments", err)
- return
- }
- }
+ allComments = append(allComments, line.Comments...)
}
}
}
+ if err := allComments.LoadAttachments(ctx); err != nil {
+ ctx.ServerError("LoadAttachments", err)
+ return
+ }
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.BaseRepoID, pull.BaseBranch)
if err != nil {
@@ -812,8 +814,27 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
}
}
+ if !fileOnly {
+ // note: use mergeBase is set to false because we already have the merge base from the pull request info
+ diffTree, err := gitdiff.GetDiffTree(ctx, gitRepo, false, startCommitID, endCommitID)
+ if err != nil {
+ ctx.ServerError("GetDiffTree", err)
+ return
+ }
+ var filesViewedState map[string]pull_model.ViewedState
+ if reviewState != nil {
+ filesViewedState = reviewState.UpdatedFiles
+ }
+
+ renderedIconPool := fileicon.NewRenderedIconPool()
+ ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(renderedIconPool, diffTree, filesViewedState)
+ ctx.PageData["FolderIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder())
+ ctx.PageData["FolderOpenIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolderOpen())
+ ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
+ }
+
ctx.Data["Diff"] = diff
- ctx.Data["DiffNotAvailable"] = diff.NumFiles == 0
+ ctx.Data["DiffNotAvailable"] = diffShortStat.NumFiles == 0
baseCommit, err := ctx.Repo.GitRepo.GetCommit(startCommitID)
if err != nil {
@@ -931,11 +952,11 @@ func UpdatePullRequest(ctx *context.Context) {
return
}
if issue.IsClosed {
- ctx.NotFound("MergePullRequest", nil)
+ ctx.NotFound(nil)
return
}
if issue.PullRequest.HasMerged {
- ctx.NotFound("MergePullRequest", nil)
+ ctx.NotFound(nil)
return
}
@@ -966,7 +987,9 @@ func UpdatePullRequest(ctx *context.Context) {
// default merge commit message
message := fmt.Sprintf("Merge branch '%s' into %s", issue.PullRequest.BaseBranch, issue.PullRequest.HeadBranch)
- if err = pull_service.Update(ctx, issue.PullRequest, ctx.Doer, message, rebase); err != nil {
+ // The update process should not be cancelled by the user
+ // so we set the context to be a background context
+ if err = pull_service.Update(graceful.GetManager().ShutdownContext(), issue.PullRequest, ctx.Doer, message, rebase); err != nil {
if pull_service.IsErrMergeConflicts(err) {
conflictError := err.(pull_service.ErrMergeConflicts)
flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
@@ -1038,7 +1061,7 @@ func MergePullRequest(ctx *context.Context) {
} else {
ctx.JSONError(ctx.Tr("repo.issues.closed_title"))
}
- case errors.Is(err, pull_service.ErrUserNotAllowedToMerge):
+ case errors.Is(err, pull_service.ErrNoPermissionToMerge):
ctx.JSONError(ctx.Tr("repo.pulls.update_not_allowed"))
case errors.Is(err, pull_service.ErrHasMerged):
ctx.JSONError(ctx.Tr("repo.pulls.has_merged"))
@@ -1046,7 +1069,7 @@ func MergePullRequest(ctx *context.Context) {
ctx.JSONError(ctx.Tr("repo.pulls.no_merge_wip"))
case errors.Is(err, pull_service.ErrNotMergeableState):
ctx.JSONError(ctx.Tr("repo.pulls.no_merge_not_ready"))
- case pull_service.IsErrDisallowedToMerge(err):
+ case errors.Is(err, pull_service.ErrNotReadyToMerge):
ctx.JSONError(ctx.Tr("repo.pulls.no_merge_not_ready"))
case asymkey_service.IsErrWontSign(err):
ctx.JSONError(err.Error()) // has no translation ...
@@ -1235,13 +1258,23 @@ func CancelAutoMergePullRequest(ctx *context.Context) {
}
func stopTimerIfAvailable(ctx *context.Context, user *user_model.User, issue *issues_model.Issue) error {
- if issues_model.StopwatchExists(ctx, user.ID, issue.ID) {
- if err := issues_model.CreateOrStopIssueStopwatch(ctx, user, issue); err != nil {
- return err
+ _, err := issues_model.FinishIssueStopwatch(ctx, user, issue)
+ return err
+}
+
+func PullsNewRedirect(ctx *context.Context) {
+ branch := ctx.PathParam("*")
+ redirectRepo := ctx.Repo.Repository
+ repo := ctx.Repo.Repository
+ if repo.IsFork {
+ if err := repo.GetBaseRepo(ctx); err != nil {
+ ctx.ServerError("GetBaseRepo", err)
+ return
}
+ redirectRepo = repo.BaseRepo
+ branch = fmt.Sprintf("%s:%s", repo.OwnerName, branch)
}
-
- return nil
+ ctx.Redirect(fmt.Sprintf("%s/compare/%s...%s?expand=1", redirectRepo.Link(), util.PathEscapeSegments(redirectRepo.DefaultBranch), util.PathEscapeSegments(branch)))
}
// CompareAndPullRequestPost response for creating pull request
@@ -1261,11 +1294,6 @@ func CompareAndPullRequestPost(ctx *context.Context) {
)
ci := ParseCompareInfo(ctx)
- defer func() {
- if ci != nil && ci.HeadGitRepo != nil {
- ci.HeadGitRepo.Close()
- }
- }()
if ctx.Written() {
return
}
@@ -1334,7 +1362,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
if err := pull_service.NewPullRequest(ctx, prOpts); err != nil {
switch {
case repo_model.IsErrUserDoesNotHaveAccessToRepo(err):
- ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
+ ctx.HTTPError(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
case git.IsErrPushRejected(err):
pushrejErr := err.(*git.ErrPushRejected)
message := pushrejErr.Message
@@ -1405,7 +1433,7 @@ func CleanUpPullRequest(ctx *context.Context) {
// Don't cleanup unmerged and unclosed PRs and agit PRs
if !pr.HasMerged && !issue.IsClosed && pr.Flow != issues_model.PullRequestFlowGithub {
- ctx.NotFound("CleanUpPullRequest", nil)
+ ctx.NotFound(nil)
return
}
@@ -1416,7 +1444,7 @@ func CleanUpPullRequest(ctx *context.Context) {
return
}
if exist {
- ctx.NotFound("CleanUpPullRequest", nil)
+ ctx.NotFound(nil)
return
}
@@ -1425,7 +1453,7 @@ func CleanUpPullRequest(ctx *context.Context) {
return
} else if pr.HeadRepo == nil {
// Forked repository has already been deleted
- ctx.NotFound("CleanUpPullRequest", nil)
+ ctx.NotFound(nil)
return
} else if err = pr.LoadBaseRepo(ctx); err != nil {
ctx.ServerError("LoadBaseRepo", err)
@@ -1437,7 +1465,7 @@ func CleanUpPullRequest(ctx *context.Context) {
if err := repo_service.CanDeleteBranch(ctx, pr.HeadRepo, pr.HeadBranch, ctx.Doer); err != nil {
if errors.Is(err, util.ErrPermissionDenied) {
- ctx.NotFound("CanDeleteBranch", nil)
+ ctx.NotFound(nil)
} else {
ctx.ServerError("CanDeleteBranch", err)
}
@@ -1537,7 +1565,7 @@ func DownloadPullDiffOrPatch(ctx *context.Context, patch bool) {
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
- ctx.NotFound("GetPullRequestByIndex", err)
+ ctx.NotFound(err)
} else {
ctx.ServerError("GetPullRequestByIndex", err)
}
@@ -1560,18 +1588,18 @@ func UpdatePullRequestTarget(ctx *context.Context) {
}
pr := issue.PullRequest
if !issue.IsPull {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
if !ctx.IsSigned || (!issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) {
- ctx.Error(http.StatusForbidden)
+ ctx.HTTPError(http.StatusForbidden)
return
}
targetBranch := ctx.FormTrim("target_branch")
if len(targetBranch) == 0 {
- ctx.Error(http.StatusNoContent)
+ ctx.HTTPError(http.StatusNoContent)
return
}
@@ -1630,7 +1658,7 @@ func SetAllowEdits(ctx *context.Context) {
pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrPullRequestNotExist(err) {
- ctx.NotFound("GetPullRequestByIndex", err)
+ ctx.NotFound(err)
} else {
ctx.ServerError("GetPullRequestByIndex", err)
}
@@ -1639,7 +1667,7 @@ func SetAllowEdits(ctx *context.Context) {
if err := pull_service.SetAllowEdits(ctx, ctx.Doer, pr, form.AllowMaintainerEdit); err != nil {
if errors.Is(err, pull_service.ErrUserHasNoPermissionForAction) {
- ctx.Error(http.StatusForbidden)
+ ctx.HTTPError(http.StatusForbidden)
return
}
ctx.ServerError("SetAllowEdits", err)
diff --git a/routers/web/repo/pull_review.go b/routers/web/repo/pull_review.go
index 3e9e615b15..929e131d61 100644
--- a/routers/web/repo/pull_review.go
+++ b/routers/web/repo/pull_review.go
@@ -133,7 +133,7 @@ func UpdateResolveConversation(ctx *context.Context) {
}
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
- ctx.NotFound("comment's repoID is incorrect", errors.New("comment's repoID is incorrect"))
+ ctx.NotFound(errors.New("comment's repoID is incorrect"))
return
}
@@ -143,12 +143,12 @@ func UpdateResolveConversation(ctx *context.Context) {
return
}
if !permResult {
- ctx.Error(http.StatusForbidden)
+ ctx.HTTPError(http.StatusForbidden)
return
}
if !comment.Issue.IsPull {
- ctx.Error(http.StatusBadRequest)
+ ctx.HTTPError(http.StatusBadRequest)
return
}
@@ -159,7 +159,7 @@ func UpdateResolveConversation(ctx *context.Context) {
return
}
} else {
- ctx.Error(http.StatusBadRequest)
+ ctx.HTTPError(http.StatusBadRequest)
return
}
@@ -209,12 +209,13 @@ func renderConversation(ctx *context.Context, comment *issues_model.Comment, ori
return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
}
- if origin == "diff" {
+ switch origin {
+ case "diff":
ctx.HTML(http.StatusOK, tplDiffConversation)
- } else if origin == "timeline" {
+ case "timeline":
ctx.HTML(http.StatusOK, tplTimelineConversation)
- } else {
- ctx.Error(http.StatusBadRequest, "Unknown origin: "+origin)
+ default:
+ ctx.HTTPError(http.StatusBadRequest, "Unknown origin: "+origin)
}
}
diff --git a/routers/web/repo/recent_commits.go b/routers/web/repo/recent_commits.go
index dc72081900..2660116062 100644
--- a/routers/web/repo/recent_commits.go
+++ b/routers/web/repo/recent_commits.go
@@ -4,12 +4,10 @@
package repo
import (
- "errors"
"net/http"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/services/context"
- contributors_service "code.gitea.io/gitea/services/repository"
)
const (
@@ -26,16 +24,3 @@ func RecentCommits(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplRecentCommits)
}
-
-// RecentCommitsData returns JSON of recent commits data
-func RecentCommitsData(ctx *context.Context) {
- if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
- if errors.Is(err, contributors_service.ErrAwaitGeneration) {
- ctx.Status(http.StatusAccepted)
- return
- }
- ctx.ServerError("RecentCommitsData", err)
- } else {
- ctx.JSON(http.StatusOK, contributorStats["total"].Weeks)
- }
-}
diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go
index 284fd27abf..36ea20c23e 100644
--- a/routers/web/repo/release.go
+++ b/routers/web/repo/release.go
@@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"net/http"
+ "strconv"
"strings"
"code.gitea.io/gitea/models/db"
@@ -17,6 +18,7 @@ import (
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
@@ -101,7 +103,7 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions)
releaseInfos := make([]*ReleaseInfo, 0, len(releases))
for _, r := range releases {
if r.Publisher, ok = cacheUsers[r.PublisherID]; !ok {
- r.Publisher, err = user_model.GetUserByID(ctx, r.PublisherID)
+ r.Publisher, err = user_model.GetPossibleUserByID(ctx, r.PublisherID)
if err != nil {
if user_model.IsErrUserNotExist(err) {
r.Publisher = user_model.NewGhostUser()
@@ -112,7 +114,9 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions)
cacheUsers[r.PublisherID] = r.Publisher
}
- rctx := renderhelper.NewRenderContextRepoComment(ctx, r.Repo)
+ rctx := renderhelper.NewRenderContextRepoComment(ctx, r.Repo, renderhelper.RepoCommentOptions{
+ FootnoteContextID: strconv.FormatInt(r.ID, 10),
+ })
r.RenderedNote, err = markdown.RenderString(rctx, r.Note)
if err != nil {
return nil, err
@@ -129,7 +133,7 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions)
}
if canReadActions {
- statuses, _, err := git_model.GetLatestCommitStatus(ctx, r.Repo.ID, r.Sha1, db.ListOptionsAll)
+ statuses, err := git_model.GetLatestCommitStatus(ctx, r.Repo.ID, r.Sha1, db.ListOptionsAll)
if err != nil {
return nil, err
}
@@ -285,7 +289,7 @@ func SingleRelease(ctx *context.Context) {
return
}
if len(releases) != 1 {
- ctx.NotFound("SingleRelease", err)
+ ctx.NotFound(err)
return
}
@@ -311,7 +315,7 @@ func LatestRelease(ctx *context.Context) {
release, err := repo_model.GetLatestReleaseByRepoID(ctx, ctx.Repo.Repository.ID)
if err != nil {
if repo_model.IsErrReleaseNotExist(err) {
- ctx.NotFound("LatestRelease", err)
+ ctx.NotFound(err)
return
}
ctx.ServerError("GetLatestReleaseByRepoID", err)
@@ -377,7 +381,7 @@ func NewRelease(ctx *context.Context) {
ctx.Data["ShowCreateTagOnlyButton"] = false
ctx.Data["tag_name"] = rel.TagName
- ctx.Data["tag_target"] = rel.Target
+ ctx.Data["tag_target"] = util.IfZero(rel.Target, ctx.Repo.Repository.DefaultBranch)
ctx.Data["title"] = rel.Title
ctx.Data["content"] = rel.Note
ctx.Data["attachments"] = rel.Attachments
@@ -420,7 +424,7 @@ func NewReleasePost(ctx *context.Context) {
return
}
- if !ctx.Repo.GitRepo.IsBranchExist(form.Target) {
+ if !gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, form.Target) {
ctx.RenderWithErr(ctx.Tr("form.target_branch_not_exist"), tplReleaseNew, &form)
return
}
@@ -525,7 +529,7 @@ func EditRelease(ctx *context.Context) {
rel, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, tagName)
if err != nil {
if repo_model.IsErrReleaseNotExist(err) {
- ctx.NotFound("GetRelease", err)
+ ctx.NotFound(err)
} else {
ctx.ServerError("GetRelease", err)
}
@@ -533,7 +537,7 @@ func EditRelease(ctx *context.Context) {
}
ctx.Data["ID"] = rel.ID
ctx.Data["tag_name"] = rel.TagName
- ctx.Data["tag_target"] = rel.Target
+ ctx.Data["tag_target"] = util.IfZero(rel.Target, ctx.Repo.Repository.DefaultBranch)
ctx.Data["title"] = rel.Title
ctx.Data["content"] = rel.Note
ctx.Data["prerelease"] = rel.IsPrerelease
@@ -568,18 +572,18 @@ func EditReleasePost(ctx *context.Context) {
rel, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, tagName)
if err != nil {
if repo_model.IsErrReleaseNotExist(err) {
- ctx.NotFound("GetRelease", err)
+ ctx.NotFound(err)
} else {
ctx.ServerError("GetRelease", err)
}
return
}
if rel.IsTag {
- ctx.NotFound("GetRelease", err)
+ ctx.NotFound(err)
return
}
ctx.Data["tag_name"] = rel.TagName
- ctx.Data["tag_target"] = rel.Target
+ ctx.Data["tag_target"] = util.IfZero(rel.Target, ctx.Repo.Repository.DefaultBranch)
ctx.Data["title"] = rel.Title
ctx.Data["content"] = rel.Note
ctx.Data["prerelease"] = rel.IsPrerelease
@@ -639,7 +643,7 @@ func deleteReleaseOrTag(ctx *context.Context, isDelTag bool) {
rel, err := repo_model.GetReleaseForRepoByID(ctx, ctx.Repo.Repository.ID, ctx.FormInt64("id"))
if err != nil {
if repo_model.IsErrReleaseNotExist(err) {
- ctx.NotFound("GetReleaseForRepoByID", err)
+ ctx.NotFound(err)
} else {
ctx.Flash.Error("DeleteReleaseByID: " + err.Error())
redirect()
diff --git a/routers/web/repo/render.go b/routers/web/repo/render.go
index efd271c01d..689174dfa1 100644
--- a/routers/web/repo/render.go
+++ b/routers/web/repo/render.go
@@ -30,7 +30,7 @@ func RenderFile(ctx *context.Context) {
}
if err != nil {
if git.IsErrNotExist(err) {
- ctx.NotFound("GetBlobByPath", err)
+ ctx.NotFound(err)
} else {
ctx.ServerError("GetBlobByPath", err)
}
diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go
index 6c2bc74e69..828ec08a8a 100644
--- a/routers/web/repo/repo.go
+++ b/routers/web/repo/repo.go
@@ -45,14 +45,14 @@ const (
// MustBeNotEmpty render when a repo is a empty git dir
func MustBeNotEmpty(ctx *context.Context) {
if ctx.Repo.Repository.IsEmpty {
- ctx.NotFound("MustBeNotEmpty", nil)
+ ctx.NotFound(nil)
}
}
// MustBeEditable check that repo can be edited
func MustBeEditable(ctx *context.Context) {
if !ctx.Repo.Repository.CanEnableEditor() {
- ctx.NotFound("", nil)
+ ctx.NotFound(nil)
return
}
}
@@ -60,7 +60,7 @@ func MustBeEditable(ctx *context.Context) {
// MustBeAbleToUpload check that repo can be uploaded to
func MustBeAbleToUpload(ctx *context.Context) {
if !setting.Repository.Upload.Enabled {
- ctx.NotFound("", nil)
+ ctx.NotFound(nil)
}
}
@@ -87,17 +87,13 @@ func checkContextUser(ctx *context.Context, uid int64) *user_model.User {
return nil
}
- if !ctx.Doer.IsAdmin {
- orgsAvailable := []*organization.Organization{}
- for i := 0; i < len(orgs); i++ {
- if orgs[i].CanCreateRepo() {
- orgsAvailable = append(orgsAvailable, orgs[i])
- }
+ var orgsAvailable []*organization.Organization
+ for i := range orgs {
+ if ctx.Doer.CanCreateRepoIn(orgs[i].AsUser()) {
+ orgsAvailable = append(orgsAvailable, orgs[i])
}
- ctx.Data["Orgs"] = orgsAvailable
- } else {
- ctx.Data["Orgs"] = orgs
}
+ ctx.Data["Orgs"] = orgsAvailable
// Not equal means current user is an organization.
if uid == ctx.Doer.ID || uid == 0 {
@@ -116,7 +112,7 @@ func checkContextUser(ctx *context.Context, uid int64) *user_model.User {
// Check ownership of organization.
if !org.IsOrganization() {
- ctx.Error(http.StatusForbidden)
+ ctx.HTTPError(http.StatusForbidden)
return nil
}
if !ctx.Doer.IsAdmin {
@@ -125,7 +121,7 @@ func checkContextUser(ctx *context.Context, uid int64) *user_model.User {
ctx.ServerError("CanCreateOrgRepo", err)
return nil
} else if !canCreate {
- ctx.Error(http.StatusForbidden)
+ ctx.HTTPError(http.StatusForbidden)
return nil
}
} else {
@@ -154,8 +150,8 @@ func createCommon(ctx *context.Context) {
ctx.Data["Licenses"] = repo_module.Licenses
ctx.Data["Readmes"] = repo_module.Readmes
ctx.Data["IsForcedPrivate"] = setting.Repository.ForcePrivate
- ctx.Data["CanCreateRepo"] = ctx.Doer.CanCreateRepo()
- ctx.Data["MaxCreationLimit"] = ctx.Doer.MaxCreationLimit()
+ ctx.Data["CanCreateRepoInDoer"] = ctx.Doer.CanCreateRepoIn(ctx.Doer)
+ ctx.Data["MaxCreationLimitOfDoer"] = ctx.Doer.MaxCreationLimit()
ctx.Data["SupportedObjectFormats"] = git.DefaultFeatures().SupportedObjectFormats
ctx.Data["DefaultObjectFormat"] = git.Sha1ObjectFormat
}
@@ -304,112 +300,18 @@ 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"
-)
-
-// 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":
- err = acceptOrRejectRepoTransfer(ctx, true)
- case "reject_transfer":
- err = acceptOrRejectRepoTransfer(ctx, false)
- 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 {
- if errors.Is(err, user_model.ErrBlockedUser) {
- ctx.Flash.Error(ctx.Tr("repo.action.blocked_user"))
- } else {
- ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.PathParam("action")), 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)
-}
-
-func acceptOrRejectRepoTransfer(ctx *context.Context, accept bool) error {
- repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository)
- if err != nil {
- return err
- }
-
- if err := repoTransfer.LoadAttributes(ctx); err != nil {
- return err
- }
-
- if !repoTransfer.CanUserAcceptTransfer(ctx, ctx.Doer) {
- return errors.New("user does not have enough permissions")
- }
-
- if accept {
- if ctx.Repo.GitRepo != nil {
- ctx.Repo.GitRepo.Close()
- ctx.Repo.GitRepo = nil
- }
-
- if err := repo_service.TransferOwnership(ctx, repoTransfer.Doer, repoTransfer.Recipient, ctx.Repo.Repository, repoTransfer.Teams); err != nil {
- return err
- }
- ctx.Flash.Success(ctx.Tr("repo.settings.transfer.success"))
- } else {
- if err := repo_service.CancelRepositoryTransfer(ctx, ctx.Repo.Repository); err != nil {
- return err
- }
- ctx.Flash.Success(ctx.Tr("repo.settings.transfer.rejected"))
+func handleActionError(ctx *context.Context, err error) {
+ switch {
+ case errors.Is(err, user_model.ErrBlockedUser):
+ ctx.Flash.Error(ctx.Tr("repo.action.blocked_user"))
+ case repo_service.IsRepositoryLimitReached(err):
+ limit := err.(repo_service.LimitReachedError).Limit
+ ctx.Flash.Error(ctx.TrN(limit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", limit))
+ case errors.Is(err, util.ErrPermissionDenied):
+ ctx.HTTPError(http.StatusNotFound)
+ default:
+ ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.PathParam("action")), err)
}
-
- ctx.Redirect(ctx.Repo.Repository.Link())
- return nil
}
// RedirectDownload return a file based on the following infos:
@@ -433,7 +335,7 @@ func RedirectDownload(ctx *context.Context) {
release := releases[0]
att, err := repo_model.GetAttachmentByReleaseIDFileName(ctx, release.ID, fileName)
if err != nil {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
if att != nil {
@@ -445,12 +347,12 @@ func RedirectDownload(ctx *context.Context) {
// We only fetch the latest release if the tag is "latest" and no release with the tag "latest" exists
release, err := repo_model.GetLatestReleaseByRepoID(ctx, ctx.Repo.Repository.ID)
if err != nil {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
att, err := repo_model.GetAttachmentByReleaseIDFileName(ctx, release.ID, fileName)
if err != nil {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
if att != nil {
@@ -458,7 +360,7 @@ func RedirectDownload(ctx *context.Context) {
return
}
}
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
}
// Download an archive of a repository
@@ -466,9 +368,9 @@ func Download(ctx *context.Context) {
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, ctx.PathParam("*"))
if err != nil {
if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) {
- ctx.Error(http.StatusBadRequest, err.Error())
+ ctx.HTTPError(http.StatusBadRequest, err.Error())
} else if errors.Is(err, archiver_service.RepoRefNotFoundError{}) {
- ctx.Error(http.StatusNotFound, err.Error())
+ ctx.HTTPError(http.StatusNotFound, err.Error())
} else {
ctx.ServerError("archiver_service.NewRequest", err)
}
@@ -523,11 +425,11 @@ func download(ctx *context.Context, archiveName string, archiver *repo_model.Rep
func InitiateDownload(ctx *context.Context) {
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, ctx.PathParam("*"))
if err != nil {
- ctx.Error(http.StatusBadRequest, "invalid archive request")
+ ctx.HTTPError(http.StatusBadRequest, "invalid archive request")
return
}
if aReq == nil {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
@@ -559,7 +461,7 @@ func SearchRepo(ctx *context.Context) {
if page <= 0 {
page = 1
}
- opts := &repo_model.SearchRepoOptions{
+ opts := repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
Page: page,
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
@@ -599,7 +501,7 @@ func SearchRepo(ctx *context.Context) {
opts.Collaborate = optional.Some(true)
case "":
default:
- ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid search mode: \"%s\"", mode))
+ ctx.HTTPError(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid search mode: \"%s\"", mode))
return
}
@@ -621,11 +523,11 @@ func SearchRepo(ctx *context.Context) {
if orderBy, ok := searchModeMap[sortMode]; ok {
opts.OrderBy = orderBy
} else {
- ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid sort mode: \"%s\"", sortMode))
+ ctx.HTTPError(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid sort mode: \"%s\"", sortMode))
return
}
} else {
- ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid sort order: \"%s\"", sortOrder))
+ ctx.HTTPError(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid sort order: \"%s\"", sortOrder))
return
}
}
diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go
index d589586dad..12216fc620 100644
--- a/routers/web/repo/search.go
+++ b/routers/web/repo/search.go
@@ -5,11 +5,11 @@ package repo
import (
"net/http"
- "strings"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/git"
code_indexer "code.gitea.io/gitea/modules/indexer/code"
+ "code.gitea.io/gitea/modules/indexer/code/gitgrep"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/routers/common"
@@ -18,16 +18,6 @@ import (
const tplSearch templates.TplName = "repo/search"
-func indexSettingToGitGrepPathspecList() (list []string) {
- for _, expr := range setting.Indexer.IncludePatterns {
- list = append(list, ":(glob)"+expr.PatternString())
- }
- for _, expr := range setting.Indexer.ExcludePatterns {
- list = append(list, ":(glob,exclude)"+expr.PatternString())
- }
- return list
-}
-
// Search render repository search page
func Search(ctx *context.Context) {
ctx.Data["PageIsViewCode"] = true
@@ -48,10 +38,10 @@ func Search(ctx *context.Context) {
if setting.Indexer.RepoIndexerEnabled {
var err error
total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{
- RepoIDs: []int64{ctx.Repo.Repository.ID},
- Keyword: prepareSearch.Keyword,
- IsKeywordFuzzy: prepareSearch.IsFuzzy,
- Language: prepareSearch.Language,
+ RepoIDs: []int64{ctx.Repo.Repository.ID},
+ Keyword: prepareSearch.Keyword,
+ SearchMode: prepareSearch.SearchMode,
+ Language: prepareSearch.Language,
Paginator: &db.ListOptions{
Page: page,
PageSize: setting.UI.RepoSearchPagingNum,
@@ -67,32 +57,14 @@ func Search(ctx *context.Context) {
ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx)
}
} else {
- res, err := git.GrepSearch(ctx, ctx.Repo.GitRepo, prepareSearch.Keyword, git.GrepOptions{
- ContextLineNumber: 1,
- IsFuzzy: prepareSearch.IsFuzzy,
- RefName: git.RefNameFromBranch(ctx.Repo.Repository.DefaultBranch).String(), // BranchName should be default branch or the first existing branch
- PathspecList: indexSettingToGitGrepPathspecList(),
- })
+ var err error
+ // ref should be default branch or the first existing branch
+ searchRef := git.RefNameFromBranch(ctx.Repo.Repository.DefaultBranch)
+ searchResults, total, err = gitgrep.PerformSearch(ctx, page, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, searchRef, prepareSearch.Keyword, prepareSearch.SearchMode)
if err != nil {
- // TODO: if no branch exists, it reports: exit status 128, fatal: this operation must be run in a work tree.
- ctx.ServerError("GrepSearch", err)
+ ctx.ServerError("gitgrep.PerformSearch", err)
return
}
- total = len(res)
- pageStart := min((page-1)*setting.UI.RepoSearchPagingNum, len(res))
- pageEnd := min(page*setting.UI.RepoSearchPagingNum, len(res))
- res = res[pageStart:pageEnd]
- for _, r := range res {
- searchResults = append(searchResults, &code_indexer.Result{
- RepoID: ctx.Repo.Repository.ID,
- Filename: r.Filename,
- CommitID: ctx.Repo.CommitID,
- // UpdatedUnix: not supported yet
- // Language: not supported yet
- // Color: not supported yet
- Lines: code_indexer.HighlightSearchResultCode(r.Filename, "", r.LineNumbers, strings.Join(r.LineCodes, "\n")),
- })
- }
}
ctx.Data["Repo"] = ctx.Repo.Repository
diff --git a/routers/web/repo/search_test.go b/routers/web/repo/search_test.go
deleted file mode 100644
index 33a1610384..0000000000
--- a/routers/web/repo/search_test.go
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright 2024 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package repo
-
-import (
- "testing"
-
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/test"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestIndexSettingToGitGrepPathspecList(t *testing.T) {
- defer test.MockVariableValue(&setting.Indexer.IncludePatterns, setting.IndexerGlobFromString("a"))()
- defer test.MockVariableValue(&setting.Indexer.ExcludePatterns, setting.IndexerGlobFromString("b"))()
- assert.Equal(t, []string{":(glob)a", ":(glob,exclude)b"}, indexSettingToGitGrepPathspecList())
-}
diff --git a/routers/web/repo/setting/default_branch.go b/routers/web/repo/setting/default_branch.go
index 881d148afc..044c9e556a 100644
--- a/routers/web/repo/setting/default_branch.go
+++ b/routers/web/repo/setting/default_branch.go
@@ -34,7 +34,7 @@ func SetDefaultBranchPost(ctx *context.Context) {
}
branch := ctx.FormString("branch")
- if err := repo_service.SetRepoDefaultBranch(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, branch); err != nil {
+ if err := repo_service.SetRepoDefaultBranch(ctx, ctx.Repo.Repository, branch); err != nil {
switch {
case git_model.IsErrBranchNotExist(err):
ctx.Status(http.StatusNotFound)
@@ -49,6 +49,6 @@ func SetDefaultBranchPost(ctx *context.Context) {
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath())
default:
- ctx.NotFound("", nil)
+ ctx.NotFound(nil)
}
}
diff --git a/routers/web/repo/setting/git_hooks.go b/routers/web/repo/setting/git_hooks.go
index 1d92211303..ba4b5e85b6 100644
--- a/routers/web/repo/setting/git_hooks.go
+++ b/routers/web/repo/setting/git_hooks.go
@@ -34,7 +34,7 @@ func GitHooksEdit(ctx *context.Context) {
hook, err := ctx.Repo.GitRepo.GetHook(name)
if err != nil {
if err == git.ErrNotValidHook {
- ctx.NotFound("GetHook", err)
+ ctx.NotFound(err)
} else {
ctx.ServerError("GetHook", err)
}
@@ -50,7 +50,7 @@ func GitHooksEditPost(ctx *context.Context) {
hook, err := ctx.Repo.GitRepo.GetHook(name)
if err != nil {
if err == git.ErrNotValidHook {
- ctx.NotFound("GetHook", err)
+ ctx.NotFound(err)
} else {
ctx.ServerError("GetHook", err)
}
diff --git a/routers/web/repo/setting/lfs.go b/routers/web/repo/setting/lfs.go
index 2df483fa34..af6708e841 100644
--- a/routers/web/repo/setting/lfs.go
+++ b/routers/web/repo/setting/lfs.go
@@ -18,6 +18,7 @@ import (
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/git/attribute"
"code.gitea.io/gitea/modules/git/pipeline"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
@@ -41,13 +42,10 @@ const (
// LFSFiles shows a repository's LFS files
func LFSFiles(ctx *context.Context) {
if !setting.LFS.StartServer {
- ctx.NotFound("LFSFiles", nil)
+ ctx.NotFound(nil)
return
}
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
total, err := git_model.CountLFSMetaObjects(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.ServerError("LFSFiles", err)
@@ -71,15 +69,12 @@ func LFSFiles(ctx *context.Context) {
// LFSLocks shows a repository's LFS locks
func LFSLocks(ctx *context.Context) {
if !setting.LFS.StartServer {
- ctx.NotFound("LFSLocks", nil)
+ ctx.NotFound(nil)
return
}
ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
total, err := git_model.CountLFSLockByRepoID(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.ServerError("LFSLocks", err)
@@ -109,17 +104,13 @@ func LFSLocks(ctx *context.Context) {
}
// Clone base repo.
- tmpBasePath, err := repo_module.CreateTemporaryPath("locks")
+ tmpBasePath, cleanup, err := repo_module.CreateTemporaryPath("locks")
if err != nil {
log.Error("Failed to create temporary path: %v", err)
ctx.ServerError("LFSLocks", err)
return
}
- defer func() {
- if err := repo_module.RemoveTemporaryPath(tmpBasePath); err != nil {
- log.Error("LFSLocks: RemoveTemporaryPath: %v", err)
- }
- }()
+ defer cleanup()
if err := git.Clone(ctx, ctx.Repo.Repository.RepoPath(), tmpBasePath, git.CloneRepoOptions{
Bare: true,
@@ -138,39 +129,24 @@ func LFSLocks(ctx *context.Context) {
}
defer gitRepo.Close()
- filenames := make([]string, len(lfsLocks))
-
- for i, lock := range lfsLocks {
- filenames[i] = lock.Path
- }
-
- if err := gitRepo.ReadTreeToIndex(ctx.Repo.Repository.DefaultBranch); err != nil {
- log.Error("Unable to read the default branch to the index: %s (%v)", ctx.Repo.Repository.DefaultBranch, err)
- ctx.ServerError("LFSLocks", fmt.Errorf("unable to read the default branch to the index: %s (%w)", ctx.Repo.Repository.DefaultBranch, err))
- return
- }
-
- name2attribute2info, err := gitRepo.CheckAttribute(git.CheckAttributeOpts{
- Attributes: []string{"lockable"},
- Filenames: filenames,
- CachedOnly: true,
- })
+ checker, err := attribute.NewBatchChecker(gitRepo, ctx.Repo.Repository.DefaultBranch, []string{attribute.Lockable})
if err != nil {
log.Error("Unable to check attributes in %s (%v)", tmpBasePath, err)
ctx.ServerError("LFSLocks", err)
return
}
+ defer checker.Close()
lockables := make([]bool, len(lfsLocks))
+ filenames := make([]string, len(lfsLocks))
for i, lock := range lfsLocks {
- attribute2info, has := name2attribute2info[lock.Path]
- if !has {
- continue
- }
- if attribute2info["lockable"] != "set" {
+ filenames[i] = lock.Path
+ attrs, err := checker.CheckPath(lock.Path)
+ if err != nil {
+ log.Error("Unable to check attributes in %s: %s (%v)", tmpBasePath, lock.Path, err)
continue
}
- lockables[i] = true
+ lockables[i] = attrs.Get(attribute.Lockable).ToBool().Value()
}
ctx.Data["Lockables"] = lockables
@@ -197,7 +173,7 @@ func LFSLocks(ctx *context.Context) {
// LFSLockFile locks a file
func LFSLockFile(ctx *context.Context) {
if !setting.LFS.StartServer {
- ctx.NotFound("LFSLocks", nil)
+ ctx.NotFound(nil)
return
}
originalPath := ctx.FormString("path")
@@ -238,7 +214,7 @@ func LFSLockFile(ctx *context.Context) {
// LFSUnlock forcibly unlocks an LFS lock
func LFSUnlock(ctx *context.Context) {
if !setting.LFS.StartServer {
- ctx.NotFound("LFSUnlock", nil)
+ ctx.NotFound(nil)
return
}
_, err := git_model.DeleteLFSLockByID(ctx, ctx.PathParamInt64("lid"), ctx.Repo.Repository, ctx.Doer, true)
@@ -252,7 +228,7 @@ func LFSUnlock(ctx *context.Context) {
// LFSFileGet serves a single LFS file
func LFSFileGet(ctx *context.Context) {
if !setting.LFS.StartServer {
- ctx.NotFound("LFSFileGet", nil)
+ ctx.NotFound(nil)
return
}
ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
@@ -260,7 +236,7 @@ func LFSFileGet(ctx *context.Context) {
p := lfs.Pointer{Oid: oid}
if !p.IsValid() {
- ctx.NotFound("LFSFileGet", nil)
+ ctx.NotFound(nil)
return
}
@@ -269,7 +245,7 @@ func LFSFileGet(ctx *context.Context) {
meta, err := git_model.GetLFSMetaObjectByOid(ctx, ctx.Repo.Repository.ID, oid)
if err != nil {
if err == git_model.ErrLFSObjectNotExist {
- ctx.NotFound("LFSFileGet", nil)
+ ctx.NotFound(nil)
return
}
ctx.ServerError("LFSFileGet", err)
@@ -291,8 +267,10 @@ func LFSFileGet(ctx *context.Context) {
buf = buf[:n]
st := typesniffer.DetectContentType(buf)
+ // FIXME: there is no IsPlainText set, but template uses it
ctx.Data["IsTextFile"] = st.IsText()
ctx.Data["FileSize"] = meta.Size
+ // FIXME: the last field is the URL-base64-encoded filename, it should not be "direct"
ctx.Data["RawFileLink"] = fmt.Sprintf("%s%s/%s.git/info/lfs/objects/%s/%s", setting.AppURL, url.PathEscape(ctx.Repo.Repository.OwnerName), url.PathEscape(ctx.Repo.Repository.Name), url.PathEscape(meta.Oid), "direct")
switch {
case st.IsRepresentableAsText():
@@ -333,8 +311,6 @@ func LFSFileGet(ctx *context.Context) {
}
ctx.Data["LineNums"] = gotemplate.HTML(output.String())
- case st.IsPDF():
- ctx.Data["IsPDFFile"] = true
case st.IsVideo():
ctx.Data["IsVideoFile"] = true
case st.IsAudio():
@@ -350,13 +326,13 @@ func LFSFileGet(ctx *context.Context) {
// LFSDelete disassociates the provided oid from the repository and if the lfs file is no longer associated with any repositories - deletes it
func LFSDelete(ctx *context.Context) {
if !setting.LFS.StartServer {
- ctx.NotFound("LFSDelete", nil)
+ ctx.NotFound(nil)
return
}
oid := ctx.PathParam("oid")
p := lfs.Pointer{Oid: oid}
if !p.IsValid() {
- ctx.NotFound("LFSDelete", nil)
+ ctx.NotFound(nil)
return
}
@@ -381,13 +357,13 @@ func LFSDelete(ctx *context.Context) {
// LFSFileFind guesses a sha for the provided oid (or uses the provided sha) and then finds the commits that contain this sha
func LFSFileFind(ctx *context.Context) {
if !setting.LFS.StartServer {
- ctx.NotFound("LFSFind", nil)
+ ctx.NotFound(nil)
return
}
oid := ctx.FormString("oid")
size := ctx.FormInt64("size")
if len(oid) == 0 || size == 0 {
- ctx.NotFound("LFSFind", nil)
+ ctx.NotFound(nil)
return
}
sha := ctx.FormString("sha")
@@ -421,7 +397,7 @@ func LFSFileFind(ctx *context.Context) {
// LFSPointerFiles will search the repository for pointer files and report which are missing LFS files in the content store
func LFSPointerFiles(ctx *context.Context) {
if !setting.LFS.StartServer {
- ctx.NotFound("LFSFileGet", nil)
+ ctx.NotFound(nil)
return
}
ctx.Data["PageIsSettingsLFS"] = true
@@ -532,7 +508,7 @@ func LFSPointerFiles(ctx *context.Context) {
// LFSAutoAssociate auto associates accessible lfs files
func LFSAutoAssociate(ctx *context.Context) {
if !setting.LFS.StartServer {
- ctx.NotFound("LFSAutoAssociate", nil)
+ ctx.NotFound(nil)
return
}
oids := ctx.FormStrings("oid")
diff --git a/routers/web/repo/setting/protected_branch.go b/routers/web/repo/setting/protected_branch.go
index 06a9e69507..0eea5e3f34 100644
--- a/routers/web/repo/setting/protected_branch.go
+++ b/routers/web/repo/setting/protected_branch.go
@@ -8,6 +8,7 @@ import (
"fmt"
"net/http"
"net/url"
+ "strconv"
"strings"
"time"
@@ -16,6 +17,7 @@ import (
"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/base"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web"
@@ -88,7 +90,7 @@ func SettingsProtectedBranch(c *context.Context) {
c.Data["recent_status_checks"] = contexts
if c.Repo.Owner.IsOrganization() {
- teams, err := organization.OrgFromUser(c.Repo.Owner).TeamsWithAccessToRepo(c, c.Repo.Repository.ID, perm.AccessModeRead)
+ teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(c, c.Repo.Owner.ID, c.Repo.Repository.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests)
if err != nil {
c.ServerError("Repo.Owner.TeamsWithAccessToRepo", err)
return
@@ -110,7 +112,7 @@ func SettingsProtectedBranchPost(ctx *context.Context) {
var protectBranch *git_model.ProtectedBranch
if f.RuleName == "" {
ctx.Flash.Error(ctx.Tr("repo.settings.protected_branch_required_rule_name"))
- ctx.Redirect(fmt.Sprintf("%s/settings/branches/edit", ctx.Repo.RepoLink))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings/branches/edit")
return
}
@@ -261,7 +263,7 @@ func SettingsProtectedBranchPost(ctx *context.Context) {
protectBranch.BlockOnOutdatedBranch = f.BlockOnOutdatedBranch
protectBranch.BlockAdminMergeOverride = f.BlockAdminMergeOverride
- err = git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{
+ if err = pull_service.CreateOrUpdateProtectedBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{
UserIDs: whitelistUsers,
TeamIDs: whitelistTeams,
ForcePushUserIDs: forcePushAllowlistUsers,
@@ -270,24 +272,10 @@ func SettingsProtectedBranchPost(ctx *context.Context) {
MergeTeamIDs: mergeWhitelistTeams,
ApprovalsUserIDs: approvalsWhitelistUsers,
ApprovalsTeamIDs: approvalsWhitelistTeams,
- })
- if err != nil {
- ctx.ServerError("UpdateProtectBranch", err)
- return
- }
-
- // FIXME: since we only need to recheck files protected rules, we could improve this
- matchedBranches, err := git_model.FindAllMatchedBranches(ctx, ctx.Repo.Repository.ID, protectBranch.RuleName)
- if err != nil {
- ctx.ServerError("FindAllMatchedBranches", err)
+ }); err != nil {
+ ctx.ServerError("CreateOrUpdateProtectedBranch", err)
return
}
- for _, branchName := range matchedBranches {
- if err = pull_service.CheckPRsForBaseBranch(ctx, ctx.Repo.Repository, branchName); err != nil {
- ctx.ServerError("CheckPRsForBaseBranch", err)
- return
- }
- }
ctx.Flash.Success(ctx.Tr("repo.settings.update_protect_branch_success", protectBranch.RuleName))
ctx.Redirect(fmt.Sprintf("%s/settings/branches?rule_name=%s", ctx.Repo.RepoLink, protectBranch.RuleName))
@@ -297,32 +285,32 @@ func SettingsProtectedBranchPost(ctx *context.Context) {
func DeleteProtectedBranchRulePost(ctx *context.Context) {
ruleID := ctx.PathParamInt64("id")
if ruleID <= 0 {
- ctx.Flash.Error(ctx.Tr("repo.settings.remove_protected_branch_failed", fmt.Sprintf("%d", ruleID)))
- ctx.JSONRedirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink))
+ ctx.Flash.Error(ctx.Tr("repo.settings.remove_protected_branch_failed", strconv.FormatInt(ruleID, 10)))
+ ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings/branches")
return
}
rule, err := git_model.GetProtectedBranchRuleByID(ctx, ctx.Repo.Repository.ID, ruleID)
if err != nil {
- ctx.Flash.Error(ctx.Tr("repo.settings.remove_protected_branch_failed", fmt.Sprintf("%d", ruleID)))
- ctx.JSONRedirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink))
+ ctx.Flash.Error(ctx.Tr("repo.settings.remove_protected_branch_failed", strconv.FormatInt(ruleID, 10)))
+ ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings/branches")
return
}
if rule == nil {
- ctx.Flash.Error(ctx.Tr("repo.settings.remove_protected_branch_failed", fmt.Sprintf("%d", ruleID)))
- ctx.JSONRedirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink))
+ ctx.Flash.Error(ctx.Tr("repo.settings.remove_protected_branch_failed", strconv.FormatInt(ruleID, 10)))
+ ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings/branches")
return
}
if err := git_model.DeleteProtectedBranch(ctx, ctx.Repo.Repository, ruleID); err != nil {
ctx.Flash.Error(ctx.Tr("repo.settings.remove_protected_branch_failed", rule.RuleName))
- ctx.JSONRedirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink))
+ ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings/branches")
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.remove_protected_branch_success", rule.RuleName))
- ctx.JSONRedirect(fmt.Sprintf("%s/settings/branches", ctx.Repo.RepoLink))
+ ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings/branches")
}
func UpdateBranchProtectionPriories(ctx *context.Context) {
@@ -340,13 +328,13 @@ func RenameBranchPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.RenameBranchForm)
if !ctx.Repo.CanCreateBranch() {
- ctx.NotFound("RenameBranch", nil)
+ ctx.NotFound(nil)
return
}
if ctx.HasError() {
ctx.Flash.Error(ctx.GetErrMsg())
- ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink))
+ ctx.Redirect(ctx.Repo.RepoLink + "/branches")
return
}
@@ -355,13 +343,13 @@ func RenameBranchPost(ctx *context.Context) {
switch {
case repo_model.IsErrUserDoesNotHaveAccessToRepo(err):
ctx.Flash.Error(ctx.Tr("repo.branch.rename_default_or_protected_branch_error"))
- ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink))
+ ctx.Redirect(ctx.Repo.RepoLink + "/branches")
case git_model.IsErrBranchAlreadyExists(err):
ctx.Flash.Error(ctx.Tr("repo.branch.branch_already_exists", form.To))
- ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink))
+ ctx.Redirect(ctx.Repo.RepoLink + "/branches")
case errors.Is(err, git_model.ErrBranchIsProtected):
ctx.Flash.Error(ctx.Tr("repo.branch.rename_protected_branch_failed"))
- ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink))
+ ctx.Redirect(ctx.Repo.RepoLink + "/branches")
default:
ctx.ServerError("RenameBranch", err)
}
@@ -370,16 +358,16 @@ func RenameBranchPost(ctx *context.Context) {
if msg == "target_exist" {
ctx.Flash.Error(ctx.Tr("repo.settings.rename_branch_failed_exist", form.To))
- ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink))
+ ctx.Redirect(ctx.Repo.RepoLink + "/branches")
return
}
if msg == "from_not_exist" {
ctx.Flash.Error(ctx.Tr("repo.settings.rename_branch_failed_not_exist", form.From))
- ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink))
+ ctx.Redirect(ctx.Repo.RepoLink + "/branches")
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.rename_branch_success", form.From, form.To))
- ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink))
+ ctx.Redirect(ctx.Repo.RepoLink + "/branches")
}
diff --git a/routers/web/repo/setting/protected_tag.go b/routers/web/repo/setting/protected_tag.go
index 1730ad4a8b..50f5a28c4c 100644
--- a/routers/web/repo/setting/protected_tag.go
+++ b/routers/web/repo/setting/protected_tag.go
@@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
+ "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
@@ -156,7 +157,7 @@ func setTagsContext(ctx *context.Context) error {
ctx.Data["Users"] = users
if ctx.Repo.Owner.IsOrganization() {
- teams, err := organization.OrgFromUser(ctx.Repo.Owner).TeamsWithAccessToRepo(ctx, ctx.Repo.Repository.ID, perm.AccessModeRead)
+ teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, ctx.Repo.Owner.ID, ctx.Repo.Repository.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests)
if err != nil {
ctx.ServerError("Repo.Owner.TeamsWithAccessToRepo", err)
return err
@@ -183,7 +184,7 @@ func selectProtectedTagByContext(ctx *context.Context) *git_model.ProtectedTag {
return tag
}
- ctx.NotFound("", fmt.Errorf("ProtectedTag[%v] not associated to repository %v", id, ctx.Repo.Repository))
+ ctx.NotFound(fmt.Errorf("ProtectedTag[%v] not associated to repository %v", id, ctx.Repo.Repository))
return nil
}
diff --git a/routers/web/repo/setting/public_access.go b/routers/web/repo/setting/public_access.go
new file mode 100644
index 0000000000..368d34294a
--- /dev/null
+++ b/routers/web/repo/setting/public_access.go
@@ -0,0 +1,155 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package setting
+
+import (
+ "net/http"
+ "slices"
+ "strconv"
+
+ "code.gitea.io/gitea/models/perm"
+ "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unit"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/services/context"
+)
+
+const tplRepoSettingsPublicAccess templates.TplName = "repo/settings/public_access"
+
+func parsePublicAccessMode(permission string, allowed []string) (ret struct {
+ AnonymousAccessMode, EveryoneAccessMode perm.AccessMode
+},
+) {
+ ret.AnonymousAccessMode = perm.AccessModeNone
+ ret.EveryoneAccessMode = perm.AccessModeNone
+
+ // if site admin forces repositories to be private, then do not allow any other access mode,
+ // otherwise the "force private" setting would be bypassed
+ if setting.Repository.ForcePrivate {
+ return ret
+ }
+ if !slices.Contains(allowed, permission) {
+ return ret
+ }
+ switch permission {
+ case paAnonymousRead:
+ ret.AnonymousAccessMode = perm.AccessModeRead
+ case paEveryoneRead:
+ ret.EveryoneAccessMode = perm.AccessModeRead
+ case paEveryoneWrite:
+ ret.EveryoneAccessMode = perm.AccessModeWrite
+ }
+ return ret
+}
+
+const (
+ paNotSet = "not-set"
+ paAnonymousRead = "anonymous-read"
+ paEveryoneRead = "everyone-read"
+ paEveryoneWrite = "everyone-write"
+)
+
+type repoUnitPublicAccess struct {
+ UnitType unit.Type
+ FormKey string
+ DisplayName string
+ PublicAccessTypes []string
+ UnitPublicAccess string
+}
+
+func repoUnitPublicAccesses(ctx *context.Context) []*repoUnitPublicAccess {
+ accesses := []*repoUnitPublicAccess{
+ {
+ UnitType: unit.TypeCode,
+ DisplayName: ctx.Locale.TrString("repo.code"),
+ PublicAccessTypes: []string{paAnonymousRead, paEveryoneRead},
+ },
+ {
+ UnitType: unit.TypeIssues,
+ DisplayName: ctx.Locale.TrString("issues"),
+ PublicAccessTypes: []string{paAnonymousRead, paEveryoneRead},
+ },
+ {
+ UnitType: unit.TypePullRequests,
+ DisplayName: ctx.Locale.TrString("pull_requests"),
+ PublicAccessTypes: []string{paAnonymousRead, paEveryoneRead},
+ },
+ {
+ UnitType: unit.TypeReleases,
+ DisplayName: ctx.Locale.TrString("repo.releases"),
+ PublicAccessTypes: []string{paAnonymousRead, paEveryoneRead},
+ },
+ {
+ UnitType: unit.TypeWiki,
+ DisplayName: ctx.Locale.TrString("repo.wiki"),
+ PublicAccessTypes: []string{paAnonymousRead, paEveryoneRead, paEveryoneWrite},
+ },
+ {
+ UnitType: unit.TypeProjects,
+ DisplayName: ctx.Locale.TrString("repo.projects"),
+ PublicAccessTypes: []string{paAnonymousRead, paEveryoneRead},
+ },
+ {
+ UnitType: unit.TypePackages,
+ DisplayName: ctx.Locale.TrString("repo.packages"),
+ PublicAccessTypes: []string{paAnonymousRead, paEveryoneRead},
+ },
+ {
+ UnitType: unit.TypeActions,
+ DisplayName: ctx.Locale.TrString("repo.actions"),
+ PublicAccessTypes: []string{paAnonymousRead, paEveryoneRead},
+ },
+ }
+ for _, ua := range accesses {
+ ua.FormKey = "repo-unit-access-" + strconv.Itoa(int(ua.UnitType))
+ for _, u := range ctx.Repo.Repository.Units {
+ if u.Type == ua.UnitType {
+ ua.UnitPublicAccess = paNotSet
+ switch {
+ case u.EveryoneAccessMode == perm.AccessModeWrite:
+ ua.UnitPublicAccess = paEveryoneWrite
+ case u.EveryoneAccessMode == perm.AccessModeRead:
+ ua.UnitPublicAccess = paEveryoneRead
+ case u.AnonymousAccessMode == perm.AccessModeRead:
+ ua.UnitPublicAccess = paAnonymousRead
+ }
+ break
+ }
+ }
+ }
+ return slices.DeleteFunc(accesses, func(ua *repoUnitPublicAccess) bool {
+ return ua.UnitPublicAccess == ""
+ })
+}
+
+func PublicAccess(ctx *context.Context) {
+ ctx.Data["PageIsSettingsPublicAccess"] = true
+ ctx.Data["RepoUnitPublicAccesses"] = repoUnitPublicAccesses(ctx)
+ ctx.Data["GlobalForcePrivate"] = setting.Repository.ForcePrivate
+ if setting.Repository.ForcePrivate {
+ ctx.Flash.Error(ctx.Tr("form.repository_force_private"), true)
+ }
+ ctx.HTML(http.StatusOK, tplRepoSettingsPublicAccess)
+}
+
+func PublicAccessPost(ctx *context.Context) {
+ accesses := repoUnitPublicAccesses(ctx)
+ for _, ua := range accesses {
+ formVal := ctx.FormString(ua.FormKey)
+ parsed := parsePublicAccessMode(formVal, ua.PublicAccessTypes)
+ err := repo.UpdateRepoUnitPublicAccess(ctx, &repo.RepoUnit{
+ RepoID: ctx.Repo.Repository.ID,
+ Type: ua.UnitType,
+ AnonymousAccessMode: parsed.AnonymousAccessMode,
+ EveryoneAccessMode: parsed.EveryoneAccessMode,
+ })
+ if err != nil {
+ ctx.ServerError("UpdateRepoUnitPublicAccess", err)
+ return
+ }
+ }
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+ ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/public_access")
+}
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/secrets.go b/routers/web/repo/setting/secrets.go
index 46cb875f9b..c6e2d18249 100644
--- a/routers/web/repo/setting/secrets.go
+++ b/routers/web/repo/setting/secrets.go
@@ -44,9 +44,8 @@ func getSecretsCtx(ctx *context.Context) (*secretsCtx, error) {
}
if ctx.Data["PageIsOrgSettings"] == true {
- err := shared_user.LoadHeaderCount(ctx)
- if err != nil {
- ctx.ServerError("LoadHeaderCount", err)
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
return nil, nil
}
return &secretsCtx{
diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go
index 5b34fc60da..6e16ead183 100644
--- a/routers/web/repo/setting/setting.go
+++ b/routers/web/repo/setting/setting.go
@@ -6,15 +6,12 @@ package setting
import (
"errors"
- "fmt"
"net/http"
"strings"
"time"
- actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
- "code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
@@ -38,6 +35,8 @@ import (
mirror_service "code.gitea.io/gitea/services/mirror"
repo_service "code.gitea.io/gitea/services/repository"
wiki_service "code.gitea.io/gitea/services/wiki"
+
+ "xorm.io/xorm/convert"
)
const (
@@ -49,15 +48,6 @@ const (
tplDeployKeys templates.TplName = "repo/settings/deploy_keys"
)
-func parseEveryoneAccessMode(permission string, allowed ...perm.AccessMode) perm.AccessMode {
- // if site admin forces repositories to be private, then do not allow any other access mode,
- // otherwise the "force private" setting would be bypassed
- if setting.Repository.ForcePrivate {
- return perm.AccessModeNone
- }
- return perm.ParseAccessMode(permission, allowed...)
-}
-
// SettingsCtxData is a middleware that sets all the general context data for the
// settings template.
func SettingsCtxData(ctx *context.Context) {
@@ -69,9 +59,10 @@ func SettingsCtxData(ctx *context.Context) {
ctx.Data["DisableNewPushMirrors"] = setting.Mirror.DisableNewPush
ctx.Data["DefaultMirrorInterval"] = setting.Mirror.DefaultInterval
ctx.Data["MinimumMirrorInterval"] = setting.Mirror.MinInterval
+ ctx.Data["CanConvertFork"] = ctx.Repo.Repository.IsFork && ctx.Doer.CanCreateRepoIn(ctx.Repo.Repository.Owner)
signing, _ := asymkey_service.SigningKey(ctx, ctx.Repo.Repository.RepoPath())
- ctx.Data["SigningKeyAvailable"] = len(signing) > 0
+ ctx.Data["SigningKeyAvailable"] = signing != nil
ctx.Data["SigningSettings"] = setting.Repository.Signing
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
@@ -106,8 +97,6 @@ func Settings(ctx *context.Context) {
// SettingsPost response for changes of a repository
func SettingsPost(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.RepoSettingForm)
-
ctx.Data["ForcePrivate"] = setting.Repository.ForcePrivate
ctx.Data["MirrorsEnabled"] = setting.Mirror.Enabled
ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull
@@ -116,877 +105,937 @@ func SettingsPost(ctx *context.Context) {
ctx.Data["MinimumMirrorInterval"] = setting.Mirror.MinInterval
signing, _ := asymkey_service.SigningKey(ctx, ctx.Repo.Repository.RepoPath())
- ctx.Data["SigningKeyAvailable"] = len(signing) > 0
+ ctx.Data["SigningKeyAvailable"] = signing != nil
ctx.Data["SigningSettings"] = setting.Repository.Signing
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
- repo := ctx.Repo.Repository
-
switch ctx.FormString("action") {
case "update":
- if ctx.HasError() {
- ctx.HTML(http.StatusOK, tplSettingsOptions)
- return
- }
+ handleSettingsPostUpdate(ctx)
+ case "mirror":
+ handleSettingsPostMirror(ctx)
+ case "mirror-sync":
+ handleSettingsPostMirrorSync(ctx)
+ case "push-mirror-sync":
+ handleSettingsPostPushMirrorSync(ctx)
+ case "push-mirror-update":
+ handleSettingsPostPushMirrorUpdate(ctx)
+ case "push-mirror-remove":
+ handleSettingsPostPushMirrorRemove(ctx)
+ case "push-mirror-add":
+ handleSettingsPostPushMirrorAdd(ctx)
+ case "advanced":
+ handleSettingsPostAdvanced(ctx)
+ case "signing":
+ handleSettingsPostSigning(ctx)
+ case "admin":
+ handleSettingsPostAdmin(ctx)
+ case "admin_index":
+ handleSettingsPostAdminIndex(ctx)
+ case "convert":
+ handleSettingsPostConvert(ctx)
+ case "convert_fork":
+ handleSettingsPostConvertFork(ctx)
+ case "transfer":
+ handleSettingsPostTransfer(ctx)
+ case "cancel_transfer":
+ handleSettingsPostCancelTransfer(ctx)
+ case "delete":
+ handleSettingsPostDelete(ctx)
+ case "delete-wiki":
+ handleSettingsPostDeleteWiki(ctx)
+ case "archive":
+ handleSettingsPostArchive(ctx)
+ case "unarchive":
+ handleSettingsPostUnarchive(ctx)
+ case "visibility":
+ handleSettingsPostVisibility(ctx)
+ default:
+ ctx.NotFound(nil)
+ }
+}
- newRepoName := form.RepoName
- // Check if repository name has been changed.
- if repo.LowerName != strings.ToLower(newRepoName) {
- // Close the GitRepo if open
- if ctx.Repo.GitRepo != nil {
- ctx.Repo.GitRepo.Close()
- ctx.Repo.GitRepo = nil
- }
- if err := repo_service.ChangeRepositoryName(ctx, ctx.Doer, repo, newRepoName); err != nil {
+func handleSettingsPostUpdate(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
+ repo := ctx.Repo.Repository
+ if ctx.HasError() {
+ ctx.HTML(http.StatusOK, tplSettingsOptions)
+ return
+ }
+
+ newRepoName := form.RepoName
+ // Check if repository name has been changed.
+ if repo.LowerName != strings.ToLower(newRepoName) {
+ // Close the GitRepo if open
+ if ctx.Repo.GitRepo != nil {
+ ctx.Repo.GitRepo.Close()
+ ctx.Repo.GitRepo = nil
+ }
+ if err := repo_service.ChangeRepositoryName(ctx, ctx.Doer, repo, newRepoName); err != nil {
+ ctx.Data["Err_RepoName"] = true
+ switch {
+ case repo_model.IsErrRepoAlreadyExist(err):
+ ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tplSettingsOptions, &form)
+ case db.IsErrNameReserved(err):
+ ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplSettingsOptions, &form)
+ case repo_model.IsErrRepoFilesAlreadyExist(err):
ctx.Data["Err_RepoName"] = true
switch {
- case repo_model.IsErrRepoAlreadyExist(err):
- ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tplSettingsOptions, &form)
- case db.IsErrNameReserved(err):
- ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplSettingsOptions, &form)
- case repo_model.IsErrRepoFilesAlreadyExist(err):
- ctx.Data["Err_RepoName"] = true
- switch {
- case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
- ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplSettingsOptions, form)
- case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
- ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplSettingsOptions, form)
- case setting.Repository.AllowDeleteOfUnadoptedRepositories:
- ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplSettingsOptions, form)
- default:
- ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplSettingsOptions, form)
- }
- case db.IsErrNamePatternNotAllowed(err):
- ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form)
+ case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
+ ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplSettingsOptions, form)
+ case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
+ ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplSettingsOptions, form)
+ case setting.Repository.AllowDeleteOfUnadoptedRepositories:
+ ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplSettingsOptions, form)
default:
- ctx.ServerError("ChangeRepositoryName", err)
+ ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplSettingsOptions, form)
}
- return
+ case db.IsErrNamePatternNotAllowed(err):
+ ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form)
+ default:
+ ctx.ServerError("ChangeRepositoryName", err)
}
-
- log.Trace("Repository name changed: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newRepoName)
- }
- // In case it's just a case change.
- repo.Name = newRepoName
- repo.LowerName = strings.ToLower(newRepoName)
- repo.Description = form.Description
- repo.Website = form.Website
- repo.IsTemplate = form.Template
-
- // Visibility of forked repository is forced sync with base repository.
- if repo.IsFork {
- form.Private = repo.BaseRepo.IsPrivate || repo.BaseRepo.Owner.Visibility == structs.VisibleTypePrivate
- }
-
- if err := repo_service.UpdateRepository(ctx, repo, false); err != nil {
- ctx.ServerError("UpdateRepository", err)
return
}
- log.Trace("Repository basic settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
- ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
- ctx.Redirect(repo.Link() + "/settings")
+ log.Trace("Repository name changed: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newRepoName)
+ }
+ // In case it's just a case change.
+ repo.Name = newRepoName
+ repo.LowerName = strings.ToLower(newRepoName)
+ repo.Description = form.Description
+ repo.Website = form.Website
+ repo.IsTemplate = form.Template
+
+ // Visibility of forked repository is forced sync with base repository.
+ if repo.IsFork {
+ form.Private = repo.BaseRepo.IsPrivate || repo.BaseRepo.Owner.Visibility == structs.VisibleTypePrivate
+ }
- case "mirror":
- if !setting.Mirror.Enabled || !repo.IsMirror || repo.IsArchived {
- ctx.NotFound("", nil)
- return
- }
+ if err := repo_service.UpdateRepository(ctx, repo, false); err != nil {
+ ctx.ServerError("UpdateRepository", err)
+ return
+ }
+ log.Trace("Repository basic settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
- pullMirror, err := repo_model.GetMirrorByRepoID(ctx, ctx.Repo.Repository.ID)
- if err == repo_model.ErrMirrorNotExist {
- ctx.NotFound("", nil)
- return
- }
- if err != nil {
- ctx.ServerError("GetMirrorByRepoID", err)
- return
- }
- // This section doesn't require repo_name/RepoName to be set in the form, don't show it
- // as an error on the UI for this action
- ctx.Data["Err_RepoName"] = nil
-
- interval, err := time.ParseDuration(form.Interval)
- if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
- ctx.Data["Err_Interval"] = true
- ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
- return
- }
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+ ctx.Redirect(repo.Link() + "/settings")
+}
- pullMirror.EnablePrune = form.EnablePrune
- pullMirror.Interval = interval
- pullMirror.ScheduleNextUpdate()
- if err := repo_model.UpdateMirror(ctx, pullMirror); err != nil {
- ctx.ServerError("UpdateMirror", err)
- return
- }
+func handleSettingsPostMirror(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
+ repo := ctx.Repo.Repository
+ if !setting.Mirror.Enabled || !repo.IsMirror || repo.IsArchived {
+ ctx.NotFound(nil)
+ return
+ }
- u, err := git.GetRemoteURL(ctx, ctx.Repo.Repository.RepoPath(), pullMirror.GetRemoteName())
- if err != nil {
- ctx.Data["Err_MirrorAddress"] = true
- handleSettingRemoteAddrError(ctx, err, form)
- return
- }
- if u.User != nil && form.MirrorPassword == "" && form.MirrorUsername == u.User.Username() {
- form.MirrorPassword, _ = u.User.Password()
- }
+ pullMirror, err := repo_model.GetMirrorByRepoID(ctx, ctx.Repo.Repository.ID)
+ if err == repo_model.ErrMirrorNotExist {
+ ctx.NotFound(nil)
+ return
+ }
+ if err != nil {
+ ctx.ServerError("GetMirrorByRepoID", err)
+ return
+ }
+ // This section doesn't require repo_name/RepoName to be set in the form, don't show it
+ // as an error on the UI for this action
+ ctx.Data["Err_RepoName"] = nil
+
+ interval, err := time.ParseDuration(form.Interval)
+ if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
+ ctx.Data["Err_Interval"] = true
+ ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
+ return
+ }
- address, err := git.ParseRemoteAddr(form.MirrorAddress, form.MirrorUsername, form.MirrorPassword)
- if err == nil {
- err = migrations.IsMigrateURLAllowed(address, ctx.Doer)
- }
- if err != nil {
- ctx.Data["Err_MirrorAddress"] = true
- handleSettingRemoteAddrError(ctx, err, form)
- return
- }
+ pullMirror.EnablePrune = form.EnablePrune
+ pullMirror.Interval = interval
+ pullMirror.ScheduleNextUpdate()
+ if err := repo_model.UpdateMirror(ctx, pullMirror); err != nil {
+ ctx.ServerError("UpdateMirror", err)
+ return
+ }
- if err := mirror_service.UpdateAddress(ctx, pullMirror, address); err != nil {
- ctx.ServerError("UpdateAddress", err)
- return
- }
+ u, err := git.GetRemoteURL(ctx, ctx.Repo.Repository.RepoPath(), pullMirror.GetRemoteName())
+ if err != nil {
+ ctx.Data["Err_MirrorAddress"] = true
+ handleSettingRemoteAddrError(ctx, err, form)
+ return
+ }
+ if u.User != nil && form.MirrorPassword == "" && form.MirrorUsername == u.User.Username() {
+ form.MirrorPassword, _ = u.User.Password()
+ }
- remoteAddress, err := util.SanitizeURL(form.MirrorAddress)
- if err != nil {
- ctx.Data["Err_MirrorAddress"] = true
- handleSettingRemoteAddrError(ctx, err, form)
- return
- }
- pullMirror.RemoteAddress = remoteAddress
+ address, err := git.ParseRemoteAddr(form.MirrorAddress, form.MirrorUsername, form.MirrorPassword)
+ if err == nil {
+ err = migrations.IsMigrateURLAllowed(address, ctx.Doer)
+ }
+ if err != nil {
+ ctx.Data["Err_MirrorAddress"] = true
+ handleSettingRemoteAddrError(ctx, err, form)
+ return
+ }
- form.LFS = form.LFS && setting.LFS.StartServer
+ if err := mirror_service.UpdateAddress(ctx, pullMirror, address); err != nil {
+ ctx.ServerError("UpdateAddress", err)
+ return
+ }
- if len(form.LFSEndpoint) > 0 {
- ep := lfs.DetermineEndpoint("", form.LFSEndpoint)
- if ep == nil {
- ctx.Data["Err_LFSEndpoint"] = true
- ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_lfs_endpoint"), tplSettingsOptions, &form)
- return
- }
- err = migrations.IsMigrateURLAllowed(ep.String(), ctx.Doer)
- if err != nil {
- ctx.Data["Err_LFSEndpoint"] = true
- handleSettingRemoteAddrError(ctx, err, form)
- return
- }
- }
+ remoteAddress, err := util.SanitizeURL(form.MirrorAddress)
+ if err != nil {
+ ctx.Data["Err_MirrorAddress"] = true
+ handleSettingRemoteAddrError(ctx, err, form)
+ return
+ }
+ pullMirror.RemoteAddress = remoteAddress
+
+ form.LFS = form.LFS && setting.LFS.StartServer
- pullMirror.LFS = form.LFS
- pullMirror.LFSEndpoint = form.LFSEndpoint
- if err := repo_model.UpdateMirror(ctx, pullMirror); err != nil {
- ctx.ServerError("UpdateMirror", err)
+ if len(form.LFSEndpoint) > 0 {
+ ep := lfs.DetermineEndpoint("", form.LFSEndpoint)
+ if ep == nil {
+ ctx.Data["Err_LFSEndpoint"] = true
+ ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_lfs_endpoint"), tplSettingsOptions, &form)
return
}
-
- ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
- ctx.Redirect(repo.Link() + "/settings")
-
- case "mirror-sync":
- if !setting.Mirror.Enabled || !repo.IsMirror || repo.IsArchived {
- ctx.NotFound("", nil)
+ err = migrations.IsMigrateURLAllowed(ep.String(), ctx.Doer)
+ if err != nil {
+ ctx.Data["Err_LFSEndpoint"] = true
+ handleSettingRemoteAddrError(ctx, err, form)
return
}
+ }
- mirror_service.AddPullMirrorToQueue(repo.ID)
-
- ctx.Flash.Info(ctx.Tr("repo.settings.pull_mirror_sync_in_progress", repo.OriginalURL))
- ctx.Redirect(repo.Link() + "/settings")
+ pullMirror.LFS = form.LFS
+ pullMirror.LFSEndpoint = form.LFSEndpoint
+ if err := repo_model.UpdateMirror(ctx, pullMirror); err != nil {
+ ctx.ServerError("UpdateMirror", err)
+ return
+ }
- case "push-mirror-sync":
- if !setting.Mirror.Enabled {
- ctx.NotFound("", nil)
- return
- }
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+ ctx.Redirect(repo.Link() + "/settings")
+}
- m, _, _ := repo_model.GetPushMirrorByIDAndRepoID(ctx, form.PushMirrorID, repo.ID)
- if m == nil {
- ctx.NotFound("", nil)
- return
- }
+func handleSettingsPostMirrorSync(ctx *context.Context) {
+ repo := ctx.Repo.Repository
+ if !setting.Mirror.Enabled || !repo.IsMirror || repo.IsArchived {
+ ctx.NotFound(nil)
+ return
+ }
- mirror_service.AddPushMirrorToQueue(m.ID)
+ mirror_service.AddPullMirrorToQueue(repo.ID)
- ctx.Flash.Info(ctx.Tr("repo.settings.push_mirror_sync_in_progress", m.RemoteAddress))
- ctx.Redirect(repo.Link() + "/settings")
+ ctx.Flash.Info(ctx.Tr("repo.settings.pull_mirror_sync_in_progress", repo.OriginalURL))
+ ctx.Redirect(repo.Link() + "/settings")
+}
- case "push-mirror-update":
- if !setting.Mirror.Enabled || repo.IsArchived {
- ctx.NotFound("", nil)
- return
- }
+func handleSettingsPostPushMirrorSync(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
+ repo := ctx.Repo.Repository
- // This section doesn't require repo_name/RepoName to be set in the form, don't show it
- // as an error on the UI for this action
- ctx.Data["Err_RepoName"] = nil
+ if !setting.Mirror.Enabled {
+ ctx.NotFound(nil)
+ return
+ }
- interval, err := time.ParseDuration(form.PushMirrorInterval)
- if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
- ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &forms.RepoSettingForm{})
- return
- }
+ m, _, _ := repo_model.GetPushMirrorByIDAndRepoID(ctx, form.PushMirrorID, repo.ID)
+ if m == nil {
+ ctx.NotFound(nil)
+ return
+ }
- m, _, _ := repo_model.GetPushMirrorByIDAndRepoID(ctx, form.PushMirrorID, repo.ID)
- if m == nil {
- ctx.NotFound("", nil)
- return
- }
+ mirror_service.AddPushMirrorToQueue(m.ID)
- m.Interval = interval
- if err := repo_model.UpdatePushMirrorInterval(ctx, m); err != nil {
- ctx.ServerError("UpdatePushMirrorInterval", err)
- return
- }
- // Background why we are adding it to Queue
- // If we observed its implementation in the context of `push-mirror-sync` where it
- // is evident that pushing to the queue is necessary for updates.
- // So, there are updates within the given interval, it is necessary to update the queue accordingly.
- if !ctx.FormBool("push_mirror_defer_sync") {
- // push_mirror_defer_sync is mainly for testing purpose, we do not really want to sync the push mirror immediately
- mirror_service.AddPushMirrorToQueue(m.ID)
- }
- ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
- ctx.Redirect(repo.Link() + "/settings")
+ ctx.Flash.Info(ctx.Tr("repo.settings.push_mirror_sync_in_progress", m.RemoteAddress))
+ ctx.Redirect(repo.Link() + "/settings")
+}
- case "push-mirror-remove":
- if !setting.Mirror.Enabled || repo.IsArchived {
- ctx.NotFound("", nil)
- return
- }
+func handleSettingsPostPushMirrorUpdate(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
+ repo := ctx.Repo.Repository
- // This section doesn't require repo_name/RepoName to be set in the form, don't show it
- // as an error on the UI for this action
- ctx.Data["Err_RepoName"] = nil
+ if !setting.Mirror.Enabled || repo.IsArchived {
+ ctx.NotFound(nil)
+ return
+ }
- m, _, _ := repo_model.GetPushMirrorByIDAndRepoID(ctx, form.PushMirrorID, repo.ID)
- if m == nil {
- ctx.NotFound("", nil)
- return
- }
+ // This section doesn't require repo_name/RepoName to be set in the form, don't show it
+ // as an error on the UI for this action
+ ctx.Data["Err_RepoName"] = nil
- if err := mirror_service.RemovePushMirrorRemote(ctx, m); err != nil {
- ctx.ServerError("RemovePushMirrorRemote", err)
- return
- }
+ interval, err := time.ParseDuration(form.PushMirrorInterval)
+ if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
+ ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &forms.RepoSettingForm{})
+ return
+ }
- if err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ID: m.ID, RepoID: m.RepoID}); err != nil {
- ctx.ServerError("DeletePushMirrorByID", err)
- return
- }
+ m, _, _ := repo_model.GetPushMirrorByIDAndRepoID(ctx, form.PushMirrorID, repo.ID)
+ if m == nil {
+ ctx.NotFound(nil)
+ return
+ }
- ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
- ctx.Redirect(repo.Link() + "/settings")
+ m.Interval = interval
+ if err := repo_model.UpdatePushMirrorInterval(ctx, m); err != nil {
+ ctx.ServerError("UpdatePushMirrorInterval", err)
+ return
+ }
+ // Background why we are adding it to Queue
+ // If we observed its implementation in the context of `push-mirror-sync` where it
+ // is evident that pushing to the queue is necessary for updates.
+ // So, there are updates within the given interval, it is necessary to update the queue accordingly.
+ if !ctx.FormBool("push_mirror_defer_sync") {
+ // push_mirror_defer_sync is mainly for testing purpose, we do not really want to sync the push mirror immediately
+ mirror_service.AddPushMirrorToQueue(m.ID)
+ }
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+ ctx.Redirect(repo.Link() + "/settings")
+}
- case "push-mirror-add":
- if setting.Mirror.DisableNewPush || repo.IsArchived {
- ctx.NotFound("", nil)
- return
- }
+func handleSettingsPostPushMirrorRemove(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
+ repo := ctx.Repo.Repository
- // This section doesn't require repo_name/RepoName to be set in the form, don't show it
- // as an error on the UI for this action
- ctx.Data["Err_RepoName"] = nil
+ if !setting.Mirror.Enabled || repo.IsArchived {
+ ctx.NotFound(nil)
+ return
+ }
- interval, err := time.ParseDuration(form.PushMirrorInterval)
- if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
- ctx.Data["Err_PushMirrorInterval"] = true
- ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
- return
- }
+ // This section doesn't require repo_name/RepoName to be set in the form, don't show it
+ // as an error on the UI for this action
+ ctx.Data["Err_RepoName"] = nil
- address, err := git.ParseRemoteAddr(form.PushMirrorAddress, form.PushMirrorUsername, form.PushMirrorPassword)
- if err == nil {
- err = migrations.IsMigrateURLAllowed(address, ctx.Doer)
- }
- if err != nil {
- ctx.Data["Err_PushMirrorAddress"] = true
- handleSettingRemoteAddrError(ctx, err, form)
- return
- }
+ m, _, _ := repo_model.GetPushMirrorByIDAndRepoID(ctx, form.PushMirrorID, repo.ID)
+ if m == nil {
+ ctx.NotFound(nil)
+ return
+ }
- remoteSuffix, err := util.CryptoRandomString(10)
- if err != nil {
- ctx.ServerError("RandomString", err)
- return
- }
+ if err := mirror_service.RemovePushMirrorRemote(ctx, m); err != nil {
+ ctx.ServerError("RemovePushMirrorRemote", err)
+ return
+ }
- remoteAddress, err := util.SanitizeURL(form.PushMirrorAddress)
- if err != nil {
- ctx.Data["Err_PushMirrorAddress"] = true
- handleSettingRemoteAddrError(ctx, err, form)
- return
- }
+ if err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ID: m.ID, RepoID: m.RepoID}); err != nil {
+ ctx.ServerError("DeletePushMirrorByID", err)
+ return
+ }
- m := &repo_model.PushMirror{
- RepoID: repo.ID,
- Repo: repo,
- RemoteName: fmt.Sprintf("remote_mirror_%s", remoteSuffix),
- SyncOnCommit: form.PushMirrorSyncOnCommit,
- Interval: interval,
- RemoteAddress: remoteAddress,
- }
- if err := db.Insert(ctx, m); err != nil {
- ctx.ServerError("InsertPushMirror", err)
- return
- }
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+ ctx.Redirect(repo.Link() + "/settings")
+}
- if err := mirror_service.AddPushMirrorRemote(ctx, m, address); err != nil {
- if err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ID: m.ID, RepoID: m.RepoID}); err != nil {
- log.Error("DeletePushMirrors %v", err)
- }
- ctx.ServerError("AddPushMirrorRemote", err)
- return
- }
+func handleSettingsPostPushMirrorAdd(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
+ repo := ctx.Repo.Repository
- ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
- ctx.Redirect(repo.Link() + "/settings")
+ if setting.Mirror.DisableNewPush || repo.IsArchived {
+ ctx.NotFound(nil)
+ return
+ }
- case "advanced":
- var repoChanged bool
- var units []repo_model.RepoUnit
- var deleteUnitTypes []unit_model.Type
+ // This section doesn't require repo_name/RepoName to be set in the form, don't show it
+ // as an error on the UI for this action
+ ctx.Data["Err_RepoName"] = nil
- // This section doesn't require repo_name/RepoName to be set in the form, don't show it
- // as an error on the UI for this action
- ctx.Data["Err_RepoName"] = nil
+ interval, err := time.ParseDuration(form.PushMirrorInterval)
+ if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
+ ctx.Data["Err_PushMirrorInterval"] = true
+ ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
+ return
+ }
- if repo.CloseIssuesViaCommitInAnyBranch != form.EnableCloseIssuesViaCommitInAnyBranch {
- repo.CloseIssuesViaCommitInAnyBranch = form.EnableCloseIssuesViaCommitInAnyBranch
- repoChanged = true
- }
+ address, err := git.ParseRemoteAddr(form.PushMirrorAddress, form.PushMirrorUsername, form.PushMirrorPassword)
+ if err == nil {
+ err = migrations.IsMigrateURLAllowed(address, ctx.Doer)
+ }
+ if err != nil {
+ ctx.Data["Err_PushMirrorAddress"] = true
+ handleSettingRemoteAddrError(ctx, err, form)
+ return
+ }
- if form.EnableCode && !unit_model.TypeCode.UnitGlobalDisabled() {
- units = append(units, repo_model.RepoUnit{
- RepoID: repo.ID,
- Type: unit_model.TypeCode,
- EveryoneAccessMode: parseEveryoneAccessMode(form.DefaultCodeEveryoneAccess, perm.AccessModeNone, perm.AccessModeRead),
- })
- } else if !unit_model.TypeCode.UnitGlobalDisabled() {
- deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeCode)
- }
+ remoteSuffix, err := util.CryptoRandomString(10)
+ if err != nil {
+ ctx.ServerError("RandomString", err)
+ return
+ }
- if form.EnableWiki && form.EnableExternalWiki && !unit_model.TypeExternalWiki.UnitGlobalDisabled() {
- if !validation.IsValidExternalURL(form.ExternalWikiURL) {
- ctx.Flash.Error(ctx.Tr("repo.settings.external_wiki_url_error"))
- ctx.Redirect(repo.Link() + "/settings")
- return
- }
+ remoteAddress, err := util.SanitizeURL(form.PushMirrorAddress)
+ if err != nil {
+ ctx.Data["Err_PushMirrorAddress"] = true
+ handleSettingRemoteAddrError(ctx, err, form)
+ return
+ }
- units = append(units, repo_model.RepoUnit{
- RepoID: repo.ID,
- Type: unit_model.TypeExternalWiki,
- Config: &repo_model.ExternalWikiConfig{
- ExternalWikiURL: form.ExternalWikiURL,
- },
- })
- deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki)
- } else if form.EnableWiki && !form.EnableExternalWiki && !unit_model.TypeWiki.UnitGlobalDisabled() {
- units = append(units, repo_model.RepoUnit{
- RepoID: repo.ID,
- Type: unit_model.TypeWiki,
- Config: new(repo_model.UnitConfig),
- EveryoneAccessMode: parseEveryoneAccessMode(form.DefaultWikiEveryoneAccess, perm.AccessModeNone, perm.AccessModeRead, perm.AccessModeWrite),
- })
- deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki)
- } else {
- if !unit_model.TypeExternalWiki.UnitGlobalDisabled() {
- deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki)
- }
- if !unit_model.TypeWiki.UnitGlobalDisabled() {
- deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki)
- }
- }
+ m := &repo_model.PushMirror{
+ RepoID: repo.ID,
+ Repo: repo,
+ RemoteName: "remote_mirror_" + remoteSuffix,
+ SyncOnCommit: form.PushMirrorSyncOnCommit,
+ Interval: interval,
+ RemoteAddress: remoteAddress,
+ }
+ if err := db.Insert(ctx, m); err != nil {
+ ctx.ServerError("InsertPushMirror", err)
+ return
+ }
- if form.DefaultWikiBranch != "" {
- if err := wiki_service.ChangeDefaultWikiBranch(ctx, repo, form.DefaultWikiBranch); err != nil {
- log.Error("ChangeDefaultWikiBranch failed, err: %v", err)
- ctx.Flash.Warning(ctx.Tr("repo.settings.failed_to_change_default_wiki_branch"))
- }
+ if err := mirror_service.AddPushMirrorRemote(ctx, m, address); err != nil {
+ if err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ID: m.ID, RepoID: m.RepoID}); err != nil {
+ log.Error("DeletePushMirrors %v", err)
}
+ ctx.ServerError("AddPushMirrorRemote", err)
+ return
+ }
- if form.EnableIssues && form.EnableExternalTracker && !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
- if !validation.IsValidExternalURL(form.ExternalTrackerURL) {
- ctx.Flash.Error(ctx.Tr("repo.settings.external_tracker_url_error"))
- ctx.Redirect(repo.Link() + "/settings")
- return
- }
- if len(form.TrackerURLFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(form.TrackerURLFormat) {
- ctx.Flash.Error(ctx.Tr("repo.settings.tracker_url_format_error"))
- ctx.Redirect(repo.Link() + "/settings")
- return
- }
- units = append(units, repo_model.RepoUnit{
- RepoID: repo.ID,
- Type: unit_model.TypeExternalTracker,
- Config: &repo_model.ExternalTrackerConfig{
- ExternalTrackerURL: form.ExternalTrackerURL,
- ExternalTrackerFormat: form.TrackerURLFormat,
- ExternalTrackerStyle: form.TrackerIssueStyle,
- ExternalTrackerRegexpPattern: form.ExternalTrackerRegexpPattern,
- },
- })
- deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)
- } else if form.EnableIssues && !form.EnableExternalTracker && !unit_model.TypeIssues.UnitGlobalDisabled() {
- units = append(units, repo_model.RepoUnit{
- RepoID: repo.ID,
- Type: unit_model.TypeIssues,
- Config: &repo_model.IssuesConfig{
- EnableTimetracker: form.EnableTimetracker,
- AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime,
- EnableDependencies: form.EnableIssueDependencies,
- },
- EveryoneAccessMode: parseEveryoneAccessMode(form.DefaultIssuesEveryoneAccess, perm.AccessModeNone, perm.AccessModeRead),
- })
- deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
- } else {
- if !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
- deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
- }
- if !unit_model.TypeIssues.UnitGlobalDisabled() {
- deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)
- }
- }
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+ ctx.Redirect(repo.Link() + "/settings")
+}
- if form.EnableProjects && !unit_model.TypeProjects.UnitGlobalDisabled() {
- units = append(units, repo_model.RepoUnit{
- RepoID: repo.ID,
- Type: unit_model.TypeProjects,
- Config: &repo_model.ProjectsConfig{
- ProjectsMode: repo_model.ProjectsMode(form.ProjectsMode),
- },
- })
- } else if !unit_model.TypeProjects.UnitGlobalDisabled() {
- deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects)
+func newRepoUnit(repo *repo_model.Repository, unitType unit_model.Type, config convert.Conversion) repo_model.RepoUnit {
+ repoUnit := repo_model.RepoUnit{RepoID: repo.ID, Type: unitType, Config: config}
+ for _, u := range repo.Units {
+ if u.Type == unitType {
+ repoUnit.EveryoneAccessMode = u.EveryoneAccessMode
+ repoUnit.AnonymousAccessMode = u.AnonymousAccessMode
}
+ }
+ return repoUnit
+}
- if form.EnableReleases && !unit_model.TypeReleases.UnitGlobalDisabled() {
- units = append(units, repo_model.RepoUnit{
- RepoID: repo.ID,
- Type: unit_model.TypeReleases,
- })
- } else if !unit_model.TypeReleases.UnitGlobalDisabled() {
- deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeReleases)
- }
+func handleSettingsPostAdvanced(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
+ repo := ctx.Repo.Repository
+ var repoChanged bool
+ var units []repo_model.RepoUnit
+ var deleteUnitTypes []unit_model.Type
- if form.EnablePackages && !unit_model.TypePackages.UnitGlobalDisabled() {
- units = append(units, repo_model.RepoUnit{
- RepoID: repo.ID,
- Type: unit_model.TypePackages,
- })
- } else if !unit_model.TypePackages.UnitGlobalDisabled() {
- deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePackages)
- }
+ // This section doesn't require repo_name/RepoName to be set in the form, don't show it
+ // as an error on the UI for this action
+ ctx.Data["Err_RepoName"] = nil
- if form.EnableActions && !unit_model.TypeActions.UnitGlobalDisabled() {
- units = append(units, repo_model.RepoUnit{
- RepoID: repo.ID,
- Type: unit_model.TypeActions,
- })
- } else if !unit_model.TypeActions.UnitGlobalDisabled() {
- deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeActions)
- }
+ if repo.CloseIssuesViaCommitInAnyBranch != form.EnableCloseIssuesViaCommitInAnyBranch {
+ repo.CloseIssuesViaCommitInAnyBranch = form.EnableCloseIssuesViaCommitInAnyBranch
+ repoChanged = true
+ }
- if form.EnablePulls && !unit_model.TypePullRequests.UnitGlobalDisabled() {
- units = append(units, repo_model.RepoUnit{
- RepoID: repo.ID,
- Type: unit_model.TypePullRequests,
- Config: &repo_model.PullRequestsConfig{
- IgnoreWhitespaceConflicts: form.PullsIgnoreWhitespace,
- AllowMerge: form.PullsAllowMerge,
- AllowRebase: form.PullsAllowRebase,
- AllowRebaseMerge: form.PullsAllowRebaseMerge,
- AllowSquash: form.PullsAllowSquash,
- AllowFastForwardOnly: form.PullsAllowFastForwardOnly,
- AllowManualMerge: form.PullsAllowManualMerge,
- AutodetectManualMerge: form.EnableAutodetectManualMerge,
- AllowRebaseUpdate: form.PullsAllowRebaseUpdate,
- DefaultDeleteBranchAfterMerge: form.DefaultDeleteBranchAfterMerge,
- DefaultMergeStyle: repo_model.MergeStyle(form.PullsDefaultMergeStyle),
- DefaultAllowMaintainerEdit: form.DefaultAllowMaintainerEdit,
- },
- })
- } else if !unit_model.TypePullRequests.UnitGlobalDisabled() {
- deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePullRequests)
- }
+ if form.EnableCode && !unit_model.TypeCode.UnitGlobalDisabled() {
+ units = append(units, newRepoUnit(repo, unit_model.TypeCode, nil))
+ } else if !unit_model.TypeCode.UnitGlobalDisabled() {
+ deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeCode)
+ }
- if len(units) == 0 {
- ctx.Flash.Error(ctx.Tr("repo.settings.update_settings_no_unit"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+ if form.EnableWiki && form.EnableExternalWiki && !unit_model.TypeExternalWiki.UnitGlobalDisabled() {
+ if !validation.IsValidExternalURL(form.ExternalWikiURL) {
+ ctx.Flash.Error(ctx.Tr("repo.settings.external_wiki_url_error"))
+ ctx.Redirect(repo.Link() + "/settings")
return
}
- if err := repo_service.UpdateRepositoryUnits(ctx, repo, units, deleteUnitTypes); err != nil {
- ctx.ServerError("UpdateRepositoryUnits", err)
- return
+ units = append(units, newRepoUnit(repo, unit_model.TypeExternalWiki, &repo_model.ExternalWikiConfig{
+ ExternalWikiURL: form.ExternalWikiURL,
+ }))
+ deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki)
+ } else if form.EnableWiki && !form.EnableExternalWiki && !unit_model.TypeWiki.UnitGlobalDisabled() {
+ units = append(units, newRepoUnit(repo, unit_model.TypeWiki, new(repo_model.UnitConfig)))
+ deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki)
+ } else {
+ if !unit_model.TypeExternalWiki.UnitGlobalDisabled() {
+ deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalWiki)
}
- if repoChanged {
- if err := repo_service.UpdateRepository(ctx, repo, false); err != nil {
- ctx.ServerError("UpdateRepository", err)
- return
- }
+ if !unit_model.TypeWiki.UnitGlobalDisabled() {
+ deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeWiki)
}
- log.Trace("Repository advanced settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
-
- ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+ }
- case "signing":
- changed := false
- trustModel := repo_model.ToTrustModel(form.TrustModel)
- if trustModel != repo.TrustModel {
- repo.TrustModel = trustModel
- changed = true
+ if form.DefaultWikiBranch != "" {
+ if err := wiki_service.ChangeDefaultWikiBranch(ctx, repo, form.DefaultWikiBranch); err != nil {
+ log.Error("ChangeDefaultWikiBranch failed, err: %v", err)
+ ctx.Flash.Warning(ctx.Tr("repo.settings.failed_to_change_default_wiki_branch"))
}
+ }
- if changed {
- if err := repo_service.UpdateRepository(ctx, repo, false); err != nil {
- ctx.ServerError("UpdateRepository", err)
- return
- }
+ if form.EnableIssues && form.EnableExternalTracker && !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
+ if !validation.IsValidExternalURL(form.ExternalTrackerURL) {
+ ctx.Flash.Error(ctx.Tr("repo.settings.external_tracker_url_error"))
+ ctx.Redirect(repo.Link() + "/settings")
+ return
}
- log.Trace("Repository signing settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
-
- ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
-
- case "admin":
- if !ctx.Doer.IsAdmin {
- ctx.Error(http.StatusForbidden)
+ if len(form.TrackerURLFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(form.TrackerURLFormat) {
+ ctx.Flash.Error(ctx.Tr("repo.settings.tracker_url_format_error"))
+ ctx.Redirect(repo.Link() + "/settings")
return
}
-
- if repo.IsFsckEnabled != form.EnableHealthCheck {
- repo.IsFsckEnabled = form.EnableHealthCheck
+ units = append(units, newRepoUnit(repo, unit_model.TypeExternalTracker, &repo_model.ExternalTrackerConfig{
+ ExternalTrackerURL: form.ExternalTrackerURL,
+ ExternalTrackerFormat: form.TrackerURLFormat,
+ ExternalTrackerStyle: form.TrackerIssueStyle,
+ ExternalTrackerRegexpPattern: form.ExternalTrackerRegexpPattern,
+ }))
+ deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)
+ } else if form.EnableIssues && !form.EnableExternalTracker && !unit_model.TypeIssues.UnitGlobalDisabled() {
+ units = append(units, newRepoUnit(repo, unit_model.TypeIssues, &repo_model.IssuesConfig{
+ EnableTimetracker: form.EnableTimetracker,
+ AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime,
+ EnableDependencies: form.EnableIssueDependencies,
+ }))
+ deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
+ } else {
+ if !unit_model.TypeExternalTracker.UnitGlobalDisabled() {
+ deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker)
}
-
- if err := repo_service.UpdateRepository(ctx, repo, false); err != nil {
- ctx.ServerError("UpdateRepository", err)
- return
+ if !unit_model.TypeIssues.UnitGlobalDisabled() {
+ deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeIssues)
}
+ }
- log.Trace("Repository admin settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+ if form.EnableProjects && !unit_model.TypeProjects.UnitGlobalDisabled() {
+ units = append(units, newRepoUnit(repo, unit_model.TypeProjects, &repo_model.ProjectsConfig{
+ ProjectsMode: repo_model.ProjectsMode(form.ProjectsMode),
+ }))
+ } else if !unit_model.TypeProjects.UnitGlobalDisabled() {
+ deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeProjects)
+ }
- ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+ if form.EnableReleases && !unit_model.TypeReleases.UnitGlobalDisabled() {
+ units = append(units, newRepoUnit(repo, unit_model.TypeReleases, nil))
+ } else if !unit_model.TypeReleases.UnitGlobalDisabled() {
+ deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeReleases)
+ }
- case "admin_index":
- if !ctx.Doer.IsAdmin {
- ctx.Error(http.StatusForbidden)
- return
- }
+ if form.EnablePackages && !unit_model.TypePackages.UnitGlobalDisabled() {
+ units = append(units, newRepoUnit(repo, unit_model.TypePackages, nil))
+ } else if !unit_model.TypePackages.UnitGlobalDisabled() {
+ deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePackages)
+ }
- switch form.RequestReindexType {
- case "stats":
- if err := stats.UpdateRepoIndexer(ctx.Repo.Repository); err != nil {
- ctx.ServerError("UpdateStatsRepondexer", err)
- return
- }
- case "code":
- if !setting.Indexer.RepoIndexerEnabled {
- ctx.Error(http.StatusForbidden)
- return
- }
- code.UpdateRepoIndexer(ctx.Repo.Repository)
- default:
- ctx.NotFound("", nil)
- return
- }
+ if form.EnableActions && !unit_model.TypeActions.UnitGlobalDisabled() {
+ units = append(units, newRepoUnit(repo, unit_model.TypeActions, nil))
+ } else if !unit_model.TypeActions.UnitGlobalDisabled() {
+ deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeActions)
+ }
- log.Trace("Repository reindex for %s requested: %s/%s", form.RequestReindexType, ctx.Repo.Owner.Name, repo.Name)
+ if form.EnablePulls && !unit_model.TypePullRequests.UnitGlobalDisabled() {
+ units = append(units, newRepoUnit(repo, unit_model.TypePullRequests, &repo_model.PullRequestsConfig{
+ IgnoreWhitespaceConflicts: form.PullsIgnoreWhitespace,
+ AllowMerge: form.PullsAllowMerge,
+ AllowRebase: form.PullsAllowRebase,
+ AllowRebaseMerge: form.PullsAllowRebaseMerge,
+ AllowSquash: form.PullsAllowSquash,
+ AllowFastForwardOnly: form.PullsAllowFastForwardOnly,
+ AllowManualMerge: form.PullsAllowManualMerge,
+ AutodetectManualMerge: form.EnableAutodetectManualMerge,
+ AllowRebaseUpdate: form.PullsAllowRebaseUpdate,
+ DefaultDeleteBranchAfterMerge: form.DefaultDeleteBranchAfterMerge,
+ DefaultMergeStyle: repo_model.MergeStyle(form.PullsDefaultMergeStyle),
+ DefaultAllowMaintainerEdit: form.DefaultAllowMaintainerEdit,
+ }))
+ } else if !unit_model.TypePullRequests.UnitGlobalDisabled() {
+ deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePullRequests)
+ }
- ctx.Flash.Success(ctx.Tr("repo.settings.reindex_requested"))
+ if len(units) == 0 {
+ ctx.Flash.Error(ctx.Tr("repo.settings.update_settings_no_unit"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+ return
+ }
- case "convert":
- if !ctx.Repo.IsOwner() {
- ctx.Error(http.StatusNotFound)
- return
- }
- if repo.Name != form.RepoName {
- ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
+ if err := repo_service.UpdateRepositoryUnits(ctx, repo, units, deleteUnitTypes); err != nil {
+ ctx.ServerError("UpdateRepositoryUnits", err)
+ return
+ }
+ if repoChanged {
+ if err := repo_service.UpdateRepository(ctx, repo, false); err != nil {
+ ctx.ServerError("UpdateRepository", err)
return
}
+ }
+ log.Trace("Repository advanced settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
- if !repo.IsMirror {
- ctx.Error(http.StatusNotFound)
- return
- }
- repo.IsMirror = false
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+}
- if _, err := repo_service.CleanUpMigrateInfo(ctx, repo); err != nil {
- ctx.ServerError("CleanUpMigrateInfo", err)
- return
- } else if err = repo_model.DeleteMirrorByRepoID(ctx, ctx.Repo.Repository.ID); err != nil {
- ctx.ServerError("DeleteMirrorByRepoID", err)
+func handleSettingsPostSigning(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
+ repo := ctx.Repo.Repository
+ trustModel := repo_model.ToTrustModel(form.TrustModel)
+ if trustModel != repo.TrustModel {
+ repo.TrustModel = trustModel
+ if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "trust_model"); err != nil {
+ ctx.ServerError("UpdateRepositoryColsNoAutoTime", err)
return
}
- log.Trace("Repository converted from mirror to regular: %s", repo.FullName())
- ctx.Flash.Success(ctx.Tr("repo.settings.convert_succeed"))
- ctx.Redirect(repo.Link())
+ log.Trace("Repository signing settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+ }
- case "convert_fork":
- if !ctx.Repo.IsOwner() {
- ctx.Error(http.StatusNotFound)
- return
- }
- if err := repo.LoadOwner(ctx); err != nil {
- ctx.ServerError("Convert Fork", err)
- return
- }
- if repo.Name != form.RepoName {
- ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
- return
- }
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+}
- if !repo.IsFork {
- ctx.Error(http.StatusNotFound)
- return
- }
+func handleSettingsPostAdmin(ctx *context.Context) {
+ if !ctx.Doer.IsAdmin {
+ ctx.HTTPError(http.StatusForbidden)
+ return
+ }
- if !ctx.Repo.Owner.CanCreateRepo() {
- maxCreationLimit := ctx.Repo.Owner.MaxCreationLimit()
- msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
- ctx.Flash.Error(msg)
- ctx.Redirect(repo.Link() + "/settings")
+ repo := ctx.Repo.Repository
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
+ if repo.IsFsckEnabled != form.EnableHealthCheck {
+ repo.IsFsckEnabled = form.EnableHealthCheck
+ if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_fsck_enabled"); err != nil {
+ ctx.ServerError("UpdateRepositoryColsNoAutoTime", err)
return
}
+ log.Trace("Repository admin settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+ }
- if err := repo_service.ConvertForkToNormalRepository(ctx, repo); err != nil {
- log.Error("Unable to convert repository %-v from fork. Error: %v", repo, err)
- ctx.ServerError("Convert Fork", err)
- return
- }
+ ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+}
- log.Trace("Repository converted from fork to regular: %s", repo.FullName())
- ctx.Flash.Success(ctx.Tr("repo.settings.convert_fork_succeed"))
- ctx.Redirect(repo.Link())
+func handleSettingsPostAdminIndex(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
+ repo := ctx.Repo.Repository
+ if !ctx.Doer.IsAdmin {
+ ctx.HTTPError(http.StatusForbidden)
+ return
+ }
- case "transfer":
- if !ctx.Repo.IsOwner() {
- ctx.Error(http.StatusNotFound)
+ switch form.RequestReindexType {
+ case "stats":
+ if err := stats.UpdateRepoIndexer(ctx.Repo.Repository); err != nil {
+ ctx.ServerError("UpdateStatsRepondexer", err)
return
}
- if repo.Name != form.RepoName {
- ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
+ case "code":
+ if !setting.Indexer.RepoIndexerEnabled {
+ ctx.HTTPError(http.StatusForbidden)
return
}
+ code.UpdateRepoIndexer(ctx.Repo.Repository)
+ default:
+ ctx.NotFound(nil)
+ return
+ }
- newOwner, err := user_model.GetUserByName(ctx, ctx.FormString("new_owner_name"))
- if err != nil {
- if user_model.IsErrUserNotExist(err) {
- ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil)
- return
- }
- ctx.ServerError("IsUserExist", err)
- return
- }
+ log.Trace("Repository reindex for %s requested: %s/%s", form.RequestReindexType, ctx.Repo.Owner.Name, repo.Name)
- if newOwner.Type == user_model.UserTypeOrganization {
- if !ctx.Doer.IsAdmin && newOwner.Visibility == structs.VisibleTypePrivate && !organization.OrgFromUser(newOwner).HasMemberWithUserID(ctx, ctx.Doer.ID) {
- // The user shouldn't know about this organization
- ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil)
- return
- }
- }
+ ctx.Flash.Success(ctx.Tr("repo.settings.reindex_requested"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+}
- // Close the GitRepo if open
- if ctx.Repo.GitRepo != nil {
- ctx.Repo.GitRepo.Close()
- ctx.Repo.GitRepo = nil
- }
+func handleSettingsPostConvert(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
+ repo := ctx.Repo.Repository
+ if !ctx.Repo.IsOwner() {
+ ctx.HTTPError(http.StatusNotFound)
+ return
+ }
+ if repo.Name != form.RepoName {
+ ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
+ return
+ }
- oldFullname := repo.FullName()
- if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, repo, nil); err != nil {
- if repo_model.IsErrRepoAlreadyExist(err) {
- ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil)
- } else if repo_model.IsErrRepoTransferInProgress(err) {
- ctx.RenderWithErr(ctx.Tr("repo.settings.transfer_in_progress"), tplSettingsOptions, nil)
- } else if errors.Is(err, user_model.ErrBlockedUser) {
- ctx.RenderWithErr(ctx.Tr("repo.settings.transfer.blocked_user"), tplSettingsOptions, nil)
- } else {
- ctx.ServerError("TransferOwnership", err)
- }
+ if !repo.IsMirror {
+ ctx.HTTPError(http.StatusNotFound)
+ return
+ }
+ repo.IsMirror = false
- return
- }
+ if _, err := repo_service.CleanUpMigrateInfo(ctx, repo); err != nil {
+ ctx.ServerError("CleanUpMigrateInfo", err)
+ return
+ } else if err = repo_model.DeleteMirrorByRepoID(ctx, ctx.Repo.Repository.ID); err != nil {
+ ctx.ServerError("DeleteMirrorByRepoID", err)
+ return
+ }
+ log.Trace("Repository converted from mirror to regular: %s", repo.FullName())
+ ctx.Flash.Success(ctx.Tr("repo.settings.convert_succeed"))
+ ctx.Redirect(repo.Link())
+}
- if ctx.Repo.Repository.Status == repo_model.RepositoryPendingTransfer {
- log.Trace("Repository transfer process was started: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newOwner)
- ctx.Flash.Success(ctx.Tr("repo.settings.transfer_started", newOwner.DisplayName()))
- } else {
- log.Trace("Repository transferred: %s -> %s", oldFullname, ctx.Repo.Repository.FullName())
- ctx.Flash.Success(ctx.Tr("repo.settings.transfer_succeed"))
- }
+func handleSettingsPostConvertFork(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
+ repo := ctx.Repo.Repository
+ if !ctx.Repo.IsOwner() {
+ ctx.HTTPError(http.StatusNotFound)
+ return
+ }
+ if err := repo.LoadOwner(ctx); err != nil {
+ ctx.ServerError("Convert Fork", err)
+ return
+ }
+ if repo.Name != form.RepoName {
+ ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
+ return
+ }
+
+ if !repo.IsFork {
+ ctx.HTTPError(http.StatusNotFound)
+ return
+ }
+
+ if !ctx.Doer.CanForkRepoIn(ctx.Repo.Owner) {
+ maxCreationLimit := ctx.Repo.Owner.MaxCreationLimit()
+ msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
+ ctx.Flash.Error(msg)
ctx.Redirect(repo.Link() + "/settings")
+ return
+ }
- case "cancel_transfer":
- if !ctx.Repo.IsOwner() {
- ctx.Error(http.StatusNotFound)
- return
- }
+ if err := repo_service.ConvertForkToNormalRepository(ctx, repo); err != nil {
+ log.Error("Unable to convert repository %-v from fork. Error: %v", repo, err)
+ ctx.ServerError("Convert Fork", err)
+ return
+ }
- repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository)
- if err != nil {
- if repo_model.IsErrNoPendingTransfer(err) {
- ctx.Flash.Error("repo.settings.transfer_abort_invalid")
- ctx.Redirect(repo.Link() + "/settings")
- } else {
- ctx.ServerError("GetPendingRepositoryTransfer", err)
- }
+ log.Trace("Repository converted from fork to regular: %s", repo.FullName())
+ ctx.Flash.Success(ctx.Tr("repo.settings.convert_fork_succeed"))
+ ctx.Redirect(repo.Link())
+}
- return
- }
+func handleSettingsPostTransfer(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
+ repo := ctx.Repo.Repository
+ if !ctx.Repo.IsOwner() {
+ ctx.HTTPError(http.StatusNotFound)
+ return
+ }
+ if repo.Name != form.RepoName {
+ ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
+ return
+ }
- if err := repoTransfer.LoadAttributes(ctx); err != nil {
- ctx.ServerError("LoadRecipient", err)
+ newOwner, err := user_model.GetUserByName(ctx, ctx.FormString("new_owner_name"))
+ if err != nil {
+ if user_model.IsErrUserNotExist(err) {
+ ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil)
return
}
+ ctx.ServerError("IsUserExist", err)
+ return
+ }
- if err := repo_service.CancelRepositoryTransfer(ctx, ctx.Repo.Repository); err != nil {
- ctx.ServerError("CancelRepositoryTransfer", err)
+ if newOwner.Type == user_model.UserTypeOrganization {
+ if !ctx.Doer.IsAdmin && newOwner.Visibility == structs.VisibleTypePrivate && !organization.OrgFromUser(newOwner).HasMemberWithUserID(ctx, ctx.Doer.ID) {
+ // The user shouldn't know about this organization
+ ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil)
return
}
+ }
- log.Trace("Repository transfer process was cancelled: %s/%s ", ctx.Repo.Owner.Name, repo.Name)
- ctx.Flash.Success(ctx.Tr("repo.settings.transfer_abort_success", repoTransfer.Recipient.Name))
- ctx.Redirect(repo.Link() + "/settings")
+ // Close the GitRepo if open
+ if ctx.Repo.GitRepo != nil {
+ ctx.Repo.GitRepo.Close()
+ ctx.Repo.GitRepo = nil
+ }
- case "delete":
- if !ctx.Repo.IsOwner() {
- ctx.Error(http.StatusNotFound)
- return
- }
- if repo.Name != form.RepoName {
- ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
- return
+ oldFullname := repo.FullName()
+ if err := repo_service.StartRepositoryTransfer(ctx, ctx.Doer, newOwner, repo, nil); err != nil {
+ if repo_model.IsErrRepoAlreadyExist(err) {
+ ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil)
+ } else if repo_model.IsErrRepoTransferInProgress(err) {
+ ctx.RenderWithErr(ctx.Tr("repo.settings.transfer_in_progress"), tplSettingsOptions, nil)
+ } else if repo_service.IsRepositoryLimitReached(err) {
+ limit := err.(repo_service.LimitReachedError).Limit
+ ctx.RenderWithErr(ctx.TrN(limit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", limit), tplSettingsOptions, nil)
+ } else if errors.Is(err, user_model.ErrBlockedUser) {
+ ctx.RenderWithErr(ctx.Tr("repo.settings.transfer.blocked_user"), tplSettingsOptions, nil)
+ } else {
+ ctx.ServerError("TransferOwnership", err)
}
- // Close the gitrepository before doing this.
- if ctx.Repo.GitRepo != nil {
- ctx.Repo.GitRepo.Close()
- }
+ return
+ }
- if err := repo_service.DeleteRepository(ctx, ctx.Doer, ctx.Repo.Repository, true); err != nil {
- ctx.ServerError("DeleteRepository", err)
- return
- }
- log.Trace("Repository deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+ if ctx.Repo.Repository.Status == repo_model.RepositoryPendingTransfer {
+ log.Trace("Repository transfer process was started: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newOwner)
+ ctx.Flash.Success(ctx.Tr("repo.settings.transfer_started", newOwner.DisplayName()))
+ } else {
+ log.Trace("Repository transferred: %s -> %s", oldFullname, ctx.Repo.Repository.FullName())
+ ctx.Flash.Success(ctx.Tr("repo.settings.transfer_succeed"))
+ }
+ ctx.Redirect(repo.Link() + "/settings")
+}
- ctx.Flash.Success(ctx.Tr("repo.settings.deletion_success"))
- ctx.Redirect(ctx.Repo.Owner.DashboardLink())
+func handleSettingsPostCancelTransfer(ctx *context.Context) {
+ repo := ctx.Repo.Repository
+ if !ctx.Repo.IsOwner() {
+ ctx.HTTPError(http.StatusNotFound)
+ return
+ }
- case "delete-wiki":
- if !ctx.Repo.IsOwner() {
- ctx.Error(http.StatusNotFound)
- return
- }
- if repo.Name != form.RepoName {
- ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
- return
+ repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository)
+ if err != nil {
+ if repo_model.IsErrNoPendingTransfer(err) {
+ ctx.Flash.Error("repo.settings.transfer_abort_invalid")
+ ctx.Redirect(repo.Link() + "/settings")
+ } else {
+ ctx.ServerError("GetPendingRepositoryTransfer", err)
}
+ return
+ }
- err := wiki_service.DeleteWiki(ctx, repo)
- if err != nil {
- log.Error("Delete Wiki: %v", err.Error())
- }
- log.Trace("Repository wiki deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+ if err := repo_service.CancelRepositoryTransfer(ctx, repoTransfer, ctx.Doer); err != nil {
+ ctx.ServerError("CancelRepositoryTransfer", err)
+ return
+ }
- ctx.Flash.Success(ctx.Tr("repo.settings.wiki_deletion_success"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+ log.Trace("Repository transfer process was cancelled: %s/%s ", ctx.Repo.Owner.Name, repo.Name)
+ ctx.Flash.Success(ctx.Tr("repo.settings.transfer_abort_success", repoTransfer.Recipient.Name))
+ ctx.Redirect(repo.Link() + "/settings")
+}
- case "archive":
- if !ctx.Repo.IsOwner() {
- ctx.Error(http.StatusForbidden)
- return
- }
+func handleSettingsPostDelete(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
+ repo := ctx.Repo.Repository
+ if !ctx.Repo.IsOwner() {
+ ctx.HTTPError(http.StatusNotFound)
+ return
+ }
+ if repo.Name != form.RepoName {
+ ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
+ return
+ }
- if repo.IsMirror {
- ctx.Flash.Error(ctx.Tr("repo.settings.archive.error_ismirror"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
- return
- }
+ // Close the gitrepository before doing this.
+ if ctx.Repo.GitRepo != nil {
+ ctx.Repo.GitRepo.Close()
+ }
- if err := repo_model.SetArchiveRepoState(ctx, repo, true); err != nil {
- log.Error("Tried to archive a repo: %s", err)
- ctx.Flash.Error(ctx.Tr("repo.settings.archive.error"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
- return
- }
+ if err := repo_service.DeleteRepository(ctx, ctx.Doer, ctx.Repo.Repository, true); err != nil {
+ ctx.ServerError("DeleteRepository", err)
+ return
+ }
+ log.Trace("Repository deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name)
- if err := actions_model.CleanRepoScheduleTasks(ctx, repo); err != nil {
- log.Error("CleanRepoScheduleTasks for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
- }
+ ctx.Flash.Success(ctx.Tr("repo.settings.deletion_success"))
+ ctx.Redirect(ctx.Repo.Owner.DashboardLink())
+}
+
+func handleSettingsPostDeleteWiki(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
+ repo := ctx.Repo.Repository
+ if !ctx.Repo.IsOwner() {
+ ctx.HTTPError(http.StatusNotFound)
+ return
+ }
+ if repo.Name != form.RepoName {
+ ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
+ return
+ }
+
+ err := wiki_service.DeleteWiki(ctx, repo)
+ if err != nil {
+ log.Error("Delete Wiki: %v", err.Error())
+ }
+ log.Trace("Repository wiki deleted: %s/%s", ctx.Repo.Owner.Name, repo.Name)
- // update issue indexer
- issue_indexer.UpdateRepoIndexer(ctx, repo.ID)
+ ctx.Flash.Success(ctx.Tr("repo.settings.wiki_deletion_success"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+}
- ctx.Flash.Success(ctx.Tr("repo.settings.archive.success"))
+func handleSettingsPostArchive(ctx *context.Context) {
+ repo := ctx.Repo.Repository
+ if !ctx.Repo.IsOwner() {
+ ctx.HTTPError(http.StatusForbidden)
+ return
+ }
- log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+ if repo.IsMirror {
+ ctx.Flash.Error(ctx.Tr("repo.settings.archive.error_ismirror"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+ return
+ }
- case "unarchive":
- if !ctx.Repo.IsOwner() {
- ctx.Error(http.StatusForbidden)
- return
- }
+ if err := repo_model.SetArchiveRepoState(ctx, repo, true); err != nil {
+ log.Error("Tried to archive a repo: %s", err)
+ ctx.Flash.Error(ctx.Tr("repo.settings.archive.error"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+ return
+ }
- if err := repo_model.SetArchiveRepoState(ctx, repo, false); err != nil {
- log.Error("Tried to unarchive a repo: %s", err)
- ctx.Flash.Error(ctx.Tr("repo.settings.unarchive.error"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
- return
- }
+ if err := actions_service.CleanRepoScheduleTasks(ctx, repo); err != nil {
+ log.Error("CleanRepoScheduleTasks for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
+ }
- if ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypeActions) {
- if err := actions_service.DetectAndHandleSchedules(ctx, repo); err != nil {
- log.Error("DetectAndHandleSchedules for un-archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
- }
- }
+ // update issue indexer
+ issue_indexer.UpdateRepoIndexer(ctx, repo.ID)
- // update issue indexer
- issue_indexer.UpdateRepoIndexer(ctx, repo.ID)
+ ctx.Flash.Success(ctx.Tr("repo.settings.archive.success"))
- ctx.Flash.Success(ctx.Tr("repo.settings.unarchive.success"))
+ log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+}
+
+func handleSettingsPostUnarchive(ctx *context.Context) {
+ repo := ctx.Repo.Repository
+ if !ctx.Repo.IsOwner() {
+ ctx.HTTPError(http.StatusForbidden)
+ return
+ }
- log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+ if err := repo_model.SetArchiveRepoState(ctx, repo, false); err != nil {
+ log.Error("Tried to unarchive a repo: %s", err)
+ ctx.Flash.Error(ctx.Tr("repo.settings.unarchive.error"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+ return
+ }
- case "visibility":
- if repo.IsFork {
- ctx.Flash.Error(ctx.Tr("repo.settings.visibility.fork_error"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
- return
+ if ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypeActions) {
+ if err := actions_service.DetectAndHandleSchedules(ctx, repo); err != nil {
+ log.Error("DetectAndHandleSchedules for un-archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err)
}
+ }
- var err error
+ // update issue indexer
+ issue_indexer.UpdateRepoIndexer(ctx, repo.ID)
- // when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public
- if setting.Repository.ForcePrivate && repo.IsPrivate && !ctx.Doer.IsAdmin {
- ctx.RenderWithErr(ctx.Tr("form.repository_force_private"), tplSettingsOptions, form)
- return
- }
+ ctx.Flash.Success(ctx.Tr("repo.settings.unarchive.success"))
- if repo.IsPrivate {
- err = repo_service.MakeRepoPublic(ctx, repo)
- } else {
- err = repo_service.MakeRepoPrivate(ctx, repo)
- }
+ log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+}
- if err != nil {
- log.Error("Tried to change the visibility of the repo: %s", err)
- ctx.Flash.Error(ctx.Tr("repo.settings.visibility.error"))
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
- return
- }
+func handleSettingsPostVisibility(ctx *context.Context) {
+ form := web.GetForm(ctx).(*forms.RepoSettingForm)
+ repo := ctx.Repo.Repository
+ if repo.IsFork {
+ ctx.Flash.Error(ctx.Tr("repo.settings.visibility.fork_error"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+ return
+ }
- ctx.Flash.Success(ctx.Tr("repo.settings.visibility.success"))
+ var err error
- log.Trace("Repository visibility changed: %s/%s", ctx.Repo.Owner.Name, repo.Name)
- ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+ // when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public
+ if setting.Repository.ForcePrivate && repo.IsPrivate && !ctx.Doer.IsAdmin {
+ ctx.RenderWithErr(ctx.Tr("form.repository_force_private"), tplSettingsOptions, form)
+ return
+ }
- default:
- ctx.NotFound("", nil)
+ if repo.IsPrivate {
+ err = repo_service.MakeRepoPublic(ctx, repo)
+ } else {
+ err = repo_service.MakeRepoPrivate(ctx, repo)
+ }
+
+ if err != nil {
+ log.Error("Tried to change the visibility of the repo: %s", err)
+ ctx.Flash.Error(ctx.Tr("repo.settings.visibility.error"))
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
+ return
}
+
+ ctx.Flash.Success(ctx.Tr("repo.settings.visibility.success"))
+
+ log.Trace("Repository visibility changed: %s/%s", ctx.Repo.Owner.Name, repo.Name)
+ ctx.Redirect(ctx.Repo.RepoLink + "/settings")
}
func handleSettingRemoteAddrError(ctx *context.Context, err error, form *forms.RepoSettingForm) {
diff --git a/routers/web/repo/setting/settings_test.go b/routers/web/repo/setting/settings_test.go
index 09586cc68d..15ebea888c 100644
--- a/routers/web/repo/setting/settings_test.go
+++ b/routers/web/repo/setting/settings_test.go
@@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/contexttest"
@@ -24,23 +25,8 @@ import (
"github.com/stretchr/testify/assert"
)
-func createSSHAuthorizedKeysTmpPath(t *testing.T) func() {
- tmpDir := t.TempDir()
-
- oldPath := setting.SSH.RootPath
- setting.SSH.RootPath = tmpDir
-
- return func() {
- setting.SSH.RootPath = oldPath
- }
-}
-
func TestAddReadOnlyDeployKey(t *testing.T) {
- if deferable := createSSHAuthorizedKeysTmpPath(t); deferable != nil {
- defer deferable()
- } else {
- return
- }
+ defer test.MockVariableValue(&setting.SSH.RootPath, t.TempDir())()
unittest.PrepareTestEnv(t)
ctx, _ := contexttest.MockContext(t, "user2/repo1/settings/keys")
@@ -54,7 +40,7 @@ func TestAddReadOnlyDeployKey(t *testing.T) {
}
web.SetForm(ctx, &addKeyForm)
DeployKeysPost(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{
Name: addKeyForm.Title,
@@ -64,11 +50,7 @@ func TestAddReadOnlyDeployKey(t *testing.T) {
}
func TestAddReadWriteOnlyDeployKey(t *testing.T) {
- if deferable := createSSHAuthorizedKeysTmpPath(t); deferable != nil {
- defer deferable()
- } else {
- return
- }
+ defer test.MockVariableValue(&setting.SSH.RootPath, t.TempDir())()
unittest.PrepareTestEnv(t)
@@ -84,7 +66,7 @@ func TestAddReadWriteOnlyDeployKey(t *testing.T) {
}
web.SetForm(ctx, &addKeyForm)
DeployKeysPost(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{
Name: addKeyForm.Title,
@@ -121,7 +103,7 @@ func TestCollaborationPost(t *testing.T) {
CollaborationPost(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
exists, err := repo_model.IsCollaborator(ctx, re.ID, 4)
assert.NoError(t, err)
@@ -147,7 +129,7 @@ func TestCollaborationPost_InactiveUser(t *testing.T) {
CollaborationPost(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
@@ -179,7 +161,7 @@ func TestCollaborationPost_AddCollaboratorTwice(t *testing.T) {
CollaborationPost(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
exists, err := repo_model.IsCollaborator(ctx, re.ID, 4)
assert.NoError(t, err)
@@ -188,7 +170,7 @@ func TestCollaborationPost_AddCollaboratorTwice(t *testing.T) {
// Try adding the same collaborator again
CollaborationPost(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
@@ -210,7 +192,7 @@ func TestCollaborationPost_NonExistentUser(t *testing.T) {
CollaborationPost(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
@@ -250,7 +232,7 @@ func TestAddTeamPost(t *testing.T) {
AddTeamPost(ctx)
assert.True(t, repo_service.HasRepository(db.DefaultContext, team, re.ID))
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assert.Empty(t, ctx.Flash.ErrorMsg)
}
@@ -290,7 +272,7 @@ func TestAddTeamPost_NotAllowed(t *testing.T) {
AddTeamPost(ctx)
assert.False(t, repo_service.HasRepository(db.DefaultContext, team, re.ID))
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
@@ -331,7 +313,7 @@ func TestAddTeamPost_AddTeamTwice(t *testing.T) {
AddTeamPost(ctx)
assert.True(t, repo_service.HasRepository(db.DefaultContext, team, re.ID))
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
@@ -364,7 +346,7 @@ func TestAddTeamPost_NonExistentTeam(t *testing.T) {
ctx.Repo = repo
AddTeamPost(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assert.NotEmpty(t, ctx.Flash.ErrorMsg)
}
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 1b0ba83af4..006abafe57 100644
--- a/routers/web/repo/setting/webhook.go
+++ b/routers/web/repo/setting/webhook.go
@@ -112,7 +112,7 @@ func getOwnerRepoCtx(ctx *context.Context) (*ownerRepoCtx, error) {
func checkHookType(ctx *context.Context) string {
hookType := strings.ToLower(ctx.PathParam("type"))
if !util.SliceContainsString(setting.Webhook.Types, hookType, true) {
- ctx.NotFound("checkHookType", nil)
+ ctx.NotFound(nil)
return ""
}
return hookType
@@ -163,27 +163,30 @@ func ParseHookEvent(form forms.WebhookForm) *webhook_module.HookEvent {
SendEverything: form.SendEverything(),
ChooseEvents: form.ChooseEvents(),
HookEvents: webhook_module.HookEvents{
- Create: form.Create,
- Delete: form.Delete,
- Fork: form.Fork,
- Issues: form.Issues,
- IssueAssign: form.IssueAssign,
- IssueLabel: form.IssueLabel,
- IssueMilestone: form.IssueMilestone,
- IssueComment: form.IssueComment,
- Release: form.Release,
- Push: form.Push,
- PullRequest: form.PullRequest,
- PullRequestAssign: form.PullRequestAssign,
- PullRequestLabel: form.PullRequestLabel,
- PullRequestMilestone: form.PullRequestMilestone,
- PullRequestComment: form.PullRequestComment,
- PullRequestReview: form.PullRequestReview,
- PullRequestSync: form.PullRequestSync,
- PullRequestReviewRequest: form.PullRequestReviewRequest,
- Wiki: form.Wiki,
- Repository: form.Repository,
- Package: form.Package,
+ webhook_module.HookEventCreate: form.Create,
+ webhook_module.HookEventDelete: form.Delete,
+ webhook_module.HookEventFork: form.Fork,
+ webhook_module.HookEventIssues: form.Issues,
+ webhook_module.HookEventIssueAssign: form.IssueAssign,
+ webhook_module.HookEventIssueLabel: form.IssueLabel,
+ webhook_module.HookEventIssueMilestone: form.IssueMilestone,
+ webhook_module.HookEventIssueComment: form.IssueComment,
+ webhook_module.HookEventRelease: form.Release,
+ webhook_module.HookEventPush: form.Push,
+ webhook_module.HookEventPullRequest: form.PullRequest,
+ webhook_module.HookEventPullRequestAssign: form.PullRequestAssign,
+ webhook_module.HookEventPullRequestLabel: form.PullRequestLabel,
+ webhook_module.HookEventPullRequestMilestone: form.PullRequestMilestone,
+ webhook_module.HookEventPullRequestComment: form.PullRequestComment,
+ webhook_module.HookEventPullRequestReview: form.PullRequestReview,
+ webhook_module.HookEventPullRequestSync: form.PullRequestSync,
+ webhook_module.HookEventPullRequestReviewRequest: form.PullRequestReviewRequest,
+ webhook_module.HookEventWiki: form.Wiki,
+ webhook_module.HookEventRepository: form.Repository,
+ webhook_module.HookEventPackage: form.Package,
+ webhook_module.HookEventStatus: form.Status,
+ webhook_module.HookEventWorkflowRun: form.WorkflowRun,
+ webhook_module.HookEventWorkflowJob: form.WorkflowJob,
},
BranchFilter: form.BranchFilter,
}
@@ -600,7 +603,7 @@ func checkWebhook(ctx *context.Context) (*ownerRepoCtx, *webhook.Webhook) {
}
if err != nil || w == nil {
if webhook.IsErrWebhookNotExist(err) {
- ctx.NotFound("GetWebhookByID", nil)
+ ctx.NotFound(nil)
} else {
ctx.ServerError("GetWebhookByID", err)
}
@@ -654,6 +657,8 @@ func TestWebhook(ctx *context.Context) {
}
// Grab latest commit or fake one if it's empty repository.
+ // Note: in old code, the "ctx.Repo.Commit" is the last commit of the default branch.
+ // New code doesn't set that commit, so it always uses the fake commit to test webhook.
commit := ctx.Repo.Commit
if commit == nil {
ghost := user_model.NewGhostUser()
@@ -715,7 +720,7 @@ func ReplayWebhook(ctx *context.Context) {
if err := webhook_service.ReplayHookTask(ctx, w, hookTaskUUID); err != nil {
if webhook.IsErrHookTaskNotExist(err) {
- ctx.NotFound("ReplayHookTask", nil)
+ ctx.NotFound(nil)
} else {
ctx.ServerError("ReplayHookTask", err)
}
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/treelist.go b/routers/web/repo/treelist.go
index d11af4669f..7d7f5a1473 100644
--- a/routers/web/repo/treelist.go
+++ b/routers/web/repo/treelist.go
@@ -4,11 +4,18 @@
package repo
import (
+ "html/template"
"net/http"
+ "path"
+ "strings"
+ pull_model "code.gitea.io/gitea/models/pull"
"code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/fileicon"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/gitdiff"
+ files_service "code.gitea.io/gitea/services/repository/files"
"github.com/go-enry/go-enry/v2"
)
@@ -52,3 +59,95 @@ func isExcludedEntry(entry *git.TreeEntry) bool {
return false
}
+
+// WebDiffFileItem is used by frontend, check the field names in frontend before changing
+type WebDiffFileItem struct {
+ FullName string
+ DisplayName string
+ NameHash string
+ DiffStatus string
+ EntryMode string
+ IsViewed bool
+ Children []*WebDiffFileItem
+ FileIcon template.HTML
+}
+
+// WebDiffFileTree is used by frontend, check the field names in frontend before changing
+type WebDiffFileTree struct {
+ TreeRoot WebDiffFileItem
+}
+
+// transformDiffTreeForWeb transforms a gitdiff.DiffTree into a WebDiffFileTree for Web UI rendering
+// it also takes a map of file names to their viewed state, which is used to mark files as viewed
+func transformDiffTreeForWeb(renderedIconPool *fileicon.RenderedIconPool, diffTree *gitdiff.DiffTree, filesViewedState map[string]pull_model.ViewedState) (dft WebDiffFileTree) {
+ dirNodes := map[string]*WebDiffFileItem{"": &dft.TreeRoot}
+ addItem := func(item *WebDiffFileItem) {
+ var parentPath string
+ pos := strings.LastIndexByte(item.FullName, '/')
+ if pos == -1 {
+ item.DisplayName = item.FullName
+ } else {
+ parentPath = item.FullName[:pos]
+ item.DisplayName = item.FullName[pos+1:]
+ }
+ parentNode, parentExists := dirNodes[parentPath]
+ if !parentExists {
+ parentNode = &dft.TreeRoot
+ fields := strings.Split(parentPath, "/")
+ for idx, field := range fields {
+ nodePath := strings.Join(fields[:idx+1], "/")
+ node, ok := dirNodes[nodePath]
+ if !ok {
+ node = &WebDiffFileItem{EntryMode: "tree", DisplayName: field, FullName: nodePath}
+ dirNodes[nodePath] = node
+ parentNode.Children = append(parentNode.Children, node)
+ }
+ parentNode = node
+ }
+ }
+ parentNode.Children = append(parentNode.Children, item)
+ }
+
+ for _, file := range diffTree.Files {
+ item := &WebDiffFileItem{FullName: file.HeadPath, DiffStatus: file.Status}
+ item.IsViewed = filesViewedState[item.FullName] == pull_model.Viewed
+ item.NameHash = git.HashFilePathForWebUI(item.FullName)
+ item.FileIcon = fileicon.RenderEntryIconHTML(renderedIconPool, &fileicon.EntryInfo{BaseName: path.Base(file.HeadPath), EntryMode: file.HeadMode})
+
+ switch file.HeadMode {
+ case git.EntryModeTree:
+ item.EntryMode = "tree"
+ case git.EntryModeCommit:
+ item.EntryMode = "commit" // submodule
+ default:
+ // default to empty, and will be treated as "blob" file because there is no "symlink" support yet
+ }
+ addItem(item)
+ }
+
+ var mergeSingleDir func(node *WebDiffFileItem)
+ mergeSingleDir = func(node *WebDiffFileItem) {
+ if len(node.Children) == 1 {
+ if child := node.Children[0]; child.EntryMode == "tree" {
+ node.FullName = child.FullName
+ node.DisplayName = node.DisplayName + "/" + child.DisplayName
+ node.Children = child.Children
+ mergeSingleDir(node)
+ }
+ }
+ }
+ for _, node := range dft.TreeRoot.Children {
+ mergeSingleDir(node)
+ }
+ return dft
+}
+
+func TreeViewNodes(ctx *context.Context) {
+ renderedIconPool := fileicon.NewRenderedIconPool()
+ results, err := files_service.GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, ctx.Repo.TreePath, ctx.FormString("sub_path"))
+ if err != nil {
+ ctx.ServerError("GetTreeViewNodes", err)
+ return
+ }
+ ctx.JSON(http.StatusOK, map[string]any{"fileTreeNodes": results, "renderedIconPool": renderedIconPool.IconSVGs})
+}
diff --git a/routers/web/repo/treelist_test.go b/routers/web/repo/treelist_test.go
new file mode 100644
index 0000000000..94ba60661b
--- /dev/null
+++ b/routers/web/repo/treelist_test.go
@@ -0,0 +1,68 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "html/template"
+ "testing"
+
+ pull_model "code.gitea.io/gitea/models/pull"
+ "code.gitea.io/gitea/modules/fileicon"
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/services/gitdiff"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestTransformDiffTreeForWeb(t *testing.T) {
+ renderedIconPool := fileicon.NewRenderedIconPool()
+ ret := transformDiffTreeForWeb(renderedIconPool, &gitdiff.DiffTree{Files: []*gitdiff.DiffTreeRecord{
+ {
+ Status: "changed",
+ HeadPath: "dir-a/dir-a-x/file-deep",
+ HeadMode: git.EntryModeBlob,
+ },
+ {
+ Status: "added",
+ HeadPath: "file1",
+ HeadMode: git.EntryModeBlob,
+ },
+ }}, map[string]pull_model.ViewedState{
+ "dir-a/dir-a-x/file-deep": pull_model.Viewed,
+ })
+
+ mockIconForFile := func(id string) template.HTML {
+ return template.HTML(`<svg class="svg git-entry-icon octicon-file" width="16" height="16" aria-hidden="true"><use xlink:href="#` + id + `"></use></svg>`)
+ }
+ assert.Equal(t, WebDiffFileTree{
+ TreeRoot: WebDiffFileItem{
+ Children: []*WebDiffFileItem{
+ {
+ EntryMode: "tree",
+ DisplayName: "dir-a/dir-a-x",
+ FullName: "dir-a/dir-a-x",
+ Children: []*WebDiffFileItem{
+ {
+ EntryMode: "",
+ DisplayName: "file-deep",
+ FullName: "dir-a/dir-a-x/file-deep",
+ NameHash: "4acf7eef1c943a09e9f754e93ff190db8583236b",
+ DiffStatus: "changed",
+ IsViewed: true,
+ FileIcon: mockIconForFile(`svg-mfi-file`),
+ },
+ },
+ },
+ {
+ EntryMode: "",
+ DisplayName: "file1",
+ FullName: "file1",
+ NameHash: "60b27f004e454aca81b0480209cce5081ec52390",
+ DiffStatus: "added",
+ FileIcon: mockIconForFile(`svg-mfi-file`),
+ },
+ },
+ },
+ }, ret)
+}
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index 05ecf2ab79..773919c054 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -12,6 +12,7 @@ import (
"io"
"net/http"
"net/url"
+ "path"
"strings"
"time"
@@ -29,6 +30,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset"
+ "code.gitea.io/gitea/modules/fileicon"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/log"
@@ -38,6 +40,7 @@ import (
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/typesniffer"
"code.gitea.io/gitea/modules/util"
+ asymkey_service "code.gitea.io/gitea/services/asymkey"
"code.gitea.io/gitea/services/context"
repo_service "code.gitea.io/gitea/services/repository"
@@ -46,69 +49,74 @@ import (
)
const (
- tplRepoEMPTY templates.TplName = "repo/empty"
- tplRepoHome templates.TplName = "repo/home"
- tplRepoViewList templates.TplName = "repo/view_list"
- tplWatchers templates.TplName = "repo/watchers"
- tplForks templates.TplName = "repo/forks"
- tplMigrating templates.TplName = "repo/migrate/migrating"
+ tplRepoEMPTY templates.TplName = "repo/empty"
+ tplRepoHome templates.TplName = "repo/home"
+ tplRepoView templates.TplName = "repo/view"
+ tplRepoViewContent templates.TplName = "repo/view_content"
+ tplRepoViewList templates.TplName = "repo/view_list"
+ tplWatchers templates.TplName = "repo/watchers"
+ tplForks templates.TplName = "repo/forks"
+ tplMigrating templates.TplName = "repo/migrate/migrating"
)
type fileInfo struct {
- isTextFile bool
- isLFSFile bool
- fileSize int64
- lfsMeta *lfs.Pointer
- st typesniffer.SniffedType
+ fileSize int64
+ lfsMeta *lfs.Pointer
+ st typesniffer.SniffedType
}
-func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) ([]byte, io.ReadCloser, *fileInfo, error) {
- dataRc, err := blob.DataAsync()
+func (fi *fileInfo) isLFSFile() bool {
+ return fi.lfsMeta != nil && fi.lfsMeta.Oid != ""
+}
+
+func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) (buf []byte, dataRc io.ReadCloser, fi *fileInfo, err error) {
+ dataRc, err = blob.DataAsync()
if err != nil {
return nil, nil, nil, err
}
- buf := make([]byte, 1024)
+ const prefetchSize = lfs.MetaFileMaxSize
+
+ buf = make([]byte, prefetchSize)
n, _ := util.ReadAtMost(dataRc, buf)
buf = buf[:n]
- st := typesniffer.DetectContentType(buf)
- isTextFile := st.IsText()
+ fi = &fileInfo{fileSize: blob.Size(), st: typesniffer.DetectContentType(buf)}
// FIXME: what happens when README file is an image?
- if !isTextFile || !setting.LFS.StartServer {
- return buf, dataRc, &fileInfo{isTextFile, false, blob.Size(), nil, st}, nil
+ if !fi.st.IsText() || !setting.LFS.StartServer {
+ return buf, dataRc, fi, nil
}
pointer, _ := lfs.ReadPointerFromBuffer(buf)
- if !pointer.IsValid() { // fallback to plain file
- return buf, dataRc, &fileInfo{isTextFile, false, blob.Size(), nil, st}, nil
+ if !pointer.IsValid() { // fallback to a plain file
+ return buf, dataRc, fi, nil
}
meta, err := git_model.GetLFSMetaObjectByOid(ctx, repoID, pointer.Oid)
- if err != nil { // fallback to plain file
+ if err != nil { // fallback to a plain file
log.Warn("Unable to access LFS pointer %s in repo %d: %v", pointer.Oid, repoID, err)
- return buf, dataRc, &fileInfo{isTextFile, false, blob.Size(), nil, st}, nil
+ return buf, dataRc, fi, nil
}
- dataRc.Close()
-
+ // close the old dataRc and open the real LFS target
+ _ = dataRc.Close()
dataRc, err = lfs.ReadMetaObject(pointer)
if err != nil {
return nil, nil, nil, err
}
- buf = make([]byte, 1024)
+ buf = make([]byte, prefetchSize)
n, err = util.ReadAtMost(dataRc, buf)
if err != nil {
- dataRc.Close()
- return nil, nil, nil, err
+ _ = dataRc.Close()
+ return nil, nil, fi, err
}
buf = buf[:n]
-
- st = typesniffer.DetectContentType(buf)
-
- return buf, dataRc, &fileInfo{st.IsText(), true, meta.Size, &meta.Pointer, st}, nil
+ fi.st = typesniffer.DetectContentType(buf)
+ fi.fileSize = blob.Size()
+ fi.lfsMeta = &meta.Pointer
+ return buf, dataRc, fi, nil
}
func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool {
@@ -116,7 +124,7 @@ func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool {
// or of directory if not in root directory.
ctx.Data["LatestCommit"] = latestCommit
if latestCommit != nil {
- verification := asymkey_model.ParseCommitWithSignature(ctx, latestCommit)
+ verification := asymkey_service.ParseCommitWithSignature(ctx, latestCommit)
if err := asymkey_model.CalculateTrustStatus(verification, ctx.Repo.Repository.GetTrustModel(), func(user *user_model.User) (bool, error) {
return repo_model.IsOwnerMemberCollaborator(ctx, ctx.Repo.Repository, user.ID)
@@ -127,7 +135,7 @@ func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool {
ctx.Data["LatestCommitVerification"] = verification
ctx.Data["LatestCommitUser"] = user_model.ValidateCommitWithEmail(ctx, latestCommit)
- statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, latestCommit.ID.String(), db.ListOptionsAll)
+ statuses, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, latestCommit.ID.String(), db.ListOptionsAll)
if err != nil {
log.Error("GetLatestCommitStatus: %v", err)
}
@@ -215,7 +223,7 @@ func checkHomeCodeViewable(ctx *context.Context) {
}
}
- ctx.NotFound("Home", errors.New(ctx.Locale.TrString("units.error.no_unit_allowed_repo")))
+ ctx.NotFound(errors.New(ctx.Locale.TrString("units.error.no_unit_allowed_repo")))
}
// LastCommit returns lastCommit data for the provided branch/tag/commit and directory (in url) and filenames in body
@@ -249,6 +257,19 @@ func LastCommit(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplRepoViewList)
}
+func prepareDirectoryFileIcons(ctx *context.Context, files []git.CommitInfo) {
+ renderedIconPool := fileicon.NewRenderedIconPool()
+ fileIcons := map[string]template.HTML{}
+ for _, f := range files {
+ fullPath := path.Join(ctx.Repo.TreePath, f.Entry.Name())
+ entryInfo := fileicon.EntryInfoFromGitTreeEntry(ctx.Repo.Commit, fullPath, f.Entry)
+ fileIcons[f.Entry.Name()] = fileicon.RenderEntryIconHTML(renderedIconPool, entryInfo)
+ }
+ fileIcons[".."] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder())
+ ctx.Data["FileIcons"] = fileIcons
+ ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
+}
+
func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entries {
tree, err := ctx.Repo.Commit.SubTree(ctx.Repo.TreePath)
if err != nil {
@@ -290,6 +311,7 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
return nil
}
ctx.Data["Files"] = files
+ prepareDirectoryFileIcons(ctx, files)
for _, f := range files {
if f.Commit == nil {
ctx.Data["HasFilesWithoutLatestCommit"] = true
@@ -378,9 +400,10 @@ func Forks(ctx *context.Context) {
}
pager := context.NewPagination(int(total), pageSize, page, 5)
+ ctx.Data["ShowRepoOwnerAvatar"] = true
+ ctx.Data["ShowRepoOwnerOnList"] = true
ctx.Data["Page"] = pager
-
- ctx.Data["Forks"] = forks
+ ctx.Data["Repos"] = forks
ctx.HTML(http.StatusOK, tplForks)
}
diff --git a/routers/web/repo/view_file.go b/routers/web/repo/view_file.go
index 5784297918..2d5bddd939 100644
--- a/routers/web/repo/view_file.go
+++ b/routers/web/repo/view_file.go
@@ -9,7 +9,6 @@ import (
"image"
"io"
"path"
- "slices"
"strings"
git_model "code.gitea.io/gitea/models/git"
@@ -19,44 +18,165 @@ import (
"code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/git/attribute"
"code.gitea.io/gitea/modules/highlight"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/typesniffer"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
issue_service "code.gitea.io/gitea/services/issue"
- files_service "code.gitea.io/gitea/services/repository/files"
"github.com/nektos/act/pkg/model"
)
-func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) {
- ctx.Data["IsViewFile"] = true
- ctx.Data["HideRepoInfo"] = true
- blob := entry.Blob()
- buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob)
+func prepareLatestCommitInfo(ctx *context.Context) bool {
+ commit, err := ctx.Repo.Commit.GetCommitByPath(ctx.Repo.TreePath)
if err != nil {
- ctx.ServerError("getFileReader", err)
- return
+ ctx.ServerError("GetCommitByPath", err)
+ return false
}
- defer dataRc.Close()
- ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefFullName.ShortName())
- ctx.Data["FileIsSymlink"] = entry.IsLink()
- ctx.Data["FileName"] = blob.Name()
- ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
+ return loadLatestCommitData(ctx, commit)
+}
- commit, err := ctx.Repo.Commit.GetCommitByPath(ctx.Repo.TreePath)
+func prepareFileViewLfsAttrs(ctx *context.Context) (*attribute.Attributes, bool) {
+ attrsMap, err := attribute.CheckAttributes(ctx, ctx.Repo.GitRepo, ctx.Repo.CommitID, attribute.CheckAttributeOpts{
+ Filenames: []string{ctx.Repo.TreePath},
+ Attributes: []string{attribute.LinguistGenerated, attribute.LinguistVendored, attribute.LinguistLanguage, attribute.GitlabLanguage},
+ })
if err != nil {
- ctx.ServerError("GetCommitByPath", err)
- return
+ ctx.ServerError("attribute.CheckAttributes", err)
+ return nil, false
+ }
+ attrs := attrsMap[ctx.Repo.TreePath]
+ if attrs == nil {
+ // this case shouldn't happen, just in case.
+ setting.PanicInDevOrTesting("no attributes found for %s", ctx.Repo.TreePath)
+ attrs = attribute.NewAttributes()
+ }
+ ctx.Data["IsVendored"], ctx.Data["IsGenerated"] = attrs.GetVendored().Value(), attrs.GetGenerated().Value()
+ return attrs, true
+}
+
+func handleFileViewRenderMarkup(ctx *context.Context, filename string, sniffedType typesniffer.SniffedType, prefetchBuf []byte, utf8Reader io.Reader) bool {
+ markupType := markup.DetectMarkupTypeByFileName(filename)
+ if markupType == "" {
+ markupType = markup.DetectRendererType(filename, sniffedType, prefetchBuf)
+ }
+ if markupType == "" {
+ return false
+ }
+
+ ctx.Data["HasSourceRenderedToggle"] = true
+
+ if ctx.FormString("display") == "source" {
+ return false
+ }
+
+ ctx.Data["MarkupType"] = markupType
+ metas := ctx.Repo.Repository.ComposeRepoFileMetas(ctx)
+ metas["RefTypeNameSubURL"] = ctx.Repo.RefTypeNameSubURL()
+ rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
+ CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
+ CurrentTreePath: path.Dir(ctx.Repo.TreePath),
+ }).
+ WithMarkupType(markupType).
+ WithRelativePath(ctx.Repo.TreePath).
+ WithMetas(metas)
+
+ var err error
+ ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, utf8Reader)
+ if err != nil {
+ ctx.ServerError("Render", err)
+ return true
+ }
+ // to prevent iframe from loading third-party url
+ ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'")
+ return true
+}
+
+func handleFileViewRenderSource(ctx *context.Context, filename string, attrs *attribute.Attributes, fInfo *fileInfo, utf8Reader io.Reader) bool {
+ if ctx.FormString("display") == "rendered" || !fInfo.st.IsRepresentableAsText() {
+ return false
+ }
+
+ if !fInfo.st.IsText() {
+ if ctx.FormString("display") == "" {
+ // not text but representable as text, e.g. SVG
+ // since there is no "display" is specified, let other renders to handle
+ return false
+ }
+ ctx.Data["HasSourceRenderedToggle"] = true
+ }
+
+ buf, _ := io.ReadAll(utf8Reader)
+ // The Open Group Base Specification: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html
+ // empty: 0 lines; "a": 1 incomplete-line; "a\n": 1 line; "a\nb": 1 line, 1 incomplete-line;
+ // Gitea uses the definition (like most modern editors):
+ // empty: 0 lines; "a": 1 line; "a\n": 2 lines; "a\nb": 2 lines;
+ // When rendering, the last empty line is not rendered in UI, while the line-number is still counted, to tell users that the file contains a trailing EOL.
+ // To make the UI more consistent, it could use an icon mark to indicate that there is no trailing EOL, and show line-number as the rendered lines.
+ // This NumLines is only used for the display on the UI: "xxx lines"
+ if len(buf) == 0 {
+ ctx.Data["NumLines"] = 0
+ } else {
+ ctx.Data["NumLines"] = bytes.Count(buf, []byte{'\n'}) + 1
+ }
+
+ language := attrs.GetLanguage().Value()
+ fileContent, lexerName, err := highlight.File(filename, language, buf)
+ ctx.Data["LexerName"] = lexerName
+ if err != nil {
+ log.Error("highlight.File failed, fallback to plain text: %v", err)
+ fileContent = highlight.PlainText(buf)
+ }
+ status := &charset.EscapeStatus{}
+ statuses := make([]*charset.EscapeStatus, len(fileContent))
+ for i, line := range fileContent {
+ statuses[i], fileContent[i] = charset.EscapeControlHTML(line, ctx.Locale)
+ status = status.Or(statuses[i])
}
+ ctx.Data["EscapeStatus"] = status
+ ctx.Data["FileContent"] = fileContent
+ ctx.Data["LineEscapeStatus"] = statuses
+ return true
+}
+
+func handleFileViewRenderImage(ctx *context.Context, fInfo *fileInfo, prefetchBuf []byte) bool {
+ if !fInfo.st.IsImage() {
+ return false
+ }
+ if fInfo.st.IsSvgImage() && !setting.UI.SVG.Enabled {
+ return false
+ }
+ if fInfo.st.IsSvgImage() {
+ ctx.Data["HasSourceRenderedToggle"] = true
+ } else {
+ img, _, err := image.DecodeConfig(bytes.NewReader(prefetchBuf))
+ if err == nil { // ignore the error for the formats that are not supported by image.DecodeConfig
+ ctx.Data["ImageSize"] = fmt.Sprintf("%dx%dpx", img.Width, img.Height)
+ }
+ }
+ return true
+}
- if !loadLatestCommitData(ctx, commit) {
+func prepareFileView(ctx *context.Context, entry *git.TreeEntry) {
+ ctx.Data["IsViewFile"] = true
+ ctx.Data["HideRepoInfo"] = true
+
+ if !prepareLatestCommitInfo(ctx) {
return
}
+ blob := entry.Blob()
+
+ ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefFullName.ShortName())
+ ctx.Data["FileIsSymlink"] = entry.IsLink()
+ ctx.Data["FileTreePath"] = ctx.Repo.TreePath
+ ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
+
if ctx.Repo.TreePath == ".editorconfig" {
_, editorconfigWarning, editorconfigErr := ctx.Repo.GetEditorconfig(ctx.Repo.Commit)
if editorconfigWarning != nil {
@@ -79,7 +199,7 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) {
if workFlowErr != nil {
ctx.Data["FileError"] = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", workFlowErr.Error())
}
- } else if slices.Contains([]string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}, ctx.Repo.TreePath) {
+ } else if issue_service.IsCodeOwnerFile(ctx.Repo.TreePath) {
if data, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize); err == nil {
_, warnings := issue_model.GetCodeOwnersFromContent(ctx, data)
if len(warnings) > 0 {
@@ -88,226 +208,103 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) {
}
}
- isDisplayingSource := ctx.FormString("display") == "source"
- isDisplayingRendered := !isDisplayingSource
+ // Don't call any other repository functions depends on git.Repository until the dataRc closed to
+ // avoid creating an unnecessary temporary cat file.
+ buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob)
+ if err != nil {
+ ctx.ServerError("getFileReader", err)
+ return
+ }
+ defer dataRc.Close()
- if fInfo.isLFSFile {
+ if fInfo.isLFSFile() {
ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/media/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
}
- isRepresentableAsText := fInfo.st.IsRepresentableAsText()
- if !isRepresentableAsText {
- // If we can't show plain text, always try to render.
- isDisplayingSource = false
- isDisplayingRendered = true
+ if !prepareFileViewEditorButtons(ctx) {
+ return
}
- ctx.Data["IsLFSFile"] = fInfo.isLFSFile
+
+ ctx.Data["IsLFSFile"] = fInfo.isLFSFile()
ctx.Data["FileSize"] = fInfo.fileSize
- ctx.Data["IsTextFile"] = fInfo.isTextFile
- ctx.Data["IsRepresentableAsText"] = isRepresentableAsText
- ctx.Data["IsDisplayingSource"] = isDisplayingSource
- ctx.Data["IsDisplayingRendered"] = isDisplayingRendered
+ ctx.Data["IsRepresentableAsText"] = fInfo.st.IsRepresentableAsText()
ctx.Data["IsExecutable"] = entry.IsExecutable()
+ ctx.Data["CanCopyContent"] = fInfo.st.IsRepresentableAsText() || fInfo.st.IsImage()
- isTextSource := fInfo.isTextFile || isDisplayingSource
- ctx.Data["IsTextSource"] = isTextSource
- if isTextSource {
- ctx.Data["CanCopyContent"] = true
- }
-
- // Check LFS Lock
- lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath)
- ctx.Data["LFSLock"] = lfsLock
- if err != nil {
- ctx.ServerError("GetTreePathLock", err)
+ attrs, ok := prepareFileViewLfsAttrs(ctx)
+ if !ok {
return
}
- if lfsLock != nil {
- u, err := user_model.GetUserByID(ctx, lfsLock.OwnerID)
- if err != nil {
- ctx.ServerError("GetTreePathLock", err)
- return
- }
- ctx.Data["LFSLockOwner"] = u.Name
- ctx.Data["LFSLockOwnerHomeLink"] = u.HomeLink()
- ctx.Data["LFSLockHint"] = ctx.Tr("repo.editor.this_file_locked")
- }
- // Assume file is not editable first.
- if fInfo.isLFSFile {
- ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_lfs_files")
- } else if !isRepresentableAsText {
- ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_non_text_files")
- }
+ // TODO: in the future maybe we need more accurate flags, for example:
+ // * IsRepresentableAsText: some files are text, some are not
+ // * IsRenderableXxx: some files are rendered by backend "markup" engine, some are rendered by frontend (pdf, 3d)
+ // * DefaultViewMode: when there is no "display" query parameter, which view mode should be used by default, source or rendered
+ utf8Reader := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
switch {
- case isRepresentableAsText:
- if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
- ctx.Data["IsFileTooLarge"] = true
- break
- }
-
- if fInfo.st.IsSvgImage() {
- ctx.Data["IsImageFile"] = true
- ctx.Data["CanCopyContent"] = true
- ctx.Data["HasSourceRenderedToggle"] = true
- }
-
- rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
-
- shouldRenderSource := ctx.FormString("display") == "source"
- readmeExist := util.IsReadmeFileName(blob.Name())
- ctx.Data["ReadmeExist"] = readmeExist
-
- markupType := markup.DetectMarkupTypeByFileName(blob.Name())
- if markupType == "" {
- markupType = markup.DetectRendererType(blob.Name(), bytes.NewReader(buf))
- }
- if markupType != "" {
- ctx.Data["HasSourceRenderedToggle"] = true
- }
- if markupType != "" && !shouldRenderSource {
- ctx.Data["IsMarkup"] = true
- ctx.Data["MarkupType"] = markupType
- metas := ctx.Repo.Repository.ComposeDocumentMetas(ctx)
- metas["RefTypeNameSubURL"] = ctx.Repo.RefTypeNameSubURL()
- rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
- CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
- CurrentTreePath: path.Dir(ctx.Repo.TreePath),
- }).
- WithMarkupType(markupType).
- WithRelativePath(ctx.Repo.TreePath).
- WithMetas(metas)
-
- ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd)
- if err != nil {
- ctx.ServerError("Render", err)
- return
- }
- // to prevent iframe load third-party url
- ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'")
- } else {
- buf, _ := io.ReadAll(rd)
-
- // The Open Group Base Specification: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html
- // empty: 0 lines; "a": 1 incomplete-line; "a\n": 1 line; "a\nb": 1 line, 1 incomplete-line;
- // Gitea uses the definition (like most modern editors):
- // empty: 0 lines; "a": 1 line; "a\n": 2 lines; "a\nb": 2 lines;
- // When rendering, the last empty line is not rendered in UI, while the line-number is still counted, to tell users that the file contains a trailing EOL.
- // To make the UI more consistent, it could use an icon mark to indicate that there is no trailing EOL, and show line-number as the rendered lines.
- // This NumLines is only used for the display on the UI: "xxx lines"
- if len(buf) == 0 {
- ctx.Data["NumLines"] = 0
- } else {
- ctx.Data["NumLines"] = bytes.Count(buf, []byte{'\n'}) + 1
- }
-
- language, err := files_service.TryGetContentLanguage(ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath)
- if err != nil {
- log.Error("Unable to get file language for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
- }
-
- fileContent, lexerName, err := highlight.File(blob.Name(), language, buf)
- ctx.Data["LexerName"] = lexerName
- if err != nil {
- log.Error("highlight.File failed, fallback to plain text: %v", err)
- fileContent = highlight.PlainText(buf)
- }
- status := &charset.EscapeStatus{}
- statuses := make([]*charset.EscapeStatus, len(fileContent))
- for i, line := range fileContent {
- statuses[i], fileContent[i] = charset.EscapeControlHTML(line, ctx.Locale)
- status = status.Or(statuses[i])
- }
- ctx.Data["EscapeStatus"] = status
- ctx.Data["FileContent"] = fileContent
- ctx.Data["LineEscapeStatus"] = statuses
- }
- if !fInfo.isLFSFile {
- if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) {
- if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID {
- ctx.Data["CanEditFile"] = false
- ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.this_file_locked")
- } else {
- ctx.Data["CanEditFile"] = true
- ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file")
- }
- } else if !ctx.Repo.RefFullName.IsBranch() {
- ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
- } else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) {
- ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit")
- }
- }
-
- case fInfo.st.IsPDF():
- ctx.Data["IsPDFFile"] = true
+ case fInfo.fileSize >= setting.UI.MaxDisplayFileSize:
+ ctx.Data["IsFileTooLarge"] = true
+ case handleFileViewRenderMarkup(ctx, entry.Name(), fInfo.st, buf, utf8Reader):
+ // it also sets ctx.Data["FileContent"] and more
+ ctx.Data["IsMarkup"] = true
+ case handleFileViewRenderSource(ctx, entry.Name(), attrs, fInfo, utf8Reader):
+ // it also sets ctx.Data["FileContent"] and more
+ ctx.Data["IsDisplayingSource"] = true
+ case handleFileViewRenderImage(ctx, fInfo, buf):
+ ctx.Data["IsImageFile"] = true
case fInfo.st.IsVideo():
ctx.Data["IsVideoFile"] = true
case fInfo.st.IsAudio():
ctx.Data["IsAudioFile"] = true
- case fInfo.st.IsImage() && (setting.UI.SVG.Enabled || !fInfo.st.IsSvgImage()):
- ctx.Data["IsImageFile"] = true
- ctx.Data["CanCopyContent"] = true
default:
- if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
- ctx.Data["IsFileTooLarge"] = true
- break
- }
+ // unable to render anything, show the "view raw" or let frontend handle it
+ }
+}
- // TODO: this logic duplicates with "isRepresentableAsText=true", it is not the same as "LFSFileGet" in "lfs.go"
- // It is used by "external renders", markupRender will execute external programs to get rendered content.
- if markupType := markup.DetectMarkupTypeByFileName(blob.Name()); markupType != "" {
- rd := io.MultiReader(bytes.NewReader(buf), dataRc)
- ctx.Data["IsMarkup"] = true
- ctx.Data["MarkupType"] = markupType
-
- rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
- CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
- CurrentTreePath: path.Dir(ctx.Repo.TreePath),
- }).
- WithMarkupType(markupType).
- WithRelativePath(ctx.Repo.TreePath)
-
- ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd)
- if err != nil {
- ctx.ServerError("Render", err)
- return
- }
- }
+func prepareFileViewEditorButtons(ctx *context.Context) bool {
+ // archived or mirror repository, the buttons should not be shown
+ if !ctx.Repo.Repository.CanEnableEditor() {
+ return true
}
- if ctx.Repo.GitRepo != nil {
- checker, deferable := ctx.Repo.GitRepo.CheckAttributeReader(ctx.Repo.CommitID)
- if checker != nil {
- defer deferable()
- attrs, err := checker.CheckPath(ctx.Repo.TreePath)
- if err == nil {
- ctx.Data["IsVendored"] = git.AttributeToBool(attrs, git.AttributeLinguistVendored).Value()
- ctx.Data["IsGenerated"] = git.AttributeToBool(attrs, git.AttributeLinguistGenerated).Value()
- }
- }
+ // The buttons should not be shown if it's not a branch
+ if !ctx.Repo.RefFullName.IsBranch() {
+ ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
+ ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
+ return true
}
- if fInfo.st.IsImage() && !fInfo.st.IsSvgImage() {
- img, _, err := image.DecodeConfig(bytes.NewReader(buf))
- if err == nil {
- // There are Image formats go can't decode
- // Instead of throwing an error in that case, we show the size only when we can decode
- ctx.Data["ImageSize"] = fmt.Sprintf("%dx%dpx", img.Width, img.Height)
- }
+ if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) {
+ ctx.Data["CanEditFile"] = true
+ ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit")
+ ctx.Data["CanDeleteFile"] = true
+ ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access")
+ return true
}
- if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) {
- if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID {
- ctx.Data["CanDeleteFile"] = false
- ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.this_file_locked")
- } else {
- ctx.Data["CanDeleteFile"] = true
- ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.delete_this_file")
+ lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath)
+ ctx.Data["LFSLock"] = lfsLock
+ if err != nil {
+ ctx.ServerError("GetTreePathLock", err)
+ return false
+ }
+ if lfsLock != nil {
+ u, err := user_model.GetUserByID(ctx, lfsLock.OwnerID)
+ if err != nil {
+ ctx.ServerError("GetTreePathLock", err)
+ return false
}
- } else if !ctx.Repo.RefFullName.IsBranch() {
- ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
- } else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) {
- ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access")
+ ctx.Data["LFSLockOwner"] = u.Name
+ ctx.Data["LFSLockOwnerHomeLink"] = u.HomeLink()
+ ctx.Data["LFSLockHint"] = ctx.Tr("repo.editor.this_file_locked")
}
+
+ // it's a lfs file and the user is not the owner of the lock
+ isLFSLocked := lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID
+ ctx.Data["CanEditFile"] = !isLFSLocked
+ ctx.Data["EditFileTooltip"] = util.Iif(isLFSLocked, ctx.Tr("repo.editor.this_file_locked"), ctx.Tr("repo.editor.edit_this_file"))
+ ctx.Data["CanDeleteFile"] = !isLFSLocked
+ ctx.Data["DeleteFileTooltip"] = util.Iif(isLFSLocked, ctx.Tr("repo.editor.this_file_locked"), ctx.Tr("repo.editor.delete_this_file"))
+ return true
}
diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go
index d141df181c..c7396d44e3 100644
--- a/routers/web/repo/view_home.go
+++ b/routers/web/repo/view_home.go
@@ -9,6 +9,7 @@ import (
"html/template"
"net/http"
"path"
+ "strconv"
"strings"
"time"
@@ -17,7 +18,11 @@ import (
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
+ user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
+ giturl "code.gitea.io/gitea/modules/git/url"
+ "code.gitea.io/gitea/modules/gitrepo"
+ "code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
@@ -74,7 +79,7 @@ func prepareOpenWithEditorApps(ctx *context.Context) {
schema, _, _ := strings.Cut(app.OpenURL, ":")
var iconHTML template.HTML
if schema == "vscode" || schema == "vscodium" || schema == "jetbrains" {
- iconHTML = svg.RenderHTML(fmt.Sprintf("gitea-%s", schema), 16)
+ iconHTML = svg.RenderHTML("gitea-"+schema, 16)
} else {
iconHTML = svg.RenderHTML("gitea-git", 16) // TODO: it could support user's customized icon in the future
}
@@ -138,7 +143,7 @@ func prepareToRenderDirectory(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefFullName.ShortName())
}
- subfolder, readmeFile, err := findReadmeFileInEntries(ctx, entries, true)
+ subfolder, readmeFile, err := findReadmeFileInEntries(ctx, ctx.Repo.TreePath, entries, true)
if err != nil {
ctx.ServerError("findReadmeFileInEntries", err)
return
@@ -257,6 +262,10 @@ func updateContextRepoEmptyAndStatus(ctx *context.Context, empty bool, status re
func handleRepoEmptyOrBroken(ctx *context.Context) {
showEmpty := true
+ if ctx.Repo.GitRepo == nil {
+ // in case the repo really exists and works, but the status was incorrectly marked as "broken", we need to open and check it again
+ ctx.Repo.GitRepo, _ = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository)
+ }
if ctx.Repo.GitRepo != nil {
reallyEmpty, err := ctx.Repo.GitRepo.IsEmpty()
if err != nil {
@@ -267,7 +276,7 @@ func handleRepoEmptyOrBroken(ctx *context.Context) {
} else if reallyEmpty {
showEmpty = true // the repo is really empty
updateContextRepoEmptyAndStatus(ctx, true, repo_model.RepositoryReady)
- } else if branches, _, _ := ctx.Repo.GitRepo.GetBranches(0, 1); len(branches) == 0 {
+ } else if branches, _, _ := ctx.Repo.GitRepo.GetBranchNames(0, 1); len(branches) == 0 {
showEmpty = true // it is not really empty, but there is no branch
// at the moment, other repo units like "actions" are not able to handle such case,
// so we just mark the repo as empty to prevent from displaying these units.
@@ -300,31 +309,94 @@ func handleRepoEmptyOrBroken(ctx *context.Context) {
ctx.Redirect(link)
}
+func handleRepoViewSubmodule(ctx *context.Context, submodule *git.SubModule) {
+ submoduleRepoURL, err := giturl.ParseRepositoryURL(ctx, submodule.URL)
+ if err != nil {
+ HandleGitError(ctx, "prepareToRenderDirOrFile: ParseRepositoryURL", err)
+ return
+ }
+ submoduleURL := giturl.MakeRepositoryWebLink(submoduleRepoURL)
+ if httplib.IsCurrentGiteaSiteURL(ctx, submoduleURL) {
+ ctx.RedirectToCurrentSite(submoduleURL)
+ } else {
+ // don't auto-redirect to external URL, to avoid open redirect or phishing
+ ctx.Data["NotFoundPrompt"] = submoduleURL
+ ctx.NotFound(nil)
+ }
+}
+
func prepareToRenderDirOrFile(entry *git.TreeEntry) func(ctx *context.Context) {
return func(ctx *context.Context) {
+ if entry.IsSubModule() {
+ submodule, err := ctx.Repo.Commit.GetSubModule(entry.Name())
+ if err != nil {
+ HandleGitError(ctx, "prepareToRenderDirOrFile: GetSubModule", err)
+ return
+ }
+ handleRepoViewSubmodule(ctx, submodule)
+ return
+ }
if entry.IsDir() {
prepareToRenderDirectory(ctx)
} else {
- prepareToRenderFile(ctx, entry)
+ prepareFileView(ctx, entry)
}
}
}
func handleRepoHomeFeed(ctx *context.Context) bool {
- if setting.Other.EnableFeed {
- isFeed, _, showFeedType := feed.GetFeedType(ctx.PathParam("reponame"), ctx.Req)
- if isFeed {
- switch {
- case ctx.Link == fmt.Sprintf("%s.%s", ctx.Repo.RepoLink, showFeedType):
- feed.ShowRepoFeed(ctx, ctx.Repo.Repository, showFeedType)
- case ctx.Repo.TreePath == "":
- feed.ShowBranchFeed(ctx, ctx.Repo.Repository, showFeedType)
- case ctx.Repo.TreePath != "":
- feed.ShowFileFeed(ctx, ctx.Repo.Repository, showFeedType)
- }
- return true
+ if !setting.Other.EnableFeed {
+ return false
+ }
+ isFeed, showFeedType := feed.GetFeedType(ctx.PathParam("reponame"), ctx.Req)
+ if !isFeed {
+ return false
+ }
+ if ctx.Link == fmt.Sprintf("%s.%s", ctx.Repo.RepoLink, showFeedType) {
+ feed.ShowRepoFeed(ctx, ctx.Repo.Repository, showFeedType)
+ } else if ctx.Repo.TreePath == "" {
+ feed.ShowBranchFeed(ctx, ctx.Repo.Repository, showFeedType)
+ } else {
+ feed.ShowFileFeed(ctx, ctx.Repo.Repository, showFeedType)
+ }
+ return true
+}
+
+func prepareHomeTreeSideBarSwitch(ctx *context.Context) {
+ showFileTree := true
+ if ctx.Doer != nil {
+ v, err := user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyCodeViewShowFileTree, "true")
+ if err != nil {
+ log.Error("GetUserSetting: %v", err)
+ } else {
+ showFileTree, _ = strconv.ParseBool(v)
}
}
+ ctx.Data["UserSettingCodeViewShowFileTree"] = showFileTree
+}
+
+func redirectSrcToRaw(ctx *context.Context) bool {
+ // GitHub redirects a tree path with "?raw=1" to the raw path
+ // It is useful to embed some raw contents into Markdown files,
+ // then viewing the Markdown in "src" path could embed the raw content correctly.
+ if ctx.Repo.TreePath != "" && ctx.FormBool("raw") {
+ ctx.Redirect(ctx.Repo.RepoLink + "/raw/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath))
+ return true
+ }
+ return false
+}
+
+func redirectFollowSymlink(ctx *context.Context, treePathEntry *git.TreeEntry) bool {
+ if ctx.Repo.TreePath == "" || !ctx.FormBool("follow_symlink") {
+ return false
+ }
+ if treePathEntry.IsLink() {
+ if res, err := git.EntryFollowLinks(ctx.Repo.Commit, ctx.Repo.TreePath, treePathEntry); err == nil {
+ redirect := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(res.TargetFullPath) + "?" + ctx.Req.URL.RawQuery
+ ctx.Redirect(redirect)
+ return true
+ } // else: don't handle the links we cannot resolve, so ignore the error
+ }
return false
}
@@ -333,6 +405,9 @@ func Home(ctx *context.Context) {
if handleRepoHomeFeed(ctx) {
return
}
+ if redirectSrcToRaw(ctx) {
+ return
+ }
// Check whether the repo is viewable: not in migration, and the code unit should be enabled
// Ideally the "feed" logic should be after this, but old code did so, so keep it as-is.
@@ -342,7 +417,7 @@ func Home(ctx *context.Context) {
}
title := ctx.Repo.Repository.Owner.Name + "/" + ctx.Repo.Repository.Name
- if len(ctx.Repo.Repository.Description) > 0 {
+ if ctx.Repo.Repository.Description != "" {
title += ": " + ctx.Repo.Repository.Description
}
ctx.Data["Title"] = title
@@ -355,6 +430,8 @@ func Home(ctx *context.Context) {
return
}
+ prepareHomeTreeSideBarSwitch(ctx)
+
// get the current git entry which doer user is currently looking at.
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
if err != nil {
@@ -362,6 +439,10 @@ func Home(ctx *context.Context) {
return
}
+ if redirectFollowSymlink(ctx, entry) {
+ return
+ }
+
// prepare the tree path
var treeNames, paths []string
branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
@@ -410,5 +491,39 @@ func Home(ctx *context.Context) {
}
}
- ctx.HTML(http.StatusOK, tplRepoHome)
+ if ctx.FormBool("only_content") {
+ ctx.HTML(http.StatusOK, tplRepoViewContent)
+ } else if len(treeNames) != 0 {
+ ctx.HTML(http.StatusOK, tplRepoView)
+ } else {
+ ctx.HTML(http.StatusOK, tplRepoHome)
+ }
+}
+
+func RedirectRepoTreeToSrc(ctx *context.Context) {
+ // Redirect "/owner/repo/tree/*" requests to "/owner/repo/src/*",
+ // then use the deprecated "/src/*" handler to guess the ref type and render a file list page.
+ // This is done intentionally so that Gitea's repo URL structure matches other forges (GitHub/GitLab) provide,
+ // allowing us to construct submodule URLs across forges easily.
+ // For example, when viewing a submodule, we can simply construct the link as:
+ // * "https://gitea/owner/repo/tree/{CommitID}"
+ // * "https://github/owner/repo/tree/{CommitID}"
+ // * "https://gitlab/owner/repo/tree/{CommitID}"
+ // Then no matter which forge the submodule is using, the link works.
+ redirect := ctx.Repo.RepoLink + "/src/" + ctx.PathParamRaw("*")
+ if ctx.Req.URL.RawQuery != "" {
+ redirect += "?" + ctx.Req.URL.RawQuery
+ }
+ ctx.Redirect(redirect)
+}
+
+func RedirectRepoBlobToCommit(ctx *context.Context) {
+ // redirect "/owner/repo/blob/*" requests to "/owner/repo/src/commit/*"
+ // just like GitHub: browse files of a commit by "https://github/owner/repo/blob/{CommitID}"
+ // TODO: maybe we could guess more types to redirect to the related pages in the future
+ redirect := ctx.Repo.RepoLink + "/src/commit/" + ctx.PathParamRaw("*")
+ if ctx.Req.URL.RawQuery != "" {
+ redirect += "?" + ctx.Req.URL.RawQuery
+ }
+ ctx.Redirect(redirect)
}
diff --git a/routers/web/repo/view_home_test.go b/routers/web/repo/view_home_test.go
new file mode 100644
index 0000000000..6264dba71c
--- /dev/null
+++ b/routers/web/repo/view_home_test.go
@@ -0,0 +1,32 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+ git_module "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/services/contexttest"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestViewHomeSubmoduleRedirect(t *testing.T) {
+ unittest.PrepareTestEnv(t)
+
+ ctx, _ := contexttest.MockContext(t, "/user2/repo1/src/branch/master/test-submodule")
+ submodule := &git_module.SubModule{Path: "test-submodule", URL: setting.AppURL + "user2/repo-other.git"}
+ handleRepoViewSubmodule(ctx, submodule)
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
+ assert.Equal(t, "/user2/repo-other", ctx.Resp.Header().Get("Location"))
+
+ ctx, _ = contexttest.MockContext(t, "/user2/repo1/src/branch/master/test-submodule")
+ submodule = &git_module.SubModule{Path: "test-submodule", URL: "https://other/user2/repo-other.git"}
+ handleRepoViewSubmodule(ctx, submodule)
+ // do not auto-redirect for external URLs, to avoid open redirect or phishing
+ assert.Equal(t, http.StatusNotFound, ctx.Resp.WrittenStatus())
+}
diff --git a/routers/web/repo/view_readme.go b/routers/web/repo/view_readme.go
index 48befe47f8..ba03febff3 100644
--- a/routers/web/repo/view_readme.go
+++ b/routers/web/repo/view_readme.go
@@ -32,15 +32,7 @@ import (
// entries == ctx.Repo.Commit.SubTree(ctx.Repo.TreePath).ListEntries()
//
// FIXME: There has to be a more efficient way of doing this
-func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, tryWellKnownDirs bool) (string, *git.TreeEntry, error) {
- // Create a list of extensions in priority order
- // 1. Markdown files - with and without localisation - e.g. README.en-us.md or README.md
- // 2. Txt files - e.g. README.txt
- // 3. No extension - e.g. README
- exts := append(localizedExtensions(".md", ctx.Locale.Language()), ".txt", "") // sorted by priority
- extCount := len(exts)
- readmeFiles := make([]*git.TreeEntry, extCount+1)
-
+func findReadmeFileInEntries(ctx *context.Context, parentDir string, entries []*git.TreeEntry, tryWellKnownDirs bool) (string, *git.TreeEntry, error) {
docsEntries := make([]*git.TreeEntry, 3) // (one of docs/, .gitea/ or .github/)
for _, entry := range entries {
if tryWellKnownDirs && entry.IsDir() {
@@ -62,16 +54,23 @@ func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, try
docsEntries[2] = entry
}
}
- continue
}
+ }
+
+ // Create a list of extensions in priority order
+ // 1. Markdown files - with and without localisation - e.g. README.en-us.md or README.md
+ // 2. Txt files - e.g. README.txt
+ // 3. No extension - e.g. README
+ exts := append(localizedExtensions(".md", ctx.Locale.Language()), ".txt", "") // sorted by priority
+ extCount := len(exts)
+ readmeFiles := make([]*git.TreeEntry, extCount+1)
+ for _, entry := range entries {
if i, ok := util.IsReadmeFileExtension(entry.Name(), exts...); ok {
- log.Debug("Potential readme file: %s", entry.Name())
+ fullPath := path.Join(parentDir, entry.Name())
if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].Name(), entry.Blob().Name()) {
if entry.IsLink() {
- target, err := entry.FollowLinks()
- if err != nil && !git.IsErrBadLink(err) {
- return "", nil, err
- } else if target != nil && (target.IsExecutable() || target.IsRegular()) {
+ res, err := git.EntryFollowLinks(ctx.Repo.Commit, fullPath, entry)
+ if err == nil && (res.TargetEntry.IsExecutable() || res.TargetEntry.IsRegular()) {
readmeFiles[i] = entry
}
} else {
@@ -80,6 +79,7 @@ func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, try
}
}
}
+
var readmeFile *git.TreeEntry
for _, f := range readmeFiles {
if f != nil {
@@ -103,7 +103,7 @@ func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, try
return "", nil, err
}
- subfolder, readmeFile, err := findReadmeFileInEntries(ctx, childEntries, false)
+ subfolder, readmeFile, err := findReadmeFileInEntries(ctx, parentDir, childEntries, false)
if err != nil && !git.IsErrNotExist(err) {
return "", nil, err
}
@@ -139,46 +139,52 @@ func localizedExtensions(ext, languageCode string) (localizedExts []string) {
}
func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.TreeEntry) {
- target := readmeFile
- if readmeFile != nil && readmeFile.IsLink() {
- target, _ = readmeFile.FollowLinks()
- }
- if target == nil {
- // if findReadmeFile() failed and/or gave us a broken symlink (which it shouldn't)
- // simply skip rendering the README
+ if readmeFile == nil {
return
}
+ readmeFullPath := path.Join(ctx.Repo.TreePath, subfolder, readmeFile.Name())
+ readmeTargetEntry := readmeFile
+ if readmeFile.IsLink() {
+ if res, err := git.EntryFollowLinks(ctx.Repo.Commit, readmeFullPath, readmeFile); err == nil {
+ readmeTargetEntry = res.TargetEntry
+ } else {
+ readmeTargetEntry = nil // if we cannot resolve the symlink, we cannot render the readme, ignore the error
+ }
+ }
+ if readmeTargetEntry == nil {
+ return // if no valid README entry found, skip rendering the README
+ }
+
ctx.Data["RawFileLink"] = ""
- ctx.Data["ReadmeInList"] = true
+ ctx.Data["ReadmeInList"] = path.Join(subfolder, readmeFile.Name()) // the relative path to the readme file to the current tree path
ctx.Data["ReadmeExist"] = true
ctx.Data["FileIsSymlink"] = readmeFile.IsLink()
- buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, target.Blob())
+ buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, readmeTargetEntry.Blob())
if err != nil {
ctx.ServerError("getFileReader", err)
return
}
defer dataRc.Close()
- ctx.Data["FileIsText"] = fInfo.isTextFile
- ctx.Data["FileName"] = path.Join(subfolder, readmeFile.Name())
+ ctx.Data["FileIsText"] = fInfo.st.IsText()
+ ctx.Data["FileTreePath"] = readmeFullPath
ctx.Data["FileSize"] = fInfo.fileSize
- ctx.Data["IsLFSFile"] = fInfo.isLFSFile
+ ctx.Data["IsLFSFile"] = fInfo.isLFSFile()
- if fInfo.isLFSFile {
+ if fInfo.isLFSFile() {
filenameBase64 := base64.RawURLEncoding.EncodeToString([]byte(readmeFile.Name()))
ctx.Data["RawFileLink"] = fmt.Sprintf("%s.git/info/lfs/objects/%s/%s", ctx.Repo.Repository.Link(), url.PathEscape(fInfo.lfsMeta.Oid), url.PathEscape(filenameBase64))
}
- if !fInfo.isTextFile {
+ if !fInfo.st.IsText() {
return
}
if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
// Pretend that this is a normal text file to display 'This file is too large to be shown'
ctx.Data["IsFileTooLarge"] = true
- ctx.Data["IsTextFile"] = true
return
}
@@ -190,10 +196,10 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil
rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
- CurrentTreePath: path.Join(ctx.Repo.TreePath, subfolder),
+ CurrentTreePath: path.Dir(readmeFullPath),
}).
WithMarkupType(markupType).
- WithRelativePath(path.Join(ctx.Repo.TreePath, subfolder, readmeFile.Name())) // ctx.Repo.TreePath is the directory not the Readme so we must append the Readme filename (and path).
+ WithRelativePath(readmeFullPath)
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd)
if err != nil {
@@ -212,7 +218,7 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil
ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlHTML(template.HTML(contentEscaped), ctx.Locale)
}
- if !fInfo.isLFSFile && ctx.Repo.CanEnableEditor(ctx, ctx.Doer) {
+ if !fInfo.isLFSFile() && ctx.Repo.Repository.CanEnableEditor() {
ctx.Data["CanEditReadmeFile"] = true
}
}
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/repo/webgit.go b/routers/web/repo/webgit.go
new file mode 100644
index 0000000000..5f390197e7
--- /dev/null
+++ b/routers/web/repo/webgit.go
@@ -0,0 +1,40 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/context"
+ files_service "code.gitea.io/gitea/services/repository/files"
+)
+
+func WebGitOperationCommonData(ctx *context.Context) {
+ // TODO: more places like "wiki page" and "merging a pull request or creating an auto merge merging task"
+ emails, err := user_model.GetActivatedEmailAddresses(ctx, ctx.Doer.ID)
+ if err != nil {
+ log.Error("WebGitOperationCommonData: GetActivatedEmailAddresses: %v", err)
+ }
+ if ctx.Doer.KeepEmailPrivate {
+ emails = append([]string{ctx.Doer.GetPlaceholderEmail()}, emails...)
+ }
+ ctx.Data["CommitCandidateEmails"] = emails
+ ctx.Data["CommitDefaultEmail"] = ctx.Doer.GetEmail()
+}
+
+func WebGitOperationGetCommitChosenEmailIdentity(ctx *context.Context, email string) (_ *files_service.IdentityOptions, valid bool) {
+ if ctx.Data["CommitCandidateEmails"] == nil {
+ setting.PanicInDevOrTesting("no CommitCandidateEmails in context data")
+ }
+ emails, _ := ctx.Data["CommitCandidateEmails"].([]string)
+ if email == "" {
+ return nil, true
+ }
+ if util.SliceContainsString(emails, email, true) {
+ return &files_service.IdentityOptions{GitUserEmail: email}, true
+ }
+ return nil, false
+}
diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go
index 19366c0104..69858c9692 100644
--- a/routers/web/repo/wiki.go
+++ b/routers/web/repo/wiki.go
@@ -7,14 +7,13 @@ package repo
import (
"bytes"
gocontext "context"
- "fmt"
+ "html/template"
"io"
"net/http"
"net/url"
"path/filepath"
"strings"
- git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/renderhelper"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
@@ -33,6 +32,7 @@ import (
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
+ git_service "code.gitea.io/gitea/services/git"
notify_service "code.gitea.io/gitea/services/notify"
wiki_service "code.gitea.io/gitea/services/wiki"
)
@@ -58,13 +58,13 @@ func MustEnableWiki(ctx *context.Context) {
ctx.Repo.Repository,
ctx.Repo.Permission)
}
- ctx.NotFound("MustEnableWiki", nil)
+ ctx.NotFound(nil)
return
}
- unit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalWiki)
+ repoUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalWiki)
if err == nil {
- ctx.Redirect(unit.ExternalWikiConfig().ExternalWikiURL)
+ ctx.Redirect(repoUnit.ExternalWikiConfig().ExternalWikiURL)
return
}
}
@@ -96,7 +96,7 @@ func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error)
}
func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, error) {
- wikiGitRepo, errGitRepo := gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository)
+ wikiGitRepo, errGitRepo := gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository.WikiStorageRepo())
if errGitRepo != nil {
ctx.ServerError("OpenRepository", errGitRepo)
return nil, nil, errGitRepo
@@ -105,12 +105,12 @@ func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, err
commit, errCommit := wikiGitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultWikiBranch)
if git.IsErrNotExist(errCommit) {
// if the default branch recorded in database is out of sync, then re-sync it
- gitRepoDefaultBranch, errBranch := gitrepo.GetWikiDefaultBranch(ctx, ctx.Repo.Repository)
+ gitRepoDefaultBranch, errBranch := gitrepo.GetDefaultBranch(ctx, ctx.Repo.Repository.WikiStorageRepo())
if errBranch != nil {
return wikiGitRepo, nil, errBranch
}
// update the default branch in the database
- errDb := repo_model.UpdateRepositoryCols(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, DefaultWikiBranch: gitRepoDefaultBranch}, "default_wiki_branch")
+ errDb := repo_model.UpdateRepositoryColsNoAutoTime(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, DefaultWikiBranch: gitRepoDefaultBranch}, "default_wiki_branch")
if errDb != nil {
return wikiGitRepo, nil, errDb
}
@@ -179,23 +179,17 @@ func wikiContentsByName(ctx *context.Context, commit *git.Commit, wikiName wiki_
}
func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
- wikiRepo, commit, err := findWikiRepoCommit(ctx)
+ wikiGitRepo, commit, err := findWikiRepoCommit(ctx)
if err != nil {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
if !git.IsErrNotExist(err) {
ctx.ServerError("GetBranchCommit", err)
}
return nil, nil
}
- // Get page list.
+ // get the wiki pages list.
entries, err := commit.ListEntries()
if err != nil {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
ctx.ServerError("ListEntries", err)
return nil, nil
}
@@ -209,9 +203,6 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
if repo_model.IsErrWikiInvalidFileName(err) {
continue
}
- if wikiRepo != nil {
- wikiRepo.Close()
- }
ctx.ServerError("WikiFilenameToName", err)
return nil, nil
} else if wikiName == "_Sidebar" || wikiName == "_Footer" {
@@ -250,58 +241,26 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
ctx.Redirect(util.URLJoin(ctx.Repo.RepoLink, "wiki/raw", string(pageName)))
}
if entry == nil || ctx.Written() {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
return nil, nil
}
- // get filecontent
+ // get page content
data := wikiContentsByEntry(ctx, entry)
if ctx.Written() {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
return nil, nil
}
- var sidebarContent []byte
- if !isSideBar {
- sidebarContent, _, _, _ = wikiContentsByName(ctx, commit, "_Sidebar")
- if ctx.Written() {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
- return nil, nil
- }
- } else {
- sidebarContent = data
- }
-
- var footerContent []byte
- if !isFooter {
- footerContent, _, _, _ = wikiContentsByName(ctx, commit, "_Footer")
- if ctx.Written() {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
- return nil, nil
- }
- } else {
- footerContent = data
- }
-
rctx := renderhelper.NewRenderContextRepoWiki(ctx, ctx.Repo.Repository)
- buf := &strings.Builder{}
- renderFn := func(data []byte) (escaped *charset.EscapeStatus, output string, err error) {
+ renderFn := func(data []byte) (escaped *charset.EscapeStatus, output template.HTML, err error) {
+ buf := &strings.Builder{}
markupRd, markupWr := io.Pipe()
defer markupWr.Close()
done := make(chan struct{})
go func() {
// We allow NBSP here this is rendered
escaped, _ = charset.EscapeControlReader(markupRd, buf, ctx.Locale, charset.RuneNBSP)
- output = buf.String()
+ output = template.HTML(buf.String())
buf.Reset()
close(done)
}()
@@ -312,75 +271,61 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
return escaped, output, err
}
- ctx.Data["EscapeStatus"], ctx.Data["content"], err = renderFn(data)
+ ctx.Data["EscapeStatus"], ctx.Data["WikiContentHTML"], err = renderFn(data)
if err != nil {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
ctx.ServerError("Render", err)
return nil, nil
}
if rctx.SidebarTocNode != nil {
- sb := &strings.Builder{}
- err = markdown.SpecializedMarkdown(rctx).Renderer().Render(sb, nil, rctx.SidebarTocNode)
- if err != nil {
+ sb := strings.Builder{}
+ if err = markdown.SpecializedMarkdown(rctx).Renderer().Render(&sb, nil, rctx.SidebarTocNode); err != nil {
log.Error("Failed to render wiki sidebar TOC: %v", err)
- } else {
- ctx.Data["sidebarTocContent"] = sb.String()
}
+ ctx.Data["WikiSidebarTocHTML"] = templates.SanitizeHTML(sb.String())
}
if !isSideBar {
- buf.Reset()
- ctx.Data["sidebarEscapeStatus"], ctx.Data["sidebarContent"], err = renderFn(sidebarContent)
+ sidebarContent, _, _, _ := wikiContentsByName(ctx, commit, "_Sidebar")
+ if ctx.Written() {
+ return nil, nil
+ }
+ ctx.Data["WikiSidebarEscapeStatus"], ctx.Data["WikiSidebarHTML"], err = renderFn(sidebarContent)
if err != nil {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
ctx.ServerError("Render", err)
return nil, nil
}
- ctx.Data["sidebarPresent"] = sidebarContent != nil
- } else {
- ctx.Data["sidebarPresent"] = false
}
if !isFooter {
- buf.Reset()
- ctx.Data["footerEscapeStatus"], ctx.Data["footerContent"], err = renderFn(footerContent)
+ footerContent, _, _, _ := wikiContentsByName(ctx, commit, "_Footer")
+ if ctx.Written() {
+ return nil, nil
+ }
+ ctx.Data["WikiFooterEscapeStatus"], ctx.Data["WikiFooterHTML"], err = renderFn(footerContent)
if err != nil {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
ctx.ServerError("Render", err)
return nil, nil
}
- ctx.Data["footerPresent"] = footerContent != nil
- } else {
- ctx.Data["footerPresent"] = false
}
// get commit count - wiki revisions
- commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
+ commitsCount, _ := wikiGitRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
ctx.Data["CommitCount"] = commitsCount
- return wikiRepo, entry
+ return wikiGitRepo, entry
}
func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
- wikiRepo, commit, err := findWikiRepoCommit(ctx)
+ wikiGitRepo, commit, err := findWikiRepoCommit(ctx)
if err != nil {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
if !git.IsErrNotExist(err) {
ctx.ServerError("GetBranchCommit", err)
}
return nil, nil
}
- // get requested pagename
+ // get requested page name
pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("*"))
if len(pageName) == 0 {
pageName = "Home"
@@ -395,64 +340,48 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry)
ctx.Data["Username"] = ctx.Repo.Owner.Name
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
- // lookup filename in wiki - get filecontent, gitTree entry , real filename
- data, entry, pageFilename, noEntry := wikiContentsByName(ctx, commit, pageName)
+ // lookup filename in wiki - get page content, gitTree entry , real filename
+ _, entry, pageFilename, noEntry := wikiContentsByName(ctx, commit, pageName)
if noEntry {
ctx.Redirect(ctx.Repo.RepoLink + "/wiki/?action=_pages")
}
if entry == nil || ctx.Written() {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
return nil, nil
}
- ctx.Data["content"] = string(data)
- ctx.Data["sidebarPresent"] = false
- ctx.Data["sidebarContent"] = ""
- ctx.Data["footerPresent"] = false
- ctx.Data["footerContent"] = ""
-
// get commit count - wiki revisions
- commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
+ commitsCount, _ := wikiGitRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename)
ctx.Data["CommitCount"] = commitsCount
// get page
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
// get Commit Count
- commitsHistory, err := wikiRepo.CommitsByFileAndRange(
+ commitsHistory, err := wikiGitRepo.CommitsByFileAndRange(
git.CommitsByFileAndRangeOptions{
Revision: ctx.Repo.Repository.DefaultWikiBranch,
File: pageFilename,
Page: page,
})
if err != nil {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
ctx.ServerError("CommitsByFileAndRange", err)
return nil, nil
}
- ctx.Data["Commits"] = git_model.ConvertFromGitCommit(ctx, commitsHistory, ctx.Repo.Repository)
+ ctx.Data["Commits"], err = git_service.ConvertFromGitCommit(ctx, commitsHistory, ctx.Repo.Repository)
+ if err != nil {
+ ctx.ServerError("ConvertFromGitCommit", err)
+ return nil, nil
+ }
pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5)
pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager
- return wikiRepo, entry
+ return wikiGitRepo, entry
}
func renderEditPage(ctx *context.Context) {
- wikiRepo, commit, err := findWikiRepoCommit(ctx)
- defer func() {
- if wikiRepo != nil {
- _ = wikiRepo.Close()
- }
- }()
+ _, commit, err := findWikiRepoCommit(ctx)
if err != nil {
if !git.IsErrNotExist(err) {
ctx.ServerError("GetBranchCommit", err)
@@ -460,7 +389,7 @@ func renderEditPage(ctx *context.Context) {
return
}
- // get requested pagename
+ // get requested page name
pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("*"))
if len(pageName) == 0 {
pageName = "Home"
@@ -478,23 +407,19 @@ func renderEditPage(ctx *context.Context) {
ctx.Redirect(ctx.Repo.RepoLink + "/wiki/?action=_pages")
}
if isRaw {
- ctx.Error(http.StatusForbidden, "Editing of raw wiki files is not allowed")
+ ctx.HTTPError(http.StatusForbidden, "Editing of raw wiki files is not allowed")
}
if entry == nil || ctx.Written() {
return
}
- // get filecontent
+ // get wiki page content
data := wikiContentsByEntry(ctx, entry)
if ctx.Written() {
return
}
- ctx.Data["content"] = string(data)
- ctx.Data["sidebarPresent"] = false
- ctx.Data["sidebarContent"] = ""
- ctx.Data["footerPresent"] = false
- ctx.Data["footerContent"] = ""
+ ctx.Data["WikiEditContent"] = string(data)
}
// WikiPost renders post of wiki page
@@ -502,14 +427,14 @@ func WikiPost(ctx *context.Context) {
switch ctx.FormString("action") {
case "_new":
if !ctx.Repo.CanWrite(unit.TypeWiki) {
- ctx.NotFound(ctx.Req.URL.RequestURI(), nil)
+ ctx.NotFound(nil)
return
}
NewWikiPost(ctx)
return
case "_delete":
if !ctx.Repo.CanWrite(unit.TypeWiki) {
- ctx.NotFound(ctx.Req.URL.RequestURI(), nil)
+ ctx.NotFound(nil)
return
}
DeleteWikiPagePost(ctx)
@@ -517,7 +442,7 @@ func WikiPost(ctx *context.Context) {
}
if !ctx.Repo.CanWrite(unit.TypeWiki) {
- ctx.NotFound(ctx.Req.URL.RequestURI(), nil)
+ ctx.NotFound(nil)
return
}
EditWikiPost(ctx)
@@ -536,14 +461,14 @@ func Wiki(ctx *context.Context) {
return
case "_edit":
if !ctx.Repo.CanWrite(unit.TypeWiki) {
- ctx.NotFound(ctx.Req.URL.RequestURI(), nil)
+ ctx.NotFound(nil)
return
}
EditWiki(ctx)
return
case "_new":
if !ctx.Repo.CanWrite(unit.TypeWiki) {
- ctx.NotFound(ctx.Req.URL.RequestURI(), nil)
+ ctx.NotFound(nil)
return
}
NewWiki(ctx)
@@ -556,12 +481,7 @@ func Wiki(ctx *context.Context) {
return
}
- wikiRepo, entry := renderViewPage(ctx)
- defer func() {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
- }()
+ wikiGitRepo, entry := renderViewPage(ctx)
if ctx.Written() {
return
}
@@ -574,10 +494,10 @@ func Wiki(ctx *context.Context) {
wikiPath := entry.Name()
if markup.DetectMarkupTypeByFileName(wikiPath) != markdown.MarkupName {
ext := strings.ToUpper(filepath.Ext(wikiPath))
- ctx.Data["FormatWarning"] = fmt.Sprintf("%s rendering is not supported at the moment. Rendered as Markdown.", ext)
+ ctx.Data["FormatWarning"] = ext + " rendering is not supported at the moment. Rendered as Markdown."
}
// Get last change information.
- lastCommit, err := wikiRepo.GetCommitByPath(wikiPath)
+ lastCommit, err := wikiGitRepo.GetCommitByPath(wikiPath)
if err != nil {
ctx.ServerError("GetCommitByPath", err)
return
@@ -597,13 +517,7 @@ func WikiRevision(ctx *context.Context) {
return
}
- wikiRepo, entry := renderRevisionPage(ctx)
- defer func() {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
- }()
-
+ wikiGitRepo, entry := renderRevisionPage(ctx)
if ctx.Written() {
return
}
@@ -615,7 +529,7 @@ func WikiRevision(ctx *context.Context) {
// Get last change information.
wikiPath := entry.Name()
- lastCommit, err := wikiRepo.GetCommitByPath(wikiPath)
+ lastCommit, err := wikiGitRepo.GetCommitByPath(wikiPath)
if err != nil {
ctx.ServerError("GetCommitByPath", err)
return
@@ -635,12 +549,7 @@ func WikiPages(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.wiki.pages")
ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(unit.TypeWiki) && !ctx.Repo.Repository.IsArchived
- wikiRepo, commit, err := findWikiRepoCommit(ctx)
- defer func() {
- if wikiRepo != nil {
- _ = wikiRepo.Close()
- }
- }()
+ _, commit, err := findWikiRepoCommit(ctx)
if err != nil {
ctx.Redirect(ctx.Repo.RepoLink + "/wiki")
return
@@ -694,16 +603,10 @@ func WikiPages(ctx *context.Context) {
// WikiRaw outputs raw blob requested by user (image for example)
func WikiRaw(ctx *context.Context) {
- wikiRepo, commit, err := findWikiRepoCommit(ctx)
- defer func() {
- if wikiRepo != nil {
- wikiRepo.Close()
- }
- }()
-
+ _, commit, err := findWikiRepoCommit(ctx)
if err != nil {
if git.IsErrNotExist(err) {
- ctx.NotFound("findEntryForFile", nil)
+ ctx.NotFound(nil)
return
}
ctx.ServerError("findEntryForfile", err)
@@ -733,13 +636,13 @@ func WikiRaw(ctx *context.Context) {
}
if entry != nil {
- if err = common.ServeBlob(ctx.Base, ctx.Repo.TreePath, entry.Blob(), nil); err != nil {
+ if err = common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, entry.Blob(), nil); err != nil {
ctx.ServerError("ServeBlob", err)
}
return
}
- ctx.NotFound("findEntryForFile", nil)
+ ctx.NotFound(nil)
}
// NewWiki render wiki create page
diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go
index c31f29164b..59bf6ed79b 100644
--- a/routers/web/repo/wiki_test.go
+++ b/routers/web/repo/wiki_test.go
@@ -29,7 +29,7 @@ const (
)
func wikiEntry(t *testing.T, repo *repo_model.Repository, wikiName wiki_service.WebPath) *git.TreeEntry {
- wikiRepo, err := gitrepo.OpenWikiRepository(git.DefaultContext, repo)
+ wikiRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo.WikiStorageRepo())
assert.NoError(t, err)
defer wikiRepo.Close()
commit, err := wikiRepo.GetBranchCommit("master")
@@ -71,7 +71,7 @@ func assertPagesMetas(t *testing.T, expectedNames []string, metas any) {
require.Len(t, pageMetas, len(expectedNames))
for i, pageMeta := range pageMetas {
- assert.EqualValues(t, expectedNames[i], pageMeta.Name)
+ assert.Equal(t, expectedNames[i], pageMeta.Name)
}
}
@@ -82,7 +82,7 @@ func TestWiki(t *testing.T) {
ctx.SetPathParam("*", "Home")
contexttest.LoadRepo(t, ctx, 1)
Wiki(ctx)
- assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+ assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
assert.EqualValues(t, "Home", ctx.Data["Title"])
assertPagesMetas(t, []string{"Home", "Page With Image", "Page With Spaced Name", "Unescaped File"}, ctx.Data["Pages"])
@@ -90,7 +90,7 @@ func TestWiki(t *testing.T) {
ctx.SetPathParam("*", "jpeg.jpg")
contexttest.LoadRepo(t, ctx, 1)
Wiki(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assert.Equal(t, "/user2/repo1/wiki/raw/jpeg.jpg", ctx.Resp.Header().Get("Location"))
}
@@ -100,7 +100,7 @@ func TestWikiPages(t *testing.T) {
ctx, _ := contexttest.MockContext(t, "user2/repo1/wiki/?action=_pages")
contexttest.LoadRepo(t, ctx, 1)
WikiPages(ctx)
- assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+ assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
assertPagesMetas(t, []string{"Home", "Page With Image", "Page With Spaced Name", "Unescaped File"}, ctx.Data["Pages"])
}
@@ -111,7 +111,7 @@ func TestNewWiki(t *testing.T) {
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
NewWiki(ctx)
- assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+ assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
assert.EqualValues(t, ctx.Tr("repo.wiki.new_page"), ctx.Data["Title"])
}
@@ -131,7 +131,7 @@ func TestNewWikiPost(t *testing.T) {
Message: message,
})
NewWikiPost(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assertWikiExists(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title))
assert.Equal(t, content, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)))
}
@@ -149,7 +149,7 @@ func TestNewWikiPost_ReservedName(t *testing.T) {
Message: message,
})
NewWikiPost(ctx)
- assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+ assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
assert.EqualValues(t, ctx.Tr("repo.wiki.reserved_page", "_edit"), ctx.Flash.ErrorMsg)
assertWikiNotExists(t, ctx.Repo.Repository, "_edit")
}
@@ -162,16 +162,16 @@ func TestEditWiki(t *testing.T) {
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
EditWiki(ctx)
- assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+ assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
assert.EqualValues(t, "Home", ctx.Data["Title"])
- assert.Equal(t, wikiContent(t, ctx.Repo.Repository, "Home"), ctx.Data["content"])
+ assert.Equal(t, wikiContent(t, ctx.Repo.Repository, "Home"), ctx.Data["WikiEditContent"])
ctx, _ = contexttest.MockContext(t, "user2/repo1/wiki/jpeg.jpg?action=_edit")
ctx.SetPathParam("*", "jpeg.jpg")
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
EditWiki(ctx)
- assert.EqualValues(t, http.StatusForbidden, ctx.Resp.Status())
+ assert.Equal(t, http.StatusForbidden, ctx.Resp.WrittenStatus())
}
func TestEditWikiPost(t *testing.T) {
@@ -190,7 +190,7 @@ func TestEditWikiPost(t *testing.T) {
Message: message,
})
EditWikiPost(ctx)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assertWikiExists(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title))
assert.Equal(t, content, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)))
if title != "Home" {
@@ -206,7 +206,7 @@ func TestDeleteWikiPagePost(t *testing.T) {
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1)
DeleteWikiPagePost(ctx)
- assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+ assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
assertWikiNotExists(t, ctx.Repo.Repository, "Home")
}
@@ -228,10 +228,10 @@ func TestWikiRaw(t *testing.T) {
contexttest.LoadRepo(t, ctx, 1)
WikiRaw(ctx)
if filetype == "" {
- assert.EqualValues(t, http.StatusNotFound, ctx.Resp.Status(), "filepath: %s", filepath)
+ assert.Equal(t, http.StatusNotFound, ctx.Resp.WrittenStatus(), "filepath: %s", filepath)
} else {
- assert.EqualValues(t, http.StatusOK, ctx.Resp.Status(), "filepath: %s", filepath)
- assert.EqualValues(t, filetype, ctx.Resp.Header().Get("Content-Type"), "filepath: %s", filepath)
+ assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus(), "filepath: %s", filepath)
+ assert.Equal(t, filetype, ctx.Resp.Header().Get("Content-Type"), "filepath: %s", filepath)
}
}
}
@@ -245,7 +245,12 @@ func TestDefaultWikiBranch(t *testing.T) {
assert.NoError(t, wiki_service.ChangeDefaultWikiBranch(db.DefaultContext, repoWithNoWiki, "main"))
// repo with wiki
- assert.NoError(t, repo_model.UpdateRepositoryCols(db.DefaultContext, &repo_model.Repository{ID: 1, DefaultWikiBranch: "wrong-branch"}))
+ assert.NoError(t, repo_model.UpdateRepositoryColsNoAutoTime(
+ db.DefaultContext,
+ &repo_model.Repository{ID: 1, DefaultWikiBranch: "wrong-branch"},
+ "default_wiki_branch",
+ ),
+ )
ctx, _ := contexttest.MockContext(t, "user2/repo1/wiki")
ctx.SetPathParam("*", "Home")
diff --git a/routers/web/shared/actions/runners.go b/routers/web/shared/actions/runners.go
index 6d77bdd2fa..648f8046a4 100644
--- a/routers/web/shared/actions/runners.go
+++ b/routers/web/shared/actions/runners.go
@@ -5,18 +5,127 @@ 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 {
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", 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 := max(ctx.FormInt("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 +162,26 @@ 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 := max(ctx.FormInt("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)
@@ -66,9 +191,9 @@ func RunnerDetails(ctx *context.Context, page int, runnerID, ownerID, repoID int
ctx.ServerError("LoadAttributes", err)
return
}
- if !runner.Editable(ownerID, repoID) {
+ if !runner.EditableInContext(ownerID, repoID) {
err = errors.New("no permission to edit this runner")
- ctx.NotFound("RunnerDetails", err)
+ ctx.NotFound(err)
return
}
@@ -97,18 +222,30 @@ 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)
ctx.ServerError("RunnerDetailsEditPost.GetRunnerByID", err)
return
}
- if !runner.Editable(ownerID, repoID) {
- ctx.NotFound("RunnerDetailsEditPost.Editable", util.NewPermissionDeniedErrorf("no permission to edit this runner"))
+ if !runner.EditableInContext(ownerID, repoID) {
+ ctx.NotFound(util.NewPermissionDeniedErrorf("no permission to edit this runner"))
return
}
@@ -129,10 +266,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 +285,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.EditableInContext(rCtx.OwnerID, rCtx.RepoID) {
+ ctx.NotFound(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 +320,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(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..a43c2c2690 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 {
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", 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.Data["DataMaxLength"] = actions_model.VariableDataMaxLength
+ ctx.Data["DescriptionMaxLength"] = actions_model.VariableDescriptionMaxLength
+ 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, form.Description)
if err != nil {
log.Error("CreateVariable: %v", err)
ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
@@ -36,30 +132,93 @@ 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
+ variable.Description = form.Description
- 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 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(nil)
+ return nil
+ }
+ return got[0]
}
-func DeleteVariable(ctx *context.Context, redirectURL string) {
+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/shared/issue/issue_label.go b/routers/web/shared/issue/issue_label.go
index eacea36b02..e2eeaaf0af 100644
--- a/routers/web/shared/issue/issue_label.go
+++ b/routers/web/shared/issue/issue_label.go
@@ -14,14 +14,18 @@ import (
)
// PrepareFilterIssueLabels reads the "labels" query parameter, sets `ctx.Data["Labels"]` and `ctx.Data["SelectLabels"]`
-func PrepareFilterIssueLabels(ctx *context.Context, repoID int64, owner *user_model.User) (labelIDs []int64) {
+func PrepareFilterIssueLabels(ctx *context.Context, repoID int64, owner *user_model.User) (ret struct {
+ AllLabels []*issues_model.Label
+ SelectedLabelIDs []int64
+},
+) {
// 1,-2 means including label 1 and excluding label 2
// 0 means issues with no label
// blank means labels will not be filtered for issues
selectLabels := ctx.FormString("labels")
if selectLabels != "" {
var err error
- labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ","))
+ ret.SelectedLabelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ","))
if err != nil {
ctx.Flash.Error(ctx.Tr("invalid_data", selectLabels), true)
}
@@ -32,7 +36,7 @@ func PrepareFilterIssueLabels(ctx *context.Context, repoID int64, owner *user_mo
repoLabels, err := issues_model.GetLabelsByRepoID(ctx, repoID, "", db.ListOptions{})
if err != nil {
ctx.ServerError("GetLabelsByRepoID", err)
- return nil
+ return ret
}
allLabels = append(allLabels, repoLabels...)
}
@@ -41,14 +45,14 @@ func PrepareFilterIssueLabels(ctx *context.Context, repoID int64, owner *user_mo
orgLabels, err := issues_model.GetLabelsByOrgID(ctx, owner.ID, "", db.ListOptions{})
if err != nil {
ctx.ServerError("GetLabelsByOrgID", err)
- return nil
+ return ret
}
allLabels = append(allLabels, orgLabels...)
}
// Get the exclusive scope for every label ID
- labelExclusiveScopes := make([]string, 0, len(labelIDs))
- for _, labelID := range labelIDs {
+ labelExclusiveScopes := make([]string, 0, len(ret.SelectedLabelIDs))
+ for _, labelID := range ret.SelectedLabelIDs {
foundExclusiveScope := false
for _, label := range allLabels {
if label.ID == labelID || label.ID == -labelID {
@@ -63,9 +67,10 @@ func PrepareFilterIssueLabels(ctx *context.Context, repoID int64, owner *user_mo
}
for _, l := range allLabels {
- l.LoadSelectedLabelsAfterClick(labelIDs, labelExclusiveScopes)
+ l.LoadSelectedLabelsAfterClick(ret.SelectedLabelIDs, labelExclusiveScopes)
}
ctx.Data["Labels"] = allLabels
ctx.Data["SelectLabels"] = selectLabels
- return labelIDs
+ ret.AllLabels = allLabels
+ return ret
}
diff --git a/routers/web/shared/label/label.go b/routers/web/shared/label/label.go
new file mode 100644
index 0000000000..6968a318c4
--- /dev/null
+++ b/routers/web/shared/label/label.go
@@ -0,0 +1,26 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package label
+
+import (
+ "code.gitea.io/gitea/modules/label"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
+ "code.gitea.io/gitea/services/forms"
+)
+
+func GetLabelEditForm(ctx *context.Context) *forms.CreateLabelForm {
+ form := web.GetForm(ctx).(*forms.CreateLabelForm)
+ if ctx.HasError() {
+ ctx.JSONError(ctx.Data["ErrorMsg"].(string))
+ return nil
+ }
+ var err error
+ form.Color, err = label.NormalizeColor(form.Color)
+ if err != nil {
+ ctx.JSONError(ctx.Tr("repo.issues.label_color_invalid"))
+ return nil
+ }
+ return form
+}
diff --git a/routers/web/shared/packages/packages.go b/routers/web/shared/packages/packages.go
index a1bcb09850..a18dedf89c 100644
--- a/routers/web/shared/packages/packages.go
+++ b/routers/web/shared/packages/packages.go
@@ -8,7 +8,6 @@ import (
"net/http"
"time"
- "code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
@@ -159,12 +158,18 @@ func SetRulePreviewContext(ctx *context.Context, owner *user_model.User) {
PackageID: p.ID,
IsInternal: optional.Some(false),
Sort: packages_model.SortCreatedDesc,
- Paginator: db.NewAbsoluteListOptions(pcr.KeepCount, 200),
})
if err != nil {
ctx.ServerError("SearchVersions", err)
return
}
+ if pcr.KeepCount > 0 {
+ if pcr.KeepCount < len(pvs) {
+ pvs = pvs[pcr.KeepCount:]
+ } else {
+ pvs = nil
+ }
+ }
for _, pv := range pvs {
if skip, err := container_service.ShouldBeSkipped(ctx, pcr, p, pv); err != nil {
ctx.ServerError("ShouldBeSkipped", err)
@@ -177,7 +182,6 @@ func SetRulePreviewContext(ctx *context.Context, owner *user_model.User) {
if pcr.MatchFullName {
toMatch = p.LowerName + "/" + pv.LowerVersion
}
-
if pcr.KeepPatternMatcher != nil && pcr.KeepPatternMatcher.MatchString(toMatch) {
continue
}
@@ -210,7 +214,7 @@ func getCleanupRuleByContext(ctx *context.Context, owner *user_model.User) *pack
pcr, err := packages_model.GetCleanupRuleByID(ctx, id)
if err != nil {
if err == packages_model.ErrPackageCleanupRuleNotExist {
- ctx.NotFound("", err)
+ ctx.NotFound(err)
} else {
ctx.ServerError("GetCleanupRuleByID", err)
}
@@ -221,7 +225,7 @@ func getCleanupRuleByContext(ctx *context.Context, owner *user_model.User) *pack
return pcr
}
- ctx.NotFound("", fmt.Errorf("PackageCleanupRule[%v] not associated to owner %v", id, owner))
+ ctx.NotFound(fmt.Errorf("PackageCleanupRule[%v] not associated to owner %v", id, owner))
return nil
}
diff --git a/routers/web/shared/project/column.go b/routers/web/shared/project/column.go
index 141c80716f..b6ffaea7f8 100644
--- a/routers/web/shared/project/column.go
+++ b/routers/web/shared/project/column.go
@@ -17,7 +17,7 @@ func MoveColumns(ctx *context.Context) {
return
}
if !project.CanBeAccessedByOwnerRepo(ctx.ContextUser.ID, ctx.Repo.Repository) {
- ctx.NotFound("CanBeAccessedByOwnerRepo", nil)
+ ctx.NotFound(nil)
return
}
diff --git a/routers/web/shared/secrets/secrets.go b/routers/web/shared/secrets/secrets.go
index 3bd421f86a..29f4e9520d 100644
--- a/routers/web/shared/secrets/secrets.go
+++ b/routers/web/shared/secrets/secrets.go
@@ -22,19 +22,21 @@ func SetSecretsContext(ctx *context.Context, ownerID, repoID int64) {
}
ctx.Data["Secrets"] = secrets
+ ctx.Data["DataMaxLength"] = secret_model.SecretDataMaxLength
+ ctx.Data["DescriptionMaxLength"] = secret_model.SecretDescriptionMaxLength
}
func PerformSecretsPost(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
form := web.GetForm(ctx).(*forms.AddSecretForm)
- s, _, err := secret_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, util.ReserveLineBreakForTextarea(form.Data))
+ s, _, err := secret_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, util.ReserveLineBreakForTextarea(form.Data), form.Description)
if err != nil {
log.Error("CreateOrUpdateSecret failed: %v", err)
- ctx.JSONError(ctx.Tr("secrets.creation.failed"))
+ ctx.JSONError(ctx.Tr("secrets.save_failed"))
return
}
- ctx.Flash.Success(ctx.Tr("secrets.creation.success", s.Name))
+ ctx.Flash.Success(ctx.Tr("secrets.save_success", s.Name))
ctx.JSONRedirect(redirectURL)
}
diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go
index 62b146c7f3..2bd0abc4c0 100644
--- a/routers/web/shared/user/header.go
+++ b/routers/web/shared/user/header.go
@@ -24,19 +24,8 @@ import (
"code.gitea.io/gitea/services/context"
)
-// prepareContextForCommonProfile store some common data into context data for user's profile related pages (including the nav menu)
-// It is designed to be fast and safe to be called multiple times in one request
-func prepareContextForCommonProfile(ctx *context.Context) {
- ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled
- ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
- ctx.Data["EnableFeed"] = setting.Other.EnableFeed
- ctx.Data["FeedURL"] = ctx.ContextUser.HomeLink()
-}
-
-// PrepareContextForProfileBigAvatar set the context for big avatar view on the profile page
-func PrepareContextForProfileBigAvatar(ctx *context.Context) {
- prepareContextForCommonProfile(ctx)
-
+// prepareContextForProfileBigAvatar set the context for big avatar view on the profile page
+func prepareContextForProfileBigAvatar(ctx *context.Context) {
ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
ctx.Data["ShowUserEmail"] = setting.UI.ShowUserEmail && ctx.ContextUser.Email != "" && ctx.IsSigned && !ctx.ContextUser.KeepEmailPrivate
if setting.Service.UserLocationMapURL != "" {
@@ -58,13 +47,12 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) {
ctx.Data["RenderedDescription"] = content
}
- showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
orgs, err := db.Find[organization.Organization](ctx, organization.FindOrgOptions{
- UserID: ctx.ContextUser.ID,
- IncludePrivate: showPrivate,
+ UserID: ctx.ContextUser.ID,
+ IncludeVisibility: organization.DoerViewOtherVisibility(ctx.Doer, ctx.ContextUser),
ListOptions: db.ListOptions{
Page: 1,
- // query one more results (without a separate counting) to see whether we need to add the "show more orgs" link
+ // query one more result (without a separate counting) to see whether we need to add the "show more orgs" link
PageSize: setting.UI.User.OrgPagingNum + 1,
},
})
@@ -138,17 +126,45 @@ func FindOwnerProfileReadme(ctx *context.Context, doer *user_model.User, optProf
return profileDbRepo, profileReadmeBlob
}
-func RenderUserHeader(ctx *context.Context) {
- prepareContextForCommonProfile(ctx)
-
- _, profileReadmeBlob := FindOwnerProfileReadme(ctx, ctx.Doer)
- ctx.Data["HasUserProfileReadme"] = profileReadmeBlob != nil
+type PrepareOwnerHeaderResult struct {
+ ProfilePublicRepo *repo_model.Repository
+ ProfilePublicReadmeBlob *git.Blob
+ ProfilePrivateRepo *repo_model.Repository
+ ProfilePrivateReadmeBlob *git.Blob
+ HasOrgProfileReadme bool
}
-func LoadHeaderCount(ctx *context.Context) error {
- prepareContextForCommonProfile(ctx)
+const (
+ RepoNameProfilePrivate = ".profile-private"
+ RepoNameProfile = ".profile"
+)
+
+func RenderUserOrgHeader(ctx *context.Context) (result *PrepareOwnerHeaderResult, err error) {
+ ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled
+ ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
+ ctx.Data["EnableFeed"] = setting.Other.EnableFeed
+ ctx.Data["FeedURL"] = ctx.ContextUser.HomeLink()
+
+ if err := loadHeaderCount(ctx); err != nil {
+ return nil, err
+ }
- repoCount, err := repo_model.CountRepository(ctx, &repo_model.SearchRepoOptions{
+ result = &PrepareOwnerHeaderResult{}
+ if ctx.ContextUser.IsOrganization() {
+ result.ProfilePublicRepo, result.ProfilePublicReadmeBlob = FindOwnerProfileReadme(ctx, ctx.Doer)
+ result.ProfilePrivateRepo, result.ProfilePrivateReadmeBlob = FindOwnerProfileReadme(ctx, ctx.Doer, RepoNameProfilePrivate)
+ result.HasOrgProfileReadme = result.ProfilePublicReadmeBlob != nil || result.ProfilePrivateReadmeBlob != nil
+ ctx.Data["HasOrgProfileReadme"] = result.HasOrgProfileReadme // many pages need it to show the "overview" tab
+ } else {
+ _, profileReadmeBlob := FindOwnerProfileReadme(ctx, ctx.Doer)
+ ctx.Data["HasUserProfileReadme"] = profileReadmeBlob != nil
+ prepareContextForProfileBigAvatar(ctx)
+ }
+ return result, nil
+}
+
+func loadHeaderCount(ctx *context.Context) error {
+ repoCount, err := repo_model.CountRepository(ctx, repo_model.SearchRepoOptions{
Actor: ctx.Doer,
OwnerID: ctx.ContextUser.ID,
Private: ctx.IsSigned,
@@ -178,29 +194,3 @@ func LoadHeaderCount(ctx *context.Context) error {
return nil
}
-
-const (
- RepoNameProfilePrivate = ".profile-private"
- RepoNameProfile = ".profile"
-)
-
-type PrepareOrgHeaderResult struct {
- ProfilePublicRepo *repo_model.Repository
- ProfilePublicReadmeBlob *git.Blob
- ProfilePrivateRepo *repo_model.Repository
- ProfilePrivateReadmeBlob *git.Blob
- HasOrgProfileReadme bool
-}
-
-func PrepareOrgHeader(ctx *context.Context) (result *PrepareOrgHeaderResult, err error) {
- if err = LoadHeaderCount(ctx); err != nil {
- return nil, err
- }
-
- result = &PrepareOrgHeaderResult{}
- result.ProfilePublicRepo, result.ProfilePublicReadmeBlob = FindOwnerProfileReadme(ctx, ctx.Doer)
- result.ProfilePrivateRepo, result.ProfilePrivateReadmeBlob = FindOwnerProfileReadme(ctx, ctx.Doer, RepoNameProfilePrivate)
- result.HasOrgProfileReadme = result.ProfilePublicReadmeBlob != nil || result.ProfilePrivateReadmeBlob != nil
- ctx.Data["HasOrgProfileReadme"] = result.HasOrgProfileReadme // many pages need it to show the "overview" tab
- return result, nil
-}
diff --git a/routers/web/shared/user/helper.go b/routers/web/shared/user/helper.go
index b82181a1df..3fc39fd3ab 100644
--- a/routers/web/shared/user/helper.go
+++ b/routers/web/shared/user/helper.go
@@ -8,9 +8,7 @@ import (
"slices"
"strconv"
- "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/optional"
)
func MakeSelfOnTop(doer *user.User, users []*user.User) []*user.User {
@@ -34,19 +32,20 @@ func MakeSelfOnTop(doer *user.User, users []*user.User) []*user.User {
// So it's better to make it work like GitHub: users could input username directly.
// Since it only converts the username to ID directly and is only used internally (to search issues), so no permission check is needed.
// Return values:
-// * nil: no filter
-// * some(id): match the id, the id could be -1 to match the issues without assignee
-// * some(NonExistingID): match no issue (due to the user doesn't exist)
-func GetFilterUserIDByName(ctx context.Context, name string) optional.Option[int64] {
+// * "": no filter
+// * "{the-id}": match the id
+// * "(none)": match no issue (due to the user doesn't exist)
+func GetFilterUserIDByName(ctx context.Context, name string) string {
if name == "" {
- return optional.None[int64]()
+ return ""
}
u, err := user.GetUserByName(ctx, name)
if err != nil {
if id, err := strconv.ParseInt(name, 10, 64); err == nil {
- return optional.Some(id)
+ return strconv.FormatInt(id, 10)
}
- return optional.Some(db.NonExistingID)
+ // The "(none)" is for internal usage only: when doer tries to search non-existing user, use "(none)" to return empty result.
+ return "(none)"
}
- return optional.Some(u.ID)
+ return strconv.FormatInt(u.ID, 10)
}
diff --git a/routers/web/user/avatar.go b/routers/web/user/avatar.go
index f77bd602b3..6d3179bc48 100644
--- a/routers/web/user/avatar.go
+++ b/routers/web/user/avatar.go
@@ -4,7 +4,6 @@
package user
import (
- "strings"
"time"
"code.gitea.io/gitea/models/avatars"
@@ -17,31 +16,22 @@ func cacheableRedirect(ctx *context.Context, location string) {
// here we should not use `setting.StaticCacheTime`, it is pretty long (default: 6 hours)
// we must make sure the redirection cache time is short enough, otherwise a user won't see the updated avatar in 6 hours
// it's OK to make the cache time short, it is only a redirection, and doesn't cost much to make a new request
- httpcache.SetCacheControlInHeader(ctx.Resp.Header(), 5*time.Minute)
+ httpcache.SetCacheControlInHeader(ctx.Resp.Header(), &httpcache.CacheControlOptions{MaxAge: 5 * time.Minute})
ctx.Redirect(location)
}
-// AvatarByUserName redirect browser to user avatar of requested size
-func AvatarByUserName(ctx *context.Context) {
- userName := ctx.PathParam("username")
- size := int(ctx.PathParamInt64("size"))
-
- var user *user_model.User
- if strings.ToLower(userName) != user_model.GhostUserLowerName {
+// AvatarByUsernameSize redirect browser to user avatar of requested size
+func AvatarByUsernameSize(ctx *context.Context) {
+ username := ctx.PathParam("username")
+ user := user_model.GetSystemUserByName(username)
+ if user == nil {
var err error
- if user, err = user_model.GetUserByName(ctx, userName); err != nil {
- if user_model.IsErrUserNotExist(err) {
- ctx.NotFound("GetUserByName", err)
- return
- }
- ctx.ServerError("Invalid user: "+userName, err)
+ if user, err = user_model.GetUserByName(ctx, username); err != nil {
+ ctx.NotFoundOrServerError("GetUserByName", user_model.IsErrUserNotExist, err)
return
}
- } else {
- user = user_model.NewGhostUser()
}
-
- cacheableRedirect(ctx, user.AvatarLinkWithSize(ctx, size))
+ cacheableRedirect(ctx, user.AvatarLinkWithSize(ctx, int(ctx.PathParamInt64("size"))))
}
// AvatarByEmailHash redirects the browser to the email avatar link
diff --git a/routers/web/user/code.go b/routers/web/user/code.go
index 665ce1a6a6..11579c40a6 100644
--- a/routers/web/user/code.go
+++ b/routers/web/user/code.go
@@ -5,6 +5,7 @@ package user
import (
"net/http"
+ "slices"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
@@ -26,11 +27,8 @@ func CodeSearch(ctx *context.Context) {
ctx.Redirect(ctx.ContextUser.HomeLink())
return
}
- shared_user.PrepareContextForProfileBigAvatar(ctx)
- shared_user.RenderUserHeader(ctx)
-
- if err := shared_user.LoadHeaderCount(ctx); err != nil {
- ctx.ServerError("LoadHeaderCount", err)
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
return
}
@@ -68,10 +66,10 @@ func CodeSearch(ctx *context.Context) {
if len(repoIDs) > 0 {
total, searchResults, searchResultLanguages, err = code_indexer.PerformSearch(ctx, &code_indexer.SearchOptions{
- RepoIDs: repoIDs,
- Keyword: prepareSearch.Keyword,
- IsKeywordFuzzy: prepareSearch.IsFuzzy,
- Language: prepareSearch.Language,
+ RepoIDs: repoIDs,
+ Keyword: prepareSearch.Keyword,
+ SearchMode: prepareSearch.SearchMode,
+ Language: prepareSearch.Language,
Paginator: &db.ListOptions{
Page: page,
PageSize: setting.UI.RepoSearchPagingNum,
@@ -89,14 +87,7 @@ func CodeSearch(ctx *context.Context) {
loadRepoIDs := make([]int64, 0, len(searchResults))
for _, result := range searchResults {
- var find bool
- for _, id := range loadRepoIDs {
- if id == result.RepoID {
- find = true
- break
- }
- }
- if !find {
+ if !slices.Contains(loadRepoIDs, result.RepoID) {
loadRepoIDs = append(loadRepoIDs, result.RepoID)
}
}
diff --git a/routers/web/user/home.go b/routers/web/user/home.go
index c79648a455..b53a3daedb 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -26,6 +26,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/container"
+ "code.gitea.io/gitea/modules/indexer"
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup/markdown"
@@ -41,8 +42,8 @@ import (
issue_service "code.gitea.io/gitea/services/issue"
pull_service "code.gitea.io/gitea/services/pull"
- "github.com/keybase/go-crypto/openpgp"
- "github.com/keybase/go-crypto/openpgp/armor"
+ "github.com/ProtonMail/go-crypto/openpgp"
+ "github.com/ProtonMail/go-crypto/openpgp/armor"
"xorm.io/builder"
)
@@ -118,7 +119,7 @@ func Dashboard(ctx *context.Context) {
ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data)
}
- feeds, count, err := feed_service.GetFeeds(ctx, activities_model.GetFeedsOptions{
+ feeds, count, err := feed_service.GetFeedsForDashboard(ctx, activities_model.GetFeedsOptions{
RequestedUser: ctxUser,
RequestedTeam: ctx.Org.Team,
Actor: ctx.Doer,
@@ -136,11 +137,10 @@ func Dashboard(ctx *context.Context) {
return
}
- ctx.Data["Feeds"] = feeds
-
- pager := context.NewPagination(int(count), setting.UI.FeedPagingNum, page, 5)
+ pager := context.NewPagination(count, setting.UI.FeedPagingNum, page, 5).WithCurRows(len(feeds))
pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager
+ ctx.Data["Feeds"] = feeds
ctx.HTML(http.StatusOK, tplDashboard)
}
@@ -176,7 +176,7 @@ func Milestones(ctx *context.Context) {
}
var (
- userRepoCond = repo_model.SearchRepositoryCondition(&repoOpts) // all repo condition user could visit
+ userRepoCond = repo_model.SearchRepositoryCondition(repoOpts) // all repo condition user could visit
repoCond = userRepoCond
repoIDs []int64
@@ -197,7 +197,7 @@ func Milestones(ctx *context.Context) {
reposQuery = reposQuery[1 : len(reposQuery)-1]
// for each ID (delimiter ",") add to int to repoIDs
- for _, rID := range strings.Split(reposQuery, ",") {
+ for rID := range strings.SplitSeq(reposQuery, ",") {
// Ensure nonempty string entries
if rID != "" && rID != "0" {
rIDint64, err := strconv.ParseInt(rID, 10, 64)
@@ -242,7 +242,7 @@ func Milestones(ctx *context.Context) {
return
}
- showRepos, _, err := repo_model.SearchRepositoryByCondition(ctx, &repoOpts, userRepoCond, false)
+ showRepos, _, err := repo_model.SearchRepositoryByCondition(ctx, repoOpts, userRepoCond, false)
if err != nil {
ctx.ServerError("SearchRepositoryByCondition", err)
return
@@ -417,7 +417,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
IsPull: optional.Some(isPullList),
SortType: sortType,
IsArchived: optional.Some(false),
- User: ctx.Doer,
+ Doer: ctx.Doer,
}
// --------------------------------------------------------------------------
// Build opts (IssuesOptions), which contains filter information.
@@ -429,7 +429,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
// Get repository IDs where User/Org/Team has access.
if ctx.Org != nil && ctx.Org.Organization != nil {
- opts.Org = ctx.Org.Organization
+ opts.Owner = ctx.Org.Organization.AsUser()
opts.Team = ctx.Org.Team
issue.PrepareFilterIssueLabels(ctx, 0, ctx.Org.Organization.AsUser())
@@ -447,7 +447,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
ctx.Data["FilterAssigneeUsername"] = assigneeUsername
opts.AssigneeID = user.GetFilterUserIDByName(ctx, assigneeUsername)
- isFuzzy := ctx.FormBool("fuzzy")
+ searchMode := ctx.FormString("search_mode")
// Search all repositories which
//
@@ -461,7 +461,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
// As team:
// - Team org's owns the repository.
// - Team has read permission to repository.
- repoOpts := &repo_model.SearchRepoOptions{
+ repoOpts := repo_model.SearchRepoOptions{
Actor: ctx.Doer,
OwnerID: ctxUser.ID,
Private: true,
@@ -500,9 +500,9 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
case issues_model.FilterModeAll:
case issues_model.FilterModeYourRepositories:
case issues_model.FilterModeAssign:
- opts.AssigneeID = optional.Some(ctx.Doer.ID)
+ opts.AssigneeID = strconv.FormatInt(ctx.Doer.ID, 10)
case issues_model.FilterModeCreate:
- opts.PosterID = optional.Some(ctx.Doer.ID)
+ opts.PosterID = strconv.FormatInt(ctx.Doer.ID, 10)
case issues_model.FilterModeMention:
opts.MentionedID = ctx.Doer.ID
case issues_model.FilterModeReviewRequested:
@@ -520,10 +520,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
opts.IsClosed = optional.Some(isShowClosed)
// Make sure page number is at least 1. Will be posted to ctx.Data.
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
opts.Paginator = &db.ListOptions{
Page: page,
PageSize: setting.UI.IssuePagingNum,
@@ -549,7 +546,9 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
var issues issues_model.IssueList
{
issueIDs, _, err := issue_indexer.SearchIssues(ctx, issue_indexer.ToSearchOptions(keyword, opts).Copy(
- func(o *issue_indexer.SearchOptions) { o.IsFuzzyKeyword = isFuzzy },
+ func(o *issue_indexer.SearchOptions) {
+ o.SearchMode = indexer.SearchModeType(searchMode)
+ },
))
if err != nil {
ctx.ServerError("issueIDsFromSearch", err)
@@ -576,17 +575,9 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
// -------------------------------
// Fill stats to post to ctx.Data.
// -------------------------------
- issueStats, err := getUserIssueStats(ctx, filterMode, issue_indexer.ToSearchOptions(keyword, opts).Copy(
+ issueStats, err := getUserIssueStats(ctx, ctxUser, filterMode, issue_indexer.ToSearchOptions(keyword, opts).Copy(
func(o *issue_indexer.SearchOptions) {
- o.IsFuzzyKeyword = isFuzzy
- // If the doer is the same as the context user, which means the doer is viewing his own dashboard,
- // it's not enough to show the repos that the doer owns or has been explicitly granted access to,
- // because the doer may create issues or be mentioned in any public repo.
- // So we need search issues in all public repos.
- o.AllPublic = ctx.Doer.ID == ctxUser.ID
- o.MentionID = nil
- o.ReviewRequestedID = nil
- o.ReviewedID = nil
+ o.SearchMode = indexer.SearchModeType(searchMode)
},
))
if err != nil {
@@ -623,9 +614,10 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
return 0
}
reviewTyp := issues_model.ReviewTypeApprove
- if typ == "reject" {
+ switch typ {
+ case "reject":
reviewTyp = issues_model.ReviewTypeReject
- } else if typ == "waiting" {
+ case "waiting":
reviewTyp = issues_model.ReviewTypeRequest
}
for _, count := range counts {
@@ -641,7 +633,8 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
ctx.Data["ViewType"] = viewType
ctx.Data["SortType"] = sortType
ctx.Data["IsShowClosed"] = isShowClosed
- ctx.Data["IsFuzzy"] = isFuzzy
+ ctx.Data["SearchModes"] = issue_indexer.SupportedSearchModes()
+ ctx.Data["SelectedSearchMode"] = ctx.FormTrim("search_mode")
if isShowClosed {
ctx.Data["State"] = "closed"
@@ -703,7 +696,7 @@ func ShowGPGKeys(ctx *context.Context) {
headers := make(map[string]string)
if len(failedEntitiesID) > 0 { // If some key need re-import to be exported
- headers["Note"] = fmt.Sprintf("The keys with the following IDs couldn't be exported and need to be reuploaded %s", strings.Join(failedEntitiesID, ", "))
+ headers["Note"] = "The keys with the following IDs couldn't be exported and need to be reuploaded " + strings.Join(failedEntitiesID, ", ")
} else if len(entities) == 0 {
headers["Note"] = "This user hasn't uploaded any GPG keys."
}
@@ -732,7 +725,7 @@ func UsernameSubRoute(ctx *context.Context) {
// check view permissions
if !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
- ctx.NotFound("user", fmt.Errorf("%s", ctx.ContextUser.Name))
+ ctx.NotFound(fmt.Errorf("%s", ctx.ContextUser.Name))
return false
}
return true
@@ -740,7 +733,7 @@ func UsernameSubRoute(ctx *context.Context) {
switch {
case strings.HasSuffix(username, ".png"):
if reloadParam(".png") {
- AvatarByUserName(ctx)
+ AvatarByUsernameSize(ctx)
}
case strings.HasSuffix(username, ".keys"):
if reloadParam(".keys") {
@@ -752,7 +745,7 @@ func UsernameSubRoute(ctx *context.Context) {
}
case strings.HasSuffix(username, ".rss"):
if !setting.Other.EnableFeed {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
if reloadParam(".rss") {
@@ -760,7 +753,7 @@ func UsernameSubRoute(ctx *context.Context) {
}
case strings.HasSuffix(username, ".atom"):
if !setting.Other.EnableFeed {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
if reloadParam(".atom") {
@@ -775,10 +768,19 @@ func UsernameSubRoute(ctx *context.Context) {
}
}
-func getUserIssueStats(ctx *context.Context, filterMode int, opts *issue_indexer.SearchOptions) (ret *issues_model.IssueStats, err error) {
+func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMode int, opts *issue_indexer.SearchOptions) (ret *issues_model.IssueStats, err error) {
ret = &issues_model.IssueStats{}
doerID := ctx.Doer.ID
+ opts = opts.Copy(func(o *issue_indexer.SearchOptions) {
+ // If the doer is the same as the context user, which means the doer is viewing his own dashboard,
+ // it's not enough to show the repos that the doer owns or has been explicitly granted access to,
+ // because the doer may create issues or be mentioned in any public repo.
+ // So we need search issues in all public repos.
+ o.AllPublic = doerID == ctxUser.ID
+ })
+
+ // Open/Closed are for the tabs of the issue list
{
openClosedOpts := opts.Copy()
switch filterMode {
@@ -787,9 +789,9 @@ func getUserIssueStats(ctx *context.Context, filterMode int, opts *issue_indexer
case issues_model.FilterModeYourRepositories:
openClosedOpts.AllPublic = false
case issues_model.FilterModeAssign:
- openClosedOpts.AssigneeID = optional.Some(doerID)
+ openClosedOpts.AssigneeID = strconv.FormatInt(doerID, 10)
case issues_model.FilterModeCreate:
- openClosedOpts.PosterID = optional.Some(doerID)
+ openClosedOpts.PosterID = strconv.FormatInt(doerID, 10)
case issues_model.FilterModeMention:
openClosedOpts.MentionID = optional.Some(doerID)
case issues_model.FilterModeReviewRequested:
@@ -809,15 +811,24 @@ func getUserIssueStats(ctx *context.Context, filterMode int, opts *issue_indexer
}
}
+ // Below stats are for the left sidebar
+ opts = opts.Copy(func(o *issue_indexer.SearchOptions) {
+ o.AssigneeID = ""
+ o.PosterID = ""
+ o.MentionID = nil
+ o.ReviewRequestedID = nil
+ o.ReviewedID = nil
+ })
+
ret.YourRepositoriesCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.AllPublic = false }))
if err != nil {
return nil, err
}
- ret.AssignCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.AssigneeID = optional.Some(doerID) }))
+ ret.AssignCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.AssigneeID = strconv.FormatInt(doerID, 10) }))
if err != nil {
return nil, err
}
- ret.CreateCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.PosterID = optional.Some(doerID) }))
+ ret.CreateCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.PosterID = strconv.FormatInt(doerID, 10) }))
if err != nil {
return nil, err
}
diff --git a/routers/web/user/home_test.go b/routers/web/user/home_test.go
index 51246551ea..68ad79b11e 100644
--- a/routers/web/user/home_test.go
+++ b/routers/web/user/home_test.go
@@ -28,7 +28,7 @@ func TestArchivedIssues(t *testing.T) {
ctx.Req.Form.Set("state", "open")
// Assume: User 30 has access to two Repos with Issues, one of the Repos being archived.
- repos, _, _ := repo_model.GetUserRepositories(db.DefaultContext, &repo_model.SearchRepoOptions{Actor: ctx.Doer})
+ repos, _, _ := repo_model.GetUserRepositories(db.DefaultContext, repo_model.SearchRepoOptions{Actor: ctx.Doer})
assert.Len(t, repos, 3)
IsArchived := make(map[int64]bool)
NumIssues := make(map[int64]int)
@@ -37,15 +37,15 @@ func TestArchivedIssues(t *testing.T) {
NumIssues[repo.ID] = repo.NumIssues
}
assert.False(t, IsArchived[50])
- assert.EqualValues(t, 1, NumIssues[50])
+ assert.Equal(t, 1, NumIssues[50])
assert.True(t, IsArchived[51])
- assert.EqualValues(t, 1, NumIssues[51])
+ assert.Equal(t, 1, NumIssues[51])
// Act
Issues(ctx)
// Assert: One Issue (ID 30) from one Repo (ID 50) is retrieved, while nothing from archived Repo 51 is retrieved
- assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+ assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
assert.Len(t, ctx.Data["Issues"], 1)
}
@@ -58,7 +58,7 @@ func TestIssues(t *testing.T) {
contexttest.LoadUser(t, ctx, 2)
ctx.Req.Form.Set("state", "closed")
Issues(ctx)
- assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+ assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
assert.EqualValues(t, true, ctx.Data["IsShowClosed"])
assert.Len(t, ctx.Data["Issues"], 1)
@@ -72,7 +72,7 @@ func TestPulls(t *testing.T) {
contexttest.LoadUser(t, ctx, 2)
ctx.Req.Form.Set("state", "open")
Pulls(ctx)
- assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+ assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
assert.Len(t, ctx.Data["Issues"], 5)
}
@@ -87,7 +87,7 @@ func TestMilestones(t *testing.T) {
ctx.Req.Form.Set("state", "closed")
ctx.Req.Form.Set("sort", "furthestduedate")
Milestones(ctx)
- assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+ assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
assert.EqualValues(t, map[int64]int64{1: 1}, ctx.Data["Counts"])
assert.EqualValues(t, true, ctx.Data["IsShowClosed"])
assert.EqualValues(t, "furthestduedate", ctx.Data["SortType"])
@@ -107,7 +107,7 @@ func TestMilestonesForSpecificRepo(t *testing.T) {
ctx.Req.Form.Set("state", "closed")
ctx.Req.Form.Set("sort", "furthestduedate")
Milestones(ctx)
- assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
+ assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
assert.EqualValues(t, map[int64]int64{1: 1}, ctx.Data["Counts"])
assert.EqualValues(t, true, ctx.Data["IsShowClosed"])
assert.EqualValues(t, "furthestduedate", ctx.Data["SortType"])
diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go
index 1c91ff6364..610a9b8076 100644
--- a/routers/web/user/notification.go
+++ b/routers/web/user/notification.go
@@ -4,7 +4,6 @@
package user
import (
- goctx "context"
"errors"
"fmt"
"net/http"
@@ -35,32 +34,6 @@ const (
tplNotificationSubscriptions templates.TplName = "user/notification/notification_subscriptions"
)
-// GetNotificationCount is the middleware that sets the notification count in the context
-func GetNotificationCount(ctx *context.Context) {
- if strings.HasPrefix(ctx.Req.URL.Path, "/api") {
- return
- }
-
- if !ctx.IsSigned {
- return
- }
-
- ctx.Data["NotificationUnreadCount"] = func() int64 {
- count, err := db.Count[activities_model.Notification](ctx, activities_model.FindNotificationOptions{
- UserID: ctx.Doer.ID,
- Status: []activities_model.NotificationStatus{activities_model.NotificationStatusUnread},
- })
- if err != nil {
- if err != goctx.Canceled {
- log.Error("Unable to GetNotificationCount for user:%-v: %v", ctx.Doer, err)
- }
- return -1
- }
-
- return count
- }
-}
-
// Notifications is the notifications page
func Notifications(ctx *context.Context) {
getNotifications(ctx)
@@ -230,10 +203,7 @@ func NotificationPurgePost(ctx *context.Context) {
// NotificationSubscriptions returns the list of subscribed issues
func NotificationSubscriptions(ctx *context.Context) {
- page := ctx.FormInt("page")
- if page < 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
sortType := ctx.FormString("sort")
ctx.Data["SortType"] = sortType
@@ -314,16 +284,8 @@ func NotificationSubscriptions(ctx *context.Context) {
ctx.Data["CommitLastStatus"] = lastStatus
ctx.Data["CommitStatuses"] = commitStatuses
ctx.Data["Issues"] = issues
-
ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] = issue_service.GetRefEndNamesAndURLs(issues, "")
- commitStatus, err := pull_service.GetIssuesLastCommitStatus(ctx, issues)
- if err != nil {
- ctx.ServerError("GetIssuesLastCommitStatus", err)
- return
- }
- ctx.Data["CommitStatus"] = commitStatus
-
approvalCounts, err := issues.GetApprovalCounts(ctx)
if err != nil {
ctx.ServerError("ApprovalCounts", err)
@@ -335,9 +297,10 @@ func NotificationSubscriptions(ctx *context.Context) {
return 0
}
reviewTyp := issues_model.ReviewTypeApprove
- if typ == "reject" {
+ switch typ {
+ case "reject":
reviewTyp = issues_model.ReviewTypeReject
- } else if typ == "waiting" {
+ case "waiting":
reviewTyp = issues_model.ReviewTypeRequest
}
for _, count := range counts {
@@ -365,10 +328,7 @@ func NotificationSubscriptions(ctx *context.Context) {
// NotificationWatching returns the list of watching repos
func NotificationWatching(ctx *context.Context) {
- page := ctx.FormInt("page")
- if page < 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
keyword := ctx.FormTrim("q")
ctx.Data["Keyword"] = keyword
@@ -416,7 +376,7 @@ func NotificationWatching(ctx *context.Context) {
private := ctx.FormOptionalBool("private")
ctx.Data["IsPrivate"] = private
- repos, count, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
+ repos, count, err := repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
PageSize: setting.UI.User.RepoPagingNum,
Page: page,
diff --git a/routers/web/user/package.go b/routers/web/user/package.go
index 1f75faf1c6..216acdf927 100644
--- a/routers/web/user/package.go
+++ b/routers/web/user/package.go
@@ -4,6 +4,8 @@
package user
import (
+ gocontext "context"
+ "errors"
"net/http"
"net/url"
@@ -20,6 +22,7 @@ import (
"code.gitea.io/gitea/modules/optional"
alpine_module "code.gitea.io/gitea/modules/packages/alpine"
arch_module "code.gitea.io/gitea/modules/packages/arch"
+ container_module "code.gitea.io/gitea/modules/packages/container"
debian_module "code.gitea.io/gitea/modules/packages/debian"
rpm_module "code.gitea.io/gitea/modules/packages/rpm"
"code.gitea.io/gitea/modules/setting"
@@ -31,6 +34,7 @@ import (
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
packages_service "code.gitea.io/gitea/services/packages"
+ container_service "code.gitea.io/gitea/services/packages/container"
)
const (
@@ -42,11 +46,11 @@ const (
// ListPackages displays a list of all packages of the context user
func ListPackages(ctx *context.Context) {
- shared_user.PrepareContextForProfileBigAvatar(ctx)
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
+ return
}
+ page := max(ctx.FormInt("page"), 1)
query := ctx.FormTrim("q")
packageType := ctx.FormTrim("type")
@@ -94,8 +98,6 @@ func ListPackages(ctx *context.Context) {
return
}
- shared_user.RenderUserHeader(ctx)
-
ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["IsPackagesPage"] = true
ctx.Data["Query"] = query
@@ -106,9 +108,8 @@ func ListPackages(ctx *context.Context) {
ctx.Data["Total"] = total
ctx.Data["RepositoryAccessMap"] = repositoryAccessMap
- err = shared_user.LoadHeaderCount(ctx)
- if err != nil {
- ctx.ServerError("LoadHeaderCount", err)
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
return
}
@@ -126,11 +127,9 @@ func ListPackages(ctx *context.Context) {
ctx.Data["IsOrganizationOwner"] = false
}
}
-
pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5)
pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager
-
ctx.HTML(http.StatusOK, tplPackagesList)
}
@@ -139,7 +138,7 @@ func RedirectToLastVersion(ctx *context.Context) {
p, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.Type(ctx.PathParam("type")), ctx.PathParam("name"))
if err != nil {
if err == packages_model.ErrPackageNotExist {
- ctx.NotFound("GetPackageByName", err)
+ ctx.NotFound(err)
} else {
ctx.ServerError("GetPackageByName", err)
}
@@ -155,7 +154,7 @@ func RedirectToLastVersion(ctx *context.Context) {
return
}
if len(pvs) == 0 {
- ctx.NotFound("", err)
+ ctx.NotFound(err)
return
}
@@ -164,16 +163,36 @@ func RedirectToLastVersion(ctx *context.Context) {
ctx.ServerError("GetPackageDescriptor", err)
return
}
-
ctx.Redirect(pd.VersionWebLink())
}
+func viewPackageContainerImage(ctx gocontext.Context, pd *packages_model.PackageDescriptor, digest string) (*container_module.Metadata, error) {
+ manifestBlob, err := container_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{
+ OwnerID: pd.Owner.ID,
+ Image: pd.Package.LowerName,
+ Digest: digest,
+ })
+ if err != nil {
+ return nil, err
+ }
+ manifestReader, err := packages_service.OpenBlobStream(manifestBlob.Blob)
+ if err != nil {
+ return nil, err
+ }
+ defer manifestReader.Close()
+ _, _, metadata, err := container_service.ParseManifestMetadata(ctx, manifestReader, pd.Owner.ID, pd.Package.LowerName)
+ return metadata, err
+}
+
// ViewPackageVersion displays a single package version
func ViewPackageVersion(ctx *context.Context) {
- pd := ctx.Package.Descriptor
-
- shared_user.RenderUserHeader(ctx)
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
+ return
+ }
+ versionSub := ctx.PathParam("version_sub")
+ pd := ctx.Package.Descriptor
ctx.Data["Title"] = pd.Package.Name
ctx.Data["IsPackagesPage"] = true
ctx.Data["PackageDescriptor"] = pd
@@ -261,21 +280,30 @@ func ViewPackageVersion(ctx *context.Context) {
ctx.Data["Groups"] = util.Sorted(groups.Values())
ctx.Data["Architectures"] = util.Sorted(architectures.Values())
- }
-
- var (
- total int64
- pvs []*packages_model.PackageVersion
- )
- switch pd.Package.Type {
case packages_model.TypeContainer:
- pvs, total, err = container_model.SearchImageTags(ctx, &container_model.ImageTagsSearchOptions{
+ imageMetadata := pd.Metadata
+ if versionSub != "" {
+ imageMetadata, err = viewPackageContainerImage(ctx, pd, versionSub)
+ if errors.Is(err, util.ErrNotExist) {
+ ctx.NotFound(nil)
+ return
+ } else if err != nil {
+ ctx.ServerError("viewPackageContainerImage", err)
+ return
+ }
+ }
+ ctx.Data["ContainerImageMetadata"] = imageMetadata
+ }
+ var pvs []*packages_model.PackageVersion
+ var pvsTotal int64
+ if pd.Package.Type == packages_model.TypeContainer {
+ pvs, pvsTotal, err = container_model.SearchImageTags(ctx, &container_model.ImageTagsSearchOptions{
Paginator: db.NewAbsoluteListOptions(0, 5),
PackageID: pd.Package.ID,
IsTagged: true,
})
- default:
- pvs, total, err = packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
+ } else {
+ pvs, pvsTotal, err = packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
Paginator: db.NewAbsoluteListOptions(0, 5),
PackageID: pd.Package.ID,
IsInternal: optional.Some(false),
@@ -285,9 +313,8 @@ func ViewPackageVersion(ctx *context.Context) {
ctx.ServerError("", err)
return
}
-
ctx.Data["LatestVersions"] = pvs
- ctx.Data["TotalVersionCount"] = total
+ ctx.Data["TotalVersionCount"] = pvsTotal
ctx.Data["CanWritePackages"] = ctx.Package.AccessMode >= perm.AccessModeWrite || ctx.IsUserSiteAdmin()
@@ -301,33 +328,27 @@ func ViewPackageVersion(ctx *context.Context) {
hasRepositoryAccess = permission.HasAnyUnitAccess()
}
ctx.Data["HasRepositoryAccess"] = hasRepositoryAccess
-
- err = shared_user.LoadHeaderCount(ctx)
- if err != nil {
- ctx.ServerError("LoadHeaderCount", err)
- return
- }
-
ctx.HTML(http.StatusOK, tplPackagesView)
}
// ListPackageVersions lists all versions of a package
func ListPackageVersions(ctx *context.Context) {
- shared_user.PrepareContextForProfileBigAvatar(ctx)
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
+ return
+ }
+
p, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.Type(ctx.PathParam("type")), ctx.PathParam("name"))
if err != nil {
if err == packages_model.ErrPackageNotExist {
- ctx.NotFound("GetPackageByName", err)
+ ctx.NotFound(err)
} else {
ctx.ServerError("GetPackageByName", err)
}
return
}
- page := ctx.FormInt("page")
- if page <= 1 {
- page = 1
- }
+ page := max(ctx.FormInt("page"), 1)
pagination := &db.ListOptions{
PageSize: setting.UI.PackagesPagingNum,
Page: page,
@@ -336,8 +357,6 @@ func ListPackageVersions(ctx *context.Context) {
query := ctx.FormTrim("q")
sort := ctx.FormTrim("sort")
- shared_user.RenderUserHeader(ctx)
-
ctx.Data["Title"] = ctx.Tr("packages.title")
ctx.Data["IsPackagesPage"] = true
ctx.Data["PackageDescriptor"] = &packages_model.PackageDescriptor{
@@ -393,12 +412,6 @@ func ListPackageVersions(ctx *context.Context) {
ctx.Data["Total"] = total
- err = shared_user.LoadHeaderCount(ctx)
- if err != nil {
- ctx.ServerError("LoadHeaderCount", err)
- return
- }
-
pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5)
pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager
@@ -410,25 +423,22 @@ func ListPackageVersions(ctx *context.Context) {
func PackageSettings(ctx *context.Context) {
pd := ctx.Package.Descriptor
- shared_user.RenderUserHeader(ctx)
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
+ return
+ }
ctx.Data["Title"] = pd.Package.Name
ctx.Data["IsPackagesPage"] = true
ctx.Data["PackageDescriptor"] = pd
- repos, _, _ := repo_model.GetUserRepositories(ctx, &repo_model.SearchRepoOptions{
+ repos, _, _ := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{
Actor: pd.Owner,
Private: true,
})
ctx.Data["Repos"] = repos
ctx.Data["CanWritePackages"] = ctx.Package.AccessMode >= perm.AccessModeWrite || ctx.IsUserSiteAdmin()
- err := shared_user.LoadHeaderCount(ctx)
- if err != nil {
- ctx.ServerError("LoadHeaderCount", err)
- return
- }
-
ctx.HTML(http.StatusOK, tplPackagesSettings)
}
@@ -496,16 +506,16 @@ func DownloadPackageFile(ctx *context.Context) {
pf, err := packages_model.GetFileForVersionByID(ctx, ctx.Package.Descriptor.Version.ID, ctx.PathParamInt64("fileid"))
if err != nil {
if err == packages_model.ErrPackageFileNotExist {
- ctx.NotFound("", err)
+ ctx.NotFound(err)
} else {
ctx.ServerError("GetFileForVersionByID", err)
}
return
}
- s, u, _, err := packages_service.GetPackageFileStream(ctx, pf)
+ s, u, _, err := packages_service.OpenFileForDownload(ctx, pf)
if err != nil {
- ctx.ServerError("GetPackageFileStream", err)
+ ctx.ServerError("OpenFileForDownload", err)
return
}
diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go
index 006ffdcf7e..d7052914b6 100644
--- a/routers/web/user/profile.go
+++ b/routers/web/user/profile.go
@@ -56,34 +56,29 @@ func OwnerProfile(ctx *context.Context) {
func userProfile(ctx *context.Context) {
// check view permissions
if !user_model.IsUserVisibleToViewer(ctx, ctx.ContextUser, ctx.Doer) {
- ctx.NotFound("user", fmt.Errorf("%s", ctx.ContextUser.Name))
+ ctx.NotFound(fmt.Errorf("%s", ctx.ContextUser.Name))
return
}
ctx.Data["Title"] = ctx.ContextUser.DisplayName()
ctx.Data["PageIsUserProfile"] = true
- // prepare heatmap data
- if setting.Service.EnableUserHeatmap {
- data, err := activities_model.GetUserHeatmapDataByUser(ctx, ctx.ContextUser, ctx.Doer)
- if err != nil {
- ctx.ServerError("GetUserHeatmapDataByUser", err)
- return
- }
- ctx.Data["HeatmapData"] = data
- ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data)
- }
-
profileDbRepo, profileReadmeBlob := shared_user.FindOwnerProfileReadme(ctx, ctx.Doer)
- showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
- prepareUserProfileTabData(ctx, showPrivate, profileDbRepo, profileReadmeBlob)
- // call PrepareContextForProfileBigAvatar later to avoid re-querying the NumFollowers & NumFollowing
- shared_user.PrepareContextForProfileBigAvatar(ctx)
+ prepareUserProfileTabData(ctx, profileDbRepo, profileReadmeBlob)
+
+ // prepare the user nav header data after "prepareUserProfileTabData" to avoid re-querying the NumFollowers & NumFollowing
+ // because ctx.Data["NumFollowers"] and "NumFollowing" logic duplicates in both of them
+ // and the "profile readme" related logic also duplicates in both of FindOwnerProfileReadme and RenderUserOrgHeader
+ // TODO: it is a bad design and should be refactored later,
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
+ return
+ }
ctx.HTML(http.StatusOK, tplProfile)
}
-func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDbRepo *repo_model.Repository, profileReadme *git.Blob) {
+func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.Repository, profileReadme *git.Blob) {
// if there is a profile readme, default to "overview" page, otherwise, default to "repositories" page
// if there is not a profile readme, the overview tab should be treated as the repositories tab
tab := ctx.FormString("tab")
@@ -166,8 +161,20 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
ctx.Data["Cards"] = following
total = int(numFollowing)
case "activity":
+ // prepare heatmap data
+ if setting.Service.EnableUserHeatmap {
+ data, err := activities_model.GetUserHeatmapDataByUser(ctx, ctx.ContextUser, ctx.Doer)
+ if err != nil {
+ ctx.ServerError("GetUserHeatmapDataByUser", err)
+ return
+ }
+ ctx.Data["HeatmapData"] = data
+ ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data)
+ }
+
date := ctx.FormString("date")
pagingNum = setting.UI.FeedPagingNum
+ showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
items, count, err := feed_service.GetFeeds(ctx, activities_model.GetFeedsOptions{
RequestedUser: ctx.ContextUser,
Actor: ctx.Doer,
@@ -190,7 +197,8 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
total = int(count)
case "stars":
ctx.Data["PageIsProfileStarList"] = true
- repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
+ ctx.Data["ShowRepoOwnerOnList"] = true
+ repos, count, err = repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
PageSize: pagingNum,
Page: page,
@@ -217,7 +225,7 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
total = int(count)
case "watching":
- repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
+ repos, count, err = repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
PageSize: pagingNum,
Page: page,
@@ -258,8 +266,8 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
}
case "organizations":
orgs, count, err := db.FindAndCount[organization.Organization](ctx, organization.FindOrgOptions{
- UserID: ctx.ContextUser.ID,
- IncludePrivate: showPrivate,
+ UserID: ctx.ContextUser.ID,
+ IncludeVisibility: organization.DoerViewOtherVisibility(ctx.Doer, ctx.ContextUser),
ListOptions: db.ListOptions{
Page: page,
PageSize: pagingNum,
@@ -272,7 +280,7 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
ctx.Data["Cards"] = orgs
total = int(count)
default: // default to "repositories"
- repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
+ repos, count, err = repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
PageSize: pagingNum,
Page: page,
@@ -302,9 +310,8 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
ctx.Data["Repos"] = repos
ctx.Data["Total"] = total
- err = shared_user.LoadHeaderCount(ctx)
- if err != nil {
- ctx.ServerError("LoadHeaderCount", err)
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
return
}
@@ -313,8 +320,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":
@@ -325,12 +332,14 @@ func Action(ctx *context.Context) {
if err != nil {
log.Error("Failed to apply action %q: %v", ctx.FormString("action"), err)
- ctx.Error(http.StatusBadRequest, fmt.Sprintf("Action %q failed", ctx.FormString("action")))
+ ctx.HTTPError(http.StatusBadRequest, fmt.Sprintf("Action %q failed", ctx.FormString("action")))
+ return
+ }
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
return
}
-
if ctx.ContextUser.IsIndividual() {
- shared_user.PrepareContextForProfileBigAvatar(ctx)
ctx.HTML(http.StatusOK, tplProfileBigAvatar)
return
} else if ctx.ContextUser.IsOrganization() {
@@ -339,6 +348,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)
- ctx.Error(http.StatusBadRequest, fmt.Sprintf("Action %q failed", ctx.FormString("action")))
+ log.Error("Failed to apply action %q: unsupported context user type: %s", ctx.FormString("action"), ctx.ContextUser.Type)
+ ctx.HTTPError(http.StatusBadRequest, fmt.Sprintf("Action %q failed", ctx.FormString("action")))
}
diff --git a/routers/web/user/search.go b/routers/web/user/search.go
index be5eee90a9..9acb9694d7 100644
--- a/routers/web/user/search.go
+++ b/routers/web/user/search.go
@@ -16,7 +16,7 @@ import (
// SearchCandidates searches candidate users for dropdown list
func SearchCandidates(ctx *context.Context) {
- users, _, err := user_model.SearchUsers(ctx, &user_model.SearchUserOptions{
+ users, _, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{
Actor: ctx.Doer,
Keyword: ctx.FormTrim("q"),
Type: user_model.UserTypeIndividual,
diff --git a/routers/web/user/setting/account.go b/routers/web/user/setting/account.go
index 3acc3c7a54..b124d5e1de 100644
--- a/routers/web/user/setting/account.go
+++ b/routers/web/user/setting/account.go
@@ -6,7 +6,6 @@ package setting
import (
"errors"
- "fmt"
"net/http"
"time"
@@ -37,7 +36,7 @@ const (
// Account renders change user's password, user's email and user suicide page
func Account(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials, setting.UserFeatureDeletion) && !setting.Service.EnableNotifyMail {
- ctx.NotFound("Not Found", fmt.Errorf("account setting are not allowed to be changed"))
+ ctx.NotFound(errors.New("account setting are not allowed to be changed"))
return
}
@@ -54,7 +53,7 @@ func Account(ctx *context.Context) {
// AccountPost response for change user's password
func AccountPost(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) {
- ctx.NotFound("Not Found", fmt.Errorf("password setting is not allowed to be changed"))
+ ctx.NotFound(errors.New("password setting is not allowed to be changed"))
return
}
@@ -105,7 +104,7 @@ func AccountPost(ctx *context.Context) {
// EmailPost response for change user's email
func EmailPost(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) {
- ctx.NotFound("Not Found", fmt.Errorf("emails are not allowed to be changed"))
+ ctx.NotFound(errors.New("emails are not allowed to be changed"))
return
}
@@ -239,7 +238,7 @@ func EmailPost(ctx *context.Context) {
// DeleteEmail response for delete user's email
func DeleteEmail(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) {
- ctx.NotFound("Not Found", fmt.Errorf("emails are not allowed to be changed"))
+ ctx.NotFound(errors.New("emails are not allowed to be changed"))
return
}
email, err := user_model.GetEmailAddressByID(ctx, ctx.Doer.ID, ctx.FormInt64("id"))
@@ -261,7 +260,7 @@ func DeleteEmail(ctx *context.Context) {
// DeleteAccount render user suicide page and response for delete user himself
func DeleteAccount(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureDeletion) {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
diff --git a/routers/web/user/setting/account_test.go b/routers/web/user/setting/account_test.go
index 9fdc5e4d53..9b8cffc868 100644
--- a/routers/web/user/setting/account_test.go
+++ b/routers/web/user/setting/account_test.go
@@ -95,7 +95,7 @@ func TestChangePassword(t *testing.T) {
AccountPost(ctx)
assert.Contains(t, ctx.Flash.ErrorMsg, req.Message)
- assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+ assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
})
}
}
diff --git a/routers/web/user/setting/applications.go b/routers/web/user/setting/applications.go
index cf71d01dc1..9c43ddd3ea 100644
--- a/routers/web/user/setting/applications.go
+++ b/routers/web/user/setting/applications.go
@@ -6,12 +6,14 @@ package setting
import (
"net/http"
+ "strings"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
@@ -39,18 +41,30 @@ func ApplicationsPost(ctx *context.Context) {
ctx.Data["PageIsSettingsApplications"] = true
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
- if ctx.HasError() {
- loadApplicationsData(ctx)
-
- ctx.HTML(http.StatusOK, tplSettingsApplications)
- return
+ _ = ctx.Req.ParseForm()
+ var scopeNames []string
+ const accessTokenScopePrefix = "scope-"
+ for k, v := range ctx.Req.Form {
+ if strings.HasPrefix(k, accessTokenScopePrefix) {
+ scopeNames = append(scopeNames, v...)
+ }
}
- scope, err := form.GetScope()
+ scope, err := auth_model.AccessTokenScope(strings.Join(scopeNames, ",")).Normalize()
if err != nil {
ctx.ServerError("GetScope", err)
return
}
+ if !scope.HasPermissionScope() {
+ ctx.Flash.Error(ctx.Tr("settings.at_least_one_permission"), true)
+ }
+
+ if ctx.HasError() {
+ loadApplicationsData(ctx)
+ ctx.HTML(http.StatusOK, tplSettingsApplications)
+ return
+ }
+
t := &auth_model.AccessToken{
UID: ctx.Doer.ID,
Name: form.Name,
@@ -99,7 +113,14 @@ func loadApplicationsData(ctx *context.Context) {
}
ctx.Data["Tokens"] = tokens
ctx.Data["EnableOAuth2"] = setting.OAuth2.Enabled
- ctx.Data["IsAdmin"] = ctx.Doer.IsAdmin
+
+ // Handle specific ordered token categories for admin or non-admin users
+ tokenCategoryNames := auth_model.GetAccessTokenCategories()
+ if !ctx.Doer.IsAdmin {
+ tokenCategoryNames = util.SliceRemoveAll(tokenCategoryNames, "admin")
+ }
+ ctx.Data["TokenCategories"] = tokenCategoryNames
+
if setting.OAuth2.Enabled {
ctx.Data["Applications"], err = db.Find[auth_model.OAuth2Application](ctx, auth_model.FindOAuth2ApplicationsOptions{
OwnerID: ctx.Doer.ID,
diff --git a/routers/web/user/setting/keys.go b/routers/web/user/setting/keys.go
index 127aed9845..6b5a7a2e2a 100644
--- a/routers/web/user/setting/keys.go
+++ b/routers/web/user/setting/keys.go
@@ -5,7 +5,7 @@
package setting
import (
- "fmt"
+ "errors"
"net/http"
asymkey_model "code.gitea.io/gitea/models/asymkey"
@@ -26,7 +26,7 @@ const (
// Keys render user's SSH/GPG public keys page
func Keys(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys, setting.UserFeatureManageGPGKeys) {
- ctx.NotFound("Not Found", fmt.Errorf("keys setting is not allowed to be changed"))
+ ctx.NotFound(errors.New("keys setting is not allowed to be changed"))
return
}
@@ -87,7 +87,7 @@ func KeysPost(ctx *context.Context) {
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
case "gpg":
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) {
- ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited"))
+ ctx.NotFound(errors.New("gpg keys setting is not allowed to be visited"))
return
}
@@ -168,7 +168,7 @@ func KeysPost(ctx *context.Context) {
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
case "ssh":
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
- ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited"))
+ ctx.NotFound(errors.New("ssh keys setting is not allowed to be visited"))
return
}
@@ -212,7 +212,7 @@ func KeysPost(ctx *context.Context) {
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
case "verify_ssh":
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
- ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited"))
+ ctx.NotFound(errors.New("ssh keys setting is not allowed to be visited"))
return
}
@@ -249,7 +249,7 @@ func DeleteKey(ctx *context.Context) {
switch ctx.FormString("type") {
case "gpg":
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageGPGKeys) {
- ctx.NotFound("Not Found", fmt.Errorf("gpg keys setting is not allowed to be visited"))
+ ctx.NotFound(errors.New("gpg keys setting is not allowed to be visited"))
return
}
if err := asymkey_model.DeleteGPGKey(ctx, ctx.Doer, ctx.FormInt64("id")); err != nil {
@@ -259,7 +259,7 @@ func DeleteKey(ctx *context.Context) {
}
case "ssh":
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageSSHKeys) {
- ctx.NotFound("Not Found", fmt.Errorf("ssh keys setting is not allowed to be visited"))
+ ctx.NotFound(errors.New("ssh keys setting is not allowed to be visited"))
return
}
diff --git a/routers/web/user/setting/oauth2_common.go b/routers/web/user/setting/oauth2_common.go
index 783deca710..f460acce10 100644
--- a/routers/web/user/setting/oauth2_common.go
+++ b/routers/web/user/setting/oauth2_common.go
@@ -28,8 +28,8 @@ func (oa *OAuth2CommonHandlers) renderEditPage(ctx *context.Context) {
ctx.Data["FormActionPath"] = fmt.Sprintf("%s/%d", oa.BasePathEditPrefix, app.ID)
if ctx.ContextUser != nil && ctx.ContextUser.IsOrganization() {
- if err := shared_user.LoadHeaderCount(ctx); err != nil {
- ctx.ServerError("LoadHeaderCount", err)
+ if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
+ ctx.ServerError("RenderUserOrgHeader", err)
return
}
}
@@ -76,14 +76,14 @@ func (oa *OAuth2CommonHandlers) EditShow(ctx *context.Context) {
app, err := auth.GetOAuth2ApplicationByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
if auth.IsErrOAuthApplicationNotFound(err) {
- ctx.NotFound("Application not found", err)
+ ctx.NotFound(err)
return
}
ctx.ServerError("GetOAuth2ApplicationByID", err)
return
}
if app.UID != oa.OwnerID {
- ctx.NotFound("Application not found", nil)
+ ctx.NotFound(nil)
return
}
ctx.Data["App"] = app
@@ -98,14 +98,14 @@ func (oa *OAuth2CommonHandlers) EditSave(ctx *context.Context) {
app, err := auth.GetOAuth2ApplicationByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
if auth.IsErrOAuthApplicationNotFound(err) {
- ctx.NotFound("Application not found", err)
+ ctx.NotFound(err)
return
}
ctx.ServerError("GetOAuth2ApplicationByID", err)
return
}
if app.UID != oa.OwnerID {
- ctx.NotFound("Application not found", nil)
+ ctx.NotFound(nil)
return
}
ctx.Data["App"] = app
@@ -135,14 +135,14 @@ func (oa *OAuth2CommonHandlers) RegenerateSecret(ctx *context.Context) {
app, err := auth.GetOAuth2ApplicationByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
if auth.IsErrOAuthApplicationNotFound(err) {
- ctx.NotFound("Application not found", err)
+ ctx.NotFound(err)
return
}
ctx.ServerError("GetOAuth2ApplicationByID", err)
return
}
if app.UID != oa.OwnerID {
- ctx.NotFound("Application not found", nil)
+ ctx.NotFound(nil)
return
}
ctx.Data["App"] = app
diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go
index ebf682bf58..98995cd69c 100644
--- a/routers/web/user/setting/profile.go
+++ b/routers/web/user/setting/profile.go
@@ -22,6 +22,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/typesniffer"
@@ -206,8 +207,8 @@ func Organization(ctx *context.Context) {
PageSize: setting.UI.Admin.UserPagingNum,
Page: ctx.FormInt("page"),
},
- UserID: ctx.Doer.ID,
- IncludePrivate: ctx.IsSigned,
+ UserID: ctx.Doer.ID,
+ IncludeVisibility: structs.VisibleTypePrivate,
}
if opts.Page <= 0 {
@@ -284,7 +285,7 @@ func Repos(ctx *context.Context) {
return
}
- userRepos, _, err := repo_model.GetUserRepositories(ctx, &repo_model.SearchRepoOptions{
+ userRepos, _, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{
Actor: ctxUser,
Private: true,
ListOptions: db.ListOptions{
@@ -309,7 +310,7 @@ func Repos(ctx *context.Context) {
ctx.Data["Dirs"] = repoNames
ctx.Data["ReposMap"] = repos
} else {
- repos, count64, err := repo_model.GetUserRepositories(ctx, &repo_model.SearchRepoOptions{Actor: ctxUser, Private: true, ListOptions: opts})
+ repos, count64, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{Actor: ctxUser, Private: true, ListOptions: opts})
if err != nil {
ctx.ServerError("GetUserRepositories", err)
return
@@ -338,13 +339,7 @@ func Repos(ctx *context.Context) {
func Appearance(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings.appearance")
ctx.Data["PageIsSettingsAppearance"] = true
-
- allThemes := webtheme.GetAvailableThemes()
- if webtheme.IsThemeAvailable(setting.UI.DefaultTheme) {
- allThemes = util.SliceRemoveAll(allThemes, setting.UI.DefaultTheme)
- allThemes = append([]string{setting.UI.DefaultTheme}, allThemes...) // move the default theme to the top
- }
- ctx.Data["AllThemes"] = allThemes
+ ctx.Data["AllThemes"] = webtheme.GetAvailableThemes()
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
var hiddenCommentTypes *big.Int
diff --git a/routers/web/user/setting/security/2fa.go b/routers/web/user/setting/security/2fa.go
index 7bb10248e8..e5e23c820c 100644
--- a/routers/web/user/setting/security/2fa.go
+++ b/routers/web/user/setting/security/2fa.go
@@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
@@ -27,7 +28,7 @@ import (
// RegenerateScratchTwoFactor regenerates the user's 2FA scratch code.
func RegenerateScratchTwoFactor(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageMFA) {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
@@ -63,7 +64,7 @@ func RegenerateScratchTwoFactor(ctx *context.Context) {
// DisableTwoFactor deletes the user's 2FA settings.
func DisableTwoFactor(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageMFA) {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
@@ -157,12 +158,13 @@ func twofaGenerateSecretAndQr(ctx *context.Context) bool {
// EnrollTwoFactor shows the page where the user can enroll into 2FA.
func EnrollTwoFactor(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageMFA) {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSecurity"] = true
+ ctx.Data["ShowTwoFactorRequiredMessage"] = false
t, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID)
if t != nil {
@@ -187,13 +189,14 @@ func EnrollTwoFactor(ctx *context.Context) {
// EnrollTwoFactorPost handles enrolling the user into 2FA.
func EnrollTwoFactorPost(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageMFA) {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
form := web.GetForm(ctx).(*forms.TwoFactorAuthForm)
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsSecurity"] = true
+ ctx.Data["ShowTwoFactorRequiredMessage"] = false
t, err := auth.GetTwoFactorByUID(ctx, ctx.Doer.ID)
if t != nil {
@@ -246,6 +249,10 @@ func EnrollTwoFactorPost(ctx *context.Context) {
return
}
+ newTwoFactorErr := auth.NewTwoFactor(ctx, t)
+ if newTwoFactorErr == nil {
+ _ = ctx.Session.Set(session.KeyUserHasTwoFactorAuth, true)
+ }
// Now we have to delete the secrets - because if we fail to insert then it's highly likely that they have already been used
// If we can detect the unique constraint failure below we can move this to after the NewTwoFactor
if err := ctx.Session.Delete("twofaSecret"); err != nil {
@@ -261,10 +268,10 @@ func EnrollTwoFactorPost(ctx *context.Context) {
log.Error("Unable to save changes to the session: %v", err)
}
- if err = auth.NewTwoFactor(ctx, t); err != nil {
+ if newTwoFactorErr != nil {
// FIXME: We need to handle a unique constraint fail here it's entirely possible that another request has beaten us.
// If there is a unique constraint fail we should just tolerate the error
- ctx.ServerError("SettingsTwoFactor: Failed to save two factor", err)
+ ctx.ServerError("SettingsTwoFactor: Failed to save two factor", newTwoFactorErr)
return
}
diff --git a/routers/web/user/setting/security/openid.go b/routers/web/user/setting/security/openid.go
index 30eb6f63f8..9a57657912 100644
--- a/routers/web/user/setting/security/openid.go
+++ b/routers/web/user/setting/security/openid.go
@@ -18,7 +18,7 @@ import (
// OpenIDPost response for change user's openid
func OpenIDPost(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
@@ -111,7 +111,7 @@ func settingsOpenIDVerify(ctx *context.Context) {
// DeleteOpenID response for delete user's openid
func DeleteOpenID(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
@@ -128,7 +128,7 @@ func DeleteOpenID(ctx *context.Context) {
// ToggleOpenIDVisibility response for toggle visibility of user's openid
func ToggleOpenIDVisibility(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
diff --git a/routers/web/user/setting/security/security.go b/routers/web/user/setting/security/security.go
index 38d1910e2d..cc4c44993a 100644
--- a/routers/web/user/setting/security/security.go
+++ b/routers/web/user/setting/security/security.go
@@ -27,7 +27,7 @@ const (
func Security(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer,
setting.UserFeatureManageMFA, setting.UserFeatureManageCredentials) {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
@@ -47,7 +47,7 @@ func Security(ctx *context.Context) {
// DeleteAccountLink delete a single account link
func DeleteAccountLink(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
diff --git a/routers/web/user/setting/security/webauthn.go b/routers/web/user/setting/security/webauthn.go
index 70bfaac6e0..eb9f46af52 100644
--- a/routers/web/user/setting/security/webauthn.go
+++ b/routers/web/user/setting/security/webauthn.go
@@ -13,6 +13,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
wa "code.gitea.io/gitea/modules/auth/webauthn"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
@@ -25,7 +26,7 @@ import (
// WebAuthnRegister initializes the webauthn registration procedure
func WebAuthnRegister(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageMFA) {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
@@ -41,7 +42,7 @@ func WebAuthnRegister(ctx *context.Context) {
return
}
if cred != nil {
- ctx.Error(http.StatusConflict, "Name already taken")
+ ctx.HTTPError(http.StatusConflict, "Name already taken")
return
}
@@ -72,7 +73,7 @@ func WebAuthnRegister(ctx *context.Context) {
// WebauthnRegisterPost receives the response of the security key
func WebauthnRegisterPost(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageMFA) {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
@@ -109,7 +110,7 @@ func WebauthnRegisterPost(ctx *context.Context) {
return
}
if dbCred != nil {
- ctx.Error(http.StatusConflict, "Name already taken")
+ ctx.HTTPError(http.StatusConflict, "Name already taken")
return
}
@@ -120,14 +121,14 @@ func WebauthnRegisterPost(ctx *context.Context) {
return
}
_ = ctx.Session.Delete("webauthnName")
-
+ _ = ctx.Session.Set(session.KeyUserHasTwoFactorAuth, true)
ctx.JSON(http.StatusCreated, cred)
}
// WebauthnDelete deletes an security key by id
func WebauthnDelete(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageMFA) {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
diff --git a/routers/web/user/setting/settings.go b/routers/web/user/setting/settings.go
new file mode 100644
index 0000000000..111931633d
--- /dev/null
+++ b/routers/web/user/setting/settings.go
@@ -0,0 +1,26 @@
+// Copyright 2025 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package setting
+
+import (
+ "net/http"
+ "strconv"
+
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/services/context"
+)
+
+func UpdatePreferences(ctx *context.Context) {
+ type preferencesForm struct {
+ CodeViewShowFileTree bool `json:"codeViewShowFileTree"`
+ }
+ form := &preferencesForm{}
+ if err := json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil {
+ ctx.HTTPError(http.StatusBadRequest, "json decode failed")
+ return
+ }
+ _ = user_model.SetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyCodeViewShowFileTree, strconv.FormatBool(form.CodeViewShowFileTree))
+ ctx.JSONOK()
+}
diff --git a/routers/web/user/stop_watch.go b/routers/web/user/stop_watch.go
index 38f74ea455..1d1cc61cc9 100644
--- a/routers/web/user/stop_watch.go
+++ b/routers/web/user/stop_watch.go
@@ -19,19 +19,19 @@ func GetStopwatches(ctx *context.Context) {
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
})
if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.HTTPError(http.StatusInternalServerError, err.Error())
return
}
count, err := issues_model.CountUserStopwatches(ctx, ctx.Doer.ID)
if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.HTTPError(http.StatusInternalServerError, err.Error())
return
}
apiSWs, err := convert.ToStopWatches(ctx, sws)
if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
+ ctx.HTTPError(http.StatusInternalServerError, err.Error())
return
}
diff --git a/routers/web/web.go b/routers/web/web.go
index 609a7d77ac..1039f9e739 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.HTTPError(http.StatusUnauthorized, "Failed to authenticate user")
return
}
ctx.Doer = ar.Doer
@@ -153,7 +154,7 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.Cont
if ctx.Doer.MustChangePassword {
if ctx.Req.URL.Path != "/user/settings/change_password" {
if strings.HasPrefix(ctx.Req.UserAgent(), "git") {
- ctx.Error(http.StatusUnauthorized, ctx.Locale.TrString("auth.must_change_password"))
+ ctx.HTTPError(http.StatusUnauthorized, ctx.Locale.TrString("auth.must_change_password"))
return
}
ctx.Data["Title"] = ctx.Tr("auth.must_change_password")
@@ -177,7 +178,7 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.Cont
return
}
- if !options.SignOutRequired && !options.DisableCSRF && ctx.Req.Method == "POST" {
+ if !options.SignOutRequired && !options.DisableCSRF && ctx.Req.Method == http.MethodPost {
ctx.Csrf.Validate(ctx)
if ctx.Written() {
return
@@ -210,7 +211,7 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.Cont
if options.AdminRequired {
if !ctx.Doer.IsAdmin {
- ctx.Error(http.StatusForbidden)
+ ctx.HTTPError(http.StatusForbidden)
return
}
ctx.Data["PageIsAdmin"] = true
@@ -232,8 +233,8 @@ func Routes() *web.Router {
routes.Head("/", misc.DummyOK) // for health check - doesn't need to be passed through gzip handler
routes.Methods("GET, HEAD, OPTIONS", "/assets/*", optionsCorsHandler(), public.FileHandlerFunc())
- routes.Methods("GET, HEAD", "/avatars/*", storageHandler(setting.Avatar.Storage, "avatars", storage.Avatars))
- routes.Methods("GET, HEAD", "/repo-avatars/*", storageHandler(setting.RepoAvatar.Storage, "repo-avatars", storage.RepoAvatars))
+ routes.Methods("GET, HEAD", "/avatars/*", avatarStorageHandler(setting.Avatar.Storage, "avatars", storage.Avatars))
+ routes.Methods("GET, HEAD", "/repo-avatars/*", avatarStorageHandler(setting.RepoAvatar.Storage, "repo-avatars", storage.RepoAvatars))
routes.Methods("GET, HEAD", "/apple-touch-icon.png", misc.StaticRedirect("/assets/img/apple-touch-icon.png"))
routes.Methods("GET, HEAD", "/apple-touch-icon-precomposed.png", misc.StaticRedirect("/assets/img/apple-touch-icon.png"))
routes.Methods("GET, HEAD", "/favicon.ico", misc.StaticRedirect("/assets/img/favicon.png"))
@@ -279,62 +280,60 @@ func Routes() *web.Router {
routes.Get("/api/swagger", append(mid, misc.Swagger)...) // Render V1 by default
}
- // TODO: These really seem like things that could be folded into Contexter or as helper functions
- mid = append(mid, user.GetNotificationCount)
- mid = append(mid, repo.GetActiveStopwatch)
mid = append(mid, goGet)
+ mid = append(mid, common.PageTmplFunctions)
- others := web.NewRouter()
- others.Use(mid...)
- registerRoutes(others)
- routes.Mount("", others)
+ webRoutes := web.NewRouter()
+ webRoutes.Use(mid...)
+ webRoutes.Group("", func() { registerWebRoutes(webRoutes) }, common.BlockExpensive(), common.QoS())
+ routes.Mount("", webRoutes)
return routes
}
var optSignInIgnoreCsrf = verifyAuthWithOptions(&common.VerifyOptions{DisableCSRF: true})
-// registerRoutes register routes
-func registerRoutes(m *web.Router) {
+// registerWebRoutes register routes
+func registerWebRoutes(m *web.Router) {
// required to be signed in or signed out
reqSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: true})
reqSignOut := verifyAuthWithOptions(&common.VerifyOptions{SignOutRequired: true})
// optional sign in (if signed in, use the user as doer, if not, no doer)
- optSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInView})
- optExploreSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInView || setting.Service.Explore.RequireSigninView})
+ optSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInViewStrict})
+ optExploreSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: setting.Service.RequireSignInViewStrict || setting.Service.Explore.RequireSigninView})
validation.AddBindingRules()
linkAccountEnabled := func(ctx *context.Context) {
if !setting.Service.EnableOpenIDSignIn && !setting.Service.EnableOpenIDSignUp && !setting.OAuth2.Enabled {
- ctx.Error(http.StatusForbidden)
+ ctx.HTTPError(http.StatusForbidden)
return
}
}
openIDSignInEnabled := func(ctx *context.Context) {
if !setting.Service.EnableOpenIDSignIn {
- ctx.Error(http.StatusForbidden)
+ ctx.HTTPError(http.StatusForbidden)
return
}
}
openIDSignUpEnabled := func(ctx *context.Context) {
if !setting.Service.EnableOpenIDSignUp {
- ctx.Error(http.StatusForbidden)
+ ctx.HTTPError(http.StatusForbidden)
return
}
}
oauth2Enabled := func(ctx *context.Context) {
if !setting.OAuth2.Enabled {
- ctx.Error(http.StatusForbidden)
+ ctx.HTTPError(http.StatusForbidden)
return
}
}
reqMilestonesDashboardPageEnabled := func(ctx *context.Context) {
if !setting.Service.ShowMilestonesDashboardPage {
- ctx.Error(http.StatusForbidden)
+ ctx.HTTPError(http.StatusForbidden)
return
}
}
@@ -342,49 +341,56 @@ func registerRoutes(m *web.Router) {
// webhooksEnabled requires webhooks to be enabled by admin.
webhooksEnabled := func(ctx *context.Context) {
if setting.DisableWebhooks {
- ctx.Error(http.StatusForbidden)
+ ctx.HTTPError(http.StatusForbidden)
+ return
+ }
+ }
+
+ starsEnabled := func(ctx *context.Context) {
+ if setting.Repository.DisableStars {
+ ctx.HTTPError(http.StatusForbidden)
return
}
}
lfsServerEnabled := func(ctx *context.Context) {
if !setting.LFS.StartServer {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
}
federationEnabled := func(ctx *context.Context) {
if !setting.Federation.Enabled {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
}
dlSourceEnabled := func(ctx *context.Context) {
if setting.Repository.DisableDownloadSourceArchives {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
}
sitemapEnabled := func(ctx *context.Context) {
if !setting.Other.EnableSitemap {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
}
packagesEnabled := func(ctx *context.Context) {
if !setting.Packages.Enabled {
- ctx.Error(http.StatusForbidden)
+ ctx.HTTPError(http.StatusForbidden)
return
}
}
feedEnabled := func(ctx *context.Context) {
if !setting.Other.EnableFeed {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
}
@@ -393,18 +399,18 @@ func registerRoutes(m *web.Router) {
return func(ctx *context.Context) {
// only check global disabled units when ignoreGlobal is false
if !ignoreGlobal && unitType.UnitGlobalDisabled() {
- ctx.NotFound("Repo unit is is disabled: "+unitType.LogString(), nil)
+ ctx.NotFound(nil)
return
}
if ctx.ContextUser == nil {
- ctx.NotFound("ContextUser is nil", nil)
+ ctx.NotFound(nil)
return
}
if ctx.ContextUser.IsOrganization() {
if ctx.Org.Organization.UnitPermission(ctx, ctx.Doer, unitType) < accessMode {
- ctx.NotFound("ContextUser is org but doer has no access to unit", nil)
+ ctx.NotFound(nil)
return
}
}
@@ -442,10 +448,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 +465,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)
})
}
@@ -498,7 +504,7 @@ func registerRoutes(m *web.Router) {
m.Get("/organizations", explore.Organizations)
m.Get("/code", func(ctx *context.Context) {
if unit.TypeCode.UnitGlobalDisabled() {
- ctx.NotFound("Repo unit code is disabled", nil)
+ ctx.NotFound(nil)
return
}
}, explore.Code)
@@ -572,6 +578,7 @@ func registerRoutes(m *web.Router) {
m.Group("/user/settings", func() {
m.Get("", user_setting.Profile)
m.Post("", web.Bind(forms.UpdateProfileForm{}), user_setting.ProfilePost)
+ m.Post("/update_preferences", user_setting.UpdatePreferences)
m.Get("/change_password", auth.MustChangePassword)
m.Post("/change_password", web.Bind(forms.MustChangePasswordForm{}), auth.MustChangePasswordPost)
m.Post("/avatar", web.Bind(forms.AvatarForm{}), user_setting.AvatarPost)
@@ -681,7 +688,7 @@ func registerRoutes(m *web.Router) {
m.Get("/activate", auth.Activate)
m.Post("/activate", auth.ActivatePost)
m.Any("/activate_email", auth.ActivateEmail)
- m.Get("/avatar/{username}/{size}", user.AvatarByUserName)
+ m.Get("/avatar/{username}/{size}", user.AvatarByUsernameSize)
m.Get("/recover_account", auth.ResetPasswd)
m.Post("/recover_account", auth.ResetPasswdPost)
m.Get("/forgot_password", auth.ForgotPasswd)
@@ -720,6 +727,7 @@ func registerRoutes(m *web.Router) {
m.Group("/monitor", func() {
m.Get("/stats", admin.MonitorStats)
m.Get("/cron", admin.CronTasks)
+ m.Get("/perftrace", admin.PerfTrace)
m.Get("/stacktrace", admin.Stacktrace)
m.Post("/stacktrace/cancel/{pid}", admin.StacktraceCancel)
m.Get("/queue", admin.Queues)
@@ -814,7 +822,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)
@@ -838,7 +846,7 @@ func registerRoutes(m *web.Router) {
reqPackageAccess := func(accessMode perm.AccessMode) func(ctx *context.Context) {
return func(ctx *context.Context) {
if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() {
- ctx.NotFound("", nil)
+ ctx.NotFound(nil)
}
}
}
@@ -846,15 +854,15 @@ func registerRoutes(m *web.Router) {
individualPermsChecker := func(ctx *context.Context) {
// org permissions have been checked in context.OrgAssignment(), but individual permissions haven't been checked.
if ctx.ContextUser.IsIndividual() {
- switch {
- case ctx.ContextUser.Visibility == structs.VisibleTypePrivate:
+ switch ctx.ContextUser.Visibility {
+ case structs.VisibleTypePrivate:
if ctx.Doer == nil || (ctx.ContextUser.ID != ctx.Doer.ID && !ctx.Doer.IsAdmin) {
- ctx.NotFound("Visit Project", nil)
+ ctx.NotFound(nil)
return
}
- case ctx.ContextUser.Visibility == structs.VisibleTypeLimited:
+ case structs.VisibleTypeLimited:
if ctx.Doer == nil {
- ctx.NotFound("Visit Project", nil)
+ ctx.NotFound(nil)
return
}
}
@@ -864,7 +872,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
@@ -890,19 +898,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() {
@@ -912,6 +921,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)
@@ -953,7 +964,8 @@ func registerRoutes(m *web.Router) {
addSettingsVariablesRoutes()
}, actions.MustEnableActions)
- m.Methods("GET,POST", "/delete", org.SettingsDelete)
+ m.Post("/rename", web.Bind(forms.RenameOrgForm{}), org.SettingsRenamePost)
+ m.Post("/delete", org.SettingsDeleteOrgPost)
m.Group("/packages", func() {
m.Get("", org.Packages)
@@ -979,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
@@ -1001,6 +1013,7 @@ func registerRoutes(m *web.Router) {
m.Get("/versions", user.ListPackageVersions)
m.Group("/{version}", func() {
m.Get("", user.ViewPackageVersion)
+ m.Get("/{version_sub}", user.ViewPackageVersion)
m.Get("/files/{fileid}", user.DownloadPackageFile)
m.Group("/settings", func() {
m.Get("", user.PackageSettings)
@@ -1018,7 +1031,7 @@ func registerRoutes(m *web.Router) {
m.Get("", org.Projects)
m.Get("/{id}", org.ViewProject)
}, reqUnitAccess(unit.TypeProjects, perm.AccessModeRead, true))
- m.Group("", func() { //nolint:dupl
+ m.Group("", func() { //nolint:dupl // duplicates lines 1421-1441
m.Get("/new", org.RenderNewProject)
m.Post("/new", web.Bind(forms.CreateProjectForm{}), org.NewProjectPost)
m.Group("/{id}", func() {
@@ -1040,7 +1053,7 @@ func registerRoutes(m *web.Router) {
})
}, reqSignIn, reqUnitAccess(unit.TypeProjects, perm.AccessModeWrite, true), func(ctx *context.Context) {
if ctx.ContextUser.IsIndividual() && ctx.ContextUser.ID != ctx.Doer.ID {
- ctx.NotFound("NewProject", nil)
+ ctx.NotFound(nil)
return
}
})
@@ -1049,7 +1062,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() {
@@ -1067,6 +1080,8 @@ func registerRoutes(m *web.Router) {
m.Post("/avatar", web.Bind(forms.AvatarForm{}), repo_setting.SettingsAvatar)
m.Post("/avatar/delete", repo_setting.SettingsDeleteAvatar)
+ m.Combo("/public_access").Get(repo_setting.PublicAccess).Post(repo_setting.PublicAccessPost)
+
m.Group("/collaboration", func() {
m.Combo("").Get(repo_setting.Collaboration).Post(repo_setting.CollaborationPost)
m.Post("/access_mode", repo_setting.ChangeCollaborationAccessMode)
@@ -1135,7 +1150,7 @@ func registerRoutes(m *web.Router) {
})
})
m.Group("/actions", func() {
- m.Get("", repo_setting.RedirectToDefaultSetting)
+ m.Get("", shared_actions.RedirectToDefaultSetting)
addSettingsRunnersRoutes()
addSettingsSecretsRoutes()
addSettingsVariablesRoutes()
@@ -1146,12 +1161,12 @@ func registerRoutes(m *web.Router) {
m.Post("/cancel", repo.MigrateCancelPost)
})
},
- reqSignIn, context.RepoAssignment, reqRepoAdmin, context.RepoRef(),
+ reqSignIn, context.RepoAssignment, reqRepoAdmin,
ctxDataSet("PageIsRepoSettings", true, "LFSStartServer", setting.LFS.StartServer),
)
// end "/{username}/{reponame}/settings"
- // user/org home, including rss feeds
+ // user/org home, including rss feeds like "/{username}/{reponame}.rss"
m.Get("/{username}/{reponame}", optSignIn, context.RepoAssignment, context.RepoRefByType(git.RefTypeBranch), repo.SetEditorconfigIfExists, repo.Home)
m.Post("/{username}/{reponame}/markup", optSignIn, context.RepoAssignment, reqUnitsWithMarkdown, web.Bind(structs.MarkupOption{}), misc.Markup)
@@ -1163,10 +1178,16 @@ func registerRoutes(m *web.Router) {
m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.TreeList)
m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.TreeList)
})
+ m.Group("/tree-view", func() {
+ m.Get("/branch/*", context.RepoRefByType(git.RefTypeBranch), repo.TreeViewNodes)
+ m.Get("/tag/*", context.RepoRefByType(git.RefTypeTag), repo.TreeViewNodes)
+ m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.TreeViewNodes)
+ })
m.Get("/compare", repo.MustBeNotEmpty, repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff)
m.Combo("/compare/*", repo.MustBeNotEmpty, repo.SetEditorconfigIfExists).
Get(repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.CompareDiff).
Post(reqSignIn, context.RepoMustNotBeArchived(), reqUnitPullsReader, repo.MustAllowPulls, web.Bind(forms.CreateIssueForm{}), repo.SetWhitespaceBehavior, repo.CompareAndPullRequestPost)
+ m.Get("/pulls/new/*", repo.PullsNewRedirect)
}, optSignIn, context.RepoAssignment, reqUnitCodeReader)
// end "/{username}/{reponame}": repo code: find, compare, list
@@ -1184,16 +1205,17 @@ func registerRoutes(m *web.Router) {
})
})
}
+ // FIXME: many "pulls" requests are sent to "issues" endpoints correctly, so the issue endpoints have to tolerate pull request permissions at the moment
+ m.Group("/{username}/{reponame}/{type:issues}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, context.RequireUnitReader(unit.TypeIssues, unit.TypePullRequests))
+ m.Group("/{username}/{reponame}/{type:pulls}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, reqUnitPullsReader)
+
m.Group("/{username}/{reponame}", func() {
m.Get("/comments/{id}/attachments", repo.GetCommentAttachments)
m.Get("/labels", repo.RetrieveLabelsForList, repo.Labels)
m.Get("/milestones", repo.Milestones)
- m.Get("/milestone/{id}", context.RepoRef(), repo.MilestoneIssuesAndPulls)
+ m.Get("/milestone/{id}", repo.MilestoneIssuesAndPulls)
m.Get("/issues/suggestions", repo.IssueSuggestions)
}, optSignIn, context.RepoAssignment, reqRepoIssuesOrPullsReader) // issue/pull attachments, labels, milestones
-
- m.Group("/{username}/{reponame}/{type:issues}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, reqUnitIssuesReader)
- m.Group("/{username}/{reponame}/{type:pulls}", addIssuesPullsViewRoutes, optSignIn, context.RepoAssignment, reqUnitPullsReader)
// end "/{username}/{reponame}": view milestone, label, issue, pull, etc
m.Group("/{username}/{reponame}/{type:issues}", func() {
@@ -1205,14 +1227,14 @@ func registerRoutes(m *web.Router) {
m.Group("/{username}/{reponame}", func() { // edit issues, pulls, labels, milestones, etc
m.Group("/issues", func() {
m.Group("/new", func() {
- m.Combo("").Get(context.RepoRef(), repo.NewIssue).
+ m.Combo("").Get(repo.NewIssue).
Post(web.Bind(forms.CreateIssueForm{}), repo.NewIssuePost)
- m.Get("/choose", context.RepoRef(), repo.NewIssueChooseTemplate)
+ m.Get("/choose", repo.NewIssueChooseTemplate)
})
m.Get("/search", repo.SearchRepoIssuesJSON)
}, reqUnitIssuesReader)
- addIssuesPullsRoutes := func() {
+ addIssuesPullsUpdateRoutes := func() {
// for "/{username}/{reponame}/issues" or "/{username}/{reponame}/pulls"
m.Group("/{index}", func() {
m.Post("/title", repo.UpdateIssueTitle)
@@ -1231,7 +1253,8 @@ func registerRoutes(m *web.Router) {
m.Post("/add", web.Bind(forms.AddTimeManuallyForm{}), repo.AddTimeManually)
m.Post("/{timeid}/delete", repo.DeleteTime)
m.Group("/stopwatch", func() {
- m.Post("/toggle", repo.IssueStopwatch)
+ m.Post("/start", repo.IssueStartStopwatch)
+ m.Post("/stop", repo.IssueStopStopwatch)
m.Post("/cancel", repo.CancelStopwatch)
})
})
@@ -1255,8 +1278,9 @@ func registerRoutes(m *web.Router) {
m.Delete("/unpin/{index}", reqRepoAdmin, repo.IssueUnpin)
m.Post("/move_pin", reqRepoAdmin, repo.IssuePinMove)
}
- m.Group("/{type:issues}", addIssuesPullsRoutes, reqUnitIssuesReader, context.RepoMustNotBeArchived())
- m.Group("/{type:pulls}", addIssuesPullsRoutes, reqUnitPullsReader, context.RepoMustNotBeArchived())
+ // FIXME: many "pulls" requests are sent to "issues" endpoints incorrectly, so the issue endpoints have to tolerate pull request permissions at the moment
+ m.Group("/{type:issues}", addIssuesPullsUpdateRoutes, context.RequireUnitReader(unit.TypeIssues, unit.TypePullRequests), context.RepoMustNotBeArchived())
+ m.Group("/{type:pulls}", addIssuesPullsUpdateRoutes, reqUnitPullsReader, context.RepoMustNotBeArchived())
m.Group("/comments/{id}", func() {
m.Post("", repo.UpdateCommentContent)
@@ -1269,7 +1293,7 @@ func registerRoutes(m *web.Router) {
m.Post("/edit", web.Bind(forms.CreateLabelForm{}), repo.UpdateLabel)
m.Post("/delete", repo.DeleteLabel)
m.Post("/initialize", web.Bind(forms.InitializeLabelsForm{}), repo.InitializeLabels)
- }, reqRepoIssuesOrPullsWriter, context.RepoRef())
+ }, reqRepoIssuesOrPullsWriter)
m.Group("/milestones", func() {
m.Combo("/new").Get(repo.NewMilestone).
@@ -1278,9 +1302,9 @@ func registerRoutes(m *web.Router) {
m.Post("/{id}/edit", web.Bind(forms.CreateMilestoneForm{}), repo.EditMilestonePost)
m.Post("/{id}/{action}", repo.ChangeMilestoneStatus)
m.Post("/delete", repo.DeleteMilestone)
- }, reqRepoIssuesOrPullsWriter, context.RepoRef())
+ }, reqRepoIssuesOrPullsWriter)
- // FIXME: need to move these routes to the proper place
+ // FIXME: many "pulls" requests are sent to "issues" endpoints incorrectly, need to move these routes to the proper place
m.Group("/issues", func() {
m.Post("/request_review", repo.UpdatePullReviewRequest)
m.Post("/dismiss_review", reqRepoAdmin, web.Bind(forms.DismissReviewForm{}), repo.DismissReview)
@@ -1290,27 +1314,38 @@ func registerRoutes(m *web.Router) {
}, reqSignIn, context.RepoAssignment, context.RepoMustNotBeArchived())
// end "/{username}/{reponame}": create or edit issues, pulls, labels, milestones
- m.Group("/{username}/{reponame}", func() { // repo code
+ m.Group("/{username}/{reponame}", func() { // repo code (at least "code reader")
m.Group("", func() {
m.Group("", func() {
- m.Combo("/_edit/*").Get(repo.EditFile).
- Post(web.Bind(forms.EditRepoFileForm{}), repo.EditFilePost)
- m.Combo("/_new/*").Get(repo.NewFile).
- Post(web.Bind(forms.EditRepoFileForm{}), repo.NewFilePost)
- m.Post("/_preview/*", web.Bind(forms.EditPreviewDiffForm{}), repo.DiffPreviewPost)
- m.Combo("/_delete/*").Get(repo.DeleteFile).
- Post(web.Bind(forms.DeleteRepoFileForm{}), repo.DeleteFilePost)
- m.Combo("/_upload/*", repo.MustBeAbleToUpload).
+ // "GET" requests only need "code reader" permission, "POST" requests need "code writer" permission.
+ // Because reader can "fork and edit"
+ canWriteToBranch := context.CanWriteToBranch()
+ m.Post("/_preview/*", repo.DiffPreviewPost) // read-only, fine with "code reader"
+ m.Post("/_fork/*", repo.ForkToEditPost) // read-only, fork to own repo, fine with "code reader"
+
+ // the path params are used in PrepareCommitFormOptions to construct the correct form action URL
+ m.Combo("/{editor_action:_edit}/*").
+ Get(repo.EditFile).
+ Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.EditFilePost)
+ m.Combo("/{editor_action:_new}/*").
+ Get(repo.EditFile).
+ Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.EditFilePost)
+ m.Combo("/{editor_action:_delete}/*").
+ Get(repo.DeleteFile).
+ Post(web.Bind(forms.DeleteRepoFileForm{}), canWriteToBranch, repo.DeleteFilePost)
+ m.Combo("/{editor_action:_upload}/*", repo.MustBeAbleToUpload).
Get(repo.UploadFile).
- Post(web.Bind(forms.UploadRepoFileForm{}), repo.UploadFilePost)
- m.Combo("/_diffpatch/*").Get(repo.NewDiffPatch).
- Post(web.Bind(forms.EditRepoFileForm{}), repo.NewDiffPatchPost)
- m.Combo("/_cherrypick/{sha:([a-f0-9]{7,64})}/*").Get(repo.CherryPick).
- Post(web.Bind(forms.CherryPickForm{}), repo.CherryPickPost)
- }, context.RepoRefByType(git.RefTypeBranch), context.CanWriteToBranch())
+ Post(web.Bind(forms.UploadRepoFileForm{}), canWriteToBranch, repo.UploadFilePost)
+ m.Combo("/{editor_action:_diffpatch}/*").
+ Get(repo.NewDiffPatch).
+ Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.NewDiffPatchPost)
+ m.Combo("/{editor_action:_cherrypick}/{sha:([a-f0-9]{7,64})}/*").
+ Get(repo.CherryPick).
+ Post(web.Bind(forms.CherryPickForm{}), canWriteToBranch, repo.CherryPickPost)
+ }, context.RepoRefByType(git.RefTypeBranch), repo.WebGitOperationCommonData)
m.Group("", func() {
m.Post("/upload-file", repo.UploadFileToServer)
- m.Post("/upload-remove", web.Bind(forms.RemoveUploadFileForm{}), repo.RemoveUploadFileFromServer)
+ m.Post("/upload-remove", repo.RemoveUploadFileFromServer)
}, repo.MustBeAbleToUpload, reqRepoCodeWriter)
}, repo.MustBeEditable, context.RepoMustNotBeArchived())
@@ -1357,7 +1392,7 @@ func registerRoutes(m *web.Router) {
m.Post("/delete", repo.DeleteRelease)
m.Post("/attachments", repo.UploadReleaseAttachment)
m.Post("/attachments/remove", repo.DeleteAttachment)
- }, reqSignIn, context.RepoMustNotBeArchived(), reqRepoReleaseWriter, context.RepoRef())
+ }, reqSignIn, context.RepoMustNotBeArchived(), reqRepoReleaseWriter)
m.Group("/releases", func() {
m.Get("/edit/*", repo.EditRelease)
m.Post("/edit/*", web.Bind(forms.EditReleaseForm{}), repo.EditReleasePost)
@@ -1383,7 +1418,7 @@ func registerRoutes(m *web.Router) {
m.Group("/{username}/{reponame}/projects", func() {
m.Get("", repo.Projects)
m.Get("/{id}", repo.ViewProject)
- m.Group("", func() { //nolint:dupl
+ m.Group("", func() { //nolint:dupl // duplicates lines 1034-1054
m.Get("/new", repo.RenderNewProject)
m.Post("/new", web.Bind(forms.CreateProjectForm{}), repo.NewProjectPost)
m.Group("/{id}", func() {
@@ -1425,10 +1460,12 @@ func registerRoutes(m *web.Router) {
m.Post("/rerun", reqRepoActionsWriter, actions.Rerun)
m.Get("/logs", actions.Logs)
})
+ m.Get("/workflow", actions.ViewWorkflowFile)
m.Post("/cancel", reqRepoActionsWriter, actions.Cancel)
m.Post("/approve", reqRepoActionsWriter, actions.Approve)
+ m.Post("/delete", reqRepoActionsWriter, actions.Delete)
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() {
@@ -1470,7 +1507,7 @@ func registerRoutes(m *web.Router) {
})
m.Group("/recent-commits", func() {
m.Get("", repo.RecentCommits)
- m.Get("/data", repo.RecentCommitsData)
+ m.Get("/data", repo.CodeFrequencyData) // "recent-commits" also uses the same data as "code-frequency"
})
}, reqUnitCodeReader)
},
@@ -1485,20 +1522,21 @@ func registerRoutes(m *web.Router) {
m.Get("", repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewIssue)
m.Get(".diff", repo.DownloadPullDiff)
m.Get(".patch", repo.DownloadPullPatch)
+ m.Get("/merge_box", repo.ViewPullMergeBox)
m.Group("/commits", func() {
- m.Get("", context.RepoRef(), repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewPullCommits)
- m.Get("/list", context.RepoRef(), repo.GetPullCommits)
- m.Get("/{sha:[a-f0-9]{7,40}}", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForSingleCommit)
+ m.Get("", repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewPullCommits)
+ m.Get("/list", repo.GetPullCommits)
+ m.Get("/{sha:[a-f0-9]{7,40}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForSingleCommit)
})
m.Post("/merge", context.RepoMustNotBeArchived(), web.Bind(forms.MergePullRequestForm{}), repo.MergePullRequest)
m.Post("/cancel_auto_merge", context.RepoMustNotBeArchived(), repo.CancelAutoMergePullRequest)
m.Post("/update", repo.UpdatePullRequest)
m.Post("/set_allow_maintainer_edit", web.Bind(forms.UpdateAllowEditsForm{}), repo.SetAllowEdits)
- m.Post("/cleanup", context.RepoMustNotBeArchived(), context.RepoRef(), repo.CleanUpPullRequest)
+ m.Post("/cleanup", context.RepoMustNotBeArchived(), repo.CleanUpPullRequest)
m.Group("/files", func() {
- m.Get("", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForAllCommitsOfPr)
- m.Get("/{sha:[a-f0-9]{7,40}}", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesStartingFromCommit)
- m.Get("/{shaFrom:[a-f0-9]{7,40}}..{shaTo:[a-f0-9]{7,40}}", context.RepoRef(), repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForRange)
+ m.Get("", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForAllCommitsOfPr)
+ m.Get("/{sha:[a-f0-9]{7,40}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesStartingFromCommit)
+ m.Get("/{shaFrom:[a-f0-9]{7,40}}..{shaTo:[a-f0-9]{7,40}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForRange)
m.Group("/reviews", func() {
m.Get("/new_comment", repo.RenderNewCodeCommentForm)
m.Post("/comments", web.Bind(forms.CodeCommentForm{}), repo.SetShowOutdatedComments, repo.CreateCodeComment)
@@ -1513,7 +1551,7 @@ func registerRoutes(m *web.Router) {
m.Group("/activity_author_data", func() {
m.Get("", repo.ActivityAuthors)
m.Get("/{period}", repo.ActivityAuthors)
- }, context.RepoRef(), repo.MustBeNotEmpty)
+ }, repo.MustBeNotEmpty)
m.Group("/archive", func() {
m.Get("/*", repo.Download)
@@ -1582,18 +1620,22 @@ func registerRoutes(m *web.Router) {
m.Get("/commit/*", context.RepoRefByType(git.RefTypeCommit), repo.Home)
m.Get("/*", context.RepoRefByType(""), repo.Home) // "/*" route is deprecated, and kept for backward compatibility
}, repo.SetEditorconfigIfExists)
+ m.Get("/tree/*", repo.RedirectRepoTreeToSrc) // redirect "/owner/repo/tree/*" requests to "/owner/repo/src/*"
+ m.Get("/blob/*", repo.RedirectRepoBlobToCommit) // redirect "/owner/repo/blob/*" requests to "/owner/repo/src/commit/*"
- m.Get("/forks", context.RepoRef(), repo.Forks)
+ m.Get("/forks", repo.Forks)
m.Get("/commit/{sha:([a-f0-9]{7,64})}.{ext:patch|diff}", repo.MustBeNotEmpty, repo.RawDiff)
m.Post("/lastcommit/*", context.RepoRefByType(git.RefTypeCommit), repo.LastCommit)
}, optSignIn, context.RepoAssignment, reqUnitCodeReader)
// 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
@@ -1617,14 +1659,15 @@ func registerRoutes(m *web.Router) {
m.Group("/devtest", func() {
m.Any("", devtest.List)
m.Any("/fetch-action-test", devtest.FetchActionTest)
- m.Any("/{sub}", devtest.Tmpl)
+ m.Any("/{sub}", devtest.TmplCommon)
+ m.Get("/repo-action-view/{run}/{job}", devtest.MockActionsView)
m.Post("/actions-mock/runs/{run}/jobs/{job}", web.Bind(actions.ViewRequest{}), devtest.MockActionsRunsJobs)
})
}
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)
+ ctx.NotFound(nil)
})
}
diff --git a/routers/web/webfinger.go b/routers/web/webfinger.go
index a87c426b3b..a4c9bf902b 100644
--- a/routers/web/webfinger.go
+++ b/routers/web/webfinger.go
@@ -7,6 +7,7 @@ import (
"fmt"
"net/http"
"net/url"
+ "strconv"
"strings"
user_model "code.gitea.io/gitea/models/user"
@@ -39,7 +40,7 @@ func WebfingerQuery(ctx *context.Context) {
resource, err := url.Parse(ctx.FormTrim("resource"))
if err != nil {
- ctx.Error(http.StatusBadRequest)
+ ctx.HTTPError(http.StatusBadRequest)
return
}
@@ -50,11 +51,11 @@ func WebfingerQuery(ctx *context.Context) {
// allow only the current host
parts := strings.SplitN(resource.Opaque, "@", 2)
if len(parts) != 2 {
- ctx.Error(http.StatusBadRequest)
+ ctx.HTTPError(http.StatusBadRequest)
return
}
if parts[1] != appURL.Host {
- ctx.Error(http.StatusBadRequest)
+ ctx.HTTPError(http.StatusBadRequest)
return
}
@@ -65,30 +66,30 @@ func WebfingerQuery(ctx *context.Context) {
err = user_model.ErrUserNotExist{}
}
default:
- ctx.Error(http.StatusBadRequest)
+ ctx.HTTPError(http.StatusBadRequest)
return
}
if err != nil {
if user_model.IsErrUserNotExist(err) {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
} else {
log.Error("Error getting user: %s Error: %v", resource.Opaque, err)
- ctx.Error(http.StatusInternalServerError)
+ ctx.HTTPError(http.StatusInternalServerError)
}
return
}
if !user_model.IsUserVisibleToViewer(ctx, u, ctx.Doer) {
- ctx.Error(http.StatusNotFound)
+ ctx.HTTPError(http.StatusNotFound)
return
}
aliases := []string{
u.HTMLURL(),
- appURL.String() + "api/v1/activitypub/user-id/" + fmt.Sprint(u.ID),
+ appURL.String() + "api/v1/activitypub/user-id/" + strconv.FormatInt(u.ID, 10),
}
if !u.KeepEmailPrivate {
- aliases = append(aliases, fmt.Sprintf("mailto:%s", u.Email))
+ aliases = append(aliases, "mailto:"+u.Email)
}
links := []*webfingerLink{
@@ -104,7 +105,7 @@ func WebfingerQuery(ctx *context.Context) {
{
Rel: "self",
Type: "application/activity+json",
- Href: appURL.String() + "api/v1/activitypub/user-id/" + fmt.Sprint(u.ID),
+ Href: appURL.String() + "api/v1/activitypub/user-id/" + strconv.FormatInt(u.ID, 10),
},
{
Rel: "http://openid.net/specs/connect/1.0/issuer",