Add branchless trail creation · Entire
Add branchless trail creation
9ebc42c·
dipree·3w ago·3 files·+96 added/-37 removed
Sessions
6604ab821878View transcript
Changes
3
cmd/entire/cli
api
Mtrail_types.go+3/-2
Mtrail_cmd.go+77/-35
Mtrail_cmd_test.go+16
77 unmodified lines
78
79
80
81
81
82
83
83
84
85
86
87
77 unmodified lines
type TrailCreateRequest struct {
Title string `json:"title"`
Body string `json:"body,omitempty"`
BranchName string `json:"branch_name"`
BranchName string `json:"branch_name,omitempty"`
// BranchAction is "create" (default) or "link". The CLI sends "link" to
// attach the already-pushed branch instead of backfilling it at base.
// attach an already-pushed branch instead of backfilling it at base. Omit
// both branch_name and branch_action to create a branchless trail.
BranchAction string `json:"branch_action,omitempty"`
Base string `json:"base,omitempty"`
Status string `json:"status,omitempty"`
}
Mcmd/entire/cli/api/trail_types.go+3/-2
554 unmodified lines
555
556
557
558
558
559
560
561
562
562
563
564
565
565
566
567
568
3 unmodified lines
572
573
574
575
576
577
578
579
580
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
12 unmodified lines
608
609
610
603
604
611
612
613
614
615
616
617
618
619
10 unmodified lines
630
631
632
623
624
625
626
627
633
634
635
636
637
638
639
640
641
642
2 unmodified lines
645
646
647
636
648
649
650
651
652
653
654
8 unmodified lines
663
664
665
651
652
653
654
666
667
668
669
670
671
672
673
674
675
676
10 unmodified lines
687
688
689
671
690
691
692
693
675
676
677
678
694
695
696
697
698
699
700
21 unmodified lines
722
723
724
706
725
726
727
728
729
730
731
732
22 unmodified lines
755
756
757
735
736
737
738
739
740
741
758
759
760
761
762
763
764
765
766
767
768
769
770
771
487 unmodified lines
1259
1260
1261
1236
1237
1262
1263
1264
1265
1266
14 unmodified lines
1281
1282
1283
1258
1259
1260
1261
1284
1285
1286
6 unmodified lines
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
554 unmodified lines
func newTrailCreateCmd() *cobra.Command {
var title, body, base, branch, status string
var checkout bool
var checkout, noBranch bool
cmd := &cobra.Command{
Use: "create",
Short: "Create a trail for the current or a new branch",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
return runTrailCreate(cmd, title, body, base, branch, status, checkout)
return runTrailCreate(cmd, title, body, base, branch, status, checkout, noBranch)
},
}
3 unmodified lines
cmd.Flags().StringVar(&branch, "branch", "", "Branch for the trail (defaults to current branch)")
cmd.Flags().StringVar(&status, "status", "", "Initial status (defaults to open)")
cmd.Flags().BoolVar(&checkout, "checkout", false, "Check out the branch after creating it")
cmd.Flags().BoolVar(&noBranch, "no-branch", false, "Create a branchless trail")
return cmd
}
//nolint:cyclop // sequential steps for creating a trail — splitting would obscure the flow
func runTrailCreate(cmd *cobra.Command, title, body, base, branch, statusStr string, checkout bool) error {
func runTrailCreate(cmd *cobra.Command, title, body, base, branch, statusStr string, checkout, noBranch bool) error {
ctx := cmd.Context()
w := cmd.OutOrStdout()
errW := cmd.ErrOrStderr()
if noBranch && cmd.Flags().Changed("branch") {
return errors.New("cannot combine --no-branch with --branch")
}
if noBranch && checkout {
return errors.New("cannot combine --no-branch with --checkout")
}
repo, err := strategy.OpenRepository(ctx)
if err != nil {
return fmt.Errorf("failed to open repository: %w", err)
}
12 unmodified lines
interactive := !cmd.Flags().Changed("title") && !cmd.Flags().Changed("branch")
if interactive {
// Interactive flow: title → body → branch (derived) → status.
if err := runTrailCreateInteractive(&title, &body, &branch, &statusStr); err != nil {
// Interactive flow: title → body → branch (unless branchless) → status.
if err := runTrailCreateInteractive(&title, &body, &branch, &statusStr, noBranch); err != nil {
return handleFormCancellation(w, "Trail creation", err)
}
} else if noBranch {
branch = ""
} else {
// Non-interactive: derive missing values from provided flags. With
// --branch omitted, use the checked-out branch (a feature branch); only
10 unmodified lines
if title == "" {
return errors.New("trail title is required")
}
if branch == "" {
return errors.New("branch name is required")
}
if err := ValidateBranchName(ctx, branch); err != nil {
return err
}
if !noBranch {
if branch == "" {
return errors.New("branch name is required")
}
if err := ValidateBranchName(ctx, branch); err != nil {
return err
}
}
if statusStr == "" {
statusStr = string(trail.StatusOpen)
2 unmodified lines
return fmt.Errorf("invalid status %q: valid values are %s", statusStr, formatValidStatuses())
}
needsCreation := branchNeedsCreation(repo, branch)
needsCreation := false
if !noBranch {
needsCreation = branchNeedsCreation(repo, branch)
}
client, err := NewAuthenticatedAPIClient(ctx, trailInsecureHTTP(cmd))
if err != nil {
8 unmodified lines
var remoteBranchPushed bool
// Don't delete a remote branch we didn't create during cleanup.
existedOnOrigin, existErr := branchExistsOnOrigin(branch)
if existErr != nil {
fmt.Fprintf(errW, "Warning: could not check whether branch %s already exists on origin: %v\n", branch, existErr)
existedOnOrigin = true
existedOnOrigin := false
if !noBranch {
var existErr error
existedOnOrigin, existErr = branchExistsOnOrigin(branch)
if existErr != nil {
fmt.Fprintf(errW, "Warning: could not check whether branch %s already exists on origin: %v\n", branch, existErr)
existedOnOrigin = true
}
}
if needsCreation {
10 unmodified lines
localBranchCreated = true
fmt.Fprintf(w, "Created branch %s\n", branch)
}
} else if currentBranch != branch {
} else if !noBranch && currentBranch != branch {
fmt.Fprintf(w, "Note: trail will be created for branch %q (not the current branch)\n", branch)
}
// Always push the branch first: the trail binds to a remote branch, so
// deliver it before creating the trail rather than letting the server
// backfill it at the base tip.
{
// For branch-backed trails, always push the branch first: the trail binds to a
// remote branch, so deliver it before creating the trail rather than letting
// the server backfill it at the base tip. Branchless trails skip this entirely.
if !noBranch {
if err := pushBranchToOrigin(branch); err != nil {
cleanupCreatedTrailBranch(repo, branch, localBranchCreated, false, errW)
return fmt.Errorf("failed to push branch %q to origin: %w\n\nhint: the trail was not created because its branch could not be delivered to the remote.\n - if this is an auth error, link your GitHub account and retry\n - if this is a non-fast-forward, update branch %q from origin and retry", branch, err, branch)
}
21 unmodified lines
return fmt.Errorf("failed to decode create response: %w", err)
fmt.Fprintf(w, "Created trail %q for branch %s (ID: %s)\n", createResp.Trail.Title, createResp.Trail.Branch, createResp.Trail.ID)
if createResp.Trail.Branch == "" {
fmt.Fprintf(w, "Created trail %q (ID: %s)\n", createResp.Trail.Title, createResp.Trail.ID)
} else {
fmt.Fprintf(w, "Created trail %q for branch %s (ID: %s)\n", createResp.Trail.Title, createResp.Trail.Branch, createResp.Trail.ID)
}
if needsCreation && currentBranch != branch {
shouldCheckout := checkout
22 unmodified lines
}
func newTrailCreateRequest(title, body, branch, base, statusStr string) api.TrailCreateRequest {
return api.TrailCreateRequest{
Title: title,
Body: body,
BranchName: branch,
BranchAction: "link",
Base: base,
Status: statusStr,
}
}
func newTrailUpdateCmd() *cobra.Command {
487 unmodified lines
// runTrailCreateInteractive runs the interactive form for trail creation.
// Prompts for title, body, branch (derived from title), and status.
func runTrailCreateInteractive(title, body, branch, statusStr *string) error {
// Prompts for title, body, branch (derived from title, unless branchless), and status.
func runTrailCreateInteractive(title, body, branch, statusStr *string, noBranch bool) error {
// Step 1: Title and body
form := NewAccessibleForm(
huh.NewGroup(
14 unmodified lines
return errors.New("trail title is required")
}
// Step 2: Branch (derived from title) and status
suggested := slugifyTitle(*title)
*branch = suggested
// Build status options, excluding done/closed
var statusOptions []huh.Option[string]
for _, s := range trail.ValidStatuses() {
6 unmodified lines
*statusStr = string(trail.StatusOpen)
}
if noBranch {
*branch = ""
form = NewAccessibleForm(
huh.NewGroup(
huh.NewSelect[string]().
Title("Status").
Options(statusOptions...).
Value(statusStr),
),
)
if err := form.Run(); err != nil {
return fmt.Errorf("form cancelled: %w", err)
}
return nil
}
// Step 2: Branch (derived from title) and status
suggested := slugifyTitle(*title)
*branch = suggested
form = NewAccessibleForm(
huh.NewGroup(
huh.NewInput().