Merge branch 'forgejo' into feat/xorm-EngineGroup

This commit is contained in:
Patrick Schratz 2025-03-15 10:43:04 +00:00
commit 5faf339a1b
27 changed files with 765 additions and 346 deletions

View file

@ -26,3 +26,8 @@ insert_final_newline = false
[options/locale/locale_*.ini]
insert_final_newline = false
# Weblate JSON output defaults to four spaces
[options/locale_next/locale_*.json]
indent_style = space
indent_size = 4

View file

@ -28,7 +28,7 @@ jobs:
runs-on: docker
container:
image: data.forgejo.org/renovate/renovate:39.185.0
image: data.forgejo.org/renovate/renovate:39.195.1
steps:
- name: Load renovate repo cache

View file

@ -223,7 +223,7 @@ jobs:
ldap:
image: data.forgejo.org/oci/test-openldap:latest
pgsql:
image: data.forgejo.org/oci/bitnami/postgresql:15
image: data.forgejo.org/oci/bitnami/postgresql:16
env:
POSTGRESQL_DATABASE: test
POSTGRESQL_PASSWORD: postgres

View file

@ -39,7 +39,7 @@ XGO_VERSION := go-1.21.x
AIR_PACKAGE ?= github.com/air-verse/air@v1 # renovate: datasource=go
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.2.0 # renovate: datasource=go
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.7.0 # renovate: datasource=go
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.6 # renovate: datasource=go
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.7 # renovate: datasource=go
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11 # renovate: datasource=go
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.6.0 # renovate: datasource=go
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.31.0 # renovate: datasource=go
@ -49,7 +49,7 @@ GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 # renovate: datasour
DEADCODE_PACKAGE ?= golang.org/x/tools/cmd/deadcode@v0.31.0 # renovate: datasource=go
GOMOCK_PACKAGE ?= go.uber.org/mock/mockgen@v0.4.0 # renovate: datasource=go
GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.18.1 # renovate: datasource=go
RENOVATE_NPM_PACKAGE ?= renovate@39.185.0 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate
RENOVATE_NPM_PACKAGE ?= renovate@39.195.1 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate
# https://github.com/disposable-email-domains/disposable-email-domains/commits/main/
DISPOSABLE_EMAILS_SHA ?= 0c27e671231d27cf66370034d7f6818037416989 # renovate: ...
@ -413,7 +413,7 @@ lint-frontend: lint-js lint-css
lint-frontend-fix: lint-js-fix lint-css-fix
.PHONY: lint-backend
lint-backend: lint-go lint-go-vet lint-editorconfig lint-renovate lint-locale lint-disposable-emails
lint-backend: lint-go lint-go-vet lint-editorconfig lint-renovate lint-locale lint-locale-usage lint-disposable-emails
.PHONY: lint-backend-fix
lint-backend-fix: lint-go-fix lint-go-vet lint-editorconfig lint-disposable-emails-fix
@ -458,7 +458,11 @@ lint-renovate: node_modules
.PHONY: lint-locale
lint-locale:
$(GO) run build/lint-locale.go
$(GO) run build/lint-locale/lint-locale.go
.PHONY: lint-locale-usage
lint-locale-usage:
$(GO) run build/lint-locale-usage/lint-locale-usage.go --allow-missing-msgids
.PHONY: lint-md
lint-md: node_modules

View file

