初始提交: Gitea 项目代码
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
actions_model "gitea.dev/models/actions"
|
||||
"gitea.dev/models/perm"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
const (
|
||||
tplOrgSettingsActionsGeneral templates.TplName = "org/settings/actions_general"
|
||||
tplUserSettingsActionsGeneral templates.TplName = "user/settings/actions_general"
|
||||
)
|
||||
|
||||
// ParseMaxTokenPermissions parses the maximum token permissions from form values
|
||||
func ParseMaxTokenPermissions(ctx *context.Context) *repo_model.ActionsTokenPermissions {
|
||||
parseMaxPerm := func(unitType unit.Type) perm.AccessMode {
|
||||
value := ctx.FormString("max_unit_access_mode_" + strconv.Itoa(int(unitType)))
|
||||
switch value {
|
||||
case "write":
|
||||
return perm.AccessModeWrite
|
||||
case "read":
|
||||
return perm.AccessModeRead
|
||||
default:
|
||||
return perm.AccessModeNone
|
||||
}
|
||||
}
|
||||
ret := new(repo_model.MakeActionsTokenPermissions(perm.AccessModeNone))
|
||||
for _, ut := range repo_model.ActionsTokenUnitTypes {
|
||||
ret.UnitAccessModes[ut] = parseMaxPerm(ut)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// GeneralSettings renders the actions general settings page
|
||||
func GeneralSettings(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("actions.actions")
|
||||
|
||||
rCtx, err := getRunnersCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getRunnersCtx", err)
|
||||
return
|
||||
}
|
||||
|
||||
if rCtx.IsOrg {
|
||||
ctx.Data["PageIsOrgSettings"] = true
|
||||
ctx.Data["PageIsOrgSettingsActionsGeneral"] = true
|
||||
} else if rCtx.IsUser {
|
||||
ctx.Data["PageIsUserSettings"] = true
|
||||
ctx.Data["PageIsUserSettingsActionsGeneral"] = true
|
||||
} else {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Load User/Org Actions Config
|
||||
actionsCfg, err := actions_model.GetOwnerActionsConfig(ctx, rCtx.OwnerID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetOwnerActionsConfig", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["TokenPermissionMode"] = actionsCfg.TokenPermissionMode
|
||||
ctx.Data["TokenPermissionModePermissive"] = repo_model.ActionsTokenPermissionModePermissive
|
||||
ctx.Data["TokenPermissionModeRestricted"] = repo_model.ActionsTokenPermissionModeRestricted
|
||||
ctx.Data["MaxTokenPermissions"] = actionsCfg.MaxTokenPermissions
|
||||
if actionsCfg.MaxTokenPermissions == nil {
|
||||
ctx.Data["MaxTokenPermissions"] = (&repo_model.ActionsConfig{}).GetMaxTokenPermissions()
|
||||
}
|
||||
ctx.Data["EnableMaxTokenPermissions"] = actionsCfg.MaxTokenPermissions != nil
|
||||
|
||||
// Load Allowed Repositories
|
||||
allowedRepos, err := repo_model.GetOwnerRepositoriesByIDs(ctx, rCtx.OwnerID, actionsCfg.AllowedCrossRepoIDs)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetOwnerRepositoriesByIDs", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["AllowedRepos"] = allowedRepos
|
||||
ctx.Data["OwnerID"] = rCtx.OwnerID
|
||||
|
||||
if rCtx.IsOrg {
|
||||
ctx.HTML(http.StatusOK, tplOrgSettingsActionsGeneral)
|
||||
} else {
|
||||
ctx.HTML(http.StatusOK, tplUserSettingsActionsGeneral)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateGeneralSettings responses for actions general settings page
|
||||
func UpdateGeneralSettings(ctx *context.Context) {
|
||||
rCtx, err := getRunnersCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getRunnersCtx", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !rCtx.IsOrg && !rCtx.IsUser {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
actionsCfg, err := actions_model.GetOwnerActionsConfig(ctx, rCtx.OwnerID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetOwnerActionsConfig", err)
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.FormBool("cross_repo_add_target") {
|
||||
targetRepoName := ctx.FormString("cross_repo_add_target_name")
|
||||
if targetRepoName != "" {
|
||||
targetRepo, err := repo_model.GetRepositoryByName(ctx, rCtx.OwnerID, targetRepoName)
|
||||
if err != nil {
|
||||
if repo_model.IsErrRepoNotExist(err) {
|
||||
ctx.JSONError("Repository doesn't exist")
|
||||
return
|
||||
}
|
||||
ctx.ServerError("GetRepositoryByName", err)
|
||||
return
|
||||
}
|
||||
if !slices.Contains(actionsCfg.AllowedCrossRepoIDs, targetRepo.ID) {
|
||||
actionsCfg.AllowedCrossRepoIDs = append(actionsCfg.AllowedCrossRepoIDs, targetRepo.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if crossRepoRemoveTargetID := ctx.FormInt64("cross_repo_remove_target_id"); crossRepoRemoveTargetID != 0 {
|
||||
actionsCfg.AllowedCrossRepoIDs = util.SliceRemoveAll(actionsCfg.AllowedCrossRepoIDs, crossRepoRemoveTargetID)
|
||||
}
|
||||
|
||||
// Update Token Permission Mode
|
||||
tokenPermissionMode, tokenPermissionModeValid := util.EnumValue(repo_model.ActionsTokenPermissionMode(ctx.FormString("token_permission_mode")))
|
||||
if tokenPermissionModeValid {
|
||||
actionsCfg.TokenPermissionMode = tokenPermissionMode
|
||||
enableMaxPermissions := ctx.FormBool("enable_max_permissions")
|
||||
// Update Maximum Permissions (radio buttons: none/read/write)
|
||||
if enableMaxPermissions {
|
||||
actionsCfg.MaxTokenPermissions = ParseMaxTokenPermissions(ctx)
|
||||
} else {
|
||||
actionsCfg.MaxTokenPermissions = nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := actions_model.SetOwnerActionsConfig(ctx, rCtx.OwnerID, actionsCfg); err != nil {
|
||||
ctx.ServerError("SetOwnerActionsConfig", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("settings.saved_successfully"))
|
||||
ctx.JSONRedirect("") // use JSONRedirect because frontend uses form-fetch-action
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
actions_model "gitea.dev/models/actions"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
shared_user "gitea.dev/routers/web/shared/user"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
)
|
||||
|
||||
const (
|
||||
// TODO: Separate secrets from runners when layout is ready
|
||||
tplRepoRunners templates.TplName = "repo/settings/actions"
|
||||
tplOrgRunners templates.TplName = "org/settings/actions"
|
||||
tplAdminRunners templates.TplName = "admin/actions"
|
||||
tplUserRunners templates.TplName = "user/settings/actions"
|
||||
tplRepoRunnerEdit templates.TplName = "repo/settings/runner_edit"
|
||||
tplOrgRunnerEdit templates.TplName = "org/settings/runners_edit"
|
||||
tplAdminRunnerEdit templates.TplName = "admin/runners/edit"
|
||||
tplUserRunnerEdit templates.TplName = "user/settings/runner_edit"
|
||||
)
|
||||
|
||||
type runnersCtx struct {
|
||||
OwnerID int64
|
||||
RepoID int64
|
||||
IsRepo bool
|
||||
IsOrg bool
|
||||
IsAdmin bool
|
||||
IsUser bool
|
||||
RunnersTemplate templates.TplName
|
||||
RunnerEditTemplate templates.TplName
|
||||
RedirectLink string
|
||||
}
|
||||
|
||||
func getRunnersCtx(ctx *context.Context) (*runnersCtx, error) {
|
||||
if ctx.Data["PageIsRepoSettings"] == true {
|
||||
return &runnersCtx{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
OwnerID: 0,
|
||||
IsRepo: true,
|
||||
RunnersTemplate: tplRepoRunners,
|
||||
RunnerEditTemplate: tplRepoRunnerEdit,
|
||||
RedirectLink: ctx.Repo.RepoLink + "/settings/actions/runners/",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if ctx.Data["PageIsOrgSettings"] == true {
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
return nil, fmt.Errorf("RenderUserOrgHeader: %w", err)
|
||||
}
|
||||
return &runnersCtx{
|
||||
RepoID: 0,
|
||||
OwnerID: ctx.Org.Organization.ID,
|
||||
IsOrg: true,
|
||||
RunnersTemplate: tplOrgRunners,
|
||||
RunnerEditTemplate: tplOrgRunnerEdit,
|
||||
RedirectLink: ctx.Org.OrgLink + "/settings/actions/runners/",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if ctx.Data["PageIsAdmin"] == true {
|
||||
return &runnersCtx{
|
||||
RepoID: 0,
|
||||
OwnerID: 0,
|
||||
IsAdmin: true,
|
||||
RunnersTemplate: tplAdminRunners,
|
||||
RunnerEditTemplate: tplAdminRunnerEdit,
|
||||
RedirectLink: setting.AppSubURL + "/-/admin/actions/runners/",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if ctx.Data["PageIsUserSettings"] == true {
|
||||
return &runnersCtx{
|
||||
OwnerID: ctx.Doer.ID,
|
||||
RepoID: 0,
|
||||
IsUser: true,
|
||||
RunnersTemplate: tplUserRunners,
|
||||
RunnerEditTemplate: tplUserRunnerEdit,
|
||||
RedirectLink: setting.AppSubURL + "/user/settings/actions/runners/",
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("unable to set Runners context")
|
||||
}
|
||||
|
||||
// Runners render settings/actions/runners page for repo level
|
||||
func Runners(ctx *context.Context) {
|
||||
ctx.Data["PageIsSharedSettingsRunners"] = true
|
||||
ctx.Data["Title"] = ctx.Tr("actions.actions")
|
||||
ctx.Data["PageType"] = "runners"
|
||||
|
||||
rCtx, err := getRunnersCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getRunnersCtx", err)
|
||||
return
|
||||
}
|
||||
|
||||
page := max(ctx.FormInt("page"), 1)
|
||||
|
||||
opts := actions_model.FindRunnerOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
Page: page,
|
||||
PageSize: 100,
|
||||
},
|
||||
Sort: ctx.Req.URL.Query().Get("sort"),
|
||||
Filter: ctx.Req.URL.Query().Get("q"),
|
||||
}
|
||||
if rCtx.IsRepo {
|
||||
opts.RepoID = rCtx.RepoID
|
||||
opts.WithAvailable = true
|
||||
} else if rCtx.IsOrg || rCtx.IsUser {
|
||||
opts.OwnerID = rCtx.OwnerID
|
||||
opts.WithAvailable = true
|
||||
}
|
||||
|
||||
runners, count, err := db.FindAndCount[actions_model.ActionRunner](ctx, opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("CountRunners", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := actions_model.RunnerList(runners).LoadAttributes(ctx); err != nil {
|
||||
ctx.ServerError("LoadAttributes", err)
|
||||
return
|
||||
}
|
||||
|
||||
// ownid=0,repo_id=0,means this token is used for global
|
||||
var token *actions_model.ActionRunnerToken
|
||||
token, err = actions_model.GetLatestRunnerToken(ctx, opts.OwnerID, opts.RepoID)
|
||||
if errors.Is(err, util.ErrNotExist) || (token != nil && !token.IsActive) {
|
||||
token, err = actions_model.NewRunnerToken(ctx, opts.OwnerID, opts.RepoID)
|
||||
if err != nil {
|
||||
ctx.ServerError("CreateRunnerToken", err)
|
||||
return
|
||||
}
|
||||
} else if err != nil {
|
||||
ctx.ServerError("GetLatestRunnerToken", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Keyword"] = opts.Filter
|
||||
ctx.Data["Runners"] = runners
|
||||
ctx.Data["Total"] = count
|
||||
ctx.Data["RegistrationToken"] = token.Token
|
||||
ctx.Data["RunnerOwnerID"] = opts.OwnerID
|
||||
ctx.Data["RunnerRepoID"] = opts.RepoID
|
||||
ctx.Data["SortType"] = opts.Sort
|
||||
ctx.Data["AllowBulkActions"] = rCtx.IsAdmin
|
||||
|
||||
pager := context.NewPagination(count, opts.PageSize, opts.Page, 5)
|
||||
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
ctx.HTML(http.StatusOK, rCtx.RunnersTemplate)
|
||||
}
|
||||
|
||||
// RunnersEdit renders runner edit page for repository level
|
||||
func RunnersEdit(ctx *context.Context) {
|
||||
ctx.Data["PageIsSharedSettingsRunners"] = true
|
||||
ctx.Data["Title"] = ctx.Tr("actions.runners.edit_runner")
|
||||
rCtx, err := getRunnersCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getRunnersCtx", err)
|
||||
return
|
||||
}
|
||||
|
||||
page := max(ctx.FormInt("page"), 1)
|
||||
|
||||
runnerID := ctx.PathParamInt64("runnerid")
|
||||
ownerID := rCtx.OwnerID
|
||||
repoID := rCtx.RepoID
|
||||
|
||||
runner, err := actions_model.GetRunnerByID(ctx, runnerID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetRunnerByID", err)
|
||||
return
|
||||
}
|
||||
if err := runner.LoadAttributes(ctx); err != nil {
|
||||
ctx.ServerError("LoadAttributes", err)
|
||||
return
|
||||
}
|
||||
if !runner.EditableInContext(ownerID, repoID) {
|
||||
err = errors.New("no permission to edit this runner")
|
||||
ctx.NotFound(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Runner"] = runner
|
||||
|
||||
opts := actions_model.FindTaskOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
Page: page,
|
||||
PageSize: 30,
|
||||
},
|
||||
Status: actions_model.StatusUnknown, // Unknown means all
|
||||
RunnerID: runner.ID,
|
||||
}
|
||||
|
||||
tasks, count, err := db.FindAndCount[actions_model.ActionTask](ctx, opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("CountTasks", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = actions_model.TaskList(tasks).LoadAttributes(ctx); err != nil {
|
||||
ctx.ServerError("TasksLoadAttributes", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Tasks"] = tasks
|
||||
pager := context.NewPagination(count, opts.PageSize, opts.Page, 5)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
ctx.HTML(http.StatusOK, rCtx.RunnerEditTemplate)
|
||||
}
|
||||
|
||||
func RunnersEditPost(ctx *context.Context) {
|
||||
rCtx, err := getRunnersCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getRunnersCtx", err)
|
||||
return
|
||||
}
|
||||
|
||||
runnerID := ctx.PathParamInt64("runnerid")
|
||||
ownerID := rCtx.OwnerID
|
||||
repoID := rCtx.RepoID
|
||||
redirectTo := rCtx.RedirectLink
|
||||
|
||||
runner, err := actions_model.GetRunnerByID(ctx, runnerID)
|
||||
if err != nil {
|
||||
log.Warn("RunnerDetailsEditPost.GetRunnerByID failed: %v, url: %s", err, ctx.Req.URL)
|
||||
ctx.ServerError("RunnerDetailsEditPost.GetRunnerByID", err)
|
||||
return
|
||||
}
|
||||
if !runner.EditableInContext(ownerID, repoID) {
|
||||
ctx.NotFound(util.NewPermissionDeniedErrorf("no permission to edit this runner"))
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*forms.EditRunnerForm)
|
||||
runner.Description = form.Description
|
||||
|
||||
err = actions_model.UpdateRunner(ctx, runner, "description")
|
||||
if err != nil {
|
||||
log.Warn("RunnerDetailsEditPost.UpdateRunner failed: %v, url: %s", err, ctx.Req.URL)
|
||||
ctx.Flash.Warning(ctx.Tr("actions.runners.update_runner_failed"))
|
||||
ctx.Redirect(redirectTo)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug("RunnerDetailsEditPost success: %s", ctx.Req.URL)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("actions.runners.update_runner_success"))
|
||||
ctx.Redirect(redirectTo)
|
||||
}
|
||||
|
||||
func ResetRunnerRegistrationToken(ctx *context.Context) {
|
||||
rCtx, err := getRunnersCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getRunnersCtx", err)
|
||||
return
|
||||
}
|
||||
|
||||
ownerID := rCtx.OwnerID
|
||||
repoID := rCtx.RepoID
|
||||
redirectTo := rCtx.RedirectLink
|
||||
|
||||
if _, err := actions_model.NewRunnerToken(ctx, ownerID, repoID); err != nil {
|
||||
ctx.ServerError("ResetRunnerRegistrationToken", err)
|
||||
return
|
||||
}
|
||||
ctx.Flash.Success(ctx.Tr("actions.runners.reset_registration_token_success"))
|
||||
ctx.JSONRedirect(redirectTo)
|
||||
}
|
||||
|
||||
// RunnerDeletePost response for deleting runner
|
||||
func RunnerDeletePost(ctx *context.Context) {
|
||||
rCtx, err := getRunnersCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getRunnersCtx", err)
|
||||
return
|
||||
}
|
||||
|
||||
runner := findActionsRunner(ctx, rCtx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if !runner.EditableInContext(rCtx.OwnerID, rCtx.RepoID) {
|
||||
ctx.NotFound(util.NewPermissionDeniedErrorf("no permission to delete this runner"))
|
||||
return
|
||||
}
|
||||
|
||||
successRedirectTo := rCtx.RedirectLink
|
||||
failedRedirectTo := rCtx.RedirectLink + url.PathEscape(ctx.PathParam("runnerid"))
|
||||
|
||||
if err := actions_model.DeleteRunner(ctx, runner.ID); err != nil {
|
||||
log.Warn("DeleteRunnerPost.UpdateRunner failed: %v, url: %s", err, ctx.Req.URL)
|
||||
ctx.Flash.Warning(ctx.Tr("actions.runners.delete_runner_failed"))
|
||||
|
||||
ctx.JSONRedirect(failedRedirectTo)
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("DeleteRunnerPost success: %s", ctx.Req.URL)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("actions.runners.delete_runner_success"))
|
||||
|
||||
ctx.JSONRedirect(successRedirectTo)
|
||||
}
|
||||
|
||||
func RunnerUpdatePost(ctx *context.Context) {
|
||||
rCtx, err := getRunnersCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getRunnersCtx", err)
|
||||
return
|
||||
}
|
||||
|
||||
runner := findActionsRunner(ctx, rCtx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if !runner.EditableInContext(rCtx.OwnerID, rCtx.RepoID) {
|
||||
ctx.NotFound(util.NewPermissionDeniedErrorf("no permission to edit this runner"))
|
||||
return
|
||||
}
|
||||
|
||||
isDisabled := ctx.FormOptionalBool("disabled")
|
||||
if !isDisabled.Has() {
|
||||
ctx.HTTPError(http.StatusBadRequest, "missing 'disabled' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
successKey := "actions.runners.enable_runner_success"
|
||||
failedKey := "actions.runners.enable_runner_failed"
|
||||
if isDisabled.Value() {
|
||||
successKey = "actions.runners.disable_runner_success"
|
||||
failedKey = "actions.runners.disable_runner_failed"
|
||||
}
|
||||
|
||||
if err := actions_model.SetRunnerDisabled(ctx, runner, isDisabled.Value()); err != nil {
|
||||
log.Warn("RunnerUpdatePost.SetRunnerDisabled failed: %v, url: %s", err, ctx.Req.URL)
|
||||
ctx.Flash.Error(ctx.Tr(failedKey))
|
||||
ctx.JSONRedirect("")
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr(successKey))
|
||||
ctx.JSONRedirect("")
|
||||
}
|
||||
|
||||
// RunnerBulkActionPost performs a bulk action (delete/disable/enable) on multiple runners.
|
||||
// Admin-only: route must be mounted inside the admin runners group; defense-in-depth check below.
|
||||
func RunnerBulkActionPost(ctx *context.Context) {
|
||||
rCtx, err := getRunnersCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getRunnersCtx", err)
|
||||
return
|
||||
}
|
||||
|
||||
var runnerIDs []int64
|
||||
if rCtx.IsAdmin {
|
||||
// ATTENTION: it completely depends on the assumption that the doer is "site admin"
|
||||
// So it doesn't do extra permission check to the runner IDs
|
||||
// In the future, if you need to support such operation on non-admin pages, be careful!
|
||||
runnerIDs = ctx.FormStringInt64s("ids")
|
||||
} else {
|
||||
ctx.HTTPError(http.StatusForbidden, "bulk actions are admin-only")
|
||||
return
|
||||
}
|
||||
|
||||
action := ctx.FormString("action")
|
||||
var successKey, failedKey string
|
||||
switch action {
|
||||
case "delete":
|
||||
successKey, failedKey = "actions.runners.delete_runner_success", "actions.runners.delete_runner_failed"
|
||||
case "disable":
|
||||
successKey, failedKey = "actions.runners.disable_runner_success", "actions.runners.disable_runner_failed"
|
||||
case "enable":
|
||||
successKey, failedKey = "actions.runners.enable_runner_success", "actions.runners.enable_runner_failed"
|
||||
default:
|
||||
ctx.HTTPError(http.StatusBadRequest, "invalid action")
|
||||
return
|
||||
}
|
||||
|
||||
runners, err := db.Find[actions_model.ActionRunner](ctx, &actions_model.FindRunnerOptions{IDs: runnerIDs})
|
||||
if err != nil {
|
||||
ctx.ServerError("FindRunners", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = db.WithTx(ctx, func(txCtx stdctx.Context) error {
|
||||
for _, r := range runners {
|
||||
switch action {
|
||||
case "delete":
|
||||
if err := actions_model.DeleteRunner(txCtx, r.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
case "disable":
|
||||
if err := actions_model.SetRunnerDisabled(txCtx, r, true); err != nil {
|
||||
return err
|
||||
}
|
||||
case "enable":
|
||||
if err := actions_model.SetRunnerDisabled(txCtx, r, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Warn("RunnerBulkActionPost.%s failed: %v, url: %s", action, err, ctx.Req.URL)
|
||||
ctx.Flash.Error(ctx.Tr(failedKey))
|
||||
ctx.JSONRedirect(rCtx.RedirectLink)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr(successKey))
|
||||
ctx.JSONRedirect(rCtx.RedirectLink)
|
||||
}
|
||||
|
||||
func findActionsRunner(ctx *context.Context, rCtx *runnersCtx) *actions_model.ActionRunner {
|
||||
runnerID := ctx.PathParamInt64("runnerid")
|
||||
opts := &actions_model.FindRunnerOptions{
|
||||
IDs: []int64{runnerID},
|
||||
}
|
||||
switch {
|
||||
case rCtx.IsRepo:
|
||||
opts.RepoID = rCtx.RepoID
|
||||
if opts.RepoID == 0 {
|
||||
panic("repoID is 0")
|
||||
}
|
||||
case rCtx.IsOrg, rCtx.IsUser:
|
||||
opts.OwnerID = rCtx.OwnerID
|
||||
if opts.OwnerID == 0 {
|
||||
panic("ownerID is 0")
|
||||
}
|
||||
case rCtx.IsAdmin:
|
||||
// do nothing
|
||||
default:
|
||||
panic("invalid actions runner context")
|
||||
}
|
||||
|
||||
got, err := db.Find[actions_model.ActionRunner](ctx, opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("FindRunner", err)
|
||||
return nil
|
||||
} else if len(got) == 0 {
|
||||
ctx.NotFound(errors.New("runner not found"))
|
||||
return nil
|
||||
}
|
||||
|
||||
return got[0]
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
actions_model "gitea.dev/models/actions"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/web"
|
||||
shared_user "gitea.dev/routers/web/shared/user"
|
||||
actions_service "gitea.dev/services/actions"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
)
|
||||
|
||||
const (
|
||||
tplRepoVariables templates.TplName = "repo/settings/actions"
|
||||
tplOrgVariables templates.TplName = "org/settings/actions"
|
||||
tplUserVariables templates.TplName = "user/settings/actions"
|
||||
tplAdminVariables templates.TplName = "admin/actions"
|
||||
)
|
||||
|
||||
type variablesCtx struct {
|
||||
OwnerID int64
|
||||
RepoID int64
|
||||
IsRepo bool
|
||||
IsOrg bool
|
||||
IsUser bool
|
||||
IsGlobal bool
|
||||
VariablesTemplate templates.TplName
|
||||
RedirectLink string
|
||||
}
|
||||
|
||||
func getVariablesCtx(ctx *context.Context) (*variablesCtx, error) {
|
||||
if ctx.Data["PageIsRepoSettings"] == true {
|
||||
return &variablesCtx{
|
||||
OwnerID: 0,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
IsRepo: true,
|
||||
VariablesTemplate: tplRepoVariables,
|
||||
RedirectLink: ctx.Repo.RepoLink + "/settings/actions/variables",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if ctx.Data["PageIsOrgSettings"] == true {
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return nil, nil //nolint:nilnil // error is already handled by ctx.ServerError
|
||||
}
|
||||
return &variablesCtx{
|
||||
OwnerID: ctx.ContextUser.ID,
|
||||
RepoID: 0,
|
||||
IsOrg: true,
|
||||
VariablesTemplate: tplOrgVariables,
|
||||
RedirectLink: ctx.Org.OrgLink + "/settings/actions/variables",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if ctx.Data["PageIsUserSettings"] == true {
|
||||
return &variablesCtx{
|
||||
OwnerID: ctx.Doer.ID,
|
||||
RepoID: 0,
|
||||
IsUser: true,
|
||||
VariablesTemplate: tplUserVariables,
|
||||
RedirectLink: setting.AppSubURL + "/user/settings/actions/variables",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if ctx.Data["PageIsAdmin"] == true {
|
||||
return &variablesCtx{
|
||||
OwnerID: 0,
|
||||
RepoID: 0,
|
||||
IsGlobal: true,
|
||||
VariablesTemplate: tplAdminVariables,
|
||||
RedirectLink: setting.AppSubURL + "/-/admin/actions/variables",
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("unable to set Variables context")
|
||||
}
|
||||
|
||||
func Variables(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("actions.variables")
|
||||
ctx.Data["PageType"] = "variables"
|
||||
ctx.Data["PageIsSharedSettingsVariables"] = true
|
||||
|
||||
vCtx, err := getVariablesCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getVariablesCtx", err)
|
||||
return
|
||||
}
|
||||
|
||||
variables, err := db.Find[actions_model.ActionVariable](ctx, actions_model.FindVariablesOpts{
|
||||
OwnerID: vCtx.OwnerID,
|
||||
RepoID: vCtx.RepoID,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("FindVariables", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Variables"] = variables
|
||||
ctx.Data["DataMaxLength"] = actions_model.VariableDataMaxLength
|
||||
ctx.Data["DescriptionMaxLength"] = actions_model.VariableDescriptionMaxLength
|
||||
ctx.HTML(http.StatusOK, vCtx.VariablesTemplate)
|
||||
}
|
||||
|
||||
func VariableCreate(ctx *context.Context) {
|
||||
vCtx, err := getVariablesCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getVariablesCtx", err)
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.HasError() { // form binding validation error
|
||||
ctx.JSONError(ctx.GetErrMsg())
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*forms.EditVariableForm)
|
||||
|
||||
v, err := actions_service.CreateVariable(ctx, vCtx.OwnerID, vCtx.RepoID, form.Name, form.Data, form.Description)
|
||||
if err != nil {
|
||||
log.Error("CreateVariable: %v", err)
|
||||
ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("actions.variables.creation.success", v.Name))
|
||||
ctx.JSONRedirect(vCtx.RedirectLink)
|
||||
}
|
||||
|
||||
func VariableUpdate(ctx *context.Context) {
|
||||
vCtx, err := getVariablesCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getVariablesCtx", err)
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.HasError() { // form binding validation error
|
||||
ctx.JSONError(ctx.GetErrMsg())
|
||||
return
|
||||
}
|
||||
|
||||
id := ctx.PathParamInt64("variable_id")
|
||||
|
||||
variable := findActionsVariable(ctx, id, vCtx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*forms.EditVariableForm)
|
||||
variable.Name = form.Name
|
||||
variable.Data = form.Data
|
||||
variable.Description = form.Description
|
||||
|
||||
if ok, err := actions_service.UpdateVariableNameData(ctx, variable); err != nil || !ok {
|
||||
log.Error("UpdateVariable: %v", err)
|
||||
ctx.JSONError(ctx.Tr("actions.variables.update.failed"))
|
||||
return
|
||||
}
|
||||
ctx.Flash.Success(ctx.Tr("actions.variables.update.success"))
|
||||
ctx.JSONRedirect(vCtx.RedirectLink)
|
||||
}
|
||||
|
||||
func findActionsVariable(ctx *context.Context, id int64, vCtx *variablesCtx) *actions_model.ActionVariable {
|
||||
opts := actions_model.FindVariablesOpts{
|
||||
IDs: []int64{id},
|
||||
}
|
||||
switch {
|
||||
case vCtx.IsRepo:
|
||||
opts.RepoID = vCtx.RepoID
|
||||
if opts.RepoID == 0 {
|
||||
panic("RepoID is 0")
|
||||
}
|
||||
case vCtx.IsOrg, vCtx.IsUser:
|
||||
opts.OwnerID = vCtx.OwnerID
|
||||
if opts.OwnerID == 0 {
|
||||
panic("OwnerID is 0")
|
||||
}
|
||||
case vCtx.IsGlobal:
|
||||
// do nothing
|
||||
default:
|
||||
panic("invalid actions variable")
|
||||
}
|
||||
|
||||
got, err := actions_model.FindVariables(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("FindVariables", err)
|
||||
return nil
|
||||
} else if len(got) == 0 {
|
||||
ctx.NotFound(nil)
|
||||
return nil
|
||||
}
|
||||
return got[0]
|
||||
}
|
||||
|
||||
func VariableDelete(ctx *context.Context) {
|
||||
vCtx, err := getVariablesCtx(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("getVariablesCtx", err)
|
||||
return
|
||||
}
|
||||
|
||||
id := ctx.PathParamInt64("variable_id")
|
||||
|
||||
variable := findActionsVariable(ctx, id, vCtx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := actions_service.DeleteVariableByID(ctx, variable.ID); err != nil {
|
||||
log.Error("Delete variable [%d] failed: %v", id, err)
|
||||
ctx.JSONError(ctx.Tr("actions.variables.deletion.failed"))
|
||||
return
|
||||
}
|
||||
ctx.Flash.Success(ctx.Tr("actions.variables.deletion.success"))
|
||||
ctx.JSONRedirect(vCtx.RedirectLink)
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package issue
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/base"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
// PrepareFilterIssueLabels reads the "labels" query parameter, sets `ctx.Data["Labels"]` and `ctx.Data["SelectLabels"]`
|
||||
func PrepareFilterIssueLabels(ctx *context.Context, repoID int64, owner *user_model.User) (ret struct {
|
||||
AllLabels []*issues_model.Label
|
||||
SelectedLabelIDs []int64
|
||||
},
|
||||
) {
|
||||
// 1,-2 means including label 1 and excluding label 2
|
||||
// 0 means issues with no label
|
||||
// blank means labels will not be filtered for issues
|
||||
selectLabels := ctx.FormString("labels")
|
||||
if selectLabels != "" {
|
||||
var err error
|
||||
ret.SelectedLabelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ","))
|
||||
if err != nil {
|
||||
ctx.Flash.Error(ctx.Tr("invalid_data", selectLabels), true)
|
||||
}
|
||||
}
|
||||
|
||||
var allLabels []*issues_model.Label
|
||||
if repoID != 0 {
|
||||
repoLabels, err := issues_model.GetLabelsByRepoID(ctx, repoID, "", db.ListOptions{})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLabelsByRepoID", err)
|
||||
return ret
|
||||
}
|
||||
allLabels = append(allLabels, repoLabels...)
|
||||
}
|
||||
|
||||
if owner != nil && owner.IsOrganization() {
|
||||
orgLabels, err := issues_model.GetLabelsByOrgID(ctx, owner.ID, "", db.ListOptions{})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLabelsByOrgID", err)
|
||||
return ret
|
||||
}
|
||||
allLabels = append(allLabels, orgLabels...)
|
||||
}
|
||||
|
||||
// Get the exclusive scope for every label ID
|
||||
labelExclusiveScopes := make([]string, 0, len(ret.SelectedLabelIDs))
|
||||
for _, labelID := range ret.SelectedLabelIDs {
|
||||
foundExclusiveScope := false
|
||||
for _, label := range allLabels {
|
||||
if label.ID == labelID || label.ID == -labelID {
|
||||
labelExclusiveScopes = append(labelExclusiveScopes, label.ExclusiveScope())
|
||||
foundExclusiveScope = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundExclusiveScope {
|
||||
labelExclusiveScopes = append(labelExclusiveScopes, "")
|
||||
}
|
||||
}
|
||||
|
||||
for _, l := range allLabels {
|
||||
l.LoadSelectedLabelsAfterClick(ret.SelectedLabelIDs, labelExclusiveScopes)
|
||||
}
|
||||
ctx.Data["Labels"] = allLabels
|
||||
ctx.Data["SelectLabels"] = selectLabels
|
||||
ret.AllLabels = allLabels
|
||||
return ret
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package label
|
||||
|
||||
import (
|
||||
"gitea.dev/modules/label"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
)
|
||||
|
||||
func GetLabelEditForm(ctx *context.Context) *forms.CreateLabelForm {
|
||||
form := web.GetForm(ctx).(*forms.CreateLabelForm)
|
||||
if ctx.HasError() {
|
||||
ctx.JSONError(ctx.GetErrMsg())
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
form.Color, err = label.NormalizeColor(form.Color)
|
||||
if err != nil {
|
||||
ctx.JSONError(ctx.Tr("repo.issues.label_color_invalid"))
|
||||
return nil
|
||||
}
|
||||
return form
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package mention
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitea.dev/models/organization"
|
||||
user_model "gitea.dev/models/user"
|
||||
)
|
||||
|
||||
// Mention is the JSON structure returned by mention autocomplete endpoints.
|
||||
type Mention struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
Name string `json:"name"`
|
||||
FullName string `json:"fullname"`
|
||||
Avatar string `json:"avatar"`
|
||||
}
|
||||
|
||||
// Collector builds a deduplicated list of Mention entries.
|
||||
type Collector struct {
|
||||
seen map[string]bool
|
||||
Result []Mention
|
||||
}
|
||||
|
||||
// NewCollector creates a new Collector.
|
||||
func NewCollector() *Collector {
|
||||
return &Collector{seen: make(map[string]bool)}
|
||||
}
|
||||
|
||||
// AddUsers adds user mentions, skipping duplicates.
|
||||
func (c *Collector) AddUsers(ctx context.Context, users []*user_model.User) {
|
||||
for _, u := range users {
|
||||
if !c.seen[u.Name] {
|
||||
c.seen[u.Name] = true
|
||||
c.Result = append(c.Result, Mention{
|
||||
Key: u.Name + " " + u.FullName,
|
||||
Value: u.Name,
|
||||
Name: u.Name,
|
||||
FullName: u.FullName,
|
||||
Avatar: u.AvatarLink(ctx),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AddMentionableTeams loads and adds team mentions for the given owner (if it's an org).
|
||||
func (c *Collector) AddMentionableTeams(ctx context.Context, doer, owner *user_model.User) error {
|
||||
if doer == nil || !owner.IsOrganization() {
|
||||
return nil
|
||||
}
|
||||
|
||||
org := organization.OrgFromUser(owner)
|
||||
isAdmin := doer.IsAdmin
|
||||
if !isAdmin {
|
||||
var err error
|
||||
isAdmin, err = org.IsOwnedBy(ctx, doer.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var teams []*organization.Team
|
||||
var err error
|
||||
if isAdmin {
|
||||
teams, err = org.LoadTeams(ctx)
|
||||
} else {
|
||||
teams, err = org.GetUserTeams(ctx, doer.ID)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, team := range teams {
|
||||
key := owner.Name + "/" + team.Name
|
||||
if !c.seen[key] {
|
||||
c.seen[key] = true
|
||||
c.Result = append(c.Result, Mention{
|
||||
Key: key,
|
||||
Value: key,
|
||||
Name: key,
|
||||
Avatar: owner.AvatarLink(ctx),
|
||||
})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package packages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
packages_model "gitea.dev/models/packages"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
cargo_service "gitea.dev/services/packages/cargo"
|
||||
container_service "gitea.dev/services/packages/container"
|
||||
)
|
||||
|
||||
func SetPackagesContext(ctx *context.Context, owner *user_model.User) {
|
||||
pcrs, err := packages_model.GetCleanupRulesByOwner(ctx, owner.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetCleanupRulesByOwner", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["CleanupRules"] = pcrs
|
||||
}
|
||||
|
||||
func SetRuleAddContext(ctx *context.Context) {
|
||||
setRuleEditContext(ctx, nil)
|
||||
}
|
||||
|
||||
func SetRuleEditContext(ctx *context.Context, owner *user_model.User) {
|
||||
pcr := getCleanupRuleByContext(ctx, owner)
|
||||
if pcr == nil {
|
||||
return
|
||||
}
|
||||
|
||||
setRuleEditContext(ctx, pcr)
|
||||
}
|
||||
|
||||
func setRuleEditContext(ctx *context.Context, pcr *packages_model.PackageCleanupRule) {
|
||||
ctx.Data["IsEditRule"] = pcr != nil
|
||||
|
||||
if pcr == nil {
|
||||
pcr = &packages_model.PackageCleanupRule{}
|
||||
}
|
||||
ctx.Data["CleanupRule"] = pcr
|
||||
ctx.Data["AvailableTypes"] = packages_model.TypeList
|
||||
}
|
||||
|
||||
func PerformRuleAddPost(ctx *context.Context, owner *user_model.User, redirectURL string, template templates.TplName) {
|
||||
performRuleEditPost(ctx, owner, nil, redirectURL, template)
|
||||
}
|
||||
|
||||
func PerformRuleEditPost(ctx *context.Context, owner *user_model.User, redirectURL string, template templates.TplName) {
|
||||
pcr := getCleanupRuleByContext(ctx, owner)
|
||||
if pcr == nil {
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*forms.PackageCleanupRuleForm)
|
||||
|
||||
if form.Action == "remove" {
|
||||
if err := packages_model.DeleteCleanupRuleByID(ctx, pcr.ID); err != nil {
|
||||
ctx.ServerError("DeleteCleanupRuleByID", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("packages.owner.settings.cleanuprules.success.delete"))
|
||||
ctx.Redirect(redirectURL)
|
||||
} else {
|
||||
performRuleEditPost(ctx, owner, pcr, redirectURL, template)
|
||||
}
|
||||
}
|
||||
|
||||
func performRuleEditPost(ctx *context.Context, owner *user_model.User, pcr *packages_model.PackageCleanupRule, redirectURL string, template templates.TplName) {
|
||||
isEditRule := pcr != nil
|
||||
|
||||
if pcr == nil {
|
||||
pcr = &packages_model.PackageCleanupRule{}
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*forms.PackageCleanupRuleForm)
|
||||
|
||||
pcr.Enabled = form.Enabled
|
||||
pcr.OwnerID = owner.ID
|
||||
pcr.KeepCount = form.KeepCount
|
||||
pcr.KeepPattern = form.KeepPattern
|
||||
pcr.RemoveDays = form.RemoveDays
|
||||
pcr.RemovePattern = form.RemovePattern
|
||||
pcr.MatchFullName = form.MatchFullName
|
||||
|
||||
ctx.Data["IsEditRule"] = isEditRule
|
||||
ctx.Data["CleanupRule"] = pcr
|
||||
ctx.Data["AvailableTypes"] = packages_model.TypeList
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.HTML(http.StatusOK, template)
|
||||
return
|
||||
}
|
||||
|
||||
if isEditRule {
|
||||
if err := packages_model.UpdateCleanupRule(ctx, pcr); err != nil {
|
||||
ctx.ServerError("UpdateCleanupRule", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
pcr.Type = packages_model.Type(form.Type)
|
||||
|
||||
if has, err := packages_model.HasOwnerCleanupRuleForPackageType(ctx, owner.ID, pcr.Type); err != nil {
|
||||
ctx.ServerError("HasOwnerCleanupRuleForPackageType", err)
|
||||
return
|
||||
} else if has {
|
||||
ctx.Data["Err_Type"] = true
|
||||
ctx.HTML(http.StatusOK, template)
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
if pcr, err = packages_model.InsertCleanupRule(ctx, pcr); err != nil {
|
||||
ctx.ServerError("InsertCleanupRule", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("packages.owner.settings.cleanuprules.success.update"))
|
||||
ctx.Redirect(fmt.Sprintf("%s/rules/%d", redirectURL, pcr.ID))
|
||||
}
|
||||
|
||||
func SetRulePreviewContext(ctx *context.Context, owner *user_model.User) {
|
||||
pcr := getCleanupRuleByContext(ctx, owner)
|
||||
if pcr == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := pcr.CompiledPattern(); err != nil {
|
||||
ctx.ServerError("CompiledPattern", err)
|
||||
return
|
||||
}
|
||||
|
||||
olderThan := time.Now().AddDate(0, 0, -pcr.RemoveDays)
|
||||
|
||||
packages, err := packages_model.GetPackagesByType(ctx, pcr.OwnerID, pcr.Type)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetPackagesByType", err)
|
||||
return
|
||||
}
|
||||
|
||||
versionsToRemove := make([]*packages_model.PackageDescriptor, 0, 10)
|
||||
|
||||
for _, p := range packages {
|
||||
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
PackageID: p.ID,
|
||||
IsInternal: optional.Some(false),
|
||||
Sort: packages_model.SortCreatedDesc,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("SearchVersions", err)
|
||||
return
|
||||
}
|
||||
if pcr.KeepCount > 0 {
|
||||
if pcr.KeepCount < len(pvs) {
|
||||
pvs = pvs[pcr.KeepCount:]
|
||||
} else {
|
||||
pvs = nil
|
||||
}
|
||||
}
|
||||
for _, pv := range pvs {
|
||||
if skip, err := container_service.ShouldBeSkipped(ctx, pcr, p, pv); err != nil {
|
||||
ctx.ServerError("ShouldBeSkipped", err)
|
||||
return
|
||||
} else if skip {
|
||||
continue
|
||||
}
|
||||
|
||||
toMatch := pv.LowerVersion
|
||||
if pcr.MatchFullName {
|
||||
toMatch = p.LowerName + "/" + pv.LowerVersion
|
||||
}
|
||||
if pcr.KeepPatternMatcher != nil && pcr.KeepPatternMatcher.MatchString(toMatch) {
|
||||
continue
|
||||
}
|
||||
if pv.CreatedUnix.AsLocalTime().After(olderThan) {
|
||||
continue
|
||||
}
|
||||
if pcr.RemovePatternMatcher != nil && !pcr.RemovePatternMatcher.MatchString(toMatch) {
|
||||
continue
|
||||
}
|
||||
|
||||
pd, err := packages_model.GetPackageDescriptor(ctx, pv)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetPackageDescriptor", err)
|
||||
return
|
||||
}
|
||||
versionsToRemove = append(versionsToRemove, pd)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data["CleanupRule"] = pcr
|
||||
ctx.Data["VersionsToRemove"] = versionsToRemove
|
||||
}
|
||||
|
||||
func getCleanupRuleByContext(ctx *context.Context, owner *user_model.User) *packages_model.PackageCleanupRule {
|
||||
id := ctx.FormInt64("id")
|
||||
if id == 0 {
|
||||
id = ctx.PathParamInt64("id")
|
||||
}
|
||||
|
||||
pcr, err := packages_model.GetCleanupRuleByID(ctx, id)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrPackageCleanupRuleNotExist {
|
||||
ctx.NotFound(err)
|
||||
} else {
|
||||
ctx.ServerError("GetCleanupRuleByID", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if pcr != nil && pcr.OwnerID == owner.ID {
|
||||
return pcr
|
||||
}
|
||||
|
||||
ctx.NotFound(fmt.Errorf("PackageCleanupRule[%v] not associated to owner %v", id, owner))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func InitializeCargoIndex(ctx *context.Context, owner *user_model.User) {
|
||||
err := cargo_service.InitializeIndexRepository(ctx, owner, owner)
|
||||
if err != nil {
|
||||
log.Error("InitializeIndexRepository failed: %v", err)
|
||||
ctx.Flash.Error(ctx.Tr("packages.owner.settings.cargo.initialize.error", err))
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("packages.owner.settings.cargo.initialize.success"))
|
||||
}
|
||||
}
|
||||
|
||||
func RebuildCargoIndex(ctx *context.Context, owner *user_model.User) {
|
||||
err := cargo_service.RebuildIndex(ctx, owner, owner)
|
||||
if err != nil {
|
||||
log.Error("RebuildIndex failed: %v", err)
|
||||
ctx.Flash.Error(ctx.Tr("packages.owner.settings.cargo.rebuild.error", err))
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("packages.owner.settings.cargo.rebuild.success"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package project
|
||||
|
||||
import (
|
||||
project_model "gitea.dev/models/project"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
// MoveColumns moves or keeps columns in a project and sorts them inside that project
|
||||
func MoveColumns(ctx *context.Context) {
|
||||
project, err := project_model.GetProjectByID(ctx, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetProjectByID", project_model.IsErrProjectNotExist, err)
|
||||
return
|
||||
}
|
||||
if !project.CanBeAccessedByOwnerRepo(ctx.ContextUser.ID, ctx.Repo.Repository) {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
type movedColumnsForm struct {
|
||||
Columns []struct {
|
||||
ColumnID int64 `json:"columnID"`
|
||||
Sorting int64 `json:"sorting"`
|
||||
} `json:"columns"`
|
||||
}
|
||||
|
||||
form := &movedColumnsForm{}
|
||||
if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil {
|
||||
ctx.ServerError("DecodeMovedColumnsForm", err)
|
||||
return
|
||||
}
|
||||
|
||||
sortedColumnIDs := make(map[int64]int64)
|
||||
for _, column := range form.Columns {
|
||||
sortedColumnIDs[column.Sorting] = column.ColumnID
|
||||
}
|
||||
|
||||
if err = project_model.MoveColumnsOnProject(ctx, project, sortedColumnIDs); err != nil {
|
||||
ctx.ServerError("MoveColumnsOnProject", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSONOK()
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"gitea.dev/models/db"
|
||||
secret_model "gitea.dev/models/secret"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
secret_service "gitea.dev/services/secrets"
|
||||
)
|
||||
|
||||
func SetSecretsContext(ctx *context.Context, ownerID, repoID int64) {
|
||||
secrets, err := db.Find[secret_model.Secret](ctx, secret_model.FindSecretsOptions{OwnerID: ownerID, RepoID: repoID})
|
||||
if err != nil {
|
||||
ctx.ServerError("FindSecrets", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Secrets"] = secrets
|
||||
ctx.Data["DataMaxLength"] = secret_model.SecretDataMaxLength
|
||||
ctx.Data["DescriptionMaxLength"] = secret_model.SecretDescriptionMaxLength
|
||||
}
|
||||
|
||||
func PerformSecretsPost(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
|
||||
form := web.GetForm(ctx).(*forms.AddSecretForm)
|
||||
|
||||
s, _, err := secret_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, util.NormalizeStringEOL(form.Data), form.Description)
|
||||
if err != nil {
|
||||
log.Error("CreateOrUpdateSecret failed: %v", err)
|
||||
ctx.JSONError(ctx.Tr("secrets.save_failed"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("secrets.save_success", s.Name))
|
||||
ctx.JSONRedirect(redirectURL)
|
||||
}
|
||||
|
||||
func PerformSecretsDelete(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
|
||||
id := ctx.FormInt64("id")
|
||||
|
||||
err := secret_service.DeleteSecretByID(ctx, ownerID, repoID, id)
|
||||
if err != nil {
|
||||
log.Error("DeleteSecretByID(%d) failed: %v", id, err)
|
||||
ctx.JSONError(ctx.Tr("secrets.deletion.failed"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("secrets.deletion.success"))
|
||||
ctx.JSONRedirect(redirectURL)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
user_service "gitea.dev/services/user"
|
||||
)
|
||||
|
||||
func BlockedUsers(ctx *context.Context, blocker *user_model.User) {
|
||||
blocks, _, err := user_model.FindBlockings(ctx, &user_model.FindBlockingOptions{
|
||||
BlockerID: blocker.ID,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("FindBlockings", err)
|
||||
return
|
||||
}
|
||||
if err := user_model.BlockingList(blocks).LoadAttributes(ctx); err != nil {
|
||||
ctx.ServerError("LoadAttributes", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["UserBlocks"] = blocks
|
||||
}
|
||||
|
||||
func blockedUsersPost(ctx *context.Context, form *forms.BlockUserForm, blocker *user_model.User) error {
|
||||
blockee, err := user_model.GetUserByName(ctx, form.Blockee)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch form.Action {
|
||||
case "block":
|
||||
err = user_service.BlockUser(ctx, ctx.Doer, blocker, blockee, form.Note)
|
||||
if errors.Is(err, util.ErrInvalidArgument) {
|
||||
return util.ErrorWrapTranslatable(err, "user.block.block.failure", err.Error())
|
||||
}
|
||||
return err
|
||||
case "unblock":
|
||||
err = user_service.UnblockUser(ctx, ctx.Doer, blocker, blockee)
|
||||
if errors.Is(err, util.ErrInvalidArgument) {
|
||||
return util.ErrorWrapTranslatable(err, "user.block.unblock.failure", err.Error())
|
||||
}
|
||||
return err
|
||||
case "note":
|
||||
block, err := user_model.GetBlocking(ctx, blocker.ID, blockee.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return user_model.UpdateBlockingNote(ctx, block.ID, form.Note)
|
||||
}
|
||||
setting.PanicInDevOrTesting("Unknown action: %q", form.Action)
|
||||
return errors.New("unknown action")
|
||||
}
|
||||
|
||||
func BlockedUsersPost(ctx *context.Context, blocker *user_model.User, redirect string) {
|
||||
if ctx.HasError() {
|
||||
ctx.JSONError(ctx.GetErrMsg())
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*forms.BlockUserForm)
|
||||
err := blockedUsersPost(ctx, form, blocker)
|
||||
if err == nil {
|
||||
ctx.JSONRedirect(redirect)
|
||||
} else if errTr := util.ErrorAsTranslatable(err); errTr != nil {
|
||||
ctx.JSONError(errTr.Translate(ctx.Locale))
|
||||
} else if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.JSONError(ctx.Locale.Tr("error.not_found"))
|
||||
} else {
|
||||
ctx.ServerError("BlockedUsersPost", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/organization"
|
||||
access_model "gitea.dev/models/perm/access"
|
||||
project_model "gitea.dev/models/project"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/gitrepo"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/markup"
|
||||
"gitea.dev/modules/markup/markdown"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
// prepareContextForProfileBigAvatar set the context for big avatar view on the profile page
|
||||
func prepareContextForProfileBigAvatar(ctx *context.Context) {
|
||||
ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
|
||||
ctx.Data["ShowUserEmail"] = setting.UI.ShowUserEmail && ctx.ContextUser.Email != "" && ctx.IsSigned && !ctx.ContextUser.KeepEmailPrivate
|
||||
if setting.Service.UserLocationMapURL != "" {
|
||||
ctx.Data["ContextUserLocationMapURL"] = setting.Service.UserLocationMapURL + url.QueryEscape(ctx.ContextUser.Location)
|
||||
}
|
||||
// Show OpenID URIs
|
||||
openIDs, err := user_model.GetUserOpenIDs(ctx, ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserOpenIDs", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["OpenIDs"] = openIDs
|
||||
if len(ctx.ContextUser.Description) != 0 {
|
||||
content, err := markdown.RenderString(markup.NewRenderContext(ctx).WithMetas(markup.ComposeSimpleDocumentMetas()), ctx.ContextUser.Description)
|
||||
if err != nil {
|
||||
ctx.ServerError("RenderString", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["RenderedDescription"] = content
|
||||
}
|
||||
|
||||
orgs, err := db.Find[organization.Organization](ctx, organization.FindOrgOptions{
|
||||
UserID: ctx.ContextUser.ID,
|
||||
IncludeVisibility: organization.DoerViewOtherVisibility(ctx.Doer, ctx.ContextUser),
|
||||
ListOptions: db.ListOptions{
|
||||
Page: 1,
|
||||
// query one more result (without a separate counting) to see whether we need to add the "show more orgs" link
|
||||
PageSize: setting.UI.User.OrgPagingNum + 1,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("FindOrgs", err)
|
||||
return
|
||||
}
|
||||
if len(orgs) > setting.UI.User.OrgPagingNum {
|
||||
orgs = orgs[:setting.UI.User.OrgPagingNum]
|
||||
ctx.Data["ShowMoreOrgs"] = true
|
||||
}
|
||||
ctx.Data["Orgs"] = orgs
|
||||
ctx.Data["HasOrgsVisible"] = organization.HasOrgsVisible(ctx, orgs, ctx.Doer)
|
||||
|
||||
badges, _, err := user_model.GetUserBadges(ctx, ctx.ContextUser)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserBadges", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Badges"] = badges
|
||||
|
||||
// in case the numbers are already provided by other functions, no need to query again (which is slow)
|
||||
if _, ok := ctx.Data["NumFollowers"]; !ok {
|
||||
_, ctx.Data["NumFollowers"], _ = user_model.GetUserFollowers(ctx, ctx.ContextUser, ctx.Doer, db.ListOptions{PageSize: 1, Page: 1})
|
||||
}
|
||||
if _, ok := ctx.Data["NumFollowing"]; !ok {
|
||||
_, ctx.Data["NumFollowing"], _ = user_model.GetUserFollowing(ctx, ctx.ContextUser, ctx.Doer, db.ListOptions{PageSize: 1, Page: 1})
|
||||
}
|
||||
|
||||
if ctx.Doer != nil {
|
||||
ctx.Data["UserBlocking"], err = user_model.GetBlocking(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
|
||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||
ctx.ServerError("GetBlocking", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func FindOwnerProfileReadme(ctx *context.Context, doer *user_model.User, optProfileRepoName ...string) (profileDbRepo *repo_model.Repository, profileReadmeBlob *git.Blob) {
|
||||
profileRepoName := util.OptionalArg(optProfileRepoName, RepoNameProfile)
|
||||
profileDbRepo, err := repo_model.GetRepositoryByName(ctx, ctx.ContextUser.ID, profileRepoName)
|
||||
if err != nil {
|
||||
if !repo_model.IsErrRepoNotExist(err) {
|
||||
log.Error("FindOwnerProfileReadme failed to GetRepositoryByName: %v", err)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
perm, err := access_model.GetDoerRepoPermission(ctx, profileDbRepo, doer)
|
||||
if err != nil {
|
||||
log.Error("FindOwnerProfileReadme failed to GetRepositoryByName: %v", err)
|
||||
return nil, nil
|
||||
}
|
||||
if profileDbRepo.IsEmpty || !perm.CanRead(unit.TypeCode) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
profileGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, profileDbRepo)
|
||||
if err != nil {
|
||||
log.Error("FindOwnerProfileReadme failed to OpenRepository: %v", err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
commit, err := profileGitRepo.GetBranchCommit(profileDbRepo.DefaultBranch)
|
||||
if err != nil {
|
||||
log.Error("FindOwnerProfileReadme failed to GetBranchCommit: %v", err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
profileReadmeBlob, _ = commit.GetBlobByPath("README.md") // no need to handle this error
|
||||
return profileDbRepo, profileReadmeBlob
|
||||
}
|
||||
|
||||
type PrepareOwnerHeaderResult struct {
|
||||
ProfilePublicRepo *repo_model.Repository
|
||||
ProfilePublicReadmeBlob *git.Blob
|
||||
ProfilePrivateRepo *repo_model.Repository
|
||||
ProfilePrivateReadmeBlob *git.Blob
|
||||
HasOrgProfileReadme bool
|
||||
}
|
||||
|
||||
const (
|
||||
RepoNameProfilePrivate = ".profile-private"
|
||||
RepoNameProfile = ".profile"
|
||||
)
|
||||
|
||||
func RenderUserOrgHeader(ctx *context.Context) (result *PrepareOwnerHeaderResult, err error) {
|
||||
ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled
|
||||
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
|
||||
ctx.Data["EnableFeed"] = setting.Other.EnableFeed
|
||||
ctx.Data["FeedURL"] = ctx.ContextUser.HomeLink()
|
||||
|
||||
if err := loadHeaderCount(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result = &PrepareOwnerHeaderResult{}
|
||||
if ctx.ContextUser.IsOrganization() {
|
||||
result.ProfilePublicRepo, result.ProfilePublicReadmeBlob = FindOwnerProfileReadme(ctx, ctx.Doer)
|
||||
result.ProfilePrivateRepo, result.ProfilePrivateReadmeBlob = FindOwnerProfileReadme(ctx, ctx.Doer, RepoNameProfilePrivate)
|
||||
result.HasOrgProfileReadme = result.ProfilePublicReadmeBlob != nil || result.ProfilePrivateReadmeBlob != nil
|
||||
ctx.Data["HasOrgProfileReadme"] = result.HasOrgProfileReadme // many pages need it to show the "overview" tab
|
||||
} else {
|
||||
_, profileReadmeBlob := FindOwnerProfileReadme(ctx, ctx.Doer)
|
||||
ctx.Data["HasUserProfileReadme"] = profileReadmeBlob != nil
|
||||
prepareContextForProfileBigAvatar(ctx)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func loadHeaderCount(ctx *context.Context) error {
|
||||
repoCount, err := repo_model.CountRepository(ctx, repo_model.SearchRepoOptions{
|
||||
Actor: ctx.Doer,
|
||||
OwnerID: ctx.ContextUser.ID,
|
||||
Private: ctx.IsSigned,
|
||||
Collaborate: optional.Some(false),
|
||||
IncludeDescription: setting.UI.SearchRepoDescription,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx.Data["RepoCount"] = repoCount
|
||||
|
||||
var projectType project_model.Type
|
||||
if ctx.ContextUser.IsOrganization() {
|
||||
projectType = project_model.TypeOrganization
|
||||
} else {
|
||||
projectType = project_model.TypeIndividual
|
||||
}
|
||||
projectCount, err := db.Count[project_model.Project](ctx, project_model.SearchOptions{
|
||||
OwnerID: ctx.ContextUser.ID,
|
||||
IsClosed: optional.Some(false),
|
||||
Type: projectType,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx.Data["ProjectCount"] = projectCount
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"gitea.dev/models/user"
|
||||
)
|
||||
|
||||
func MakeSelfOnTop(doer *user.User, users []*user.User) []*user.User {
|
||||
if doer != nil {
|
||||
idx := slices.IndexFunc(users, func(u *user.User) bool {
|
||||
return u.ID == doer.ID
|
||||
})
|
||||
if idx > 0 {
|
||||
newUsers := make([]*user.User, len(users))
|
||||
newUsers[0] = users[idx]
|
||||
copy(newUsers[1:], users[:idx])
|
||||
copy(newUsers[idx+1:], users[idx+1:])
|
||||
return newUsers
|
||||
}
|
||||
}
|
||||
return users
|
||||
}
|
||||
|
||||
// GetFilterUserIDByName tries to get the user ID from the given username.
|
||||
// Before, the "issue filter" passes user ID to query the list, but in many cases, it's impossible to pre-fetch the full user list.
|
||||
// So it's better to make it work like GitHub: users could input username directly.
|
||||
// Since it only converts the username to ID directly and is only used internally (to search issues), so no permission check is needed.
|
||||
// Return values:
|
||||
// * "": no filter
|
||||
// * "{the-id}": match the id
|
||||
// * "(none)": match no issue (due to the user doesn't exist)
|
||||
func GetFilterUserIDByName(ctx context.Context, name string) string {
|
||||
if name == "" {
|
||||
return ""
|
||||
}
|
||||
u, err := user.GetUserByName(ctx, name)
|
||||
if err != nil {
|
||||
if id, err := strconv.ParseInt(name, 10, 64); err == nil {
|
||||
return strconv.FormatInt(id, 10)
|
||||
}
|
||||
// The "(none)" is for internal usage only: when doer tries to search non-existing user, use "(none)" to return empty result.
|
||||
return "(none)"
|
||||
}
|
||||
return strconv.FormatInt(u.ID, 10)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dev/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMakeSelfOnTop(t *testing.T) {
|
||||
users := MakeSelfOnTop(nil, []*user.User{{ID: 2}, {ID: 1}})
|
||||
assert.Len(t, users, 2)
|
||||
assert.EqualValues(t, 2, users[0].ID)
|
||||
|
||||
users = MakeSelfOnTop(&user.User{ID: 1}, []*user.User{{ID: 2}, {ID: 1}})
|
||||
assert.Len(t, users, 2)
|
||||
assert.EqualValues(t, 1, users[0].ID)
|
||||
|
||||
users = MakeSelfOnTop(&user.User{ID: 2}, []*user.User{{ID: 2}, {ID: 1}})
|
||||
assert.Len(t, users, 2)
|
||||
assert.EqualValues(t, 2, users[0].ID)
|
||||
}
|
||||
Reference in New Issue
Block a user