Add branchless trail creation · Entire

Add branchless trail creation

9ebc42c· dipree·3w ago·3 files·+96 added/-37 removed

Sessions

6604ab821878View transcript

Changes

3

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().