cli: resolve the recap repo scope and display name in one pass · Entire
cli: resolve the recap repo scope and display name in one pass
9c03a7f→main·
Soph·1w ago·2 files·+45 added/-37 removed
Review finding on the cell path: newRecapClient resolved the origin remote (plus the control-plane mirror lookup) via currentRepoID, then runRecap resolved the same remote again via currentRepoSlug for the scope-line display name — two git resolutions per recap.
currentRepoID becomes currentRepoRef and returns (repoID, repoSlug) from the single resolution it already does; newRecapClient threads both through, and the data-API path returns its slug as both scope and name. runRecap no longer re-resolves. An un-mirrored repo yields empty scope AND name, preserving the unscoped-recap-isn't-mislabelled rule.
Co-Authored-By: Claude Fable 5 noreply@anthropic.com
Sessions
6dd06fa6260aView transcript
[?
Investigate PR #1626 Green StatusClaude Code·2 steps](/content/gh/entireio/cli/session/6a0099e5-7fd7-4791-8a8e-21dfd2dc683f#timeline-6dd06fa6260a/index.html)
Changes
2
cmd/entire/cli
Mentireapi_client.go+25/-18
Mrecap.go+20/-19
13 unmodified lines
14
15
16
17
18
19
20
17
18
19
20
21
22
23
24
39 unmodified lines
64
65
66
66
67
68
69
70
71
72
73
67
68
69
70
71
72
73
74
75
76
77
78
79
77
78
80
81
82
83
84
82
85
86
87
88
86
89
90
91
92
90
93
94
92
95
96
97
98
99
100
101
102
13 unmodified lines
"github.com/entireio/cli/internal/coreapi"
// currentRepoIDTimeout bounds currentRepoID's control-plane lookup. The lookup
// is best-effort decoration (recap degrades to personal-only without it), so a
// stalled core must not hang the command — mirror expertsCellResolveTimeout.
const currentRepoIDTimeout = 5 * time.Second
// currentRepoRefTimeout bounds currentRepoRef's control-plane lookup. The
// lookup is best-effort decoration (recap degrades to personal-only without
// it), so a stalled core must not hang the command — mirror
// expertsCellResolveTimeout.
const currentRepoRefTimeout = 5 * time.Second
// runAuthenticatedActivityAPI runs fn with an authenticated client for the
// activity/recap surface. It prefers the caller's home entire-api cell (the same
39 unmodified lines
}
// currentRepoID best-effort resolves the current repo (its "origin" remote) to
// the ULID entire-api uses for repo-scoped params — recap's /me/recap?repo=.
// entire.io/api documents the mirror id as exactly that repo_id (repo_id =
// mirror_repos.id), and the CLI already lists mirrors via the control plane, so
// no extra resolution is needed. Any failure returns "" — recap then shows the
// personal side only rather than erroring.
func currentRepoID(ctx context.Context) string {
ctx, cancel := context.WithTimeout(ctx, currentRepoIDTimeout)
// currentRepoRef best-effort resolves the current repo (its "origin" remote)
// to the ULID entire-api uses for repo-scoped params — recap's /me/recap?repo=
// — plus the human owner/repo slug for display, from a single remote
// resolution (the caller needs both; resolving twice would double the git and
// control-plane work). entire.io/api documents the mirror id as exactly that
// repo_id (repo_id = mirror_repos.id), and the CLI already lists mirrors via the
// control plane, so no extra resolution is needed. Any failure returns
// "", "" — recap then shows the personal side only rather than erroring.
func currentRepoRef(ctx context.Context) (repoID, repoSlug string) {
ctx, cancel := context.WithTimeout(ctx, currentRepoRefTimeout)
defer cancel()
forge, owner, repo, err := gitremote.ResolveRemoteRepo(ctx, "origin")
if err != nil {
return ""
}
if err != nil || owner == "" || repo == "" {
return "", ""
}
provider, ok := forgeToMirrorProvider(forge)
if !ok {
return ""
}
return "", ""
c, err := coreapi.New()
if err != nil {
return ""
}
mirrors, err := listMirrorsForRepo(ctx, c, provider, strings.ToLower(owner), repo)
if err != nil {
return ""
}
repoID = firstActiveRepoID(mirrors)
if repoID == "" {
return "", ""
}
return repoID, owner + "/" + repo
}
// firstActiveRepoID returns the id of the repo's first active mirror (the repo
Mcmd/entire/cli/entireapi_client.go+25/-18
122 unmodified lines
123
124
125
126
126
127
128
129
130
131
132
133
8 unmodified lines
142
143
144
141
142
143
144
145
146
147
148
145
146
147
36 unmodified lines
184
185
186
191
192
193
194
195
187
188
189
190
191
192
193
194
195
196
2 unmodified lines
199
200
201
204
202
203
204
205
206
207
210
208
209
210
211
212
6 unmodified lines
219
220
221
223
222
223
224
225
227
226
227
228
230
229
230
231
232
233
234
122 unmodified lines
if err != nil {
return err
}
client, repoScope, err := newRecapClient(ctx, f.insecureHTTP)
// repoName is the human owner/repo label for the scope line: the ?repo=
// scope value is a repo_id ULID when routed to a cell (echoed back verbatim
// by the response), which is meaningless to show a user. Both are empty when
// no repo query is sent, so an unscoped recap isn't mislabelled.
client, repoScope, repoName, err := newRecapClient(ctx, f.insecureHTTP)
if err != nil {
if errors.Is(err, api.ErrInsecureHTTP) {
fmt.Fprintf(errW, "ENTIRE_API_BASE_URL is set to an insecure http:// URL (%s). Use https:// for production, or pass --insecure-http-auth for local dev.\n", api.BaseURL())
}
return err
}
rangeKey := f.rangeKey()
// The ?repo= value is a repo_id ULID when routed to a cell, which the recap
// response echoes back; show the human owner/repo name instead. Only when
// actually scoped (a repo query was sent), so an unscoped recap isn't
// mislabelled as scoped to the current repo.
repoName := ""
if repoScope != "" {
repoName = currentRepoSlug(ctx)
}
if f.useTUI(interactive.IsTerminalWriter(w), interactive.CanPromptInteractively(), IsAccessibleMode()) {
return runRecapTUI(ctx, client, recapTUIOptions{
Range: rangeKey,
36 unmodified lines
// these were all relabelled as keyring read failures via keyringReadError,
// which sent users on wild goose chases when the keyring was fine and the real
// problem was downstream.
// newRecapClient returns the recap client and the value to pass as /me/recap's
// ?repo= (its team/contributors scope): the current repo's ULID when routed to an
// entire-api cell (which addresses repos by id), or its owner/repo slug on
the
// data API (which addresses them by name). Empty when the current repo
// can't be resolved — recap then shows the personal side only.
// newRecapClient returns the recap client, the value to pass as /me/recap's
// ?repo= (its team/contributors scope), and the repo's owner/repo display name.
// The scope is the current repo's ULID when routed to an entire-api cell (which
// addresses repos by id), or its owner/repo slug on the data API (which
// addresses them by name); both come from one remote/mirror resolution, so the
// caller never re-resolves for display. Empty when the current repo can't be
// resolved — recap then shows the personal side only.
//
// It prefers the caller's home entire-api cell (the shared client) and falls
// back to the data API on ANY cell-client failure — the cell path is a
2 unmodified lines
// are silent; unexpected ones are debug-logged (logCellClientFallback). Only
// failures of the data-API path itself surface — except ErrNotLoggedIn, which
// recap tolerates, rendering and letting the server answer 401.
func newRecapClient(ctx context.Context, insecureHTTP bool) (*api.Client, string, error) {
func newRecapClient(ctx context.Context, insecureHTTP bool) (client *api.Client, repoScope, repoName string, err error) {
// Best-effort upgrade: on any cell failure fall back to the data API path
// below, which tolerates a missing login (renders, lets the server 401) so
// the not-logged-in case keeps working.
cellClient, cellErr := auth.NewEntireAPICellClient(ctx, insecureHTTP, nil)
if cellErr == nil {
return cellClient, currentRepoID(ctx), nil
repoID, repoSlug := currentRepoRef(ctx)
return cellClient, repoID, repoSlug, nil
}
logCellClientFallback(ctx, cellErr)
6 unmodified lines
err = nil
}
if err != nil {
return nil, "", err
return nil, "", "", err
}
if token != "" && !insecureHTTP {
if err := api.RequireSecureURL(api.BaseURL()); err != nil {
return nil, "", fmt.Errorf("base URL check: %w", err)
return nil, "", "", fmt.Errorf("base URL check: %w", err)
}
}
return api.NewClient(token), currentRepoSlug(ctx), nil
// The data API scopes by slug, so scope and display name coincide.
slug := currentRepoSlug(ctx)
return api.NewClient(token), slug, slug, nil
}
func handleRecapFetchError(w io.Writer, err error) error {