feat(trail): surface trail web URL in create/show · Entire
feat(trail): surface trail web URL in create/show
7af26cb→main·
toothbrush·3w ago·3 files·+76 added/-6 removed
trail create now prints the trail's browser URL, and both create and
show prefer the canonical url the server returns over a locally
constructed one — so the CLI tracks any route change without needing a
lockstep update. Local construction remains only as a fallback for older
servers that omit the field.
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
Sessions
df9e84f74c5eView transcript
Changes
3
cmd/entire/cli
api
Mtrail_types.go+6/-2
Mtrail_cmd.go+27/-4
Mtrail_cmd_test.go+43
20 unmodified lines
21
22
23
24
25
24
25
26
27
28
29
30
31
32
20 unmodified lines
// TrailResource represents a single trail from the API.
type TrailResource struct {
ID string `json:"id,omitempty"`
Number int `json:"number,omitempty"`
// URL is the canonical browser URL for the trail. The server provides it so
// clients need not reconstruct the route; it may be empty against older
// servers that predate the field.
URL string `json:"url,omitempty"`
Branch string `json:"branch"`
Base string `json:"base"`
Title string `json:"title"`
}
Mcmd/entire/cli/api/trail_types.go+6/-2
132 unmodified lines
133
134
135
136
136
137
138
139
140
141
142
143
143
144
145
94 unmodified lines
240
241
242
244
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
541 unmodified lines
812
813
814
792
815
816
817
818
132 unmodified lines
// the core metadata already came from the list, so a detail failure
// falls back to the list body with a warning rather than failing.
m := found.ToMetadata()
webURL := ""
webURL := trailDisplayURL(*found, forge, owner, repo)
// Seed the description from the list body so a failed (or skipped)
// detail fetch still shows something; a successful detail fetch
// supersedes it with the richer body_document text below.
bodyText := found.Body
descriptionLoaded := strings.TrimSpace(found.Body) != ""
if found.Number > 0 {
webURL = trailWebURL(api.BaseURL(), forge, owner, repo, found.Number)
if bt, derr := fetchTrailDescription(ctx, client, forge, owner, repo, found.Number); derr == nil {
// A successful fetch means we authoritatively consulted the
// description, but it only supersedes the seeded list body when
94 unmodified lines
}
}
return ""
}
// trailWebURL builds the browser URL for a trail:
// printCreatedTrail reports a newly created trail, including its browser URL
// (the same URL `trail show` surfaces) when one is available.
func printCreatedTrail(w io.Writer, t api.TrailResource, forge, owner, repo string) {
fmt.Fprintf(w, "Created trail %q for branch %s (ID: %s)\n", t.Title, t.Branch, t.ID)
if url := trailDisplayURL(t, forge, owner, repo); url != "" {
fmt.Fprintf(w, " URL: %s\n", url)
}
}
// trailDisplayURL returns the trail's browser URL. It prefers the canonical URL
// the server now returns, so the CLI tracks any route change without being
// updated in lockstep, and falls back to a locally constructed URL only for
// older servers that omit the field.
func trailDisplayURL(t api.TrailResource, forge, owner, repo string) string {
if strings.TrimSpace(t.URL) != "" {
return t.URL
}
if t.Number > 0 {
return trailWebURL(api.BaseURL(), forge, owner, repo, t.Number)
}
return ""
}
// trailWebURL builds a fallback browser URL for a trail used only when the
// server does not supply one (older servers):
// <web-origin>/<forge>/<owner>/<repo>/trails/<number>. In production the web app
// is served from the same origin as the data API, so the API base URL doubles
// as the web origin. A split local-dev setup (API and frontend on different
541 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)
printCreatedTrail(w, createResp.Trail, forge, owner, repoName)
if needsCreation && currentBranch != branch {
shouldCheckout := checkout
}
}
Mcmd/entire/cli/trail_cmd.go+27/-4
301 unmodified lines
302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350
301 unmodified lines
}
func TestPrintCreatedTrail(t *testing.T) {
t.Parallel()
// The server-provided URL is used verbatim.
var out bytes.Buffer
printCreatedTrail(&out, api.TrailResource{Title: "Fix it", Branch: "feat/x", ID: "abc123", Number: 575, URL: "https://entire.io/gh/acme/repo/trails/575/fix-it"}, "gh", "acme", "repo")
text := out.String()
if !strings.Contains(text, Created trail "Fix it" for branch feat/x (ID: abc123)) {
t.Fatalf("missing create summary line, got:\n%s", text) } if !strings.Contains(text, "URL: https://entire.io/gh/acme/repo/trails/575/fix-it") {
t.Fatalf("expected the server-provided URL, got:\n%s", text) }
// Without a number, omit the URL line. out.Reset() printCreatedTrail(&out, api.TrailResource{Title: "No num", Branch: "feat/y", ID: "def456"}, "gh", "acme", "repo") if text := out.String(); strings.Contains(text, "URL:") {
t.Fatalf("expected URL omitted when number and URL are absent, got:\n%s", text) } }
func TestTrailDisplayURL(t *testing.T) {
t.Parallel()
// Server URL wins, even when a number is present. got := trailDisplayURL(api.TrailResource{Number: 5, URL: "https://server/url"}, "gh", "acme", "repo") if got != "https://server/url" {
t.Fatalf("expected server URL, got %q", got) }
// Falls back to a constructed URL for older servers that omit it. got = trailDisplayURL(api.TrailResource{Number: 5}, "gh", "acme", "repo") if !strings.HasSuffix(got, "/gh/acme/repo/trails/5") {
t.Fatalf("expected constructed fallback URL, got %q", got) }
// Nothing to show when neither is available. if got := trailDisplayURL(api.TrailResource{}, "gh", "acme", "repo"); got != "" {
t.Fatalf("expected empty URL, got %q", got) } }
func TestTrailDescriptionForDisplay(t *testing.T) {
t.Parallel() if got := trailDescriptionForDisplay("the body", true); got != "the body" {
t.Fatalf("unexpected description output, got: %v", got) }}