@gitkraken/compose-tools 0.1.0 → 0.2.0
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.
- package/CHANGELOG.md +32 -1
- package/README.md +80 -14
- package/dist/errors/workflow.d.ts +16 -6
- package/dist/errors/workflow.js +1 -1
- package/dist/git/apply-branches.d.ts +0 -2
- package/dist/git/apply-branches.js +1 -1
- package/dist/git/apply-target.d.ts +13 -2
- package/dist/git/apply-target.js +1 -1
- package/dist/git/commit-chain.d.ts +27 -7
- package/dist/git/commit-chain.js +7 -5
- package/dist/git/diff-collector.d.ts +37 -10
- package/dist/git/diff-collector.js +7 -7
- package/dist/git/port-dispatch.d.ts +9 -3
- package/dist/git/port-dispatch.js +3 -3
- package/dist/git/require-exec.js +1 -1
- package/dist/git/safety.d.ts +3 -3
- package/dist/git/safety.js +1 -1
- package/dist/git/types.d.ts +6 -1
- package/dist/git/utils/conventions.d.ts +0 -1
- package/dist/git/utils/conventions.js +4 -4
- package/dist/git/utils/temp-index.d.ts +1 -1
- package/dist/git/utils/temp-index.js +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +1 -1
- package/dist/ports/git.d.ts +27 -7
- package/dist/tasks/ai-loop.d.ts +9 -15
- package/dist/tasks/ai-loop.js +2 -2
- package/dist/tasks/analyze/analyze.js +1 -1
- package/dist/tasks/analyze/types.d.ts +3 -1
- package/dist/tasks/analyze/validator.js +1 -1
- package/dist/tasks/compose-group/compose-group.js +1 -1
- package/dist/tasks/compose-group/prompt.js +27 -21
- package/dist/tasks/compose-group/types.d.ts +3 -1
- package/dist/tasks/group/group.js +1 -1
- package/dist/tasks/group/prompt.js +27 -21
- package/dist/tasks/group/types.d.ts +3 -1
- package/dist/tasks/group/validator.js +9 -4
- package/dist/tasks/order/order.js +1 -1
- package/dist/tasks/order/types.d.ts +3 -1
- package/dist/tasks/order/validator.js +1 -1
- package/dist/tasks/partition/partition.js +1 -1
- package/dist/tasks/partition/types.d.ts +3 -1
- package/dist/tasks/partition/validator.js +1 -1
- package/dist/tasks/quick-compose/prompt.js +14 -5
- package/dist/tasks/quick-compose/quick-compose.js +1 -1
- package/dist/tasks/quick-compose/types.d.ts +3 -1
- package/dist/tasks/quick-compose/validator.js +10 -2
- package/dist/utils/cancellation.d.ts +26 -0
- package/dist/utils/cancellation.js +1 -0
- package/dist/workflows/compose.d.ts +78 -10
- package/dist/workflows/compose.js +2 -2
- package/dist/workflows/split.d.ts +11 -3
- package/dist/workflows/split.js +3 -3
- package/dist/workflows/types.d.ts +5 -0
- package/dist/workflows/undo.d.ts +18 -11
- package/dist/workflows/undo.js +1 -1
- package/package.json +2 -2
- package/dist/cancellation.d.ts +0 -20
- package/dist/cancellation.js +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,12 +6,43 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/) and this
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.0] - 2026-05-11
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Adds an optional `maxTokens` field to every task input (`analyze`, `group`, `compose-group`, `order`, `partition`, `quick-compose`) and to the top-level `ComposeInput`, `ComposePlanInput`, and `SplitInput`. Forwarded to `runAiLoop` → `model.generate`. Defaults stay at 16384; bump on very large diffs whose structured output overflows the cap.
|
|
14
|
+
- Adds optional `includeWorkdir` to the `{ type: 'commit-range' }` source, with three independent flags — `includeStaged`, `includeUnstaged`, `includeUntracked`. Combines a contiguous commit range with the user's working changes in a single composed input. Constraint: `to` must equal the current branch tip (including-workdir on a mid-range slice has no defined WIP basis); the library validates this and throws `ComposeWorkflowInputError` otherwise.
|
|
15
|
+
- Adds `applyUpTo` partial-apply on `ComposeInput` and `ApplyComposePlanInput`. `{ kind: 'all' }` (default) applies the entire plan; `{ kind: 'count', count: N }` applies the first `N` commits in plan order; `{ kind: 'commitId', id }` applies up to and including the named commit. Hunks not covered by the applied prefix become uncommitted leftover in the worktree. Incompatible with `target: 'auto'`; throws `ComposeWorkflowInputError` on combination.
|
|
16
|
+
- Adds `stashLabel` on `ComposeInput` and `ApplyComposePlanInput`. Custom message for the safety stash the apply step creates when the worktree is dirty; defaults to `compose backup`. Consumers wanting their own abandoned-stash recovery UX use a stable prefix here and scan `git stash list` for it after process kills.
|
|
17
|
+
- Adds new fields on apply results — `appliedCommitIds` (the prefix of `plan.allOrderedCommits.map(c => c.id)` actually written; same as the full plan when `applyUpTo` is unset or `'all'`), `unappliedHunkIndices` (hunk indices from the plan not covered by the applied commits), and `stashConflict` (set when the post-apply stash pop hit conflicts — apply itself succeeded and the named stash is preserved for manual recovery). Type exports: `ApplyUpTo`, `StashConflict`.
|
|
18
|
+
- Adds optional `applyPatchToWorkdir` op on `ComposeGitOps` — worktree-only `git apply` (no `--cached`). The library uses it to write leftover hunks during partial apply. CLI / hybrid consumers fall through to `exec(['apply', ...])` automatically; libgit2-style consumers should provide a libgit2-backed implementation.
|
|
19
|
+
- Adds `withIndex?: boolean` on the `stashPop` op via a new `StashPopOptions` argument (`stashPop?: (opts?: StashPopOptions) => Promise<void>`). Maps to `git stash pop --index` so originally-staged content is restored as staged after the apply. The previous signature was `(opts?: OpOptions)`; `StashPopOptions` extends `OpOptions`, so existing implementations remain type-compatible. Ops-only consumers should honor `withIndex` to preserve the user's pre-stash staging.
|
|
20
|
+
- Adds optional `hunkFilter` on `ApplyComposePlanInput`. Required to match the filter passed to `composePlan` when the plan was originally created — the library re-applies the filter to the freshly collected hunks before its drift check. Plans created with a filter that are applied without the equivalent filter will fail the drift check (or, in degenerate cases, apply with the filter silently ignored, reintroducing excluded content as leftover). Consumers that serialize plans across processes should record the filter spec (e.g. excluded file names) alongside the plan and reconstruct the filter on apply.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **Breaking:** Renames the workflow error classes to compose-scoped names — `WorkflowError` → `ComposeWorkflowError`, `WorkflowInputError` → `ComposeWorkflowInputError`, `WorkflowErrorCode` → `ComposeWorkflowErrorCode`. The shape and codes are unchanged; consumers only need to update import / `instanceof` sites.
|
|
25
|
+
- **Breaking:** Renames the cancellation field on workflow and task inputs from `signal` to `cancellation` to make the contract clear: this token only flows into the AI loop, not into git operations. Mid-flight cancellation of a git op risks a half-mutated repo, so by design git operations always run to completion.
|
|
26
|
+
- **Breaking:** Removes the `signal` field from `OpOptions` and from `ApplyComposePlanInput`, `UndoComposeInput`, `ValidateUndoComposeInput`, and `listUndoIds` — those are git-only paths and run to completion.
|
|
27
|
+
- The AI loop now throws a native `AbortError` on cancellation; workflows translate it into `ComposeWorkflowError('CANCELLED')` for consumers via the new `rethrowAsWorkflowError` helper.
|
|
28
|
+
- The apply step now backs up pending workdir state via `git stash push --include-untracked` before mutating, and pops it back afterward. Conflicts during the post-apply pop surface as `result.stashConflict` rather than throwing — apply itself succeeded, the named stash is preserved, and consumers handle restoration through their own UX. Set `stashLabel` for a stable prefix consumers can recognize in `git stash list` for abandoned-stash recovery flows. Apply-time observers will see new stash entries created and (on success) immediately popped; if you've been scanning the reflog or the stash list around an apply, expect this entry pair.
|
|
29
|
+
- Tightens upfront guidance against duplicate hunks in the `group`, `compose-group`, and `quick-compose` system prompts — an explicit "Critical constraints" section spells out the four hard rules and asks the model to self-check the hunk count before producing the output tag.
|
|
30
|
+
- Sharpens the default commit-message guidance to require both a summary line and a description body separated by a blank line.
|
|
31
|
+
- Validation error feedback now shows where each duplicated hunk landed (`hunk N → branch-1/commit-2, branch-2/commit-3`) and reports the total reference count vs the expected count, so duplicate / drift / truncation patterns are obvious at a glance.
|
|
32
|
+
- JSON-parse failures in validators now include the underlying error message and a 300-character tail of the model's output, making it obvious when a response was truncated by `maxTokens` rather than malformed.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- `applyComposePlan` now correctly handles plans created with a `hunkFilter` that dropped hunks. The drift check applies the same filter to the fresh collection before comparing diff hashes, and falls back to source-state hashes (filter-agnostic) when diff hashes mismatch on any verify path. Previously this surfaced as a spurious `SAFETY_CHECK_FAILED` even when the user hadn't drifted; now requires the consumer to pass the same `hunkFilter` to `applyComposePlan` — see the "Added" entry for `hunkFilter` on `ApplyComposePlanInput`.
|
|
37
|
+
- The `rewrite-range` and `branch-replace` apply paths now capture the user's pre-apply state with a single top-level stash that runs before any branch ref movement. Previously the stash could be taken after the ref move had already shifted HEAD's effective SHA, producing a stash whose recorded parent disagreed with its content; on pop this could surface as reverse-staged changes ("staged: file added/removed" the user never staged) or `would be overwritten by merge` / `untracked working tree files would be overwritten` failures.
|
|
38
|
+
- The apply step's leftover-patch now covers only `branch`-source hunks when a stash backup was taken; WIP-source restoration (modified files via 3-way merge, originally-staged content via index restore, untracked files via `u`-commit checkout) is now left entirely to the post-apply pop. Avoids the dual-write that produced `untracked already exists` and `would be overwritten` failures in earlier versions.
|
|
39
|
+
|
|
9
40
|
## [0.1.0] - 2026-04-23
|
|
10
41
|
|
|
11
42
|
### Added
|
|
12
43
|
|
|
13
44
|
- Adds `compose(input)` — one-call sugar that runs planning and apply together; ideal when the consumer does not need to surface the plan for review
|
|
14
|
-
- Adds `composePlan(input)` and `applyComposePlan(input)` — a two-phase API for consumers
|
|
45
|
+
- Adds `composePlan(input)` and `applyComposePlan(input)` — a two-phase API for consumers that surface the plan for user review or editing before applying; `applyComposePlan` reverifies the workdir matches the plan's snapshot before mutating
|
|
15
46
|
- Adds `split(input)` — partitions an existing multi-commit branch into a stack of smaller, reviewable branches
|
|
16
47
|
- Adds `undoCompose(input)` and `validateUndoCompose(input)` — surgical reversal of only the git mutations a compose operation recorded, plus a dry-run validator that reports blockers and warnings without mutating the repo
|
|
17
48
|
- Adds `ComposeGitPort` — the git surface used by every workflow; accepts raw `exec`, a hybrid of `exec` plus targeted high-level op overrides (e.g. a signing-aware `commitTree`), or an ops-only implementation for libgit2-backed consumers; missing ops throw `ComposeGitPortMissingOpError` naming the missing op
|
package/README.md
CHANGED
|
@@ -66,7 +66,9 @@ const git: ComposeGitPort = {
|
|
|
66
66
|
|
|
67
67
|
**Libgit2-style — high-level ops only, no `exec`.** A libgit2-backed consumer implements the specific ops used by its flow. If a flow needs an op the consumer didn't provide and `exec` isn't available either, the library throws `ComposeGitPortMissingOpError` naming the missing op.
|
|
68
68
|
|
|
69
|
-
The full list of high-level ops is in the `ComposeGitOps` type; the most commonly overridden are `commitTree`, `readTree`, `writeTree`, `applyPatchToIndex`, `diffTree`, `stageAll`.
|
|
69
|
+
The full list of high-level ops is in the `ComposeGitOps` type; the most commonly overridden are `commitTree`, `readTree`, `writeTree`, `applyPatchToIndex`, `applyPatchToWorkdir`, `diffTree`, `stageAll`. `OpOptions` is currently empty — git operations are not cancellable in this library; they always run to completion to avoid leaving the repo half-mutated. Cancellation flows only into the AI loop (see [`cancellation`](#cancellation) below).
|
|
70
|
+
|
|
71
|
+
`stashPop` accepts a `StashPopOptions` argument with `withIndex?: boolean` (maps to `git stash pop --index`); ops-only consumers should honor `withIndex` to preserve the user's pre-stash staging after the apply step's safety stash is popped.
|
|
70
72
|
|
|
71
73
|
### `AiModelPort`
|
|
72
74
|
|
|
@@ -122,7 +124,7 @@ const result = await compose({
|
|
|
122
124
|
|
|
123
125
|
### `composePlan(input)` + `applyComposePlan(input)` — two-phase
|
|
124
126
|
|
|
125
|
-
Returns the plan without mutating the repo, lets the user review/edit it, then applies. This is the shape
|
|
127
|
+
Returns the plan without mutating the repo, lets the user review/edit it, then applies. This is the shape interactive review UIs use (plan → review surface → user edits → apply).
|
|
126
128
|
|
|
127
129
|
```typescript
|
|
128
130
|
import { composePlan, applyComposePlan } from '@gitkraken/compose-tools';
|
|
@@ -148,7 +150,7 @@ const applied = await applyComposePlan({
|
|
|
148
150
|
});
|
|
149
151
|
```
|
|
150
152
|
|
|
151
|
-
`applyComposePlan` reverifies the workdir matches `snapshot` before mutating. If it drifted between plan and apply, it throws `
|
|
153
|
+
`applyComposePlan` reverifies the workdir matches `snapshot` before mutating. If it drifted between plan and apply, it throws `ComposeWorkflowError('SAFETY_CHECK_FAILED')`.
|
|
152
154
|
|
|
153
155
|
### `split(input)`
|
|
154
156
|
|
|
@@ -181,7 +183,7 @@ See [Undo](#undo) below.
|
|
|
181
183
|
|---|---|
|
|
182
184
|
| `{ type: 'workdir', stagedOnly?, noUntracked? }` | Collects staged, unstaged, and untracked changes. `stagedOnly: true` limits to the index; `noUntracked: true` skips untracked files. |
|
|
183
185
|
| `{ type: 'branch', name, mergeTarget? }` | Collects `mergeBase(mergeTarget, name) → name` as one combined diff. `mergeTarget` defaults to `'main'`. |
|
|
184
|
-
| `{ type: 'commit-range', branch, from, to }` | Collects a contiguous range on `branch` with per-hunk author info. `from` is **exclusive** (parent of the first commit to include); `to` is **inclusive**. For a single-commit recompose: `from = parent(sha), to = sha`. |
|
|
186
|
+
| `{ type: 'commit-range', branch, from, to, includeWorkdir? }` | Collects a contiguous range on `branch` with per-hunk author info. `from` is **exclusive** (parent of the first commit to include); `to` is **inclusive**. For a single-commit recompose: `from = parent(sha), to = sha`. Pass `includeWorkdir: { includeStaged?, includeUnstaged?, includeUntracked? }` to combine the range with the user's working changes in a single composed input — useful for "rewrite recent commits and the WIP on top of them" flows. When set, `to` must equal the current branch tip; the library throws `ComposeWorkflowInputError` otherwise. |
|
|
185
187
|
|
|
186
188
|
## Targets
|
|
187
189
|
|
|
@@ -257,7 +259,7 @@ Consumers build their own per-call telemetry from these. No built-in telemetry i
|
|
|
257
259
|
|
|
258
260
|
### `onBeforePrompt`
|
|
259
261
|
|
|
260
|
-
Gating hook — run immediately before each `model.generate`. Returning `false` aborts the workflow with `
|
|
262
|
+
Gating hook — run immediately before each `model.generate`. Returning `false` aborts the workflow with `ComposeWorkflowError('CANCELLED')`. Use for large-prompt confirmations, cost warnings, org policy checks.
|
|
261
263
|
|
|
262
264
|
```typescript
|
|
263
265
|
onBeforePrompt: async ({ phase, attempt, tokenEstimate, charCount, messageCount }) => {
|
|
@@ -278,6 +280,8 @@ hunkFilter: async (hunks) => hunks.filter(h => !isAiIgnored(h.fileName)),
|
|
|
278
280
|
|
|
279
281
|
The filter must preserve the original `index` values on remaining hunks (only drop entries, never renumber). The library rebuilds the safety snapshot to reflect just the filtered subset, so `verifyResultingDiff` still has a correct expectation. `result.hunksFilteredCount` tells the consumer how many were dropped.
|
|
280
282
|
|
|
283
|
+
**`hunkFilter` and the two-phase `composePlan` / `applyComposePlan` flow:** `applyComposePlan` accepts the same `hunkFilter` field. **You must pass an equivalent filter at apply time** — the library re-applies it to the freshly collected hunks before the drift check, otherwise the cached (filtered) snapshot's diff hash won't match the (unfiltered) fresh diff. For consumers that serialize the plan across processes (e.g. CLI save-to-JSON workflows), record the filter spec — the list of excluded file names, the AIIgnore glob set, etc. — alongside the plan and reconstruct the function on apply. The filter function itself isn't serializable; its inputs typically are.
|
|
284
|
+
|
|
281
285
|
### `signing`
|
|
282
286
|
|
|
283
287
|
GPG signing for produced commits. Typically derived from `git config commit.gpgsign` + `user.signingkey`.
|
|
@@ -300,9 +304,56 @@ How the primary author is picked when a commit's hunks come from multiple origin
|
|
|
300
304
|
| `'first-hunk'` | Use the first hunk's author; other distinct authors become `Co-authored-by:` trailers |
|
|
301
305
|
| function | Fully custom — inspect the hunks and return the primary identity (or `undefined` to use the commit creator's identity). Co-authors are still derived from the union of remaining authors |
|
|
302
306
|
|
|
303
|
-
### `
|
|
307
|
+
### `cancellation`
|
|
308
|
+
|
|
309
|
+
Standard `AbortSignal`, but with a deliberately narrow scope: it is forwarded **only into the AI loop** — every `model.generate` call and the prompt-gate hook. Git operations (collection, snapshot verification, applying, undo manifest persistence) run to completion regardless of whether `cancellation` was aborted; mid-flight cancellation of a git op risks a half-mutated repo, so by design only the AI loop is cancellable.
|
|
310
|
+
|
|
311
|
+
On AI-loop abort, the workflow translates the native `AbortError` into `ComposeWorkflowError('CANCELLED')` so consumers only have to check for the workflow-domain shape.
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
const controller = new AbortController();
|
|
315
|
+
const result = await compose({ git, model, cancellation: controller.signal });
|
|
316
|
+
// later: controller.abort() — interrupts the next AI call; any git op already in
|
|
317
|
+
// flight finishes normally, then the workflow throws ComposeWorkflowError('CANCELLED').
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
`applyComposePlan`, `undoCompose`, `validateUndoCompose`, and `listUndoIds` do **not** accept `cancellation` — they are git-only and run to completion.
|
|
321
|
+
|
|
322
|
+
### `applyUpTo`
|
|
323
|
+
|
|
324
|
+
Apply only a prefix of the planned commits. Hunks not covered by the applied prefix become uncommitted leftover in the worktree (modified for tracked files, untracked for new files; the user's pre-apply staging on those hunks is preserved by the post-apply stash restore).
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
applyUpTo: { kind: 'all' } // default — apply the entire plan
|
|
328
|
+
applyUpTo: { kind: 'count', count: 2 } // first 2 in plan order
|
|
329
|
+
applyUpTo: { kind: 'commitId', id: 'commit-3' } // up to and including commit-3
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Incompatible with `target: 'auto'` — the library throws `ComposeWorkflowInputError` on that combination. Compatible with `head`, `branch`, `branch-replace`, and `rewrite-range`.
|
|
333
|
+
|
|
334
|
+
The result includes `appliedCommitIds` (the SHAs that were actually written, a prefix of `plan.allOrderedCommits.map(c => c.id)`) and `unappliedHunkIndices` (the hunk indices the partial apply left behind).
|
|
335
|
+
|
|
336
|
+
### `stashLabel`
|
|
337
|
+
|
|
338
|
+
Custom message for the safety stash the apply step creates when the worktree is dirty. Defaults to `compose backup`.
|
|
304
339
|
|
|
305
|
-
|
|
340
|
+
```typescript
|
|
341
|
+
stashLabel: `myapp-compose-${new Date().toISOString()}`
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
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).
|
|
345
|
+
|
|
346
|
+
### `maxTokens`
|
|
347
|
+
|
|
348
|
+
Output-token cap forwarded to every `model.generate` call in the pipeline. Defaults to `runAiLoop`'s 16384.
|
|
349
|
+
|
|
350
|
+
Bump it on very large diffs whose structured output (e.g. analyze's per-hunk annotations) overflows the cap mid-JSON. When that happens, the validator surfaces a JSON-parse failure with a `Response tail: …` preview of the last 300 characters of the model's output, so it's obvious the response was truncated rather than malformed.
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
const result = await compose({ git, model, maxTokens: 32_000 });
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
`maxTokens` is also accepted on every individual task (`analyze`, `group`, `compose-group`, `order`, `partition`, `quick-compose`) for the same reason.
|
|
306
357
|
|
|
307
358
|
### `session` (resumable workflows)
|
|
308
359
|
|
|
@@ -385,6 +436,9 @@ Every top-level entry point returns an object with these common fields:
|
|
|
385
436
|
| `commitShas` | apply paths | `{ commitId: sha }` mapping for every commit written |
|
|
386
437
|
| `undoId` | apply paths | Pass to `undoCompose()` to reverse |
|
|
387
438
|
| `partitions` / `partitionGroups` | apply paths with partitioning | Applied partition info |
|
|
439
|
+
| `appliedCommitIds` | apply paths | Plan commit IDs actually written. Same as `plan.allOrderedCommits.map(c => c.id)` for full apply; a prefix when `applyUpTo` is set. |
|
|
440
|
+
| `unappliedHunkIndices` | apply paths | Hunk indices from the plan not covered by `appliedCommitIds`. Empty for full apply. |
|
|
441
|
+
| `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. |
|
|
388
442
|
|
|
389
443
|
## Undo
|
|
390
444
|
|
|
@@ -437,7 +491,19 @@ for (const warning of validation.warnings) console.warn(`Warning: ${warning.type
|
|
|
437
491
|
await undoCompose({ git, undoId, force: true });
|
|
438
492
|
```
|
|
439
493
|
|
|
440
|
-
Downgrades the "branch has new commits" blocker to a warning.
|
|
494
|
+
Downgrades the "branch has new commits" blocker to a warning. Equivalent to `force: { branchHasNewCommits: true }`.
|
|
495
|
+
|
|
496
|
+
For finer control, pass an object form:
|
|
497
|
+
|
|
498
|
+
```typescript
|
|
499
|
+
await undoCompose({
|
|
500
|
+
git,
|
|
501
|
+
undoId,
|
|
502
|
+
force: { branchHasNewCommits: true, dirtyWorkdir: true },
|
|
503
|
+
});
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
`dirtyWorkdir: true` allows undo to proceed despite uncommitted changes — the HEAD restore step does a `reset --hard`, so anything in the worktree is clobbered. Use only when the dirty state is a known artifact of the operation being undone (e.g. a failed stash pop after a successful apply where the consumer chose to roll back) and the caller has confirmed work loss with the user. `head_sha_unreachable` and `manifest_not_found` are never force-able regardless of flags.
|
|
441
507
|
|
|
442
508
|
### List all undo IDs
|
|
443
509
|
|
|
@@ -462,21 +528,21 @@ The AI pipeline is built from composable task functions. Call them individually
|
|
|
462
528
|
| `composeGroup(input)` | Combined analyze + group in a single AI call (used by `balanced` / `quick` depths) |
|
|
463
529
|
| `quickCompose(input)` | Full pipeline (analyze + group + order + optional partition) in a single AI call |
|
|
464
530
|
|
|
465
|
-
Each task accepts an optional `session` to maintain AI conversation context (or to resume from cached `completedSteps`), and returns an updated session in its output. Each task also accepts `promptOverride`, `onBeforeModelCall`, `onAfterModelCall`, `onBeforePrompt`, and `
|
|
531
|
+
Each task accepts an optional `session` to maintain AI conversation context (or to resume from cached `completedSteps`), and returns an updated session in its output. Each task also accepts `promptOverride`, `maxTokens`, `onBeforeModelCall`, `onAfterModelCall`, `onBeforePrompt`, and `cancellation` for the same behaviors as the top-level entry points.
|
|
466
532
|
|
|
467
533
|
## Error handling
|
|
468
534
|
|
|
469
535
|
| Error | Codes |
|
|
470
536
|
|---|---|
|
|
471
537
|
| `GitError` | `NO_CHANGES`, `BRANCH_NOT_FOUND`, `OPERATION_FAILED`, `SAFETY_CHECK_FAILED`, `CHERRY_PICK_CONFLICT` |
|
|
472
|
-
| `
|
|
473
|
-
| `
|
|
538
|
+
| `ComposeWorkflowError` | `SAFETY_CHECK_FAILED` (workdir changed between plan and apply), `INTERNAL`, `CANCELLED` |
|
|
539
|
+
| `ComposeWorkflowInputError` | Invalid workflow input (missing `model`, incompatible source/target combination, etc.) |
|
|
474
540
|
| `TaskInputError` | Invalid task input |
|
|
475
541
|
| `AIError` | AI output failed schema validation after all retries. Code: `VALIDATION_EXHAUSTED` |
|
|
476
542
|
| `ComposeGitPortMissingOpError` | A flow needs a `ComposeGitOps` method the consumer didn't provide and `exec` isn't available either. Has `.op` (missing op name) and `.hint`. |
|
|
477
543
|
|
|
478
544
|
```typescript
|
|
479
|
-
import { compose, GitError,
|
|
545
|
+
import { compose, GitError, ComposeWorkflowError, AIError } from '@gitkraken/compose-tools';
|
|
480
546
|
|
|
481
547
|
try {
|
|
482
548
|
await compose({ git, model });
|
|
@@ -487,9 +553,9 @@ try {
|
|
|
487
553
|
console.error(`Conflict cherry-picking ${error.detail?.sha}`);
|
|
488
554
|
} else if (error instanceof AIError) {
|
|
489
555
|
console.error('AI could not produce valid output after retries');
|
|
490
|
-
} else if (error instanceof
|
|
556
|
+
} else if (error instanceof ComposeWorkflowError && error.code === 'CANCELLED') {
|
|
491
557
|
console.log('Operation cancelled');
|
|
492
|
-
} else if (error instanceof
|
|
558
|
+
} else if (error instanceof ComposeWorkflowError) {
|
|
493
559
|
console.error(`Workflow failed: ${error.code} — ${error.message}`);
|
|
494
560
|
}
|
|
495
561
|
}
|
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
//#region src/errors/workflow.d.ts
|
|
2
|
-
type
|
|
3
|
-
|
|
2
|
+
type ComposeWorkflowErrorCode = 'SAFETY_CHECK_FAILED' | 'INTERNAL' | 'CANCELLED';
|
|
3
|
+
/**
|
|
4
|
+
* Thrown when a workflow input is malformed or self-inconsistent (e.g. a
|
|
5
|
+
* source/target combination that cannot apply, or a session with mismatched
|
|
6
|
+
* commit-message format).
|
|
7
|
+
*/
|
|
8
|
+
declare class ComposeWorkflowInputError extends Error {
|
|
4
9
|
constructor(message: string);
|
|
5
10
|
}
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Thrown when a workflow fails at a domain checkpoint — safety verification
|
|
13
|
+
* drift, internal invariant violation, or cancellation. Carries a `code` so
|
|
14
|
+
* consumers can branch on the failure mode.
|
|
15
|
+
*/
|
|
16
|
+
declare class ComposeWorkflowError extends Error {
|
|
17
|
+
readonly code: ComposeWorkflowErrorCode;
|
|
18
|
+
constructor(code: ComposeWorkflowErrorCode, message: string, options?: {
|
|
9
19
|
cause?: unknown;
|
|
10
20
|
});
|
|
11
21
|
}
|
|
12
22
|
//#endregion
|
|
13
|
-
export {
|
|
23
|
+
export { ComposeWorkflowError, ComposeWorkflowErrorCode, ComposeWorkflowInputError };
|
package/dist/errors/workflow.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var e=class extends Error{constructor(e){super(e),this.name=`
|
|
1
|
+
var e=class extends Error{constructor(e){super(e),this.name=`ComposeWorkflowInputError`}},t=class extends Error{code;constructor(e,t,n){super(t,n),this.name=`ComposeWorkflowError`,this.code=e}};export{t as ComposeWorkflowError,e as ComposeWorkflowInputError};
|
|
@@ -8,7 +8,6 @@ interface ApplyBranchStructureParams {
|
|
|
8
8
|
branches: ApplyBranchEntry[];
|
|
9
9
|
originalBranchName?: string;
|
|
10
10
|
originalBranchSha?: string;
|
|
11
|
-
signal?: AbortSignal;
|
|
12
11
|
}
|
|
13
12
|
interface ApplyBranchEntry {
|
|
14
13
|
/** The partition(s) for this branch group. Each partition becomes a git branch. */
|
|
@@ -22,7 +21,6 @@ declare function createStackedBranches(params: {
|
|
|
22
21
|
partitions: BranchPartition[];
|
|
23
22
|
originalBranchName?: string;
|
|
24
23
|
originalBranchSha?: string;
|
|
25
|
-
signal?: AbortSignal;
|
|
26
24
|
}): Promise<void>;
|
|
27
25
|
//#endregion
|
|
28
26
|
export { ApplyBranchEntry, ApplyBranchStructureParams, applyBranchStructure, createStackedBranches };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{checkout as e,createBranch as t,currentBranch as n,deleteBranch as r}from"./port-dispatch.js";async function i(i){let{git:a,branches:o,originalBranchName:s,originalBranchSha:c
|
|
1
|
+
import{checkout as e,createBranch as t,currentBranch as n,deleteBranch as r}from"./port-dispatch.js";async function i(i){let{git:a,branches:o,originalBranchName:s,originalBranchSha:c}=i;if(s&&c){let i=await n(a)===s;i&&await e(a,c,{detach:!0}),await r(a,s,{force:!0});for(let e of o)for(let n of e.partitions){let e=n.commitIds[n.commitIds.length-1];await t(a,n.branchName,e)}if(i&&o.length>0){let t=o[o.length-1],n=t.partitions[t.partitions.length-1];await e(a,n.branchName)}}else for(let e of o)for(let n of e.partitions){let e=n.commitIds[n.commitIds.length-1];await t(a,n.branchName,e)}}async function a(e){return i({git:e.git,branches:[{partitions:e.partitions,tipSha:e.partitions[e.partitions.length-1].commitIds[e.partitions[e.partitions.length-1].commitIds.length-1]}],originalBranchName:e.originalBranchName,originalBranchSha:e.originalBranchSha})}export{i as applyBranchStructure,a as createStackedBranches};
|
|
@@ -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 { ComposeTarget } from "../workflows/types.js";
|
|
5
|
-
import { ApplyMutations, SafetySnapshot } from "./types.js";
|
|
5
|
+
import { ApplyMutations, SafetySnapshot, StashConflict } from "./types.js";
|
|
6
6
|
import { CollectionResult } from "./diff-collector.js";
|
|
7
7
|
|
|
8
8
|
//#region src/git/apply-target.d.ts
|
|
@@ -19,12 +19,22 @@ interface ApplyToTargetParams {
|
|
|
19
19
|
hunks: ComposeHunk[];
|
|
20
20
|
snapshot: SafetySnapshot;
|
|
21
21
|
collection: CollectionResult;
|
|
22
|
-
signal?: AbortSignal;
|
|
23
22
|
signing?: SigningConfig;
|
|
24
23
|
authorAttribution?: 'plurality' | 'first-hunk' | ((hunks: ComposeHunk[]) => {
|
|
25
24
|
name: string;
|
|
26
25
|
email: string;
|
|
27
26
|
} | undefined);
|
|
27
|
+
/** Stash message used by the apply helpers when stashing pending workdir state. */
|
|
28
|
+
stashLabel?: string;
|
|
29
|
+
/** Patch applied to the worktree after the chain is reset. Used to restore the unapplied
|
|
30
|
+
* hunks of a partial apply so the user's content isn't lost when their leftover hunks lived in
|
|
31
|
+
* commits being rewritten (commits-only / commit-range+workdir partial apply). */
|
|
32
|
+
leftoverPatch?: string;
|
|
33
|
+
/** Variant of {@link leftoverPatch} containing only branch-source hunks. Applied instead of
|
|
34
|
+
* the full leftover patch when stashing happens — WIP-source leftover hunks are restored
|
|
35
|
+
* by the stash pop's 3-way merge / index restore / untracked checkout, so writing them
|
|
36
|
+
* to the worktree pre-pop causes "would be overwritten" or "already exists" errors. */
|
|
37
|
+
leftoverPatchBranchOnly?: string;
|
|
28
38
|
}
|
|
29
39
|
interface ApplyToTargetResult {
|
|
30
40
|
partitions?: BranchPartition[];
|
|
@@ -33,6 +43,7 @@ interface ApplyToTargetResult {
|
|
|
33
43
|
commitShas: Record<string, string>;
|
|
34
44
|
/** Git mutations performed during apply, for undo manifest construction. */
|
|
35
45
|
mutations: ApplyMutations;
|
|
46
|
+
stashConflict?: StashConflict;
|
|
36
47
|
}
|
|
37
48
|
declare function applyToTarget(params: ApplyToTargetParams): Promise<ApplyToTargetResult>;
|
|
38
49
|
//#endregion
|
package/dist/git/apply-target.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{GitError as e}from"../errors/git.js";import{cleanWorkdir as t,discardWorkdirChanges as n,revParse as r}from"./port-dispatch.js";import{applyBranchStructure as i}from"./apply-branches.js";import{applyCommitChain as a,applyRangeRewrite as o,applyRecompose as s,buildCommitChain as c}from"./commit-chain.js";import{verifyMultiBranchDiff as l,verifyResultingDiff as u}from"./safety.js";async function d(t){let{git:n,target:l,branchPlans:d,allOrderedCommits:m,hunks:h,snapshot:v,collection:y,
|
|
1
|
+
import{GitError as e}from"../errors/git.js";import{cleanWorkdir as t,discardWorkdirChanges as n,revParse as r}from"./port-dispatch.js";import{applyBranchStructure as i}from"./apply-branches.js";import{applyCommitChain as a,applyRangeRewrite as o,applyRecompose as s,buildCommitChain as c}from"./commit-chain.js";import{verifyMultiBranchDiff as l,verifyResultingDiff as u}from"./safety.js";async function d(t){let{git:n,target:l,branchPlans:d,allOrderedCommits:m,hunks:h,snapshot:v,collection:y,signing:b,authorAttribution:x,stashLabel:S,leftoverPatch:C,leftoverPatchBranchOnly:w}=t;if(l.type===`rewrite-range`){if(!y.branchName||!y.branchSha)throw new e(`OPERATION_FAILED`,`rewrite-range target requires a source that records branchName and branchSha (branch or commit-range).`);let{headSha:t,shaMap:r}=await c({git:n,baseSha:l.rangeFrom,commits:m,hunks:h,signing:b,authorAttribution:x}),i=await o({git:n,branch:l.branch,rangeFrom:l.rangeFrom,rangeTo:l.rangeTo,newHeadSha:t,originalBranchSha:y.branchSha,preserveSuffix:l.preserveSuffix,stashLabel:S,leftoverPatch:C,leftoverPatchBranchOnly:w});return{commitShas:Object.fromEntries(r),mutations:{createdBranches:[],deletedBranches:[],movedBranches:[{name:l.branch,previousSha:y.branchSha,newSha:i.finalTipSha}]},stashConflict:i.stashConflict}}if(y.branchName&&!y.mergeBaseSha)throw new e(`OPERATION_FAILED`,`Branch source is missing mergeBaseSha — cannot determine base commit`);let T=y.mergeBaseSha??await r(n,`HEAD`);if(l.type===`auto`&&d.length>1)return f(n,T,d,h,v,y,l.preserveOriginal,b,x);if(l.type===`auto`&&d.length===1&&d[0].partitioning)return p(n,T,d[0],h,v,y,l.preserveOriginal,b,x);let{headSha:E,shaMap:D}=await c({git:n,baseSha:T,commits:m,hunks:h,signing:b,authorAttribution:x}),O=Object.fromEntries(D),k=await u(n,T,E,v);if(!k.valid)throw new e(`SAFETY_CHECK_FAILED`,`Diff verification failed: ${k.error}`);if(l.type===`branch-replace`){if(!y.branchName||!y.branchSha)throw new e(`OPERATION_FAILED`,`branch-replace target requires a branch source with branchName and branchSha`);let t=await s({git:n,branchName:y.branchName,newHeadSha:E,originalBranchSha:y.branchSha,stashLabel:S,leftoverPatch:C,leftoverPatchBranchOnly:w});return{commitShas:O,mutations:{createdBranches:[],deletedBranches:[],movedBranches:[{name:y.branchName,previousSha:y.branchSha,newSha:E}]},stashConflict:t.stashConflict}}if(l.type===`branch`){let e=await a({git:n,newHeadSha:E,branchName:l.name,untrackedIncluded:y.untrackedIncluded,stashLabel:S,leftoverPatch:C,leftoverPatchBranchOnly:w});return{commitShas:O,mutations:{createdBranches:[{name:l.name,sha:E}],deletedBranches:[],movedBranches:[]},stashConflict:e.stashConflict}}if(l.type===`auto`){let e=d[0]?.branchGroup.name||y.branchName;if(e){let t=!l.preserveOriginal&&y.branchName&&y.branchSha?{originalBranchName:y.branchName,originalBranchSha:y.branchSha}:{},r={branchName:e,title:d[0]?.branchGroup.title??``,description:d[0]?.branchGroup.description??``,commitIds:m.map(e=>e.id)};await i({git:n,branches:[{partitions:[r],tipSha:E}],...t});let a=_([r],O,t.originalBranchName,t.originalBranchSha);return{partitions:[r],commitShas:O,mutations:a}}let t=await a({git:n,newHeadSha:E,untrackedIncluded:y.untrackedIncluded,stashLabel:S});return{commitShas:O,mutations:g(),stashConflict:t.stashConflict}}let A=await a({git:n,newHeadSha:E,untrackedIncluded:y.untrackedIncluded,stashLabel:S,leftoverPatch:C,leftoverPatchBranchOnly:w});return{commitShas:O,mutations:g(),stashConflict:A.stashConflict}}async function f(r,a,o,s,u,d,f,p,g){let v=[],y=[],b=[],x=[],S={};for(let e of o){let t=m(e),{headSha:n,shaMap:i}=await c({git:r,baseSha:a,commits:t,hunks:s,signing:p,authorAttribution:g});b.push(n);for(let[e,t]of i)S[e]=t;if(e.partitioning&&e.partitioning.partitions.length>1){let t=h(e.partitioning.partitions,i);v.push(...t),y.push(t),x.push({partitions:t,tipSha:n})}else{let r={branchName:e.branchGroup.name,title:e.branchGroup.title,description:e.branchGroup.description,commitIds:t.map(e=>i.get(e.id)??e.id)};v.push(r),y.push([r]),x.push({partitions:[r],tipSha:n})}}let C=await l(r,a,b,u);if(!C.valid)throw new e(`SAFETY_CHECK_FAILED`,`Diff verification failed: ${C.error}`);let w=!f&&d.branchName&&d.branchSha?{originalBranchName:d.branchName,originalBranchSha:d.branchSha}:{};return await i({git:r,branches:x,...w}),!d.branchName&&!f&&(await n(r),d.untrackedIncluded&&await t(r)),{partitions:v,partitionGroups:y,commitShas:S,mutations:_(v,S,w.originalBranchName,w.originalBranchSha)}}async function p(r,o,s,l,d,f,p,v,y){let{headSha:b,shaMap:x}=await c({git:r,baseSha:o,commits:m(s),hunks:l,signing:v,authorAttribution:y}),S=await u(r,o,b,d);if(!S.valid)throw new e(`SAFETY_CHECK_FAILED`,`Diff verification failed: ${S.error}`);let C=h(s.partitioning.partitions,x),w=Object.fromEntries(x);if(C.length>1){let e=!p&&f.branchName&&f.branchSha?{originalBranchName:f.branchName,originalBranchSha:f.branchSha}:{};return await i({git:r,branches:[{partitions:C,tipSha:b}],...e}),!f.branchName&&!p&&(await n(r),f.untrackedIncluded&&await t(r,{force:!0,directories:!0})),{partitions:C,commitShas:w,mutations:_(C,w,e.originalBranchName,e.originalBranchSha)}}return await a({git:r,newHeadSha:b,untrackedIncluded:f.untrackedIncluded}),{partitions:C,commitShas:w,mutations:g()}}function m(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 h(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 g(){return{createdBranches:[],deletedBranches:[],movedBranches:[]}}function _(e,t,n,r){let i=e.map(e=>{let n=e.commitIds[e.commitIds.length-1];return{name:e.branchName,sha:t[n]??n}}),a=[];return n&&r&&a.push({name:n,sha:r}),{createdBranches:i,deletedBranches:a,movedBranches:[]}}export{d as applyToTarget};
|
|
@@ -1,5 +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
4
|
|
|
4
5
|
//#region src/git/commit-chain.d.ts
|
|
5
6
|
type AuthorAttributionStrategy = 'plurality' | 'first-hunk' | ((hunks: ComposeHunk[]) => {
|
|
@@ -11,7 +12,6 @@ interface BuildCommitChainParams {
|
|
|
11
12
|
baseSha: string;
|
|
12
13
|
commits: CommitSuggestion[];
|
|
13
14
|
hunks: ComposeHunk[];
|
|
14
|
-
signal?: AbortSignal;
|
|
15
15
|
signing?: SigningConfig;
|
|
16
16
|
authorAttribution?: AuthorAttributionStrategy;
|
|
17
17
|
}
|
|
@@ -31,17 +31,33 @@ interface ApplyCommitChainParams {
|
|
|
31
31
|
* conflicts between stash pop and the newly committed tree.
|
|
32
32
|
*/
|
|
33
33
|
untrackedIncluded?: boolean;
|
|
34
|
-
|
|
34
|
+
/** Stash message used when a backup stash is needed. */
|
|
35
|
+
stashLabel?: string;
|
|
36
|
+
/** Patch applied to the worktree (no staging) after the chain is reset and before the stash is popped.
|
|
37
|
+
* Used when no stash backup is taken (workdir was clean pre-apply). */
|
|
38
|
+
leftoverPatch?: string;
|
|
39
|
+
/** Branch-source-only variant of {@link leftoverPatch}. Used when a stash IS taken: WIP-source
|
|
40
|
+
* leftover (modified/untracked files the user had pre-apply) is restored by the pop's 3-way
|
|
41
|
+
* merge, index restore, and untracked checkout. Writing those hunks pre-pop causes git to
|
|
42
|
+
* reject the pop ("would be overwritten" / "already exists"). Branch-source leftover is the
|
|
43
|
+
* only category the stash has no record of, so it must still go through the patch path. */
|
|
44
|
+
leftoverPatchBranchOnly?: string;
|
|
35
45
|
}
|
|
36
|
-
|
|
46
|
+
interface ApplyChainResult {
|
|
47
|
+
stashConflict?: StashConflict;
|
|
48
|
+
}
|
|
49
|
+
declare function applyCommitChain(params: ApplyCommitChainParams): Promise<ApplyChainResult>;
|
|
37
50
|
interface ApplyRecomposeParams {
|
|
38
51
|
git: ComposeGitPort;
|
|
39
52
|
branchName: string;
|
|
40
53
|
newHeadSha: string;
|
|
41
54
|
originalBranchSha: string;
|
|
42
|
-
|
|
55
|
+
stashLabel?: string;
|
|
56
|
+
leftoverPatch?: string;
|
|
57
|
+
/** See {@link ApplyCommitChainParams.leftoverPatchBranchOnly}. */
|
|
58
|
+
leftoverPatchBranchOnly?: string;
|
|
43
59
|
}
|
|
44
|
-
declare function applyRecompose(params: ApplyRecomposeParams): Promise<
|
|
60
|
+
declare function applyRecompose(params: ApplyRecomposeParams): Promise<ApplyChainResult>;
|
|
45
61
|
interface ApplyRangeRewriteParams {
|
|
46
62
|
git: ComposeGitPort;
|
|
47
63
|
/** Branch whose tip will be updated to the result. */
|
|
@@ -56,13 +72,17 @@ interface ApplyRangeRewriteParams {
|
|
|
56
72
|
originalBranchSha: string;
|
|
57
73
|
/** When true (default), cherry-pick commits in `rangeTo..originalBranchSha` onto `newHeadSha`. */
|
|
58
74
|
preserveSuffix?: boolean;
|
|
59
|
-
|
|
75
|
+
stashLabel?: string;
|
|
76
|
+
leftoverPatch?: string;
|
|
77
|
+
/** See {@link ApplyCommitChainParams.leftoverPatchBranchOnly}. */
|
|
78
|
+
leftoverPatchBranchOnly?: string;
|
|
60
79
|
}
|
|
61
80
|
interface ApplyRangeRewriteResult {
|
|
62
81
|
/** Final branch tip SHA after range rewrite + (optional) suffix cherry-picks. */
|
|
63
82
|
finalTipSha: string;
|
|
64
83
|
/** SHAs that were successfully cherry-picked, in order applied. Empty when preserveSuffix=false or no suffix. */
|
|
65
84
|
preservedSuffixShas: string[];
|
|
85
|
+
stashConflict?: StashConflict;
|
|
66
86
|
}
|
|
67
87
|
/**
|
|
68
88
|
* Rewrite a contiguous range of commits on a branch in-place.
|
|
@@ -84,4 +104,4 @@ interface ApplyRangeRewriteResult {
|
|
|
84
104
|
*/
|
|
85
105
|
declare function applyRangeRewrite(params: ApplyRangeRewriteParams): Promise<ApplyRangeRewriteResult>;
|
|
86
106
|
//#endregion
|
|
87
|
-
export { ApplyCommitChainParams, ApplyRangeRewriteParams, ApplyRangeRewriteResult, ApplyRecomposeParams, AuthorAttributionStrategy, BuildCommitChainParams, BuildCommitChainResult, applyCommitChain, applyRangeRewrite, applyRecompose, buildCommitChain };
|
|
107
|
+
export { ApplyChainResult, ApplyCommitChainParams, ApplyRangeRewriteParams, ApplyRangeRewriteResult, ApplyRecomposeParams, AuthorAttributionStrategy, BuildCommitChainParams, BuildCommitChainResult, applyCommitChain, applyRangeRewrite, applyRecompose, buildCommitChain };
|
package/dist/git/commit-chain.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import{GitError as e}from"../errors/git.js";import{applyPatchToIndex as t,
|
|
2
|
-
`)[0]}': ${r}`,{cause:t})}for(let e of i)e.originalFileName&&e.diffHeader.includes(`rename from`)&&
|
|
3
|
-
`).filter(Boolean);
|
|
4
|
-
`)
|
|
1
|
+
import{GitError as e}from"../errors/git.js";import{applyPatchToIndex as t,applyPatchToWorkdir as n,checkout as r,cherryPick as i,cherryPickAbort as a,commitTree as o,currentBranch as s,listUntracked as c,localeNeutralEnv as l,log as u,readTree as d,reset as f,revParse as p,stageAll as m,stashDrop as h,stashPop as g,stashPush as _,statusPorcelain as v,updateRef as y,writeTree as b}from"./port-dispatch.js";import{buildPatchForCommit as x}from"./utils/patch-builder.js";import{randomUUID as S}from"node:crypto";import*as C from"node:fs";import*as w from"node:os";import*as T from"node:path";async function E(n){let{git:r,baseSha:i,commits:a,hunks:s,signing:c,authorAttribution:l=`plurality`}=n,u=T.join(w.tmpdir(),`compose-idx-${S()}`),f=i,p=new Map,m=new Set;try{for(let n of a){let i=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}),a=x(i.map(e=>m.has(e.diffHeader)?{...e,diffHeader:H(e.diffHeader,e.fileName)}:e));if(!a.trim())throw new e(`OPERATION_FAILED`,`Empty patch for commit: ${n.message}`);let h={GIT_INDEX_FILE:u};await d(r,f,{indexEnv:h});try{await t(r,a,{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 i)e.originalFileName&&e.diffHeader.includes(`rename from`)&&m.add(e.diffHeader);let g=await b(r,{indexEnv:h}),_=R(i,l),v=B(n.message,_.coAuthors),y=await o(r,g,[f],v,{signing:c,author:_.author});p.set(n.id,y),f=y}return{headSha:f,shaMap:p}}finally{V(u)}}async function D(e){let{git:t,newHeadSha:i,branchName:a,untrackedIncluded:o,stashLabel:s,leftoverPatch:c,leftoverPatchBranchOnly:l}=e;o&&await O(t);let u=s??j,d=await M(t,u),p=d==null?c:l;try{a?await r(t,a,{createBranch:!0,startPoint:i}):await f(t,i,`hard`),p&&await n(t,p)}catch(e){throw d!=null&&await P(t,d,u),e}if(d!=null){let e=await P(t,d,u);if(e.conflicted)return{stashConflict:{stashLabel:u,conflictedFiles:e.conflictedFiles}}}return{}}async function O(e){let t=await c(e);t.length>0&&await m(e,{paths:t})}async function k(e){let{git:t,branchName:r,newHeadSha:i,stashLabel:a,leftoverPatch:o,leftoverPatchBranchOnly:c}=e,l=await s(t)===r,u=a??j,d=l?await M(t,u):null,p;try{if(await y(t,`refs/heads/${r}`,i),l){let e=d==null?o:c;await f(t,i,`hard`),e&&await n(t,e)}}catch(e){throw d!=null&&await P(t,d,u),e}if(d!=null){let e=await P(t,d,u);e.conflicted&&(p={stashLabel:u,conflictedFiles:e.conflictedFiles})}return{stashConflict:p}}async function A(t){let{git:o,branch:c,rangeTo:l,newHeadSha:d,originalBranchSha:m,preserveSuffix:h=!0,stashLabel:g,leftoverPatch:_,leftoverPatchBranchOnly:v}=t,b=await s(o)===c,x=[],S=d,C=g??j,w=[];h&&l!==m&&(w=(await u(o,{format:`%H`,range:`${l}..${m}`,reverse:!0})).trim().split(`
|
|
3
|
+
`).filter(Boolean));let T=b||w.length>0?await M(o,C):null,E;try{if(w.length>0){await r(o,d,{detach:!0});for(let t of w)try{await i(o,t),x.push(t)}catch(n){throw await a(o).catch(()=>{}),new e(`CHERRY_PICK_CONFLICT`,`Could not preserve commit ${t.slice(0,8)} after rewriting the range. Cherry-pick aborted; no changes were made to the branch.`,{cause:n,detail:{conflictingCommit:t,successfullyCherryPicked:[...x]}})}S=await p(o,`HEAD`)}if(await y(o,`refs/heads/${c}`,S),b){let e=T==null?_:v;await r(o,c),await f(o,S,`hard`),e&&await n(o,e)}}catch(e){throw T!=null&&await P(o,T,C),e}if(T!=null){let e=await P(o,T,C);e.conflicted&&(E={stashLabel:C,conflictedFiles:e.conflictedFiles})}return{finalTipSha:S,preservedSuffixShas:x,stashConflict:E}}const j=`compose backup`;async function M(e,t){if(!(await v(e)).trim())return null;await _(e,{message:t,includeUntracked:!0});try{let t=(await p(e,`stash@{0}`)).trim();return t.length>0?t:null}catch{return null}}async function N(e,t){if(!e.exec)return null;let n;try{n=await e.exec([`stash`,`list`,`--format=%H %gd`],{env:l})}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 P(e,t,n){let r=await N(e,t);if(r==null)return{conflicted:!0,conflictedFiles:[]};let i;try{return await g(e,{withIndex:!0,ref:r}),{conflicted:!1,conflictedFiles:[]}}catch(e){i=e}let a=await L(e);if(a.length>0)return{conflicted:!0,conflictedFiles:a};if(F(i)){let n=await N(e,t);if(n!=null)try{await h(e,n)}catch{}return{conflicted:!1,conflictedFiles:[]}}return{conflicted:!0,conflictedFiles:[]}}function F(e){let t=I(e);return t.includes(`already exists, no checkout`)||t.includes(`could not restore untracked files from stash`)}function I(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 L(e){try{let t=await v(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 R(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=z(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(z(t.author),t.author);for(let e of t.coAuthors??[])i.set(z(e),e)}let a=r?z(r):null,o=[];for(let[e,t]of i)e!==a&&o.push(t);return{author:r,coAuthors:o}}function z(e){return`${e.name.toLowerCase()}\0${e.email.toLowerCase()}`}function B(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 V(e){try{C.unlinkSync(e)}catch{}}function H(e,t){let n=e.split(`
|
|
5
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(`
|
|
6
|
-
`)}export{
|
|
8
|
+
`)}export{D as applyCommitChain,A as applyRangeRewrite,k as applyRecompose,E as buildCommitChain};
|
|
@@ -36,16 +36,13 @@ interface CollectionResult {
|
|
|
36
36
|
mergeBaseSha?: string;
|
|
37
37
|
untrackedIncluded: boolean;
|
|
38
38
|
}
|
|
39
|
-
|
|
40
|
-
signal?: AbortSignal;
|
|
41
|
-
}
|
|
42
|
-
declare function collectFromSource(git: ComposeGitPort, source: ComposeSource, options?: CollectOptions): Promise<CollectionResult>;
|
|
39
|
+
declare function collectFromSource(git: ComposeGitPort, source: ComposeSource): Promise<CollectionResult>;
|
|
43
40
|
interface WorkingDiffOptions {
|
|
44
41
|
includeUnstaged: boolean;
|
|
45
42
|
includeUntracked: boolean;
|
|
46
43
|
}
|
|
47
|
-
declare function collectWorkingDiffs(git: ComposeGitPort, options: WorkingDiffOptions
|
|
48
|
-
declare function collectBranchDiffs(git: ComposeGitPort, branchName: string, baseBranch: string
|
|
44
|
+
declare function collectWorkingDiffs(git: ComposeGitPort, options: WorkingDiffOptions): Promise<WorkingDiffResult>;
|
|
45
|
+
declare function collectBranchDiffs(git: ComposeGitPort, branchName: string, baseBranch: string): Promise<BranchDiffResult>;
|
|
49
46
|
/**
|
|
50
47
|
* Collect hunks from a contiguous range of commits on a branch, tagging each
|
|
51
48
|
* hunk with the author of the commit that introduced it.
|
|
@@ -59,8 +56,37 @@ declare function collectBranchDiffs(git: ComposeGitPort, branchName: string, bas
|
|
|
59
56
|
* The snapshot records `branchSha = to` and `baseSha = from` so the apply
|
|
60
57
|
* phase can re-verify the range state hasn't drifted.
|
|
61
58
|
*/
|
|
62
|
-
declare function collectCommitRangeDiffs(git: ComposeGitPort, branchName: string, from: string, to: string
|
|
63
|
-
|
|
59
|
+
declare function collectCommitRangeDiffs(git: ComposeGitPort, branchName: string, from: string, to: string): Promise<BranchDiffResult>;
|
|
60
|
+
/**
|
|
61
|
+
* Collect a commit-range plus selected portions of the working directory as one combined input.
|
|
62
|
+
*
|
|
63
|
+
* Range hunks come first (with per-commit author info preserved), then workdir hunks in stable
|
|
64
|
+
* order. The safety snapshot covers only the requested portions, so unstaged drift cannot block
|
|
65
|
+
* an apply that didn't include unstaged.
|
|
66
|
+
*
|
|
67
|
+
* Requires `to` to equal both the branch tip and HEAD — the workdir basis is `to`.
|
|
68
|
+
*/
|
|
69
|
+
declare function collectCommitRangeWithWorkdir(git: ComposeGitPort, branchName: string, from: string, to: string, options: {
|
|
70
|
+
includeStaged: boolean;
|
|
71
|
+
includeUnstaged: boolean;
|
|
72
|
+
includeUntracked: boolean;
|
|
73
|
+
}): Promise<BranchDiffResult>;
|
|
74
|
+
/**
|
|
75
|
+
* Build a tree representing HEAD plus the requested workdir portions, and return the
|
|
76
|
+
* workdir-only hunks (relative to HEAD) parsed from the same tree-to-tree diff.
|
|
77
|
+
*
|
|
78
|
+
* Each portion is staged into a temp index in dependency order: staged first (HEAD→index),
|
|
79
|
+
* then unstaged (index→worktree-tracked), then untracked (intent-to-add + content).
|
|
80
|
+
*/
|
|
81
|
+
declare function collectWorkdirPortionForRange(git: ComposeGitPort, headSha: string, options: {
|
|
82
|
+
includeStaged: boolean;
|
|
83
|
+
includeUnstaged: boolean;
|
|
84
|
+
includeUntracked: boolean;
|
|
85
|
+
}): Promise<{
|
|
86
|
+
workdirHunks: ComposeHunk[];
|
|
87
|
+
finalTreeSha: string;
|
|
88
|
+
}>;
|
|
89
|
+
declare function collectCommitsForSplit(git: ComposeGitPort, branchName: string, baseBranch: string): Promise<SplitCollectionResult>;
|
|
64
90
|
/**
|
|
65
91
|
* Compute per-source content hashes alongside the authoritative combined hash.
|
|
66
92
|
*
|
|
@@ -73,8 +99,9 @@ declare function collectCommitsForSplit(git: ComposeGitPort, branchName: string,
|
|
|
73
99
|
* verify path with a generic error message.
|
|
74
100
|
*/
|
|
75
101
|
declare function collectSourceHashes(git: ComposeGitPort, options: {
|
|
102
|
+
includeStaged?: boolean;
|
|
76
103
|
includeUnstaged: boolean;
|
|
77
104
|
includeUntracked: boolean;
|
|
78
|
-
}
|
|
105
|
+
}): Promise<SafetySnapshot['sourceHashes']>;
|
|
79
106
|
//#endregion
|
|
80
|
-
export { BranchCommitInfo, BranchDiffResult,
|
|
107
|
+
export { BranchCommitInfo, BranchDiffResult, CollectionResult, SplitCollectionResult, WorkingDiffOptions, WorkingDiffResult, collectBranchDiffs, collectCommitRangeDiffs, collectCommitRangeWithWorkdir, collectCommitsForSplit, collectFromSource, collectSourceHashes, collectWorkdirPortionForRange, collectWorkingDiffs };
|