Skip to content

Commit 90ac27c

Browse files
committed
Refuse a container flag that a move or copy would ignore
todos copy 456 --todolist 901 copied the to-do into its own list and reported success: --todolist is a root flag todos copy never reads. todolistgroups copy --list failed the same way and was fixed on its own, as were cards copy --card-table and files move --vault. Every command that moves or copies recordings now goes through guardFilingFlags, which names the container flags the command inherits and reads. Any other one that was set, the root's --todolist or a flag the command's group adds (--card-table, --vault, --folder, --list), is refused before anything is read or sent, with the command's own --to-<container> flags as the hint. The three one-off refusals go. --project and --in stay with each command, which holds a filed recording to them. The same-project modes are covered too: todos move --to 1 --todolist and todolistgroups position --list, which ignored the flag, now refuse it. cards move and cards column move read --card-table, so they keep it. The test walks the whole command tree: every move or copy command declares what it reads, and each container flag it inherits but does not read is refused with no request sent.
1 parent 6418a69 commit 90ac27c

12 files changed

Lines changed: 298 additions & 84 deletions

‎internal/commands/cards.go‎

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func NewCardsCmd() *cobra.Command {
4343
newCardsCreateCmd(&project, &cardTable),
4444
newCardsUpdateCmd(),
4545
newCardsMoveCmd(&project, &cardTable),
46-
newCardsCopyCmd(&project, &cardTable),
46+
newCardsCopyCmd(&project),
4747
newCardsDoneCmd(&project, &cardTable),
4848
newCardsColumnsCmd(&project, &cardTable),
4949
newCardsColumnCmd(&project, &cardTable),
@@ -1374,7 +1374,13 @@ Basecamp to finish the move and shows the card where it landed.`,
13741374
if targetColumn != "" || onHold || positionSet || wormholeSet {
13751375
return output.ErrUsage("--to-project and --to-card-table cannot be combined with --to, --on-hold, --position, or --to-wormhole")
13761376
}
1377-
if err := validateFilingFlags(*cardTable, toProject, toCardTable); err != nil {
1377+
if *cardTable != "" {
1378+
return output.ErrUsageHint(
1379+
"--card-table names the card's own card table, not where it goes",
1380+
"Use --to-card-table for the destination card table",
1381+
)
1382+
}
1383+
if err := validateFilingFlags(toProject, toCardTable); err != nil {
13781384
return err
13791385
}
13801386
} else if wormholeSet {
@@ -1563,7 +1569,7 @@ Basecamp to finish the move and shows the card where it landed.`,
15631569
cmd.Flags().StringVar(&toWormhole, "to-wormhole", "", "Send the card to another project's card table through an existing wormhole (wormhole ID or destination-column URL)")
15641570
addFilingDestinationFlags(cmd, &toProject, &toCardTable, "Move")
15651571

1566-
return cmd
1572+
return guardFilingFlags(cmd, "card-table")
15671573
}
15681574

15691575
// moveCardThroughWormhole teleports a card across projects through a wormhole to
@@ -1797,7 +1803,7 @@ func parseColumnID(ref string) (int64, error) {
17971803
return id, nil
17981804
}
17991805

