Skip to content

Migrate to urfave v3 #34510

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 29 commits into from
Jun 10, 2025
Merged
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d281b11
migrate to urfave v3
TheFox0x7 May 18, 2025
df9c7f2
inline flags for ldap commands
TheFox0x7 May 18, 2025
d29b785
align tests
TheFox0x7 May 19, 2025
ffd04d0
add tests for smtp command
TheFox0x7 May 20, 2025
553b43a
add oauth coverage
TheFox0x7 May 20, 2025
69cde4d
fix formatting
TheFox0x7 May 20, 2025
4ae57e8
install signals in main
TheFox0x7 May 23, 2025
eba1408
add coverage for cert subcommand
TheFox0x7 May 24, 2025
173ad6c
add coverage for admin user delete command
TheFox0x7 May 25, 2025
13b9d6e
add coverage for change password
TheFox0x7 May 25, 2025
02497c0
add coverage for must change password
TheFox0x7 May 25, 2025
8e2333f
drop argsSet in favor of required
TheFox0x7 May 25, 2025
97589cf
Merge branch 'main' into migrate-to-urfave-v3
TheFox0x7 May 25, 2025
23dd90f
fix
wxiaoguang Jun 4, 2025
70deea2
Merge branch 'main' into migrate-to-urfave-v3
wxiaoguang Jun 4, 2025
dfd47ce
fix
wxiaoguang Jun 4, 2025
cda23a7
fix
wxiaoguang Jun 4, 2025
c47d666
fix
wxiaoguang Jun 4, 2025
9441550
fix name, fix lint
wxiaoguang Jun 4, 2025
b2d5244
fix
wxiaoguang Jun 4, 2025
ba990d0
revert ldap flag rework
TheFox0x7 Jun 7, 2025
40fb2ae
correct docs root
TheFox0x7 Jun 7, 2025
a8701d4
wrap errors in cert
TheFox0x7 Jun 8, 2025
8461c8e
make errors conformant to golangs standard
TheFox0x7 Jun 8, 2025
aa6277f
account for custom path in cert generation
TheFox0x7 Jun 8, 2025
d4a27cb
same but for errors
TheFox0x7 Jun 8, 2025
38933ea
don't call newAuthService in init stage
wxiaoguang Jun 10, 2025
3fcc07a
Merge pull request #2 from wxiaoguang/migrate-to-urfave-v3
TheFox0x7 Jun 10, 2025
43443c1
Merge branch 'main' into migrate-to-urfave-v3
wxiaoguang Jun 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions assets/go-licenses.json

Large diffs are not rendered by default.

10 changes: 4 additions & 6 deletions cmd/actions.go
Original file line number Diff line number Diff line change
@@ -4,20 +4,21 @@
package cmd

