forked from hinterland/hearth
feat: vendor gitea 1.16.2
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
||||
"code.gitea.io/gitea/services/contexttest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestShadowPassword(t *testing.T) {
|
||||
@@ -74,19 +75,29 @@ func TestShadowPassword(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSelfCheckPost(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.PublicURLDetection)()
|
||||
defer test.MockVariableValue(&setting.AppURL, "http://config/sub/")()
|
||||
defer test.MockVariableValue(&setting.AppSubURL, "/sub")()
|
||||
|
||||
ctx, resp := contexttest.MockContext(t, "GET http://host/sub/admin/self_check?location_origin=http://frontend")
|
||||
SelfCheckPost(ctx)
|
||||
assert.Equal(t, http.StatusOK, resp.Code)
|
||||
|
||||
data := struct {
|
||||
Problems []string `json:"problems"`
|
||||
}{}
|
||||
err := json.Unmarshal(resp.Body.Bytes(), &data)
|
||||
assert.NoError(t, err)
|
||||
|
||||
setting.PublicURLDetection = setting.PublicURLLegacy
|
||||
ctx, resp := contexttest.MockContext(t, "GET http://host/sub/admin/self_check?location_origin=http://frontend")
|
||||
SelfCheckPost(ctx)
|
||||
assert.Equal(t, http.StatusOK, resp.Code)
|
||||
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &data))
|
||||
assert.Equal(t, []string{
|
||||
ctx.Locale.TrString("admin.self_check.location_origin_mismatch", "http://frontend/sub/", "http://config/sub/"),
|
||||
}, data.Problems)
|
||||
|
||||
setting.PublicURLDetection = setting.PublicURLAuto
|
||||
ctx, resp = contexttest.MockContext(t, "GET http://host/sub/admin/self_check?location_origin=http://frontend")
|
||||
SelfCheckPost(ctx)
|
||||
assert.Equal(t, http.StatusOK, resp.Code)
|
||||
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &data))
|
||||
assert.Equal(t, []string{
|
||||
ctx.Locale.TrString("admin.self_check.location_origin_mismatch", "http://frontend/sub/", "http://host/sub/"),
|
||||
}, data.Problems)
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ func EditApplicationPost(ctx *context.Context) {
|
||||
|
||||
// ApplicationsRegenerateSecret handles the post request for regenerating the secret
|
||||
func ApplicationsRegenerateSecret(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings")
|
||||
ctx.Data["Title"] = ctx.Tr("settings_title")
|
||||
ctx.Data["PageIsAdminApplications"] = true
|
||||
|
||||
oa := newOAuth2CommonHandlers()
|
||||
|
||||
@@ -136,6 +136,7 @@ func parseLDAPConfig(form forms.AuthenticationForm) *ldap.Source {
|
||||
AttributesInBind: form.AttributesInBind,
|
||||
AttributeSSHPublicKey: form.AttributeSSHPublicKey,
|
||||
AttributeAvatar: form.AttributeAvatar,
|
||||
SSHKeysAreVerified: form.SSHKeysAreVerified,
|
||||
SearchPageSize: pageSize,
|
||||
Filter: form.Filter,
|
||||
GroupsEnabled: form.GroupsEnabled,
|
||||
@@ -272,7 +273,7 @@ func NewAuthSourcePost(ctx *context.Context) {
|
||||
discoveryURL, err := url.Parse(oauth2Config.OpenIDConnectAutoDiscoveryURL)
|
||||
if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") {
|
||||
ctx.Data["Err_DiscoveryURL"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("admin.auths.invalid_openIdConnectAutoDiscoveryURL"), tplAuthNew, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("admin.auths.invalid_openIdConnectAutoDiscoveryURL"), tplAuthNew, form)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -280,13 +281,13 @@ func NewAuthSourcePost(ctx *context.Context) {
|
||||
var err error
|
||||
config, err = parseSSPIConfig(ctx, form)
|
||||
if err != nil {
|
||||
ctx.RenderWithErr(err.Error(), tplAuthNew, form)
|
||||
ctx.RenderWithErrDeprecated(err.Error(), tplAuthNew, form)
|
||||
return
|
||||
}
|
||||
existing, err := db.Find[auth.Source](ctx, auth.FindSourcesOptions{LoginType: auth.SSPI})
|
||||
if err != nil || len(existing) > 0 {
|
||||
ctx.Data["Err_Type"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_of_type_exist"), tplAuthNew, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("admin.auths.login_source_of_type_exist"), tplAuthNew, form)
|
||||
return
|
||||
}
|
||||
default:
|
||||
@@ -310,11 +311,11 @@ func NewAuthSourcePost(ctx *context.Context) {
|
||||
}); err != nil {
|
||||
if auth.IsErrSourceAlreadyExist(err) {
|
||||
ctx.Data["Err_Name"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_exist", err.(auth.ErrSourceAlreadyExist).Name), tplAuthNew, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("admin.auths.login_source_exist", err.(auth.ErrSourceAlreadyExist).Name), tplAuthNew, form)
|
||||
} else if oauth2.IsErrOpenIDConnectInitialize(err) {
|
||||
ctx.Data["Err_DiscoveryURL"] = true
|
||||
unwrapped := err.(oauth2.ErrOpenIDConnectInitialize).Unwrap()
|
||||
ctx.RenderWithErr(ctx.Tr("admin.auths.unable_to_initialize_openid", unwrapped), tplAuthNew, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("admin.auths.unable_to_initialize_openid", unwrapped), tplAuthNew, form)
|
||||
} else {
|
||||
ctx.ServerError("auth.CreateSource", err)
|
||||
}
|
||||
@@ -402,14 +403,14 @@ func EditAuthSourcePost(ctx *context.Context) {
|
||||
discoveryURL, err := url.Parse(oauth2Config.OpenIDConnectAutoDiscoveryURL)
|
||||
if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") {
|
||||
ctx.Data["Err_DiscoveryURL"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("admin.auths.invalid_openIdConnectAutoDiscoveryURL"), tplAuthEdit, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("admin.auths.invalid_openIdConnectAutoDiscoveryURL"), tplAuthEdit, form)
|
||||
return
|
||||
}
|
||||
}
|
||||
case auth.SSPI:
|
||||
config, err = parseSSPIConfig(ctx, form)
|
||||
if err != nil {
|
||||
ctx.RenderWithErr(err.Error(), tplAuthEdit, form)
|
||||
ctx.RenderWithErrDeprecated(err.Error(), tplAuthEdit, form)
|
||||
return
|
||||
}
|
||||
default:
|
||||
@@ -425,7 +426,7 @@ func EditAuthSourcePost(ctx *context.Context) {
|
||||
if err := auth.UpdateSource(ctx, source); err != nil {
|
||||
if auth.IsErrSourceAlreadyExist(err) {
|
||||
ctx.Data["Err_Name"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_exist", err.(auth.ErrSourceAlreadyExist).Name), tplAuthEdit, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("admin.auths.login_source_exist", err.(auth.ErrSourceAlreadyExist).Name), tplAuthEdit, form)
|
||||
} else if oauth2.IsErrOpenIDConnectInitialize(err) {
|
||||
ctx.Flash.Error(err.Error(), true)
|
||||
ctx.Data["Err_DiscoveryURL"] = true
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
// Copyright 2026 The Gitea Authors.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
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/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"
|
||||
)
|
||||
|
||||
const (
|
||||
tplBadges templates.TplName = "admin/badge/list"
|
||||
tplBadgeNew templates.TplName = "admin/badge/new"
|
||||
tplBadgeView templates.TplName = "admin/badge/view"
|
||||
tplBadgeEdit templates.TplName = "admin/badge/edit"
|
||||
tplBadgeUsers templates.TplName = "admin/badge/users"
|
||||
)
|
||||
|
||||
// BadgeSearchDefaultAdminSort is the default sort type for admin view
|
||||
const BadgeSearchDefaultAdminSort = "oldest"
|
||||
|
||||
// Badges show all the badges
|
||||
func Badges(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.badges")
|
||||
ctx.Data["PageIsAdminBadges"] = true
|
||||
|
||||
RenderBadgeSearch(ctx, &user_model.SearchBadgeOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
Page: max(ctx.FormInt("page"), 1),
|
||||
PageSize: setting.UI.Admin.UserPagingNum,
|
||||
},
|
||||
}, tplBadges)
|
||||
}
|
||||
|
||||
// NewBadge render adding a new badge
|
||||
func NewBadge(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.badges.new_badge")
|
||||
ctx.Data["PageIsAdminBadges"] = true
|
||||
|
||||
ctx.HTML(http.StatusOK, tplBadgeNew)
|
||||
}
|
||||
|
||||
// NewBadgePost response for adding a new badge
|
||||
func NewBadgePost(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.AdminCreateBadgeForm)
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.JSONError(ctx.GetErrMsg())
|
||||
return
|
||||
}
|
||||
|
||||
b := &user_model.Badge{
|
||||
Slug: form.Slug,
|
||||
Description: form.Description,
|
||||
ImageURL: form.ImageURL,
|
||||
}
|
||||
|
||||
if err := user_model.CreateBadge(ctx, b); err != nil {
|
||||
if errors.Is(err, util.ErrAlreadyExist) {
|
||||
ctx.JSONError(ctx.Tr("admin.badges.slug_been_taken"))
|
||||
} else {
|
||||
ctx.ServerError("CreateBadge", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace("Badge created by admin (%s): %s", ctx.Doer.Name, b.Slug)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("admin.badges.new_success", b.Slug))
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/-/admin/badges/slug/" + url.PathEscape(b.Slug))
|
||||
}
|
||||
|
||||
func prepareBadgeInfo(ctx *context.Context) *user_model.Badge {
|
||||
b, err := user_model.GetBadge(ctx, ctx.PathParam("badge_slug"))
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/badges")
|
||||
} else {
|
||||
ctx.ServerError("GetBadge", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
ctx.Data["Badge"] = b
|
||||
return b
|
||||
}
|
||||
|
||||
func ViewBadge(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.badges.details")
|
||||
ctx.Data["PageIsAdminBadges"] = true
|
||||
|
||||
prepareBadgeInfo(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
badge := ctx.Data["Badge"].(*user_model.Badge)
|
||||
opts := &user_model.GetBadgeUsersOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
Page: 1,
|
||||
PageSize: setting.UI.Admin.UserPagingNum,
|
||||
},
|
||||
BadgeSlug: badge.Slug,
|
||||
}
|
||||
users, count, err := user_model.GetBadgeUsers(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetBadgeUsers", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Users"] = users
|
||||
ctx.Data["UsersTotal"] = int(count)
|
||||
|
||||
ctx.HTML(http.StatusOK, tplBadgeView)
|
||||
}
|
||||
|
||||
// EditBadge show editing badge page
|
||||
func EditBadge(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.badges.edit_badge")
|
||||
ctx.Data["PageIsAdminBadges"] = true
|
||||
prepareBadgeInfo(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplBadgeEdit)
|
||||
}
|
||||
|
||||
// EditBadgePost response for editing badge
|
||||
func EditBadgePost(ctx *context.Context) {
|
||||
b := prepareBadgeInfo(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*forms.AdminEditBadgeForm)
|
||||
if ctx.HasError() {
|
||||
ctx.JSONError(ctx.GetErrMsg())
|
||||
return
|
||||
}
|
||||
|
||||
b.ImageURL = form.ImageURL
|
||||
b.Description = form.Description
|
||||
|
||||
if err := user_model.UpdateBadge(ctx, b); err != nil {
|
||||
ctx.ServerError("UpdateBadge", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace("Badge updated by admin (%s): %s", ctx.Doer.Name, b.Slug)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("admin.badges.update_success"))
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/-/admin/badges/slug/" + url.PathEscape(ctx.PathParam("badge_slug")))
|
||||
}
|
||||
|
||||
// DeleteBadge response for deleting a badge
|
||||
func DeleteBadge(ctx *context.Context) {
|
||||
b, err := user_model.GetBadge(ctx, ctx.PathParam("badge_slug"))
|
||||
if err != nil {
|
||||
ctx.ServerError("GetBadge", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = user_model.DeleteBadge(ctx, b); err != nil {
|
||||
ctx.ServerError("DeleteBadge", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace("Badge deleted by admin (%s): %s", ctx.Doer.Name, b.Slug)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("admin.badges.deletion_success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/badges")
|
||||
}
|
||||
|
||||
func BadgeUsers(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.badges.users_with_badge", ctx.PathParam("badge_slug"))
|
||||
ctx.Data["PageIsAdminBadges"] = true
|
||||
|
||||
page := max(ctx.FormInt("page"), 1)
|
||||
|
||||
badge := &user_model.Badge{Slug: ctx.PathParam("badge_slug")}
|
||||
opts := &user_model.GetBadgeUsersOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
Page: page,
|
||||
PageSize: setting.UI.Admin.UserPagingNum,
|
||||
},
|
||||
BadgeSlug: badge.Slug,
|
||||
}
|
||||
users, count, err := user_model.GetBadgeUsers(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetBadgeUsers", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Users"] = users
|
||||
ctx.Data["Total"] = count
|
||||
ctx.Data["Page"] = context.NewPagination(count, setting.UI.Admin.UserPagingNum, page, 5)
|
||||
|
||||
ctx.HTML(http.StatusOK, tplBadgeUsers)
|
||||
}
|
||||
|
||||
// BadgeUsersPost response for actions for user badges
|
||||
func BadgeUsersPost(ctx *context.Context) {
|
||||
name := strings.ToLower(ctx.FormString("user"))
|
||||
|
||||
u, err := user_model.GetUserByName(ctx, name)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.Flash.Error(ctx.Tr("form.user_not_exist"))
|
||||
ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath())
|
||||
} else {
|
||||
ctx.ServerError("GetUserByName", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err = user_model.AddUserBadge(ctx, u, &user_model.Badge{Slug: ctx.PathParam("badge_slug")}); err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.Flash.Error(ctx.Tr("admin.badges.not_found"))
|
||||
ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath())
|
||||
} else if errors.Is(err, util.ErrAlreadyExist) {
|
||||
ctx.Flash.Error(ctx.Tr("admin.badges.user_already_has"))
|
||||
ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath())
|
||||
} else {
|
||||
ctx.ServerError("AddUserBadge", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("admin.badges.user_add_success"))
|
||||
ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath())
|
||||
}
|
||||
|
||||
// DeleteBadgeUser delete a badge from a user
|
||||
func DeleteBadgeUser(ctx *context.Context) {
|
||||
badgeUsersURL := setting.AppSubURL + "/-/admin/badges/slug/" + url.PathEscape(ctx.PathParam("badge_slug")) + "/users"
|
||||
|
||||
user, err := user_model.GetUserByID(ctx, ctx.FormInt64("id"))
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.Flash.Error(ctx.Tr("form.user_not_exist"))
|
||||
ctx.JSONRedirect(badgeUsersURL)
|
||||
return
|
||||
} else {
|
||||
ctx.ServerError("GetUserByID", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := user_model.RemoveUserBadge(ctx, user, &user_model.Badge{Slug: ctx.PathParam("badge_slug")}); err == nil {
|
||||
ctx.Flash.Success(ctx.Tr("admin.badges.user_remove_success"))
|
||||
} else {
|
||||
ctx.ServerError("RemoveUserBadge", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSONRedirect(badgeUsersURL)
|
||||
}
|
||||
|
||||
func RenderBadgeSearch(ctx *context.Context, opts *user_model.SearchBadgeOptions, tplName templates.TplName) {
|
||||
var (
|
||||
badges []*user_model.Badge
|
||||
count int64
|
||||
err error
|
||||
orderBy db.SearchOrderBy
|
||||
)
|
||||
|
||||
sortOrder := ctx.FormString("sort")
|
||||
if sortOrder == "" {
|
||||
sortOrder = BadgeSearchDefaultAdminSort
|
||||
}
|
||||
ctx.Data["SortType"] = sortOrder
|
||||
|
||||
switch sortOrder {
|
||||
case "newest":
|
||||
orderBy = "`badge`.id DESC"
|
||||
case "oldest":
|
||||
orderBy = "`badge`.id ASC"
|
||||
case "reversealphabetically":
|
||||
orderBy = "`badge`.slug DESC"
|
||||
case "alphabetically":
|
||||
orderBy = "`badge`.slug ASC"
|
||||
default:
|
||||
// In case the sort type is invalid, keep admin default sorting.
|
||||
ctx.Data["SortType"] = "oldest"
|
||||
orderBy = "`badge`.id ASC"
|
||||
}
|
||||
|
||||
opts.Keyword = ctx.FormTrim("q")
|
||||
opts.OrderBy = orderBy
|
||||
if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) {
|
||||
badges, count, err = user_model.SearchBadges(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("SearchBadges", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data["Keyword"] = opts.Keyword
|
||||
ctx.Data["Total"] = count
|
||||
ctx.Data["Badges"] = badges
|
||||
|
||||
pager := context.NewPagination(count, opts.PageSize, opts.Page, 5)
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
ctx.HTML(http.StatusOK, tplName)
|
||||
}
|
||||
@@ -5,9 +5,9 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
system_model "code.gitea.io/gitea/models/system"
|
||||
@@ -123,10 +123,7 @@ func Config(ctx *context.Context) {
|
||||
ctx.Data["PageIsAdminConfigSummary"] = true
|
||||
|
||||
ctx.Data["CustomConf"] = setting.CustomConf
|
||||
ctx.Data["AppUrl"] = setting.AppURL
|
||||
ctx.Data["AppBuiltWith"] = setting.AppBuiltWith
|
||||
ctx.Data["Domain"] = setting.Domain
|
||||
ctx.Data["OfflineMode"] = setting.OfflineMode
|
||||
ctx.Data["RunUser"] = setting.RunUser
|
||||
ctx.Data["RunMode"] = util.ToTitleCase(setting.RunMode)
|
||||
ctx.Data["GitVersion"] = git.DefaultFeatures().VersionInfo()
|
||||
@@ -145,7 +142,6 @@ func Config(ctx *context.Context) {
|
||||
ctx.Data["Service"] = setting.Service
|
||||
ctx.Data["DbCfg"] = setting.Database
|
||||
ctx.Data["Webhook"] = setting.Webhook
|
||||
|
||||
ctx.Data["MailerEnabled"] = false
|
||||
if setting.MailService != nil {
|
||||
ctx.Data["MailerEnabled"] = true
|
||||
@@ -191,52 +187,27 @@ func ConfigSettings(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.config_settings")
|
||||
ctx.Data["PageIsAdminConfig"] = true
|
||||
ctx.Data["PageIsAdminConfigSettings"] = true
|
||||
ctx.Data["DefaultOpenWithEditorAppsString"] = setting.DefaultOpenWithEditorApps().ToTextareaString()
|
||||
ctx.HTML(http.StatusOK, tplConfigSettings)
|
||||
}
|
||||
|
||||
func validateConfigKeyValue(dynKey, input string) error {
|
||||
opt := config.GetConfigOption(dynKey)
|
||||
if opt == nil {
|
||||
return util.NewInvalidArgumentErrorf("unknown config key: %s", dynKey)
|
||||
}
|
||||
|
||||
const limit = 64 * 1024
|
||||
if len(input) > limit {
|
||||
return util.NewInvalidArgumentErrorf("value length exceeds limit of %d", limit)
|
||||
}
|
||||
|
||||
if !json.Valid([]byte(input)) {
|
||||
return util.NewInvalidArgumentErrorf("invalid json value for key: %s", dynKey)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ChangeConfig(ctx *context.Context) {
|
||||
cfg := setting.Config()
|
||||
|
||||
marshalBool := func(v string) ([]byte, error) {
|
||||
b, _ := strconv.ParseBool(v)
|
||||
return json.Marshal(b)
|
||||
}
|
||||
|
||||
marshalString := func(emptyDefault string) func(v string) ([]byte, error) {
|
||||
return func(v string) ([]byte, error) {
|
||||
return json.Marshal(util.IfZero(v, emptyDefault))
|
||||
}
|
||||
}
|
||||
|
||||
marshalOpenWithApps := func(value string) ([]byte, error) {
|
||||
// TODO: move the block alongside OpenWithEditorAppsType.ToTextareaString
|
||||
lines := strings.Split(value, "\n")
|
||||
var openWithEditorApps setting.OpenWithEditorAppsType
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
displayName, openURL, ok := strings.Cut(line, "=")
|
||||
displayName, openURL = strings.TrimSpace(displayName), strings.TrimSpace(openURL)
|
||||
if !ok || displayName == "" || openURL == "" {
|
||||
continue
|
||||
}
|
||||
openWithEditorApps = append(openWithEditorApps, setting.OpenWithEditorApp{
|
||||
DisplayName: strings.TrimSpace(displayName),
|
||||
OpenURL: strings.TrimSpace(openURL),
|
||||
})
|
||||
}
|
||||
return json.Marshal(openWithEditorApps)
|
||||
}
|
||||
marshallers := map[string]func(string) ([]byte, error){
|
||||
cfg.Picture.DisableGravatar.DynKey(): marshalBool,
|
||||
cfg.Picture.EnableFederatedAvatar.DynKey(): marshalBool,
|
||||
cfg.Repository.OpenWithEditorApps.DynKey(): marshalOpenWithApps,
|
||||
cfg.Repository.GitGuideRemoteName.DynKey(): marshalString(cfg.Repository.GitGuideRemoteName.DefaultValue()),
|
||||
}
|
||||
|
||||
_ = ctx.Req.ParseForm()
|
||||
configKeys := ctx.Req.Form["key"]
|
||||
configValues := ctx.Req.Form["value"]
|
||||
@@ -249,18 +220,16 @@ loop:
|
||||
}
|
||||
value := configValues[i]
|
||||
|
||||
marshaller, hasMarshaller := marshallers[key]
|
||||
if !hasMarshaller {
|
||||
ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key))
|
||||
break loop
|
||||
}
|
||||
|
||||
marshaledValue, err := marshaller(value)
|
||||
err := validateConfigKeyValue(key, value)
|
||||
if err != nil {
|
||||
ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key))
|
||||
if errors.Is(err, util.ErrInvalidArgument) {
|
||||
ctx.JSONError(err.Error())
|
||||
} else {
|
||||
ctx.JSONError(ctx.Tr("admin.config.set_setting_failed", key))
|
||||
}
|
||||
break loop
|
||||
}
|
||||
configSettings[key] = string(marshaledValue)
|
||||
configSettings[key] = value
|
||||
}
|
||||
if ctx.Written() {
|
||||
return
|
||||
|
||||
@@ -18,10 +18,10 @@ import (
|
||||
func MonitorDiagnosis(ctx *context.Context) {
|
||||
seconds := min(max(ctx.FormInt64("seconds"), 1), 300)
|
||||
|
||||
httplib.ServeSetHeaders(ctx.Resp, &httplib.ServeHeaderOptions{
|
||||
ContentType: "application/zip",
|
||||
Disposition: "attachment",
|
||||
Filename: fmt.Sprintf("gitea-diagnosis-%s.zip", time.Now().Format("20060102-150405")),
|
||||
httplib.ServeSetHeaders(ctx.Resp, httplib.ServeHeaderOptions{
|
||||
ContentType: "application/zip",
|
||||
Filename: fmt.Sprintf("gitea-diagnosis-%s.zip", time.Now().Format("20060102-150405")),
|
||||
ContentDisposition: httplib.ContentDispositionAttachment,
|
||||
})
|
||||
|
||||
zipWriter := zip.NewWriter(ctx.Resp)
|
||||
|
||||
@@ -93,7 +93,7 @@ func Emails(ctx *context.Context) {
|
||||
ctx.Data["Total"] = count
|
||||
ctx.Data["Emails"] = emails
|
||||
|
||||
pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
|
||||
pager := context.NewPagination(count, opts.PageSize, opts.Page, 5)
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ func Notices(ctx *context.Context) {
|
||||
|
||||
ctx.Data["Total"] = total
|
||||
|
||||
ctx.Data["Page"] = context.NewPagination(int(total), setting.UI.Admin.NoticePagingNum, page, 5)
|
||||
ctx.Data["Page"] = context.NewPagination(total, setting.UI.Admin.NoticePagingNum, page, 5)
|
||||
|
||||
ctx.HTML(http.StatusOK, tplNotices)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ func Organizations(ctx *context.Context) {
|
||||
|
||||
explore.RenderUserSearch(ctx, user_model.SearchUserOptions{
|
||||
Actor: ctx.Doer,
|
||||
Type: user_model.UserTypeOrganization,
|
||||
Types: []user_model.UserType{user_model.UserTypeOrganization},
|
||||
IncludeReserved: true, // administrator needs to list all accounts include reserved
|
||||
ListOptions: db.ListOptions{
|
||||
PageSize: setting.UI.Admin.OrgPagingNum,
|
||||
|
||||
@@ -73,7 +73,7 @@ func Packages(ctx *context.Context) {
|
||||
ctx.Data["TotalBlobSize"] = totalBlobSize - totalUnreferencedBlobSize
|
||||
ctx.Data["TotalUnreferencedBlobSize"] = totalUnreferencedBlobSize
|
||||
|
||||
pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5)
|
||||
pager := context.NewPagination(total, setting.UI.PackagesPagingNum, page, 5)
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
@@ -93,7 +93,7 @@ func DeletePackageVersion(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("packages.settings.delete.success"))
|
||||
ctx.Flash.Success(ctx.Tr("packages.settings.delete.version.success"))
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/-/admin/packages?page=" + url.QueryEscape(ctx.FormString("page")) + "&q=" + url.QueryEscape(ctx.FormString("q")) + "&type=" + url.QueryEscape(ctx.FormString("type")))
|
||||
}
|
||||
|
||||
|
||||
@@ -11,10 +11,10 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"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/routers/web/explore"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
@@ -134,12 +134,12 @@ func AdoptOrDeleteRepository(ctx *context.Context) {
|
||||
ctx.ServerError("IsRepositoryExist", err)
|
||||
return
|
||||
}
|
||||
isDir, err := util.IsDir(repo_model.RepoPath(ctxUser.Name, repoName))
|
||||
exist, err := gitrepo.IsRepositoryExist(ctx, repo_model.StorageRepo(repo_model.RelativePath(ctxUser.Name, repoName)))
|
||||
if err != nil {
|
||||
ctx.ServerError("IsDir", err)
|
||||
return
|
||||
}
|
||||
if has || !isDir {
|
||||
if has || !exist {
|
||||
// Fallthrough to failure mode
|
||||
} else if action == "adopt" {
|
||||
if _, err := repo_service.AdoptRepository(ctx, ctx.Doer, ctxUser, repo_service.CreateRepoOptions{
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
func RedirectToDefaultSetting(ctx *context.Context) {
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/actions/runners")
|
||||
}
|
||||
@@ -67,7 +67,7 @@ func Users(ctx *context.Context) {
|
||||
|
||||
explore.RenderUserSearch(ctx, user_model.SearchUserOptions{
|
||||
Actor: ctx.Doer,
|
||||
Type: user_model.UserTypeIndividual,
|
||||
Types: []user_model.UserType{user_model.UserTypeIndividual},
|
||||
ListOptions: db.ListOptions{
|
||||
PageSize: setting.UI.Admin.UserPagingNum,
|
||||
},
|
||||
@@ -151,12 +151,12 @@ func NewUserPost(ctx *context.Context) {
|
||||
if u.LoginType == auth.NoType || u.LoginType == auth.Plain {
|
||||
if len(form.Password) < setting.MinPasswordLength {
|
||||
ctx.Data["Err_Password"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplUserNew, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplUserNew, &form)
|
||||
return
|
||||
}
|
||||
if !password.IsComplexEnough(form.Password) {
|
||||
ctx.Data["Err_Password"] = true
|
||||
ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplUserNew, &form)
|
||||
ctx.RenderWithErrDeprecated(password.BuildComplexityError(ctx.Locale), tplUserNew, &form)
|
||||
return
|
||||
}
|
||||
if err := password.IsPwned(ctx, form.Password); err != nil {
|
||||
@@ -166,7 +166,7 @@ func NewUserPost(ctx *context.Context) {
|
||||
log.Error(err.Error())
|
||||
errMsg = ctx.Tr("auth.password_pwned_err")
|
||||
}
|
||||
ctx.RenderWithErr(errMsg, tplUserNew, &form)
|
||||
ctx.RenderWithErrDeprecated(errMsg, tplUserNew, &form)
|
||||
return
|
||||
}
|
||||
u.MustChangePassword = form.MustChangePassword
|
||||
@@ -176,22 +176,22 @@ func NewUserPost(ctx *context.Context) {
|
||||
switch {
|
||||
case user_model.IsErrUserAlreadyExist(err):
|
||||
ctx.Data["Err_UserName"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplUserNew, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.username_been_taken"), tplUserNew, &form)
|
||||
case user_model.IsErrEmailAlreadyUsed(err):
|
||||
ctx.Data["Err_Email"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserNew, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.email_been_used"), tplUserNew, &form)
|
||||
case user_model.IsErrEmailInvalid(err), user_model.IsErrEmailCharIsNotSupported(err):
|
||||
ctx.Data["Err_Email"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserNew, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.email_invalid"), tplUserNew, &form)
|
||||
case db.IsErrNameReserved(err):
|
||||
ctx.Data["Err_UserName"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("user.form.name_reserved", err.(db.ErrNameReserved).Name), tplUserNew, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_reserved", err.(db.ErrNameReserved).Name), tplUserNew, &form)
|
||||
case db.IsErrNamePatternNotAllowed(err):
|
||||
ctx.Data["Err_UserName"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplUserNew, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplUserNew, &form)
|
||||
case db.IsErrNameCharsNotAllowed(err):
|
||||
ctx.Data["Err_UserName"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("user.form.name_chars_not_allowed", err.(db.ErrNameCharsNotAllowed).Name), tplUserNew, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_chars_not_allowed", err.(db.ErrNameCharsNotAllowed).Name), tplUserNew, &form)
|
||||
default:
|
||||
ctx.ServerError("CreateUser", err)
|
||||
}
|
||||
@@ -345,23 +345,23 @@ func EditUserPost(ctx *context.Context) {
|
||||
}
|
||||
|
||||
if form.UserName != "" {
|
||||
if err := user_service.RenameUser(ctx, u, form.UserName); err != nil {
|
||||
if err := user_service.RenameUser(ctx, u, form.UserName, ctx.Doer); err != nil {
|
||||
switch {
|
||||
case user_model.IsErrUserIsNotLocal(err):
|
||||
ctx.Data["Err_UserName"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("form.username_change_not_local_user"), tplUserEdit, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.username_change_not_local_user"), tplUserEdit, &form)
|
||||
case user_model.IsErrUserAlreadyExist(err):
|
||||
ctx.Data["Err_UserName"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplUserEdit, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.username_been_taken"), tplUserEdit, &form)
|
||||
case db.IsErrNameReserved(err):
|
||||
ctx.Data["Err_UserName"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("user.form.name_reserved", form.UserName), tplUserEdit, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_reserved", form.UserName), tplUserEdit, &form)
|
||||
case db.IsErrNamePatternNotAllowed(err):
|
||||
ctx.Data["Err_UserName"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", form.UserName), tplUserEdit, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_pattern_not_allowed", form.UserName), tplUserEdit, &form)
|
||||
case db.IsErrNameCharsNotAllowed(err):
|
||||
ctx.Data["Err_UserName"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("user.form.name_chars_not_allowed", form.UserName), tplUserEdit, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_chars_not_allowed", form.UserName), tplUserEdit, &form)
|
||||
default:
|
||||
ctx.ServerError("RenameUser", err)
|
||||
}
|
||||
@@ -392,16 +392,16 @@ func EditUserPost(ctx *context.Context) {
|
||||
switch {
|
||||
case errors.Is(err, password.ErrMinLength):
|
||||
ctx.Data["Err_Password"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplUserEdit, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplUserEdit, &form)
|
||||
case errors.Is(err, password.ErrComplexity):
|
||||
ctx.Data["Err_Password"] = true
|
||||
ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplUserEdit, &form)
|
||||
ctx.RenderWithErrDeprecated(password.BuildComplexityError(ctx.Locale), tplUserEdit, &form)
|
||||
case errors.Is(err, password.ErrIsPwned):
|
||||
ctx.Data["Err_Password"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("auth.password_pwned", "https://haveibeenpwned.com/Passwords"), tplUserEdit, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_pwned", "https://haveibeenpwned.com/Passwords"), tplUserEdit, &form)
|
||||
case password.IsErrIsPwnedRequest(err):
|
||||
ctx.Data["Err_Password"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplUserEdit, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_pwned_err"), tplUserEdit, &form)
|
||||
default:
|
||||
ctx.ServerError("UpdateUser", err)
|
||||
}
|
||||
@@ -413,10 +413,10 @@ func EditUserPost(ctx *context.Context) {
|
||||
switch {
|
||||
case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err):
|
||||
ctx.Data["Err_Email"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplUserEdit, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.email_invalid"), tplUserEdit, &form)
|
||||
case user_model.IsErrEmailAlreadyUsed(err):
|
||||
ctx.Data["Err_Email"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplUserEdit, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.email_been_used"), tplUserEdit, &form)
|
||||
default:
|
||||
ctx.ServerError("AddOrSetPrimaryEmailAddress", err)
|
||||
}
|
||||
@@ -444,7 +444,7 @@ func EditUserPost(ctx *context.Context) {
|
||||
|
||||
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
|
||||
if user_model.IsErrDeleteLastAdminUser(err) {
|
||||
ctx.RenderWithErr(ctx.Tr("auth.last_admin"), tplUserEdit, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("auth.last_admin"), tplUserEdit, &form)
|
||||
} else {
|
||||
ctx.ServerError("UpdateUser", err)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ var (
|
||||
func TwoFactor(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("twofa")
|
||||
|
||||
if CheckAutoLogin(ctx) {
|
||||
if performAutoLogin(ctx) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -92,14 +92,14 @@ func TwoFactorPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.RenderWithErr(ctx.Tr("auth.twofa_passcode_incorrect"), tplTwofa, forms.TwoFactorAuthForm{})
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("auth.twofa_passcode_incorrect"), tplTwofa, forms.TwoFactorAuthForm{})
|
||||
}
|
||||
|
||||
// TwoFactorScratch shows the scratch code form for two-factor authentication.
|
||||
func TwoFactorScratch(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("twofa_scratch")
|
||||
|
||||
if CheckAutoLogin(ctx) {
|
||||
if performAutoLogin(ctx) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ func TwoFactorScratchPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
handleSignInFull(ctx, u, remember, false)
|
||||
handleSignInFull(ctx, u, remember)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
@@ -160,5 +160,5 @@ func TwoFactorScratchPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.RenderWithErr(ctx.Tr("auth.twofa_scratch_token_incorrect"), tplTwofaScratch, forms.TwoFactorScratchAuthForm{})
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("auth.twofa_scratch_token_incorrect"), tplTwofaScratch, forms.TwoFactorScratchAuthForm{})
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
@@ -44,6 +45,33 @@ const (
|
||||
TplActivatePrompt templates.TplName = "user/auth/activate_prompt" // for showing a message for user activation
|
||||
)
|
||||
|
||||
type CommonAuthOptions struct {
|
||||
EnableCaptcha bool
|
||||
}
|
||||
|
||||
func prepareCommonAuthPageData(ctx *context.Context, opt CommonAuthOptions) {
|
||||
ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
|
||||
ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth
|
||||
|
||||
// for OpenID Connect
|
||||
ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
|
||||
ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
|
||||
|
||||
if opt.EnableCaptcha {
|
||||
ctx.Data["EnableCaptcha"] = true
|
||||
ctx.Data["RecaptchaAPIScriptURL"] = strings.TrimSuffix(setting.Service.RecaptchaURL, "/") + "/api.js"
|
||||
ctx.Data["CaptchaType"] = setting.Service.CaptchaType
|
||||
ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
|
||||
ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
|
||||
ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey
|
||||
ctx.Data["McaptchaURL"] = strings.TrimSuffix(setting.Service.McaptchaURL, "/")
|
||||
ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey
|
||||
if setting.Service.CaptchaType == setting.ImageCaptcha {
|
||||
ctx.Data["Captcha"] = context.GetImageCaptcha()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// autoSignIn reads cookie and try to auto-login.
|
||||
func autoSignIn(ctx *context.Context) (bool, error) {
|
||||
isSucceed := false
|
||||
@@ -102,7 +130,6 @@ func autoSignIn(ctx *context.Context) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
ctx.Csrf.PrepareForSessionUser(ctx)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -127,20 +154,52 @@ func resetLocale(ctx *context.Context, u *user_model.User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func RedirectAfterLogin(ctx *context.Context) {
|
||||
func rememberAuthRedirectLink(ctx *context.Context) {
|
||||
redirectTo := ctx.FormString("redirect_to")
|
||||
if redirectTo == "" {
|
||||
redirectTo = ctx.GetSiteCookie("redirect_to")
|
||||
if ref, err := url.Parse(ctx.Req.Referer()); err == nil && httplib.IsCurrentGiteaSiteURL(ctx, ctx.Req.Referer()) {
|
||||
// the request paths starting with "/user/" are either:
|
||||
// * auth related pages: don't redirect back to them
|
||||
// * user settings pages: they have "require sign-in" protection already, no "referer redirect" would happen
|
||||
skipRefererRedirect := strings.HasPrefix(ref.Path, setting.AppSubURL+"/user/")
|
||||
if !skipRefererRedirect {
|
||||
redirectTo = ref.RequestURI()
|
||||
}
|
||||
}
|
||||
}
|
||||
middleware.DeleteRedirectToCookie(ctx.Resp)
|
||||
nextRedirectTo := setting.AppSubURL + string(setting.LandingPageURL)
|
||||
if setting.LandingPageURL == setting.LandingPageLogin {
|
||||
nextRedirectTo = setting.AppSubURL + "/" // do not cycle-redirect to the login page
|
||||
if redirectTo != "" {
|
||||
middleware.SetRedirectToCookie(ctx.Resp, redirectTo)
|
||||
}
|
||||
ctx.RedirectToCurrentSite(redirectTo, nextRedirectTo)
|
||||
}
|
||||
|
||||
func CheckAutoLogin(ctx *context.Context) bool {
|
||||
func consumeAuthRedirectLink(ctx *context.Context) string {
|
||||
redirects := []string{ctx.FormString("redirect_to"), middleware.GetRedirectToCookie(ctx.Req)}
|
||||
middleware.DeleteRedirectToCookie(ctx.Resp)
|
||||
if setting.LandingPageURL == setting.LandingPageLogin {
|
||||
redirects = append(redirects, setting.AppSubURL+"/") // do not cycle-redirect to the login page
|
||||
} else {
|
||||
redirects = append(redirects, setting.AppSubURL+string(setting.LandingPageURL))
|
||||
}
|
||||
for _, link := range redirects {
|
||||
if link != "" && httplib.IsCurrentGiteaSiteURL(ctx, link) {
|
||||
return link
|
||||
}
|
||||
}
|
||||
return setting.AppSubURL + "/"
|
||||
}
|
||||
|
||||
func redirectAfterAuth(ctx *context.Context) {
|
||||
if setting.Config().Instance.MaintenanceMode.Value(ctx).IsActive() {
|
||||
// in maintenance mode, redirect to admin dashboard, it is the only accessible page
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin")
|
||||
return
|
||||
}
|
||||
ctx.RedirectToCurrentSite(consumeAuthRedirectLink(ctx))
|
||||
}
|
||||
|
||||
func performAutoLogin(ctx *context.Context) bool {
|
||||
rememberAuthRedirectLink(ctx)
|
||||
|
||||
isSucceed, err := autoSignIn(ctx) // try to auto-login
|
||||
if err != nil {
|
||||
if errors.Is(err, auth_service.ErrAuthTokenInvalidHash) {
|
||||
@@ -151,45 +210,73 @@ func CheckAutoLogin(ctx *context.Context) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
redirectTo := ctx.FormString("redirect_to")
|
||||
if len(redirectTo) > 0 {
|
||||
middleware.SetRedirectToCookie(ctx.Resp, redirectTo)
|
||||
}
|
||||
|
||||
if isSucceed {
|
||||
RedirectAfterLogin(ctx)
|
||||
redirectAfterAuth(ctx)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func prepareSignInPageData(ctx *context.Context) {
|
||||
func performAutoLoginOAuth2(ctx *context.Context, data *preparedSignInData) bool {
|
||||
// If only 1 OAuth provider is present and other login methods are disabled, redirect to the OAuth provider.
|
||||
onlySingleOAuth2 := len(data.oauth2Providers) == 1 &&
|
||||
!setting.Service.EnablePasswordSignInForm &&
|
||||
!setting.Service.EnableOpenIDSignIn &&
|
||||
!setting.Service.EnablePasskeyAuth &&
|
||||
!data.enableSSPI
|
||||
|
||||
if !onlySingleOAuth2 {
|
||||
return false
|
||||
}
|
||||
|
||||
skipToOAuthURL := setting.AppSubURL + "/user/oauth2/" + url.PathEscape(data.oauth2Providers[0].DisplayName())
|
||||
if redirectTo := ctx.FormString("redirect_to"); redirectTo != "" {
|
||||
skipToOAuthURL += "?redirect_to=" + url.QueryEscape(redirectTo)
|
||||
}
|
||||
ctx.Redirect(skipToOAuthURL)
|
||||
return true
|
||||
}
|
||||
|
||||
type preparedSignInData struct {
|
||||
oauth2Providers []oauth2.Provider
|
||||
enableSSPI bool
|
||||
}
|
||||
|
||||
func prepareSignInPageData(ctx *context.Context) (ret preparedSignInData) {
|
||||
var err error
|
||||
ret.enableSSPI = auth.IsSSPIEnabled(ctx)
|
||||
ret.oauth2Providers, err = oauth2.GetOAuth2Providers(ctx, optional.Some(true))
|
||||
if err != nil {
|
||||
log.Error("Failed to get OAuth2 providers: %v", err)
|
||||
}
|
||||
ctx.Data["Title"] = ctx.Tr("sign_in")
|
||||
ctx.Data["OAuth2Providers"], _ = oauth2.GetOAuth2Providers(ctx, optional.Some(true))
|
||||
ctx.Data["OAuth2Providers"] = ret.oauth2Providers
|
||||
ctx.Data["Title"] = ctx.Tr("sign_in")
|
||||
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login"
|
||||
ctx.Data["PageIsSignIn"] = true
|
||||
ctx.Data["PageIsLogin"] = true
|
||||
ctx.Data["EnableSSPI"] = auth.IsSSPIEnabled(ctx)
|
||||
ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
|
||||
ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth
|
||||
ctx.Data["EnableSSPI"] = ret.enableSSPI
|
||||
|
||||
if setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin {
|
||||
context.SetCaptchaData(ctx)
|
||||
}
|
||||
prepareCommonAuthPageData(ctx, CommonAuthOptions{
|
||||
EnableCaptcha: setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin,
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
// SignIn render sign in page
|
||||
func SignIn(ctx *context.Context) {
|
||||
if CheckAutoLogin(ctx) {
|
||||
if performAutoLogin(ctx) {
|
||||
return
|
||||
}
|
||||
if ctx.IsSigned {
|
||||
RedirectAfterLogin(ctx)
|
||||
redirectAfterAuth(ctx)
|
||||
return
|
||||
}
|
||||
data := prepareSignInPageData(ctx)
|
||||
if performAutoLoginOAuth2(ctx, &data) {
|
||||
return
|
||||
}
|
||||
prepareSignInPageData(ctx)
|
||||
ctx.HTML(http.StatusOK, tplSignIn)
|
||||
}
|
||||
|
||||
@@ -218,10 +305,10 @@ func SignInPost(ctx *context.Context) {
|
||||
u, source, err := auth_service.UserSignIn(ctx, form.UserName, form.Password)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrInvalidArgument) {
|
||||
ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tplSignIn, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.username_password_incorrect"), tplSignIn, &form)
|
||||
log.Warn("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err)
|
||||
} else if user_model.IsErrEmailAlreadyUsed(err) {
|
||||
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSignIn, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.email_been_used"), tplSignIn, &form)
|
||||
log.Warn("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err)
|
||||
} else if user_model.IsErrUserProhibitLogin(err) {
|
||||
log.Warn("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err)
|
||||
@@ -296,19 +383,19 @@ func SignInPost(ctx *context.Context) {
|
||||
|
||||
// This handles the final part of the sign-in process of the user.
|
||||
func handleSignIn(ctx *context.Context, u *user_model.User, remember bool) {
|
||||
redirect := handleSignInFull(ctx, u, remember, true)
|
||||
handleSignInFull(ctx, u, remember)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
ctx.Redirect(redirect)
|
||||
redirectAfterAuth(ctx)
|
||||
}
|
||||
|
||||
func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRedirect bool) string {
|
||||
func handleSignInFull(ctx *context.Context, u *user_model.User, remember bool) {
|
||||
if remember {
|
||||
nt, token, err := auth_service.CreateAuthTokenForUserID(ctx, u.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("CreateAuthTokenForUserID", err)
|
||||
return setting.AppSubURL + "/"
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day)
|
||||
@@ -317,7 +404,7 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
|
||||
userHasTwoFactorAuth, err := auth.HasTwoFactorOrWebAuthn(ctx, u.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("HasTwoFactorOrWebAuthn", err)
|
||||
return setting.AppSubURL + "/"
|
||||
return
|
||||
}
|
||||
|
||||
if err := updateSession(ctx, []string{
|
||||
@@ -336,7 +423,7 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
|
||||
session.KeyUserHasTwoFactorAuth: userHasTwoFactorAuth,
|
||||
}); err != nil {
|
||||
ctx.ServerError("RegenerateSession", err)
|
||||
return setting.AppSubURL + "/"
|
||||
return
|
||||
}
|
||||
|
||||
// Language setting of the user overwrites the one previously set
|
||||
@@ -347,7 +434,7 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
|
||||
}
|
||||
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
|
||||
ctx.ServerError("UpdateUser Language", fmt.Errorf("Error updating user language [user: %d, locale: %s]", u.ID, ctx.Locale.Language()))
|
||||
return setting.AppSubURL + "/"
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,27 +444,11 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember, obeyRe
|
||||
ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req)
|
||||
}
|
||||
|
||||
// force to generate a new CSRF token
|
||||
ctx.Csrf.PrepareForSessionUser(ctx)
|
||||
|
||||
// Register last login
|
||||
if err := user_service.UpdateUser(ctx, u, &user_service.UpdateOptions{SetLastLogin: true}); err != nil {
|
||||
ctx.ServerError("UpdateUser", err)
|
||||
return setting.AppSubURL + "/"
|
||||
return
|
||||
}
|
||||
|
||||
if redirectTo := ctx.GetSiteCookie("redirect_to"); redirectTo != "" && httplib.IsCurrentGiteaSiteURL(ctx, redirectTo) {
|
||||
middleware.DeleteRedirectToCookie(ctx.Resp)
|
||||
if obeyRedirect {
|
||||
ctx.RedirectToCurrentSite(redirectTo)
|
||||
}
|
||||
return redirectTo
|
||||
}
|
||||
|
||||
if obeyRedirect {
|
||||
ctx.Redirect(setting.AppSubURL + "/")
|
||||
}
|
||||
return setting.AppSubURL + "/"
|
||||
}
|
||||
|
||||
// extractUserNameFromOAuth2 tries to extract a normalized username from the given OAuth2 user.
|
||||
@@ -403,7 +474,6 @@ func HandleSignOut(ctx *context.Context) {
|
||||
_ = ctx.Session.Flush()
|
||||
_ = ctx.Session.Destroy(ctx.Resp, ctx.Req)
|
||||
ctx.DeleteSiteCookie(setting.CookieRememberName)
|
||||
ctx.Csrf.DeleteCookie(ctx)
|
||||
middleware.DeleteRedirectToCookie(ctx.Resp)
|
||||
}
|
||||
|
||||
@@ -415,57 +485,74 @@ func SignOut(ctx *context.Context) {
|
||||
Data: ctx.Session.ID(),
|
||||
})
|
||||
}
|
||||
|
||||
// prepare the sign-out URL before destroying the session
|
||||
redirectTo := buildSignOutRedirectURL(ctx)
|
||||
HandleSignOut(ctx)
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/")
|
||||
ctx.Redirect(redirectTo)
|
||||
}
|
||||
|
||||
// SignUp render the register page
|
||||
func SignUp(ctx *context.Context) {
|
||||
func buildSignOutRedirectURL(ctx *context.Context) string {
|
||||
if ctx.Doer != nil && ctx.Doer.LoginType == auth.OAuth2 {
|
||||
if s := buildOIDCEndSessionURL(ctx, ctx.Doer); s != "" {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
// The assumption is: if reverse proxy auth is enabled, then the users should only sign-in via reverse proxy auth.
|
||||
// TODO: in the future, if we need to distinguish different sign-in methods, we need to save the sign-in method in session and check here
|
||||
if setting.Service.EnableReverseProxyAuth && setting.ReverseProxyLogoutRedirect != "" {
|
||||
return setting.ReverseProxyLogoutRedirect
|
||||
}
|
||||
return setting.AppSubURL + "/"
|
||||
}
|
||||
|
||||
func prepareSignUpPageData(ctx *context.Context) bool {
|
||||
ctx.Data["Title"] = ctx.Tr("sign_up")
|
||||
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
|
||||
ctx.Data["PageIsSignUp"] = true
|
||||
ctx.Data["EnableSSPI"] = auth.IsSSPIEnabled(ctx)
|
||||
|
||||
hasUsers, _ := user_model.HasUsers(ctx)
|
||||
hasUsers, err := user_model.HasUsers(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("HasUsers", err)
|
||||
return false
|
||||
}
|
||||
ctx.Data["IsFirstTimeRegistration"] = !hasUsers.HasAnyUser
|
||||
|
||||
oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
|
||||
if err != nil {
|
||||
ctx.ServerError("UserSignUp", err)
|
||||
return
|
||||
ctx.ServerError("GetOAuth2Providers", err)
|
||||
return false
|
||||
}
|
||||
|
||||
ctx.Data["OAuth2Providers"] = oauth2Providers
|
||||
context.SetCaptchaData(ctx)
|
||||
|
||||
ctx.Data["PageIsSignUp"] = true
|
||||
prepareCommonAuthPageData(ctx, CommonAuthOptions{
|
||||
EnableCaptcha: setting.Service.EnableCaptcha,
|
||||
})
|
||||
|
||||
// Show Disabled Registration message if DisableRegistration or AllowOnlyExternalRegistration options are true
|
||||
ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration || setting.Service.AllowOnlyExternalRegistration
|
||||
|
||||
redirectTo := ctx.FormString("redirect_to")
|
||||
if len(redirectTo) > 0 {
|
||||
middleware.SetRedirectToCookie(ctx.Resp, redirectTo)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// SignUp render the register page
|
||||
func SignUp(ctx *context.Context) {
|
||||
if !prepareSignUpPageData(ctx) {
|
||||
return
|
||||
}
|
||||
rememberAuthRedirectLink(ctx)
|
||||
ctx.HTML(http.StatusOK, tplSignUp)
|
||||
}
|
||||
|
||||
// SignUpPost response for sign up information submission
|
||||
func SignUpPost(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.RegisterForm)
|
||||
ctx.Data["Title"] = ctx.Tr("sign_up")
|
||||
|
||||
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
|
||||
|
||||
oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
|
||||
if err != nil {
|
||||
ctx.ServerError("UserSignUp", err)
|
||||
if !prepareSignUpPageData(ctx) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["OAuth2Providers"] = oauth2Providers
|
||||
context.SetCaptchaData(ctx)
|
||||
|
||||
ctx.Data["PageIsSignUp"] = true
|
||||
form := web.GetForm(ctx).(*forms.RegisterForm)
|
||||
|
||||
// Permission denied if DisableRegistration or AllowOnlyExternalRegistration options are true
|
||||
if setting.Service.DisableRegistration || setting.Service.AllowOnlyExternalRegistration {
|
||||
@@ -484,23 +571,23 @@ func SignUpPost(ctx *context.Context) {
|
||||
}
|
||||
|
||||
if !form.IsEmailDomainAllowed() {
|
||||
ctx.RenderWithErr(ctx.Tr("auth.email_domain_blacklisted"), tplSignUp, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("auth.email_domain_blacklisted"), tplSignUp, &form)
|
||||
return
|
||||
}
|
||||
|
||||
if form.Password != form.Retype {
|
||||
ctx.Data["Err_Password"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplSignUp, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.password_not_match"), tplSignUp, &form)
|
||||
return
|
||||
}
|
||||
if len(form.Password) < setting.MinPasswordLength {
|
||||
ctx.Data["Err_Password"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplSignUp, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplSignUp, &form)
|
||||
return
|
||||
}
|
||||
if !password.IsComplexEnough(form.Password) {
|
||||
ctx.Data["Err_Password"] = true
|
||||
ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplSignUp, &form)
|
||||
ctx.RenderWithErrDeprecated(password.BuildComplexityError(ctx.Locale), tplSignUp, &form)
|
||||
return
|
||||
}
|
||||
if err := password.IsPwned(ctx, form.Password); err != nil {
|
||||
@@ -510,7 +597,7 @@ func SignUpPost(ctx *context.Context) {
|
||||
errMsg = ctx.Tr("auth.password_pwned_err")
|
||||
}
|
||||
ctx.Data["Err_Password"] = true
|
||||
ctx.RenderWithErr(errMsg, tplSignUp, &form)
|
||||
ctx.RenderWithErrDeprecated(errMsg, tplSignUp, &form)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -551,16 +638,15 @@ func createUserInContext(ctx *context.Context, tpl templates.TplName, form any,
|
||||
case setting.OAuth2AccountLinkingAuto:
|
||||
var user *user_model.User
|
||||
user = &user_model.User{Name: u.Name}
|
||||
hasUser, err := user_model.GetUser(ctx, user)
|
||||
hasUser, err := user_model.GetIndividualUser(ctx, user)
|
||||
if !hasUser || err != nil {
|
||||
user = &user_model.User{Email: u.Email}
|
||||
hasUser, err = user_model.GetUser(ctx, user)
|
||||
hasUser, err = user_model.GetIndividualUser(ctx, user)
|
||||
if !hasUser || err != nil {
|
||||
ctx.ServerError("UserLinkAccount", err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: probably we should respect 'remember' user's choice...
|
||||
oauth2LinkAccount(ctx, user, possibleLinkAccountData, true)
|
||||
return false // user is already created here, all redirects are handled
|
||||
@@ -580,25 +666,25 @@ func createUserInContext(ctx *context.Context, tpl templates.TplName, form any,
|
||||
switch {
|
||||
case user_model.IsErrUserAlreadyExist(err):
|
||||
ctx.Data["Err_UserName"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tpl, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.username_been_taken"), tpl, form)
|
||||
case user_model.IsErrEmailAlreadyUsed(err):
|
||||
ctx.Data["Err_Email"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tpl, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.email_been_used"), tpl, form)
|
||||
case user_model.IsErrEmailCharIsNotSupported(err):
|
||||
ctx.Data["Err_Email"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tpl, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.email_invalid"), tpl, form)
|
||||
case user_model.IsErrEmailInvalid(err):
|
||||
ctx.Data["Err_Email"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tpl, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.email_invalid"), tpl, form)
|
||||
case db.IsErrNameReserved(err):
|
||||
ctx.Data["Err_UserName"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("user.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, form)
|
||||
case db.IsErrNamePatternNotAllowed(err):
|
||||
ctx.Data["Err_UserName"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("user.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, form)
|
||||
case db.IsErrNameCharsNotAllowed(err):
|
||||
ctx.Data["Err_UserName"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("user.form.name_chars_not_allowed", err.(db.ErrNameCharsNotAllowed).Name), tpl, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("user.form.name_chars_not_allowed", err.(db.ErrNameCharsNotAllowed).Name), tpl, form)
|
||||
default:
|
||||
ctx.ServerError("CreateUser", err)
|
||||
}
|
||||
@@ -811,8 +897,6 @@ func handleAccountActivation(ctx *context.Context, user *user_model.User) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Csrf.PrepareForSessionUser(ctx)
|
||||
|
||||
if err := resetLocale(ctx, user); err != nil {
|
||||
ctx.ServerError("resetLocale", err)
|
||||
return
|
||||
@@ -824,13 +908,7 @@ func handleAccountActivation(ctx *context.Context, user *user_model.User) {
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("auth.account_activated"))
|
||||
if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 {
|
||||
middleware.DeleteRedirectToCookie(ctx.Resp)
|
||||
ctx.RedirectToCurrentSite(redirectTo)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Redirect(setting.AppSubURL + "/")
|
||||
redirectAfterAuth(ctx)
|
||||
}
|
||||
|
||||
// ActivateEmail render the activate email page
|
||||
|
||||
@@ -4,11 +4,14 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
auth_model "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/test"
|
||||
@@ -19,6 +22,7 @@ import (
|
||||
"github.com/markbates/goth"
|
||||
"github.com/markbates/goth/gothic"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func addOAuth2Source(t *testing.T, authName string, cfg oauth2.Source) {
|
||||
@@ -29,10 +33,10 @@ func addOAuth2Source(t *testing.T, authName string, cfg oauth2.Source) {
|
||||
IsActive: true,
|
||||
Cfg: &cfg,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestUserLogin(t *testing.T) {
|
||||
func TestWebAuthUserLogin(t *testing.T) {
|
||||
ctx, resp := contexttest.MockContext(t, "/user/login")
|
||||
SignIn(ctx)
|
||||
assert.Equal(t, http.StatusOK, resp.Code)
|
||||
@@ -60,19 +64,19 @@ func TestUserLogin(t *testing.T) {
|
||||
assert.Equal(t, "/", test.RedirectURL(resp))
|
||||
}
|
||||
|
||||
func TestSignUpOAuth2Login(t *testing.T) {
|
||||
func TestWebAuthOAuth2(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)()
|
||||
|
||||
_ = oauth2.Init(t.Context())
|
||||
addOAuth2Source(t, "dummy-auth-source", oauth2.Source{})
|
||||
addOAuth2Source(t, "dummy+auth's source", oauth2.Source{})
|
||||
|
||||
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
|
||||
return goth.User{Provider: "dummy+auth's source", UserID: "dummy-user"}, nil
|
||||
})()
|
||||
mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockMemStore("dummy-sid")}
|
||||
ctx, resp := contexttest.MockContext(t, "/user/oauth2/dummy-auth-source/callback?code=dummy-code", mockOpt)
|
||||
ctx.SetPathParam("provider", "dummy-auth-source")
|
||||
ctx, resp := contexttest.MockContext(t, "/user/oauth2/..../callback?code=dummy-code", mockOpt)
|
||||
ctx.SetPathParamRaw("provider", "dummy+auth%27s%20source")
|
||||
SignInOAuthCallback(ctx)
|
||||
assert.Equal(t, http.StatusSeeOther, resp.Code)
|
||||
assert.Equal(t, "/user/link_account", test.RedirectURL(resp))
|
||||
@@ -80,16 +84,86 @@ func TestSignUpOAuth2Login(t *testing.T) {
|
||||
// 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"])
|
||||
assert.Equal(t, template.HTML("auth.oauth_callback_unable_auto_reg:dummy+auth's source,email"), ctx.Data["AutoRegistrationFailedPrompt"])
|
||||
})
|
||||
|
||||
t.Run("OAuth2CallbackError", func(t *testing.T) {
|
||||
mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockMemStore("dummy-sid")}
|
||||
ctx, resp := contexttest.MockContext(t, "/user/oauth2/dummy-auth-source/callback", mockOpt)
|
||||
ctx.SetPathParam("provider", "dummy-auth-source")
|
||||
ctx, resp := contexttest.MockContext(t, "/user/oauth2/...../callback", mockOpt)
|
||||
ctx.SetPathParamRaw("provider", "dummy+auth%27s%20source")
|
||||
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")
|
||||
})
|
||||
|
||||
t.Run("RedirectSingleProvider", func(t *testing.T) {
|
||||
enablePassword := &setting.Service.EnablePasswordSignInForm
|
||||
enableOpenID := &setting.Service.EnableOpenIDSignIn
|
||||
enablePasskey := &setting.Service.EnablePasskeyAuth
|
||||
defer test.MockVariableValue(enablePassword, false)()
|
||||
defer test.MockVariableValue(enableOpenID, false)()
|
||||
defer test.MockVariableValue(enablePasskey, false)()
|
||||
|
||||
testSignIn := func(t *testing.T, link string, expectedCode int, expectedRedirect string) {
|
||||
ctx, resp := contexttest.MockContext(t, link)
|
||||
SignIn(ctx)
|
||||
assert.Equal(t, expectedCode, resp.Code)
|
||||
if expectedCode == http.StatusSeeOther {
|
||||
assert.Equal(t, expectedRedirect, test.RedirectURL(resp))
|
||||
}
|
||||
}
|
||||
testSignIn(t, "/user/login", http.StatusSeeOther, "/user/oauth2/dummy+auth%27s%20source")
|
||||
testSignIn(t, "/user/login?redirect_to=/", http.StatusSeeOther, "/user/oauth2/dummy+auth%27s%20source?redirect_to=%2F")
|
||||
|
||||
*enablePassword, *enableOpenID, *enablePasskey = true, false, false
|
||||
testSignIn(t, "/user/login", http.StatusOK, "")
|
||||
*enablePassword, *enableOpenID, *enablePasskey = false, true, false
|
||||
testSignIn(t, "/user/login", http.StatusOK, "")
|
||||
*enablePassword, *enableOpenID, *enablePasskey = false, false, true
|
||||
testSignIn(t, "/user/login", http.StatusOK, "")
|
||||
|
||||
*enablePassword, *enableOpenID, *enablePasskey = false, false, false
|
||||
addOAuth2Source(t, "dummy-auth-source-2", oauth2.Source{})
|
||||
testSignIn(t, "/user/login", http.StatusOK, "")
|
||||
})
|
||||
|
||||
t.Run("OIDCLogout", func(t *testing.T) {
|
||||
var mockServer *httptest.Server
|
||||
mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/.well-known/openid-configuration":
|
||||
_, _ = w.Write([]byte(`{
|
||||
"issuer": "` + mockServer.URL + `",
|
||||
"authorization_endpoint": "` + mockServer.URL + `/authorize",
|
||||
"token_endpoint": "` + mockServer.URL + `/token",
|
||||
"userinfo_endpoint": "` + mockServer.URL + `/userinfo",
|
||||
"end_session_endpoint": "https://example.com/oidc-logout?oidc-key=oidc-val"
|
||||
}`))
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer mockServer.Close()
|
||||
|
||||
addOAuth2Source(t, "oidc-auth-source", oauth2.Source{
|
||||
Provider: "openidConnect",
|
||||
ClientID: "mock-client-id",
|
||||
OpenIDConnectAutoDiscoveryURL: mockServer.URL + "/.well-known/openid-configuration",
|
||||
})
|
||||
authSource, err := auth_model.GetActiveOAuth2SourceByAuthName(t.Context(), "oidc-auth-source")
|
||||
require.NoError(t, err)
|
||||
|
||||
mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockMemStore("dummy-sid")}
|
||||
ctx, resp := contexttest.MockContext(t, "/user/logout", mockOpt)
|
||||
ctx.Doer = &user_model.User{ID: 1, LoginType: auth_model.OAuth2, LoginSource: authSource.ID}
|
||||
SignOut(ctx)
|
||||
assert.Equal(t, http.StatusSeeOther, resp.Code)
|
||||
u, err := url.Parse(test.RedirectURL(resp))
|
||||
require.NoError(t, err)
|
||||
expectedValues := url.Values{"oidc-key": []string{"oidc-val"}, "post_logout_redirect_uri": []string{setting.AppURL}, "client_id": []string{"mock-client-id"}}
|
||||
assert.Equal(t, expectedValues, u.Query())
|
||||
u.RawQuery = ""
|
||||
assert.Equal(t, "https://example.com/oidc-logout", u.String())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -24,30 +24,27 @@ import (
|
||||
|
||||
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.
|
||||
func prepareLinkAccountPageData(ctx *context.Context) {
|
||||
// TODO Make insecure passwords optional for local accounts also, once email-based Second-Factor Auth is available
|
||||
ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration
|
||||
|
||||
ctx.Data["Title"] = ctx.Tr("link_account")
|
||||
ctx.Data["LinkAccountMode"] = true
|
||||
ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha
|
||||
ctx.Data["Captcha"] = context.GetImageCaptcha()
|
||||
ctx.Data["CaptchaType"] = setting.Service.CaptchaType
|
||||
ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
|
||||
ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
|
||||
ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
|
||||
ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey
|
||||
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"
|
||||
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
|
||||
ctx.Data["ShowRegistrationButton"] = false
|
||||
ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration
|
||||
|
||||
prepareCommonAuthPageData(ctx, CommonAuthOptions{
|
||||
EnableCaptcha: setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha,
|
||||
})
|
||||
}
|
||||
|
||||
// LinkAccount shows the page where the user can decide to login or create a new account
|
||||
func LinkAccount(ctx *context.Context) {
|
||||
prepareLinkAccountPageData(ctx)
|
||||
|
||||
linkAccountData := oauth2GetLinkAccountData(ctx)
|
||||
|
||||
@@ -99,10 +96,10 @@ func LinkAccount(ctx *context.Context) {
|
||||
|
||||
func handleSignInError(ctx *context.Context, userName string, ptrForm any, tmpl templates.TplName, invoker string, err error) {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tmpl, ptrForm)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.username_password_incorrect"), tmpl, ptrForm)
|
||||
} else if errors.Is(err, util.ErrInvalidArgument) {
|
||||
ctx.Data["user_exists"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("form.username_password_incorrect"), tmpl, ptrForm)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.username_password_incorrect"), tmpl, ptrForm)
|
||||
} else if user_model.IsErrUserProhibitLogin(err) {
|
||||
ctx.Data["user_exists"] = true
|
||||
log.Info("Failed authentication attempt for %s from %s: %v", userName, ctx.RemoteAddr(), err)
|
||||
@@ -126,28 +123,10 @@ func handleSignInError(ctx *context.Context, userName string, ptrForm any, tmpl
|
||||
// LinkAccountPostSignIn handle the coupling of external account with another account using signIn
|
||||
func LinkAccountPostSignIn(ctx *context.Context) {
|
||||
signInForm := web.GetForm(ctx).(*forms.SignInForm)
|
||||
ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration
|
||||
ctx.Data["Title"] = ctx.Tr("link_account")
|
||||
ctx.Data["LinkAccountMode"] = true
|
||||
ctx.Data["LinkAccountModeSignIn"] = true
|
||||
ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha
|
||||
ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
|
||||
ctx.Data["Captcha"] = context.GetImageCaptcha()
|
||||
ctx.Data["CaptchaType"] = setting.Service.CaptchaType
|
||||
ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
|
||||
ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
|
||||
ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey
|
||||
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"
|
||||
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
|
||||
ctx.Data["LinkAccountModeSignIn"] = true
|
||||
|
||||
prepareLinkAccountPageData(ctx)
|
||||
|
||||
linkAccountData := oauth2GetLinkAccountData(ctx)
|
||||
if linkAccountData == nil {
|
||||
@@ -218,30 +197,10 @@ func oauth2LinkAccount(ctx *context.Context, u *user_model.User, linkAccountData
|
||||
// LinkAccountPostRegister handle the creation of a new account for an external account using signUp
|
||||
func LinkAccountPostRegister(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.RegisterForm)
|
||||
// TODO Make insecure passwords optional for local accounts also,
|
||||
// once email-based Second-Factor Auth is available
|
||||
ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration
|
||||
ctx.Data["Title"] = ctx.Tr("link_account")
|
||||
ctx.Data["LinkAccountMode"] = true
|
||||
ctx.Data["LinkAccountModeRegister"] = true
|
||||
ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha && setting.Service.RequireExternalRegistrationCaptcha
|
||||
ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
|
||||
ctx.Data["Captcha"] = context.GetImageCaptcha()
|
||||
ctx.Data["CaptchaType"] = setting.Service.CaptchaType
|
||||
ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
|
||||
ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
|
||||
ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey
|
||||
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"
|
||||
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup"
|
||||
ctx.Data["LinkAccountModeRegister"] = true
|
||||
|
||||
prepareLinkAccountPageData(ctx)
|
||||
|
||||
linkAccountData := oauth2GetLinkAccountData(ctx)
|
||||
if linkAccountData == nil {
|
||||
@@ -266,7 +225,7 @@ func LinkAccountPostRegister(ctx *context.Context) {
|
||||
}
|
||||
|
||||
if !form.IsEmailDomainAllowed() {
|
||||
ctx.RenderWithErr(ctx.Tr("auth.email_domain_blacklisted"), tplLinkAccount, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("auth.email_domain_blacklisted"), tplLinkAccount, &form)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -280,12 +239,12 @@ func LinkAccountPostRegister(ctx *context.Context) {
|
||||
} else {
|
||||
if (len(strings.TrimSpace(form.Password)) > 0 || len(strings.TrimSpace(form.Retype)) > 0) && form.Password != form.Retype {
|
||||
ctx.Data["Err_Password"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplLinkAccount, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.password_not_match"), tplLinkAccount, &form)
|
||||
return
|
||||
}
|
||||
if len(strings.TrimSpace(form.Password)) > 0 && len(form.Password) < setting.MinPasswordLength {
|
||||
ctx.Data["Err_Password"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplLinkAccount, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplLinkAccount, &form)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -304,6 +263,11 @@ func LinkAccountPostRegister(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
oauth2SignInSync(ctx, linkAccountData.AuthSourceID, u, linkAccountData.GothUser)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
authSource, err := auth.GetSourceByID(ctx, linkAccountData.AuthSourceID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetSourceByID", err)
|
||||
|
||||
@@ -10,18 +10,20 @@ import (
|
||||
"html"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
auth_module "code.gitea.io/gitea/modules/auth"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"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/web/middleware"
|
||||
source_service "code.gitea.io/gitea/services/auth/source"
|
||||
"code.gitea.io/gitea/services/auth/source/oauth2"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
@@ -42,10 +44,7 @@ func SignInOAuth(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
redirectTo := ctx.FormString("redirect_to")
|
||||
if len(redirectTo) > 0 {
|
||||
middleware.SetRedirectToCookie(ctx.Resp, redirectTo)
|
||||
}
|
||||
rememberAuthRedirectLink(ctx)
|
||||
|
||||
// try to do a direct callback flow, so we don't authenticate the user again but use the valid accesstoken to get the user
|
||||
user, gothUser, err := oAuth2UserLoginCallback(ctx, authSource, ctx.Req, ctx.Resp)
|
||||
@@ -120,7 +119,7 @@ func SignInOAuthCallback(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
if err, ok := err.(*go_oauth2.RetrieveError); ok {
|
||||
ctx.Flash.Error("OAuth2 RetrieveError: "+err.Error(), true)
|
||||
ctx.Flash.Error("OAuth2 RetrieveError: " + err.Error())
|
||||
ctx.Redirect(setting.AppSubURL + "/user/login")
|
||||
return
|
||||
}
|
||||
@@ -278,7 +277,7 @@ type LinkAccountData struct {
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register(LinkAccountData{})
|
||||
gob.Register(LinkAccountData{}) // TODO: CHI-SESSION-GOB-REGISTER
|
||||
}
|
||||
|
||||
func oauth2GetLinkAccountData(ctx *context.Context) *LinkAccountData {
|
||||
@@ -303,21 +302,42 @@ func showLinkingLogin(ctx *context.Context, authSourceID int64, gothUser goth.Us
|
||||
ctx.Redirect(setting.AppSubURL + "/user/link_account")
|
||||
}
|
||||
|
||||
func oauth2UpdateAvatarIfNeed(ctx *context.Context, url string, u *user_model.User) {
|
||||
if setting.OAuth2Client.UpdateAvatar && len(url) > 0 {
|
||||
resp, err := http.Get(url)
|
||||
if err == nil {
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
}
|
||||
// ignore any error
|
||||
if err == nil && resp.StatusCode == http.StatusOK {
|
||||
data, err := io.ReadAll(io.LimitReader(resp.Body, setting.Avatar.MaxFileSize+1))
|
||||
if err == nil && int64(len(data)) <= setting.Avatar.MaxFileSize {
|
||||
_ = user_service.UploadAvatar(ctx, u, data)
|
||||
}
|
||||
}
|
||||
var oauth2AvatarHTTPClient = &http.Client{Timeout: 30 * time.Second}
|
||||
|
||||
func oauth2UpdateAvatarIfNeed(ctx *context.Context, avatarURL string, u *user_model.User) {
|
||||
if !setting.OAuth2Client.UpdateAvatar || len(avatarURL) == 0 {
|
||||
return
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, avatarURL, nil)
|
||||
if err != nil {
|
||||
log.Warn("invalid avatar URL %q: %v", avatarURL, err)
|
||||
return
|
||||
}
|
||||
// Some hosts (e.g. Wikimedia) reject Go's default User-Agent.
|
||||
req.Header.Set("User-Agent", "Gitea "+setting.AppVer)
|
||||
|
||||
resp, err := oauth2AvatarHTTPClient.Do(req)
|
||||
if err != nil {
|
||||
log.Warn("fetch %q failed: %v", avatarURL, err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Warn("fetch %q returned status %d", avatarURL, resp.StatusCode)
|
||||
return
|
||||
}
|
||||
data, err := io.ReadAll(io.LimitReader(resp.Body, setting.Avatar.MaxFileSize+1))
|
||||
if err != nil {
|
||||
log.Warn("read body from %q failed: %v", avatarURL, err)
|
||||
return
|
||||
}
|
||||
if int64(len(data)) > setting.Avatar.MaxFileSize {
|
||||
log.Warn("avatar from %q exceeds max size %d", avatarURL, setting.Avatar.MaxFileSize)
|
||||
return
|
||||
}
|
||||
if err := user_service.UploadAvatar(ctx, u, data); err != nil {
|
||||
log.Warn("UploadAvatar for user %q failed: %v", u.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,21 +413,12 @@ func handleOAuth2SignIn(ctx *context.Context, authSource *auth.Source, u *user_m
|
||||
return
|
||||
}
|
||||
|
||||
// force to generate a new CSRF token
|
||||
ctx.Csrf.PrepareForSessionUser(ctx)
|
||||
|
||||
if err := resetLocale(ctx, u); err != nil {
|
||||
ctx.ServerError("resetLocale", err)
|
||||
return
|
||||
}
|
||||
|
||||
if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 {
|
||||
middleware.DeleteRedirectToCookie(ctx.Resp)
|
||||
ctx.RedirectToCurrentSite(redirectTo)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Redirect(setting.AppSubURL + "/")
|
||||
redirectAfterAuth(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -493,7 +504,7 @@ func oAuth2UserLoginCallback(ctx *context.Context, authSource *auth.Source, requ
|
||||
LoginSource: authSource.ID,
|
||||
}
|
||||
|
||||
hasUser, err := user_model.GetUser(ctx, user)
|
||||
hasUser, err := user_model.GetIndividualUser(ctx, user)
|
||||
if err != nil {
|
||||
return nil, goth.User{}, err
|
||||
}
|
||||
@@ -519,3 +530,38 @@ func oAuth2UserLoginCallback(ctx *context.Context, authSource *auth.Source, requ
|
||||
// no user found to login
|
||||
return nil, gothUser, nil
|
||||
}
|
||||
|
||||
// buildOIDCEndSessionURL constructs an OIDC RP-Initiated Logout URL for the
|
||||
// given user. Returns "" if the user's auth source is not OIDC or doesn't
|
||||
// advertise an end_session_endpoint.
|
||||
func buildOIDCEndSessionURL(ctx *context.Context, doer *user_model.User) string {
|
||||
authSource, err := auth.GetSourceByID(ctx, doer.LoginSource)
|
||||
if err != nil {
|
||||
log.Error("Failed to get auth source for OIDC logout (source=%d): %v", doer.LoginSource, err)
|
||||
return ""
|
||||
}
|
||||
|
||||
oauth2Cfg, ok := authSource.Cfg.(*oauth2.Source)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
endSessionEndpoint := oauth2.GetOIDCEndSessionEndpoint(authSource.Name)
|
||||
if endSessionEndpoint == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
endSessionURL, err := url.Parse(endSessionEndpoint)
|
||||
if err != nil {
|
||||
log.Error("Failed to parse end_session_endpoint %q: %v", endSessionEndpoint, err)
|
||||
return ""
|
||||
}
|
||||
|
||||
// RP-Initiated Logout 1.0: use client_id to identify the client to the IdP.
|
||||
// https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout
|
||||
params := endSessionURL.Query()
|
||||
params.Set("client_id", oauth2Cfg.ClientID)
|
||||
params.Set("post_logout_redirect_uri", httplib.GuessCurrentAppURL(ctx))
|
||||
endSessionURL.RawQuery = params.Encode()
|
||||
return endSessionURL.String()
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"html/template"
|
||||
@@ -179,11 +180,11 @@ func AuthorizeOAuth(ctx *context.Context) {
|
||||
errs := binding.Errors{}
|
||||
errs = form.Validate(ctx.Req, errs)
|
||||
if len(errs) > 0 {
|
||||
errstring := ""
|
||||
var errstring strings.Builder
|
||||
for _, e := range errs {
|
||||
errstring += e.Error() + "\n"
|
||||
errstring.WriteString(e.Error() + "\n")
|
||||
}
|
||||
ctx.ServerError("AuthorizeOAuth: Validate: ", fmt.Errorf("errors occurred during validation: %s", errstring))
|
||||
ctx.ServerError("AuthorizeOAuth: Validate: ", fmt.Errorf("errors occurred during validation: %s", errstring.String()))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -230,8 +231,7 @@ func AuthorizeOAuth(ctx *context.Context) {
|
||||
|
||||
// pkce support
|
||||
switch form.CodeChallengeMethod {
|
||||
case "S256":
|
||||
case "plain":
|
||||
case "S256", "plain":
|
||||
if err := ctx.Session.Set("CodeChallengeMethod", form.CodeChallengeMethod); err != nil {
|
||||
handleAuthorizeError(ctx, AuthorizeError{
|
||||
ErrorCode: ErrorCodeServerError,
|
||||
@@ -561,6 +561,13 @@ func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm, server
|
||||
})
|
||||
return
|
||||
}
|
||||
if grant.ApplicationID != app.ID {
|
||||
handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
|
||||
ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidGrant,
|
||||
ErrorDescription: "refresh token belongs to a different client",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// check if token got already used
|
||||
if setting.OAuth2.InvalidateRefreshTokens && (grant.Counter != token.Counter || token.Counter == 0) {
|
||||
@@ -614,6 +621,14 @@ func handleAuthorizationCode(ctx *context.Context, form forms.AccessTokenForm, s
|
||||
})
|
||||
return
|
||||
}
|
||||
if authorizationCode.IsExpired() {
|
||||
_ = authorizationCode.Invalidate(ctx)
|
||||
handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
|
||||
ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidGrant,
|
||||
ErrorDescription: "authorization code expired",
|
||||
})
|
||||
return
|
||||
}
|
||||
// check if code verifier authorizes the client, PKCE support
|
||||
if !authorizationCode.ValidateCodeChallenge(form.CodeVerifier) {
|
||||
handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
|
||||
@@ -622,6 +637,13 @@ func handleAuthorizationCode(ctx *context.Context, form forms.AccessTokenForm, s
|
||||
})
|
||||
return
|
||||
}
|
||||
if authorizationCode.RedirectURI != "" && form.RedirectURI != authorizationCode.RedirectURI {
|
||||
handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
|
||||
ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidGrant,
|
||||
ErrorDescription: "redirect_uri differs from the original authorization request",
|
||||
})
|
||||
return
|
||||
}
|
||||
// check if granted for this application
|
||||
if authorizationCode.Grant.ApplicationID != app.ID {
|
||||
handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
|
||||
@@ -632,9 +654,15 @@ func handleAuthorizationCode(ctx *context.Context, form forms.AccessTokenForm, s
|
||||
}
|
||||
// remove token from database to deny duplicate usage
|
||||
if err := authorizationCode.Invalidate(ctx); err != nil {
|
||||
errDescription := "cannot process your request"
|
||||
errCode := oauth2_provider.AccessTokenErrorCodeInvalidRequest
|
||||
if errors.Is(err, auth.ErrOAuth2AuthorizationCodeInvalidated) {
|
||||
errDescription = "authorization code already used"
|
||||
errCode = oauth2_provider.AccessTokenErrorCodeInvalidGrant
|
||||
}
|
||||
handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
|
||||
ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidRequest,
|
||||
ErrorDescription: "cannot proceed your request",
|
||||
ErrorCode: errCode,
|
||||
ErrorDescription: errDescription,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ func oauth2UpdateSSHPubIfNeed(ctx *context.Context, authSource *auth.Source, got
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !asymkey_model.SynchronizePublicKeys(ctx, user, authSource, sshKeys) {
|
||||
if !asymkey_model.SynchronizePublicKeys(ctx, user, authSource, sshKeys, false) {
|
||||
return nil
|
||||
}
|
||||
return asymkey_service.RewriteAllPublicKeys(ctx)
|
||||
|
||||
@@ -35,7 +35,7 @@ func SignInOpenID(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if CheckAutoLogin(ctx) {
|
||||
if performAutoLogin(ctx) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ func SignInOpenIDPost(ctx *context.Context) {
|
||||
|
||||
id, err := openid.Normalize(form.Openid)
|
||||
if err != nil {
|
||||
ctx.RenderWithErr(err.Error(), tplSignInOpenID, &form)
|
||||
ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &form)
|
||||
return
|
||||
}
|
||||
form.Openid = id
|
||||
@@ -91,7 +91,7 @@ func SignInOpenIDPost(ctx *context.Context) {
|
||||
|
||||
err = allowedOpenIDURI(id)
|
||||
if err != nil {
|
||||
ctx.RenderWithErr(err.Error(), tplSignInOpenID, &form)
|
||||
ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &form)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -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("Unable to find OpenID provider in "+redirectTo, tplSignInOpenID, &form)
|
||||
ctx.RenderWithErrDeprecated("Unable to find OpenID provider in "+redirectTo, tplSignInOpenID, &form)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ func signInOpenIDVerify(ctx *context.Context) {
|
||||
|
||||
id, err := openid.Verify(fullURL)
|
||||
if err != nil {
|
||||
ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
|
||||
ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
|
||||
Openid: id,
|
||||
})
|
||||
return
|
||||
@@ -143,7 +143,7 @@ func signInOpenIDVerify(ctx *context.Context) {
|
||||
u, err := user_model.GetUserByOpenID(ctx, id)
|
||||
if err != nil {
|
||||
if !user_model.IsErrUserNotExist(err) {
|
||||
ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
|
||||
ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
|
||||
Openid: id,
|
||||
})
|
||||
return
|
||||
@@ -162,14 +162,14 @@ func signInOpenIDVerify(ctx *context.Context) {
|
||||
|
||||
parsedURL, err := url.Parse(fullURL)
|
||||
if err != nil {
|
||||
ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
|
||||
ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
|
||||
Openid: id,
|
||||
})
|
||||
return
|
||||
}
|
||||
values, err := url.ParseQuery(parsedURL.RawQuery)
|
||||
if err != nil {
|
||||
ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
|
||||
ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
|
||||
Openid: id,
|
||||
})
|
||||
return
|
||||
@@ -183,7 +183,7 @@ func signInOpenIDVerify(ctx *context.Context) {
|
||||
u, err = user_model.GetUserByEmail(ctx, email)
|
||||
if err != nil {
|
||||
if !user_model.IsErrUserNotExist(err) {
|
||||
ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
|
||||
ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
|
||||
Openid: id,
|
||||
})
|
||||
return
|
||||
@@ -199,7 +199,7 @@ func signInOpenIDVerify(ctx *context.Context) {
|
||||
u, _ = user_model.GetUserByName(ctx, nickname)
|
||||
if err != nil {
|
||||
if !user_model.IsErrUserNotExist(err) {
|
||||
ctx.RenderWithErr(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
|
||||
ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
|
||||
Openid: id,
|
||||
})
|
||||
return
|
||||
@@ -229,19 +229,26 @@ func signInOpenIDVerify(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// ConnectOpenID shows a form to connect an OpenID URI to an existing account
|
||||
func ConnectOpenID(ctx *context.Context) {
|
||||
oid, _ := ctx.Session.Get("openid_verified_uri").(string)
|
||||
func prepareConnectOpenIDPageData(ctx *context.Context) (oid string) {
|
||||
oid, _ = ctx.Session.Get("openid_verified_uri").(string)
|
||||
if oid == "" {
|
||||
ctx.Redirect(setting.AppSubURL + "/user/login/openid")
|
||||
return
|
||||
return ""
|
||||
}
|
||||
ctx.Data["Title"] = "OpenID connect"
|
||||
ctx.Data["PageIsSignIn"] = true
|
||||
ctx.Data["PageIsOpenIDConnect"] = true
|
||||
ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
|
||||
ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
|
||||
ctx.Data["OpenID"] = oid
|
||||
prepareCommonAuthPageData(ctx, CommonAuthOptions{EnableCaptcha: false})
|
||||
return oid
|
||||
}
|
||||
|
||||
// ConnectOpenID shows a form to connect an OpenID URI to an existing account
|
||||
func ConnectOpenID(ctx *context.Context) {
|
||||
oid := prepareConnectOpenIDPageData(ctx)
|
||||
if oid == "" {
|
||||
return
|
||||
}
|
||||
userName, _ := ctx.Session.Get("openid_determined_username").(string)
|
||||
if userName != "" {
|
||||
ctx.Data["user_name"] = userName
|
||||
@@ -252,16 +259,10 @@ func ConnectOpenID(ctx *context.Context) {
|
||||
// ConnectOpenIDPost handles submission of a form to connect an OpenID URI to an existing account
|
||||
func ConnectOpenIDPost(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.ConnectOpenIDForm)
|
||||
oid, _ := ctx.Session.Get("openid_verified_uri").(string)
|
||||
oid := prepareConnectOpenIDPageData(ctx)
|
||||
if oid == "" {
|
||||
ctx.Redirect(setting.AppSubURL + "/user/login/openid")
|
||||
return
|
||||
}
|
||||
ctx.Data["Title"] = "OpenID connect"
|
||||
ctx.Data["PageIsSignIn"] = true
|
||||
ctx.Data["PageIsOpenIDConnect"] = true
|
||||
ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
|
||||
ctx.Data["OpenID"] = oid
|
||||
|
||||
u, _, err := auth.UserSignIn(ctx, form.UserName, form.Password)
|
||||
if err != nil {
|
||||
@@ -273,7 +274,7 @@ func ConnectOpenIDPost(ctx *context.Context) {
|
||||
userOID := &user_model.UserOpenID{UID: u.ID, URI: oid}
|
||||
if err = user_model.AddUserOpenID(ctx, userOID); err != nil {
|
||||
if user_model.IsErrOpenIDAlreadyUsed(err) {
|
||||
ctx.RenderWithErr(ctx.Tr("form.openid_been_used", oid), tplConnectOID, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.openid_been_used", oid), tplConnectOID, &form)
|
||||
return
|
||||
}
|
||||
ctx.ServerError("AddUserOpenID", err)
|
||||
@@ -287,28 +288,29 @@ func ConnectOpenIDPost(ctx *context.Context) {
|
||||
handleSignIn(ctx, u, remember)
|
||||
}
|
||||
|
||||
// RegisterOpenID shows a form to create a new user authenticated via an OpenID URI
|
||||
func RegisterOpenID(ctx *context.Context) {
|
||||
oid, _ := ctx.Session.Get("openid_verified_uri").(string)
|
||||
func prepareRegisterOpenIDPageData(ctx *context.Context) (oid string) {
|
||||
oid, _ = ctx.Session.Get("openid_verified_uri").(string)
|
||||
if oid == "" {
|
||||
ctx.Redirect(setting.AppSubURL + "/user/login/openid")
|
||||
return
|
||||
return ""
|
||||
}
|
||||
ctx.Data["Title"] = "OpenID signup"
|
||||
ctx.Data["PageIsSignIn"] = true
|
||||
ctx.Data["PageIsOpenIDRegister"] = true
|
||||
ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
|
||||
ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
|
||||
ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha
|
||||
ctx.Data["Captcha"] = context.GetImageCaptcha()
|
||||
ctx.Data["CaptchaType"] = setting.Service.CaptchaType
|
||||
ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
|
||||
ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
|
||||
ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
|
||||
ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey
|
||||
ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL
|
||||
ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey
|
||||
ctx.Data["OpenID"] = oid
|
||||
prepareCommonAuthPageData(ctx, CommonAuthOptions{
|
||||
EnableCaptcha: setting.Service.EnableCaptcha,
|
||||
})
|
||||
return oid
|
||||
}
|
||||
|
||||
// RegisterOpenID shows a form to create a new user authenticated via an OpenID URI
|
||||
func RegisterOpenID(ctx *context.Context) {
|
||||
oid := prepareRegisterOpenIDPageData(ctx)
|
||||
if oid == "" {
|
||||
return
|
||||
}
|
||||
|
||||
userName, _ := ctx.Session.Get("openid_determined_username").(string)
|
||||
if userName != "" {
|
||||
ctx.Data["user_name"] = userName
|
||||
@@ -322,19 +324,12 @@ func RegisterOpenID(ctx *context.Context) {
|
||||
|
||||
// RegisterOpenIDPost handles submission of a form to create a new user authenticated via an OpenID URI
|
||||
func RegisterOpenIDPost(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.SignUpOpenIDForm)
|
||||
oid, _ := ctx.Session.Get("openid_verified_uri").(string)
|
||||
oid := prepareRegisterOpenIDPageData(ctx)
|
||||
if oid == "" {
|
||||
ctx.Redirect(setting.AppSubURL + "/user/login/openid")
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Title"] = "OpenID signup"
|
||||
ctx.Data["PageIsSignIn"] = true
|
||||
ctx.Data["PageIsOpenIDRegister"] = true
|
||||
ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
|
||||
context.SetCaptchaData(ctx)
|
||||
ctx.Data["OpenID"] = oid
|
||||
form := web.GetForm(ctx).(*forms.SignUpOpenIDForm)
|
||||
|
||||
if setting.Service.AllowOnlyInternalRegistration {
|
||||
ctx.HTTPError(http.StatusForbidden)
|
||||
@@ -352,7 +347,7 @@ func RegisterOpenIDPost(ctx *context.Context) {
|
||||
length := max(setting.MinPasswordLength, 256)
|
||||
password, err := util.CryptoRandomString(int64(length))
|
||||
if err != nil {
|
||||
ctx.RenderWithErr(err.Error(), tplSignUpOID, form)
|
||||
ctx.RenderWithErrDeprecated(err.Error(), tplSignUpOID, form)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -370,7 +365,7 @@ func RegisterOpenIDPost(ctx *context.Context) {
|
||||
userOID := &user_model.UserOpenID{UID: u.ID, URI: oid}
|
||||
if err = user_model.AddUserOpenID(ctx, userOID); err != nil {
|
||||
if user_model.IsErrOpenIDAlreadyUsed(err) {
|
||||
ctx.RenderWithErr(ctx.Tr("form.openid_been_used", oid), tplSignUpOID, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.openid_been_used", oid), tplSignUpOID, &form)
|
||||
return
|
||||
}
|
||||
ctx.ServerError("AddUserOpenID", err)
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
"code.gitea.io/gitea/services/mailer"
|
||||
@@ -75,7 +74,7 @@ func ForgotPasswdPost(ctx *context.Context) {
|
||||
|
||||
if !u.IsLocal() && !u.IsOAuth2() {
|
||||
ctx.Data["Err_Email"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("auth.non_local_account"), tplForgotPassword, nil)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("auth.non_local_account"), tplForgotPassword, nil)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -172,7 +171,7 @@ func ResetPasswdPost(ctx *context.Context) {
|
||||
if !twofa.VerifyScratchToken(ctx.FormString("token")) {
|
||||
ctx.Data["IsResetForm"] = true
|
||||
ctx.Data["Err_Token"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("auth.twofa_scratch_token_incorrect"), tplResetPassword, nil)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("auth.twofa_scratch_token_incorrect"), tplResetPassword, nil)
|
||||
return
|
||||
}
|
||||
regenerateScratchToken = true
|
||||
@@ -186,7 +185,7 @@ func ResetPasswdPost(ctx *context.Context) {
|
||||
if !ok || twofa.LastUsedPasscode == passcode {
|
||||
ctx.Data["IsResetForm"] = true
|
||||
ctx.Data["Err_Passcode"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("auth.twofa_passcode_incorrect"), tplResetPassword, nil)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("auth.twofa_passcode_incorrect"), tplResetPassword, nil)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -207,13 +206,13 @@ func ResetPasswdPost(ctx *context.Context) {
|
||||
ctx.Data["Err_Password"] = true
|
||||
switch {
|
||||
case errors.Is(err, password.ErrMinLength):
|
||||
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplResetPassword, nil)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplResetPassword, nil)
|
||||
case errors.Is(err, password.ErrComplexity):
|
||||
ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplResetPassword, nil)
|
||||
ctx.RenderWithErrDeprecated(password.BuildComplexityError(ctx.Locale), tplResetPassword, nil)
|
||||
case errors.Is(err, password.ErrIsPwned):
|
||||
ctx.RenderWithErr(ctx.Tr("auth.password_pwned", "https://haveibeenpwned.com/Passwords"), tplResetPassword, nil)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_pwned", "https://haveibeenpwned.com/Passwords"), tplResetPassword, nil)
|
||||
case password.IsErrIsPwnedRequest(err):
|
||||
ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplResetPassword, nil)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_pwned_err"), tplResetPassword, nil)
|
||||
default:
|
||||
ctx.ServerError("UpdateAuth", err)
|
||||
}
|
||||
@@ -236,7 +235,7 @@ func ResetPasswdPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
handleSignInFull(ctx, u, remember, false)
|
||||
handleSignInFull(ctx, u, remember)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
@@ -276,7 +275,7 @@ func MustChangePasswordPost(ctx *context.Context) {
|
||||
|
||||
if form.Password != form.Retype {
|
||||
ctx.Data["Err_Password"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplMustChangePassword, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.password_not_match"), tplMustChangePassword, &form)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -288,16 +287,16 @@ func MustChangePasswordPost(ctx *context.Context) {
|
||||
switch {
|
||||
case errors.Is(err, password.ErrMinLength):
|
||||
ctx.Data["Err_Password"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplMustChangePassword, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplMustChangePassword, &form)
|
||||
case errors.Is(err, password.ErrComplexity):
|
||||
ctx.Data["Err_Password"] = true
|
||||
ctx.RenderWithErr(password.BuildComplexityError(ctx.Locale), tplMustChangePassword, &form)
|
||||
ctx.RenderWithErrDeprecated(password.BuildComplexityError(ctx.Locale), tplMustChangePassword, &form)
|
||||
case errors.Is(err, password.ErrIsPwned):
|
||||
ctx.Data["Err_Password"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("auth.password_pwned", "https://haveibeenpwned.com/Passwords"), tplMustChangePassword, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_pwned", "https://haveibeenpwned.com/Passwords"), tplMustChangePassword, &form)
|
||||
case password.IsErrIsPwnedRequest(err):
|
||||
ctx.Data["Err_Password"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("auth.password_pwned_err"), tplMustChangePassword, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("auth.password_pwned_err"), tplMustChangePassword, &form)
|
||||
default:
|
||||
ctx.ServerError("UpdateAuth", err)
|
||||
}
|
||||
@@ -308,11 +307,5 @@ func MustChangePasswordPost(ctx *context.Context) {
|
||||
|
||||
log.Trace("User updated password: %s", ctx.Doer.Name)
|
||||
|
||||
if redirectTo := ctx.GetSiteCookie("redirect_to"); redirectTo != "" {
|
||||
middleware.DeleteRedirectToCookie(ctx.Resp)
|
||||
ctx.RedirectToCurrentSite(redirectTo)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Redirect(setting.AppSubURL + "/")
|
||||
redirectAfterAuth(ctx)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ var tplWebAuthn templates.TplName = "user/auth/webauthn"
|
||||
func WebAuthn(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("twofa")
|
||||
|
||||
if CheckAutoLogin(ctx) {
|
||||
if performAutoLogin(ctx) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -156,12 +156,8 @@ func WebAuthnPasskeyLogin(ctx *context.Context) {
|
||||
}
|
||||
|
||||
remember := false // TODO: implement remember me
|
||||
redirect := handleSignInFull(ctx, user, remember, false)
|
||||
if redirect == "" {
|
||||
redirect = setting.AppSubURL + "/"
|
||||
}
|
||||
|
||||
ctx.JSONRedirect(redirect)
|
||||
handleSignInFull(ctx, user, remember)
|
||||
ctx.JSONRedirect(consumeAuthRedirectLink(ctx))
|
||||
}
|
||||
|
||||
// WebAuthnLoginAssertion submits a WebAuthn challenge to the browser
|
||||
@@ -274,11 +270,7 @@ func WebAuthnLoginAssertionPost(ctx *context.Context) {
|
||||
}
|
||||
|
||||
remember := ctx.Session.Get("twofaRemember").(bool)
|
||||
redirect := handleSignInFull(ctx, user, remember, false)
|
||||
if redirect == "" {
|
||||
redirect = setting.AppSubURL + "/"
|
||||
}
|
||||
handleSignInFull(ctx, user, remember)
|
||||
_ = ctx.Session.Delete("twofaUid")
|
||||
|
||||
ctx.JSONRedirect(redirect)
|
||||
ctx.JSONRedirect(consumeAuthRedirectLink(ctx))
|
||||
}
|
||||
|
||||
@@ -6,11 +6,14 @@ package web
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"image/png"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/assetfs"
|
||||
"code.gitea.io/gitea/modules/avatar"
|
||||
"code.gitea.io/gitea/modules/httpcache"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@@ -22,79 +25,70 @@ import (
|
||||
func avatarStorageHandler(storageSetting *setting.Storage, prefix string, objStore storage.ObjectStorage) http.HandlerFunc {
|
||||
prefix = strings.Trim(prefix, "/")
|
||||
funcInfo := routing.GetFuncInfo(avatarStorageHandler, prefix)
|
||||
exeModTime := assetfs.GetExecutableModTime()
|
||||
fallbackEtag := fmt.Sprintf(`"avatar-%s"`, exeModTime.Format("20060102150405"))
|
||||
|
||||
if storageSetting.ServeDirect() {
|
||||
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
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(req.URL.Path, "/"+prefix+"/") {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
defer routing.RecordFuncInfo(req.Context(), funcInfo)()
|
||||
|
||||
rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/")
|
||||
rPath = util.PathJoinRelX(rPath)
|
||||
|
||||
u, err := objStore.URL(rPath, path.Base(rPath), req.Method, nil)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) || errors.Is(err, os.ErrNotExist) {
|
||||
log.Warn("Unable to find %s %s", prefix, rPath)
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
log.Error("Error whilst getting URL for %s %s. Error: %v", prefix, rPath, err)
|
||||
http.Error(w, fmt.Sprintf("Error whilst getting URL for %s %s", prefix, rPath), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, req, u.String(), http.StatusTemporaryRedirect)
|
||||
handleError := func(w http.ResponseWriter, req *http.Request, avatarPath string, err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if errors.Is(err, os.ErrNotExist) || errors.Is(err, util.ErrNotExist) {
|
||||
// if avatar doesn't exist, generate a random one and serve it with proper cache control headers
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
if !httpcache.HandleGenericETagPublicCache(req, w, fallbackEtag, &exeModTime) {
|
||||
if req.Method == http.MethodGet {
|
||||
img := avatar.RandomImageWithSize(96, []byte(avatarPath))
|
||||
_ = png.Encode(w, img)
|
||||
} // else: for HEAD request, just return the headers without body
|
||||
}
|
||||
} else {
|
||||
// for internal errors, log the error and return 500
|
||||
log.Error("Error when serving avatar %s: %s", req.URL.Path, err)
|
||||
http.Error(w, "unable to serve avatar image", http.StatusInternalServerError)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(req.URL.Path, "/"+prefix+"/") {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
defer routing.RecordFuncInfo(req.Context(), funcInfo)()
|
||||
|
||||
rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/")
|
||||
rPath = util.PathJoinRelX(rPath)
|
||||
if rPath == "" || rPath == "." {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
avatarPath, ok := strings.CutPrefix(req.URL.Path, "/"+prefix+"/")
|
||||
if !ok {
|
||||
http.Error(w, "invalid avatar path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
avatarPath = util.PathJoinRelX(avatarPath)
|
||||
if avatarPath == "" || avatarPath == "." {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
fi, err := objStore.Stat(rPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) || errors.Is(err, os.ErrNotExist) {
|
||||
log.Warn("Unable to find %s %s", prefix, rPath)
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
if storageSetting.ServeDirect() {
|
||||
// Old logic: no check for existence by Stat, so old code's "errors.Is(err, os.ErrNotExist)" didn't work.
|
||||
// So in theory, it doesn't work with the non-existing avatar fallback, it just gets the URL and redirects to it.
|
||||
// Checking "stat" requires one more request to the storage, which is inefficient.
|
||||
// Workaround: disable "SERVE_DIRECT". Leave the problem to the future.
|
||||
u, err := objStore.ServeDirectURL(avatarPath, path.Base(avatarPath), req.Method, nil)
|
||||
if handleError(w, req, avatarPath, err) {
|
||||
return
|
||||
}
|
||||
log.Error("Error whilst opening %s %s. Error: %v", prefix, rPath, err)
|
||||
http.Error(w, fmt.Sprintf("Error whilst opening %s %s", prefix, rPath), http.StatusInternalServerError)
|
||||
http.Redirect(w, req, u.String(), http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
|
||||
fr, err := objStore.Open(rPath)
|
||||
if err != nil {
|
||||
log.Error("Error whilst opening %s %s. Error: %v", prefix, rPath, err)
|
||||
http.Error(w, fmt.Sprintf("Error whilst opening %s %s", prefix, rPath), http.StatusInternalServerError)
|
||||
fr, err := objStore.Open(avatarPath)
|
||||
if handleError(w, req, avatarPath, err) {
|
||||
return
|
||||
}
|
||||
defer fr.Close()
|
||||
|
||||
fi, err := fr.Stat()
|
||||
if handleError(w, req, avatarPath, err) {
|
||||
return
|
||||
}
|
||||
|
||||
httpcache.SetCacheControlInHeader(w.Header(), httpcache.CacheControlForPublicStatic())
|
||||
http.ServeContent(w, req, path.Base(rPath), fi.ModTime(), fr)
|
||||
http.ServeContent(w, req, path.Base(avatarPath), fi.ModTime(), fr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ import (
|
||||
"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/charset"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/indexer/code"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
@@ -51,16 +53,7 @@ func FetchActionTest(ctx *context.Context) {
|
||||
ctx.JSONRedirect("")
|
||||
}
|
||||
|
||||
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 prepareMockDataGiteaUI(_ *context.Context) {}
|
||||
|
||||
func prepareMockDataBadgeCommitSign(ctx *context.Context) {
|
||||
var commits []*asymkey.SignCommit
|
||||
@@ -166,6 +159,29 @@ func prepareMockDataBadgeActionsSvg(ctx *context.Context) {
|
||||
ctx.Data["SelectedStyle"] = selectedStyle
|
||||
}
|
||||
|
||||
func prepareMockDataRelativeTime(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["TimePast3m"] = now.Add(-3 * time.Minute)
|
||||
ctx.Data["TimePast1h"] = now.Add(-1 * time.Hour)
|
||||
ctx.Data["TimePast3h"] = now.Add(-3 * time.Hour)
|
||||
ctx.Data["TimePast1d"] = now.Add(-24 * time.Hour)
|
||||
ctx.Data["TimePast2d"] = now.Add(-2 * 24 * time.Hour)
|
||||
ctx.Data["TimePast3d"] = now.Add(-3 * 24 * time.Hour)
|
||||
ctx.Data["TimePast26h"] = now.Add(-26 * time.Hour)
|
||||
ctx.Data["TimePast40d"] = now.Add(-40 * 24 * time.Hour)
|
||||
ctx.Data["TimePast60d"] = now.Add(-60 * 24 * time.Hour)
|
||||
ctx.Data["TimePast1y"] = now.Add(-366 * 24 * time.Hour)
|
||||
ctx.Data["TimeFuture1h"] = now.Add(1 * time.Hour)
|
||||
ctx.Data["TimeFuture3h"] = now.Add(3 * time.Hour)
|
||||
ctx.Data["TimeFuture3d"] = now.Add(3 * 24 * time.Hour)
|
||||
ctx.Data["TimeFuture1y"] = now.Add(366 * 24 * time.Hour)
|
||||
}
|
||||
|
||||
func prepareMockData(ctx *context.Context) {
|
||||
switch ctx.Req.URL.Path {
|
||||
case "/devtest/gitea-ui":
|
||||
@@ -174,9 +190,35 @@ func prepareMockData(ctx *context.Context) {
|
||||
prepareMockDataBadgeCommitSign(ctx)
|
||||
case "/devtest/badge-actions-svg":
|
||||
prepareMockDataBadgeActionsSvg(ctx)
|
||||
case "/devtest/relative-time":
|
||||
prepareMockDataRelativeTime(ctx)
|
||||
case "/devtest/unicode-escape":
|
||||
prepareMockDataUnicodeEscape(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func prepareMockDataUnicodeEscape(ctx *context.Context) {
|
||||
content := "// demo code\n"
|
||||
content += "if accessLevel != \"user\u202E \u2066// Check if admin (invisible char)\u2069 \u2066\" { }\n"
|
||||
content += "if O𝐾 { } // ambiguous char\n"
|
||||
content += "if O𝐾 && accessLevel != \"user\u202E \u2066// ambiguous char + invisible char\u2069 \u2066\" { }\n"
|
||||
content += "str := `\xef` // broken char\n"
|
||||
content += "str := `\x00 \x19 \x7f` // control char\n"
|
||||
|
||||
lineNums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
|
||||
highlightLines := code.HighlightSearchResultCode("demo.go", "", lineNums, content)
|
||||
escapeStatus := &charset.EscapeStatus{}
|
||||
lineEscapeStatus := make([]*charset.EscapeStatus, len(highlightLines))
|
||||
for i, hl := range highlightLines {
|
||||
lineEscapeStatus[i], hl.FormattedContent = charset.EscapeControlHTML(hl.FormattedContent, ctx.Locale)
|
||||
escapeStatus = escapeStatus.Or(lineEscapeStatus[i])
|
||||
}
|
||||
ctx.Data["HighlightLines"] = highlightLines
|
||||
ctx.Data["EscapeStatus"] = escapeStatus
|
||||
ctx.Data["LineEscapeStatus"] = lineEscapeStatus
|
||||
}
|
||||
|
||||
func TmplCommon(ctx *context.Context) {
|
||||
prepareMockData(ctx)
|
||||
if ctx.Req.Method == http.MethodPost {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/mailer"
|
||||
|
||||
@@ -34,17 +35,18 @@ func MailPreviewRender(ctx *context.Context) {
|
||||
|
||||
func prepareMailPreviewRender(ctx *context.Context, tmplName string) {
|
||||
tmplSubject := mailer.LoadedTemplates().SubjectTemplates.Lookup(tmplName)
|
||||
if tmplSubject == nil {
|
||||
ctx.Data["RenderMailSubject"] = "default subject"
|
||||
} else {
|
||||
// FIXME: MAIL-TEMPLATE-SUBJECT: only "issue" related messages support using subject from templates
|
||||
subject := "(default subject)"
|
||||
if tmplSubject != nil {
|
||||
var buf strings.Builder
|
||||
err := tmplSubject.Execute(&buf, nil)
|
||||
if err != nil {
|
||||
ctx.Data["RenderMailSubject"] = err.Error()
|
||||
subject = "ERROR: " + err.Error()
|
||||
} else {
|
||||
ctx.Data["RenderMailSubject"] = buf.String()
|
||||
subject = util.IfZero(buf.String(), subject)
|
||||
}
|
||||
}
|
||||
ctx.Data["RenderMailSubject"] = subject
|
||||
ctx.Data["RenderMailTemplateName"] = tmplName
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/web/repo/actions"
|
||||
@@ -35,6 +36,10 @@ func generateMockStepsLog(logCur actions.LogCursor, opts generateMockStepsLogOpt
|
||||
"##[group]test group for: step={step}, cursor={cursor}",
|
||||
"in group msg for: step={step}, cursor={cursor}",
|
||||
"##[endgroup]",
|
||||
"::error::mock error for: step={step}, cursor={cursor}",
|
||||
"::warning::mock warning for: step={step}, cursor={cursor}",
|
||||
"::notice::mock notice for: step={step}, cursor={cursor}",
|
||||
"::debug::mock debug for: step={step}, cursor={cursor}",
|
||||
)
|
||||
// usually the cursor is the "file offset", but here we abuse it as "line number" to make the mock easier, intentionally
|
||||
cur := logCur.Cursor
|
||||
@@ -58,24 +63,29 @@ func generateMockStepsLog(logCur actions.LogCursor, opts generateMockStepsLogOpt
|
||||
}
|
||||
|
||||
func MockActionsView(ctx *context.Context) {
|
||||
ctx.Data["RunID"] = ctx.PathParam("run")
|
||||
ctx.Data["JobID"] = ctx.PathParam("job")
|
||||
ctx.Data["RunID"] = ctx.PathParamInt64("run")
|
||||
ctx.Data["JobID"] = ctx.PathParamInt64("job")
|
||||
ctx.HTML(http.StatusOK, "devtest/repo-action-view")
|
||||
}
|
||||
|
||||
func MockActionsRunsJobs(ctx *context.Context) {
|
||||
runID := ctx.PathParamInt64("run")
|
||||
|
||||
req := web.GetForm(ctx).(*actions.ViewRequest)
|
||||
resp := &actions.ViewResponse{}
|
||||
resp.State.Run.RepoID = 12345
|
||||
resp.State.Run.TitleHTML = `mock run title <a href="/">link</a>`
|
||||
resp.State.Run.Link = setting.AppSubURL + "/devtest/repo-action-view/runs/" + strconv.FormatInt(runID, 10)
|
||||
resp.State.Run.Status = actions_model.StatusRunning.String()
|
||||
resp.State.Run.CanCancel = runID == 10
|
||||
resp.State.Run.CanApprove = runID == 20
|
||||
resp.State.Run.CanRerun = runID == 30
|
||||
resp.State.Run.CanRerunFailed = runID == 30
|
||||
resp.State.Run.CanDeleteArtifact = true
|
||||
resp.State.Run.WorkflowID = "workflow-id"
|
||||
resp.State.Run.WorkflowLink = "./workflow-link"
|
||||
resp.State.Run.Duration = "1h 23m 45s"
|
||||
resp.State.Run.TriggeredAt = time.Now().Add(-time.Hour).Unix()
|
||||
resp.State.Run.TriggerEvent = "push"
|
||||
resp.State.Run.Commit = actions.ViewCommit{
|
||||
ShortSha: "ccccdddd",
|
||||
Link: "./commit-link",
|
||||
@@ -112,6 +122,7 @@ func MockActionsRunsJobs(ctx *context.Context) {
|
||||
|
||||
resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{
|
||||
ID: runID * 10,
|
||||
JobID: "job-100",
|
||||
Name: "job 100",
|
||||
Status: actions_model.StatusRunning.String(),
|
||||
CanRerun: true,
|
||||
@@ -119,19 +130,58 @@ func MockActionsRunsJobs(ctx *context.Context) {
|
||||
})
|
||||
resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{
|
||||
ID: runID*10 + 1,
|
||||
JobID: "job-101",
|
||||
Name: "job 101",
|
||||
Status: actions_model.StatusWaiting.String(),
|
||||
CanRerun: false,
|
||||
Duration: "2h",
|
||||
Needs: []string{"job-100"},
|
||||
})
|
||||
resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{
|
||||
ID: runID*10 + 2,
|
||||
Name: "job 102",
|
||||
JobID: "job-102",
|
||||
Name: "ULTRA LOOOOOOOOOOOONG job name 102 that exceeds the limit",
|
||||
Status: actions_model.StatusFailure.String(),
|
||||
CanRerun: false,
|
||||
Duration: "3h",
|
||||
Needs: []string{"job-100", "job-101"},
|
||||
})
|
||||
resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{
|
||||
ID: runID*10 + 3,
|
||||
JobID: "job-103",
|
||||
Name: "job 103",
|
||||
Status: actions_model.StatusCancelled.String(),
|
||||
CanRerun: false,
|
||||
Duration: "2m",
|
||||
Needs: []string{"job-100"},
|
||||
})
|
||||
|
||||
// add more jobs to a run for UI testing
|
||||
if resp.State.Run.CanCancel {
|
||||
for i := range 10 {
|
||||
resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{
|
||||
ID: runID*1000 + int64(i),
|
||||
JobID: "job-dup-test-" + strconv.Itoa(i),
|
||||
Name: "job dup test " + strconv.Itoa(i),
|
||||
Status: actions_model.StatusSuccess.String(),
|
||||
CanRerun: false,
|
||||
Duration: "2m",
|
||||
Needs: []string{"job-103", "job-101", "job-100"},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fillViewRunResponseCurrentJob(ctx, resp)
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func fillViewRunResponseCurrentJob(ctx *context.Context, resp *actions.ViewResponse) {
|
||||
jobID := ctx.PathParamInt64("job")
|
||||
if jobID == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
req := web.GetForm(ctx).(*actions.ViewRequest)
|
||||
var mockLogOptions []generateMockStepsLogOptions
|
||||
resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, &actions.ViewJobStep{
|
||||
Summary: "step 0 (mock slow)",
|
||||
@@ -155,7 +205,6 @@ func MockActionsRunsJobs(ctx *context.Context) {
|
||||
mockLogOptions = append(mockLogOptions, generateMockStepsLogOptions{mockCountFirst: 30, mockCountGeneral: 3, groupRepeat: 3})
|
||||
|
||||
if len(req.LogCursors) == 0 {
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -181,5 +230,4 @@ func MockActionsRunsJobs(ctx *context.Context) {
|
||||
} else {
|
||||
time.Sleep(time.Duration(100) * time.Millisecond) // actually, frontend reload every 1 second, any smaller delay is fine
|
||||
}
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ func Code(ctx *context.Context) {
|
||||
ctx.Data["UsersPageIsDisabled"] = setting.Service.Explore.DisableUsersPage
|
||||
ctx.Data["OrganizationsPageIsDisabled"] = setting.Service.Explore.DisableOrganizationsPage
|
||||
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
|
||||
ctx.Data["Title"] = ctx.Tr("explore")
|
||||
ctx.Data["Title"] = ctx.Tr("explore_title")
|
||||
ctx.Data["PageIsExplore"] = true
|
||||
ctx.Data["PageIsExploreCode"] = true
|
||||
ctx.Data["PageIsViewCode"] = true
|
||||
@@ -66,7 +66,7 @@ func Code(ctx *context.Context) {
|
||||
}
|
||||
|
||||
var (
|
||||
total int
|
||||
total int64
|
||||
searchResults []*code_indexer.Result
|
||||
searchResultLanguages []*code_indexer.SearchResultLanguages
|
||||
)
|
||||
|
||||
@@ -22,7 +22,7 @@ func Organizations(ctx *context.Context) {
|
||||
|
||||
ctx.Data["UsersPageIsDisabled"] = setting.Service.Explore.DisableUsersPage
|
||||
ctx.Data["CodePageIsDisabled"] = setting.Service.Explore.DisableCodePage
|
||||
ctx.Data["Title"] = ctx.Tr("explore")
|
||||
ctx.Data["Title"] = ctx.Tr("explore_title")
|
||||
ctx.Data["PageIsExplore"] = true
|
||||
ctx.Data["PageIsExploreOrganizations"] = true
|
||||
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
|
||||
@@ -46,7 +46,7 @@ func Organizations(ctx *context.Context) {
|
||||
|
||||
RenderUserSearch(ctx, user_model.SearchUserOptions{
|
||||
Actor: ctx.Doer,
|
||||
Type: user_model.UserTypeOrganization,
|
||||
Types: []user_model.UserType{user_model.UserTypeOrganization},
|
||||
ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum},
|
||||
Visible: visibleTypes,
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ type RepoSearchOptions struct {
|
||||
// This function is also used to render the Admin Repository Management page.
|
||||
func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
|
||||
// Sitemap index for sitemap paths
|
||||
page := int(ctx.PathParamInt64("idx"))
|
||||
page := ctx.PathParamInt("idx")
|
||||
isSitemap := ctx.PathParam("idx") != ""
|
||||
if page <= 1 {
|
||||
page = ctx.FormInt("page")
|
||||
@@ -137,7 +137,7 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
|
||||
ctx.Data["Repos"] = repos
|
||||
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
|
||||
|
||||
pager := context.NewPagination(int(count), opts.PageSize, page, 5)
|
||||
pager := context.NewPagination(count, opts.PageSize, page, 5)
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
@@ -149,7 +149,7 @@ func Repos(ctx *context.Context) {
|
||||
ctx.Data["UsersPageIsDisabled"] = setting.Service.Explore.DisableUsersPage
|
||||
ctx.Data["OrganizationsPageIsDisabled"] = setting.Service.Explore.DisableOrganizationsPage
|
||||
ctx.Data["CodePageIsDisabled"] = setting.Service.Explore.DisableCodePage
|
||||
ctx.Data["Title"] = ctx.Tr("explore")
|
||||
ctx.Data["Title"] = ctx.Tr("explore_title")
|
||||
ctx.Data["PageIsExplore"] = true
|
||||
ctx.Data["ShowRepoOwnerOnList"] = true
|
||||
ctx.Data["PageIsExploreRepositories"] = true
|
||||
|
||||
@@ -34,7 +34,7 @@ func isKeywordValid(keyword string) bool {
|
||||
// RenderUserSearch render user search page
|
||||
func RenderUserSearch(ctx *context.Context, opts user_model.SearchUserOptions, tplName templates.TplName) {
|
||||
// Sitemap index for sitemap paths
|
||||
opts.Page = int(ctx.PathParamInt64("idx"))
|
||||
opts.Page = ctx.PathParamInt("idx")
|
||||
isSitemap := ctx.PathParam("idx") != ""
|
||||
if opts.Page <= 1 {
|
||||
opts.Page = ctx.FormInt("page")
|
||||
@@ -119,7 +119,7 @@ func RenderUserSearch(ctx *context.Context, opts user_model.SearchUserOptions, t
|
||||
ctx.Data["ShowUserEmail"] = setting.UI.ShowUserEmail
|
||||
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
|
||||
|
||||
pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
|
||||
pager := context.NewPagination(count, opts.PageSize, opts.Page, 5)
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
@@ -134,7 +134,7 @@ func Users(ctx *context.Context) {
|
||||
}
|
||||
ctx.Data["OrganizationsPageIsDisabled"] = setting.Service.Explore.DisableOrganizationsPage
|
||||
ctx.Data["CodePageIsDisabled"] = setting.Service.Explore.DisableCodePage
|
||||
ctx.Data["Title"] = ctx.Tr("explore")
|
||||
ctx.Data["Title"] = ctx.Tr("explore_title")
|
||||
ctx.Data["PageIsExplore"] = true
|
||||
ctx.Data["PageIsExploreUsers"] = true
|
||||
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
|
||||
@@ -153,7 +153,7 @@ func Users(ctx *context.Context) {
|
||||
|
||||
RenderUserSearch(ctx, user_model.SearchUserOptions{
|
||||
Actor: ctx.Doer,
|
||||
Type: user_model.UserTypeIndividual,
|
||||
Types: []user_model.UserType{user_model.UserTypeIndividual},
|
||||
ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum},
|
||||
IsActive: optional.Some(true),
|
||||
Visible: []structs.VisibleType{structs.VisibleTypePublic, structs.VisibleTypeLimited, structs.VisibleTypePrivate},
|
||||
|
||||
@@ -158,16 +158,16 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
|
||||
if link.Href == "#" {
|
||||
link.Href = srcLink
|
||||
}
|
||||
titleExtra = ctx.Locale.Tr("action.mirror_sync_push", act.GetRepoAbsoluteLink(ctx), srcLink, act.GetBranch(), act.ShortRepoPath(ctx))
|
||||
titleExtra = ctx.Locale.Tr("action.mirror_sync_push", act.GetRepoAbsoluteLink(ctx), srcLink, act.RefName, act.ShortRepoPath(ctx))
|
||||
case activities_model.ActionMirrorSyncCreate:
|
||||
srcLink := toSrcLink(ctx, act)
|
||||
if link.Href == "#" {
|
||||
link.Href = srcLink
|
||||
}
|
||||
titleExtra = ctx.Locale.Tr("action.mirror_sync_create", act.GetRepoAbsoluteLink(ctx), srcLink, act.GetBranch(), act.ShortRepoPath(ctx))
|
||||
titleExtra = ctx.Locale.Tr("action.mirror_sync_create", act.GetRepoAbsoluteLink(ctx), srcLink, act.RefName, act.ShortRepoPath(ctx))
|
||||
case activities_model.ActionMirrorSyncDelete:
|
||||
link.Href = act.GetRepoAbsoluteLink(ctx)
|
||||
titleExtra = ctx.Locale.Tr("action.mirror_sync_delete", act.GetRepoAbsoluteLink(ctx), act.GetBranch(), act.ShortRepoPath(ctx))
|
||||
titleExtra = ctx.Locale.Tr("action.mirror_sync_delete", act.GetRepoAbsoluteLink(ctx), act.RefName, act.ShortRepoPath(ctx))
|
||||
case activities_model.ActionApprovePullRequest:
|
||||
pullLink := toPullLink(ctx, act)
|
||||
titleExtra = ctx.Locale.Tr("action.approve_pull_request", pullLink, act.GetIssueInfos()[0], act.ShortRepoPath(ctx))
|
||||
|
||||
@@ -23,7 +23,6 @@ func TestCheckGetOrgFeedsAsOrgMember(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
|
||||
})
|
||||
@@ -31,7 +30,6 @@ func TestCheckGetOrgFeedsAsOrgMember(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
|
||||
})
|
||||
|
||||
@@ -6,13 +6,13 @@ package web
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/web/repo"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
func addOwnerRepoGitHTTPRouters(m *web.Router) {
|
||||
func addOwnerRepoGitHTTPRouters(m *web.Router, middlewares ...any) {
|
||||
m.Group("/{username}/{reponame}", func() {
|
||||
m.Methods("POST,OPTIONS", "/git-upload-pack", repo.ServiceUploadPack)
|
||||
m.Methods("POST,OPTIONS", "/git-receive-pack", repo.ServiceReceivePack)
|
||||
m.Methods("POST,OPTIONS", "/git-upload-archive", repo.ServiceUploadArchive)
|
||||
m.Methods("GET,OPTIONS", "/info/refs", repo.GetInfoRefs)
|
||||
m.Methods("GET,OPTIONS", "/HEAD", repo.GetTextFile("HEAD"))
|
||||
m.Methods("GET,OPTIONS", "/objects/info/alternates", repo.GetTextFile("objects/info/alternates"))
|
||||
@@ -22,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, repo.HTTPGitEnabledHandler, repo.CorsHandler(), context.UserAssignmentWeb())
|
||||
}, middlewares...)
|
||||
}
|
||||
|
||||
@@ -64,6 +64,15 @@ type componentStatus struct {
|
||||
}
|
||||
|
||||
// Check is the health check API handler
|
||||
//
|
||||
// HINT: HEALTH-CHECK-ENDPOINT: there is no clear definition about what "health" means.
|
||||
// In most cases, end users don't need to check such endpoint, because even if database is down,
|
||||
// Gitea will reover after database is up again. Sysop should monitor database and cache status directly.
|
||||
//
|
||||
// And keep in mind: this health check should NEVER be used as a "restart" trigger, for example: Docker's "HEALTHCHECK".
|
||||
// * If Gitea is upgrading and migrating database, there will be a long time before this endpoint starts to return "pass" status.
|
||||
// In this case, if the checker restarts Gitea just because it doesn't get "pass" status in short time,
|
||||
// the instance will just be restarted again and again before the migration finishes and the situation just goes worse.
|
||||
func Check(w http.ResponseWriter, r *http.Request) {
|
||||
rsp := response{
|
||||
Status: pass,
|
||||
|
||||
@@ -69,7 +69,7 @@ func HomeSitemap(ctx *context.Context) {
|
||||
m := sitemap.NewSitemapIndex()
|
||||
if !setting.Service.Explore.DisableUsersPage {
|
||||
_, cnt, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{
|
||||
Type: user_model.UserTypeIndividual,
|
||||
Types: []user_model.UserType{user_model.UserTypeIndividual},
|
||||
ListOptions: db.ListOptions{PageSize: 1},
|
||||
IsActive: optional.Some(true),
|
||||
Visible: []structs.VisibleType{structs.VisibleTypePublic},
|
||||
|
||||
@@ -6,12 +6,15 @@ package misc
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/httpcache"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
func SSHInfo(rw http.ResponseWriter, req *http.Request) {
|
||||
@@ -47,3 +50,15 @@ func StaticRedirect(target string) func(w http.ResponseWriter, req *http.Request
|
||||
http.Redirect(w, req, path.Join(setting.StaticURLPrefix, target), http.StatusMovedPermanently)
|
||||
}
|
||||
}
|
||||
|
||||
func LocationRedirect(target string) func(w http.ResponseWriter, req *http.Request) {
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
http.Redirect(w, req, target, http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
func WebBannerDismiss(ctx *context.Context) {
|
||||
_, rev, _ := setting.Config().Instance.WebBanner.ValueRevision(ctx)
|
||||
middleware.SetSiteCookie(ctx.Resp, middleware.CookieWebBannerDismissed, strconv.Itoa(rev), 48*3600)
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
@@ -6,15 +6,9 @@ package misc
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// tplSwagger swagger page template
|
||||
const tplSwagger templates.TplName = "swagger/ui"
|
||||
|
||||
// Swagger render swagger-ui page with v1 json
|
||||
func Swagger(ctx *context.Context) {
|
||||
ctx.Data["APIJSONVersion"] = "v1"
|
||||
ctx.HTML(http.StatusOK, tplSwagger)
|
||||
ctx.HTML(http.StatusOK, "swagger/openapi-viewer")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package misc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
"code.gitea.io/gitea/services/webtheme"
|
||||
)
|
||||
|
||||
func WebThemeList(ctx *context.Context) {
|
||||
curWebTheme := ctx.TemplateContext.CurrentWebTheme()
|
||||
renderUtils := templates.NewRenderUtils(ctx)
|
||||
allThemes := webtheme.GetAvailableThemes()
|
||||
|
||||
var results []map[string]any
|
||||
for _, theme := range allThemes {
|
||||
results = append(results, map[string]any{
|
||||
"name": renderUtils.RenderThemeItem(theme, 14),
|
||||
"value": theme.InternalName,
|
||||
"class": "item js-aria-clickable" + util.Iif(theme.InternalName == curWebTheme.InternalName, " selected", ""),
|
||||
})
|
||||
}
|
||||
ctx.JSON(http.StatusOK, map[string]any{"results": results})
|
||||
}
|
||||
|
||||
func WebThemeApply(ctx *context.Context) {
|
||||
themeName := ctx.FormString("theme")
|
||||
if ctx.Doer != nil {
|
||||
opts := &user_service.UpdateOptions{Theme: optional.Some(themeName)}
|
||||
_ = user_service.UpdateUser(ctx, ctx.Doer, opts)
|
||||
} else {
|
||||
middleware.SetSiteCookie(ctx.Resp, middleware.CookieTheme, themeName, 0)
|
||||
}
|
||||
}
|
||||
@@ -141,7 +141,7 @@ func home(ctx *context.Context, viewRepositories bool) {
|
||||
ctx.Data["Repos"] = repos
|
||||
ctx.Data["Total"] = count
|
||||
|
||||
pager := context.NewPagination(int(count), setting.UI.User.RepoPagingNum, page, 5)
|
||||
pager := context.NewPagination(count, setting.UI.User.RepoPagingNum, page, 5)
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package org
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
org_service "code.gitea.io/gitea/services/org"
|
||||
@@ -56,7 +58,7 @@ func Members(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
pager := context.NewPagination(int(total), setting.UI.MembersPagingNum, page, 5)
|
||||
pager := context.NewPagination(total, setting.UI.MembersPagingNum, page, 5)
|
||||
opts.ListOptions.Page = page
|
||||
opts.ListOptions.PageSize = setting.UI.MembersPagingNum
|
||||
members, membersIsPublic, err := organization.FindOrgMembers(ctx, opts)
|
||||
@@ -76,11 +78,11 @@ func Members(ctx *context.Context) {
|
||||
// MembersAction response for operation to a member of organization
|
||||
func MembersAction(ctx *context.Context) {
|
||||
member, err := user_model.GetUserByID(ctx, ctx.FormInt64("uid"))
|
||||
if err != nil {
|
||||
log.Error("GetUserByID: %v", err)
|
||||
}
|
||||
if member == nil {
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/members")
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.HTTPError(http.StatusNotFound)
|
||||
return
|
||||
} else if err != nil {
|
||||
ctx.ServerError("GetUserByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -105,40 +107,25 @@ func MembersAction(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
err = org_service.RemoveOrgUser(ctx, org, member)
|
||||
if organization.IsErrLastOrgOwner(err) {
|
||||
ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
|
||||
ctx.JSONRedirect(ctx.Org.OrgLink + "/members")
|
||||
return
|
||||
}
|
||||
case "leave":
|
||||
err = org_service.RemoveOrgUser(ctx, org, ctx.Doer)
|
||||
if err == nil {
|
||||
ctx.Flash.Success(ctx.Tr("form.organization_leave_success", org.DisplayName()))
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"redirect": "", // keep the user stay on current page, in case they want to do other operations.
|
||||
})
|
||||
} else if organization.IsErrLastOrgOwner(err) {
|
||||
ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
|
||||
ctx.JSONRedirect(ctx.Org.OrgLink + "/members")
|
||||
} else {
|
||||
log.Error("RemoveOrgUser(%d,%d): %v", org.ID, ctx.Doer.ID, err)
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
ctx.JSONOK()
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Error("Action(%s): %v", ctx.PathParam("action"), err)
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"ok": false,
|
||||
"err": err.Error(),
|
||||
})
|
||||
if organization.IsErrLastOrgOwner(err) {
|
||||
ctx.JSONError(ctx.Tr("form.last_org_owner"))
|
||||
return
|
||||
}
|
||||
|
||||
redirect := ctx.Org.OrgLink + "/members"
|
||||
if ctx.PathParam("action") == "leave" {
|
||||
redirect = setting.AppSubURL + "/"
|
||||
}
|
||||
|
||||
ctx.JSONRedirect(redirect)
|
||||
log.Error("Action(%s): %v", ctx.PathParam("action"), err)
|
||||
ctx.JSONError(err.Error()) // FIXME: legacy logic, errors are handled together, it's not right, need to distinguish between different errors
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
shared_mention "code.gitea.io/gitea/routers/web/shared/mention"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// GetMentionsInOwner returns JSON data for mention autocomplete on owner-level pages.
|
||||
func GetMentionsInOwner(ctx *context.Context) {
|
||||
// for individual users, we don't have a concept of "mentionable" users or teams, so just return an empty list
|
||||
if !ctx.ContextUser.IsOrganization() {
|
||||
ctx.JSON(http.StatusOK, []shared_mention.Mention{})
|
||||
return
|
||||
}
|
||||
|
||||
// for org, return members and teams
|
||||
c := shared_mention.NewCollector()
|
||||
org := organization.OrgFromUser(ctx.ContextUser)
|
||||
|
||||
// Get org members
|
||||
members, _, err := org.GetMembers(ctx, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetMembers", err)
|
||||
return
|
||||
}
|
||||
c.AddUsers(ctx, members)
|
||||
|
||||
// Get mentionable teams
|
||||
if err := c.AddMentionableTeams(ctx, ctx.Doer, ctx.ContextUser); err != nil {
|
||||
ctx.ServerError("AddMentionableTeams", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, util.SliceNilAsEmpty(c.Result))
|
||||
}
|
||||
@@ -65,13 +65,13 @@ func CreatePost(ctx *context.Context) {
|
||||
ctx.Data["Err_OrgName"] = true
|
||||
switch {
|
||||
case user_model.IsErrUserAlreadyExist(err):
|
||||
ctx.RenderWithErr(ctx.Tr("form.org_name_been_taken"), tplCreateOrg, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.org_name_been_taken"), tplCreateOrg, &form)
|
||||
case db.IsErrNameReserved(err):
|
||||
ctx.RenderWithErr(ctx.Tr("org.form.name_reserved", err.(db.ErrNameReserved).Name), tplCreateOrg, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("org.form.name_reserved", err.(db.ErrNameReserved).Name), tplCreateOrg, &form)
|
||||
case db.IsErrNamePatternNotAllowed(err):
|
||||
ctx.RenderWithErr(ctx.Tr("org.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplCreateOrg, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("org.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplCreateOrg, &form)
|
||||
case organization.IsErrUserNotAllowedCreateOrg(err):
|
||||
ctx.RenderWithErr(ctx.Tr("org.form.create_org_not_allowed"), tplCreateOrg, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("org.form.create_org_not_allowed"), tplCreateOrg, &form)
|
||||
default:
|
||||
ctx.ServerError("CreateOrganization", err)
|
||||
}
|
||||
|
||||
@@ -11,9 +11,8 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
org_model "code.gitea.io/gitea/models/organization"
|
||||
project_model "code.gitea.io/gitea/models/project"
|
||||
attachment_model "code.gitea.io/gitea/models/repo"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
@@ -25,6 +24,8 @@ import (
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
project_service "code.gitea.io/gitea/services/projects"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -112,12 +113,7 @@ func Projects(ctx *context.Context) {
|
||||
project.RenderedContent = renderUtils.MarkdownToHtml(project.Description)
|
||||
}
|
||||
|
||||
numPages := 0
|
||||
if total > 0 {
|
||||
numPages = (int(total) - 1/setting.UI.IssuePagingNum)
|
||||
}
|
||||
|
||||
pager := context.NewPagination(int(total), setting.UI.IssuePagingNum, page, numPages)
|
||||
pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5)
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
@@ -332,12 +328,26 @@ func ViewProject(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
assigneeID := ctx.FormString("assignee")
|
||||
milestoneID := ctx.FormInt64("milestone")
|
||||
|
||||
// Prepare milestone IDs for filtering
|
||||
var milestoneIDs []int64
|
||||
if milestoneID > 0 {
|
||||
milestoneIDs = []int64{milestoneID}
|
||||
} else if milestoneID == db.NoConditionID {
|
||||
milestoneIDs = []int64{db.NoConditionID}
|
||||
}
|
||||
|
||||
opts := issues_model.IssuesOptions{
|
||||
LabelIDs: preparedLabelFilter.SelectedLabelIDs,
|
||||
AssigneeID: assigneeID,
|
||||
Owner: project.Owner,
|
||||
Doer: ctx.Doer,
|
||||
LabelIDs: preparedLabelFilter.SelectedLabelIDs,
|
||||
AssigneeID: assigneeID,
|
||||
MilestoneIDs: milestoneIDs,
|
||||
Owner: project.Owner,
|
||||
}
|
||||
if ctx.Doer != nil {
|
||||
opts.Doer = ctx.Doer
|
||||
} else {
|
||||
opts.AllPublic = true
|
||||
}
|
||||
|
||||
issuesMap, err := project_service.LoadIssuesFromProject(ctx, project, &opts)
|
||||
@@ -350,10 +360,10 @@ func ViewProject(ctx *context.Context) {
|
||||
}
|
||||
|
||||
if project.CardType != project_model.CardTypeTextOnly {
|
||||
issuesAttachmentMap := make(map[int64][]*attachment_model.Attachment)
|
||||
issuesAttachmentMap := make(map[int64][]*repo_model.Attachment)
|
||||
for _, issuesList := range issuesMap {
|
||||
for _, issue := range issuesList {
|
||||
if issueAttachment, err := attachment_model.GetAttachmentsByIssueIDImagesLatest(ctx, issue.ID); err == nil {
|
||||
if issueAttachment, err := repo_model.GetAttachmentsByIssueIDImagesLatest(ctx, issue.ID); err == nil {
|
||||
issuesAttachmentMap[issue.ID] = issueAttachment
|
||||
}
|
||||
}
|
||||
@@ -411,10 +421,46 @@ func ViewProject(ctx *context.Context) {
|
||||
ctx.Data["Labels"] = labels
|
||||
ctx.Data["NumLabels"] = len(labels)
|
||||
|
||||
// Get milestones for filtering
|
||||
// For organization projects, we need to get milestones from all repos the user has access to
|
||||
var milestones issues_model.MilestoneList
|
||||
if project.RepoID > 0 {
|
||||
// Repo-specific project
|
||||
milestones, err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
|
||||
RepoID: project.RepoID,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetRepoMilestones", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Organization-wide project - get milestones from all organization repos
|
||||
// but only from repositories the current user can access.
|
||||
// Use RepoCond with a subquery to avoid materializing all repo IDs in memory
|
||||
// which can hit SQL parameter limits for orgs with many repos.
|
||||
accessCond := repo_model.AccessibleRepositoryCondition(ctx.Doer, unit.TypeIssues)
|
||||
repoCond := builder.And(
|
||||
builder.Eq{"owner_id": project.OwnerID},
|
||||
accessCond,
|
||||
)
|
||||
milestones, err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
|
||||
RepoCond: repoCond,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetOrgMilestones", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
openMilestones, closedMilestones := milestones.SplitByOpenClosed()
|
||||
ctx.Data["OpenMilestones"] = openMilestones
|
||||
ctx.Data["ClosedMilestones"] = closedMilestones
|
||||
ctx.Data["MilestoneID"] = milestoneID
|
||||
|
||||
// Get assignees.
|
||||
assigneeUsers, err := org_model.GetOrgAssignees(ctx, project.OwnerID)
|
||||
assigneeUsers, err := project_service.LoadIssuesAssigneesForProject(ctx, issuesMap)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetRepoAssignees", err)
|
||||
ctx.ServerError("LoadIssuesAssigneesForProject", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package org
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
@@ -69,24 +70,25 @@ func SettingsPost(ctx *context.Context) {
|
||||
}
|
||||
|
||||
org := ctx.Org.Organization
|
||||
|
||||
if form.Email != "" {
|
||||
if err := user_service.ReplacePrimaryEmailAddress(ctx, org.AsUser(), form.Email); err != nil {
|
||||
if err := org_service.UpdateOrgEmailAddress(ctx, org, form.Email); err != nil {
|
||||
if errors.Is(err, util.ErrInvalidArgument) {
|
||||
ctx.Data["Err_Email"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("form.email_invalid"), tplSettingsOptions, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.email_invalid"), tplSettingsOptions, &form)
|
||||
return
|
||||
}
|
||||
ctx.ServerError("UpdateOrgEmailAddress", err)
|
||||
return
|
||||
}
|
||||
|
||||
opts := &user_service.UpdateOptions{
|
||||
FullName: optional.Some(form.FullName),
|
||||
Description: optional.Some(form.Description),
|
||||
Website: optional.Some(form.Website),
|
||||
Location: optional.Some(form.Location),
|
||||
RepoAdminChangeTeamAccess: optional.Some(form.RepoAdminChangeTeamAccess),
|
||||
FullName: optional.FromPtr(form.FullName),
|
||||
Description: optional.FromPtr(form.Description),
|
||||
Website: optional.FromPtr(form.Website),
|
||||
Location: optional.FromPtr(form.Location),
|
||||
RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess),
|
||||
}
|
||||
if ctx.Doer.IsAdmin {
|
||||
opts.MaxRepoCreation = optional.Some(form.MaxRepoCreation)
|
||||
opts.MaxRepoCreation = optional.FromPtr(form.MaxRepoCreation)
|
||||
}
|
||||
|
||||
if err := user_service.UpdateUser(ctx, org.AsUser(), opts); err != nil {
|
||||
@@ -213,7 +215,7 @@ func SettingsRenamePost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := user_service.RenameUser(ctx, ctx.Org.Organization.AsUser(), newOrgName); err != nil {
|
||||
if err := user_service.RenameUser(ctx, ctx.Org.Organization.AsUser(), newOrgName, ctx.Doer); err != nil {
|
||||
if user_model.IsErrUserAlreadyExist(err) {
|
||||
ctx.JSONError(ctx.Tr("org.form.name_been_taken", newOrgName))
|
||||
} else if db.IsErrNameReserved(err) {
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
func RedirectToDefaultSetting(ctx *context.Context) {
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/settings/actions/runners")
|
||||
}
|
||||
@@ -84,7 +84,7 @@ func OAuth2ApplicationEdit(ctx *context.Context) {
|
||||
|
||||
// OAuthApplicationsRegenerateSecret handles the post request for regenerating the secret
|
||||
func OAuthApplicationsRegenerateSecret(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings")
|
||||
ctx.Data["Title"] = ctx.Tr("settings_title")
|
||||
ctx.Data["PageIsOrgSettings"] = true
|
||||
ctx.Data["PageIsSettingsApplications"] = true
|
||||
|
||||
|
||||
@@ -359,7 +359,7 @@ func NewTeamPost(ctx *context.Context) {
|
||||
}
|
||||
|
||||
if t.AccessMode < perm.AccessModeAdmin && len(unitPerms) == 0 {
|
||||
ctx.RenderWithErr(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -367,7 +367,7 @@ func NewTeamPost(ctx *context.Context) {
|
||||
ctx.Data["Err_TeamName"] = true
|
||||
switch {
|
||||
case org_model.IsErrTeamAlreadyExist(err):
|
||||
ctx.RenderWithErr(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form)
|
||||
default:
|
||||
ctx.ServerError("NewTeam", err)
|
||||
}
|
||||
@@ -536,7 +536,7 @@ func EditTeamPost(ctx *context.Context) {
|
||||
}
|
||||
|
||||
if t.AccessMode < perm.AccessModeAdmin && len(unitPerms) == 0 {
|
||||
ctx.RenderWithErr(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.team_no_units_error"), tplTeamNew, &form)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -544,7 +544,7 @@ func EditTeamPost(ctx *context.Context) {
|
||||
ctx.Data["Err_TeamName"] = true
|
||||
switch {
|
||||
case org_model.IsErrTeamAlreadyExist(err):
|
||||
ctx.RenderWithErr(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.team_name_been_taken"), tplTeamNew, &form)
|
||||
default:
|
||||
ctx.ServerError("UpdateTeam", err)
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ import (
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
|
||||
"github.com/nektos/act/pkg/model"
|
||||
"gopkg.in/yaml.v3"
|
||||
act_model "github.com/nektos/act/pkg/model"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -38,9 +38,10 @@ const (
|
||||
tplViewActions templates.TplName = "repo/actions/view"
|
||||
)
|
||||
|
||||
type Workflow struct {
|
||||
Entry git.TreeEntry
|
||||
ErrMsg string
|
||||
type WorkflowInfo struct {
|
||||
Entry git.TreeEntry
|
||||
ErrMsg string
|
||||
Workflow *act_model.Workflow
|
||||
}
|
||||
|
||||
// MustEnableActions check if actions are enabled in settings
|
||||
@@ -77,7 +78,11 @@ func List(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
workflows := prepareWorkflowDispatchTemplate(ctx, commit)
|
||||
workflows, curWorkflowID := prepareWorkflowTemplate(ctx, commit)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
prepareWorkflowDispatchTemplate(ctx, workflows, curWorkflowID)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
@@ -112,55 +117,46 @@ func WorkflowDispatchInputs(ctx *context.Context) {
|
||||
ctx.ServerError("GetTagCommit/GetBranchCommit", err)
|
||||
return
|
||||
}
|
||||
prepareWorkflowDispatchTemplate(ctx, commit)
|
||||
workflows, curWorkflowID := prepareWorkflowTemplate(ctx, commit)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
prepareWorkflowDispatchTemplate(ctx, workflows, curWorkflowID)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
ctx.HTML(http.StatusOK, tplDispatchInputsActions)
|
||||
}
|
||||
|
||||
func prepareWorkflowDispatchTemplate(ctx *context.Context, commit *git.Commit) (workflows []Workflow) {
|
||||
workflowID := ctx.FormString("workflow")
|
||||
ctx.Data["CurWorkflow"] = workflowID
|
||||
ctx.Data["CurWorkflowExists"] = false
|
||||
|
||||
var curWorkflow *model.Workflow
|
||||
func prepareWorkflowTemplate(ctx *context.Context, commit *git.Commit) (workflows []WorkflowInfo, curWorkflowID string) {
|
||||
curWorkflowID = ctx.FormString("workflow")
|
||||
|
||||
_, entries, err := actions.ListWorkflows(commit)
|
||||
if err != nil {
|
||||
ctx.ServerError("ListWorkflows", err)
|
||||
return nil
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
// Get all runner labels
|
||||
runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
IsOnline: optional.Some(true),
|
||||
WithAvailable: true,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("FindRunners", err)
|
||||
return nil
|
||||
}
|
||||
allRunnerLabels := make(container.Set[string])
|
||||
for _, r := range runners {
|
||||
allRunnerLabels.AddMultiple(r.AgentLabels...)
|
||||
}
|
||||
|
||||
workflows = make([]Workflow, 0, len(entries))
|
||||
workflows = make([]WorkflowInfo, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
workflow := Workflow{Entry: *entry}
|
||||
workflow := WorkflowInfo{Entry: *entry}
|
||||
content, err := actions.GetContentFromEntry(entry)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetContentFromEntry", err)
|
||||
return nil
|
||||
return nil, ""
|
||||
}
|
||||
wf, err := model.ReadWorkflow(bytes.NewReader(content))
|
||||
wf, err := act_model.ReadWorkflow(bytes.NewReader(content))
|
||||
if err != nil {
|
||||
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error())
|
||||
workflows = append(workflows, workflow)
|
||||
continue
|
||||
}
|
||||
if err := actions.ValidateWorkflowContent(content); err != nil {
|
||||
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error())
|
||||
workflows = append(workflows, workflow)
|
||||
continue
|
||||
}
|
||||
workflow.Workflow = wf
|
||||
// The workflow must contain at least one job without "needs". Otherwise, a deadlock will occur and no jobs will be able to run.
|
||||
hasJobWithoutNeeds := false
|
||||
// Check whether you have matching runner and a job without "needs"
|
||||
@@ -173,22 +169,6 @@ func prepareWorkflowDispatchTemplate(ctx *context.Context, commit *git.Commit) (
|
||||
if !hasJobWithoutNeeds && len(j.Needs()) == 0 {
|
||||
hasJobWithoutNeeds = true
|
||||
}
|
||||
runsOnList := j.RunsOn()
|
||||
for _, ro := range runsOnList {
|
||||
if strings.Contains(ro, "${{") {
|
||||
// Skip if it contains expressions.
|
||||
// The expressions could be very complex and could not be evaluated here,
|
||||
// so just skip it, it's OK since it's just a tooltip message.
|
||||
continue
|
||||
}
|
||||
if !allRunnerLabels.Contains(ro) {
|
||||
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", ro)
|
||||
break
|
||||
}
|
||||
}
|
||||
if workflow.ErrMsg != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasJobWithoutNeeds {
|
||||
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job_without_needs")
|
||||
@@ -197,61 +177,75 @@ func prepareWorkflowDispatchTemplate(ctx *context.Context, commit *git.Commit) (
|
||||
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job")
|
||||
}
|
||||
workflows = append(workflows, workflow)
|
||||
|
||||
if workflow.Entry.Name() == workflowID {
|
||||
curWorkflow = wf
|
||||
ctx.Data["CurWorkflowExists"] = true
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data["workflows"] = workflows
|
||||
ctx.Data["RepoLink"] = ctx.Repo.Repository.Link()
|
||||
|
||||
ctx.Data["AllowDisableOrEnableWorkflow"] = ctx.Repo.IsAdmin()
|
||||
actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
|
||||
ctx.Data["ActionsConfig"] = actionsConfig
|
||||
ctx.Data["CurWorkflow"] = curWorkflowID
|
||||
ctx.Data["CurWorkflowDisabled"] = actionsConfig.IsWorkflowDisabled(curWorkflowID)
|
||||
|
||||
if len(workflowID) > 0 && ctx.Repo.CanWrite(unit.TypeActions) {
|
||||
ctx.Data["AllowDisableOrEnableWorkflow"] = ctx.Repo.IsAdmin()
|
||||
isWorkflowDisabled := actionsConfig.IsWorkflowDisabled(workflowID)
|
||||
ctx.Data["CurWorkflowDisabled"] = isWorkflowDisabled
|
||||
|
||||
if !isWorkflowDisabled && curWorkflow != nil {
|
||||
workflowDispatchConfig := workflowDispatchConfig(curWorkflow)
|
||||
if workflowDispatchConfig != nil {
|
||||
ctx.Data["WorkflowDispatchConfig"] = workflowDispatchConfig
|
||||
|
||||
branchOpts := git_model.FindBranchOptions{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
IsDeletedBranch: optional.Some(false),
|
||||
ListOptions: db.ListOptions{
|
||||
ListAll: true,
|
||||
},
|
||||
}
|
||||
branches, err := git_model.FindBranchNames(ctx, branchOpts)
|
||||
if err != nil {
|
||||
ctx.ServerError("FindBranchNames", err)
|
||||
return nil
|
||||
}
|
||||
// always put default branch on the top if it exists
|
||||
if slices.Contains(branches, ctx.Repo.Repository.DefaultBranch) {
|
||||
branches = util.SliceRemoveAll(branches, ctx.Repo.Repository.DefaultBranch)
|
||||
branches = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...)
|
||||
}
|
||||
ctx.Data["Branches"] = branches
|
||||
|
||||
tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetTagNamesByRepoID", err)
|
||||
return nil
|
||||
}
|
||||
ctx.Data["Tags"] = tags
|
||||
}
|
||||
}
|
||||
}
|
||||
return workflows
|
||||
return workflows, curWorkflowID
|
||||
}
|
||||
|
||||
func prepareWorkflowList(ctx *context.Context, workflows []Workflow) {
|
||||
func prepareWorkflowDispatchTemplate(ctx *context.Context, workflowInfos []WorkflowInfo, curWorkflowID string) {
|
||||
actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
|
||||
if curWorkflowID == "" || !ctx.Repo.CanWrite(unit.TypeActions) || actionsConfig.IsWorkflowDisabled(curWorkflowID) {
|
||||
return
|
||||
}
|
||||
|
||||
var curWorkflow *act_model.Workflow
|
||||
for _, workflowInfo := range workflowInfos {
|
||||
if workflowInfo.Entry.Name() == curWorkflowID {
|
||||
if workflowInfo.Workflow == nil {
|
||||
log.Debug("CurWorkflowID %s is found but its workflowInfo.Workflow is nil", curWorkflowID)
|
||||
return
|
||||
}
|
||||
curWorkflow = workflowInfo.Workflow
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if curWorkflow == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["CurWorkflowExists"] = true
|
||||
curWfDispatchCfg := workflowDispatchConfig(curWorkflow)
|
||||
if curWfDispatchCfg == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["WorkflowDispatchConfig"] = curWfDispatchCfg
|
||||
|
||||
branchOpts := git_model.FindBranchOptions{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
IsDeletedBranch: optional.Some(false),
|
||||
ListOptions: db.ListOptions{
|
||||
ListAll: true,
|
||||
},
|
||||
}
|
||||
branches, err := git_model.FindBranchNames(ctx, branchOpts)
|
||||
if err != nil {
|
||||
ctx.ServerError("FindBranchNames", err)
|
||||
return
|
||||
}
|
||||
// always put default branch on the top
|
||||
branches = util.SliceRemoveAll(branches, ctx.Repo.Repository.DefaultBranch)
|
||||
branches = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...)
|
||||
ctx.Data["Branches"] = branches
|
||||
|
||||
tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetTagNamesByRepoID", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Tags"] = tags
|
||||
}
|
||||
|
||||
func prepareWorkflowList(ctx *context.Context, workflows []WorkflowInfo) {
|
||||
actorID := ctx.FormInt64("actor")
|
||||
status := ctx.FormInt("status")
|
||||
workflowID := ctx.FormString("workflow")
|
||||
@@ -302,6 +296,49 @@ func prepareWorkflowList(ctx *context.Context, workflows []Workflow) {
|
||||
log.Error("LoadIsRefDeleted", err)
|
||||
}
|
||||
|
||||
// Check for each run if there is at least one online runner that can run its jobs
|
||||
runErrors := make(map[int64]string)
|
||||
runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
IsOnline: optional.Some(true),
|
||||
WithAvailable: true,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("FindRunners", err)
|
||||
return
|
||||
}
|
||||
for _, run := range runs {
|
||||
if !run.Status.In(actions_model.StatusWaiting, actions_model.StatusRunning) {
|
||||
continue
|
||||
}
|
||||
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetRunJobsByRunID", err)
|
||||
return
|
||||
}
|
||||
for _, job := range jobs {
|
||||
if !job.Status.IsWaiting() {
|
||||
continue
|
||||
}
|
||||
if err := actions.ValidateWorkflowContent(job.WorkflowPayload); err != nil {
|
||||
runErrors[run.ID] = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error())
|
||||
break
|
||||
}
|
||||
hasOnlineRunner := false
|
||||
for _, runner := range runners {
|
||||
if !runner.IsDisabled && runner.CanMatchLabels(job.RunsOn) {
|
||||
hasOnlineRunner = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasOnlineRunner {
|
||||
runErrors[run.ID] = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", strings.Join(job.RunsOn, ","))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.Data["RunErrors"] = runErrors
|
||||
|
||||
ctx.Data["Runs"] = runs
|
||||
|
||||
actors, err := actions_model.GetActors(ctx, ctx.Repo.Repository.ID)
|
||||
@@ -313,7 +350,7 @@ func prepareWorkflowList(ctx *context.Context, workflows []Workflow) {
|
||||
|
||||
ctx.Data["StatusInfoList"] = actions_model.GetStatusInfoList(ctx, ctx.Locale)
|
||||
|
||||
pager := context.NewPagination(int(total), opts.PageSize, opts.Page, 5)
|
||||
pager := context.NewPagination(total, opts.PageSize, opts.Page, 5)
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(runs) > 0
|
||||
@@ -362,7 +399,7 @@ type WorkflowDispatch struct {
|
||||
Inputs []WorkflowDispatchInput
|
||||
}
|
||||
|
||||
func workflowDispatchConfig(w *model.Workflow) *WorkflowDispatch {
|
||||
func workflowDispatchConfig(w *act_model.Workflow) *WorkflowDispatch {
|
||||
switch w.RawOn.Kind {
|
||||
case yaml.ScalarNode:
|
||||
var val string
|
||||
|
||||
@@ -24,10 +24,11 @@ import (
|
||||
"code.gitea.io/gitea/modules/actions"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/common"
|
||||
@@ -36,43 +37,184 @@ import (
|
||||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
|
||||
"github.com/nektos/act/pkg/model"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
func getRunIndex(ctx *context_module.Context) int64 {
|
||||
// if run param is "latest", get the latest run index
|
||||
if ctx.PathParam("run") == "latest" {
|
||||
if run, _ := actions_model.GetLatestRun(ctx, ctx.Repo.Repository.ID); run != nil {
|
||||
return run.Index
|
||||
func findCurrentJobByPathParam(ctx *context_module.Context, jobs []*actions_model.ActionRunJob) (job *actions_model.ActionRunJob, hasPathParam bool) {
|
||||
selectedJobID := ctx.PathParamInt64("job")
|
||||
if selectedJobID <= 0 {
|
||||
return nil, false
|
||||
}
|
||||
for _, job = range jobs {
|
||||
if job.ID == selectedJobID {
|
||||
return job, true
|
||||
}
|
||||
}
|
||||
return ctx.PathParamInt64("run")
|
||||
return nil, true
|
||||
}
|
||||
|
||||
func getCurrentRunByPathParam(ctx *context_module.Context) (run *actions_model.ActionRun) {
|
||||
var err error
|
||||
// if run param is "latest", get the latest run id
|
||||
if ctx.PathParam("run") == "latest" {
|
||||
run, err = actions_model.GetLatestRun(ctx, ctx.Repo.Repository.ID)
|
||||
} else {
|
||||
run, err = actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("run"))
|
||||
}
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.NotFound(nil)
|
||||
} else if err != nil {
|
||||
ctx.ServerError("GetRun:"+ctx.PathParam("run"), err)
|
||||
}
|
||||
return run
|
||||
}
|
||||
|
||||
// resolveCurrentRunForView resolves GET Actions page URLs and supports both ID-based and legacy index-based forms.
|
||||
//
|
||||
// By default, run summary pages (/actions/runs/{run}) use a best-effort ID-first fallback,
|
||||
// and job pages (/actions/runs/{run}/jobs/{job}) try to confirm an ID-based URL first and prefer the ID-based interpretation when both are valid.
|
||||
//
|
||||
// `by_id=1` param explicitly forces the ID-based path, and `by_index=1` explicitly forces the legacy index-based path.
|
||||
// If both are present, `by_id` takes precedence.
|
||||
func resolveCurrentRunForView(ctx *context_module.Context) *actions_model.ActionRun {
|
||||
// `by_id` explicitly requests ID-based resolution, so the request skips the legacy index-based disambiguation logic and resolves the run by ID directly.
|
||||
// It takes precedence over `by_index` when both query parameters are present.
|
||||
if ctx.PathParam("run") == "latest" || ctx.FormBool("by_id") {
|
||||
return getCurrentRunByPathParam(ctx)
|
||||
}
|
||||
|
||||
runNum := ctx.PathParamInt64("run")
|
||||
if runNum <= 0 {
|
||||
ctx.NotFound(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
byIndex := ctx.FormBool("by_index")
|
||||
|
||||
if ctx.PathParam("job") == "" {
|
||||
// The URL does not contain a {job} path parameter, so it cannot use the
|
||||
// job-specific rules to disambiguate ID-based URLs from legacy index-based URLs.
|
||||
// Because of that, this path is handled with a best-effort ID-first fallback by default.
|
||||
//
|
||||
// When the same repository contains:
|
||||
// - a run whose ID matches runNum, and
|
||||
// - a different run whose repo-scope index also matches runNum
|
||||
// this path prefers the ID match and may show a different run than the old legacy URL originally intended,
|
||||
// unless `by_index=1` explicitly forces the legacy index-based interpretation.
|
||||
|
||||
if !byIndex {
|
||||
runByID, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runNum)
|
||||
if err == nil {
|
||||
return runByID
|
||||
}
|
||||
if !errors.Is(err, util.ErrNotExist) {
|
||||
ctx.ServerError("GetRun:"+ctx.PathParam("run"), err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
runByIndex, err := actions_model.GetRunByRepoAndIndex(ctx, ctx.Repo.Repository.ID, runNum)
|
||||
if err == nil {
|
||||
ctx.Redirect(fmt.Sprintf("%s/actions/runs/%d", ctx.Repo.RepoLink, runByIndex.ID), http.StatusFound)
|
||||
return nil
|
||||
}
|
||||
if !errors.Is(err, util.ErrNotExist) {
|
||||
ctx.ServerError("GetRunByRepoAndIndex", err)
|
||||
return nil
|
||||
}
|
||||
ctx.NotFound(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
jobNum := ctx.PathParamInt64("job")
|
||||
if jobNum < 0 {
|
||||
ctx.NotFound(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
// A job index should not be larger than MaxJobNumPerRun, so larger values can skip the legacy index-based path and be treated as job IDs directly.
|
||||
if !byIndex && jobNum >= actions_model.MaxJobNumPerRun {
|
||||
return getCurrentRunByPathParam(ctx)
|
||||
}
|
||||
|
||||
var runByID, runByIndex *actions_model.ActionRun
|
||||
var targetJobByIndex *actions_model.ActionRunJob
|
||||
|
||||
if !byIndex {
|
||||
// Probe the repo-scoped job ID first and only accept it when the job exists and belongs to the same runNum.
|
||||
job, err := actions_model.GetRunJobByRepoAndID(ctx, ctx.Repo.Repository.ID, jobNum)
|
||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||
ctx.ServerError("GetRunJobByRepoAndID", err)
|
||||
return nil
|
||||
}
|
||||
if job != nil {
|
||||
if err := job.LoadRun(ctx); err != nil {
|
||||
ctx.ServerError("LoadRun", err)
|
||||
return nil
|
||||
}
|
||||
if job.Run.ID == runNum {
|
||||
runByID = job.Run
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to resolve the request as a legacy run-index/job-index URL.
|
||||
{
|
||||
run, err := actions_model.GetRunByRepoAndIndex(ctx, ctx.Repo.Repository.ID, runNum)
|
||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||
ctx.ServerError("GetRunByRepoAndIndex", err)
|
||||
return nil
|
||||
}
|
||||
if run != nil {
|
||||
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetRunJobsByRunID", err)
|
||||
return nil
|
||||
}
|
||||
if jobNum < int64(len(jobs)) {
|
||||
runByIndex = run
|
||||
targetJobByIndex = jobs[jobNum]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if runByID == nil && runByIndex == nil {
|
||||
ctx.NotFound(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
if runByID != nil && runByIndex == nil {
|
||||
return runByID
|
||||
}
|
||||
|
||||
if runByID == nil && runByIndex != nil {
|
||||
ctx.Redirect(fmt.Sprintf("%s/actions/runs/%d/jobs/%d", ctx.Repo.RepoLink, runByIndex.ID, targetJobByIndex.ID), http.StatusFound)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reaching this point means both ID-based and legacy index-based interpretations are valid. Prefer the ID-based interpretation by default.
|
||||
// Use `by_index=1` query parameter to access the legacy index-based interpretation when necessary.
|
||||
return runByID
|
||||
}
|
||||
|
||||
func View(ctx *context_module.Context) {
|
||||
ctx.Data["PageIsActions"] = true
|
||||
runIndex := getRunIndex(ctx)
|
||||
jobIndex := ctx.PathParamInt64("job")
|
||||
ctx.Data["RunIndex"] = runIndex
|
||||
ctx.Data["JobIndex"] = jobIndex
|
||||
ctx.Data["ActionsURL"] = ctx.Repo.RepoLink + "/actions"
|
||||
|
||||
if getRunJobs(ctx, runIndex, jobIndex); ctx.Written() {
|
||||
run := resolveCurrentRunForView(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
ctx.Data["RunID"] = run.ID
|
||||
ctx.Data["JobID"] = ctx.PathParamInt64("job") // it can be 0 when no job (e.g.: run summary view)
|
||||
ctx.Data["ActionsURL"] = ctx.Repo.RepoLink + "/actions"
|
||||
|
||||
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)
|
||||
run := getCurrentRunByPathParam(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
commit, err := ctx.Repo.GitRepo.GetCommit(run.CommitSHA)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetCommit", func(err error) bool {
|
||||
@@ -115,6 +257,7 @@ type ViewResponse struct {
|
||||
|
||||
State struct {
|
||||
Run struct {
|
||||
RepoID int64 `json:"repoId"`
|
||||
Link string `json:"link"`
|
||||
Title string `json:"title"`
|
||||
TitleHTML template.HTML `json:"titleHTML"`
|
||||
@@ -122,6 +265,7 @@ type ViewResponse struct {
|
||||
CanCancel bool `json:"canCancel"`
|
||||
CanApprove bool `json:"canApprove"` // the run needs an approval and the doer has permission to approve
|
||||
CanRerun bool `json:"canRerun"`
|
||||
CanRerunFailed bool `json:"canRerunFailed"`
|
||||
CanDeleteArtifact bool `json:"canDeleteArtifact"`
|
||||
Done bool `json:"done"`
|
||||
WorkflowID string `json:"workflowID"`
|
||||
@@ -129,6 +273,10 @@ type ViewResponse struct {
|
||||
IsSchedule bool `json:"isSchedule"`
|
||||
Jobs []*ViewJob `json:"jobs"`
|
||||
Commit ViewCommit `json:"commit"`
|
||||
// Summary view: run duration and trigger time/event
|
||||
Duration string `json:"duration"`
|
||||
TriggeredAt int64 `json:"triggeredAt"` // unix seconds for relative time
|
||||
TriggerEvent string `json:"triggerEvent"` // e.g. pull_request, push, schedule
|
||||
} `json:"run"`
|
||||
CurrentJob struct {
|
||||
Title string `json:"title"`
|
||||
@@ -142,11 +290,13 @@ type ViewResponse struct {
|
||||
}
|
||||
|
||||
type ViewJob struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
CanRerun bool `json:"canRerun"`
|
||||
Duration string `json:"duration"`
|
||||
ID int64 `json:"id"`
|
||||
JobID string `json:"jobId,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
CanRerun bool `json:"canRerun"`
|
||||
Duration string `json:"duration"`
|
||||
Needs []string `json:"needs,omitempty"`
|
||||
}
|
||||
|
||||
type ViewCommit struct {
|
||||
@@ -186,12 +336,8 @@ type ViewStepLogLine struct {
|
||||
Timestamp float64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
func getActionsViewArtifacts(ctx context.Context, repoID, runIndex int64) (artifactsViewItems []*ArtifactsViewItem, err error) {
|
||||
run, err := actions_model.GetRunByIndex(ctx, repoID, runIndex)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
artifacts, err := actions_model.ListUploadedArtifactsMeta(ctx, run.ID)
|
||||
func getActionsViewArtifacts(ctx context.Context, repoID, runID int64) (artifactsViewItems []*ArtifactsViewItem, err error) {
|
||||
artifacts, err := actions_model.ListUploadedArtifactsMeta(ctx, repoID, runID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -206,30 +352,36 @@ func getActionsViewArtifacts(ctx context.Context, repoID, runIndex int64) (artif
|
||||
}
|
||||
|
||||
func ViewPost(ctx *context_module.Context) {
|
||||
req := web.GetForm(ctx).(*ViewRequest)
|
||||
runIndex := getRunIndex(ctx)
|
||||
jobIndex := ctx.PathParamInt64("job")
|
||||
|
||||
current, jobs := getRunJobs(ctx, runIndex, jobIndex)
|
||||
run, jobs := getCurrentRunJobsByPathParam(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
run := current.Run
|
||||
if err := run.LoadAttributes(ctx); err != nil {
|
||||
ctx.ServerError("run.LoadAttributes", err)
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
resp := &ViewResponse{}
|
||||
resp.Artifacts, err = getActionsViewArtifacts(ctx, ctx.Repo.Repository.ID, runIndex)
|
||||
fillViewRunResponseSummary(ctx, resp, run, jobs)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
fillViewRunResponseCurrentJob(ctx, resp, run, jobs)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func fillViewRunResponseSummary(ctx *context_module.Context, resp *ViewResponse, run *actions_model.ActionRun, jobs []*actions_model.ActionRunJob) {
|
||||
var err error
|
||||
resp.Artifacts, err = getActionsViewArtifacts(ctx, ctx.Repo.Repository.ID, run.ID)
|
||||
if err != nil {
|
||||
if !errors.Is(err, util.ErrNotExist) {
|
||||
ctx.ServerError("getActionsViewArtifacts", err)
|
||||
return
|
||||
}
|
||||
ctx.ServerError("getActionsViewArtifacts", err)
|
||||
return
|
||||
}
|
||||
|
||||
resp.State.Run.RepoID = ctx.Repo.Repository.ID
|
||||
// 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, ctx.Repo.Repository)
|
||||
@@ -238,6 +390,14 @@ func ViewPost(ctx *context_module.Context) {
|
||||
resp.State.Run.CanApprove = run.NeedApproval && ctx.Repo.CanWrite(unit.TypeActions)
|
||||
resp.State.Run.CanRerun = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
|
||||
resp.State.Run.CanDeleteArtifact = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions)
|
||||
if resp.State.Run.CanRerun {
|
||||
for _, job := range jobs {
|
||||
if job.Status == actions_model.StatusFailure || job.Status == actions_model.StatusCancelled {
|
||||
resp.State.Run.CanRerunFailed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.State.Run.Done = run.Status.IsDone()
|
||||
resp.State.Run.WorkflowID = run.WorkflowID
|
||||
resp.State.Run.WorkflowLink = run.WorkflowLink()
|
||||
@@ -247,10 +407,12 @@ func ViewPost(ctx *context_module.Context) {
|
||||
for _, v := range jobs {
|
||||
resp.State.Run.Jobs = append(resp.State.Run.Jobs, &ViewJob{
|
||||
ID: v.ID,
|
||||
JobID: v.JobID,
|
||||
Name: v.Name,
|
||||
Status: v.Status.String(),
|
||||
CanRerun: resp.State.Run.CanRerun,
|
||||
Duration: v.Duration().String(),
|
||||
Needs: v.Needs,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -278,6 +440,20 @@ func ViewPost(ctx *context_module.Context) {
|
||||
Pusher: pusher,
|
||||
Branch: branch,
|
||||
}
|
||||
resp.State.Run.Duration = run.Duration().String()
|
||||
resp.State.Run.TriggeredAt = run.Created.AsTime().Unix()
|
||||
resp.State.Run.TriggerEvent = run.TriggerEvent
|
||||
}
|
||||
|
||||
func fillViewRunResponseCurrentJob(ctx *context_module.Context, resp *ViewResponse, run *actions_model.ActionRun, jobs []*actions_model.ActionRunJob) {
|
||||
req := web.GetForm(ctx).(*ViewRequest)
|
||||
current, hasPathParam := findCurrentJobByPathParam(ctx, jobs)
|
||||
if current == nil {
|
||||
if hasPathParam {
|
||||
ctx.NotFound(nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var task *actions_model.ActionTask
|
||||
if current.TaskID > 0 {
|
||||
@@ -302,7 +478,7 @@ 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, logs, err := convertToViewModel(ctx, req.LogCursors, task)
|
||||
steps, logs, err := convertToViewModel(ctx, ctx.Locale, req.LogCursors, task)
|
||||
if err != nil {
|
||||
ctx.ServerError("convertToViewModel", err)
|
||||
return
|
||||
@@ -310,11 +486,9 @@ func ViewPost(ctx *context_module.Context) {
|
||||
resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, steps...)
|
||||
resp.Logs.StepsLog = append(resp.Logs.StepsLog, logs...)
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func convertToViewModel(ctx *context_module.Context, cursors []LogCursor, task *actions_model.ActionTask) ([]*ViewJobStep, []*ViewStepLog, error) {
|
||||
func convertToViewModel(ctx context.Context, locale translation.Locale, cursors []LogCursor, task *actions_model.ActionTask) ([]*ViewJobStep, []*ViewStepLog, error) {
|
||||
var viewJobs []*ViewJobStep
|
||||
var logs []*ViewStepLog
|
||||
|
||||
@@ -344,7 +518,7 @@ func convertToViewModel(ctx *context_module.Context, cursors []LogCursor, task *
|
||||
Lines: []*ViewStepLogLine{
|
||||
{
|
||||
Index: 1,
|
||||
Message: ctx.Locale.TrString("actions.runs.expire_log_message"),
|
||||
Message: 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),
|
||||
@@ -396,246 +570,202 @@ func convertToViewModel(ctx *context_module.Context, cursors []LogCursor, task *
|
||||
return viewJobs, logs, nil
|
||||
}
|
||||
|
||||
// Rerun will rerun jobs in the given run
|
||||
// If jobIndexStr is a blank string, it means rerun all jobs
|
||||
func Rerun(ctx *context_module.Context) {
|
||||
runIndex := getRunIndex(ctx)
|
||||
jobIndexStr := ctx.PathParam("job")
|
||||
var jobIndex int64
|
||||
if jobIndexStr != "" {
|
||||
jobIndex, _ = strconv.ParseInt(jobIndexStr, 10, 64)
|
||||
}
|
||||
|
||||
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetRunByIndex", err)
|
||||
return
|
||||
}
|
||||
|
||||
// rerun is not allowed if the run is not done
|
||||
// checkRunRerunAllowed checks whether a rerun is permitted for the given run,
|
||||
// writing the appropriate JSON error to ctx and returning false when it is not.
|
||||
func checkRunRerunAllowed(ctx *context_module.Context, run *actions_model.ActionRun) bool {
|
||||
if !run.Status.IsDone() {
|
||||
ctx.JSONError(ctx.Locale.Tr("actions.runs.not_done"))
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
// can not rerun job when workflow is disabled
|
||||
cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions)
|
||||
cfg := cfgUnit.ActionsConfig()
|
||||
if cfg.IsWorkflowDisabled(run.WorkflowID) {
|
||||
ctx.JSONError(ctx.Locale.Tr("actions.workflow.disabled"))
|
||||
return
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// reset run's start and stop time
|
||||
run.PreviousDuration = run.Duration()
|
||||
run.Started = 0
|
||||
run.Stopped = 0
|
||||
run.Status = actions_model.StatusWaiting
|
||||
if err := actions_model.UpdateRun(ctx, run, "started", "stopped", "status", "previous_duration"); err != nil {
|
||||
ctx.ServerError("UpdateRun", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := run.LoadAttributes(ctx); err != nil {
|
||||
ctx.ServerError("run.LoadAttributes", err)
|
||||
return
|
||||
}
|
||||
notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run)
|
||||
|
||||
job, jobs := getRunJobs(ctx, runIndex, jobIndex)
|
||||
// Rerun will rerun jobs in the given run
|
||||
// If jobIDStr is a blank string, it means rerun all jobs
|
||||
func Rerun(ctx *context_module.Context) {
|
||||
run, jobs := getCurrentRunJobsByPathParam(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if jobIndexStr == "" { // rerun all jobs
|
||||
for _, j := range jobs {
|
||||
// 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.ServerError("RerunJob", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx.JSONOK()
|
||||
if !checkRunRerunAllowed(ctx, run) {
|
||||
return
|
||||
}
|
||||
|
||||
rerunJobs := actions_service.GetAllRerunJobs(job, jobs)
|
||||
currentJob, hasPathParam := findCurrentJobByPathParam(ctx, jobs)
|
||||
if hasPathParam && currentJob == nil {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
for _, j := range rerunJobs {
|
||||
// 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.ServerError("RerunJob", err)
|
||||
return
|
||||
}
|
||||
var jobsToRerun []*actions_model.ActionRunJob
|
||||
if currentJob != nil {
|
||||
jobsToRerun = actions_service.GetAllRerunJobs(currentJob, jobs)
|
||||
} else {
|
||||
jobsToRerun = jobs
|
||||
}
|
||||
|
||||
if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, jobsToRerun); err != nil {
|
||||
ctx.ServerError("RerunWorkflowRunJobs", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shouldBlock bool) error {
|
||||
status := job.Status
|
||||
if !status.IsDone() {
|
||||
return nil
|
||||
// RerunFailed reruns all failed jobs in the given run
|
||||
func RerunFailed(ctx *context_module.Context) {
|
||||
run, jobs := getCurrentRunJobsByPathParam(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
job.TaskID = 0
|
||||
job.Status = actions_model.StatusWaiting
|
||||
if shouldBlock {
|
||||
job.Status = actions_model.StatusBlocked
|
||||
}
|
||||
job.Started = 0
|
||||
job.Stopped = 0
|
||||
|
||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
_, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": status}, "task_id", "status", "started", "stopped")
|
||||
return err
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
actions_service.CreateCommitStatus(ctx, job)
|
||||
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Logs(ctx *context_module.Context) {
|
||||
runIndex := getRunIndex(ctx)
|
||||
jobIndex := ctx.PathParamInt64("job")
|
||||
|
||||
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)
|
||||
if !checkRunRerunAllowed(ctx, run) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = common.DownloadActionsRunJobLogsWithIndex(ctx.Base, ctx.Repo.Repository, run.ID, jobIndex); err != nil {
|
||||
ctx.NotFoundOrServerError("DownloadActionsRunJobLogsWithIndex", func(err error) bool {
|
||||
if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, actions_service.GetFailedRerunJobs(jobs)); err != nil {
|
||||
ctx.ServerError("RerunWorkflowRunJobs", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
func Logs(ctx *context_module.Context) {
|
||||
run := getCurrentRunByPathParam(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
jobID := ctx.PathParamInt64("job")
|
||||
|
||||
if err := common.DownloadActionsRunJobLogsWithID(ctx.Base, ctx.Repo.Repository, run.ID, jobID); err != nil {
|
||||
ctx.NotFoundOrServerError("DownloadActionsRunJobLogsWithID", func(err error) bool {
|
||||
return errors.Is(err, util.ErrNotExist)
|
||||
}, err)
|
||||
}
|
||||
}
|
||||
|
||||
func Cancel(ctx *context_module.Context) {
|
||||
runIndex := getRunIndex(ctx)
|
||||
|
||||
_, jobs := getRunJobs(ctx, runIndex, -1)
|
||||
run, jobs := getCurrentRunJobsByPathParam(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
var updatedjobs []*actions_model.ActionRunJob
|
||||
var updatedJobs []*actions_model.ActionRunJob
|
||||
|
||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
for _, job := range jobs {
|
||||
status := job.Status
|
||||
if status.IsDone() {
|
||||
continue
|
||||
}
|
||||
if job.TaskID == 0 {
|
||||
job.Status = actions_model.StatusCancelled
|
||||
job.Stopped = timeutil.TimeStampNow()
|
||||
n, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n == 0 {
|
||||
return errors.New("job has changed, try again")
|
||||
}
|
||||
if n > 0 {
|
||||
updatedjobs = append(updatedjobs, job)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := actions_model.StopTask(ctx, job.TaskID, actions_model.StatusCancelled); err != nil {
|
||||
return err
|
||||
}
|
||||
cancelledJobs, err := actions_model.CancelJobs(ctx, jobs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cancel jobs: %w", err)
|
||||
}
|
||||
updatedJobs = append(updatedJobs, cancelledJobs...)
|
||||
return nil
|
||||
}); err != nil {
|
||||
ctx.ServerError("StopTask", err)
|
||||
return
|
||||
}
|
||||
|
||||
actions_service.CreateCommitStatus(ctx, jobs...)
|
||||
actions_service.CreateCommitStatusForRunJobs(ctx, run, jobs...)
|
||||
actions_service.EmitJobsIfReadyByJobs(updatedJobs)
|
||||
|
||||
for _, job := range updatedjobs {
|
||||
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]
|
||||
if len(updatedJobs) > 0 {
|
||||
job := updatedJobs[0]
|
||||
actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job)
|
||||
}
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
func Approve(ctx *context_module.Context) {
|
||||
runIndex := getRunIndex(ctx)
|
||||
|
||||
current, jobs := getRunJobs(ctx, runIndex, -1)
|
||||
run := getCurrentRunByPathParam(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
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
|
||||
if err := actions_model.UpdateRun(ctx, run, "need_approval", "approved_by"); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, job := range jobs {
|
||||
if len(job.Needs) == 0 && job.Status.IsBlocked() {
|
||||
job.Status = actions_model.StatusWaiting
|
||||
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.ServerError("UpdateRunJob", err)
|
||||
approveRuns(ctx, []int64{run.ID})
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
actions_service.CreateCommitStatus(ctx, jobs...)
|
||||
|
||||
if len(updatedjobs) > 0 {
|
||||
job := updatedjobs[0]
|
||||
actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job)
|
||||
}
|
||||
|
||||
for _, job := range updatedjobs {
|
||||
_ = job.LoadAttributes(ctx)
|
||||
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
|
||||
}
|
||||
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
func Delete(ctx *context_module.Context) {
|
||||
runIndex := getRunIndex(ctx)
|
||||
repoID := ctx.Repo.Repository.ID
|
||||
func approveRuns(ctx *context_module.Context, runIDs []int64) {
|
||||
doer := ctx.Doer
|
||||
repo := ctx.Repo.Repository
|
||||
|
||||
run, err := actions_model.GetRunByIndex(ctx, repoID, runIndex)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.JSONErrorNotFound()
|
||||
return
|
||||
updatedJobs := make([]*actions_model.ActionRunJob, 0)
|
||||
runMap := make(map[int64]*actions_model.ActionRun, len(runIDs))
|
||||
runJobs := make(map[int64][]*actions_model.ActionRunJob, len(runIDs))
|
||||
|
||||
err := db.WithTx(ctx, func(ctx context.Context) (err error) {
|
||||
for _, runID := range runIDs {
|
||||
run, err := actions_model.GetRunByRepoAndID(ctx, repo.ID, runID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runMap[run.ID] = run
|
||||
run.Repo = repo
|
||||
run.NeedApproval = false
|
||||
run.ApprovedBy = doer.ID
|
||||
if err := actions_model.UpdateRun(ctx, run, "need_approval", "approved_by"); err != nil {
|
||||
return err
|
||||
}
|
||||
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runJobs[run.ID] = jobs
|
||||
for _, job := range jobs {
|
||||
job.Status, err = actions_service.PrepareToStartJobWithConcurrency(ctx, job)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if job.Status == actions_model.StatusWaiting {
|
||||
n, err := actions_model.UpdateRunJob(ctx, job, nil, "status")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n > 0 {
|
||||
updatedJobs = append(updatedJobs, job)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.ServerError("GetRunByIndex", err)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("approveRuns", func(err error) bool {
|
||||
return errors.Is(err, util.ErrNotExist)
|
||||
}, err)
|
||||
return
|
||||
}
|
||||
|
||||
for runID, run := range runMap {
|
||||
actions_service.CreateCommitStatusForRunJobs(ctx, run, runJobs[runID]...)
|
||||
}
|
||||
|
||||
if len(updatedJobs) > 0 {
|
||||
job := updatedJobs[0]
|
||||
actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job)
|
||||
}
|
||||
|
||||
for _, job := range updatedJobs {
|
||||
_ = job.LoadAttributes(ctx)
|
||||
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func Delete(ctx *context_module.Context) {
|
||||
run := getCurrentRunByPathParam(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -652,17 +782,11 @@ func Delete(ctx *context_module.Context) {
|
||||
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.
|
||||
func getRunJobs(ctx *context_module.Context, runIndex, jobIndex int64) (*actions_model.ActionRunJob, []*actions_model.ActionRunJob) {
|
||||
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.NotFound(nil)
|
||||
return nil, nil
|
||||
}
|
||||
ctx.ServerError("GetRunByIndex", err)
|
||||
// getRunJobs loads the run and its jobs for runID
|
||||
// Any error will be written to the ctx, empty jobs will also result in 404 error, then the return values are all nil.
|
||||
func getCurrentRunJobsByPathParam(ctx *context_module.Context) (*actions_model.ActionRun, []*actions_model.ActionRunJob) {
|
||||
run := getCurrentRunByPathParam(ctx)
|
||||
if ctx.Written() {
|
||||
return nil, nil
|
||||
}
|
||||
run.Repo = ctx.Repo.Repository
|
||||
@@ -676,28 +800,19 @@ func getRunJobs(ctx *context_module.Context, runIndex, jobIndex int64) (*actions
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
for _, v := range jobs {
|
||||
v.Run = run
|
||||
for _, job := range jobs {
|
||||
job.Run = run
|
||||
}
|
||||
|
||||
if jobIndex >= 0 && jobIndex < int64(len(jobs)) {
|
||||
return jobs[jobIndex], jobs
|
||||
}
|
||||
return jobs[0], jobs
|
||||
return run, jobs
|
||||
}
|
||||
|
||||
func ArtifactsDeleteView(ctx *context_module.Context) {
|
||||
runIndex := getRunIndex(ctx)
|
||||
artifactName := ctx.PathParam("artifact_name")
|
||||
|
||||
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)
|
||||
run := getCurrentRunByPathParam(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
if err = actions_model.SetArtifactNeedDelete(ctx, run.ID, artifactName); err != nil {
|
||||
artifactName := ctx.PathParam("artifact_name")
|
||||
if err := actions_model.SetArtifactNeedDelete(ctx, run.ID, artifactName); err != nil {
|
||||
ctx.ServerError("SetArtifactNeedDelete", err)
|
||||
return
|
||||
}
|
||||
@@ -705,19 +820,12 @@ func ArtifactsDeleteView(ctx *context_module.Context) {
|
||||
}
|
||||
|
||||
func ArtifactsDownloadView(ctx *context_module.Context) {
|
||||
runIndex := getRunIndex(ctx)
|
||||
artifactName := ctx.PathParam("artifact_name")
|
||||
|
||||
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.HTTPError(http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
ctx.ServerError("GetRunByIndex", err)
|
||||
run := getCurrentRunByPathParam(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
artifactName := ctx.PathParam("artifact_name")
|
||||
artifacts, err := db.Find[actions_model.ActionArtifact](ctx, actions_model.FindArtifactsOptions{
|
||||
RunID: run.ID,
|
||||
ArtifactName: artifactName,
|
||||
@@ -739,8 +847,9 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip; filename*=UTF-8''%s.zip", url.PathEscape(artifactName), artifactName))
|
||||
|
||||
// A v4 Artifact may only contain a single file
|
||||
// Multiple files are uploaded as a single file archive
|
||||
// All other cases fall back to the legacy v1–v3 zip handling below
|
||||
if len(artifacts) == 1 && actions.IsArtifactV4(artifacts[0]) {
|
||||
err := actions.DownloadArtifactV4(ctx.Base, artifacts[0])
|
||||
if err != nil {
|
||||
@@ -752,39 +861,82 @@ func ArtifactsDownloadView(ctx *context_module.Context) {
|
||||
|
||||
// Artifacts using the v1-v3 backend are stored as multiple individual files per artifact on the backend
|
||||
// Those need to be zipped for download
|
||||
writer := zip.NewWriter(ctx.Resp)
|
||||
defer writer.Close()
|
||||
for _, art := range artifacts {
|
||||
ctx.Resp.Header().Set("Content-Disposition", httplib.EncodeContentDispositionAttachment(artifactName+".zip"))
|
||||
zipWriter := zip.NewWriter(ctx.Resp)
|
||||
defer zipWriter.Close()
|
||||
|
||||
writeArtifactToZip := func(art *actions_model.ActionArtifact) error {
|
||||
f, err := storage.ActionsArtifacts.Open(art.StoragePath)
|
||||
if err != nil {
|
||||
ctx.ServerError("ActionsArtifacts.Open", err)
|
||||
return
|
||||
return fmt.Errorf("ActionsArtifacts.Open: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var r io.ReadCloser
|
||||
if art.ContentEncoding == "gzip" {
|
||||
var r io.ReadCloser = f
|
||||
if art.ContentEncodingOrType == actions_model.ContentEncodingV3Gzip {
|
||||
r, err = gzip.NewReader(f)
|
||||
if err != nil {
|
||||
ctx.ServerError("gzip.NewReader", err)
|
||||
return
|
||||
return fmt.Errorf("gzip.NewReader: %w", err)
|
||||
}
|
||||
} else {
|
||||
r = f
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
w, err := writer.Create(art.ArtifactPath)
|
||||
w, err := zipWriter.Create(art.ArtifactPath)
|
||||
if err != nil {
|
||||
ctx.ServerError("writer.Create", err)
|
||||
return
|
||||
return fmt.Errorf("zipWriter.Create: %w", err)
|
||||
}
|
||||
if _, err := io.Copy(w, r); err != nil {
|
||||
ctx.ServerError("io.Copy", err)
|
||||
_, err = io.Copy(w, r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("io.Copy: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, art := range artifacts {
|
||||
err := writeArtifactToZip(art)
|
||||
if err != nil {
|
||||
ctx.ServerError("writeArtifactToZip", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ApproveAllChecks(ctx *context_module.Context) {
|
||||
repo := ctx.Repo.Repository
|
||||
commitID := ctx.FormString("commit_id")
|
||||
|
||||
commitStatuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, commitID, db.ListOptionsAll)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLatestCommitStatus", err)
|
||||
return
|
||||
}
|
||||
runs, err := actions_service.GetRunsFromCommitStatuses(ctx, commitStatuses)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetRunsFromCommitStatuses", err)
|
||||
return
|
||||
}
|
||||
|
||||
runIDs := make([]int64, 0, len(runs))
|
||||
for _, run := range runs {
|
||||
if run.NeedApproval {
|
||||
runIDs = append(runIDs, run.ID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(runIDs) == 0 {
|
||||
ctx.JSONOK()
|
||||
return
|
||||
}
|
||||
|
||||
approveRuns(ctx, runIDs)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("actions.approve_all_success"))
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
func DisableWorkflowFile(ctx *context_module.Context) {
|
||||
disableOrEnableWorkflowFile(ctx, false)
|
||||
}
|
||||
@@ -796,7 +948,7 @@ func EnableWorkflowFile(ctx *context_module.Context) {
|
||||
func disableOrEnableWorkflowFile(ctx *context_module.Context, isEnable bool) {
|
||||
workflow := ctx.FormString("workflow")
|
||||
if len(workflow) == 0 {
|
||||
ctx.ServerError("workflow", nil)
|
||||
ctx.JSONError("workflow is required")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -809,7 +961,7 @@ func disableOrEnableWorkflowFile(ctx *context_module.Context, isEnable bool) {
|
||||
cfg.DisableWorkflow(workflow)
|
||||
}
|
||||
|
||||
if err := repo_model.UpdateRepoUnit(ctx, cfgUnit); err != nil {
|
||||
if err := repo_model.UpdateRepoUnitConfig(ctx, cfgUnit); err != nil {
|
||||
ctx.ServerError("UpdateRepoUnit", err)
|
||||
return
|
||||
}
|
||||
@@ -840,7 +992,7 @@ func Run(ctx *context_module.Context) {
|
||||
ctx.ServerError("ref", nil)
|
||||
return
|
||||
}
|
||||
err := actions_service.DispatchActionWorkflow(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, workflowID, ref, func(workflowDispatch *model.WorkflowDispatch, inputs map[string]any) error {
|
||||
_, 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" {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConvertToViewModel(t *testing.T) {
|
||||
task := &actions_model.ActionTask{
|
||||
Status: actions_model.StatusSuccess,
|
||||
Steps: []*actions_model.ActionTaskStep{
|
||||
{Name: "Run step-name", Index: 0, Status: actions_model.StatusSuccess, LogLength: 1, Started: timeutil.TimeStamp(1), Stopped: timeutil.TimeStamp(5)},
|
||||
},
|
||||
Stopped: timeutil.TimeStamp(20),
|
||||
}
|
||||
|
||||
viewJobSteps, _, err := convertToViewModel(t.Context(), translation.MockLocale{}, nil, task)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedViewJobs := []*ViewJobStep{
|
||||
{
|
||||
Summary: "Set up job",
|
||||
Duration: "0s",
|
||||
Status: "success",
|
||||
},
|
||||
{
|
||||
Summary: "Run step-name",
|
||||
Duration: "4s",
|
||||
Status: "success",
|
||||
},
|
||||
{
|
||||
Summary: "Complete job",
|
||||
Duration: "15s",
|
||||
Status: "success",
|
||||
},
|
||||
}
|
||||
assert.Equal(t, expectedViewJobs, viewJobSteps)
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
"code.gitea.io/gitea/models/git"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
@@ -25,38 +25,33 @@ func Activity(ctx *context.Context) {
|
||||
|
||||
ctx.Data["PageIsPulse"] = true
|
||||
|
||||
ctx.Data["Period"] = ctx.PathParam("period")
|
||||
|
||||
timeUntil := time.Now()
|
||||
var timeFrom time.Time
|
||||
|
||||
switch ctx.Data["Period"] {
|
||||
period, timeFrom := "weekly", timeUntil.Add(-time.Hour*168)
|
||||
switch ctx.PathParam("period") {
|
||||
case "daily":
|
||||
timeFrom = timeUntil.Add(-time.Hour * 24)
|
||||
period, timeFrom = "daily", timeUntil.Add(-time.Hour*24)
|
||||
case "halfweekly":
|
||||
timeFrom = timeUntil.Add(-time.Hour * 72)
|
||||
period, timeFrom = "halfweekly", timeUntil.Add(-time.Hour*72)
|
||||
case "weekly":
|
||||
timeFrom = timeUntil.Add(-time.Hour * 168)
|
||||
period, timeFrom = "weekly", timeUntil.Add(-time.Hour*168)
|
||||
case "monthly":
|
||||
timeFrom = timeUntil.AddDate(0, -1, 0)
|
||||
period, timeFrom = "monthly", timeUntil.AddDate(0, -1, 0)
|
||||
case "quarterly":
|
||||
timeFrom = timeUntil.AddDate(0, -3, 0)
|
||||
period, timeFrom = "quarterly", timeUntil.AddDate(0, -3, 0)
|
||||
case "semiyearly":
|
||||
timeFrom = timeUntil.AddDate(0, -6, 0)
|
||||
period, timeFrom = "semiyearly", timeUntil.AddDate(0, -6, 0)
|
||||
case "yearly":
|
||||
timeFrom = timeUntil.AddDate(-1, 0, 0)
|
||||
default:
|
||||
ctx.Data["Period"] = "weekly"
|
||||
timeFrom = timeUntil.Add(-time.Hour * 168)
|
||||
period, timeFrom = "yearly", timeUntil.AddDate(-1, 0, 0)
|
||||
}
|
||||
ctx.Data["DateFrom"] = timeFrom
|
||||
ctx.Data["DateUntil"] = timeUntil
|
||||
ctx.Data["PeriodText"] = ctx.Tr("repo.activity.period." + ctx.Data["Period"].(string))
|
||||
ctx.Data["Period"] = period
|
||||
ctx.Data["PeriodText"] = ctx.Tr("repo.activity.period." + period)
|
||||
|
||||
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)
|
||||
branchExist, _ := git_model.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)
|
||||
|
||||
@@ -6,34 +6,45 @@ package repo
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
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/httpcache"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/common"
|
||||
"code.gitea.io/gitea/services/attachment"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/context/upload"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
)
|
||||
|
||||
func attachmentReadScope(unitType unit.Type) (auth_model.AccessTokenScope, bool) {
|
||||
switch unitType {
|
||||
case unit.TypeIssues, unit.TypePullRequests:
|
||||
return auth_model.AccessTokenScopeReadIssue, true
|
||||
case unit.TypeReleases:
|
||||
return auth_model.AccessTokenScopeReadRepository, true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
// UploadIssueAttachment response for Issue/PR attachments
|
||||
func UploadIssueAttachment(ctx *context.Context) {
|
||||
uploadAttachment(ctx, ctx.Repo.Repository.ID, setting.Attachment.AllowedTypes)
|
||||
uploadAttachment(ctx, ctx.Repo.Repository.ID, attachment.UploadAttachmentForIssue)
|
||||
}
|
||||
|
||||
// UploadReleaseAttachment response for uploading release attachments
|
||||
func UploadReleaseAttachment(ctx *context.Context) {
|
||||
uploadAttachment(ctx, ctx.Repo.Repository.ID, setting.Repository.Release.AllowedTypes)
|
||||
uploadAttachment(ctx, ctx.Repo.Repository.ID, attachment.UploadAttachmentForRelease)
|
||||
}
|
||||
|
||||
// UploadAttachment response for uploading attachments
|
||||
func uploadAttachment(ctx *context.Context, repoID int64, allowedTypes string) {
|
||||
func uploadAttachment(ctx *context.Context, repoID int64, uploadFunc attachment.UploadAttachmentFunc) {
|
||||
if !setting.Attachment.Enabled {
|
||||
ctx.HTTPError(http.StatusNotFound, "attachment is not enabled")
|
||||
return
|
||||
@@ -47,7 +58,7 @@ func uploadAttachment(ctx *context.Context, repoID int64, allowedTypes string) {
|
||||
defer file.Close()
|
||||
|
||||
uploaderFile := attachment.NewLimitedUploaderKnownSize(file, header.Size)
|
||||
attach, err := attachment.UploadAttachmentGeneralSizeLimit(ctx, uploaderFile, allowedTypes, &repo_model.Attachment{
|
||||
attach, err := uploadFunc(ctx, uploaderFile, &repo_model.Attachment{
|
||||
Name: header.Filename,
|
||||
UploaderID: ctx.Doer.ID,
|
||||
RepoID: repoID,
|
||||
@@ -57,7 +68,7 @@ func uploadAttachment(ctx *context.Context, repoID int64, allowedTypes string) {
|
||||
ctx.HTTPError(http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
ctx.ServerError("UploadAttachmentGeneralSizeLimit", err)
|
||||
ctx.ServerError("uploadAttachment(uploadFunc)", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -120,7 +131,7 @@ func DeleteAttachment(ctx *context.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// GetAttachment serve attachments with the given UUID
|
||||
// ServeAttachment serve attachments with the given UUID
|
||||
func ServeAttachment(ctx *context.Context, uuid string) {
|
||||
attach, err := repo_model.GetAttachmentByUUID(ctx, uuid)
|
||||
if err != nil {
|
||||
@@ -151,16 +162,19 @@ func ServeAttachment(ctx *context.Context, uuid string) {
|
||||
return
|
||||
}
|
||||
} else { // If we have the linked type, we need to check access
|
||||
var perm access_model.Permission
|
||||
if ctx.Repo.Repository == nil {
|
||||
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
|
||||
var (
|
||||
perm access_model.Permission
|
||||
repo = ctx.Repo.Repository
|
||||
)
|
||||
if repo == nil {
|
||||
repo, err = repo_model.GetRepositoryByID(ctx, repoID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetRepositoryByID", err)
|
||||
return
|
||||
}
|
||||
perm, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
|
||||
perm, err = access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserRepoPermission", err)
|
||||
ctx.ServerError("GetDoerRepoPermission", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
@@ -171,6 +185,13 @@ func ServeAttachment(ctx *context.Context, uuid string) {
|
||||
ctx.HTTPError(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if requiredScope, ok := attachmentReadScope(unitType); ok {
|
||||
context.CheckTokenScopes(ctx, repo, requiredScope)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := attach.IncreaseDownloadCount(ctx); err != nil {
|
||||
@@ -180,7 +201,7 @@ func ServeAttachment(ctx *context.Context, uuid string) {
|
||||
|
||||
if setting.Attachment.Storage.ServeDirect() {
|
||||
// If we have a signed url (S3, object storage), redirect to this directly.
|
||||
u, err := storage.Attachments.URL(attach.RelativePath(), attach.Name, ctx.Req.Method, nil)
|
||||
u, err := storage.Attachments.ServeDirectURL(attach.RelativePath(), attach.Name, ctx.Req.Method, nil)
|
||||
|
||||
if u != nil && err == nil {
|
||||
ctx.Redirect(u.String())
|
||||
@@ -188,7 +209,7 @@ func ServeAttachment(ctx *context.Context, uuid string) {
|
||||
}
|
||||
}
|
||||
|
||||
if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+attach.UUID+`"`) {
|
||||
if httpcache.HandleGenericETagPrivateCache(ctx.Req, ctx.Resp, `"`+attach.UUID+`"`, attach.CreatedUnix.AsTimePtr()) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -200,7 +221,7 @@ func ServeAttachment(ctx *context.Context, uuid string) {
|
||||
}
|
||||
defer fr.Close()
|
||||
|
||||
common.ServeContentByReadSeeker(ctx.Base, attach.Name, util.ToPointer(attach.CreatedUnix.AsTime()), fr)
|
||||
httplib.ServeUserContentByFile(ctx.Req, ctx.Resp, fr, httplib.ServeHeaderOptions{Filename: attach.Name})
|
||||
}
|
||||
|
||||
// GetAttachment serve attachments
|
||||
|
||||
@@ -4,19 +4,20 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
gotemplate "html/template"
|
||||
"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/gitrepo"
|
||||
"code.gitea.io/gitea/modules/highlight"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@@ -26,24 +27,23 @@ import (
|
||||
)
|
||||
|
||||
type blameRow struct {
|
||||
RowNumber int
|
||||
Avatar gotemplate.HTML
|
||||
RepoLink string
|
||||
PartSha string
|
||||
RowNumber int
|
||||
|
||||
Avatar template.HTML
|
||||
PreviousSha string
|
||||
PreviousShaURL string
|
||||
IsFirstCommit bool
|
||||
CommitURL string
|
||||
CommitMessage string
|
||||
CommitSince gotemplate.HTML
|
||||
Code gotemplate.HTML
|
||||
EscapeStatus *charset.EscapeStatus
|
||||
CommitSince template.HTML
|
||||
|
||||
Code template.HTML
|
||||
EscapeStatus *charset.EscapeStatus
|
||||
}
|
||||
|
||||
// RefBlame render blame page
|
||||
func RefBlame(ctx *context.Context) {
|
||||
ctx.Data["PageIsViewCode"] = true
|
||||
ctx.Data["IsBlame"] = true
|
||||
prepareRepoViewContent(ctx, ctx.Repo.RefTypeNameSubURL())
|
||||
|
||||
// Get current entry user currently looking at.
|
||||
if ctx.Repo.TreePath == "" {
|
||||
@@ -56,17 +56,6 @@ func RefBlame(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
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["TreeNames"] = treeNames
|
||||
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
|
||||
@@ -111,7 +100,7 @@ func RefBlame(ctx *context.Context) {
|
||||
}
|
||||
|
||||
type blameResult struct {
|
||||
Parts []*git.BlamePart
|
||||
Parts []*gitrepo.BlamePart
|
||||
UsesIgnoreRevs bool
|
||||
FaultyIgnoreRevsFile bool
|
||||
}
|
||||
@@ -119,7 +108,7 @@ type blameResult struct {
|
||||
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, repo.RepoPath(), commit, file, bypassBlameIgnore)
|
||||
blameReader, err := gitrepo.CreateBlameReader(ctx, objectFormat, repo, commit, file, bypassBlameIgnore)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -135,7 +124,7 @@ func performBlame(ctx *context.Context, repo *repo_model.Repository, commit *git
|
||||
if len(r.Parts) == 0 && r.UsesIgnoreRevs {
|
||||
// try again without ignored revs
|
||||
|
||||
blameReader, err = git.CreateBlameReader(ctx, objectFormat, repo.RepoPath(), commit, file, true)
|
||||
blameReader, err = gitrepo.CreateBlameReader(ctx, objectFormat, repo, commit, file, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -155,12 +144,12 @@ func performBlame(ctx *context.Context, repo *repo_model.Repository, commit *git
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func fillBlameResult(br *git.BlameReader, r *blameResult) error {
|
||||
func fillBlameResult(br *gitrepo.BlameReader, r *blameResult) error {
|
||||
r.UsesIgnoreRevs = br.UsesIgnoreRevs()
|
||||
|
||||
previousHelper := make(map[string]*git.BlamePart)
|
||||
previousHelper := make(map[string]*gitrepo.BlamePart)
|
||||
|
||||
r.Parts = make([]*git.BlamePart, 0, 5)
|
||||
r.Parts = make([]*gitrepo.BlamePart, 0, 5)
|
||||
for {
|
||||
blamePart, err := br.NextPart()
|
||||
if err != nil {
|
||||
@@ -185,7 +174,7 @@ func fillBlameResult(br *git.BlameReader, r *blameResult) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func processBlameParts(ctx *context.Context, blameParts []*git.BlamePart) map[string]*user_model.UserCommit {
|
||||
func processBlameParts(ctx *context.Context, blameParts []*gitrepo.BlamePart) map[string]*user_model.UserCommit {
|
||||
// store commit data by SHA to look up avatar info etc
|
||||
commitNames := make(map[string]*user_model.UserCommit)
|
||||
// and as blameParts can reference the same commits multiple
|
||||
@@ -232,76 +221,64 @@ func processBlameParts(ctx *context.Context, blameParts []*git.BlamePart) map[st
|
||||
return commitNames
|
||||
}
|
||||
|
||||
func renderBlame(ctx *context.Context, blameParts []*git.BlamePart, commitNames map[string]*user_model.UserCommit) {
|
||||
repoLink := ctx.Repo.RepoLink
|
||||
func renderBlameFillFirstBlameRow(repoLink string, avatarUtils *templates.AvatarUtils, part *gitrepo.BlamePart, commit *user_model.UserCommit, br *blameRow) {
|
||||
if commit.User != nil {
|
||||
br.Avatar = avatarUtils.Avatar(commit.User, 18)
|
||||
} else {
|
||||
br.Avatar = avatarUtils.AvatarByEmail(commit.Author.Email, commit.Author.Name, 18)
|
||||
}
|
||||
|
||||
br.PreviousSha = part.PreviousSha
|
||||
br.PreviousShaURL = fmt.Sprintf("%s/blame/commit/%s/%s", repoLink, url.PathEscape(part.PreviousSha), util.PathEscapeSegments(part.PreviousPath))
|
||||
br.CommitURL = fmt.Sprintf("%s/commit/%s", repoLink, url.PathEscape(part.Sha))
|
||||
br.CommitMessage = commit.Message()
|
||||
br.CommitSince = templates.TimeSince(commit.Author.When)
|
||||
}
|
||||
|
||||
func renderBlame(ctx *context.Context, blameParts []*gitrepo.BlamePart, commitNames map[string]*user_model.UserCommit) {
|
||||
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)
|
||||
}
|
||||
|
||||
lines := make([]string, 0)
|
||||
buf := &bytes.Buffer{}
|
||||
rows := make([]*blameRow, 0)
|
||||
avatarUtils := templates.NewAvatarUtils(ctx)
|
||||
rowNumber := 0 // will be 1-based
|
||||
for _, part := range blameParts {
|
||||
for partLineIdx, line := range part.Lines {
|
||||
rowNumber++
|
||||
|
||||
br := &blameRow{RowNumber: rowNumber}
|
||||
rows = append(rows, br)
|
||||
|
||||
if int64(buf.Len()) < setting.UI.MaxDisplayFileSize {
|
||||
buf.WriteString(line)
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
|
||||
if partLineIdx == 0 {
|
||||
renderBlameFillFirstBlameRow(ctx.Repo.RepoLink, avatarUtils, part, commitNames[part.Sha], br)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
escapeStatus := &charset.EscapeStatus{}
|
||||
|
||||
var lexerName string
|
||||
|
||||
avatarUtils := templates.NewAvatarUtils(ctx)
|
||||
i := 0
|
||||
commitCnt := 0
|
||||
for _, part := range blameParts {
|
||||
for index, line := range part.Lines {
|
||||
i++
|
||||
lines = append(lines, line)
|
||||
|
||||
br := &blameRow{
|
||||
RowNumber: i,
|
||||
}
|
||||
|
||||
commit := commitNames[part.Sha]
|
||||
if index == 0 {
|
||||
// Count commit number
|
||||
commitCnt++
|
||||
|
||||
// User avatar image
|
||||
commitSince := templates.TimeSince(commit.Author.When)
|
||||
|
||||
var avatar string
|
||||
if commit.User != nil {
|
||||
avatar = string(avatarUtils.Avatar(commit.User, 18))
|
||||
} else {
|
||||
avatar = string(avatarUtils.AvatarByEmail(commit.Author.Email, commit.Author.Name, 18, "tw-mr-2"))
|
||||
}
|
||||
|
||||
br.Avatar = gotemplate.HTML(avatar)
|
||||
br.RepoLink = repoLink
|
||||
br.PartSha = part.Sha
|
||||
br.PreviousSha = part.PreviousSha
|
||||
br.PreviousShaURL = fmt.Sprintf("%s/blame/commit/%s/%s", repoLink, url.PathEscape(part.PreviousSha), util.PathEscapeSegments(part.PreviousPath))
|
||||
br.CommitURL = fmt.Sprintf("%s/commit/%s", repoLink, url.PathEscape(part.Sha))
|
||||
br.CommitMessage = commit.CommitMessage
|
||||
br.CommitSince = commitSince
|
||||
}
|
||||
|
||||
if i != len(lines)-1 {
|
||||
line += "\n"
|
||||
}
|
||||
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
|
||||
if lexerName == "" {
|
||||
lexerName = lexerNameForLine
|
||||
}
|
||||
|
||||
br.EscapeStatus, br.Code = charset.EscapeControlHTML(line, ctx.Locale)
|
||||
rows = append(rows, br)
|
||||
escapeStatus = escapeStatus.Or(br.EscapeStatus)
|
||||
bufContent := buf.Bytes()
|
||||
bufContent = charset.ToUTF8(bufContent, charset.ConvertOpts{})
|
||||
highlighted, _, lexerDisplayName := highlight.RenderCodeSlowGuess(path.Base(ctx.Repo.TreePath), language, util.UnsafeBytesToString(bufContent))
|
||||
unsafeLines := highlight.UnsafeSplitHighlightedLines(highlighted)
|
||||
for i, br := range rows {
|
||||
var line template.HTML
|
||||
if i < len(unsafeLines) {
|
||||
line = template.HTML(util.UnsafeBytesToString(unsafeLines[i]))
|
||||
}
|
||||
br.EscapeStatus, br.Code = charset.EscapeControlHTML(line, ctx.Locale)
|
||||
escapeStatus = escapeStatus.Or(br.EscapeStatus)
|
||||
}
|
||||
|
||||
ctx.Data["EscapeStatus"] = escapeStatus
|
||||
ctx.Data["BlameRows"] = rows
|
||||
ctx.Data["CommitCnt"] = commitCnt
|
||||
ctx.Data["LexerName"] = lexerName
|
||||
ctx.Data["LexerName"] = lexerDisplayName
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
@@ -40,6 +41,7 @@ func Branches(ctx *context.Context) {
|
||||
ctx.Data["AllowsPulls"] = ctx.Repo.Repository.AllowsPulls(ctx)
|
||||
ctx.Data["IsWriter"] = ctx.Repo.CanWrite(unit.TypeCode)
|
||||
ctx.Data["IsMirror"] = ctx.Repo.Repository.IsMirror
|
||||
// TODO: Can be replaced by ctx.Repo.PullRequestCtx.CanCreateNewPull()
|
||||
ctx.Data["CanPull"] = ctx.Repo.CanWrite(unit.TypeCode) ||
|
||||
(ctx.IsSigned && repo_model.HasForkedRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID))
|
||||
ctx.Data["PageIsViewCode"] = true
|
||||
@@ -82,7 +84,7 @@ func Branches(ctx *context.Context) {
|
||||
ctx.Data["CommitStatus"] = commitStatus
|
||||
ctx.Data["CommitStatuses"] = commitStatuses
|
||||
ctx.Data["DefaultBranchBranch"] = defaultBranch
|
||||
pager := context.NewPagination(int(branchesCount), pageSize, page, 5)
|
||||
pager := context.NewPagination(branchesCount, pageSize, page, 5)
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
ctx.HTML(http.StatusOK, tplBranch)
|
||||
@@ -93,7 +95,7 @@ func DeleteBranchPost(ctx *context.Context) {
|
||||
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 {
|
||||
if err := repo_service.DeleteBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName); err != nil {
|
||||
switch {
|
||||
case git.IsErrBranchNotExist(err):
|
||||
log.Debug("DeleteBranch: Can't delete non existing branch '%s'", branchName)
|
||||
@@ -133,8 +135,7 @@ func RestoreBranchPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := git.Push(ctx, ctx.Repo.Repository.RepoPath(), git.PushOptions{
|
||||
Remote: ctx.Repo.Repository.RepoPath(),
|
||||
if err := gitrepo.Push(ctx, ctx.Repo.Repository, ctx.Repo.Repository, git.PushOptions{
|
||||
Branch: fmt.Sprintf("%s:%s%s", deletedBranch.CommitID, git.BranchPrefix, deletedBranch.Name),
|
||||
Env: repo_module.PushingEnvironment(ctx.Doer, ctx.Repo.Repository),
|
||||
}); err != nil {
|
||||
@@ -194,9 +195,9 @@ func CreateBranch(ctx *context.Context) {
|
||||
}
|
||||
err = release_service.CreateNewTag(ctx, ctx.Doer, ctx.Repo.Repository, target, form.NewBranchName, "")
|
||||
} else if ctx.Repo.RefFullName.IsBranch() {
|
||||
err = repo_service.CreateNewBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.Repo.BranchName, form.NewBranchName)
|
||||
err = repo_service.CreateNewBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.BranchName, form.NewBranchName)
|
||||
} else {
|
||||
err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.Repo.CommitID, form.NewBranchName)
|
||||
err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.CommitID, form.NewBranchName)
|
||||
}
|
||||
if err != nil {
|
||||
if release_service.IsErrProtectedTagName(err) {
|
||||
|
||||
@@ -101,7 +101,7 @@ func Commits(ctx *context.Context) {
|
||||
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
|
||||
ctx.Data["CommitCount"] = commitsCount
|
||||
|
||||
pager := context.NewPagination(int(commitsCount), pageSize, page, 5)
|
||||
pager := context.NewPagination(commitsCount, pageSize, page, 5)
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
ctx.HTML(http.StatusOK, tplCommits)
|
||||
@@ -170,7 +170,7 @@ func Graph(ctx *context.Context) {
|
||||
divOnly := ctx.FormBool("div-only")
|
||||
queryParams := ctx.Req.URL.Query()
|
||||
queryParams.Del("div-only")
|
||||
paginator := context.NewPagination(int(graphCommitsCount), setting.UI.GraphMaxCommitNum, page, 5)
|
||||
paginator := context.NewPagination(graphCommitsCount, setting.UI.GraphMaxCommitNum, page, 5)
|
||||
paginator.AddParamFromQuery(queryParams)
|
||||
ctx.Data["Page"] = paginator
|
||||
if divOnly {
|
||||
@@ -222,7 +222,7 @@ func FileHistory(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
commitsCount, err := ctx.Repo.GitRepo.FileCommitsCount(ctx.Repo.RefFullName.ShortName(), ctx.Repo.TreePath)
|
||||
commitsCount, err := gitrepo.FileCommitsCount(ctx, ctx.Repo.Repository, ctx.Repo.RefFullName.ShortName(), ctx.Repo.TreePath)
|
||||
if err != nil {
|
||||
ctx.ServerError("FileCommitsCount", err)
|
||||
return
|
||||
@@ -254,7 +254,7 @@ func FileHistory(ctx *context.Context) {
|
||||
ctx.Data["FileTreePath"] = ctx.Repo.TreePath
|
||||
ctx.Data["CommitCount"] = commitsCount
|
||||
|
||||
pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5)
|
||||
pager := context.NewPagination(commitsCount, setting.Git.CommitsRangeSize, page, 5)
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
ctx.HTML(http.StatusOK, tplCommits)
|
||||
@@ -276,20 +276,24 @@ func Diff(ctx *context.Context) {
|
||||
userName := ctx.Repo.Owner.Name
|
||||
repoName := ctx.Repo.Repository.Name
|
||||
commitID := ctx.PathParam("sha")
|
||||
var (
|
||||
gitRepo *git.Repository
|
||||
err error
|
||||
)
|
||||
|
||||
diffBlobExcerptData := &gitdiff.DiffBlobExcerptData{
|
||||
BaseLink: ctx.Repo.RepoLink + "/blob_excerpt",
|
||||
DiffStyle: GetDiffViewStyle(ctx),
|
||||
AfterCommitID: commitID,
|
||||
}
|
||||
gitRepo := ctx.Repo.GitRepo
|
||||
var gitRepoStore gitrepo.Repository = ctx.Repo.Repository
|
||||
|
||||
if ctx.Data["PageIsWiki"] != nil {
|
||||
gitRepo, err = gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo())
|
||||
var err error
|
||||
gitRepoStore = ctx.Repo.Repository.WikiStorageRepo()
|
||||
gitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, gitRepoStore)
|
||||
if err != nil {
|
||||
ctx.ServerError("Repo.GitRepo.GetCommit", err)
|
||||
return
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
} else {
|
||||
gitRepo = ctx.Repo.GitRepo
|
||||
diffBlobExcerptData.BaseLink = ctx.Repo.RepoLink + "/wiki/blob_excerpt"
|
||||
}
|
||||
|
||||
commit, err := gitRepo.GetCommit(commitID)
|
||||
@@ -324,7 +328,7 @@ func Diff(ctx *context.Context) {
|
||||
ctx.NotFound(err)
|
||||
return
|
||||
}
|
||||
diffShortStat, err := gitdiff.GetDiffShortStat(gitRepo, "", commitID)
|
||||
diffShortStat, err := gitdiff.GetDiffShortStat(ctx, gitRepoStore, gitRepo, "", commitID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetDiffShortStat", err)
|
||||
return
|
||||
@@ -360,6 +364,7 @@ func Diff(ctx *context.Context) {
|
||||
ctx.Data["Title"] = commit.Summary() + " · " + base.ShortSha(commitID)
|
||||
ctx.Data["Commit"] = commit
|
||||
ctx.Data["Diff"] = diff
|
||||
ctx.Data["DiffBlobExcerptData"] = diffBlobExcerptData
|
||||
|
||||
if !fileOnly {
|
||||
diffTree, err := gitdiff.GetDiffTree(ctx, gitRepo, false, parentCommitID, commitID)
|
||||
@@ -410,6 +415,8 @@ func Diff(ctx *context.Context) {
|
||||
ctx.ServerError("PostProcessCommitMessage", err)
|
||||
return
|
||||
}
|
||||
} else if !git.IsErrNotExist(err) {
|
||||
log.Error("GetNote: %v", err)
|
||||
}
|
||||
|
||||
pr, _ := issues_model.GetPullRequestByMergedCommit(ctx, ctx.Repo.Repository.ID, commitID)
|
||||
|
||||
@@ -33,9 +33,9 @@ func prepareRecentlyPushedNewBranches(ctx *context.Context) {
|
||||
opts.BaseRepo = ctx.Repo.Repository.BaseRepo
|
||||
}
|
||||
|
||||
baseRepoPerm, err := access_model.GetUserRepoPermission(ctx, opts.BaseRepo, ctx.Doer)
|
||||
baseRepoPerm, err := access_model.GetDoerRepoPermission(ctx, opts.BaseRepo, ctx.Doer)
|
||||
if err != nil {
|
||||
log.Error("GetUserRepoPermission: %v", err)
|
||||
log.Error("GetDoerRepoPermission: %v", err)
|
||||
return
|
||||
}
|
||||
if !opts.Repo.CanContentChange() || !opts.BaseRepo.CanContentChange() {
|
||||
|
||||
@@ -4,17 +4,16 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
gocontext "context"
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
@@ -41,8 +40,9 @@ import (
|
||||
"code.gitea.io/gitea/routers/common"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/context/upload"
|
||||
git_service "code.gitea.io/gitea/services/git"
|
||||
"code.gitea.io/gitea/services/gitdiff"
|
||||
pull_service "code.gitea.io/gitea/services/pull"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -148,9 +148,9 @@ func setCsvCompareContext(ctx *context.Context) {
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var closer io.Closer = reader
|
||||
csvReader, err := csv_module.CreateReaderAndDetermineDelimiter(ctx, charset.ToUTF8WithFallbackReader(reader, charset.ConvertOpts{}))
|
||||
return csvReader, reader, err
|
||||
return csvReader, closer, err
|
||||
}
|
||||
|
||||
baseReader, baseBlobCloser, err := csvReaderFromCommit(markup.NewRenderContext(ctx).WithRelativePath(diffFile.OldName), baseBlob)
|
||||
@@ -191,146 +191,93 @@ func setCsvCompareContext(ctx *context.Context) {
|
||||
}
|
||||
|
||||
// ParseCompareInfo parse compare info between two commit for preparing comparing references
|
||||
func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
|
||||
func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo {
|
||||
baseRepo := ctx.Repo.Repository
|
||||
ci := &common.CompareInfo{}
|
||||
|
||||
fileOnly := ctx.FormBool("file-only")
|
||||
|
||||
// Get compared branches information
|
||||
// A full compare url is of the form:
|
||||
//
|
||||
// 1. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headBranch}
|
||||
// 2. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headOwner}:{:headBranch}
|
||||
// 3. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headOwner}/{:headRepoName}:{:headBranch}
|
||||
// 4. /{:baseOwner}/{:baseRepoName}/compare/{:headBranch}
|
||||
// 5. /{:baseOwner}/{:baseRepoName}/compare/{:headOwner}:{:headBranch}
|
||||
// 6. /{:baseOwner}/{:baseRepoName}/compare/{:headOwner}/{:headRepoName}:{:headBranch}
|
||||
//
|
||||
// Here we obtain the infoPath "{:baseBranch}...[{:headOwner}/{:headRepoName}:]{:headBranch}" as ctx.PathParam("*")
|
||||
// with the :baseRepo in ctx.Repo.
|
||||
//
|
||||
// Note: Generally :headRepoName is not provided here - we are only passed :headOwner.
|
||||
//
|
||||
// How do we determine the :headRepo?
|
||||
//
|
||||
// 1. If :headOwner is not set then the :headRepo = :baseRepo
|
||||
// 2. If :headOwner is set - then look for the fork of :baseRepo owned by :headOwner
|
||||
// 3. But... :baseRepo could be a fork of :headOwner's repo - so check that
|
||||
// 4. Now, :baseRepo and :headRepos could be forks of the same repo - so check that
|
||||
//
|
||||
// format: <base branch>...[<head repo>:]<head branch>
|
||||
// base<-head: master...head:feature
|
||||
// same repo: master...feature
|
||||
// 1 Parse compare router param
|
||||
compareReq := common.ParseCompareRouterParam(ctx.PathParam("*"))
|
||||
|
||||
var (
|
||||
isSameRepo bool
|
||||
infoPath string
|
||||
err error
|
||||
)
|
||||
|
||||
infoPath = ctx.PathParam("*")
|
||||
var infos []string
|
||||
if infoPath == "" {
|
||||
infos = []string{baseRepo.DefaultBranch, baseRepo.DefaultBranch}
|
||||
} else {
|
||||
infos = strings.SplitN(infoPath, "...", 2)
|
||||
if len(infos) != 2 {
|
||||
if infos = strings.SplitN(infoPath, "..", 2); len(infos) == 2 {
|
||||
ci.DirectComparison = true
|
||||
ctx.Data["PageIsComparePull"] = false
|
||||
} else {
|
||||
infos = []string{baseRepo.DefaultBranch, infoPath}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data["BaseName"] = baseRepo.OwnerName
|
||||
ci.BaseBranch = infos[0]
|
||||
ctx.Data["BaseBranch"] = ci.BaseBranch
|
||||
|
||||
// If there is no head repository, it means compare between same repository.
|
||||
headInfos := strings.Split(infos[1], ":")
|
||||
if len(headInfos) == 1 {
|
||||
isSameRepo = true
|
||||
ci.HeadUser = ctx.Repo.Owner
|
||||
ci.HeadBranch = headInfos[0]
|
||||
} else if len(headInfos) == 2 {
|
||||
headInfosSplit := strings.Split(headInfos[0], "/")
|
||||
if len(headInfosSplit) == 1 {
|
||||
ci.HeadUser, err = user_model.GetUserOrOrgByName(ctx, headInfos[0])
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.NotFound(nil)
|
||||
} else {
|
||||
ctx.ServerError("GetUserByName", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
ci.HeadBranch = headInfos[1]
|
||||
isSameRepo = ci.HeadUser.ID == ctx.Repo.Owner.ID
|
||||
if isSameRepo {
|
||||
ci.HeadRepo = baseRepo
|
||||
}
|
||||
} else {
|
||||
ci.HeadRepo, err = repo_model.GetRepositoryByOwnerAndName(ctx, headInfosSplit[0], headInfosSplit[1])
|
||||
if err != nil {
|
||||
if repo_model.IsErrRepoNotExist(err) {
|
||||
ctx.NotFound(nil)
|
||||
} else {
|
||||
ctx.ServerError("GetRepositoryByOwnerAndName", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := ci.HeadRepo.LoadOwner(ctx); err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
ctx.NotFound(nil)
|
||||
} else {
|
||||
ctx.ServerError("GetUserByName", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
ci.HeadBranch = headInfos[1]
|
||||
ci.HeadUser = ci.HeadRepo.Owner
|
||||
isSameRepo = ci.HeadRepo.ID == ctx.Repo.Repository.ID
|
||||
}
|
||||
} else {
|
||||
ctx.NotFound(nil)
|
||||
// remove the check when we support compare with carets
|
||||
if compareReq.BaseOriRefSuffix != "" {
|
||||
ctx.HTTPError(http.StatusBadRequest, "Unsupported comparison syntax: ref with suffix")
|
||||
return nil
|
||||
}
|
||||
ctx.Data["HeadUser"] = ci.HeadUser
|
||||
ctx.Data["HeadBranch"] = ci.HeadBranch
|
||||
ctx.Repo.PullRequest.SameRepo = isSameRepo
|
||||
|
||||
// Check if base branch is valid.
|
||||
baseIsCommit := ctx.Repo.GitRepo.IsCommitExist(ci.BaseBranch)
|
||||
baseIsBranch := gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, ci.BaseBranch)
|
||||
baseIsTag := gitrepo.IsTagExist(ctx, ctx.Repo.Repository, ci.BaseBranch)
|
||||
// 2 get repository and owner for head
|
||||
headOwner, headRepo, err := common.GetHeadOwnerAndRepo(ctx, baseRepo, compareReq)
|
||||
switch {
|
||||
case errors.Is(err, util.ErrInvalidArgument):
|
||||
ctx.HTTPError(http.StatusBadRequest, err.Error())
|
||||
return nil
|
||||
case errors.Is(err, util.ErrNotExist):
|
||||
ctx.NotFound(nil)
|
||||
return nil
|
||||
case err != nil:
|
||||
ctx.ServerError("GetHeadOwnerAndRepo", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if !baseIsCommit && !baseIsBranch && !baseIsTag {
|
||||
// Check if baseBranch is short sha commit hash
|
||||
if baseCommit, _ := ctx.Repo.GitRepo.GetCommit(ci.BaseBranch); baseCommit != nil {
|
||||
ci.BaseBranch = baseCommit.ID.String()
|
||||
ctx.Data["BaseBranch"] = ci.BaseBranch
|
||||
baseIsCommit = true
|
||||
} else if ci.BaseBranch == ctx.Repo.GetObjectFormat().EmptyObjectID().String() {
|
||||
if isSameRepo {
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ci.HeadBranch))
|
||||
} else {
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ci.HeadRepo.FullName()) + ":" + util.PathEscapeSegments(ci.HeadBranch))
|
||||
}
|
||||
isSameRepo := baseRepo.ID == headRepo.ID
|
||||
|
||||
// 3 permission check
|
||||
// base repository's code unit read permission check has been done on web.go
|
||||
permBase := ctx.Repo.Permission
|
||||
|
||||
// If we're not merging from the same repo:
|
||||
if !isSameRepo {
|
||||
// Assert ctx.Doer has permission to read headRepo's codes
|
||||
permHead, err := access_model.GetDoerRepoPermission(ctx, headRepo, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetDoerRepoPermission", err)
|
||||
return nil
|
||||
} else {
|
||||
}
|
||||
if !permHead.CanRead(unit.TypeCode) {
|
||||
if log.IsTrace() {
|
||||
log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v",
|
||||
ctx.Doer,
|
||||
headRepo,
|
||||
permHead)
|
||||
}
|
||||
ctx.NotFound(nil)
|
||||
return nil
|
||||
}
|
||||
ctx.Data["CanWriteToHeadRepo"] = permHead.CanWrite(unit.TypeCode)
|
||||
}
|
||||
ctx.Data["BaseIsCommit"] = baseIsCommit
|
||||
ctx.Data["BaseIsBranch"] = baseIsBranch
|
||||
ctx.Data["BaseIsTag"] = baseIsTag
|
||||
|
||||
// 4 get base and head refs
|
||||
baseRefName := util.IfZero(compareReq.BaseOriRef, baseRepo.GetPullRequestTargetBranch(ctx))
|
||||
headRefName := util.IfZero(compareReq.HeadOriRef, headRepo.DefaultBranch)
|
||||
|
||||
baseRef := ctx.Repo.GitRepo.UnstableGuessRefByShortName(baseRefName)
|
||||
if baseRef == "" {
|
||||
ctx.NotFound(nil)
|
||||
return nil
|
||||
}
|
||||
var headGitRepo *git.Repository
|
||||
if isSameRepo {
|
||||
headGitRepo = ctx.Repo.GitRepo
|
||||
} else {
|
||||
headGitRepo, err = gitrepo.OpenRepository(ctx, headRepo)
|
||||
if err != nil {
|
||||
ctx.ServerError("OpenRepository", err)
|
||||
return nil
|
||||
}
|
||||
defer headGitRepo.Close()
|
||||
}
|
||||
headRef := headGitRepo.UnstableGuessRefByShortName(headRefName)
|
||||
if headRef == "" {
|
||||
ctx.NotFound(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx.Data["BaseName"] = baseRepo.OwnerName
|
||||
ctx.Data["BaseBranch"] = baseRef.ShortName() // for legacy templates
|
||||
ctx.Data["HeadUser"] = headOwner
|
||||
ctx.Data["HeadBranch"] = headRef.ShortName() // for legacy templates
|
||||
ctx.Data["IsPull"] = true
|
||||
|
||||
// Now we have the repository that represents the base
|
||||
context.InitRepoPullRequestCtx(ctx, baseRepo, headRepo)
|
||||
|
||||
// The current base and head repositories and branches may not
|
||||
// actually be the intended branches that the user wants to
|
||||
@@ -367,31 +314,31 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
|
||||
}
|
||||
}
|
||||
|
||||
has := ci.HeadRepo != nil
|
||||
has := headRepo != nil
|
||||
// 3. If the base is a forked from "RootRepo" and the owner of
|
||||
// the "RootRepo" is the :headUser - set headRepo to that
|
||||
if !has && rootRepo != nil && rootRepo.OwnerID == ci.HeadUser.ID {
|
||||
ci.HeadRepo = rootRepo
|
||||
if !has && rootRepo != nil && rootRepo.OwnerID == headOwner.ID {
|
||||
headRepo = rootRepo
|
||||
has = true
|
||||
}
|
||||
|
||||
// 4. If the ctx.Doer has their own fork of the baseRepo and the headUser is the ctx.Doer
|
||||
// set the headRepo to the ownFork
|
||||
if !has && ownForkRepo != nil && ownForkRepo.OwnerID == ci.HeadUser.ID {
|
||||
ci.HeadRepo = ownForkRepo
|
||||
if !has && ownForkRepo != nil && ownForkRepo.OwnerID == headOwner.ID {
|
||||
headRepo = ownForkRepo
|
||||
has = true
|
||||
}
|
||||
|
||||
// 5. If the headOwner has a fork of the baseRepo - use that
|
||||
if !has {
|
||||
ci.HeadRepo = repo_model.GetForkedRepo(ctx, ci.HeadUser.ID, baseRepo.ID)
|
||||
has = ci.HeadRepo != nil
|
||||
headRepo = repo_model.GetForkedRepo(ctx, headOwner.ID, baseRepo.ID)
|
||||
has = headRepo != nil
|
||||
}
|
||||
|
||||
// 6. If the baseRepo is a fork and the headUser has a fork of that use that
|
||||
if !has && baseRepo.IsFork {
|
||||
ci.HeadRepo = repo_model.GetForkedRepo(ctx, ci.HeadUser.ID, baseRepo.ForkID)
|
||||
has = ci.HeadRepo != nil
|
||||
headRepo = repo_model.GetForkedRepo(ctx, headOwner.ID, baseRepo.ForkID)
|
||||
has = headRepo != nil
|
||||
}
|
||||
|
||||
// 7. Otherwise if we're not the same repo and haven't found a repo give up
|
||||
@@ -399,70 +346,15 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
|
||||
ctx.Data["PageIsComparePull"] = false
|
||||
}
|
||||
|
||||
// 8. Finally open the git repo
|
||||
if isSameRepo {
|
||||
ci.HeadRepo = ctx.Repo.Repository
|
||||
ci.HeadGitRepo = ctx.Repo.GitRepo
|
||||
} else if has {
|
||||
ci.HeadGitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ci.HeadRepo)
|
||||
if err != nil {
|
||||
ctx.ServerError("RepositoryFromRequestContextOrOpen", err)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
ctx.NotFound(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx.Data["HeadRepo"] = ci.HeadRepo
|
||||
ctx.Data["HeadRepo"] = headRepo
|
||||
ctx.Data["BaseCompareRepo"] = ctx.Repo.Repository
|
||||
|
||||
// Now we need to assert that the ctx.Doer has permission to read
|
||||
// the baseRepo's code and pulls
|
||||
// (NOT headRepo's)
|
||||
permBase, err := access_model.GetUserRepoPermission(ctx, baseRepo, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserRepoPermission", err)
|
||||
return nil
|
||||
}
|
||||
if !permBase.CanRead(unit.TypeCode) {
|
||||
if log.IsTrace() {
|
||||
log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in baseRepo has Permissions: %-+v",
|
||||
ctx.Doer,
|
||||
baseRepo,
|
||||
permBase)
|
||||
}
|
||||
ctx.NotFound(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
// If we're not merging from the same repo:
|
||||
if !isSameRepo {
|
||||
// Assert ctx.Doer has permission to read headRepo's codes
|
||||
permHead, err := access_model.GetUserRepoPermission(ctx, ci.HeadRepo, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserRepoPermission", err)
|
||||
return nil
|
||||
}
|
||||
if !permHead.CanRead(unit.TypeCode) {
|
||||
if log.IsTrace() {
|
||||
log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v",
|
||||
ctx.Doer,
|
||||
ci.HeadRepo,
|
||||
permHead)
|
||||
}
|
||||
ctx.NotFound(nil)
|
||||
return nil
|
||||
}
|
||||
ctx.Data["CanWriteToHeadRepo"] = permHead.CanWrite(unit.TypeCode)
|
||||
}
|
||||
|
||||
// If we have a rootRepo and it's different from:
|
||||
// 1. the computed base
|
||||
// 2. the computed head
|
||||
// then get the branches of it
|
||||
if rootRepo != nil &&
|
||||
rootRepo.ID != ci.HeadRepo.ID &&
|
||||
rootRepo.ID != headRepo.ID &&
|
||||
rootRepo.ID != baseRepo.ID {
|
||||
canRead := access_model.CheckRepoUnitUser(ctx, rootRepo, ctx.Doer, unit.TypeCode)
|
||||
if canRead {
|
||||
@@ -486,7 +378,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
|
||||
// 3. The rootRepo (if we have one)
|
||||
// then get the branches from it.
|
||||
if ownForkRepo != nil &&
|
||||
ownForkRepo.ID != ci.HeadRepo.ID &&
|
||||
ownForkRepo.ID != headRepo.ID &&
|
||||
ownForkRepo.ID != baseRepo.ID &&
|
||||
(rootRepo == nil || ownForkRepo.ID != rootRepo.ID) {
|
||||
canRead := access_model.CheckRepoUnitUser(ctx, ownForkRepo, ctx.Doer, unit.TypeCode)
|
||||
@@ -504,28 +396,9 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if head branch is valid.
|
||||
headIsCommit := ci.HeadGitRepo.IsCommitExist(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 {
|
||||
ci.HeadBranch = headCommit.ID.String()
|
||||
ctx.Data["HeadBranch"] = ci.HeadBranch
|
||||
headIsCommit = true
|
||||
} else {
|
||||
ctx.NotFound(nil)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
ctx.Data["HeadIsCommit"] = headIsCommit
|
||||
ctx.Data["HeadIsBranch"] = headIsBranch
|
||||
ctx.Data["HeadIsTag"] = headIsTag
|
||||
|
||||
// Treat as pull request if both references are branches
|
||||
if ctx.Data["PageIsComparePull"] == nil {
|
||||
ctx.Data["PageIsComparePull"] = headIsBranch && baseIsBranch && permBase.CanReadIssuesOrPulls(true)
|
||||
ctx.Data["PageIsComparePull"] = baseRef.IsBranch() && headRef.IsBranch() && permBase.CanReadIssuesOrPulls(true)
|
||||
}
|
||||
|
||||
if ctx.Data["PageIsComparePull"] == true && !permBase.CanReadIssuesOrPulls(true) {
|
||||
@@ -539,41 +412,94 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
|
||||
return nil
|
||||
}
|
||||
|
||||
baseBranchRef := ci.BaseBranch
|
||||
if baseIsBranch {
|
||||
baseBranchRef = git.BranchPrefix + ci.BaseBranch
|
||||
} else if baseIsTag {
|
||||
baseBranchRef = git.TagPrefix + ci.BaseBranch
|
||||
}
|
||||
headBranchRef := ci.HeadBranch
|
||||
if headIsBranch {
|
||||
headBranchRef = git.BranchPrefix + ci.HeadBranch
|
||||
} else if headIsTag {
|
||||
headBranchRef = git.TagPrefix + ci.HeadBranch
|
||||
}
|
||||
|
||||
ci.CompareInfo, err = pull_service.GetCompareInfo(ctx, baseRepo, ci.HeadRepo, ci.HeadGitRepo, baseBranchRef, headBranchRef, ci.DirectComparison, fileOnly)
|
||||
compareInfo, err := git_service.GetCompareInfo(ctx, baseRepo, headRepo, headGitRepo, baseRef, headRef, compareReq.DirectComparison(), fileOnly)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.Data["IsNoMergeBase"] = true
|
||||
return compareInfo
|
||||
}
|
||||
ctx.ServerError("GetCompareInfo", err)
|
||||
return nil
|
||||
}
|
||||
if ci.DirectComparison {
|
||||
ctx.Data["BeforeCommitID"] = ci.CompareInfo.BaseCommitID
|
||||
if compareReq.DirectComparison() {
|
||||
ctx.Data["BeforeCommitID"] = compareInfo.BaseCommitID
|
||||
} else {
|
||||
ctx.Data["BeforeCommitID"] = ci.CompareInfo.MergeBase
|
||||
ctx.Data["BeforeCommitID"] = compareInfo.MergeBase
|
||||
}
|
||||
|
||||
return ci
|
||||
return compareInfo
|
||||
}
|
||||
|
||||
// autoTitleFromBranchName humanizes a branch name into a PR title.
|
||||
func autoTitleFromBranchName(name string) string {
|
||||
var buf strings.Builder
|
||||
var prevIsSpace bool
|
||||
runes := []rune(name)
|
||||
for i, r := range runes {
|
||||
isSpace := unicode.IsSpace(r)
|
||||
if r == '-' || r == '_' || isSpace {
|
||||
if !prevIsSpace {
|
||||
buf.WriteRune(' ')
|
||||
}
|
||||
prevIsSpace = true
|
||||
continue
|
||||
}
|
||||
if !prevIsSpace && unicode.IsUpper(r) {
|
||||
needSpace := i > 0 && unicode.IsLower(runes[i-1]) || i < len(runes)-1 && unicode.IsLower(runes[i+1])
|
||||
if needSpace {
|
||||
buf.WriteRune(' ')
|
||||
}
|
||||
}
|
||||
buf.WriteRune(unicode.ToLower(r))
|
||||
prevIsSpace = isSpace
|
||||
}
|
||||
out := strings.TrimSpace(buf.String())
|
||||
if out == "" {
|
||||
return out
|
||||
}
|
||||
outRunes := []rune(out)
|
||||
outRunes[0] = unicode.ToUpper(outRunes[0])
|
||||
return string(outRunes)
|
||||
}
|
||||
|
||||
func prepareNewPullRequestTitleContent(ci *git_service.CompareInfo, commits []*git_model.SignCommitWithStatuses, defaultTitleSource string) (title, content string) {
|
||||
useFirstCommitAsTitle := len(commits) == 1 || (defaultTitleSource == setting.RepoPRTitleSourceFirstCommit && len(commits) > 0)
|
||||
if useFirstCommitAsTitle {
|
||||
// the "commits" are from "ShowPrettyFormatLogToList", which is ordered from newest to oldest, here take the oldest one
|
||||
c := commits[len(commits)-1]
|
||||
title = strings.TrimSpace(c.UserCommit.Summary())
|
||||
} else {
|
||||
title = autoTitleFromBranchName(ci.HeadRef.ShortName())
|
||||
}
|
||||
|
||||
if len(commits) == 1 {
|
||||
c := commits[0]
|
||||
_, content, _ = strings.Cut(strings.TrimSpace(c.UserCommit.CommitMessage), "\n")
|
||||
content = strings.TrimSpace(content)
|
||||
content = string(charset.ToUTF8([]byte(content), charset.ConvertOpts{}))
|
||||
}
|
||||
|
||||
var titleTrailer string
|
||||
// TODO: 255 doesn't seem to be a good limit for title, just keep the old behavior
|
||||
title, titleTrailer = util.EllipsisDisplayStringX(title, 255)
|
||||
if titleTrailer != "" {
|
||||
if content != "" {
|
||||
content = titleTrailer + "\n\n" + content
|
||||
} else {
|
||||
content = titleTrailer + "\n"
|
||||
}
|
||||
}
|
||||
return title, content
|
||||
}
|
||||
|
||||
// PrepareCompareDiff renders compare diff page
|
||||
func PrepareCompareDiff(
|
||||
ctx *context.Context,
|
||||
ci *common.CompareInfo,
|
||||
ci *git_service.CompareInfo,
|
||||
whitespaceBehavior gitcmd.TrustedCmdArgs,
|
||||
) (nothingToCompare bool) {
|
||||
repo := ctx.Repo.Repository
|
||||
headCommitID := ci.CompareInfo.HeadCommitID
|
||||
headCommitID := ci.HeadCommitID
|
||||
|
||||
ctx.Data["CommitRepoLink"] = ci.HeadRepo.Link()
|
||||
ctx.Data["AfterCommitID"] = headCommitID
|
||||
@@ -585,17 +511,15 @@ func PrepareCompareDiff(
|
||||
ctx.Data["TitleQuery"] = newPrFormTitle
|
||||
ctx.Data["BodyQuery"] = newPrFormBody
|
||||
|
||||
if (headCommitID == ci.CompareInfo.MergeBase && !ci.DirectComparison) ||
|
||||
headCommitID == ci.CompareInfo.BaseCommitID {
|
||||
if (headCommitID == ci.MergeBase && !ci.DirectComparison()) ||
|
||||
headCommitID == ci.BaseCommitID {
|
||||
ctx.Data["IsNothingToCompare"] = true
|
||||
if unit, err := repo.GetUnit(ctx, unit.TypePullRequests); err == nil {
|
||||
config := unit.PullRequestsConfig()
|
||||
|
||||
if !config.AutodetectManualMerge {
|
||||
allowEmptyPr := !(ci.BaseBranch == ci.HeadBranch && ctx.Repo.Repository.Name == ci.HeadRepo.Name)
|
||||
ctx.Data["AllowEmptyPr"] = allowEmptyPr
|
||||
|
||||
return !allowEmptyPr
|
||||
ctx.Data["AllowEmptyPr"] = !ci.IsSameRef()
|
||||
return ci.IsSameRef()
|
||||
}
|
||||
|
||||
ctx.Data["AllowEmptyPr"] = false
|
||||
@@ -603,9 +527,9 @@ func PrepareCompareDiff(
|
||||
return true
|
||||
}
|
||||
|
||||
beforeCommitID := ci.CompareInfo.MergeBase
|
||||
if ci.DirectComparison {
|
||||
beforeCommitID = ci.CompareInfo.BaseCommitID
|
||||
beforeCommitID := ci.MergeBase
|
||||
if ci.DirectComparison() {
|
||||
beforeCommitID = ci.BaseCommitID
|
||||
}
|
||||
|
||||
maxLines, maxFiles := setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffFiles
|
||||
@@ -625,19 +549,24 @@ func PrepareCompareDiff(
|
||||
MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters,
|
||||
MaxFiles: maxFiles,
|
||||
WhitespaceBehavior: whitespaceBehavior,
|
||||
DirectComparison: ci.DirectComparison,
|
||||
DirectComparison: ci.DirectComparison(),
|
||||
}, ctx.FormStrings("files")...)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetDiff", err)
|
||||
return false
|
||||
}
|
||||
diffShortStat, err := gitdiff.GetDiffShortStat(ci.HeadGitRepo, beforeCommitID, headCommitID)
|
||||
diffShortStat, err := gitdiff.GetDiffShortStat(ctx, ci.HeadRepo, ci.HeadGitRepo, beforeCommitID, headCommitID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetDiffShortStat", err)
|
||||
return false
|
||||
}
|
||||
ctx.Data["DiffShortStat"] = diffShortStat
|
||||
ctx.Data["Diff"] = diff
|
||||
ctx.Data["DiffBlobExcerptData"] = &gitdiff.DiffBlobExcerptData{
|
||||
BaseLink: ci.HeadRepo.Link() + "/blob_excerpt",
|
||||
DiffStyle: GetDiffViewStyle(ctx),
|
||||
AfterCommitID: headCommitID,
|
||||
}
|
||||
ctx.Data["DiffNotAvailable"] = diffShortStat.NumFiles == 0
|
||||
|
||||
if !fileOnly {
|
||||
@@ -668,7 +597,7 @@ func PrepareCompareDiff(
|
||||
return false
|
||||
}
|
||||
|
||||
commits, err := processGitCommits(ctx, ci.CompareInfo.Commits)
|
||||
commits, err := processGitCommits(ctx, ci.Commits)
|
||||
if err != nil {
|
||||
ctx.ServerError("processGitCommits", err)
|
||||
return false
|
||||
@@ -676,34 +605,11 @@ func PrepareCompareDiff(
|
||||
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())
|
||||
|
||||
body := strings.Split(strings.TrimSpace(c.UserCommit.Message()), "\n")
|
||||
if len(body) > 1 {
|
||||
ctx.Data["content"] = strings.Join(body[1:], "\n")
|
||||
}
|
||||
}
|
||||
|
||||
if len(title) > 255 {
|
||||
var trailer string
|
||||
title, trailer = util.EllipsisDisplayStringX(title, 255)
|
||||
if len(trailer) > 0 {
|
||||
if ctx.Data["content"] != nil {
|
||||
ctx.Data["content"] = fmt.Sprintf("%s\n\n%s", trailer, ctx.Data["content"])
|
||||
} else {
|
||||
ctx.Data["content"] = trailer + "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data["title"] = title
|
||||
ctx.Data["Username"] = ci.HeadUser.Name
|
||||
ctx.Data["title"], ctx.Data["content"] = prepareNewPullRequestTitleContent(ci, commits, setting.Repository.PullRequest.DefaultTitleSource)
|
||||
ctx.Data["Username"] = ci.HeadRepo.OwnerName
|
||||
ctx.Data["Reponame"] = ci.HeadRepo.Name
|
||||
|
||||
setCompareContext(ctx, beforeCommit, headCommit, ci.HeadUser.Name, repo.Name)
|
||||
setCompareContext(ctx, beforeCommit, headCommit, ci.HeadRepo.OwnerName, repo.Name)
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -733,17 +639,20 @@ func CompareDiff(ctx *context.Context) {
|
||||
|
||||
ctx.Data["PageIsViewCode"] = true
|
||||
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
|
||||
ctx.Data["DirectComparison"] = ci.DirectComparison
|
||||
ctx.Data["OtherCompareSeparator"] = ".."
|
||||
ctx.Data["CompareSeparator"] = "..."
|
||||
if ci.DirectComparison {
|
||||
ctx.Data["CompareSeparator"] = ".."
|
||||
ctx.Data["OtherCompareSeparator"] = "..."
|
||||
}
|
||||
ctx.Data["CompareInfo"] = ci
|
||||
|
||||
nothingToCompare := PrepareCompareDiff(ctx, ci, gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)))
|
||||
if ctx.Written() {
|
||||
return
|
||||
var nothingToCompare bool
|
||||
noMergeBase := ctx.Data["IsNoMergeBase"] == true
|
||||
if noMergeBase {
|
||||
ctx.Flash.Error(ctx.Tr("repo.pulls.no_common_history"), true)
|
||||
ctx.Data["PageIsComparePull"] = false
|
||||
ctx.Data["CommitCount"] = 0
|
||||
nothingToCompare = true
|
||||
} else {
|
||||
nothingToCompare = PrepareCompareDiff(ctx, ci, gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)))
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
baseTags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
|
||||
@@ -759,16 +668,13 @@ func CompareDiff(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
headBranches, err := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{
|
||||
RepoID: ci.HeadRepo.ID,
|
||||
ListOptions: db.ListOptionsAll,
|
||||
IsDeletedBranch: optional.Some(false),
|
||||
})
|
||||
headBranches, headTags, err := getBranchesAndTagsForRepo(ctx, ci.HeadRepo)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetBranches", err)
|
||||
ctx.ServerError("GetBranchesAndTagsForRepo", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["HeadBranches"] = headBranches
|
||||
ctx.Data["HeadTags"] = headTags
|
||||
|
||||
// For compare repo branches
|
||||
PrepareBranchList(ctx)
|
||||
@@ -776,15 +682,13 @@ func CompareDiff(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
headTags, err := repo_model.GetTagNamesByRepoID(ctx, ci.HeadRepo.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetTagNamesByRepoID", err)
|
||||
if noMergeBase {
|
||||
ctx.HTML(http.StatusOK, tplCompare)
|
||||
return
|
||||
}
|
||||
ctx.Data["HeadTags"] = headTags
|
||||
|
||||
if ctx.Data["PageIsComparePull"] == true {
|
||||
pr, err := issues_model.GetUnmergedPullRequest(ctx, ci.HeadRepo.ID, ctx.Repo.Repository.ID, ci.HeadBranch, ci.BaseBranch, issues_model.PullRequestFlowGithub)
|
||||
pr, err := issues_model.GetUnmergedPullRequest(ctx, ci.HeadRepo.ID, ctx.Repo.Repository.ID, ci.HeadRef.ShortName(), ci.BaseRef.ShortName(), issues_model.PullRequestFlowGithub)
|
||||
if err != nil {
|
||||
if !issues_model.IsErrPullRequestNotExist(err) {
|
||||
ctx.ServerError("GetUnmergedPullRequest", err)
|
||||
@@ -816,11 +720,7 @@ func CompareDiff(ctx *context.Context) {
|
||||
beforeCommitID := ctx.Data["BeforeCommitID"].(string)
|
||||
afterCommitID := ctx.Data["AfterCommitID"].(string)
|
||||
|
||||
separator := "..."
|
||||
if ci.DirectComparison {
|
||||
separator = ".."
|
||||
}
|
||||
ctx.Data["Title"] = "Comparing " + base.ShortSha(beforeCommitID) + separator + base.ShortSha(afterCommitID)
|
||||
ctx.Data["Title"] = "Comparing " + base.ShortSha(beforeCommitID) + ci.CompareSeparator + base.ShortSha(afterCommitID)
|
||||
|
||||
ctx.Data["IsDiffCompare"] = true
|
||||
|
||||
@@ -865,125 +765,118 @@ func CompareDiff(ctx *context.Context) {
|
||||
ctx.HTML(http.StatusOK, tplCompare)
|
||||
}
|
||||
|
||||
// attachCommentsToLines attaches comments to their corresponding diff lines
|
||||
func attachCommentsToLines(section *gitdiff.DiffSection, lineComments map[int64][]*issues_model.Comment) {
|
||||
for _, line := range section.Lines {
|
||||
if comments, ok := lineComments[int64(line.LeftIdx*-1)]; ok {
|
||||
line.Comments = append(line.Comments, comments...)
|
||||
}
|
||||
if comments, ok := lineComments[int64(line.RightIdx)]; ok {
|
||||
line.Comments = append(line.Comments, comments...)
|
||||
}
|
||||
sort.SliceStable(line.Comments, func(i, j int) bool {
|
||||
return line.Comments[i].CreatedUnix < line.Comments[j].CreatedUnix
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// attachHiddenCommentIDs calculates and attaches hidden comment IDs to expand buttons
|
||||
func attachHiddenCommentIDs(section *gitdiff.DiffSection, lineComments map[int64][]*issues_model.Comment) {
|
||||
for _, line := range section.Lines {
|
||||
gitdiff.FillHiddenCommentIDsForDiffLine(line, lineComments)
|
||||
}
|
||||
}
|
||||
|
||||
// ExcerptBlob render blob excerpt contents
|
||||
func ExcerptBlob(ctx *context.Context) {
|
||||
commitID := ctx.PathParam("sha")
|
||||
lastLeft := ctx.FormInt("last_left")
|
||||
lastRight := ctx.FormInt("last_right")
|
||||
idxLeft := ctx.FormInt("left")
|
||||
idxRight := ctx.FormInt("right")
|
||||
leftHunkSize := ctx.FormInt("left_hunk_size")
|
||||
rightHunkSize := ctx.FormInt("right_hunk_size")
|
||||
anchor := ctx.FormString("anchor")
|
||||
direction := ctx.FormString("direction")
|
||||
opts := gitdiff.BlobExcerptOptions{
|
||||
LastLeft: ctx.FormInt("last_left"),
|
||||
LastRight: ctx.FormInt("last_right"),
|
||||
LeftIndex: ctx.FormInt("left"),
|
||||
RightIndex: ctx.FormInt("right"),
|
||||
LeftHunkSize: ctx.FormInt("left_hunk_size"),
|
||||
RightHunkSize: ctx.FormInt("right_hunk_size"),
|
||||
Direction: ctx.FormString("direction"),
|
||||
Language: ctx.FormString("filelang"),
|
||||
}
|
||||
filePath := ctx.FormString("path")
|
||||
gitRepo := ctx.Repo.GitRepo
|
||||
|
||||
diffBlobExcerptData := &gitdiff.DiffBlobExcerptData{
|
||||
BaseLink: ctx.Repo.RepoLink + "/blob_excerpt",
|
||||
DiffStyle: GetDiffViewStyle(ctx),
|
||||
AfterCommitID: commitID,
|
||||
}
|
||||
|
||||
if ctx.Data["PageIsWiki"] == true {
|
||||
var err error
|
||||
gitRepo, err = gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo())
|
||||
gitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository.WikiStorageRepo())
|
||||
if err != nil {
|
||||
ctx.ServerError("OpenRepository", err)
|
||||
return
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
diffBlobExcerptData.BaseLink = ctx.Repo.RepoLink + "/wiki/blob_excerpt"
|
||||
}
|
||||
chunkSize := gitdiff.BlobExcerptChunkSize
|
||||
|
||||
commit, err := gitRepo.GetCommit(commitID)
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, "GetCommit")
|
||||
ctx.ServerError("GetCommit", err)
|
||||
return
|
||||
}
|
||||
section := &gitdiff.DiffSection{
|
||||
FileName: filePath,
|
||||
}
|
||||
if direction == "up" && (idxLeft-lastLeft) > chunkSize {
|
||||
idxLeft -= chunkSize
|
||||
idxRight -= chunkSize
|
||||
leftHunkSize += chunkSize
|
||||
rightHunkSize += chunkSize
|
||||
section.Lines, err = getExcerptLines(commit, filePath, idxLeft-1, idxRight-1, chunkSize)
|
||||
} else if direction == "down" && (idxLeft-lastLeft) > chunkSize {
|
||||
section.Lines, err = getExcerptLines(commit, filePath, lastLeft, lastRight, chunkSize)
|
||||
lastLeft += chunkSize
|
||||
lastRight += chunkSize
|
||||
} else {
|
||||
offset := -1
|
||||
if direction == "down" {
|
||||
offset = 0
|
||||
}
|
||||
section.Lines, err = getExcerptLines(commit, filePath, lastLeft, lastRight, idxRight-lastRight+offset)
|
||||
leftHunkSize = 0
|
||||
rightHunkSize = 0
|
||||
idxLeft = lastLeft
|
||||
idxRight = lastRight
|
||||
}
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, "getExcerptLines")
|
||||
return
|
||||
}
|
||||
if idxRight > lastRight {
|
||||
lineText := " "
|
||||
if rightHunkSize > 0 || leftHunkSize > 0 {
|
||||
lineText = fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", idxLeft, leftHunkSize, idxRight, rightHunkSize)
|
||||
}
|
||||
lineText = html.EscapeString(lineText)
|
||||
lineSection := &gitdiff.DiffLine{
|
||||
Type: gitdiff.DiffLineSection,
|
||||
Content: lineText,
|
||||
SectionInfo: &gitdiff.DiffLineSectionInfo{
|
||||
Path: filePath,
|
||||
LastLeftIdx: lastLeft,
|
||||
LastRightIdx: lastRight,
|
||||
LeftIdx: idxLeft,
|
||||
RightIdx: idxRight,
|
||||
LeftHunkSize: leftHunkSize,
|
||||
RightHunkSize: rightHunkSize,
|
||||
},
|
||||
}
|
||||
switch direction {
|
||||
case "up":
|
||||
section.Lines = append([]*gitdiff.DiffLine{lineSection}, section.Lines...)
|
||||
case "down":
|
||||
section.Lines = append(section.Lines, lineSection)
|
||||
}
|
||||
}
|
||||
ctx.Data["section"] = section
|
||||
ctx.Data["FileNameHash"] = git.HashFilePathForWebUI(filePath)
|
||||
ctx.Data["AfterCommitID"] = commitID
|
||||
ctx.Data["Anchor"] = anchor
|
||||
ctx.HTML(http.StatusOK, tplBlobExcerpt)
|
||||
}
|
||||
|
||||
func getExcerptLines(commit *git.Commit, filePath string, idxLeft, idxRight, chunkSize int) ([]*gitdiff.DiffLine, error) {
|
||||
blob, err := commit.Tree.GetBlobByPath(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
ctx.ServerError("GetBlobByPath", err)
|
||||
return
|
||||
}
|
||||
reader, err := blob.DataAsync()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
ctx.ServerError("DataAsync", err)
|
||||
return
|
||||
}
|
||||
defer reader.Close()
|
||||
scanner := bufio.NewScanner(reader)
|
||||
var diffLines []*gitdiff.DiffLine
|
||||
for line := 0; line < idxRight+chunkSize; line++ {
|
||||
if ok := scanner.Scan(); !ok {
|
||||
break
|
||||
}
|
||||
if line < idxRight {
|
||||
continue
|
||||
}
|
||||
lineText := scanner.Text()
|
||||
diffLine := &gitdiff.DiffLine{
|
||||
LeftIdx: idxLeft + (line - idxRight) + 1,
|
||||
RightIdx: line + 1,
|
||||
Type: gitdiff.DiffLinePlain,
|
||||
Content: " " + lineText,
|
||||
}
|
||||
diffLines = append(diffLines, diffLine)
|
||||
|
||||
section, err := gitdiff.BuildBlobExcerptDiffSection(filePath, reader, opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("BuildBlobExcerptDiffSection", err)
|
||||
return
|
||||
}
|
||||
if err = scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("getExcerptLines scan: %w", err)
|
||||
|
||||
diffBlobExcerptData.PullIssueIndex = ctx.FormInt64("pull_issue_index")
|
||||
if diffBlobExcerptData.PullIssueIndex > 0 {
|
||||
if !ctx.Repo.CanRead(unit.TypePullRequests) {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, diffBlobExcerptData.PullIssueIndex)
|
||||
if err != nil {
|
||||
log.Error("GetIssueByIndex error: %v", err)
|
||||
} else if issue.IsPull {
|
||||
// FIXME: DIFF-CONVERSATION-DATA: the following data assignment is fragile
|
||||
ctx.Data["Issue"] = issue
|
||||
ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
|
||||
return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
|
||||
}
|
||||
// and "diff/comment_form.tmpl" (reply comment) needs them
|
||||
ctx.Data["PageIsPullFiles"] = true
|
||||
ctx.Data["AfterCommitID"] = diffBlobExcerptData.AfterCommitID
|
||||
|
||||
allComments, err := issues_model.FetchCodeComments(ctx, issue, ctx.Doer, ctx.FormBool("show_outdated"))
|
||||
if err != nil {
|
||||
log.Error("FetchCodeComments error: %v", err)
|
||||
} else {
|
||||
if lineComments, ok := allComments[filePath]; ok {
|
||||
attachCommentsToLines(section, lineComments)
|
||||
attachHiddenCommentIDs(section, lineComments)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return diffLines, nil
|
||||
|
||||
ctx.Data["section"] = section
|
||||
ctx.Data["FileNameHash"] = git.HashFilePathForWebUI(filePath)
|
||||
ctx.Data["DiffBlobExcerptData"] = diffBlobExcerptData
|
||||
|
||||
ctx.HTML(http.StatusOK, tplBlobExcerpt)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
|
||||
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
git_service "code.gitea.io/gitea/services/git"
|
||||
"code.gitea.io/gitea/services/gitdiff"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAttachCommentsToLines(t *testing.T) {
|
||||
section := &gitdiff.DiffSection{
|
||||
Lines: []*gitdiff.DiffLine{
|
||||
{LeftIdx: 5, RightIdx: 10},
|
||||
{LeftIdx: 6, RightIdx: 11},
|
||||
},
|
||||
}
|
||||
|
||||
lineComments := map[int64][]*issues_model.Comment{
|
||||
-5: {{ID: 100, CreatedUnix: 1000}}, // left side comment
|
||||
10: {{ID: 200, CreatedUnix: 2000}}, // right side comment
|
||||
11: {{ID: 300, CreatedUnix: 1500}, {ID: 301, CreatedUnix: 2500}}, // multiple comments
|
||||
}
|
||||
|
||||
attachCommentsToLines(section, lineComments)
|
||||
|
||||
// First line should have left and right comments
|
||||
assert.Len(t, section.Lines[0].Comments, 2)
|
||||
assert.Equal(t, int64(100), section.Lines[0].Comments[0].ID)
|
||||
assert.Equal(t, int64(200), section.Lines[0].Comments[1].ID)
|
||||
|
||||
// Second line should have two comments, sorted by creation time
|
||||
assert.Len(t, section.Lines[1].Comments, 2)
|
||||
assert.Equal(t, int64(300), section.Lines[1].Comments[0].ID)
|
||||
assert.Equal(t, int64(301), section.Lines[1].Comments[1].ID)
|
||||
}
|
||||
|
||||
func TestNewPullRequestTitleContent(t *testing.T) {
|
||||
ci := &git_service.CompareInfo{HeadRef: "refs/heads/head-branch"}
|
||||
|
||||
mockCommit := func(msg string) *git_model.SignCommitWithStatuses {
|
||||
return &git_model.SignCommitWithStatuses{
|
||||
SignCommit: &asymkey_model.SignCommit{
|
||||
UserCommit: &user_model.UserCommit{
|
||||
Commit: &git.Commit{
|
||||
CommitMessage: msg,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// no commit
|
||||
title, content := prepareNewPullRequestTitleContent(ci, nil, setting.RepoPRTitleSourceAuto)
|
||||
assert.Equal(t, "Head branch", title)
|
||||
assert.Empty(t, content)
|
||||
|
||||
title, content = prepareNewPullRequestTitleContent(ci, nil, setting.RepoPRTitleSourceFirstCommit)
|
||||
assert.Equal(t, "Head branch", title)
|
||||
assert.Empty(t, content)
|
||||
|
||||
// single commit
|
||||
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("single-commit-title\nbody")}, setting.RepoPRTitleSourceAuto)
|
||||
assert.Equal(t, "single-commit-title", title)
|
||||
assert.Equal(t, "body", content)
|
||||
|
||||
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("single-commit-title\nbody")}, setting.RepoPRTitleSourceFirstCommit)
|
||||
assert.Equal(t, "single-commit-title", title)
|
||||
assert.Equal(t, "body", content)
|
||||
|
||||
// multiple commits
|
||||
commits := []*git_model.SignCommitWithStatuses{
|
||||
// ordered from newest to oldest
|
||||
mockCommit("title2\nbody2"),
|
||||
mockCommit("title1\nbody1"),
|
||||
}
|
||||
title, content = prepareNewPullRequestTitleContent(ci, commits, setting.RepoPRTitleSourceAuto)
|
||||
assert.Equal(t, "Head branch", title)
|
||||
assert.Empty(t, content)
|
||||
|
||||
title, content = prepareNewPullRequestTitleContent(ci, commits, setting.RepoPRTitleSourceFirstCommit)
|
||||
assert.Equal(t, "title1", title)
|
||||
assert.Empty(t, content)
|
||||
|
||||
// title string handling
|
||||
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("title-" + strings.Repeat("a", 255))}, setting.RepoPRTitleSourceFirstCommit)
|
||||
assert.Equal(t, "title-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa…", title)
|
||||
assert.Equal(t, "…aaaaaaaaa\n", content)
|
||||
|
||||
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("a\xf0\xf0\xf0\nb\xf0\xf0\xf0")}, setting.RepoPRTitleSourceFirstCommit)
|
||||
assert.Equal(t, "a?", title) // FIXME: GIT-COMMIT-MESSAGE-ENCODING: "title" doesn't use the same charset converting logic as "content"
|
||||
assert.Equal(t, "b"+string(utf8.RuneError)+string(utf8.RuneError), content)
|
||||
}
|
||||
|
||||
func TestAutoTitleFromBranchName(t *testing.T) {
|
||||
cases := []struct {
|
||||
branch string
|
||||
want string
|
||||
}{
|
||||
{"fix/the-bug", "Fix/the bug"},
|
||||
{"Already-Capitalized", "Already capitalized"},
|
||||
{"ALL-CAPS-BRANCH", "All caps branch"},
|
||||
{"FixHTMLBug", "Fix html bug"},
|
||||
{"MixedCase-Name", "Mixed case name"},
|
||||
{"fooBar-baz", "Foo bar baz"},
|
||||
{"foo/BAR", "Foo/bar"},
|
||||
{"_leading-underscore", "Leading underscore"},
|
||||
{"CamelCase", "Camel case"},
|
||||
{"foo--double-dash", "Foo double dash"},
|
||||
{"123-fix", "123 fix"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
assert.Equal(t, c.want, autoTitleFromBranchName(c.branch), "branch: %q", c.branch)
|
||||
}
|
||||
}
|
||||
@@ -7,76 +7,61 @@ package repo
|
||||
import (
|
||||
"time"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/httpcache"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/lfs"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/routers/common"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
func checkDownloadTokenScope(ctx *context.Context) bool {
|
||||
context.CheckRepoScopedToken(ctx, ctx.Repo.Repository, auth_model.Read)
|
||||
return !ctx.Written()
|
||||
}
|
||||
|
||||
// ServeBlobOrLFS download a git.Blob redirecting to LFS if necessary
|
||||
func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified *time.Time) error {
|
||||
if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
|
||||
if httpcache.HandleGenericETagPrivateCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
|
||||
return nil
|
||||
}
|
||||
|
||||
dataRc, err := blob.DataAsync()
|
||||
lfsPointerBuf, err := blob.GetBlobBytes(lfs.MetaFileMaxSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
closed := false
|
||||
defer func() {
|
||||
if closed {
|
||||
return
|
||||
}
|
||||
if err = dataRc.Close(); err != nil {
|
||||
log.Error("ServeBlobOrLFS: Close: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
pointer, _ := lfs.ReadPointer(dataRc)
|
||||
pointer, _ := lfs.ReadPointerFromBuffer(lfsPointerBuf)
|
||||
if pointer.IsValid() {
|
||||
meta, _ := git_model.GetLFSMetaObjectByOid(ctx, ctx.Repo.Repository.ID, pointer.Oid)
|
||||
if meta == nil {
|
||||
if err = dataRc.Close(); err != nil {
|
||||
log.Error("ServeBlobOrLFS: Close: %v", err)
|
||||
}
|
||||
closed = true
|
||||
return common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, lastModified)
|
||||
}
|
||||
if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+pointer.Oid+`"`) {
|
||||
if httpcache.HandleGenericETagPrivateCache(ctx.Req, ctx.Resp, `"`+pointer.Oid+`"`, meta.UpdatedUnix.AsTimePtr()) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if setting.LFS.Storage.ServeDirect() {
|
||||
// If we have a signed url (S3, object storage, blob storage), redirect to this directly.
|
||||
u, err := storage.LFS.URL(pointer.RelativePath(), blob.Name(), ctx.Req.Method, nil)
|
||||
u, err := storage.LFS.ServeDirectURL(pointer.RelativePath(), blob.Name(), ctx.Req.Method, nil)
|
||||
if u != nil && err == nil {
|
||||
ctx.Redirect(u.String())
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
lfsDataRc, err := lfs.ReadMetaObject(meta.Pointer)
|
||||
lfsDataFile, err := lfs.ReadMetaObject(meta.Pointer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err = lfsDataRc.Close(); err != nil {
|
||||
log.Error("ServeBlobOrLFS: Close: %v", err)
|
||||
}
|
||||
}()
|
||||
common.ServeContentByReadSeeker(ctx.Base, ctx.Repo.TreePath, lastModified, lfsDataRc)
|
||||
defer lfsDataFile.Close()
|
||||
httplib.ServeUserContentByFile(ctx.Req, ctx.Resp, lfsDataFile, httplib.ServeHeaderOptions{Filename: ctx.Repo.TreePath})
|
||||
return nil
|
||||
}
|
||||
if err = dataRc.Close(); err != nil {
|
||||
log.Error("ServeBlobOrLFS: Close: %v", err)
|
||||
}
|
||||
closed = true
|
||||
|
||||
return common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, lastModified)
|
||||
}
|
||||
@@ -109,6 +94,10 @@ func getBlobForEntry(ctx *context.Context) (*git.Blob, *time.Time) {
|
||||
|
||||
// SingleDownload download a file by repos path
|
||||
func SingleDownload(ctx *context.Context) {
|
||||
if !checkDownloadTokenScope(ctx) {
|
||||
return
|
||||
}
|
||||
|
||||
blob, lastModified := getBlobForEntry(ctx)
|
||||
if blob == nil {
|
||||
return
|
||||
@@ -121,6 +110,10 @@ func SingleDownload(ctx *context.Context) {
|
||||
|
||||
// SingleDownloadOrLFS download a file by repos path redirecting to LFS if necessary
|
||||
func SingleDownloadOrLFS(ctx *context.Context) {
|
||||
if !checkDownloadTokenScope(ctx) {
|
||||
return
|
||||
}
|
||||
|
||||
blob, lastModified := getBlobForEntry(ctx)
|
||||
if blob == nil {
|
||||
return
|
||||
@@ -133,6 +126,10 @@ func SingleDownloadOrLFS(ctx *context.Context) {
|
||||
|
||||
// DownloadByID download a file by sha1 ID
|
||||
func DownloadByID(ctx *context.Context) {
|
||||
if !checkDownloadTokenScope(ctx) {
|
||||
return
|
||||
}
|
||||
|
||||
blob, err := ctx.Repo.GitRepo.GetBlob(ctx.PathParam("sha"))
|
||||
if err != nil {
|
||||
if git.IsErrNotExist(err) {
|
||||
@@ -149,6 +146,10 @@ func DownloadByID(ctx *context.Context) {
|
||||
|
||||
// DownloadByIDOrLFS download a file by sha1 ID taking account of LFS
|
||||
func DownloadByIDOrLFS(ctx *context.Context) {
|
||||
if !checkDownloadTokenScope(ctx) {
|
||||
return
|
||||
}
|
||||
|
||||
blob, err := ctx.Repo.GitRepo.GetBlob(ctx.PathParam("sha"))
|
||||
if err != nil {
|
||||
if git.IsErrNotExist(err) {
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"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/util"
|
||||
@@ -41,7 +40,12 @@ const (
|
||||
editorCommitChoiceNewBranch string = "commit-to-new-branch"
|
||||
)
|
||||
|
||||
func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) *context.CommitFormOptions {
|
||||
func prepareEditorPage(ctx *context.Context, editorAction string) *context.CommitFormOptions {
|
||||
prepareHomeTreeSideBarSwitch(ctx)
|
||||
return prepareEditorPageFormOptions(ctx, editorAction)
|
||||
}
|
||||
|
||||
func prepareEditorPageFormOptions(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))
|
||||
@@ -73,8 +77,6 @@ func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) *
|
||||
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["IsEditingFileOnly"] = ctx.FormString("return_uri") != ""
|
||||
ctx.Data["ReturnURI"] = ctx.FormString("return_uri")
|
||||
|
||||
@@ -216,7 +218,8 @@ func redirectForCommitChoice[T any](ctx *context.Context, parsed *preparedEditor
|
||||
}
|
||||
|
||||
// redirect to the newly updated file
|
||||
redirectTo := util.URLJoin(ctx.Repo.RepoLink, "src/branch", util.PathEscapeSegments(parsed.NewBranchName), util.PathEscapeSegments(treePath))
|
||||
redirectTo := ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(parsed.NewBranchName) + "/" + util.PathEscapeSegments(treePath)
|
||||
redirectTo = strings.TrimSuffix(redirectTo, "/")
|
||||
ctx.JSONRedirect(redirectTo)
|
||||
}
|
||||
|
||||
@@ -283,7 +286,7 @@ func EditFile(ctx *context.Context) {
|
||||
// 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))
|
||||
|
||||
prepareEditorCommitFormOptions(ctx, editorAction)
|
||||
prepareEditorPage(ctx, editorAction)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
@@ -312,17 +315,16 @@ func EditFile(ctx *context.Context) {
|
||||
ctx.ServerError("ReadAll", err)
|
||||
return
|
||||
}
|
||||
var fileContent string
|
||||
if content, err := charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true}); err != nil {
|
||||
fileContent = string(buf)
|
||||
} else {
|
||||
fileContent = string(content)
|
||||
}
|
||||
ctx.Data["FileContent"] = fileContent
|
||||
ctx.Data["FileContent"] = string(charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true, ErrorReturnOrigin: true}))
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data["EditorconfigJson"] = getContextRepoEditorConfig(ctx, ctx.Repo.TreePath)
|
||||
editorConfig := getCodeEditorConfigByEditorconfig(ctx, ctx.Repo.TreePath)
|
||||
editorConfig.Autofocus = !isNewFile
|
||||
if isNewFile {
|
||||
editorConfig.Filename = ""
|
||||
}
|
||||
ctx.Data["CodeEditorConfig"] = editorConfig
|
||||
ctx.HTML(http.StatusOK, tplEditFile)
|
||||
}
|
||||
|
||||
@@ -378,15 +380,16 @@ func EditFilePost(ctx *context.Context) {
|
||||
|
||||
// DeleteFile render delete file page
|
||||
func DeleteFile(ctx *context.Context) {
|
||||
prepareEditorCommitFormOptions(ctx, "_delete")
|
||||
prepareEditorPage(ctx, "_delete")
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
ctx.Data["PageIsDelete"] = true
|
||||
prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath)
|
||||
ctx.HTML(http.StatusOK, tplDeleteFile)
|
||||
}
|
||||
|
||||
// DeleteFilePost response for deleting file
|
||||
// DeleteFilePost response for deleting file or directory
|
||||
func DeleteFilePost(ctx *context.Context) {
|
||||
parsed := prepareEditorCommitSubmittedForm[*forms.DeleteRepoFileForm](ctx)
|
||||
if ctx.Written() {
|
||||
@@ -394,17 +397,37 @@ func DeleteFilePost(ctx *context.Context) {
|
||||
}
|
||||
|
||||
treePath := ctx.Repo.TreePath
|
||||
_, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
|
||||
if treePath == "" {
|
||||
ctx.JSONError("cannot delete root directory") // it should not happen unless someone is trying to be malicious
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the path is a directory
|
||||
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath)
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetTreeEntryByPath", git.IsErrNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
var commitMessage string
|
||||
if entry.IsDir() {
|
||||
commitMessage = parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete_directory", treePath))
|
||||
} else {
|
||||
commitMessage = parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete", 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,
|
||||
Operation: "delete",
|
||||
TreePath: treePath,
|
||||
DeleteRecursively: true,
|
||||
},
|
||||
},
|
||||
Message: parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete", treePath)),
|
||||
Message: commitMessage,
|
||||
Signoff: parsed.form.Signoff,
|
||||
Author: parsed.GitCommitter,
|
||||
Committer: parsed.GitCommitter,
|
||||
@@ -414,7 +437,11 @@ func DeleteFilePost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath))
|
||||
if entry.IsDir() {
|
||||
ctx.Flash.Success(ctx.Tr("repo.editor.directory_delete_success", treePath))
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath))
|
||||
}
|
||||
redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, parsed.NewBranchName, treePath)
|
||||
redirectForCommitChoice(ctx, parsed, redirectTreePath)
|
||||
}
|
||||
@@ -422,7 +449,7 @@ func DeleteFilePost(ctx *context.Context) {
|
||||
func UploadFile(ctx *context.Context) {
|
||||
ctx.Data["PageIsUpload"] = true
|
||||
prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath)
|
||||
opts := prepareEditorCommitFormOptions(ctx, "_upload")
|
||||
opts := prepareEditorPage(ctx, "_upload")
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -14,12 +14,13 @@ import (
|
||||
)
|
||||
|
||||
func NewDiffPatch(ctx *context.Context) {
|
||||
prepareEditorCommitFormOptions(ctx, "_diffpatch")
|
||||
prepareEditorPage(ctx, "_diffpatch")
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["PageIsPatch"] = true
|
||||
ctx.Data["CodeEditorConfig"] = CodeEditorConfig{Filename: "diff.patch"}
|
||||
ctx.HTML(http.StatusOK, tplPatchFile)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
@@ -16,7 +17,7 @@ import (
|
||||
)
|
||||
|
||||
func CherryPick(ctx *context.Context) {
|
||||
prepareEditorCommitFormOptions(ctx, "_cherrypick")
|
||||
prepareEditorPage(ctx, "_cherrypick")
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
@@ -64,9 +65,9 @@ func CherryPickPost(ctx *context.Context) {
|
||||
// Drop through to the "apply" method
|
||||
buf := &bytes.Buffer{}
|
||||
if parsed.form.Revert {
|
||||
err = git.GetReverseRawDiff(ctx, ctx.Repo.Repository.RepoPath(), fromCommitID, buf)
|
||||
err = gitrepo.GetReverseRawDiff(ctx, ctx.Repo.Repository, fromCommitID, buf)
|
||||
} else {
|
||||
err = git.GetRawDiff(ctx.Repo.GitRepo, fromCommitID, "patch", buf)
|
||||
err = git.GetRawDiff(ctx.Repo.GitRepo, fromCommitID, git.RawDiffPatch, buf)
|
||||
}
|
||||
if err == nil {
|
||||
opts.Content = buf.String()
|
||||
|
||||
@@ -6,12 +6,13 @@ package repo
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
files_service "code.gitea.io/gitea/services/repository/files"
|
||||
)
|
||||
|
||||
func DiffPreviewPost(ctx *context.Context) {
|
||||
content := ctx.FormString("content")
|
||||
newContent := ctx.FormString("content")
|
||||
treePath := files_service.CleanGitTreePath(ctx.Repo.TreePath)
|
||||
if treePath == "" {
|
||||
ctx.HTTPError(http.StatusBadRequest, "file name to diff is invalid")
|
||||
@@ -27,7 +28,12 @@ func DiffPreviewPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
diff, err := files_service.GetDiffPreview(ctx, ctx.Repo.Repository, ctx.Repo.BranchName, treePath, content)
|
||||
oldContent, err := entry.Blob().GetBlobContent(setting.UI.MaxDisplayFileSize)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetBlobContent", err)
|
||||
return
|
||||
}
|
||||
diff, err := files_service.GetDiffPreview(ctx, ctx.Repo.Repository, ctx.Repo.BranchName, treePath, oldContent, newContent)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetDiffPreview", err)
|
||||
return
|
||||
|
||||
@@ -7,15 +7,19 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"strconv"
|
||||
"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/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
context_service "code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
@@ -61,17 +65,39 @@ func getClosestParentWithFiles(gitRepo *git.Repository, branchName, originTreePa
|
||||
return f(originTreePath, commit)
|
||||
}
|
||||
|
||||
// getContextRepoEditorConfig returns the editorconfig JSON string for given treePath or "null"
|
||||
func getContextRepoEditorConfig(ctx *context_service.Context, treePath string) string {
|
||||
// CodeEditorConfig is also used by frontend, defined in "codeeditor" module
|
||||
type CodeEditorConfig struct {
|
||||
Filename string `json:"filename"` // the base name, not full path
|
||||
Autofocus bool `json:"autofocus"`
|
||||
PreviewableExtensions []string `json:"previewableExtensions,omitempty"`
|
||||
LineWrapExtensions []string `json:"lineWrapExtensions,omitempty"`
|
||||
LineWrap bool `json:"lineWrap"`
|
||||
Previewable bool `json:"previewable,omitempty"`
|
||||
|
||||
// the following can be read from .editorconfig if exists, or use default value
|
||||
IndentStyle string `json:"indentStyle"` // in most cases, keep it empty by default, detected by the source code
|
||||
IndentSize int `json:"indentSize"`
|
||||
TabWidth int `json:"tabWidth"`
|
||||
TrimTrailingWhitespace *bool `json:"trimTrailingWhitespace,omitempty"`
|
||||
}
|
||||
|
||||
func getCodeEditorConfigByEditorconfig(ctx *context_service.Context, treePath string) CodeEditorConfig {
|
||||
ret := CodeEditorConfig{Filename: path.Base(treePath)}
|
||||
ret.PreviewableExtensions = markup.PreviewableExtensions()
|
||||
ret.LineWrapExtensions = setting.Repository.Editor.LineWrapExtensions
|
||||
ret.LineWrap = util.SliceContainsString(ret.LineWrapExtensions, path.Ext(treePath), true)
|
||||
ret.Previewable = util.SliceContainsString(ret.PreviewableExtensions, path.Ext(treePath), true)
|
||||
ec, _, err := ctx.Repo.GetEditorconfig()
|
||||
if err == nil {
|
||||
def, err := ec.GetDefinitionForFilename(treePath)
|
||||
if err == nil {
|
||||
jsonStr, _ := json.Marshal(def)
|
||||
return string(jsonStr)
|
||||
ret.IndentStyle = util.IfZero(def.IndentStyle, ret.IndentStyle)
|
||||
ret.IndentSize, _ = strconv.Atoi(def.IndentSize)
|
||||
ret.TabWidth = def.TabWidth
|
||||
ret.TrimTrailingWhitespace = def.TrimTrailingWhitespace
|
||||
}
|
||||
}
|
||||
return "null"
|
||||
return ret
|
||||
}
|
||||
|
||||
// getParentTreeFields returns list of parent tree names and corresponding tree paths based on given treePath.
|
||||
@@ -102,8 +128,7 @@ func getUniqueRepositoryName(ctx context.Context, ownerID int64, name string) st
|
||||
}
|
||||
|
||||
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(),
|
||||
return gitrepo.Push(ctx, baseRepo, targetRepo, git.PushOptions{
|
||||
Branch: baseBranchName + ":" + targetBranchName,
|
||||
Env: repo_module.PushingEnvironment(doer, targetRepo),
|
||||
})
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
// Copyright 2022 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/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
const (
|
||||
tplFindFiles templates.TplName = "repo/find/files"
|
||||
)
|
||||
|
||||
// FindFiles render the page to find repository files
|
||||
func FindFiles(ctx *context.Context) {
|
||||
path := ctx.PathParam("*")
|
||||
ctx.Data["TreeLink"] = ctx.Repo.RepoLink + "/src/" + util.PathEscapeSegments(path)
|
||||
ctx.Data["DataLink"] = ctx.Repo.RepoLink + "/tree-list/" + util.PathEscapeSegments(path)
|
||||
ctx.HTML(http.StatusOK, tplFindFiles)
|
||||
}
|
||||
@@ -5,13 +5,11 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
gocontext "context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"path"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
@@ -19,7 +17,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
@@ -27,6 +24,7 @@ import (
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@@ -57,9 +55,9 @@ func CorsHandler() func(next http.Handler) http.Handler {
|
||||
}
|
||||
}
|
||||
|
||||
// httpBase implementation git smart HTTP protocol
|
||||
// httpBase does the common work for git http services,
|
||||
// including early response, authentication, repository lookup and permission check.
|
||||
func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
|
||||
username := ctx.PathParam("username")
|
||||
reponame := strings.TrimSuffix(ctx.PathParam("reponame"), ".git")
|
||||
|
||||
if ctx.FormString("go-get") == "1" {
|
||||
@@ -67,16 +65,23 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
|
||||
return nil
|
||||
}
|
||||
|
||||
var serviceType string
|
||||
var isPull, receivePack bool
|
||||
switch util.OptionalArg(optGitService) {
|
||||
case "git-receive-pack":
|
||||
serviceType = ServiceTypeReceivePack
|
||||
receivePack = true
|
||||
case "git-upload-pack":
|
||||
serviceType = ServiceTypeUploadPack
|
||||
isPull = true
|
||||
case "git-upload-archive":
|
||||
serviceType = ServiceTypeUploadArchive
|
||||
isPull = true
|
||||
default:
|
||||
case "":
|
||||
isPull = ctx.Req.Method == http.MethodHead || ctx.Req.Method == http.MethodGet
|
||||
default: // unknown service
|
||||
ctx.Resp.WriteHeader(http.StatusBadRequest)
|
||||
return nil
|
||||
}
|
||||
|
||||
var accessMode perm.AccessMode
|
||||
@@ -124,10 +129,7 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
|
||||
|
||||
// Only public pull don't need auth.
|
||||
isPublicPull := repoExist && !repo.IsPrivate && isPull
|
||||
var (
|
||||
askAuth = !isPublicPull || setting.Service.RequireSignInViewStrict
|
||||
environ []string
|
||||
)
|
||||
askAuth := !isPublicPull || setting.Service.RequireSignInViewStrict
|
||||
|
||||
// don't allow anonymous pulls if organization is not public
|
||||
if isPublicPull {
|
||||
@@ -160,7 +162,7 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ctx.IsBasicAuth && ctx.Data["IsApiToken"] != true && ctx.Data["IsActionsToken"] != true {
|
||||
if ctx.IsBasicAuth && ctx.Data["IsApiToken"] != true && !ctx.Doer.IsGiteaActions() {
|
||||
_, err = auth_model.GetTwoFactorByUID(ctx, ctx.Doer.ID)
|
||||
if err == nil {
|
||||
// TODO: This response should be changed to "invalid credentials" for security reasons once the expectation behind it (creating an app token to authenticate) is properly documented
|
||||
@@ -177,56 +179,21 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
|
||||
return nil
|
||||
}
|
||||
|
||||
environ = []string{
|
||||
repo_module.EnvRepoUsername + "=" + username,
|
||||
repo_module.EnvRepoName + "=" + reponame,
|
||||
repo_module.EnvPusherName + "=" + ctx.Doer.Name,
|
||||
repo_module.EnvPusherID + fmt.Sprintf("=%d", ctx.Doer.ID),
|
||||
repo_module.EnvAppURL + "=" + setting.AppURL,
|
||||
}
|
||||
|
||||
if repoExist {
|
||||
// Because of special ref "refs/for" .. , need delay write permission check
|
||||
if git.DefaultFeatures().SupportProcReceive {
|
||||
// Only the main code repo accepts refs/for pushes, so wiki pushes must keep write checks.
|
||||
if git.DefaultFeatures().SupportProcReceive && !isWiki {
|
||||
accessMode = perm.AccessModeRead
|
||||
}
|
||||
|
||||
if ctx.Data["IsActionsToken"] == true {
|
||||
taskID := ctx.Data["ActionsTaskID"].(int64)
|
||||
task, err := actions_model.GetTaskByID(ctx, taskID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetTaskByID", err)
|
||||
return nil
|
||||
}
|
||||
if task.RepoID != repo.ID {
|
||||
ctx.PlainText(http.StatusForbidden, "User permission denied")
|
||||
return nil
|
||||
}
|
||||
p, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetDoerRepoPermission", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if task.IsForkPullRequest {
|
||||
if accessMode > perm.AccessModeRead {
|
||||
ctx.PlainText(http.StatusForbidden, "User permission denied")
|
||||
return nil
|
||||
}
|
||||
environ = append(environ, fmt.Sprintf("%s=%d", repo_module.EnvActionPerm, perm.AccessModeRead))
|
||||
} else {
|
||||
if accessMode > perm.AccessModeWrite {
|
||||
ctx.PlainText(http.StatusForbidden, "User permission denied")
|
||||
return nil
|
||||
}
|
||||
environ = append(environ, fmt.Sprintf("%s=%d", repo_module.EnvActionPerm, perm.AccessModeWrite))
|
||||
}
|
||||
} else {
|
||||
p, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserRepoPermission", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if !p.CanAccess(accessMode, unitType) {
|
||||
ctx.PlainText(http.StatusNotFound, "Repository not found")
|
||||
return nil
|
||||
}
|
||||
if !p.CanAccess(accessMode, unitType) {
|
||||
ctx.PlainText(http.StatusNotFound, "Repository not found")
|
||||
return nil
|
||||
}
|
||||
|
||||
if !isPull && repo.IsMirror {
|
||||
@@ -234,16 +201,6 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if !ctx.Doer.KeepEmailPrivate {
|
||||
environ = append(environ, repo_module.EnvPusherEmail+"="+ctx.Doer.Email)
|
||||
}
|
||||
|
||||
if isWiki {
|
||||
environ = append(environ, repo_module.EnvRepoIsWiki+"=true")
|
||||
} else {
|
||||
environ = append(environ, repo_module.EnvRepoIsWiki+"=false")
|
||||
}
|
||||
}
|
||||
|
||||
if !repoExist {
|
||||
@@ -287,17 +244,18 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
|
||||
ctx.PlainText(http.StatusForbidden, "repository wiki is disabled")
|
||||
return nil
|
||||
}
|
||||
log.Error("Failed to get the wiki unit in %-v Error: %v", repo, err)
|
||||
ctx.ServerError("GetUnit(UnitTypeWiki) for "+repo.FullName(), err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
environ = append(environ, repo_module.EnvRepoID+fmt.Sprintf("=%d", repo.ID))
|
||||
var environ []string
|
||||
if !isPull {
|
||||
// if not "pull", then must be "push", and doer must exist
|
||||
environ = repo_module.DoerPushingEnvironment(ctx.Doer, repo, isWiki)
|
||||
}
|
||||
|
||||
ctx.Req.URL.Path = strings.ToLower(ctx.Req.URL.Path) // blue: In case some repo name has upper case name
|
||||
|
||||
return &serviceHandler{repo, isWiki, environ}
|
||||
return &serviceHandler{serviceType, repo, isWiki, environ}
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -319,7 +277,9 @@ func dummyInfoRefs(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
refs, _, err := gitcmd.NewCommand("receive-pack", "--stateless-rpc", "--advertise-refs", ".").RunStdBytes(ctx, &gitcmd.RunOpts{Dir: tmpDir})
|
||||
refs, _, err := gitcmd.NewCommand("receive-pack", "--stateless-rpc", "--advertise-refs", ".").
|
||||
WithDir(tmpDir).
|
||||
RunStdBytes(ctx)
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("%v - %s", err, string(refs)))
|
||||
}
|
||||
@@ -338,16 +298,18 @@ func dummyInfoRefs(ctx *context.Context) {
|
||||
}
|
||||
|
||||
type serviceHandler struct {
|
||||
serviceType string
|
||||
|
||||
repo *repo_model.Repository
|
||||
isWiki bool
|
||||
environ []string
|
||||
}
|
||||
|
||||
func (h *serviceHandler) getRepoDir() string {
|
||||
func (h *serviceHandler) getStorageRepo() gitrepo.Repository {
|
||||
if h.isWiki {
|
||||
return h.repo.WikiPath()
|
||||
return h.repo.WikiStorageRepo()
|
||||
}
|
||||
return h.repo.RepoPath()
|
||||
return h.repo
|
||||
}
|
||||
|
||||
func setHeaderNoCache(ctx *context.Context) {
|
||||
@@ -358,7 +320,7 @@ func setHeaderNoCache(ctx *context.Context) {
|
||||
|
||||
func setHeaderCacheForever(ctx *context.Context) {
|
||||
now := time.Now().Unix()
|
||||
expires := now + 31536000
|
||||
expires := now + 365*86400 // 365 days
|
||||
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")
|
||||
@@ -375,65 +337,58 @@ func isSlashRune(r rune) bool { return r == '/' || r == '\\' }
|
||||
|
||||
func (h *serviceHandler) sendFile(ctx *context.Context, contentType, file string) {
|
||||
if containsParentDirectorySeparator(file) {
|
||||
log.Error("request file path contains invalid path: %v", file)
|
||||
log.Debug("request file path contains invalid path: %v", file)
|
||||
ctx.Resp.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
reqFile := filepath.Join(h.getRepoDir(), file)
|
||||
|
||||
fi, err := os.Stat(reqFile)
|
||||
if os.IsNotExist(err) {
|
||||
ctx.Resp.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
fs := gitrepo.GetRepoFS(h.getStorageRepo())
|
||||
ctx.Resp.Header().Set("Content-Type", contentType)
|
||||
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)
|
||||
http.ServeFileFS(ctx.Resp, ctx.Req, fs, path.Clean(file))
|
||||
}
|
||||
|
||||
// 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(service string) (*gitcmd.Command, error) {
|
||||
if service == "receive-pack" {
|
||||
return gitcmd.NewCommand("receive-pack"), nil
|
||||
func prepareGitCmdWithAllowedService(service string, allowedServices []string) *gitcmd.Command {
|
||||
if !slices.Contains(allowedServices, service) {
|
||||
return nil
|
||||
}
|
||||
if service == "upload-pack" {
|
||||
return gitcmd.NewCommand("upload-pack"), nil
|
||||
switch service {
|
||||
case ServiceTypeReceivePack:
|
||||
return gitcmd.NewCommand(ServiceTypeReceivePack)
|
||||
case ServiceTypeUploadPack:
|
||||
return gitcmd.NewCommand(ServiceTypeUploadPack)
|
||||
case ServiceTypeUploadArchive:
|
||||
return gitcmd.NewCommand(ServiceTypeUploadArchive)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("service %q is not allowed", service)
|
||||
}
|
||||
|
||||
func serviceRPC(ctx *context.Context, service string) {
|
||||
defer func() {
|
||||
if err := ctx.Req.Body.Close(); err != nil {
|
||||
log.Error("serviceRPC: Close: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
defer ctx.Req.Body.Close()
|
||||
h := httpBase(ctx, "git-"+service)
|
||||
if h == nil {
|
||||
ctx.Resp.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
expectedContentType := fmt.Sprintf("application/x-git-%s-request", service)
|
||||
if ctx.Req.Header.Get("Content-Type") != expectedContentType {
|
||||
log.Error("Content-Type (%q) doesn't match expected: %q", ctx.Req.Header.Get("Content-Type"), expectedContentType)
|
||||
ctx.Resp.WriteHeader(http.StatusUnauthorized)
|
||||
log.Debug("Content-Type (%q) doesn't match expected: %q", ctx.Req.Header.Get("Content-Type"), expectedContentType)
|
||||
ctx.Resp.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
cmd, err := prepareGitCmdWithAllowedService(service)
|
||||
if err != nil {
|
||||
log.Error("Failed to prepareGitCmdWithService: %v", err)
|
||||
ctx.Resp.WriteHeader(http.StatusUnauthorized)
|
||||
cmd := prepareGitCmdWithAllowedService(service, []string{ServiceTypeUploadPack, ServiceTypeReceivePack, ServiceTypeUploadArchive})
|
||||
if cmd == nil {
|
||||
ctx.Resp.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// git upload-archive does not have a "--stateless-rpc" option
|
||||
if service == ServiceTypeUploadPack || service == ServiceTypeReceivePack {
|
||||
cmd.AddArguments("--stateless-rpc")
|
||||
}
|
||||
|
||||
ctx.Resp.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-result", service))
|
||||
|
||||
@@ -441,10 +396,10 @@ func serviceRPC(ctx *context.Context, service string) {
|
||||
|
||||
// Handle GZIP.
|
||||
if ctx.Req.Header.Get("Content-Encoding") == "gzip" {
|
||||
var err error
|
||||
reqBody, err = gzip.NewReader(reqBody)
|
||||
if err != nil {
|
||||
log.Error("Fail to create gzip reader: %v", err)
|
||||
ctx.Resp.WriteHeader(http.StatusInternalServerError)
|
||||
ctx.Resp.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -456,39 +411,35 @@ func serviceRPC(ctx *context.Context, service string) {
|
||||
h.environ = append(h.environ, "GIT_PROTOCOL="+protocol)
|
||||
}
|
||||
|
||||
var stderr bytes.Buffer
|
||||
cmd.AddArguments("--stateless-rpc").AddDynamicArguments(h.getRepoDir())
|
||||
if err := cmd.Run(ctx, &gitcmd.RunOpts{
|
||||
Dir: h.getRepoDir(),
|
||||
Env: append(os.Environ(), h.environ...),
|
||||
Stdout: ctx.Resp,
|
||||
Stdin: reqBody,
|
||||
Stderr: &stderr,
|
||||
UseContextTimeout: true,
|
||||
}); err != nil {
|
||||
if !git.IsErrCanceledOrKilled(err) {
|
||||
log.Error("Fail to serve RPC(%s) in %s: %v - %s", service, h.getRepoDir(), err, stderr.String())
|
||||
if err := gitrepo.RunCmdWithStderr(ctx, h.getStorageRepo(), cmd.AddArguments(".").
|
||||
WithEnv(append(os.Environ(), h.environ...)).
|
||||
WithStdinCopy(reqBody).
|
||||
WithStdoutCopy(ctx.Resp),
|
||||
); err != nil {
|
||||
if !gitcmd.IsErrorCanceledOrKilled(err) {
|
||||
log.Error("Fail to serve RPC(%s) in %s: %v", service, h.getStorageRepo().RelativePath(), err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
ServiceTypeUploadPack = "upload-pack"
|
||||
ServiceTypeReceivePack = "receive-pack"
|
||||
ServiceTypeUploadArchive = "upload-archive"
|
||||
)
|
||||
|
||||
// ServiceUploadPack implements Git Smart HTTP protocol
|
||||
func ServiceUploadPack(ctx *context.Context) {
|
||||
serviceRPC(ctx, "upload-pack")
|
||||
serviceRPC(ctx, ServiceTypeUploadPack)
|
||||
}
|
||||
|
||||
// ServiceReceivePack implements Git Smart HTTP protocol
|
||||
func ServiceReceivePack(ctx *context.Context) {
|
||||
serviceRPC(ctx, "receive-pack")
|
||||
serviceRPC(ctx, ServiceTypeReceivePack)
|
||||
}
|
||||
|
||||
func updateServerInfo(ctx gocontext.Context, dir string) []byte {
|
||||
out, _, err := gitcmd.NewCommand("update-server-info").RunStdBytes(ctx, &gitcmd.RunOpts{Dir: dir})
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("%v - %s", err, string(out)))
|
||||
}
|
||||
return out
|
||||
func ServiceUploadArchive(ctx *context.Context) {
|
||||
serviceRPC(ctx, ServiceTypeUploadArchive)
|
||||
}
|
||||
|
||||
func packetWrite(str string) []byte {
|
||||
@@ -501,33 +452,45 @@ func packetWrite(str string) []byte {
|
||||
|
||||
// GetInfoRefs implements Git dumb HTTP
|
||||
func GetInfoRefs(ctx *context.Context) {
|
||||
service := strings.TrimPrefix(ctx.Req.FormValue("service"), "git-")
|
||||
h := httpBase(ctx, "git-"+service)
|
||||
h := httpBase(ctx, ctx.FormString("service")) // git http protocol: "?service=git-<service>"
|
||||
if h == nil {
|
||||
return
|
||||
}
|
||||
setHeaderNoCache(ctx)
|
||||
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)
|
||||
if h.serviceType == "" {
|
||||
// it's said that some legacy git clients will send requests to "/info/refs" without "service" parameter,
|
||||
// although there should be no such case client in the modern days. TODO: not quite sure why we need this UpdateServerInfo logic
|
||||
if err := gitrepo.UpdateServerInfo(ctx, h.getStorageRepo()); err != nil {
|
||||
ctx.ServerError("UpdateServerInfo", err)
|
||||
return
|
||||
}
|
||||
h.environ = append(os.Environ(), h.environ...)
|
||||
|
||||
refs, _, err := cmd.AddArguments("--stateless-rpc", "--advertise-refs", ".").RunStdBytes(ctx, &gitcmd.RunOpts{Env: h.environ, Dir: h.getRepoDir()})
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("%v - %s", err, string(refs)))
|
||||
}
|
||||
|
||||
ctx.Resp.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", service))
|
||||
ctx.Resp.WriteHeader(http.StatusOK)
|
||||
_, _ = ctx.Resp.Write(packetWrite("# service=git-" + service + "\n"))
|
||||
_, _ = ctx.Resp.Write([]byte("0000"))
|
||||
_, _ = ctx.Resp.Write(refs)
|
||||
} else {
|
||||
updateServerInfo(ctx, h.getRepoDir())
|
||||
h.sendFile(ctx, "text/plain; charset=utf-8", "info/refs")
|
||||
return
|
||||
}
|
||||
|
||||
cmd := prepareGitCmdWithAllowedService(h.serviceType, []string{ServiceTypeUploadPack, ServiceTypeReceivePack})
|
||||
if cmd == nil {
|
||||
ctx.Resp.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
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...)
|
||||
|
||||
cmd = cmd.AddArguments("--stateless-rpc", "--advertise-refs", ".").WithEnv(h.environ)
|
||||
refs, _, err := gitrepo.RunCmdBytes(ctx, h.getStorageRepo(), cmd)
|
||||
if err != nil {
|
||||
ctx.ServerError("RunGitServiceAdvertiseRefs", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Resp.Header().Set("Content-Type", fmt.Sprintf("application/x-git-%s-advertisement", h.serviceType))
|
||||
ctx.Resp.WriteHeader(http.StatusOK)
|
||||
_, _ = ctx.Resp.Write(packetWrite("# service=git-" + h.serviceType + "\n"))
|
||||
_, _ = ctx.Resp.Write([]byte("0000"))
|
||||
_, _ = ctx.Resp.Write(refs)
|
||||
}
|
||||
|
||||
// GetTextFile implements Git dumb HTTP
|
||||
|
||||
@@ -14,13 +14,13 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
project_model "code.gitea.io/gitea/models/project"
|
||||
"code.gitea.io/gitea/models/renderhelper"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/htmlutil"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
@@ -109,11 +109,6 @@ func MustAllowPulls(ctx *context.Context) {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
// User can send pull request if owns a forked repository.
|
||||
if ctx.IsSigned && repo_model.HasForkedRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) {
|
||||
ctx.Repo.PullRequest.Allowed = true
|
||||
}
|
||||
}
|
||||
|
||||
func retrieveProjectsInternal(ctx *context.Context, repo *repo_model.Repository) (open, closed []*project_model.Project) {
|
||||
@@ -374,7 +369,7 @@ func UpdateIssueContent(ctx *context.Context) {
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"content": content,
|
||||
"content": commentContentHTML(ctx, content),
|
||||
"contentVersion": issue.ContentVersion,
|
||||
"attachments": attachmentsHTML(ctx, issue.Attachments, issue.Content),
|
||||
})
|
||||
@@ -634,6 +629,13 @@ func updateAttachments(ctx *context.Context, item any, files []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func commentContentHTML(ctx *context.Context, content template.HTML) template.HTML {
|
||||
if strings.TrimSpace(string(content)) == "" {
|
||||
return htmlutil.HTMLFormat(`<span class="no-content">%s</span>`, ctx.Tr("repo.issues.no_content"))
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment, content string) template.HTML {
|
||||
attachHTML, err := ctx.RenderToHTML(tplAttachment, map[string]any{
|
||||
"ctxData": ctx.Data,
|
||||
@@ -646,47 +648,3 @@ func attachmentsHTML(ctx *context.Context, attachments []*repo_model.Attachment,
|
||||
}
|
||||
return attachHTML
|
||||
}
|
||||
|
||||
// handleMentionableAssigneesAndTeams gets all teams that current user can mention, and fills the assignee users to the context data
|
||||
func handleMentionableAssigneesAndTeams(ctx *context.Context, assignees []*user_model.User) {
|
||||
// TODO: need to figure out how many places this is really used, and rename it to "MentionableAssignees"
|
||||
// at the moment it is used on the issue list page, for the markdown editor mention
|
||||
ctx.Data["Assignees"] = assignees
|
||||
|
||||
if ctx.Doer == nil || !ctx.Repo.Owner.IsOrganization() {
|
||||
return
|
||||
}
|
||||
|
||||
var isAdmin bool
|
||||
var err error
|
||||
var teams []*organization.Team
|
||||
org := organization.OrgFromUser(ctx.Repo.Owner)
|
||||
// Admin has super access.
|
||||
if ctx.Doer.IsAdmin {
|
||||
isAdmin = true
|
||||
} else {
|
||||
isAdmin, err = org.IsOwnedBy(ctx, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("IsOwnedBy", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if isAdmin {
|
||||
teams, err = org.LoadTeams(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("LoadTeams", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
teams, err = org.GetUserTeams(ctx, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserTeams", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data["MentionableTeams"] = teams
|
||||
ctx.Data["MentionableTeamsOrg"] = ctx.Repo.Owner.Name
|
||||
ctx.Data["MentionableTeamsOrgAvatar"] = ctx.Repo.Owner.AvatarLink(ctx)
|
||||
}
|
||||
|
||||
@@ -9,19 +9,19 @@ import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
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/htmlutil"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
@@ -32,31 +32,22 @@ import (
|
||||
|
||||
// NewComment create a comment for issue
|
||||
func NewComment(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.CreateCommentForm)
|
||||
issue := GetActionIssue(ctx)
|
||||
if ctx.Written() {
|
||||
if issue == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) {
|
||||
if log.IsTrace() {
|
||||
if ctx.IsSigned {
|
||||
issueType := "issues"
|
||||
if issue.IsPull {
|
||||
issueType = "pulls"
|
||||
}
|
||||
log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
|
||||
"User in Repo has Permissions: %-+v",
|
||||
ctx.Doer,
|
||||
issue.PosterID,
|
||||
issueType,
|
||||
ctx.Repo.Repository,
|
||||
ctx.Repo.Permission)
|
||||
} else {
|
||||
log.Trace("Permission Denied: Not logged in")
|
||||
}
|
||||
}
|
||||
if ctx.HasError() {
|
||||
ctx.JSONError(ctx.GetErrMsg())
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*forms.CreateCommentForm)
|
||||
issueType := util.Iif(issue.IsPull, "pulls", "issues")
|
||||
|
||||
if !ctx.IsSigned || (ctx.Doer.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) {
|
||||
log.Trace("Permission Denied: User %-v not the Poster (ID: %d) and cannot read %s in Repo %-v.\n"+
|
||||
"User in Repo has Permissions: %-+v", ctx.Doer, issue.PosterID, issueType, ctx.Repo.Repository, ctx.Repo.Permission)
|
||||
ctx.HTTPError(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
@@ -66,152 +57,134 @@ func NewComment(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var attachments []string
|
||||
if setting.Attachment.Enabled {
|
||||
attachments = form.Files
|
||||
}
|
||||
redirect := fmt.Sprintf("%s/%s/%d", ctx.Repo.RepoLink, issueType, issue.Index)
|
||||
attachments := util.Iif(setting.Attachment.Enabled, form.Files, nil)
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.JSONError(ctx.GetErrMsg())
|
||||
return
|
||||
}
|
||||
|
||||
var comment *issues_model.Comment
|
||||
defer func() {
|
||||
// Check if issue admin/poster changes the status of issue.
|
||||
if (ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) || (ctx.IsSigned && issue.IsPoster(ctx.Doer.ID))) &&
|
||||
(form.Status == "reopen" || form.Status == "close") &&
|
||||
!(issue.IsPull && issue.PullRequest.HasMerged) {
|
||||
// Duplication and conflict check should apply to reopen pull request.
|
||||
var pr *issues_model.PullRequest
|
||||
|
||||
if form.Status == "reopen" && issue.IsPull {
|
||||
pull := issue.PullRequest
|
||||
var err error
|
||||
pr, err = issues_model.GetUnmergedPullRequest(ctx, pull.HeadRepoID, pull.BaseRepoID, pull.HeadBranch, pull.BaseBranch, pull.Flow)
|
||||
if err != nil {
|
||||
if !issues_model.IsErrPullRequestNotExist(err) {
|
||||
ctx.JSONError(ctx.Tr("repo.issues.dependency.pr_close_blocked"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Regenerate patch and test conflict.
|
||||
if pr == nil {
|
||||
issue.PullRequest.HeadCommitID = ""
|
||||
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
|
||||
// get head commit of PR
|
||||
if pull.Flow == issues_model.PullRequestFlowGithub {
|
||||
prHeadRef := pull.GetGitHeadRefName()
|
||||
if err := pull.LoadBaseRepo(ctx); err != nil {
|
||||
ctx.ServerError("Unable to load base repo", err)
|
||||
return
|
||||
}
|
||||
prHeadCommitID, err := git.GetFullCommitID(ctx, pull.BaseRepo.RepoPath(), prHeadRef)
|
||||
if err != nil {
|
||||
ctx.ServerError("Get head commit Id of pr fail", err)
|
||||
return
|
||||
}
|
||||
|
||||
// get head commit of branch in the head repo
|
||||
if err := pull.LoadHeadRepo(ctx); err != nil {
|
||||
ctx.ServerError("Unable to load head repo", err)
|
||||
return
|
||||
}
|
||||
if ok := gitrepo.IsBranchExist(ctx, pull.HeadRepo, pull.BaseBranch); !ok {
|
||||
// todo localize
|
||||
ctx.JSONError("The origin branch is delete, cannot reopen.")
|
||||
return
|
||||
}
|
||||
headBranchRef := pull.GetGitHeadBranchRefName()
|
||||
headBranchCommitID, err := git.GetFullCommitID(ctx, pull.HeadRepo.RepoPath(), headBranchRef)
|
||||
if err != nil {
|
||||
ctx.ServerError("Get head commit Id of head branch fail", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = pull.LoadIssue(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("load the issue of pull request error", err)
|
||||
return
|
||||
}
|
||||
|
||||
if prHeadCommitID != headBranchCommitID {
|
||||
// force push to base repo
|
||||
err := git.Push(ctx, pull.HeadRepo.RepoPath(), git.PushOptions{
|
||||
Remote: pull.BaseRepo.RepoPath(),
|
||||
Branch: pull.HeadBranch + ":" + prHeadRef,
|
||||
Force: true,
|
||||
Env: repo_module.InternalPushingEnvironment(pull.Issue.Poster, pull.BaseRepo),
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("force push error", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if pr != nil {
|
||||
ctx.Flash.Info(ctx.Tr("repo.pulls.open_unmerged_pull_exists", pr.Index))
|
||||
// allow empty content if there are attachments
|
||||
if form.Content != "" || len(attachments) > 0 {
|
||||
comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Content, attachments)
|
||||
if err != nil {
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
|
||||
} else {
|
||||
if form.Status == "close" && !issue.IsClosed {
|
||||
if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil {
|
||||
log.Error("CloseIssue: %v", err)
|
||||
if issues_model.IsErrDependenciesLeft(err) {
|
||||
if issue.IsPull {
|
||||
ctx.JSONError(ctx.Tr("repo.issues.dependency.pr_close_blocked"))
|
||||
} else {
|
||||
ctx.JSONError(ctx.Tr("repo.issues.dependency.issue_close_blocked"))
|
||||
}
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := stopTimerIfAvailable(ctx, ctx.Doer, issue); err != nil {
|
||||
ctx.ServerError("stopTimerIfAvailable", err)
|
||||
return
|
||||
}
|
||||
log.Trace("Issue [%d] status changed to closed: %v", issue.ID, issue.IsClosed)
|
||||
}
|
||||
} else if form.Status == "reopen" && issue.IsClosed {
|
||||
if err := issue_service.ReopenIssue(ctx, issue, ctx.Doer, ""); err != nil {
|
||||
log.Error("ReopenIssue: %v", err)
|
||||
ctx.ServerError("CreateIssueComment", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
// redirect to the comment's hashtag
|
||||
redirect += "#" + comment.HashTag()
|
||||
} else if form.Status == "" {
|
||||
// if no status change (close, reopen), it is a plain comment, and content is required
|
||||
// "approve/reject" are handled differently in SubmitReview
|
||||
ctx.JSONError(ctx.Tr("repo.issues.comment_no_content"))
|
||||
return
|
||||
}
|
||||
|
||||
// ATTENTION: From now on, do not use ctx.JSONError, don't return on user error, because the comment has been created.
|
||||
// Always use ctx.Flash.Xxx and then redirect, then the message will be displayed
|
||||
// TODO: need further refactoring to the code below
|
||||
|
||||
// Check if doer can change the status of issue (close, reopen).
|
||||
if (ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) || (ctx.IsSigned && issue.IsPoster(ctx.Doer.ID))) &&
|
||||
(form.Status == "reopen" || form.Status == "close") &&
|
||||
!(issue.IsPull && issue.PullRequest.HasMerged) {
|
||||
// Duplication and conflict check should apply to reopen pull request.
|
||||
var branchOtherUnmergedPR *issues_model.PullRequest
|
||||
var err error
|
||||
if form.Status == "reopen" && issue.IsPull {
|
||||
pull := issue.PullRequest
|
||||
branchOtherUnmergedPR, err = issues_model.GetUnmergedPullRequest(ctx, pull.HeadRepoID, pull.BaseRepoID, pull.HeadBranch, pull.BaseBranch, pull.Flow)
|
||||
if err != nil {
|
||||
if !issues_model.IsErrPullRequestNotExist(err) {
|
||||
ctx.Flash.Error(ctx.Tr("repo.issues.dependency.pr_close_blocked"))
|
||||
}
|
||||
}
|
||||
|
||||
if branchOtherUnmergedPR != nil {
|
||||
ctx.Flash.Error(ctx.Tr("repo.pulls.open_unmerged_pull_exists", branchOtherUnmergedPR.Index))
|
||||
} else {
|
||||
// Regenerate patch and test conflict.
|
||||
issue.PullRequest.HeadCommitID = ""
|
||||
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
|
||||
// get head commit of PR
|
||||
if branchOtherUnmergedPR != nil && pull.Flow == issues_model.PullRequestFlowGithub {
|
||||
prHeadRef := pull.GetGitHeadRefName()
|
||||
if err := pull.LoadBaseRepo(ctx); err != nil {
|
||||
ctx.ServerError("Unable to load base repo", err)
|
||||
return
|
||||
}
|
||||
prHeadCommitID, err := gitrepo.GetFullCommitID(ctx, pull.BaseRepo, prHeadRef)
|
||||
if err != nil {
|
||||
ctx.ServerError("Get head commit Id of pr fail", err)
|
||||
return
|
||||
}
|
||||
|
||||
// get head commit of branch in the head repo
|
||||
if err := pull.LoadHeadRepo(ctx); err != nil {
|
||||
ctx.ServerError("Unable to load head repo", err)
|
||||
return
|
||||
}
|
||||
if exist, _ := git_model.IsBranchExist(ctx, pull.HeadRepo.ID, pull.BaseBranch); !exist {
|
||||
ctx.Flash.Error("The origin branch is delete, cannot reopen.")
|
||||
return
|
||||
}
|
||||
headBranchRef := git.RefNameFromBranch(pull.HeadBranch)
|
||||
headBranchCommitID, err := gitrepo.GetFullCommitID(ctx, pull.HeadRepo, headBranchRef.String())
|
||||
if err != nil {
|
||||
ctx.ServerError("Get head commit Id of head branch fail", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = pull.LoadIssue(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("load the issue of pull request error", err)
|
||||
return
|
||||
}
|
||||
|
||||
if prHeadCommitID != headBranchCommitID {
|
||||
// force push to base repo
|
||||
err := gitrepo.Push(ctx, pull.HeadRepo, pull.BaseRepo, git.PushOptions{
|
||||
Branch: pull.HeadBranch + ":" + prHeadRef,
|
||||
Force: true,
|
||||
Env: repo_module.InternalPushingEnvironment(pull.Issue.Poster, pull.BaseRepo),
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("force push error", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect to comment hashtag if there is any actual content.
|
||||
typeName := "issues"
|
||||
if issue.IsPull {
|
||||
typeName = "pulls"
|
||||
if form.Status == "close" && !issue.IsClosed {
|
||||
if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil {
|
||||
log.Error("CloseIssue: %v", err)
|
||||
if issues_model.IsErrDependenciesLeft(err) {
|
||||
if issue.IsPull {
|
||||
ctx.Flash.Error(ctx.Tr("repo.issues.dependency.pr_close_blocked"))
|
||||
} else {
|
||||
ctx.Flash.Error(ctx.Tr("repo.issues.dependency.issue_close_blocked"))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := stopTimerIfAvailable(ctx, ctx.Doer, issue); err != nil {
|
||||
ctx.ServerError("stopTimerIfAvailable", err)
|
||||
return
|
||||
}
|
||||
log.Trace("Issue [%d] status changed to closed: %v", issue.ID, issue.IsClosed)
|
||||
}
|
||||
} else if form.Status == "reopen" && issue.IsClosed && branchOtherUnmergedPR == nil {
|
||||
if err := issue_service.ReopenIssue(ctx, issue, ctx.Doer, ""); err != nil {
|
||||
log.Error("ReopenIssue: %v", err)
|
||||
ctx.Flash.Error("Unable to reopen.")
|
||||
}
|
||||
}
|
||||
if comment != nil {
|
||||
ctx.JSONRedirect(fmt.Sprintf("%s/%s/%d#%s", ctx.Repo.RepoLink, typeName, issue.Index, comment.HashTag()))
|
||||
} else {
|
||||
ctx.JSONRedirect(fmt.Sprintf("%s/%s/%d", ctx.Repo.RepoLink, typeName, issue.Index))
|
||||
}
|
||||
}()
|
||||
} // end if: handle close or reopen
|
||||
|
||||
// Fix #321: Allow empty comments, as long as we have attachments.
|
||||
if len(form.Content) == 0 && len(attachments) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Content, attachments)
|
||||
if err != nil {
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.JSONError(ctx.Tr("repo.issues.comment.blocked_user"))
|
||||
} else {
|
||||
ctx.ServerError("CreateIssueComment", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace("Comment created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID)
|
||||
ctx.JSONRedirect(redirect)
|
||||
}
|
||||
|
||||
// UpdateCommentContent change comment of issue's content
|
||||
@@ -291,12 +264,8 @@ func UpdateCommentContent(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
if strings.TrimSpace(string(renderedContent)) == "" {
|
||||
renderedContent = htmlutil.HTMLFormat(`<span class="no-content">%s</span>`, ctx.Tr("repo.issues.no_content"))
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"content": renderedContent,
|
||||
"content": commentContentHTML(ctx, renderedContent),
|
||||
"contentVersion": comment.ContentVersion,
|
||||
"attachments": attachmentsHTML(ctx, comment.Attachments, comment.Content),
|
||||
})
|
||||
|
||||
@@ -6,13 +6,14 @@ package repo
|
||||
import (
|
||||
"bytes"
|
||||
"html"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/avatars"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/modules/htmlutil"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
|
||||
@@ -53,29 +54,25 @@ func GetContentHistoryList(ctx *context.Context) {
|
||||
// value is historyId
|
||||
var results []map[string]any
|
||||
for _, item := range items {
|
||||
var actionText string
|
||||
var actionHTML template.HTML
|
||||
if item.IsDeleted {
|
||||
actionTextDeleted := ctx.Locale.TrString("repo.issues.content_history.deleted")
|
||||
actionText = "<i data-history-is-deleted='1'>" + actionTextDeleted + "</i>"
|
||||
actionHTML = htmlutil.HTMLFormat(`<i data-history-is-deleted="1">%s</i>`, ctx.Locale.TrString("repo.issues.content_history.deleted"))
|
||||
} else if item.IsFirstCreated {
|
||||
actionText = ctx.Locale.TrString("repo.issues.content_history.created")
|
||||
actionHTML = ctx.Locale.Tr("repo.issues.content_history.created")
|
||||
} else {
|
||||
actionText = ctx.Locale.TrString("repo.issues.content_history.edited")
|
||||
actionHTML = ctx.Locale.Tr("repo.issues.content_history.edited")
|
||||
}
|
||||
|
||||
username := item.UserName
|
||||
if setting.UI.DefaultShowFullName && strings.TrimSpace(item.UserFullName) != "" {
|
||||
username = strings.TrimSpace(item.UserFullName)
|
||||
var fullNameHTML template.HTML
|
||||
userName, fullName := item.UserName, strings.TrimSpace(item.UserFullName)
|
||||
if fullName != "" {
|
||||
fullNameHTML = htmlutil.HTMLFormat(` (<span class="tw-inline-flex tw-max-w-[160px]"><span class="gt-ellipsis">%s</span></span>)`, fullName)
|
||||
}
|
||||
|
||||
src := html.EscapeString(item.UserAvatarLink)
|
||||
class := avatars.DefaultAvatarClass + " tw-mr-2"
|
||||
name := html.EscapeString(username)
|
||||
avatarHTML := string(templates.AvatarHTML(src, 28, class, username))
|
||||
timeSinceHTML := string(templates.TimeSince(item.EditedUnix))
|
||||
|
||||
avatarHTML := templates.AvatarHTML(item.UserAvatarLink, 24, avatars.DefaultAvatarClass+" tw-mr-2", userName)
|
||||
timeSinceHTML := templates.TimeSince(item.EditedUnix)
|
||||
results = append(results, map[string]any{
|
||||
"name": avatarHTML + "<strong>" + name + "</strong> " + actionText + " " + timeSinceHTML,
|
||||
"name": htmlutil.HTMLFormat("%s <strong>%s</strong>%s %s %s", avatarHTML, userName, fullNameHTML, actionHTML, timeSinceHTML),
|
||||
"value": item.HistoryID,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -55,9 +55,9 @@ func AddDependency(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
// Can ctx.Doer read issues in the dep repo?
|
||||
depRepoPerm, err := access_model.GetUserRepoPermission(ctx, dep.Repo, ctx.Doer)
|
||||
depRepoPerm, err := access_model.GetDoerRepoPermission(ctx, dep.Repo, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserRepoPermission", err)
|
||||
ctx.ServerError("GetDoerRepoPermission", err)
|
||||
return
|
||||
}
|
||||
if !depRepoPerm.CanReadIssuesOrPulls(dep.IsPull) {
|
||||
@@ -119,7 +119,7 @@ func RemoveDependency(ctx *context.Context) {
|
||||
case "blocking":
|
||||
depType = issues_model.DependencyTypeBlocking
|
||||
default:
|
||||
ctx.HTTPError(http.StatusBadRequest, "GetDependecyType")
|
||||
ctx.HTTPError(http.StatusBadRequest, "GetDependencyType")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/common"
|
||||
"code.gitea.io/gitea/routers/web/shared/issue"
|
||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
@@ -45,15 +46,7 @@ func SearchIssues(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var isClosed optional.Option[bool]
|
||||
switch ctx.FormString("state") {
|
||||
case "closed":
|
||||
isClosed = optional.Some(true)
|
||||
case "all":
|
||||
isClosed = optional.None[bool]()
|
||||
default:
|
||||
isClosed = optional.Some(false)
|
||||
}
|
||||
isClosed := common.ParseIssueFilterStateIsClosed(ctx.FormString("state"))
|
||||
|
||||
var (
|
||||
repoIDs []int64
|
||||
@@ -268,15 +261,7 @@ func SearchRepoIssuesJSON(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var isClosed optional.Option[bool]
|
||||
switch ctx.FormString("state") {
|
||||
case "closed":
|
||||
isClosed = optional.Some(true)
|
||||
case "all":
|
||||
isClosed = optional.None[bool]()
|
||||
default:
|
||||
isClosed = optional.Some(false)
|
||||
}
|
||||
isClosed := common.ParseIssueFilterStateIsClosed(ctx.FormString("state"))
|
||||
|
||||
keyword := ctx.FormTrim("q")
|
||||
if strings.IndexByte(keyword, 0) >= 0 {
|
||||
@@ -477,14 +462,7 @@ func renderMilestones(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
openMilestones, closedMilestones := issues_model.MilestoneList{}, issues_model.MilestoneList{}
|
||||
for _, milestone := range milestones {
|
||||
if milestone.IsClosed {
|
||||
closedMilestones = append(closedMilestones, milestone)
|
||||
} else {
|
||||
openMilestones = append(openMilestones, milestone)
|
||||
}
|
||||
}
|
||||
openMilestones, closedMilestones := issues_model.MilestoneList(milestones).SplitByOpenClosed()
|
||||
ctx.Data["OpenMilestones"] = openMilestones
|
||||
ctx.Data["ClosedMilestones"] = closedMilestones
|
||||
}
|
||||
@@ -580,17 +558,10 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID, projectID int6
|
||||
}
|
||||
}
|
||||
|
||||
var isShowClosed optional.Option[bool]
|
||||
switch ctx.FormString("state") {
|
||||
case "closed":
|
||||
isShowClosed = optional.Some(true)
|
||||
case "all":
|
||||
isShowClosed = optional.None[bool]()
|
||||
default:
|
||||
isShowClosed = optional.Some(false)
|
||||
}
|
||||
isShowClosed := common.ParseIssueFilterStateIsClosed(ctx.FormString("state"))
|
||||
|
||||
// if there are closed issues and no open issues, default to showing all issues
|
||||
if len(ctx.FormString("state")) == 0 && issueStats.OpenCount == 0 && issueStats.ClosedCount != 0 {
|
||||
if ctx.FormString("state") == "" && issueStats.OpenCount == 0 && issueStats.ClosedCount != 0 {
|
||||
isShowClosed = optional.None[bool]()
|
||||
}
|
||||
|
||||
@@ -604,9 +575,9 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID, projectID int6
|
||||
}
|
||||
|
||||
// prepare pager
|
||||
total := int(issueStats.OpenCount + issueStats.ClosedCount)
|
||||
total := issueStats.OpenCount + issueStats.ClosedCount
|
||||
if isShowClosed.Has() {
|
||||
total = util.Iif(isShowClosed.Value(), int(issueStats.ClosedCount), int(issueStats.OpenCount))
|
||||
total = util.Iif(isShowClosed.Value(), issueStats.ClosedCount, issueStats.OpenCount)
|
||||
}
|
||||
page := max(ctx.FormInt("page"), 1)
|
||||
pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5)
|
||||
@@ -691,10 +662,7 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID, projectID int6
|
||||
ctx.ServerError("GetRepoAssignees", err)
|
||||
return
|
||||
}
|
||||
handleMentionableAssigneesAndTeams(ctx, shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers))
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
|
||||
|
||||
ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] = issue_service.GetRefEndNamesAndURLs(issues, ctx.Repo.RepoLink)
|
||||
|
||||
|
||||
@@ -121,11 +121,9 @@ func NewIssue(ctx *context.Context) {
|
||||
}
|
||||
|
||||
pageMetaData.MilestonesData.SelectedMilestoneID = ctx.FormInt64("milestone")
|
||||
pageMetaData.ProjectsData.SelectedProjectID = ctx.FormInt64("project")
|
||||
if pageMetaData.ProjectsData.SelectedProjectID > 0 {
|
||||
if len(ctx.Req.URL.Query().Get("project")) > 0 {
|
||||
ctx.Data["redirect_after_creation"] = "project"
|
||||
}
|
||||
pageMetaData.ProjectsData.SelectedProjectIDs, _ = base.StringsToInt64s(strings.Split(ctx.FormString("project"), ","))
|
||||
if len(pageMetaData.ProjectsData.SelectedProjectIDs) == 1 {
|
||||
ctx.Data["redirect_after_creation"] = "project"
|
||||
}
|
||||
|
||||
tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
|
||||
@@ -273,7 +271,7 @@ func ValidateRepoMetasForNewIssue(ctx *context.Context, form forms.CreateIssueFo
|
||||
ctx.NotFound(nil)
|
||||
return ret
|
||||
}
|
||||
pageMetaData.ProjectsData.SelectedProjectID = form.ProjectID
|
||||
pageMetaData.ProjectsData.SelectedProjectIDs = util.Iif(form.ProjectID > 0, []int64{form.ProjectID}, nil)
|
||||
|
||||
// prepare assignees
|
||||
candidateAssignees := toSet(pageMetaData.AssigneesData.CandidateAssignees, func(user *user_model.User) int64 { return user.ID })
|
||||
|
||||
@@ -34,9 +34,14 @@ type issueSidebarAssigneesData struct {
|
||||
}
|
||||
|
||||
type issueSidebarProjectsData struct {
|
||||
SelectedProjectID int64
|
||||
OpenProjects []*project_model.Project
|
||||
ClosedProjects []*project_model.Project
|
||||
SelectedProjectIDs []int64 // TODO: support multiple projects in the future
|
||||
|
||||
// the "selected" fields are only valid when len(SelectedProjectIDs)==1
|
||||
SelectedProjectColumns []*project_model.Column
|
||||
SelectedProjectColumn *project_model.Column
|
||||
|
||||
OpenProjects []*project_model.Project
|
||||
ClosedProjects []*project_model.Project
|
||||
}
|
||||
|
||||
type IssuePageMetaData struct {
|
||||
@@ -92,6 +97,11 @@ func retrieveRepoIssueMetaData(ctx *context.Context, repo *repo_model.Repository
|
||||
return data
|
||||
}
|
||||
|
||||
data.retrieveProjectData(ctx)
|
||||
if ctx.Written() {
|
||||
return data
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -155,13 +165,36 @@ func (d *IssuePageMetaData) retrieveAssigneesData(ctx *context.Context) {
|
||||
}
|
||||
d.AssigneesData.SelectedAssigneeIDs = strings.Join(ids, ",")
|
||||
}
|
||||
// FIXME: this is a tricky part which writes ctx.Data["Mentionable*"]
|
||||
handleMentionableAssigneesAndTeams(ctx, d.AssigneesData.CandidateAssignees)
|
||||
ctx.Data["Assignees"] = d.AssigneesData.CandidateAssignees
|
||||
}
|
||||
|
||||
func (d *IssuePageMetaData) retrieveProjectData(ctx *context.Context) {
|
||||
if d.Issue == nil || d.Issue.Project == nil {
|
||||
return
|
||||
}
|
||||
d.ProjectsData.SelectedProjectIDs = []int64{d.Issue.Project.ID}
|
||||
columns, err := d.Issue.Project.GetColumns(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetProjectColumns", err)
|
||||
return
|
||||
}
|
||||
d.ProjectsData.SelectedProjectColumns = columns
|
||||
columnID, err := d.Issue.ProjectColumnID(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("ProjectColumnID", err)
|
||||
return
|
||||
}
|
||||
for _, col := range columns {
|
||||
if col.ID == columnID {
|
||||
d.ProjectsData.SelectedProjectColumn = col
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *IssuePageMetaData) retrieveProjectsDataForIssueWriter(ctx *context.Context) {
|
||||
if d.Issue != nil && d.Issue.Project != nil {
|
||||
d.ProjectsData.SelectedProjectID = d.Issue.Project.ID
|
||||
d.ProjectsData.SelectedProjectIDs = []int64{d.Issue.Project.ID}
|
||||
}
|
||||
d.ProjectsData.OpenProjects, d.ProjectsData.ClosedProjects = retrieveProjectsInternal(ctx, ctx.Repo.Repository)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
@@ -34,7 +33,7 @@ func IssuePullPosters(ctx *context.Context) {
|
||||
func issuePosters(ctx *context.Context, isPullList bool) {
|
||||
repo := ctx.Repo.Repository
|
||||
search := strings.TrimSpace(ctx.FormString("q"))
|
||||
posters, err := repo_model.GetIssuePostersWithSearch(ctx, repo, isPullList, search, setting.UI.DefaultShowFullName)
|
||||
posters, err := repo_model.GetIssuePostersWithSearch(ctx, repo, isPullList, search)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, err)
|
||||
return
|
||||
@@ -54,9 +53,7 @@ func issuePosters(ctx *context.Context, isPullList bool) {
|
||||
resp.Results = make([]*userSearchInfo, len(posters))
|
||||
for i, user := range posters {
|
||||
resp.Results[i] = &userSearchInfo{UserID: user.ID, UserName: user.Name, AvatarLink: user.AvatarLink(ctx)}
|
||||
if setting.UI.DefaultShowFullName {
|
||||
resp.Results[i].FullName = user.FullName
|
||||
}
|
||||
resp.Results[i].FullName = user.FullName
|
||||
}
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ func DeleteTime(c *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
t, err := issues_model.GetTrackedTimeByID(c, c.PathParamInt64("timeid"))
|
||||
t, err := issues_model.GetTrackedTimeByID(c, issue.ID, c.PathParamInt64("timeid"))
|
||||
if err != nil {
|
||||
if db.IsErrNotExist(err) {
|
||||
c.NotFound(err)
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
@@ -26,7 +25,6 @@ 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"
|
||||
@@ -34,6 +32,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/templates/vars"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/context/upload"
|
||||
@@ -58,7 +57,7 @@ func roleDescriptor(ctx *context.Context, repo *repo_model.Repository, poster *u
|
||||
// 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)
|
||||
perm, err = access_model.GetIndividualUserRepoPermission(ctx, repo, poster)
|
||||
if err != nil {
|
||||
return roleDesc, err
|
||||
}
|
||||
@@ -146,9 +145,9 @@ func checkBlockedByIssues(ctx *context.Context, blockers []*issues_model.Depende
|
||||
perm = existPerm
|
||||
} else {
|
||||
var err error
|
||||
perm, err = access_model.GetUserRepoPermission(ctx, &blocker.Repository, ctx.Doer)
|
||||
perm, err = access_model.GetDoerRepoPermission(ctx, &blocker.Repository, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserRepoPermission", err)
|
||||
ctx.ServerError("GetDoerRepoPermission", err)
|
||||
return nil, nil
|
||||
}
|
||||
repoPerms[blocker.RepoID] = perm
|
||||
@@ -193,7 +192,7 @@ func filterXRefComments(ctx *context.Context, issue *issues_model.Issue) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
perm, err := access_model.GetUserRepoPermission(ctx, c.RefRepo, ctx.Doer)
|
||||
perm, err := access_model.GetDoerRepoPermission(ctx, c.RefRepo, ctx.Doer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -409,7 +408,7 @@ func ViewIssue(ctx *context.Context) {
|
||||
}
|
||||
|
||||
ctx.Data["Reference"] = issue.Ref
|
||||
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login?redirect_to=" + url.QueryEscape(ctx.Data["Link"].(string))
|
||||
ctx.Data["SignInLink"] = middleware.RedirectLinkUserLogin(ctx.Req)
|
||||
ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.Doer.ID)
|
||||
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
|
||||
ctx.Data["HasProjectsWritePermission"] = ctx.Repo.CanWrite(unit.TypeProjects)
|
||||
@@ -437,6 +436,9 @@ func ViewIssue(ctx *context.Context) {
|
||||
|
||||
func ViewPullMergeBox(ctx *context.Context) {
|
||||
issue := prepareIssueViewLoad(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
if !issue.IsPull {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
@@ -467,7 +469,7 @@ func prepareIssueViewSidebarDependency(ctx *context.Context, issue *issues_model
|
||||
ctx.Data["AllowCrossRepositoryDependencies"] = setting.Service.AllowCrossRepositoryDependencies
|
||||
|
||||
// Get Dependencies
|
||||
blockedBy, err := issue.BlockedByDependencies(ctx, db.ListOptions{})
|
||||
blockedBy, _, err := issue.BlockedByDependencies(ctx, db.ListOptions{})
|
||||
if err != nil {
|
||||
ctx.ServerError("BlockedByDependencies", err)
|
||||
return
|
||||
@@ -493,7 +495,7 @@ func preparePullViewSigning(ctx *context.Context, issue *issues_model.Issue) {
|
||||
pull := issue.PullRequest
|
||||
ctx.Data["WillSign"] = false
|
||||
if ctx.Doer != nil {
|
||||
sign, key, _, err := asymkey_service.SignMerge(ctx, pull, ctx.Doer, pull.BaseRepo.RepoPath(), pull.BaseBranch, pull.GetGitHeadRefName())
|
||||
sign, key, _, err := asymkey_service.SignMerge(ctx, pull, ctx.Doer, ctx.Repo.GitRepo)
|
||||
ctx.Data["WillSign"] = sign
|
||||
ctx.Data["SigningKeyMergeDisplay"] = asymkey_model.GetDisplaySigningKey(key)
|
||||
if err != nil {
|
||||
@@ -563,8 +565,10 @@ func preparePullViewDeleteBranch(ctx *context.Context, issue *issues_model.Issue
|
||||
pull := issue.PullRequest
|
||||
isPullBranchDeletable := canDelete &&
|
||||
pull.HeadRepo != nil &&
|
||||
gitrepo.IsBranchExist(ctx, pull.HeadRepo, pull.HeadBranch) &&
|
||||
(!pull.HasMerged || ctx.Data["HeadBranchCommitID"] == ctx.Data["PullHeadCommitID"])
|
||||
if isPullBranchDeletable {
|
||||
isPullBranchDeletable, _ = git_model.IsBranchExist(ctx, pull.HeadRepo.ID, pull.HeadBranch)
|
||||
}
|
||||
|
||||
if isPullBranchDeletable && pull.HasMerged {
|
||||
exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pull.HeadRepoID, pull.HeadBranch)
|
||||
@@ -841,9 +845,9 @@ func preparePullViewReviewAndMerge(ctx *context.Context, issue *issues_model.Iss
|
||||
if err := pull.LoadHeadRepo(ctx); err != nil {
|
||||
log.Error("LoadHeadRepo: %v", err)
|
||||
} else if pull.HeadRepo != nil {
|
||||
perm, err := access_model.GetUserRepoPermission(ctx, pull.HeadRepo, ctx.Doer)
|
||||
perm, err := access_model.GetDoerRepoPermission(ctx, pull.HeadRepo, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserRepoPermission", err)
|
||||
ctx.ServerError("GetDoerRepoPermission", err)
|
||||
return
|
||||
}
|
||||
if perm.CanWrite(unit.TypeCode) {
|
||||
@@ -863,9 +867,9 @@ func preparePullViewReviewAndMerge(ctx *context.Context, issue *issues_model.Iss
|
||||
if err := pull.LoadBaseRepo(ctx); err != nil {
|
||||
log.Error("LoadBaseRepo: %v", err)
|
||||
}
|
||||
perm, err := access_model.GetUserRepoPermission(ctx, pull.BaseRepo, ctx.Doer)
|
||||
perm, err := access_model.GetDoerRepoPermission(ctx, pull.BaseRepo, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserRepoPermission", err)
|
||||
ctx.ServerError("GetDoerRepoPermission", err)
|
||||
return
|
||||
}
|
||||
if !canWriteToHeadRepo { // maintainers maybe allowed to push to head repo even if they can't write to it
|
||||
@@ -901,9 +905,8 @@ func preparePullViewReviewAndMerge(ctx *context.Context, issue *issues_model.Iss
|
||||
// Check correct values and select default
|
||||
if ms, ok := ctx.Data["MergeStyle"].(repo_model.MergeStyle); !ok ||
|
||||
!prConfig.IsMergeStyleAllowed(ms) {
|
||||
defaultMergeStyle := prConfig.GetDefaultMergeStyle()
|
||||
if prConfig.IsMergeStyleAllowed(defaultMergeStyle) && !ok {
|
||||
mergeStyle = defaultMergeStyle
|
||||
if prConfig.IsMergeStyleAllowed(prConfig.DefaultMergeStyle) && !ok {
|
||||
mergeStyle = prConfig.DefaultMergeStyle
|
||||
} else if prConfig.AllowMerge {
|
||||
mergeStyle = repo_model.MergeStyleMerge
|
||||
} else if prConfig.AllowRebase {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
shared_mention "code.gitea.io/gitea/routers/web/shared/mention"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// GetMentionsInRepo returns JSON data for mention autocomplete (assignees, participants, mentionable teams).
|
||||
func GetMentionsInRepo(ctx *context.Context) {
|
||||
c := shared_mention.NewCollector()
|
||||
|
||||
// Get participants if issue_index is provided
|
||||
if issueIndex := ctx.FormInt64("issue_index"); issueIndex > 0 {
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, issueIndex)
|
||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||
ctx.ServerError("GetIssueByIndex", err)
|
||||
return
|
||||
}
|
||||
if issue != nil {
|
||||
userIDs, err := issue.GetParticipantIDsByIssue(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetParticipantIDsByIssue", err)
|
||||
return
|
||||
}
|
||||
users, err := user_model.GetUsersByIDs(ctx, userIDs)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUsersByIDs", err)
|
||||
return
|
||||
}
|
||||
c.AddUsers(ctx, users)
|
||||
}
|
||||
}
|
||||
|
||||
// Get repo assignees
|
||||
assignees, err := repo_model.GetRepoAssignees(ctx, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetRepoAssignees", err)
|
||||
return
|
||||
}
|
||||
c.AddUsers(ctx, assignees)
|
||||
|
||||
// Get mentionable teams for org repos
|
||||
if err := c.AddMentionableTeams(ctx, ctx.Doer, ctx.Repo.Owner); err != nil {
|
||||
ctx.ServerError("AddMentionableTeams", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, util.SliceNilAsEmpty(c.Result))
|
||||
}
|
||||
@@ -7,8 +7,11 @@ import (
|
||||
"strconv"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/gitdiff"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
)
|
||||
|
||||
@@ -28,36 +31,24 @@ func SetEditorconfigIfExists(ctx *context.Context) {
|
||||
ctx.Data["Editorconfig"] = ec
|
||||
}
|
||||
|
||||
func GetDiffViewStyle(ctx *context.Context) string {
|
||||
return util.Iif(ctx.Data["IsSplitStyle"] == true, gitdiff.DiffStyleSplit, gitdiff.DiffStyleUnified)
|
||||
}
|
||||
|
||||
// SetDiffViewStyle set diff style as render variable
|
||||
func SetDiffViewStyle(ctx *context.Context) {
|
||||
queryStyle := ctx.FormString("style")
|
||||
|
||||
if !ctx.IsSigned {
|
||||
ctx.Data["IsSplitStyle"] = queryStyle == "split"
|
||||
return
|
||||
style := ctx.FormString("style")
|
||||
if ctx.IsSigned {
|
||||
style = util.IfZero(style, ctx.Doer.DiffViewStyle)
|
||||
style = util.Iif(style == gitdiff.DiffStyleSplit, gitdiff.DiffStyleSplit, gitdiff.DiffStyleUnified)
|
||||
if style != ctx.Doer.DiffViewStyle {
|
||||
err := user_service.UpdateUser(ctx, ctx.Doer, &user_service.UpdateOptions{DiffViewStyle: optional.Some(style)})
|
||||
if err != nil {
|
||||
log.Error("UpdateUser DiffViewStyle: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
userStyle = ctx.Doer.DiffViewStyle
|
||||
style string
|
||||
)
|
||||
|
||||
if queryStyle == "unified" || queryStyle == "split" {
|
||||
style = queryStyle
|
||||
} else if userStyle == "unified" || userStyle == "split" {
|
||||
style = userStyle
|
||||
} else {
|
||||
style = "unified"
|
||||
}
|
||||
|
||||
ctx.Data["IsSplitStyle"] = style == "split"
|
||||
|
||||
opts := &user_service.UpdateOptions{
|
||||
DiffViewStyle: optional.Some(style),
|
||||
}
|
||||
if err := user_service.UpdateUser(ctx, ctx.Doer, opts); err != nil {
|
||||
ctx.ServerError("UpdateUser", err)
|
||||
}
|
||||
}
|
||||
|
||||
// SetWhitespaceBehavior set whitespace behavior as render variable
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/services/contexttest"
|
||||
"code.gitea.io/gitea/services/gitdiff"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDiffViewStyle(t *testing.T) {
|
||||
unittest.PrepareTestEnv(t)
|
||||
|
||||
t.Run("AnonymousUser", func(t *testing.T) {
|
||||
ctx, _ := contexttest.MockContext(t, "/any")
|
||||
SetDiffViewStyle(ctx)
|
||||
assert.Equal(t, gitdiff.DiffStyleUnified, GetDiffViewStyle(ctx))
|
||||
|
||||
ctx, _ = contexttest.MockContext(t, "/any?style=split")
|
||||
SetDiffViewStyle(ctx)
|
||||
assert.Equal(t, gitdiff.DiffStyleSplit, GetDiffViewStyle(ctx))
|
||||
|
||||
ctx, _ = contexttest.MockContext(t, "/any")
|
||||
SetDiffViewStyle(ctx)
|
||||
assert.Equal(t, gitdiff.DiffStyleUnified, GetDiffViewStyle(ctx)) // at the moment, anonymous users don't have a saved preference
|
||||
})
|
||||
|
||||
t.Run("SignedInUser", func(t *testing.T) {
|
||||
ctx, _ := contexttest.MockContext(t, "/any")
|
||||
contexttest.LoadUser(t, ctx, 2)
|
||||
SetDiffViewStyle(ctx)
|
||||
assert.Equal(t, gitdiff.DiffStyleUnified, GetDiffViewStyle(ctx))
|
||||
|
||||
ctx, _ = contexttest.MockContext(t, "/any?style=split")
|
||||
contexttest.LoadUser(t, ctx, 2)
|
||||
SetDiffViewStyle(ctx)
|
||||
assert.Equal(t, gitdiff.DiffStyleSplit, GetDiffViewStyle(ctx))
|
||||
|
||||
ctx, _ = contexttest.MockContext(t, "/any")
|
||||
contexttest.LoadUser(t, ctx, 2)
|
||||
SetDiffViewStyle(ctx)
|
||||
assert.Equal(t, gitdiff.DiffStyleSplit, GetDiffViewStyle(ctx))
|
||||
|
||||
ctx, _ = contexttest.MockContext(t, "/any?style=unified")
|
||||
contexttest.LoadUser(t, ctx, 2)
|
||||
SetDiffViewStyle(ctx)
|
||||
assert.Equal(t, gitdiff.DiffStyleUnified, GetDiffViewStyle(ctx))
|
||||
|
||||
ctx, _ = contexttest.MockContext(t, "/any")
|
||||
contexttest.LoadUser(t, ctx, 2)
|
||||
SetDiffViewStyle(ctx)
|
||||
assert.Equal(t, gitdiff.DiffStyleUnified, GetDiffViewStyle(ctx))
|
||||
})
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
"code.gitea.io/gitea/services/migrations"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
"code.gitea.io/gitea/services/task"
|
||||
)
|
||||
|
||||
@@ -78,44 +79,44 @@ func handleMigrateError(ctx *context.Context, owner *user_model.User, err error,
|
||||
|
||||
switch {
|
||||
case migrations.IsRateLimitError(err):
|
||||
ctx.RenderWithErr(ctx.Tr("form.visit_rate_limit"), tpl, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.visit_rate_limit"), tpl, form)
|
||||
case migrations.IsTwoFactorAuthError(err):
|
||||
ctx.RenderWithErr(ctx.Tr("form.2fa_auth_required"), tpl, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.2fa_auth_required"), tpl, form)
|
||||
case repo_model.IsErrReachLimitOfRepo(err):
|
||||
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, tpl, form)
|
||||
ctx.RenderWithErrDeprecated(msg, tpl, form)
|
||||
case repo_model.IsErrRepoAlreadyExist(err):
|
||||
ctx.Data["Err_RepoName"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tpl, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.repo_name_been_taken"), tpl, 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"), tpl, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tpl, form)
|
||||
case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
|
||||
ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form)
|
||||
case setting.Repository.AllowDeleteOfUnadoptedRepositories:
|
||||
ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form)
|
||||
default:
|
||||
ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tpl, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist"), tpl, form)
|
||||
}
|
||||
case db.IsErrNameReserved(err):
|
||||
ctx.Data["Err_RepoName"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, form)
|
||||
case db.IsErrNamePatternNotAllowed(err):
|
||||
ctx.Data["Err_RepoName"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, form)
|
||||
default:
|
||||
err = util.SanitizeErrorCredentialURLs(err)
|
||||
if strings.Contains(err.Error(), "Authentication failed") ||
|
||||
strings.Contains(err.Error(), "Bad credentials") ||
|
||||
strings.Contains(err.Error(), "could not read Username") {
|
||||
ctx.Data["Err_Auth"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("form.auth_failed", err.Error()), tpl, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.auth_failed", err.Error()), tpl, form)
|
||||
} else if strings.Contains(err.Error(), "fatal:") {
|
||||
ctx.Data["Err_CloneAddr"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("repo.migrate.failed", err.Error()), tpl, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.failed", err.Error()), tpl, form)
|
||||
} else {
|
||||
ctx.ServerError(name, err)
|
||||
}
|
||||
@@ -127,24 +128,24 @@ func handleMigrateRemoteAddrError(ctx *context.Context, err error, tpl templates
|
||||
addrErr := err.(*git.ErrInvalidCloneAddr)
|
||||
switch {
|
||||
case addrErr.IsProtocolInvalid:
|
||||
ctx.RenderWithErr(ctx.Tr("repo.mirror_address_protocol_invalid"), tpl, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.mirror_address_protocol_invalid"), tpl, form)
|
||||
case addrErr.IsURLError:
|
||||
ctx.RenderWithErr(ctx.Tr("form.url_error", addrErr.Host), tpl, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.url_error", addrErr.Host), tpl, form)
|
||||
case addrErr.IsPermissionDenied:
|
||||
if addrErr.LocalPath {
|
||||
ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied"), tpl, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.permission_denied"), tpl, form)
|
||||
} else {
|
||||
ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied_blocked"), tpl, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.permission_denied_blocked"), tpl, form)
|
||||
}
|
||||
case addrErr.IsInvalidPath:
|
||||
ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_local_path"), tpl, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.invalid_local_path"), tpl, form)
|
||||
default:
|
||||
log.Error("Error whilst updating url: %v", err)
|
||||
ctx.RenderWithErr(ctx.Tr("form.url_error", "unknown"), tpl, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.url_error", "unknown"), tpl, form)
|
||||
}
|
||||
} else {
|
||||
log.Error("Error whilst updating url: %v", err)
|
||||
ctx.RenderWithErr(ctx.Tr("form.url_error", "unknown"), tpl, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.url_error", "unknown"), tpl, form)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +193,7 @@ func MigratePost(ctx *context.Context) {
|
||||
ep := lfs.DetermineEndpoint("", form.LFSEndpoint)
|
||||
if ep == nil {
|
||||
ctx.Data["Err_LFSEndpoint"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_lfs_endpoint"), tpl, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.invalid_lfs_endpoint"), tpl, &form)
|
||||
return
|
||||
}
|
||||
err = migrations.IsMigrateURLAllowed(ep.String(), ctx.Doer)
|
||||
@@ -237,7 +238,7 @@ func MigratePost(ctx *context.Context) {
|
||||
opts.AWSSecretAccessKey = form.AWSSecretAccessKey
|
||||
}
|
||||
|
||||
err = repo_model.CheckCreateRepository(ctx, ctx.Doer, ctxUser, opts.RepoName, false)
|
||||
err = repo_service.CheckCreateRepository(ctx, ctx.Doer, ctxUser, opts.RepoName, false)
|
||||
if err != nil {
|
||||
handleMigrateError(ctx, ctxUser, err, "MigratePost", tpl, form)
|
||||
return
|
||||
|
||||
@@ -88,7 +88,7 @@ func Milestones(ctx *context.Context) {
|
||||
ctx.Data["Keyword"] = keyword
|
||||
ctx.Data["IsShowClosed"] = isShowClosed
|
||||
|
||||
pager := context.NewPagination(int(total), setting.UI.IssuePagingNum, page, 5)
|
||||
pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5)
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
@@ -118,7 +118,7 @@ func NewMilestonePost(ctx *context.Context) {
|
||||
deadlineUnix, err := common.ParseDeadlineDateToEndOfDay(form.Deadline)
|
||||
if err != nil {
|
||||
ctx.Data["Err_Deadline"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ func EditMilestonePost(ctx *context.Context) {
|
||||
deadlineUnix, err := common.ParseDeadlineDateToEndOfDay(form.Deadline)
|
||||
if err != nil {
|
||||
ctx.Data["Err_Deadline"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.milestones.invalid_due_date_format"), tplMilestoneNew, &form)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ func Packages(ctx *context.Context) {
|
||||
ctx.Data["Total"] = total
|
||||
ctx.Data["RepositoryAccessMap"] = map[int64]bool{ctx.Repo.Repository.ID: true} // There is only the current repository
|
||||
|
||||
pager := context.NewPagination(int(total), setting.UI.PackagesPagingNum, page, 5)
|
||||
pager := context.NewPagination(total, setting.UI.PackagesPagingNum, page, 5)
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
|
||||
@@ -66,13 +66,6 @@ func Projects(ctx *context.Context) {
|
||||
ctx.Data["OpenCount"] = repo.NumOpenProjects
|
||||
ctx.Data["ClosedCount"] = repo.NumClosedProjects
|
||||
|
||||
var total int
|
||||
if !isShowClosed {
|
||||
total = repo.NumOpenProjects
|
||||
} else {
|
||||
total = repo.NumClosedProjects
|
||||
}
|
||||
|
||||
projects, count, err := db.FindAndCount[project_model.Project](ctx, project_model.SearchOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
PageSize: setting.UI.IssuePagingNum,
|
||||
@@ -111,12 +104,7 @@ func Projects(ctx *context.Context) {
|
||||
ctx.Data["State"] = "open"
|
||||
}
|
||||
|
||||
numPages := 0
|
||||
if count > 0 {
|
||||
numPages = (int(count) - 1/setting.UI.IssuePagingNum)
|
||||
}
|
||||
|
||||
pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, numPages)
|
||||
pager := context.NewPagination(count, setting.UI.IssuePagingNum, page, 5)
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
@@ -311,13 +299,25 @@ func ViewProject(ctx *context.Context) {
|
||||
}
|
||||
|
||||
preparedLabelFilter := issue.PrepareFilterIssueLabels(ctx, ctx.Repo.Repository.ID, ctx.Repo.Owner)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
assigneeID := ctx.FormString("assignee")
|
||||
milestoneID := ctx.FormInt64("milestone")
|
||||
|
||||
var milestoneIDs []int64
|
||||
if milestoneID > 0 {
|
||||
milestoneIDs = []int64{milestoneID}
|
||||
} else if milestoneID == db.NoConditionID {
|
||||
milestoneIDs = []int64{db.NoConditionID}
|
||||
}
|
||||
|
||||
issuesMap, err := project_service.LoadIssuesFromProject(ctx, project, &issues_model.IssuesOptions{
|
||||
RepoIDs: []int64{ctx.Repo.Repository.ID},
|
||||
LabelIDs: preparedLabelFilter.SelectedLabelIDs,
|
||||
AssigneeID: assigneeID,
|
||||
RepoIDs: []int64{ctx.Repo.Repository.ID},
|
||||
LabelIDs: preparedLabelFilter.SelectedLabelIDs,
|
||||
AssigneeID: assigneeID,
|
||||
MilestoneIDs: milestoneIDs,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("LoadIssuesOfColumns", err)
|
||||
@@ -408,6 +408,12 @@ func ViewProject(ctx *context.Context) {
|
||||
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
|
||||
ctx.Data["AssigneeID"] = assigneeID
|
||||
|
||||
renderMilestones(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
ctx.Data["MilestoneID"] = milestoneID
|
||||
|
||||
rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository)
|
||||
project.RenderedContent, err = markdown.RenderString(rctx, project.Description)
|
||||
if err != nil {
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/commitstatus"
|
||||
"code.gitea.io/gitea/modules/emoji"
|
||||
"code.gitea.io/gitea/modules/fileicon"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
@@ -35,15 +36,18 @@ import (
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/utils"
|
||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||
actions_service "code.gitea.io/gitea/services/actions"
|
||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||
"code.gitea.io/gitea/services/automerge"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/context/upload"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
git_service "code.gitea.io/gitea/services/git"
|
||||
"code.gitea.io/gitea/services/gitdiff"
|
||||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
pull_service "code.gitea.io/gitea/services/pull"
|
||||
@@ -91,9 +95,9 @@ func getRepository(ctx *context.Context, repoID int64) *repo_model.Repository {
|
||||
return nil
|
||||
}
|
||||
|
||||
perm, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
|
||||
perm, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserRepoPermission", err)
|
||||
ctx.ServerError("GetDoerRepoPermission", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -202,7 +206,7 @@ func GetPullDiffStats(ctx *context.Context) {
|
||||
log.Error("Failed to GetRefCommitID: %v, repo: %v", err, ctx.Repo.Repository.FullName())
|
||||
return
|
||||
}
|
||||
diffShortStat, err := gitdiff.GetDiffShortStat(ctx.Repo.GitRepo, mergeBaseCommitID, headCommitID)
|
||||
diffShortStat, err := gitdiff.GetDiffShortStat(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, mergeBaseCommitID, headCommitID)
|
||||
if err != nil {
|
||||
log.Error("Failed to GetDiffShortStat: %v, repo: %v", err, ctx.Repo.Repository.FullName())
|
||||
return
|
||||
@@ -235,7 +239,8 @@ 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 = gitcmd.NewCommand("rev-list", "-1", "--skip=1").AddDynamicArguments(commitSHA).RunStdString(ctx, &gitcmd.RunOpts{Dir: ctx.Repo.GitRepo.Path})
|
||||
parentCommit, _, err = gitrepo.RunCmdString(ctx, ctx.Repo.Repository,
|
||||
gitcmd.NewCommand("rev-list", "-1", "--skip=1").AddDynamicArguments(commitSHA))
|
||||
if err == nil {
|
||||
parentCommit = strings.TrimSpace(parentCommit)
|
||||
}
|
||||
@@ -255,7 +260,7 @@ func GetMergedBaseCommitID(ctx *context.Context, issue *issues_model.Issue) stri
|
||||
return baseCommit
|
||||
}
|
||||
|
||||
func preparePullViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_service.CompareInfo {
|
||||
func preparePullViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git_service.CompareInfo {
|
||||
if !issue.IsPull {
|
||||
return nil
|
||||
}
|
||||
@@ -266,7 +271,7 @@ func preparePullViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *p
|
||||
}
|
||||
|
||||
// prepareMergedViewPullInfo show meta information for a merged pull request view page
|
||||
func prepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_service.CompareInfo {
|
||||
func prepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git_service.CompareInfo {
|
||||
pull := issue.PullRequest
|
||||
|
||||
setMergeTarget(ctx, pull)
|
||||
@@ -274,10 +279,10 @@ func prepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue)
|
||||
|
||||
baseCommit := GetMergedBaseCommitID(ctx, issue)
|
||||
|
||||
compareInfo, err := pull_service.GetCompareInfo(ctx, ctx.Repo.Repository, ctx.Repo.Repository, ctx.Repo.GitRepo,
|
||||
baseCommit, pull.GetGitHeadRefName(), false, false)
|
||||
compareInfo, err := git_service.GetCompareInfo(ctx, ctx.Repo.Repository, ctx.Repo.Repository, ctx.Repo.GitRepo,
|
||||
git.RefName(baseCommit), git.RefName(pull.GetGitHeadRefName()), false, false)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "fatal: Not a valid object name") || strings.Contains(err.Error(), "unknown revision or path not in the working tree") {
|
||||
if gitcmd.IsStdErrorNotValidObjectName(err) || strings.Contains(err.Error(), "unknown revision or path not in the working tree") {
|
||||
ctx.Data["IsPullRequestBroken"] = true
|
||||
ctx.Data["BaseTarget"] = pull.BaseBranch
|
||||
ctx.Data["NumCommits"] = 0
|
||||
@@ -311,8 +316,65 @@ func prepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue)
|
||||
return compareInfo
|
||||
}
|
||||
|
||||
type pullCommitStatusCheckData struct {
|
||||
MissingRequiredChecks []string // list of missing required checks
|
||||
IsContextRequired func(string) bool // function to check whether a context is required
|
||||
RequireApprovalRunCount int // number of workflow runs that require approval
|
||||
CanApprove bool // whether the user can approve workflow runs
|
||||
ApproveLink string // link to approve all checks
|
||||
RequiredChecksState commitstatus.CommitStatusState
|
||||
LatestCommitStatus *git_model.CommitStatus
|
||||
}
|
||||
|
||||
func (d *pullCommitStatusCheckData) CommitStatusCheckPrompt(locale translation.Locale) string {
|
||||
if d.RequiredChecksState.IsPending() || len(d.MissingRequiredChecks) > 0 {
|
||||
return locale.TrString("repo.pulls.status_checking")
|
||||
} else if d.RequiredChecksState.IsSuccess() {
|
||||
if d.LatestCommitStatus != nil && d.LatestCommitStatus.State.IsFailure() {
|
||||
return locale.TrString("repo.pulls.status_checks_failure_optional")
|
||||
}
|
||||
return locale.TrString("repo.pulls.status_checks_success")
|
||||
} else if d.RequiredChecksState.IsWarning() {
|
||||
return locale.TrString("repo.pulls.status_checks_warning")
|
||||
} else if d.RequiredChecksState.IsFailure() {
|
||||
return locale.TrString("repo.pulls.status_checks_failure_required")
|
||||
} else if d.RequiredChecksState.IsError() {
|
||||
return locale.TrString("repo.pulls.status_checks_error")
|
||||
}
|
||||
return locale.TrString("repo.pulls.status_checking")
|
||||
}
|
||||
|
||||
func getViewPullHeadBranchInfo(ctx *context.Context, pull *issues_model.PullRequest, baseGitRepo *git.Repository) (headCommitID string, headCommitExists bool, err error) {
|
||||
if pull.HeadRepo == nil {
|
||||
return "", false, nil
|
||||
}
|
||||
headGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pull.HeadRepo)
|
||||
if err != nil {
|
||||
return "", false, util.Iif(errors.Is(err, util.ErrNotExist), nil, err)
|
||||
}
|
||||
defer closer.Close()
|
||||
|
||||
if pull.Flow == issues_model.PullRequestFlowGithub {
|
||||
headCommitExists, _ = git_model.IsBranchExist(ctx, pull.HeadRepo.ID, pull.HeadBranch)
|
||||
} else {
|
||||
headCommitExists = gitrepo.IsReferenceExist(ctx, pull.BaseRepo, pull.GetGitHeadRefName())
|
||||
}
|
||||
|
||||
if headCommitExists {
|
||||
if pull.Flow != issues_model.PullRequestFlowGithub {
|
||||
headCommitID, err = baseGitRepo.GetRefCommitID(pull.GetGitHeadRefName())
|
||||
} else {
|
||||
headCommitID, err = headGitRepo.GetBranchCommitID(pull.HeadBranch)
|
||||
}
|
||||
if err != nil {
|
||||
return "", false, util.Iif(errors.Is(err, util.ErrNotExist), nil, err)
|
||||
}
|
||||
}
|
||||
return headCommitID, headCommitExists, nil
|
||||
}
|
||||
|
||||
// prepareViewPullInfo show meta information for a pull request preview page
|
||||
func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_service.CompareInfo {
|
||||
func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git_service.CompareInfo {
|
||||
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
|
||||
|
||||
repo := ctx.Repo.Repository
|
||||
@@ -349,7 +411,9 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_
|
||||
defer baseGitRepo.Close()
|
||||
}
|
||||
|
||||
if !gitrepo.IsBranchExist(ctx, pull.BaseRepo, pull.BaseBranch) {
|
||||
statusCheckData := &pullCommitStatusCheckData{}
|
||||
|
||||
if exist, _ := git_model.IsBranchExist(ctx, pull.BaseRepo.ID, pull.BaseBranch); !exist {
|
||||
ctx.Data["BaseBranchNotExist"] = true
|
||||
ctx.Data["IsPullRequestBroken"] = true
|
||||
ctx.Data["BaseTarget"] = pull.BaseBranch
|
||||
@@ -369,15 +433,16 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_
|
||||
git_model.CommitStatusesHideActionsURL(ctx, commitStatuses)
|
||||
}
|
||||
|
||||
statusCheckData.LatestCommitStatus = git_model.CalcCommitStatus(commitStatuses)
|
||||
if len(commitStatuses) > 0 {
|
||||
ctx.Data["LatestCommitStatuses"] = commitStatuses
|
||||
ctx.Data["LatestCommitStatus"] = git_model.CalcCommitStatus(commitStatuses)
|
||||
ctx.Data["LatestCommitStatus"] = statusCheckData.LatestCommitStatus
|
||||
}
|
||||
|
||||
compareInfo, err := pull_service.GetCompareInfo(ctx, pull.BaseRepo, pull.BaseRepo, baseGitRepo,
|
||||
pull.MergeBase, pull.GetGitHeadRefName(), false, false)
|
||||
compareInfo, err := git_service.GetCompareInfo(ctx, pull.BaseRepo, pull.BaseRepo, baseGitRepo,
|
||||
git.RefName(pull.MergeBase), git.RefName(pull.GetGitHeadRefName()), false, false)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "fatal: Not a valid object name") {
|
||||
if gitcmd.IsStdErrorNotValidObjectName(err) {
|
||||
ctx.Data["IsPullRequestBroken"] = true
|
||||
ctx.Data["BaseTarget"] = pull.BaseBranch
|
||||
ctx.Data["NumCommits"] = 0
|
||||
@@ -394,34 +459,10 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_
|
||||
return compareInfo
|
||||
}
|
||||
|
||||
var headBranchExist bool
|
||||
var headBranchSha string
|
||||
// HeadRepo may be missing
|
||||
if pull.HeadRepo != nil {
|
||||
headGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, pull.HeadRepo)
|
||||
if err != nil {
|
||||
ctx.ServerError("RepositoryFromContextOrOpen", err)
|
||||
return nil
|
||||
}
|
||||
defer closer.Close()
|
||||
|
||||
if pull.Flow == issues_model.PullRequestFlowGithub {
|
||||
headBranchExist = gitrepo.IsBranchExist(ctx, pull.HeadRepo, pull.HeadBranch)
|
||||
} else {
|
||||
headBranchExist = gitrepo.IsReferenceExist(ctx, pull.BaseRepo, pull.GetGitHeadRefName())
|
||||
}
|
||||
|
||||
if headBranchExist {
|
||||
if pull.Flow != issues_model.PullRequestFlowGithub {
|
||||
headBranchSha, err = baseGitRepo.GetRefCommitID(pull.GetGitHeadRefName())
|
||||
} else {
|
||||
headBranchSha, err = headGitRepo.GetBranchCommitID(pull.HeadBranch)
|
||||
}
|
||||
if err != nil {
|
||||
ctx.ServerError("GetBranchCommitID", err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
headBranchSha, headBranchExist, err := getViewPullHeadBranchInfo(ctx, pull, baseGitRepo)
|
||||
if err != nil {
|
||||
ctx.ServerError("getViewPullHeadBranchInfo", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if headBranchExist {
|
||||
@@ -456,6 +497,9 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx.Data["StatusCheckData"] = statusCheckData
|
||||
statusCheckData.ApproveLink = fmt.Sprintf("%s/actions/approve-all-checks?commit_id=%s", repo.Link(), sha)
|
||||
|
||||
commitStatuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLatestCommitStatus", err)
|
||||
@@ -465,9 +509,24 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_
|
||||
git_model.CommitStatusesHideActionsURL(ctx, commitStatuses)
|
||||
}
|
||||
|
||||
runs, err := actions_service.GetRunsFromCommitStatuses(ctx, commitStatuses)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetRunsFromCommitStatuses", err)
|
||||
return nil
|
||||
}
|
||||
for _, run := range runs {
|
||||
if run.NeedApproval {
|
||||
statusCheckData.RequireApprovalRunCount++
|
||||
}
|
||||
}
|
||||
if statusCheckData.RequireApprovalRunCount > 0 {
|
||||
statusCheckData.CanApprove = ctx.Repo.CanWrite(unit.TypeActions)
|
||||
}
|
||||
|
||||
statusCheckData.LatestCommitStatus = git_model.CalcCommitStatus(commitStatuses)
|
||||
if len(commitStatuses) > 0 {
|
||||
ctx.Data["LatestCommitStatuses"] = commitStatuses
|
||||
ctx.Data["LatestCommitStatus"] = git_model.CalcCommitStatus(commitStatuses)
|
||||
ctx.Data["LatestCommitStatus"] = statusCheckData.LatestCommitStatus
|
||||
}
|
||||
|
||||
if pb != nil && pb.EnableStatusCheck {
|
||||
@@ -486,9 +545,9 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_
|
||||
missingRequiredChecks = append(missingRequiredChecks, requiredContext)
|
||||
}
|
||||
}
|
||||
ctx.Data["MissingRequiredChecks"] = missingRequiredChecks
|
||||
statusCheckData.MissingRequiredChecks = missingRequiredChecks
|
||||
|
||||
ctx.Data["is_context_required"] = func(context string) bool {
|
||||
statusCheckData.IsContextRequired = func(context string) bool {
|
||||
for _, c := range pb.StatusCheckContexts {
|
||||
if c == context {
|
||||
return true
|
||||
@@ -504,7 +563,7 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_
|
||||
}
|
||||
return false
|
||||
}
|
||||
ctx.Data["RequiredStatusCheckState"] = pull_service.MergeRequiredContextsCommitStatus(commitStatuses, pb.StatusCheckContexts)
|
||||
statusCheckData.RequiredChecksState = pull_service.MergeRequiredContextsCommitStatus(commitStatuses, pb.StatusCheckContexts)
|
||||
}
|
||||
|
||||
ctx.Data["HeadBranchMovedOn"] = headBranchSha != sha
|
||||
@@ -522,10 +581,10 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *pull_
|
||||
}
|
||||
}
|
||||
|
||||
compareInfo, err := pull_service.GetCompareInfo(ctx, pull.BaseRepo, pull.BaseRepo, baseGitRepo,
|
||||
git.BranchPrefix+pull.BaseBranch, pull.GetGitHeadRefName(), false, false)
|
||||
compareInfo, err := git_service.GetCompareInfo(ctx, pull.BaseRepo, pull.BaseRepo, baseGitRepo,
|
||||
git.RefNameFromBranch(pull.BaseBranch), git.RefName(pull.GetGitHeadRefName()), false, false)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "fatal: Not a valid object name") {
|
||||
if gitcmd.IsStdErrorNotValidObjectName(err) {
|
||||
ctx.Data["IsPullRequestBroken"] = true
|
||||
ctx.Data["BaseTarget"] = pull.BaseBranch
|
||||
ctx.Data["NumCommits"] = 0
|
||||
@@ -655,6 +714,8 @@ func indexCommit(commits []*git.Commit, commitID string) *git.Commit {
|
||||
|
||||
// ViewPullFiles render pull request changed files list page
|
||||
func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) {
|
||||
var err error
|
||||
|
||||
ctx.Data["PageIsPullList"] = true
|
||||
ctx.Data["PageIsPullFiles"] = true
|
||||
|
||||
@@ -681,43 +742,53 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) {
|
||||
}
|
||||
|
||||
isSingleCommit := beforeCommitID == "" && afterCommitID != ""
|
||||
ctx.Data["IsShowingOnlySingleCommit"] = isSingleCommit
|
||||
// FIXME: when afterCommitID==headCommitID, isSingleCommit and isShowAllCommits can be both true, which doesn't seem right
|
||||
isShowAllCommits := (beforeCommitID == "" || beforeCommitID == prInfo.MergeBase) && (afterCommitID == "" || afterCommitID == headCommitID)
|
||||
|
||||
ctx.Data["IsShowingOnlySingleCommit"] = isSingleCommit
|
||||
ctx.Data["IsShowingAllCommits"] = isShowAllCommits
|
||||
|
||||
if afterCommitID == "" || afterCommitID == headCommitID {
|
||||
afterCommitID = headCommitID
|
||||
}
|
||||
// "commits list" is half-open, half-closed: (base, head]
|
||||
// * base commit is not in the list
|
||||
// * if the PR is empty, the list is also empty (head commit is not in the list)
|
||||
|
||||
afterCommitID = util.IfZero(afterCommitID, headCommitID)
|
||||
afterCommit := indexCommit(prInfo.Commits, afterCommitID)
|
||||
if afterCommit == nil && afterCommitID == headCommitID {
|
||||
afterCommit, err = gitRepo.GetCommit(afterCommitID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetCommit(afterCommitID)", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if afterCommit == nil {
|
||||
ctx.HTTPError(http.StatusBadRequest, "after commit not found in PR commits")
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
var beforeCommit *git.Commit
|
||||
if !isSingleCommit {
|
||||
if beforeCommitID == "" || beforeCommitID == prInfo.MergeBase {
|
||||
beforeCommitID = prInfo.MergeBase
|
||||
// mergebase commit is not in the list of the pull request commits
|
||||
beforeCommit, err = gitRepo.GetCommit(beforeCommitID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetCommit", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
beforeCommit = indexCommit(prInfo.Commits, beforeCommitID)
|
||||
if beforeCommit == nil {
|
||||
ctx.HTTPError(http.StatusBadRequest, "before commit not found in PR commits")
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if isSingleCommit {
|
||||
beforeCommit, err = afterCommit.Parent(0)
|
||||
if err != nil {
|
||||
ctx.ServerError("Parent", err)
|
||||
ctx.ServerError("afterCommit.Parent", err)
|
||||
return
|
||||
}
|
||||
beforeCommitID = beforeCommit.ID.String()
|
||||
} else {
|
||||
beforeCommitID = util.IfZero(beforeCommitID, prInfo.MergeBase)
|
||||
beforeCommit = indexCommit(prInfo.Commits, beforeCommitID)
|
||||
if beforeCommit == nil && beforeCommitID == prInfo.MergeBase {
|
||||
// mergebase commit is not in the list of the pull request commits
|
||||
beforeCommit, err = gitRepo.GetCommit(beforeCommitID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetCommit(beforeCommitID)", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
if beforeCommit == nil {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Username"] = ctx.Repo.Owner.Name
|
||||
@@ -766,7 +837,7 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) {
|
||||
}
|
||||
}
|
||||
|
||||
diffShortStat, err := gitdiff.GetDiffShortStat(ctx.Repo.GitRepo, beforeCommitID, afterCommitID)
|
||||
diffShortStat, err := gitdiff.GetDiffShortStat(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, beforeCommitID, afterCommitID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetDiffShortStat", err)
|
||||
return
|
||||
@@ -832,6 +903,12 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) {
|
||||
}
|
||||
|
||||
ctx.Data["Diff"] = diff
|
||||
ctx.Data["DiffBlobExcerptData"] = &gitdiff.DiffBlobExcerptData{
|
||||
BaseLink: ctx.Repo.RepoLink + "/blob_excerpt",
|
||||
PullIssueIndex: pull.Index,
|
||||
DiffStyle: GetDiffViewStyle(ctx),
|
||||
AfterCommitID: afterCommitID,
|
||||
}
|
||||
ctx.Data["DiffNotAvailable"] = diffShortStat.NumFiles == 0
|
||||
|
||||
if ctx.IsSigned && ctx.Doer != nil {
|
||||
@@ -848,10 +925,7 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) {
|
||||
ctx.ServerError("GetRepoAssignees", err)
|
||||
return
|
||||
}
|
||||
handleMentionableAssigneesAndTeams(ctx, shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers))
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
|
||||
|
||||
currentReview, err := issues_model.GetCurrentReview(ctx, ctx.Doer, issue)
|
||||
if err != nil && !issues_model.IsErrReviewNotExist(err) {
|
||||
@@ -896,13 +970,13 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) {
|
||||
|
||||
if pull.HeadRepo != nil {
|
||||
if !pull.HasMerged && ctx.Doer != nil {
|
||||
perm, err := access_model.GetUserRepoPermission(ctx, pull.HeadRepo, ctx.Doer)
|
||||
headPerm, err := access_model.GetDoerRepoPermission(ctx, pull.HeadRepo, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserRepoPermission", err)
|
||||
ctx.ServerError("GetDoerRepoPermission", err)
|
||||
return
|
||||
}
|
||||
|
||||
if perm.CanWrite(unit.TypeCode) || issues_model.CanMaintainerWriteToBranch(ctx, perm, pull.HeadBranch, ctx.Doer) {
|
||||
if issues_model.CanMaintainerWriteToBranch(ctx, headPerm, pull.HeadBranch, ctx.Doer) {
|
||||
ctx.Data["CanEditFile"] = true
|
||||
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file")
|
||||
ctx.Data["HeadRepoLink"] = pull.HeadRepo.Link()
|
||||
@@ -1121,7 +1195,7 @@ func MergePullRequest(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
if err := pull_service.Merge(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil {
|
||||
if err := pull_service.Merge(ctx, pr, ctx.Doer, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil {
|
||||
if pull_service.IsErrInvalidMergeStyle(err) {
|
||||
ctx.JSONError(ctx.Tr("repo.pulls.invalid_merge_option"))
|
||||
} else if pull_service.IsErrMergeConflicts(err) {
|
||||
@@ -1304,6 +1378,10 @@ func CompareAndPullRequestPost(ctx *context.Context) {
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
if ctx.Data["IsNoMergeBase"] == true {
|
||||
ctx.JSONError(ctx.Tr("repo.pulls.no_common_history"))
|
||||
return
|
||||
}
|
||||
|
||||
validateRet := ValidateRepoMetasForNewIssue(ctx, *form, true)
|
||||
if ctx.Written() {
|
||||
@@ -1327,7 +1405,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
|
||||
}
|
||||
|
||||
// Check if a pull request already exists with the same head and base branch.
|
||||
pr, err := issues_model.GetUnmergedPullRequest(ctx, ci.HeadRepo.ID, repo.ID, ci.HeadBranch, ci.BaseBranch, issues_model.PullRequestFlowGithub)
|
||||
pr, err := issues_model.GetUnmergedPullRequest(ctx, ci.HeadRepo.ID, repo.ID, ci.HeadRef.ShortName(), ci.BaseRef.ShortName(), issues_model.PullRequestFlowGithub)
|
||||
if err != nil && !issues_model.IsErrPullRequestNotExist(err) {
|
||||
ctx.ServerError("GetUnmergedPullRequest", err)
|
||||
return
|
||||
@@ -1357,11 +1435,11 @@ func CompareAndPullRequestPost(ctx *context.Context) {
|
||||
pullRequest := &issues_model.PullRequest{
|
||||
HeadRepoID: ci.HeadRepo.ID,
|
||||
BaseRepoID: repo.ID,
|
||||
HeadBranch: ci.HeadBranch,
|
||||
BaseBranch: ci.BaseBranch,
|
||||
HeadBranch: ci.HeadRef.ShortName(),
|
||||
BaseBranch: ci.BaseRef.ShortName(),
|
||||
HeadRepo: ci.HeadRepo,
|
||||
BaseRepo: repo,
|
||||
MergeBase: ci.CompareInfo.MergeBase,
|
||||
MergeBase: ci.MergeBase,
|
||||
Type: issues_model.PullRequestGitea,
|
||||
AllowMaintainerEdit: form.AllowMaintainerEdit,
|
||||
}
|
||||
@@ -1376,6 +1454,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
|
||||
AssigneeIDs: assigneeIDs,
|
||||
Reviewers: validateRet.Reviewers,
|
||||
TeamReviewers: validateRet.TeamReviewers,
|
||||
ProjectID: projectID,
|
||||
}
|
||||
if err := pull_service.NewPullRequest(ctx, prOpts); err != nil {
|
||||
switch {
|
||||
@@ -1427,15 +1506,6 @@ func CompareAndPullRequestPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if projectID > 0 && ctx.Repo.CanWrite(unit.TypeProjects) {
|
||||
if err := issues_model.IssueAssignOrRemoveProject(ctx, pullIssue, ctx.Doer, projectID, 0); err != nil {
|
||||
if !errors.Is(err, util.ErrPermissionDenied) {
|
||||
ctx.ServerError("IssueAssignOrRemoveProject", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Trace("Pull request created: %d/%d", repo.ID, pullIssue.ID)
|
||||
ctx.JSONRedirect(pullIssue.Link())
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ func TestRenderConversation(t *testing.T) {
|
||||
|
||||
run := func(name string, cb func(t *testing.T, ctx *context.Context, resp *httptest.ResponseRecorder)) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctx, resp := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
|
||||
ctx, resp := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.PageRenderer()})
|
||||
contexttest.LoadUser(t, ctx, pr.Issue.PosterID)
|
||||
contexttest.LoadRepo(t, ctx, pr.BaseRepoID)
|
||||
contexttest.LoadGitRepo(t, ctx)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
stdCtx "context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -40,7 +41,7 @@ const (
|
||||
)
|
||||
|
||||
// calReleaseNumCommitsBehind calculates given release has how many commits behind release target.
|
||||
func calReleaseNumCommitsBehind(repoCtx *context.Repository, release *repo_model.Release, countCache map[string]int64) error {
|
||||
func calReleaseNumCommitsBehind(ctx stdCtx.Context, repoCtx *context.Repository, release *repo_model.Release, countCache map[string]int64) error {
|
||||
target := release.Target
|
||||
if target == "" {
|
||||
target = repoCtx.Repository.DefaultBranch
|
||||
@@ -60,7 +61,7 @@ func calReleaseNumCommitsBehind(repoCtx *context.Repository, release *repo_model
|
||||
return fmt.Errorf("GetBranchCommit(DefaultBranch): %w", err)
|
||||
}
|
||||
}
|
||||
countCache[target], err = commit.CommitsCount()
|
||||
countCache[target], err = gitrepo.CommitsCountOfCommit(ctx, repoCtx.Repository, commit.ID.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("CommitsCount: %w", err)
|
||||
}
|
||||
@@ -103,13 +104,9 @@ 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.GetPossibleUserByID(ctx, r.PublisherID)
|
||||
r.PublisherID, r.Publisher, err = user_model.GetPossibleUserByID(ctx, r.PublisherID)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
r.Publisher = user_model.NewGhostUser()
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
cacheUsers[r.PublisherID] = r.Publisher
|
||||
}
|
||||
@@ -123,7 +120,7 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions)
|
||||
}
|
||||
|
||||
if !r.IsDraft {
|
||||
if err := calReleaseNumCommitsBehind(ctx.Repo, r, countCache); err != nil {
|
||||
if err := calReleaseNumCommitsBehind(ctx, ctx.Repo, r, countCache); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -186,7 +183,7 @@ func Releases(ctx *context.Context) {
|
||||
ctx.Data["Releases"] = releases
|
||||
|
||||
numReleases := ctx.Data["NumReleases"].(int64)
|
||||
pager := context.NewPagination(int(numReleases), listOptions.PageSize, listOptions.Page, 5)
|
||||
pager := context.NewPagination(numReleases, listOptions.PageSize, listOptions.Page, 5)
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
ctx.HTML(http.StatusOK, tplReleasesList)
|
||||
@@ -238,7 +235,7 @@ func TagsList(ctx *context.Context) {
|
||||
ctx.Data["Releases"] = releases
|
||||
ctx.Data["TagCount"] = count
|
||||
|
||||
pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
|
||||
pager := context.NewPagination(count, opts.PageSize, opts.Page, 5)
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
ctx.Data["PageIsViewCode"] = !ctx.Repo.Repository.UnitEnabled(ctx, unit.TypeReleases)
|
||||
@@ -391,6 +388,32 @@ func NewRelease(ctx *context.Context) {
|
||||
ctx.HTML(http.StatusOK, tplReleaseNew)
|
||||
}
|
||||
|
||||
// GenerateReleaseNotes builds release notes content for the given tag and base.
|
||||
func GenerateReleaseNotes(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.GenerateReleaseNotesForm)
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.JSONError(ctx.GetErrMsg())
|
||||
return
|
||||
}
|
||||
|
||||
content, err := release_service.GenerateReleaseNotes(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, release_service.GenerateReleaseNotesOptions{
|
||||
TagName: form.TagName,
|
||||
TagTarget: form.TagTarget,
|
||||
PreviousTag: form.PreviousTag,
|
||||
})
|
||||
if err != nil {
|
||||
if errTr := util.ErrorAsTranslatable(err); errTr != nil {
|
||||
ctx.JSONError(errTr.Translate(ctx.Locale))
|
||||
} else {
|
||||
ctx.ServerError("GenerateReleaseNotes", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, map[string]any{"content": content})
|
||||
}
|
||||
|
||||
// NewReleasePost response for creating a release
|
||||
func NewReleasePost(ctx *context.Context) {
|
||||
newReleaseCommon(ctx)
|
||||
@@ -424,14 +447,15 @@ func NewReleasePost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if !gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, form.Target) {
|
||||
ctx.RenderWithErr(ctx.Tr("form.target_branch_not_exist"), tplReleaseNew, &form)
|
||||
form.Target = util.IfZero(form.Target, ctx.Repo.Repository.DefaultBranch)
|
||||
if exist, _ := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, form.Target); !exist {
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.target_branch_not_exist"), tplReleaseNew, &form)
|
||||
return
|
||||
}
|
||||
|
||||
if !form.TagOnly && form.Title == "" {
|
||||
// if not "tag only", then the title of the release cannot be empty
|
||||
ctx.RenderWithErr(ctx.Tr("repo.release.title_empty"), tplReleaseNew, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.release.title_empty"), tplReleaseNew, &form)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -439,13 +463,13 @@ func NewReleasePost(ctx *context.Context) {
|
||||
ctx.Data["Err_TagName"] = true
|
||||
switch {
|
||||
case release_service.IsErrTagAlreadyExists(err):
|
||||
ctx.RenderWithErr(ctx.Tr("repo.branch.tag_collision", form.TagName), tplReleaseNew, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.branch.tag_collision", form.TagName), tplReleaseNew, &form)
|
||||
case repo_model.IsErrReleaseAlreadyExist(err):
|
||||
ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form)
|
||||
case release_service.IsErrInvalidTagName(err):
|
||||
ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_invalid"), tplReleaseNew, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.release.tag_name_invalid"), tplReleaseNew, &form)
|
||||
case release_service.IsErrProtectedTagName(err):
|
||||
ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_protected"), tplReleaseNew, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.release.tag_name_protected"), tplReleaseNew, &form)
|
||||
default:
|
||||
ctx.ServerError("handleTagReleaseError", err)
|
||||
}
|
||||
@@ -498,7 +522,7 @@ func NewReleasePost(ctx *context.Context) {
|
||||
// add new logic: if tag-only, do not convert the tag to a release
|
||||
if form.TagOnly || !rel.IsTag {
|
||||
ctx.Data["Err_TagName"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.release.tag_name_already_exist"), tplReleaseNew, &form)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -519,11 +543,13 @@ func NewReleasePost(ctx *context.Context) {
|
||||
|
||||
// EditRelease render release edit page
|
||||
func EditRelease(ctx *context.Context) {
|
||||
newReleaseCommon(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Title"] = ctx.Tr("repo.release.edit_release")
|
||||
ctx.Data["PageIsReleaseList"] = true
|
||||
ctx.Data["PageIsEditRelease"] = true
|
||||
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
|
||||
upload.AddUploadContext(ctx, "release")
|
||||
|
||||
tagName := ctx.PathParam("*")
|
||||
rel, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, tagName)
|
||||
@@ -535,6 +561,11 @@ func EditRelease(ctx *context.Context) {
|
||||
}
|
||||
return
|
||||
}
|
||||
if rel.IsTag {
|
||||
ctx.NotFound(err) // for a pure tag release, don't allow to edit it as a release
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["ID"] = rel.ID
|
||||
ctx.Data["tag_name"] = rel.TagName
|
||||
ctx.Data["tag_target"] = util.IfZero(rel.Target, ctx.Repo.Repository.DefaultBranch)
|
||||
@@ -564,8 +595,13 @@ func EditRelease(ctx *context.Context) {
|
||||
// EditReleasePost response for edit release
|
||||
func EditReleasePost(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.EditReleaseForm)
|
||||
|
||||
newReleaseCommon(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Title"] = ctx.Tr("repo.release.edit_release")
|
||||
ctx.Data["PageIsReleaseList"] = true
|
||||
ctx.Data["PageIsEditRelease"] = true
|
||||
|
||||
tagName := ctx.PathParam("*")
|
||||
@@ -579,7 +615,7 @@ func EditReleasePost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
if rel.IsTag {
|
||||
ctx.NotFound(err)
|
||||
ctx.NotFound(err) // for a pure tag release, don't allow to edit it as a release
|
||||
return
|
||||
}
|
||||
ctx.Data["tag_name"] = rel.TagName
|
||||
|
||||
@@ -158,7 +158,7 @@ func TestCalReleaseNumCommitsBehind(t *testing.T) {
|
||||
|
||||
countCache := make(map[string]int64)
|
||||
for _, release := range releases {
|
||||
err := calReleaseNumCommitsBehind(ctx.Repo, release, countCache)
|
||||
err := calReleaseNumCommitsBehind(ctx, ctx.Repo, release, countCache)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -32,24 +32,21 @@ func RenderFile(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
dataRc, err := blob.DataAsync()
|
||||
blobReader, err := blob.DataAsync()
|
||||
if err != nil {
|
||||
ctx.ServerError("DataAsync", err)
|
||||
return
|
||||
}
|
||||
defer dataRc.Close()
|
||||
|
||||
if markupType := markup.DetectMarkupTypeByFileName(blob.Name()); markupType == "" {
|
||||
http.Error(ctx.Resp, "Unsupported file type render", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer blobReader.Close()
|
||||
|
||||
rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
|
||||
CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
|
||||
CurrentTreePath: path.Dir(ctx.Repo.TreePath),
|
||||
}).WithRelativePath(ctx.Repo.TreePath).WithInStandalonePage(true)
|
||||
|
||||
renderer, err := markup.FindRendererByContext(rctx)
|
||||
}).WithRelativePath(ctx.Repo.TreePath).WithStandalonePage(markup.StandalonePageOptions{
|
||||
CurrentWebTheme: ctx.TemplateContext.CurrentWebTheme(),
|
||||
RenderQueryString: ctx.Req.URL.RawQuery,
|
||||
})
|
||||
renderer, rendererInput, err := rctx.DetectMarkupRendererByReader(blobReader)
|
||||
if err != nil {
|
||||
http.Error(ctx.Resp, "Unable to find renderer", http.StatusBadRequest)
|
||||
return
|
||||
@@ -71,7 +68,7 @@ func RenderFile(ctx *context.Context) {
|
||||
ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'")
|
||||
}
|
||||
|
||||
err = markup.RenderWithRenderer(rctx, renderer, dataRc, ctx.Resp)
|
||||
err = markup.RenderWithRenderer(rctx, renderer, rendererInput, ctx.Resp)
|
||||
if err != nil {
|
||||
log.Error("Failed to render file %q: %v", ctx.Repo.TreePath, err)
|
||||
http.Error(ctx.Resp, "Failed to render file", http.StatusInternalServerError)
|
||||
|
||||
@@ -70,7 +70,7 @@ func CommitInfoCache(ctx *context.Context) {
|
||||
ctx.ServerError("GetBranchCommit", err)
|
||||
return
|
||||
}
|
||||
ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount()
|
||||
ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetCommitsCount", err)
|
||||
return
|
||||
@@ -186,28 +186,28 @@ func handleCreateError(ctx *context.Context, owner *user_model.User, err error,
|
||||
case repo_model.IsErrReachLimitOfRepo(err):
|
||||
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, tpl, form)
|
||||
ctx.RenderWithErrDeprecated(msg, tpl, form)
|
||||
case repo_model.IsErrRepoAlreadyExist(err):
|
||||
ctx.Data["Err_RepoName"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tpl, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.repo_name_been_taken"), tpl, 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"), tpl, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tpl, form)
|
||||
case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
|
||||
ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.adopt"), tpl, form)
|
||||
case setting.Repository.AllowDeleteOfUnadoptedRepositories:
|
||||
ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.delete"), tpl, form)
|
||||
default:
|
||||
ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tpl, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist"), tpl, form)
|
||||
}
|
||||
case db.IsErrNameReserved(err):
|
||||
ctx.Data["Err_RepoName"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, form)
|
||||
case db.IsErrNamePatternNotAllowed(err):
|
||||
ctx.Data["Err_RepoName"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, form)
|
||||
default:
|
||||
ctx.ServerError(name, err)
|
||||
}
|
||||
@@ -254,7 +254,7 @@ func CreatePost(ctx *context.Context) {
|
||||
}
|
||||
|
||||
if !opts.IsValid() {
|
||||
ctx.RenderWithErr(ctx.Tr("repo.template.one_item"), tplCreate, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.template.one_item"), tplCreate, form)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@ func CreatePost(ctx *context.Context) {
|
||||
}
|
||||
|
||||
if !templateRepo.IsTemplate {
|
||||
ctx.RenderWithErr(ctx.Tr("repo.template.invalid"), tplCreate, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.template.invalid"), tplCreate, form)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -364,31 +364,47 @@ func RedirectDownload(ctx *context.Context) {
|
||||
|
||||
// Download an archive of a repository
|
||||
func Download(ctx *context.Context) {
|
||||
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, ctx.PathParam("*"))
|
||||
if !checkDownloadTokenScope(ctx) {
|
||||
return
|
||||
}
|
||||
|
||||
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.PathParam("*"), ctx.FormStrings("path"))
|
||||
if err != nil {
|
||||
if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) {
|
||||
if errors.Is(err, util.ErrInvalidArgument) {
|
||||
ctx.HTTPError(http.StatusBadRequest, err.Error())
|
||||
} else if errors.Is(err, archiver_service.RepoRefNotFoundError{}) {
|
||||
} else if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.HTTPError(http.StatusNotFound, err.Error())
|
||||
} else {
|
||||
ctx.ServerError("archiver_service.NewRequest", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
archiver_service.ServeRepoArchive(ctx.Base, ctx.Repo.Repository, ctx.Repo.GitRepo, aReq)
|
||||
err = archiver_service.ServeRepoArchive(ctx.Base, aReq)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrInvalidArgument) {
|
||||
ctx.HTTPError(http.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
ctx.ServerError("archiver_service.ServeRepoArchive", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// InitiateDownload will enqueue an archival request, as needed. It may submit
|
||||
// a request that's already in-progress, but the archiver service will just
|
||||
// kind of drop it on the floor if this is the case.
|
||||
func InitiateDownload(ctx *context.Context) {
|
||||
if setting.Repository.StreamArchives {
|
||||
if !checkDownloadTokenScope(ctx) {
|
||||
return
|
||||
}
|
||||
|
||||
paths := ctx.FormStrings("path")
|
||||
if setting.Repository.StreamArchives || len(paths) > 0 {
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"complete": true,
|
||||
})
|
||||
return
|
||||
}
|
||||
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository.ID, ctx.Repo.GitRepo, ctx.PathParam("*"))
|
||||
aReq, err := archiver_service.NewRequest(ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.PathParam("*"), paths)
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusBadRequest, "invalid archive request")
|
||||
return
|
||||
@@ -398,7 +414,7 @@ func InitiateDownload(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
archiver, err := repo_model.GetRepoArchiver(ctx, aReq.RepoID, aReq.Type, aReq.CommitID)
|
||||
archiver, err := repo_model.GetRepoArchiver(ctx, aReq.Repo.ID, aReq.Type, aReq.CommitID)
|
||||
if err != nil {
|
||||
ctx.ServerError("archiver_service.StartArchive", err)
|
||||
return
|
||||
@@ -518,9 +534,9 @@ func SearchRepo(ctx *context.Context) {
|
||||
|
||||
ctx.SetTotalCountHeader(count)
|
||||
|
||||
latestCommitStatuses, err := commitstatus_service.FindReposLastestCommitStatuses(ctx, repos)
|
||||
latestCommitStatuses, err := commitstatus_service.FindReposLatestCommitStatuses(ctx, repos)
|
||||
if err != nil {
|
||||
log.Error("FindReposLastestCommitStatuses: %v", err)
|
||||
log.Error("FindReposLatestCommitStatuses: %v", err)
|
||||
ctx.JSON(http.StatusInternalServerError, nil)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ func Search(ctx *context.Context) {
|
||||
page = 1
|
||||
}
|
||||
|
||||
var total int
|
||||
var total int64
|
||||
var searchResults []*code_indexer.Result
|
||||
var searchResultLanguages []*code_indexer.SearchResultLanguages
|
||||
if setting.Indexer.RepoIndexerEnabled {
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/actions"
|
||||
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/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
shared_actions "code.gitea.io/gitea/routers/web/shared/actions"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
)
|
||||
|
||||
const tplRepoActionsGeneralSettings templates.TplName = "repo/settings/actions"
|
||||
|
||||
func ActionsGeneralSettings(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("actions.general")
|
||||
ctx.Data["PageType"] = "general"
|
||||
ctx.Data["PageIsActionsSettingsGeneral"] = true
|
||||
|
||||
actionsUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit_model.TypeActions)
|
||||
if err != nil && !repo_model.IsErrUnitTypeNotExist(err) {
|
||||
ctx.ServerError("GetUnit", err)
|
||||
return
|
||||
}
|
||||
if actionsUnit == nil { // no actions unit
|
||||
ctx.HTML(http.StatusOK, tplRepoActionsGeneralSettings)
|
||||
return
|
||||
}
|
||||
|
||||
actionsCfg := actionsUnit.ActionsConfig()
|
||||
|
||||
// Token permission settings
|
||||
ctx.Data["TokenPermissionModePermissive"] = repo_model.ActionsTokenPermissionModePermissive
|
||||
ctx.Data["TokenPermissionModeRestricted"] = repo_model.ActionsTokenPermissionModeRestricted
|
||||
|
||||
// Follow owner config (only for repos in orgs)
|
||||
ctx.Data["OverrideOwnerConfig"] = actionsCfg.OverrideOwnerConfig
|
||||
if actionsCfg.OverrideOwnerConfig {
|
||||
ctx.Data["MaxTokenPermissions"] = actionsCfg.GetMaxTokenPermissions()
|
||||
ctx.Data["TokenPermissionMode"] = actionsCfg.TokenPermissionMode
|
||||
ctx.Data["EnableMaxTokenPermissions"] = actionsCfg.MaxTokenPermissions != nil
|
||||
} else {
|
||||
ownerActionsConfig, err := actions.GetOwnerActionsConfig(ctx, ctx.Repo.Repository.OwnerID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetOwnerActionsConfig", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["MaxTokenPermissions"] = ownerActionsConfig.GetMaxTokenPermissions()
|
||||
ctx.Data["TokenPermissionMode"] = ownerActionsConfig.TokenPermissionMode
|
||||
ctx.Data["EnableMaxTokenPermissions"] = ownerActionsConfig.MaxTokenPermissions != nil
|
||||
}
|
||||
|
||||
if ctx.Repo.Repository.IsPrivate {
|
||||
collaborativeOwnerIDs := actionsCfg.CollaborativeOwnerIDs
|
||||
collaborativeOwners, err := user_model.GetUsersByIDs(ctx, collaborativeOwnerIDs)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUsersByIDs", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["CollaborativeOwners"] = collaborativeOwners
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplRepoActionsGeneralSettings)
|
||||
}
|
||||
|
||||
func ActionsUnitPost(ctx *context.Context) {
|
||||
redirectURL := ctx.Repo.RepoLink + "/settings/actions/general"
|
||||
enableActionsUnit := ctx.FormBool("enable_actions")
|
||||
repo := ctx.Repo.Repository
|
||||
|
||||
var err error
|
||||
if enableActionsUnit && !unit_model.TypeActions.UnitGlobalDisabled() {
|
||||
err = repo_service.UpdateRepositoryUnits(ctx, repo, []repo_model.RepoUnit{newRepoUnit(repo, unit_model.TypeActions, nil)}, nil)
|
||||
} else if !unit_model.TypeActions.UnitGlobalDisabled() {
|
||||
err = repo_service.UpdateRepositoryUnits(ctx, repo, nil, []unit_model.Type{unit_model.TypeActions})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ctx.ServerError("UpdateRepositoryUnits", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
|
||||
ctx.Redirect(redirectURL)
|
||||
}
|
||||
|
||||
func AddCollaborativeOwner(ctx *context.Context) {
|
||||
collUser, err := user_model.GetUserByName(ctx, ctx.FormString("collaborative_owner"))
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.JSONError(ctx.Tr("form.user_not_exist"))
|
||||
} else {
|
||||
ctx.ServerError("GetUserByName", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
actionsUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit_model.TypeActions)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUnit", err)
|
||||
return
|
||||
}
|
||||
actionsCfg := actionsUnit.ActionsConfig()
|
||||
actionsCfg.AddCollaborativeOwner(collUser.ID)
|
||||
if err := repo_model.UpdateRepoUnitConfig(ctx, actionsUnit); err != nil {
|
||||
ctx.ServerError("UpdateRepoUnitConfig", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
func DeleteCollaborativeOwner(ctx *context.Context) {
|
||||
ownerID := ctx.FormInt64("id")
|
||||
|
||||
actionsUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit_model.TypeActions)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUnit", err)
|
||||
return
|
||||
}
|
||||
actionsCfg := actionsUnit.ActionsConfig()
|
||||
if !actionsCfg.IsCollaborativeOwner(ownerID) {
|
||||
ctx.Flash.Error(ctx.Tr("actions.general.collaborative_owner_not_exist"))
|
||||
ctx.JSONErrorNotFound()
|
||||
return
|
||||
}
|
||||
actionsCfg.RemoveCollaborativeOwner(ownerID)
|
||||
if err := repo_model.UpdateRepoUnitConfig(ctx, actionsUnit); err != nil {
|
||||
ctx.ServerError("UpdateRepoUnitConfig", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
// UpdateTokenPermissions updates the token permission settings for the repository
|
||||
func UpdateTokenPermissions(ctx *context.Context) {
|
||||
redirectURL := ctx.Repo.RepoLink + "/settings/actions/general"
|
||||
|
||||
actionsUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit_model.TypeActions)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUnit", err)
|
||||
return
|
||||
}
|
||||
|
||||
actionsCfg := actionsUnit.ActionsConfig()
|
||||
|
||||
// Update Override Owner Config (for repos in orgs)
|
||||
// If checked, it means we WANT to override (opt-out of following)
|
||||
actionsCfg.OverrideOwnerConfig = ctx.FormBool("override_owner_config")
|
||||
|
||||
// Update permission mode (only if overriding owner config)
|
||||
shouldUpdate := actionsCfg.OverrideOwnerConfig
|
||||
|
||||
if shouldUpdate {
|
||||
permissionMode, permissionModeValid := util.EnumValue(repo_model.ActionsTokenPermissionMode(ctx.FormString("token_permission_mode")))
|
||||
if !permissionModeValid {
|
||||
ctx.Flash.Error("Invalid token permission mode")
|
||||
ctx.Redirect(redirectURL)
|
||||
return
|
||||
}
|
||||
actionsCfg.TokenPermissionMode = permissionMode
|
||||
}
|
||||
|
||||
// Update Maximum Permissions (radio buttons: none/read/write)
|
||||
enableMaxPermissions := ctx.FormBool("enable_max_permissions")
|
||||
if shouldUpdate {
|
||||
if enableMaxPermissions {
|
||||
actionsCfg.MaxTokenPermissions = shared_actions.ParseMaxTokenPermissions(ctx)
|
||||
} else {
|
||||
// If not enabled, ensure any sent permissions are ignored and set to nil
|
||||
actionsCfg.MaxTokenPermissions = nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := repo_model.UpdateRepoUnitConfig(ctx, actionsUnit); err != nil {
|
||||
ctx.ServerError("UpdateRepoUnitConfig", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
|
||||
ctx.Redirect(redirectURL)
|
||||
}
|
||||
@@ -208,7 +208,7 @@ func DeleteTeam(ctx *context.Context) {
|
||||
}
|
||||
|
||||
if err = repo_service.RemoveRepositoryFromTeam(ctx, team, ctx.Repo.Repository.ID); err != nil {
|
||||
ctx.ServerError("team.RemoveRepositorys", err)
|
||||
ctx.ServerError("team.RemoveRepositories", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -76,16 +76,16 @@ func DeployKeysPost(ctx *context.Context) {
|
||||
switch {
|
||||
case asymkey_model.IsErrDeployKeyAlreadyExist(err):
|
||||
ctx.Data["Err_Content"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("repo.settings.key_been_used"), tplDeployKeys, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.settings.key_been_used"), tplDeployKeys, &form)
|
||||
case asymkey_model.IsErrKeyAlreadyExist(err):
|
||||
ctx.Data["Err_Content"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplDeployKeys, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("settings.ssh_key_been_used"), tplDeployKeys, &form)
|
||||
case asymkey_model.IsErrKeyNameAlreadyUsed(err):
|
||||
ctx.Data["Err_Title"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form)
|
||||
case asymkey_model.IsErrDeployKeyNameAlreadyUsed(err):
|
||||
ctx.Data["Err_Title"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form)
|
||||
default:
|
||||
ctx.ServerError("AddDeployKey", err)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/routers/web/repo"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
@@ -41,6 +42,7 @@ func GitHooksEdit(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
ctx.Data["Hook"] = hook
|
||||
ctx.Data["CodeEditorConfig"] = repo.CodeEditorConfig{Filename: name + ".sh", IndentStyle: "tab", TabWidth: 4}
|
||||
ctx.HTML(http.StatusOK, tplGithookEdit)
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"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/gitrepo"
|
||||
"code.gitea.io/gitea/modules/lfs"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
@@ -53,7 +54,7 @@ func LFSFiles(ctx *context.Context) {
|
||||
}
|
||||
ctx.Data["Total"] = total
|
||||
|
||||
pager := context.NewPagination(int(total), setting.UI.ExplorePagingNum, page, 5)
|
||||
pager := context.NewPagination(total, setting.UI.ExplorePagingNum, page, 5)
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings.lfs")
|
||||
ctx.Data["PageIsSettingsLFS"] = true
|
||||
lfsMetaObjects, err := git_model.GetLFSMetaObjects(ctx, ctx.Repo.Repository.ID, pager.Paginater.Current(), setting.UI.ExplorePagingNum)
|
||||
@@ -82,7 +83,7 @@ func LFSLocks(ctx *context.Context) {
|
||||
}
|
||||
ctx.Data["Total"] = total
|
||||
|
||||
pager := context.NewPagination(int(total), setting.UI.ExplorePagingNum, page, 5)
|
||||
pager := context.NewPagination(total, setting.UI.ExplorePagingNum, page, 5)
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings.lfs_locks")
|
||||
ctx.Data["PageIsSettingsLFS"] = true
|
||||
lfsLocks, err := git_model.GetLFSLockByRepoID(ctx, ctx.Repo.Repository.ID, pager.Paginater.Current(), setting.UI.ExplorePagingNum)
|
||||
@@ -112,7 +113,7 @@ func LFSLocks(ctx *context.Context) {
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
if err := git.Clone(ctx, ctx.Repo.Repository.RepoPath(), tmpBasePath, git.CloneRepoOptions{
|
||||
if err := gitrepo.CloneRepoToLocal(ctx, ctx.Repo.Repository, tmpBasePath, git.CloneRepoOptions{
|
||||
Bare: true,
|
||||
Shared: true,
|
||||
}); err != nil {
|
||||
@@ -300,13 +301,13 @@ func LFSFileGet(ctx *context.Context) {
|
||||
if index != len(lines)-1 {
|
||||
line += "\n"
|
||||
}
|
||||
output.WriteString(fmt.Sprintf(`<li class="L%d" rel="L%d">%s</li>`, index+1, index+1, line))
|
||||
fmt.Fprintf(&output, `<li class="L%d" rel="L%d">%s</li>`, index+1, index+1, line)
|
||||
}
|
||||
ctx.Data["FileContent"] = gotemplate.HTML(output.String())
|
||||
|
||||
output.Reset()
|
||||
for i := 0; i < len(lines); i++ {
|
||||
output.WriteString(fmt.Sprintf(`<span id="L%d">%d</span>`, i+1, i+1))
|
||||
fmt.Fprintf(&output, `<span id="L%d">%d</span>`, i+1, i+1)
|
||||
}
|
||||
ctx.Data["LineNums"] = gotemplate.HTML(output.String())
|
||||
|
||||
@@ -406,7 +407,9 @@ func LFSPointerFiles(ctx *context.Context) {
|
||||
err = func() error {
|
||||
pointerChan := make(chan lfs.PointerBlob)
|
||||
errChan := make(chan error, 1)
|
||||
go lfs.SearchPointerBlobs(ctx, ctx.Repo.GitRepo, pointerChan, errChan)
|
||||
go func() {
|
||||
errChan <- lfs.SearchPointerBlobs(ctx, ctx.Repo.GitRepo, pointerChan)
|
||||
}()
|
||||
|
||||
numPointers := 0
|
||||
var numAssociated, numNoExist, numAssociatable int
|
||||
@@ -482,11 +485,6 @@ func LFSPointerFiles(ctx *context.Context) {
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
err, has := <-errChan
|
||||
if has {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx.Data["Pointers"] = results
|
||||
ctx.Data["NumPointers"] = numPointers
|
||||
ctx.Data["NumAssociated"] = numAssociated
|
||||
@@ -494,7 +492,8 @@ func LFSPointerFiles(ctx *context.Context) {
|
||||
ctx.Data["NumNoExist"] = numNoExist
|
||||
ctx.Data["NumNotAssociated"] = numPointers - numAssociated
|
||||
|
||||
return nil
|
||||
err := <-errChan
|
||||
return err
|
||||
}()
|
||||
if err != nil {
|
||||
ctx.ServerError("LFSPointerFiles", err)
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/glob"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/web/repo"
|
||||
@@ -312,10 +313,14 @@ func DeleteProtectedBranchRulePost(ctx *context.Context) {
|
||||
}
|
||||
|
||||
func UpdateBranchProtectionPriories(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.ProtectBranchPriorityForm)
|
||||
repo := ctx.Repo.Repository
|
||||
|
||||
if err := git_model.UpdateProtectBranchPriorities(ctx, repo, form.IDs); err != nil {
|
||||
var form struct {
|
||||
IDs []int64 `json:"ids"`
|
||||
}
|
||||
if err := json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil {
|
||||
ctx.JSONError("invalid argument")
|
||||
return
|
||||
}
|
||||
if err := git_model.UpdateProtectBranchPriorities(ctx, ctx.Repo.Repository, form.IDs); err != nil {
|
||||
ctx.ServerError("UpdateProtectBranchPriorities", err)
|
||||
return
|
||||
}
|
||||
@@ -336,7 +341,7 @@ func RenameBranchPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
msg, err := repository.RenameBranch(ctx, ctx.Repo.Repository, ctx.Doer, ctx.Repo.GitRepo, form.From, form.To)
|
||||
msg, err := repository.RenameBranch(ctx, ctx.Repo.Repository, ctx.Doer, form.From, form.To)
|
||||
if err != nil {
|
||||
switch {
|
||||
case repo_model.IsErrUserDoesNotHaveAccessToRepo(err):
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
shared "code.gitea.io/gitea/routers/web/shared/secrets"
|
||||
@@ -46,7 +45,7 @@ func getSecretsCtx(ctx *context.Context) (*secretsCtx, error) {
|
||||
if ctx.Data["PageIsOrgSettings"] == true {
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // error is already handled by ctx.ServerError
|
||||
}
|
||||
return &secretsCtx{
|
||||
OwnerID: ctx.ContextUser.ID,
|
||||
@@ -74,7 +73,6 @@ func Secrets(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("actions.actions")
|
||||
ctx.Data["PageType"] = "secrets"
|
||||
ctx.Data["PageIsSharedSettingsSecrets"] = true
|
||||
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
|
||||
|
||||
sCtx, err := getSecretsCtx(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -28,8 +28,8 @@ import (
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/validation"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
repo_router "code.gitea.io/gitea/routers/web/repo"
|
||||
actions_service "code.gitea.io/gitea/services/actions"
|
||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
"code.gitea.io/gitea/services/migrations"
|
||||
@@ -62,7 +62,7 @@ func SettingsCtxData(ctx *context.Context) {
|
||||
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())
|
||||
signing, _ := gitrepo.GetSigningKey(ctx)
|
||||
ctx.Data["SigningKeyAvailable"] = signing != nil
|
||||
ctx.Data["SigningSettings"] = setting.Repository.Signing
|
||||
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
|
||||
@@ -89,6 +89,11 @@ func SettingsCtxData(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
ctx.Data["PushMirrors"] = pushMirrors
|
||||
|
||||
repo_router.PrepareBranchList(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Settings show a repository's settings page
|
||||
@@ -105,7 +110,7 @@ func SettingsPost(ctx *context.Context) {
|
||||
ctx.Data["DefaultMirrorInterval"] = setting.Mirror.DefaultInterval
|
||||
ctx.Data["MinimumMirrorInterval"] = setting.Mirror.MinInterval
|
||||
|
||||
signing, _ := asymkey_service.SigningKey(ctx, ctx.Repo.Repository.RepoPath())
|
||||
signing, _ := gitrepo.GetSigningKey(ctx)
|
||||
ctx.Data["SigningKeyAvailable"] = signing != nil
|
||||
ctx.Data["SigningSettings"] = setting.Repository.Signing
|
||||
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
|
||||
@@ -176,23 +181,23 @@ func handleSettingsPostUpdate(ctx *context.Context) {
|
||||
ctx.Data["Err_RepoName"] = true
|
||||
switch {
|
||||
case repo_model.IsErrRepoAlreadyExist(err):
|
||||
ctx.RenderWithErr(ctx.Tr("form.repo_name_been_taken"), tplSettingsOptions, &form)
|
||||
ctx.RenderWithErrDeprecated(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)
|
||||
ctx.RenderWithErrDeprecated(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)
|
||||
ctx.RenderWithErrDeprecated(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)
|
||||
ctx.RenderWithErrDeprecated(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)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.repository_files_already_exist.delete"), tplSettingsOptions, form)
|
||||
default:
|
||||
ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplSettingsOptions, form)
|
||||
ctx.RenderWithErrDeprecated(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)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form)
|
||||
default:
|
||||
ctx.ServerError("ChangeRepositoryName", err)
|
||||
}
|
||||
@@ -208,11 +213,6 @@ func handleSettingsPostUpdate(ctx *context.Context) {
|
||||
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
|
||||
@@ -247,7 +247,7 @@ func handleSettingsPostMirror(ctx *context.Context) {
|
||||
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)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -298,7 +298,7 @@ func handleSettingsPostMirror(ctx *context.Context) {
|
||||
ep := lfs.DetermineEndpoint("", form.LFSEndpoint)
|
||||
if ep == nil {
|
||||
ctx.Data["Err_LFSEndpoint"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_lfs_endpoint"), tplSettingsOptions, &form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.invalid_lfs_endpoint"), tplSettingsOptions, &form)
|
||||
return
|
||||
}
|
||||
err = migrations.IsMigrateURLAllowed(ep.String(), ctx.Doer)
|
||||
@@ -369,7 +369,7 @@ func handleSettingsPostPushMirrorUpdate(ctx *context.Context) {
|
||||
|
||||
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{})
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &forms.RepoSettingForm{})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -445,7 +445,7 @@ func handleSettingsPostPushMirrorAdd(ctx *context.Context) {
|
||||
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)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &form)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -614,12 +614,6 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
|
||||
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePackages)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if form.EnablePulls && !unit_model.TypePullRequests.UnitGlobalDisabled() {
|
||||
units = append(units, newRepoUnit(repo, unit_model.TypePullRequests, &repo_model.PullRequestsConfig{
|
||||
IgnoreWhitespaceConflicts: form.PullsIgnoreWhitespace,
|
||||
@@ -634,6 +628,7 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
|
||||
DefaultDeleteBranchAfterMerge: form.DefaultDeleteBranchAfterMerge,
|
||||
DefaultMergeStyle: repo_model.MergeStyle(form.PullsDefaultMergeStyle),
|
||||
DefaultAllowMaintainerEdit: form.DefaultAllowMaintainerEdit,
|
||||
DefaultTargetBranch: strings.TrimSpace(form.DefaultTargetBranch),
|
||||
}))
|
||||
} else if !unit_model.TypePullRequests.UnitGlobalDisabled() {
|
||||
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePullRequests)
|
||||
@@ -738,7 +733,7 @@ func handleSettingsPostConvert(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
if repo.Name != form.RepoName {
|
||||
ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -772,7 +767,7 @@ func handleSettingsPostConvertFork(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
if repo.Name != form.RepoName {
|
||||
ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -808,14 +803,14 @@ func handleSettingsPostTransfer(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
if repo.Name != form.RepoName {
|
||||
ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, 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)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil)
|
||||
return
|
||||
}
|
||||
ctx.ServerError("IsUserExist", err)
|
||||
@@ -825,7 +820,7 @@ func handleSettingsPostTransfer(ctx *context.Context) {
|
||||
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)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -839,14 +834,14 @@ func handleSettingsPostTransfer(ctx *context.Context) {
|
||||
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)
|
||||
ctx.RenderWithErrDeprecated(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)
|
||||
ctx.RenderWithErrDeprecated(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)
|
||||
ctx.RenderWithErrDeprecated(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)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.settings.transfer.blocked_user"), tplSettingsOptions, nil)
|
||||
} else {
|
||||
ctx.ServerError("TransferOwnership", err)
|
||||
}
|
||||
@@ -900,7 +895,7 @@ func handleSettingsPostDelete(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
if repo.Name != form.RepoName {
|
||||
ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -927,7 +922,7 @@ func handleSettingsPostDeleteWiki(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
if repo.Name != form.RepoName {
|
||||
ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.enterred_invalid_repo_name"), tplSettingsOptions, nil)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1004,39 +999,33 @@ func handleSettingsPostUnarchive(ctx *context.Context) {
|
||||
}
|
||||
|
||||
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")
|
||||
ctx.JSONError(ctx.Tr("repo.settings.visibility.fork_error"))
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
private := ctx.FormOptionalBool("private").ValueOrDefault(true) // default to true for privacy & safety
|
||||
|
||||
// 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)
|
||||
if !private && setting.Repository.ForcePrivate && !ctx.Doer.IsAdmin {
|
||||
ctx.JSONError(ctx.Tr("form.repository_force_private"))
|
||||
return
|
||||
}
|
||||
if private && repo.FullName() != ctx.FormString("confirm_repo_name") {
|
||||
ctx.JSONError(ctx.Tr("form.enterred_invalid_repo_name"))
|
||||
return
|
||||
}
|
||||
|
||||
if repo.IsPrivate {
|
||||
err = repo_service.MakeRepoPublic(ctx, repo)
|
||||
} else {
|
||||
err = repo_service.MakeRepoPrivate(ctx, repo)
|
||||
}
|
||||
|
||||
err := repo_service.MakeRepoPrivate(ctx, repo, private)
|
||||
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")
|
||||
ctx.JSONError(ctx.Tr("repo.settings.visibility.error"))
|
||||
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")
|
||||
ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings")
|
||||
}
|
||||
|
||||
func handleSettingRemoteAddrError(ctx *context.Context, err error, form *forms.RepoSettingForm) {
|
||||
@@ -1044,21 +1033,21 @@ func handleSettingRemoteAddrError(ctx *context.Context, err error, form *forms.R
|
||||
addrErr := err.(*git.ErrInvalidCloneAddr)
|
||||
switch {
|
||||
case addrErr.IsProtocolInvalid:
|
||||
ctx.RenderWithErr(ctx.Tr("repo.mirror_address_protocol_invalid"), tplSettingsOptions, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.mirror_address_protocol_invalid"), tplSettingsOptions, form)
|
||||
case addrErr.IsURLError:
|
||||
ctx.RenderWithErr(ctx.Tr("form.url_error", addrErr.Host), tplSettingsOptions, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.url_error", addrErr.Host), tplSettingsOptions, form)
|
||||
case addrErr.IsPermissionDenied:
|
||||
if addrErr.LocalPath {
|
||||
ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied"), tplSettingsOptions, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.permission_denied"), tplSettingsOptions, form)
|
||||
} else {
|
||||
ctx.RenderWithErr(ctx.Tr("repo.migrate.permission_denied_blocked"), tplSettingsOptions, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.permission_denied_blocked"), tplSettingsOptions, form)
|
||||
}
|
||||
case addrErr.IsInvalidPath:
|
||||
ctx.RenderWithErr(ctx.Tr("repo.migrate.invalid_local_path"), tplSettingsOptions, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.migrate.invalid_local_path"), tplSettingsOptions, form)
|
||||
default:
|
||||
ctx.ServerError("Unknown error", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
ctx.RenderWithErr(ctx.Tr("repo.mirror_address_url_invalid"), tplSettingsOptions, form)
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("repo.mirror_address_url_invalid"), tplSettingsOptions, form)
|
||||
}
|
||||
|
||||
@@ -234,6 +234,7 @@ func createWebhook(ctx *context.Context, params webhookParams) {
|
||||
w := &webhook.Webhook{
|
||||
RepoID: orCtx.RepoID,
|
||||
URL: params.URL,
|
||||
Name: strings.TrimSpace(params.WebhookForm.Name),
|
||||
HTTPMethod: params.HTTPMethod,
|
||||
ContentType: params.ContentType,
|
||||
Secret: params.WebhookForm.Secret,
|
||||
@@ -288,6 +289,7 @@ func editWebhook(ctx *context.Context, params webhookParams) {
|
||||
}
|
||||
|
||||
w.URL = params.URL
|
||||
w.Name = strings.TrimSpace(params.WebhookForm.Name)
|
||||
w.ContentType = params.ContentType
|
||||
w.Secret = params.WebhookForm.Secret
|
||||
w.HookEvent = ParseHookEvent(params.WebhookForm)
|
||||
@@ -448,12 +450,21 @@ func MatrixHooksEditPost(ctx *context.Context) {
|
||||
editWebhook(ctx, matrixHookParams(ctx))
|
||||
}
|
||||
|
||||
func matrixRoomIDEncode(roomID string) string {
|
||||
// See https://spec.matrix.org/latest/appendices/#room-ids
|
||||
// Some (unrelated) demo links: https://spec.matrix.org/latest/appendices/#matrixto-navigation
|
||||
// API spec: https://spec.matrix.org/v1.18/client-server-api/#sending-events-to-a-room
|
||||
// Some of their examples show links like: "PUT /rooms/!roomid:domain/state/m.example.event"
|
||||
return strings.NewReplacer("%21", "!", "%3A", ":").Replace(url.PathEscape(roomID))
|
||||
}
|
||||
|
||||
func matrixHookParams(ctx *context.Context) webhookParams {
|
||||
form := web.GetForm(ctx).(*forms.NewMatrixHookForm)
|
||||
|
||||
// TODO: need to migrate to the latest (v3) API: https://spec.matrix.org/v1.18/client-server-api/
|
||||
return webhookParams{
|
||||
Type: webhook_module.MATRIX,
|
||||
URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, url.PathEscape(form.RoomID)),
|
||||
URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, matrixRoomIDEncode(form.RoomID)),
|
||||
ContentType: webhook.ContentTypeJSON,
|
||||
HTTPMethod: http.MethodPut,
|
||||
WebhookForm: form.WebhookForm,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user