Add `entire trail checkout` subcommand · Entire
Add entire trail checkout subcommand
643ba8e→main·
Check out the branch of a trail, resolved from the first argument or --trail (number, id, or branch), or the current branch when omitted.
- Looks the trail up against the current clone's origin remote, so a resolved trail inherently belongs to the repo you run it from.
- Reuses switchToBranchForResume for the local-vs-remote checkout, the uncommitted-changes guard, and the fetch prompt (--force skips it).
- Extracts a shared resolveTrailBySelector helper and refactors runTrailShow to use it.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
Sessions
Changes
2
cmd/entire/cli
Atrail_checkout_cmd_test.go+119
Mtrail_cmd.go+121/-22
package cli
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/entireio/cli/cmd/entire/cli/api"
)
func TestResolveTrailBySelector_FindsBySelector(t *testing.T) {
// Not t.Parallel(): the subtests share one httptest server closed on
// return, so they must run synchronously before the deferred Close.
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
if err := json.NewEncoder(w).Encode(api.TrailListResponse{
Trails: []api.TrailResource{
{ID: "trl_a", Number: 1, Branch: "feature/a", Title: "Alpha"},
{ID: "trl_b", Number: 575, Branch: "feature/b", Title: "Bravo"},
},
Total: 2,
}); err != nil {
t.Errorf("encode response: %v", err)
}
}))
defer srv.Close()
client := api.NewClientWithBaseURL("tok", srv.URL)
cases := []struct {
name string
selector string
wantID string
}{
{"by number", "575", "trl_b"},
{"by id", "trl_a", "trl_a"},
{"by branch", "feature/b", "trl_b"},
{"trims whitespace", " feature/a ", "trl_a"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
found, err := resolveTrailBySelector(context.Background(), client, "gh", "acme", "repo", tc.selector)
if err != nil {
t.Fatalf("resolveTrailBySelector: %v", err)
}
if found == nil || found.ID != tc.wantID {
t.Fatalf("found = %#v, want ID %q", found, tc.wantID)
}
})
}
}
func TestResolveTrailBySelector_NotFoundIsAnError(t *testing.T) {
t.Parallel()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
if err := json.NewEncoder(w).Encode(api.TrailListResponse{Trails: []api.TrailResource{}}); err != nil {
t.Errorf("encode response: %v", err)
}
}))
defer srv.Close()
client := api.NewClientWithBaseURL("tok", srv.URL)
found, err := resolveTrailBySelector(context.Background(), client, "gh", "acme", "repo", "does-not-exist")
if err == nil {
t.Fatalf("expected error for missing trail, got found = %#v", found)
}
if found != nil {
t.Fatalf("found = %#v, want nil on error", found)
}
if !strings.Contains(err.Error(), "does-not-exist") {
t.Fatalf("error %q should name the selector", err)
}
}
func TestDescribeTrailRef(t *testing.T) {
t.Parallel()
cases := []struct {
name string
in api.TrailResource
want string
}{
{"number and title", api.TrailResource{Number: 575, Title: "Add foo"}, "trail #575 (Add foo)"},
{"number without title", api.TrailResource{Number: 575}, "trail #575"},
{"title without number", api.TrailResource{Title: "Add foo"}, `trail "Add foo"`},
{"neither", api.TrailResource{}, "trail"},
{"title trimmed", api.TrailResource{Number: 1, Title: " Add foo "}, "trail #1 (Add foo)"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got := describeTrailRef(&tc.in)
if got != tc.want {
t.Fatalf("describeTrailRef(%#v) = %q, want %q", tc.in, got, tc.want)
}
})
}
}
func TestTrailCheckoutRejectsArgWithTrailFlag(t *testing.T) {
t.Parallel()
cmd := newTrailCheckoutCmd()
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
cmd.SetArgs([]string{"feature/b", "--trail", "575"})
err := cmd.Execute()
if err == nil {
t.Fatal("expected error combining a positional arg with --trail, got nil")
}
if !strings.Contains(err.Error(), "cannot combine") {
t.Fatalf("error = %q, want it to mention 'cannot combine'", err)
}
}