@ -0,0 +1,331 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package main
import (
"fmt"
"go/ast"
goParser "go/parser"
"go/token"
"io/fs"
"os"
"path/filepath"
"strconv"
"strings"
"text/template"
tmplParser "text/template/parse"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/locale"
fjTemplates "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/util"
)
// this works by first gathering all valid source string IDs from `en-US` reference files
// and then checking if all used source strings are actually defined
type OnMsgidHandler func(fset *token.FileSet, pos token.Pos, msgid string)
type LocatedError struct {
Location string
Kind string
Err error
}
func (e LocatedError) Error() string {
var sb strings.Builder
sb.WriteString(e.Location)
sb.WriteString(":\t")
if e.Kind != "" {
sb.WriteString(e.Kind)
sb.WriteString(": ")
}
sb.WriteString("ERROR: ")
sb.WriteString(e.Err.Error())
return sb.String()
}
func isLocaleTrFunction(funcname string) bool {
return funcname == "Tr" || funcname == "TrN"
}
// the `Handle*File` functions follow the following calling convention:
// * `fname` is the name of the input file
// * `src` is either `nil` (then the function invokes `ReadFile` to read the file)
// or the contents of the file as {`[]byte`, or a `string`}
func (omh OnMsgidHandler) HandleGoFile(fname string, src any) error {
fset := token.NewFileSet()
node, err := goParser.ParseFile(fset, fname, src, goParser.SkipObjectResolution)
if err != nil {
return LocatedError{
Location: fname,
Kind: "Go parser",
Err: err,
}
}
ast.Inspect(node, func(n ast.Node) bool {
// search for function calls of the form `anything.Tr(any-string-lit)`
call, ok := n.(*ast.CallExpr)
if !ok || len(call.Args) != 1 {
return true
}
funSel, ok := call.Fun.(*ast.SelectorExpr)
if (!ok) || !isLocaleTrFunction(funSel.Sel.Name) {
return true
}
argLit, ok := call.Args[0].(*ast.BasicLit)
if (!ok) || argLit.Kind != token.STRING {
return true
}
// extract string content
arg, err := strconv.Unquote(argLit.Value)
if err != nil {
return true
}
// found interesting string
omh(fset, argLit.ValuePos, arg)
return true
})
return nil
}
// derived from source: modules/templates/scopedtmpl/scopedtmpl.go, L169-L213
func (omh OnMsgidHandler) handleTemplateNode(fset *token.FileSet, node tmplParser.Node) {
switch node.Type() {
case tmplParser.NodeAction:
omh.handleTemplatePipeNode(fset, node.(*tmplParser.ActionNode).Pipe)
case tmplParser.NodeList:
nodeList := node.(*tmplParser.ListNode)
omh.handleTemplateFileNodes(fset, nodeList.Nodes)
case tmplParser.NodePipe:
omh.handleTemplatePipeNode(fset, node.(*tmplParser.PipeNode))
case tmplParser.NodeTemplate:
omh.handleTemplatePipeNode(fset, node.(*tmplParser.TemplateNode).Pipe)
case tmplParser.NodeIf:
nodeIf := node.(*tmplParser.IfNode)
omh.handleTemplateBranchNode(fset, nodeIf.BranchNode)
case tmplParser.NodeRange:
nodeRange := node.(*tmplParser.RangeNode)
omh.handleTemplateBranchNode(fset, nodeRange.BranchNode)
case tmplParser.NodeWith:
nodeWith := node.(*tmplParser.WithNode)
omh.handleTemplateBranchNode(fset, nodeWith.BranchNode)
case tmplParser.NodeCommand:
nodeCommand := node.(*tmplParser.CommandNode)
omh.handleTemplateFileNodes(fset, nodeCommand.Args)
if len(nodeCommand.Args) != 2 {
return
}
nodeChain, ok := nodeCommand.Args[0].(*tmplParser.ChainNode)
if !ok {
return
}
nodeString, ok := nodeCommand.Args[1].(*tmplParser.StringNode)
if !ok {
return
}
nodeIdent, ok := nodeChain.Node.(*tmplParser.IdentifierNode)
if !ok || nodeIdent.Ident != "ctx" {
return
}
if len(nodeChain.Field) != 2 || nodeChain.Field[0] != "Locale" || !isLocaleTrFunction(nodeChain.Field[1]) {
return
}
// found interesting string
// the column numbers are a bit "off", but much better than nothing
omh(fset, token.Pos(nodeString.Pos), nodeString.Text)
default:
}
}
func (omh OnMsgidHandler) handleTemplatePipeNode(fset *token.FileSet, pipeNode *tmplParser.PipeNode) {
if pipeNode == nil {
return
}
// NOTE: we can't pass `pipeNode.Cmds` to handleTemplateFileNodes due to incompatible argument types
for _, node := range pipeNode.Cmds {
omh.handleTemplateNode(fset, node)
}
}
func (omh OnMsgidHandler) handleTemplateBranchNode(fset *token.FileSet, branchNode tmplParser.BranchNode) {
omh.handleTemplatePipeNode(fset, branchNode.Pipe)
omh.handleTemplateFileNodes(fset, branchNode.List.Nodes)
if branchNode.ElseList != nil {
omh.handleTemplateFileNodes(fset, branchNode.ElseList.Nodes)
}
}
func (omh OnMsgidHandler) handleTemplateFileNodes(fset *token.FileSet, nodes []tmplParser.Node) {
for _, node := range nodes {
omh.handleTemplateNode(fset, node)
}
}
func (omh OnMsgidHandler) HandleTemplateFile(fname string, src any) error {
var tmplContent []byte
switch src2 := src.(type) {
case nil:
var err error
tmplContent, err = os.ReadFile(fname)
if err != nil {
return LocatedError{
Location: fname,
Kind: "ReadFile",
Err: err,
}
}
case []byte:
tmplContent = src2
case string:
// SAFETY: we do not modify tmplContent below
tmplContent = util.UnsafeStringToBytes(src2)
default:
panic("invalid type for 'src'")
}
fset := token.NewFileSet()
fset.AddFile(fname, 1, len(tmplContent)).SetLinesForContent(tmplContent)
// SAFETY: we do not modify tmplContent2 below
tmplContent2 := util.UnsafeBytesToString(tmplContent)
tmpl := template.New(fname)
tmpl.Funcs(fjTemplates.NewFuncMap())
tmplParsed, err := tmpl.Parse(tmplContent2)
if err != nil {
return LocatedError{
Location: fname,
Kind: "Template parser",
Err: err,
}
}
omh.handleTemplateFileNodes(fset, tmplParsed.Tree.Root.Nodes)
return nil
}
// This command assumes that we get started from the project root directory
//
// Possible command line flags:
//
// --allow-missing-msgids don't return an error code if missing message IDs are found
//
// EXIT CODES:
//
// 0 success, no issues found
// 1 unable to walk directory tree
// 2 unable to parse locale ini/json files
// 3 unable to parse go or text/template files
// 4 found missing message IDs
//
//nolint:forbidigo
func main() {
allowMissingMsgids := false
for _, arg := range os.Args[1:] {
if arg == "--allow-missing-msgids" {
allowMissingMsgids = true
}
}
onError := func(err error) {
if err == nil {
return
}
fmt.Println(err.Error())
os.Exit(3)
}
msgids := make(container.Set[string])
onMsgid := func(trKey, trValue string) error {
msgids[trKey] = struct{}{}
return nil
}
localeFile := filepath.Join(filepath.Join("options", "locale"), "locale_en-US.ini")
localeContent, err := os.ReadFile(localeFile)
if err != nil {
fmt.Printf("%s:\tERROR: %s\n", localeFile, err.Error())
os.Exit(2)
}
if err = locale.IterateMessagesContent(localeContent, onMsgid); err != nil {
fmt.Printf("%s:\tERROR: %s\n", localeFile, err.Error())
os.Exit(2)
}
localeFile = filepath.Join(filepath.Join("options", "locale_next"), "locale_en-US.json")
localeContent, err = os.ReadFile(localeFile)
if err != nil {
fmt.Printf("%s:\tERROR: %s\n", localeFile, err.Error())
os.Exit(2)
}
if err := locale.IterateMessagesNextContent(localeContent, onMsgid); err != nil {
fmt.Printf("%s:\tERROR: %s\n", localeFile, err.Error())
os.Exit(2)
}
gotAnyMsgidError := false
omh := OnMsgidHandler(func(fset *token.FileSet, pos token.Pos, msgid string) {
if !msgids.Contains(msgid) {
gotAnyMsgidError = true
fmt.Printf("%s:\tmissing msgid: %s\n", fset.Position(pos).String(), msgid)
}
})
if err := filepath.WalkDir(".", func(fpath string, d fs.DirEntry, err error) error {
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
name := d.Name()
if d.IsDir() {
if name == "docker" || name == ".git" || name == "node_modules" {
return fs.SkipDir
}
} else if name == "bindata.go" {
// skip false positives
} else if strings.HasSuffix(name, ".go") {
onError(omh.HandleGoFile(fpath, nil))
} else if strings.HasSuffix(name, ".tmpl") {
if strings.HasPrefix(fpath, "tests") && strings.HasSuffix(name, ".ini.tmpl") {
// skip false positives
} else {
onError(omh.HandleTemplateFile(fpath, nil))
}
}
return nil
}); err != nil {
fmt.Printf("walkdir ERROR: %s\n", err.Error())
os.Exit(1)
}
if !allowMissingMsgids && gotAnyMsgidError {
os.Exit(4)
}
}

View file

