# Add branchless trail creation

`fc3274c`·

dipree·3w ago·3 files·+221 added/-113 removed

## Sessions

6604ab821878View transcript

## Changes

3

- cmd/entire/cli

- api

- Mtrail_types.go+3/-2

- Mtrail_cmd.go+202/-111

- Mtrail_cmd_test.go+16

```
77 unmodified lines

78
79
80
81
81
82
83
83
84
85
86
87

77 unmodified lines

```

```go
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"`
}
```

```go
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)
		},
	}

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
}
```

```go
func runTrailCreate(cmd *cobra.Command, title, body, base, branch, statusStr string, checkout bool) error {
	ctx := cmd.Context()
	w := cmd.OutOrStdout()
	errW := cmd.ErrOrStderr()

if err := validateTrailCreateFlagCombos(cmd, checkout, noBranch); err != nil {
		return err
	}

repo, err := strategy.OpenRepository(ctx)
	if err != nil {
		return fmt.Errorf("failed to open repository: %w", err)
	}
	defer repo.Close()

// Determine base branch.
	if base == "" {
		base = strategy.GetDefaultBranchName(repo)
		if base == "" {
			base = defaultBaseBranch
		}
	}

base = resolveTrailCreateBase(repo, base)
	_, currentBranch, _ := isOnDefaultBranchRepo(repo) //nolint:errcheck // best-effort; reuse the open repo for the current branch name
	title, body, base, branch, statusStr, err = resolveTrailCreateFields(cmd, w, title, body, base, branch, statusStr, currentBranch, noBranch)
	if err != nil {
		return err
	}
	if err := validateTrailCreateFields(ctx, title, branch, statusStr, noBranch); err != nil {
		return err
	}

client, err := NewAuthenticatedAPIClient(ctx, trailInsecureHTTP(cmd))
	if err != nil {
		return renderDataAPIAuthError(cmd.ErrOrStderr(), err)
	}
	forge, owner, repoName, err := resolveTrailRemote(ctx)
	if err != nil {
		return err
	}

branchState, err := prepareTrailCreateBranch(w, errW, repo, branch, currentBranch, noBranch)
	if err != nil {
		return err
	}

createResp, err := postTrailCreate(ctx, client, forge, owner, repoName, title, body, branch, base, statusStr)
	if err != nil {
		cleanupCreatedTrailBranch(repo, branch, branchState.LocalCreated, branchState.RemotePushed, errW)
		return err
	}
	printTrailCreateResult(w, createResp.Trail)

return maybeCheckoutTrailCreateBranch(ctx, cmd, w, branch, currentBranch, checkout, branchState.NeedsCreation)
}
```

```go
type trailCreateBranchState struct {
	NeedsCreation bool
	LocalCreated  bool
	RemotePushed  bool
}
```

```go
func validateTrailCreateFlagCombos(cmd *cobra.Command, checkout, noBranch bool) error {
	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")
	}
	return nil
}
```

### Test Cases

```go
func TestNewTrailCreateRequestCanBeBranchless(t *testing.T) {
	req := newTrailCreateRequest("title", "body", "", "main", "open")

require.Equal(t, api.TrailCreateRequest{
		Title:  "title",
		Body:   "body",
		Base:   "main",
		Status: "open",
	}, req)

encoded, err := json.Marshal(req)
	require.NoError(t, err)
	require.NotContains(t, string(encoded), "branch_name")
	require.NotContains(t, string(encoded), "branch_action")
}
```

```go
func TestCleanupCreatedTrailBranch(t *testing.T) {
	cases := []struct {
		name             string
	}
```
