Skip to content

Commit 8a42a6e

Browse files
committed
Merge branch 'fix/776-branch-verb-subcommands' into 'master'
fix(cli): reserve branch verbs as subcommands, not branch names Closes #776 See merge request postgres-ai/database-lab!1193
2 parents a59212b + 4e2d898 commit 8a42a6e

4 files changed

Lines changed: 386 additions & 18 deletions

File tree

‎engine/cmd/cli/commands/branch/actions.go‎

Lines changed: 87 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,62 @@ func switchLocalContext(branchName string) error {
8282
return err
8383
}
8484

85-
func list(cliCtx *cli.Context) error {
86-
dblabClient, err := commands.ClientByCLIContext(cliCtx)
87-
if err != nil {
88-
return err
85+
// bareFormFlag is a flag of the bare `dblab branch` form, with the spelling to use instead when a
86+
// subcommand is given. None of these flags may gain an EnvVars or FilePath source: the guard below
87+
// reads IsSet, which is also true for a value that did not come from the command line.
88+
type bareFormFlag struct {
89+
name, hint string
90+
}
91+
92+
var bareFormFlags = []bareFormFlag{
93+
{name: "delete", hint: "use either `dblab branch --delete BRANCH_NAME` or `dblab branch delete BRANCH_NAME`"},
94+
{name: "parent-branch", hint: "place it after the subcommand: `dblab branch create --parent-branch VALUE BRANCH_NAME`"},
95+
{name: "snapshot-id", hint: "place it after the subcommand: `dblab branch create --snapshot-id VALUE BRANCH_NAME`"},
96+
{name: "protected", hint: "it applies to the bare form only: `dblab branch --protected VALUE BRANCH_NAME`"},
97+
}
98+
99+
// rejectBareFormFlags runs before `dblab branch` dispatches, and fails when a bare-form flag is
100+
// followed by a subcommand, the built-in help included. urfave/cli resolves the subcommand before
101+
// the bare-form action runs, so the flag would otherwise be parsed and then dropped:
102+
// `dblab branch --snapshot-id X create dev` would create from the wrong snapshot and
103+
// `dblab branch --protected 2h list` would list without changing any protection.
104+
func rejectBareFormFlags(cliCtx *cli.Context) error {
105+
subcommand := subcommandNamed(cliCtx.Command, cliCtx.Args().First())
106+
if subcommand == nil {
107+
return nil
108+
}
109+
110+
for _, flag := range bareFormFlags {
111+
if !cliCtx.IsSet(flag.name) {
112+
continue
113+
}
114+
115+
return commands.NewActionError(fmt.Sprintf("--%s cannot precede the %s subcommand; %s",
116+
flag.name, subcommand.Name, flag.hint))
89117
}
90118

119+
return nil
120+
}
121+
122+
func subcommandNamed(command *cli.Command, name string) *cli.Command {
123+
if command == nil || name == "" {
124+
return nil
125+
}
126+
127+
for _, subcommand := range command.Subcommands {
128+
if subcommand.HasName(name) {
129+
return subcommand
130+
}
131+
}
132+
133+
return nil
134+
}
135+
136+
// branchAction dispatches the bare `dblab branch` form: --protected updates the protection of the
137+
// named branch, a positional name creates a branch, --delete removes one, and no arguments lists
138+
// them. The list, create, delete and switch subcommands express the same operations unambiguously
139+
// and take precedence, so a name that collides with one of them never silently creates a branch.
140+
func branchAction(cliCtx *cli.Context) error {
91141
branchName := cliCtx.Args().First()
92142

93143
// update branch protection.
@@ -104,12 +154,20 @@ func list(cliCtx *cli.Context) error {
104154
return create(cliCtx)
105155
}
106156

107-
// delete branch.
108-
if deleteName := cliCtx.String("delete"); deleteName != "" {
157+
// delete branch. an explicitly empty name reaches the guard in deleteBranch.
158+
if cliCtx.IsSet("delete") {
109159
return deleteBranch(cliCtx)
110160
}
111161

112-
// list branches.
162+
return listBranches(cliCtx)
163+
}
164+
165+
func listBranches(cliCtx *cli.Context) error {
166+
dblabClient, err := commands.ClientByCLIContext(cliCtx)
167+
if err != nil {
168+
return err
169+
}
170+
113171
branches, err := dblabClient.ListBranchesView(cliCtx.Context)
114172
if err != nil {
115173
return err
@@ -234,13 +292,17 @@ func isBranchExist(cliCtx *cli.Context, branchName string) error {
234292
}
235293

236294
func create(cliCtx *cli.Context) error {
295+
branchName := cliCtx.Args().First()
296+
297+
if branchName == "" {
298+
return commands.NewActionError("BRANCH_NAME is required to create a branch")
299+
}
300+
237301
dblabClient, err := commands.ClientByCLIContext(cliCtx)
238302
if err != nil {
239303
return err
240304
}
241305

242-
branchName := cliCtx.Args().First()
243-
244306
baseBranch := cliCtx.String("parent-branch")
245307
snapshotID := cliCtx.String("snapshot-id")
246308

@@ -315,15 +377,24 @@ func getBaseBranch(cliCtx *cli.Context) string {
315377
return baseBranch
316378
}
317379

380+
// deleteBranch removes the branch named by the --delete flag of the bare `dblab branch` form, or by
381+
// the positional argument of the `delete` subcommand.
318382
func deleteBranch(cliCtx *cli.Context) error {
383+
branchName := cliCtx.String("delete")
384+
if branchName == "" {
385+
branchName = cliCtx.Args().First()
386+
}
387+
388+
if branchName == "" {
389+
return commands.NewActionError("BRANCH_NAME is required to delete a branch")
390+
}
391+
319392
dblabClient, err := commands.ClientByCLIContext(cliCtx)
320393
if err != nil {
321394
return err
322395
}
323396

324-
branchName := cliCtx.String("delete")
325-
326-
branching, err := getBranchingFromEnv()
397+
branching, err := loadBranching()
327398
if err != nil {
328399
return err
329400
}
@@ -400,6 +471,10 @@ func history(cliCtx *cli.Context) error {
400471
return err
401472
}
402473

474+
// loadBranching reads the branching state of the current environment. It is a variable so tests can
475+
// run deleteBranch without the invoking user's CLI config.
476+
var loadBranching = getBranchingFromEnv
477+
403478
func getBranchingFromEnv() (config.Branching, error) {
404479
branching := config.Branching{}
405480

0 commit comments

Comments
 (0)