@ -0,0 +1,44 @@
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package main
import (
"go/token"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func HandleGoFileWrapped(t *testing.T, fname, src string) []string {
var ret []string
omh := OnMsgidHandler(func(fset *token.FileSet, pos token.Pos, msgid string) {
ret = append(ret, msgid)
})
require.NoError(t, omh.HandleGoFile(fname, src))
return ret
}
func HandleTemplateFileWrapped(t *testing.T, fname, src string) []string {
var ret []string
omh := OnMsgidHandler(func(fset *token.FileSet, pos token.Pos, msgid string) {
ret = append(ret, msgid)
})
require.NoError(t, omh.HandleTemplateFile(fname, src))
return ret
}
func TestUsagesParser(t *testing.T) {
t.Run("go, simple", func(t *testing.T) {
assert.EqualValues(t,
[]string{"what.an.example"},
HandleGoFileWrapped(t, "<g1>", "package main\nfunc Render(ctx *context.Context) string { return ctx.Tr(\"what.an.example\"); }\n"))
})
t.Run("template, simple", func(t *testing.T) {
assert.EqualValues(t,
[]string{"what.an.example"},
HandleTemplateFileWrapped(t, "<t1>", "{{ ctx.Locale.Tr \"what.an.example\" }}\n"))
})
}

View file

@ -5,7 +5,6 @@
package main
import (
"encoding/json" //nolint:depguard
"fmt"
"html"
"io/fs"
@ -15,9 +14,10 @@ import (
"slices"
"strings"
"code.gitea.io/gitea/modules/locale"
"github.com/microcosm-cc/bluemonday"
"github.com/sergi/go-diff/diffmatchpatch"
"gopkg.in/ini.v1" //nolint:depguard
)
var (
@ -98,49 +98,28 @@ func checkValue(trKey, value string) []string {
}
func checkLocaleContent(localeContent []byte) []string {
// Same configuration as Forgejo uses.
cfg := ini.Empty(ini.LoadOptions{
IgnoreContinuation: true,
})
cfg.NameMapper = ini.SnackCase
errors := []string{}
if err := cfg.Append(localeContent); err != nil {
if err := locale.IterateMessagesContent(localeContent, func(trKey, trValue string) error {
errors = append(errors, checkValue(trKey, trValue)...)
return nil
}); err != nil {
panic(err)
}
errors := []string{}
for _, section := range cfg.Sections() {
for _, key := range section.Keys() {
var trKey string
if section.Name() == "" || section.Name() == "DEFAULT" || section.Name() == "common" {
trKey = key.Name()
} else {
trKey = section.Name() + "." + key.Name()
}
errors = append(errors, checkValue(trKey, key.Value())...)
}
}
return errors
}
func checkLocaleNextContent(data map[string]any, trKey ...string) []string {
func checkLocaleNextContent(localeContent []byte) []string {
errors := []string{}
for key, value := range data {
currentKey := key
if len(trKey) == 1 {
currentKey = trKey[0] + "." + key
}
switch value := value.(type) {
case string:
errors = append(errors, checkValue(currentKey, value)...)
case map[string]any:
errors = append(errors, checkLocaleNextContent(value, currentKey)...)
default:
panic(fmt.Sprintf("Unexpected type during linting locale next: %s - %T", currentKey, value))
}
if err := locale.IterateMessagesNextContent(localeContent, func(trKey, trValue string) error {
errors = append(errors, checkValue(trKey, trValue)...)
return nil
}); err != nil {
panic(err)
}
return errors
}
@ -168,6 +147,7 @@ func main() {
localeContent, err := os.ReadFile(filepath.Join(localeDir, localeFile.Name()))
if err != nil {
fmt.Println(localeFile.Name())
panic(err)
}
@ -195,15 +175,11 @@ func main() {
for _, localeFile := range localeFiles {
localeContent, err := os.ReadFile(filepath.Join(localeDir, localeFile.Name()))
if err != nil {
fmt.Println(localeFile.Name())
panic(err)
}
var localeData map[string]any
if err := json.Unmarshal(localeContent, &localeData); err != nil {
panic(err)
}
if err := checkLocaleNextContent(localeData); len(err) > 0 {
if err := checkLocaleNextContent(localeContent); len(err) > 0 {
fmt.Println(localeFile.Name())
fmt.Println(strings.Join(err, "\n"))
fmt.Println()

View file

@ -69,26 +69,26 @@ func TestNextLocalizationPolicy(t *testing.T) {
initRemoveTags()
t.Run("Nested locales", func(t *testing.T) {
assert.Empty(t, checkLocaleNextContent(map[string]any{
"settings": map[string]any{
"hidden_comment_types_description": `Comment types checked here will not be shown inside issue pages. Checking "Label" for example removes all "<user> added/removed <label>" comments.`,
},
}))
assert.Empty(t, checkLocaleNextContent([]byte(`{
"settings": {
"hidden_comment_types_description": "Comment types checked here will not be shown inside issue pages. Checking \"Label\" for example removes all \"<user> added/removed <label>\" comments."
}
}`)))
assert.EqualValues(t, []string{"settings.hidden_comment_types_description: \"\x1b[31m<not-an-allowed-key>\x1b[0m REPLACED-TAG\""}, checkLocaleNextContent(map[string]any{
"settings": map[string]any{
"hidden_comment_types_description": `"<not-an-allowed-key> <label>"`,
},
}))
assert.EqualValues(t, []string{"settings.hidden_comment_types_description: \"\x1b[31m<not-an-allowed-key>\x1b[0m REPLACED-TAG\""}, checkLocaleNextContent([]byte(`{
"settings": {
"hidden_comment_types_description": "\"<not-an-allowed-key> <label>\""
}
}`)))
})
t.Run("Flat locales", func(t *testing.T) {
assert.Empty(t, checkLocaleNextContent(map[string]any{
"settings.hidden_comment_types_description": `Comment types checked here will not be shown inside issue pages. Checking "Label" for example removes all "<user> added/removed <label>" comments.`,
}))
assert.Empty(t, checkLocaleNextContent([]byte(`{
"settings.hidden_comment_types_description": "Comment types checked here will not be shown inside issue pages. Checking \"Label\" for example removes all \"<user> added/removed <label>\" comments."
}`)))
assert.EqualValues(t, []string{"settings.hidden_comment_types_description: \"\x1b[31m<not-an-allowed-key>\x1b[0m REPLACED-TAG\""}, checkLocaleNextContent(map[string]any{
"settings.hidden_comment_types_description": `"<not-an-allowed-key> <label>"`,
}))
assert.EqualValues(t, []string{"settings.hidden_comment_types_description: \"\x1b[31m<not-an-allowed-key>\x1b[0m REPLACED-TAG\""}, checkLocaleNextContent([]byte(`{
"settings.hidden_comment_types_description": "\"<not-an-allowed-key> <label>\""
}`)))
})
}

74
modules/locale/utils.go Normal file
View file

@ -0,0 +1,74 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// extracted from `/build/lint-locale.go`, `/build/lint-locale-usage.go`
package locale
import (
"encoding/json" //nolint:depguard
"fmt"
"gopkg.in/ini.v1" //nolint:depguard
)
func IterateMessagesContent(localeContent []byte, onMsgid func(string, string) error) error {
// Same configuration as Forgejo uses.
cfg := ini.Empty(ini.LoadOptions{
IgnoreContinuation: true,
})
cfg.NameMapper = ini.SnackCase
if err := cfg.Append(localeContent); err != nil {
return err
}
for _, section := range cfg.Sections() {
for _, key := range section.Keys() {
var trKey string
if section.Name() == "" || section.Name() == "DEFAULT" || section.Name() == "common" {
trKey = key.Name()
} else {
trKey = section.Name() + "." + key.Name()
}
if err := onMsgid(trKey, key.Value()); err != nil {
return err
}
}
}
return nil
}
func iterateMessagesNextInner(onMsgid func(string, string) error, data map[string]any, trKey ...string) error {
for key, value := range data {
currentKey := key
if len(trKey) == 1 {
currentKey = trKey[0] + "." + key
}
switch value := value.(type) {
case string:
if err := onMsgid(currentKey, value); err != nil {
return err
}
case map[string]any:
if err := iterateMessagesNextInner(onMsgid, value, currentKey); err != nil {
return err
}
default:
return fmt.Errorf("unexpected type: %s - %T", currentKey, value)
}
}
return nil
}
func IterateMessagesNextContent(localeContent []byte, onMsgid func(string, string) error) error {
var localeData map[string]any
if err := json.Unmarshal(localeContent, &localeData); err != nil {
return err
}
return iterateMessagesNextInner(onMsgid, localeData)
}

View file

@ -12,4 +12,16 @@ type DispatchWorkflowOption struct {
Ref string `json:"ref"`
// Input keys and values configured in the workflow file.
Inputs map[string]string `json:"inputs"`
// Flag to return the run info
// default: false
ReturnRunInfo bool `json:"return_run_info"`
}
// DispatchWorkflowRun represents a workflow run
// swagger:model
type DispatchWorkflowRun struct {
// the workflow run id
ID int64 `json:"id"`
// the jobs name
Jobs []string `json:"jobs"`
}

View file

@ -1,20 +1,20 @@
{
"home.welcome.no_activity": "No activity",
"home.welcome.activity_hint": "There is nothing in your feed yet. Your actions and activity from repositories that you watch will show up here.",
"home.explore_repos": "Explore repositories",
"home.explore_users": "Explore users",
"home.explore_orgs": "Explore organizations",
"repo.pulls.merged_title_desc": {
"one": "merged %[1]d commit from <code>%[2]s</code> into <code>%[3]s</code> %[4]s",
"other": "merged %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code> %[4]s"
},
"repo.pulls.title_desc": {
"one": "wants to merge %[1]d commit from <code>%[2]s</code> into <code id=\"%[4]s\">%[3]s</code>",
"other": "wants to merge %[1]d commits from <code>%[2]s</code> into <code id=\"%[4]s\">%[3]s</code>"
},
"search.milestone_kind": "Search milestones…",
"incorrect_root_url": "This Forgejo instance is configured to be served on \"%s\". You are currently viewing Forgejo through a different URL, which may cause parts of the application to break. The canonical URL is controlled by Forgejo admins via the ROOT_URL setting in the app.ini.",
"themes.names.forgejo-auto": "Forgejo (follow system theme)",
"themes.names.forgejo-light": "Forgejo light",
"themes.names.forgejo-dark": "Forgejo dark"
"home.welcome.no_activity": "No activity",
"home.welcome.activity_hint": "There is nothing in your feed yet. Your actions and activity from repositories that you watch will show up here.",
"home.explore_repos": "Explore repositories",
"home.explore_users": "Explore users",
"home.explore_orgs": "Explore organizations",
"repo.pulls.merged_title_desc": {
"one": "merged %[1]d commit from <code>%[2]s</code> into <code>%[3]s</code> %[4]s",
"other": "merged %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code> %[4]s"
},
"repo.pulls.title_desc": {
"one": "wants to merge %[1]d commit from <code>%[2]s</code> into <code id=\"%[4]s\">%[3]s</code>",
"other": "wants to merge %[1]d commits from <code>%[2]s</code> into <code id=\"%[4]s\">%[3]s</code>"
},
"search.milestone_kind": "Search milestones…",
"incorrect_root_url": "This Forgejo instance is configured to be served on \"%s\". You are currently viewing Forgejo through a different URL, which may cause parts of the application to break. The canonical URL is controlled by Forgejo admins via the ROOT_URL setting in the app.ini.",
"themes.names.forgejo-auto": "Forgejo (follow system theme)",
"themes.names.forgejo-light": "Forgejo light",
"themes.names.forgejo-dark": "Forgejo dark"
}

200
package-lock.json generated
View file

@ -32,7 +32,7 @@
"idiomorph": "0.3.0",
"jquery": "3.7.1",
"katex": "0.16.21",
"mermaid": "11.4.1",
"mermaid": "11.5.0",
"mini-css-extract-plugin": "2.9.2",
"minimatch": "10.0.1",
"monaco-editor": "0.52.2",
@ -63,13 +63,13 @@
"devDependencies": {
"@axe-core/playwright": "4.10.1",
"@eslint-community/eslint-plugin-eslint-comments": "4.4.1",
"@playwright/test": "1.50.1",
"@stoplight/spectral-cli": "6.14.2",
"@playwright/test": "1.51.0",
"@stoplight/spectral-cli": "6.14.3",
"@stylistic/eslint-plugin-js": "4.2.0",
"@stylistic/stylelint-plugin": "3.1.2",
"@typescript-eslint/parser": "8.26.0",
"@vitejs/plugin-vue": "5.1.5",
"@vitest/coverage-v8": "3.0.5",
"@vitest/coverage-v8": "3.0.8",
"@vitest/eslint-plugin": "1.1.25",
"@vue/test-utils": "2.4.6",
"eslint": "9.22.0",
@ -88,7 +88,7 @@
"eslint-plugin-vue-scoped-css": "2.9.0",
"eslint-plugin-wc": "2.2.1",
"globals": "16.0.0",
"happy-dom": "17.4.3",
"happy-dom": "17.4.4",
"license-checker-rseidelsohn": "4.4.2",
"markdownlint-cli": "0.44.0",
"postcss-html": "1.8.0",
@ -100,7 +100,7 @@
"typescript": "5.7.3",
"typescript-eslint": "8.26.0",
"vite-string-plugin": "1.3.4",
"vitest": "3.0.5"
"vitest": "3.0.8"
},
"engines": {
"node": ">= 18.0.0"
@ -1684,13 +1684,13 @@
}
},
"node_modules/@playwright/test": {
"version": "1.50.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.1.tgz",
"integrity": "sha512-Jii3aBg+CEDpgnuDxEp/h7BimHcUTDlpEtce89xEumlJ5ef2hqepZ+PWp1DDpYC/VO9fmWVI1IlEaoI5fK9FXQ==",
"version": "1.51.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.51.0.tgz",
"integrity": "sha512-dJ0dMbZeHhI+wb77+ljx/FeC8VBP6j/rj9OAojO08JI80wTZy6vRk9KvHKiDCUh4iMpEiseMgqRBIeW+eKX6RA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.50.1"
"playwright": "1.51.0"
},
"bin": {
"playwright": "cli.js"
@ -2130,15 +2130,15 @@
}
},
"node_modules/@stoplight/spectral-cli": {
"version": "6.14.2",
"resolved": "https://registry.npmjs.org/@stoplight/spectral-cli/-/spectral-cli-6.14.2.tgz",
"integrity": "sha512-yn49Tkin/Zzjwt39CbQvj3NZVolvXrjO3OLNn+Yd+LhQE5C96QNsULuE4q98e7+WRcLu4gK+Z0l5CxYNtsoPtw==",
"version": "6.14.3",
"resolved": "https://registry.npmjs.org/@stoplight/spectral-cli/-/spectral-cli-6.14.3.tgz",
"integrity": "sha512-vKy7d2yqBfOf94uB6KXzujDl6/qjXa8mCQ6cfsQ8xYsoArZN9iBHpS3271hR5IyTm3R1GwMgaSZ1h0sfZjZrZw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@stoplight/json": "~3.21.0",
"@stoplight/path": "1.3.2",
"@stoplight/spectral-core": "^1.19.4",
"@stoplight/spectral-core": "^1.19.5",
"@stoplight/spectral-formatters": "^1.4.1",
"@stoplight/spectral-parsers": "^1.0.4",
"@stoplight/spectral-ref-resolver": "^1.0.4",
@ -3229,9 +3229,9 @@
}
},
"node_modules/@vitest/coverage-v8": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.5.tgz",
"integrity": "sha512-zOOWIsj5fHh3jjGwQg+P+J1FW3s4jBu1Zqga0qW60yutsBtqEqNEJKWYh7cYn1yGD+1bdPsPdC/eL4eVK56xMg==",
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.8.tgz",
"integrity": "sha512-y7SAKsQirsEJ2F8bulBck4DoluhI2EEgTimHd6EEUgJBGKy9tC25cpywh1MH4FvDGoG2Unt7+asVd1kj4qOSAw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -3252,8 +3252,8 @@
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@vitest/browser": "3.0.5",
"vitest": "3.0.5"
"@vitest/browser": "3.0.8",
"vitest": "3.0.8"
},
"peerDependenciesMeta": {
"@vitest/browser": {
@ -3293,15 +3293,15 @@
}
},
"node_modules/@vitest/expect": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.5.tgz",
"integrity": "sha512-nNIOqupgZ4v5jWuQx2DSlHLEs7Q4Oh/7AYwNyE+k0UQzG7tSmjPXShUikn1mpNGzYEN2jJbTvLejwShMitovBA==",
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.8.tgz",
"integrity": "sha512-Xu6TTIavTvSSS6LZaA3EebWFr6tsoXPetOWNMOlc7LO88QVVBwq2oQWBoDiLCN6YTvNYsGSjqOO8CAdjom5DCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "3.0.5",
"@vitest/utils": "3.0.5",
"chai": "^5.1.2",
"@vitest/spy": "3.0.8",
"@vitest/utils": "3.0.8",
"chai": "^5.2.0",
"tinyrainbow": "^2.0.0"
},
"funding": {
@ -3309,13 +3309,13 @@
}
},
"node_modules/@vitest/mocker": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.5.tgz",
"integrity": "sha512-CLPNBFBIE7x6aEGbIjaQAX03ZZlBMaWwAjBdMkIf/cAn6xzLTiM3zYqO/WAbieEjsAZir6tO71mzeHZoodThvw==",
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.8.tgz",
"integrity": "sha512-n3LjS7fcW1BCoF+zWZxG7/5XvuYH+lsFg+BDwwAz0arIwHQJFUEsKBQ0BLU49fCxuM/2HSeBPHQD8WjgrxMfow==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "3.0.5",
"@vitest/spy": "3.0.8",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.17"
},
@ -3363,9 +3363,9 @@
}
},
"node_modules/@vitest/pretty-format": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.5.tgz",
"integrity": "sha512-CjUtdmpOcm4RVtB+up8r2vVDLR16Mgm/bYdkGFe3Yj/scRfCpbSi2W/BDSDcFK7ohw8UXvjMbOp9H4fByd/cOA==",
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.8.tgz",
"integrity": "sha512-BNqwbEyitFhzYMYHUVbIvepOyeQOSFA/NeJMIP9enMntkkxLgOcgABH6fjyXG85ipTgvero6noreavGIqfJcIg==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -3376,29 +3376,29 @@
}
},
"node_modules/@vitest/runner": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.5.tgz",
"integrity": "sha512-BAiZFityFexZQi2yN4OX3OkJC6scwRo8EhRB0Z5HIGGgd2q+Nq29LgHU/+ovCtd0fOfXj5ZI6pwdlUmC5bpi8A==",
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.8.tgz",
"integrity": "sha512-c7UUw6gEcOzI8fih+uaAXS5DwjlBaCJUo7KJ4VvJcjL95+DSR1kova2hFuRt3w41KZEFcOEiq098KkyrjXeM5w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "3.0.5",
"pathe": "^2.0.2"
"@vitest/utils": "3.0.8",
"pathe": "^2.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/snapshot": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.5.tgz",
"integrity": "sha512-GJPZYcd7v8QNUJ7vRvLDmRwl+a1fGg4T/54lZXe+UOGy47F9yUfE18hRCtXL5aHN/AONu29NGzIXSVFh9K0feA==",
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.8.tgz",
"integrity": "sha512-x8IlMGSEMugakInj44nUrLSILh/zy1f2/BgH0UeHpNyOocG18M9CWVIFBaXPt8TrqVZWmcPjwfG/ht5tnpba8A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "3.0.5",
"@vitest/pretty-format": "3.0.8",
"magic-string": "^0.30.17",
"pathe": "^2.0.2"
"pathe": "^2.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
@ -3415,9 +3415,9 @@
}
},
"node_modules/@vitest/spy": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.5.tgz",
"integrity": "sha512-5fOzHj0WbUNqPK6blI/8VzZdkBlQLnT25knX0r4dbZI9qoZDf3qAdjoMmDcLG5A83W6oUUFJgUd0EYBc2P5xqg==",
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.8.tgz",
"integrity": "sha512-MR+PzJa+22vFKYb934CejhR4BeRpMSoxkvNoDit68GQxRLSf11aT6CTj3XaqUU9rxgWJFnqicN/wxw6yBRkI1Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -3428,14 +3428,14 @@
}
},
"node_modules/@vitest/utils": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.5.tgz",
"integrity": "sha512-N9AX0NUoUtVwKwy21JtwzaqR5L5R5A99GAbrHfCCXK1lp593i/3AZAXhSP43wRQuxYsflrdzEfXZFo1reR1Nkg==",
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.8.tgz",
"integrity": "sha512-nkBC3aEhfX2PdtQI/QwAWp8qZWwzASsU4Npbcd5RdMPBSSLCpkZp52P3xku3s3uA0HIEhGvEcF8rNkBsz9dQ4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "3.0.5",
"loupe": "^3.1.2",
"@vitest/pretty-format": "3.0.8",
"loupe": "^3.1.3",
"tinyrainbow": "^2.0.0"
},
"funding": {
@ -7590,9 +7590,9 @@
}
},
"node_modules/happy-dom": {
"version": "17.4.3",
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-17.4.3.tgz",
"integrity": "sha512-8mDGIKxi2hAg0DkEYjBHPi5QykWiqdNNQQWrwLXLfro1eAZk8+lSnzbUrnU25bamG9PjEQGoFrA32ezQNJQdww==",
"version": "17.4.4",
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-17.4.4.tgz",
"integrity": "sha512-/Pb0ctk3HTZ5xEL3BZ0hK1AqDSAUuRQitOmROPHhfUYEWpmTImwfD8vFDGADmMAX0JYgbcgxWoLFKtsWhcpuVA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -9489,37 +9489,43 @@
}
},
"node_modules/mermaid": {
"version": "11.4.1",
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.4.1.tgz",
"integrity": "sha512-Mb01JT/x6CKDWaxigwfZYuYmDZ6xtrNwNlidKZwkSrDaY9n90tdrJTV5Umk+wP1fZscGptmKFXHsXMDEVZ+Q6A==",
"version": "11.5.0",
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.5.0.tgz",
"integrity": "sha512-IYhyukID3zzDj1EihKiN1lp+PXNImoJ3Iyz73qeDAgnus4BNGsJV1n471P4PyeGxPVONerZxignwGxGTSwZnlg==",
"license": "MIT",
"dependencies": {
"@braintree/sanitize-url": "^7.0.1",
"@iconify/utils": "^2.1.32",
"@braintree/sanitize-url": "^7.0.4",
"@iconify/utils": "^2.1.33",
"@mermaid-js/parser": "^0.3.0",
"@types/d3": "^7.4.3",
"cytoscape": "^3.29.2",
"cytoscape": "^3.29.3",
"cytoscape-cose-bilkent": "^4.1.0",
"cytoscape-fcose": "^2.2.0",
"d3": "^7.9.0",
"d3-sankey": "^0.12.3",
"dagre-d3-es": "7.0.11",
"dayjs": "^1.11.10",
"dompurify": "^3.2.1",
"dayjs": "^1.11.13",
"dompurify": "^3.2.4",
"katex": "^0.16.9",
"khroma": "^2.1.0",
"lodash-es": "^4.17.21",
"marked": "^13.0.2",
"marked": "^15.0.7",
"roughjs": "^4.6.6",
"stylis": "^4.3.1",
"stylis": "^4.3.6",
"ts-dedent": "^2.2.0",
"uuid": "^9.0.1"
"uuid": "^11.1.0"
}
},
"node_modules/mermaid/node_modules/dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
"license": "MIT"
},
"node_modules/mermaid/node_modules/marked": {
"version": "13.0.3",
"resolved": "https://registry.npmjs.org/marked/-/marked-13.0.3.tgz",
"integrity": "sha512-rqRix3/TWzE9rIoFGIn8JmsVfhiuC8VIQ8IdX5TfzmeBucdY05/0UlzKaw0eVtpcN/OdVFpBk7CjKGo9iHJ/zA==",
"version": "15.0.7",
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.7.tgz",
"integrity": "sha512-dgLIeKGLx5FwziAnsk4ONoGwHwGPJzselimvlVskE9XLN4Orv9u2VA3GWw/lYUqjfA0rUT/6fqKwfZJapP9BEg==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
@ -10862,13 +10868,13 @@
}
},
"node_modules/playwright": {
"version": "1.50.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.1.tgz",
"integrity": "sha512-G8rwsOQJ63XG6BbKj2w5rHeavFjy5zynBA9zsJMMtBoe/Uf757oG12NXz6e6OirF7RCrTVAKFXbLmn1RbL7Qaw==",
"version": "1.51.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.0.tgz",
"integrity": "sha512-442pTfGM0xxfCYxuBa/Pu6B2OqxqqaYq39JS8QDMGThUvIOCd6s0ANDog3uwA0cHavVlnTQzGCN7Id2YekDSXA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.50.1"
"playwright-core": "1.51.0"
},
"bin": {
"playwright": "cli.js"
@ -10881,9 +10887,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.50.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.1.tgz",
"integrity": "sha512-ra9fsNWayuYumt+NiM069M6OkcRb1FZSK8bgi66AtpFoWkg2+y0bJSNmkFrWhMbEBbVKC/EruAHH3g0zmtwGmQ==",
"version": "1.51.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.51.0.tgz",
"integrity": "sha512-x47yPE3Zwhlil7wlNU/iktF7t2r/URR3VLbH6EknJd/04Qc/PSJ0EY3CMXipmglLG+zyRxW6HNo2EGbKLHPWMg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@ -13917,16 +13923,16 @@
}
},
"node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/validate-npm-package-license": {
@ -14017,16 +14023,16 @@
}
},
"node_modules/vite-node": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.5.tgz",
"integrity": "sha512-02JEJl7SbtwSDJdYS537nU6l+ktdvcREfLksk/NDAqtdKWGqHl+joXzEubHROmS3E6pip+Xgu2tFezMu75jH7A==",
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.8.tgz",
"integrity": "sha512-6PhR4H9VGlcwXZ+KWCdMqbtG649xCPZqfI9j2PsK1FcXgEzro5bGHcVKFCTqPLaNKZES8Evqv4LwvZARsq5qlg==",
"dev": true,
"license": "MIT",
"dependencies": {
"cac": "^6.7.14",
"debug": "^4.4.0",
"es-module-lexer": "^1.6.0",
"pathe": "^2.0.2",
"pathe": "^2.0.3",
"vite": "^5.0.0 || ^6.0.0"
},
"bin": {
@ -14538,31 +14544,31 @@
}
},
"node_modules/vitest": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.5.tgz",
"integrity": "sha512-4dof+HvqONw9bvsYxtkfUp2uHsTN9bV2CZIi1pWgoFpL1Lld8LA1ka9q/ONSsoScAKG7NVGf2stJTI7XRkXb2Q==",
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.8.tgz",
"integrity": "sha512-dfqAsNqRGUc8hB9OVR2P0w8PZPEckti2+5rdZip0WIz9WW0MnImJ8XiR61QhqLa92EQzKP2uPkzenKOAHyEIbA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/expect": "3.0.5",
"@vitest/mocker": "3.0.5",
"@vitest/pretty-format": "^3.0.5",
"@vitest/runner": "3.0.5",
"@vitest/snapshot": "3.0.5",
"@vitest/spy": "3.0.5",
"@vitest/utils": "3.0.5",
"chai": "^5.1.2",
"@vitest/expect": "3.0.8",
"@vitest/mocker": "3.0.8",
"@vitest/pretty-format": "^3.0.8",
"@vitest/runner": "3.0.8",
"@vitest/snapshot": "3.0.8",
"@vitest/spy": "3.0.8",
"@vitest/utils": "3.0.8",
"chai": "^5.2.0",
"debug": "^4.4.0",
"expect-type": "^1.1.0",
"magic-string": "^0.30.17",
"pathe": "^2.0.2",
"pathe": "^2.0.3",
"std-env": "^3.8.0",
"tinybench": "^2.9.0",
"tinyexec": "^0.3.2",
"tinypool": "^1.0.2",
"tinyrainbow": "^2.0.0",
"vite": "^5.0.0 || ^6.0.0",
"vite-node": "3.0.5",
"vite-node": "3.0.8",
"why-is-node-running": "^2.3.0"
},
"bin": {
@ -14578,8 +14584,8 @@
"@edge-runtime/vm": "*",
"@types/debug": "^4.1.12",
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"@vitest/browser": "3.0.5",
"@vitest/ui": "3.0.5",
"@vitest/browser": "3.0.8",
"@vitest/ui": "3.0.8",
"happy-dom": "*",
"jsdom": "*"
},

