mirror of
https://github.com/go-gitea/gitea.git
synced 2025-10-27 05:55:21 +08:00
Merge branch 'main' into lunny/merge_tree_conflict_check
This commit is contained in:
commit
562ab41116
@ -153,6 +153,7 @@ linters:
|
||||
text: '(?i)exitAfterDefer:'
|
||||
paths:
|
||||
- node_modules
|
||||
- .venv
|
||||
- public
|
||||
- web_src
|
||||
- third_party$
|
||||
@ -172,6 +173,7 @@ formatters:
|
||||
generated: lax
|
||||
paths:
|
||||
- node_modules
|
||||
- .venv
|
||||
- public
|
||||
- web_src
|
||||
- third_party$
|
||||
|
||||
@ -26,20 +26,16 @@ WORKDIR ${GOPATH}/src/code.gitea.io/gitea
|
||||
RUN if [ -n "${GITEA_VERSION}" ]; then git checkout "${GITEA_VERSION}"; fi \
|
||||
&& make clean-all build
|
||||
|
||||
# Begin env-to-ini build
|
||||
RUN go build contrib/environment-to-ini/environment-to-ini.go
|
||||
|
||||
# Copy local files
|
||||
COPY docker/root /tmp/local
|
||||
|
||||
# Set permissions
|
||||
RUN chmod 755 /tmp/local/usr/bin/entrypoint \
|
||||
/tmp/local/usr/local/bin/gitea \
|
||||
/tmp/local/usr/local/bin/* \
|
||||
/tmp/local/etc/s6/gitea/* \
|
||||
/tmp/local/etc/s6/openssh/* \
|
||||
/tmp/local/etc/s6/.s6-svscan/* \
|
||||
/go/src/code.gitea.io/gitea/gitea \
|
||||
/go/src/code.gitea.io/gitea/environment-to-ini
|
||||
/go/src/code.gitea.io/gitea/gitea
|
||||
|
||||
FROM docker.io/library/alpine:3.22
|
||||
LABEL maintainer="maintainers@gitea.io"
|
||||
@ -82,4 +78,3 @@ CMD ["/usr/bin/s6-svscan", "/etc/s6"]
|
||||
|
||||
COPY --from=build-env /tmp/local /
|
||||
COPY --from=build-env /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea
|
||||
COPY --from=build-env /go/src/code.gitea.io/gitea/environment-to-ini /usr/local/bin/environment-to-ini
|
||||
|
||||
@ -26,18 +26,12 @@ WORKDIR ${GOPATH}/src/code.gitea.io/gitea
|
||||
RUN if [ -n "${GITEA_VERSION}" ]; then git checkout "${GITEA_VERSION}"; fi \
|
||||
&& make clean-all build
|
||||
|
||||
# Begin env-to-ini build
|
||||
RUN go build contrib/environment-to-ini/environment-to-ini.go
|
||||
|
||||
# Copy local files
|
||||
COPY docker/rootless /tmp/local
|
||||
|
||||
# Set permissions
|
||||
RUN chmod 755 /tmp/local/usr/local/bin/docker-entrypoint.sh \
|
||||
/tmp/local/usr/local/bin/docker-setup.sh \
|
||||
/tmp/local/usr/local/bin/gitea \
|
||||
/go/src/code.gitea.io/gitea/gitea \
|
||||
/go/src/code.gitea.io/gitea/environment-to-ini
|
||||
RUN chmod 755 /tmp/local/usr/local/bin/* \
|
||||
/go/src/code.gitea.io/gitea/gitea
|
||||
|
||||
FROM docker.io/library/alpine:3.22
|
||||
LABEL maintainer="maintainers@gitea.io"
|
||||
@ -71,7 +65,6 @@ RUN chown git:git /var/lib/gitea /etc/gitea
|
||||
|
||||
COPY --from=build-env /tmp/local /
|
||||
COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea
|
||||
COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/environment-to-ini /usr/local/bin/environment-to-ini
|
||||
|
||||
# git:git
|
||||
USER 1000:1000
|
||||
|
||||
10
Makefile
10
Makefile
@ -31,11 +31,11 @@ XGO_VERSION := go-1.25.x
|
||||
|
||||
AIR_PACKAGE ?= github.com/air-verse/air@v1
|
||||
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3
|
||||
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.9.1
|
||||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.4.0
|
||||
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.9.2
|
||||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.5.0
|
||||
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15
|
||||
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.7.0
|
||||
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@717e3cb29becaaf00e56953556c6d80f8a01b286
|
||||
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.1
|
||||
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
|
||||
GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1
|
||||
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
|
||||
@ -258,7 +258,7 @@ clean: ## delete backend and integration files
|
||||
|
||||
.PHONY: fmt
|
||||
fmt: ## format the Go and template code
|
||||
@GOFUMPT_PACKAGE=$(GOFUMPT_PACKAGE) $(GO) run build/code-batch-process.go gitea-fmt -w '{file-list}'
|
||||
@GOFUMPT_PACKAGE=$(GOFUMPT_PACKAGE) $(GO) run tools/code-batch-process.go gitea-fmt -w '{file-list}'
|
||||
$(eval TEMPLATES := $(shell find templates -type f -name '*.tmpl'))
|
||||
@# strip whitespace after '{{' or '(' and before '}}' or ')' unless there is only
|
||||
@# whitespace before it
|
||||
@ -472,7 +472,7 @@ test\#%:
|
||||
coverage:
|
||||
grep '^\(mode: .*\)\|\(.*:[0-9]\+\.[0-9]\+,[0-9]\+\.[0-9]\+ [0-9]\+ [0-9]\+\)$$' coverage.out > coverage-bodged.out
|
||||
grep '^\(mode: .*\)\|\(.*:[0-9]\+\.[0-9]\+,[0-9]\+\.[0-9]\+ [0-9]\+ [0-9]\+\)$$' integration.coverage.out > integration.coverage-bodged.out
|
||||
$(GO) run build/gocovmerge.go integration.coverage-bodged.out coverage-bodged.out > coverage.all
|
||||
$(GO) run tools/gocovmerge.go integration.coverage-bodged.out coverage-bodged.out > coverage.all
|
||||
|
||||
.PHONY: unit-test-coverage
|
||||
unit-test-coverage:
|
||||
|
||||
156
cmd/config.go
Normal file
156
cmd/config.go
Normal file
@ -0,0 +1,156 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func cmdConfig() *cli.Command {
|
||||
subcmdConfigEditIni := &cli.Command{
|
||||
Name: "edit-ini",
|
||||
Usage: "Load an existing INI file, apply environment variables, keep specified keys, and output to a new INI file.",
|
||||
Description: `
|
||||
Help users to edit the Gitea configuration INI file.
|
||||
|
||||
# Keep Specified Keys
|
||||
|
||||
If you need to re-create the configuration file with only a subset of keys,
|
||||
you can provide an INI template file for the kept keys and use the "--config-keep-keys" flag.
|
||||
For example, if a helm chart needs to reset the settings and only keep SECRET_KEY,
|
||||
it can use a template file (only keys take effect, values are ignored):
|
||||
|
||||
[security]
|
||||
SECRET_KEY=
|
||||
|
||||
$ ./gitea config edit-ini --config app-old.ini --config-keep-keys app-keys.ini --out app-new.ini
|
||||
|
||||
# Map Environment Variables to INI Configuration
|
||||
|
||||
Environment variables of the form "GITEA__section_name__KEY_NAME"
|
||||
will be mapped to the ini section "[section_name]" and the key
|
||||
"KEY_NAME" with the value as provided.
|
||||
|
||||
Environment variables of the form "GITEA__section_name__KEY_NAME__FILE"
|
||||
will be mapped to the ini section "[section_name]" and the key
|
||||
"KEY_NAME" with the value loaded from the specified file.
|
||||
|
||||
Environment variable keys can only contain characters "0-9A-Z_",
|
||||
if a section or key name contains dot ".", it needs to be escaped as _0x2E_.
|
||||
For example, to apply this config:
|
||||
|
||||
[git.config]
|
||||
foo.bar=val
|
||||
|
||||
$ export GITEA__git_0x2E_config__foo_0x2E_bar=val
|
||||
|
||||
# Put All Together
|
||||
|
||||
$ ./gitea config edit-ini --config app.ini --config-keep-keys app-keys.ini --apply-env {--in-place|--out app-new.ini}
|
||||
`,
|
||||
Flags: []cli.Flag{
|
||||
// "--config" flag is provided by global flags, and this flag is also used by "environment-to-ini" script wrapper
|
||||
// "--in-place" is also used by "environment-to-ini" script wrapper for its old behavior: always overwrite the existing config file
|
||||
&cli.BoolFlag{
|
||||
Name: "in-place",
|
||||
Usage: "Output to the same config file as input. This flag will be ignored if --out is set.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "config-keep-keys",
|
||||
Usage: "An INI template file containing keys for keeping. Only the keys defined in the INI template will be kept from old config. If not set, all keys will be kept.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "apply-env",
|
||||
Usage: "Apply all GITEA__* variables from the environment to the config.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "out",
|
||||
Usage: "Destination config file to write to.",
|
||||
},
|
||||
},
|
||||
Action: runConfigEditIni,
|
||||
}
|
||||
|
||||
return &cli.Command{
|
||||
Name: "config",
|
||||
Usage: "Manage Gitea configuration",
|
||||
Commands: []*cli.Command{
|
||||
subcmdConfigEditIni,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runConfigEditIni(_ context.Context, c *cli.Command) error {
|
||||
// the config system may change the environment variables, so get a copy first, to be used later
|
||||
env := append([]string{}, os.Environ()...)
|
||||
|
||||
// don't use the guessed setting.CustomConf, instead, require the user to provide --config explicitly
|
||||
if !c.IsSet("config") {
|
||||
return errors.New("flag is required but not set: --config")
|
||||
}
|
||||
configFileIn := c.String("config")
|
||||
|
||||
cfgIn, err := setting.NewConfigProviderFromFile(configFileIn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load config file %q: %v", configFileIn, err)
|
||||
}
|
||||
|
||||
// determine output config file: use "--out" flag or use "--in-place" flag to overwrite input file
|
||||
inPlace := c.Bool("in-place")
|
||||
configFileOut := c.String("out")
|
||||
if configFileOut == "" {
|
||||
if !inPlace {
|
||||
return errors.New("either --in-place or --out must be specified")
|
||||
}
|
||||
configFileOut = configFileIn // in-place edit
|
||||
}
|
||||
|
||||
needWriteOut := configFileOut != configFileIn
|
||||
|
||||
cfgOut := cfgIn
|
||||
configKeepKeys := c.String("config-keep-keys")
|
||||
if configKeepKeys != "" {
|
||||
needWriteOut = true
|
||||
cfgOut, err = setting.NewConfigProviderFromFile(configKeepKeys)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load config-keep-keys template file %q: %v", configKeepKeys, err)
|
||||
}
|
||||
|
||||
for _, secOut := range cfgOut.Sections() {
|
||||
for _, keyOut := range secOut.Keys() {
|
||||
secIn := cfgIn.Section(secOut.Name())
|
||||
keyIn := setting.ConfigSectionKey(secIn, keyOut.Name())
|
||||
if keyIn != nil {
|
||||
keyOut.SetValue(keyIn.String())
|
||||
} else {
|
||||
secOut.DeleteKey(keyOut.Name())
|
||||
}
|
||||
}
|
||||
if len(secOut.Keys()) == 0 {
|
||||
cfgOut.DeleteSection(secOut.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.Bool("apply-env") {
|
||||
if setting.EnvironmentToConfig(cfgOut, env) {
|
||||
needWriteOut = true
|
||||
}
|
||||
}
|
||||
|
||||
if needWriteOut {
|
||||
err = cfgOut.SaveTo(configFileOut)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
85
cmd/config_test.go
Normal file
85
cmd/config_test.go
Normal file
@ -0,0 +1,85 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConfigEdit(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configOld := tmpDir + "/app-old.ini"
|
||||
configTemplate := tmpDir + "/app-template.ini"
|
||||
_ = os.WriteFile(configOld, []byte(`
|
||||
[sec]
|
||||
k1=v1
|
||||
k2=v2
|
||||
`), os.ModePerm)
|
||||
|
||||
_ = os.WriteFile(configTemplate, []byte(`
|
||||
[sec]
|
||||
k1=in-template
|
||||
|
||||
[sec2]
|
||||
k3=v3
|
||||
`), os.ModePerm)
|
||||
|
||||
t.Setenv("GITEA__EnV__KeY", "val")
|
||||
|
||||
t.Run("OutputToNewWithEnv", func(t *testing.T) {
|
||||
configNew := tmpDir + "/app-new.ini"
|
||||
err := NewMainApp(AppVersion{}).Run(t.Context(), []string{
|
||||
"./gitea", "--config", configOld,
|
||||
"config", "edit-ini",
|
||||
"--apply-env",
|
||||
"--config-keep-keys", configTemplate,
|
||||
"--out", configNew,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// "k1" old value is kept because its key is in the template
|
||||
// "k2" is removed because it isn't in the template
|
||||
// "k3" isn't in new config because it isn't in the old config
|
||||
// [env] is applied from environment variable
|
||||
data, _ := os.ReadFile(configNew)
|
||||
require.Equal(t, `[sec]
|
||||
k1 = v1
|
||||
|
||||
[env]
|
||||
KeY = val
|
||||
`, string(data))
|
||||
})
|
||||
|
||||
t.Run("OutputToExisting(environment-to-ini)", func(t *testing.T) {
|
||||
// the legacy "environment-to-ini" (now a wrapper script) behavior:
|
||||
// if no "--out", then "--in-place" must be used to overwrite the existing "--config" file
|
||||
err := NewMainApp(AppVersion{}).Run(t.Context(), []string{
|
||||
"./gitea", "config", "edit-ini",
|
||||
"--apply-env",
|
||||
"--config", configOld,
|
||||
})
|
||||
require.ErrorContains(t, err, "either --in-place or --out must be specified")
|
||||
|
||||
// simulate the "environment-to-ini" behavior with "--in-place"
|
||||
err = NewMainApp(AppVersion{}).Run(t.Context(), []string{
|
||||
"./gitea", "config", "edit-ini",
|
||||
"--in-place",
|
||||
"--apply-env",
|
||||
"--config", configOld,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
data, _ := os.ReadFile(configOld)
|
||||
require.Equal(t, `[sec]
|
||||
k1 = v1
|
||||
k2 = v2
|
||||
|
||||
[env]
|
||||
KeY = val
|
||||
`, string(data))
|
||||
})
|
||||
}
|
||||
@ -128,6 +128,7 @@ func NewMainApp(appVer AppVersion) *cli.Command {
|
||||
|
||||
// these sub-commands do not need the config file, and they do not depend on any path or environment variable.
|
||||
subCmdStandalone := []*cli.Command{
|
||||
cmdConfig(),
|
||||
cmdCert(),
|
||||
CmdGenerate,
|
||||
CmdDocs,
|
||||
|
||||
@ -156,7 +156,6 @@ func serveInstall(cmd *cli.Command) error {
|
||||
case <-graceful.GetManager().IsShutdown():
|
||||
<-graceful.GetManager().Done()
|
||||
log.Info("PID: %d Gitea Web Finished", os.Getpid())
|
||||
log.GetManager().Close()
|
||||
return err
|
||||
default:
|
||||
}
|
||||
@ -231,7 +230,6 @@ func serveInstalled(c *cli.Command) error {
|
||||
err := listen(webRoutes, true)
|
||||
<-graceful.GetManager().Done()
|
||||
log.Info("PID: %d Gitea Web Finished", os.Getpid())
|
||||
log.GetManager().Close()
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@ -1,47 +0,0 @@
|
||||
Environment To Ini
|
||||
==================
|
||||
|
||||
Multiple docker users have requested that the Gitea docker is changed
|
||||
to permit arbitrary configuration via environment variables.
|
||||
|
||||
Gitea needs to use an ini file for configuration because the running
|
||||
environment that starts the docker may not be the same as that used
|
||||
by the hooks. An ini file also gives a good default and means that
|
||||
users do not have to completely provide a full environment.
|
||||
|
||||
With those caveats above, this command provides a generic way of
|
||||
converting suitably structured environment variables into any ini
|
||||
value.
|
||||
|
||||
To use the command is very simple just run it and the default gitea
|
||||
app.ini will be rewritten to take account of the variables provided,
|
||||
however there are various options to give slightly different
|
||||
behavior and these can be interrogated with the `-h` option.
|
||||
|
||||
The environment variables should be of the form:
|
||||
|
||||
GITEA__SECTION_NAME__KEY_NAME
|
||||
|
||||
Note, SECTION_NAME in the notation above is case-insensitive.
|
||||
|
||||
Environment variables are usually restricted to a reduced character
|
||||
set "0-9A-Z_" - in order to allow the setting of sections with
|
||||
characters outside of that set, they should be escaped as following:
|
||||
"_0X2E_" for "." and "_0X2D_" for "-". The entire section and key names
|
||||
can be escaped as a UTF8 byte string if necessary. E.g. to configure:
|
||||
|
||||
"""
|
||||
...
|
||||
[log.console]
|
||||
COLORIZE=false
|
||||
STDERR=true
|
||||
...
|
||||
"""
|
||||
|
||||
You would set the environment variables: "GITEA__LOG_0x2E_CONSOLE__COLORIZE=false"
|
||||
and "GITEA__LOG_0x2E_CONSOLE__STDERR=false". Other examples can be found
|
||||
on the configuration cheat sheet.
|
||||
|
||||
To build locally, run:
|
||||
|
||||
go build contrib/environment-to-ini/environment-to-ini.go
|
||||
@ -1,112 +0,0 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := cli.Command{}
|
||||
app.Name = "environment-to-ini"
|
||||
app.Usage = "Use provided environment to update configuration ini"
|
||||
app.Description = `As a helper to allow docker users to update the gitea configuration
|
||||
through the environment, this command allows environment variables to
|
||||
be mapped to values in the ini.
|
||||
|
||||
Environment variables of the form "GITEA__SECTION_NAME__KEY_NAME"
|
||||
will be mapped to the ini section "[section_name]" and the key
|
||||
"KEY_NAME" with the value as provided.
|
||||
|
||||
Environment variables of the form "GITEA__SECTION_NAME__KEY_NAME__FILE"
|
||||
will be mapped to the ini section "[section_name]" and the key
|
||||
"KEY_NAME" with the value loaded from the specified file.
|
||||
|
||||
Environment variables are usually restricted to a reduced character
|
||||
set "0-9A-Z_" - in order to allow the setting of sections with
|
||||
characters outside of that set, they should be escaped as following:
|
||||
"_0X2E_" for ".". The entire section and key names can be escaped as
|
||||
a UTF8 byte string if necessary. E.g. to configure:
|
||||
|
||||
"""
|
||||
...
|
||||
[log.console]
|
||||
COLORIZE=false
|
||||
STDERR=true
|
||||
...
|
||||
"""
|
||||
|
||||
You would set the environment variables: "GITEA__LOG_0x2E_CONSOLE__COLORIZE=false"
|
||||
and "GITEA__LOG_0x2E_CONSOLE__STDERR=false". Other examples can be found
|
||||
on the configuration cheat sheet.`
|
||||
app.Flags = []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "custom-path",
|
||||
Aliases: []string{"C"},
|
||||
Value: setting.CustomPath,
|
||||
Usage: "Custom path file path",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "config",
|
||||
Aliases: []string{"c"},
|
||||
Value: setting.CustomConf,
|
||||
Usage: "Custom configuration file path",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "work-path",
|
||||
Aliases: []string{"w"},
|
||||
Value: setting.AppWorkPath,
|
||||
Usage: "Set the gitea working path",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "out",
|
||||
Aliases: []string{"o"},
|
||||
Value: "",
|
||||
Usage: "Destination file to write to",
|
||||
},
|
||||
}
|
||||
app.Action = runEnvironmentToIni
|
||||
err := app.Run(context.Background(), os.Args)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to run app with %s: %v", os.Args, err)
|
||||
}
|
||||
}
|
||||
|
||||
func runEnvironmentToIni(_ context.Context, c *cli.Command) error {
|
||||
// the config system may change the environment variables, so get a copy first, to be used later
|
||||
env := append([]string{}, os.Environ()...)
|
||||
setting.InitWorkPathAndCfgProvider(os.Getenv, setting.ArgWorkPathAndCustomConf{
|
||||
WorkPath: c.String("work-path"),
|
||||
CustomPath: c.String("custom-path"),
|
||||
CustomConf: c.String("config"),
|
||||
})
|
||||
|
||||
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to load custom conf '%s': %v", setting.CustomConf, err)
|
||||
}
|
||||
|
||||
changed := setting.EnvironmentToConfig(cfg, env)
|
||||
|
||||
// try to save the config file
|
||||
destination := c.String("out")
|
||||
if len(destination) == 0 {
|
||||
destination = setting.CustomConf
|
||||
}
|
||||
if destination != setting.CustomConf || changed {
|
||||
log.Info("Settings saved to: %q", destination)
|
||||
err = cfg.SaveTo(destination)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -2540,7 +2540,19 @@ LEVEL = Info
|
||||
;; * sanitized: Sanitize the content and render it inside current page, default to only allow a few HTML tags and attributes. Customized sanitizer rules can be defined in [markup.sanitizer.*] .
|
||||
;; * no-sanitizer: Disable the sanitizer and render the content inside current page. It's **insecure** and may lead to XSS attack if the content contains malicious code.
|
||||
;; * iframe: Render the content in a separate standalone page and embed it into current page by iframe. The iframe is in sandbox mode with same-origin disabled, and the JS code are safely isolated from parent page.
|
||||
;RENDER_CONTENT_MODE=sanitized
|
||||
;RENDER_CONTENT_MODE = sanitized
|
||||
;; The sandbox applied to the iframe and Content-Security-Policy header when RENDER_CONTENT_MODE is `iframe`.
|
||||
;; It defaults to a safe set of "allow-*" restrictions (space separated).
|
||||
;; You can also set it by your requirements or use "disabled" to disable the sandbox completely.
|
||||
;; When set it, make sure there is no security risk:
|
||||
;; * PDF-only content: generally safe to use "disabled", and it needs to be "disabled" because PDF only renders with no sandbox.
|
||||
;; * HTML content with JS: if the "RENDER_COMMAND" can guarantee there is no XSS, then it is safe, otherwise, you need to fine tune the "allow-*" restrictions.
|
||||
;RENDER_CONTENT_SANDBOX =
|
||||
;; Whether post-process the rendered HTML content, including:
|
||||
;; resolve relative links and image sources, recognizing issue/commit references, escaping invisible characters,
|
||||
;; mentioning users, rendering permlink code blocks, replacing emoji shorthands, etc.
|
||||
;; By default, this is true when RENDER_CONTENT_MODE is `sanitized`, otherwise false.
|
||||
;NEED_POST_PROCESS = false
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
2
docker/root/usr/local/bin/environment-to-ini
Normal file
2
docker/root/usr/local/bin/environment-to-ini
Normal file
@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
exec /app/gitea/gitea config edit-ini --in-place --apply-env "$@"
|
||||
2
docker/rootless/usr/local/bin/environment-to-ini
Normal file
2
docker/rootless/usr/local/bin/environment-to-ini
Normal file
@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
exec /app/gitea/gitea config edit-ini --in-place --apply-env "$@"
|
||||
@ -49,6 +49,7 @@ export default defineConfig([
|
||||
},
|
||||
linterOptions: {
|
||||
reportUnusedDisableDirectives: 2,
|
||||
reportUnusedInlineConfigs: 2,
|
||||
},
|
||||
plugins: {
|
||||
'@eslint-community/eslint-comments': comments,
|
||||
|
||||
1
main.go
1
main.go
@ -44,6 +44,7 @@ func main() {
|
||||
}
|
||||
app := cmd.NewMainApp(cmd.AppVersion{Version: Version, Extra: formatBuiltWith()})
|
||||
_ = cmd.RunMainApp(app, os.Args...) // all errors should have been handled by the RunMainApp
|
||||
// flush the queued logs before exiting, it is a MUST, otherwise there will be log loss
|
||||
log.GetManager().Close()
|
||||
}
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ import (
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/shared/types"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
@ -173,6 +174,13 @@ func (r *ActionRunner) GenerateToken() (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
// CanMatchLabels checks whether the runner's labels can match a job's "runs-on"
|
||||
// See https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idruns-on
|
||||
func (r *ActionRunner) CanMatchLabels(jobRunsOn []string) bool {
|
||||
runnerLabelSet := container.SetOf(r.AgentLabels...)
|
||||
return runnerLabelSet.Contains(jobRunsOn...) // match all labels
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(&ActionRunner{})
|
||||
}
|
||||
|
||||
@ -13,7 +13,6 @@ import (
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
@ -245,7 +244,7 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
|
||||
var job *ActionRunJob
|
||||
log.Trace("runner labels: %v", runner.AgentLabels)
|
||||
for _, v := range jobs {
|
||||
if isSubset(runner.AgentLabels, v.RunsOn) {
|
||||
if runner.CanMatchLabels(v.RunsOn) {
|
||||
job = v
|
||||
break
|
||||
}
|
||||
@ -475,20 +474,6 @@ func FindOldTasksToExpire(ctx context.Context, olderThan timeutil.TimeStamp, lim
|
||||
Find(&tasks)
|
||||
}
|
||||
|
||||
func isSubset(set, subset []string) bool {
|
||||
m := make(container.Set[string], len(set))
|
||||
for _, v := range set {
|
||||
m.Add(v)
|
||||
}
|
||||
|
||||
for _, v := range subset {
|
||||
if !m.Contains(v) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func convertTimestamp(timestamp *timestamppb.Timestamp) timeutil.TimeStamp {
|
||||
if timestamp.GetSeconds() == 0 && timestamp.GetNanos() == 0 {
|
||||
return timeutil.TimeStamp(0)
|
||||
|
||||
@ -11,6 +11,7 @@ import (
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/migration"
|
||||
"code.gitea.io/gitea/modules/secret"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@ -123,17 +124,17 @@ func (task *Task) MigrateConfig() (*migration.MigrateOptions, error) {
|
||||
// decrypt credentials
|
||||
if opts.CloneAddrEncrypted != "" {
|
||||
if opts.CloneAddr, err = secret.DecryptSecret(setting.SecretKey, opts.CloneAddrEncrypted); err != nil {
|
||||
return nil, err
|
||||
log.Error("Unable to decrypt CloneAddr, maybe SECRET_KEY is wrong: %v", err)
|
||||
}
|
||||
}
|
||||
if opts.AuthPasswordEncrypted != "" {
|
||||
if opts.AuthPassword, err = secret.DecryptSecret(setting.SecretKey, opts.AuthPasswordEncrypted); err != nil {
|
||||
return nil, err
|
||||
log.Error("Unable to decrypt AuthPassword, maybe SECRET_KEY is wrong: %v", err)
|
||||
}
|
||||
}
|
||||
if opts.AuthTokenEncrypted != "" {
|
||||
if opts.AuthToken, err = secret.DecryptSecret(setting.SecretKey, opts.AuthTokenEncrypted); err != nil {
|
||||
return nil, err
|
||||
log.Error("Unable to decrypt AuthToken, maybe SECRET_KEY is wrong: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -111,11 +111,11 @@ func (t *TwoFactor) SetSecret(secretString string) error {
|
||||
func (t *TwoFactor) ValidateTOTP(passcode string) (bool, error) {
|
||||
decodedStoredSecret, err := base64.StdEncoding.DecodeString(t.Secret)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, fmt.Errorf("ValidateTOTP invalid base64: %w", err)
|
||||
}
|
||||
secretBytes, err := secret.AesDecrypt(t.getEncryptionKey(), decodedStoredSecret)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, fmt.Errorf("ValidateTOTP unable to decrypt (maybe SECRET_KEY is wrong): %w", err)
|
||||
}
|
||||
secretStr := string(secretBytes)
|
||||
return totp.Validate(passcode, secretStr), nil
|
||||
|
||||
@ -139,3 +139,23 @@
|
||||
updated: 1683636626
|
||||
need_approval: 0
|
||||
approved_by: 0
|
||||
-
|
||||
id: 804
|
||||
title: "use a private action"
|
||||
repo_id: 60
|
||||
owner_id: 40
|
||||
workflow_id: "run.yaml"
|
||||
index: 189
|
||||
trigger_user_id: 40
|
||||
ref: "refs/heads/master"
|
||||
commit_sha: "6e64b26de7ba966d01d90ecfaf5c7f14ef203e86"
|
||||
event: "push"
|
||||
trigger_event: "push"
|
||||
is_fork_pull_request: 0
|
||||
status: 1
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
created: 1683636108
|
||||
updated: 1683636626
|
||||
need_approval: 0
|
||||
approved_by: 0
|
||||
|
||||
@ -129,3 +129,17 @@
|
||||
status: 5
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
-
|
||||
id: 205
|
||||
run_id: 804
|
||||
repo_id: 6
|
||||
owner_id: 10
|
||||
commit_sha: 6e64b26de7ba966d01d90ecfaf5c7f14ef203e86
|
||||
is_fork_pull_request: 0
|
||||
name: job_2
|
||||
attempt: 1
|
||||
job_id: job_2
|
||||
task_id: 48
|
||||
status: 1
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
|
||||
@ -177,3 +177,23 @@
|
||||
log_length: 0
|
||||
log_size: 0
|
||||
log_expired: 0
|
||||
-
|
||||
id: 55
|
||||
job_id: 205
|
||||
attempt: 1
|
||||
runner_id: 1
|
||||
status: 6 # 6 is the status code for "running"
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
repo_id: 6
|
||||
owner_id: 10
|
||||
commit_sha: 6e64b26de7ba966d01d90ecfaf5c7f14ef203e86
|
||||
is_fork_pull_request: 0
|
||||
token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc478422b
|
||||
token_salt: ERxJGHvg3I
|
||||
token_last_eight: 182199eb
|
||||
log_filename: collaborative-owner-test/1a/49.log
|
||||
log_in_storage: 1
|
||||
log_length: 707
|
||||
log_size: 90179
|
||||
log_expired: 0
|
||||
|
||||
@ -225,3 +225,27 @@
|
||||
is_deleted: false
|
||||
deleted_by_id: 0
|
||||
deleted_unix: 0
|
||||
|
||||
-
|
||||
id: 27
|
||||
repo_id: 1
|
||||
name: 'DefaultBranch'
|
||||
commit_id: '90c1019714259b24fb81711d4416ac0f18667dfa'
|
||||
commit_message: 'add license'
|
||||
commit_time: 1709345946
|
||||
pusher_id: 1
|
||||
is_deleted: false
|
||||
deleted_by_id: 0
|
||||
deleted_unix: 0
|
||||
|
||||
-
|
||||
id: 28
|
||||
repo_id: 1
|
||||
name: 'sub-home-md-img-check'
|
||||
commit_id: '4649299398e4d39a5c09eb4f534df6f1e1eb87cc'
|
||||
commit_message: "Test how READMEs render images when found in a subfolder"
|
||||
commit_time: 1678403550
|
||||
pusher_id: 1
|
||||
is_deleted: false
|
||||
deleted_by_id: 0
|
||||
deleted_unix: 0
|
||||
|
||||
@ -733,3 +733,10 @@
|
||||
type: 3
|
||||
config: "{\"IgnoreWhitespaceConflicts\":false,\"AllowMerge\":true,\"AllowRebase\":true,\"AllowRebaseMerge\":true,\"AllowSquash\":true}"
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 111
|
||||
repo_id: 3
|
||||
type: 10
|
||||
config: "{}"
|
||||
created_unix: 946684810
|
||||
|
||||
@ -8,7 +8,6 @@ import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
@ -42,30 +41,6 @@ func (err ErrLFSLockNotExist) Unwrap() error {
|
||||
return util.ErrNotExist
|
||||
}
|
||||
|
||||
// ErrLFSUnauthorizedAction represents a "LFSUnauthorizedAction" kind of error.
|
||||
type ErrLFSUnauthorizedAction struct {
|
||||
RepoID int64
|
||||
UserName string
|
||||
Mode perm.AccessMode
|
||||
}
|
||||
|
||||
// IsErrLFSUnauthorizedAction checks if an error is a ErrLFSUnauthorizedAction.
|
||||
func IsErrLFSUnauthorizedAction(err error) bool {
|
||||
_, ok := err.(ErrLFSUnauthorizedAction)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrLFSUnauthorizedAction) Error() string {
|
||||
if err.Mode == perm.AccessModeWrite {
|
||||
return fmt.Sprintf("User %s doesn't have write access for lfs lock [rid: %d]", err.UserName, err.RepoID)
|
||||
}
|
||||
return fmt.Sprintf("User %s doesn't have read access for lfs lock [rid: %d]", err.UserName, err.RepoID)
|
||||
}
|
||||
|
||||
func (err ErrLFSUnauthorizedAction) Unwrap() error {
|
||||
return util.ErrPermissionDenied
|
||||
}
|
||||
|
||||
// ErrLFSLockAlreadyExist represents a "LFSLockAlreadyExist" kind of error.
|
||||
type ErrLFSLockAlreadyExist struct {
|
||||
RepoID int64
|
||||
@ -93,12 +68,6 @@ type ErrLFSFileLocked struct {
|
||||
UserName string
|
||||
}
|
||||
|
||||
// IsErrLFSFileLocked checks if an error is a ErrLFSFileLocked.
|
||||
func IsErrLFSFileLocked(err error) bool {
|
||||
_, ok := err.(ErrLFSFileLocked)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrLFSFileLocked) Error() string {
|
||||
return fmt.Sprintf("File is lfs locked [repo: %d, locked by: %s, path: %s]", err.RepoID, err.UserName, err.Path)
|
||||
}
|
||||
|
||||
@ -11,10 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
@ -71,10 +68,6 @@ func (l *LFSLock) LoadOwner(ctx context.Context) error {
|
||||
// CreateLFSLock creates a new lock.
|
||||
func CreateLFSLock(ctx context.Context, repo *repo_model.Repository, lock *LFSLock) (*LFSLock, error) {
|
||||
return db.WithTx2(ctx, func(ctx context.Context) (*LFSLock, error) {
|
||||
if err := CheckLFSAccessForRepo(ctx, lock.OwnerID, repo, perm.AccessModeWrite); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lock.Path = util.PathJoinRel(lock.Path)
|
||||
lock.RepoID = repo.ID
|
||||
|
||||
@ -165,10 +158,6 @@ func DeleteLFSLockByID(ctx context.Context, id int64, repo *repo_model.Repositor
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := CheckLFSAccessForRepo(ctx, u.ID, repo, perm.AccessModeWrite); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !force && u.ID != lock.OwnerID {
|
||||
return nil, errors.New("user doesn't own lock and force flag is not set")
|
||||
}
|
||||
@ -180,22 +169,3 @@ func DeleteLFSLockByID(ctx context.Context, id int64, repo *repo_model.Repositor
|
||||
return lock, nil
|
||||
})
|
||||
}
|
||||
|
||||
// CheckLFSAccessForRepo check needed access mode base on action
|
||||
func CheckLFSAccessForRepo(ctx context.Context, ownerID int64, repo *repo_model.Repository, mode perm.AccessMode) error {
|
||||
if ownerID == 0 {
|
||||
return ErrLFSUnauthorizedAction{repo.ID, "undefined", mode}
|
||||
}
|
||||
u, err := user_model.GetUserByID(ctx, ownerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
perm, err := access_model.GetUserRepoPermission(ctx, repo, u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !perm.CanAccess(mode, unit.TypeCode) {
|
||||
return ErrLFSUnauthorizedAction{repo.ID, u.DisplayName(), mode}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -5,7 +5,6 @@ package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
@ -25,7 +24,7 @@ import (
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
var ErrBranchIsProtected = errors.New("branch is protected")
|
||||
var ErrBranchIsProtected = util.ErrorWrap(util.ErrPermissionDenied, "branch is protected")
|
||||
|
||||
// ProtectedBranch struct
|
||||
type ProtectedBranch struct {
|
||||
|
||||
@ -5,9 +5,11 @@ package access
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
perm_model "code.gitea.io/gitea/models/perm"
|
||||
@ -253,6 +255,43 @@ func finalProcessRepoUnitPermission(user *user_model.User, perm *Permission) {
|
||||
}
|
||||
}
|
||||
|
||||
// GetActionsUserRepoPermission returns the actions user permissions to the repository
|
||||
func GetActionsUserRepoPermission(ctx context.Context, repo *repo_model.Repository, actionsUser *user_model.User, taskID int64) (perm Permission, err error) {
|
||||
if actionsUser.ID != user_model.ActionsUserID {
|
||||
return perm, errors.New("api GetActionsUserRepoPermission can only be called by the actions user")
|
||||
}
|
||||
task, err := actions_model.GetTaskByID(ctx, taskID)
|
||||
if err != nil {
|
||||
return perm, err
|
||||
}
|
||||
|
||||
var accessMode perm_model.AccessMode
|
||||
if task.RepoID != repo.ID {
|
||||
taskRepo, exist, err := db.GetByID[repo_model.Repository](ctx, task.RepoID)
|
||||
if err != nil || !exist {
|
||||
return perm, err
|
||||
}
|
||||
actionsCfg := repo.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
|
||||
if !actionsCfg.IsCollaborativeOwner(taskRepo.OwnerID) || !taskRepo.IsPrivate {
|
||||
// The task repo can access the current repo only if the task repo is private and
|
||||
// the owner of the task repo is a collaborative owner of the current repo.
|
||||
// FIXME allow public repo read access if tokenless pull is enabled
|
||||
return perm, nil
|
||||
}
|
||||
accessMode = perm_model.AccessModeRead
|
||||
} else if task.IsForkPullRequest {
|
||||
accessMode = perm_model.AccessModeRead
|
||||
} else {
|
||||
accessMode = perm_model.AccessModeWrite
|
||||
}
|
||||
|
||||
if err := repo.LoadUnits(ctx); err != nil {
|
||||
return perm, err
|
||||
}
|
||||
perm.SetUnitsWithDefaultAccessMode(repo.Units, accessMode)
|
||||
return perm, nil
|
||||
}
|
||||
|
||||
// GetUserRepoPermission returns the user permissions to the repository
|
||||
func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (perm Permission, err error) {
|
||||
defer func() {
|
||||
|
||||
@ -170,6 +170,9 @@ func (cfg *PullRequestsConfig) GetDefaultMergeStyle() MergeStyle {
|
||||
|
||||
type ActionsConfig struct {
|
||||
DisabledWorkflows []string
|
||||
// CollaborativeOwnerIDs is a list of owner IDs used to share actions from private repos.
|
||||
// Only workflows from the private repos whose owners are in CollaborativeOwnerIDs can access the current repo's actions.
|
||||
CollaborativeOwnerIDs []int64
|
||||
}
|
||||
|
||||
func (cfg *ActionsConfig) EnableWorkflow(file string) {
|
||||
@ -192,6 +195,20 @@ func (cfg *ActionsConfig) DisableWorkflow(file string) {
|
||||
cfg.DisabledWorkflows = append(cfg.DisabledWorkflows, file)
|
||||
}
|
||||
|
||||
func (cfg *ActionsConfig) AddCollaborativeOwner(ownerID int64) {
|
||||
if !slices.Contains(cfg.CollaborativeOwnerIDs, ownerID) {
|
||||
cfg.CollaborativeOwnerIDs = append(cfg.CollaborativeOwnerIDs, ownerID)
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *ActionsConfig) RemoveCollaborativeOwner(ownerID int64) {
|
||||
cfg.CollaborativeOwnerIDs = util.SliceRemoveAll(cfg.CollaborativeOwnerIDs, ownerID)
|
||||
}
|
||||
|
||||
func (cfg *ActionsConfig) IsCollaborativeOwner(ownerID int64) bool {
|
||||
return slices.Contains(cfg.CollaborativeOwnerIDs, ownerID)
|
||||
}
|
||||
|
||||
// FromDB fills up a ActionsConfig from serialized format.
|
||||
func (cfg *ActionsConfig) FromDB(bs []byte) error {
|
||||
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
|
||||
@ -178,8 +178,8 @@ func GetSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) (map[
|
||||
for _, secret := range append(ownerSecrets, repoSecrets...) {
|
||||
v, err := secret_module.DecryptSecret(setting.SecretKey, secret.Data)
|
||||
if err != nil {
|
||||
log.Error("decrypt secret %v %q: %v", secret.ID, secret.Name, err)
|
||||
return nil, err
|
||||
log.Error("Unable to decrypt Actions secret %v %q, maybe SECRET_KEY is wrong: %v", secret.ID, secret.Name, err)
|
||||
continue
|
||||
}
|
||||
secrets[secret.Name] = v
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ package user
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
@ -22,7 +23,7 @@ type SearchUserOptions struct {
|
||||
db.ListOptions
|
||||
|
||||
Keyword string
|
||||
Type UserType
|
||||
Types []UserType
|
||||
UID int64
|
||||
LoginName string // this option should be used only for admin user
|
||||
SourceID int64 // this option should be used only for admin user
|
||||
@ -43,16 +44,16 @@ type SearchUserOptions struct {
|
||||
|
||||
func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Session {
|
||||
var cond builder.Cond
|
||||
cond = builder.Eq{"type": opts.Type}
|
||||
cond = builder.In("type", opts.Types)
|
||||
if opts.IncludeReserved {
|
||||
switch opts.Type {
|
||||
case UserTypeIndividual:
|
||||
switch {
|
||||
case slices.Contains(opts.Types, UserTypeIndividual):
|
||||
cond = cond.Or(builder.Eq{"type": UserTypeUserReserved}).Or(
|
||||
builder.Eq{"type": UserTypeBot},
|
||||
).Or(
|
||||
builder.Eq{"type": UserTypeRemoteUser},
|
||||
)
|
||||
case UserTypeOrganization:
|
||||
case slices.Contains(opts.Types, UserTypeOrganization):
|
||||
cond = cond.Or(builder.Eq{"type": UserTypeOrganizationReserved})
|
||||
}
|
||||
}
|
||||
|
||||
@ -249,8 +249,13 @@ func (u *User) MaxCreationLimit() int {
|
||||
}
|
||||
|
||||
// CanCreateRepoIn checks whether the doer(u) can create a repository in the owner
|
||||
// NOTE: functions calling this assume a failure due to repository count limit; it ONLY checks the repo number LIMIT, if new checks are added, those functions should be revised
|
||||
// NOTE: functions calling this assume a failure due to repository count limit, or the owner is not a real user.
|
||||
// It ONLY checks the repo number LIMIT or whether owner user is real. If new checks are added, those functions should be revised.
|
||||
// TODO: the callers can only return ErrReachLimitOfRepo, need to fine tune to support other error types in the future.
|
||||
func (u *User) CanCreateRepoIn(owner *User) bool {
|
||||
if u.ID <= 0 || owner.ID <= 0 {
|
||||
return false // fake user like Ghost or Actions user
|
||||
}
|
||||
if u.IsAdmin {
|
||||
return true
|
||||
}
|
||||
@ -1444,3 +1449,15 @@ func DisabledFeaturesWithLoginType(user *User) *container.Set[string] {
|
||||
}
|
||||
return &setting.Admin.UserDisabledFeatures
|
||||
}
|
||||
|
||||
// GetUserOrOrgIDByName returns the id for a user or an org by name
|
||||
func GetUserOrOrgIDByName(ctx context.Context, name string) (int64, error) {
|
||||
var id int64
|
||||
has, err := db.GetEngine(ctx).Table("user").Where("name = ?", name).Cols("id").Get(&id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
} else if !has {
|
||||
return 0, fmt.Errorf("user or org with name %s: %w", name, util.ErrNotExist)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
@ -48,17 +48,16 @@ func IsGiteaActionsUserName(name string) bool {
|
||||
// NewActionsUser creates and returns a fake user for running the actions.
|
||||
func NewActionsUser() *User {
|
||||
return &User{
|
||||
ID: ActionsUserID,
|
||||
Name: ActionsUserName,
|
||||
LowerName: ActionsUserName,
|
||||
IsActive: true,
|
||||
FullName: "Gitea Actions",
|
||||
Email: ActionsUserEmail,
|
||||
KeepEmailPrivate: true,
|
||||
LoginName: ActionsUserName,
|
||||
Type: UserTypeBot,
|
||||
AllowCreateOrganization: true,
|
||||
Visibility: structs.VisibleTypePublic,
|
||||
ID: ActionsUserID,
|
||||
Name: ActionsUserName,
|
||||
LowerName: ActionsUserName,
|
||||
IsActive: true,
|
||||
FullName: "Gitea Actions",
|
||||
Email: ActionsUserEmail,
|
||||
KeepEmailPrivate: true,
|
||||
LoginName: ActionsUserName,
|
||||
Type: UserTypeBot,
|
||||
Visibility: structs.VisibleTypePublic,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -126,7 +126,7 @@ func TestSearchUsers(t *testing.T) {
|
||||
|
||||
// test orgs
|
||||
testOrgSuccess := func(opts user_model.SearchUserOptions, expectedOrgIDs []int64) {
|
||||
opts.Type = user_model.UserTypeOrganization
|
||||
opts.Types = []user_model.UserType{user_model.UserTypeOrganization}
|
||||
testSuccess(opts, expectedOrgIDs)
|
||||
}
|
||||
|
||||
@ -150,7 +150,7 @@ func TestSearchUsers(t *testing.T) {
|
||||
|
||||
// test users
|
||||
testUserSuccess := func(opts user_model.SearchUserOptions, expectedUserIDs []int64) {
|
||||
opts.Type = user_model.UserTypeIndividual
|
||||
opts.Types = []user_model.UserType{user_model.UserTypeIndividual}
|
||||
testSuccess(opts, expectedUserIDs)
|
||||
}
|
||||
|
||||
@ -648,33 +648,36 @@ func TestGetInactiveUsers(t *testing.T) {
|
||||
func TestCanCreateRepo(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Repository.MaxCreationLimit)()
|
||||
const noLimit = -1
|
||||
doerNormal := &user_model.User{}
|
||||
doerAdmin := &user_model.User{IsAdmin: true}
|
||||
doerActions := user_model.NewActionsUser()
|
||||
doerNormal := &user_model.User{ID: 2}
|
||||
doerAdmin := &user_model.User{ID: 1, IsAdmin: true}
|
||||
t.Run("NoGlobalLimit", func(t *testing.T) {
|
||||
setting.Repository.MaxCreationLimit = noLimit
|
||||
|
||||
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit}))
|
||||
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0}))
|
||||
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100}))
|
||||
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0}))
|
||||
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100}))
|
||||
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit}))
|
||||
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit}))
|
||||
assert.False(t, doerActions.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit}))
|
||||
assert.False(t, doerAdmin.CanCreateRepoIn(doerActions))
|
||||
})
|
||||
|
||||
t.Run("GlobalLimit50", func(t *testing.T) {
|
||||
setting.Repository.MaxCreationLimit = 50
|
||||
|
||||
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit}))
|
||||
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: noLimit})) // limited by global limit
|
||||
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0}))
|
||||
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100}))
|
||||
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: 100}))
|
||||
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit}))
|
||||
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: noLimit})) // limited by global limit
|
||||
assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0}))
|
||||
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100}))
|
||||
assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: 100}))
|
||||
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: noLimit}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: 100}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: noLimit}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100}))
|
||||
assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: 100}))
|
||||
})
|
||||
}
|
||||
|
||||
@ -47,5 +47,5 @@ func TestGetNonExistentNotes(t *testing.T) {
|
||||
note := Note{}
|
||||
err = GetNote(t.Context(), bareRepo1, "non_existent_sha", ¬e)
|
||||
assert.Error(t, err)
|
||||
assert.IsType(t, ErrNotExist{}, err)
|
||||
assert.ErrorAs(t, err, &ErrNotExist{})
|
||||
}
|
||||
|
||||
@ -11,7 +11,6 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@ -30,12 +29,15 @@ type ServeFunction = func(net.Listener) error
|
||||
|
||||
// Server represents our graceful server
|
||||
type Server struct {
|
||||
network string
|
||||
address string
|
||||
listener net.Listener
|
||||
wg sync.WaitGroup
|
||||
state state
|
||||
lock *sync.RWMutex
|
||||
network string
|
||||
address string
|
||||
listener net.Listener
|
||||
|
||||
lock sync.RWMutex
|
||||
state state
|
||||
connCounter int64
|
||||
connEmptyCond *sync.Cond
|
||||
|
||||
BeforeBegin func(network, address string)
|
||||
OnShutdown func()
|
||||
PerWriteTimeout time.Duration
|
||||
@ -50,14 +52,13 @@ func NewServer(network, address, name string) *Server {
|
||||
log.Info("Starting new %s server: %s:%s on PID: %d", name, network, address, os.Getpid())
|
||||
}
|
||||
srv := &Server{
|
||||
wg: sync.WaitGroup{},
|
||||
state: stateInit,
|
||||
lock: &sync.RWMutex{},
|
||||
network: network,
|
||||
address: address,
|
||||
PerWriteTimeout: setting.PerWriteTimeout,
|
||||
PerWritePerKbTimeout: setting.PerWritePerKbTimeout,
|
||||
}
|
||||
srv.connEmptyCond = sync.NewCond(&srv.lock)
|
||||
|
||||
srv.BeforeBegin = func(network, addr string) {
|
||||
log.Debug("Starting server on %s:%s (PID: %d)", network, addr, syscall.Getpid())
|
||||
@ -154,7 +155,7 @@ func (srv *Server) Serve(serve ServeFunction) error {
|
||||
GetManager().RegisterServer()
|
||||
err := serve(srv.listener)
|
||||
log.Debug("Waiting for connections to finish... (PID: %d)", syscall.Getpid())
|
||||
srv.wg.Wait()
|
||||
srv.waitForActiveConnections()
|
||||
srv.setState(stateTerminate)
|
||||
GetManager().ServerDone()
|
||||
// use of closed means that the listeners are closed - i.e. we should be shutting down - return nil
|
||||
@ -178,16 +179,62 @@ func (srv *Server) setState(st state) {
|
||||
srv.state = st
|
||||
}
|
||||
|
||||
func (srv *Server) waitForActiveConnections() {
|
||||
srv.lock.Lock()
|
||||
for srv.connCounter > 0 {
|
||||
srv.connEmptyCond.Wait()
|
||||
}
|
||||
srv.lock.Unlock()
|
||||
}
|
||||
|
||||
func (srv *Server) wrapConnection(c net.Conn) (net.Conn, error) {
|
||||
srv.lock.Lock()
|
||||
defer srv.lock.Unlock()
|
||||
|
||||
if srv.state != stateRunning {
|
||||
_ = c.Close()
|
||||
return nil, syscall.EINVAL // same as AcceptTCP
|
||||
}
|
||||
|
||||
srv.connCounter++
|
||||
return &wrappedConn{Conn: c, server: srv}, nil
|
||||
}
|
||||
|
||||
func (srv *Server) removeConnection(_ *wrappedConn) {
|
||||
srv.lock.Lock()
|
||||
defer srv.lock.Unlock()
|
||||
|
||||
srv.connCounter--
|
||||
if srv.connCounter <= 0 {
|
||||
srv.connEmptyCond.Broadcast()
|
||||
}
|
||||
}
|
||||
|
||||
// closeAllConnections forcefully closes all active connections
|
||||
func (srv *Server) closeAllConnections() {
|
||||
srv.lock.Lock()
|
||||
if srv.connCounter > 0 {
|
||||
log.Warn("After graceful shutdown period, %d connections are still active. Forcefully close.", srv.connCounter)
|
||||
srv.connCounter = 0 // OS will close all the connections after the process exits, so we just assume there is no active connection now
|
||||
}
|
||||
srv.lock.Unlock()
|
||||
srv.connEmptyCond.Broadcast()
|
||||
}
|
||||
|
||||
type filer interface {
|
||||
File() (*os.File, error)
|
||||
}
|
||||
|
||||
type wrappedListener struct {
|
||||
net.Listener
|
||||
stopped bool
|
||||
server *Server
|
||||
server *Server
|
||||
}
|
||||
|
||||
var (
|
||||
_ net.Listener = (*wrappedListener)(nil)
|
||||
_ filer = (*wrappedListener)(nil)
|
||||
)
|
||||
|
||||
func newWrappedListener(l net.Listener, srv *Server) *wrappedListener {
|
||||
return &wrappedListener{
|
||||
Listener: l,
|
||||
@ -195,46 +242,24 @@ func newWrappedListener(l net.Listener, srv *Server) *wrappedListener {
|
||||
}
|
||||
}
|
||||
|
||||
func (wl *wrappedListener) Accept() (net.Conn, error) {
|
||||
var c net.Conn
|
||||
// Set keepalive on TCPListeners connections.
|
||||
func (wl *wrappedListener) Accept() (c net.Conn, err error) {
|
||||
if tcl, ok := wl.Listener.(*net.TCPListener); ok {
|
||||
// Set keepalive on TCPListeners connections if possible, see http.tcpKeepAliveListener
|
||||
tc, err := tcl.AcceptTCP()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = tc.SetKeepAlive(true) // see http.tcpKeepAliveListener
|
||||
_ = tc.SetKeepAlivePeriod(3 * time.Minute) // see http.tcpKeepAliveListener
|
||||
_ = tc.SetKeepAlive(true)
|
||||
_ = tc.SetKeepAlivePeriod(3 * time.Minute)
|
||||
c = tc
|
||||
} else {
|
||||
var err error
|
||||
c, err = wl.Listener.Accept()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
closed := int32(0)
|
||||
|
||||
c = &wrappedConn{
|
||||
Conn: c,
|
||||
server: wl.server,
|
||||
closed: &closed,
|
||||
perWriteTimeout: wl.server.PerWriteTimeout,
|
||||
perWritePerKbTimeout: wl.server.PerWritePerKbTimeout,
|
||||
}
|
||||
|
||||
wl.server.wg.Add(1)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (wl *wrappedListener) Close() error {
|
||||
if wl.stopped {
|
||||
return syscall.EINVAL
|
||||
}
|
||||
|
||||
wl.stopped = true
|
||||
return wl.Listener.Close()
|
||||
return wl.server.wrapConnection(c)
|
||||
}
|
||||
|
||||
func (wl *wrappedListener) File() (*os.File, error) {
|
||||
@ -244,17 +269,14 @@ func (wl *wrappedListener) File() (*os.File, error) {
|
||||
|
||||
type wrappedConn struct {
|
||||
net.Conn
|
||||
server *Server
|
||||
closed *int32
|
||||
deadline time.Time
|
||||
perWriteTimeout time.Duration
|
||||
perWritePerKbTimeout time.Duration
|
||||
server *Server
|
||||
deadline time.Time
|
||||
}
|
||||
|
||||
func (w *wrappedConn) Write(p []byte) (n int, err error) {
|
||||
if w.perWriteTimeout > 0 {
|
||||
minTimeout := time.Duration(len(p)/1024) * w.perWritePerKbTimeout
|
||||
minDeadline := time.Now().Add(minTimeout).Add(w.perWriteTimeout)
|
||||
if w.server.PerWriteTimeout > 0 {
|
||||
minTimeout := time.Duration(len(p)/1024) * w.server.PerWritePerKbTimeout
|
||||
minDeadline := time.Now().Add(minTimeout).Add(w.server.PerWriteTimeout)
|
||||
|
||||
w.deadline = w.deadline.Add(minTimeout)
|
||||
if minDeadline.After(w.deadline) {
|
||||
@ -266,19 +288,6 @@ func (w *wrappedConn) Write(p []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
func (w *wrappedConn) Close() error {
|
||||
if atomic.CompareAndSwapInt32(w.closed, 0, 1) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
select {
|
||||
case <-GetManager().IsHammer():
|
||||
// Likely deadlocked request released at hammertime
|
||||
log.Warn("Panic during connection close! %v. Likely there has been a deadlocked request which has been released by forced shutdown.", err)
|
||||
default:
|
||||
log.Error("Panic during connection close! %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
w.server.wg.Done()
|
||||
}
|
||||
w.server.removeConnection(w)
|
||||
return w.Conn.Close()
|
||||
}
|
||||
|
||||
@ -5,7 +5,6 @@ package graceful
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
@ -48,26 +47,8 @@ func (srv *Server) doShutdown() {
|
||||
}
|
||||
|
||||
func (srv *Server) doHammer() {
|
||||
defer func() {
|
||||
// We call srv.wg.Done() until it panics.
|
||||
// This happens if we call Done() when the WaitGroup counter is already at 0
|
||||
// So if it panics -> we're done, Serve() will return and the
|
||||
// parent will goroutine will exit.
|
||||
if r := recover(); r != nil {
|
||||
log.Error("WaitGroup at 0: Error: %v", r)
|
||||
}
|
||||
}()
|
||||
if srv.getState() != stateShuttingDown {
|
||||
return
|
||||
}
|
||||
log.Warn("Forcefully shutting down parent")
|
||||
for {
|
||||
if srv.getState() == stateTerminate {
|
||||
break
|
||||
}
|
||||
srv.wg.Done()
|
||||
|
||||
// Give other goroutines a chance to finish before we forcibly stop them.
|
||||
runtime.Gosched()
|
||||
}
|
||||
srv.closeAllConnections()
|
||||
}
|
||||
|
||||
@ -126,6 +126,7 @@ func setServeHeadersByFile(r *http.Request, w http.ResponseWriter, mineBuf []byt
|
||||
// no sandbox attribute for pdf as it breaks rendering in at least safari. this
|
||||
// should generally be safe as scripts inside PDF can not escape the PDF document
|
||||
// see https://bugs.chromium.org/p/chromium/issues/detail?id=413851 for more discussion
|
||||
// HINT: PDF-RENDER-SANDBOX: PDF won't render in sandboxed context
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'")
|
||||
}
|
||||
|
||||
|
||||
20
modules/markup/external/external.go
vendored
20
modules/markup/external/external.go
vendored
@ -15,6 +15,8 @@ import (
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/kballard/go-shellquote"
|
||||
)
|
||||
|
||||
// RegisterRenderers registers all supported third part renderers according settings
|
||||
@ -56,14 +58,11 @@ func (p *Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
|
||||
return p.MarkupSanitizerRules
|
||||
}
|
||||
|
||||
// SanitizerDisabled disabled sanitize if return true
|
||||
func (p *Renderer) SanitizerDisabled() bool {
|
||||
return p.RenderContentMode == setting.RenderContentModeNoSanitizer || p.RenderContentMode == setting.RenderContentModeIframe
|
||||
}
|
||||
|
||||
// DisplayInIFrame represents whether render the content with an iframe
|
||||
func (p *Renderer) DisplayInIFrame() bool {
|
||||
return p.RenderContentMode == setting.RenderContentModeIframe
|
||||
func (p *Renderer) GetExternalRendererOptions() (ret markup.ExternalRendererOptions) {
|
||||
ret.SanitizerDisabled = p.RenderContentMode == setting.RenderContentModeNoSanitizer || p.RenderContentMode == setting.RenderContentModeIframe
|
||||
ret.DisplayInIframe = p.RenderContentMode == setting.RenderContentModeIframe
|
||||
ret.ContentSandbox = p.RenderContentSandbox
|
||||
return ret
|
||||
}
|
||||
|
||||
func envMark(envName string) string {
|
||||
@ -81,7 +80,10 @@ func (p *Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.
|
||||
envMark("GITEA_PREFIX_SRC"), baseLinkSrc,
|
||||
envMark("GITEA_PREFIX_RAW"), baseLinkRaw,
|
||||
).Replace(p.Command)
|
||||
commands := strings.Fields(command)
|
||||
commands, err := shellquote.Split(command)
|
||||
if err != nil || len(commands) == 0 {
|
||||
return fmt.Errorf("%s invalid command %q: %w", p.Name(), p.Command, err)
|
||||
}
|
||||
args := commands[1:]
|
||||
|
||||
if p.IsInputFile {
|
||||
|
||||
@ -5,11 +5,13 @@ package internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
"io"
|
||||
)
|
||||
|
||||
type finalProcessor struct {
|
||||
renderInternal *RenderInternal
|
||||
extraHeadHTML template.HTML
|
||||
|
||||
output io.Writer
|
||||
buf bytes.Buffer
|
||||
@ -25,6 +27,32 @@ func (p *finalProcessor) Close() error {
|
||||
// because "postProcess" already does so. In the future we could optimize the code to process data on the fly.
|
||||
buf := p.buf.Bytes()
|
||||
buf = bytes.ReplaceAll(buf, []byte(` data-attr-class="`+p.renderInternal.secureIDPrefix), []byte(` class="`))
|
||||
_, err := p.output.Write(buf)
|
||||
|
||||
tmp := bytes.TrimSpace(buf)
|
||||
isLikelyHTML := len(tmp) != 0 && tmp[0] == '<' && tmp[len(tmp)-1] == '>' && bytes.Index(tmp, []byte(`</`)) > 0
|
||||
if !isLikelyHTML {
|
||||
// not HTML, write back directly
|
||||
_, err := p.output.Write(buf)
|
||||
return err
|
||||
}
|
||||
|
||||
// add our extra head HTML into output
|
||||
headBytes := []byte("<head>")
|
||||
posHead := bytes.Index(buf, headBytes)
|
||||
var part1, part2 []byte
|
||||
if posHead >= 0 {
|
||||
part1, part2 = buf[:posHead+len(headBytes)], buf[posHead+len(headBytes):]
|
||||
} else {
|
||||
part1, part2 = nil, buf
|
||||
}
|
||||
if len(part1) > 0 {
|
||||
if _, err := p.output.Write(part1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := io.WriteString(p.output, string(p.extraHeadHTML)); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := p.output.Write(part2)
|
||||
return err
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRenderInternal(t *testing.T) {
|
||||
func TestRenderInternalAttrs(t *testing.T) {
|
||||
cases := []struct {
|
||||
input, protected, recovered string
|
||||
}{
|
||||
@ -30,7 +30,7 @@ func TestRenderInternal(t *testing.T) {
|
||||
for _, c := range cases {
|
||||
var r RenderInternal
|
||||
out := &bytes.Buffer{}
|
||||
in := r.init("sec", out)
|
||||
in := r.init("sec", out, "")
|
||||
protected := r.ProtectSafeAttrs(template.HTML(c.input))
|
||||
assert.EqualValues(t, c.protected, protected)
|
||||
_, _ = io.WriteString(in, string(protected))
|
||||
@ -41,7 +41,7 @@ func TestRenderInternal(t *testing.T) {
|
||||
var r1, r2 RenderInternal
|
||||
protected := r1.ProtectSafeAttrs(`<div class="test"></div>`)
|
||||
assert.EqualValues(t, `<div class="test"></div>`, protected, "non-initialized RenderInternal should not protect any attributes")
|
||||
_ = r1.init("sec", nil)
|
||||
_ = r1.init("sec", nil, "")
|
||||
protected = r1.ProtectSafeAttrs(`<div class="test"></div>`)
|
||||
assert.EqualValues(t, `<div data-attr-class="sec:test"></div>`, protected)
|
||||
assert.Equal(t, "data-attr-class", r1.SafeAttr("class"))
|
||||
@ -54,8 +54,37 @@ func TestRenderInternal(t *testing.T) {
|
||||
assert.Empty(t, recovered)
|
||||
|
||||
out2 := &bytes.Buffer{}
|
||||
in2 := r2.init("sec-other", out2)
|
||||
in2 := r2.init("sec-other", out2, "")
|
||||
_, _ = io.WriteString(in2, string(protected))
|
||||
_ = in2.Close()
|
||||
assert.Equal(t, `<div data-attr-class="sec:test"></div>`, out2.String(), "different secureID should not recover the value")
|
||||
}
|
||||
|
||||
func TestRenderInternalExtraHead(t *testing.T) {
|
||||
t.Run("HeadExists", func(t *testing.T) {
|
||||
out := &bytes.Buffer{}
|
||||
var r RenderInternal
|
||||
in := r.init("sec", out, `<MY-TAG>`)
|
||||
_, _ = io.WriteString(in, `<head>any</head>`)
|
||||
_ = in.Close()
|
||||
assert.Equal(t, `<head><MY-TAG>any</head>`, out.String())
|
||||
})
|
||||
|
||||
t.Run("HeadNotExists", func(t *testing.T) {
|
||||
out := &bytes.Buffer{}
|
||||
var r RenderInternal
|
||||
in := r.init("sec", out, `<MY-TAG>`)
|
||||
_, _ = io.WriteString(in, `<div></div>`)
|
||||
_ = in.Close()
|
||||
assert.Equal(t, `<MY-TAG><div></div>`, out.String())
|
||||
})
|
||||
|
||||
t.Run("NotHTML", func(t *testing.T) {
|
||||
out := &bytes.Buffer{}
|
||||
var r RenderInternal
|
||||
in := r.init("sec", out, `<MY-TAG>`)
|
||||
_, _ = io.WriteString(in, `<any>`)
|
||||
_ = in.Close()
|
||||
assert.Equal(t, `<any>`, out.String())
|
||||
})
|
||||
}
|
||||
|
||||
@ -29,19 +29,19 @@ type RenderInternal struct {
|
||||
secureIDPrefix string
|
||||
}
|
||||
|
||||
func (r *RenderInternal) Init(output io.Writer) io.WriteCloser {
|
||||
func (r *RenderInternal) Init(output io.Writer, extraHeadHTML template.HTML) io.WriteCloser {
|
||||
buf := make([]byte, 12)
|
||||
_, err := rand.Read(buf)
|
||||
if err != nil {
|
||||
panic("unable to generate secure id")
|
||||
}
|
||||
return r.init(base64.URLEncoding.EncodeToString(buf), output)
|
||||
return r.init(base64.URLEncoding.EncodeToString(buf), output, extraHeadHTML)
|
||||
}
|
||||
|
||||
func (r *RenderInternal) init(secID string, output io.Writer) io.WriteCloser {
|
||||
func (r *RenderInternal) init(secID string, output io.Writer, extraHeadHTML template.HTML) io.WriteCloser {
|
||||
r.secureID = secID
|
||||
r.secureIDPrefix = r.secureID + ":"
|
||||
return &finalProcessor{renderInternal: r, output: output}
|
||||
return &finalProcessor{renderInternal: r, output: output, extraHeadHTML: extraHeadHTML}
|
||||
}
|
||||
|
||||
func (r *RenderInternal) RecoverProtectedValue(v string) (string, bool) {
|
||||
|
||||
@ -6,12 +6,14 @@ package markup
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/htmlutil"
|
||||
"code.gitea.io/gitea/modules/markup/internal"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
@ -120,31 +122,38 @@ func (ctx *RenderContext) WithHelper(helper RenderHelper) *RenderContext {
|
||||
return ctx
|
||||
}
|
||||
|
||||
// Render renders markup file to HTML with all specific handling stuff.
|
||||
func Render(ctx *RenderContext, input io.Reader, output io.Writer) error {
|
||||
// FindRendererByContext finds renderer by RenderContext
|
||||
// TODO: it should be merged with other similar functions like GetRendererByFileName, DetectMarkupTypeByFileName, etc
|
||||
func FindRendererByContext(ctx *RenderContext) (Renderer, error) {
|
||||
if ctx.RenderOptions.MarkupType == "" && ctx.RenderOptions.RelativePath != "" {
|
||||
ctx.RenderOptions.MarkupType = DetectMarkupTypeByFileName(ctx.RenderOptions.RelativePath)
|
||||
if ctx.RenderOptions.MarkupType == "" {
|
||||
return util.NewInvalidArgumentErrorf("unsupported file to render: %q", ctx.RenderOptions.RelativePath)
|
||||
return nil, util.NewInvalidArgumentErrorf("unsupported file to render: %q", ctx.RenderOptions.RelativePath)
|
||||
}
|
||||
}
|
||||
|
||||
renderer := renderers[ctx.RenderOptions.MarkupType]
|
||||
if renderer == nil {
|
||||
return util.NewInvalidArgumentErrorf("unsupported markup type: %q", ctx.RenderOptions.MarkupType)
|
||||
return nil, util.NewNotExistErrorf("unsupported markup type: %q", ctx.RenderOptions.MarkupType)
|
||||
}
|
||||
|
||||
if ctx.RenderOptions.RelativePath != "" {
|
||||
if externalRender, ok := renderer.(ExternalRenderer); ok && externalRender.DisplayInIFrame() {
|
||||
if !ctx.RenderOptions.InStandalonePage {
|
||||
// for an external "DisplayInIFrame" render, it could only output its content in a standalone page
|
||||
// otherwise, a <iframe> should be outputted to embed the external rendered page
|
||||
return renderIFrame(ctx, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
return renderer, nil
|
||||
}
|
||||
|
||||
return render(ctx, renderer, input, output)
|
||||
func RendererNeedPostProcess(renderer Renderer) bool {
|
||||
if r, ok := renderer.(PostProcessRenderer); ok && r.NeedPostProcess() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Render renders markup file to HTML with all specific handling stuff.
|
||||
func Render(ctx *RenderContext, input io.Reader, output io.Writer) error {
|
||||
renderer, err := FindRendererByContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return RenderWithRenderer(ctx, renderer, input, output)
|
||||
}
|
||||
|
||||
// RenderString renders Markup string to HTML with all specific handling stuff and return string
|
||||
@ -156,24 +165,20 @@ func RenderString(ctx *RenderContext, content string) (string, error) {
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func renderIFrame(ctx *RenderContext, output io.Writer) error {
|
||||
// set height="0" ahead, otherwise the scrollHeight would be max(150, realHeight)
|
||||
// at the moment, only "allow-scripts" is allowed for sandbox mode.
|
||||
// "allow-same-origin" should never be used, it leads to XSS attack, and it makes the JS in iframe can access parent window's config and CSRF token
|
||||
// TODO: when using dark theme, if the rendered content doesn't have proper style, the default text color is black, which is not easy to read
|
||||
_, err := io.WriteString(output, fmt.Sprintf(`
|
||||
<iframe src="%s/%s/%s/render/%s/%s"
|
||||
name="giteaExternalRender"
|
||||
onload="this.height=giteaExternalRender.document.documentElement.scrollHeight"
|
||||
width="100%%" height="0" scrolling="no" frameborder="0" style="overflow: hidden"
|
||||
sandbox="allow-scripts"
|
||||
></iframe>`,
|
||||
setting.AppSubURL,
|
||||
func renderIFrame(ctx *RenderContext, sandbox string, output io.Writer) error {
|
||||
src := fmt.Sprintf("%s/%s/%s/render/%s/%s", setting.AppSubURL,
|
||||
url.PathEscape(ctx.RenderOptions.Metas["user"]),
|
||||
url.PathEscape(ctx.RenderOptions.Metas["repo"]),
|
||||
ctx.RenderOptions.Metas["RefTypeNameSubURL"],
|
||||
url.PathEscape(ctx.RenderOptions.RelativePath),
|
||||
))
|
||||
util.PathEscapeSegments(ctx.RenderOptions.Metas["RefTypeNameSubURL"]),
|
||||
util.PathEscapeSegments(ctx.RenderOptions.RelativePath),
|
||||
)
|
||||
|
||||
var sandboxAttrValue template.HTML
|
||||
if sandbox != "" {
|
||||
sandboxAttrValue = htmlutil.HTMLFormat(`sandbox="%s"`, sandbox)
|
||||
}
|
||||
iframe := htmlutil.HTMLFormat(`<iframe data-src="%s" class="external-render-iframe" %s></iframe>`, src, sandboxAttrValue)
|
||||
_, err := io.WriteString(output, string(iframe))
|
||||
return err
|
||||
}
|
||||
|
||||
@ -185,13 +190,34 @@ func pipes() (io.ReadCloser, io.WriteCloser, func()) {
|
||||
}
|
||||
}
|
||||
|
||||
func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error {
|
||||
func getExternalRendererOptions(renderer Renderer) (ret ExternalRendererOptions, _ bool) {
|
||||
if externalRender, ok := renderer.(ExternalRenderer); ok {
|
||||
return externalRender.GetExternalRendererOptions(), true
|
||||
}
|
||||
return ret, false
|
||||
}
|
||||
|
||||
func RenderWithRenderer(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error {
|
||||
var extraHeadHTML template.HTML
|
||||
if extOpts, ok := getExternalRendererOptions(renderer); ok && extOpts.DisplayInIframe {
|
||||
if !ctx.RenderOptions.InStandalonePage {
|
||||
// for an external "DisplayInIFrame" render, it could only output its content in a standalone page
|
||||
// otherwise, a <iframe> should be outputted to embed the external rendered page
|
||||
return renderIFrame(ctx, extOpts.ContentSandbox, output)
|
||||
}
|
||||
// else: this is a standalone page, fallthrough to the real rendering, and add extra JS/CSS
|
||||
extraStyleHref := setting.AppSubURL + "/assets/css/external-render-iframe.css"
|
||||
extraScriptSrc := setting.AppSubURL + "/assets/js/external-render-iframe.js"
|
||||
// "<script>" must go before "<link>", to make Golang's http.DetectContentType() can still recognize the content as "text/html"
|
||||
extraHeadHTML = htmlutil.HTMLFormat(`<script src="%s"></script><link rel="stylesheet" href="%s">`, extraScriptSrc, extraStyleHref)
|
||||
}
|
||||
|
||||
ctx.usedByRender = true
|
||||
if ctx.RenderHelper != nil {
|
||||
defer ctx.RenderHelper.CleanUp()
|
||||
}
|
||||
|
||||
finalProcessor := ctx.RenderInternal.Init(output)
|
||||
finalProcessor := ctx.RenderInternal.Init(output, extraHeadHTML)
|
||||
defer finalProcessor.Close()
|
||||
|
||||
// input -> (pw1=pr1) -> renderer -> (pw2=pr2) -> SanitizeReader -> finalProcessor -> output
|
||||
@ -202,7 +228,7 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr
|
||||
eg, _ := errgroup.WithContext(ctx)
|
||||
var pw2 io.WriteCloser = util.NopCloser{Writer: finalProcessor}
|
||||
|
||||
if r, ok := renderer.(ExternalRenderer); !ok || !r.SanitizerDisabled() {
|
||||
if r, ok := renderer.(ExternalRenderer); !ok || !r.GetExternalRendererOptions().SanitizerDisabled {
|
||||
var pr2 io.ReadCloser
|
||||
var close2 func()
|
||||
pr2, pw2, close2 = pipes()
|
||||
@ -214,7 +240,7 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr
|
||||
}
|
||||
|
||||
eg.Go(func() (err error) {
|
||||
if r, ok := renderer.(PostProcessRenderer); ok && r.NeedPostProcess() {
|
||||
if RendererNeedPostProcess(renderer) {
|
||||
err = PostProcessDefault(ctx, pr1, pw2)
|
||||
} else {
|
||||
_, err = io.Copy(pw2, pr1)
|
||||
|
||||
@ -25,13 +25,15 @@ type PostProcessRenderer interface {
|
||||
NeedPostProcess() bool
|
||||
}
|
||||
|
||||
type ExternalRendererOptions struct {
|
||||
SanitizerDisabled bool
|
||||
DisplayInIframe bool
|
||||
ContentSandbox string
|
||||
}
|
||||
|
||||
// ExternalRenderer defines an interface for external renderers
|
||||
type ExternalRenderer interface {
|
||||
// SanitizerDisabled disabled sanitize if return true
|
||||
SanitizerDisabled() bool
|
||||
|
||||
// DisplayInIFrame represents whether render the content with an iframe
|
||||
DisplayInIFrame() bool
|
||||
GetExternalRendererOptions() ExternalRendererOptions
|
||||
}
|
||||
|
||||
// RendererContentDetector detects if the content can be rendered
|
||||
|
||||
@ -41,6 +41,7 @@ type ConfigSection interface {
|
||||
HasKey(key string) bool
|
||||
NewKey(name, value string) (ConfigKey, error)
|
||||
Key(key string) ConfigKey
|
||||
DeleteKey(key string)
|
||||
Keys() []ConfigKey
|
||||
ChildSections() []ConfigSection
|
||||
}
|
||||
@ -51,6 +52,7 @@ type ConfigProvider interface {
|
||||
Sections() []ConfigSection
|
||||
NewSection(name string) (ConfigSection, error)
|
||||
GetSection(name string) (ConfigSection, error)
|
||||
DeleteSection(name string)
|
||||
Save() error
|
||||
SaveTo(filename string) error
|
||||
|
||||
@ -168,6 +170,10 @@ func (s *iniConfigSection) Keys() (keys []ConfigKey) {
|
||||
return keys
|
||||
}
|
||||
|
||||
func (s *iniConfigSection) DeleteKey(key string) {
|
||||
s.sec.DeleteKey(key)
|
||||
}
|
||||
|
||||
func (s *iniConfigSection) ChildSections() (sections []ConfigSection) {
|
||||
for _, s := range s.sec.ChildSections() {
|
||||
sections = append(sections, &iniConfigSection{s})
|
||||
@ -249,6 +255,10 @@ func (p *iniConfigProvider) GetSection(name string) (ConfigSection, error) {
|
||||
return &iniConfigSection{sec: sec}, nil
|
||||
}
|
||||
|
||||
func (p *iniConfigProvider) DeleteSection(name string) {
|
||||
p.ini.DeleteSection(name)
|
||||
}
|
||||
|
||||
var errDisableSaving = errors.New("this config can't be saved, developers should prepare a new config to save")
|
||||
|
||||
// Save saves the content into file
|
||||
|
||||
@ -65,7 +65,7 @@ func checkGlobMatch(t *testing.T, globstr string, list []indexerMatchList) {
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
assert.Equal(t, m.position, -1, "Test string `%s` doesn't match `%s` anywhere; expected @%d", m.value, globstr, m.position)
|
||||
assert.Equal(t, -1, m.position, "Test string `%s` doesn't match `%s` anywhere; expected @%d", m.value, globstr, m.position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,6 +63,7 @@ type MarkupRenderer struct {
|
||||
NeedPostProcess bool
|
||||
MarkupSanitizerRules []MarkupSanitizerRule
|
||||
RenderContentMode string
|
||||
RenderContentSandbox string
|
||||
}
|
||||
|
||||
// MarkupSanitizerRule defines the policy for whitelisting attributes on
|
||||
@ -253,13 +254,24 @@ func newMarkupRenderer(name string, sec ConfigSection) {
|
||||
renderContentMode = RenderContentModeSanitized
|
||||
}
|
||||
|
||||
// ATTENTION! at the moment, only a safe set like "allow-scripts" are allowed for sandbox mode.
|
||||
// "allow-same-origin" should never be used, it leads to XSS attack, and it makes the JS in iframe can access parent window's config and CSRF token
|
||||
renderContentSandbox := sec.Key("RENDER_CONTENT_SANDBOX").MustString("allow-scripts allow-popups")
|
||||
if renderContentSandbox == "disabled" {
|
||||
renderContentSandbox = ""
|
||||
}
|
||||
|
||||
ExternalMarkupRenderers = append(ExternalMarkupRenderers, &MarkupRenderer{
|
||||
Enabled: sec.Key("ENABLED").MustBool(false),
|
||||
MarkupName: name,
|
||||
FileExtensions: exts,
|
||||
Command: command,
|
||||
IsInputFile: sec.Key("IS_INPUT_FILE").MustBool(false),
|
||||
NeedPostProcess: sec.Key("NEED_POSTPROCESS").MustBool(true),
|
||||
RenderContentMode: renderContentMode,
|
||||
Enabled: sec.Key("ENABLED").MustBool(false),
|
||||
MarkupName: name,
|
||||
FileExtensions: exts,
|
||||
Command: command,
|
||||
IsInputFile: sec.Key("IS_INPUT_FILE").MustBool(false),
|
||||
|
||||
RenderContentMode: renderContentMode,
|
||||
RenderContentSandbox: renderContentSandbox,
|
||||
|
||||
// if no sanitizer is needed, no post process is needed
|
||||
NeedPostProcess: sec.Key("NEED_POST_PROCESS").MustBool(renderContentMode == RenderContentModeSanitized),
|
||||
})
|
||||
}
|
||||
|
||||
@ -24,13 +24,6 @@ type FileOptions struct {
|
||||
Signoff bool `json:"signoff"`
|
||||
}
|
||||
|
||||
type FileOptionsWithSHA struct {
|
||||
FileOptions
|
||||
// the blob ID (SHA) for the file that already exists, it is required for changing existing files
|
||||
// required: true
|
||||
SHA string `json:"sha" binding:"Required"`
|
||||
}
|
||||
|
||||
func (f *FileOptions) GetFileOptions() *FileOptions {
|
||||
return f
|
||||
}
|
||||
@ -41,7 +34,7 @@ type FileOptionsInterface interface {
|
||||
|
||||
var _ FileOptionsInterface = (*FileOptions)(nil)
|
||||
|
||||
// CreateFileOptions options for creating files
|
||||
// CreateFileOptions options for creating a file
|
||||
// Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)
|
||||
type CreateFileOptions struct {
|
||||
FileOptions
|
||||
@ -50,16 +43,21 @@ type CreateFileOptions struct {
|
||||
ContentBase64 string `json:"content"`
|
||||
}
|
||||
|
||||
// DeleteFileOptions options for deleting files (used for other File structs below)
|
||||
// DeleteFileOptions options for deleting a file
|
||||
// Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)
|
||||
type DeleteFileOptions struct {
|
||||
FileOptionsWithSHA
|
||||
FileOptions
|
||||
// the blob ID (SHA) for the file to delete
|
||||
// required: true
|
||||
SHA string `json:"sha" binding:"Required"`
|
||||
}
|
||||
|
||||
// UpdateFileOptions options for updating files
|
||||
// UpdateFileOptions options for updating or creating a file
|
||||
// Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)
|
||||
type UpdateFileOptions struct {
|
||||
FileOptionsWithSHA
|
||||
FileOptions
|
||||
// the blob ID (SHA) for the file that already exists to update, or leave it empty to create a new file
|
||||
SHA string `json:"sha"`
|
||||
// content must be base64 encoded
|
||||
// required: true
|
||||
ContentBase64 string `json:"content"`
|
||||
|
||||
@ -6,6 +6,7 @@ package util
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
)
|
||||
|
||||
// Common Errors forming the base of our error system
|
||||
@ -40,22 +41,6 @@ func (w errorWrapper) Unwrap() error {
|
||||
return w.Err
|
||||
}
|
||||
|
||||
type LocaleWrapper struct {
|
||||
err error
|
||||
TrKey string
|
||||
TrArgs []any
|
||||
}
|
||||
|
||||
// Error returns the message
|
||||
func (w LocaleWrapper) Error() string {
|
||||
return w.err.Error()
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying error
|
||||
func (w LocaleWrapper) Unwrap() error {
|
||||
return w.err
|
||||
}
|
||||
|
||||
// ErrorWrap returns an error that formats as the given text but unwraps as the provided error
|
||||
func ErrorWrap(unwrap error, message string, args ...any) error {
|
||||
if len(args) == 0 {
|
||||
@ -84,15 +69,39 @@ func NewNotExistErrorf(message string, args ...any) error {
|
||||
return ErrorWrap(ErrNotExist, message, args...)
|
||||
}
|
||||
|
||||
// ErrorWrapLocale wraps an err with a translation key and arguments
|
||||
func ErrorWrapLocale(err error, trKey string, trArgs ...any) error {
|
||||
return LocaleWrapper{err: err, TrKey: trKey, TrArgs: trArgs}
|
||||
// ErrorTranslatable wraps an error with translation information
|
||||
type ErrorTranslatable interface {
|
||||
error
|
||||
Unwrap() error
|
||||
Translate(ErrorLocaleTranslator) template.HTML
|
||||
}
|
||||
|
||||
func ErrorAsLocale(err error) *LocaleWrapper {
|
||||
var e LocaleWrapper
|
||||
type errorTranslatableWrapper struct {
|
||||
err error
|
||||
trKey string
|
||||
trArgs []any
|
||||
}
|
||||
|
||||
type ErrorLocaleTranslator interface {
|
||||
Tr(key string, args ...any) template.HTML
|
||||
}
|
||||
|
||||
func (w *errorTranslatableWrapper) Error() string { return w.err.Error() }
|
||||
|
||||
func (w *errorTranslatableWrapper) Unwrap() error { return w.err }
|
||||
|
||||
func (w *errorTranslatableWrapper) Translate(t ErrorLocaleTranslator) template.HTML {
|
||||
return t.Tr(w.trKey, w.trArgs...)
|
||||
}
|
||||
|
||||
func ErrorWrapTranslatable(err error, trKey string, trArgs ...any) ErrorTranslatable {
|
||||
return &errorTranslatableWrapper{err: err, trKey: trKey, trArgs: trArgs}
|
||||
}
|
||||
|
||||
func ErrorAsTranslatable(err error) ErrorTranslatable {
|
||||
var e *errorTranslatableWrapper
|
||||
if errors.As(err, &e) {
|
||||
return &e
|
||||
return e
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
29
modules/util/error_test.go
Normal file
29
modules/util/error_test.go
Normal file
@ -0,0 +1,29 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestErrorTranslatable(t *testing.T) {
|
||||
var err error
|
||||
|
||||
err = ErrorWrapTranslatable(io.EOF, "key", 1)
|
||||
assert.ErrorIs(t, err, io.EOF)
|
||||
assert.Equal(t, "EOF", err.Error())
|
||||
assert.Equal(t, "key", err.(*errorTranslatableWrapper).trKey)
|
||||
assert.Equal(t, []any{1}, err.(*errorTranslatableWrapper).trArgs)
|
||||
|
||||
err = ErrorWrap(err, "new msg %d", 100)
|
||||
assert.ErrorIs(t, err, io.EOF)
|
||||
assert.Equal(t, "new msg 100", err.Error())
|
||||
|
||||
errTr := ErrorAsTranslatable(err)
|
||||
assert.Equal(t, "EOF", errTr.Error())
|
||||
assert.Equal(t, "key", errTr.(*errorTranslatableWrapper).trKey)
|
||||
}
|
||||
@ -3586,6 +3586,7 @@ variables.update.success=Proměnná byla upravena.
|
||||
logs.always_auto_scroll=Vždy automaticky posouvat logy
|
||||
logs.always_expand_running=Vždy rozšířit běžící logy
|
||||
|
||||
|
||||
[projects]
|
||||
deleted.display_name=Odstraněný projekt
|
||||
type-1.display_name=Samostatný projekt
|
||||
|
||||
@ -3645,6 +3645,7 @@ variables.update.success=Die Variable wurde bearbeitet.
|
||||
logs.always_auto_scroll=Autoscroll für Logs immer aktivieren
|
||||
logs.always_expand_running=Laufende Logs immer erweitern
|
||||
|
||||
|
||||
[projects]
|
||||
deleted.display_name=Gelöschtes Projekt
|
||||
type-1.display_name=Individuelles Projekt
|
||||
|
||||
@ -3280,6 +3280,7 @@ variables.update.failed=Αποτυχία επεξεργασίας μεταβλη
|
||||
variables.update.success=Η μεταβλητή έχει τροποποιηθεί.
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
type-1.display_name=Ατομικό Έργο
|
||||
type-2.display_name=Έργο Αποθετηρίου
|
||||
|
||||
@ -3914,6 +3914,15 @@ variables.update.success = The variable has been edited.
|
||||
logs.always_auto_scroll = Always auto scroll logs
|
||||
logs.always_expand_running = Always expand running logs
|
||||
|
||||
general = General
|
||||
general.enable_actions = Enable Actions
|
||||
general.collaborative_owners_management = Collaborative Owners Management
|
||||
general.collaborative_owners_management_help = A collaborative owner is a user or an organization whose private repository has access to the actions and workflows of this repository.
|
||||
general.add_collaborative_owner = Add Collaborative Owner
|
||||
general.collaborative_owner_not_exist = The collaborative owner does not exist.
|
||||
general.remove_collaborative_owner = Remove Collaborative Owner
|
||||
general.remove_collaborative_owner_desc = Removing a collaborative owner will prevent the repositories of the owner from accessing the actions in this repository. Continue?
|
||||
|
||||
[projects]
|
||||
deleted.display_name = Deleted Project
|
||||
type-1.display_name = Individual Project
|
||||
|
||||
@ -3257,6 +3257,7 @@ variables.update.failed=Error al editar la variable.
|
||||
variables.update.success=La variable ha sido editada.
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
type-1.display_name=Proyecto individual
|
||||
type-2.display_name=Proyecto repositorio
|
||||
|
||||
@ -2446,6 +2446,7 @@ runs.commit=کامیت
|
||||
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
|
||||
[git.filemode]
|
||||
|
||||
@ -1693,6 +1693,7 @@ runs.commit=Commit
|
||||
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
|
||||
[git.filemode]
|
||||
|
||||
@ -3906,6 +3906,7 @@ variables.update.success=La variable a bien été modifiée.
|
||||
logs.always_auto_scroll=Toujours faire défiler les journaux automatiquement
|
||||
logs.always_expand_running=Toujours développer les journaux en cours
|
||||
|
||||
|
||||
[projects]
|
||||
deleted.display_name=Projet supprimé
|
||||
type-1.display_name=Projet personnel
|
||||
|
||||
@ -3914,6 +3914,7 @@ variables.update.success=Tá an t-athróg curtha in eagar.
|
||||
logs.always_auto_scroll=Logchomhaid scrollaithe uathoibríoch i gcónaí
|
||||
logs.always_expand_running=Leathnaigh logs reatha i gcónaí
|
||||
|
||||
|
||||
[projects]
|
||||
deleted.display_name=Tionscadal scriosta
|
||||
type-1.display_name=Tionscadal Aonair
|
||||
|
||||
@ -1605,6 +1605,7 @@ runs.commit=Commit
|
||||
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
|
||||
[git.filemode]
|
||||
|
||||
@ -1428,6 +1428,7 @@ variables.update.failed=Gagal mengedit variabel.
|
||||
variables.update.success=Variabel telah diedit.
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
type-1.display_name=Proyek Individu
|
||||
type-2.display_name=Proyek Repositori
|
||||
|
||||
@ -1334,6 +1334,7 @@ runs.commit=Framlag
|
||||
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
|
||||
[git.filemode]
|
||||
|
||||
@ -2706,6 +2706,7 @@ runs.commit=Commit
|
||||
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
|
||||
[git.filemode]
|
||||
|
||||
@ -3910,6 +3910,7 @@ variables.update.success=変数を更新しました。
|
||||
logs.always_auto_scroll=常にログを自動スクロール
|
||||
logs.always_expand_running=常に実行中のログを展開
|
||||
|
||||
|
||||
[projects]
|
||||
deleted.display_name=削除されたプロジェクト
|
||||
type-1.display_name=個人プロジェクト
|
||||
|
||||
@ -1554,6 +1554,7 @@ runs.commit=커밋
|
||||
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
|
||||
[git.filemode]
|
||||
|
||||
@ -3282,6 +3282,7 @@ variables.update.failed=Neizdevās labot mainīgo.
|
||||
variables.update.success=Mainīgais tika labots.
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
type-1.display_name=Individuālais projekts
|
||||
type-2.display_name=Repozitorija projekts
|
||||
|
||||
@ -2458,6 +2458,7 @@ runs.commit=Commit
|
||||
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
|
||||
[git.filemode]
|
||||
|
||||
@ -2347,6 +2347,7 @@ runs.commit=Commit
|
||||
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
|
||||
[git.filemode]
|
||||
|
||||
@ -3615,6 +3615,7 @@ variables.update.failed=Falha ao editar a variável.
|
||||
variables.update.success=A variável foi editada.
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
deleted.display_name=Excluir Projeto
|
||||
type-1.display_name=Projeto Individual
|
||||
|
||||
@ -3914,6 +3914,7 @@ variables.update.success=A variável foi editada.
|
||||
logs.always_auto_scroll=Rolar registos de forma automática e permanente
|
||||
logs.always_expand_running=Expandir sempre os registos que vão rolando
|
||||
|
||||
|
||||
[projects]
|
||||
deleted.display_name=Planeamento eliminado
|
||||
type-1.display_name=Planeamento individual
|
||||
|
||||
@ -3225,6 +3225,7 @@ variables.update.failed=Не удалось изменить переменну
|
||||
variables.update.success=Переменная изменена.
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
type-1.display_name=Индивидуальный проект
|
||||
type-2.display_name=Проект репозитория
|
||||
|
||||
@ -2391,6 +2391,7 @@ runs.commit=කැප
|
||||
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
|
||||
[git.filemode]
|
||||
|
||||
@ -1292,6 +1292,7 @@ runners.labels=Štítky
|
||||
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
|
||||
[git.filemode]
|
||||
|
||||
@ -1968,6 +1968,7 @@ runs.commit=Commit
|
||||
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
|
||||
[git.filemode]
|
||||
|
||||
@ -3907,6 +3907,7 @@ variables.update.success=Değişken düzenlendi.
|
||||
logs.always_auto_scroll=Günlükleri her zaman otomatik kaydır
|
||||
logs.always_expand_running=Çalıştırma günlüklerini her zaman genişlet
|
||||
|
||||
|
||||
[projects]
|
||||
deleted.display_name=Silinmiş Proje
|
||||
type-1.display_name=Kişisel Proje
|
||||
|
||||
@ -3428,6 +3428,7 @@ variables.update.success=Змінну відредаговано.
|
||||
logs.always_auto_scroll=Завжди автоматично прокручувати журнали
|
||||
logs.always_expand_running=Завжди розгортати поточні журнали
|
||||
|
||||
|
||||
[projects]
|
||||
deleted.display_name=Видалений проєкт
|
||||
type-1.display_name=Індивідуальний проєкт
|
||||
|
||||
@ -3911,6 +3911,7 @@ variables.update.success=变量已编辑。
|
||||
logs.always_auto_scroll=总是自动滚动日志
|
||||
logs.always_expand_running=总是展开运行日志
|
||||
|
||||
|
||||
[projects]
|
||||
deleted.display_name=已删除项目
|
||||
type-1.display_name=个人项目
|
||||
|
||||
@ -980,6 +980,7 @@ runners.task_list.repository=儲存庫
|
||||
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
|
||||
[git.filemode]
|
||||
|
||||
@ -3554,6 +3554,7 @@ variables.update.failed=編輯變數失敗。
|
||||
variables.update.success=已編輯變數。
|
||||
|
||||
|
||||
|
||||
[projects]
|
||||
deleted.display_name=已刪除的專案
|
||||
type-1.display_name=個人專案
|
||||
|
||||
36
package.json
36
package.json
@ -6,13 +6,13 @@
|
||||
"pnpm": ">= 10.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@citation-js/core": "0.7.18",
|
||||
"@citation-js/plugin-bibtex": "0.7.18",
|
||||
"@citation-js/plugin-csl": "0.7.18",
|
||||
"@citation-js/core": "0.7.21",
|
||||
"@citation-js/plugin-bibtex": "0.7.21",
|
||||
"@citation-js/plugin-csl": "0.7.21",
|
||||
"@citation-js/plugin-software-formats": "0.6.1",
|
||||
"@github/markdown-toolbar-element": "2.2.3",
|
||||
"@github/paste-markdown": "1.5.3",
|
||||
"@github/relative-time-element": "4.4.8",
|
||||
"@github/relative-time-element": "4.5.0",
|
||||
"@github/text-expander-element": "2.9.2",
|
||||
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
|
||||
"@primer/octicons": "19.19.0",
|
||||
@ -21,8 +21,8 @@
|
||||
"@techknowlogick/license-checker-webpack-plugin": "0.3.0",
|
||||
"add-asset-webpack-plugin": "3.1.1",
|
||||
"ansi_up": "6.0.6",
|
||||
"asciinema-player": "3.12.0",
|
||||
"chart.js": "4.5.0",
|
||||
"asciinema-player": "3.12.1",
|
||||
"chart.js": "4.5.1",
|
||||
"chartjs-adapter-dayjs-4": "1.0.4",
|
||||
"chartjs-plugin-zoom": "2.2.0",
|
||||
"clippie": "4.1.8",
|
||||
@ -35,7 +35,7 @@
|
||||
"htmx.org": "2.0.7",
|
||||
"idiomorph": "0.7.4",
|
||||
"jquery": "3.7.1",
|
||||
"katex": "0.16.23",
|
||||
"katex": "0.16.25",
|
||||
"mermaid": "11.12.0",
|
||||
"mini-css-extract-plugin": "2.9.4",
|
||||
"monaco-editor": "0.54.0",
|
||||
@ -46,7 +46,7 @@
|
||||
"postcss": "8.5.6",
|
||||
"postcss-loader": "8.2.0",
|
||||
"sortablejs": "1.15.6",
|
||||
"swagger-ui-dist": "5.29.4",
|
||||
"swagger-ui-dist": "5.29.5",
|
||||
"tailwindcss": "3.4.17",
|
||||
"throttle-debounce": "5.0.2",
|
||||
"tinycolor2": "1.6.0",
|
||||
@ -66,8 +66,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint-community/eslint-plugin-eslint-comments": "4.5.0",
|
||||
"@playwright/test": "1.56.0",
|
||||
"@stylistic/eslint-plugin": "5.4.0",
|
||||
"@playwright/test": "1.56.1",
|
||||
"@stylistic/eslint-plugin": "5.5.0",
|
||||
"@stylistic/stylelint-plugin": "4.0.0",
|
||||
"@types/codemirror": "5.60.16",
|
||||
"@types/dropzone": "5.7.9",
|
||||
@ -79,10 +79,10 @@
|
||||
"@types/throttle-debounce": "5.0.2",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/toastify-js": "1.12.4",
|
||||
"@typescript-eslint/parser": "8.46.0",
|
||||
"@typescript-eslint/parser": "8.46.2",
|
||||
"@vitejs/plugin-vue": "6.0.1",
|
||||
"@vitest/eslint-plugin": "1.3.16",
|
||||
"eslint": "9.37.0",
|
||||
"@vitest/eslint-plugin": "1.3.23",
|
||||
"eslint": "9.38.0",
|
||||
"eslint-import-resolver-typescript": "4.4.4",
|
||||
"eslint-plugin-array-func": "5.1.0",
|
||||
"eslint-plugin-github": "6.0.0",
|
||||
@ -92,11 +92,11 @@
|
||||
"eslint-plugin-regexp": "2.10.0",
|
||||
"eslint-plugin-sonarjs": "3.0.5",
|
||||
"eslint-plugin-unicorn": "61.0.2",
|
||||
"eslint-plugin-vue": "10.5.0",
|
||||
"eslint-plugin-vue": "10.5.1",
|
||||
"eslint-plugin-vue-scoped-css": "2.12.0",
|
||||
"eslint-plugin-wc": "3.0.2",
|
||||
"globals": "16.4.0",
|
||||
"happy-dom": "20.0.2",
|
||||
"happy-dom": "20.0.8",
|
||||
"markdownlint-cli": "0.45.0",
|
||||
"material-icon-theme": "5.27.0",
|
||||
"nolyfill": "1.0.44",
|
||||
@ -108,10 +108,10 @@
|
||||
"stylelint-declaration-strict-value": "1.10.11",
|
||||
"stylelint-value-no-unknown-custom-properties": "6.0.1",
|
||||
"svgo": "4.0.0",
|
||||
"typescript-eslint": "8.46.0",
|
||||
"updates": "16.8.0",
|
||||
"typescript-eslint": "8.46.2",
|
||||
"updates": "16.8.1",
|
||||
"vite-string-plugin": "1.4.6",
|
||||
"vitest": "3.2.4",
|
||||
"vitest": "4.0.1",
|
||||
"vue-tsc": "3.1.1"
|
||||
},
|
||||
"browserslist": [
|
||||
|
||||
1473
pnpm-lock.yaml
1473
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -103,7 +103,7 @@ func GetAllOrgs(ctx *context.APIContext) {
|
||||
|
||||
users, maxResults, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{
|
||||
Actor: ctx.Doer,
|
||||
Type: user_model.UserTypeOrganization,
|
||||
Types: []user_model.UserType{user_model.UserTypeOrganization},
|
||||
OrderBy: db.SearchOrderByAlphabetically,
|
||||
ListOptions: listOptions,
|
||||
Visible: []api.VisibleType{api.VisibleTypePublic, api.VisibleTypeLimited, api.VisibleTypePrivate},
|
||||
|
||||
@ -425,7 +425,7 @@ func SearchUsers(ctx *context.APIContext) {
|
||||
|
||||
users, maxResults, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{
|
||||
Actor: ctx.Doer,
|
||||
Type: user_model.UserTypeIndividual,
|
||||
Types: []user_model.UserType{user_model.UserTypeIndividual},
|
||||
LoginName: ctx.FormTrim("login_name"),
|
||||
SourceID: ctx.FormInt64("source_id"),
|
||||
OrderBy: db.SearchOrderByAlphabetically,
|
||||
|
||||
@ -70,7 +70,6 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
@ -190,27 +189,11 @@ func repoAssignment() func(ctx *context.APIContext) {
|
||||
|
||||
if ctx.Doer != nil && ctx.Doer.ID == user_model.ActionsUserID {
|
||||
taskID := ctx.Data["ActionsTaskID"].(int64)
|
||||
task, err := actions_model.GetTaskByID(ctx, taskID)
|
||||
ctx.Repo.Permission, err = access_model.GetActionsUserRepoPermission(ctx, repo, ctx.Doer, taskID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if task.RepoID != repo.ID {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if task.IsForkPullRequest {
|
||||
ctx.Repo.Permission.AccessMode = perm.AccessModeRead
|
||||
} else {
|
||||
ctx.Repo.Permission.AccessMode = perm.AccessModeWrite
|
||||
}
|
||||
|
||||
if err := ctx.Repo.Repository.LoadUnits(ctx); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
ctx.Repo.Permission.SetUnitsWithDefaultAccessMode(ctx.Repo.Repository.Units, ctx.Repo.Permission.AccessMode)
|
||||
} else {
|
||||
needTwoFactor, err := doerNeedTwoFactorAuth(ctx, ctx.Doer)
|
||||
if err != nil {
|
||||
|
||||
@ -202,7 +202,7 @@ func GetAll(ctx *context.APIContext) {
|
||||
publicOrgs, maxResults, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{
|
||||
Actor: ctx.Doer,
|
||||
ListOptions: listOptions,
|
||||
Type: user_model.UserTypeOrganization,
|
||||
Types: []user_model.UserType{user_model.UserTypeOrganization},
|
||||
OrderBy: db.SearchOrderByAlphabetically,
|
||||
Visible: vMode,
|
||||
})
|
||||
|
||||
@ -225,7 +225,7 @@ func CreateBranch(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
} else if len(opt.OldBranchName) > 0 { //nolint:staticcheck // deprecated field
|
||||
if gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, opt.OldBranchName) { //nolint:staticcheck // deprecated field
|
||||
if exist, _ := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, opt.OldBranchName); exist { //nolint:staticcheck // deprecated field
|
||||
oldCommit, err = ctx.Repo.GitRepo.GetBranchCommit(opt.OldBranchName) //nolint:staticcheck // deprecated field
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
@ -1011,7 +1011,11 @@ func EditBranchProtection(ctx *context.APIContext) {
|
||||
isPlainRule := !git_model.IsRuleNameSpecial(bpName)
|
||||
var isBranchExist bool
|
||||
if isPlainRule {
|
||||
isBranchExist = gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, bpName)
|
||||
isBranchExist, err = git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, bpName)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if isBranchExist {
|
||||
|
||||
@ -525,7 +525,7 @@ func CreateFile(ctx *context.APIContext) {
|
||||
func UpdateFile(ctx *context.APIContext) {
|
||||
// swagger:operation PUT /repos/{owner}/{repo}/contents/{filepath} repository repoUpdateFile
|
||||
// ---
|
||||
// summary: Update a file in a repository
|
||||
// summary: Update a file in a repository if SHA is set, or create the file if SHA is not set
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
@ -554,6 +554,8 @@ func UpdateFile(ctx *context.APIContext) {
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/FileResponse"
|
||||
// "201":
|
||||
// "$ref": "#/responses/FileResponse"
|
||||
// "403":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
@ -572,8 +574,9 @@ func UpdateFile(ctx *context.APIContext) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
return
|
||||
}
|
||||
willCreate := apiOpts.SHA == ""
|
||||
opts.Files = append(opts.Files, &files_service.ChangeRepoFile{
|
||||
Operation: "update",
|
||||
Operation: util.Iif(willCreate, "create", "update"),
|
||||
ContentReader: contentReader,
|
||||
SHA: apiOpts.SHA,
|
||||
FromTreePath: apiOpts.FromPath,
|
||||
@ -587,7 +590,7 @@ func UpdateFile(ctx *context.APIContext) {
|
||||
handleChangeRepoFilesError(ctx, err)
|
||||
} else {
|
||||
fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
|
||||
ctx.JSON(http.StatusOK, fileResponse)
|
||||
ctx.JSON(util.Iif(willCreate, http.StatusCreated, http.StatusOK), fileResponse)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -756,7 +756,12 @@ func EditPullRequest(ctx *context.APIContext) {
|
||||
|
||||
// change pull target branch
|
||||
if !pr.HasMerged && len(form.Base) != 0 && form.Base != pr.BaseBranch {
|
||||
if !gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, form.Base) {
|
||||
branchExist, err := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, form.Base)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if !branchExist {
|
||||
ctx.APIError(http.StatusNotFound, fmt.Errorf("new base '%s' not exist", form.Base))
|
||||
return
|
||||
}
|
||||
@ -938,7 +943,7 @@ func MergePullRequest(ctx *context.APIContext) {
|
||||
} else if errors.Is(err, pull_service.ErrNoPermissionToMerge) {
|
||||
ctx.APIError(http.StatusMethodNotAllowed, "User not allowed to merge PR")
|
||||
} else if errors.Is(err, pull_service.ErrHasMerged) {
|
||||
ctx.APIError(http.StatusMethodNotAllowed, "")
|
||||
ctx.APIError(http.StatusMethodNotAllowed, "The PR is already merged")
|
||||
} else if errors.Is(err, pull_service.ErrIsWorkInProgress) {
|
||||
ctx.APIError(http.StatusMethodNotAllowed, "Work in progress PRs cannot be merged")
|
||||
} else if errors.Is(err, pull_service.ErrNotMergeableState) {
|
||||
@ -989,8 +994,14 @@ func MergePullRequest(ctx *context.APIContext) {
|
||||
message += "\n\n" + form.MergeMessageField
|
||||
}
|
||||
|
||||
deleteBranchAfterMerge, err := pull_service.ShouldDeleteBranchAfterMerge(ctx, form.DeleteBranchAfterMerge, ctx.Repo.Repository, pr)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
if form.MergeWhenChecksSucceed {
|
||||
scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message, form.DeleteBranchAfterMerge)
|
||||
scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message, deleteBranchAfterMerge)
|
||||
if err != nil {
|
||||
if pull_model.IsErrAlreadyScheduledToAutoMerge(err) {
|
||||
ctx.APIError(http.StatusConflict, err)
|
||||
@ -1035,47 +1046,10 @@ func MergePullRequest(ctx *context.APIContext) {
|
||||
}
|
||||
log.Trace("Pull request merged: %d", pr.ID)
|
||||
|
||||
// for agit flow, we should not delete the agit reference after merge
|
||||
if form.DeleteBranchAfterMerge && pr.Flow == issues_model.PullRequestFlowGithub {
|
||||
// check permission even it has been checked in repo_service.DeleteBranch so that we don't need to
|
||||
// do RetargetChildrenOnMerge
|
||||
if err := repo_service.CanDeleteBranch(ctx, pr.HeadRepo, pr.HeadBranch, ctx.Doer); err == nil {
|
||||
// Don't cleanup when there are other PR's that use this branch as head branch.
|
||||
exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if exist {
|
||||
ctx.Status(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
var headRepo *git.Repository
|
||||
if ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == pr.HeadRepoID && ctx.Repo.GitRepo != nil {
|
||||
headRepo = ctx.Repo.GitRepo
|
||||
} else {
|
||||
headRepo, err = gitrepo.OpenRepository(ctx, pr.HeadRepo)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
defer headRepo.Close()
|
||||
}
|
||||
|
||||
if err := repo_service.DeleteBranch(ctx, ctx.Doer, pr.HeadRepo, headRepo, pr.HeadBranch, pr); err != nil {
|
||||
switch {
|
||||
case git.IsErrBranchNotExist(err):
|
||||
ctx.APIErrorNotFound(err)
|
||||
case errors.Is(err, repo_service.ErrBranchIsDefault):
|
||||
ctx.APIError(http.StatusForbidden, errors.New("can not delete default branch"))
|
||||
case errors.Is(err, git_model.ErrBranchIsProtected):
|
||||
ctx.APIError(http.StatusForbidden, errors.New("branch protected"))
|
||||
default:
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if deleteBranchAfterMerge {
|
||||
if err = repo_service.DeleteBranchAfterMerge(ctx, ctx.Doer, pr.ID, nil); err != nil {
|
||||
// no way to tell users that what error happens, and the PR has been merged, so ignore the error
|
||||
log.Debug("DeleteBranchAfterMerge: pr %d, err: %v", pr.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -28,6 +28,7 @@ import (
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/validation"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
@ -270,6 +271,8 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre
|
||||
db.IsErrNamePatternNotAllowed(err) ||
|
||||
label.IsErrTemplateLoad(err) {
|
||||
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||
} else if errors.Is(err, util.ErrPermissionDenied) {
|
||||
ctx.APIError(http.StatusForbidden, err)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
|
||||
@ -77,7 +77,7 @@ func Search(ctx *context.APIContext) {
|
||||
Actor: ctx.Doer,
|
||||
Keyword: ctx.FormTrim("q"),
|
||||
UID: uid,
|
||||
Type: user_model.UserTypeIndividual,
|
||||
Types: []user_model.UserType{user_model.UserTypeIndividual},
|
||||
SearchByEmail: true,
|
||||
Visible: visible,
|
||||
ListOptions: listOptions,
|
||||
|
||||
@ -6,6 +6,7 @@ package utils
|
||||
import (
|
||||
"errors"
|
||||
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
@ -27,7 +28,7 @@ func ResolveRefCommit(ctx reqctx.RequestContext, repo *repo_model.Repository, in
|
||||
return nil, err
|
||||
}
|
||||
refCommit := RefCommit{InputRef: inputRef}
|
||||
if gitrepo.IsBranchExist(ctx, repo, inputRef) {
|
||||
if exist, _ := git_model.IsBranchExist(ctx, repo.ID, inputRef); exist {
|
||||
refCommit.RefName = git.RefNameFromBranch(inputRef)
|
||||
} else if gitrepo.IsTagExist(ctx, repo, inputRef) {
|
||||
refCommit.RefName = git.RefNameFromTag(inputRef)
|
||||
|
||||
@ -21,7 +21,9 @@ import (
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/private"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/agit"
|
||||
gitea_context "code.gitea.io/gitea/services/context"
|
||||
pull_service "code.gitea.io/gitea/services/pull"
|
||||
)
|
||||
@ -452,25 +454,18 @@ func preReceiveFor(ctx *preReceiveContext, refFullName git.RefName) {
|
||||
return
|
||||
}
|
||||
|
||||
baseBranchName := refFullName.ForBranchName()
|
||||
|
||||
baseBranchExist := gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, baseBranchName)
|
||||
|
||||
if !baseBranchExist {
|
||||
for p, v := range baseBranchName {
|
||||
if v == '/' && gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, baseBranchName[:p]) && p != len(baseBranchName)-1 {
|
||||
baseBranchExist = true
|
||||
break
|
||||
}
|
||||
_, _, err := agit.GetAgitBranchInfo(ctx, ctx.Repo.Repository.ID, refFullName.ForBranchName())
|
||||
if err != nil {
|
||||
if !errors.Is(err, util.ErrNotExist) {
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: fmt.Sprintf("Unexpected ref: %s", refFullName),
|
||||
})
|
||||
} else {
|
||||
ctx.JSON(http.StatusInternalServerError, private.Response{
|
||||
Err: err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if !baseBranchExist {
|
||||
ctx.JSON(http.StatusForbidden, private.Response{
|
||||
UserMsg: fmt.Sprintf("Unexpected ref: %s", refFullName),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func generateGitEnv(opts *private.HookOptions) (env []string) {
|
||||
|
||||
@ -29,7 +29,7 @@ func Organizations(ctx *context.Context) {
|
||||
|
||||
explore.RenderUserSearch(ctx, user_model.SearchUserOptions{
|
||||
Actor: ctx.Doer,
|
||||
Type: user_model.UserTypeOrganization,
|
||||
Types: []user_model.UserType{user_model.UserTypeOrganization},
|
||||
IncludeReserved: true, // administrator needs to list all accounts include reserved
|
||||
ListOptions: db.ListOptions{
|
||||
PageSize: setting.UI.Admin.OrgPagingNum,
|
||||
|
||||
@ -67,7 +67,7 @@ func Users(ctx *context.Context) {
|
||||
|
||||
explore.RenderUserSearch(ctx, user_model.SearchUserOptions{
|
||||
Actor: ctx.Doer,
|
||||
Type: user_model.UserTypeIndividual,
|
||||
Types: []user_model.UserType{user_model.UserTypeIndividual},
|
||||
ListOptions: db.ListOptions{
|
||||
PageSize: setting.UI.Admin.UserPagingNum,
|
||||
},
|
||||
|
||||
@ -46,7 +46,7 @@ func Organizations(ctx *context.Context) {
|
||||
|
||||
RenderUserSearch(ctx, user_model.SearchUserOptions{
|
||||
Actor: ctx.Doer,
|
||||
Type: user_model.UserTypeOrganization,
|
||||
Types: []user_model.UserType{user_model.UserTypeOrganization},
|
||||
ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum},
|
||||
Visible: visibleTypes,
|
||||
|
||||
|
||||
@ -153,7 +153,7 @@ func Users(ctx *context.Context) {
|
||||
|
||||
RenderUserSearch(ctx, user_model.SearchUserOptions{
|
||||
Actor: ctx.Doer,
|
||||
Type: user_model.UserTypeIndividual,
|
||||
Types: []user_model.UserType{user_model.UserTypeIndividual},
|
||||
ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum},
|
||||
IsActive: optional.Some(true),
|
||||
Visible: []structs.VisibleType{structs.VisibleTypePublic, structs.VisibleTypeLimited, structs.VisibleTypePrivate},
|
||||
|
||||
@ -69,7 +69,7 @@ func HomeSitemap(ctx *context.Context) {
|
||||
m := sitemap.NewSitemapIndex()
|
||||
if !setting.Service.Explore.DisableUsersPage {
|
||||
_, cnt, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{
|
||||
Type: user_model.UserTypeIndividual,
|
||||
Types: []user_model.UserType{user_model.UserTypeIndividual},
|
||||
ListOptions: db.ListOptions{PageSize: 1},
|
||||
IsActive: optional.Some(true),
|
||||
Visible: []structs.VisibleType{structs.VisibleTypePublic},
|
||||
|
||||
@ -28,7 +28,7 @@ import (
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
|
||||
"github.com/nektos/act/pkg/model"
|
||||
act_model "github.com/nektos/act/pkg/model"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@ -38,9 +38,10 @@ const (
|
||||
tplViewActions templates.TplName = "repo/actions/view"
|
||||
)
|
||||
|
||||
type Workflow struct {
|
||||
Entry git.TreeEntry
|
||||
ErrMsg string
|
||||
type WorkflowInfo struct {
|
||||
Entry git.TreeEntry
|
||||
ErrMsg string
|
||||
Workflow *act_model.Workflow
|
||||
}
|
||||
|
||||
// MustEnableActions check if actions are enabled in settings
|
||||
@ -77,7 +78,11 @@ func List(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
workflows := prepareWorkflowDispatchTemplate(ctx, commit)
|
||||
workflows, curWorkflowID := prepareWorkflowTemplate(ctx, commit)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
prepareWorkflowDispatchTemplate(ctx, workflows, curWorkflowID)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
@ -112,55 +117,41 @@ func WorkflowDispatchInputs(ctx *context.Context) {
|
||||
ctx.ServerError("GetTagCommit/GetBranchCommit", err)
|
||||
return
|
||||
}
|
||||
prepareWorkflowDispatchTemplate(ctx, commit)
|
||||
workflows, curWorkflowID := prepareWorkflowTemplate(ctx, commit)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
prepareWorkflowDispatchTemplate(ctx, workflows, curWorkflowID)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
ctx.HTML(http.StatusOK, tplDispatchInputsActions)
|
||||
}
|
||||
|
||||
func prepareWorkflowDispatchTemplate(ctx *context.Context, commit *git.Commit) (workflows []Workflow) {
|
||||
workflowID := ctx.FormString("workflow")
|
||||
ctx.Data["CurWorkflow"] = workflowID
|
||||
ctx.Data["CurWorkflowExists"] = false
|
||||
|
||||
var curWorkflow *model.Workflow
|
||||
func prepareWorkflowTemplate(ctx *context.Context, commit *git.Commit) (workflows []WorkflowInfo, curWorkflowID string) {
|
||||
curWorkflowID = ctx.FormString("workflow")
|
||||
|
||||
_, entries, err := actions.ListWorkflows(commit)
|
||||
if err != nil {
|
||||
ctx.ServerError("ListWorkflows", err)
|
||||
return nil
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
// Get all runner labels
|
||||
runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
IsOnline: optional.Some(true),
|
||||
WithAvailable: true,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("FindRunners", err)
|
||||
return nil
|
||||
}
|
||||
allRunnerLabels := make(container.Set[string])
|
||||
for _, r := range runners {
|
||||
allRunnerLabels.AddMultiple(r.AgentLabels...)
|
||||
}
|
||||
|
||||
workflows = make([]Workflow, 0, len(entries))
|
||||
workflows = make([]WorkflowInfo, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
workflow := Workflow{Entry: *entry}
|
||||
workflow := WorkflowInfo{Entry: *entry}
|
||||
content, err := actions.GetContentFromEntry(entry)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetContentFromEntry", err)
|
||||
return nil
|
||||
return nil, ""
|
||||
}
|
||||
wf, err := model.ReadWorkflow(bytes.NewReader(content))
|
||||
wf, err := act_model.ReadWorkflow(bytes.NewReader(content))
|
||||
if err != nil {
|
||||
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error())
|
||||
workflows = append(workflows, workflow)
|
||||
continue
|
||||
}
|
||||
workflow.Workflow = wf
|
||||
// The workflow must contain at least one job without "needs". Otherwise, a deadlock will occur and no jobs will be able to run.
|
||||
hasJobWithoutNeeds := false
|
||||
// Check whether you have matching runner and a job without "needs"
|
||||
@ -173,22 +164,6 @@ func prepareWorkflowDispatchTemplate(ctx *context.Context, commit *git.Commit) (
|
||||
if !hasJobWithoutNeeds && len(j.Needs()) == 0 {
|
||||
hasJobWithoutNeeds = true
|
||||
}
|
||||
runsOnList := j.RunsOn()
|
||||
for _, ro := range runsOnList {
|
||||
if strings.Contains(ro, "${{") {
|
||||
// Skip if it contains expressions.
|
||||
// The expressions could be very complex and could not be evaluated here,
|
||||
// so just skip it, it's OK since it's just a tooltip message.
|
||||
continue
|
||||
}
|
||||
if !allRunnerLabels.Contains(ro) {
|
||||
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", ro)
|
||||
break
|
||||
}
|
||||
}
|
||||
if workflow.ErrMsg != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasJobWithoutNeeds {
|
||||
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job_without_needs")
|
||||
@ -197,61 +172,75 @@ func prepareWorkflowDispatchTemplate(ctx *context.Context, commit *git.Commit) (
|
||||
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job")
|
||||
}
|
||||
workflows = append(workflows, workflow)
|
||||
|
||||
if workflow.Entry.Name() == workflowID {
|
||||
curWorkflow = wf
|
||||
ctx.Data["CurWorkflowExists"] = true
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data["workflows"] = workflows
|
||||
ctx.Data["RepoLink"] = ctx.Repo.Repository.Link()
|
||||
|
||||
ctx.Data["AllowDisableOrEnableWorkflow"] = ctx.Repo.IsAdmin()
|
||||
actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
|
||||
ctx.Data["ActionsConfig"] = actionsConfig
|
||||
ctx.Data["CurWorkflow"] = curWorkflowID
|
||||
ctx.Data["CurWorkflowDisabled"] = actionsConfig.IsWorkflowDisabled(curWorkflowID)
|
||||
|
||||
if len(workflowID) > 0 && ctx.Repo.CanWrite(unit.TypeActions) {
|
||||
ctx.Data["AllowDisableOrEnableWorkflow"] = ctx.Repo.IsAdmin()
|
||||
isWorkflowDisabled := actionsConfig.IsWorkflowDisabled(workflowID)
|
||||
ctx.Data["CurWorkflowDisabled"] = isWorkflowDisabled
|
||||
|
||||
if !isWorkflowDisabled && curWorkflow != nil {
|
||||
workflowDispatchConfig := workflowDispatchConfig(curWorkflow)
|
||||
if workflowDispatchConfig != nil {
|
||||
ctx.Data["WorkflowDispatchConfig"] = workflowDispatchConfig
|
||||
|
||||
branchOpts := git_model.FindBranchOptions{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
IsDeletedBranch: optional.Some(false),
|
||||
ListOptions: db.ListOptions{
|
||||
ListAll: true,
|
||||
},
|
||||
}
|
||||
branches, err := git_model.FindBranchNames(ctx, branchOpts)
|
||||
if err != nil {
|
||||
ctx.ServerError("FindBranchNames", err)
|
||||
return nil
|
||||
}
|
||||
// always put default branch on the top if it exists
|
||||
if slices.Contains(branches, ctx.Repo.Repository.DefaultBranch) {
|
||||
branches = util.SliceRemoveAll(branches, ctx.Repo.Repository.DefaultBranch)
|
||||
branches = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...)
|
||||
}
|
||||
ctx.Data["Branches"] = branches
|
||||
|
||||
tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetTagNamesByRepoID", err)
|
||||
return nil
|
||||
}
|
||||
ctx.Data["Tags"] = tags
|
||||
}
|
||||
}
|
||||
}
|
||||
return workflows
|
||||
return workflows, curWorkflowID
|
||||
}
|
||||
|
||||
func prepareWorkflowList(ctx *context.Context, workflows []Workflow) {
|
||||
func prepareWorkflowDispatchTemplate(ctx *context.Context, workflowInfos []WorkflowInfo, curWorkflowID string) {
|
||||
actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
|
||||
if curWorkflowID == "" || !ctx.Repo.CanWrite(unit.TypeActions) || actionsConfig.IsWorkflowDisabled(curWorkflowID) {
|
||||
return
|
||||
}
|
||||
|
||||
var curWorkflow *act_model.Workflow
|
||||
for _, workflowInfo := range workflowInfos {
|
||||
if workflowInfo.Entry.Name() == curWorkflowID {
|
||||
if workflowInfo.Workflow == nil {
|
||||
log.Debug("CurWorkflowID %s is found but its workflowInfo.Workflow is nil", curWorkflowID)
|
||||
return
|
||||
}
|
||||
curWorkflow = workflowInfo.Workflow
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if curWorkflow == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["CurWorkflowExists"] = true
|
||||
curWfDispatchCfg := workflowDispatchConfig(curWorkflow)
|
||||
if curWfDispatchCfg == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["WorkflowDispatchConfig"] = curWfDispatchCfg
|
||||
|
||||
branchOpts := git_model.FindBranchOptions{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
IsDeletedBranch: optional.Some(false),
|
||||
ListOptions: db.ListOptions{
|
||||
ListAll: true,
|
||||
},
|
||||
}
|
||||
branches, err := git_model.FindBranchNames(ctx, branchOpts)
|
||||
if err != nil {
|
||||
ctx.ServerError("FindBranchNames", err)
|
||||
return
|
||||
}
|
||||
// always put default branch on the top
|
||||
branches = util.SliceRemoveAll(branches, ctx.Repo.Repository.DefaultBranch)
|
||||
branches = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...)
|
||||
ctx.Data["Branches"] = branches
|
||||
|
||||
tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetTagNamesByRepoID", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Tags"] = tags
|
||||
}
|
||||
|
||||
func prepareWorkflowList(ctx *context.Context, workflows []WorkflowInfo) {
|
||||
actorID := ctx.FormInt64("actor")
|
||||
status := ctx.FormInt("status")
|
||||
workflowID := ctx.FormString("workflow")
|
||||
@ -302,6 +291,45 @@ func prepareWorkflowList(ctx *context.Context, workflows []Workflow) {
|
||||
log.Error("LoadIsRefDeleted", err)
|
||||
}
|
||||
|
||||
// Check for each run if there is at least one online runner that can run its jobs
|
||||
runErrors := make(map[int64]string)
|
||||
runners, err := db.Find[actions_model.ActionRunner](ctx, actions_model.FindRunnerOptions{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
IsOnline: optional.Some(true),
|
||||
WithAvailable: true,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("FindRunners", err)
|
||||
return
|
||||
}
|
||||
for _, run := range runs {
|
||||
if !run.Status.In(actions_model.StatusWaiting, actions_model.StatusRunning) {
|
||||
continue
|
||||
}
|
||||
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetRunJobsByRunID", err)
|
||||
return
|
||||
}
|
||||
for _, job := range jobs {
|
||||
if !job.Status.IsWaiting() {
|
||||
continue
|
||||
}
|
||||
hasOnlineRunner := false
|
||||
for _, runner := range runners {
|
||||
if runner.CanMatchLabels(job.RunsOn) {
|
||||
hasOnlineRunner = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasOnlineRunner {
|
||||
runErrors[run.ID] = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", strings.Join(job.RunsOn, ","))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.Data["RunErrors"] = runErrors
|
||||
|
||||
ctx.Data["Runs"] = runs
|
||||
|
||||
actors, err := actions_model.GetActors(ctx, ctx.Repo.Repository.ID)
|
||||
@ -362,7 +390,7 @@ type WorkflowDispatch struct {
|
||||
Inputs []WorkflowDispatchInput
|
||||
}
|
||||
|
||||
func workflowDispatchConfig(w *model.Workflow) *WorkflowDispatch {
|
||||
func workflowDispatchConfig(w *act_model.Workflow) *WorkflowDispatch {
|
||||
switch w.RawOn.Kind {
|
||||
case yaml.ScalarNode:
|
||||
var val string
|
||||
|
||||
@ -943,8 +943,8 @@ func Run(ctx *context_module.Context) {
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
if errLocale := util.ErrorAsLocale(err); errLocale != nil {
|
||||
ctx.Flash.Error(ctx.Tr(errLocale.TrKey, errLocale.TrArgs...))
|
||||
if errTr := util.ErrorAsTranslatable(err); errTr != nil {
|
||||
ctx.Flash.Error(errTr.Translate(ctx.Locale))
|
||||
ctx.Redirect(redirectURL)
|
||||
} else {
|
||||
ctx.ServerError("DispatchActionWorkflow", err)
|
||||
|
||||
@ -306,7 +306,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
|
||||
|
||||
// Check if base branch is valid.
|
||||
baseIsCommit := ctx.Repo.GitRepo.IsCommitExist(ci.BaseBranch)
|
||||
baseIsBranch := gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, ci.BaseBranch)
|
||||
baseIsBranch, _ := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, ci.BaseBranch)
|
||||
baseIsTag := gitrepo.IsTagExist(ctx, ctx.Repo.Repository, ci.BaseBranch)
|
||||
|
||||
if !baseIsCommit && !baseIsBranch && !baseIsTag {
|
||||
@ -508,7 +508,7 @@ func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
|
||||
|
||||
// Check if head branch is valid.
|
||||
headIsCommit := ci.HeadGitRepo.IsCommitExist(ci.HeadBranch)
|
||||
headIsBranch := gitrepo.IsBranchExist(ctx, ci.HeadRepo, ci.HeadBranch)
|
||||
headIsBranch, _ := git_model.IsBranchExist(ctx, ci.HeadRepo.ID, ci.HeadBranch)
|
||||
headIsTag := gitrepo.IsTagExist(ctx, ci.HeadRepo, ci.HeadBranch)
|
||||
if !headIsCommit && !headIsBranch && !headIsTag {
|
||||
// Check if headBranch is short sha commit hash
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user