Migrate to urfave v3 (#34510)

migrate cli to urfave v3

add more cli tests

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
pull/34680/head^2
TheFox0x7 2025-06-10 14:35:12 +02:00 committed by GitHub
parent 2c341b6803
commit e9f5105e95
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
51 changed files with 1718 additions and 783 deletions

File diff suppressed because one or more lines are too long

View File

@ -4,12 +4,13 @@
package cmd package cmd
import ( import (
"context"
"fmt" "fmt"
"code.gitea.io/gitea/modules/private" "code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
var ( var (
@ -17,7 +18,7 @@ var (
CmdActions = &cli.Command{ CmdActions = &cli.Command{
Name: "actions", Name: "actions",
Usage: "Manage Gitea Actions", Usage: "Manage Gitea Actions",
Subcommands: []*cli.Command{ Commands: []*cli.Command{
subcmdActionsGenRunnerToken, subcmdActionsGenRunnerToken,
}, },
} }
@ -38,10 +39,7 @@ var (
} }
) )
func runGenerateActionsRunnerToken(c *cli.Context) error { func runGenerateActionsRunnerToken(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals()
defer cancel()
setting.MustInstalled() setting.MustInstalled()
scope := c.String("scope") scope := c.String("scope")

View File

@ -15,7 +15,7 @@ import (
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository" repo_module "code.gitea.io/gitea/modules/repository"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
var ( var (
@ -23,7 +23,7 @@ var (
CmdAdmin = &cli.Command{ CmdAdmin = &cli.Command{
Name: "admin", Name: "admin",
Usage: "Perform common administrative operations", Usage: "Perform common administrative operations",
Subcommands: []*cli.Command{ Commands: []*cli.Command{
subcmdUser, subcmdUser,
subcmdRepoSyncReleases, subcmdRepoSyncReleases,
subcmdRegenerate, subcmdRegenerate,
@ -41,7 +41,7 @@ var (
subcmdRegenerate = &cli.Command{ subcmdRegenerate = &cli.Command{
Name: "regenerate", Name: "regenerate",
Usage: "Regenerate specific files", Usage: "Regenerate specific files",
Subcommands: []*cli.Command{ Commands: []*cli.Command{
microcmdRegenHooks, microcmdRegenHooks,
microcmdRegenKeys, microcmdRegenKeys,
}, },
@ -50,15 +50,15 @@ var (
subcmdAuth = &cli.Command{ subcmdAuth = &cli.Command{
Name: "auth", Name: "auth",
Usage: "Modify external auth providers", Usage: "Modify external auth providers",
Subcommands: []*cli.Command{ Commands: []*cli.Command{
microcmdAuthAddOauth, microcmdAuthAddOauth(),
microcmdAuthUpdateOauth, microcmdAuthUpdateOauth(),
microcmdAuthAddLdapBindDn, microcmdAuthAddLdapBindDn(),
microcmdAuthUpdateLdapBindDn, microcmdAuthUpdateLdapBindDn(),
microcmdAuthAddLdapSimpleAuth, microcmdAuthAddLdapSimpleAuth(),
microcmdAuthUpdateLdapSimpleAuth, microcmdAuthUpdateLdapSimpleAuth(),
microcmdAuthAddSMTP, microcmdAuthAddSMTP(),
microcmdAuthUpdateSMTP, microcmdAuthUpdateSMTP(),
microcmdAuthList, microcmdAuthList,
microcmdAuthDelete, microcmdAuthDelete,
}, },
@ -70,9 +70,9 @@ var (
Action: runSendMail, Action: runSendMail,
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
Name: "title", Name: "title",
Usage: `a title of a message`, Usage: "a title of a message",
Value: "", Required: true,
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: "content", Name: "content",
@ -86,17 +86,16 @@ var (
}, },
}, },
} }
)
idFlag = &cli.Int64Flag{ func idFlag() *cli.Int64Flag {
return &cli.Int64Flag{
Name: "id", Name: "id",
Usage: "ID of authentication source", Usage: "ID of authentication source",
} }
) }
func runRepoSyncReleases(_ *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()
func runRepoSyncReleases(ctx context.Context, _ *cli.Command) error {
if err := initDB(ctx); err != nil { if err := initDB(ctx); err != nil {
return err return err
} }

View File

@ -4,6 +4,7 @@
package cmd package cmd
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"os" "os"
@ -13,14 +14,14 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
auth_service "code.gitea.io/gitea/services/auth" auth_service "code.gitea.io/gitea/services/auth"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
var ( var (
microcmdAuthDelete = &cli.Command{ microcmdAuthDelete = &cli.Command{
Name: "delete", Name: "delete",
Usage: "Delete specific auth source", Usage: "Delete specific auth source",
Flags: []cli.Flag{idFlag}, Flags: []cli.Flag{idFlag()},
Action: runDeleteAuth, Action: runDeleteAuth,
} }
microcmdAuthList = &cli.Command{ microcmdAuthList = &cli.Command{
@ -56,10 +57,7 @@ var (
} }
) )
func runListAuth(c *cli.Context) error { func runListAuth(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals()
defer cancel()
if err := initDB(ctx); err != nil { if err := initDB(ctx); err != nil {
return err return err
} }
@ -90,14 +88,11 @@ func runListAuth(c *cli.Context) error {
return nil return nil
} }
func runDeleteAuth(c *cli.Context) error { func runDeleteAuth(ctx context.Context, c *cli.Command) error {
if !c.IsSet("id") { if !c.IsSet("id") {
return errors.New("--id flag is missing") return errors.New("--id flag is missing")
} }
ctx, cancel := installSignals()
defer cancel()
if err := initDB(ctx); err != nil { if err := initDB(ctx); err != nil {
return err return err
} }

View File

@ -12,7 +12,7 @@ import (
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/auth/source/ldap" "code.gitea.io/gitea/services/auth/source/ldap"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
type ( type (
@ -24,8 +24,8 @@ type (
} }
) )
var ( func commonLdapCLIFlags() []cli.Flag {
commonLdapCLIFlags = []cli.Flag{ return []cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
Name: "name", Name: "name",
Usage: "Authentication name.", Usage: "Authentication name.",
@ -103,8 +103,10 @@ var (
Usage: "The attribute of the users LDAP record containing the users avatar.", Usage: "The attribute of the users LDAP record containing the users avatar.",
}, },
} }
}
ldapBindDnCLIFlags = append(commonLdapCLIFlags, func ldapBindDnCLIFlags() []cli.Flag {
return append(commonLdapCLIFlags(),
&cli.StringFlag{ &cli.StringFlag{
Name: "bind-dn", Name: "bind-dn",
Usage: "The DN to bind to the LDAP server with when searching for the user.", Usage: "The DN to bind to the LDAP server with when searching for the user.",
@ -157,49 +159,59 @@ var (
Name: "group-team-map-removal", Name: "group-team-map-removal",
Usage: "Remove users from synchronized teams if user does not belong to corresponding LDAP group", Usage: "Remove users from synchronized teams if user does not belong to corresponding LDAP group",
}) })
}
ldapSimpleAuthCLIFlags = append(commonLdapCLIFlags, func ldapSimpleAuthCLIFlags() []cli.Flag {
return append(commonLdapCLIFlags(),
&cli.StringFlag{ &cli.StringFlag{
Name: "user-dn", Name: "user-dn",
Usage: "The user's DN.", Usage: "The user's DN.",
}) })
}
microcmdAuthAddLdapBindDn = &cli.Command{ func microcmdAuthAddLdapBindDn() *cli.Command {
return &cli.Command{
Name: "add-ldap", Name: "add-ldap",
Usage: "Add new LDAP (via Bind DN) authentication source", Usage: "Add new LDAP (via Bind DN) authentication source",
Action: func(c *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
return newAuthService().addLdapBindDn(c) return newAuthService().addLdapBindDn(ctx, cmd)
}, },
Flags: ldapBindDnCLIFlags, Flags: ldapBindDnCLIFlags(),
} }
}
microcmdAuthUpdateLdapBindDn = &cli.Command{ func microcmdAuthUpdateLdapBindDn() *cli.Command {
return &cli.Command{
Name: "update-ldap", Name: "update-ldap",
Usage: "Update existing LDAP (via Bind DN) authentication source", Usage: "Update existing LDAP (via Bind DN) authentication source",
Action: func(c *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
return newAuthService().updateLdapBindDn(c) return newAuthService().updateLdapBindDn(ctx, cmd)
}, },
Flags: append([]cli.Flag{idFlag}, ldapBindDnCLIFlags...), Flags: append([]cli.Flag{idFlag()}, ldapBindDnCLIFlags()...),
} }
}
microcmdAuthAddLdapSimpleAuth = &cli.Command{ func microcmdAuthAddLdapSimpleAuth() *cli.Command {
return &cli.Command{
Name: "add-ldap-simple", Name: "add-ldap-simple",
Usage: "Add new LDAP (simple auth) authentication source", Usage: "Add new LDAP (simple auth) authentication source",
Action: func(c *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
return newAuthService().addLdapSimpleAuth(c) return newAuthService().addLdapSimpleAuth(ctx, cmd)
}, },
Flags: ldapSimpleAuthCLIFlags, Flags: ldapSimpleAuthCLIFlags(),
} }
}
microcmdAuthUpdateLdapSimpleAuth = &cli.Command{ func microcmdAuthUpdateLdapSimpleAuth() *cli.Command {
return &cli.Command{
Name: "update-ldap-simple", Name: "update-ldap-simple",
Usage: "Update existing LDAP (simple auth) authentication source", Usage: "Update existing LDAP (simple auth) authentication source",
Action: func(c *cli.Context) error { Action: func(ctx context.Context, cmd *cli.Command) error {
return newAuthService().updateLdapSimpleAuth(c) return newAuthService().updateLdapSimpleAuth(ctx, cmd)
}, },
Flags: append([]cli.Flag{idFlag}, ldapSimpleAuthCLIFlags...), Flags: append([]cli.Flag{idFlag()}, ldapSimpleAuthCLIFlags()...),
} }
) }
// newAuthService creates a service with default functions. // newAuthService creates a service with default functions.
func newAuthService() *authService { func newAuthService() *authService {
@ -212,7 +224,7 @@ func newAuthService() *authService {
} }
// parseAuthSourceLdap assigns values on authSource according to command line flags. // parseAuthSourceLdap assigns values on authSource according to command line flags.
func parseAuthSourceLdap(c *cli.Context, authSource *auth.Source) { func parseAuthSourceLdap(c *cli.Command, authSource *auth.Source) {
if c.IsSet("name") { if c.IsSet("name") {
authSource.Name = c.String("name") authSource.Name = c.String("name")
} }
@ -232,7 +244,7 @@ func parseAuthSourceLdap(c *cli.Context, authSource *auth.Source) {
} }
// parseLdapConfig assigns values on config according to command line flags. // parseLdapConfig assigns values on config according to command line flags.
func parseLdapConfig(c *cli.Context, config *ldap.Source) error { func parseLdapConfig(c *cli.Command, config *ldap.Source) error {
if c.IsSet("name") { if c.IsSet("name") {
config.Name = c.String("name") config.Name = c.String("name")
} }
@ -245,7 +257,7 @@ func parseLdapConfig(c *cli.Context, config *ldap.Source) error {
if c.IsSet("security-protocol") { if c.IsSet("security-protocol") {
p, ok := findLdapSecurityProtocolByName(c.String("security-protocol")) p, ok := findLdapSecurityProtocolByName(c.String("security-protocol"))
if !ok { if !ok {
return fmt.Errorf("Unknown security protocol name: %s", c.String("security-protocol")) return fmt.Errorf("unknown security protocol name: %s", c.String("security-protocol"))
} }
config.SecurityProtocol = p config.SecurityProtocol = p
} }
@ -337,32 +349,27 @@ func findLdapSecurityProtocolByName(name string) (ldap.SecurityProtocol, bool) {
// getAuthSource gets the login source by its id defined in the command line flags. // getAuthSource gets the login source by its id defined in the command line flags.
// It returns an error if the id is not set, does not match any source or if the source is not of expected type. // It returns an error if the id is not set, does not match any source or if the source is not of expected type.
func (a *authService) getAuthSource(ctx context.Context, c *cli.Context, authType auth.Type) (*auth.Source, error) { func (a *authService) getAuthSource(ctx context.Context, c *cli.Command, authType auth.Type) (*auth.Source, error) {
if err := argsSet(c, "id"); err != nil { if err := argsSet(c, "id"); err != nil {
return nil, err return nil, err
} }
authSource, err := a.getAuthSourceByID(ctx, c.Int64("id")) authSource, err := a.getAuthSourceByID(ctx, c.Int64("id"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
if authSource.Type != authType { if authSource.Type != authType {
return nil, fmt.Errorf("Invalid authentication type. expected: %s, actual: %s", authType.String(), authSource.Type.String()) return nil, fmt.Errorf("invalid authentication type. expected: %s, actual: %s", authType.String(), authSource.Type.String())
} }
return authSource, nil return authSource, nil
} }
// addLdapBindDn adds a new LDAP via Bind DN authentication source. // addLdapBindDn adds a new LDAP via Bind DN authentication source.
func (a *authService) addLdapBindDn(c *cli.Context) error { func (a *authService) addLdapBindDn(ctx context.Context, c *cli.Command) error {
if err := argsSet(c, "name", "security-protocol", "host", "port", "user-search-base", "user-filter", "email-attribute"); err != nil { if err := argsSet(c, "name", "security-protocol", "host", "port", "user-search-base", "user-filter", "email-attribute"); err != nil {
return err return err
} }
ctx, cancel := installSignals()
defer cancel()
if err := a.initDB(ctx); err != nil { if err := a.initDB(ctx); err != nil {
return err return err
} }
@ -384,10 +391,7 @@ func (a *authService) addLdapBindDn(c *cli.Context) error {
} }
// updateLdapBindDn updates a new LDAP via Bind DN authentication source. // updateLdapBindDn updates a new LDAP via Bind DN authentication source.
func (a *authService) updateLdapBindDn(c *cli.Context) error { func (a *authService) updateLdapBindDn(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals()
defer cancel()
if err := a.initDB(ctx); err != nil { if err := a.initDB(ctx); err != nil {
return err return err
} }
@ -406,14 +410,11 @@ func (a *authService) updateLdapBindDn(c *cli.Context) error {
} }
// addLdapSimpleAuth adds a new LDAP (simple auth) authentication source. // addLdapSimpleAuth adds a new LDAP (simple auth) authentication source.
func (a *authService) addLdapSimpleAuth(c *cli.Context) error { func (a *authService) addLdapSimpleAuth(ctx context.Context, c *cli.Command) error {
if err := argsSet(c, "name", "security-protocol", "host", "port", "user-dn", "user-filter", "email-attribute"); err != nil { if err := argsSet(c, "name", "security-protocol", "host", "port", "user-dn", "user-filter", "email-attribute"); err != nil {
return err return err
} }
ctx, cancel := installSignals()
defer cancel()
if err := a.initDB(ctx); err != nil { if err := a.initDB(ctx); err != nil {
return err return err
} }
@ -435,10 +436,7 @@ func (a *authService) addLdapSimpleAuth(c *cli.Context) error {
} }
// updateLdapSimpleAuth updates a new LDAP (simple auth) authentication source. // updateLdapSimpleAuth updates a new LDAP (simple auth) authentication source.
func (a *authService) updateLdapSimpleAuth(c *cli.Context) error { func (a *authService) updateLdapSimpleAuth(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals()
defer cancel()
if err := a.initDB(ctx); err != nil { if err := a.initDB(ctx); err != nil {
return err return err
} }

View File

@ -12,7 +12,7 @@ import (
"code.gitea.io/gitea/services/auth/source/ldap" "code.gitea.io/gitea/services/auth/source/ldap"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
func TestAddLdapBindDn(t *testing.T) { func TestAddLdapBindDn(t *testing.T) {
@ -134,7 +134,7 @@ func TestAddLdapBindDn(t *testing.T) {
"--user-filter", "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)", "--user-filter", "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)",
"--email-attribute", "mail", "--email-attribute", "mail",
}, },
errMsg: "Unknown security protocol name: zzzzz", errMsg: "unknown security protocol name: zzzzz",
}, },
// case 3 // case 3
{ {
@ -238,12 +238,13 @@ func TestAddLdapBindDn(t *testing.T) {
} }
// Create a copy of command to test // Create a copy of command to test
app := cli.NewApp() app := cli.Command{
app.Flags = microcmdAuthAddLdapBindDn.Flags Flags: microcmdAuthAddLdapBindDn().Flags,
app.Action = service.addLdapBindDn Action: service.addLdapBindDn,
}
// Run it // Run it
err := app.Run(c.args) err := app.Run(t.Context(), c.args)
if c.errMsg != "" { if c.errMsg != "" {
assert.EqualError(t, err, c.errMsg, "case %d: error should match", n) assert.EqualError(t, err, c.errMsg, "case %d: error should match", n)
} else { } else {
@ -345,12 +346,12 @@ func TestAddLdapSimpleAuth(t *testing.T) {
"--name", "ldap (simple auth) source", "--name", "ldap (simple auth) source",
"--security-protocol", "zzzzz", "--security-protocol", "zzzzz",
"--host", "ldap-server", "--host", "ldap-server",
"--port", "123", "--port", "1234",
"--user-filter", "(&(objectClass=posixAccount)(cn=%s))", "--user-filter", "(&(objectClass=posixAccount)(cn=%s))",
"--email-attribute", "mail", "--email-attribute", "mail",
"--user-dn", "cn=%s,ou=Users,dc=domain,dc=org", "--user-dn", "cn=%s,ou=Users,dc=domain,dc=org",
}, },
errMsg: "Unknown security protocol name: zzzzz", errMsg: "unknown security protocol name: zzzzz",
}, },
// case 3 // case 3
{ {
@ -467,12 +468,13 @@ func TestAddLdapSimpleAuth(t *testing.T) {
} }
// Create a copy of command to test // Create a copy of command to test
app := cli.NewApp() app := &cli.Command{
app.Flags = microcmdAuthAddLdapSimpleAuth.Flags Flags: microcmdAuthAddLdapSimpleAuth().Flags,
app.Action = service.addLdapSimpleAuth Action: service.addLdapSimpleAuth,
}
// Run it // Run it
err := app.Run(c.args) err := app.Run(t.Context(), c.args)
if c.errMsg != "" { if c.errMsg != "" {
assert.EqualError(t, err, c.errMsg, "case %d: error should match", n) assert.EqualError(t, err, c.errMsg, "case %d: error should match", n)
} else { } else {
@ -859,7 +861,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
"--id", "1", "--id", "1",
"--security-protocol", "xxxxx", "--security-protocol", "xxxxx",
}, },
errMsg: "Unknown security protocol name: xxxxx", errMsg: "unknown security protocol name: xxxxx",
}, },
// case 22 // case 22
{ {
@ -878,7 +880,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
Type: auth.OAuth2, Type: auth.OAuth2,
Cfg: &ldap.Source{}, Cfg: &ldap.Source{},
}, },
errMsg: "Invalid authentication type. expected: LDAP (via BindDN), actual: OAuth2", errMsg: "invalid authentication type. expected: LDAP (via BindDN), actual: OAuth2",
}, },
// case 24 // case 24
{ {
@ -942,12 +944,12 @@ func TestUpdateLdapBindDn(t *testing.T) {
} }
// Create a copy of command to test // Create a copy of command to test
app := cli.NewApp() app := cli.Command{
app.Flags = microcmdAuthUpdateLdapBindDn.Flags Flags: microcmdAuthUpdateLdapBindDn().Flags,
app.Action = service.updateLdapBindDn Action: service.updateLdapBindDn,
}
// Run it // Run it
err := app.Run(c.args) err := app.Run(t.Context(), c.args)
if c.errMsg != "" { if c.errMsg != "" {
assert.EqualError(t, err, c.errMsg, "case %d: error should match", n) assert.EqualError(t, err, c.errMsg, "case %d: error should match", n)
} else { } else {
@ -1250,7 +1252,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
"--id", "1", "--id", "1",
"--security-protocol", "xxxxx", "--security-protocol", "xxxxx",
}, },
errMsg: "Unknown security protocol name: xxxxx", errMsg: "unknown security protocol name: xxxxx",
}, },
// case 18 // case 18
{ {
@ -1269,7 +1271,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
Type: auth.PAM, Type: auth.PAM,
Cfg: &ldap.Source{}, Cfg: &ldap.Source{},
}, },
errMsg: "Invalid authentication type. expected: LDAP (simple auth), actual: PAM", errMsg: "invalid authentication type. expected: LDAP (simple auth), actual: PAM",
}, },
// case 20 // case 20
{ {
@ -1330,12 +1332,12 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
} }
// Create a copy of command to test // Create a copy of command to test
app := cli.NewApp() app := cli.Command{
app.Flags = microcmdAuthUpdateLdapSimpleAuth.Flags Flags: microcmdAuthUpdateLdapSimpleAuth().Flags,
app.Action = service.updateLdapSimpleAuth Action: service.updateLdapSimpleAuth,
}
// Run it // Run it
err := app.Run(c.args) err := app.Run(t.Context(), c.args)
if c.errMsg != "" { if c.errMsg != "" {
assert.EqualError(t, err, c.errMsg, "case %d: error should match", n) assert.EqualError(t, err, c.errMsg, "case %d: error should match", n)
} else { } else {

View File

@ -4,6 +4,7 @@
package cmd package cmd
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"net/url" "net/url"
@ -12,11 +13,11 @@ import (
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/auth/source/oauth2" "code.gitea.io/gitea/services/auth/source/oauth2"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
var ( func oauthCLIFlags() []cli.Flag {
oauthCLIFlags = []cli.Flag{ return []cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
Name: "name", Name: "name",
Value: "", Value: "",
@ -121,23 +122,34 @@ var (
Usage: "Activate automatic team membership removal depending on groups", Usage: "Activate automatic team membership removal depending on groups",
}, },
} }
}
microcmdAuthAddOauth = &cli.Command{ func microcmdAuthAddOauth() *cli.Command {
Name: "add-oauth", return &cli.Command{
Usage: "Add new Oauth authentication source", Name: "add-oauth",
Action: runAddOauth, Usage: "Add new Oauth authentication source",
Flags: oauthCLIFlags, Action: func(ctx context.Context, cmd *cli.Command) error {
return newAuthService().runAddOauth(ctx, cmd)
},
Flags: oauthCLIFlags(),
} }
}
microcmdAuthUpdateOauth = &cli.Command{ func microcmdAuthUpdateOauth() *cli.Command {
Name: "update-oauth", return &cli.Command{
Usage: "Update existing Oauth authentication source", Name: "update-oauth",
Action: runUpdateOauth, Usage: "Update existing Oauth authentication source",
Flags: append(oauthCLIFlags[:1], append([]cli.Flag{idFlag}, oauthCLIFlags[1:]...)...), Action: func(ctx context.Context, cmd *cli.Command) error {
return newAuthService().runUpdateOauth(ctx, cmd)
},
Flags: append(oauthCLIFlags()[:1], append([]cli.Flag{&cli.Int64Flag{
Name: "id",
Usage: "ID of authentication source",
}}, oauthCLIFlags()[1:]...)...),
} }
) }
func parseOAuth2Config(c *cli.Context) *oauth2.Source { func parseOAuth2Config(c *cli.Command) *oauth2.Source {
var customURLMapping *oauth2.CustomURLMapping var customURLMapping *oauth2.CustomURLMapping
if c.IsSet("use-custom-urls") { if c.IsSet("use-custom-urls") {
customURLMapping = &oauth2.CustomURLMapping{ customURLMapping = &oauth2.CustomURLMapping{
@ -168,11 +180,8 @@ func parseOAuth2Config(c *cli.Context) *oauth2.Source {
} }
} }
func runAddOauth(c *cli.Context) error { func (a *authService) runAddOauth(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals() if err := a.initDB(ctx); err != nil {
defer cancel()
if err := initDB(ctx); err != nil {
return err return err
} }
@ -184,7 +193,7 @@ func runAddOauth(c *cli.Context) error {
} }
} }
return auth_model.CreateSource(ctx, &auth_model.Source{ return a.createAuthSource(ctx, &auth_model.Source{
Type: auth_model.OAuth2, Type: auth_model.OAuth2,
Name: c.String("name"), Name: c.String("name"),
IsActive: true, IsActive: true,
@ -193,19 +202,16 @@ func runAddOauth(c *cli.Context) error {
}) })
} }
func runUpdateOauth(c *cli.Context) error { func (a *authService) runUpdateOauth(ctx context.Context, c *cli.Command) error {
if !c.IsSet("id") { if !c.IsSet("id") {
return errors.New("--id flag is missing") return errors.New("--id flag is missing")
} }
ctx, cancel := installSignals() if err := a.initDB(ctx); err != nil {
defer cancel()
if err := initDB(ctx); err != nil {
return err return err
} }
source, err := auth_model.GetSourceByID(ctx, c.Int64("id")) source, err := a.getAuthSourceByID(ctx, c.Int64("id"))
if err != nil { if err != nil {
return err return err
} }
@ -296,5 +302,5 @@ func runUpdateOauth(c *cli.Context) error {
oAuth2Config.CustomURLMapping = customURLMapping oAuth2Config.CustomURLMapping = customURLMapping
source.Cfg = oAuth2Config source.Cfg = oAuth2Config
source.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "") source.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "")
return auth_model.UpdateSource(ctx, source) return a.updateAuthSource(ctx, source)
} }

View File

@ -0,0 +1,333 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/services/auth/source/oauth2"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v3"
)
func TestAddOauth(t *testing.T) {
testCases := []struct {
name string
args []string
source *auth_model.Source
errMsg string
}{
{
name: "valid config",
args: []string{
"--name", "test",
"--provider", "github",
"--key", "some_key",
"--secret", "some_secret",
},
source: &auth_model.Source{
Type: auth_model.OAuth2,
Name: "test",
IsActive: true,
Cfg: &oauth2.Source{
Scopes: []string{},
Provider: "github",
ClientID: "some_key",
ClientSecret: "some_secret",
},
TwoFactorPolicy: "",
},
},
{
name: "valid config with openid connect",
args: []string{
"--name", "test",
"--provider", "openidConnect",
"--key", "some_key",
"--secret", "some_secret",
"--auto-discover-url", "https://example.com",
},
source: &auth_model.Source{
Type: auth_model.OAuth2,
Name: "test",
IsActive: true,
Cfg: &oauth2.Source{
Scopes: []string{},
Provider: "openidConnect",
ClientID: "some_key",
ClientSecret: "some_secret",
OpenIDConnectAutoDiscoveryURL: "https://example.com",
},
TwoFactorPolicy: "",
},
},
{
name: "valid config with options",
args: []string{
"--name", "test",
"--provider", "gitlab",
"--key", "some_key",
"--secret", "some_secret",
"--use-custom-urls", "true",
"--custom-token-url", "https://example.com/token",
"--custom-auth-url", "https://example.com/auth",
"--custom-profile-url", "https://example.com/profile",
"--custom-email-url", "https://example.com/email",
"--custom-tenant-id", "some_tenant",
"--icon-url", "https://example.com/icon",
"--scopes", "scope1,scope2",
"--skip-local-2fa", "true",
"--required-claim-name", "claim_name",
"--required-claim-value", "claim_value",
"--group-claim-name", "group_name",
"--admin-group", "admin",
"--restricted-group", "restricted",
"--group-team-map", `{"group1": [1,2]}`,
"--group-team-map-removal=true",
},
source: &auth_model.Source{
Type: auth_model.OAuth2,
Name: "test",
IsActive: true,
Cfg: &oauth2.Source{
Provider: "gitlab",
ClientID: "some_key",
ClientSecret: "some_secret",
CustomURLMapping: &oauth2.CustomURLMapping{
TokenURL: "https://example.com/token",
AuthURL: "https://example.com/auth",
ProfileURL: "https://example.com/profile",
EmailURL: "https://example.com/email",
Tenant: "some_tenant",
},
IconURL: "https://example.com/icon",
Scopes: []string{"scope1", "scope2"},
RequiredClaimName: "claim_name",
RequiredClaimValue: "claim_value",
GroupClaimName: "group_name",
AdminGroup: "admin",
RestrictedGroup: "restricted",
GroupTeamMap: `{"group1": [1,2]}`,
GroupTeamMapRemoval: true,
},
TwoFactorPolicy: "skip",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var createdSource *auth_model.Source
a := &authService{
initDB: func(ctx context.Context) error {
return nil
},
createAuthSource: func(ctx context.Context, source *auth_model.Source) error {
createdSource = source
return nil
},
}
app := &cli.Command{
Flags: microcmdAuthAddOauth().Flags,
Action: a.runAddOauth,
}
args := []string{"oauth-test"}
args = append(args, tc.args...)
err := app.Run(t.Context(), args)
if tc.errMsg != "" {
assert.EqualError(t, err, tc.errMsg)
} else {
assert.NoError(t, err)
assert.Equal(t, tc.source, createdSource)
}
})
}
}
func TestUpdateOauth(t *testing.T) {
testCases := []struct {
name string
args []string
id int64
existingAuthSource *auth_model.Source
authSource *auth_model.Source
errMsg string
}{
{
name: "missing id",
args: []string{
"--name", "test",
},
errMsg: "--id flag is missing",
},
{
name: "valid config",
id: 1,
existingAuthSource: &auth_model.Source{
ID: 1,
Type: auth_model.OAuth2,
Name: "old name",
IsActive: true,
Cfg: &oauth2.Source{
Provider: "github",
ClientID: "old_key",
ClientSecret: "old_secret",
},
TwoFactorPolicy: "",
},
args: []string{
"--id", "1",
"--name", "test",
"--provider", "gitlab",
"--key", "new_key",
"--secret", "new_secret",
},
authSource: &auth_model.Source{
ID: 1,
Type: auth_model.OAuth2,
Name: "test",
IsActive: true,
Cfg: &oauth2.Source{
Provider: "gitlab",
ClientID: "new_key",
ClientSecret: "new_secret",
CustomURLMapping: &oauth2.CustomURLMapping{},
},
TwoFactorPolicy: "",
},
},
{
name: "valid config with options",
id: 1,
existingAuthSource: &auth_model.Source{
ID: 1,
Type: auth_model.OAuth2,
Name: "old name",
IsActive: true,
Cfg: &oauth2.Source{
Provider: "gitlab",
ClientID: "old_key",
ClientSecret: "old_secret",
CustomURLMapping: &oauth2.CustomURLMapping{
TokenURL: "https://old.example.com/token",
AuthURL: "https://old.example.com/auth",
ProfileURL: "https://old.example.com/profile",
EmailURL: "https://old.example.com/email",
Tenant: "old_tenant",
},
IconURL: "https://old.example.com/icon",
Scopes: []string{"old_scope1", "old_scope2"},
RequiredClaimName: "old_claim_name",
RequiredClaimValue: "old_claim_value",
GroupClaimName: "old_group_name",
AdminGroup: "old_admin",
RestrictedGroup: "old_restricted",
GroupTeamMap: `{"old_group1": [1,2]}`,
GroupTeamMapRemoval: true,
},
TwoFactorPolicy: "",
},
args: []string{
"--id", "1",
"--name", "test",
"--provider", "github",
"--key", "new_key",
"--secret", "new_secret",
"--use-custom-urls", "true",
"--custom-token-url", "https://example.com/token",
"--custom-auth-url", "https://example.com/auth",
"--custom-profile-url", "https://example.com/profile",
"--custom-email-url", "https://example.com/email",
"--custom-tenant-id", "new_tenant",
"--icon-url", "https://example.com/icon",
"--scopes", "scope1,scope2",
"--skip-local-2fa=true",
"--required-claim-name", "claim_name",
"--required-claim-value", "claim_value",
"--group-claim-name", "group_name",
"--admin-group", "admin",
"--restricted-group", "restricted",
"--group-team-map", `{"group1": [1,2]}`,
"--group-team-map-removal=false",
},
authSource: &auth_model.Source{
ID: 1,
Type: auth_model.OAuth2,
Name: "test",
IsActive: true,
Cfg: &oauth2.Source{
Provider: "github",
ClientID: "new_key",
ClientSecret: "new_secret",
CustomURLMapping: &oauth2.CustomURLMapping{
TokenURL: "https://example.com/token",
AuthURL: "https://example.com/auth",
ProfileURL: "https://example.com/profile",
EmailURL: "https://example.com/email",
Tenant: "new_tenant",
},
IconURL: "https://example.com/icon",
Scopes: []string{"scope1", "scope2"},
RequiredClaimName: "claim_name",
RequiredClaimValue: "claim_value",
GroupClaimName: "group_name",
AdminGroup: "admin",
RestrictedGroup: "restricted",
GroupTeamMap: `{"group1": [1,2]}`,
GroupTeamMapRemoval: false,
},
TwoFactorPolicy: "skip",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
a := &authService{
initDB: func(ctx context.Context) error {
return nil
},
getAuthSourceByID: func(ctx context.Context, id int64) (*auth_model.Source, error) {
return &auth_model.Source{
ID: 1,
Type: auth_model.OAuth2,
Name: "test",
IsActive: true,
Cfg: &oauth2.Source{
CustomURLMapping: &oauth2.CustomURLMapping{},
},
TwoFactorPolicy: "skip",
}, nil
},
updateAuthSource: func(ctx context.Context, source *auth_model.Source) error {
assert.Equal(t, tc.authSource, source)
return nil
},
}
app := &cli.Command{
Flags: microcmdAuthUpdateOauth().Flags,
Action: a.runUpdateOauth,
}
args := []string{"oauth-test"}
args = append(args, tc.args...)
err := app.Run(t.Context(), args)
if tc.errMsg != "" {
assert.EqualError(t, err, tc.errMsg)
} else {
assert.NoError(t, err)
}
})
}
}

View File

@ -4,6 +4,7 @@
package cmd package cmd
import ( import (
"context"
"errors" "errors"
"strings" "strings"
@ -11,11 +12,11 @@ import (
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/auth/source/smtp" "code.gitea.io/gitea/services/auth/source/smtp"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
var ( func smtpCLIFlags() []cli.Flag {
smtpCLIFlags = []cli.Flag{ return []cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
Name: "name", Name: "name",
Value: "", Value: "",
@ -71,23 +72,34 @@ var (
Value: true, Value: true,
}, },
} }
}
microcmdAuthAddSMTP = &cli.Command{ func microcmdAuthUpdateSMTP() *cli.Command {
Name: "add-smtp", return &cli.Command{
Usage: "Add new SMTP authentication source", Name: "update-smtp",
Action: runAddSMTP, Usage: "Update existing SMTP authentication source",
Flags: smtpCLIFlags, Action: func(ctx context.Context, cmd *cli.Command) error {
return newAuthService().runUpdateSMTP(ctx, cmd)
},
Flags: append(smtpCLIFlags()[:1], append([]cli.Flag{&cli.Int64Flag{
Name: "id",
Usage: "ID of authentication source",
}}, smtpCLIFlags()[1:]...)...),
} }
}
microcmdAuthUpdateSMTP = &cli.Command{ func microcmdAuthAddSMTP() *cli.Command {
Name: "update-smtp", return &cli.Command{
Usage: "Update existing SMTP authentication source", Name: "add-smtp",
Action: runUpdateSMTP, Usage: "Add new SMTP authentication source",
Flags: append(smtpCLIFlags[:1], append([]cli.Flag{idFlag}, smtpCLIFlags[1:]...)...), Action: func(ctx context.Context, cmd *cli.Command) error {
return newAuthService().runAddSMTP(ctx, cmd)
},
Flags: smtpCLIFlags(),
} }
) }
func parseSMTPConfig(c *cli.Context, conf *smtp.Source) error { func parseSMTPConfig(c *cli.Command, conf *smtp.Source) error {
if c.IsSet("auth-type") { if c.IsSet("auth-type") {
conf.Auth = c.String("auth-type") conf.Auth = c.String("auth-type")
validAuthTypes := []string{"PLAIN", "LOGIN", "CRAM-MD5"} validAuthTypes := []string{"PLAIN", "LOGIN", "CRAM-MD5"}
@ -120,11 +132,8 @@ func parseSMTPConfig(c *cli.Context, conf *smtp.Source) error {
return nil return nil
} }
func runAddSMTP(c *cli.Context) error { func (a *authService) runAddSMTP(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals() if err := a.initDB(ctx); err != nil {
defer cancel()
if err := initDB(ctx); err != nil {
return err return err
} }
@ -152,7 +161,7 @@ func runAddSMTP(c *cli.Context) error {
smtpConfig.Auth = "PLAIN" smtpConfig.Auth = "PLAIN"
} }
return auth_model.CreateSource(ctx, &auth_model.Source{ return a.createAuthSource(ctx, &auth_model.Source{
Type: auth_model.SMTP, Type: auth_model.SMTP,
Name: c.String("name"), Name: c.String("name"),
IsActive: active, IsActive: active,
@ -161,19 +170,16 @@ func runAddSMTP(c *cli.Context) error {
}) })
} }
func runUpdateSMTP(c *cli.Context) error { func (a *authService) runUpdateSMTP(ctx context.Context, c *cli.Command) error {
if !c.IsSet("id") { if !c.IsSet("id") {
return errors.New("--id flag is missing") return errors.New("--id flag is missing")
} }
ctx, cancel := installSignals() if err := a.initDB(ctx); err != nil {
defer cancel()
if err := initDB(ctx); err != nil {
return err return err
} }
source, err := auth_model.GetSourceByID(ctx, c.Int64("id")) source, err := a.getAuthSourceByID(ctx, c.Int64("id"))
if err != nil { if err != nil {
return err return err
} }
@ -194,5 +200,5 @@ func runUpdateSMTP(c *cli.Context) error {
source.Cfg = smtpConfig source.Cfg = smtpConfig
source.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "") source.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "")
return auth_model.UpdateSource(ctx, source) return a.updateAuthSource(ctx, source)
} }

285
cmd/admin_auth_smtp_test.go Normal file
View File

@ -0,0 +1,285 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/services/auth/source/smtp"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v3"
)
func TestAddSMTP(t *testing.T) {
testCases := []struct {
name string
args []string
source *auth_model.Source
errMsg string
}{
{
name: "missing name",
args: []string{
"--host", "localhost",
"--port", "25",
},
errMsg: "name must be set",
},
{
name: "missing host",
args: []string{
"--name", "test",
"--port", "25",
},
errMsg: "host must be set",
},
{
name: "missing port",
args: []string{
"--name", "test",
"--host", "localhost",
},
errMsg: "port must be set",
},
{
name: "valid config",
args: []string{
"--name", "test",
"--host", "localhost",
"--port", "25",
},
source: &auth_model.Source{
Type: auth_model.SMTP,
Name: "test",
IsActive: true,
Cfg: &smtp.Source{
Auth: "PLAIN",
Host: "localhost",
Port: 25,
// ForceSMTPS: true,
// SkipVerify: true,
},
TwoFactorPolicy: "skip",
},
},
{
name: "valid config with options",
args: []string{
"--name", "test",
"--host", "localhost",
"--port", "25",
"--auth-type", "LOGIN",
"--force-smtps=false",
"--skip-verify=false",
"--helo-hostname", "example.com",
"--disable-helo=false",
"--allowed-domains", "example.com,example.org",
"--skip-local-2fa=false",
"--active=false",
},
source: &auth_model.Source{
Type: auth_model.SMTP,
Name: "test",
IsActive: false,
Cfg: &smtp.Source{
Auth: "LOGIN",
Host: "localhost",
Port: 25,
ForceSMTPS: false,
SkipVerify: false,
HeloHostname: "example.com",
DisableHelo: false,
AllowedDomains: "example.com,example.org",
},
TwoFactorPolicy: "",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
a := &authService{
initDB: func(ctx context.Context) error {
return nil
},
createAuthSource: func(ctx context.Context, source *auth_model.Source) error {
assert.Equal(t, tc.source, source)
return nil
},
}
cmd := &cli.Command{
Flags: microcmdAuthAddSMTP().Flags,
Action: a.runAddSMTP,
}
args := []string{"smtp-test"}
args = append(args, tc.args...)
t.Log(args)
err := cmd.Run(t.Context(), args)
if tc.errMsg != "" {
assert.EqualError(t, err, tc.errMsg)
} else {
assert.NoError(t, err)
}
})
}
}
func TestUpdateSMTP(t *testing.T) {
testCases := []struct {
name string
args []string
existingAuthSource *auth_model.Source
authSource *auth_model.Source
errMsg string
}{
{
name: "missing id",
args: []string{
"--name", "test",
"--host", "localhost",
"--port", "25",
},
errMsg: "--id flag is missing",
},
{
name: "valid config",
existingAuthSource: &auth_model.Source{
ID: 1,
Type: auth_model.SMTP,
Name: "old name",
IsActive: true,
Cfg: &smtp.Source{
Auth: "PLAIN",
Host: "old host",
Port: 26,
ForceSMTPS: true,
SkipVerify: true,
},
TwoFactorPolicy: "",
},
args: []string{
"--id", "1",
"--name", "test",
"--host", "localhost",
"--port", "25",
},
authSource: &auth_model.Source{
ID: 1,
Type: auth_model.SMTP,
Name: "test",
IsActive: true,
Cfg: &smtp.Source{
Auth: "PLAIN",
Host: "localhost",
Port: 25,
ForceSMTPS: true,
SkipVerify: true,
},
TwoFactorPolicy: "skip",
},
},
{
name: "valid config with options",
existingAuthSource: &auth_model.Source{
ID: 1,
Type: auth_model.SMTP,
Name: "old name",
IsActive: true,
Cfg: &smtp.Source{
Auth: "PLAIN",
Host: "old host",
Port: 26,
ForceSMTPS: true,
SkipVerify: true,
HeloHostname: "old.example.com",
DisableHelo: false,
AllowedDomains: "old.example.com",
},
TwoFactorPolicy: "",
},
args: []string{
"--id", "1",
"--name", "test",
"--host", "localhost",
"--port", "25",
"--auth-type", "LOGIN",
"--force-smtps=false",
"--skip-verify=false",
"--helo-hostname", "example.com",
"--disable-helo=true",
"--allowed-domains", "example.com,example.org",
"--skip-local-2fa=true",
"--active=false",
},
authSource: &auth_model.Source{
ID: 1,
Type: auth_model.SMTP,
Name: "test",
IsActive: false,
Cfg: &smtp.Source{
Auth: "LOGIN",
Host: "localhost",
Port: 25,
ForceSMTPS: false,
SkipVerify: false,
HeloHostname: "example.com",
DisableHelo: true,
AllowedDomains: "example.com,example.org",
},
TwoFactorPolicy: "skip",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
a := &authService{
initDB: func(ctx context.Context) error {
return nil
},
getAuthSourceByID: func(ctx context.Context, id int64) (*auth_model.Source, error) {
return &auth_model.Source{
ID: 1,
Type: auth_model.SMTP,
Name: "test",
IsActive: true,
Cfg: &smtp.Source{
Auth: "PLAIN",
SkipVerify: true,
ForceSMTPS: true,
},
TwoFactorPolicy: "skip",
}, nil
},
updateAuthSource: func(ctx context.Context, source *auth_model.Source) error {
assert.Equal(t, tc.authSource, source)
return nil
},
}
app := &cli.Command{
Flags: microcmdAuthUpdateSMTP().Flags,
Action: a.runUpdateSMTP,
}
args := []string{"smtp-tests"}
args = append(args, tc.args...)
err := app.Run(t.Context(), args)
if tc.errMsg != "" {
assert.EqualError(t, err, tc.errMsg)
} else {
assert.NoError(t, err)
}
})
}
}

View File

@ -4,11 +4,13 @@
package cmd package cmd
import ( import (
"context"
"code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/graceful"
asymkey_service "code.gitea.io/gitea/services/asymkey" asymkey_service "code.gitea.io/gitea/services/asymkey"
repo_service "code.gitea.io/gitea/services/repository" repo_service "code.gitea.io/gitea/services/repository"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
var ( var (
@ -25,20 +27,14 @@ var (
} }
) )
func runRegenerateHooks(_ *cli.Context) error { func runRegenerateHooks(ctx context.Context, _ *cli.Command) error {
ctx, cancel := installSignals()
defer cancel()
if err := initDB(ctx); err != nil { if err := initDB(ctx); err != nil {
return err return err
} }
return repo_service.SyncRepositoryHooks(graceful.GetManager().ShutdownContext()) return repo_service.SyncRepositoryHooks(graceful.GetManager().ShutdownContext())
} }
func runRegenerateKeys(_ *cli.Context) error { func runRegenerateKeys(ctx context.Context, _ *cli.Command) error {
ctx, cancel := installSignals()
defer cancel()
if err := initDB(ctx); err != nil { if err := initDB(ctx); err != nil {
return err return err
} }

View File

@ -4,18 +4,18 @@
package cmd package cmd
import ( import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
var subcmdUser = &cli.Command{ var subcmdUser = &cli.Command{
Name: "user", Name: "user",
Usage: "Modify users", Usage: "Modify users",
Subcommands: []*cli.Command{ Commands: []*cli.Command{
microcmdUserCreate, microcmdUserCreate(),
microcmdUserList, microcmdUserList,
microcmdUserChangePassword, microcmdUserChangePassword(),
microcmdUserDelete, microcmdUserDelete(),
microcmdUserGenerateAccessToken, microcmdUserGenerateAccessToken,
microcmdUserMustChangePassword, microcmdUserMustChangePassword(),
}, },
} }

View File

@ -4,6 +4,7 @@
package cmd package cmd
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
@ -13,44 +14,41 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
user_service "code.gitea.io/gitea/services/user" user_service "code.gitea.io/gitea/services/user"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
var microcmdUserChangePassword = &cli.Command{ func microcmdUserChangePassword() *cli.Command {
Name: "change-password", return &cli.Command{
Usage: "Change a user's password", Name: "change-password",
Action: runChangePassword, Usage: "Change a user's password",
Flags: []cli.Flag{ Action: runChangePassword,
&cli.StringFlag{ Flags: []cli.Flag{
Name: "username", &cli.StringFlag{
Aliases: []string{"u"}, Name: "username",
Value: "", Aliases: []string{"u"},
Usage: "The user to change password for", Usage: "The user to change password for",
Required: true,
},
&cli.StringFlag{
Name: "password",
Aliases: []string{"p"},
Usage: "New password to set for user",
Required: true,
},
&cli.BoolFlag{
Name: "must-change-password",
Usage: "User must change password (can be disabled by --must-change-password=false)",
Value: true,
},
}, },
&cli.StringFlag{ }
Name: "password",
Aliases: []string{"p"},
Value: "",
Usage: "New password to set for user",
},
&cli.BoolFlag{
Name: "must-change-password",
Usage: "User must change password (can be disabled by --must-change-password=false)",
Value: true,
},
},
} }
func runChangePassword(c *cli.Context) error { func runChangePassword(ctx context.Context, c *cli.Command) error {
if err := argsSet(c, "username", "password"); err != nil { if !setting.IsInTesting {
return err if err := initDB(ctx); err != nil {
} return err
}
ctx, cancel := installSignals()
defer cancel()
if err := initDB(ctx); err != nil {
return err
} }
user, err := user_model.GetUserByName(ctx, c.String("username")) user, err := user_model.GetUserByName(ctx, c.String("username"))

View File

@ -0,0 +1,91 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"testing"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestChangePasswordCommand(t *testing.T) {
ctx := t.Context()
defer func() {
require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
}()
t.Run("change password successfully", func(t *testing.T) {
// defer func() {
// require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
// }()
// Prepare test user
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
err := microcmdUserCreate().Run(ctx, []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
require.NoError(t, err)
// load test user
userBase := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
// Change the password
err = microcmdUserChangePassword().Run(ctx, []string{"change-password", "--username", "testuser", "--password", "newpassword"})
require.NoError(t, err)
// Verify the password has been changed
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
assert.NotEqual(t, userBase.Passwd, user.Passwd)
assert.NotEqual(t, userBase.Salt, user.Salt)
// Additional check for must-change-password flag
require.NoError(t, microcmdUserChangePassword().Run(ctx, []string{"change-password", "--username", "testuser", "--password", "anotherpassword", "--must-change-password=false"}))
user = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
assert.False(t, user.MustChangePassword)
require.NoError(t, microcmdUserChangePassword().Run(ctx, []string{"change-password", "--username", "testuser", "--password", "yetanotherpassword", "--must-change-password"}))
user = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
assert.True(t, user.MustChangePassword)
})
t.Run("failure cases", func(t *testing.T) {
testCases := []struct {
name string
args []string
expectedErr string
}{
{
name: "user does not exist",
args: []string{"change-password", "--username", "nonexistentuser", "--password", "newpassword"},
expectedErr: "user does not exist",
},
{
name: "missing username",
args: []string{"change-password", "--password", "newpassword"},
expectedErr: `"username" not set`,
},
{
name: "missing password",
args: []string{"change-password", "--username", "testuser"},
expectedErr: `"password" not set`,
},
{
name: "too short password",
args: []string{"change-password", "--username", "testuser", "--password", "1"},
expectedErr: "password is not long enough",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := microcmdUserChangePassword().Run(ctx, tc.args)
require.Error(t, err)
require.Contains(t, err.Error(), tc.expectedErr)
})
}
})
}

View File

@ -16,87 +16,95 @@ import (
"code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
var microcmdUserCreate = &cli.Command{ func microcmdUserCreate() *cli.Command {
Name: "create", return &cli.Command{
Usage: "Create a new user in database", Name: "create",
Action: runCreateUser, Usage: "Create a new user in database",
Flags: []cli.Flag{ Action: runCreateUser,
&cli.StringFlag{ MutuallyExclusiveFlags: []cli.MutuallyExclusiveFlags{
Name: "name", {
Usage: "Username. DEPRECATED: use username instead", Flags: [][]cli.Flag{
{
&cli.StringFlag{
Name: "name",
Usage: "Username. DEPRECATED: use username instead",
},
&cli.StringFlag{
Name: "username",
Usage: "Username",
},
},
},
Required: true,
},
}, },
&cli.StringFlag{ Flags: []cli.Flag{
Name: "username", &cli.StringFlag{
Usage: "Username", Name: "user-type",
Usage: "Set user's type: individual or bot",
Value: "individual",
},
&cli.StringFlag{
Name: "password",
Usage: "User password",
},
&cli.StringFlag{
Name: "email",
Usage: "User email address",
Required: true,
},
&cli.BoolFlag{
Name: "admin",
Usage: "User is an admin",
},
&cli.BoolFlag{
Name: "random-password",
Usage: "Generate a random password for the user",
},
&cli.BoolFlag{
Name: "must-change-password",
Usage: "User must change password after initial login, defaults to true for all users except the first one (can be disabled by --must-change-password=false)",
HideDefault: true,
},
&cli.IntFlag{
Name: "random-password-length",
Usage: "Length of the random password to be generated",
Value: 12,
},
&cli.BoolFlag{
Name: "access-token",
Usage: "Generate access token for the user",
},
&cli.StringFlag{
Name: "access-token-name",
Usage: `Name of the generated access token`,
Value: "gitea-admin",
},
&cli.StringFlag{
Name: "access-token-scopes",
Usage: `Scopes of the generated access token, comma separated. Examples: "all", "public-only,read:issue", "write:repository,write:user"`,
Value: "all",
},
&cli.BoolFlag{
Name: "restricted",
Usage: "Make a restricted user account",
},
&cli.StringFlag{
Name: "fullname",
Usage: `The full, human-readable name of the user`,
},
}, },
&cli.StringFlag{ }
Name: "user-type",
Usage: "Set user's type: individual or bot",
Value: "individual",
},
&cli.StringFlag{
Name: "password",
Usage: "User password",
},
&cli.StringFlag{
Name: "email",
Usage: "User email address",
},
&cli.BoolFlag{
Name: "admin",
Usage: "User is an admin",
},
&cli.BoolFlag{
Name: "random-password",
Usage: "Generate a random password for the user",
},
&cli.BoolFlag{
Name: "must-change-password",
Usage: "User must change password after initial login, defaults to true for all users except the first one (can be disabled by --must-change-password=false)",
DisableDefaultText: true,
},
&cli.IntFlag{
Name: "random-password-length",
Usage: "Length of the random password to be generated",
Value: 12,
},
&cli.BoolFlag{
Name: "access-token",
Usage: "Generate access token for the user",
},
&cli.StringFlag{
Name: "access-token-name",
Usage: `Name of the generated access token`,
Value: "gitea-admin",
},
&cli.StringFlag{
Name: "access-token-scopes",
Usage: `Scopes of the generated access token, comma separated. Examples: "all", "public-only,read:issue", "write:repository,write:user"`,
Value: "all",
},
&cli.BoolFlag{
Name: "restricted",
Usage: "Make a restricted user account",
},
&cli.StringFlag{
Name: "fullname",
Usage: `The full, human-readable name of the user`,
},
},
} }
func runCreateUser(c *cli.Context) error { func runCreateUser(ctx context.Context, c *cli.Command) error {
// this command highly depends on the many setting options (create org, visibility, etc.), so it must have a full setting load first // this command highly depends on the many setting options (create org, visibility, etc.), so it must have a full setting load first
// duplicate setting loading should be safe at the moment, but it should be refactored & improved in the future. // duplicate setting loading should be safe at the moment, but it should be refactored & improved in the future.
setting.LoadSettings() setting.LoadSettings()
if err := argsSet(c, "email"); err != nil {
return err
}
userTypes := map[string]user_model.UserType{ userTypes := map[string]user_model.UserType{
"individual": user_model.UserTypeIndividual, "individual": user_model.UserTypeIndividual,
"bot": user_model.UserTypeBot, "bot": user_model.UserTypeBot,
@ -113,12 +121,6 @@ func runCreateUser(c *cli.Context) error {
return errors.New("password can only be set for individual users") return errors.New("password can only be set for individual users")
} }
} }
if c.IsSet("name") && c.IsSet("username") {
return errors.New("cannot set both --name and --username flags")
}
if !c.IsSet("name") && !c.IsSet("username") {
return errors.New("one of --name or --username flags must be set")
}
if c.IsSet("password") && c.IsSet("random-password") { if c.IsSet("password") && c.IsSet("random-password") {
return errors.New("cannot set both -random-password and -password flags") return errors.New("cannot set both -random-password and -password flags")
@ -129,16 +131,12 @@ func runCreateUser(c *cli.Context) error {
username = c.String("username") username = c.String("username")
} else { } else {
username = c.String("name") username = c.String("name")
_, _ = fmt.Fprintf(c.App.ErrWriter, "--name flag is deprecated. Use --username instead.\n") _, _ = fmt.Fprintf(c.ErrWriter, "--name flag is deprecated. Use --username instead.\n")
} }
ctx := c.Context
if !setting.IsInTesting { if !setting.IsInTesting {
// FIXME: need to refactor the "installSignals/initDB" related code later // FIXME: need to refactor the "initDB" related code later
// it doesn't make sense to call it in (almost) every command action function // it doesn't make sense to call it in (almost) every command action function
var cancel context.CancelFunc
ctx, cancel = installSignals()
defer cancel()
if err := initDB(ctx); err != nil { if err := initDB(ctx); err != nil {
return err return err
} }

View File

@ -18,8 +18,6 @@ import (
) )
func TestAdminUserCreate(t *testing.T) { func TestAdminUserCreate(t *testing.T) {
app := NewMainApp(AppVersion{})
reset := func() { reset := func() {
require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{})) require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{})) require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
@ -31,8 +29,9 @@ func TestAdminUserCreate(t *testing.T) {
IsAdmin bool IsAdmin bool
MustChangePassword bool MustChangePassword bool
} }
createCheck := func(name, args string) check { createCheck := func(name, args string) check {
require.NoError(t, app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s --password foobar", name, name, args)))) require.NoError(t, microcmdUserCreate().Run(t.Context(), strings.Fields(fmt.Sprintf("create --username %s --email %s@gitea.local %s --password foobar", name, name, args))))
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name}) u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name})
return check{IsAdmin: u.IsAdmin, MustChangePassword: u.MustChangePassword} return check{IsAdmin: u.IsAdmin, MustChangePassword: u.MustChangePassword}
} }
@ -51,7 +50,7 @@ func TestAdminUserCreate(t *testing.T) {
}) })
createUser := func(name string, args ...string) error { createUser := func(name string, args ...string) error {
return app.Run(append([]string{"./gitea", "admin", "user", "create", "--username", name, "--email", name + "@gitea.local"}, args...)) return microcmdUserCreate().Run(t.Context(), append([]string{"create", "--username", name, "--email", name + "@gitea.local"}, args...))
} }
t.Run("UserType", func(t *testing.T) { t.Run("UserType", func(t *testing.T) {

View File

@ -4,53 +4,56 @@
package cmd package cmd
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/storage"
user_service "code.gitea.io/gitea/services/user" user_service "code.gitea.io/gitea/services/user"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
var microcmdUserDelete = &cli.Command{ func microcmdUserDelete() *cli.Command {
Name: "delete", return &cli.Command{
Usage: "Delete specific user by id, name or email", Name: "delete",
Flags: []cli.Flag{ Usage: "Delete specific user by id, name or email",
&cli.Int64Flag{ Flags: []cli.Flag{
Name: "id", &cli.Int64Flag{
Usage: "ID of user of the user to delete", Name: "id",
Usage: "ID of user of the user to delete",
},
&cli.StringFlag{
Name: "username",
Aliases: []string{"u"},
Usage: "Username of the user to delete",
},
&cli.StringFlag{
Name: "email",
Aliases: []string{"e"},
Usage: "Email of the user to delete",
},
&cli.BoolFlag{
Name: "purge",
Usage: "Purge user, all their repositories, organizations and comments",
},
}, },
&cli.StringFlag{ Action: runDeleteUser,
Name: "username", }
Aliases: []string{"u"},
Usage: "Username of the user to delete",
},
&cli.StringFlag{
Name: "email",
Aliases: []string{"e"},
Usage: "Email of the user to delete",
},
&cli.BoolFlag{
Name: "purge",
Usage: "Purge user, all their repositories, organizations and comments",
},
},
Action: runDeleteUser,
} }
func runDeleteUser(c *cli.Context) error { func runDeleteUser(ctx context.Context, c *cli.Command) error {
if !c.IsSet("id") && !c.IsSet("username") && !c.IsSet("email") { if !c.IsSet("id") && !c.IsSet("username") && !c.IsSet("email") {
return errors.New("You must provide the id, username or email of a user to delete") return errors.New("You must provide the id, username or email of a user to delete")
} }
ctx, cancel := installSignals() if !setting.IsInTesting {
defer cancel() if err := initDB(ctx); err != nil {
return err
if err := initDB(ctx); err != nil { }
return err
} }
if err := storage.Init(); err != nil { if err := storage.Init(); err != nil {
@ -70,11 +73,11 @@ func runDeleteUser(c *cli.Context) error {
return err return err
} }
if c.IsSet("username") && user.LowerName != strings.ToLower(strings.TrimSpace(c.String("username"))) { if c.IsSet("username") && user.LowerName != strings.ToLower(strings.TrimSpace(c.String("username"))) {
return fmt.Errorf("The user %s who has email %s does not match the provided username %s", user.Name, c.String("email"), c.String("username")) return fmt.Errorf("the user %s who has email %s does not match the provided username %s", user.Name, c.String("email"), c.String("username"))
} }
if c.IsSet("id") && user.ID != c.Int64("id") { if c.IsSet("id") && user.ID != c.Int64("id") {
return fmt.Errorf("The user %s does not match the provided id %d", user.Name, c.Int64("id")) return fmt.Errorf("the user %s does not match the provided id %d", user.Name, c.Int64("id"))
} }
return user_service.DeleteUser(ctx, user, c.Bool("purge")) return user_service.DeleteUser(ctx, user, c.Bool("purge"))

View File

@ -0,0 +1,111 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"strconv"
"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"
"github.com/stretchr/testify/require"
)
func TestAdminUserDelete(t *testing.T) {
ctx := t.Context()
defer func() {
require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
require.NoError(t, db.TruncateBeans(db.DefaultContext, &auth_model.AccessToken{}))
}()
setupTestUser := func(t *testing.T) {
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
err := microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
require.NoError(t, err)
}
t.Run("delete user by id", func(t *testing.T) {
setupTestUser(t)
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
err := microcmdUserDelete().Run(ctx, []string{"delete-test", "--id", strconv.FormatInt(u.ID, 10)})
require.NoError(t, err)
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
})
t.Run("delete user by username", func(t *testing.T) {
setupTestUser(t)
err := microcmdUserDelete().Run(ctx, []string{"delete-test", "--username", "testuser"})
require.NoError(t, err)
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
})
t.Run("delete user by email", func(t *testing.T) {
setupTestUser(t)
err := microcmdUserDelete().Run(ctx, []string{"delete-test", "--email", "testuser@gitea.local"})
require.NoError(t, err)
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
})
t.Run("delete user by all 3 attributes", func(t *testing.T) {
setupTestUser(t)
u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
err := microcmdUserDelete().Run(ctx, []string{"delete", "--id", strconv.FormatInt(u.ID, 10), "--username", "testuser", "--email", "testuser@gitea.local"})
require.NoError(t, err)
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
})
}
func TestAdminUserDeleteFailure(t *testing.T) {
testCases := []struct {
name string
args []string
expectedErr string
}{
{
name: "no user to delete",
args: []string{"delete", "--username", "nonexistentuser"},
expectedErr: "user does not exist",
},
{
name: "user exists but provided username does not match",
args: []string{"delete", "--email", "testuser@gitea.local", "--username", "wrongusername"},
expectedErr: "the user testuser who has email testuser@gitea.local does not match the provided username wrongusername",
},
{
name: "user exists but provided id does not match",
args: []string{"delete", "--username", "testuser", "--id", "999"},
expectedErr: "the user testuser does not match the provided id 999",
},
{
name: "no required flags are provided",
args: []string{"delete"},
expectedErr: "You must provide the id, username or email of a user to delete",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctx := t.Context()
if strings.Contains(tc.name, "user exists") {
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"})
err := microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
require.NoError(t, err)
}
err := microcmdUserDelete().Run(ctx, tc.args)
require.Error(t, err)
require.Contains(t, err.Error(), tc.expectedErr)
})
require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
require.NoError(t, db.TruncateBeans(db.DefaultContext, &auth_model.AccessToken{}))
}
}

View File

@ -4,13 +4,14 @@
package cmd package cmd
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
var microcmdUserGenerateAccessToken = &cli.Command{ var microcmdUserGenerateAccessToken = &cli.Command{
@ -41,14 +42,11 @@ var microcmdUserGenerateAccessToken = &cli.Command{
Action: runGenerateAccessToken, Action: runGenerateAccessToken,
} }
func runGenerateAccessToken(c *cli.Context) error { func runGenerateAccessToken(ctx context.Context, c *cli.Command) error {
if !c.IsSet("username") { if !c.IsSet("username") {
return errors.New("you must provide a username to generate a token for") return errors.New("you must provide a username to generate a token for")
} }
ctx, cancel := installSignals()
defer cancel()
if err := initDB(ctx); err != nil { if err := initDB(ctx); err != nil {
return err return err
} }

View File

@ -4,13 +4,14 @@
package cmd package cmd
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"text/tabwriter" "text/tabwriter"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
var microcmdUserList = &cli.Command{ var microcmdUserList = &cli.Command{
@ -25,10 +26,7 @@ var microcmdUserList = &cli.Command{
}, },
} }
func runListUsers(c *cli.Context) error { func runListUsers(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals()
defer cancel()
if err := initDB(ctx); err != nil { if err := initDB(ctx); err != nil {
return err return err
} }

View File

@ -4,40 +4,41 @@
package cmd package cmd
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
var microcmdUserMustChangePassword = &cli.Command{ func microcmdUserMustChangePassword() *cli.Command {
Name: "must-change-password", return &cli.Command{
Usage: "Set the must change password flag for the provided users or all users", Name: "must-change-password",
Action: runMustChangePassword, Usage: "Set the must change password flag for the provided users or all users",
Flags: []cli.Flag{ Action: runMustChangePassword,
&cli.BoolFlag{ Flags: []cli.Flag{
Name: "all", &cli.BoolFlag{
Aliases: []string{"A"}, Name: "all",
Usage: "All users must change password, except those explicitly excluded with --exclude", Aliases: []string{"A"},
Usage: "All users must change password, except those explicitly excluded with --exclude",
},
&cli.StringSliceFlag{
Name: "exclude",
Aliases: []string{"e"},
Usage: "Do not change the must-change-password flag for these users",
},
&cli.BoolFlag{
Name: "unset",
Usage: "Instead of setting the must-change-password flag, unset it",
},
}, },
&cli.StringSliceFlag{ }
Name: "exclude",
Aliases: []string{"e"},
Usage: "Do not change the must-change-password flag for these users",
},
&cli.BoolFlag{
Name: "unset",
Usage: "Instead of setting the must-change-password flag, unset it",
},
},
} }
func runMustChangePassword(c *cli.Context) error { func runMustChangePassword(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals()
defer cancel()
if c.NArg() == 0 && !c.IsSet("all") { if c.NArg() == 0 && !c.IsSet("all") {
return errors.New("either usernames or --all must be provided") return errors.New("either usernames or --all must be provided")
} }
@ -46,8 +47,10 @@ func runMustChangePassword(c *cli.Context) error {
all := c.Bool("all") all := c.Bool("all")
exclude := c.StringSlice("exclude") exclude := c.StringSlice("exclude")
if err := initDB(ctx); err != nil { if !setting.IsInTesting {
return err if err := initDB(ctx); err != nil {
return err
}
} }
n, err := user_model.SetMustChangePassword(ctx, all, mustChangePassword, c.Args().Slice(), exclude) n, err := user_model.SetMustChangePassword(ctx, all, mustChangePassword, c.Args().Slice(), exclude)

View File

@ -0,0 +1,78 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"testing"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMustChangePassword(t *testing.T) {
defer func() {
require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
}()
err := microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"})
require.NoError(t, err)
err = microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuserexclude", "--email", "testuserexclude@gitea.local", "--random-password"})
require.NoError(t, err)
// Reset password change flag
err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all", "--unset"})
require.NoError(t, err)
testUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
assert.False(t, testUser.MustChangePassword)
testUserExclude := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
assert.False(t, testUserExclude.MustChangePassword)
// Make all users change password
err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all"})
require.NoError(t, err)
testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
assert.True(t, testUser.MustChangePassword)
testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
assert.True(t, testUserExclude.MustChangePassword)
// Reset password change flag but exclude all tested users
err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all", "--unset", "--exclude", "testuser,testuserexclude"})
require.NoError(t, err)
testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
assert.True(t, testUser.MustChangePassword)
testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
assert.True(t, testUserExclude.MustChangePassword)
// Reset password change flag by listing multiple users
err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--unset", "testuser", "testuserexclude"})
require.NoError(t, err)
testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
assert.False(t, testUser.MustChangePassword)
testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
assert.False(t, testUserExclude.MustChangePassword)
// Exclude a user from all user
err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all", "--exclude", "testuserexclude"})
require.NoError(t, err)
testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
assert.True(t, testUser.MustChangePassword)
testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
assert.False(t, testUserExclude.MustChangePassword)
// Unset a flag for single user
err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--unset", "testuser"})
require.NoError(t, err)
testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"})
assert.False(t, testUser.MustChangePassword)
testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"})
assert.False(t, testUserExclude.MustChangePassword)
}

View File

@ -6,6 +6,7 @@
package cmd package cmd
import ( import (
"context"
"crypto/ecdsa" "crypto/ecdsa"
"crypto/elliptic" "crypto/elliptic"
"crypto/rand" "crypto/rand"
@ -13,6 +14,7 @@ import (
"crypto/x509" "crypto/x509"
"crypto/x509/pkix" "crypto/x509/pkix"
"encoding/pem" "encoding/pem"
"fmt"
"log" "log"
"math/big" "math/big"
"net" "net"
@ -20,47 +22,59 @@ import (
"strings" "strings"
"time" "time"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
// CmdCert represents the available cert sub-command. // cmdCert represents the available cert sub-command.
var CmdCert = &cli.Command{ func cmdCert() *cli.Command {
Name: "cert", return &cli.Command{
Usage: "Generate self-signed certificate", Name: "cert",
Description: `Generate a self-signed X.509 certificate for a TLS server. Usage: "Generate self-signed certificate",
Description: `Generate a self-signed X.509 certificate for a TLS server.
Outputs to 'cert.pem' and 'key.pem' and will overwrite existing files.`, Outputs to 'cert.pem' and 'key.pem' and will overwrite existing files.`,
Action: runCert, Action: runCert,
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
Name: "host", Name: "host",
Value: "", Usage: "Comma-separated hostnames and IPs to generate a certificate for",
Usage: "Comma-separated hostnames and IPs to generate a certificate for", Required: true,
},
&cli.StringFlag{
Name: "ecdsa-curve",
Value: "",
Usage: "ECDSA curve to use to generate a key. Valid values are P224, P256, P384, P521",
},
&cli.IntFlag{
Name: "rsa-bits",
Value: 3072,
Usage: "Size of RSA key to generate. Ignored if --ecdsa-curve is set",
},
&cli.StringFlag{
Name: "start-date",
Value: "",
Usage: "Creation date formatted as Jan 1 15:04:05 2011",
},
&cli.DurationFlag{
Name: "duration",
Value: 365 * 24 * time.Hour,
Usage: "Duration that certificate is valid for",
},
&cli.BoolFlag{
Name: "ca",
Usage: "whether this cert should be its own Certificate Authority",
},
&cli.StringFlag{
Name: "out",
Value: "cert.pem",
Usage: "Path to the file where there certificate will be saved",
},
&cli.StringFlag{
Name: "keyout",
Value: "key.pem",
Usage: "Path to the file where there certificate key will be saved",
},
}, },
&cli.StringFlag{ }
Name: "ecdsa-curve",
Value: "",
Usage: "ECDSA curve to use to generate a key. Valid values are P224, P256, P384, P521",
},
&cli.IntFlag{
Name: "rsa-bits",
Value: 3072,
Usage: "Size of RSA key to generate. Ignored if --ecdsa-curve is set",
},
&cli.StringFlag{
Name: "start-date",
Value: "",
Usage: "Creation date formatted as Jan 1 15:04:05 2011",
},
&cli.DurationFlag{
Name: "duration",
Value: 365 * 24 * time.Hour,
Usage: "Duration that certificate is valid for",
},
&cli.BoolFlag{
Name: "ca",
Usage: "whether this cert should be its own Certificate Authority",
},
},
} }
func publicKey(priv any) any { func publicKey(priv any) any {
@ -89,11 +103,7 @@ func pemBlockForKey(priv any) *pem.Block {
} }
} }
func runCert(c *cli.Context) error { func runCert(_ context.Context, c *cli.Command) error {
if err := argsSet(c, "host"); err != nil {
return err
}
var priv any var priv any
var err error var err error
switch c.String("ecdsa-curve") { switch c.String("ecdsa-curve") {
@ -108,17 +118,17 @@ func runCert(c *cli.Context) error {
case "P521": case "P521":
priv, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader) priv, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
default: default:
log.Fatalf("Unrecognized elliptic curve: %q", c.String("ecdsa-curve")) err = fmt.Errorf("unrecognized elliptic curve: %q", c.String("ecdsa-curve"))
} }
if err != nil { if err != nil {
log.Fatalf("Failed to generate private key: %v", err) return fmt.Errorf("failed to generate private key: %w", err)
} }
var notBefore time.Time var notBefore time.Time
if startDate := c.String("start-date"); startDate != "" { if startDate := c.String("start-date"); startDate != "" {
notBefore, err = time.Parse("Jan 2 15:04:05 2006", startDate) notBefore, err = time.Parse("Jan 2 15:04:05 2006", startDate)
if err != nil { if err != nil {
log.Fatalf("Failed to parse creation date: %v", err) return fmt.Errorf("failed to parse creation date %w", err)
} }
} else { } else {
notBefore = time.Now() notBefore = time.Now()
@ -129,7 +139,7 @@ func runCert(c *cli.Context) error {
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil { if err != nil {
log.Fatalf("Failed to generate serial number: %v", err) return fmt.Errorf("failed to generate serial number: %w", err)
} }
template := x509.Certificate{ template := x509.Certificate{
@ -162,35 +172,35 @@ func runCert(c *cli.Context) error {
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv) derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv)
if err != nil { if err != nil {
log.Fatalf("Failed to create certificate: %v", err) return fmt.Errorf("failed to create certificate: %w", err)
} }
certOut, err := os.Create("cert.pem") certOut, err := os.Create(c.String("out"))
if err != nil { if err != nil {
log.Fatalf("Failed to open cert.pem for writing: %v", err) return fmt.Errorf("failed to open %s for writing: %w", c.String("keyout"), err)
} }
err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
if err != nil { if err != nil {
log.Fatalf("Failed to encode certificate: %v", err) return fmt.Errorf("failed to encode certificate: %w", err)
} }
err = certOut.Close() err = certOut.Close()
if err != nil { if err != nil {
log.Fatalf("Failed to write cert: %v", err) return fmt.Errorf("failed to write cert: %w", err)
} }
log.Println("Written cert.pem") fmt.Fprintf(c.Writer, "Written cert to %s\n", c.String("out"))
keyOut, err := os.OpenFile("key.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600) keyOut, err := os.OpenFile(c.String("keyout"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
if err != nil { if err != nil {
log.Fatalf("Failed to open key.pem for writing: %v", err) return fmt.Errorf("failed to open %s for writing: %w", c.String("keyout"), err)
} }
err = pem.Encode(keyOut, pemBlockForKey(priv)) err = pem.Encode(keyOut, pemBlockForKey(priv))
if err != nil { if err != nil {
log.Fatalf("Failed to encode key: %v", err) return fmt.Errorf("failed to encode key: %w", err)
} }
err = keyOut.Close() err = keyOut.Close()
if err != nil { if err != nil {
log.Fatalf("Failed to write key: %v", err) return fmt.Errorf("failed to write key: %w", err)
} }
log.Println("Written key.pem") fmt.Fprintf(c.Writer, "Written key to %s\n", c.String("keyout"))
return nil return nil
} }

123
cmd/cert_test.go Normal file
View File

@ -0,0 +1,123 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCertCommand(t *testing.T) {
cases := []struct {
name string
args []string
}{
{
name: "RSA cert generation",
args: []string{
"cert-test",
"--host", "localhost",
"--rsa-bits", "2048",
"--duration", "1h",
"--start-date", "Jan 1 00:00:00 2024",
},
},
{
name: "ECDSA cert generation",
args: []string{
"cert-test",
"--host", "localhost",
"--ecdsa-curve", "P256",
"--duration", "1h",
"--start-date", "Jan 1 00:00:00 2024",
},
},
{
name: "mixed host, certificate authority",
args: []string{
"cert-test",
"--host", "localhost,127.0.0.1",
"--duration", "1h",
"--start-date", "Jan 1 00:00:00 2024",
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
app := cmdCert()
tempDir := t.TempDir()
certFile := filepath.Join(tempDir, "cert.pem")
keyFile := filepath.Join(tempDir, "key.pem")
err := app.Run(t.Context(), append(c.args, "--out", certFile, "--keyout", keyFile))
require.NoError(t, err)
assert.FileExists(t, certFile)
assert.FileExists(t, keyFile)
})
}
}
func TestCertCommandFailures(t *testing.T) {
cases := []struct {
name string
args []string
errMsg string
}{
{
name: "Start Date Parsing failure",
args: []string{
"cert-test",
"--host", "localhost",
"--start-date", "invalid-date",
},
errMsg: "parsing time",
},
{
name: "Unknown curve",
args: []string{
"cert-test",
"--host", "localhost",
"--ecdsa-curve", "invalid-curve",
},
errMsg: "unrecognized elliptic curve",
},
{
name: "Key generation failure",
args: []string{
"cert-test",
"--host", "localhost",
"--rsa-bits", "invalid-bits",
},
},
{
name: "Missing parameters",
args: []string{
"cert-test",
},
errMsg: `"host" not set`,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
app := cmdCert()
tempDir := t.TempDir()
certFile := filepath.Join(tempDir, "cert.pem")
keyFile := filepath.Join(tempDir, "key.pem")
err := app.Run(t.Context(), append(c.args, "--out", certFile, "--keyout", keyFile))
require.Error(t, err)
if c.errMsg != "" {
assert.ErrorContains(t, err, c.errMsg)
}
assert.NoFileExists(t, certFile)
assert.NoFileExists(t, keyFile)
})
}
}

View File

@ -18,20 +18,19 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
// argsSet checks that all the required arguments are set. args is a list of // argsSet checks that all the required arguments are set. args is a list of
// arguments that must be set in the passed Context. // arguments that must be set in the passed Context.
func argsSet(c *cli.Context, args ...string) error { func argsSet(c *cli.Command, args ...string) error {
for _, a := range args { for _, a := range args {
if !c.IsSet(a) { if !c.IsSet(a) {
return errors.New(a + " is not set") return errors.New(a + " is not set")
} }
if util.IsEmptyString(c.String(a)) { if c.Value(a) == nil {
return errors.New(a + " is required") return errors.New(a + " is required")
} }
} }
@ -109,7 +108,7 @@ func setupConsoleLogger(level log.Level, colorize bool, out io.Writer) {
log.GetManager().GetLogger(log.DEFAULT).ReplaceAllWriters(writer) log.GetManager().GetLogger(log.DEFAULT).ReplaceAllWriters(writer)
} }
func globalBool(c *cli.Context, name string) bool { func globalBool(c *cli.Command, name string) bool {
for _, ctx := range c.Lineage() { for _, ctx := range c.Lineage() {
if ctx.Bool(name) { if ctx.Bool(name) {
return true return true
@ -120,8 +119,8 @@ func globalBool(c *cli.Context, name string) bool {
// PrepareConsoleLoggerLevel by default, use INFO level for console logger, but some sub-commands (for git/ssh protocol) shouldn't output any log to stdout. // PrepareConsoleLoggerLevel by default, use INFO level for console logger, but some sub-commands (for git/ssh protocol) shouldn't output any log to stdout.
// Any log appears in git stdout pipe will break the git protocol, eg: client can't push and hangs forever. // Any log appears in git stdout pipe will break the git protocol, eg: client can't push and hangs forever.
func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(*cli.Context) error { func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(context.Context, *cli.Command) (context.Context, error) {
return func(c *cli.Context) error { return func(ctx context.Context, c *cli.Command) (context.Context, error) {
level := defaultLevel level := defaultLevel
if globalBool(c, "quiet") { if globalBool(c, "quiet") {
level = log.FATAL level = log.FATAL
@ -130,6 +129,6 @@ func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(*cli.Context) error
level = log.TRACE level = log.TRACE
} }
log.SetConsoleLogger(log.DEFAULT, "console-default", level) log.SetConsoleLogger(log.DEFAULT, "console-default", level)
return nil return ctx, nil
} }
} }

View File

@ -4,11 +4,13 @@
package cmd package cmd
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"strings" "strings"
"github.com/urfave/cli/v2" cli_docs "github.com/urfave/cli-docs/v3"
"github.com/urfave/cli/v3"
) )
// CmdDocs represents the available docs sub-command. // CmdDocs represents the available docs sub-command.
@ -30,16 +32,16 @@ var CmdDocs = &cli.Command{
}, },
} }
func runDocs(ctx *cli.Context) error { func runDocs(_ context.Context, cmd *cli.Command) error {
docs, err := ctx.App.ToMarkdown() docs, err := cli_docs.ToMarkdown(cmd.Root())
if ctx.Bool("man") { if cmd.Bool("man") {
docs, err = ctx.App.ToMan() docs, err = cli_docs.ToMan(cmd.Root())
} }
if err != nil { if err != nil {
return err return err
} }
if !ctx.Bool("man") { if !cmd.Bool("man") {
// Clean up markdown. The following bug was fixed in v2, but is present in v1. // Clean up markdown. The following bug was fixed in v2, but is present in v1.
// It affects markdown output (even though the issue is referring to man pages) // It affects markdown output (even though the issue is referring to man pages)
// https://github.com/urfave/cli/issues/1040 // https://github.com/urfave/cli/issues/1040
@ -51,8 +53,8 @@ func runDocs(ctx *cli.Context) error {
} }
out := os.Stdout out := os.Stdout
if ctx.String("output") != "" { if cmd.String("output") != "" {
fi, err := os.Create(ctx.String("output")) fi, err := os.Create(cmd.String("output"))
if err != nil { if err != nil {
return err return err
} }

View File

@ -20,7 +20,7 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/doctor" "code.gitea.io/gitea/services/doctor"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"xorm.io/xorm" "xorm.io/xorm"
) )
@ -30,7 +30,7 @@ var CmdDoctor = &cli.Command{
Usage: "Diagnose and optionally fix problems, convert or re-create database tables", Usage: "Diagnose and optionally fix problems, convert or re-create database tables",
Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.", Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.",
Subcommands: []*cli.Command{ Commands: []*cli.Command{
cmdDoctorCheck, cmdDoctorCheck,
cmdRecreateTable, cmdRecreateTable,
cmdDoctorConvert, cmdDoctorConvert,
@ -93,16 +93,13 @@ You should back-up your database before doing this and ensure that your database
Action: runRecreateTable, Action: runRecreateTable,
} }
func runRecreateTable(ctx *cli.Context) error { func runRecreateTable(ctx context.Context, cmd *cli.Command) error {
stdCtx, cancel := installSignals()
defer cancel()
// Redirect the default golog to here // Redirect the default golog to here
golog.SetFlags(0) golog.SetFlags(0)
golog.SetPrefix("") golog.SetPrefix("")
golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info)) golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info))
debug := ctx.Bool("debug") debug := cmd.Bool("debug")
setting.MustInstalled() setting.MustInstalled()
setting.LoadDBSetting() setting.LoadDBSetting()
@ -113,15 +110,15 @@ func runRecreateTable(ctx *cli.Context) error {
} }
setting.Database.LogSQL = debug setting.Database.LogSQL = debug
if err := db.InitEngine(stdCtx); err != nil { if err := db.InitEngine(ctx); err != nil {
fmt.Println(err) fmt.Println(err)
fmt.Println("Check if you are using the right config file. You can use a --config directive to specify one.") fmt.Println("Check if you are using the right config file. You can use a --config directive to specify one.")
return nil return nil
} }
args := ctx.Args() args := cmd.Args()
names := make([]string, 0, ctx.NArg()) names := make([]string, 0, cmd.NArg())
for i := 0; i < ctx.NArg(); i++ { for i := 0; i < cmd.NArg(); i++ {
names = append(names, args.Get(i)) names = append(names, args.Get(i))
} }
@ -131,7 +128,7 @@ func runRecreateTable(ctx *cli.Context) error {
} }
recreateTables := migrate_base.RecreateTables(beans...) recreateTables := migrate_base.RecreateTables(beans...)
return db.InitEngineWithMigration(stdCtx, func(ctx context.Context, x *xorm.Engine) error { return db.InitEngineWithMigration(ctx, func(ctx context.Context, x *xorm.Engine) error {
if err := migrations.EnsureUpToDate(ctx, x); err != nil { if err := migrations.EnsureUpToDate(ctx, x); err != nil {
return err return err
} }
@ -139,11 +136,11 @@ func runRecreateTable(ctx *cli.Context) error {
}) })
} }
func setupDoctorDefaultLogger(ctx *cli.Context, colorize bool) { func setupDoctorDefaultLogger(cmd *cli.Command, colorize bool) {
// Silence the default loggers // Silence the default loggers
setupConsoleLogger(log.FATAL, log.CanColorStderr, os.Stderr) setupConsoleLogger(log.FATAL, log.CanColorStderr, os.Stderr)
logFile := ctx.String("log-file") logFile := cmd.String("log-file")
switch logFile { switch logFile {
case "": case "":
return // if no doctor log-file is set, do not show any log from default logger return // if no doctor log-file is set, do not show any log from default logger
@ -161,23 +158,20 @@ func setupDoctorDefaultLogger(ctx *cli.Context, colorize bool) {
} }
} }
func runDoctorCheck(ctx *cli.Context) error { func runDoctorCheck(ctx context.Context, cmd *cli.Command) error {
stdCtx, cancel := installSignals()
defer cancel()
colorize := log.CanColorStdout colorize := log.CanColorStdout
if ctx.IsSet("color") { if cmd.IsSet("color") {
colorize = ctx.Bool("color") colorize = cmd.Bool("color")
} }
setupDoctorDefaultLogger(ctx, colorize) setupDoctorDefaultLogger(cmd, colorize)
// Finally redirect the default golang's log to here // Finally redirect the default golang's log to here
golog.SetFlags(0) golog.SetFlags(0)
golog.SetPrefix("") golog.SetPrefix("")
golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info)) golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info))
if ctx.IsSet("list") { if cmd.IsSet("list") {
w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0) w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0)
_, _ = w.Write([]byte("Default\tName\tTitle\n")) _, _ = w.Write([]byte("Default\tName\tTitle\n"))
doctor.SortChecks(doctor.Checks) doctor.SortChecks(doctor.Checks)
@ -195,12 +189,12 @@ func runDoctorCheck(ctx *cli.Context) error {
} }
var checks []*doctor.Check var checks []*doctor.Check
if ctx.Bool("all") { if cmd.Bool("all") {
checks = make([]*doctor.Check, len(doctor.Checks)) checks = make([]*doctor.Check, len(doctor.Checks))
copy(checks, doctor.Checks) copy(checks, doctor.Checks)
} else if ctx.IsSet("run") { } else if cmd.IsSet("run") {
addDefault := ctx.Bool("default") addDefault := cmd.Bool("default")
runNamesSet := container.SetOf(ctx.StringSlice("run")...) runNamesSet := container.SetOf(cmd.StringSlice("run")...)
for _, check := range doctor.Checks { for _, check := range doctor.Checks {
if (addDefault && check.IsDefault) || runNamesSet.Contains(check.Name) { if (addDefault && check.IsDefault) || runNamesSet.Contains(check.Name) {
checks = append(checks, check) checks = append(checks, check)
@ -217,5 +211,5 @@ func runDoctorCheck(ctx *cli.Context) error {
} }
} }
} }
return doctor.RunChecks(stdCtx, colorize, ctx.Bool("fix"), checks) return doctor.RunChecks(ctx, colorize, cmd.Bool("fix"), checks)
} }

View File

@ -4,13 +4,14 @@
package cmd package cmd
import ( import (
"context"
"fmt" "fmt"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
// cmdDoctorConvert represents the available convert sub-command. // cmdDoctorConvert represents the available convert sub-command.
@ -21,11 +22,8 @@ var cmdDoctorConvert = &cli.Command{
Action: runDoctorConvert, Action: runDoctorConvert,
} }
func runDoctorConvert(ctx *cli.Context) error { func runDoctorConvert(ctx context.Context, cmd *cli.Command) error {
stdCtx, cancel := installSignals() if err := initDB(ctx); err != nil {
defer cancel()
if err := initDB(stdCtx); err != nil {
return err return err
} }

View File

@ -11,7 +11,7 @@ import (
"code.gitea.io/gitea/services/doctor" "code.gitea.io/gitea/services/doctor"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
func TestDoctorRun(t *testing.T) { func TestDoctorRun(t *testing.T) {
@ -22,12 +22,13 @@ func TestDoctorRun(t *testing.T) {
SkipDatabaseInitialization: true, SkipDatabaseInitialization: true,
}) })
app := cli.NewApp() app := &cli.Command{
app.Commands = []*cli.Command{cmdDoctorCheck} Commands: []*cli.Command{cmdDoctorCheck},
err := app.Run([]string{"./gitea", "check", "--run", "test-check"}) }
err := app.Run(t.Context(), []string{"./gitea", "check", "--run", "test-check"})
assert.NoError(t, err) assert.NoError(t, err)
err = app.Run([]string{"./gitea", "check", "--run", "no-such"}) err = app.Run(t.Context(), []string{"./gitea", "check", "--run", "no-such"})
assert.ErrorContains(t, err, `unknown checks: "no-such"`) assert.ErrorContains(t, err, `unknown checks: "no-such"`)
err = app.Run([]string{"./gitea", "check", "--run", "test-check,no-such"}) err = app.Run(t.Context(), []string{"./gitea", "check", "--run", "test-check,no-such"})
assert.ErrorContains(t, err, `unknown checks: "no-such"`) assert.ErrorContains(t, err, `unknown checks: "no-such"`)
} }

View File

@ -5,6 +5,7 @@
package cmd package cmd
import ( import (
"context"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
@ -20,7 +21,7 @@ import (
"gitea.com/go-chi/session" "gitea.com/go-chi/session"
"github.com/mholt/archiver/v3" "github.com/mholt/archiver/v3"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
// CmdDump represents the available dump sub-command. // CmdDump represents the available dump sub-command.
@ -101,17 +102,17 @@ func fatal(format string, args ...any) {
log.Fatal(format, args...) log.Fatal(format, args...)
} }
func runDump(ctx *cli.Context) error { func runDump(ctx context.Context, cmd *cli.Command) error {
setting.MustInstalled() setting.MustInstalled()
quite := ctx.Bool("quiet") quite := cmd.Bool("quiet")
verbose := ctx.Bool("verbose") verbose := cmd.Bool("verbose")
if verbose && quite { if verbose && quite {
fatal("Option --quiet and --verbose cannot both be set") fatal("Option --quiet and --verbose cannot both be set")
} }
// outFileName is either "-" or a file name (will be made absolute) // outFileName is either "-" or a file name (will be made absolute)
outFileName, outType := dump.PrepareFileNameAndType(ctx.String("file"), ctx.String("type")) outFileName, outType := dump.PrepareFileNameAndType(cmd.String("file"), cmd.String("type"))
if outType == "" { if outType == "" {
fatal("Invalid output type") fatal("Invalid output type")
} }
@ -136,10 +137,7 @@ func runDump(ctx *cli.Context) error {
setting.DisableLoggerInit() setting.DisableLoggerInit()
setting.LoadSettings() // cannot access session settings otherwise setting.LoadSettings() // cannot access session settings otherwise
stdCtx, cancel := installSignals() err := db.InitEngine(ctx)
defer cancel()
err := db.InitEngine(stdCtx)
if err != nil { if err != nil {
return err return err
} }
@ -165,7 +163,7 @@ func runDump(ctx *cli.Context) error {
} }
dumper.GlobalExcludeAbsPath(outFileName) dumper.GlobalExcludeAbsPath(outFileName)
if ctx.IsSet("skip-repository") && ctx.Bool("skip-repository") { if cmd.IsSet("skip-repository") && cmd.Bool("skip-repository") {
log.Info("Skip dumping local repositories") log.Info("Skip dumping local repositories")
} else { } else {
log.Info("Dumping local repositories... %s", setting.RepoRootPath) log.Info("Dumping local repositories... %s", setting.RepoRootPath)
@ -173,7 +171,7 @@ func runDump(ctx *cli.Context) error {
fatal("Failed to include repositories: %v", err) fatal("Failed to include repositories: %v", err)
} }
if ctx.IsSet("skip-lfs-data") && ctx.Bool("skip-lfs-data") { if cmd.IsSet("skip-lfs-data") && cmd.Bool("skip-lfs-data") {
log.Info("Skip dumping LFS data") log.Info("Skip dumping LFS data")
} else if !setting.LFS.StartServer { } else if !setting.LFS.StartServer {
log.Info("LFS isn't enabled. Skip dumping LFS data") log.Info("LFS isn't enabled. Skip dumping LFS data")
@ -188,12 +186,12 @@ func runDump(ctx *cli.Context) error {
} }
} }
if ctx.Bool("skip-db") { if cmd.Bool("skip-db") {
// Ensure that we don't dump the database file that may reside in setting.AppDataPath or elsewhere. // Ensure that we don't dump the database file that may reside in setting.AppDataPath or elsewhere.
dumper.GlobalExcludeAbsPath(setting.Database.Path) dumper.GlobalExcludeAbsPath(setting.Database.Path)
log.Info("Skipping database") log.Info("Skipping database")
} else { } else {
tmpDir := ctx.String("tempdir") tmpDir := cmd.String("tempdir")
if _, err := os.Stat(tmpDir); os.IsNotExist(err) { if _, err := os.Stat(tmpDir); os.IsNotExist(err) {
fatal("Path does not exist: %s", tmpDir) fatal("Path does not exist: %s", tmpDir)
} }
@ -209,7 +207,7 @@ func runDump(ctx *cli.Context) error {
} }
}() }()
targetDBType := ctx.String("database") targetDBType := cmd.String("database")
if len(targetDBType) > 0 && targetDBType != setting.Database.Type.String() { if len(targetDBType) > 0 && targetDBType != setting.Database.Type.String() {
log.Info("Dumping database %s => %s...", setting.Database.Type, targetDBType) log.Info("Dumping database %s => %s...", setting.Database.Type, targetDBType)
} else { } else {
@ -230,7 +228,7 @@ func runDump(ctx *cli.Context) error {
fatal("Failed to include specified app.ini: %v", err) fatal("Failed to include specified app.ini: %v", err)
} }
if ctx.IsSet("skip-custom-dir") && ctx.Bool("skip-custom-dir") { if cmd.IsSet("skip-custom-dir") && cmd.Bool("skip-custom-dir") {
log.Info("Skipping custom directory") log.Info("Skipping custom directory")
} else { } else {
customDir, err := os.Stat(setting.CustomPath) customDir, err := os.Stat(setting.CustomPath)
@ -263,7 +261,7 @@ func runDump(ctx *cli.Context) error {
excludes = append(excludes, opts.ProviderConfig) excludes = append(excludes, opts.ProviderConfig)
} }
if ctx.IsSet("skip-index") && ctx.Bool("skip-index") { if cmd.IsSet("skip-index") && cmd.Bool("skip-index") {
excludes = append(excludes, setting.Indexer.RepoPath) excludes = append(excludes, setting.Indexer.RepoPath)
excludes = append(excludes, setting.Indexer.IssuePath) excludes = append(excludes, setting.Indexer.IssuePath)
} }
@ -278,7 +276,7 @@ func runDump(ctx *cli.Context) error {
} }
} }
if ctx.IsSet("skip-attachment-data") && ctx.Bool("skip-attachment-data") { if cmd.IsSet("skip-attachment-data") && cmd.Bool("skip-attachment-data") {
log.Info("Skip dumping attachment data") log.Info("Skip dumping attachment data")
} else if err := storage.Attachments.IterateObjects("", func(objPath string, object storage.Object) error { } else if err := storage.Attachments.IterateObjects("", func(objPath string, object storage.Object) error {
info, err := object.Stat() info, err := object.Stat()
@ -290,7 +288,7 @@ func runDump(ctx *cli.Context) error {
fatal("Failed to dump attachments: %v", err) fatal("Failed to dump attachments: %v", err)
} }
if ctx.IsSet("skip-package-data") && ctx.Bool("skip-package-data") { if cmd.IsSet("skip-package-data") && cmd.Bool("skip-package-data") {
log.Info("Skip dumping package data") log.Info("Skip dumping package data")
} else if !setting.Packages.Enabled { } else if !setting.Packages.Enabled {
log.Info("Packages isn't enabled. Skip dumping package data") log.Info("Packages isn't enabled. Skip dumping package data")
@ -307,7 +305,7 @@ func runDump(ctx *cli.Context) error {
// Doesn't check if LogRootPath exists before processing --skip-log intentionally, // Doesn't check if LogRootPath exists before processing --skip-log intentionally,
// ensuring that it's clear the dump is skipped whether the directory's initialized // ensuring that it's clear the dump is skipped whether the directory's initialized
// yet or not. // yet or not.
if ctx.IsSet("skip-log") && ctx.Bool("skip-log") { if cmd.IsSet("skip-log") && cmd.Bool("skip-log") {
log.Info("Skip dumping log files") log.Info("Skip dumping log files")
} else { } else {
isExist, err := util.IsExist(setting.Log.RootPath) isExist, err := util.IsExist(setting.Log.RootPath)

View File

@ -19,7 +19,7 @@ import (
"code.gitea.io/gitea/services/convert" "code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/migrations" "code.gitea.io/gitea/services/migrations"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
// CmdDumpRepository represents the available dump repository sub-command. // CmdDumpRepository represents the available dump repository sub-command.
@ -79,16 +79,13 @@ wiki, issues, labels, releases, release_assets, milestones, pull_requests, comme
}, },
} }
func runDumpRepository(ctx *cli.Context) error { func runDumpRepository(ctx context.Context, cmd *cli.Command) error {
setupConsoleLogger(log.INFO, log.CanColorStderr, os.Stderr) setupConsoleLogger(log.INFO, log.CanColorStderr, os.Stderr)
setting.DisableLoggerInit() setting.DisableLoggerInit()
setting.LoadSettings() // cannot access skip_tls_verify settings otherwise setting.LoadSettings() // cannot access skip_tls_verify settings otherwise
stdCtx, cancel := installSignals() if err := initDB(ctx); err != nil {
defer cancel()
if err := initDB(stdCtx); err != nil {
return err return err
} }
@ -105,8 +102,8 @@ func runDumpRepository(ctx *cli.Context) error {
var ( var (
serviceType structs.GitServiceType serviceType structs.GitServiceType
cloneAddr = ctx.String("clone_addr") cloneAddr = cmd.String("clone_addr")
serviceStr = ctx.String("git_service") serviceStr = cmd.String("git_service")
) )
if strings.HasPrefix(strings.ToLower(cloneAddr), "https://github.com/") { if strings.HasPrefix(strings.ToLower(cloneAddr), "https://github.com/") {
@ -124,13 +121,13 @@ func runDumpRepository(ctx *cli.Context) error {
opts := base.MigrateOptions{ opts := base.MigrateOptions{
GitServiceType: serviceType, GitServiceType: serviceType,
CloneAddr: cloneAddr, CloneAddr: cloneAddr,
AuthUsername: ctx.String("auth_username"), AuthUsername: cmd.String("auth_username"),
AuthPassword: ctx.String("auth_password"), AuthPassword: cmd.String("auth_password"),
AuthToken: ctx.String("auth_token"), AuthToken: cmd.String("auth_token"),
RepoName: ctx.String("repo_name"), RepoName: cmd.String("repo_name"),
} }
if len(ctx.String("units")) == 0 { if len(cmd.String("units")) == 0 {
opts.Wiki = true opts.Wiki = true
opts.Issues = true opts.Issues = true
opts.Milestones = true opts.Milestones = true
@ -140,7 +137,7 @@ func runDumpRepository(ctx *cli.Context) error {
opts.PullRequests = true opts.PullRequests = true
opts.ReleaseAssets = true opts.ReleaseAssets = true
} else { } else {
units := strings.Split(ctx.String("units"), ",") units := strings.Split(cmd.String("units"), ",")
for _, unit := range units { for _, unit := range units {
switch strings.ToLower(strings.TrimSpace(unit)) { switch strings.ToLower(strings.TrimSpace(unit)) {
case "": case "":
@ -169,7 +166,7 @@ func runDumpRepository(ctx *cli.Context) error {
// the repo_dir will be removed if error occurs in DumpRepository // the repo_dir will be removed if error occurs in DumpRepository
// make sure the directory doesn't exist or is empty, prevent from deleting user files // make sure the directory doesn't exist or is empty, prevent from deleting user files
repoDir := ctx.String("repo_dir") repoDir := cmd.String("repo_dir")
if exists, err := util.IsExist(repoDir); err != nil { if exists, err := util.IsExist(repoDir); err != nil {
return fmt.Errorf("unable to stat repo_dir %q: %w", repoDir, err) return fmt.Errorf("unable to stat repo_dir %q: %w", repoDir, err)
} else if exists { } else if exists {
@ -184,7 +181,7 @@ func runDumpRepository(ctx *cli.Context) error {
if err := migrations.DumpRepository( if err := migrations.DumpRepository(
context.Background(), context.Background(),
repoDir, repoDir,
ctx.String("owner_name"), cmd.String("owner_name"),
opts, opts,
); err != nil { ); err != nil {
log.Fatal("Failed to dump repository: %v", err) log.Fatal("Failed to dump repository: %v", err)

View File

@ -4,6 +4,7 @@
package cmd package cmd
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"os" "os"
@ -19,7 +20,7 @@ import (
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"github.com/gobwas/glob" "github.com/gobwas/glob"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
// CmdEmbedded represents the available extract sub-command. // CmdEmbedded represents the available extract sub-command.
@ -28,7 +29,7 @@ var (
Name: "embedded", Name: "embedded",
Usage: "Extract embedded resources", Usage: "Extract embedded resources",
Description: "A command for extracting embedded resources, like templates and images", Description: "A command for extracting embedded resources, like templates and images",
Subcommands: []*cli.Command{ Commands: []*cli.Command{
subcmdList, subcmdList,
subcmdView, subcmdView,
subcmdExtract, subcmdExtract,
@ -100,7 +101,7 @@ type assetFile struct {
path string path string
} }
func initEmbeddedExtractor(c *cli.Context) error { func initEmbeddedExtractor(c *cli.Command) error {
setupConsoleLogger(log.ERROR, log.CanColorStderr, os.Stderr) setupConsoleLogger(log.ERROR, log.CanColorStderr, os.Stderr)
patterns, err := compileCollectPatterns(c.Args().Slice()) patterns, err := compileCollectPatterns(c.Args().Slice())
@ -115,7 +116,7 @@ func initEmbeddedExtractor(c *cli.Context) error {
return nil return nil
} }
func runList(c *cli.Context) error { func runList(_ context.Context, c *cli.Command) error {
if err := runListDo(c); err != nil { if err := runListDo(c); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err) fmt.Fprintf(os.Stderr, "%v\n", err)
return err return err
@ -123,7 +124,7 @@ func runList(c *cli.Context) error {
return nil return nil
} }
func runView(c *cli.Context) error { func runView(_ context.Context, c *cli.Command) error {
if err := runViewDo(c); err != nil { if err := runViewDo(c); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err) fmt.Fprintf(os.Stderr, "%v\n", err)
return err return err
@ -131,7 +132,7 @@ func runView(c *cli.Context) error {
return nil return nil
} }
func runExtract(c *cli.Context) error { func runExtract(_ context.Context, c *cli.Command) error {
if err := runExtractDo(c); err != nil { if err := runExtractDo(c); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err) fmt.Fprintf(os.Stderr, "%v\n", err)
return err return err
@ -139,7 +140,7 @@ func runExtract(c *cli.Context) error {
return nil return nil
} }
func runListDo(c *cli.Context) error { func runListDo(c *cli.Command) error {
if err := initEmbeddedExtractor(c); err != nil { if err := initEmbeddedExtractor(c); err != nil {
return err return err
} }
@ -151,7 +152,7 @@ func runListDo(c *cli.Context) error {
return nil return nil
} }
func runViewDo(c *cli.Context) error { func runViewDo(c *cli.Command) error {
if err := initEmbeddedExtractor(c); err != nil { if err := initEmbeddedExtractor(c); err != nil {
return err return err
} }
@ -174,7 +175,7 @@ func runViewDo(c *cli.Context) error {
return nil return nil
} }
func runExtractDo(c *cli.Context) error { func runExtractDo(c *cli.Command) error {
if err := initEmbeddedExtractor(c); err != nil { if err := initEmbeddedExtractor(c); err != nil {
return err return err
} }
@ -271,7 +272,7 @@ func extractAsset(d string, a assetFile, overwrite, rename bool) error {
return nil return nil
} }
func collectAssetFilesByPattern(c *cli.Context, globs []glob.Glob, path string, layer *assetfs.Layer) { func collectAssetFilesByPattern(c *cli.Command, globs []glob.Glob, path string, layer *assetfs.Layer) {
fs := assetfs.Layered(layer) fs := assetfs.Layered(layer)
files, err := fs.ListAllFiles(".", true) files, err := fs.ListAllFiles(".", true)
if err != nil { if err != nil {

View File

@ -5,13 +5,14 @@
package cmd package cmd
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"code.gitea.io/gitea/modules/generate" "code.gitea.io/gitea/modules/generate"
"github.com/mattn/go-isatty" "github.com/mattn/go-isatty"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
var ( var (
@ -19,7 +20,7 @@ var (
CmdGenerate = &cli.Command{ CmdGenerate = &cli.Command{
Name: "generate", Name: "generate",
Usage: "Generate Gitea's secrets/keys/tokens", Usage: "Generate Gitea's secrets/keys/tokens",
Subcommands: []*cli.Command{ Commands: []*cli.Command{
subcmdSecret, subcmdSecret,
}, },
} }
@ -27,7 +28,7 @@ var (
subcmdSecret = &cli.Command{ subcmdSecret = &cli.Command{
Name: "secret", Name: "secret",
Usage: "Generate a secret token", Usage: "Generate a secret token",
Subcommands: []*cli.Command{ Commands: []*cli.Command{
microcmdGenerateInternalToken, microcmdGenerateInternalToken,
microcmdGenerateLfsJwtSecret, microcmdGenerateLfsJwtSecret,
microcmdGenerateSecretKey, microcmdGenerateSecretKey,
@ -54,7 +55,7 @@ var (
} }
) )
func runGenerateInternalToken(c *cli.Context) error { func runGenerateInternalToken(_ context.Context, c *cli.Command) error {
internalToken, err := generate.NewInternalToken() internalToken, err := generate.NewInternalToken()
if err != nil { if err != nil {
return err return err
@ -69,7 +70,7 @@ func runGenerateInternalToken(c *cli.Context) error {
return nil return nil
} }
func runGenerateLfsJwtSecret(c *cli.Context) error { func runGenerateLfsJwtSecret(_ context.Context, c *cli.Command) error {
_, jwtSecretBase64, err := generate.NewJwtSecretWithBase64() _, jwtSecretBase64, err := generate.NewJwtSecretWithBase64()
if err != nil { if err != nil {
return err return err
@ -84,7 +85,7 @@ func runGenerateLfsJwtSecret(c *cli.Context) error {
return nil return nil
} }
func runGenerateSecretKey(c *cli.Context) error { func runGenerateSecretKey(_ context.Context, c *cli.Command) error {
secretKey, err := generate.NewSecretKey() secretKey, err := generate.NewSecretKey()
if err != nil { if err != nil {
return err return err

View File

@ -20,7 +20,7 @@ import (
repo_module "code.gitea.io/gitea/modules/repository" repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
const ( const (
@ -34,7 +34,7 @@ var (
Usage: "(internal) Should only be called by Git", Usage: "(internal) Should only be called by Git",
Description: "Delegate commands to corresponding Git hooks", Description: "Delegate commands to corresponding Git hooks",
Before: PrepareConsoleLoggerLevel(log.FATAL), Before: PrepareConsoleLoggerLevel(log.FATAL),
Subcommands: []*cli.Command{ Commands: []*cli.Command{
subcmdHookPreReceive, subcmdHookPreReceive,
subcmdHookUpdate, subcmdHookUpdate,
subcmdHookPostReceive, subcmdHookPostReceive,
@ -161,12 +161,10 @@ func (n *nilWriter) WriteString(s string) (int, error) {
return len(s), nil return len(s), nil
} }
func runHookPreReceive(c *cli.Context) error { func runHookPreReceive(ctx context.Context, c *cli.Command) error {
if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal { if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
return nil return nil
} }
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug")) setup(ctx, c.Bool("debug"))
@ -292,7 +290,7 @@ Gitea or set your environment appropriately.`, "")
// runHookUpdate avoid to do heavy operations on update hook because it will be // runHookUpdate avoid to do heavy operations on update hook because it will be
// invoked for every ref update which does not like pre-receive and post-receive // invoked for every ref update which does not like pre-receive and post-receive
func runHookUpdate(c *cli.Context) error { func runHookUpdate(_ context.Context, c *cli.Command) error {
if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal { if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal {
return nil return nil
} }
@ -309,15 +307,12 @@ func runHookUpdate(c *cli.Context) error {
return nil return nil
} }
func runHookPostReceive(c *cli.Context) error { func runHookPostReceive(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug")) setup(ctx, c.Bool("debug"))
// First of all run update-server-info no matter what // First of all run update-server-info no matter what
if _, _, err := git.NewCommand("update-server-info").RunStdString(ctx, nil); err != nil { if _, _, err := git.NewCommand("update-server-info").RunStdString(ctx, nil); err != nil {
return fmt.Errorf("Failed to call 'git update-server-info': %w", err) return fmt.Errorf("failed to call 'git update-server-info': %w", err)
} }
// Now if we're an internal don't do anything else // Now if we're an internal don't do anything else
@ -496,10 +491,7 @@ func pushOptions() map[string]string {
return opts return opts
} }
func runHookProcReceive(c *cli.Context) error { func runHookProcReceive(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug")) setup(ctx, c.Bool("debug"))
if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 { if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {

View File

@ -4,6 +4,7 @@
package cmd package cmd
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
@ -11,7 +12,7 @@ import (
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private" "code.gitea.io/gitea/modules/private"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
// CmdKeys represents the available keys sub-command // CmdKeys represents the available keys sub-command
@ -49,7 +50,7 @@ var CmdKeys = &cli.Command{
}, },
} }
func runKeys(c *cli.Context) error { func runKeys(ctx context.Context, c *cli.Command) error {
if !c.IsSet("username") { if !c.IsSet("username") {
return errors.New("No username provided") return errors.New("No username provided")
} }
@ -68,9 +69,6 @@ func runKeys(c *cli.Context) error {
return errors.New("No key type and content provided") return errors.New("No key type and content provided")
} }
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug")) setup(ctx, c.Bool("debug"))
authorizedString, extra := private.AuthorizedPublicKeyByContent(ctx, content) authorizedString, extra := private.AuthorizedPublicKeyByContent(ctx, content)
@ -78,6 +76,6 @@ func runKeys(c *cli.Context) error {
if extra.Error != nil { if extra.Error != nil {
return extra.Error return extra.Error
} }
_, _ = fmt.Fprintln(c.App.Writer, strings.TrimSpace(authorizedString.Text)) _, _ = fmt.Fprintln(c.Root().Writer, strings.TrimSpace(authorizedString.Text))
return nil return nil
} }

View File

@ -4,24 +4,18 @@
package cmd package cmd
import ( import (
"context"
"fmt" "fmt"
"code.gitea.io/gitea/modules/private" "code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
func runSendMail(c *cli.Context) error { func runSendMail(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals()
defer cancel()
setting.MustInstalled() setting.MustInstalled()
if err := argsSet(c, "title"); err != nil {
return err
}
subject := c.String("title") subject := c.String("title")
confirmSkiped := c.Bool("force") confirmSkiped := c.Bool("force")
body := c.String("content") body := c.String("content")

View File

@ -4,6 +4,7 @@
package cmd package cmd
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"strings" "strings"
@ -11,7 +12,7 @@ import (
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
// cmdHelp is our own help subcommand with more information // cmdHelp is our own help subcommand with more information
@ -22,18 +23,18 @@ func cmdHelp() *cli.Command {
Aliases: []string{"h"}, Aliases: []string{"h"},
Usage: "Shows a list of commands or help for one command", Usage: "Shows a list of commands or help for one command",
ArgsUsage: "[command]", ArgsUsage: "[command]",
Action: func(c *cli.Context) (err error) { Action: func(ctx context.Context, c *cli.Command) (err error) {
lineage := c.Lineage() // The order is from child to parent: help, doctor, Gitea, {Command:nil} lineage := c.Lineage() // The order is from child to parent: help, doctor, Gitea
targetCmdIdx := 0 targetCmdIdx := 0
if c.Command.Name == "help" { if c.Name == "help" {
targetCmdIdx = 1 targetCmdIdx = 1
} }
if lineage[targetCmdIdx+1].Command != nil { if lineage[targetCmdIdx] != lineage[targetCmdIdx].Root() {
err = cli.ShowCommandHelp(lineage[targetCmdIdx+1], lineage[targetCmdIdx].Command.Name) err = cli.ShowCommandHelp(ctx, lineage[targetCmdIdx+1] /* parent cmd */, lineage[targetCmdIdx].Name /* sub cmd */)
} else { } else {
err = cli.ShowAppHelp(c) err = cli.ShowAppHelp(c)
} }
_, _ = fmt.Fprintf(c.App.Writer, ` _, _ = fmt.Fprintf(c.Root().Writer, `
DEFAULT CONFIGURATION: DEFAULT CONFIGURATION:
AppPath: %s AppPath: %s
WorkPath: %s WorkPath: %s
@ -74,25 +75,25 @@ func appGlobalFlags() []cli.Flag {
} }
} }
func prepareSubcommandWithConfig(command *cli.Command, globalFlags []cli.Flag) { func prepareSubcommandWithGlobalFlags(command *cli.Command) {
command.Flags = append(append([]cli.Flag{}, globalFlags...), command.Flags...) command.Flags = append(append([]cli.Flag{}, appGlobalFlags()...), command.Flags...)
command.Action = prepareWorkPathAndCustomConf(command.Action) command.Action = prepareWorkPathAndCustomConf(command.Action)
command.HideHelp = true command.HideHelp = true
if command.Name != "help" { if command.Name != "help" {
command.Subcommands = append(command.Subcommands, cmdHelp()) command.Commands = append(command.Commands, cmdHelp())
} }
for i := range command.Subcommands { for i := range command.Commands {
prepareSubcommandWithConfig(command.Subcommands[i], globalFlags) prepareSubcommandWithGlobalFlags(command.Commands[i])
} }
} }
// prepareWorkPathAndCustomConf wraps the Action to prepare the work path and custom config // prepareWorkPathAndCustomConf wraps the Action to prepare the work path and custom config
// It can't use "Before", because each level's sub-command's Before will be called one by one, so the "init" would be done multiple times // It can't use "Before", because each level's sub-command's Before will be called one by one, so the "init" would be done multiple times
func prepareWorkPathAndCustomConf(action cli.ActionFunc) func(ctx *cli.Context) error { func prepareWorkPathAndCustomConf(action cli.ActionFunc) func(context.Context, *cli.Command) error {
return func(ctx *cli.Context) error { return func(ctx context.Context, cmd *cli.Command) error {
var args setting.ArgWorkPathAndCustomConf var args setting.ArgWorkPathAndCustomConf
// from children to parent, check the global flags // from children to parent, check the global flags
for _, curCtx := range ctx.Lineage() { for _, curCtx := range cmd.Lineage() {
if curCtx.IsSet("work-path") && args.WorkPath == "" { if curCtx.IsSet("work-path") && args.WorkPath == "" {
args.WorkPath = curCtx.String("work-path") args.WorkPath = curCtx.String("work-path")
} }
@ -104,11 +105,11 @@ func prepareWorkPathAndCustomConf(action cli.ActionFunc) func(ctx *cli.Context)
} }
} }
setting.InitWorkPathAndCommonConfig(os.Getenv, args) setting.InitWorkPathAndCommonConfig(os.Getenv, args)
if ctx.Bool("help") || action == nil { if cmd.Bool("help") || action == nil {
// the default behavior of "urfave/cli": "nil action" means "show help" // the default behavior of "urfave/cli": "nil action" means "show help"
return cmdHelp().Action(ctx) return cmdHelp().Action(ctx, cmd)
} }
return action(ctx) return action(ctx, cmd)
} }
} }
@ -117,14 +118,13 @@ type AppVersion struct {
Extra string Extra string
} }
func NewMainApp(appVer AppVersion) *cli.App { func NewMainApp(appVer AppVersion) *cli.Command {
app := cli.NewApp() app := &cli.Command{}
app.Name = "Gitea" app.Name = "gitea" // must be lower-cased because it appears in the "USAGE" section like "gitea doctor [command [command options]]"
app.HelpName = "gitea"
app.Usage = "A painless self-hosted Git service" app.Usage = "A painless self-hosted Git service"
app.Description = `Gitea program contains "web" and other subcommands. If no subcommand is given, it starts the web server by default. Use "web" subcommand for more web server arguments, use other subcommands for other purposes.` app.Description = `Gitea program contains "web" and other subcommands. If no subcommand is given, it starts the web server by default. Use "web" subcommand for more web server arguments, use other subcommands for other purposes.`
app.Version = appVer.Version + appVer.Extra app.Version = appVer.Version + appVer.Extra
app.EnableBashCompletion = true app.EnableShellCompletion = true
// these sub-commands need to use config file // these sub-commands need to use config file
subCmdWithConfig := []*cli.Command{ subCmdWithConfig := []*cli.Command{
@ -147,20 +147,19 @@ func NewMainApp(appVer AppVersion) *cli.App {
// these sub-commands do not need the config file, and they do not depend on any path or environment variable. // these sub-commands do not need the config file, and they do not depend on any path or environment variable.
subCmdStandalone := []*cli.Command{ subCmdStandalone := []*cli.Command{
CmdCert, cmdCert(),
CmdGenerate, CmdGenerate,
CmdDocs, CmdDocs,
} }
app.DefaultCommand = CmdWeb.Name app.DefaultCommand = CmdWeb.Name
globalFlags := appGlobalFlags()
app.Flags = append(app.Flags, cli.VersionFlag) app.Flags = append(app.Flags, cli.VersionFlag)
app.Flags = append(app.Flags, globalFlags...) app.Flags = append(app.Flags, appGlobalFlags()...)
app.HideHelp = true // use our own help action to show helps (with more information like default config) app.HideHelp = true // use our own help action to show helps (with more information like default config)
app.Before = PrepareConsoleLoggerLevel(log.INFO) app.Before = PrepareConsoleLoggerLevel(log.INFO)
for i := range subCmdWithConfig { for i := range subCmdWithConfig {
prepareSubcommandWithConfig(subCmdWithConfig[i], globalFlags) prepareSubcommandWithGlobalFlags(subCmdWithConfig[i])
} }
app.Commands = append(app.Commands, subCmdWithConfig...) app.Commands = append(app.Commands, subCmdWithConfig...)
app.Commands = append(app.Commands, subCmdStandalone...) app.Commands = append(app.Commands, subCmdStandalone...)
@ -169,8 +168,10 @@ func NewMainApp(appVer AppVersion) *cli.App {
return app return app
} }
func RunMainApp(app *cli.App, args ...string) error { func RunMainApp(app *cli.Command, args ...string) error {
err := app.Run(args) ctx, cancel := installSignals()
defer cancel()
err := app.Run(ctx, args)
if err == nil { if err == nil {
return nil return nil
} }

View File

@ -4,6 +4,7 @@
package cmd package cmd
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -16,7 +17,7 @@ import (
"code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
@ -27,10 +28,10 @@ func makePathOutput(workPath, customPath, customConf string) string {
return fmt.Sprintf("WorkPath=%s\nCustomPath=%s\nCustomConf=%s", workPath, customPath, customConf) return fmt.Sprintf("WorkPath=%s\nCustomPath=%s\nCustomConf=%s", workPath, customPath, customConf)
} }
func newTestApp(testCmdAction func(ctx *cli.Context) error) *cli.App { func newTestApp(testCmdAction cli.ActionFunc) *cli.Command {
app := NewMainApp(AppVersion{}) app := NewMainApp(AppVersion{})
testCmd := &cli.Command{Name: "test-cmd", Action: testCmdAction} testCmd := &cli.Command{Name: "test-cmd", Action: testCmdAction}
prepareSubcommandWithConfig(testCmd, appGlobalFlags()) prepareSubcommandWithGlobalFlags(testCmd)
app.Commands = append(app.Commands, testCmd) app.Commands = append(app.Commands, testCmd)
app.DefaultCommand = testCmd.Name app.DefaultCommand = testCmd.Name
return app return app
@ -42,7 +43,7 @@ type runResult struct {
ExitCode int ExitCode int
} }
func runTestApp(app *cli.App, args ...string) (runResult, error) { func runTestApp(app *cli.Command, args ...string) (runResult, error) {
outBuf := new(strings.Builder) outBuf := new(strings.Builder)
errBuf := new(strings.Builder) errBuf := new(strings.Builder)
app.Writer = outBuf app.Writer = outBuf
@ -65,7 +66,7 @@ func TestCliCmd(t *testing.T) {
defaultCustomConf := filepath.Join(defaultCustomPath, "conf/app.ini") defaultCustomConf := filepath.Join(defaultCustomPath, "conf/app.ini")
cli.CommandHelpTemplate = "(command help template)" cli.CommandHelpTemplate = "(command help template)"
cli.AppHelpTemplate = "(app help template)" cli.RootCommandHelpTemplate = "(app help template)"
cli.SubcommandHelpTemplate = "(subcommand help template)" cli.SubcommandHelpTemplate = "(subcommand help template)"
cases := []struct { cases := []struct {
@ -109,12 +110,12 @@ func TestCliCmd(t *testing.T) {
}, },
} }
app := newTestApp(func(ctx *cli.Context) error {
_, _ = fmt.Fprint(ctx.App.Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf))
return nil
})
for _, c := range cases { for _, c := range cases {
t.Run(c.cmd, func(t *testing.T) { t.Run(c.cmd, func(t *testing.T) {
app := newTestApp(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 { for k, v := range c.env {
t.Setenv(k, v) t.Setenv(k, v)
} }
@ -128,28 +129,28 @@ func TestCliCmd(t *testing.T) {
} }
func TestCliCmdError(t *testing.T) { func TestCliCmdError(t *testing.T) {
app := newTestApp(func(ctx *cli.Context) error { return errors.New("normal error") }) app := newTestApp(func(ctx context.Context, cmd *cli.Command) error { return errors.New("normal error") })
r, err := runTestApp(app, "./gitea", "test-cmd") r, err := runTestApp(app, "./gitea", "test-cmd")
assert.Error(t, err) assert.Error(t, err)
assert.Equal(t, 1, r.ExitCode) assert.Equal(t, 1, r.ExitCode)
assert.Empty(t, r.Stdout) assert.Empty(t, r.Stdout)
assert.Equal(t, "Command error: normal error\n", r.Stderr) assert.Equal(t, "Command error: normal error\n", r.Stderr)
app = newTestApp(func(ctx *cli.Context) error { return cli.Exit("exit error", 2) }) app = newTestApp(func(ctx context.Context, cmd *cli.Command) error { return cli.Exit("exit error", 2) })
r, err = runTestApp(app, "./gitea", "test-cmd") r, err = runTestApp(app, "./gitea", "test-cmd")
assert.Error(t, err) assert.Error(t, err)
assert.Equal(t, 2, r.ExitCode) assert.Equal(t, 2, r.ExitCode)
assert.Empty(t, r.Stdout) assert.Empty(t, r.Stdout)
assert.Equal(t, "exit error\n", r.Stderr) assert.Equal(t, "exit error\n", r.Stderr)
app = newTestApp(func(ctx *cli.Context) error { return nil }) app = newTestApp(func(ctx context.Context, cmd *cli.Command) error { return nil })
r, err = runTestApp(app, "./gitea", "test-cmd", "--no-such") r, err = runTestApp(app, "./gitea", "test-cmd", "--no-such")
assert.Error(t, err) assert.Error(t, err)
assert.Equal(t, 1, r.ExitCode) assert.Equal(t, 1, r.ExitCode)
assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n\n", r.Stdout) assert.Empty(t, r.Stdout)
assert.Empty(t, r.Stderr) // the cli package's strange behavior, the error message is not in stderr .... assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n\n", r.Stderr)
app = newTestApp(func(ctx *cli.Context) error { return nil }) app = newTestApp(func(ctx context.Context, cmd *cli.Command) error { return nil })
r, err = runTestApp(app, "./gitea", "test-cmd") r, err = runTestApp(app, "./gitea", "test-cmd")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, -1, r.ExitCode) // the cli.OsExiter is not called assert.Equal(t, -1, r.ExitCode) // the cli.OsExiter is not called

View File

@ -4,12 +4,13 @@
package cmd package cmd
import ( import (
"context"
"os" "os"
"time" "time"
"code.gitea.io/gitea/modules/private" "code.gitea.io/gitea/modules/private"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
var ( var (
@ -18,7 +19,7 @@ var (
Name: "manager", Name: "manager",
Usage: "Manage the running gitea process", Usage: "Manage the running gitea process",
Description: "This is a command for managing the running gitea process", Description: "This is a command for managing the running gitea process",
Subcommands: []*cli.Command{ Commands: []*cli.Command{
subcmdShutdown, subcmdShutdown,
subcmdRestart, subcmdRestart,
subcmdReloadTemplates, subcmdReloadTemplates,
@ -108,46 +109,31 @@ var (
} }
) )
func runShutdown(c *cli.Context) error { func runShutdown(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug")) setup(ctx, c.Bool("debug"))
extra := private.Shutdown(ctx) extra := private.Shutdown(ctx)
return handleCliResponseExtra(extra) return handleCliResponseExtra(extra)
} }
func runRestart(c *cli.Context) error { func runRestart(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug")) setup(ctx, c.Bool("debug"))
extra := private.Restart(ctx) extra := private.Restart(ctx)
return handleCliResponseExtra(extra) return handleCliResponseExtra(extra)
} }
func runReloadTemplates(c *cli.Context) error { func runReloadTemplates(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug")) setup(ctx, c.Bool("debug"))
extra := private.ReloadTemplates(ctx) extra := private.ReloadTemplates(ctx)
return handleCliResponseExtra(extra) return handleCliResponseExtra(extra)
} }
func runFlushQueues(c *cli.Context) error { func runFlushQueues(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug")) setup(ctx, c.Bool("debug"))
extra := private.FlushQueues(ctx, c.Duration("timeout"), c.Bool("non-blocking")) extra := private.FlushQueues(ctx, c.Duration("timeout"), c.Bool("non-blocking"))
return handleCliResponseExtra(extra) return handleCliResponseExtra(extra)
} }
func runProcesses(c *cli.Context) error { func runProcesses(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug")) setup(ctx, c.Bool("debug"))
extra := private.Processes(ctx, os.Stdout, c.Bool("flat"), c.Bool("no-system"), c.Bool("stacktraces"), c.Bool("json"), c.String("cancel")) extra := private.Processes(ctx, os.Stdout, c.Bool("flat"), c.Bool("no-system"), c.Bool("stacktraces"), c.Bool("json"), c.String("cancel"))
return handleCliResponseExtra(extra) return handleCliResponseExtra(extra)

View File

@ -4,6 +4,7 @@
package cmd package cmd
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"os" "os"
@ -11,7 +12,7 @@ import (
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/private" "code.gitea.io/gitea/modules/private"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
var ( var (
@ -60,7 +61,7 @@ var (
subcmdLogging = &cli.Command{ subcmdLogging = &cli.Command{
Name: "logging", Name: "logging",
Usage: "Adjust logging commands", Usage: "Adjust logging commands",
Subcommands: []*cli.Command{ Commands: []*cli.Command{
{ {
Name: "pause", Name: "pause",
Usage: "Pause logging (Gitea will buffer logs up to a certain point and will drop them after that point)", Usage: "Pause logging (Gitea will buffer logs up to a certain point and will drop them after that point)",
@ -104,7 +105,7 @@ var (
}, { }, {
Name: "add", Name: "add",
Usage: "Add a logger", Usage: "Add a logger",
Subcommands: []*cli.Command{ Commands: []*cli.Command{
{ {
Name: "file", Name: "file",
Usage: "Add a file logger", Usage: "Add a file logger",
@ -195,10 +196,7 @@ var (
} }
) )
func runRemoveLogger(c *cli.Context) error { func runRemoveLogger(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug")) setup(ctx, c.Bool("debug"))
logger := c.String("logger") logger := c.String("logger")
if len(logger) == 0 { if len(logger) == 0 {
@ -210,10 +208,7 @@ func runRemoveLogger(c *cli.Context) error {
return handleCliResponseExtra(extra) return handleCliResponseExtra(extra)
} }
func runAddConnLogger(c *cli.Context) error { func runAddConnLogger(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug")) setup(ctx, c.Bool("debug"))
vals := map[string]any{} vals := map[string]any{}
mode := "conn" mode := "conn"
@ -237,13 +232,10 @@ func runAddConnLogger(c *cli.Context) error {
if c.IsSet("reconnect-on-message") { if c.IsSet("reconnect-on-message") {
vals["reconnectOnMsg"] = c.Bool("reconnect-on-message") vals["reconnectOnMsg"] = c.Bool("reconnect-on-message")
} }
return commonAddLogger(c, mode, vals) return commonAddLogger(ctx, c, mode, vals)
} }
func runAddFileLogger(c *cli.Context) error { func runAddFileLogger(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug")) setup(ctx, c.Bool("debug"))
vals := map[string]any{} vals := map[string]any{}
mode := "file" mode := "file"
@ -270,10 +262,10 @@ func runAddFileLogger(c *cli.Context) error {
if c.IsSet("compression-level") { if c.IsSet("compression-level") {
vals["compressionLevel"] = c.Int("compression-level") vals["compressionLevel"] = c.Int("compression-level")
} }
return commonAddLogger(c, mode, vals) return commonAddLogger(ctx, c, mode, vals)
} }
func commonAddLogger(c *cli.Context, mode string, vals map[string]any) error { func commonAddLogger(ctx context.Context, c *cli.Command, mode string, vals map[string]any) error {
if len(c.String("level")) > 0 { if len(c.String("level")) > 0 {
vals["level"] = log.LevelFromString(c.String("level")).String() vals["level"] = log.LevelFromString(c.String("level")).String()
} }
@ -300,46 +292,33 @@ func commonAddLogger(c *cli.Context, mode string, vals map[string]any) error {
if c.IsSet("writer") { if c.IsSet("writer") {
writer = c.String("writer") writer = c.String("writer")
} }
ctx, cancel := installSignals()
defer cancel()
extra := private.AddLogger(ctx, logger, writer, mode, vals) extra := private.AddLogger(ctx, logger, writer, mode, vals)
return handleCliResponseExtra(extra) return handleCliResponseExtra(extra)
} }
func runPauseLogging(c *cli.Context) error { func runPauseLogging(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug")) setup(ctx, c.Bool("debug"))
userMsg := private.PauseLogging(ctx) userMsg := private.PauseLogging(ctx)
_, _ = fmt.Fprintln(os.Stdout, userMsg) _, _ = fmt.Fprintln(os.Stdout, userMsg)
return nil return nil
} }
func runResumeLogging(c *cli.Context) error { func runResumeLogging(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug")) setup(ctx, c.Bool("debug"))
userMsg := private.ResumeLogging(ctx) userMsg := private.ResumeLogging(ctx)
_, _ = fmt.Fprintln(os.Stdout, userMsg) _, _ = fmt.Fprintln(os.Stdout, userMsg)
return nil return nil
} }
func runReleaseReopenLogging(c *cli.Context) error { func runReleaseReopenLogging(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug")) setup(ctx, c.Bool("debug"))
userMsg := private.ReleaseReopenLogging(ctx) userMsg := private.ReleaseReopenLogging(ctx)
_, _ = fmt.Fprintln(os.Stdout, userMsg) _, _ = fmt.Fprintln(os.Stdout, userMsg)
return nil return nil
} }
func runSetLogSQL(c *cli.Context) error { func runSetLogSQL(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals()
defer cancel()
setup(ctx, c.Bool("debug")) setup(ctx, c.Bool("debug"))
extra := private.SetLogSQL(ctx, !c.Bool("off")) extra := private.SetLogSQL(ctx, !c.Bool("off"))

View File

@ -11,7 +11,7 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/versioned_migration" "code.gitea.io/gitea/services/versioned_migration"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
// CmdMigrate represents the available migrate sub-command. // CmdMigrate represents the available migrate sub-command.
@ -22,11 +22,8 @@ var CmdMigrate = &cli.Command{
Action: runMigrate, Action: runMigrate,
} }
func runMigrate(ctx *cli.Context) error { func runMigrate(ctx context.Context, c *cli.Command) error {
stdCtx, cancel := installSignals() if err := initDB(ctx); err != nil {
defer cancel()
if err := initDB(stdCtx); err != nil {
return err return err
} }

View File

@ -22,7 +22,7 @@ import (
"code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/services/versioned_migration" "code.gitea.io/gitea/services/versioned_migration"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
// CmdMigrateStorage represents the available migrate storage sub-command. // CmdMigrateStorage represents the available migrate storage sub-command.
@ -213,11 +213,8 @@ func migrateActionsArtifacts(ctx context.Context, dstStorage storage.ObjectStora
}) })
} }
func runMigrateStorage(ctx *cli.Context) error { func runMigrateStorage(ctx context.Context, cmd *cli.Command) error {
stdCtx, cancel := installSignals() if err := initDB(ctx); err != nil {
defer cancel()
if err := initDB(stdCtx); err != nil {
return err return err
} }
@ -238,51 +235,51 @@ func runMigrateStorage(ctx *cli.Context) error {
var dstStorage storage.ObjectStorage var dstStorage storage.ObjectStorage
var err error var err error
switch strings.ToLower(ctx.String("storage")) { switch strings.ToLower(cmd.String("storage")) {
case "": case "":
fallthrough fallthrough
case string(setting.LocalStorageType): case string(setting.LocalStorageType):
p := ctx.String("path") p := cmd.String("path")
if p == "" { if p == "" {
log.Fatal("Path must be given when storage is local") log.Fatal("Path must be given when storage is local")
return nil return nil
} }
dstStorage, err = storage.NewLocalStorage( dstStorage, err = storage.NewLocalStorage(
stdCtx, ctx,
&setting.Storage{ &setting.Storage{
Path: p, Path: p,
}) })
case string(setting.MinioStorageType): case string(setting.MinioStorageType):
dstStorage, err = storage.NewMinioStorage( dstStorage, err = storage.NewMinioStorage(
stdCtx, ctx,
&setting.Storage{ &setting.Storage{
MinioConfig: setting.MinioStorageConfig{ MinioConfig: setting.MinioStorageConfig{
Endpoint: ctx.String("minio-endpoint"), Endpoint: cmd.String("minio-endpoint"),
AccessKeyID: ctx.String("minio-access-key-id"), AccessKeyID: cmd.String("minio-access-key-id"),
SecretAccessKey: ctx.String("minio-secret-access-key"), SecretAccessKey: cmd.String("minio-secret-access-key"),
Bucket: ctx.String("minio-bucket"), Bucket: cmd.String("minio-bucket"),
Location: ctx.String("minio-location"), Location: cmd.String("minio-location"),
BasePath: ctx.String("minio-base-path"), BasePath: cmd.String("minio-base-path"),
UseSSL: ctx.Bool("minio-use-ssl"), UseSSL: cmd.Bool("minio-use-ssl"),
InsecureSkipVerify: ctx.Bool("minio-insecure-skip-verify"), InsecureSkipVerify: cmd.Bool("minio-insecure-skip-verify"),
ChecksumAlgorithm: ctx.String("minio-checksum-algorithm"), ChecksumAlgorithm: cmd.String("minio-checksum-algorithm"),
BucketLookUpType: ctx.String("minio-bucket-lookup-type"), BucketLookUpType: cmd.String("minio-bucket-lookup-type"),
}, },
}) })
case string(setting.AzureBlobStorageType): case string(setting.AzureBlobStorageType):
dstStorage, err = storage.NewAzureBlobStorage( dstStorage, err = storage.NewAzureBlobStorage(
stdCtx, ctx,
&setting.Storage{ &setting.Storage{
AzureBlobConfig: setting.AzureBlobStorageConfig{ AzureBlobConfig: setting.AzureBlobStorageConfig{
Endpoint: ctx.String("azureblob-endpoint"), Endpoint: cmd.String("azureblob-endpoint"),
AccountName: ctx.String("azureblob-account-name"), AccountName: cmd.String("azureblob-account-name"),
AccountKey: ctx.String("azureblob-account-key"), AccountKey: cmd.String("azureblob-account-key"),
Container: ctx.String("azureblob-container"), Container: cmd.String("azureblob-container"),
BasePath: ctx.String("azureblob-base-path"), BasePath: cmd.String("azureblob-base-path"),
}, },
}) })
default: default:
return fmt.Errorf("unsupported storage type: %s", ctx.String("storage")) return fmt.Errorf("unsupported storage type: %s", cmd.String("storage"))
} }
if err != nil { if err != nil {
return err return err
@ -299,14 +296,14 @@ func runMigrateStorage(ctx *cli.Context) error {
"actions-artifacts": migrateActionsArtifacts, "actions-artifacts": migrateActionsArtifacts,
} }
tp := strings.ToLower(ctx.String("type")) tp := strings.ToLower(cmd.String("type"))
if m, ok := migratedMethods[tp]; ok { if m, ok := migratedMethods[tp]; ok {
if err := m(stdCtx, dstStorage); err != nil { if err := m(ctx, dstStorage); err != nil {
return err return err
} }
log.Info("%s files have successfully been copied to the new storage.", tp) log.Info("%s files have successfully been copied to the new storage.", tp)
return nil return nil
} }
return fmt.Errorf("unsupported storage: %s", ctx.String("type")) return fmt.Errorf("unsupported storage: %s", cmd.String("type"))
} }

View File

@ -4,12 +4,13 @@
package cmd package cmd
import ( import (
"context"
"strings" "strings"
"code.gitea.io/gitea/modules/private" "code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
// CmdRestoreRepository represents the available restore a repository sub-command. // CmdRestoreRepository represents the available restore a repository sub-command.
@ -48,10 +49,7 @@ wiki, issues, labels, releases, release_assets, milestones, pull_requests, comme
}, },
} }
func runRestoreRepository(c *cli.Context) error { func runRestoreRepository(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals()
defer cancel()
setting.MustInstalled() setting.MustInstalled()
var units []string var units []string
if s := c.String("units"); s != "" { if s := c.String("units"); s != "" {

View File

@ -33,7 +33,7 @@ import (
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"github.com/kballard/go-shellquote" "github.com/kballard/go-shellquote"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
// CmdServ represents the available serv sub-command. // CmdServ represents the available serv sub-command.
@ -152,10 +152,7 @@ func getLFSAuthToken(ctx context.Context, lfsVerb string, results *private.ServC
return "Bearer " + tokenString, nil return "Bearer " + tokenString, nil
} }
func runServ(c *cli.Context) error { func runServ(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals()
defer cancel()
// FIXME: This needs to internationalised // FIXME: This needs to internationalised
setup(ctx, c.Bool("debug")) setup(ctx, c.Bool("debug"))

View File

@ -28,7 +28,7 @@ import (
"code.gitea.io/gitea/routers/install" "code.gitea.io/gitea/routers/install"
"github.com/felixge/fgprof" "github.com/felixge/fgprof"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
// PIDFile could be set from build tag // PIDFile could be set from build tag
@ -130,19 +130,19 @@ func showWebStartupMessage(msg string) {
} }
} }
func serveInstall(ctx *cli.Context) error { func serveInstall(cmd *cli.Command) error {
showWebStartupMessage("Prepare to run install page") showWebStartupMessage("Prepare to run install page")
routers.InitWebInstallPage(graceful.GetManager().HammerContext()) routers.InitWebInstallPage(graceful.GetManager().HammerContext())
// Flag for port number in case first time run conflict // Flag for port number in case first time run conflict
if ctx.IsSet("port") { if cmd.IsSet("port") {
if err := setPort(ctx.String("port")); err != nil { if err := setPort(cmd.String("port")); err != nil {
return err return err
} }
} }
if ctx.IsSet("install-port") { if cmd.IsSet("install-port") {
if err := setPort(ctx.String("install-port")); err != nil { if err := setPort(cmd.String("install-port")); err != nil {
return err return err
} }
} }
@ -163,7 +163,7 @@ func serveInstall(ctx *cli.Context) error {
return nil return nil
} }
func serveInstalled(ctx *cli.Context) error { func serveInstalled(c *cli.Command) error {
setting.InitCfgProvider(setting.CustomConf) setting.InitCfgProvider(setting.CustomConf)
setting.LoadCommonSettings() setting.LoadCommonSettings()
setting.MustInstalled() setting.MustInstalled()
@ -218,8 +218,8 @@ func serveInstalled(ctx *cli.Context) error {
setting.AppDataTempDir("").RemoveOutdated(3 * 24 * time.Hour) setting.AppDataTempDir("").RemoveOutdated(3 * 24 * time.Hour)
// Override the provided port number within the configuration // Override the provided port number within the configuration
if ctx.IsSet("port") { if c.IsSet("port") {
if err := setPort(ctx.String("port")); err != nil { if err := setPort(c.String("port")); err != nil {
return err return err
} }
} }
@ -244,7 +244,7 @@ func servePprof() {
finished() finished()
} }
func runWeb(ctx *cli.Context) error { func runWeb(_ context.Context, cmd *cli.Command) error {
defer func() { defer func() {
if panicked := recover(); panicked != nil { if panicked := recover(); panicked != nil {
log.Fatal("PANIC: %v\n%s", panicked, log.Stack(2)) log.Fatal("PANIC: %v\n%s", panicked, log.Stack(2))
@ -262,12 +262,12 @@ func runWeb(ctx *cli.Context) error {
} }
// Set pid file setting // Set pid file setting
if ctx.IsSet("pid") { if cmd.IsSet("pid") {
createPIDFile(ctx.String("pid")) createPIDFile(cmd.String("pid"))
} }
if !setting.InstallLock { if !setting.InstallLock {
if err := serveInstall(ctx); err != nil { if err := serveInstall(cmd); err != nil {
return err return err
} }
} else { } else {
@ -278,7 +278,7 @@ func runWeb(ctx *cli.Context) error {
go servePprof() go servePprof()
} }
return serveInstalled(ctx) return serveInstalled(cmd)
} }
func setPort(port string) error { func setPort(port string) error {

View File

@ -12,21 +12,19 @@ import (
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
"os/signal"
"path" "path"
"strconv" "strconv"
"strings" "strings"
"syscall"
"github.com/google/go-github/v71/github" "github.com/google/go-github/v71/github"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
const defaultVersion = "v1.18" // to backport to const defaultVersion = "v1.18" // to backport to
func main() { func main() {
app := cli.NewApp() app := &cli.Command{}
app.Name = "backport" app.Name = "backport"
app.Usage = "Backport provided PR-number on to the current or previous released version" app.Usage = "Backport provided PR-number on to the current or previous released version"
app.Description = `Backport will look-up the PR in Gitea's git log and attempt to cherry-pick it on the current version` app.Description = `Backport will look-up the PR in Gitea's git log and attempt to cherry-pick it on the current version`
@ -91,7 +89,7 @@ func main() {
Usage: "Set this flag to continue from a git cherry-pick that has broken", Usage: "Set this flag to continue from a git cherry-pick that has broken",
}, },
} }
cli.AppHelpTemplate = `NAME: cli.RootCommandHelpTemplate = `NAME:
{{.Name}} - {{.Usage}} {{.Name}} - {{.Usage}}
USAGE: USAGE:
{{.HelpName}} {{if .VisibleFlags}}[options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} {{.HelpName}} {{if .VisibleFlags}}[options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}
@ -105,16 +103,12 @@ OPTIONS:
` `
app.Action = runBackport app.Action = runBackport
if err := app.Run(context.Background(), os.Args); err != nil {
if err := app.Run(os.Args); err != nil {
fmt.Fprintf(os.Stderr, "Unable to backport: %v\n", err) fmt.Fprintf(os.Stderr, "Unable to backport: %v\n", err)
} }
} }
func runBackport(c *cli.Context) error { func runBackport(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals()
defer cancel()
continuing := c.Bool("continue") continuing := c.Bool("continue")
var pr string var pr string
@ -460,25 +454,3 @@ func determineSHAforPR(ctx context.Context, prStr, accessToken string) (string,
return "", nil return "", nil
} }
func installSignals() (context.Context, context.CancelFunc) {
ctx, cancel := context.WithCancel(context.Background())
go func() {
// install notify
signalChannel := make(chan os.Signal, 1)
signal.Notify(
signalChannel,
syscall.SIGINT,
syscall.SIGTERM,
)
select {
case <-signalChannel:
case <-ctx.Done():
}
cancel()
signal.Reset()
}()
return ctx, cancel
}

View File

@ -4,16 +4,17 @@
package main package main
import ( import (
"context"
"os" "os"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
func main() { func main() {
app := cli.NewApp() app := cli.Command{}
app.Name = "environment-to-ini" app.Name = "environment-to-ini"
app.Usage = "Use provided environment to update configuration ini" app.Usage = "Use provided environment to update configuration ini"
app.Description = `As a helper to allow docker users to update the gitea configuration app.Description = `As a helper to allow docker users to update the gitea configuration
@ -72,13 +73,13 @@ func main() {
}, },
} }
app.Action = runEnvironmentToIni app.Action = runEnvironmentToIni
err := app.Run(os.Args) err := app.Run(context.Background(), os.Args)
if err != nil { if err != nil {
log.Fatal("Failed to run app with %s: %v", os.Args, err) log.Fatal("Failed to run app with %s: %v", os.Args, err)
} }
} }
func runEnvironmentToIni(c *cli.Context) error { 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 // the config system may change the environment variables, so get a copy first, to be used later
env := append([]string{}, os.Environ()...) env := append([]string{}, os.Environ()...)
setting.InitWorkPathAndCfgProvider(os.Getenv, setting.ArgWorkPathAndCustomConf{ setting.InitWorkPathAndCfgProvider(os.Getenv, setting.ArgWorkPathAndCustomConf{

6
go.mod
View File

@ -110,7 +110,8 @@ require (
github.com/syndtr/goleveldb v1.0.0 github.com/syndtr/goleveldb v1.0.0
github.com/tstranex/u2f v1.0.0 github.com/tstranex/u2f v1.0.0
github.com/ulikunitz/xz v0.5.12 github.com/ulikunitz/xz v0.5.12
github.com/urfave/cli/v2 v2.27.6 github.com/urfave/cli-docs/v3 v3.0.0-alpha6
github.com/urfave/cli/v3 v3.3.3
github.com/wneessen/go-mail v0.6.2 github.com/wneessen/go-mail v0.6.2
github.com/xeipuuv/gojsonschema v1.2.0 github.com/xeipuuv/gojsonschema v1.2.0
github.com/yohcop/openid-go v1.0.1 github.com/yohcop/openid-go v1.0.1
@ -186,7 +187,7 @@ require (
github.com/couchbase/go-couchbase v0.1.1 // indirect github.com/couchbase/go-couchbase v0.1.1 // indirect
github.com/couchbase/gomemcached v0.3.3 // indirect github.com/couchbase/gomemcached v0.3.3 // indirect
github.com/couchbase/goutils v0.1.2 // indirect github.com/couchbase/goutils v0.1.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect github.com/davidmz/go-pageant v1.0.2 // indirect
@ -296,7 +297,6 @@ require (
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
github.com/zeebo/assert v1.3.0 // indirect github.com/zeebo/assert v1.3.0 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect github.com/zeebo/blake3 v0.2.4 // indirect
go.etcd.io/bbolt v1.4.0 // indirect go.etcd.io/bbolt v1.4.0 // indirect

12
go.sum
View File

@ -223,8 +223,8 @@ github.com/couchbase/goutils v0.1.2 h1:gWr8B6XNWPIhfalHNog3qQKfGiYyh4K4VhO3P2o9B
github.com/couchbase/goutils v0.1.2/go.mod h1:h89Ek/tiOxxqjz30nPPlwZdQbdB8BwgnuBxeoUe/ViE= github.com/couchbase/goutils v0.1.2/go.mod h1:h89Ek/tiOxxqjz30nPPlwZdQbdB8BwgnuBxeoUe/ViE=
github.com/couchbase/moss v0.1.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs= github.com/couchbase/moss v0.1.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
@ -761,8 +761,10 @@ github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs= github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs=
github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM= github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM=
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= github.com/urfave/cli-docs/v3 v3.0.0-alpha6 h1:w/l/N0xw1rO/aHRIGXJ0lDwwYFOzilup1qGvIytP3BI=
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/urfave/cli-docs/v3 v3.0.0-alpha6/go.mod h1:p7Z4lg8FSTrPB9GTaNyTrK3ygffHZcK3w0cU2VE+mzU=
github.com/urfave/cli/v3 v3.3.3 h1:byCBaVdIXuLPIDm5CYZRVG6NvT7tv1ECqdU4YzlEa3I=
github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
@ -782,8 +784,6 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yohcop/openid-go v1.0.1 h1:DPRd3iPO5F6O5zX2e62XpVAbPT6wV51cuucH0z9g3js= github.com/yohcop/openid-go v1.0.1 h1:DPRd3iPO5F6O5zX2e62XpVAbPT6wV51cuucH0z9g3js=

View File

@ -21,7 +21,7 @@ import (
_ "code.gitea.io/gitea/modules/markup/markdown" _ "code.gitea.io/gitea/modules/markup/markdown"
_ "code.gitea.io/gitea/modules/markup/orgmode" _ "code.gitea.io/gitea/modules/markup/orgmode"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
// these flags will be set by the build flags // these flags will be set by the build flags

View File

@ -13,7 +13,7 @@ import (
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
) )
func Test_CmdKeys(t *testing.T) { func Test_CmdKeys(t *testing.T) {
@ -36,18 +36,21 @@ func Test_CmdKeys(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
out := new(bytes.Buffer) var stdout, stderr bytes.Buffer
app := cli.NewApp() app := &cli.Command{
app.Writer = out Writer: &stdout,
app.Commands = []*cli.Command{cmd.CmdKeys} ErrWriter: &stderr,
Commands: []*cli.Command{cmd.CmdKeys},
}
cmd.CmdKeys.HideHelp = true cmd.CmdKeys.HideHelp = true
err := app.Run(append([]string{"prog"}, tt.args...)) err := app.Run(t.Context(), append([]string{"prog"}, tt.args...))
if tt.wantErr { if tt.wantErr {
assert.Error(t, err) assert.Error(t, err)
assert.Equal(t, tt.expectedOutput, stderr.String())
} else { } else {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, tt.expectedOutput, stdout.String())
} }
assert.Equal(t, tt.expectedOutput, out.String())
}) })
} }
}) })