View file

@ -31,7 +31,7 @@
"idiomorph": "0.3.0",
"jquery": "3.7.1",
"katex": "0.16.21",
"mermaid": "11.4.1",
"mermaid": "11.5.0",
"mini-css-extract-plugin": "2.9.2",
"minimatch": "10.0.1",
"monaco-editor": "0.52.2",
@ -62,13 +62,13 @@
"devDependencies": {
"@axe-core/playwright": "4.10.1",
"@eslint-community/eslint-plugin-eslint-comments": "4.4.1",
"@playwright/test": "1.50.1",
"@stoplight/spectral-cli": "6.14.2",
"@playwright/test": "1.51.0",
"@stoplight/spectral-cli": "6.14.3",
"@stylistic/eslint-plugin-js": "4.2.0",
"@stylistic/stylelint-plugin": "3.1.2",
"@typescript-eslint/parser": "8.26.0",
"@vitejs/plugin-vue": "5.1.5",
"@vitest/coverage-v8": "3.0.5",
"@vitest/coverage-v8": "3.0.8",
"@vitest/eslint-plugin": "1.1.25",
"@vue/test-utils": "2.4.6",
"eslint": "9.22.0",
@ -87,7 +87,7 @@
"eslint-plugin-vue-scoped-css": "2.9.0",
"eslint-plugin-wc": "2.2.1",
"globals": "16.0.0",
"happy-dom": "17.4.3",
"happy-dom": "17.4.4",
"license-checker-rseidelsohn": "4.4.2",
"markdownlint-cli": "0.44.0",
"postcss-html": "1.8.0",
@ -99,7 +99,7 @@
"typescript": "5.7.3",
"typescript-eslint": "8.26.0",
"vite-string-plugin": "1.3.4",
"vitest": "3.0.5"
"vitest": "3.0.8"
},
"browserslist": [
"defaults"

10
poetry.lock generated
View file

@ -404,13 +404,13 @@ files = [
[[package]]
name = "yamllint"
version = "1.35.1"
version = "1.36.0"
description = "A linter for YAML files."
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
files = [
{file = "yamllint-1.35.1-py3-none-any.whl", hash = "sha256:2e16e504bb129ff515b37823b472750b36b6de07963bd74b307341ef5ad8bdc3"},
{file = "yamllint-1.35.1.tar.gz", hash = "sha256:7a003809f88324fd2c877734f2d575ee7881dd9043360657cc8049c809eba6cd"},
{file = "yamllint-1.36.0-py3-none-any.whl", hash = "sha256:22ab6a7db9d5f0954bd7a50ef70a41c1465c61f955893070b0767c0ec6693b7b"},
{file = "yamllint-1.36.0.tar.gz", hash = "sha256:3835a65994858679ea06fd998dd968c3f71935cd93742990405999d888e21130"},
]
[package.dependencies]
@ -423,4 +423,4 @@ dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "sphinx"]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "f0f2f5f83d2277f5c289c62b96a2526fc89a7d54a88ac5fd044a022486c1cbb7"
content-hash = "c8ff4ba89b1588b4222e79f9832b3596b5c5d5a2fce8c5659b90bd31bd0f61b0"

View file

@ -6,7 +6,7 @@ python = "^3.10"
[tool.poetry.group.dev.dependencies]
djlint = "1.36.4"
yamllint = "1.35.1"
yamllint = "1.36.0"
codespell = "2.4.1"
[tool.djlint]

View file

@ -670,7 +670,8 @@ func DispatchWorkflow(ctx *context.APIContext) {
return opt.Inputs[key]
}
if err := workflow.Dispatch(ctx, inputGetter, ctx.Repo.Repository, ctx.Doer); err != nil {
run, jobs, err := workflow.Dispatch(ctx, inputGetter, ctx.Repo.Repository, ctx.Doer)
if err != nil {
if actions_service.IsInputRequiredErr(err) {
ctx.Error(http.StatusBadRequest, "workflow.Dispatch", err)
} else {
@ -679,5 +680,14 @@ func DispatchWorkflow(ctx *context.APIContext) {
return
}
ctx.JSON(http.StatusNoContent, nil)
workflowRun := &api.DispatchWorkflowRun{
ID: run.ID,
Jobs: jobs,
}
if opt.ReturnRunInfo {
ctx.JSON(http.StatusCreated, workflowRun)
} else {
ctx.JSON(http.StatusNoContent, nil)
}
}

View file

@ -39,3 +39,10 @@ type swaggerRunJobList struct {
// in:body
Body []*api.ActionRunJob `json:"body"`
}
// DispatchWorkflowRun is a Workflow Run after dispatching
// swagger:response DispatchWorkflowRun
type swaggerDispatchWorkflowRun struct {
// in:body
Body *api.DispatchWorkflowRun `json:"body"`
}

View file

@ -46,7 +46,8 @@ func ManualRunWorkflow(ctx *context_module.Context) {
return ctx.Req.PostFormValue(formKey)
}
if err := workflow.Dispatch(ctx, formKeyGetter, ctx.Repo.Repository, ctx.Doer); err != nil {
_, _, err = workflow.Dispatch(ctx, formKeyGetter, ctx.Repo.Repository, ctx.Doer)
if err != nil {
if actions_service.IsInputRequiredErr(err) {
ctx.Flash.Error(ctx.Locale.Tr("actions.workflow.dispatch.input_required", err.(actions_service.InputRequiredErr).Name))
ctx.Redirect(location)

View file

@ -20,6 +20,7 @@ import (
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/webhook"
"code.gitea.io/gitea/services/convert"
@ -49,15 +50,15 @@ type Workflow struct {
type InputValueGetter func(key string) string
func (entry *Workflow) Dispatch(ctx context.Context, inputGetter InputValueGetter, repo *repo_model.Repository, doer *user.User) error {
func (entry *Workflow) Dispatch(ctx context.Context, inputGetter InputValueGetter, repo *repo_model.Repository, doer *user.User) (r *actions_model.ActionRun, j []string, err error) {
content, err := actions.GetContentFromEntry(entry.GitEntry)
if err != nil {
return err
return nil, nil, err
}
wf, err := act_model.ReadWorkflow(bytes.NewReader(content))
if err != nil {
return err
return nil, nil, err
}
fullWorkflowID := ".forgejo/workflows/" + entry.WorkflowID
@ -79,7 +80,7 @@ func (entry *Workflow) Dispatch(ctx context.Context, inputGetter InputValueGette
if len(name) == 0 {
name = key
}
return InputRequiredErr{Name: name}
return nil, nil, InputRequiredErr{Name: name}
}
continue
}
@ -92,9 +93,11 @@ func (entry *Workflow) Dispatch(ctx context.Context, inputGetter InputValueGette
}
if int64(len(inputs)) > setting.Actions.LimitDispatchInputs {
return errors.New("to many inputs")
return nil, nil, errors.New("to many inputs")
}
jobNames := util.KeysOfMap(wf.Jobs)
payload := &structs.WorkflowDispatchPayload{
Inputs: inputs,
Ref: entry.Ref,
@ -105,7 +108,7 @@ func (entry *Workflow) Dispatch(ctx context.Context, inputGetter InputValueGette
p, err := json.Marshal(payload)
if err != nil {
return err
return nil, nil, err
}
run := &actions_model.ActionRun{
@ -126,15 +129,15 @@ func (entry *Workflow) Dispatch(ctx context.Context, inputGetter InputValueGette
vars, err := actions_model.GetVariablesOfRun(ctx, run)
if err != nil {
return err
return nil, nil, err
}
jobs, err := jobparser.Parse(content, jobparser.WithVars(vars))
if err != nil {
return err
return nil, nil, err
}
return actions_model.InsertRun(ctx, run, jobs)
return run, jobNames, actions_model.InsertRun(ctx, run, jobs)
}
func GetWorkflowFromCommit(gitRepo *git.Repository, ref, workflowID string) (*Workflow, error) {

View file

@ -16,6 +16,7 @@ import (
"time"
issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log"
base "code.gitea.io/gitea/modules/migration"
@ -543,11 +544,19 @@ func (g *GitlabDownloader) GetComments(commentable base.Commentable) ([]*base.Co
}
for _, stateEvent := range stateEvents {
// If the user is deleted, then `stateEvent.User == nil` holds. Fallback
// to the Ghost user in that case.
posterID := int64(user_model.GhostUserID)
posterName := user_model.GhostUserName
if stateEvent.User != nil {
posterID = int64(stateEvent.User.ID)
posterName = stateEvent.User.Username
}
comment := &base.Comment{
IssueIndex: commentable.GetLocalIndex(),
Index: int64(stateEvent.ID),
PosterID: int64(stateEvent.User.ID),
PosterName: stateEvent.User.Username,
PosterID: posterID,
PosterName: posterName,
Content: "",
Created: *stateEvent.CreatedAt,
}

View file

@ -39,21 +39,13 @@
<i class="icon">{{svg "octicon-filter" 16}}</i>
<input name="search" placeholder="{{ctx.Locale.Tr "repo.filter_branch_and_tag"}}...">
</div>
<div class="header">
<div class="ui grid">
<div class="two column row">
<a class="reference column" href="#" data-target=".base-branch-list">
<span class="text black">
{{svg "octicon-git-branch" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.branches"}}
</span>
</a>
<a class="reference column" href="#" data-target=".base-tag-list">
<span class="text black">
{{svg "octicon-tag" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.tags"}}
</span>
</a>
</div>
</div>
<div class="branch-tag-tab">
<a class="branch-tag-item muted" href="#" data-target=".base-branch-list">
{{svg "octicon-git-branch"}}{{ctx.Locale.Tr "repo.branches"}}
</a>
<a class="branch-tag-item muted" href="#" data-target=".base-tag-list">
{{svg "octicon-tag"}}{{ctx.Locale.Tr "repo.tags"}}
</a>
</div>
<div class="scrolling menu reference-list-menu base-branch-list">
{{range .Branches}}
@ -108,21 +100,13 @@
<i class="icon">{{svg "octicon-filter" 16}}</i>
<input name="search" placeholder="{{ctx.Locale.Tr "repo.filter_branch_and_tag"}}...">
</div>
<div class="header">
<div class="ui grid">
<div class="two column row">
<a class="reference column" href="#" data-target=".head-branch-list">
<span class="text black">
{{svg "octicon-git-branch" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.branches"}}
</span>
</a>
<a class="reference column" href="#" data-target=".head-tag-list">
<span class="text black">
{{svg "octicon-tag" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.tags"}}
</span>
</a>
</div>
</div>
<div class="branch-tag-tab">
<a class="branch-tag-item muted" href="#" data-target=".head-branch-list">
{{svg "octicon-git-branch"}}{{ctx.Locale.Tr "repo.branches"}}
</a>
<a class="branch-tag-item muted" href="#" data-target=".head-tag-list">
{{svg "octicon-tag"}}{{ctx.Locale.Tr "repo.tags"}}
</a>
</div>
<div class="scrolling menu reference-list-menu head-branch-list">
{{range .HeadBranches}}

View file

@ -15,21 +15,13 @@
<i class="icon">{{svg "octicon-filter" 16}}</i>
<input name="search" placeholder="{{ctx.Locale.Tr "repo.filter_branch_and_tag"}}...">
</div>
<div class="header">
<div class="ui grid">
<div class="two column row">
<a class="reference column muted" href="#" data-target="#branch-list">
<span class="text black">
{{svg "octicon-git-branch" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.branches"}}
</span>
</a>
<a class="reference column muted" href="#" data-target="#tag-list">
<span class="text">
{{svg "octicon-tag" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.tags"}}
</span>
</a>
</div>
</div>
<div class="branch-tag-tab">
<a class="branch-tag-item muted" href="#" data-target="#branch-list">
{{svg "octicon-git-branch"}}{{ctx.Locale.Tr "repo.branches"}}
</a>
<a class="branch-tag-item muted" href="#" data-target="#tag-list">
{{svg "octicon-tag"}}{{ctx.Locale.Tr "repo.tags"}}
</a>
</div>
<div class="branch-tag-divider"></div>
<div id="branch-list" class="scrolling menu reference-list-menu {{if not .Issue}}new-issue{{end}}">

View file

@ -1,83 +0,0 @@
<h4 class="ui top attached header">
{{ctx.Locale.Tr "user.block.title"}}
</h4>
<div class="ui attached segment">
<p>{{ctx.Locale.Tr "user.block.info_1"}}</p>
<ul>
<li>{{ctx.Locale.Tr "user.block.info_2"}}</li>
<li>{{ctx.Locale.Tr "user.block.info_3"}}</li>
<li>{{ctx.Locale.Tr "user.block.info_4"}}</li>
<li>{{ctx.Locale.Tr "user.block.info_5"}}</li>
<li>{{ctx.Locale.Tr "user.block.info_6"}}</li>
<li>{{ctx.Locale.Tr "user.block.info_7"}}</li>
</ul>
</div>
<div class="ui segment">
<form class="ui form ignore-dirty" action="{{$.Link}}" method="post">
{{.CsrfTokenHtml}}
<input type="hidden" name="action" value="block" />
<div id="search-user-box" class="field ui fluid search input">
<input class="prompt tw-mr-2" name="blockee" placeholder="{{ctx.Locale.Tr "search.user_kind"}}" autocomplete="off" required>
<button class="ui red button">{{ctx.Locale.Tr "user.block.block"}}</button>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "user.block.note.title"}}</label>
<input name="note">
<p class="help">{{ctx.Locale.Tr "user.block.note.info"}}</p>
</div>
</form>
</div>
<h4 class="ui top attached header">
{{ctx.Locale.Tr "user.block.list"}}
</h4>
<div class="ui attached segment">
<div class="flex-list">
{{range .UserBlocks}}
<div class="flex-item">
<div class="flex-item-leading">
{{ctx.AvatarUtils.Avatar .Blockee}}
</div>
<div class="flex-item-main">
<div class="flex-item-title">
<a class="item" href="{{.Blockee.HTMLURL}}">{{.Blockee.GetDisplayName}}</a>
</div>
{{if .Note}}
<div class="flex-item-body">
<i>{{ctx.Locale.Tr "user.block.note"}}:</i> {{.Note}}
</div>
{{end}}
</div>
<div class="flex-item-trailing">
<button class="ui compact mini button show-modal" data-modal="#block-user-note-modal" data-modal-modal-blockee="{{.Blockee.Name}}" data-modal-modal-note="{{.Note}}">{{ctx.Locale.Tr "user.block.note.edit"}}</button>
<form action="{{$.Link}}" method="post">
{{$.CsrfTokenHtml}}
<input type="hidden" name="action" value="unblock" />
<input type="hidden" name="blockee" value="{{.Blockee.Name}}" />
<button class="ui compact mini button">{{ctx.Locale.Tr "user.block.unblock"}}</button>
</form>
</div>
</div>
{{else}}
<div class="item">{{ctx.Locale.Tr "user.block.list.none"}}</div>
{{end}}
</div>
</div>
<div class="ui small modal" id="block-user-note-modal">
<div class="header">{{ctx.Locale.Tr "user.block.note.edit"}}</div>
<div class="content">
<form class="ui form" action="{{$.Link}}" method="post">
{{.CsrfTokenHtml}}
<input type="hidden" name="action" value="note" />
<input type="hidden" name="blockee" class="modal-blockee" />
<div class="field">
<label>{{ctx.Locale.Tr "user.block.note.title"}}</label>
<input name="note" class="modal-note" />
<p class="help">{{ctx.Locale.Tr "user.block.note.info"}}</p>
</div>
<div class="text right actions">
<button class="ui cancel button">{{ctx.Locale.Tr "cancel"}}</button>
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
</div>
</form>
</div>
</div>

View file

@ -23128,6 +23128,33 @@
"description": "Git reference for the workflow",
"type": "string",
"x-go-name": "Ref"
},
"return_run_info": {
"description": "Flag to return the run info",
"type": "boolean",
"default": false,
"x-go-name": "ReturnRunInfo"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"DispatchWorkflowRun": {
"description": "DispatchWorkflowRun represents a workflow run",
"type": "object",
"properties": {
"id": {
"description": "the workflow run id",
"type": "integer",
"format": "int64",
"x-go-name": "ID"
},
"jobs": {
"description": "the jobs name",
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "Jobs"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
@ -28511,6 +28538,12 @@
}
}
},
"DispatchWorkflowRun": {
"description": "DispatchWorkflowRun is a Workflow Run after dispatching",
"schema": {
"$ref": "#/definitions/DispatchWorkflowRun"
}
},
"EmailList": {
"description": "EmailList",
"schema": {

View file

@ -752,9 +752,18 @@ func TestWorkflowDispatchEvent(t *testing.T) {
return ""
}
err = workflow.Dispatch(db.DefaultContext, inputGetter, repo, user2)
var r *actions_model.ActionRun
var j []string
r, j, err = workflow.Dispatch(db.DefaultContext, inputGetter, repo, user2)
require.NoError(t, err)
assert.Equal(t, 1, unittest.GetCount(t, &actions_model.ActionRun{RepoID: repo.ID}))
assert.Equal(t, "test", r.Title)
assert.Equal(t, "dispatch.yml", r.WorkflowID)
assert.Equal(t, sha, r.CommitSHA)
assert.Equal(t, actions_module.GithubEventWorkflowDispatch, r.TriggerEvent)
assert.Len(t, j, 1)
assert.Equal(t, "test", j[0])
})
}

View file

@ -1153,6 +1153,19 @@ td .commit-summary {
text-align: center;
}
.branch-tag-tab {
padding: 0 10px 10px;
}
.branch-tag-item {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0 10px;
border: 1px solid transparent;
border-bottom: none;
}
.repository.compare.pull .comment.form .content::before,
.repository.compare.pull .comment.form .content::after {
right: 100%;

View file

@ -265,10 +265,10 @@ export default sfc; // activate IDE's Vue plugin
</div>
<div v-if="showBranchesInDropdown" class="branch-tag-tab">
<a class="branch-tag-item muted" :class="{active: mode === 'branches'}" href="#" @click="handleTabSwitch('branches')">
<svg-icon name="octicon-git-branch" :size="16" class="tw-mr-1"/>{{ textBranches }}
<svg-icon name="octicon-git-branch" :size="16"/>{{ textBranches }}
</a>
<a v-if="!noTag" class="branch-tag-item muted" :class="{active: mode === 'tags'}" href="#" @click="handleTabSwitch('tags')">
<svg-icon name="octicon-tag" :size="16" class="tw-mr-1"/>{{ textTags }}
<svg-icon name="octicon-tag" :size="16"/>{{ textTags }}
</a>
</div>
<div class="branch-tag-divider"/>
@ -316,17 +316,6 @@ export default sfc; // activate IDE's Vue plugin
</div>
</template>
<style scoped>
.branch-tag-tab {
padding: 0 10px;
}
.branch-tag-item {
display: inline-block;
padding: 10px;
border: 1px solid transparent;
border-bottom: none;
}
.branch-tag-item.active {
border-color: var(--color-secondary);
background: var(--color-menu);