This commit is contained in:
@@ -7,8 +7,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/modules/private"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"gitea.dev/modules/private"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -8,12 +8,12 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/gitrepo"
|
||||
"gitea.dev/modules/log"
|
||||
repo_module "gitea.dev/modules/repository"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -10,9 +10,9 @@ import (
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
auth_service "code.gitea.io/gitea/services/auth"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
auth_service "gitea.dev/services/auth"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/auth/source/ldap"
|
||||
"gitea.dev/models/auth"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/auth/source/ldap"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/services/auth/source/ldap"
|
||||
"gitea.dev/models/auth"
|
||||
"gitea.dev/modules/test"
|
||||
"gitea.dev/services/auth/source/ldap"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/urfave/cli/v3"
|
||||
|
||||
@@ -9,9 +9,9 @@ import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/auth/source/oauth2"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/auth/source/oauth2"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/services/auth/source/oauth2"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/services/auth/source/oauth2"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/urfave/cli/v3"
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/auth/source/smtp"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/auth/source/smtp"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/services/auth/source/smtp"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/services/auth/source/smtp"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/urfave/cli/v3"
|
||||
|
||||
@@ -8,11 +8,11 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/log"
|
||||
repo_service "gitea.dev/services/repository"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -9,14 +9,14 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
activities_model "gitea.dev/models/activities"
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/git/gitcmd"
|
||||
"gitea.dev/modules/timeutil"
|
||||
repo_service "gitea.dev/services/repository"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -6,9 +6,9 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
"gitea.dev/modules/graceful"
|
||||
asymkey_service "gitea.dev/services/asymkey"
|
||||
repo_service "gitea.dev/services/repository"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -8,11 +8,11 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/auth/password"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/auth/password"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
user_service "gitea.dev/services/user"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -82,7 +83,9 @@ func TestChangePasswordCommand(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := microcmdUserChangePassword().Run(ctx, tc.args)
|
||||
cmd := microcmdUserChangePassword()
|
||||
cmd.Writer, cmd.ErrWriter = io.Discard, io.Discard
|
||||
err := cmd.Run(ctx, tc.args)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tc.expectedErr)
|
||||
})
|
||||
|
||||
@@ -9,12 +9,12 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
pwd "code.gitea.io/gitea/modules/auth/password"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
user_model "gitea.dev/models/user"
|
||||
pwd "gitea.dev/modules/auth/password"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
@@ -158,7 +158,8 @@ func runCreateUser(ctx context.Context, c *cli.Command) error {
|
||||
}
|
||||
|
||||
isAdmin := c.Bool("admin")
|
||||
mustChangePassword := true // always default to true
|
||||
// Only local, existing, regular users should be forced to update their password. Bot users for example are non-interactive
|
||||
mustChangePassword := userType == user_model.UserTypeIndividual
|
||||
if c.IsSet("must-change-password") {
|
||||
if userType != user_model.UserTypeIndividual {
|
||||
return errors.New("must-change-password flag can only be set for individual users")
|
||||
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -63,6 +63,7 @@ func TestAdminUserCreate(t *testing.T) {
|
||||
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "u"})
|
||||
assert.Equal(t, user_model.UserTypeBot, u.Type)
|
||||
assert.Empty(t, u.Passwd)
|
||||
assert.False(t, u.MustChangePassword, "bot users should not be forced to change password")
|
||||
})
|
||||
|
||||
t.Run("AccessToken", func(t *testing.T) {
|
||||
|
||||
@@ -9,10 +9,10 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/storage"
|
||||
user_service "gitea.dev/services/user"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
user_model "gitea.dev/models/user"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
user_model "gitea.dev/models/user"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -6,9 +6,9 @@ package cmd
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"io"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
@@ -107,6 +108,7 @@ func TestCertCommandFailures(t *testing.T) {
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
app := cmdCert()
|
||||
app.Writer, app.ErrWriter = io.Discard, io.Discard
|
||||
tempDir := t.TempDir()
|
||||
|
||||
certFile := filepath.Join(tempDir, "cert.pem")
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func TestDefaultCommand(t *testing.T) {
|
||||
test := func(t *testing.T, args []string, expectedRetName string, expectedRetValid bool) {
|
||||
called := false
|
||||
cmd := &cli.Command{
|
||||
DefaultCommand: "test",
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "test",
|
||||
Action: func(ctx context.Context, command *cli.Command) error {
|
||||
retName, retValid := isValidDefaultSubCommand(command)
|
||||
assert.Equal(t, expectedRetName, retName)
|
||||
assert.Equal(t, expectedRetValid, retValid)
|
||||
called = true
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
assert.NoError(t, cmd.Run(t.Context(), args))
|
||||
assert.True(t, called)
|
||||
}
|
||||
test(t, []string{"./gitea"}, "", true)
|
||||
test(t, []string{"./gitea", "test"}, "", true)
|
||||
test(t, []string{"./gitea", "other"}, "other", false)
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Tests here reload the config system multiple times with uncontrollable details.
|
||||
// So they must be in a separate package, to avoid affecting other tests
|
||||
|
||||
package cmdtest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/cmd"
|
||||
"gitea.dev/models/unittest"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/test"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
unittest.MainTest(m)
|
||||
}
|
||||
|
||||
func makePathOutput(workPath, customPath, customConf string) string {
|
||||
return fmt.Sprintf("WorkPath=%s\nCustomPath=%s\nCustomConf=%s", workPath, customPath, customConf)
|
||||
}
|
||||
|
||||
func newTestApp(testCmd cli.Command) *cli.Command {
|
||||
app := cmd.NewMainApp(cmd.AppVersion{})
|
||||
testCmd.Name = util.IfZero(testCmd.Name, "test-cmd")
|
||||
cmd.PrepareSubcommandWithGlobalFlags(&testCmd)
|
||||
app.Commands = append(app.Commands, &testCmd)
|
||||
app.DefaultCommand = testCmd.Name
|
||||
return app
|
||||
}
|
||||
|
||||
type runResult struct {
|
||||
Stdout string
|
||||
Stderr string
|
||||
ExitCode int
|
||||
}
|
||||
|
||||
func runTestApp(app *cli.Command, args ...string) (runResult, error) {
|
||||
outBuf := new(strings.Builder)
|
||||
errBuf := new(strings.Builder)
|
||||
app.Writer = outBuf
|
||||
app.ErrWriter = errBuf
|
||||
exitCode := -1
|
||||
defer test.MockVariableValue(&cli.ErrWriter, app.ErrWriter)()
|
||||
defer test.MockVariableValue(&cli.OsExiter, func(code int) {
|
||||
if exitCode == -1 {
|
||||
exitCode = code // save the exit code once and then reset the writer (to simulate the exit)
|
||||
app.Writer, app.ErrWriter, cli.ErrWriter = io.Discard, io.Discard, io.Discard
|
||||
}
|
||||
})()
|
||||
err := cmd.RunMainApp(app, args...)
|
||||
return runResult{outBuf.String(), errBuf.String(), exitCode}, err
|
||||
}
|
||||
|
||||
func TestCliCmd(t *testing.T) {
|
||||
defaultWorkPath := filepath.FromSlash("/tmp/mocked-work-path")
|
||||
defaultCustomPath := filepath.Join(defaultWorkPath, "custom")
|
||||
defaultCustomConf := filepath.Join(defaultCustomPath, "conf/app.ini")
|
||||
defer setting.MockBuiltinPaths(defaultWorkPath, "", "")()
|
||||
|
||||
cli.CommandHelpTemplate = "(command help template)"
|
||||
cli.RootCommandHelpTemplate = "(app help template)"
|
||||
cli.SubcommandHelpTemplate = "(subcommand help template)"
|
||||
|
||||
cases := []struct {
|
||||
env map[string]string
|
||||
cmd string
|
||||
exp string
|
||||
}{
|
||||
// help commands
|
||||
{
|
||||
cmd: "./gitea -h",
|
||||
exp: "DEFAULT CONFIGURATION:",
|
||||
},
|
||||
{
|
||||
cmd: "./gitea help",
|
||||
exp: "DEFAULT CONFIGURATION:",
|
||||
},
|
||||
|
||||
{
|
||||
cmd: "./gitea -c /dev/null -h",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
|
||||
{
|
||||
cmd: "./gitea -c /dev/null help",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
{
|
||||
cmd: "./gitea help -c /dev/null",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
|
||||
{
|
||||
cmd: "./gitea -c /dev/null test-cmd -h",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
{
|
||||
cmd: "./gitea test-cmd -c /dev/null -h",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
{
|
||||
cmd: "./gitea test-cmd -h -c /dev/null",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
|
||||
{
|
||||
cmd: "./gitea -c /dev/null test-cmd help",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
{
|
||||
cmd: "./gitea test-cmd -c /dev/null help",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
{
|
||||
cmd: "./gitea test-cmd help -c /dev/null",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
|
||||
// parse paths
|
||||
{
|
||||
cmd: "./gitea test-cmd",
|
||||
exp: makePathOutput(defaultWorkPath, defaultCustomPath, defaultCustomConf),
|
||||
},
|
||||
{
|
||||
cmd: "./gitea -c /tmp/app.ini test-cmd",
|
||||
exp: makePathOutput(defaultWorkPath, defaultCustomPath, "/tmp/app.ini"),
|
||||
},
|
||||
{
|
||||
cmd: "./gitea test-cmd -c /tmp/app.ini",
|
||||
exp: makePathOutput(defaultWorkPath, defaultCustomPath, "/tmp/app.ini"),
|
||||
},
|
||||
{
|
||||
env: map[string]string{"GITEA_WORK_DIR": "/tmp"},
|
||||
cmd: "./gitea test-cmd",
|
||||
exp: makePathOutput("/tmp", "/tmp/custom", "/tmp/custom/conf/app.ini"),
|
||||
},
|
||||
{
|
||||
env: map[string]string{"GITEA_WORK_DIR": "/tmp"},
|
||||
cmd: "./gitea test-cmd --work-path /tmp/other",
|
||||
exp: makePathOutput("/tmp/other", "/tmp/other/custom", "/tmp/other/custom/conf/app.ini"),
|
||||
},
|
||||
{
|
||||
env: map[string]string{"GITEA_WORK_DIR": "/tmp"},
|
||||
cmd: "./gitea test-cmd --config /tmp/app-other.ini",
|
||||
exp: makePathOutput("/tmp", "/tmp/custom", "/tmp/app-other.ini"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.cmd, func(t *testing.T) {
|
||||
app := newTestApp(cli.Command{
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
_, _ = fmt.Fprint(cmd.Root().Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf))
|
||||
return nil
|
||||
},
|
||||
})
|
||||
for k, v := range c.env {
|
||||
t.Setenv(k, v)
|
||||
}
|
||||
args := strings.Split(c.cmd, " ") // for test only, "split" is good enough
|
||||
r, err := runTestApp(app, args...)
|
||||
assert.NoError(t, err, c.cmd)
|
||||
assert.NotEmpty(t, c.exp, c.cmd)
|
||||
if !assert.Contains(t, r.Stdout, c.exp, c.cmd) {
|
||||
t.Log("Full output:\n" + r.Stdout)
|
||||
t.Log("Expected:\n" + c.exp)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCliCmdError(t *testing.T) {
|
||||
app := newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return errors.New("normal error") }})
|
||||
r, err := runTestApp(app, "./gitea", "test-cmd")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 1, r.ExitCode)
|
||||
assert.Empty(t, r.Stdout)
|
||||
assert.Equal(t, "Command error: normal error\n", r.Stderr)
|
||||
|
||||
app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return cli.Exit("exit error", 2) }})
|
||||
r, err = runTestApp(app, "./gitea", "test-cmd")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 2, r.ExitCode)
|
||||
assert.Empty(t, r.Stdout)
|
||||
assert.Equal(t, "exit error\n", r.Stderr)
|
||||
|
||||
app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return nil }})
|
||||
r, err = runTestApp(app, "./gitea", "test-cmd", "--no-such")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 1, r.ExitCode)
|
||||
assert.Empty(t, r.Stdout)
|
||||
assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n", r.Stderr)
|
||||
|
||||
app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return nil }})
|
||||
r, err = runTestApp(app, "./gitea", "test-cmd")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, -1, r.ExitCode) // the cli.OsExiter is not called
|
||||
assert.Empty(t, r.Stdout)
|
||||
assert.Empty(t, r.Stderr)
|
||||
}
|
||||
|
||||
func TestCliCmdBefore(t *testing.T) {
|
||||
ctxNew := context.WithValue(context.Background(), any("key"), "value")
|
||||
configValues := map[string]string{}
|
||||
setting.CustomConf = "/tmp/any.ini"
|
||||
var actionCtx context.Context
|
||||
app := newTestApp(cli.Command{
|
||||
Before: func(context.Context, *cli.Command) (context.Context, error) {
|
||||
configValues["before"] = setting.CustomConf
|
||||
return ctxNew, nil
|
||||
},
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
configValues["action"] = setting.CustomConf
|
||||
actionCtx = ctx
|
||||
return nil
|
||||
},
|
||||
})
|
||||
_, err := runTestApp(app, "./gitea", "--config", "/dev/null", "test-cmd")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, ctxNew, actionCtx)
|
||||
assert.Equal(t, "/tmp/any.ini", configValues["before"], "BeforeFunc must be called before preparing config")
|
||||
assert.Equal(t, "/dev/null", configValues["action"])
|
||||
}
|
||||
|
||||
func TestCliCmdCompletion(t *testing.T) {
|
||||
app := newTestApp(cli.Command{
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error { return nil },
|
||||
})
|
||||
res, err := runTestApp(app, "./gitea", "completion", "bash", "--nonexist")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 1, res.ExitCode)
|
||||
assert.Equal(t, "", res.Stdout)
|
||||
assert.Equal(t, "Incorrect Usage: flag provided but not defined: -nonexist\n", res.Stderr)
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -12,16 +12,15 @@ import (
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/migrations"
|
||||
migrate_base "code.gitea.io/gitea/models/migrations/base"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/services/doctor"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/migrations"
|
||||
migrate_base "gitea.dev/models/migrations/base"
|
||||
"gitea.dev/modules/container"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/services/doctor"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func newDoctorCommand() *cli.Command {
|
||||
@@ -132,7 +131,7 @@ func runRecreateTable(ctx context.Context, cmd *cli.Command) error {
|
||||
}
|
||||
recreateTables := migrate_base.RecreateTables(beans...)
|
||||
|
||||
return db.InitEngineWithMigration(context.Background(), func(ctx context.Context, x *xorm.Engine) error {
|
||||
return db.InitEngineWithMigration(context.Background(), func(ctx context.Context, x db.EngineMigration) error {
|
||||
if err := migrations.EnsureUpToDate(ctx, x); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/services/doctor"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/services/doctor"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/urfave/cli/v3"
|
||||
|
||||
@@ -11,13 +11,13 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/dump"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/dump"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/storage"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"gitea.com/go-chi/session"
|
||||
"github.com/urfave/cli/v3"
|
||||
@@ -203,8 +203,8 @@ func runDump(ctx context.Context, cmd *cli.Command) error {
|
||||
}
|
||||
}()
|
||||
|
||||
targetDBType := cmd.String("database")
|
||||
if len(targetDBType) > 0 && targetDBType != setting.Database.Type.String() {
|
||||
targetDBType := setting.DatabaseType(cmd.String("database"))
|
||||
if targetDBType != "" && targetDBType != setting.Database.Type {
|
||||
log.Info("Dumping database %s => %s...", setting.Database.Type, targetDBType)
|
||||
} else {
|
||||
log.Info("Dumping database...")
|
||||
|
||||
@@ -10,14 +10,14 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
base "code.gitea.io/gitea/modules/migration"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
"code.gitea.io/gitea/services/migrations"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/log"
|
||||
base "gitea.dev/modules/migration"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/convert"
|
||||
"gitea.dev/services/migrations"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -11,14 +11,14 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/assetfs"
|
||||
"code.gitea.io/gitea/modules/glob"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/options"
|
||||
"code.gitea.io/gitea/modules/public"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"gitea.dev/modules/assetfs"
|
||||
"gitea.dev/modules/glob"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/options"
|
||||
"gitea.dev/modules/public"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -6,10 +6,12 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"code.gitea.io/gitea/modules/generate"
|
||||
"gitea.dev/modules/generate"
|
||||
"gitea.dev/modules/ssh"
|
||||
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/urfave/cli/v3"
|
||||
@@ -21,6 +23,7 @@ func newGenerateCommand() *cli.Command {
|
||||
Usage: "Generate Gitea's secrets/keys/tokens",
|
||||
Commands: []*cli.Command{
|
||||
newGenerateSecretCommand(),
|
||||
newGenerateSSHCommand(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -37,6 +40,17 @@ func newGenerateSecretCommand() *cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func newGenerateSSHCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "ssh",
|
||||
Usage: "Generate ssh keys",
|
||||
Commands: []*cli.Command{
|
||||
newGenerateSSHKeyCommand(),
|
||||
newGenerateSSHHostKeysCommand(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newGenerateInternalTokenCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "INTERNAL_TOKEN",
|
||||
@@ -62,6 +76,30 @@ func newGenerateSecretKeyCommand() *cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func newGenerateSSHKeyCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "key",
|
||||
Usage: "Generate a new ssh key",
|
||||
Flags: []cli.Flag{
|
||||
&cli.IntFlag{Name: "bits", Aliases: []string{"b"}, Usage: "Number of bits in the key, ignored when key is ed25519"},
|
||||
&cli.StringFlag{Name: "type", Aliases: []string{"t"}, Value: "ed25519", Usage: "Specifies the type of key to create."},
|
||||
&cli.StringFlag{Name: "file", Aliases: []string{"f"}, Usage: "Specifies the path or base directory for the key file", Required: true},
|
||||
},
|
||||
Action: runGenerateKeyPair,
|
||||
}
|
||||
}
|
||||
|
||||
func newGenerateSSHHostKeysCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "host-keys",
|
||||
Usage: "Generate host keys of all default key types (rsa, ecdsa, and ed25519) if they do not already exist.",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{Name: "dir", Aliases: []string{"d"}, Usage: "Specifies the base directory for the key files", Required: true},
|
||||
},
|
||||
Action: runGenerateHostKey,
|
||||
}
|
||||
}
|
||||
|
||||
func runGenerateInternalToken(_ context.Context, c *cli.Command) error {
|
||||
internalToken, err := generate.NewInternalToken()
|
||||
if err != nil {
|
||||
@@ -103,3 +141,41 @@ func runGenerateSecretKey(_ context.Context, c *cli.Command) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runGenerateHostKey(_ context.Context, c *cli.Command) error {
|
||||
file := c.String("dir")
|
||||
info, err := os.Stat(file)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
if err = os.MkdirAll(file, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
} else if !info.IsDir() {
|
||||
return errors.New("file already exists and is not a directory")
|
||||
}
|
||||
fmt.Fprintf(c.Writer, "Generating host keys in %s\n", file)
|
||||
_, err = ssh.InitDefaultHostKeys(file)
|
||||
return err
|
||||
}
|
||||
|
||||
func runGenerateKeyPair(_ context.Context, c *cli.Command) error {
|
||||
file := c.String("file")
|
||||
keyType := c.String("type")
|
||||
|
||||
fmt.Fprintf(c.Writer, "Generating public/private %s key pair.\n", keyType)
|
||||
|
||||
// Check if file exists to prevent overwriting
|
||||
if _, err := os.Stat(file); err == nil {
|
||||
if !confirm(c.Reader, c.Writer, "%s already exists.\nOverwrite (y/n)? ", file) {
|
||||
fmt.Println("Aborting")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
bits := c.Int("bits")
|
||||
err := ssh.GenKeyPair(file, generate.SSHKeyType(keyType), bits)
|
||||
if err == nil {
|
||||
fmt.Printf("Your SSH key has been saved in %s\n", file)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
@@ -38,22 +38,15 @@ func argsSet(c *cli.Command, args ...string) error {
|
||||
}
|
||||
|
||||
// confirm waits for user input which confirms an action
|
||||
func confirm() (bool, error) {
|
||||
func confirm(stdin io.Reader, stdout io.Writer, msg string, args ...any) bool {
|
||||
var response string
|
||||
|
||||
_, err := fmt.Scanln(&response)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(stdout, msg, args...)
|
||||
_, _ = fmt.Fscanln(stdin, &response)
|
||||
switch strings.ToLower(response) {
|
||||
case "y", "yes":
|
||||
return true, nil
|
||||
case "n", "no":
|
||||
return false, nil
|
||||
default:
|
||||
return false, errors.New(response + " isn't a correct confirmation string")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func initDB(ctx context.Context) error {
|
||||
@@ -124,7 +117,7 @@ func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(context.Context, *cl
|
||||
if setting.InstallLock {
|
||||
// During config loading, there might also be logs (for example: deprecation warnings).
|
||||
// It must make sure that console logger is set up before config is loaded.
|
||||
log.Error("Config is loaded before console logger is setup, it will cause bugs. Please fix it.")
|
||||
log.Error("Config is loaded before console logger is setup, it will cause bugs. Please fix it. CustomConf=%s", setting.CustomConf)
|
||||
return nil, errors.New("console logger must be setup before config is loaded")
|
||||
}
|
||||
level := defaultLevel
|
||||
@@ -134,7 +127,7 @@ func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(context.Context, *cl
|
||||
if globalBool(c, "debug") || globalBool(c, "verbose") {
|
||||
level = log.TRACE
|
||||
}
|
||||
log.SetConsoleLogger(log.DEFAULT, "console-default", level)
|
||||
log.SetupStderrLogger(log.DEFAULT, "console-stderr", level)
|
||||
return ctx, nil
|
||||
}
|
||||
}
|
||||
@@ -142,9 +135,9 @@ func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(context.Context, *cl
|
||||
func isValidDefaultSubCommand(cmd *cli.Command) (string, bool) {
|
||||
// Dirty patch for urfave/cli's strange design.
|
||||
// "./gitea bad-cmd" should not start the web server.
|
||||
rootArgs := cmd.Root().Args().Slice()
|
||||
if len(rootArgs) != 0 && rootArgs[0] != cmd.Name {
|
||||
return rootArgs[0], false
|
||||
args := cmd.Args().Slice()
|
||||
if len(args) != 0 {
|
||||
return args[0], false
|
||||
}
|
||||
return "", true
|
||||
}
|
||||
@@ -14,12 +14,12 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/private"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/git/gitcmd"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/private"
|
||||
repo_module "gitea.dev/modules/repository"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
@@ -151,9 +151,6 @@ func (d *delayWriter) WriteString(s string) (n int, err error) {
|
||||
}
|
||||
|
||||
func (d *delayWriter) Close() error {
|
||||
if d == nil {
|
||||
return nil
|
||||
}
|
||||
stopped := d.timer.Stop()
|
||||
if stopped || d.buf == nil {
|
||||
return nil
|
||||
@@ -163,16 +160,6 @@ func (d *delayWriter) Close() error {
|
||||
return err
|
||||
}
|
||||
|
||||
type nilWriter struct{}
|
||||
|
||||
func (n *nilWriter) Write(p []byte) (int, error) {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (n *nilWriter) WriteString(s string) (int, error) {
|
||||
return len(s), nil
|
||||
}
|
||||
|
||||
func parseGitHookCommitRefLine(line string) (oldCommitID, newCommitID string, refFullName git.RefName, ok bool) {
|
||||
fields := strings.Split(line, " ")
|
||||
if len(fields) != 3 {
|
||||
@@ -227,8 +214,7 @@ Gitea or set your environment appropriately.`, "")
|
||||
total := 0
|
||||
lastline := 0
|
||||
|
||||
var out io.Writer
|
||||
out = &nilWriter{}
|
||||
out := io.Discard
|
||||
if setting.Git.VerbosePush {
|
||||
if setting.Git.VerbosePushDelay > 0 {
|
||||
dWriter := newDelayWriter(os.Stdout, setting.Git.VerbosePushDelay)
|
||||
@@ -350,12 +336,10 @@ Gitea or set your environment appropriately.`, "")
|
||||
return nil
|
||||
}
|
||||
|
||||
var out io.Writer
|
||||
var dWriter *delayWriter
|
||||
out = &nilWriter{}
|
||||
out := io.Discard
|
||||
if setting.Git.VerbosePush {
|
||||
if setting.Git.VerbosePushDelay > 0 {
|
||||
dWriter = newDelayWriter(os.Stdout, setting.Git.VerbosePushDelay)
|
||||
dWriter := newDelayWriter(os.Stdout, setting.Git.VerbosePushDelay)
|
||||
defer dWriter.Close()
|
||||
out = dWriter
|
||||
} else {
|
||||
@@ -382,101 +366,62 @@ Gitea or set your environment appropriately.`, "")
|
||||
PushTrigger: repo_module.PushTrigger(os.Getenv(repo_module.EnvPushTrigger)),
|
||||
IsWiki: isWiki,
|
||||
}
|
||||
oldCommitIDs := make([]string, hookBatchSize)
|
||||
newCommitIDs := make([]string, hookBatchSize)
|
||||
refFullNames := make([]git.RefName, hookBatchSize)
|
||||
count := 0
|
||||
total := 0
|
||||
wasEmpty := false
|
||||
masterPushed := false
|
||||
|
||||
oldCommitIDs := make([]string, 0, hookBatchSize)
|
||||
newCommitIDs := make([]string, 0, hookBatchSize)
|
||||
refFullNames := make([]git.RefName, 0, hookBatchSize)
|
||||
results := make([]private.HookPostReceiveBranchResult, 0)
|
||||
|
||||
defer func() {
|
||||
hookPrintResults(results)
|
||||
}()
|
||||
|
||||
processBatch := func() error {
|
||||
if len(refFullNames) == 0 {
|
||||
return nil
|
||||
}
|
||||
_, _ = fmt.Fprintf(out, " Processing %d references\n", len(refFullNames))
|
||||
hookOptions.OldCommitIDs = oldCommitIDs
|
||||
hookOptions.NewCommitIDs = newCommitIDs
|
||||
hookOptions.RefFullNames = refFullNames
|
||||
resp, extra := private.HookPostReceive(ctx, repoUser, repoName, hookOptions)
|
||||
if extra.HasError() {
|
||||
return fail(ctx, extra.UserMsg, "HookPostReceive failed: %v", extra.Error)
|
||||
}
|
||||
results = append(results, resp.Results...)
|
||||
oldCommitIDs = oldCommitIDs[:0]
|
||||
newCommitIDs = newCommitIDs[:0]
|
||||
refFullNames = refFullNames[:0]
|
||||
return nil
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
for scanner.Scan() {
|
||||
// TODO: support news feeds for wiki
|
||||
// wiki doesn't need "post-receive" at the moment
|
||||
if isWiki {
|
||||
continue
|
||||
}
|
||||
|
||||
var ok bool
|
||||
oldCommitIDs[count], newCommitIDs[count], refFullNames[count], ok = parseGitHookCommitRefLine(scanner.Text())
|
||||
oldCommitID, newCommitID, refFullName, ok := parseGitHookCommitRefLine(scanner.Text())
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
_, _ = fmt.Fprintf(out, ".")
|
||||
|
||||
fmt.Fprintf(out, ".")
|
||||
commitID, _ := git.NewIDFromString(newCommitIDs[count])
|
||||
if refFullNames[count] == git.BranchPrefix+"master" && !commitID.IsZero() && count == total {
|
||||
masterPushed = true
|
||||
}
|
||||
count++
|
||||
total++
|
||||
|
||||
if count >= hookBatchSize {
|
||||
fmt.Fprintf(out, " Processing %d references\n", count)
|
||||
hookOptions.OldCommitIDs = oldCommitIDs
|
||||
hookOptions.NewCommitIDs = newCommitIDs
|
||||
hookOptions.RefFullNames = refFullNames
|
||||
resp, extra := private.HookPostReceive(ctx, repoUser, repoName, hookOptions)
|
||||
if extra.HasError() {
|
||||
_ = dWriter.Close()
|
||||
hookPrintResults(results)
|
||||
return fail(ctx, extra.UserMsg, "HookPostReceive failed: %v", extra.Error)
|
||||
oldCommitIDs = append(oldCommitIDs, oldCommitID)
|
||||
newCommitIDs = append(newCommitIDs, newCommitID)
|
||||
refFullNames = append(refFullNames, refFullName)
|
||||
if len(refFullNames) >= hookBatchSize {
|
||||
// process and start a new batch
|
||||
if err := processBatch(); err != nil {
|
||||
return err
|
||||
}
|
||||
wasEmpty = wasEmpty || resp.RepoWasEmpty
|
||||
results = append(results, resp.Results...)
|
||||
count = 0
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
_ = dWriter.Close()
|
||||
hookPrintResults(results)
|
||||
return fail(ctx, "Hook failed: stdin read error", "scanner error: %v", err)
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
if wasEmpty && masterPushed {
|
||||
// We need to tell the repo to reset the default branch to master
|
||||
extra := private.SetDefaultBranch(ctx, repoUser, repoName, "master")
|
||||
if extra.HasError() {
|
||||
return fail(ctx, extra.UserMsg, "SetDefaultBranch failed: %v", extra.Error)
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(out, "Processed %d references in total\n", total)
|
||||
|
||||
_ = dWriter.Close()
|
||||
hookPrintResults(results)
|
||||
return nil
|
||||
}
|
||||
|
||||
hookOptions.OldCommitIDs = oldCommitIDs[:count]
|
||||
hookOptions.NewCommitIDs = newCommitIDs[:count]
|
||||
hookOptions.RefFullNames = refFullNames[:count]
|
||||
|
||||
fmt.Fprintf(out, " Processing %d references\n", count)
|
||||
|
||||
resp, extra := private.HookPostReceive(ctx, repoUser, repoName, hookOptions)
|
||||
if resp == nil {
|
||||
_ = dWriter.Close()
|
||||
hookPrintResults(results)
|
||||
return fail(ctx, extra.UserMsg, "HookPostReceive failed: %v", extra.Error)
|
||||
}
|
||||
wasEmpty = wasEmpty || resp.RepoWasEmpty
|
||||
results = append(results, resp.Results...)
|
||||
|
||||
fmt.Fprintf(out, "Processed %d references in total\n", total)
|
||||
|
||||
if wasEmpty && masterPushed {
|
||||
// We need to tell the repo to reset the default branch to master
|
||||
extra := private.SetDefaultBranch(ctx, repoUser, repoName, "master")
|
||||
if extra.HasError() {
|
||||
return fail(ctx, extra.UserMsg, "SetDefaultBranch failed: %v", extra.Error)
|
||||
}
|
||||
}
|
||||
_ = dWriter.Close()
|
||||
hookPrintResults(results)
|
||||
|
||||
return nil
|
||||
return processBatch()
|
||||
}
|
||||
|
||||
func hookPrintResults(results []private.HookPostReceiveBranchResult) {
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/private"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/private"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/modules/private"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"gitea.dev/modules/private"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
@@ -22,14 +22,10 @@ func runSendMail(ctx context.Context, c *cli.Command) error {
|
||||
|
||||
if !confirmSkipped {
|
||||
if len(body) == 0 {
|
||||
fmt.Print("warning: Content is empty")
|
||||
fmt.Println("warning: Content is empty")
|
||||
}
|
||||
|
||||
fmt.Print("Proceed with sending email? [Y/n] ")
|
||||
isConfirmed, err := confirm()
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !isConfirmed {
|
||||
if !confirm(c.Reader, c.Writer, "Proceed with sending email? [Y/n] ") {
|
||||
fmt.Println("The mail was not sent")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5,13 +5,13 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
@@ -48,7 +48,7 @@ DEFAULT CONFIGURATION:
|
||||
}
|
||||
}
|
||||
|
||||
func prepareSubcommandWithGlobalFlags(originCmd *cli.Command) {
|
||||
func PrepareSubcommandWithGlobalFlags(originCmd *cli.Command) {
|
||||
originBefore := originCmd.Before
|
||||
originCmd.Before = func(ctxOrig context.Context, cmd *cli.Command) (ctx context.Context, err error) {
|
||||
ctx = ctxOrig
|
||||
@@ -145,7 +145,7 @@ func NewMainApp(appVer AppVersion) *cli.Command {
|
||||
|
||||
app.Before = PrepareConsoleLoggerLevel(log.INFO)
|
||||
for i := range subCmdWithConfig {
|
||||
prepareSubcommandWithGlobalFlags(subCmdWithConfig[i])
|
||||
PrepareSubcommandWithGlobalFlags(subCmdWithConfig[i])
|
||||
}
|
||||
app.Commands = append(app.Commands, subCmdWithConfig...)
|
||||
app.Commands = append(app.Commands, subCmdStandalone...)
|
||||
@@ -154,16 +154,37 @@ func NewMainApp(appVer AppVersion) *cli.Command {
|
||||
return app
|
||||
}
|
||||
|
||||
// usageErr marks a usage error already reported by cliOnUsageError, so RunMainApp does not print it again.
|
||||
type usageErr struct{ err error }
|
||||
|
||||
func (e usageErr) Error() string { return e.err.Error() }
|
||||
func (e usageErr) Unwrap() error { return e.err }
|
||||
|
||||
// cliOnUsageError reports usage errors itself instead of letting urfave/cli dump the full help to stdout (since urfave/cli v3.10).
|
||||
func cliOnUsageError(_ context.Context, cmd *cli.Command, err error, _ bool) error {
|
||||
_, _ = fmt.Fprintf(cmd.Root().ErrWriter, "Incorrect Usage: %s\n", err.Error())
|
||||
return usageErr{err}
|
||||
}
|
||||
|
||||
func setCLIOnUsageError(cmd *cli.Command) {
|
||||
_ = cmd.Walk(func(c *cli.Command) error {
|
||||
c.OnUsageError = cliOnUsageError
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func RunMainApp(app *cli.Command, args ...string) error {
|
||||
ctx, cancel := installSignals()
|
||||
defer cancel()
|
||||
setCLIOnUsageError(app)
|
||||
// the completion subcommands are built during app.Run, after the Walk above, so cover them via this hook
|
||||
app.ConfigureShellCompletionCommand = setCLIOnUsageError
|
||||
err := app.Run(ctx, args)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if strings.HasPrefix(err.Error(), "flag provided but not defined:") {
|
||||
// the cli package should already have output the error message, so just exit
|
||||
cli.OsExiter(1)
|
||||
if _, ok := errors.AsType[usageErr](err); ok {
|
||||
cli.OsExiter(1) // cliOnUsageError already reported it
|
||||
return err
|
||||
}
|
||||
_, _ = fmt.Fprintf(app.ErrWriter, "Command error: %v\n", err)
|
||||
|
||||
@@ -5,17 +5,9 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"gitea.dev/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/urfave/cli/v3"
|
||||
@@ -25,209 +17,29 @@ func TestMain(m *testing.M) {
|
||||
unittest.MainTest(m)
|
||||
}
|
||||
|
||||
func makePathOutput(workPath, customPath, customConf string) string {
|
||||
return fmt.Sprintf("WorkPath=%s\nCustomPath=%s\nCustomConf=%s", workPath, customPath, customConf)
|
||||
}
|
||||
|
||||
func newTestApp(testCmd cli.Command) *cli.Command {
|
||||
app := NewMainApp(AppVersion{})
|
||||
testCmd.Name = util.IfZero(testCmd.Name, "test-cmd")
|
||||
prepareSubcommandWithGlobalFlags(&testCmd)
|
||||
app.Commands = append(app.Commands, &testCmd)
|
||||
app.DefaultCommand = testCmd.Name
|
||||
return app
|
||||
}
|
||||
|
||||
type runResult struct {
|
||||
Stdout string
|
||||
Stderr string
|
||||
ExitCode int
|
||||
}
|
||||
|
||||
func runTestApp(app *cli.Command, args ...string) (runResult, error) {
|
||||
outBuf := new(strings.Builder)
|
||||
errBuf := new(strings.Builder)
|
||||
app.Writer = outBuf
|
||||
app.ErrWriter = errBuf
|
||||
exitCode := -1
|
||||
defer test.MockVariableValue(&cli.ErrWriter, app.ErrWriter)()
|
||||
defer test.MockVariableValue(&cli.OsExiter, func(code int) {
|
||||
if exitCode == -1 {
|
||||
exitCode = code // save the exit code once and then reset the writer (to simulate the exit)
|
||||
app.Writer, app.ErrWriter, cli.ErrWriter = io.Discard, io.Discard, io.Discard
|
||||
}
|
||||
})()
|
||||
err := RunMainApp(app, args...)
|
||||
return runResult{outBuf.String(), errBuf.String(), exitCode}, err
|
||||
}
|
||||
|
||||
func TestCliCmd(t *testing.T) {
|
||||
defaultWorkPath := filepath.Dir(setting.AppPath)
|
||||
defaultCustomPath := filepath.Join(defaultWorkPath, "custom")
|
||||
defaultCustomConf := filepath.Join(defaultCustomPath, "conf/app.ini")
|
||||
|
||||
cli.CommandHelpTemplate = "(command help template)"
|
||||
cli.RootCommandHelpTemplate = "(app help template)"
|
||||
cli.SubcommandHelpTemplate = "(subcommand help template)"
|
||||
|
||||
cases := []struct {
|
||||
env map[string]string
|
||||
cmd string
|
||||
exp string
|
||||
}{
|
||||
// help commands
|
||||
{
|
||||
cmd: "./gitea -h",
|
||||
exp: "DEFAULT CONFIGURATION:",
|
||||
},
|
||||
{
|
||||
cmd: "./gitea help",
|
||||
exp: "DEFAULT CONFIGURATION:",
|
||||
},
|
||||
|
||||
{
|
||||
cmd: "./gitea -c /dev/null -h",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
|
||||
{
|
||||
cmd: "./gitea -c /dev/null help",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
{
|
||||
cmd: "./gitea help -c /dev/null",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
|
||||
{
|
||||
cmd: "./gitea -c /dev/null test-cmd -h",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
{
|
||||
cmd: "./gitea test-cmd -c /dev/null -h",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
{
|
||||
cmd: "./gitea test-cmd -h -c /dev/null",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
|
||||
{
|
||||
cmd: "./gitea -c /dev/null test-cmd help",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
{
|
||||
cmd: "./gitea test-cmd -c /dev/null help",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
{
|
||||
cmd: "./gitea test-cmd help -c /dev/null",
|
||||
exp: "ConfigFile: /dev/null",
|
||||
},
|
||||
|
||||
// parse paths
|
||||
{
|
||||
cmd: "./gitea test-cmd",
|
||||
exp: makePathOutput(defaultWorkPath, defaultCustomPath, defaultCustomConf),
|
||||
},
|
||||
{
|
||||
cmd: "./gitea -c /tmp/app.ini test-cmd",
|
||||
exp: makePathOutput(defaultWorkPath, defaultCustomPath, "/tmp/app.ini"),
|
||||
},
|
||||
{
|
||||
cmd: "./gitea test-cmd -c /tmp/app.ini",
|
||||
exp: makePathOutput(defaultWorkPath, defaultCustomPath, "/tmp/app.ini"),
|
||||
},
|
||||
{
|
||||
env: map[string]string{"GITEA_WORK_DIR": "/tmp"},
|
||||
cmd: "./gitea test-cmd",
|
||||
exp: makePathOutput("/tmp", "/tmp/custom", "/tmp/custom/conf/app.ini"),
|
||||
},
|
||||
{
|
||||
env: map[string]string{"GITEA_WORK_DIR": "/tmp"},
|
||||
cmd: "./gitea test-cmd --work-path /tmp/other",
|
||||
exp: makePathOutput("/tmp/other", "/tmp/other/custom", "/tmp/other/custom/conf/app.ini"),
|
||||
},
|
||||
{
|
||||
env: map[string]string{"GITEA_WORK_DIR": "/tmp"},
|
||||
cmd: "./gitea test-cmd --config /tmp/app-other.ini",
|
||||
exp: makePathOutput("/tmp", "/tmp/custom", "/tmp/app-other.ini"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.cmd, func(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.InstallLock, false)()
|
||||
app := newTestApp(cli.Command{
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
_, _ = fmt.Fprint(cmd.Root().Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf))
|
||||
return nil
|
||||
func TestDefaultCommand(t *testing.T) {
|
||||
test := func(t *testing.T, args []string, expectedRetName string, expectedRetValid bool) {
|
||||
called := false
|
||||
cmd := &cli.Command{
|
||||
DefaultCommand: "test",
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "test",
|
||||
Action: func(ctx context.Context, command *cli.Command) error {
|
||||
retName, retValid := isValidDefaultSubCommand(command)
|
||||
assert.Equal(t, expectedRetName, retName)
|
||||
assert.Equal(t, expectedRetValid, retValid)
|
||||
called = true
|
||||
return nil
|
||||
},
|
||||
},
|
||||
})
|
||||
for k, v := range c.env {
|
||||
t.Setenv(k, v)
|
||||
}
|
||||
args := strings.Split(c.cmd, " ") // for test only, "split" is good enough
|
||||
r, err := runTestApp(app, args...)
|
||||
assert.NoError(t, err, c.cmd)
|
||||
assert.NotEmpty(t, c.exp, c.cmd)
|
||||
if !assert.Contains(t, r.Stdout, c.exp, c.cmd) {
|
||||
t.Log("Full output:\n" + r.Stdout)
|
||||
t.Log("Expected:\n" + c.exp)
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
assert.NoError(t, cmd.Run(t.Context(), args))
|
||||
assert.True(t, called)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCliCmdError(t *testing.T) {
|
||||
app := newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return errors.New("normal error") }})
|
||||
r, err := runTestApp(app, "./gitea", "test-cmd")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 1, r.ExitCode)
|
||||
assert.Empty(t, r.Stdout)
|
||||
assert.Equal(t, "Command error: normal error\n", r.Stderr)
|
||||
|
||||
app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return cli.Exit("exit error", 2) }})
|
||||
r, err = runTestApp(app, "./gitea", "test-cmd")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 2, r.ExitCode)
|
||||
assert.Empty(t, r.Stdout)
|
||||
assert.Equal(t, "exit error\n", r.Stderr)
|
||||
|
||||
app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return nil }})
|
||||
r, err = runTestApp(app, "./gitea", "test-cmd", "--no-such")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 1, r.ExitCode)
|
||||
assert.Empty(t, r.Stdout)
|
||||
assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n\n", r.Stderr)
|
||||
|
||||
app = newTestApp(cli.Command{Action: func(ctx context.Context, cmd *cli.Command) error { return nil }})
|
||||
r, err = runTestApp(app, "./gitea", "test-cmd")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, -1, r.ExitCode) // the cli.OsExiter is not called
|
||||
assert.Empty(t, r.Stdout)
|
||||
assert.Empty(t, r.Stderr)
|
||||
}
|
||||
|
||||
func TestCliCmdBefore(t *testing.T) {
|
||||
ctxNew := context.WithValue(context.Background(), any("key"), "value")
|
||||
configValues := map[string]string{}
|
||||
setting.CustomConf = "/tmp/any.ini"
|
||||
var actionCtx context.Context
|
||||
app := newTestApp(cli.Command{
|
||||
Before: func(context.Context, *cli.Command) (context.Context, error) {
|
||||
configValues["before"] = setting.CustomConf
|
||||
return ctxNew, nil
|
||||
},
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
configValues["action"] = setting.CustomConf
|
||||
actionCtx = ctx
|
||||
return nil
|
||||
},
|
||||
})
|
||||
_, err := runTestApp(app, "./gitea", "--config", "/dev/null", "test-cmd")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, ctxNew, actionCtx)
|
||||
assert.Equal(t, "/tmp/any.ini", configValues["before"], "BeforeFunc must be called before preparing config")
|
||||
assert.Equal(t, "/dev/null", configValues["action"])
|
||||
test(t, []string{"./gitea"}, "", true)
|
||||
test(t, []string{"./gitea", "test"}, "", true)
|
||||
test(t, []string{"./gitea", "other"}, "other", false)
|
||||
test(t, []string{"./gitea", "test", "extra"}, "extra", false)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/private"
|
||||
"gitea.dev/modules/private"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -5,60 +5,14 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/private"
|
||||
"gitea.dev/modules/private"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func defaultLoggingFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "logger",
|
||||
Usage: `Logger name - will default to "default"`,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "writer",
|
||||
Usage: "Name of the log writer - will default to mode",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "level",
|
||||
Usage: "Logging level for the new logger",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "stacktrace-level",
|
||||
Aliases: []string{"L"},
|
||||
Usage: "Stacktrace logging level",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "flags",
|
||||
Aliases: []string{"F"},
|
||||
Usage: "Flags for the logger",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "expression",
|
||||
Aliases: []string{"e"},
|
||||
Usage: "Matching expression for the logger",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "prefix",
|
||||
Aliases: []string{"p"},
|
||||
Usage: "Prefix for the logger",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "color",
|
||||
Usage: "Use color in the logs",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "debug",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newLoggingCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "logging",
|
||||
@@ -91,92 +45,6 @@ func newLoggingCommand() *cli.Command {
|
||||
},
|
||||
},
|
||||
Action: runReleaseReopenLogging,
|
||||
}, {
|
||||
Name: "remove",
|
||||
Usage: "Remove a logger",
|
||||
ArgsUsage: "[name] Name of logger to remove",
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "debug",
|
||||
}, &cli.StringFlag{
|
||||
Name: "logger",
|
||||
Usage: `Logger name - will default to "default"`,
|
||||
},
|
||||
},
|
||||
Action: runRemoveLogger,
|
||||
}, {
|
||||
Name: "add",
|
||||
Usage: "Add a logger",
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "file",
|
||||
Usage: "Add a file logger",
|
||||
Flags: append(defaultLoggingFlags(), []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "filename",
|
||||
Aliases: []string{"f"},
|
||||
Usage: "Filename for the logger - this must be set.",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "rotate",
|
||||
Aliases: []string{"r"},
|
||||
Usage: "Rotate logs",
|
||||
},
|
||||
&cli.Int64Flag{
|
||||
Name: "max-size",
|
||||
Aliases: []string{"s"},
|
||||
Usage: "Maximum size in bytes before rotation",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "daily",
|
||||
Aliases: []string{"d"},
|
||||
Usage: "Rotate logs daily",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "max-days",
|
||||
Aliases: []string{"D"},
|
||||
Usage: "Maximum number of daily logs to keep",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "compress",
|
||||
Aliases: []string{"z"},
|
||||
Usage: "Compress rotated logs",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "compression-level",
|
||||
Aliases: []string{"Z"},
|
||||
Usage: "Compression level to use",
|
||||
},
|
||||
}...),
|
||||
Action: runAddFileLogger,
|
||||
}, {
|
||||
Name: "conn",
|
||||
Usage: "Add a net conn logger",
|
||||
Flags: append(defaultLoggingFlags(), []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "reconnect-on-message",
|
||||
Aliases: []string{"R"},
|
||||
Usage: "Reconnect to host for every message",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "reconnect",
|
||||
Aliases: []string{"r"},
|
||||
Usage: "Reconnect to host when connection is dropped",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "protocol",
|
||||
Aliases: []string{"P"},
|
||||
Usage: "Set protocol to use: tcp, unix, or udp (defaults to tcp)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "address",
|
||||
Aliases: []string{"a"},
|
||||
Usage: "Host address and port to connect to (defaults to :7020)",
|
||||
},
|
||||
}...),
|
||||
Action: runAddConnLogger,
|
||||
},
|
||||
},
|
||||
}, {
|
||||
Name: "log-sql",
|
||||
Usage: "Set LogSQL",
|
||||
@@ -195,107 +63,6 @@ func newLoggingCommand() *cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func runRemoveLogger(ctx context.Context, c *cli.Command) error {
|
||||
setup(ctx, c.Bool("debug"))
|
||||
logger := c.String("logger")
|
||||
if len(logger) == 0 {
|
||||
logger = log.DEFAULT
|
||||
}
|
||||
writer := c.Args().First()
|
||||
|
||||
extra := private.RemoveLogger(ctx, logger, writer)
|
||||
return handleCliResponseExtra(extra)
|
||||
}
|
||||
|
||||
func runAddConnLogger(ctx context.Context, c *cli.Command) error {
|
||||
setup(ctx, c.Bool("debug"))
|
||||
vals := map[string]any{}
|
||||
mode := "conn"
|
||||
vals["net"] = "tcp"
|
||||
if c.IsSet("protocol") {
|
||||
switch c.String("protocol") {
|
||||
case "udp":
|
||||
vals["net"] = "udp"
|
||||
case "unix":
|
||||
vals["net"] = "unix"
|
||||
}
|
||||
}
|
||||
if c.IsSet("address") {
|
||||
vals["address"] = c.String("address")
|
||||
} else {
|
||||
vals["address"] = ":7020"
|
||||
}
|
||||
if c.IsSet("reconnect") {
|
||||
vals["reconnect"] = c.Bool("reconnect")
|
||||
}
|
||||
if c.IsSet("reconnect-on-message") {
|
||||
vals["reconnectOnMsg"] = c.Bool("reconnect-on-message")
|
||||
}
|
||||
return commonAddLogger(ctx, c, mode, vals)
|
||||
}
|
||||
|
||||
func runAddFileLogger(ctx context.Context, c *cli.Command) error {
|
||||
setup(ctx, c.Bool("debug"))
|
||||
vals := map[string]any{}
|
||||
mode := "file"
|
||||
if c.IsSet("filename") {
|
||||
vals["filename"] = c.String("filename")
|
||||
} else {
|
||||
return errors.New("filename must be set when creating a file logger")
|
||||
}
|
||||
if c.IsSet("rotate") {
|
||||
vals["rotate"] = c.Bool("rotate")
|
||||
}
|
||||
if c.IsSet("max-size") {
|
||||
vals["maxsize"] = c.Int64("max-size")
|
||||
}
|
||||
if c.IsSet("daily") {
|
||||
vals["daily"] = c.Bool("daily")
|
||||
}
|
||||
if c.IsSet("max-days") {
|
||||
vals["maxdays"] = c.Int("max-days")
|
||||
}
|
||||
if c.IsSet("compress") {
|
||||
vals["compress"] = c.Bool("compress")
|
||||
}
|
||||
if c.IsSet("compression-level") {
|
||||
vals["compressionLevel"] = c.Int("compression-level")
|
||||
}
|
||||
return commonAddLogger(ctx, c, mode, vals)
|
||||
}
|
||||
|
||||
func commonAddLogger(ctx context.Context, c *cli.Command, mode string, vals map[string]any) error {
|
||||
if len(c.String("level")) > 0 {
|
||||
vals["level"] = log.LevelFromString(c.String("level")).String()
|
||||
}
|
||||
if len(c.String("stacktrace-level")) > 0 {
|
||||
vals["stacktraceLevel"] = log.LevelFromString(c.String("stacktrace-level")).String()
|
||||
}
|
||||
if len(c.String("expression")) > 0 {
|
||||
vals["expression"] = c.String("expression")
|
||||
}
|
||||
if len(c.String("prefix")) > 0 {
|
||||
vals["prefix"] = c.String("prefix")
|
||||
}
|
||||
if len(c.String("flags")) > 0 {
|
||||
vals["flags"] = log.FlagsFromString(c.String("flags"))
|
||||
}
|
||||
if c.IsSet("color") {
|
||||
vals["colorize"] = c.Bool("color")
|
||||
}
|
||||
logger := log.DEFAULT
|
||||
if c.IsSet("logger") {
|
||||
logger = c.String("logger")
|
||||
}
|
||||
writer := mode
|
||||
if c.IsSet("writer") {
|
||||
writer = c.String("writer")
|
||||
}
|
||||
|
||||
extra := private.AddLogger(ctx, logger, writer, mode, vals)
|
||||
return handleCliResponseExtra(extra)
|
||||
}
|
||||
|
||||
func runPauseLogging(ctx context.Context, c *cli.Command) error {
|
||||
setup(ctx, c.Bool("debug"))
|
||||
userMsg := private.PauseLogging(ctx)
|
||||
|
||||
@@ -6,10 +6,10 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/services/versioned_migration"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/services/versioned_migration"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -10,17 +10,17 @@ import (
|
||||
"io/fs"
|
||||
"strings"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/services/versioned_migration"
|
||||
actions_model "gitea.dev/models/actions"
|
||||
"gitea.dev/models/db"
|
||||
git_model "gitea.dev/models/git"
|
||||
packages_model "gitea.dev/models/packages"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/log"
|
||||
packages_module "gitea.dev/modules/packages"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/storage"
|
||||
"gitea.dev/services/versioned_migration"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -8,13 +8,13 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
"gitea.dev/models/packages"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
packages_module "gitea.dev/modules/packages"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/storage"
|
||||
packages_service "gitea.dev/services/packages"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/private"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"gitea.dev/modules/private"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@@ -15,21 +15,21 @@ import (
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/lfstransfer"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/pprof"
|
||||
"code.gitea.io/gitea/modules/private"
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/services/lfs"
|
||||
asymkey_model "gitea.dev/models/asymkey"
|
||||
git_model "gitea.dev/models/git"
|
||||
"gitea.dev/models/perm"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/git/gitcmd"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/lfstransfer"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/pprof"
|
||||
"gitea.dev/modules/private"
|
||||
"gitea.dev/modules/process"
|
||||
repo_module "gitea.dev/modules/repository"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/services/lfs"
|
||||
|
||||
"github.com/kballard/go-shellquote"
|
||||
"github.com/urfave/cli/v3"
|
||||
@@ -113,23 +113,25 @@ func handleCliResponseExtra(extra private.ResponseExtra) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getAccessMode(verb, lfsVerb string) perm.AccessMode {
|
||||
// getAccessMode maps an SSH git/LFS verb to the access mode it requires, with
|
||||
// ok=false for an unrecognised verb. Callers MUST reject the request when ok is
|
||||
// false: AccessModeNone would otherwise pass the `userMode < mode` permission
|
||||
// check in routers/private/serv.go and grant access.
|
||||
func getAccessMode(verb, lfsVerb string) (mode perm.AccessMode, ok bool) {
|
||||
switch verb {
|
||||
case git.CmdVerbUploadPack, git.CmdVerbUploadArchive:
|
||||
return perm.AccessModeRead
|
||||
return perm.AccessModeRead, true
|
||||
case git.CmdVerbReceivePack:
|
||||
return perm.AccessModeWrite
|
||||
return perm.AccessModeWrite, true
|
||||
case git.CmdVerbLfsAuthenticate, git.CmdVerbLfsTransfer:
|
||||
switch lfsVerb {
|
||||
case git.CmdSubVerbLfsUpload:
|
||||
return perm.AccessModeWrite
|
||||
return perm.AccessModeWrite, true
|
||||
case git.CmdSubVerbLfsDownload:
|
||||
return perm.AccessModeRead
|
||||
return perm.AccessModeRead, true
|
||||
}
|
||||
}
|
||||
// should be unreachable
|
||||
setting.PanicInDevOrTesting("unknown verb: %s %s", verb, lfsVerb)
|
||||
return perm.AccessModeNone
|
||||
return perm.AccessModeNone, false
|
||||
}
|
||||
|
||||
func runServ(ctx context.Context, c *cli.Command) error {
|
||||
@@ -247,7 +249,10 @@ func runServ(ctx context.Context, c *cli.Command) error {
|
||||
}
|
||||
}
|
||||
|
||||
requestedMode := getAccessMode(verb, lfsVerb)
|
||||
requestedMode, ok := getAccessMode(verb, lfsVerb)
|
||||
if !ok {
|
||||
return fail(ctx, "Unknown git command", "Unknown git command %s %s", verb, lfsVerb)
|
||||
}
|
||||
|
||||
results, extra := private.ServCommand(ctx, keyID, username, reponame, requestedMode, verb, lfsVerb)
|
||||
if extra.HasError() {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dev/models/perm"
|
||||
"gitea.dev/modules/git"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetAccessMode(t *testing.T) {
|
||||
cases := []struct {
|
||||
verb, lfsVerb string
|
||||
expected perm.AccessMode
|
||||
}{
|
||||
{git.CmdVerbUploadPack, "", perm.AccessModeRead},
|
||||
{git.CmdVerbUploadArchive, "", perm.AccessModeRead},
|
||||
{git.CmdVerbReceivePack, "", perm.AccessModeWrite},
|
||||
{git.CmdVerbLfsAuthenticate, git.CmdSubVerbLfsUpload, perm.AccessModeWrite},
|
||||
{git.CmdVerbLfsAuthenticate, git.CmdSubVerbLfsDownload, perm.AccessModeRead},
|
||||
{git.CmdVerbLfsTransfer, git.CmdSubVerbLfsUpload, perm.AccessModeWrite},
|
||||
{git.CmdVerbLfsTransfer, git.CmdSubVerbLfsDownload, perm.AccessModeRead},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.verb+"/"+tc.lfsVerb, func(t *testing.T) {
|
||||
mode, ok := getAccessMode(tc.verb, tc.lfsVerb)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, tc.expected, mode)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetAccessModeUnknownVerb locks in the invariant that getAccessMode reports
|
||||
// ok=false for unrecognised verbs and LFS sub-verbs, so runServ rejects them. An
|
||||
// unknown verb has no valid access mode; if it were treated as AccessModeNone (0)
|
||||
// it would pass the `userMode < mode` permission check in routers/private/serv.go
|
||||
// and hand out valid LFS JWTs for any private repository.
|
||||
func TestGetAccessModeUnknownVerb(t *testing.T) {
|
||||
cases := []struct{ verb, lfsVerb string }{
|
||||
{git.CmdVerbLfsAuthenticate, ""},
|
||||
{git.CmdVerbLfsAuthenticate, "badverb"},
|
||||
{git.CmdVerbLfsTransfer, "badverb"},
|
||||
{"git-unknown-verb", ""},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.verb+"/"+tc.lfsVerb, func(t *testing.T) {
|
||||
mode, ok := getAccessMode(tc.verb, tc.lfsVerb)
|
||||
assert.False(t, ok)
|
||||
assert.Equal(t, perm.AccessModeNone, mode)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -15,17 +15,17 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/gtprof"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
"code.gitea.io/gitea/modules/public"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers"
|
||||
"code.gitea.io/gitea/routers/install"
|
||||
"gitea.dev/modules/container"
|
||||
"gitea.dev/modules/graceful"
|
||||
"gitea.dev/modules/gtprof"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/process"
|
||||
"gitea.dev/modules/public"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/routers"
|
||||
"gitea.dev/routers/install"
|
||||
|
||||
"github.com/felixge/fgprof"
|
||||
"github.com/urfave/cli/v3"
|
||||
@@ -150,7 +150,7 @@ func serveInstall(cmd *cli.Command) error {
|
||||
c := install.Routes()
|
||||
err := listen(c, false)
|
||||
if err != nil {
|
||||
log.Critical("Unable to open listener for installer. Is Gitea already running?")
|
||||
log.Error("Unable to open listener for installer. Is Gitea already running?")
|
||||
graceful.GetManager().DoGracefulShutdown()
|
||||
}
|
||||
select {
|
||||
@@ -375,7 +375,7 @@ func listen(m http.Handler, handleRedirector bool) error {
|
||||
log.Fatal("Invalid protocol: %s", setting.Protocol)
|
||||
}
|
||||
if err != nil {
|
||||
log.Critical("Failed to start server: %v", err)
|
||||
log.Error("Failed to start server: %v", err)
|
||||
}
|
||||
log.Info("HTTP Listener: %s Closed", listenAddr)
|
||||
return err
|
||||
|
||||
@@ -6,6 +6,7 @@ package cmd
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -13,11 +14,11 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"gitea.dev/modules/graceful"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/process"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/util"
|
||||
|
||||
"github.com/caddyserver/certmagic"
|
||||
)
|
||||
@@ -93,10 +94,21 @@ func runACME(listenAddr string, m http.Handler) error {
|
||||
myACME := certmagic.NewACMEIssuer(magic, certmagic.DefaultACME)
|
||||
magic.Issuers = []certmagic.Issuer{myACME}
|
||||
|
||||
// this obtains certificates or renews them if necessary
|
||||
err := magic.ManageSync(graceful.GetManager().HammerContext(), []string{setting.Domain})
|
||||
// Obtain certificates or renew them if necessary. ManageSync fails closed on
|
||||
// renewal errors even when a still-valid certificate is already on disk, which
|
||||
// takes HTTPS down on restart (https://github.com/go-gitea/gitea/issues/38519).
|
||||
// Prefer keeping the existing cert and retrying renewals asynchronously.
|
||||
ctx := graceful.GetManager().ShutdownContext()
|
||||
err := magic.ManageSync(ctx, []string{setting.Domain})
|
||||
if err != nil {
|
||||
return err
|
||||
cert, cacheErr := magic.CacheManagedCertificate(ctx, setting.Domain)
|
||||
if cacheErr != nil || cert.Expired() {
|
||||
return errors.Join(err, cacheErr)
|
||||
}
|
||||
log.Error("ACME certificate manage failed; continuing with existing certificate: %v", err)
|
||||
if err := magic.ManageAsync(ctx, []string{setting.Domain}); err != nil {
|
||||
log.Error("Failed to start async ACME management: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
tlsConfig := magic.TLSConfig()
|
||||
|
||||
@@ -9,9 +9,9 @@ import (
|
||||
"net/http/fcgi"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"gitea.dev/modules/graceful"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
)
|
||||
|
||||
func runHTTP(network, listenAddr, name string, m http.Handler, useProxyProtocol bool) error {
|
||||
|
||||
@@ -9,9 +9,9 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"gitea.dev/modules/graceful"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"github.com/klauspost/cpuid/v2"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user