-
Notifications
You must be signed in to change notification settings - Fork 3.5k
WIP: Better defaults for worktree vs local checkout #3957
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
590103e
Surface-derived workspace defaults, pinned worktree bases, and worktr…
t3dotgg 233e0b4
Address review bot findings
t3dotgg e4061d4
Preserve explicit startFromOrigin on cross-project draft reuse
t3dotgg d31b1c8
Decouple env-mode and origin-pin re-derivation on cross-project draft…
t3dotgg File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,203 @@ | ||
| import { | ||
| GitCommandError, | ||
| WORKTREE_BASE_FRESHNESS_WINDOW, | ||
| WORKTREE_BASE_USABLE_HORIZON, | ||
| type WorktreeBaseProvenance, | ||
| } from "@t3tools/contracts"; | ||
| import * as Clock from "effect/Clock"; | ||
| import * as Context from "effect/Context"; | ||
| import * as DateTime from "effect/DateTime"; | ||
| import * as Deferred from "effect/Deferred"; | ||
| import * as Duration from "effect/Duration"; | ||
| import * as Effect from "effect/Effect"; | ||
| import * as Exit from "effect/Exit"; | ||
| import * as Layer from "effect/Layer"; | ||
|
|
||
| import * as GitWorkflowService from "./GitWorkflowService.ts"; | ||
|
|
||
| export interface WorktreeBasePin { | ||
| /** Commit SHA the new worktree is created from. */ | ||
| readonly commitSha: string; | ||
| /** Remote-tracking ref the SHA was resolved from. */ | ||
| readonly remoteRefName: string; | ||
| readonly provenance: WorktreeBaseProvenance; | ||
| /** Time of the successful fetch backing this pin (null when unknown). */ | ||
| readonly fetchedAt: string | null; | ||
| } | ||
|
|
||
| export interface EnsureFreshRemoteResult { | ||
| readonly fetchedAt: string | null; | ||
| readonly fetchSucceeded: boolean; | ||
| } | ||
|
|
||
| /** | ||
| * Plans the base commit for new worktree threads. | ||
| * | ||
| * Freshness ladder ("always current" without a blocking fetch on every send): | ||
| * 1. A fetch of the repo's primary remote within {@link WORKTREE_BASE_FRESHNESS_WINDOW} | ||
| * counts as fresh — pin the remote-tracking commit immediately. | ||
| * 2. Otherwise fetch now (deduped per repository: concurrent sends and the | ||
| * composer-open prefetch share one in-flight fetch). | ||
| * 3. If that fetch fails but the last successful fetch is within | ||
| * {@link WORKTREE_BASE_USABLE_HORIZON}, proceed on the last-known remote | ||
| * commit with `provenance: "stale"` (fail-visible). | ||
| * 4. With no usable fetch, fail closed — the caller surfaces the error and | ||
| * never silently falls back to a local ref. | ||
| */ | ||
| export class WorktreeBasePlanner extends Context.Service< | ||
| WorktreeBasePlanner, | ||
| { | ||
| /** | ||
| * Fetch the repository's primary remote unless a successful fetch is | ||
| * already within the freshness window. Never fails: fetch failures are | ||
| * reported through `fetchSucceeded` so composer-open prefetches are | ||
| * fire-and-forget. | ||
| */ | ||
| readonly ensureFreshRemote: (input: { | ||
| readonly cwd: string; | ||
| }) => Effect.Effect<EnsureFreshRemoteResult>; | ||
|
|
||
| /** | ||
| * Resolve the pinned base commit for a new worktree from `baseRef`, | ||
| * applying the freshness ladder above. | ||
| */ | ||
| readonly pinRemoteBase: (input: { | ||
| readonly cwd: string; | ||
| readonly baseRef: string; | ||
| }) => Effect.Effect<WorktreeBasePin, GitCommandError>; | ||
| } | ||
| >()("t3/git/WorktreeBasePlanner") {} | ||
|
|
||
| interface RepoFetchState { | ||
| /** Completion time of the last successful fetch (ISO). */ | ||
| lastSuccessAt: string | null; | ||
| /** Shared in-flight fetch so concurrent callers coalesce. */ | ||
| inFlight: Deferred.Deferred<boolean> | null; | ||
| } | ||
|
|
||
| export const make = Effect.gen(function* () { | ||
| const gitWorkflow = yield* GitWorkflowService.GitWorkflowService; | ||
|
|
||
| // Keyed by cwd. Bootstraps and the composer-open prefetch always pass the | ||
| // project root, so a per-cwd key deduplicates the flows that matter. | ||
| const fetchStateByCwd = new Map<string, RepoFetchState>(); | ||
|
|
||
| const getState = (cwd: string): RepoFetchState => { | ||
| const existing = fetchStateByCwd.get(cwd); | ||
| if (existing) { | ||
| return existing; | ||
| } | ||
| const created: RepoFetchState = { lastSuccessAt: null, inFlight: null }; | ||
| fetchStateByCwd.set(cwd, created); | ||
| return created; | ||
| }; | ||
|
|
||
| const isWithin = (isoTime: string | null, window: Duration.Duration): Effect.Effect<boolean> => | ||
| Effect.gen(function* () { | ||
| if (isoTime === null) { | ||
| return false; | ||
| } | ||
| const at = Date.parse(isoTime); | ||
| if (Number.isNaN(at)) { | ||
| return false; | ||
| } | ||
| const now = yield* Clock.currentTimeMillis; | ||
| return now - at <= Duration.toMillis(window); | ||
| }); | ||
|
|
||
| const runDedupedFetch = (cwd: string): Effect.Effect<boolean> => | ||
| Effect.gen(function* () { | ||
| const state = getState(cwd); | ||
| const existingInFlight = state.inFlight; | ||
| if (existingInFlight) { | ||
| return yield* Deferred.await(existingInFlight); | ||
| } | ||
| // Claim the slot synchronously (no suspension between the check above | ||
| // and this assignment) so concurrent callers can't both start a fetch. | ||
| const inFlight = Deferred.makeUnsafe<boolean>(); | ||
| state.inFlight = inFlight; | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| return yield* gitWorkflow.fetchRemote({ cwd, remoteName: "origin" }).pipe( | ||
| Effect.flatMap(() => | ||
| DateTime.now.pipe( | ||
| Effect.map((now) => { | ||
| state.lastSuccessAt = DateTime.formatIso(now); | ||
| return true; | ||
| }), | ||
| ), | ||
| ), | ||
| Effect.catch((error) => | ||
| Effect.logDebug("worktree base fetch failed", { | ||
| cwd, | ||
| detail: error.message, | ||
| }).pipe(Effect.map(() => false)), | ||
| ), | ||
| Effect.onExit((exit) => { | ||
| state.inFlight = null; | ||
| return Deferred.done(inFlight, Exit.isSuccess(exit) ? exit : Exit.succeed(false)).pipe( | ||
| Effect.asVoid, | ||
| ); | ||
| }), | ||
| ); | ||
| }); | ||
|
|
||
| const ensureFreshRemote: WorktreeBasePlanner["Service"]["ensureFreshRemote"] = Effect.fn( | ||
| "WorktreeBasePlanner.ensureFreshRemote", | ||
| )(function* (input) { | ||
| const state = getState(input.cwd); | ||
| if (yield* isWithin(state.lastSuccessAt, WORKTREE_BASE_FRESHNESS_WINDOW)) { | ||
| return { fetchedAt: state.lastSuccessAt, fetchSucceeded: true }; | ||
| } | ||
| const fetchSucceeded = yield* runDedupedFetch(input.cwd); | ||
| return { fetchedAt: state.lastSuccessAt, fetchSucceeded }; | ||
| }); | ||
|
|
||
| const pinRemoteBase: WorktreeBasePlanner["Service"]["pinRemoteBase"] = Effect.fn( | ||
| "WorktreeBasePlanner.pinRemoteBase", | ||
| )(function* (input) { | ||
| const { fetchedAt, fetchSucceeded } = yield* ensureFreshRemote({ cwd: input.cwd }); | ||
|
|
||
| const resolvePin = (provenance: WorktreeBaseProvenance) => | ||
| gitWorkflow | ||
| .resolveRemoteTrackingCommit({ | ||
| cwd: input.cwd, | ||
| refName: input.baseRef, | ||
| fallbackRemoteName: "origin", | ||
| }) | ||
| .pipe( | ||
| Effect.map( | ||
| (resolved): WorktreeBasePin => ({ | ||
| commitSha: resolved.commitSha, | ||
| remoteRefName: resolved.remoteRefName, | ||
| provenance, | ||
| fetchedAt, | ||
| }), | ||
| ), | ||
| ); | ||
|
|
||
| if (fetchSucceeded) { | ||
| return yield* resolvePin("fresh"); | ||
| } | ||
|
|
||
| // Fail-visible: the remote is unreachable, but a recent fetch means the | ||
| // remote-tracking ref is still a sane base. Proceed with stale provenance. | ||
| if (yield* isWithin(fetchedAt, WORKTREE_BASE_USABLE_HORIZON)) { | ||
| return yield* resolvePin("stale"); | ||
| } | ||
|
|
||
| // Fail closed: no usable remote data. The caller must not silently fall | ||
| // back to a local ref — surfacing this error is the contract. | ||
| return yield* new GitCommandError({ | ||
| operation: "WorktreeBasePlanner.pinRemoteBase", | ||
| command: "fetch", | ||
| cwd: input.cwd, | ||
| detail: `Couldn't prepare a fresh worktree from ${input.baseRef}: the remote could not be reached and no recent fetch is available. Nothing has started and your checkout was not modified.`, | ||
| }); | ||
| }); | ||
|
|
||
| return WorktreeBasePlanner.of({ | ||
| ensureFreshRemote, | ||
| pinRemoteBase, | ||
| }); | ||
| }); | ||
|
|
||
| export const layer = Layer.effect(WorktreeBasePlanner, make); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.