import (
"context"
"fmt"

"code.gitea.io/gitea/modules/private"
"code.gitea.io/gitea/modules/setting"

"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)

var (
// CmdActions represents the available actions sub-commands.
CmdActions = &cli.Command{
Name: "actions",
Usage: "Manage Gitea Actions",
Subcommands: []*cli.Command{
Commands: []*cli.Command{
subcmdActionsGenRunnerToken,
},
}
@@ -38,10 +39,7 @@ var (
}
)

func runGenerateActionsRunnerToken(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()

func runGenerateActionsRunnerToken(ctx context.Context, c *cli.Command) error {
setting.MustInstalled()

scope := c.String("scope")
41 changes: 20 additions & 21 deletions cmd/admin.go
Original file line number Diff line number Diff line change
@@ -15,15 +15,15 @@ import (
"code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"

"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)

var (
// CmdAdmin represents the available admin sub-command.
CmdAdmin = &cli.Command{
Name: "admin",
Usage: "Perform common administrative operations",
Subcommands: []*cli.Command{
Commands: []*cli.Command{
subcmdUser,
subcmdRepoSyncReleases,
subcmdRegenerate,
@@ -41,7 +41,7 @@ var (
subcmdRegenerate = &cli.Command{
Name: "regenerate",
Usage: "Regenerate specific files",
Subcommands: []*cli.Command{
Commands: []*cli.Command{
microcmdRegenHooks,
microcmdRegenKeys,
},
@@ -50,15 +50,15 @@ var (
subcmdAuth = &cli.Command{
Name: "auth",
Usage: "Modify external auth providers",
Subcommands: []*cli.Command{
microcmdAuthAddOauth,
microcmdAuthUpdateOauth,
microcmdAuthAddLdapBindDn,
microcmdAuthUpdateLdapBindDn,
microcmdAuthAddLdapSimpleAuth,
microcmdAuthUpdateLdapSimpleAuth,
microcmdAuthAddSMTP,
microcmdAuthUpdateSMTP,
Commands: []*cli.Command{
microcmdAuthAddOauth(),
microcmdAuthUpdateOauth(),
microcmdAuthAddLdapBindDn(),
microcmdAuthUpdateLdapBindDn(),
microcmdAuthAddLdapSimpleAuth(),
microcmdAuthUpdateLdapSimpleAuth(),
microcmdAuthAddSMTP(),
microcmdAuthUpdateSMTP(),
microcmdAuthList,
microcmdAuthDelete,
},
@@ -70,9 +70,9 @@ var (
Action: runSendMail,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "title",
Usage: `a title of a message`,
Value: "",
Name: "title",
Usage: "a title of a message",
Required: true,
},
&cli.StringFlag{
Name: "content",
@@ -86,17 +86,16 @@ var (
},
},
}
)

idFlag = &cli.Int64Flag{
func idFlag() *cli.Int64Flag {
return &cli.Int64Flag{
Name: "id",
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 {
return err
}
15 changes: 5 additions & 10 deletions cmd/admin_auth.go
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@
package cmd

import (
"context"
"errors"
"fmt"
"os"
@@ -13,14 +14,14 @@ import (
"code.gitea.io/gitea/models/db"
auth_service "code.gitea.io/gitea/services/auth"

"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)

var (
microcmdAuthDelete = &cli.Command{
Name: "delete",
Usage: "Delete specific auth source",
Flags: []cli.Flag{idFlag},
Flags: []cli.Flag{idFlag()},
Action: runDeleteAuth,
}
microcmdAuthList = &cli.Command{
@@ -56,10 +57,7 @@ var (
}
)

func runListAuth(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()

func runListAuth(ctx context.Context, c *cli.Command) error {
if err := initDB(ctx); err != nil {
return err
}
@@ -90,14 +88,11 @@ func runListAuth(c *cli.Context) error {
return nil
}

func runDeleteAuth(c *cli.Context) error {
func runDeleteAuth(ctx context.Context, c *cli.Command) error {
if !c.IsSet("id") {
return errors.New("--id flag is missing")
}

ctx, cancel := installSignals()
defer cancel()

if err := initDB(ctx); err != nil {
return err
}
88 changes: 43 additions & 45 deletions cmd/admin_auth_ldap.go
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ import (
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/auth/source/ldap"

"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)

type (
@@ -24,8 +24,8 @@ type (
}
)

var (
commonLdapCLIFlags = []cli.Flag{
func commonLdapCLIFlags() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "name",
Usage: "Authentication name.",
@@ -103,8 +103,10 @@ var (
Usage: "The attribute of the user’s LDAP record containing the user’s avatar.",
},
}
}

ldapBindDnCLIFlags = append(commonLdapCLIFlags,
func ldapBindDnCLIFlags() []cli.Flag {
return append(commonLdapCLIFlags(),
&cli.StringFlag{
Name: "bind-dn",
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",
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{
Name: "user-dn",
Usage: "The user's DN.",
})
}

microcmdAuthAddLdapBindDn = &cli.Command{
func microcmdAuthAddLdapBindDn() *cli.Command {
return &cli.Command{
Name: "add-ldap",
Usage: "Add new LDAP (via Bind DN) authentication source",
Action: func(c *cli.Context) error {
return newAuthService().addLdapBindDn(c)
Action: func(ctx context.Context, cmd *cli.Command) error {
return newAuthService().addLdapBindDn(ctx, cmd)
},
Flags: ldapBindDnCLIFlags,
Flags: ldapBindDnCLIFlags(),
}
}

microcmdAuthUpdateLdapBindDn = &cli.Command{
func microcmdAuthUpdateLdapBindDn() *cli.Command {
return &cli.Command{
Name: "update-ldap",
Usage: "Update existing LDAP (via Bind DN) authentication source",
Action: func(c *cli.Context) error {
return newAuthService().updateLdapBindDn(c)
Action: func(ctx context.Context, cmd *cli.Command) error {
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",
Usage: "Add new LDAP (simple auth) authentication source",
Action: func(c *cli.Context) error {
return newAuthService().addLdapSimpleAuth(c)
Action: func(ctx context.Context, cmd *cli.Command) error {
return newAuthService().addLdapSimpleAuth(ctx, cmd)
},
Flags: ldapSimpleAuthCLIFlags,
Flags: ldapSimpleAuthCLIFlags(),
}
}

microcmdAuthUpdateLdapSimpleAuth = &cli.Command{
func microcmdAuthUpdateLdapSimpleAuth() *cli.Command {
return &cli.Command{
Name: "update-ldap-simple",
Usage: "Update existing LDAP (simple auth) authentication source",
Action: func(c *cli.Context) error {
return newAuthService().updateLdapSimpleAuth(c)
Action: func(ctx context.Context, cmd *cli.Command) error {
return newAuthService().updateLdapSimpleAuth(ctx, cmd)
},
Flags: append([]cli.Flag{idFlag}, ldapSimpleAuthCLIFlags...),
Flags: append([]cli.Flag{idFlag()}, ldapSimpleAuthCLIFlags()...),
}
)
}

// newAuthService creates a service with default functions.
func newAuthService() *authService {
@@ -212,7 +224,7 @@ func newAuthService() *authService {
}

// 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") {
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.
func parseLdapConfig(c *cli.Context, config *ldap.Source) error {
func parseLdapConfig(c *cli.Command, config *ldap.Source) error {
if c.IsSet("name") {
config.Name = c.String("name")
}
@@ -245,7 +257,7 @@ func parseLdapConfig(c *cli.Context, config *ldap.Source) error {
if c.IsSet("security-protocol") {
p, ok := findLdapSecurityProtocolByName(c.String("security-protocol"))
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
}
@@ -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.
// 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 {
return nil, err
}

authSource, err := a.getAuthSourceByID(ctx, c.Int64("id"))
if err != nil {
return nil, err
}

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
}

// 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 {
return err
}

ctx, cancel := installSignals()
defer cancel()

if err := a.initDB(ctx); err != nil {
return err
}
@@ -384,10 +391,7 @@ func (a *authService) addLdapBindDn(c *cli.Context) error {
}

// updateLdapBindDn updates a new LDAP via Bind DN authentication source.
func (a *authService) updateLdapBindDn(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()

func (a *authService) updateLdapBindDn(ctx context.Context, c *cli.Command) error {
if err := a.initDB(ctx); err != nil {
return err
}
@@ -406,14 +410,11 @@ func (a *authService) updateLdapBindDn(c *cli.Context) error {
}

// 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 {
return err
}

ctx, cancel := installSignals()
defer cancel()

if err := a.initDB(ctx); err != nil {
return err
}
@@ -435,10 +436,7 @@ func (a *authService) addLdapSimpleAuth(c *cli.Context) error {
}

// updateLdapSimpleAuth updates a new LDAP (simple auth) authentication source.
func (a *authService) updateLdapSimpleAuth(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()

func (a *authService) updateLdapSimpleAuth(ctx context.Context, c *cli.Command) error {
if err := a.initDB(ctx); err != nil {
return err
}
54 changes: 28 additions & 26 deletions cmd/admin_auth_ldap_test.go
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ import (
"code.gitea.io/gitea/services/auth/source/ldap"

"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)

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)",
"--email-attribute", "mail",
},
errMsg: "Unknown security protocol name: zzzzz",
errMsg: "unknown security protocol name: zzzzz",
},
// case 3
{
@@ -238,12 +238,13 @@ func TestAddLdapBindDn(t *testing.T) {
}

// Create a copy of command to test
app := cli.NewApp()
app.Flags = microcmdAuthAddLdapBindDn.Flags
app.Action = service.addLdapBindDn
app := cli.Command{
Flags: microcmdAuthAddLdapBindDn().Flags,
Action: service.addLdapBindDn,
}

// Run it
err := app.Run(c.args)
err := app.Run(t.Context(), c.args)
if c.errMsg != "" {
assert.EqualError(t, err, c.errMsg, "case %d: error should match", n)
} else {
@@ -345,12 +346,12 @@ func TestAddLdapSimpleAuth(t *testing.T) {
"--name", "ldap (simple auth) source",
"--security-protocol", "zzzzz",
"--host", "ldap-server",
"--port", "123",
"--port", "1234",
"--user-filter", "(&(objectClass=posixAccount)(cn=%s))",
"--email-attribute", "mail",
"--user-dn", "cn=%s,ou=Users,dc=domain,dc=org",
},
errMsg: "Unknown security protocol name: zzzzz",
errMsg: "unknown security protocol name: zzzzz",
},
// case 3
{
@@ -467,12 +468,13 @@ func TestAddLdapSimpleAuth(t *testing.T) {
}

// Create a copy of command to test
app := cli.NewApp()
app.Flags = microcmdAuthAddLdapSimpleAuth.Flags
app.Action = service.addLdapSimpleAuth
app := &cli.Command{
Flags: microcmdAuthAddLdapSimpleAuth().Flags,
Action: service.addLdapSimpleAuth,
}

// Run it
err := app.Run(c.args)
err := app.Run(t.Context(), c.args)
if c.errMsg != "" {
assert.EqualError(t, err, c.errMsg, "case %d: error should match", n)
} else {
@@ -859,7 +861,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
"--id", "1",
"--security-protocol", "xxxxx",
},
errMsg: "Unknown security protocol name: xxxxx",
errMsg: "unknown security protocol name: xxxxx",
},
// case 22
{
@@ -878,7 +880,7 @@ func TestUpdateLdapBindDn(t *testing.T) {
Type: auth.OAuth2,
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
{
@@ -942,12 +944,12 @@ func TestUpdateLdapBindDn(t *testing.T) {
}

// Create a copy of command to test
app := cli.NewApp()
app.Flags = microcmdAuthUpdateLdapBindDn.Flags
app.Action = service.updateLdapBindDn

app := cli.Command{
Flags: microcmdAuthUpdateLdapBindDn().Flags,
Action: service.updateLdapBindDn,
}
// Run it
err := app.Run(c.args)
err := app.Run(t.Context(), c.args)
if c.errMsg != "" {
assert.EqualError(t, err, c.errMsg, "case %d: error should match", n)
} else {
@@ -1250,7 +1252,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
"--id", "1",
"--security-protocol", "xxxxx",
},
errMsg: "Unknown security protocol name: xxxxx",
errMsg: "unknown security protocol name: xxxxx",
},
// case 18
{
@@ -1269,7 +1271,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
Type: auth.PAM,
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
{
@@ -1330,12 +1332,12 @@ func TestUpdateLdapSimpleAuth(t *testing.T) {
}

// Create a copy of command to test
app := cli.NewApp()
app.Flags = microcmdAuthUpdateLdapSimpleAuth.Flags
app.Action = service.updateLdapSimpleAuth

app := cli.Command{
Flags: microcmdAuthUpdateLdapSimpleAuth().Flags,
Action: service.updateLdapSimpleAuth,
}
// Run it
err := app.Run(c.args)
err := app.Run(t.Context(), c.args)
if c.errMsg != "" {
assert.EqualError(t, err, c.errMsg, "case %d: error should match", n)
} else {
62 changes: 34 additions & 28 deletions cmd/admin_auth_oauth.go
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@
package cmd

import (
"context"
"errors"
"fmt"
"net/url"
@@ -12,11 +13,11 @@ import (
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/auth/source/oauth2"

"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)

var (
oauthCLIFlags = []cli.Flag{
func oauthCLIFlags() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "name",
Value: "",
@@ -121,23 +122,34 @@ var (
Usage: "Activate automatic team membership removal depending on groups",
},
}
}

microcmdAuthAddOauth = &cli.Command{
Name: "add-oauth",
Usage: "Add new Oauth authentication source",
Action: runAddOauth,
Flags: oauthCLIFlags,
func microcmdAuthAddOauth() *cli.Command {
return &cli.Command{
Name: "add-oauth",
Usage: "Add new Oauth authentication source",
Action: func(ctx context.Context, cmd *cli.Command) error {
return newAuthService().runAddOauth(ctx, cmd)
},
Flags: oauthCLIFlags(),
}
}

microcmdAuthUpdateOauth = &cli.Command{
Name: "update-oauth",
Usage: "Update existing Oauth authentication source",
Action: runUpdateOauth,
Flags: append(oauthCLIFlags[:1], append([]cli.Flag{idFlag}, oauthCLIFlags[1:]...)...),
func microcmdAuthUpdateOauth() *cli.Command {
return &cli.Command{
Name: "update-oauth",
Usage: "Update existing Oauth authentication source",
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
if c.IsSet("use-custom-urls") {
customURLMapping = &oauth2.CustomURLMapping{
@@ -168,11 +180,8 @@ func parseOAuth2Config(c *cli.Context) *oauth2.Source {
}
}

func runAddOauth(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()

if err := initDB(ctx); err != nil {
func (a *authService) runAddOauth(ctx context.Context, c *cli.Command) error {
if err := a.initDB(ctx); err != nil {
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,
Name: c.String("name"),
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") {
return errors.New("--id flag is missing")
}

ctx, cancel := installSignals()
defer cancel()

if err := initDB(ctx); err != nil {
if err := a.initDB(ctx); err != nil {
return err
}

source, err := auth_model.GetSourceByID(ctx, c.Int64("id"))
source, err := a.getAuthSourceByID(ctx, c.Int64("id"))
if err != nil {
return err
}
@@ -296,5 +302,5 @@ func runUpdateOauth(c *cli.Context) error {
oAuth2Config.CustomURLMapping = customURLMapping
source.Cfg = oAuth2Config
source.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "")
return auth_model.UpdateSource(ctx, source)
return a.updateAuthSource(ctx, source)
}
333 changes: 333 additions & 0 deletions cmd/admin_auth_oauth_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
62 changes: 34 additions & 28 deletions cmd/admin_auth_stmp.go → cmd/admin_auth_smtp.go
Original file line number Diff line number Diff line change
@@ -4,18 +4,19 @@
package cmd

import (
"context"
"errors"
"strings"

auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/auth/source/smtp"

"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)

var (
smtpCLIFlags = []cli.Flag{
func smtpCLIFlags() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: "name",
Value: "",
@@ -71,23 +72,34 @@ var (
Value: true,
},
}
}

microcmdAuthAddSMTP = &cli.Command{
Name: "add-smtp",
Usage: "Add new SMTP authentication source",
Action: runAddSMTP,
Flags: smtpCLIFlags,
func microcmdAuthUpdateSMTP() *cli.Command {
return &cli.Command{
Name: "update-smtp",
Usage: "Update existing SMTP authentication source",
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{
Name: "update-smtp",
Usage: "Update existing SMTP authentication source",
Action: runUpdateSMTP,
Flags: append(smtpCLIFlags[:1], append([]cli.Flag{idFlag}, smtpCLIFlags[1:]...)...),
func microcmdAuthAddSMTP() *cli.Command {
return &cli.Command{
Name: "add-smtp",
Usage: "Add new SMTP authentication source",
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") {
conf.Auth = c.String("auth-type")
validAuthTypes := []string{"PLAIN", "LOGIN", "CRAM-MD5"}
@@ -120,11 +132,8 @@ func parseSMTPConfig(c *cli.Context, conf *smtp.Source) error {
return nil
}

func runAddSMTP(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()

if err := initDB(ctx); err != nil {
func (a *authService) runAddSMTP(ctx context.Context, c *cli.Command) error {
if err := a.initDB(ctx); err != nil {
return err
}

@@ -152,7 +161,7 @@ func runAddSMTP(c *cli.Context) error {
smtpConfig.Auth = "PLAIN"
}

return auth_model.CreateSource(ctx, &auth_model.Source{
return a.createAuthSource(ctx, &auth_model.Source{
Type: auth_model.SMTP,
Name: c.String("name"),
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") {
return errors.New("--id flag is missing")
}

ctx, cancel := installSignals()
defer cancel()

if err := initDB(ctx); err != nil {
if err := a.initDB(ctx); err != nil {
return err
}

source, err := auth_model.GetSourceByID(ctx, c.Int64("id"))
source, err := a.getAuthSourceByID(ctx, c.Int64("id"))
if err != nil {
return err
}
@@ -194,5 +200,5 @@ func runUpdateSMTP(c *cli.Context) error {

source.Cfg = smtpConfig
source.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "")
return auth_model.UpdateSource(ctx, source)
return a.updateAuthSource(ctx, source)
}
285 changes: 285 additions & 0 deletions cmd/admin_auth_smtp_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
14 changes: 5 additions & 9 deletions cmd/admin_regenerate.go
Original file line number Diff line number Diff line change
@@ -4,11 +4,13 @@
package cmd

import (
"context"

"code.gitea.io/gitea/modules/graceful"
asymkey_service "code.gitea.io/gitea/services/asymkey"
repo_service "code.gitea.io/gitea/services/repository"

"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)

var (
@@ -25,20 +27,14 @@ var (
}
)

func runRegenerateHooks(_ *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()

func runRegenerateHooks(ctx context.Context, _ *cli.Command) error {
if err := initDB(ctx); err != nil {
return err
}
return repo_service.SyncRepositoryHooks(graceful.GetManager().ShutdownContext())
}

func runRegenerateKeys(_ *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()

func runRegenerateKeys(ctx context.Context, _ *cli.Command) error {
if err := initDB(ctx); err != nil {
return err
}
12 changes: 6 additions & 6 deletions cmd/admin_user.go
Original file line number Diff line number Diff line change
@@ -4,18 +4,18 @@
package cmd

import (
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)

var subcmdUser = &cli.Command{
Name: "user",
Usage: "Modify users",
Subcommands: []*cli.Command{
microcmdUserCreate,
Commands: []*cli.Command{
microcmdUserCreate(),
microcmdUserList,
microcmdUserChangePassword,
microcmdUserDelete,
microcmdUserChangePassword(),
microcmdUserDelete(),
microcmdUserGenerateAccessToken,
microcmdUserMustChangePassword,
microcmdUserMustChangePassword(),
},
}
64 changes: 31 additions & 33 deletions cmd/admin_user_change_password.go
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@
package cmd

import (
"context"
"errors"
"fmt"

@@ -13,44 +14,41 @@ import (
"code.gitea.io/gitea/modules/setting"
user_service "code.gitea.io/gitea/services/user"

"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)

var microcmdUserChangePassword = &cli.Command{
Name: "change-password",
Usage: "Change a user's password",
Action: runChangePassword,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "username",
Aliases: []string{"u"},
Value: "",
Usage: "The user to change password for",
func microcmdUserChangePassword() *cli.Command {
return &cli.Command{
Name: "change-password",
Usage: "Change a user's password",
Action: runChangePassword,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "username",
Aliases: []string{"u"},
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 {
if err := argsSet(c, "username", "password"); err != nil {
return err
}
}

ctx, cancel := installSignals()
defer cancel()

if err := initDB(ctx); err != nil {
return err
func runChangePassword(ctx context.Context, c *cli.Command) error {
if !setting.IsInTesting {
if err := initDB(ctx); err != nil {
return err
}
}

user, err := user_model.GetUserByName(ctx, c.String("username"))
91 changes: 91 additions & 0 deletions cmd/admin_user_change_password_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
})
}
164 changes: 81 additions & 83 deletions cmd/admin_user_create.go
Original file line number Diff line number Diff line change
@@ -16,87 +16,95 @@ import (
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"

"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)

var microcmdUserCreate = &cli.Command{
Name: "create",
Usage: "Create a new user in database",
Action: runCreateUser,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "name",
Usage: "Username. DEPRECATED: use username instead",
func microcmdUserCreate() *cli.Command {
return &cli.Command{
Name: "create",
Usage: "Create a new user in database",
Action: runCreateUser,
MutuallyExclusiveFlags: []cli.MutuallyExclusiveFlags{
{
Flags: [][]cli.Flag{
{
&cli.StringFlag{
Name: "name",
Usage: "Username. DEPRECATED: use username instead",
},
&cli.StringFlag{
Name: "username",
Usage: "Username",
},
},
},
Required: true,
},
},
&cli.StringFlag{
Name: "username",
Usage: "Username",
Flags: []cli.Flag{
&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",
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
// duplicate setting loading should be safe at the moment, but it should be refactored & improved in the future.
setting.LoadSettings()

if err := argsSet(c, "email"); err != nil {
return err
}

userTypes := map[string]user_model.UserType{
"individual": user_model.UserTypeIndividual,
"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")
}
}
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") {
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")
} else {
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 {
// 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
var cancel context.CancelFunc
ctx, cancel = installSignals()
defer cancel()
if err := initDB(ctx); err != nil {
return err
}
7 changes: 3 additions & 4 deletions cmd/admin_user_create_test.go
Original file line number Diff line number Diff line change
@@ -18,8 +18,6 @@ import (
)

func TestAdminUserCreate(t *testing.T) {
app := NewMainApp(AppVersion{})

reset := func() {
require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{}))
require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{}))
@@ -31,8 +29,9 @@ func TestAdminUserCreate(t *testing.T) {
IsAdmin bool
MustChangePassword bool
}

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})
return check{IsAdmin: u.IsAdmin, MustChangePassword: u.MustChangePassword}
}
@@ -51,7 +50,7 @@ func TestAdminUserCreate(t *testing.T) {
})

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) {
67 changes: 35 additions & 32 deletions cmd/admin_user_delete.go
Original file line number Diff line number Diff line change
@@ -4,53 +4,56 @@
package cmd

import (
"context"
"errors"
"fmt"
"strings"

user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
user_service "code.gitea.io/gitea/services/user"

"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)

var microcmdUserDelete = &cli.Command{
Name: "delete",
Usage: "Delete specific user by id, name or email",
Flags: []cli.Flag{
&cli.Int64Flag{
Name: "id",
Usage: "ID of user of the user to delete",
func microcmdUserDelete() *cli.Command {
return &cli.Command{
Name: "delete",
Usage: "Delete specific user by id, name or email",
Flags: []cli.Flag{
&cli.Int64Flag{
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{
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,
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") {
return errors.New("You must provide the id, username or email of a user to delete")
}

ctx, cancel := installSignals()
defer cancel()

if err := initDB(ctx); err != nil {
return err
if !setting.IsInTesting {
if err := initDB(ctx); err != nil {
return err
}
}

if err := storage.Init(); err != nil {
@@ -70,11 +73,11 @@ func runDeleteUser(c *cli.Context) error {
return err
}
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") {
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"))
111 changes: 111 additions & 0 deletions cmd/admin_user_delete_test.go
Original file line number Diff line number Diff line change
@@ -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{}))
}
}
8 changes: 3 additions & 5 deletions cmd/admin_user_generate_access_token.go
Original file line number Diff line number Diff line change
@@ -4,13 +4,14 @@
package cmd

import (
"context"
"errors"
"fmt"

auth_model "code.gitea.io/gitea/models/auth"
user_model "code.gitea.io/gitea/models/user"

"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)

var microcmdUserGenerateAccessToken = &cli.Command{
@@ -41,14 +42,11 @@ var microcmdUserGenerateAccessToken = &cli.Command{
Action: runGenerateAccessToken,
}

func runGenerateAccessToken(c *cli.Context) error {
func runGenerateAccessToken(ctx context.Context, c *cli.Command) error {
if !c.IsSet("username") {
return errors.New("you must provide a username to generate a token for")
}

ctx, cancel := installSignals()
defer cancel()

if err := initDB(ctx); err != nil {
return err
}
8 changes: 3 additions & 5 deletions cmd/admin_user_list.go
Original file line number Diff line number Diff line change
@@ -4,13 +4,14 @@
package cmd

import (
"context"
"fmt"
"os"
"text/tabwriter"

user_model "code.gitea.io/gitea/models/user"

"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)

var microcmdUserList = &cli.Command{
@@ -25,10 +26,7 @@ var microcmdUserList = &cli.Command{
},
}

func runListUsers(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()

func runListUsers(ctx context.Context, c *cli.Command) error {
if err := initDB(ctx); err != nil {
return err
}
55 changes: 29 additions & 26 deletions cmd/admin_user_must_change_password.go
Original file line number Diff line number Diff line change
@@ -4,40 +4,41 @@
package cmd

import (
"context"
"errors"
"fmt"

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{
Name: "must-change-password",
Usage: "Set the must change password flag for the provided users or all users",
Action: runMustChangePassword,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "all",
Aliases: []string{"A"},
Usage: "All users must change password, except those explicitly excluded with --exclude",
func microcmdUserMustChangePassword() *cli.Command {
return &cli.Command{
Name: "must-change-password",
Usage: "Set the must change password flag for the provided users or all users",
Action: runMustChangePassword,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "all",
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 {
ctx, cancel := installSignals()
defer cancel()

func runMustChangePassword(ctx context.Context, c *cli.Command) error {
if c.NArg() == 0 && !c.IsSet("all") {
return errors.New("either usernames or --all must be provided")
}
@@ -46,8 +47,10 @@ func runMustChangePassword(c *cli.Context) error {
all := c.Bool("all")
exclude := c.StringSlice("exclude")

if err := initDB(ctx); err != nil {
return err
if !setting.IsInTesting {
if err := initDB(ctx); err != nil {
return err
}
}

n, err := user_model.SetMustChangePassword(ctx, all, mustChangePassword, c.Args().Slice(), exclude)
78 changes: 78 additions & 0 deletions cmd/admin_user_must_change_password_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
124 changes: 67 additions & 57 deletions cmd/cert.go
Original file line number Diff line number Diff line change
@@ -6,61 +6,75 @@
package cmd

import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"log"
"math/big"
"net"
"os"
"strings"
"time"

"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)

// CmdCert represents the available cert sub-command.
var CmdCert = &cli.Command{
Name: "cert",
Usage: "Generate self-signed certificate",
Description: `Generate a self-signed X.509 certificate for a TLS server.
// cmdCert represents the available cert sub-command.
func cmdCert() *cli.Command {
return &cli.Command{
Name: "cert",
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.`,
Action: runCert,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "host",
Value: "",
Usage: "Comma-separated hostnames and IPs to generate a certificate for",
Action: runCert,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "host",
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 {
@@ -89,11 +103,7 @@ func pemBlockForKey(priv any) *pem.Block {
}
}

func runCert(c *cli.Context) error {
if err := argsSet(c, "host"); err != nil {
return err
}

func runCert(_ context.Context, c *cli.Command) error {
var priv any
var err error
switch c.String("ecdsa-curve") {
@@ -108,17 +118,17 @@ func runCert(c *cli.Context) error {
case "P521":
priv, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
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 {
log.Fatalf("Failed to generate private key: %v", err)
return fmt.Errorf("failed to generate private key: %w", err)
}

var notBefore time.Time
if startDate := c.String("start-date"); startDate != "" {
notBefore, err = time.Parse("Jan 2 15:04:05 2006", startDate)
if err != nil {
log.Fatalf("Failed to parse creation date: %v", err)
return fmt.Errorf("failed to parse creation date %w", err)
}
} else {
notBefore = time.Now()
@@ -129,7 +139,7 @@ func runCert(c *cli.Context) error {
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
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{
@@ -162,35 +172,35 @@ func runCert(c *cli.Context) error {

derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv)
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 {
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})
if err != nil {
log.Fatalf("Failed to encode certificate: %v", err)
return fmt.Errorf("failed to encode certificate: %w", err)
}
err = certOut.Close()
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 {
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))
if err != nil {
log.Fatalf("Failed to encode key: %v", err)
return fmt.Errorf("failed to encode key: %w", err)
}
err = keyOut.Close()
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
}
123 changes: 123 additions & 0 deletions cmd/cert_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}
15 changes: 7 additions & 8 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
@@ -18,20 +18,19 @@ import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"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
// 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 {
if !c.IsSet(a) {
return errors.New(a + " is not set")
}

if util.IsEmptyString(c.String(a)) {
if c.Value(a) == nil {
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)
}

func globalBool(c *cli.Context, name string) bool {
func globalBool(c *cli.Command, name string) bool {
for _, ctx := range c.Lineage() {
if ctx.Bool(name) {
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.
// 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 {
return func(c *cli.Context) error {
func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(context.Context, *cli.Command) (context.Context, error) {
return func(ctx context.Context, c *cli.Command) (context.Context, error) {
level := defaultLevel
if globalBool(c, "quiet") {
level = log.FATAL
@@ -130,6 +129,6 @@ func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(*cli.Context) error
level = log.TRACE
}
log.SetConsoleLogger(log.DEFAULT, "console-default", level)
return nil
return ctx, nil
}
}
18 changes: 10 additions & 8 deletions cmd/docs.go
Original file line number Diff line number Diff line change
@@ -4,11 +4,13 @@
package cmd

import (
"context"
"fmt"
"os"
"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.
@@ -30,16 +32,16 @@ var CmdDocs = &cli.Command{
},
}

func runDocs(ctx *cli.Context) error {
docs, err := ctx.App.ToMarkdown()
if ctx.Bool("man") {
docs, err = ctx.App.ToMan()
func runDocs(_ context.Context, cmd *cli.Command) error {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For reviewers: Double check if the docs are correct as the generation is weird, but it might not have the same issue as with tea due to custom help.

docs, err := cli_docs.ToMarkdown(cmd.Root())
if cmd.Bool("man") {
docs, err = cli_docs.ToMan(cmd.Root())
}
if err != nil {
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.
// It affects markdown output (even though the issue is referring to man pages)
// https://github.com/urfave/cli/issues/1040
@@ -51,8 +53,8 @@ func runDocs(ctx *cli.Context) error {
}

out := os.Stdout
if ctx.String("output") != "" {
fi, err := os.Create(ctx.String("output"))
if cmd.String("output") != "" {
fi, err := os.Create(cmd.String("output"))
if err != nil {
return err
}
48 changes: 21 additions & 27 deletions cmd/doctor.go
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/doctor"

"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
"xorm.io/xorm"
)

@@ -30,7 +30,7 @@ var CmdDoctor = &cli.Command{
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.",

Subcommands: []*cli.Command{
Commands: []*cli.Command{
cmdDoctorCheck,
cmdRecreateTable,
cmdDoctorConvert,
@@ -93,16 +93,13 @@ You should back-up your database before doing this and ensure that your database
Action: runRecreateTable,
}

func runRecreateTable(ctx *cli.Context) error {
stdCtx, cancel := installSignals()
defer cancel()

func runRecreateTable(ctx context.Context, cmd *cli.Command) error {
// Redirect the default golog to here
golog.SetFlags(0)
golog.SetPrefix("")
golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info))

debug := ctx.Bool("debug")
debug := cmd.Bool("debug")
setting.MustInstalled()
setting.LoadDBSetting()

@@ -113,15 +110,15 @@ func runRecreateTable(ctx *cli.Context) error {
}

setting.Database.LogSQL = debug
if err := db.InitEngine(stdCtx); err != nil {
if err := db.InitEngine(ctx); err != nil {
fmt.Println(err)
fmt.Println("Check if you are using the right config file. You can use a --config directive to specify one.")
return nil
}

args := ctx.Args()
names := make([]string, 0, ctx.NArg())
for i := 0; i < ctx.NArg(); i++ {
args := cmd.Args()
names := make([]string, 0, cmd.NArg())
for i := 0; i < cmd.NArg(); i++ {
names = append(names, args.Get(i))
}

@@ -131,19 +128,19 @@ func runRecreateTable(ctx *cli.Context) error {
}
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 {
return err
}
return recreateTables(x)
})
}

func setupDoctorDefaultLogger(ctx *cli.Context, colorize bool) {
func setupDoctorDefaultLogger(cmd *cli.Command, colorize bool) {
// Silence the default loggers
setupConsoleLogger(log.FATAL, log.CanColorStderr, os.Stderr)

logFile := ctx.String("log-file")
logFile := cmd.String("log-file")
switch logFile {
case "":
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 {
stdCtx, cancel := installSignals()
defer cancel()

func runDoctorCheck(ctx context.Context, cmd *cli.Command) error {
colorize := log.CanColorStdout
if ctx.IsSet("color") {
colorize = ctx.Bool("color")
if cmd.IsSet("color") {
colorize = cmd.Bool("color")
}

setupDoctorDefaultLogger(ctx, colorize)
setupDoctorDefaultLogger(cmd, colorize)

// Finally redirect the default golang's log to here
golog.SetFlags(0)
golog.SetPrefix("")
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.Write([]byte("Default\tName\tTitle\n"))
doctor.SortChecks(doctor.Checks)
@@ -195,12 +189,12 @@ func runDoctorCheck(ctx *cli.Context) error {
}

var checks []*doctor.Check
if ctx.Bool("all") {
if cmd.Bool("all") {
checks = make([]*doctor.Check, len(doctor.Checks))
copy(checks, doctor.Checks)
} else if ctx.IsSet("run") {
addDefault := ctx.Bool("default")
runNamesSet := container.SetOf(ctx.StringSlice("run")...)
} else if cmd.IsSet("run") {
addDefault := cmd.Bool("default")
runNamesSet := container.SetOf(cmd.StringSlice("run")...)
for _, check := range doctor.Checks {
if (addDefault && check.IsDefault) || runNamesSet.Contains(check.Name) {
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)
}
10 changes: 4 additions & 6 deletions cmd/doctor_convert.go
Original file line number Diff line number Diff line change
@@ -4,13 +4,14 @@
package cmd

import (
"context"
"fmt"

"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"

"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)

// cmdDoctorConvert represents the available convert sub-command.
@@ -21,11 +22,8 @@ var cmdDoctorConvert = &cli.Command{
Action: runDoctorConvert,
}

func runDoctorConvert(ctx *cli.Context) error {
stdCtx, cancel := installSignals()
defer cancel()

if err := initDB(stdCtx); err != nil {
func runDoctorConvert(ctx context.Context, cmd *cli.Command) error {
if err := initDB(ctx); err != nil {
return err
}

13 changes: 7 additions & 6 deletions cmd/doctor_test.go
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@ import (
"code.gitea.io/gitea/services/doctor"

"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)

func TestDoctorRun(t *testing.T) {
@@ -22,12 +22,13 @@ func TestDoctorRun(t *testing.T) {

SkipDatabaseInitialization: true,
})
app := cli.NewApp()
app.Commands = []*cli.Command{cmdDoctorCheck}
err := app.Run([]string{"./gitea", "check", "--run", "test-check"})
app := &cli.Command{
Commands: []*cli.Command{cmdDoctorCheck},
}
err := app.Run(t.Context(), []string{"./gitea", "check", "--run", "test-check"})
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"`)
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"`)
}
36 changes: 17 additions & 19 deletions cmd/dump.go
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@
package cmd

import (
"context"
"os"
"path"
"path/filepath"
@@ -20,7 +21,7 @@ import (

"gitea.com/go-chi/session"
"github.com/mholt/archiver/v3"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)

// CmdDump represents the available dump sub-command.
@@ -101,17 +102,17 @@ func fatal(format string, args ...any) {
log.Fatal(format, args...)
}

func runDump(ctx *cli.Context) error {
func runDump(ctx context.Context, cmd *cli.Command) error {
setting.MustInstalled()

quite := ctx.Bool("quiet")
verbose := ctx.Bool("verbose")
quite := cmd.Bool("quiet")
verbose := cmd.Bool("verbose")
if verbose && quite {
fatal("Option --quiet and --verbose cannot both be set")
}

// 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 == "" {
fatal("Invalid output type")
}
@@ -136,10 +137,7 @@ func runDump(ctx *cli.Context) error {
setting.DisableLoggerInit()
setting.LoadSettings() // cannot access session settings otherwise

stdCtx, cancel := installSignals()
defer cancel()

err := db.InitEngine(stdCtx)
err := db.InitEngine(ctx)
if err != nil {
return err
}
@@ -165,15 +163,15 @@ func runDump(ctx *cli.Context) error {
}
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")
} else {
log.Info("Dumping local repositories... %s", setting.RepoRootPath)
if err := dumper.AddRecursiveExclude("repos", setting.RepoRootPath, nil); err != nil {
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")
} else if !setting.LFS.StartServer {
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.
dumper.GlobalExcludeAbsPath(setting.Database.Path)
log.Info("Skipping database")
} else {
tmpDir := ctx.String("tempdir")
tmpDir := cmd.String("tempdir")
if _, err := os.Stat(tmpDir); os.IsNotExist(err) {
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() {
log.Info("Dumping database %s => %s...", setting.Database.Type, targetDBType)
} else {
@@ -230,7 +228,7 @@ func runDump(ctx *cli.Context) error {
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")
} else {
customDir, err := os.Stat(setting.CustomPath)
@@ -263,7 +261,7 @@ func runDump(ctx *cli.Context) error {
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.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")
} else if err := storage.Attachments.IterateObjects("", func(objPath string, object storage.Object) error {
info, err := object.Stat()
@@ -290,7 +288,7 @@ func runDump(ctx *cli.Context) error {
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")
} else if !setting.Packages.Enabled {
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,
// ensuring that it's clear the dump is skipped whether the directory's initialized
// 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")
} else {
isExist, err := util.IsExist(setting.Log.RootPath)
29 changes: 13 additions & 16 deletions cmd/dump_repo.go
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ import (
"code.gitea.io/gitea/services/convert"
"code.gitea.io/gitea/services/migrations"

"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)

// 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)

setting.DisableLoggerInit()
setting.LoadSettings() // cannot access skip_tls_verify settings otherwise

stdCtx, cancel := installSignals()
defer cancel()

if err := initDB(stdCtx); err != nil {
if err := initDB(ctx); err != nil {
return err
}

@@ -105,8 +102,8 @@ func runDumpRepository(ctx *cli.Context) error {

var (
serviceType structs.GitServiceType
cloneAddr = ctx.String("clone_addr")
serviceStr = ctx.String("git_service")
cloneAddr = cmd.String("clone_addr")
serviceStr = cmd.String("git_service")
)

if strings.HasPrefix(strings.ToLower(cloneAddr), "https://github.com/") {
@@ -124,13 +121,13 @@ func runDumpRepository(ctx *cli.Context) error {
opts := base.MigrateOptions{
GitServiceType: serviceType,
CloneAddr: cloneAddr,
AuthUsername: ctx.String("auth_username"),
AuthPassword: ctx.String("auth_password"),
AuthToken: ctx.String("auth_token"),
RepoName: ctx.String("repo_name"),
AuthUsername: cmd.String("auth_username"),
AuthPassword: cmd.String("auth_password"),
AuthToken: cmd.String("auth_token"),
RepoName: cmd.String("repo_name"),
}

if len(ctx.String("units")) == 0 {
if len(cmd.String("units")) == 0 {
opts.Wiki = true
opts.Issues = true
opts.Milestones = true
@@ -140,7 +137,7 @@ func runDumpRepository(ctx *cli.Context) error {
opts.PullRequests = true
opts.ReleaseAssets = true
} else {
units := strings.Split(ctx.String("units"), ",")
units := strings.Split(cmd.String("units"), ",")
for _, unit := range units {
switch strings.ToLower(strings.TrimSpace(unit)) {
case "":
@@ -169,7 +166,7 @@ func runDumpRepository(ctx *cli.Context) error {

// 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
repoDir := ctx.String("repo_dir")
repoDir := cmd.String("repo_dir")
if exists, err := util.IsExist(repoDir); err != nil {
return fmt.Errorf("unable to stat repo_dir %q: %w", repoDir, err)
} else if exists {
@@ -184,7 +181,7 @@ func runDumpRepository(ctx *cli.Context) error {
if err := migrations.DumpRepository(
context.Background(),
repoDir,
ctx.String("owner_name"),
cmd.String("owner_name"),
opts,
); err != nil {
log.Fatal("Failed to dump repository: %v", err)
21 changes: 11 additions & 10 deletions cmd/embedded.go
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@
package cmd

import (
"context"
"errors"
"fmt"
"os"
@@ -19,7 +20,7 @@ import (
"code.gitea.io/gitea/modules/util"

"github.com/gobwas/glob"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)

// CmdEmbedded represents the available extract sub-command.
@@ -28,7 +29,7 @@ var (
Name: "embedded",
Usage: "Extract embedded resources",
Description: "A command for extracting embedded resources, like templates and images",
Subcommands: []*cli.Command{
Commands: []*cli.Command{
subcmdList,
subcmdView,
subcmdExtract,
@@ -100,7 +101,7 @@ type assetFile struct {
path string
}

func initEmbeddedExtractor(c *cli.Context) error {
func initEmbeddedExtractor(c *cli.Command) error {
setupConsoleLogger(log.ERROR, log.CanColorStderr, os.Stderr)

patterns, err := compileCollectPatterns(c.Args().Slice())
@@ -115,31 +116,31 @@ func initEmbeddedExtractor(c *cli.Context) error {
return nil
}

func runList(c *cli.Context) error {
func runList(_ context.Context, c *cli.Command) error {
if err := runListDo(c); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
return err
}
return nil
}

func runView(c *cli.Context) error {
func runView(_ context.Context, c *cli.Command) error {
if err := runViewDo(c); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
return err
}
return nil
}

func runExtract(c *cli.Context) error {
func runExtract(_ context.Context, c *cli.Command) error {
if err := runExtractDo(c); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
return err
}
return nil
}

func runListDo(c *cli.Context) error {
func runListDo(c *cli.Command) error {
if err := initEmbeddedExtractor(c); err != nil {
return err
}
@@ -151,7 +152,7 @@ func runListDo(c *cli.Context) error {
return nil
}

func runViewDo(c *cli.Context) error {
func runViewDo(c *cli.Command) error {
if err := initEmbeddedExtractor(c); err != nil {
return err
}
@@ -174,7 +175,7 @@ func runViewDo(c *cli.Context) error {
return nil
}

func runExtractDo(c *cli.Context) error {
func runExtractDo(c *cli.Command) error {
if err := initEmbeddedExtractor(c); err != nil {
return err
}
@@ -271,7 +272,7 @@ func extractAsset(d string, a assetFile, overwrite, rename bool) error {
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)
files, err := fs.ListAllFiles(".", true)
if err != nil {
13 changes: 7 additions & 6 deletions cmd/generate.go
Original file line number Diff line number Diff line change
@@ -5,29 +5,30 @@
package cmd

import (
"context"
"fmt"
"os"

"code.gitea.io/gitea/modules/generate"

"github.com/mattn/go-isatty"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)

var (
// CmdGenerate represents the available generate sub-command.
CmdGenerate = &cli.Command{
Name: "generate",
Usage: "Generate Gitea's secrets/keys/tokens",
Subcommands: []*cli.Command{
Commands: []*cli.Command{
subcmdSecret,
},
}

subcmdSecret = &cli.Command{
Name: "secret",
Usage: "Generate a secret token",
Subcommands: []*cli.Command{
Commands: []*cli.Command{
microcmdGenerateInternalToken,
microcmdGenerateLfsJwtSecret,
microcmdGenerateSecretKey,
@@ -54,7 +55,7 @@ var (
}
)

func runGenerateInternalToken(c *cli.Context) error {
func runGenerateInternalToken(_ context.Context, c *cli.Command) error {
internalToken, err := generate.NewInternalToken()
if err != nil {
return err
@@ -69,7 +70,7 @@ func runGenerateInternalToken(c *cli.Context) error {
return nil
}

func runGenerateLfsJwtSecret(c *cli.Context) error {
func runGenerateLfsJwtSecret(_ context.Context, c *cli.Command) error {
_, jwtSecretBase64, err := generate.NewJwtSecretWithBase64()
if err != nil {
return err
@@ -84,7 +85,7 @@ func runGenerateLfsJwtSecret(c *cli.Context) error {
return nil
}

func runGenerateSecretKey(c *cli.Context) error {
func runGenerateSecretKey(_ context.Context, c *cli.Command) error {
secretKey, err := generate.NewSecretKey()
if err != nil {
return err
22 changes: 7 additions & 15 deletions cmd/hook.go
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@ import (
repo_module "code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"

"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)

const (
@@ -34,7 +34,7 @@ var (
Usage: "(internal) Should only be called by Git",
Description: "Delegate commands to corresponding Git hooks",
Before: PrepareConsoleLoggerLevel(log.FATAL),
Subcommands: []*cli.Command{
Commands: []*cli.Command{
subcmdHookPreReceive,
subcmdHookUpdate,
subcmdHookPostReceive,
@@ -161,12 +161,10 @@ func (n *nilWriter) WriteString(s string) (int, error) {
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 {
return nil
}
ctx, cancel := installSignals()
defer cancel()

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
// 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 {
return nil
}
@@ -309,15 +307,12 @@ func runHookUpdate(c *cli.Context) error {
return nil
}

func runHookPostReceive(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()

func runHookPostReceive(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))

// First of all run update-server-info no matter what
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
@@ -496,10 +491,7 @@ func pushOptions() map[string]string {
return opts
}

func runHookProcReceive(c *cli.Context) error {
ctx, cancel := installSignals()
defer cancel()

func runHookProcReceive(ctx context.Context, c *cli.Command) error {
setup(ctx, c.Bool("debug"))

if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
Loading