@gitkraken/compose-tools 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +8 -5
  3. package/dist/ai/types.d.ts +46 -1
  4. package/dist/git/apply-target.d.ts +2 -1
  5. package/dist/git/apply-target.js +1 -1
  6. package/dist/git/commit-chain.d.ts +17 -6
  7. package/dist/git/commit-chain.js +7 -6
  8. package/dist/git/diff-collector.d.ts +35 -4
  9. package/dist/git/diff-collector.js +4 -4
  10. package/dist/git/index.js +1 -1
  11. package/dist/git/port-dispatch.js +1 -1
  12. package/dist/git/safety.js +1 -1
  13. package/dist/git/types.d.ts +19 -1
  14. package/dist/index.d.ts +3 -3
  15. package/dist/ports/git.d.ts +8 -1
  16. package/dist/tasks/ai-loop.d.ts +10 -1
  17. package/dist/tasks/ai-loop.js +1 -1
  18. package/dist/tasks/analyze/analyze.js +1 -1
  19. package/dist/tasks/analyze/prompt.js +1 -0
  20. package/dist/tasks/analyze/validator.d.ts +1 -1
  21. package/dist/tasks/analyze/validator.js +1 -1
  22. package/dist/tasks/compose-group/compose-group.js +1 -1
  23. package/dist/tasks/compose-group/prompt.js +6 -6
  24. package/dist/tasks/compose-group/types.d.ts +3 -11
  25. package/dist/tasks/group/group.js +1 -1
  26. package/dist/tasks/group/prompt.js +4 -4
  27. package/dist/tasks/group/types.d.ts +6 -1
  28. package/dist/tasks/group/validator.d.ts +10 -2
  29. package/dist/tasks/group/validator.js +5 -5
  30. package/dist/tasks/quick-compose/prompt.js +1 -1
  31. package/dist/tasks/quick-compose/quick-compose.js +1 -1
  32. package/dist/tasks/quick-compose/validator.d.ts +1 -1
  33. package/dist/tasks/quick-compose/validator.js +2 -2
  34. package/dist/tasks/refine/prompt.d.ts +1 -1
  35. package/dist/tasks/refine/prompt.js +4 -4
  36. package/dist/tasks/refine/refine.js +1 -1
  37. package/dist/tasks/refine/types.d.ts +10 -4
  38. package/dist/tasks/refine/validator.d.ts +1 -1
  39. package/dist/tasks/refine/validator.js +1 -1
  40. package/dist/workflows/compose.d.ts +6 -2
  41. package/dist/workflows/compose.js +3 -3
  42. package/dist/workflows/refine.d.ts +2 -2
  43. package/dist/workflows/refine.js +1 -1
  44. package/dist/workflows/types.d.ts +17 -0
  45. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -6,6 +6,37 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/) and this
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.8.1] - 2026-08-05
10
+
11
+ ### Changed
12
+
13
+ - `commit-range` + `includeWorkdir` now **preserves** a workdir layer it was told not to compose, instead of rejecting the input. A layer excluded via `includeStaged`/`includeUnstaged` is captured as a patch immediately before the apply's worktree reset and re-applied to the worktree afterward — so `{ includeStaged: true, includeUnstaged: false }` composes the staged changes together with the range and leaves the unstaged work sitting in the worktree, which is what those flags read as meaning. This reverses the `ComposeWorkflowInputError` raised for that combination: the rejection was right about the danger (an excluded layer is absent from the collected input, therefore absent from `leftoverPatch`, and unprotected by a safety stash — none is taken when `includeWorkdir` is set — so the reset destroyed it) but resolved it by refusing the operation rather than by protecting the layer. Excluded content survives; its position does not — an excluded staged layer comes back as unstaged changes. Untracked files are unaffected, as before: the reset leaves them alone. Layer exclusion stays deliberately distinct from `hunkFilter` and partial apply, which drop specific changes the caller singled out of the composed input; an excluded layer was never part of that input at all.
14
+ - One `includeWorkdir` combination is still rejected, now narrowly: `includeStaged: false` together with `includeUnstaged: true` while staged changes exist. Unstaged changes are diffed against the index and therefore already presuppose the staged content, so once the two touch the same lines there is no coherent way to replay the excluded staged layer over a result composed from the unstaged one. `ComposeWorkflowInputError` names the fix (include the layer, or commit/stash it). Every other combination now composes.
15
+ - Drift in an excluded layer no longer fails the pre-apply safety check. `verifySnapshot` previously reported `Excluded ... changes appeared since the AI analysis` when a change appeared in an excluded layer between collection and apply; the capture now happens immediately before the reset, so whatever the layer holds at that moment is what gets restored and a late change no longer needs to block the apply.
16
+ - Documented that **every `includeWorkdir` layer defaults to `true`**, in both the `ComposeSource` type and the README. `{ includeStaged: true }` reads like an allowlist but composes the entire working directory, unstaged and untracked changes included; keeping a layer out requires naming it with `false`.
17
+
18
+ ## [0.8.0] - 2026-08-03
19
+
20
+ ### Added
21
+
22
+ - Adds `stashNotRestored` on `ComposeResult`, `ApplyComposePlanResult`, and the apply-layer results — reported when the post-apply safety-stash restore was skipped or failed without conflicts (reasons: `stash-ref-not-found`, `untracked-restore-failed`, `pop-failed`). Previously a git-aborted pop and a missing stash ref were indistinguishable from a clean restore (or misreported as a conflict with no files). The stash entry is now left in place in these cases instead of being dropped, so the WIP it holds stays recoverable. Type export: `StashNotRestored`.
23
+ - Adds optional `diffFilter` and `noRenames` fields to `DiffTreeOptions` (mapping to `git diff`'s `--diff-filter` and `--no-renames`), so a consumer-supplied `diffTree` port can honor both. Used internally to compare a stash's untracked payload against the working tree without rename detection folding a missing file into a rename.
24
+
25
+ ### Changed
26
+
27
+ - A hunk index claimed by more than one commit is now repaired deterministically instead of costing a retry. The response validator for `group` / `composeGroup` / `refinePlan` keeps the index in one commit and drops the other claims, then accepts the response. The winner is the locked claimant when one exists, otherwise the first claim in the model's **response** order — not necessarily first in the final chain, since the `order` phase and `restoreLockedOrdering` can both move it later. Dropping a later claim removes a wrong extra assignment rather than guessing intent — the hunk is still assigned somewhere. Previously the whole prompt was re-sent, which on a large diff meant re-transmitting every hunk to correct one repeated number; over four eval runs duplicate hunks accounted for 12 of 29 outright `VALIDATION_EXHAUSTED` failures. Repairs are reported, never silent: via `repairs` on the `onAfterModelCall` info object, on `GroupOutput`, on `RefineOutput`, on `RefinePlanResult.phases[]`, and on the matching `PhaseDetail` in `ComposeResult.phases`. Type exports: `ValidationRepair`, `DuplicateHunksRepair`, `RepairedHunkClaim`, `RepairScope`. "Reported, never silent" describes this repair, not validator corrections generally: refine's locked-content substitution and `restoreLockedOrdering` remain silent, and one of them rewrites a commit message the user will see. Two cases deliberately fall back to a retry instead of being repaired: claims spanning **two branches in branches mode** (branch chains are built in parallel from a common base there, so splitting adjacent hunks across them risks a merge conflict raised as a raw exec error; in commits mode the branches are never built as parallel chains, so the same contest is repaired like any other), and commits whose every hunk was a duplicate claim (repairing those would silently discard a commit the model intended, and a zero-hunk commit throws `Empty patch for commit` at apply time). Responses that are wrong in any other way still fail exactly as before. Repairing genuinely missing hunks is deliberately not covered, and neither is `quickCompose`, whose structurally identical duplicate check still rejects outright.
28
+ - **Commit messages are not reconciled with a repair.** A commit that loses a hunk keeps prose describing it, and the winner gains a hunk its message never mentions. The index math does not guess intent, but the resulting message can be wrong in the user's permanent history.
29
+ - `onAfterModelCall` now fires after the response validator runs rather than immediately after `model.generate` returns, so it can carry that call's `repairs`. `repairs` is populated only when the response was accepted. Its `durationMs` still measures only the model call, and the hook still fires when the validator throws. An exception thrown by the hook is now swallowed: an observability callback must not be able to abort a compose or mask the validator's own error.
30
+ - `ComposeGroupOutput` is now an alias of `GroupOutput` rather than a hand-written structural clone that had already drifted (it was missing `hunksCondensedCount`, and would have missed `repairs`).
31
+
32
+ ### Fixed
33
+
34
+ - Composing with a `hunkFilter` that drops a non-trailing hunk no longer fails validation (or worse, tempts the model into renumbering hunks). The filter contract preserves the surviving hunks' original `index` values, but the prompts and all response validators assumed indices form a dense `0..N-1` range derived from the hunk count &mdash; so a correct response referencing the real indices was rejected, retried until `VALIDATION_EXHAUSTED`, and the retry feedback actively steered the model toward renumbering, which would have validated cleanly and resolved to the wrong hunks. Validation (analyze, group / compose-group, quick-compose, refine) now checks against the actual set of surviving hunk indices, prompts instruct the model to use the `index` values exactly as shown, and retry feedback lists the real indices. The fresh `compose()` apply path also now resolves plan indices against the filtered hunk set (matching `applyComposePlan`), so a plan referencing a filter-dropped index fails loudly instead of committing excluded content. Internal `refine` task input changed from `hunkCount: number` to `hunkIndices: ReadonlySet<number>` (not part of the public API).
35
+ - `applyComposePlan` with a `hunkFilter` no longer loses filter-excluded content on destructive applies. The plan-apply path computed the leftover patch from the already-filtered hunk set, so hunks the filter dropped were never re-materialized as working-tree changes after the apply reset the source &mdash; the fresh `compose()` path already computed leftovers from the pre-filter collection. Both paths now agree: filter-dropped hunks always land in the leftover patch and are reported in `unappliedHunkIndices`.
36
+ - `commit-range` + `includeWorkdir` no longer silently destroys uncommitted changes in an excluded layer. Excluding `includeStaged` or `includeUnstaged` while that layer held changes previously dropped the layer from the composed input and then hard-reset the worktree over it &mdash; no error, no safety stash, success reported. Collection now rejects the combination with `ComposeWorkflowInputError` before any AI spend. A layer that becomes dirty after collection still aborts before the destructive apply &mdash; the two-phase plan/apply flow re-collects and throws `ComposeWorkflowInputError`, and the single-shot flow raises a `SAFETY_CHECK_FAILED` safety-check failure at pre-apply verification. **Breaking**: input that previously "succeeded" (destructively on `rewrite-range`, harmlessly on `branch`/`auto` targets) is now rejected &mdash; include the layer, or commit/stash it before composing. `includeUntracked: false` is unaffected; untracked files survive the rewrite.
37
+ - The apply path no longer deletes the safety stash &mdash; silently destroying its untracked files &mdash; when the post-rewrite working tree looks identical to the stash. The tree-equivalence shortcut compared the stash commit's own tree against the working tree, but a `--include-untracked` stash keeps its untracked payload in a third parent, invisible to that comparison &mdash; so the trees matched precisely when the stashed untracked files were missing from disk, and the stash holding their only copy was dropped. The shortcut is now skipped whenever the stash swept up untracked files (detected from status output and confirmed by checking whether the stash's third-parent tree is non-empty &mdash; `git stash -u` always adds a third parent but leaves its tree empty when nothing untracked was taken, so `status.showUntrackedFiles=no` can't hide a real payload), letting the normal pop restore them. When git instead aborts the pop because the stashed untracked files already exist on disk &mdash; the routine outcome when the compose re-materialized them as tracked content &mdash; the stash is dropped only after verifying every payload file is present in the working tree with identical content; otherwise it is kept and reported via `stashNotRestored`.
38
+ - The `auto`-target apply path with a single partitioned branch plan no longer swallows the safety-stash outcome: it now forwards the caller's `stashLabel` and propagates `stashConflict` / `stashNotRestored` like every other apply path.
39
+
9
40
  ## [0.7.0] - 2026-07-30
10
41
 
11
42
  ### Added
package/README.md CHANGED
@@ -183,7 +183,7 @@ See [Undo](#undo) below.
183
183
  |---|---|
184
184
  | `{ type: 'workdir', stagedOnly?, noUntracked? }` | Collects staged, unstaged, and untracked changes. `stagedOnly: true` limits to the index; `noUntracked: true` skips untracked files. |
185
185
  | `{ type: 'branch', name, mergeTarget? }` | Collects `mergeBase(mergeTarget, name) → name` as one combined diff. `mergeTarget` defaults to `'main'`. |
186
- | `{ type: 'commit-range', branch, from, to, includeWorkdir? }` | Collects a contiguous range on `branch` as a single combined diff. Both `from` and `to` are **inclusive** — they name the first and last commits being rewritten. For a single-commit recompose: `from = sha, to = sha`. Per-hunk authorship is attributed via a line-range-overlap heuristic against per-commit history. Pass `includeWorkdir: { includeStaged?, includeUnstaged?, includeUntracked? }` to combine the range with the user's working changes in one composed input. When set, `to` must equal the current branch tip AND HEAD must be non-detached. |
186
+ | `{ type: 'commit-range', branch, from, to, includeWorkdir? }` | Collects a contiguous range on `branch` as a single combined diff. Both `from` and `to` are **inclusive** — they name the first and last commits being rewritten. For a single-commit recompose: `from = sha, to = sha`. Per-hunk authorship is attributed via a line-range-overlap heuristic against per-commit history. Pass `includeWorkdir: { includeStaged?, includeUnstaged?, includeUntracked? }` to combine the range with the user's working changes in one composed input; **each layer defaults to `true`**, so `{ includeStaged: true }` composes the whole working directory — name a layer with `false` to keep it out. When set, `to` must equal the current branch tip and HEAD must be non-detached. A layer excluded via `includeStaged`/`includeUnstaged` is preserved rather than composed: its content is captured before the apply resets the worktree and re-applied afterward as unstaged changes. Excluding staged while composing unstaged is rejected when staged changes exist — unstaged changes are diffed against the index, so the excluded staged layer has nowhere coherent to be replayed. |
187
187
 
188
188
  ## Targets
189
189
 
@@ -259,8 +259,10 @@ onBeforeModelCall: ({ phase, attempt, maxAttempts, callId }) => {
259
259
  // callId: stable UUID — correlates with onAfterModelCall and with result.feedbackTargetCallId
260
260
  },
261
261
 
262
- onAfterModelCall: ({ phase, attempt, callId, success, durationMs, usage, error, promptVersion }) => {
263
- // Fires on both success and error
262
+ onAfterModelCall: ({ phase, attempt, callId, success, durationMs, usage, error, promptVersion, repairs }) => {
263
+ // Fires on both success and error, after the response validator runs (durationMs still
264
+ // measures only the model call). `repairs` lists deterministic fixes applied to an accepted
265
+ // response — see ValidationRepair. An exception thrown here is swallowed.
264
266
  },
265
267
  ```
266
268
 
@@ -364,7 +366,7 @@ Custom message for the safety stash the apply step creates when the worktree is
364
366
  stashLabel: `myapp-compose-${new Date().toISOString()}`
365
367
  ```
366
368
 
367
- The apply step always stashes pending workdir state via `git stash push --include-untracked` before mutating, and pops it back after. If the post-apply pop hits conflicts, the result includes `stashConflict: { stashLabel, conflictedFiles }` — apply itself succeeded, the named stash is preserved, and the consumer surfaces recovery UX (typical pattern: scan `git stash list` for a stable prefix on next mode-entry to detect abandoned stashes after process kills).
369
+ The apply step always stashes pending workdir state via `git stash push --include-untracked` before mutating, and pops it back after. If the post-apply pop hits conflicts, the result includes `stashConflict: { stashLabel, conflictedFiles }` — apply itself succeeded, the named stash is preserved, and the consumer surfaces recovery UX (typical pattern: scan `git stash list` for a stable prefix on next mode-entry to detect abandoned stashes after process kills). If the pop is skipped or fails without conflicts, the result instead includes `stashNotRestored: { stashLabel, stashSha, reason }` — the stash entry is left in place (when it still exists) so the WIP it holds stays recoverable.
368
370
 
369
371
  ### `maxTokens`
370
372
 
@@ -451,7 +453,7 @@ Every top-level entry point returns an object with these common fields:
451
453
  | `snapshot` | all paths | Safety hash fingerprint of the collection — carry it to apply time |
452
454
  | `source` | all paths | `{ type, hunks, commitMessages?, branchName?, baseBranch? }` — the hunks the AI operated on |
453
455
  | `session` | all paths | Pass to a subsequent call to resume |
454
- | `phases` | all paths | `PhaseDetail[]` with `{ name, durationMs, attempts, usage?, modelId?, promptVersion?, callIds? }` per phase |
456
+ | `phases` | all paths | `PhaseDetail[]` with `{ name, durationMs, attempts, usage?, modelId?, promptVersion?, callIds?, repairs? }` per phase |
455
457
  | `usage` | all paths | Aggregated `{ inputTokens, outputTokens, ... }` across all AI calls |
456
458
  | `diffStats` | sugar / plan | `{ fileCount, hunkCount, addedLines, removedLines }` |
457
459
  | `feedbackTargetCallId` | when AI ran | callId of the last validated model response — stamp this on thumbs up/down feedback events |
@@ -462,6 +464,7 @@ Every top-level entry point returns an object with these common fields:
462
464
  | `appliedCommitIds` | apply paths | Plan commit IDs actually written. Same as `plan.allOrderedCommits.map(c => c.id)` for full apply; a subset when `applyCommitIds` is set. |
463
465
  | `unappliedHunkIndices` | apply paths | Hunk indices from the plan not covered by `appliedCommitIds`. Empty for full apply. |
464
466
  | `stashConflict` | apply paths | `{ stashLabel, conflictedFiles }` set when the post-apply stash pop hit conflicts. Apply itself succeeded; the named stash is preserved for the consumer's recovery UX. |
467
+ | `stashNotRestored` | apply paths | `{ stashLabel, stashSha, reason }` set when the post-apply stash pop was skipped or failed without conflicts (`stash-ref-not-found`, `untracked-restore-failed`, or `pop-failed`). Apply itself succeeded; the stash entry is kept (when it still exists) so the WIP it holds stays recoverable. |
465
468
 
466
469
  ## Undo
467
470
 
@@ -1,4 +1,44 @@
1
1
  //#region src/ai/types.d.ts
2
+ /** Which commits contested a hunk index, and which one kept it. */
3
+ interface RepairedHunkClaim {
4
+ hunkIndex: number;
5
+ /** Commit that kept the hunk. `branchId/commitId` in branches mode, bare commit id in commits mode. */
6
+ keptIn: string;
7
+ /** Commits the claim was dropped from, in the order they appeared in the response. */
8
+ droppedFrom: string[];
9
+ /** Where THIS claim's contest lived; sibling claims in one repair can differ. */
10
+ scope: RepairScope;
11
+ }
12
+ /**
13
+ * Where a contested claim came from. Nothing emits `across-branches` today: in branches mode those
14
+ * contests are rejected rather than repaired (no claim to put a scope on), and in commits mode a
15
+ * multi-branch response's contests are `across-commits`. The member exists so a future repair
16
+ * covering the branches-mode case does not have to widen the union.
17
+ */
18
+ type RepairScope = 'within-commit' | 'across-commits' | 'across-branches';
19
+ /**
20
+ * A hunk index claimed more than once was kept in a single commit and dropped from the others.
21
+ * The winner is the locked claimant when one exists, otherwise the first claim in the model's
22
+ * response order — which is not necessarily first in the final chain, since the `order` phase and
23
+ * `restoreLockedOrdering` can both place it later.
24
+ */
25
+ interface DuplicateHunksRepair {
26
+ kind: 'duplicate-hunks';
27
+ /** The hunk indices whose extra claims were dropped, ascending. */
28
+ hunkIndices: number[];
29
+ claims: RepairedHunkClaim[];
30
+ /** Unstable log string. Read `claims` for anything programmatic; this shape is free to change. */
31
+ detail: string;
32
+ }
33
+ /**
34
+ * A deterministic fix a response validator applied to an AI response that would otherwise have
35
+ * been rejected, sparing a retry. A repaired response is treated as valid — repairs are reported
36
+ * for observability, not as a failure signal.
37
+ *
38
+ * This covers duplicate-hunk repair only. It is not an exhaustive record of every correction a
39
+ * validator makes: refine's locked-content substitution and `restoreLockedOrdering` stay silent.
40
+ */
41
+ type ValidationRepair = DuplicateHunksRepair;
2
42
  interface TokenUsage {
3
43
  inputTokens: number;
4
44
  outputTokens: number;
@@ -20,6 +60,11 @@ interface PhaseDetail {
20
60
  promptVersion?: string;
21
61
  /** Stable call IDs for each model.generate invocation in this phase (one per attempt). */
22
62
  callIds?: string[];
63
+ /**
64
+ * Deterministic repairs applied to this phase's accepted response instead of spending a retry.
65
+ * Undefined when the response needed no repair.
66
+ */
67
+ repairs?: ValidationRepair[];
23
68
  }
24
69
  interface ProgressEvent {
25
70
  type: 'attempt_start' | 'validation_failed';
@@ -28,4 +73,4 @@ interface ProgressEvent {
28
73
  errors?: string[];
29
74
  }
30
75
  //#endregion
31
- export { PhaseDetail, ProgressEvent, TokenUsage, addUsage, emptyUsage };
76
+ export { DuplicateHunksRepair, PhaseDetail, ProgressEvent, RepairScope, RepairedHunkClaim, TokenUsage, ValidationRepair, addUsage, emptyUsage };
@@ -2,7 +2,7 @@ import { ComposeGitPort, SigningConfig } from "../ports/git.js";
2
2
  import { BranchGroup, BranchPartition, CommitSuggestion, ComposeHunk } from "../tasks/types.js";
3
3
  import { PartitionResult } from "../tasks/partition/types.js";
4
4
  import { ComposeSource, ComposeTarget } from "../workflows/types.js";
5
- import { ApplyMutations, SafetySnapshot, StashConflict } from "./types.js";
5
+ import { ApplyMutations, SafetySnapshot, StashConflict, StashNotRestored } from "./types.js";
6
6
  import { CollectionResult } from "./diff-collector.js";
7
7
 
8
8
  //#region src/git/apply-target.d.ts
@@ -40,6 +40,7 @@ interface ApplyToTargetResult {
40
40
  /** Git mutations performed during apply, for undo manifest construction. */
41
41
  mutations: ApplyMutations;
42
42
  stashConflict?: StashConflict;
43
+ stashNotRestored?: StashNotRestored;
43
44
  }
44
45
  declare function applyToTarget(params: ApplyToTargetParams): Promise<ApplyToTargetResult>;
45
46
  //#endregion
@@ -1 +1 @@
1
- import{GitError as e}from"../errors/git.js";import{applyPatchToWorkdir as t,checkout as n,cleanWorkdir as r,currentBranch as i,deleteBranch as a,discardWorkdirChanges as o,reset as s,revParse as c,revParseVerify as l,updateRef as u}from"./port-dispatch.js";import{applyBranchStructure as d}from"./apply-branches.js";import{reparentDescendants as f}from"./reparent.js";import{applyCommitChain as p,applyRangeRewrite as m,buildCommitChain as h,moveRefsAtSha as g}from"./commit-chain.js";import{verifyMultiBranchDiff as _,verifyResultingDiff as v}from"./safety.js";async function y(t){let{git:n,source:r,target:i,branchPlans:a,allOrderedCommits:o,hunks:s,snapshot:l,collection:u,signing:f,authorAttribution:g,stashLabel:_,leftoverPatch:y}=t;if(i.type===`rewrite-range`){if(!u.branchName||!u.branchSha)throw new e(`OPERATION_FAILED`,`rewrite-range target requires a source that records branchName and branchSha (branch or commit-range).`);let t;try{t=await c(n,`${i.rangeFrom}^`)}catch{throw new e(`OPERATION_FAILED`,`rewrite-range target's rangeFrom (${i.rangeFrom.slice(0,8)}) has no parent (root commit) — rewriting the root commit is not currently supported.`)}let{headSha:a,shaMap:d}=await h({git:n,baseSha:t,commits:o,hunks:s,signing:f,authorAttribution:g}),p=await v(n,t,a,l);if(!p.valid)throw new e(`SAFETY_CHECK_FAILED`,`Diff verification failed: ${p.error}`);let b=r.type===`commit-range`&&r.includeWorkdir!=null,x=await m({git:n,branch:i.branch,rangeFrom:i.rangeFrom,rangeTo:i.rangeTo,newHeadSha:a,originalBranchSha:u.branchSha,stashLabel:_,leftoverPatch:y,includeWorkdirActive:b});return{commitShas:Object.fromEntries(d),mutations:E(x.refMovements),stashConflict:x.stashConflict}}if(u.branchName&&!u.mergeBaseSha)throw new e(`OPERATION_FAILED`,`Branch source is missing mergeBaseSha — cannot determine base commit`);let S=u.mergeBaseSha??await c(n,`HEAD`),C=O(r,i);if(i.type===`auto`&&a.length>1)return b(n,S,a,s,l,u,C,f,g);if(i.type===`auto`&&a.length===1&&a[0].partitioning)return x(n,S,a[0],s,l,u,C,f,g);let{headSha:T,shaMap:A}=await h({git:n,baseSha:S,commits:o,hunks:s,signing:f,authorAttribution:g}),j=Object.fromEntries(A),M=await v(n,S,T,l);if(!M.valid)throw new e(`SAFETY_CHECK_FAILED`,`Diff verification failed: ${M.error}`);if(i.type===`branch`)return k({git:n,source:r,target:i,newHeadSha:T,collection:u,stashLabel:_,leftoverPatch:y,commitShas:j});if(i.type===`auto`){let e=a[0]?.branchGroup.name||u.branchName;if(e){let t=!C&&u.branchName&&u.branchSha?{originalBranchName:u.branchName,originalBranchSha:u.branchSha}:{},r={branchName:e,title:a[0]?.branchGroup.title??``,description:a[0]?.branchGroup.description??``,commitIds:o.map(e=>e.id)};await d({git:n,branches:[{partitions:[r],tipSha:T}],...t});let i=D([r],j,t.originalBranchName,t.originalBranchSha);return{partitions:[r],commitShas:j,mutations:i}}let t=await p({git:n,newHeadSha:T,stashLabel:_});return{commitShas:j,mutations:w(),stashConflict:t.stashConflict}}let N=await p({git:n,newHeadSha:T,stashLabel:_,leftoverPatch:y});return{commitShas:j,mutations:w(),stashConflict:N.stashConflict}}async function b(t,n,i,a,s,c,l,u,f){let p=[],m=[],g=[],v=[],y={};for(let e of i){let r=S(e),{headSha:i,shaMap:o}=await h({git:t,baseSha:n,commits:r,hunks:a,signing:u,authorAttribution:f});g.push(i);for(let[e,t]of o)y[e]=t;if(e.partitioning&&e.partitioning.partitions.length>1){let t=C(e.partitioning.partitions,o);p.push(...t),m.push(t),v.push({partitions:t,tipSha:i})}else{let t={branchName:e.branchGroup.name,title:e.branchGroup.title,description:e.branchGroup.description,commitIds:r.map(e=>o.get(e.id)??e.id)};p.push(t),m.push([t]),v.push({partitions:[t],tipSha:i})}}let b=await _(t,n,g,s);if(!b.valid)throw new e(`SAFETY_CHECK_FAILED`,`Diff verification failed: ${b.error}`);let x=!l&&c.branchName&&c.branchSha?{originalBranchName:c.branchName,originalBranchSha:c.branchSha}:{};return await d({git:t,branches:v,...x}),!c.branchName&&!l&&(await o(t),c.untrackedIncluded&&await r(t)),{partitions:p,partitionGroups:m,commitShas:y,mutations:D(p,y,x.originalBranchName,x.originalBranchSha)}}async function x(t,n,i,a,s,c,l,u,f){let{headSha:m,shaMap:g}=await h({git:t,baseSha:n,commits:S(i),hunks:a,signing:u,authorAttribution:f}),_=await v(t,n,m,s);if(!_.valid)throw new e(`SAFETY_CHECK_FAILED`,`Diff verification failed: ${_.error}`);let y=C(i.partitioning.partitions,g),b=Object.fromEntries(g);if(y.length>1){let e=!l&&c.branchName&&c.branchSha?{originalBranchName:c.branchName,originalBranchSha:c.branchSha}:{};return await d({git:t,branches:[{partitions:y,tipSha:m}],...e}),!c.branchName&&!l&&(await o(t),c.untrackedIncluded&&await r(t,{force:!0,directories:!0})),{partitions:y,commitShas:b,mutations:D(y,b,e.originalBranchName,e.originalBranchSha)}}return await p({git:t,newHeadSha:m}),{partitions:y,commitShas:b,mutations:w()}}function S(t){return t.orderedCommitIds.map(n=>{let r=t.branchGroup.commits.find(e=>e.id===n);if(!r)throw new e(`OPERATION_FAILED`,`Commit ${n} not found in branch '${t.branchGroup.id}'`);return r})}function C(t,n){return t.map(t=>({...t,commitIds:t.commitIds.map(t=>{let r=n.get(t);if(!r)throw new e(`OPERATION_FAILED`,`No SHA found for commit ID '${t}'`);return r})}))}function w(){return{createdRefs:[],deletedRefs:[],movedRefs:[],composedBranchCount:0}}function T(t){if(t.startsWith(`refs/heads/`))return{kind:`branch`,name:t.slice(11)};if(t.startsWith(`refs/tags/`))return{kind:`tag`,name:t.slice(10)};throw new e(`OPERATION_FAILED`,`Unexpected refname '${t}' in apply mutations — only refs/heads/* and refs/tags/* are tracked.`)}function E(e){let t=[];for(let n of e){let{kind:e,name:r}=T(n.refname);t.push({kind:e,name:r,previousSha:n.previousSha,newSha:n.newSha})}return{createdRefs:[],deletedRefs:[],movedRefs:t,composedBranchCount:1}}function D(e,t,n,r){let i=e.map(e=>{let n=e.commitIds[e.commitIds.length-1];return{kind:`branch`,name:e.branchName,sha:t[n]??n}}),a=[];return n&&r&&a.push({kind:`branch`,name:n,sha:r}),{createdRefs:i,deletedRefs:a,movedRefs:[],composedBranchCount:i.length}}function O(e,t){return t.type!==`branch`&&t.type!==`auto`||e.type===`commit-range`?!0:t.preserveOriginal===!0}async function k(r){let{git:o,source:d,target:p,newHeadSha:m,collection:h,leftoverPatch:_,commitShas:v}=r,y=O(d,p),b=d.type===`branch`&&!y&&h.branchName!=null&&h.branchSha!=null;if(b&&p.name===h.branchName)throw new e(`OPERATION_FAILED`,`target 'branch' with name '${p.name}' conflicts with the source branch (preserveOriginal=false). Use target 'rewrite-range' to rewrite the branch in place under its existing name.`);let x=await l(o,`refs/heads/${p.name}`),S=x?{createdRefs:[],deletedRefs:[],movedRefs:[{kind:`branch`,name:p.name,previousSha:x,newSha:m}],composedBranchCount:1}:{createdRefs:[{kind:`branch`,name:p.name,sha:m}],deletedRefs:[],movedRefs:[],composedBranchCount:1},C=await i(o),w=await c(o,`HEAD`);if(await u(o,`refs/heads/${p.name}`,m),b){let e=h.branchSha,r=h.branchName,i=await f({git:o,oldToSha:e,newTopSha:m,sourceBranchName:void 0});for(let e of i.reparentedRefs){let{kind:t,name:n}=T(e.refname);S.movedRefs.push({kind:t,name:n,previousSha:e.oldSha,newSha:e.newSha})}let c=await g(o,e,m,`refs/heads/${r}`);for(let e of c){let{kind:t,name:n}=T(e.refname);S.movedRefs.push({kind:t,name:n,previousSha:e.previousSha,newSha:e.newSha})}C===r&&(await n(o,p.name),await s(o,m,`hard`),_&&await t(o,_)),await a(o,r,{force:!0}),S.deletedRefs.push({kind:`branch`,name:r,sha:e})}else d.type===`workdir`&&!y&&(await s(o,w,`hard`),_&&await t(o,_));return{commitShas:v,mutations:S}}export{y as applyToTarget};
1
+ import{GitError as e}from"../errors/git.js";import{applyPatchToWorkdir as t,checkout as n,cleanWorkdir as r,currentBranch as i,deleteBranch as a,discardWorkdirChanges as o,reset as s,revParse as c,revParseVerify as l,updateRef as u}from"./port-dispatch.js";import{applyBranchStructure as d}from"./apply-branches.js";import{resolveWorkdirLayers as f}from"./diff-collector.js";import{reparentDescendants as p}from"./reparent.js";import{applyCommitChain as m,applyRangeRewrite as h,buildCommitChain as g,moveRefsAtSha as _}from"./commit-chain.js";import{verifyMultiBranchDiff as v,verifyResultingDiff as y}from"./safety.js";async function b(t){let{git:n,source:r,target:i,branchPlans:a,allOrderedCommits:o,hunks:s,snapshot:l,collection:u,signing:p,authorAttribution:_,stashLabel:v,leftoverPatch:b}=t,C=r.type===`commit-range`&&r.includeWorkdir!=null?f(r.includeWorkdir):void 0;if(i.type===`rewrite-range`){if(!u.branchName||!u.branchSha)throw new e(`OPERATION_FAILED`,`rewrite-range target requires a source that records branchName and branchSha (branch or commit-range).`);let t;try{t=await c(n,`${i.rangeFrom}^`)}catch{throw new e(`OPERATION_FAILED`,`rewrite-range target's rangeFrom (${i.rangeFrom.slice(0,8)}) has no parent (root commit) — rewriting the root commit is not currently supported.`)}let{headSha:r,shaMap:a}=await g({git:n,baseSha:t,commits:o,hunks:s,signing:p,authorAttribution:_}),d=await y(n,t,r,l);if(!d.valid)throw new e(`SAFETY_CHECK_FAILED`,`Diff verification failed: ${d.error}`);let f=await h({git:n,branch:i.branch,rangeFrom:i.rangeFrom,rangeTo:i.rangeTo,newHeadSha:r,originalBranchSha:u.branchSha,stashLabel:v,leftoverPatch:b,includeWorkdir:C});return{commitShas:Object.fromEntries(a),mutations:D(f.refMovements),stashConflict:f.stashConflict,stashNotRestored:f.stashNotRestored}}if(u.branchName&&!u.mergeBaseSha)throw new e(`OPERATION_FAILED`,`Branch source is missing mergeBaseSha — cannot determine base commit`);let w=u.mergeBaseSha??await c(n,`HEAD`),E=k(r,i);if(i.type===`auto`&&a.length>1)return x(n,w,a,s,l,u,E,p,_);if(i.type===`auto`&&a.length===1&&a[0].partitioning)return S(n,w,a[0],s,l,u,E,p,_,v,C);let{headSha:j,shaMap:M}=await g({git:n,baseSha:w,commits:o,hunks:s,signing:p,authorAttribution:_}),N=Object.fromEntries(M),P=await y(n,w,j,l);if(!P.valid)throw new e(`SAFETY_CHECK_FAILED`,`Diff verification failed: ${P.error}`);if(i.type===`branch`)return A({git:n,source:r,target:i,newHeadSha:j,collection:u,stashLabel:v,leftoverPatch:b,commitShas:N});if(i.type===`auto`){let e=a[0]?.branchGroup.name||u.branchName;if(e){let t=!E&&u.branchName&&u.branchSha?{originalBranchName:u.branchName,originalBranchSha:u.branchSha}:{},r={branchName:e,title:a[0]?.branchGroup.title??``,description:a[0]?.branchGroup.description??``,commitIds:o.map(e=>e.id)};await d({git:n,branches:[{partitions:[r],tipSha:j}],...t});let i=O([r],N,t.originalBranchName,t.originalBranchSha);return{partitions:[r],commitShas:N,mutations:i}}let t=await m({git:n,newHeadSha:j,stashLabel:v,includeWorkdir:C});return{commitShas:N,mutations:T(),stashConflict:t.stashConflict,stashNotRestored:t.stashNotRestored}}let F=await m({git:n,newHeadSha:j,stashLabel:v,leftoverPatch:b,includeWorkdir:C});return{commitShas:N,mutations:T(),stashConflict:F.stashConflict,stashNotRestored:F.stashNotRestored}}async function x(t,n,i,a,s,c,l,u,f){let p=[],m=[],h=[],_=[],y={};for(let e of i){let r=C(e),{headSha:i,shaMap:o}=await g({git:t,baseSha:n,commits:r,hunks:a,signing:u,authorAttribution:f});h.push(i);for(let[e,t]of o)y[e]=t;if(e.partitioning&&e.partitioning.partitions.length>1){let t=w(e.partitioning.partitions,o);p.push(...t),m.push(t),_.push({partitions:t,tipSha:i})}else{let t={branchName:e.branchGroup.name,title:e.branchGroup.title,description:e.branchGroup.description,commitIds:r.map(e=>o.get(e.id)??e.id)};p.push(t),m.push([t]),_.push({partitions:[t],tipSha:i})}}let b=await v(t,n,h,s);if(!b.valid)throw new e(`SAFETY_CHECK_FAILED`,`Diff verification failed: ${b.error}`);let x=!l&&c.branchName&&c.branchSha?{originalBranchName:c.branchName,originalBranchSha:c.branchSha}:{};return await d({git:t,branches:_,...x}),!c.branchName&&!l&&(await o(t),c.untrackedIncluded&&await r(t)),{partitions:p,partitionGroups:m,commitShas:y,mutations:O(p,y,x.originalBranchName,x.originalBranchSha)}}async function S(t,n,i,a,s,c,l,u,f,p,h){let{headSha:_,shaMap:v}=await g({git:t,baseSha:n,commits:C(i),hunks:a,signing:u,authorAttribution:f}),b=await y(t,n,_,s);if(!b.valid)throw new e(`SAFETY_CHECK_FAILED`,`Diff verification failed: ${b.error}`);let x=w(i.partitioning.partitions,v),S=Object.fromEntries(v);if(x.length>1){let e=!l&&c.branchName&&c.branchSha?{originalBranchName:c.branchName,originalBranchSha:c.branchSha}:{};return await d({git:t,branches:[{partitions:x,tipSha:_}],...e}),!c.branchName&&!l&&(await o(t),c.untrackedIncluded&&await r(t,{force:!0,directories:!0})),{partitions:x,commitShas:S,mutations:O(x,S,e.originalBranchName,e.originalBranchSha)}}let E=await m({git:t,newHeadSha:_,stashLabel:p,includeWorkdir:h});return{partitions:x,commitShas:S,mutations:T(),stashConflict:E.stashConflict,stashNotRestored:E.stashNotRestored}}function C(t){return t.orderedCommitIds.map(n=>{let r=t.branchGroup.commits.find(e=>e.id===n);if(!r)throw new e(`OPERATION_FAILED`,`Commit ${n} not found in branch '${t.branchGroup.id}'`);return r})}function w(t,n){return t.map(t=>({...t,commitIds:t.commitIds.map(t=>{let r=n.get(t);if(!r)throw new e(`OPERATION_FAILED`,`No SHA found for commit ID '${t}'`);return r})}))}function T(){return{createdRefs:[],deletedRefs:[],movedRefs:[],composedBranchCount:0}}function E(t){if(t.startsWith(`refs/heads/`))return{kind:`branch`,name:t.slice(11)};if(t.startsWith(`refs/tags/`))return{kind:`tag`,name:t.slice(10)};throw new e(`OPERATION_FAILED`,`Unexpected refname '${t}' in apply mutations — only refs/heads/* and refs/tags/* are tracked.`)}function D(e){let t=[];for(let n of e){let{kind:e,name:r}=E(n.refname);t.push({kind:e,name:r,previousSha:n.previousSha,newSha:n.newSha})}return{createdRefs:[],deletedRefs:[],movedRefs:t,composedBranchCount:1}}function O(e,t,n,r){let i=e.map(e=>{let n=e.commitIds[e.commitIds.length-1];return{kind:`branch`,name:e.branchName,sha:t[n]??n}}),a=[];return n&&r&&a.push({kind:`branch`,name:n,sha:r}),{createdRefs:i,deletedRefs:a,movedRefs:[],composedBranchCount:i.length}}function k(e,t){return t.type!==`branch`&&t.type!==`auto`||e.type===`commit-range`?!0:t.preserveOriginal===!0}async function A(r){let{git:o,source:d,target:f,newHeadSha:m,collection:h,leftoverPatch:g,commitShas:v}=r,y=k(d,f),b=d.type===`branch`&&!y&&h.branchName!=null&&h.branchSha!=null;if(b&&f.name===h.branchName)throw new e(`OPERATION_FAILED`,`target 'branch' with name '${f.name}' conflicts with the source branch (preserveOriginal=false). Use target 'rewrite-range' to rewrite the branch in place under its existing name.`);let x=await l(o,`refs/heads/${f.name}`),S=x?{createdRefs:[],deletedRefs:[],movedRefs:[{kind:`branch`,name:f.name,previousSha:x,newSha:m}],composedBranchCount:1}:{createdRefs:[{kind:`branch`,name:f.name,sha:m}],deletedRefs:[],movedRefs:[],composedBranchCount:1},C=await i(o),w=await c(o,`HEAD`);if(await u(o,`refs/heads/${f.name}`,m),b){let e=h.branchSha,r=h.branchName,i=await p({git:o,oldToSha:e,newTopSha:m,sourceBranchName:void 0});for(let e of i.reparentedRefs){let{kind:t,name:n}=E(e.refname);S.movedRefs.push({kind:t,name:n,previousSha:e.oldSha,newSha:e.newSha})}let c=await _(o,e,m,`refs/heads/${r}`);for(let e of c){let{kind:t,name:n}=E(e.refname);S.movedRefs.push({kind:t,name:n,previousSha:e.previousSha,newSha:e.newSha})}C===r&&(await n(o,f.name),await s(o,m,`hard`),g&&await t(o,g)),await a(o,r,{force:!0}),S.deletedRefs.push({kind:`branch`,name:r,sha:e})}else d.type===`workdir`&&!y&&(await s(o,w,`hard`),g&&await t(o,g));return{commitShas:v,mutations:S}}export{b as applyToTarget};
@@ -1,6 +1,6 @@
1
1
  import { ComposeGitPort, SigningConfig } from "../ports/git.js";
2
2
  import { CommitSuggestion, ComposeHunk } from "../tasks/types.js";
3
- import { StashConflict } from "./types.js";
3
+ import { StashConflict, StashNotRestored } from "./types.js";
4
4
 
5
5
  //#region src/git/commit-chain.d.ts
6
6
  type AuthorAttributionStrategy = 'plurality' | 'first-hunk' | ((hunks: ComposeHunk[]) => {
@@ -21,6 +21,12 @@ interface BuildCommitChainResult {
21
21
  shaMap: Map<string, string>;
22
22
  }
23
23
  declare function buildCommitChain(params: BuildCommitChainParams): Promise<BuildCommitChainResult>;
24
+ /** Resolved `ComposeSource.includeWorkdir` layer flags — every layer decided, no defaulting left. */
25
+ type WorkdirLayers = {
26
+ includeStaged: boolean;
27
+ includeUnstaged: boolean;
28
+ includeUntracked: boolean;
29
+ };
24
30
  interface ApplyCommitChainParams {
25
31
  git: ComposeGitPort;
26
32
  newHeadSha: string;
@@ -32,9 +38,12 @@ interface ApplyCommitChainParams {
32
38
  * unstaged WIP, so the user's unselected content isn't lost when a destructive apply rewrites
33
39
  * the source. */
34
40
  leftoverPatch?: string;
41
+ /** See {@link ApplyRangeRewriteParams.includeWorkdir} — same contract on this apply path. */
42
+ includeWorkdir?: WorkdirLayers;
35
43
  }
36
44
  interface ApplyChainResult {
37
45
  stashConflict?: StashConflict;
46
+ stashNotRestored?: StashNotRestored;
38
47
  }
39
48
  declare function applyCommitChain(params: ApplyCommitChainParams): Promise<ApplyChainResult>;
40
49
  interface ApplyRangeRewriteParams {
@@ -51,10 +60,11 @@ interface ApplyRangeRewriteParams {
51
60
  originalBranchSha: string;
52
61
  stashLabel?: string;
53
62
  leftoverPatch?: string;
54
- /** When `true`, the source's `includeWorkdir` was active, so WIP was already mixed into the
55
- * input and no safety stash is taken. Leftover hunks are materialized via {@link leftoverPatch}
56
- * instead. When `false` (or undefined), a safety stash protects unrelated dirty WIP. */
57
- includeWorkdirActive?: boolean;
63
+ /** Resolved `includeWorkdir` layer flags when the source folded WIP into the input. When set, no
64
+ * safety stash is taken — the WIP is part of the source, and leftover hunks are materialized via
65
+ * {@link leftoverPatch} — and any layer the caller excluded is captured before the worktree reset
66
+ * and re-applied after it. When undefined, a safety stash protects unrelated dirty WIP. */
67
+ includeWorkdir?: WorkdirLayers;
58
68
  }
59
69
  interface ApplyRangeRewriteResult {
60
70
  /** Final source-branch tip SHA after descendant re-parenting and any ref-at-tip moves. */
@@ -72,6 +82,7 @@ interface ApplyRangeRewriteResult {
72
82
  newSha: string;
73
83
  }[];
74
84
  stashConflict?: StashConflict;
85
+ stashNotRestored?: StashNotRestored;
75
86
  }
76
87
  /**
77
88
  * Rewrite a contiguous range of commits on a branch in-place. Given a new commit chain
@@ -92,4 +103,4 @@ declare function moveRefsAtSha(git: ComposeGitPort, oldSha: string, newSha: stri
92
103
  newSha: string;
93
104
  }[]>;
94
105
  //#endregion
95
- export { ApplyChainResult, ApplyCommitChainParams, ApplyRangeRewriteParams, ApplyRangeRewriteResult, AuthorAttributionStrategy, BuildCommitChainParams, BuildCommitChainResult, applyCommitChain, applyRangeRewrite, buildCommitChain, moveRefsAtSha };
106
+ export { ApplyChainResult, ApplyCommitChainParams, ApplyRangeRewriteParams, ApplyRangeRewriteResult, AuthorAttributionStrategy, BuildCommitChainParams, BuildCommitChainResult, WorkdirLayers, applyCommitChain, applyRangeRewrite, buildCommitChain, moveRefsAtSha };
@@ -1,7 +1,8 @@
1
- import{GitError as e}from"../errors/git.js";import{applyPatchToIndex as t,applyPatchToWorkdir as n,checkout as r,commitTree as i,currentBranch as a,localeNeutralEnv as o,readTree as s,reset as c,revParse as l,stageAll as u,stashDrop as d,stashPop as f,stashPush as p,statusPorcelain as m,updateRef as h,writeTree as g}from"./port-dispatch.js";import{listRefsWithPeeledSha as _}from"./utils/refs.js";import{reparentDescendants as v}from"./reparent.js";import{buildPatchForCommit as y}from"./utils/patch-builder.js";import{randomUUID as b}from"node:crypto";import*as x from"node:fs";import*as S from"node:os";import*as C from"node:path";async function w(n){let{git:r,baseSha:a,commits:o,hunks:c,signing:l,authorAttribution:u=`plurality`}=n,d=C.join(S.tmpdir(),`compose-idx-${b()}`),f=a,p=new Map,m=new Set;try{for(let n of o){let a=n.hunkIndices.map(t=>{let n=c.find(e=>e.index===t);if(!n)throw new e(`OPERATION_FAILED`,`Hunk index ${t} not found`);return n}),o=y(a.map(e=>m.has(e.diffHeader)?{...e,diffHeader:B(e.diffHeader,e.fileName)}:e));if(!o.trim())throw new e(`OPERATION_FAILED`,`Empty patch for commit: ${n.message}`);let h={GIT_INDEX_FILE:d};await s(r,f,{indexEnv:h});try{await t(r,o,{indexEnv:h})}catch(t){let r=t instanceof Error?t.message:String(t);throw new e(`OPERATION_FAILED`,`git apply failed for commit '${n.message.split(`
2
- `)[0]}': ${r}`,{cause:t})}for(let e of a)e.originalFileName&&e.diffHeader.includes(`rename from`)&&m.add(e.diffHeader);let _=await g(r,{indexEnv:h}),v=I(a,u),b=R(n.message,v.coAuthors),x=await i(r,_,[f],b,{signing:l,author:v.author});p.set(n.id,x),f=x}return{headSha:f,shaMap:p}}finally{z(d)}}async function T(e){let{git:t,newHeadSha:i,branchName:a,stashLabel:o,leftoverPatch:s}=e,l=o??O,u=await k(t,l);try{a?await r(t,a,{createBranch:!0,startPoint:i}):await c(t,i,`hard`),s&&await n(t,s)}catch(e){throw u!=null&&await j(t,u,l),e}if(u!=null){let e=await j(t,u,l);if(e.conflicted)return{stashConflict:{stashLabel:l,conflictedFiles:e.conflictedFiles}}}return{}}async function E(e){let{git:t,branch:i,rangeTo:o,newHeadSha:s,originalBranchSha:l,stashLabel:u,leftoverPatch:d,includeWorkdirActive:f}=e,p=await a(t)===i,m=u??O,g=p&&!f?await k(t,m):null,_,y=s,b=[];try{let e=await v({git:t,oldToSha:o,newTopSha:s,sourceBranchName:i});for(let t of e.reparentedRefs)b.push({refname:t.refname,previousSha:t.oldSha,newSha:t.newSha});e.sourceBranchNewSha==null?(await h(t,`refs/heads/${i}`,s),b.push({refname:`refs/heads/${i}`,previousSha:l,newSha:s}),y=s):y=e.sourceBranchNewSha;let a=await D(t,o,s,`refs/heads/${i}`);b.push(...a),p&&(await r(t,i),await c(t,y,`hard`),g==null&&d&&await n(t,d))}catch(e){throw g!=null&&await j(t,g,m),e}if(g!=null){let e=await j(t,g,m);e.conflicted&&(_={stashLabel:m,conflictedFiles:e.conflictedFiles})}return{finalTipSha:y,refMovements:b,stashConflict:_}}async function D(e,t,n,r){let i=[];for(let{refname:a,sha:o}of await _(e,{pointsAt:t}))a!==r&&(!a.startsWith(`refs/heads/`)&&!a.startsWith(`refs/tags/`)||o===t&&(await h(e,a,n),i.push({refname:a,previousSha:t,newSha:n})));return i}const O=`compose backup`;async function k(e,t){if(!(await m(e)).trim())return null;await p(e,{message:t,includeUntracked:!0});try{let t=(await l(e,`stash@{0}`)).trim();return t.length>0?t:null}catch{return null}}async function A(e,t){if(!e.exec)return null;let n;try{n=await e.exec([`stash`,`list`,`--format=%H %gd`],{env:o})}catch{return null}for(let e of n.split(`
3
- `)){let n=e.indexOf(` `);if(!(n<0)&&e.slice(0,n)===t){let t=e.slice(n+1).trim();return t.length>0?t:null}}return null}async function j(e,t,n){try{let n=(await l(e,`${t}^{tree}`)).trim(),r=await M(e);if(n.length>0&&n===r){let n=await A(e,t);if(n!=null)try{await d(e,n)}catch{}return{conflicted:!1,conflictedFiles:[]}}}catch{}let r=await A(e,t);if(r==null)return{conflicted:!0,conflictedFiles:[]};let i;try{return await f(e,{withIndex:!0,ref:r}),{conflicted:!1,conflictedFiles:[]}}catch(e){i=e}let a=await F(e);if(a.length>0)return{conflicted:!0,conflictedFiles:a};if(N(i)){let n=await A(e,t);if(n!=null)try{await d(e,n)}catch{}return{conflicted:!1,conflictedFiles:[]}}return{conflicted:!0,conflictedFiles:[]}}async function M(e){let t=C.join(S.tmpdir(),`compose-equiv-${b()}`);try{let n={GIT_INDEX_FILE:t};return await s(e,`HEAD`,{indexEnv:n}),await u(e,{indexEnv:n}),(await g(e,{indexEnv:n})).trim()}finally{z(t)}}function N(e){let t=P(e);return t.includes(`already exists, no checkout`)||t.includes(`could not restore untracked files from stash`)}function P(e){if(e==null)return``;if(e instanceof Error){let t=e.stderr;return t!=null&&t.length>0?`${e.message}\n${t}`:e.message}return String(e)}async function F(e){try{let t=await m(e),n=[];for(let e of t.split(`
4
- `))/^(UU|AA|DD|AU|UA|UD|DU) /.test(e)&&n.push(e.slice(3).trim());return n}catch{return[]}}function I(e,t){let n=e.filter(e=>e.author!=null);if(n.length===0)return{author:void 0,coAuthors:[]};let r;if(typeof t==`function`)r=t(e);else if(t===`first-hunk`)r=n[0].author;else{let e=new Map;for(let t of n){let n=L(t.author),r=e.get(n);r?r.count++:e.set(n,{identity:t.author,count:1})}let t=0;for(let{identity:n,count:i}of e.values())i>t&&(r=n,t=i)}let i=new Map;for(let t of e){t.author&&i.set(L(t.author),t.author);for(let e of t.coAuthors??[])i.set(L(e),e)}let a=r?L(r):null,o=[];for(let[e,t]of i)e!==a&&o.push(t);return{author:r,coAuthors:o}}function L(e){return`${e.name.toLowerCase()}\0${e.email.toLowerCase()}`}function R(e,t){if(t.length===0)return e;let n=t.map(e=>`Co-authored-by: ${e.name} <${e.email}>`).join(`
5
- `);return`${e.trimEnd()}\n\n${n}\n`}function z(e){try{x.unlinkSync(e)}catch{}}function B(e,t){let n=e.split(`
1
+ import{GitError as e}from"../errors/git.js";import{applyPatchToIndex as t,applyPatchToWorkdir as n,checkout as r,commitTree as i,currentBranch as a,diffTree as o,localeNeutralEnv as s,readTree as c,reset as l,revParse as u,revParseVerify as d,stageAll as f,stashDrop as p,stashPop as m,stashPush as h,statusPorcelain as g,updateRef as _,writeTree as v}from"./port-dispatch.js";import{listRefsWithPeeledSha as y}from"./utils/refs.js";import{captureExcludedLayersPatch as b}from"./diff-collector.js";import{reparentDescendants as x}from"./reparent.js";import{buildPatchForCommit as S}from"./utils/patch-builder.js";import{randomUUID as C}from"node:crypto";import*as w from"node:fs";import*as T from"node:os";import*as E from"node:path";async function D(n){let{git:r,baseSha:a,commits:o,hunks:s,signing:l,authorAttribution:u=`plurality`}=n,d=E.join(T.tmpdir(),`compose-idx-${C()}`),f=a,p=new Map,m=new Set;try{for(let n of o){let a=n.hunkIndices.map(t=>{let n=s.find(e=>e.index===t);if(!n)throw new e(`OPERATION_FAILED`,`Hunk index ${t} not found`);return n}),o=S(a.map(e=>m.has(e.diffHeader)?{...e,diffHeader:q(e.diffHeader,e.fileName)}:e));if(!o.trim())throw new e(`OPERATION_FAILED`,`Empty patch for commit: ${n.message}`);let h={GIT_INDEX_FILE:d};await c(r,f,{indexEnv:h});try{await t(r,o,{indexEnv:h})}catch(t){let r=t instanceof Error?t.message:String(t);throw new e(`OPERATION_FAILED`,`git apply failed for commit '${n.message.split(`
2
+ `)[0]}': ${r}`,{cause:t})}for(let e of a)e.originalFileName&&e.diffHeader.includes(`rename from`)&&m.add(e.diffHeader);let g=await v(r,{indexEnv:h}),_=U(a,u),y=G(n.message,_.coAuthors),b=await i(r,g,[f],y,{signing:l,author:_.author});p.set(n.id,b),f=b}return{headSha:f,shaMap:p}}finally{K(d)}}async function O(e,t,r){t&&await n(e,t),r&&await n(e,r)}async function k(e){let{git:t,newHeadSha:n,branchName:i,stashLabel:a,leftoverPatch:o,includeWorkdir:s}=e,c=a??M,d=s==null?await N(t,c):null,f=s==null?void 0:await b(t,await u(t,`HEAD`),s);try{i?await r(t,i,{createBranch:!0,startPoint:n}):await l(t,n,`hard`),await O(t,o,f)}catch(e){throw d!=null&&await L(t,d),e}if(d!=null){let e=await L(t,d);if(e.kind===`conflicted`)return{stashConflict:{stashLabel:c,conflictedFiles:e.conflictedFiles}};if(e.kind===`not-restored`)return{stashNotRestored:{stashLabel:c,stashSha:d.sha,reason:e.reason}}}return{}}async function A(e){let{git:t,branch:n,rangeTo:i,newHeadSha:o,originalBranchSha:s,stashLabel:c,leftoverPatch:d,includeWorkdir:f}=e,p=await a(t)===n,m=c??M,h=p&&f==null?await N(t,m):null,g=p&&f!=null?await b(t,await u(t,`HEAD`),f):void 0,v,y,S=o,C=[];try{let e=await x({git:t,oldToSha:i,newTopSha:o,sourceBranchName:n});for(let t of e.reparentedRefs)C.push({refname:t.refname,previousSha:t.oldSha,newSha:t.newSha});e.sourceBranchNewSha==null?(await _(t,`refs/heads/${n}`,o),C.push({refname:`refs/heads/${n}`,previousSha:s,newSha:o}),S=o):S=e.sourceBranchNewSha;let a=await j(t,i,o,`refs/heads/${n}`);C.push(...a),p&&(await r(t,n),await l(t,S,`hard`),await O(t,d,g))}catch(e){throw h!=null&&await L(t,h),e}if(h!=null){let e=await L(t,h);e.kind===`conflicted`?v={stashLabel:m,conflictedFiles:e.conflictedFiles}:e.kind===`not-restored`&&(y={stashLabel:m,stashSha:h.sha,reason:e.reason})}return{finalTipSha:S,refMovements:C,stashConflict:v,stashNotRestored:y}}async function j(e,t,n,r){let i=[];for(let{refname:a,sha:o}of await y(e,{pointsAt:t}))a!==r&&(!a.startsWith(`refs/heads/`)&&!a.startsWith(`refs/tags/`)||o===t&&(await _(e,a,n),i.push({refname:a,previousSha:t,newSha:n})));return i}const M=`compose backup`;async function N(e,t){let n=await g(e);if(!n.trim())return null;let r=n.split(`
3
+ `).some(e=>e.startsWith(`?? `));await h(e,{message:t,includeUntracked:!0});try{let t=(await u(e,`stash@{0}`)).trim();return t.length===0?null:(r||=await F(e,t),{sha:t,hasUntracked:r})}catch{return null}}const P=new Set([`4b825dc642cb6eb9a060e54bf8d69288fbee4904`,`6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321`]);async function F(e,t){try{let n=await d(e,`${t}^3^{tree}`);return n==null?!0:!P.has(n)}catch{return!0}}async function I(e,t){if(!e.exec)return null;let n;try{n=await e.exec([`stash`,`list`,`--format=%H %gd`],{env:s})}catch{return null}for(let e of n.split(`
4
+ `)){let n=e.indexOf(` `);if(!(n<0)&&e.slice(0,n)===t){let t=e.slice(n+1).trim();return t.length>0?t:null}}return null}async function L(e,t){if(!t.hasUntracked)try{let n=(await u(e,`${t.sha}^{tree}`)).trim(),r=await z(e);if(n.length>0&&n===r){let n=await I(e,t.sha);if(n!=null)try{await p(e,n)}catch{}return{kind:`restored`}}}catch{}let n=await I(e,t.sha);if(n==null)return{kind:`not-restored`,reason:`stash-ref-not-found`};let r;try{return await m(e,{withIndex:!0,ref:n}),{kind:`restored`}}catch(e){r=e}let i=await H(e);if(i.length>0)return{kind:`conflicted`,conflictedFiles:i};if(B(r)){if(await R(e,t.sha)){let n=await I(e,t.sha);if(n!=null)try{await p(e,n)}catch{}return{kind:`restored`}}return{kind:`not-restored`,reason:`untracked-restore-failed`}}return{kind:`not-restored`,reason:`pop-failed`}}async function R(e,t){try{let n=await d(e,`${t}^3^{tree}`);return n==null?!1:(await o(e,n,await z(e),{nameOnly:!0,binary:!1,diffFilter:`DMT`,noRenames:!0})).trim().length===0}catch{return!1}}async function z(e){let t=E.join(T.tmpdir(),`compose-equiv-${C()}`);try{let n={GIT_INDEX_FILE:t};return await c(e,`HEAD`,{indexEnv:n}),await f(e,{indexEnv:n}),(await v(e,{indexEnv:n})).trim()}finally{K(t)}}function B(e){let t=V(e);return t.includes(`already exists, no checkout`)||t.includes(`could not restore untracked files from stash`)}function V(e){if(e==null)return``;if(e instanceof Error){let t=e.stderr;return t!=null&&t.length>0?`${e.message}\n${t}`:e.message}return String(e)}async function H(e){try{let t=await g(e),n=[];for(let e of t.split(`
5
+ `))/^(UU|AA|DD|AU|UA|UD|DU) /.test(e)&&n.push(e.slice(3).trim());return n}catch{return[]}}function U(e,t){let n=e.filter(e=>e.author!=null);if(n.length===0)return{author:void 0,coAuthors:[]};let r;if(typeof t==`function`)r=t(e);else if(t===`first-hunk`)r=n[0].author;else{let e=new Map;for(let t of n){let n=W(t.author),r=e.get(n);r?r.count++:e.set(n,{identity:t.author,count:1})}let t=0;for(let{identity:n,count:i}of e.values())i>t&&(r=n,t=i)}let i=new Map;for(let t of e){t.author&&i.set(W(t.author),t.author);for(let e of t.coAuthors??[])i.set(W(e),e)}let a=r?W(r):null,o=[];for(let[e,t]of i)e!==a&&o.push(t);return{author:r,coAuthors:o}}function W(e){return`${e.name.toLowerCase()}\0${e.email.toLowerCase()}`}function G(e,t){if(t.length===0)return e;let n=t.map(e=>`Co-authored-by: ${e.name} <${e.email}>`).join(`
6
+ `);return`${e.trimEnd()}\n\n${n}\n`}function K(e){try{w.unlinkSync(e)}catch{}}function q(e,t){let n=e.split(`
6
7
  `),r=[];for(let e of n)e.startsWith(`diff --git `)?r.push(`diff --git a/${t} b/${t}`):e.startsWith(`similarity index`)||e.startsWith(`rename from`)||e.startsWith(`rename to`)||(e.startsWith(`--- a/`)?r.push(`--- a/${t}`):r.push(e));return r.join(`
7
- `)}export{T as applyCommitChain,E as applyRangeRewrite,w as buildCommitChain,D as moveRefsAtSha};
8
+ `)}export{k as applyCommitChain,A as applyRangeRewrite,D as buildCommitChain,j as moveRefsAtSha};
@@ -87,9 +87,10 @@ declare function collectCombinedDiffRange(git: ComposeGitPort, parentOfFromSha:
87
87
  * snapshot hashes that same combined diff — the bytes the AI sees match the bytes the
88
88
  * apply phase verifies against.
89
89
  *
90
- * Requires `to` to equal both the branch tip and HEAD, and HEAD to be non-detached.
91
- * Mixing WIP with a range only makes sense when the user is actively on the branch tip
92
- * being recomposed.
90
+ * Requires `to` to equal both the branch tip and HEAD, and HEAD to be non-detached — mixing WIP
91
+ * with a range only makes sense when the user is actively on the branch tip being recomposed.
92
+ * A layer the caller excluded stays out of the collected input entirely; preserving its content
93
+ * across the apply's worktree reset is the apply path's job, via {@link captureExcludedLayersPatch}.
93
94
  */
94
95
  declare function collectCommitRangeWithWorkdir(git: ComposeGitPort, branchName: string, from: string, to: string, options: {
95
96
  includeStaged: boolean;
@@ -109,6 +110,36 @@ declare function collectWorkdirPortionForRange(git: ComposeGitPort, headSha: str
109
110
  }): Promise<{
110
111
  finalTreeSha: string;
111
112
  }>;
113
+ /** Resolve `ComposeSource.includeWorkdir`'s optional per-layer flags, each of which defaults to `true`. */
114
+ declare function resolveWorkdirLayers(includeWorkdir: {
115
+ includeStaged?: boolean;
116
+ includeUnstaged?: boolean;
117
+ includeUntracked?: boolean;
118
+ }): {
119
+ includeStaged: boolean;
120
+ includeUnstaged: boolean;
121
+ includeUntracked: boolean;
122
+ };
123
+ /**
124
+ * Capture the content of the workdir layers the caller excluded from a `commit-range +
125
+ * includeWorkdir` compose, as one patch. Returns `undefined` when both tracked layers are
126
+ * included, or when the excluded ones hold nothing.
127
+ *
128
+ * An excluded layer is deliberately absent from the collected input, so it is also absent from the
129
+ * plan and from the leftover patch — and a range rewrite hard-resets the worktree to the new tip.
130
+ * This patch is the only remaining record of that content, so the apply path takes it before
131
+ * mutating anything and re-applies it after the reset. Untracked files are not included: the reset
132
+ * leaves them alone.
133
+ */
134
+ declare function captureExcludedLayersPatch(git: ComposeGitPort, headSha: string, options: {
135
+ includeStaged: boolean;
136
+ includeUnstaged: boolean;
137
+ }): Promise<string | undefined>;
138
+ /**
139
+ * Compute the tree SHA of the worktree's tracked content, working against a temp copy of the
140
+ * real index so the consumer's staged state is never modified.
141
+ */
142
+ declare function computeTrackedWorktreeTreeSha(git: ComposeGitPort): Promise<string>;
112
143
  declare function collectCommitsForSplit(git: ComposeGitPort, branchName: string, baseBranch: string): Promise<SplitCollectionResult>;
113
144
  /**
114
145
  * Compute per-source content hashes alongside the authoritative combined hash.
@@ -127,4 +158,4 @@ declare function collectSourceHashes(git: ComposeGitPort, options: {
127
158
  includeUntracked: boolean;
128
159
  }): Promise<SafetySnapshot['sourceHashes']>;
129
160
  //#endregion
130
- export { BranchCommitInfo, BranchDiffResult, CollectionResult, SplitCollectionResult, WorkingDiffOptions, WorkingDiffResult, collectBranchDiffs, collectCombinedDiffRange, collectCommitRangeDiffs, collectCommitRangeWithWorkdir, collectCommitsForSplit, collectFromSource, collectSourceHashes, collectWorkdirPortionForRange, collectWorkingDiffs };
161
+ export { BranchCommitInfo, BranchDiffResult, CollectionResult, SplitCollectionResult, WorkingDiffOptions, WorkingDiffResult, captureExcludedLayersPatch, collectBranchDiffs, collectCombinedDiffRange, collectCommitRangeDiffs, collectCommitRangeWithWorkdir, collectCommitsForSplit, collectFromSource, collectSourceHashes, collectWorkdirPortionForRange, collectWorkingDiffs, computeTrackedWorktreeTreeSha, resolveWorkdirLayers };
@@ -1,7 +1,7 @@
1
- import{GitError as e}from"../errors/git.js";import{ComposeWorkflowInputError as t}from"../errors/workflow.js";import{applyPatchToIndex as n,createBranch as r,currentBranch as i,diffTree as a,gitDir as o,listUntracked as s,log as c,mergeBase as l,mergeBaseIsAncestor as u,readTree as d,revParse as f,revParseVerify as p,stageAll as m,writeTree as h}from"./port-dispatch.js";import{parseUnifiedDiff as g}from"./utils/hunk-parser.js";import{getAuthorAndCoAuthorsForCombinedDiffHunk as _}from"./utils/authorship.js";import{sha256 as v}from"./utils/crypto.js";import{withUntrackedInTempIndex as y}from"./utils/temp-index.js";import{randomUUID as b}from"node:crypto";import{copyFileSync as x,unlinkSync as S}from"node:fs";import{tmpdir as C}from"node:os";import{join as w}from"node:path";async function T(e,t){if(t.type===`workdir`){let n=!t.stagedOnly&&!t.noUntracked;return{...await E(e,{includeUnstaged:!t.stagedOnly,includeUntracked:n}),untrackedIncluded:n}}if(t.type===`commit-range`){if(t.includeWorkdir){let n=t.includeWorkdir.includeStaged??!0,r=t.includeWorkdir.includeUnstaged??!0,i=t.includeWorkdir.includeUntracked??!0,a=await L(e,t.branch,t.from,t.to,{includeStaged:n,includeUnstaged:r,includeUntracked:i});return{hunks:a.hunks,snapshot:a.snapshot,combinedDiff:a.combinedDiff,commitMessages:a.commits.map(e=>e.message),commitOrder:a.commits.map(e=>e.sha),branchName:t.branch,baseBranch:void 0,branchSha:a.snapshot.branchSha,mergeBaseSha:a.snapshot.baseSha,untrackedIncluded:i}}let n=await M(e,t.branch,t.from,t.to);return{hunks:n.hunks,snapshot:n.snapshot,combinedDiff:n.combinedDiff,commitMessages:n.commits.map(e=>e.message),commitOrder:n.commits.map(e=>e.sha),branchName:t.branch,baseBranch:void 0,branchSha:n.snapshot.branchSha,mergeBaseSha:n.snapshot.baseSha,untrackedIncluded:!1}}let n=t.mergeTarget??`main`,r=await j(e,t.name,n);return{hunks:r.hunks,snapshot:r.snapshot,combinedDiff:r.combinedDiff,commitMessages:r.commits.map(e=>e.message),commitOrder:r.commits.map(e=>e.sha),branchName:t.name,baseBranch:n,branchSha:r.snapshot.branchSha,mergeBaseSha:r.snapshot.baseSha,untrackedIncluded:!1}}async function E(e,t){let n=t.includeUnstaged||t.includeUntracked;return t.includeUntracked?k(e,n):D(e,n)}async function D(e,t){let n=await f(e,`HEAD`),r=await O(e,n,t);return{hunks:g(r),snapshot:V(r,n,void 0,void 0,await H(e,{includeUnstaged:t,includeUntracked:!1})),combinedDiff:r}}async function O(e,t,n){if(!n)return a(e,t,await h(e));let r=w(await o(e),`index`),i=w(C(),`compose-snapshot-idx-${b()}`);try{x(r,i);let n={GIT_INDEX_FILE:i};return await m(e,{trackedOnly:!0,indexEnv:n}),a(e,t,await h(e,{indexEnv:n}))}finally{try{S(i)}catch{}}}async function k(e,t){return y(e,async n=>{await A(e,n,t);let r=await h(e,{indexEnv:n}),i=await f(e,`HEAD`),o=await a(e,i,r);return{hunks:g(o),snapshot:V(o,i,void 0,void 0,await H(e,{includeUnstaged:t,includeUntracked:!0})),combinedDiff:o}})}async function A(e,t,n){n&&await m(e,{trackedOnly:!0,indexEnv:t}),await m(e,{indexEnv:t})}async function j(e,t,n){let r=await B(e,t);return P(e,await l(e,await B(e,n),r),await f(e,r))}async function M(e,t,n,r){await B(e,t);let i=await f(e,n),a=await f(e,r);return P(e,await N(e,i),a)}async function N(e,n){try{return await f(e,`${n}^`)}catch{throw new t(`Cannot recompose commit ${n.slice(0,8)} — it has no parent (root commit). Recomposing the root commit is not currently supported.`)}}async function P(n,r,i){if(!await u(n,r,i))throw new t(`Cannot collect range ${r.slice(0,8)}..${i.slice(0,8)}: '${r.slice(0,8)}' is not an ancestor of '${i.slice(0,8)}'.`);let o=await a(n,r,i),s=g(o),l=I(await c(n,{format:`%H%n%an%n%ae%n%aI%n%B%n---COMMIT-END---`,range:`${r}..${i}`,reverse:!0}));if(l.length===0)throw new e(`NO_CHANGES`,`No commits in range '${r.slice(0,8)}..${i.slice(0,8)}'.`);let d=await F(n,l);for(let e of s){let{author:t,coAuthors:n}=_(d,e);t!=null&&(e.author=t),n.length>0&&(e.coAuthors=n)}let p=V(o,await f(n,`HEAD`),i,r);return{hunks:s,commits:l.map(e=>({sha:e.sha,message:e.message})),snapshot:p,combinedDiff:o}}async function F(e,t){let n=[];for(let r of t){let t;try{t=await a(e,`${r.sha}^`,r.sha)}catch{continue}let i=g(t,0);for(let e of i)e.author={name:r.authorName,email:r.authorEmail,date:r.authorDate};n.push(...i)}return n}function I(e){let t=[],n=e.split(`---COMMIT-END---`).filter(e=>e.trim());for(let e of n){let n=e.replace(/^\n+/,``).split(`
1
+ import{GitError as e}from"../errors/git.js";import{ComposeWorkflowInputError as t}from"../errors/workflow.js";import{applyPatchToIndex as n,createBranch as r,currentBranch as i,diffTree as a,gitDir as o,listUntracked as s,log as c,mergeBase as l,mergeBaseIsAncestor as u,readTree as d,revParse as f,revParseVerify as p,stageAll as m,writeTree as h}from"./port-dispatch.js";import{parseUnifiedDiff as g}from"./utils/hunk-parser.js";import{getAuthorAndCoAuthorsForCombinedDiffHunk as _}from"./utils/authorship.js";import{sha256 as v}from"./utils/crypto.js";import{withUntrackedInTempIndex as y}from"./utils/temp-index.js";import{randomUUID as b}from"node:crypto";import{copyFileSync as x,unlinkSync as S}from"node:fs";import{tmpdir as C}from"node:os";import{join as w}from"node:path";async function T(e,t){if(t.type===`workdir`){let n=!t.stagedOnly&&!t.noUntracked;return{...await E(e,{includeUnstaged:!t.stagedOnly,includeUntracked:n}),untrackedIncluded:n}}if(t.type===`commit-range`){if(t.includeWorkdir){let n=t.includeWorkdir.includeStaged??!0,r=t.includeWorkdir.includeUnstaged??!0,i=t.includeWorkdir.includeUntracked??!0,a=await L(e,t.branch,t.from,t.to,{includeStaged:n,includeUnstaged:r,includeUntracked:i});return{hunks:a.hunks,snapshot:a.snapshot,combinedDiff:a.combinedDiff,commitMessages:a.commits.map(e=>e.message),commitOrder:a.commits.map(e=>e.sha),branchName:t.branch,baseBranch:void 0,branchSha:a.snapshot.branchSha,mergeBaseSha:a.snapshot.baseSha,untrackedIncluded:i}}let n=await M(e,t.branch,t.from,t.to);return{hunks:n.hunks,snapshot:n.snapshot,combinedDiff:n.combinedDiff,commitMessages:n.commits.map(e=>e.message),commitOrder:n.commits.map(e=>e.sha),branchName:t.branch,baseBranch:void 0,branchSha:n.snapshot.branchSha,mergeBaseSha:n.snapshot.baseSha,untrackedIncluded:!1}}let n=t.mergeTarget??`main`,r=await j(e,t.name,n);return{hunks:r.hunks,snapshot:r.snapshot,combinedDiff:r.combinedDiff,commitMessages:r.commits.map(e=>e.message),commitOrder:r.commits.map(e=>e.sha),branchName:t.name,baseBranch:n,branchSha:r.snapshot.branchSha,mergeBaseSha:r.snapshot.baseSha,untrackedIncluded:!1}}async function E(e,t){let n=t.includeUnstaged||t.includeUntracked;return t.includeUntracked?k(e,n):D(e,n)}async function D(e,t){let n=await f(e,`HEAD`),r=await O(e,n,t);return{hunks:g(r),snapshot:W(r,n,void 0,void 0,await G(e,{includeUnstaged:t,includeUntracked:!1})),combinedDiff:r}}async function O(e,t,n){return n?a(e,t,await V(e)):a(e,t,await h(e))}async function k(e,t){return y(e,async n=>{await A(e,n,t);let r=await h(e,{indexEnv:n}),i=await f(e,`HEAD`),o=await a(e,i,r);return{hunks:g(o),snapshot:W(o,i,void 0,void 0,await G(e,{includeUnstaged:t,includeUntracked:!0})),combinedDiff:o}})}async function A(e,t,n){n&&await m(e,{trackedOnly:!0,indexEnv:t}),await m(e,{indexEnv:t})}async function j(e,t,n){let r=await U(e,t);return P(e,await l(e,await U(e,n),r),await f(e,r))}async function M(e,t,n,r){await U(e,t);let i=await f(e,n),a=await f(e,r);return P(e,await N(e,i),a)}async function N(e,n){try{return await f(e,`${n}^`)}catch{throw new t(`Cannot recompose commit ${n.slice(0,8)} — it has no parent (root commit). Recomposing the root commit is not currently supported.`)}}async function P(n,r,i){if(!await u(n,r,i))throw new t(`Cannot collect range ${r.slice(0,8)}..${i.slice(0,8)}: '${r.slice(0,8)}' is not an ancestor of '${i.slice(0,8)}'.`);let o=await a(n,r,i),s=g(o),l=I(await c(n,{format:`%H%n%an%n%ae%n%aI%n%B%n---COMMIT-END---`,range:`${r}..${i}`,reverse:!0}));if(l.length===0)throw new e(`NO_CHANGES`,`No commits in range '${r.slice(0,8)}..${i.slice(0,8)}'.`);let d=await F(n,l);for(let e of s){let{author:t,coAuthors:n}=_(d,e);t!=null&&(e.author=t),n.length>0&&(e.coAuthors=n)}let p=W(o,await f(n,`HEAD`),i,r);return{hunks:s,commits:l.map(e=>({sha:e.sha,message:e.message})),snapshot:p,combinedDiff:o}}async function F(e,t){let n=[];for(let r of t){let t;try{t=await a(e,`${r.sha}^`,r.sha)}catch{continue}let i=g(t,0);for(let e of i)e.author={name:r.authorName,email:r.authorEmail,date:r.authorDate};n.push(...i)}return n}function I(e){let t=[],n=e.split(`---COMMIT-END---`).filter(e=>e.trim());for(let e of n){let n=e.replace(/^\n+/,``).split(`
2
2
  `);if(n.length<5)continue;let r=n[0],i=n[1],a=n[2],o=n[3],s=n.slice(4).join(`
3
- `).trimEnd(),c=o?new Date(o):void 0,l=c&&!Number.isNaN(c.getTime())?c:void 0;t.push({sha:r,authorName:i,authorEmail:a,authorDate:l,message:s})}return t}async function L(e,n,r,o,s){let l=await f(e,await B(e,n)),d=await f(e,r),p=await f(e,o),m=await f(e,`HEAD`);if(p!==l)throw new t(`commit-range with includeWorkdir requires 'to' to equal branch tip. Branch '${n}' tip is ${l}; got 'to'=${p}.`);if(m!==p)throw new t(`commit-range with includeWorkdir requires HEAD to equal 'to'. HEAD=${m}, 'to'=${p}.`);if(await i(e)==null)throw new t(`commit-range with includeWorkdir requires HEAD to be on a branch (non-detached). Check out the branch holding the range before composing.`);let h=await N(e,d),{finalTreeSha:v}=await R(e,m,s),y=await a(e,h,v);if(!await u(e,h,p))throw new t(`Cannot collect range ${h.slice(0,8)}..${p.slice(0,8)}: '${h.slice(0,8)}' is not an ancestor of '${p.slice(0,8)}'.`);let b=g(y),x=I(await c(e,{format:`%H%n%an%n%ae%n%aI%n%B%n---COMMIT-END---`,range:`${h}..${p}`,reverse:!0})),S=await F(e,x);for(let e of b){let{author:t,coAuthors:n}=_(S,e);t!=null&&(e.author=t),n.length>0&&(e.coAuthors=n)}let C=V(y,m,p,h,await H(e,s));return{hunks:b,commits:x.map(e=>({sha:e.sha,message:e.message})),snapshot:C,combinedDiff:y}}async function R(e,t,r){let i=w(C(),`compose-workdir-${b()}`),c={GIT_INDEX_FILE:i};try{if(await d(e,t,{indexEnv:c}),r.includeStaged){let r=await a(e,t,await h(e));r.trim()&&await n(e,r,{indexEnv:c})}if(r.includeUnstaged){let t=await h(e),r=w(C(),`compose-workdir-u-${b()}`),i={GIT_INDEX_FILE:r};try{x(w(await o(e),`index`),r),await m(e,{trackedOnly:!0,indexEnv:i});let s=await a(e,t,await h(e,{indexEnv:i}));s.trim()&&await n(e,s,{indexEnv:c})}finally{try{S(r)}catch{}}}if(r.includeUntracked){let t=await s(e);t.length>0&&(await m(e,{intentToAdd:!0,paths:t,indexEnv:c}),await m(e,{paths:t,indexEnv:c}))}return{finalTreeSha:await h(e,{indexEnv:c})}}finally{try{S(i)}catch{}}}async function z(e,t,n){let r=await B(e,t),i=await l(e,await B(e,n),r),o=await f(e,r),s=(await c(e,{format:`%H`,range:`${i}..${r}`,reverse:!0})).trim().split(`
3
+ `).trimEnd(),c=o?new Date(o):void 0,l=c&&!Number.isNaN(c.getTime())?c:void 0;t.push({sha:r,authorName:i,authorEmail:a,authorDate:l,message:s})}return t}async function L(e,n,r,o,s){let l=await f(e,await U(e,n)),d=await f(e,r),p=await f(e,o),m=await f(e,`HEAD`);if(p!==l)throw new t(`commit-range with includeWorkdir requires 'to' to equal branch tip. Branch '${n}' tip is ${l}; got 'to'=${p}.`);if(m!==p)throw new t(`commit-range with includeWorkdir requires HEAD to equal 'to'. HEAD=${m}, 'to'=${p}.`);if(await i(e)==null)throw new t(`commit-range with includeWorkdir requires HEAD to be on a branch (non-detached). Check out the branch holding the range before composing.`);if(!s.includeStaged&&s.includeUnstaged&&(await a(e,m,await h(e))).trim())throw new t(`Cannot exclude staged changes while composing unstaged changes: unstaged changes are diffed against the index, so the excluded staged layer cannot be replayed over the composed result. Include the staged layer, or commit/stash it before composing.`);let v=await N(e,d),{finalTreeSha:y}=await R(e,m,s),b=await a(e,v,y);if(!await u(e,v,p))throw new t(`Cannot collect range ${v.slice(0,8)}..${p.slice(0,8)}: '${v.slice(0,8)}' is not an ancestor of '${p.slice(0,8)}'.`);let x=g(b),S=I(await c(e,{format:`%H%n%an%n%ae%n%aI%n%B%n---COMMIT-END---`,range:`${v}..${p}`,reverse:!0})),C=await F(e,S);for(let e of x){let{author:t,coAuthors:n}=_(C,e);t!=null&&(e.author=t),n.length>0&&(e.coAuthors=n)}let w=W(b,m,p,v,await G(e,s));return{hunks:x,commits:S.map(e=>({sha:e.sha,message:e.message})),snapshot:w,combinedDiff:b}}async function R(e,t,r){let i=w(C(),`compose-workdir-${b()}`),o={GIT_INDEX_FILE:i};try{if(await d(e,t,{indexEnv:o}),r.includeStaged){let r=await a(e,t,await h(e));r.trim()&&await n(e,r,{indexEnv:o})}if(r.includeUnstaged){let t=await a(e,await h(e),await V(e));t.trim()&&await n(e,t,{indexEnv:o})}if(r.includeUntracked){let t=await s(e);t.length>0&&(await m(e,{intentToAdd:!0,paths:t,indexEnv:o}),await m(e,{paths:t,indexEnv:o}))}return{finalTreeSha:await h(e,{indexEnv:o})}}finally{try{S(i)}catch{}}}function z(e){return{includeStaged:e.includeStaged??!0,includeUnstaged:e.includeUnstaged??!0,includeUntracked:e.includeUntracked??!0}}async function B(e,t,n){if(n.includeStaged&&n.includeUnstaged)return;let r;return r=!n.includeStaged&&!n.includeUnstaged?await a(e,t,await V(e)):n.includeStaged?await a(e,await h(e),await V(e)):await a(e,t,await h(e)),r.trim()?r:void 0}async function V(e){let t=w(C(),`compose-wt-tree-${b()}`),n={GIT_INDEX_FILE:t};try{return x(w(await o(e),`index`),t),await m(e,{trackedOnly:!0,indexEnv:n}),await h(e,{indexEnv:n})}finally{try{S(t)}catch{}}}async function H(e,t,n){let r=await U(e,t),i=await l(e,await U(e,n),r),o=await f(e,r),s=(await c(e,{format:`%H`,range:`${i}..${r}`,reverse:!0})).trim().split(`
4
4
  `).filter(Boolean),u=[];for(let t of s){let[n,r,i]=await Promise.all([c(e,{format:`%B`,sha:t}).then(e=>e.trim()),a(e,`${t}^`,t),a(e,`${t}^`,t,{binary:!1,nameOnly:!0})]),o=i.trim().split(`
5
- `).filter(Boolean);u.push({sha:t,message:n,diff:r,filesChanged:o})}return{commits:u,mergeBaseSha:i,branchSha:o}}async function B(t,n){if(await p(t,`refs/heads/${n}`)||await p(t,n))return n;if(await p(t,`origin/${n}`))return await r(t,n,`origin/${n}`),n;throw new e(`BRANCH_NOT_FOUND`,`Branch '${n}' not found locally or on origin. Run 'git fetch' if the branch exists on the remote.`)}function V(e,t,n,r,i){return{diffHash:v(e),headSha:t,branchSha:n,baseSha:r,sourceHashes:i}}async function H(e,t){if(!e.exec)return;let n=t.includeStaged??!0,r={};if(n)try{r.staged=v(await e.exec([`diff`,`--cached`,`--binary`,`HEAD`]))}catch{}if(t.includeUnstaged)try{r.unstaged=v(await e.exec([`diff`,`--binary`]))}catch{}if(t.includeUntracked)try{let t=(await e.exec([`ls-files`,`--others`,`--exclude-standard`])).trim().split(`
5
+ `).filter(Boolean);u.push({sha:t,message:n,diff:r,filesChanged:o})}return{commits:u,mergeBaseSha:i,branchSha:o}}async function U(t,n){if(await p(t,`refs/heads/${n}`)||await p(t,n))return n;if(await p(t,`origin/${n}`))return await r(t,n,`origin/${n}`),n;throw new e(`BRANCH_NOT_FOUND`,`Branch '${n}' not found locally or on origin. Run 'git fetch' if the branch exists on the remote.`)}function W(e,t,n,r,i){return{diffHash:v(e),headSha:t,branchSha:n,baseSha:r,sourceHashes:i}}async function G(e,t){if(!e.exec)return;let n=t.includeStaged??!0,r={};if(n)try{r.staged=v(await e.exec([`diff`,`--cached`,`--binary`,`HEAD`]))}catch{}if(t.includeUnstaged)try{r.unstaged=v(await e.exec([`diff`,`--binary`]))}catch{}if(t.includeUntracked)try{let t=(await e.exec([`ls-files`,`--others`,`--exclude-standard`])).trim().split(`
6
6
  `).filter(Boolean);if(t.length>0){let n=[];for(let r of t)try{let t=await e.exec([`hash-object`,`--`,r]);n.push(`${r}:${t.trim()}`)}catch{}r.untracked=v(n.join(`
7
- `))}else r.untracked=v(``)}catch{}return Object.keys(r).length>0?r:void 0}export{j as collectBranchDiffs,P as collectCombinedDiffRange,M as collectCommitRangeDiffs,L as collectCommitRangeWithWorkdir,z as collectCommitsForSplit,T as collectFromSource,H as collectSourceHashes,R as collectWorkdirPortionForRange,E as collectWorkingDiffs};
7
+ `))}else r.untracked=v(``)}catch{}return Object.keys(r).length>0?r:void 0}export{B as captureExcludedLayersPatch,j as collectBranchDiffs,P as collectCombinedDiffRange,M as collectCommitRangeDiffs,L as collectCommitRangeWithWorkdir,H as collectCommitsForSplit,T as collectFromSource,G as collectSourceHashes,R as collectWorkdirPortionForRange,E as collectWorkingDiffs,V as computeTrackedWorktreeTreeSha,z as resolveWorkdirLayers};
package/dist/git/index.js CHANGED
@@ -1 +1 @@
1
- import{applyBranchStructure as e,createStackedBranches as t}from"./apply-branches.js";import{applyCommitChain as n,buildCommitChain as r}from"./commit-chain.js";import{collectBranchDiffs as i,collectCommitsForSplit as a,collectFromSource as o,collectWorkingDiffs as s}from"./diff-collector.js";import{verifyMultiBranchDiff as c,verifyResultingDiff as l,verifySnapshot as u}from"./safety.js";import{applyToTarget as d}from"./apply-target.js";export{e as applyBranchStructure,n as applyCommitChain,d as applyToTarget,r as buildCommitChain,i as collectBranchDiffs,a as collectCommitsForSplit,o as collectFromSource,s as collectWorkingDiffs,t as createStackedBranches,c as verifyMultiBranchDiff,l as verifyResultingDiff,u as verifySnapshot};
1
+ import{applyBranchStructure as e,createStackedBranches as t}from"./apply-branches.js";import{collectBranchDiffs as n,collectCommitsForSplit as r,collectFromSource as i,collectWorkingDiffs as a}from"./diff-collector.js";import{applyCommitChain as o,buildCommitChain as s}from"./commit-chain.js";import{verifyMultiBranchDiff as c,verifyResultingDiff as l,verifySnapshot as u}from"./safety.js";import{applyToTarget as d}from"./apply-target.js";export{e as applyBranchStructure,o as applyCommitChain,d as applyToTarget,s as buildCommitChain,n as collectBranchDiffs,r as collectCommitsForSplit,i as collectFromSource,a as collectWorkingDiffs,t as createStackedBranches,c as verifyMultiBranchDiff,l as verifyResultingDiff,u as verifySnapshot};
@@ -1,4 +1,4 @@
1
- import{ComposeGitPortMissingOpError as e}from"../ports/git.js";function t(t,n){throw new e(t,n)}async function n(e,n,r){return e.revParse?e.revParse(n,r):e.exec?(await e.exec([`rev-parse`,n])).trim():t(`revParse`)}async function r(e,n,r){if(e.revParseVerify)return e.revParseVerify(n,r);if(e.exec)try{return(await e.exec([`rev-parse`,`--verify`,n])).trim()}catch{return null}return t(`revParseVerify`)}async function i(e,n){return e.gitDir?e.gitDir(n):e.exec?(await e.exec([`rev-parse`,`--absolute-git-dir`])).trim():t(`gitDir`)}async function a(e,n){if(e.currentBranch)return e.currentBranch(n);if(e.exec)try{return(await e.exec([`symbolic-ref`,`--short`,`HEAD`])).trim()||null}catch{return null}return t(`currentBranch`)}async function o(e,n,r,i){return e.mergeBase?e.mergeBase(n,r,i):e.exec?(await e.exec([`merge-base`,n,r])).trim():t(`mergeBase`)}async function s(e,n,r,i){if(e.mergeBaseIsAncestor)return e.mergeBaseIsAncestor(n,r,i);if(e.exec)try{return await e.exec([`merge-base`,`--is-ancestor`,n,r]),!0}catch{return!1}return t(`mergeBaseIsAncestor`)}async function c(e,n,r,i){if(e.diffTree)return e.diffTree(n,r,i);if(e.exec){let t=[`diff`];return i?.binary!==!1&&t.push(`--binary`),i?.nameOnly&&t.push(`--name-only`),t.push(n,r),e.exec(t)}return t(`diffTree`)}async function l(e,n){if(e.log)return e.log(n);if(e.exec){let t=[`log`,`--format=${n.format}`];if(n.noMerges&&t.push(`--no-merges`),n.reverse&&t.push(`--reverse`),n.topoOrder&&t.push(`--topo-order`),n.limit!=null&&t.push(`-n`,String(n.limit)),n.author&&t.push(`--author=${n.author}`),n.sha)t.push(`-1`,n.sha);else if(n.range)for(let e of n.range.split(/\s+/))e&&t.push(e);return e.exec(t)}return t(`log`)}async function u(e,n){if(e.forEachRef)return e.forEachRef(n);if(e.exec){let t=[`for-each-ref`,`--format=${n.format}`];return n.sort&&t.push(`--sort=${n.sort}`),n.pointsAt&&t.push(`--points-at=${n.pointsAt}`),n.prefix&&t.push(n.prefix),n.limit!=null&&t.push(`--count=${n.limit}`),e.exec(t)}return t(`forEachRef`)}async function d(e,n){if(e.revListChildren)return e.revListChildren(n);if(e.exec){let t=[`rev-list`,`--children`];return n.reverse&&t.push(`--reverse`),t.push(n.range),f(await e.exec(t))}return t(`revListChildren`)}function f(e){let t=[];for(let n of e.split(`
1
+ import{ComposeGitPortMissingOpError as e}from"../ports/git.js";function t(t,n){throw new e(t,n)}async function n(e,n,r){return e.revParse?e.revParse(n,r):e.exec?(await e.exec([`rev-parse`,n])).trim():t(`revParse`)}async function r(e,n,r){if(e.revParseVerify)return e.revParseVerify(n,r);if(e.exec)try{return(await e.exec([`rev-parse`,`--verify`,n])).trim()}catch{return null}return t(`revParseVerify`)}async function i(e,n){return e.gitDir?e.gitDir(n):e.exec?(await e.exec([`rev-parse`,`--absolute-git-dir`])).trim():t(`gitDir`)}async function a(e,n){if(e.currentBranch)return e.currentBranch(n);if(e.exec)try{return(await e.exec([`symbolic-ref`,`--short`,`HEAD`])).trim()||null}catch{return null}return t(`currentBranch`)}async function o(e,n,r,i){return e.mergeBase?e.mergeBase(n,r,i):e.exec?(await e.exec([`merge-base`,n,r])).trim():t(`mergeBase`)}async function s(e,n,r,i){if(e.mergeBaseIsAncestor)return e.mergeBaseIsAncestor(n,r,i);if(e.exec)try{return await e.exec([`merge-base`,`--is-ancestor`,n,r]),!0}catch{return!1}return t(`mergeBaseIsAncestor`)}async function c(e,n,r,i){if(e.diffTree)return e.diffTree(n,r,i);if(e.exec){let t=[`diff`];return i?.binary!==!1&&t.push(`--binary`),i?.nameOnly&&t.push(`--name-only`),i?.diffFilter&&t.push(`--diff-filter=${i.diffFilter}`),i?.noRenames&&t.push(`--no-renames`),t.push(n,r),e.exec(t)}return t(`diffTree`)}async function l(e,n){if(e.log)return e.log(n);if(e.exec){let t=[`log`,`--format=${n.format}`];if(n.noMerges&&t.push(`--no-merges`),n.reverse&&t.push(`--reverse`),n.topoOrder&&t.push(`--topo-order`),n.limit!=null&&t.push(`-n`,String(n.limit)),n.author&&t.push(`--author=${n.author}`),n.sha)t.push(`-1`,n.sha);else if(n.range)for(let e of n.range.split(/\s+/))e&&t.push(e);return e.exec(t)}return t(`log`)}async function u(e,n){if(e.forEachRef)return e.forEachRef(n);if(e.exec){let t=[`for-each-ref`,`--format=${n.format}`];return n.sort&&t.push(`--sort=${n.sort}`),n.pointsAt&&t.push(`--points-at=${n.pointsAt}`),n.prefix&&t.push(n.prefix),n.limit!=null&&t.push(`--count=${n.limit}`),e.exec(t)}return t(`forEachRef`)}async function d(e,n){if(e.revListChildren)return e.revListChildren(n);if(e.exec){let t=[`rev-list`,`--children`];return n.reverse&&t.push(`--reverse`),t.push(n.range),f(await e.exec(t))}return t(`revListChildren`)}function f(e){let t=[];for(let n of e.split(`
2
2
  `)){let e=n.trim();if(!e)continue;let r=e.split(/\s+/);t.push({sha:r[0],children:r.slice(1)})}return t}async function p(e,n){return e.writeTree?e.writeTree(n):e.exec?(await e.exec([`write-tree`],{env:n?.indexEnv})).trim():t(`writeTree`)}async function m(e,n,r){if(e.readTree)return e.readTree(n,r);if(e.exec){await e.exec([`read-tree`,n],{env:r?.indexEnv});return}return t(`readTree`)}async function h(e,n){if(e.stageAll)return e.stageAll(n);if(e.exec){let t=[`add`];n?.intentToAdd?t.push(`-N`):n?.trackedOnly?t.push(`-u`):n?.paths||t.push(`-A`),n?.paths&&t.push(`--`,...n.paths),await e.exec(t,{env:n?.indexEnv});return}return t(`stageAll`)}async function g(e,n){return e.statusPorcelain?e.statusPorcelain(n):e.exec?e.exec([`status`,`--porcelain`]):t(`statusPorcelain`)}async function _(e,n){return e.listUntracked?e.listUntracked(n):e.exec?(await e.exec([`ls-files`,`--others`,`--exclude-standard`])).trim().split(`
3
3
  `).filter(Boolean):t(`listUntracked`)}async function v(e,n,r,i){return e.showFile?e.showFile(n,r,i):e.exec?e.exec([`show`,`${n}:${r}`]):t(`showFile`)}async function y(e,n,r){if(e.getConfig)return e.getConfig(n,r);if(e.exec)try{return(await e.exec([`config`,n])).trim()||null}catch{return null}return t(`getConfig`)}async function b(e,n,r,i,a){if(e.commitTree)return e.commitTree(n,r,i,a);if(e.exec){let t=[`commit-tree`,n];for(let e of r)t.push(`-p`,e);t.push(`-m`,i),a?.signing?.enabled&&t.push(a.signing.signingKey?`-S${a.signing.signingKey}`:`-S`);let o={};return a?.author&&(o.GIT_AUTHOR_NAME=a.author.name,o.GIT_AUTHOR_EMAIL=a.author.email,o.GIT_COMMITTER_NAME=a.author.name,o.GIT_COMMITTER_EMAIL=a.author.email,a.author.date&&(o.GIT_AUTHOR_DATE=a.author.date.toISOString())),a?.signing?.gpgProgram&&(o.GIT_GPG_PROGRAM=a.signing.gpgProgram),(await e.exec(t,{env:Object.keys(o).length?o:void 0})).trim()}return t(`commitTree`)}async function x(e,n,r){if(e.applyPatchToIndex)return e.applyPatchToIndex(n,r);if(e.exec){let t=[`apply`,`--cached`];r?.allowBinaryReplacement!==!1&&t.push(`--allow-binary-replacement`),await e.exec(t,{env:r?.indexEnv,stdin:n});return}return t(`applyPatchToIndex`)}async function S(e,n,r){if(e.applyPatchToWorkdir)return e.applyPatchToWorkdir(n,r);if(e.exec){let t=[`apply`];r?.allowBinaryReplacement!==!1&&t.push(`--allow-binary-replacement`),await e.exec(t,{stdin:n});return}return t(`applyPatchToWorkdir`)}async function C(e,n,r,i){return e.mergeTree?e.mergeTree(n,r,i):e.exec?(await e.exec([`merge-tree`,`--write-tree`,n,r])).trim().split(`
4
4
  `)[0]:t(`mergeTree`)}async function w(e,n,r,i){if(e.createBranch)return e.createBranch(n,r,i);if(e.exec){await e.exec([`branch`,n,r]);return}return t(`createBranch`)}async function T(e,n,r){if(e.deleteBranch)return e.deleteBranch(n,r);if(e.exec){await e.exec([`branch`,r?.force?`-D`:`-d`,n]);return}return t(`deleteBranch`)}async function E(e,n,r,i){if(e.updateRef)return e.updateRef(n,r,i);if(e.exec){if(i?.remove){await e.exec([`update-ref`,`-d`,n]);return}let t=[`update-ref`,n,r];i?.previousSha&&t.push(i.previousSha),await e.exec(t);return}return t(`updateRef`)}async function D(e,n,r){if(e.checkout)return e.checkout(n,r);if(e.exec){let t=[`checkout`];r?.createBranch&&t.push(`-b`),r?.detach&&t.push(`--detach`),t.push(n),r?.createBranch&&r?.startPoint&&t.push(r.startPoint),await e.exec(t);return}return t(`checkout`)}async function O(e,n,r,i){if(e.reset)return e.reset(n,r,i);if(e.exec){await e.exec([`reset`,`--${r}`,n]);return}return t(`reset`)}async function k(e,n){if(e.cleanWorkdir)return e.cleanWorkdir(n);if(e.exec){let t=`-`;n?.force!==!1&&(t+=`f`),n?.directories!==!1&&(t+=`d`),t===`-`&&(t=`-fd`),await e.exec([`clean`,t]);return}return t(`cleanWorkdir`)}async function A(e,n,r){if(e.discardWorkdirChanges)return e.discardWorkdirChanges(n,r);if(e.exec){let t=[`checkout`,`--`,...n?.length?n:[`.`]];await e.exec(t);return}return t(`discardWorkdirChanges`)}async function j(e,n){if(e.stashPush)return e.stashPush(n);if(e.exec){let t=[`stash`,`push`];n?.includeUntracked&&t.push(`--include-untracked`),n?.message&&t.push(`-m`,n.message),await e.exec(t);return}return t(`stashPush`)}const M={LANG:`C`,LC_ALL:`C`};async function N(e,n){if(e.stashPop)return e.stashPop(n);if(e.exec){let t=[`stash`,`pop`];n?.withIndex&&t.push(`--index`),n?.ref&&t.push(n.ref),await e.exec(t,{env:M});return}return t(`stashPop`)}async function P(e,n,r){if(e.stashDrop)return e.stashDrop(n,r);if(e.exec){await e.exec([`stash`,`drop`,n]);return}return t(`stashDrop`)}async function F(e,n,r){if(e.cherryPick)return e.cherryPick(n,r);if(e.exec){await e.exec([`cherry-pick`,n]);return}return t(`cherryPick`)}async function I(e,n){if(e.cherryPickAbort)return e.cherryPickAbort(n);if(e.exec){await e.exec([`cherry-pick`,`--abort`]);return}return t(`cherryPickAbort`)}export{x as applyPatchToIndex,S as applyPatchToWorkdir,D as checkout,F as cherryPick,I as cherryPickAbort,k as cleanWorkdir,b as commitTree,w as createBranch,a as currentBranch,T as deleteBranch,c as diffTree,A as discardWorkdirChanges,u as forEachRef,y as getConfig,i as gitDir,_ as listUntracked,M as localeNeutralEnv,l as log,o as mergeBase,s as mergeBaseIsAncestor,C as mergeTree,m as readTree,O as reset,d as revListChildren,n as revParse,r as revParseVerify,v as showFile,h as stageAll,P as stashDrop,N as stashPop,j as stashPush,g as statusPorcelain,E as updateRef,p as writeTree};
@@ -1 +1 @@
1
- import{commitTree as e,diffTree as t,gitDir as n,mergeTree as r,revParse as i,stageAll as a,writeTree as o}from"./port-dispatch.js";import{sha256 as s}from"./utils/crypto.js";import{withUntrackedInTempIndex as c}from"./utils/temp-index.js";import{collectSourceHashes as l,collectWorkdirPortionForRange as u}from"./diff-collector.js";import{randomUUID as d}from"node:crypto";import{copyFileSync as f,unlinkSync as p}from"node:fs";import{tmpdir as m}from"node:os";import{join as h}from"node:path";async function g(e,t,n){return t.branchSha&&t.baseSha?(n.includeStaged??!1)||n.includeUnstaged||n.includeUntracked?v(e,t,n):x(e,t):_(e,t,n)}async function _(e,n,r){let l=await i(e,`HEAD`);if(l!==n.headSha)return{valid:!1,error:`HEAD changed from ${n.headSha} to ${l}`};let u;if(u=r.includeUntracked?await c(e,async n=>(r.includeUnstaged&&await a(e,{trackedOnly:!0,indexEnv:n}),await a(e,{indexEnv:n}),t(e,l,await o(e,{indexEnv:n})))):r.includeUnstaged?await b(e,l):await t(e,l,await o(e)),s(u)!==n.diffHash){let t=await y(e,n,r);return t.length>0?{valid:!1,error:`Working directory ${t.join(` and `)} ${t.length===1?`has`:`have`} changed since the AI analysis`}:n.sourceHashes==null?{valid:!1,error:`Working directory changes have been modified since the AI analysis`}:{valid:!0}}return{valid:!0}}async function v(e,n,r){if(await i(e,n.branchSha)!==n.branchSha)return{valid:!1,error:`Branch has been modified since the AI analysis`};let a=await i(e,`HEAD`);if(a!==n.headSha)return{valid:!1,error:`HEAD changed from ${n.headSha} to ${a}`};let{finalTreeSha:o}=await u(e,a,{includeStaged:r.includeStaged??!0,includeUnstaged:r.includeUnstaged,includeUntracked:r.includeUntracked});if(s(await t(e,n.baseSha,o))!==n.diffHash){let t=await y(e,n,r);return t.length>0?{valid:!1,error:`Working directory ${t.join(` and `)} ${t.length===1?`has`:`have`} changed since the AI analysis`}:n.sourceHashes==null?{valid:!1,error:`Working directory or branch state has drifted since the AI analysis`}:{valid:!0}}return{valid:!0}}async function y(e,t,n){if(!t.sourceHashes)return[];let r=await l(e,{includeStaged:n.includeStaged,includeUnstaged:n.includeUnstaged,includeUntracked:n.includeUntracked});if(!r)return[];let i=[];return t.sourceHashes.staged!=null&&r.staged!==t.sourceHashes.staged&&i.push(`staged changes`),t.sourceHashes.unstaged!=null&&r.unstaged!==t.sourceHashes.unstaged&&i.push(`unstaged changes`),t.sourceHashes.untracked!=null&&r.untracked!==t.sourceHashes.untracked&&i.push(`untracked files`),i}async function b(e,r){let i=h(await n(e),`index`),s=h(m(),`compose-verify-idx-${d()}`);try{f(i,s);let n={GIT_INDEX_FILE:s};return await a(e,{trackedOnly:!0,indexEnv:n}),t(e,r,await o(e,{indexEnv:n}))}finally{try{p(s)}catch{}}}async function x(e,n){return await i(e,n.branchSha)===n.branchSha?s(await t(e,n.baseSha,n.branchSha))===n.diffHash?{valid:!0}:{valid:!1,error:`Branch diff has changed since the AI analysis`}:{valid:!1,error:`Branch has been modified since the AI analysis`}}async function S(e,n,r,i){return s(await t(e,n,r))===i.diffHash?{valid:!0}:{valid:!1,error:`Output diff does not match input — the commit chain does not reproduce the original changes`}}async function C(n,i,a,o){if(a.length===1)return S(n,i,a[0],o);let c=a[0];for(let t=1;t<a.length;t++)c=await e(n,await r(n,c,a[t]),[c,a[t]],`verify merge`);return s(await t(n,i,c))===o.diffHash?{valid:!0}:{valid:!1,error:`Combined diff of all branches does not match the original changes`}}export{C as verifyMultiBranchDiff,S as verifyResultingDiff,g as verifySnapshot};
1
+ import{commitTree as e,diffTree as t,mergeTree as n,revParse as r,stageAll as i,writeTree as a}from"./port-dispatch.js";import{sha256 as o}from"./utils/crypto.js";import{withUntrackedInTempIndex as s}from"./utils/temp-index.js";import{collectSourceHashes as c,collectWorkdirPortionForRange as l,computeTrackedWorktreeTreeSha as u}from"./diff-collector.js";async function d(e,t,n){return t.branchSha&&t.baseSha?n.includeStaged!=null||n.includeUnstaged||n.includeUntracked?p(e,t,n):h(e,t):f(e,t,n)}async function f(e,n,c){let l=await r(e,`HEAD`);if(l!==n.headSha)return{valid:!1,error:`HEAD changed from ${n.headSha} to ${l}`};let d;if(d=c.includeUntracked?await s(e,async n=>(c.includeUnstaged&&await i(e,{trackedOnly:!0,indexEnv:n}),await i(e,{indexEnv:n}),t(e,l,await a(e,{indexEnv:n})))):c.includeUnstaged?await t(e,l,await u(e)):await t(e,l,await a(e)),o(d)!==n.diffHash){let t=await m(e,n,c);return t.length>0?{valid:!1,error:`Working directory ${t.join(` and `)} ${t.length===1?`has`:`have`} changed since the AI analysis`}:n.sourceHashes==null?{valid:!1,error:`Working directory changes have been modified since the AI analysis`}:{valid:!0}}return{valid:!0}}async function p(e,n,i){if(await r(e,n.branchSha)!==n.branchSha)return{valid:!1,error:`Branch has been modified since the AI analysis`};let a=await r(e,`HEAD`);if(a!==n.headSha)return{valid:!1,error:`HEAD changed from ${n.headSha} to ${a}`};let{finalTreeSha:s}=await l(e,a,{includeStaged:i.includeStaged??!0,includeUnstaged:i.includeUnstaged,includeUntracked:i.includeUntracked});if(o(await t(e,n.baseSha,s))!==n.diffHash){let t=await m(e,n,i);return t.length>0?{valid:!1,error:`Working directory ${t.join(` and `)} ${t.length===1?`has`:`have`} changed since the AI analysis`}:n.sourceHashes==null?{valid:!1,error:`Working directory or branch state has drifted since the AI analysis`}:{valid:!0}}return{valid:!0}}async function m(e,t,n){if(!t.sourceHashes)return[];let r=await c(e,{includeStaged:n.includeStaged,includeUnstaged:n.includeUnstaged,includeUntracked:n.includeUntracked});if(!r)return[];let i=[];return t.sourceHashes.staged!=null&&r.staged!==t.sourceHashes.staged&&i.push(`staged changes`),t.sourceHashes.unstaged!=null&&r.unstaged!==t.sourceHashes.unstaged&&i.push(`unstaged changes`),t.sourceHashes.untracked!=null&&r.untracked!==t.sourceHashes.untracked&&i.push(`untracked files`),i}async function h(e,n){return await r(e,n.branchSha)===n.branchSha?o(await t(e,n.baseSha,n.branchSha))===n.diffHash?{valid:!0}:{valid:!1,error:`Branch diff has changed since the AI analysis`}:{valid:!1,error:`Branch has been modified since the AI analysis`}}async function g(e,n,r,i){return o(await t(e,n,r))===i.diffHash?{valid:!0}:{valid:!1,error:`Output diff does not match input — the commit chain does not reproduce the original changes`}}async function _(r,i,a,s){if(a.length===1)return g(r,i,a[0],s);let c=a[0];for(let t=1;t<a.length;t++)c=await e(r,await n(r,c,a[t]),[c,a[t]],`verify merge`);return o(await t(r,i,c))===s.diffHash?{valid:!0}:{valid:!1,error:`Combined diff of all branches does not match the original changes`}}export{_ as verifyMultiBranchDiff,g as verifyResultingDiff,d as verifySnapshot};
@@ -48,5 +48,23 @@ interface StashConflict {
48
48
  stashLabel: string;
49
49
  conflictedFiles: string[];
50
50
  }
51
+ /**
52
+ * Reported when the apply path's safety stash could not be restored (and did not conflict).
53
+ * The apply itself succeeded. The stash entry is left in place when it still exists, so the
54
+ * pre-apply WIP it holds can be recovered manually.
55
+ */
56
+ interface StashNotRestored {
57
+ stashLabel: string;
58
+ /** SHA of the stash commit, usable for recovery even if the stash ref is gone. */
59
+ stashSha: string;
60
+ /**
61
+ * - `'stash-ref-not-found'` — the stash entry could not be located (no longer in the stash
62
+ * list, or the git backend can't enumerate stashes), so no pop ran.
63
+ * - `'untracked-restore-failed'` — git aborted the pop while restoring the stash's untracked
64
+ * files (typically because a path it holds already exists in the working tree).
65
+ * - `'pop-failed'` — the pop failed for another reason without leaving conflicts.
66
+ */
67
+ reason: 'stash-ref-not-found' | 'untracked-restore-failed' | 'pop-failed';
68
+ }
51
69
  //#endregion
52
- export { ApplyMutations, SafetySnapshot, StashConflict };
70
+ export { ApplyMutations, SafetySnapshot, StashConflict, StashNotRestored };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { PhaseDetail, ProgressEvent, TokenUsage } from "./ai/types.js";
1
+ import { DuplicateHunksRepair, PhaseDetail, ProgressEvent, RepairScope, RepairedHunkClaim, TokenUsage, ValidationRepair } from "./ai/types.js";
2
2
  import { AIError, AIErrorCode } from "./errors/ai.js";
3
3
  import { GitError, GitErrorCode } from "./errors/git.js";
4
4
  import { DiffTooLargeError, TaskInputError } from "./errors/task.js";
@@ -15,7 +15,7 @@ import { QuickComposeInput, QuickComposeOutput, QuickComposeResult } from "./tas
15
15
  import { ComposeSession } from "./tasks/session.js";
16
16
  import { PartitionInput, PartitionOutput, PartitionResult, PartitionStrategy } from "./tasks/partition/types.js";
17
17
  import { ComposeContext, ComposeSource, ComposeTarget, OverrideSafetyFailures } from "./workflows/types.js";
18
- import { StashConflict } from "./git/types.js";
18
+ import { StashConflict, StashNotRestored } from "./git/types.js";
19
19
  import { ComposeBranchPlan } from "./git/apply-target.js";
20
20
  import { InteriorRefViolation, detectInteriorRefs } from "./git/safety-range.js";
21
21
  import { parseUnifiedDiff } from "./git/utils/hunk-parser.js";
@@ -33,4 +33,4 @@ import { RefinePlanInput, RefinePlanResult, RefineProgressEvent, refinePlan } fr
33
33
  import { SplitInput, SplitPlan, SplitProgressEvent, SplitResult, split } from "./workflows/split.js";
34
34
  import { UndoComposeInput, ValidateUndoComposeInput, listUndoIds, undoCompose, validateUndoCompose } from "./workflows/undo.js";
35
35
  import { UndoForceOptions } from "@gitkraken/shared-tools";
36
- export { AIError, type AIErrorCode, type AfterModelCallInfo, type AnalysisResult, type AnalyzeInput, type AnalyzeOutput, type ApplyComposePlanInput, type ApplyComposePlanResult, type ApplyPatchOptions, type BeforeModelCallInfo, type BeforePromptInfo, type BranchGroup, type BranchOrdering, type BranchPartition, type CheckoutOptions, type CleanOptions, type CommitDetail, type CommitSuggestion, type CommitTreeOptions, type ComposeApplyPlan, type ComposeBranchPlan, type ComposeContext, type ComposeDepth, type ComposeDiffStats, type ComposeGitOps, type ComposeGitPort, ComposeGitPortMissingOpError, type ComposeGroupInput, type ComposeGroupOutput, type ComposeHunk, type ComposeInput, type ComposePlan, type ComposePlanInput, type ComposePlanResult, type ComposeProgressEvent, type ComposeResult, type ComposeSession, type ComposeSource, type ComposeStrategy, type ComposeTarget, type ComposeTaskName, ComposeWorkflowError, type ComposeWorkflowErrorCode, ComposeWorkflowInputError, type Concern, type ConcernDependency, type ConventionDescriptor, DEFAULT_INPUT_TOKEN_BUDGET, type DeleteBranchOptions, DiffTooLargeError, type DiffTreeOptions, type ForEachRefOptions, type GitCommitIdentity, GitError, type GitErrorCode, type GroupInput, type GroupOutput, type GroupingResult, type GroupingStrategy, type HunkAnnotation, type HunkOrderingConstraint, type IndexScopedOptions, type InteriorRefViolation, type LogOptions, type OnAfterModelCall, type OnBeforeModelCall, type OnBeforePrompt, type OpOptions, type OrderInput, type OrderOutput, type OrderingResult, type OverrideSafetyFailures, type PartitionInput, type PartitionOutput, type PartitionResult, type PartitionStrategy, type PhaseDetail, type ProgressEvent, type PromptOverride, type QuickComposeInput, type QuickComposeOutput, type QuickComposeResult, REDACTED_HUNK_CONTENT, type RedactHunkContent, type RefinePlanInput, type RefinePlanResult, type RefineProgressEvent, type SigningConfig, type SplitInput, type SplitPlan, type SplitProgressEvent, type SplitResult, type StageAllOptions, type StashConflict, type StashPushOptions, TaskInputError, type TokenUsage, type UndoComposeInput, type UndoForceOptions, type UpdateRefOptions, type ValidateUndoComposeInput, analyze, applyComposePlan, compose, composeGroup, composePlan, defaultPhaseMessages, detectInteriorRefs, estimatedCharactersPerToken, group, isNoiseHunk, listUndoIds, order, parseUnifiedDiff, partition, quickCompose, refinePlan, split, undoCompose, validateUndoCompose, withDefaultNoiseRedaction };
36
+ export { AIError, type AIErrorCode, type AfterModelCallInfo, type AnalysisResult, type AnalyzeInput, type AnalyzeOutput, type ApplyComposePlanInput, type ApplyComposePlanResult, type ApplyPatchOptions, type BeforeModelCallInfo, type BeforePromptInfo, type BranchGroup, type BranchOrdering, type BranchPartition, type CheckoutOptions, type CleanOptions, type CommitDetail, type CommitSuggestion, type CommitTreeOptions, type ComposeApplyPlan, type ComposeBranchPlan, type ComposeContext, type ComposeDepth, type ComposeDiffStats, type ComposeGitOps, type ComposeGitPort, ComposeGitPortMissingOpError, type ComposeGroupInput, type ComposeGroupOutput, type ComposeHunk, type ComposeInput, type ComposePlan, type ComposePlanInput, type ComposePlanResult, type ComposeProgressEvent, type ComposeResult, type ComposeSession, type ComposeSource, type ComposeStrategy, type ComposeTarget, type ComposeTaskName, ComposeWorkflowError, type ComposeWorkflowErrorCode, ComposeWorkflowInputError, type Concern, type ConcernDependency, type ConventionDescriptor, DEFAULT_INPUT_TOKEN_BUDGET, type DeleteBranchOptions, DiffTooLargeError, type DiffTreeOptions, type DuplicateHunksRepair, type ForEachRefOptions, type GitCommitIdentity, GitError, type GitErrorCode, type GroupInput, type GroupOutput, type GroupingResult, type GroupingStrategy, type HunkAnnotation, type HunkOrderingConstraint, type IndexScopedOptions, type InteriorRefViolation, type LogOptions, type OnAfterModelCall, type OnBeforeModelCall, type OnBeforePrompt, type OpOptions, type OrderInput, type OrderOutput, type OrderingResult, type OverrideSafetyFailures, type PartitionInput, type PartitionOutput, type PartitionResult, type PartitionStrategy, type PhaseDetail, type ProgressEvent, type PromptOverride, type QuickComposeInput, type QuickComposeOutput, type QuickComposeResult, REDACTED_HUNK_CONTENT, type RedactHunkContent, type RefinePlanInput, type RefinePlanResult, type RefineProgressEvent, type RepairScope, type RepairedHunkClaim, type SigningConfig, type SplitInput, type SplitPlan, type SplitProgressEvent, type SplitResult, type StageAllOptions, type StashConflict, type StashNotRestored, type StashPushOptions, TaskInputError, type TokenUsage, type UndoComposeInput, type UndoForceOptions, type UpdateRefOptions, type ValidateUndoComposeInput, type ValidationRepair, analyze, applyComposePlan, compose, composeGroup, composePlan, defaultPhaseMessages, detectInteriorRefs, estimatedCharactersPerToken, group, isNoiseHunk, listUndoIds, order, parseUnifiedDiff, partition, quickCompose, refinePlan, split, undoCompose, validateUndoCompose, withDefaultNoiseRedaction };