1800-
func newCardsCopyCmd(project, cardTable *string) *cobra.Command {
1806+
func newCardsCopyCmd(project *string) *cobra.Command {
18011807
var toProject, toCardTable string
18021808
var includeComments bool
18031809

@@ -1826,8 +1832,8 @@ the new card.`,
18261832
},
18271833
RunE: func(cmd *cobra.Command, args []string) error {
18281834
filingSet := cmd.Flags().Changed("to-project") || cmd.Flags().Changed("to-card-table")
1829-
if filingSet || *cardTable != "" {
1830-
if err := validateFilingFlags(*cardTable, toProject, toCardTable); err != nil {
1835+
if filingSet {
1836+
if err := validateFilingFlags(toProject, toCardTable); err != nil {
18311837
return err
18321838
}
18331839
}
@@ -1874,7 +1880,7 @@ the new card.`,
18741880
addFilingDestinationFlags(cmd, &toProject, &toCardTable, "Copy")
18751881
cmd.Flags().BoolVar(&includeComments, "include-comments", false, "Copy the card's comments too")
18761882

1877-
return cmd
1883+
return guardFilingFlags(cmd)
18781884
}
18791885

18801886
// addFilingDestinationFlags adds --to-project and --to-card-table, which name
@@ -1885,15 +1891,8 @@ func addFilingDestinationFlags(cmd *cobra.Command, toProject, toCardTable *strin
18851891
}
18861892

18871893
// validateFilingFlags checks --to-project and --to-card-table before any
1888-
// request. The group's --card-table names the card's own table, so it is
1889-
// refused here rather than mistaken for the destination.
1890-
func validateFilingFlags(sourceCardTable, toProject, toCardTable string) error {
1891-
if sourceCardTable != "" {
1892-
return output.ErrUsageHint(
1893-
"--card-table names the card's own card table, not where it goes",
1894-
"Use --to-card-table for the destination card table",
1895-
)
1896-
}
1894+
// request.
1895+
func validateFilingFlags(toProject, toCardTable string) error {
18971896
if toProject == "" && toCardTable == "" {
18981897
return output.ErrUsage("--to-project or --to-card-table needs a value")
18991898
}
@@ -2748,7 +2747,7 @@ You can pass either a column ID or a Basecamp URL:
27482747
cmd.Flags().IntVar(&position, "position", 0, "Target position (1-indexed)")
27492748
cmd.Flags().IntVar(&position, "pos", 0, "Target position (alias for --position)")
27502749

2751-
return cmd
2750+
return guardFilingFlags(cmd, "card-table")
27522751
}
27532752

27542753
func newCardsColumnWatchCmd() *cobra.Command {

‎internal/commands/cards_filing_test.go‎

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -444,16 +444,6 @@ func TestCardsCopyStillRunningSaysNotToCopyAgain(t *testing.T) {
444444
assert.Len(t, transport.posts, 1)
445445
}
446446

447-
func TestCardsCopyRefusesTheSourceCardTableFlag(t *testing.T) {
448-
transport := &mockFilingTransport{}
449-
run := runFilingCommand(t, transport, "copy", "456", "--card-table", "555")
450-
e := requireFilingError(t, run)
451-
452-
assert.Equal(t, output.CodeUsage, e.Code)
453-
assert.Contains(t, e.Hint, "--to-card-table")
454-
assert.Empty(t, transport.posts)
455-
}
456-
457447
func TestCardsMoveToAForeignColumnPointsAtCrossProjectMoves(t *testing.T) {
458448
transport := &mockFilingTransport{}
459449
run := runFilingCommand(t, transport, "move", "456", "--to", "8888", "--in", "123")

‎internal/commands/files.go‎

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2295,11 +2295,6 @@ You can pass IDs or Basecamp URLs for both the item and the folder:
22952295
Annotations: map[string]string{"agent_notes": "Moves in place within one project: the ID, URL, and comments stay the same. --position is 1-indexed (default: first). A folder in another project is refused."},
22962296
Args: cobra.ExactArgs(1),
22972297
RunE: func(cmd *cobra.Command, args []string) error {
2298-
// --vault/--folder filter the files group's listings; here they would be
2299-
// silently ignored, and reading them as the destination is a guess.
2300-
if cmd.Flags().Changed("vault") || cmd.Flags().Changed("folder") {
2301-
return output.ErrUsageHint("files move takes its destination from --to", "basecamp files move <id|url> --to <folder-id|url>")
2302-
}
23032298
if to == "" {
23042299
return missingArg(cmd, "--to")
23052300
}
@@ -2386,7 +2381,7 @@ You can pass IDs or Basecamp URLs for both the item and the folder:
23862381
cmd.Flags().StringVar(&to, "to", "", "Destination folder ID or URL")
23872382
cmd.Flags().IntVar(&position, "position", 0, "Position in the folder (1-indexed, default: first; past the end means last)")
23882383

2389-
return cmd
2384+
return guardFilingFlags(cmd)
23902385
}
23912386

23922387
// filesMoveRef is an ID or URL given to files move, reduced to what the move

‎internal/commands/files_test.go‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1748,8 +1748,6 @@ func TestFilesMoveWithoutDestinationShowsHelp(t *testing.T) {
17481748

17491749
func TestFilesMoveRefusesBadArgumentsBeforeTheWire(t *testing.T) {
17501750
cases := map[string][]string{
1751-
"--folder for --to": {"123", "--folder", "456"},
1752-
"--vault beside --to": {"123", "--to", "456", "--vault", "789"},
17531751
"zero position": {"123", "--to", "456", "--position", "0"},
17541752
"negative position": {"123", "--to", "456", "--position", "-1"},
17551753
"non-numeric destination": {"123", "--to", "Designs"},

‎internal/commands/filing.go‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"time"
1212

1313
"github.com/spf13/cobra"
14+
"github.com/spf13/pflag"
1415

1516
"github.com/basecamp/basecamp-sdk/go/pkg/basecamp"
1617

@@ -466,3 +467,72 @@ func rejectProjectConflict(cmd *cobra.Command, app *appctx.App, noun, flagProjec
466467
}
467468
return check(urlProjectID, "the "+noun+" URL")
468469
}
470+
471+
// AnnotationFilingReads lists, space-separated, the container flags that a
472+
// command moving or copying recordings inherits and reads. guardFilingFlags
473+
// sets it.
474+
const AnnotationFilingReads = "filing_reads"
475+
476+
// guardFilingFlags makes a command that moves or copies recordings refuse,
477+
// before it reads or sends anything, a container flag it inherits but never
478+
// reads. Such a flag would be ignored: todos copy --todolist 901 copied the
479+
// to-do into its own list and reported success. reads names the inherited
480+
// container flags the command does read, in some mode.
481+
//
482+
// A container flag is the root's --todolist or a flag that a command group
483+
// adds for its commands (--card-table, --vault, --list, …). --project and
484+
// --in are left to each command, which holds a filed recording to them; the
485+
// root's account, profile and output flags are not containers.
486+
func guardFilingFlags(cmd *cobra.Command, reads ...string) *cobra.Command {
487+
if cmd.Annotations == nil {
488+
cmd.Annotations = map[string]string{}
489+
}
490+
cmd.Annotations[AnnotationFilingReads] = strings.Join(reads, " ")
491+
run := cmd.RunE
492+
cmd.RunE = func(cmd *cobra.Command, args []string) error {
493+
if err := refuseUnreadContainerFlags(cmd, reads); err != nil {
494+
return err
495+
}
496+
return run(cmd, args)
497+
}
498+
return cmd
499+
}
500+
501+
func refuseUnreadContainerFlags(cmd *cobra.Command, reads []string) error {
502+
var unread *pflag.Flag
503+
cmd.InheritedFlags().VisitAll(func(f *pflag.Flag) {
504+
if unread == nil && f.Changed && isContainerFlag(cmd, f) && !slices.Contains(reads, f.Name) {
505+
unread = f
506+
}
507+
})
508+
if unread == nil {
509+
return nil
510+
}
511+
hint := fmt.Sprintf("%s takes no destination; drop --%s", cmd.CommandPath(), unread.Name)
512+
if dests := destinationFlags(cmd); len(dests) > 0 {
513+
hint = fmt.Sprintf("Name where it goes with %s (see %s --help)", strings.Join(dests, " or "), cmd.CommandPath())
514+
}
515+
return output.ErrUsageHint(fmt.Sprintf("%s does not read --%s, so it would ignore it", cmd.CommandPath(), unread.Name), hint)
516+
}
517+
518+
func isContainerFlag(cmd *cobra.Command, f *pflag.Flag) bool {
519+
switch f.Name {
520+
case "project", "in":
521+
return false
522+
case "todolist":
523+
return true
524+
}
525+
return cmd.Root().PersistentFlags().Lookup(f.Name) != f
526+
}
527+
528+
// destinationFlags lists a command's own flags that name where it files a
529+
// recording: --to (a folder or column) and the --to-<container> flags.
530+
func destinationFlags(cmd *cobra.Command) []string {
531+
var dests []string
532+
cmd.LocalNonPersistentFlags().VisitAll(func(f *pflag.Flag) {
533+
if strings.HasPrefix(f.Name, "to-") || (f.Name == "to" && f.Value.Type() == "string") {
534+
dests = append(dests, "--"+f.Name)
535+
}
536+
})
537+
return dests
538+
}

0 commit comments

Comments
 (0)