Conversation
Replace git-based trail CRUD operations with API calls when the user is authenticated via `entire login`. Falls back to the existing git-backed store when not logged in, preserving backward compatibility. Key changes: - Add trail.Store interface with GitStore and APIStore implementations - APIStore uses Bearer auth from keyring, resolves org/repo from origin - On first API-backed operation, migrate existing git trails and place a DEPRECATED_MOVED_TO_DATABASE_STORAGE marker on entire/trails/v1 - Skip git fetch/push of trails branch when using API store - Add gitremote package for shared remote URL parsing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 414167a7bfdb
PR SummaryHigh Risk Overview Adds Updates trail CLI commands and commit-hook checkpoint linking to resolve the appropriate store at runtime, and skips fetching/pushing the trails branch when using the API-backed store. Written by Cursor Bugbot for commit 48fa9a8. Configure here. |
Entire-Checkpoint: 2c5705f6152f
There was a problem hiding this comment.
Pull request overview
This PR introduces an abstraction for trail storage so the CLI can use either the existing git-orphan-branch trail storage or a new API-backed storage, with optional migration from git to the API.
Changes:
- Introduces a
trail.Storeinterface and renames the git-backed implementation toGitStore. - Adds an
APIStoreimplementation plus store-resolution and migration helpers (ResolveStore,MigrateIfNeeded). - Updates trail commands and checkpoint linking to resolve the appropriate store (API vs git) and skip git-branch fetch/push when API-backed.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| cmd/entire/cli/trail/trail.go | Adds trail.Store interface and updates package docs for dual backends. |
| cmd/entire/cli/trail/store.go | Renames git-backed store to GitStore, adds NewGitStore, deprecates NewStore. |
| cmd/entire/cli/trail/api_store.go | Implements API-backed trail CRUD via HTTP. |
| cmd/entire/cli/trail/resolve.go | Adds backend selection (ResolveStore) and API-backed detection helper. |
| cmd/entire/cli/trail/migrate.go | Adds git→API migration + migration marker mechanics. |
| cmd/entire/cli/trail_cmd.go | Updates trail CLI commands to resolve store and conditionally fetch/push git trails branch. |
| cmd/entire/cli/strategy/manual_commit_hooks.go | Uses resolved store when best-effort linking checkpoints to trails. |
| cmd/entire/cli/gitremote/remote.go | Adds git remote URL parsing + origin owner/repo discovery for API usage. |
You can also share your feedback on Copilot code review. Take the survey.
| slog.Warn("trail migration failed, using API store anyway", | ||
| slog.String("error", migrateErr.Error()), | ||
| ) | ||
| } else if migrated > 0 { | ||
| fmt.Printf("Migrated %d trail(s) to Entire API.\n", migrated) | ||
| } |
| // No branch or empty — nothing to migrate, mark as done | ||
| if markErr := placeMigrationMarker(repo, 0); markErr != nil { | ||
| slog.Warn("failed to place migration marker", slog.String("error", markErr.Error())) | ||
| } |
| // AddCheckpoint links a checkpoint to a trail. | ||
| // NOTE: The backend does not yet have a dedicated endpoint for this. | ||
| // This is a no-op that logs the intent. See "Missing API Endpoints". | ||
| func (s *APIStore) AddCheckpoint(_ ID, _ CheckpointRef) error { | ||
| // TODO: Implement when POST /:org/:repo/trails/:trailId/checkpoints is available. | ||
| // For now, checkpoint-to-trail linking is implicit via branch name in the DB. | ||
| return nil | ||
| } |
| type GitStore struct { | ||
| repo *git.Repository | ||
| } | ||
|
|
| repo, err := strategy.OpenRepository(context.Background()) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to open repository: %w", err) | ||
| } | ||
|
|
||
| // Determine base branch | ||
| if base == "" { | ||
| base = strategy.GetDefaultBranchName(repo) | ||
| if base == "" { | ||
| base = defaultBaseBranch | ||
| } | ||
| } | ||
|
|
||
| _, currentBranch, _ := IsOnDefaultBranch(context.Background()) //nolint:errcheck // best-effort detection | ||
| 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 { | ||
| return err | ||
| } | ||
| } else { | ||
| // Non-interactive: derive missing values from provided flags | ||
| if branch == "" { | ||
| if cmd.Flags().Changed("title") { | ||
| branch = slugifyTitle(title) | ||
| } else { | ||
| branch = currentBranch | ||
| } | ||
| } | ||
| if title == "" { | ||
| title = trail.HumanizeBranchName(branch) | ||
| } | ||
| } | ||
| if branch == "" { | ||
| return errors.New("branch name is required") | ||
| } | ||
|
|
||
| // Create the branch if it doesn't exist | ||
| needsCreation := branchNeedsCreation(repo, branch) | ||
| if needsCreation { | ||
| if err := createBranch(repo, branch); err != nil { | ||
| return fmt.Errorf("failed to create branch %q: %w", branch, err) | ||
| } | ||
| fmt.Fprintf(w, "Created branch %s\n", branch) | ||
| } else if currentBranch != branch { | ||
| fmt.Fprintf(errW, "Note: trail will be created for branch %q (not the current branch)\n", branch) | ||
| } | ||
|
|
||
| // Resolve trail store (API or git) | ||
| ctx := context.Background() | ||
| store, err := resolveTrailStore(ctx) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to open trail store: %w", err) | ||
| } |
| // ResolveStore returns the best available Store for the current context. | ||
| // If the user is authenticated, returns an APIStore (migrating git trails if needed). | ||
| // Otherwise, falls back to a GitStore. | ||
| func ResolveStore(ctx context.Context, repo *git.Repository) (Store, error) { //nolint:ireturn,unparam // intentional interface return; error reserved for future auth failures | ||
| apiStore, err := NewAPIStore(ctx) | ||
| if err != nil { | ||
| if errors.Is(err, ErrNotAuthenticated) { | ||
| // Not logged in — fall back to git store silently | ||
| return NewGitStore(repo), nil |
| branchName := GetCurrentBranchName(repo) | ||
| if branchName != "" && branchName != GetDefaultBranchName(repo) { | ||
| store := trail.NewStore(repo) | ||
| existing, findErr := store.FindByBranch(branchName) | ||
| if findErr == nil && existing != nil { | ||
| appendCheckpointToExistingTrail(store, existing.TrailID, result.CheckpointID, head.Hash(), result.Prompts) | ||
| trailStore, resolveErr := trail.ResolveStore(ctx, repo) | ||
| if resolveErr == nil { | ||
| existing, findErr := trailStore.FindByBranch(branchName) | ||
| if findErr == nil && existing != nil { | ||
| appendCheckpointToExistingTrail(trailStore, existing.TrailID, result.CheckpointID, head.Hash(), result.Prompts) | ||
| } |
Summary
trail.Storeinterface with two implementations:GitStore(existing git branch storage) andAPIStore(new REST API via Entire backend)entire login), all trail commands (trail,trail list,trail create,trail update) use the API. Falls back to git-backed storage when not logged in.entire/trails/v1git branch to the API and places aDEPRECATED_MOVED_TO_ENTIRE_STORAGEmarker file on the branchgitremotepackage for parsing org/repo from git remote URLsmanual_commit_hooks.go) updated to use the store interface so checkpoint linking works with either backendMissing backend API endpoints
GET /:org/:repo/trails?branch=namequery paramList()POST /:org/:repo/trails/:trailId/checkpointsGET /:trailIdresponse or sub-endpointsFiles changed
trail/trail.goStoreinterfacetrail/store.goStore→GitStore, addedNewGitStore()trail/api_store.gotrail/resolve.goResolveStore()picks API vs git;IsAPIBacked()helpertrail/migrate.gogitremote/remote.gotrail_cmd.goresolveTrailStore(); skip git push when API-backedstrategy/manual_commit_hooks.gotrail.Storeinterface instead of concrete*trail.GitStoreTest plan
mise run test)mise run test:integration)mise run test:e2e:canary)entire trail listwithout login (git fallback)entire loginthenentire trail create/list/update(API path)Depends on #692 (device auth login)
Related backend: entirehq/entire.io#1115
🤖 Generated with Claude Code