feat: update gitea vendor
runner nix smoke / nix label and flake smoke (push) Failing after 1m28s

This commit is contained in:
2026-09-26 21:18:24 +00:00
parent c439c1b948
commit d9b2a4e787
3538 changed files with 116131 additions and 44340 deletions
@@ -8,13 +8,13 @@ import (
"slices"
"strconv"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
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 (
@@ -4,21 +4,22 @@
package actions
import (
stdctx "context"
"errors"
"fmt"
"net/http"
"net/url"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
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 (
@@ -158,8 +159,10 @@ func Runners(ctx *context.Context) {
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)
pager.AddParamFromQuery(ctx.Req.URL.Query())
ctx.Data["Page"] = pager
@@ -362,8 +365,76 @@ func RunnerUpdatePost(ctx *context.Context) {
ctx.JSONRedirect("")
}
func RedirectToDefaultSetting(ctx *context.Context) {
ctx.Redirect(ctx.Repo.RepoLink + "/settings/actions/runners")
// 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
}
if !rCtx.IsAdmin {
ctx.HTTPError(http.StatusForbidden, "bulk actions are admin-only")
return
}
// 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")
if len(runnerIDs) == 0 {
ctx.HTTPError(http.StatusBadRequest, "missing runner IDs")
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 {
@@ -0,0 +1,368 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"errors"
"net/http"
"slices"
"strings"
actions_model "gitea.dev/models/actions"
repo_model "gitea.dev/models/repo"
actions_module "gitea.dev/modules/actions"
"gitea.dev/modules/actions/jobparser"
"gitea.dev/modules/container"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
"gitea.dev/modules/templates"
"gitea.dev/modules/util"
shared_user "gitea.dev/routers/web/shared/user"
actions_service "gitea.dev/services/actions"
"gitea.dev/services/context"
)
const (
tplOrgScopedWorkflows templates.TplName = "org/settings/actions"
tplUserScopedWorkflows templates.TplName = "user/settings/actions"
tplAdminScopedWorkflows templates.TplName = "admin/actions"
)
type scopedWorkflowsCtx struct {
OwnerID int64 // 0 = instance-level
IsOrg bool
IsUser bool
IsGlobal bool
Template templates.TplName
RedirectLink string
// SearchUID is the uid passed to the repo-search box. For org/user it scopes the search to that owner;
// for admin (0) it searches all repos and therefore requires admin access on the route.
SearchUID int64
}
func getScopedWorkflowsCtx(ctx *context.Context) (*scopedWorkflowsCtx, error) {
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 &scopedWorkflowsCtx{
OwnerID: ctx.Org.Organization.ID,
IsOrg: true,
Template: tplOrgScopedWorkflows,
RedirectLink: ctx.Org.OrgLink + "/settings/actions/scoped-workflows",
SearchUID: ctx.Org.Organization.ID,
}, nil
}
if ctx.Data["PageIsUserSettings"] == true {
return &scopedWorkflowsCtx{
OwnerID: ctx.Doer.ID,
IsUser: true,
Template: tplUserScopedWorkflows,
RedirectLink: setting.AppSubURL + "/user/settings/actions/scoped-workflows",
SearchUID: ctx.Doer.ID,
}, nil
}
if ctx.Data["PageIsAdmin"] == true {
return &scopedWorkflowsCtx{
OwnerID: 0,
IsGlobal: true,
Template: tplAdminScopedWorkflows,
RedirectLink: setting.AppSubURL + "/-/admin/actions/scoped-workflows",
SearchUID: 0,
}, nil
}
return nil, errors.New("unable to set scoped workflows context")
}
// scopedWorkflowInfo is one scoped workflow shown on the settings page, merged with its stored merge-gate config.
type scopedWorkflowInfo struct {
EntryName string
DisplayName string
Required bool
Patterns string // newline-joined stored status-check patterns (kept even when not required, as history)
Contexts []string // the commit-status contexts this workflow is expected to post, to preview which patterns match
Missing bool // the workflow file no longer exists on the source default branch, but a stored config lingers and must stay clearable
}
// scopedWorkflowSourceView is the per-source data shown on the settings page.
type scopedWorkflowSourceView struct {
Repo *repo_model.Repository
ScopedWorkflowInfos []scopedWorkflowInfo
}
func ScopedWorkflows(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("actions.scoped_workflows")
ctx.Data["PageType"] = "scoped-workflows"
ctx.Data["PageIsSharedSettingsScopedWorkflows"] = true
swCtx, err := getScopedWorkflowsCtx(ctx)
if err != nil {
ctx.ServerError("getScopedWorkflowsCtx", err)
return
}
if ctx.Written() {
return
}
switch {
case swCtx.IsOrg:
ctx.Data["ScopedWorkflowsDesc"] = ctx.Tr("actions.scoped_workflows.desc_org")
case swCtx.IsUser:
ctx.Data["ScopedWorkflowsDesc"] = ctx.Tr("actions.scoped_workflows.desc_user")
default: // instance-level
ctx.Data["ScopedWorkflowsDesc"] = ctx.Tr("actions.scoped_workflows.desc_global")
}
sources, err := actions_model.GetScopedWorkflowSourcesByOwner(ctx, swCtx.OwnerID)
if err != nil {
ctx.ServerError("GetScopedWorkflowSourcesByOwner", err)
return
}
views := make([]*scopedWorkflowSourceView, 0, len(sources))
for _, src := range sources {
repo, err := repo_model.GetRepositoryByID(ctx, src.SourceRepoID)
if err != nil {
log.Error("scoped workflows settings: load source repo %d: %v", src.SourceRepoID, err)
continue
}
views = append(views, &scopedWorkflowSourceView{
Repo: repo,
ScopedWorkflowInfos: listSourceScopedWorkflowFiles(ctx, repo, src.WorkflowConfigs),
})
}
ctx.Data["ScopedWorkflowSources"] = views
ctx.Data["RepoSearchUID"] = swCtx.SearchUID
// owner/user scopes the repo search to the owner (exclusive);
// instance-level (admin) searches all repos and so must submit owner/name to disambiguate the selection across owners.
ctx.Data["ScopedWorkflowsSearchExclusive"] = !swCtx.IsGlobal
ctx.Data["ScopedWorkflowsSearchFullName"] = swCtx.IsGlobal
ctx.Data["RedirectLink"] = swCtx.RedirectLink
ctx.Data["ScopedWorkflowDirs"] = strings.Join(setting.Actions.ScopedWorkflowDirs, ", ")
ctx.HTML(http.StatusOK, swCtx.Template)
}
// parsePatternLines splits a textarea value into trimmed, non-empty status-check patterns (one per line).
func parsePatternLines(raw string) []string {
var patterns []string
for line := range strings.SplitSeq(raw, "\n") {
if p := strings.TrimSpace(line); p != "" {
patterns = append(patterns, p)
}
}
return patterns
}
// deriveScopedStatusContexts returns the commit-status contexts a scoped workflow is expected to post on a consumer:
// "<source FullName>: <display> / <job> (<event>)" for each parsed job (matrix-expanded, matching run creation) and triggering event.
// Job names that depend on run-context expressions cannot resolve here (no run context) and appear as authored; a glob pattern still matches them.
func deriveScopedStatusContexts(prefix, displayName string, content []byte, events []*jobparser.Event) []string {
parsed, err := jobparser.Parse(content)
if err != nil {
return nil
}
eventNames := make([]string, 0, len(events))
for _, e := range events {
// only events whose runs post a commit status can be a required check; workflow_dispatch, schedule, etc. post none.
if actions_module.ShouldEventCreateCommitStatus(e.Name) {
eventNames = append(eventNames, e.Name)
}
}
seen := make(container.Set[string])
contexts := make([]string, 0, len(parsed)*len(eventNames))
for _, sw := range parsed {
_, job := sw.Job()
if job == nil {
continue
}
jobName := util.EllipsisDisplayString(job.Name, 255) // run creation truncates job names the same way
for _, ev := range eventNames {
ctxName := actions_module.ScopedWorkflowStatusContextName(prefix, displayName, jobName, ev)
if seen.Contains(ctxName) {
continue
}
seen.Add(ctxName)
contexts = append(contexts, ctxName)
}
}
return contexts
}
func listSourceScopedWorkflowFiles(ctx *context.Context, repo *repo_model.Repository, configs map[string]*actions_model.ScopedWorkflowConfig) []scopedWorkflowInfo {
rendered := make(container.Set[string], len(configs))
files := make([]scopedWorkflowInfo, 0, len(configs))
// An empty source repo (or one that fails to parse) has no live workflow files, but a previously-saved config may still linger;
// fall through to surface those as orphan rows below so they remain clearable.
if !repo.IsEmpty {
_, parsed, err := actions_service.LoadParsedScopedWorkflows(ctx, repo)
if err != nil {
log.Error("scoped workflows settings: parse %s: %v", repo.RelativePath(), err)
} else {
for _, p := range parsed {
info := scopedWorkflowInfo{
EntryName: p.EntryName,
DisplayName: p.DisplayName,
Contexts: deriveScopedStatusContexts(repo.FullName(), p.DisplayName, p.Content, p.Events),
}
if cfg := configs[p.EntryName]; cfg != nil {
info.Required = cfg.Required
info.Patterns = strings.Join(cfg.Patterns, "\n")
}
rendered.Add(p.EntryName)
files = append(files, info)
}
}
}
// Surface configs whose workflow file no longer exists on the source default branch as orphan rows.
// A required orphan still gates merges (must-present), so the owner/admin must be able to see and clear it;
// otherwise the only escape would be removing the whole source registration.
orphans := make([]scopedWorkflowInfo, 0, len(configs))
for name, cfg := range configs {
if cfg == nil || rendered.Contains(name) {
continue
}
orphans = append(orphans, scopedWorkflowInfo{
EntryName: name,
DisplayName: name,
Required: cfg.Required,
Patterns: strings.Join(cfg.Patterns, "\n"),
Missing: true,
})
}
// map iteration order is random; sort orphans for a stable settings page
slices.SortFunc(orphans, func(a, b scopedWorkflowInfo) int { return strings.Compare(a.EntryName, b.EntryName) })
return append(files, orphans...)
}
func ScopedWorkflowAdd(ctx *context.Context) {
swCtx, err := getScopedWorkflowsCtx(ctx)
if err != nil {
ctx.ServerError("getScopedWorkflowsCtx", err)
return
}
if ctx.Written() {
return
}
repoName := ctx.FormString("repo_name")
var repo *repo_model.Repository
if swCtx.IsGlobal {
// instance-level: the source may be any repo on the instance, identified by owner/name
ownerName, name, ok := strings.Cut(repoName, "/")
if !ok {
ctx.JSONError(ctx.Tr("actions.scoped_workflows.source.not_found"))
return
}
repo, err = repo_model.GetRepositoryByOwnerAndName(ctx, ownerName, name)
} else {
// owner-level: resolve within the owner, which also enforces that the source is one of the owner's own repositories
repo, err = repo_model.GetRepositoryByName(ctx, swCtx.OwnerID, repoName)
}
if err != nil {
ctx.JSONError(ctx.Tr("actions.scoped_workflows.source.not_found"))
return
}
if err := actions_model.AddScopedWorkflowSource(ctx, swCtx.OwnerID, repo.ID); err != nil {
ctx.ServerError("AddScopedWorkflowSource", err)
return
}
ctx.Flash.Success(ctx.Tr("actions.scoped_workflows.source.add_success"))
ctx.JSONRedirect(swCtx.RedirectLink)
}
func ScopedWorkflowSetRequired(ctx *context.Context) {
swCtx, err := getScopedWorkflowsCtx(ctx)
if err != nil {
ctx.ServerError("getScopedWorkflowsCtx", err)
return
}
if ctx.Written() {
return
}
repoID := ctx.FormInt64("repo_id")
// the source must be registered for this owner
if _, err := actions_model.GetScopedWorkflowSource(ctx, swCtx.OwnerID, repoID); err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.JSONError(ctx.Tr("actions.scoped_workflows.source.not_found"))
} else {
ctx.ServerError("GetScopedWorkflowSource", err)
}
return
}
// Live workflow entry names on the source default branch, used to distinguish orphan configs (whose workflow file no longer exists) from live ones.
sourceRepo, err := repo_model.GetRepositoryByID(ctx, repoID)
if err != nil {
ctx.ServerError("GetRepositoryByID", err)
return
}
liveSet := make(container.Set[string])
if !sourceRepo.IsEmpty { // an empty source has no live workflows
_, parsed, err := actions_service.LoadParsedScopedWorkflows(ctx, sourceRepo)
if err != nil {
ctx.ServerError("LoadParsedScopedWorkflows", err)
return
}
for _, p := range parsed {
liveSet.Add(p.EntryName)
}
}
// Every workflow row submits its ID in workflow_ids and its patterns (one per line) in required_patterns[<id>];
// checked rows additionally submit their ID in required_workflow_ids.
// A required workflow must have at least one pattern.
requiredSet := make(container.Set[string])
for _, workflowID := range ctx.FormStrings("required_workflow_ids") {
requiredSet.Add(workflowID)
}
configs := make(map[string]*actions_model.ScopedWorkflowConfig)
for _, workflowID := range ctx.FormStrings("workflow_ids") {
patterns := parsePatternLines(ctx.FormString("required_patterns[" + workflowID + "]"))
required := requiredSet.Contains(workflowID)
if required && len(patterns) == 0 {
ctx.JSONError(ctx.Tr("actions.scoped_workflows.required.patterns_empty"))
return
}
// Keep a config only if it is required, or it is a still-existing.
// An orphan (file no longer in the source) that is not required is dropped.
if required || (liveSet.Contains(workflowID) && len(patterns) > 0) {
configs[workflowID] = &actions_model.ScopedWorkflowConfig{Required: required, Patterns: patterns}
}
}
if err := actions_model.SetScopedWorkflowSourceConfigs(ctx, swCtx.OwnerID, repoID, configs); err != nil {
ctx.ServerError("SetScopedWorkflowSourceConfigs", err)
return
}
ctx.Flash.Success(ctx.Tr("actions.scoped_workflows.required.update_success"))
ctx.JSONRedirect(swCtx.RedirectLink)
}
func ScopedWorkflowRemove(ctx *context.Context) {
swCtx, err := getScopedWorkflowsCtx(ctx)
if err != nil {
ctx.ServerError("getScopedWorkflowsCtx", err)
return
}
if ctx.Written() {
return
}
repoID := ctx.FormInt64("repo_id")
if err := actions_model.RemoveScopedWorkflowSource(ctx, swCtx.OwnerID, repoID); err != nil {
ctx.ServerError("RemoveScopedWorkflowSource", err)
return
}
ctx.Flash.Success(ctx.Tr("actions.scoped_workflows.source.remove_success"))
ctx.JSONRedirect(swCtx.RedirectLink)
}
@@ -0,0 +1,75 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"testing"
actions_module "gitea.dev/modules/actions"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDeriveScopedStatusContexts(t *testing.T) {
t.Run("jobs x events; job name is its name: or its id", func(t *testing.T) {
content := []byte(`name: CI
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- run: echo
build:
name: Build It
runs-on: ubuntu-latest
steps:
- run: echo
`)
events, err := actions_module.GetEventsFromContent(content)
require.NoError(t, err)
got := deriveScopedStatusContexts("org/src", "CI", content, events)
assert.ElementsMatch(t, []string{
"org/src: CI / lint (push)",
"org/src: CI / lint (pull_request)",
"org/src: CI / Build It (push)",
"org/src: CI / Build It (pull_request)",
}, got)
})
t.Run("only status-producing events; workflow_dispatch/schedule/workflow_call skipped", func(t *testing.T) {
content := []byte(`name: CI
on:
push:
workflow_dispatch:
workflow_call:
schedule:
- cron: "0 0 * * *"
jobs:
j:
runs-on: ubuntu-latest
steps:
- run: echo
`)
events, err := actions_module.GetEventsFromContent(content)
require.NoError(t, err)
got := deriveScopedStatusContexts("org/src", "CI", content, events)
assert.Equal(t, []string{"org/src: CI / j (push)"}, got) // only push posts a commit status
})
t.Run("a workflow_dispatch-only workflow has no expected contexts", func(t *testing.T) {
content := []byte(`name: Deploy
on: workflow_dispatch
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- run: echo
`)
events, err := actions_module.GetEventsFromContent(content)
require.NoError(t, err)
got := deriveScopedStatusContexts("org/src", "Deploy", content, events)
assert.Empty(t, got) // workflow_dispatch posts no commit status -> nothing to preview (and it cannot be a required check)
})
}
@@ -7,16 +7,16 @@ import (
"errors"
"net/http"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
actions_service "code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
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 (
@@ -6,11 +6,11 @@ package issue
import (
"strings"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/services/context"
"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"]`
@@ -38,6 +38,7 @@ func PrepareFilterIssueLabels(ctx *context.Context, repoID int64, owner *user_mo
ctx.ServerError("GetLabelsByRepoID", err)
return ret
}
issues_model.SortLabelsForDisplay(repoLabels)
allLabels = append(allLabels, repoLabels...)
}
@@ -47,6 +48,7 @@ func PrepareFilterIssueLabels(ctx *context.Context, repoID int64, owner *user_mo
ctx.ServerError("GetLabelsByOrgID", err)
return ret
}
issues_model.SortLabelsForDisplay(orgLabels)
allLabels = append(allLabels, orgLabels...)
}
@@ -4,16 +4,16 @@
package label
import (
"code.gitea.io/gitea/modules/label"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"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.Data["ErrorMsg"].(string))
ctx.JSONError(ctx.GetErrMsg())
return nil
}
var err error
@@ -6,8 +6,8 @@ package mention
import (
"context"
"code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user"
"gitea.dev/models/organization"
user_model "gitea.dev/models/user"
)
// Mention is the JSON structure returned by mention autocomplete endpoints.
@@ -8,16 +8,16 @@ import (
"net/http"
"time"
packages_model "code.gitea.io/gitea/models/packages"
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/templates"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
cargo_service "code.gitea.io/gitea/services/packages/cargo"
container_service "code.gitea.io/gitea/services/packages/container"
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) {
@@ -4,9 +4,9 @@
package project
import (
project_model "code.gitea.io/gitea/models/project"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/services/context"
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
@@ -4,14 +4,14 @@
package secrets
import (
"code.gitea.io/gitea/models/db"
secret_model "code.gitea.io/gitea/models/secret"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
secret_service "code.gitea.io/gitea/services/secrets"
"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) {
@@ -6,11 +6,13 @@ package user
import (
"errors"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
user_service "code.gitea.io/gitea/services/user"
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) {
@@ -28,49 +30,51 @@ func BlockedUsers(ctx *context.Context, blocker *user_model.User) {
ctx.Data["UserBlocks"] = blocks
}
func BlockedUsersPost(ctx *context.Context, blocker *user_model.User) {
form := web.GetForm(ctx).(*forms.BlockUserForm)
if ctx.HasError() {
ctx.ServerError("FormValidation", nil)
return
}
func blockedUsersPost(ctx *context.Context, form *forms.BlockUserForm, blocker *user_model.User) error {
blockee, err := user_model.GetUserByName(ctx, form.Blockee)
if err != nil {
ctx.ServerError("GetUserByName", nil)
return
return err
}
switch form.Action {
case "block":
if err := user_service.BlockUser(ctx, ctx.Doer, blocker, blockee, form.Note); err != nil {
if errors.Is(err, user_model.ErrCanNotBlock) || errors.Is(err, user_model.ErrBlockOrganization) {
ctx.Flash.Error(ctx.Tr("user.block.block.failure", err.Error()))
} else {
ctx.ServerError("BlockUser", err)
return
}
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":
if err := user_service.UnblockUser(ctx, ctx.Doer, blocker, blockee); err != nil {
if errors.Is(err, user_model.ErrCanNotUnblock) || errors.Is(err, user_model.ErrBlockOrganization) {
ctx.Flash.Error(ctx.Tr("user.block.unblock.failure", err.Error()))
} else {
ctx.ServerError("UnblockUser", err)
return
}
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 {
ctx.ServerError("GetBlocking", err)
return
}
if block != nil {
if err := user_model.UpdateBlockingNote(ctx, block.ID, form.Note); err != nil {
ctx.ServerError("UpdateBlockingNote", err)
return
}
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)
}
}
@@ -4,24 +4,25 @@
package user
import (
"errors"
"net/url"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
access_model "code.gitea.io/gitea/models/perm/access"
project_model "code.gitea.io/gitea/models/project"
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/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
"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
@@ -83,10 +84,9 @@ func prepareContextForProfileBigAvatar(ctx *context.Context) {
}
if ctx.Doer != nil {
if block, err := user_model.GetBlocking(ctx, ctx.Doer.ID, ctx.ContextUser.ID); err != 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)
} else {
ctx.Data["UserBlocking"] = block
}
}
}
@@ -8,25 +8,47 @@ import (
"slices"
"strconv"
"code.gitea.io/gitea/models/user"
"gitea.dev/models/user"
)
type SearchUserInfo struct {
UserID int64 `json:"user_id"`
UserName string `json:"username"`
AvatarLink string `json:"avatar_link"`
FullName string `json:"full_name"`
}
type SearchUserResponse struct {
Results []*SearchUserInfo `json:"results"`
}
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
}
if doer == nil {
return users
}
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
}
func ToSearchUserResponse(ctx context.Context, self *user.User, users []*user.User) (ret *SearchUserResponse) {
infos := MakeSelfOnTop(self, users)
resp := &SearchUserResponse{Results: make([]*SearchUserInfo, len(infos))}
for i, u := range infos {
resp.Results[i] = &SearchUserInfo{UserID: u.ID, UserName: u.Name, AvatarLink: u.AvatarLink(ctx)}
resp.Results[i].FullName = u.FullName
}
return resp
}
// 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.
@@ -6,7 +6,7 @@ package user
import (
"testing"
"code.gitea.io/gitea/models/user"
"gitea.dev/models/user"
"github.com/stretchr/testify/assert"
)