@gitkraken/compose-tools 0.2.0 → 0.3.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.
- package/CHANGELOG.md +47 -0
- package/README.md +38 -17
- package/dist/git/apply-target.d.ts +5 -9
- package/dist/git/apply-target.js +1 -1
- package/dist/git/commit-chain.d.ts +36 -48
- package/dist/git/commit-chain.js +7 -7
- package/dist/git/diff-collector.d.ts +40 -17
- package/dist/git/diff-collector.js +6 -7
- package/dist/git/index.d.ts +2 -2
- package/dist/git/index.js +1 -1
- package/dist/git/port-dispatch.d.ts +4 -2
- package/dist/git/port-dispatch.js +4 -3
- package/dist/git/reparent.d.ts +39 -0
- package/dist/git/reparent.js +5 -0
- package/dist/git/safety-range.d.ts +33 -0
- package/dist/git/safety-range.js +5 -0
- package/dist/git/types.d.ts +10 -14
- package/dist/git/utils/authorship.d.ts +22 -0
- package/dist/git/utils/authorship.js +1 -0
- package/dist/git/utils/hunk-parser.d.ts +1 -1
- package/dist/git/utils/hunk-parser.js +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/ports/git.d.ts +22 -1
- package/dist/tasks/analyze/analyze.js +1 -1
- package/dist/tasks/analyze/prompt.d.ts +2 -0
- package/dist/tasks/analyze/prompt.js +5 -5
- package/dist/tasks/analyze/types.d.ts +3 -0
- package/dist/tasks/compose-group/compose-group.js +1 -1
- package/dist/tasks/compose-group/prompt.d.ts +2 -0
- package/dist/tasks/compose-group/prompt.js +29 -29
- package/dist/tasks/compose-group/types.d.ts +3 -0
- package/dist/tasks/group/group.js +1 -1
- package/dist/tasks/group/prompt.d.ts +2 -0
- package/dist/tasks/group/prompt.js +22 -22
- package/dist/tasks/group/types.d.ts +3 -0
- package/dist/tasks/quick-compose/prompt.d.ts +2 -0
- package/dist/tasks/quick-compose/prompt.js +11 -11
- package/dist/tasks/quick-compose/quick-compose.js +1 -1
- package/dist/tasks/quick-compose/types.d.ts +3 -0
- package/dist/tasks/types.d.ts +10 -9
- package/dist/tasks/utils/redact.d.ts +14 -0
- package/dist/tasks/utils/redact.js +1 -0
- package/dist/workflows/compose.d.ts +25 -14
- package/dist/workflows/compose.js +3 -2
- package/dist/workflows/split.d.ts +3 -0
- package/dist/workflows/split.js +2 -2
- package/dist/workflows/types.d.ts +41 -18
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,53 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/) and this
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.1] - 2026-05-14
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- `applyCommitChain` no longer permanently `git add`s untracked files in the real index before stashing. The mutated index broke `git stash pop --index` on the way out — surfacing as a spurious `stashConflict` with empty `conflictedFiles`. Now relies on `git stash push --include-untracked` and the existing benign-failure detector, matching `applyRangeRewrite`.
|
|
14
|
+
- `tryPopStash` now skips the pop when the workdir's tracked tree already equals the stash's workdir tree, dropping the stash directly. Avoids spurious `stashConflict`s from `git stash pop --index`'s index-reinstate step — which can fail to apply `diff-tree HEAD <stash-index>` against a rewritten HEAD even when the new commits reproduce the WIP (e.g. split renames staged-add + unstaged-delete that compose merges into a single rename commit).
|
|
15
|
+
|
|
16
|
+
## [0.3.0] - 2026-05-13
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- Adds line-range-overlap authorship heuristic ([getAuthorAndCoAuthorsForCombinedDiffHunk](src/git/utils/authorship.ts)) for `commit-range` and `branch` sources. Walks per-commit hunks across the source range and attributes each combined-diff hunk to the source-commit author with the highest cumulative line overlap, with the remaining contributors surfaced as co-authors. Ported from GitLens.
|
|
21
|
+
- Adds descendant re-parenting on rewrite-target applies. When `rewrite-range` rewrites history (or `target: 'branch'`/`'auto'` with branch source and `!preserveOriginal`), every ref whose tip is a strict descendant of the source range's tip (commits on the source branch beyond the range, plus any other branches forked off the tip) is rebuilt onto the new last commit via `git commit-tree`, preserving each commit's tree and remapping parents through a shared `oldSha -> newSha` map. Shared downstream commits across multiple refs get one new SHA each; merges in the downstream are rebuilt structurally. Affected refs are recorded in the undo manifest's `movedRefs` field and can be restored by `undoCompose`.
|
|
22
|
+
- Adds ref-at-tip movement — refs pointing exactly at the source range's tip (other than the source branch, which is moved by its own path) are moved to the new last commit alongside the source branch. Captured in the undo manifest the same way.
|
|
23
|
+
- Adds `validateRewriteRangeSafety` — runs at `composePlan` time when the target rewrites history (`rewrite-range`, or `branch`/`auto` with branch source and `!preserveOriginal`). Rejects the operation up front when:
|
|
24
|
+
- Any interior commit of `[from, ..., predecessor(to)]` has more than one child, or a child outside the rewritten range — this catches side branches forked off interior commits and internal sub-branches that merge back inside the range.
|
|
25
|
+
- Any ref (branch, tag, or detached HEAD) points at a commit in `[from, ..., predecessor(to)]`. The source branch ref is exempted (it moves to the new tip).
|
|
26
|
+
- Adds new port ops — `mergeBaseIsAncestor` (covers `merge-base --is-ancestor`), `revListChildren` (covers `rev-list --children`), and a `pointsAt` option on `forEachRef`. Used by the rewrite-target safety check and descendant re-parenting. CLI consumers (via `exec`) need no changes; libgit2-backed consumers should implement these for libgit2-only setups.
|
|
27
|
+
- Adds `redactHunkContent` — a `(hunk) => boolean` predicate on `ComposeInput`, `ComposePlanInput`, `SplitInput`, and on each AI-task input (`AnalyzeInput`, `GroupInput`, `ComposeGroupInput`, `QuickComposeInput`). When the predicate returns true for a hunk, the AI prompt embeds a placeholder (`REDACTED_HUNK_CONTENT`) in place of the hunk's patch body. The hunk's `fileName`, `hunkHeader`, `index`, line counts, and rename metadata are still sent so the AI can group the hunk into a commit based on filename alone. The hunk's actual `content` is still used by the apply path — no data loss, just AI-visibility control. Intended for `.aiexclude`/`.aiignore`/`.cursorignore`-style consumer features that previously had to filter excluded files entirely out of compose via `hunkFilter`. Applied at every prompt that embeds hunk content (`analyze`, `group`, `compose-group`, `quick-compose`); the `order` and `partition` prompts already omit content so the predicate is a no-op there. Type exports: `RedactHunkContent`, runtime export `REDACTED_HUNK_CONTENT`.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- **Breaking**: `commit-range` source's `from` and `to` are both now **inclusive**. `from` refers to the first commit being rewritten, not its parent. Callers passing `from = parent(firstCommit)` today must pass `from = firstCommit` instead. Single-commit recompose: `from = sha, to = sha`. Internally the library computes `parent(from)` for git operations.
|
|
32
|
+
- **Breaking**: `commit-range` and `branch` sources now collect hunks from a single combined diff (`diffTree(parent(from), to)`) instead of per-commit diffs. Hunks no longer carry `sourceCommitSha`. Per-hunk authorship is heuristic (line-range overlap against per-commit history) rather than deterministic per-source-commit. All hunks share one pre-image, so reordering/splitting them across new commits no longer breaks apply — plain `git apply --cached` is correct.
|
|
33
|
+
- **Breaking**: `commit-range` source with `includeWorkdir` now produces a single combined diff `diffTree(parent(from), finalTree)` and parses all hunks from it, where `finalTree` is HEAD's tree with the requested workdir portions applied. Previously the hunks fed to the AI were concatenated from two separately-parsed diffs (`parent(from)..to` for range hunks, `HEAD..finalTree` for workdir hunks), which calibrated them against two different pre-images and could fail apply-time diff verification when the AI mixed them across commits. The hash captured by the safety snapshot now matches the bytes the AI consumed, byte-for-byte.
|
|
34
|
+
- **Breaking**: `commit-range` source with `includeWorkdir` additionally requires HEAD to be non-detached (previously only the SHA equality check was enforced).
|
|
35
|
+
- **Breaking**: All sources using existing commits now validate `from` is an ancestor of `to`. Surfaces as `ComposeWorkflowInputError` at collection time when violated.
|
|
36
|
+
- **Breaking**: Filtering (`hunkFilter`) and partial apply (`applyCommitIds`) are now accepted on more source/target combinations. The new rules (enforced by `validateFilterOrPartialApply`):
|
|
37
|
+
- `workdir` source → any legal target: always allowed.
|
|
38
|
+
- `branch`/`commit-range` source → `branch`/`auto` with `preserveOriginal: true`: always allowed (source is preserved, leftover lives in the original commits).
|
|
39
|
+
- `branch`/`commit-range` source → destructive targets (`rewrite-range`, `branch`/`auto` with `preserveOriginal: false`): allowed only when HEAD is non-detached and points at the source's tip (branch tip or `to`).
|
|
40
|
+
- **Breaking**: Removes the `preserveSuffix` option from the `rewrite-range` target. Under the new descendant re-parenting model, commits on the source branch beyond `rangeTo` are always re-parented onto the new tip — opting out is no longer supported because doing so would silently strand history.
|
|
41
|
+
- The `applyRangeRewrite` result type now includes a `refMovements` field listing every ref that was moved by the apply (source branch, re-parented descendant branches/tags, refs that pointed at the source tip), with full refnames. The previous `preservedSuffixShas` field on `ApplyRangeRewriteResult` is replaced.
|
|
42
|
+
- **Breaking**: Consolidates ref-mutation tracking on a single kind-tagged shape. `ApplyMutations` (compose) now has `createdRefs` / `deletedRefs` / `movedRefs`, each entry tagged `{ kind: 'branch' | 'tag'; name; ... }`. Previously: `createdBranches` / `deletedBranches` / `movedBranches` + optional `movedRefs` with full refnames. The same consolidation applies to `UndoManifest` (shared-tools): `createdRefs` / `deletedRefs` / `movedRefs` replace the per-namespace fields. `UndoResult` similarly: `deletedRefs` / `recreatedRefs` / `restoredRefs` (kind-tagged). The previous `UndoManifest.createdRefs` field for internal/auxiliary refs is renamed to `internalRefs`, and `UndoResult.cleanedRefs` is renamed to `cleanedInternalRefs`, so the names don't collide with the new user-visible ref fields.
|
|
43
|
+
- **Breaking**: Rewrite-target apply and safety paths now restrict their ref filters to `refs/heads/*` and `refs/tags/*`. Any other refs (remote-tracking, stash, notes, replace, internal undo metadata, third-party custom namespaces) are skipped — neither moved by the apply nor flagged by the safety check.
|
|
44
|
+
- **Breaking**: Reworks the apply-time leftover model:
|
|
45
|
+
- `target: 'branch'`/`'auto'` with workdir source no longer uses a stash dance. The new branch ref is created via `update-ref` (no checkout, no HEAD move). When `preserveOriginal: false`, the worktree is reset to the user's original HEAD and any leftover hunks are materialized as unstaged WIP. When `preserveOriginal: true` (default), the worktree is left entirely untouched. The user stays on their original branch in both cases.
|
|
46
|
+
- `target: 'branch'`/`'auto'` with branch source and `preserveOriginal: false` similarly skips the stash dance. If the user was on the source branch, HEAD migrates to the new branch and leftover is applied as unstaged.
|
|
47
|
+
- `target: 'rewrite-range'` only takes a safety stash when the user is on the source branch AND `includeWorkdir` was NOT active for the source. With `includeWorkdir` active, the WIP was already mixed into the source input, so the stash is skipped and leftover is materialized via the leftover patch as unstaged.
|
|
48
|
+
|
|
49
|
+
### Removed
|
|
50
|
+
|
|
51
|
+
- **Breaking**: Removes `sourceCommitSha` from `ComposeHunk`. The field was internal-facing and never reached the AI prompt; replaced by the heuristic authorship pass.
|
|
52
|
+
- **Breaking**: Removes the `source` field from `ComposeHunk` (previously `'staged' | 'unstaged' | 'untracked' | 'branch'`). The only downstream consumer was the leftover-patch's `'branch'`-only filter, which is no longer needed under the unified leftover model above. `parseUnifiedDiff`'s `source` argument is gone — call it with just `(diffText)` or `(diffText, startIndex)`.
|
|
53
|
+
- **Breaking**: Removes the `ApplyUpTo` type. Replaced by the simpler `applyCommitIds?: string[]` field on `ComposeInput` and `ApplyComposePlanInput` — a flat list of plan commit ids to include. Undefined means "all". The old `{ kind: 'count' }` and `{ kind: 'commitId' }` cumulative forms are gone; if you need them, derive a list by slicing `plan.allOrderedCommits` and mapping to ids.
|
|
54
|
+
- **Breaking**: Removes `leftoverPatchBranchOnly` from `ApplyCommitChainParams`, `ApplyRangeRewriteParams`, and `ApplyBranchTargetParams`. The single `leftoverPatch` is now applied conditionally based on whether a stash was taken (skipped when stashing, because the pop's 3-way merge restores pre-apply WIP); the `'branch'`-source filter that previously partitioned this is gone.
|
|
55
|
+
|
|
9
56
|
## [0.2.0] - 2026-05-11
|
|
10
57
|
|
|
11
58
|
### Added
|
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ const git: ComposeGitPort = {
|
|
|
49
49
|
};
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
The library builds every git operation it needs (diff, write-tree, commit-tree, update-ref, stash,
|
|
52
|
+
The library builds every git operation it needs (diff, write-tree, commit-tree, update-ref, stash, for-each-ref, etc.) from `exec`.
|
|
53
53
|
|
|
54
54
|
**Hybrid — `exec` plus targeted high-level ops.** Provide a high-level op when your backend has a better implementation. The library prefers the high-level op over routing through `exec`.
|
|
55
55
|
|
|
@@ -183,17 +183,26 @@ 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`
|
|
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. |
|
|
187
187
|
|
|
188
188
|
## Targets
|
|
189
189
|
|
|
190
190
|
| Target | Description |
|
|
191
191
|
|---|---|
|
|
192
192
|
| `{ type: 'head' }` | Commit onto the current branch. Default for `workdir` source. |
|
|
193
|
-
| `{ type: 'branch', name, preserveOriginal? }` | Create a single named branch. `preserveOriginal: true` keeps the source branch. |
|
|
194
|
-
| `{ type: '
|
|
195
|
-
| `{ type: '
|
|
196
|
-
|
|
193
|
+
| `{ type: 'branch', name, preserveOriginal? }` | Create a single named branch. `preserveOriginal: true` keeps the source branch / WIP; `false` cleans it up (deletes the source branch ref, or clears the committed WIP from the worktree). |
|
|
194
|
+
| `{ type: 'auto', preserveOriginal? }` | Create one branch per concern, optionally subdividing into stacked sub-branches. Same `preserveOriginal` semantics as `branch`. Default for `branch`/`commit-range` sources. |
|
|
195
|
+
| `{ type: 'rewrite-range', branch, rangeFrom, rangeTo }` | Replace a contiguous range on `branch` in-place. The new chain stacks on `parent(rangeFrom)`. Refs whose tip is a strict descendant of `rangeTo` get their downstream chains re-parented onto the new tip via `commit-tree`; refs at `rangeTo` itself move with the source branch. |
|
|
196
|
+
|
|
197
|
+
### Source/target pairing matrix
|
|
198
|
+
|
|
199
|
+
| source | head | branch | auto | rewrite-range |
|
|
200
|
+
| -------------- | :--: | :----: | :--: | :-----------: |
|
|
201
|
+
| `workdir` | ✓ | ✓ | ✓ | ✗ |
|
|
202
|
+
| `branch` | ✗ | ✓ | ✓ | ✓ |
|
|
203
|
+
| `commit-range` | ✗ | ✓ | ✓ | ✓ |
|
|
204
|
+
|
|
205
|
+
Invalid pairings throw `ComposeWorkflowInputError` at `composePlan` time.
|
|
197
206
|
|
|
198
207
|
## Options
|
|
199
208
|
|
|
@@ -282,6 +291,19 @@ The filter must preserve the original `index` values on remaining hunks (only dr
|
|
|
282
291
|
|
|
283
292
|
**`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
293
|
|
|
294
|
+
### `redactHunkContent`
|
|
295
|
+
|
|
296
|
+
Predicate that returns `true` for hunks whose patch body should be masked from AI prompts. Matching hunks keep their `fileName`, `hunkHeader`, `index`, line counts, and rename metadata — only the body is replaced with a placeholder. The apply path still uses the real content from the collection.
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
const aiExcluded = new Set(['secrets.env', 'config/keys.json']);
|
|
300
|
+
redactHunkContent: (h) => aiExcluded.has(h.fileName) || aiExcluded.has(h.originalFileName ?? h.fileName),
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Intended for `.aiexclude` / `.aiignore` / `.cursorignore`-style features where the AI should group a hunk into a commit by filename alone without ever seeing its content. Distinct from `hunkFilter`: filtered hunks are dropped from the plan entirely (and never end up in any commit), while redacted hunks stay in the plan and get applied normally — only the AI prompts hide their content.
|
|
304
|
+
|
|
305
|
+
Apply paths don't observe `redactHunkContent` (no AI runs at apply time), so it isn't required on `applyComposePlan` inputs. Per-task inputs (`analyze`, `group`, `compose-group`, `quick-compose`) accept it directly when running tasks à la carte. The `order` and `partition` tasks never embed hunk content, so the predicate is a no-op there.
|
|
306
|
+
|
|
285
307
|
### `signing`
|
|
286
308
|
|
|
287
309
|
GPG signing for produced commits. Typically derived from `git config commit.gpgsign` + `user.signingkey`.
|
|
@@ -319,19 +341,20 @@ const result = await compose({ git, model, cancellation: controller.signal });
|
|
|
319
341
|
|
|
320
342
|
`applyComposePlan`, `undoCompose`, `validateUndoCompose`, and `listUndoIds` do **not** accept `cancellation` — they are git-only and run to completion.
|
|
321
343
|
|
|
322
|
-
### `
|
|
344
|
+
### `applyCommitIds`
|
|
323
345
|
|
|
324
|
-
Apply only
|
|
346
|
+
Apply only the listed plan commit ids. Hunks belonging to commits not in the list — plus any hunks dropped by `hunkFilter` — become uncommitted leftover in the worktree (modified for tracked files, untracked for new files; index state is not preserved when the source included WIP).
|
|
325
347
|
|
|
326
348
|
```typescript
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
applyUpTo: { kind: 'commitId', id: 'commit-3' } // up to and including commit-3
|
|
349
|
+
applyCommitIds: undefined // default — apply the entire plan
|
|
350
|
+
applyCommitIds: ['commit-1', 'commit-3'] // apply only these two commits, in plan order
|
|
330
351
|
```
|
|
331
352
|
|
|
332
|
-
|
|
353
|
+
The list is interpreted as a set: order doesn't matter (the plan's own order is preserved), but every id must appear in `plan.allOrderedCommits` — unknown ids throw `ComposeWorkflowInputError`.
|
|
354
|
+
|
|
355
|
+
When `applyCommitIds` (or `hunkFilter`) is set and the target rewrites existing commits (`rewrite-range`, or `branch`/`auto` with `preserveOriginal: false`) on a `branch`/`commit-range` source, the library additionally requires HEAD to be on a branch (non-detached) and pointing at the source's tip (branch tip or range `to`). The leftover patch is materialized on top of the rewritten branch as unstaged WIP. Other source/target combinations impose no HEAD constraint.
|
|
333
356
|
|
|
334
|
-
The result includes `appliedCommitIds` (the SHAs
|
|
357
|
+
The result includes `appliedCommitIds` (the SHAs actually written) and `unappliedHunkIndices` (the hunk indices the partial apply left behind).
|
|
335
358
|
|
|
336
359
|
### `stashLabel`
|
|
337
360
|
|
|
@@ -436,7 +459,7 @@ Every top-level entry point returns an object with these common fields:
|
|
|
436
459
|
| `commitShas` | apply paths | `{ commitId: sha }` mapping for every commit written |
|
|
437
460
|
| `undoId` | apply paths | Pass to `undoCompose()` to reverse |
|
|
438
461
|
| `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
|
|
462
|
+
| `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. |
|
|
440
463
|
| `unappliedHunkIndices` | apply paths | Hunk indices from the plan not covered by `appliedCommitIds`. Empty for full apply. |
|
|
441
464
|
| `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. |
|
|
442
465
|
|
|
@@ -534,7 +557,7 @@ Each task accepts an optional `session` to maintain AI conversation context (or
|
|
|
534
557
|
|
|
535
558
|
| Error | Codes |
|
|
536
559
|
|---|---|
|
|
537
|
-
| `GitError` | `NO_CHANGES`, `BRANCH_NOT_FOUND`, `OPERATION_FAILED`, `SAFETY_CHECK_FAILED
|
|
560
|
+
| `GitError` | `NO_CHANGES`, `BRANCH_NOT_FOUND`, `OPERATION_FAILED`, `SAFETY_CHECK_FAILED` |
|
|
538
561
|
| `ComposeWorkflowError` | `SAFETY_CHECK_FAILED` (workdir changed between plan and apply), `INTERNAL`, `CANCELLED` |
|
|
539
562
|
| `ComposeWorkflowInputError` | Invalid workflow input (missing `model`, incompatible source/target combination, etc.) |
|
|
540
563
|
| `TaskInputError` | Invalid task input |
|
|
@@ -549,8 +572,6 @@ try {
|
|
|
549
572
|
} catch (error) {
|
|
550
573
|
if (error instanceof GitError && error.code === 'NO_CHANGES') {
|
|
551
574
|
console.log('Nothing to compose');
|
|
552
|
-
} else if (error instanceof GitError && error.code === 'CHERRY_PICK_CONFLICT') {
|
|
553
|
-
console.error(`Conflict cherry-picking ${error.detail?.sha}`);
|
|
554
575
|
} else if (error instanceof AIError) {
|
|
555
576
|
console.error('AI could not produce valid output after retries');
|
|
556
577
|
} else if (error instanceof ComposeWorkflowError && error.code === 'CANCELLED') {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
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
|
-
import { ComposeTarget } from "../workflows/types.js";
|
|
4
|
+
import { ComposeSource, ComposeTarget } from "../workflows/types.js";
|
|
5
5
|
import { ApplyMutations, SafetySnapshot, StashConflict } from "./types.js";
|
|
6
6
|
import { CollectionResult } from "./diff-collector.js";
|
|
7
7
|
|
|
@@ -13,6 +13,7 @@ interface ComposeBranchPlan {
|
|
|
13
13
|
}
|
|
14
14
|
interface ApplyToTargetParams {
|
|
15
15
|
git: ComposeGitPort;
|
|
16
|
+
source: ComposeSource;
|
|
16
17
|
target: ComposeTarget;
|
|
17
18
|
branchPlans: ComposeBranchPlan[];
|
|
18
19
|
allOrderedCommits: CommitSuggestion[];
|
|
@@ -26,15 +27,10 @@ interface ApplyToTargetParams {
|
|
|
26
27
|
} | undefined);
|
|
27
28
|
/** Stash message used by the apply helpers when stashing pending workdir state. */
|
|
28
29
|
stashLabel?: string;
|
|
29
|
-
/** Patch applied to the worktree
|
|
30
|
-
*
|
|
31
|
-
*
|
|
30
|
+
/** Patch applied to the worktree (unstaged) to materialize hunks not included in the chain
|
|
31
|
+
* (filter-dropped or partial-apply omitted). Lets the user keep their unselected content
|
|
32
|
+
* as WIP after a destructive apply rewrites the source. */
|
|
32
33
|
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;
|
|
38
34
|
}
|
|
39
35
|
interface ApplyToTargetResult {
|
|
40
36
|
partitions?: BranchPartition[];
|
package/dist/git/apply-target.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{GitError as e}from"../errors/git.js";import{
|
|
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,updateRef as l}from"./port-dispatch.js";import{applyBranchStructure as u}from"./apply-branches.js";import{reparentDescendants as d}from"./reparent.js";import{applyCommitChain as f,applyRangeRewrite as p,buildCommitChain as m,moveRefsAtSha as h}from"./commit-chain.js";import{verifyMultiBranchDiff as g,verifyResultingDiff as _}from"./safety.js";async function v(t){let{git:n,source:r,target:i,branchPlans:a,allOrderedCommits:o,hunks:s,snapshot:l,collection:d,signing:h,authorAttribution:g,stashLabel:v,leftoverPatch:x}=t;if(i.type===`rewrite-range`){if(!d.branchName||!d.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:u}=await m({git:n,baseSha:t,commits:o,hunks:s,signing:h,authorAttribution:g}),f=await _(n,t,a,l);if(!f.valid)throw new e(`SAFETY_CHECK_FAILED`,`Diff verification failed: ${f.error}`);let y=r.type===`commit-range`&&r.includeWorkdir!=null,b=await p({git:n,branch:i.branch,rangeFrom:i.rangeFrom,rangeTo:i.rangeTo,newHeadSha:a,originalBranchSha:d.branchSha,stashLabel:v,leftoverPatch:x,includeWorkdirActive:y});return{commitShas:Object.fromEntries(u),mutations:T(b.refMovements),stashConflict:b.stashConflict}}if(d.branchName&&!d.mergeBaseSha)throw new e(`OPERATION_FAILED`,`Branch source is missing mergeBaseSha — cannot determine base commit`);let S=d.mergeBaseSha??await c(n,`HEAD`),w=D(r,i);if(i.type===`auto`&&a.length>1)return y(n,S,a,s,l,d,w,h,g);if(i.type===`auto`&&a.length===1&&a[0].partitioning)return b(n,S,a[0],s,l,d,w,h,g);let{headSha:k,shaMap:A}=await m({git:n,baseSha:S,commits:o,hunks:s,signing:h,authorAttribution:g}),j=Object.fromEntries(A),M=await _(n,S,k,l);if(!M.valid)throw new e(`SAFETY_CHECK_FAILED`,`Diff verification failed: ${M.error}`);if(i.type===`branch`)return O({git:n,source:r,target:i,newHeadSha:k,collection:d,stashLabel:v,leftoverPatch:x,commitShas:j});if(i.type===`auto`){let e=a[0]?.branchGroup.name||d.branchName;if(e){let t=!w&&d.branchName&&d.branchSha?{originalBranchName:d.branchName,originalBranchSha:d.branchSha}:{},r={branchName:e,title:a[0]?.branchGroup.title??``,description:a[0]?.branchGroup.description??``,commitIds:o.map(e=>e.id)};await u({git:n,branches:[{partitions:[r],tipSha:k}],...t});let i=E([r],j,t.originalBranchName,t.originalBranchSha);return{partitions:[r],commitShas:j,mutations:i}}let t=await f({git:n,newHeadSha:k,stashLabel:v});return{commitShas:j,mutations:C(),stashConflict:t.stashConflict}}let N=await f({git:n,newHeadSha:k,stashLabel:v,leftoverPatch:x});return{commitShas:j,mutations:C(),stashConflict:N.stashConflict}}async function y(t,n,i,a,s,c,l,d,f){let p=[],h=[],_=[],v=[],y={};for(let e of i){let r=x(e),{headSha:i,shaMap:o}=await m({git:t,baseSha:n,commits:r,hunks:a,signing:d,authorAttribution:f});_.push(i);for(let[e,t]of o)y[e]=t;if(e.partitioning&&e.partitioning.partitions.length>1){let t=S(e.partitioning.partitions,o);p.push(...t),h.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),h.push([t]),v.push({partitions:[t],tipSha:i})}}let b=await g(t,n,_,s);if(!b.valid)throw new e(`SAFETY_CHECK_FAILED`,`Diff verification failed: ${b.error}`);let C=!l&&c.branchName&&c.branchSha?{originalBranchName:c.branchName,originalBranchSha:c.branchSha}:{};return await u({git:t,branches:v,...C}),!c.branchName&&!l&&(await o(t),c.untrackedIncluded&&await r(t)),{partitions:p,partitionGroups:h,commitShas:y,mutations:E(p,y,C.originalBranchName,C.originalBranchSha)}}async function b(t,n,i,a,s,c,l,d,p){let{headSha:h,shaMap:g}=await m({git:t,baseSha:n,commits:x(i),hunks:a,signing:d,authorAttribution:p}),v=await _(t,n,h,s);if(!v.valid)throw new e(`SAFETY_CHECK_FAILED`,`Diff verification failed: ${v.error}`);let y=S(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 u({git:t,branches:[{partitions:y,tipSha:h}],...e}),!c.branchName&&!l&&(await o(t),c.untrackedIncluded&&await r(t,{force:!0,directories:!0})),{partitions:y,commitShas:b,mutations:E(y,b,e.originalBranchName,e.originalBranchSha)}}return await f({git:t,newHeadSha:h}),{partitions:y,commitShas:b,mutations:C()}}function x(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 S(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 C(){return{createdRefs:[],deletedRefs:[],movedRefs:[]}}function w(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 T(e){let t=[];for(let n of e){let{kind:e,name:r}=w(n.refname);t.push({kind:e,name:r,previousSha:n.previousSha,newSha:n.newSha})}return{createdRefs:[],deletedRefs:[],movedRefs:t}}function E(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:[]}}function D(e,t){return t.type!==`branch`&&t.type!==`auto`||e.type===`commit-range`?!0:t.preserveOriginal===!0}async function O(r){let{git:o,source:u,target:f,newHeadSha:p,collection:m,leftoverPatch:g,commitShas:_}=r,v=D(u,f),y=u.type===`branch`&&!v&&m.branchName!=null&&m.branchSha!=null;if(y&&f.name===m.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 b={createdRefs:[{kind:`branch`,name:f.name,sha:p}],deletedRefs:[],movedRefs:[]},x=await i(o),S=await c(o,`HEAD`);if(await l(o,`refs/heads/${f.name}`,p),y){let e=m.branchSha,r=m.branchName,i=await d({git:o,oldToSha:e,newTopSha:p,sourceBranchName:void 0});for(let e of i.reparentedRefs){let{kind:t,name:n}=w(e.refname);b.movedRefs.push({kind:t,name:n,previousSha:e.oldSha,newSha:e.newSha})}let c=await h(o,e,p,`refs/heads/${r}`);for(let e of c){let{kind:t,name:n}=w(e.refname);b.movedRefs.push({kind:t,name:n,previousSha:e.previousSha,newSha:e.newSha})}x===r&&(await n(o,f.name),await s(o,p,`hard`),g&&await t(o,g)),await a(o,r,{force:!0}),b.deletedRefs.push({kind:`branch`,name:r,sha:e})}else u.type===`workdir`&&!v&&(await s(o,S,`hard`),g&&await t(o,g));return{commitShas:_,mutations:b}}export{v as applyToTarget};
|
|
@@ -25,39 +25,18 @@ interface ApplyCommitChainParams {
|
|
|
25
25
|
git: ComposeGitPort;
|
|
26
26
|
newHeadSha: string;
|
|
27
27
|
branchName?: string;
|
|
28
|
-
/**
|
|
29
|
-
* Whether untracked files were included in the compose.
|
|
30
|
-
* When true, untracked files are staged before stashing to avoid
|
|
31
|
-
* conflicts between stash pop and the newly committed tree.
|
|
32
|
-
*/
|
|
33
|
-
untrackedIncluded?: boolean;
|
|
34
28
|
/** Stash message used when a backup stash is needed. */
|
|
35
29
|
stashLabel?: string;
|
|
36
30
|
/** Patch applied to the worktree (no staging) after the chain is reset and before the stash is popped.
|
|
37
|
-
*
|
|
31
|
+
* Materializes the hunks not included in the chain (filter-dropped / partial-apply omitted) as
|
|
32
|
+
* unstaged WIP, so the user's unselected content isn't lost when a destructive apply rewrites
|
|
33
|
+
* the source. */
|
|
38
34
|
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;
|
|
45
35
|
}
|
|
46
36
|
interface ApplyChainResult {
|
|
47
37
|
stashConflict?: StashConflict;
|
|
48
38
|
}
|
|
49
39
|
declare function applyCommitChain(params: ApplyCommitChainParams): Promise<ApplyChainResult>;
|
|
50
|
-
interface ApplyRecomposeParams {
|
|
51
|
-
git: ComposeGitPort;
|
|
52
|
-
branchName: string;
|
|
53
|
-
newHeadSha: string;
|
|
54
|
-
originalBranchSha: string;
|
|
55
|
-
stashLabel?: string;
|
|
56
|
-
leftoverPatch?: string;
|
|
57
|
-
/** See {@link ApplyCommitChainParams.leftoverPatchBranchOnly}. */
|
|
58
|
-
leftoverPatchBranchOnly?: string;
|
|
59
|
-
}
|
|
60
|
-
declare function applyRecompose(params: ApplyRecomposeParams): Promise<ApplyChainResult>;
|
|
61
40
|
interface ApplyRangeRewriteParams {
|
|
62
41
|
git: ComposeGitPort;
|
|
63
42
|
/** Branch whose tip will be updated to the result. */
|
|
@@ -70,38 +49,47 @@ interface ApplyRangeRewriteParams {
|
|
|
70
49
|
newHeadSha: string;
|
|
71
50
|
/** Resolved SHA: branch tip before the operation started. */
|
|
72
51
|
originalBranchSha: string;
|
|
73
|
-
/** When true (default), cherry-pick commits in `rangeTo..originalBranchSha` onto `newHeadSha`. */
|
|
74
|
-
preserveSuffix?: boolean;
|
|
75
52
|
stashLabel?: string;
|
|
76
53
|
leftoverPatch?: string;
|
|
77
|
-
/**
|
|
78
|
-
|
|
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;
|
|
79
58
|
}
|
|
80
59
|
interface ApplyRangeRewriteResult {
|
|
81
|
-
/** Final branch tip SHA after
|
|
60
|
+
/** Final source-branch tip SHA after descendant re-parenting and any ref-at-tip moves. */
|
|
82
61
|
finalTipSha: string;
|
|
83
|
-
/**
|
|
84
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Every ref movement performed by this apply: the source branch move, every
|
|
64
|
+
* descendant ref re-parented onto the new tip, and every ref that pointed at
|
|
65
|
+
* the old range tip and was moved to the new tip. Names are full refnames
|
|
66
|
+
* (e.g. `refs/heads/main`, `refs/tags/v1.0.0`). The caller categorizes these
|
|
67
|
+
* into branch vs tag for the undo manifest.
|
|
68
|
+
*/
|
|
69
|
+
refMovements: {
|
|
70
|
+
refname: string;
|
|
71
|
+
previousSha: string;
|
|
72
|
+
newSha: string;
|
|
73
|
+
}[];
|
|
85
74
|
stashConflict?: StashConflict;
|
|
86
75
|
}
|
|
87
76
|
/**
|
|
88
|
-
* Rewrite a contiguous range of commits on a branch in-place.
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* work done later on the branch is preserved.
|
|
94
|
-
*
|
|
95
|
-
* Behavior:
|
|
96
|
-
* 1. Stash the user's workdir if dirty (restored in the caller via finally).
|
|
97
|
-
* 2. If `preserveSuffix` (default true) and the range didn't reach the branch
|
|
98
|
-
* tip, cherry-pick every commit in `rangeTo..originalBranchSha` onto
|
|
99
|
-
* `newHeadSha`. On conflict, abort the cherry-pick and throw
|
|
100
|
-
* `GitError('CHERRY_PICK_CONFLICT', …)` — repo state is left safe.
|
|
101
|
-
* 3. `update-ref` the branch to the final tip.
|
|
102
|
-
* 4. If the user was on the branch, `reset --hard` to the final tip so their
|
|
103
|
-
* checked-out state follows the rewrite.
|
|
77
|
+
* Rewrite a contiguous range of commits on a branch in-place. Given a new commit chain
|
|
78
|
+
* already built on top of `parent(rangeFrom)` (at `newHeadSha`), re-parents every ref whose
|
|
79
|
+
* tip is a descendant of `rangeTo` onto the new tip (via `reparentDescendants`), moves the
|
|
80
|
+
* source branch, and moves any other refs that pointed at `rangeTo`. If the user was on the
|
|
81
|
+
* branch, their HEAD and worktree are reset to the final tip.
|
|
104
82
|
*/
|
|
105
83
|
declare function applyRangeRewrite(params: ApplyRangeRewriteParams): Promise<ApplyRangeRewriteResult>;
|
|
84
|
+
/**
|
|
85
|
+
* Move every `refs/heads/*` and `refs/tags/*` ref pointing at `oldSha` to `newSha`, except
|
|
86
|
+
* for `excludeRefname` (typically the source branch, handled by the caller). Returns the
|
|
87
|
+
* moves for the undo manifest.
|
|
88
|
+
*/
|
|
89
|
+
declare function moveRefsAtSha(git: ComposeGitPort, oldSha: string, newSha: string, excludeRefname?: string): Promise<{
|
|
90
|
+
refname: string;
|
|
91
|
+
previousSha: string;
|
|
92
|
+
newSha: string;
|
|
93
|
+
}[]>;
|
|
106
94
|
//#endregion
|
|
107
|
-
export { ApplyChainResult, ApplyCommitChainParams, ApplyRangeRewriteParams, ApplyRangeRewriteResult,
|
|
95
|
+
export { ApplyChainResult, ApplyCommitChainParams, ApplyRangeRewriteParams, ApplyRangeRewriteResult, AuthorAttributionStrategy, BuildCommitChainParams, BuildCommitChainResult, applyCommitChain, applyRangeRewrite, buildCommitChain, moveRefsAtSha };
|
package/dist/git/commit-chain.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import{GitError as e}from"../errors/git.js";import{applyPatchToIndex as t,applyPatchToWorkdir as n,checkout as r,
|
|
2
|
-
`)[0]}': ${r}`,{cause:t})}for(let e of
|
|
3
|
-
`).
|
|
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
|
|
5
|
-
`))/^(UU|AA|DD|AU|UA|UD|DU) /.test(e)&&n.push(e.slice(3).trim());return n}catch{return[]}}function
|
|
6
|
-
`);return`${e.trimEnd()}\n\n${n}\n`}function
|
|
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,forEachRef as o,localeNeutralEnv as s,readTree as c,reset as l,revParse as u,stageAll as d,stashDrop as f,stashPop as p,stashPush as m,statusPorcelain as h,updateRef as g,writeTree as _}from"./port-dispatch.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:s,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=s.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 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 _(r,{indexEnv:h}),v=I(a,u),b=R(n.message,v.coAuthors),x=await i(r,g,[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,c=o??O,u=await k(t,c);try{a?await r(t,a,{createBranch:!0,startPoint:i}):await l(t,i,`hard`),s&&await n(t,s)}catch(e){throw u!=null&&await j(t,u,c),e}if(u!=null){let e=await j(t,u,c);if(e.conflicted)return{stashConflict:{stashLabel:c,conflictedFiles:e.conflictedFiles}}}return{}}async function E(e){let{git:t,branch:i,rangeTo:o,newHeadSha:s,originalBranchSha:c,stashLabel:u,leftoverPatch:d,includeWorkdirActive:f}=e,p=await a(t)===i,m=u??O,h=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 g(t,`refs/heads/${i}`,s),b.push({refname:`refs/heads/${i}`,previousSha:c,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 l(t,y,`hard`),h==null&&d&&await n(t,d))}catch(e){throw h!=null&&await j(t,h,m),e}if(h!=null){let e=await j(t,h,m);e.conflicted&&(_={stashLabel:m,conflictedFiles:e.conflictedFiles})}return{finalTipSha:y,refMovements:b,stashConflict:_}}async function D(e,t,n,r){let i=await o(e,{format:`%(refname) %(objectname)`,pointsAt:t}),a=[];for(let o of i.split(`
|
|
3
|
+
`)){let i=o.trim();if(!i)continue;let s=i.indexOf(` `);if(s<0)continue;let c=i.slice(0,s),l=i.slice(s+1).trim();c!==r&&(!c.startsWith(`refs/heads/`)&&!c.startsWith(`refs/tags/`)||l===t&&(await g(e,c,n),a.push({refname:c,previousSha:t,newSha:n})))}return a}const O=`compose backup`;async function k(e,t){if(!(await h(e)).trim())return null;await m(e,{message:t,includeUntracked:!0});try{let t=(await u(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: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 j(e,t,n){try{let n=(await u(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 f(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 p(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 f(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 c(e,`HEAD`,{indexEnv:n}),await d(e,{indexEnv:n}),(await _(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 h(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 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(`
|
|
6
|
+
`);return`${e.trimEnd()}\n\n${n}\n`}function z(e){try{x.unlinkSync(e)}catch{}}function B(e,t){let n=e.split(`
|
|
7
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(`
|
|
8
|
-
`)}export{
|
|
8
|
+
`)}export{T as applyCommitChain,E as applyRangeRewrite,w as buildCommitChain,D as moveRefsAtSha};
|
|
@@ -33,6 +33,11 @@ interface CollectionResult {
|
|
|
33
33
|
branchName?: string;
|
|
34
34
|
baseBranch?: string;
|
|
35
35
|
branchSha?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Base SHA for the recomposed chain — `mergeBase(mergeTarget, branch)` for `branch`
|
|
38
|
+
* sources, `parent(from)` for `commit-range` sources. Named `mergeBaseSha` for
|
|
39
|
+
* historical reasons but really means "parent of the first rewritten commit".
|
|
40
|
+
*/
|
|
36
41
|
mergeBaseSha?: string;
|
|
37
42
|
untrackedIncluded: boolean;
|
|
38
43
|
}
|
|
@@ -42,29 +47,49 @@ interface WorkingDiffOptions {
|
|
|
42
47
|
includeUntracked: boolean;
|
|
43
48
|
}
|
|
44
49
|
declare function collectWorkingDiffs(git: ComposeGitPort, options: WorkingDiffOptions): Promise<WorkingDiffResult>;
|
|
50
|
+
/**
|
|
51
|
+
* Branch source — thin adapter over {@link collectCombinedDiffRange}. The merge-base with
|
|
52
|
+
* `baseBranch` plays the role of `parent(from)`.
|
|
53
|
+
*/
|
|
45
54
|
declare function collectBranchDiffs(git: ComposeGitPort, branchName: string, baseBranch: string): Promise<BranchDiffResult>;
|
|
46
55
|
/**
|
|
47
|
-
* Collect hunks from a contiguous range of commits on a branch
|
|
48
|
-
*
|
|
56
|
+
* Collect hunks from a contiguous range of commits on a branch via a single
|
|
57
|
+
* combined diff. Both `from` and `to` are **inclusive** — they name the first
|
|
58
|
+
* and last commits being rewritten. For a single-commit recompose:
|
|
59
|
+
* `from = sha, to = sha`. The library computes `parent(from)` internally and
|
|
60
|
+
* delegates to {@link collectCombinedDiffRange}.
|
|
49
61
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* (ordered by commit, then by file/hunk within each commit).
|
|
62
|
+
* The returned hunks come from `diffTree(parent(from), to)`, so every hunk
|
|
63
|
+
* shares one pre-image (the tree at `parent(from)`) and the AI can freely
|
|
64
|
+
* reorder/split them across new commits. Per-hunk authorship is attributed
|
|
65
|
+
* via a line-range-overlap heuristic against per-commit hunks in the range.
|
|
55
66
|
*
|
|
56
|
-
* The snapshot records `branchSha = to` and `baseSha = from` so the
|
|
57
|
-
* phase can re-verify the range
|
|
67
|
+
* The snapshot records `branchSha = to` and `baseSha = parent(from)` so the
|
|
68
|
+
* apply phase can re-verify the range hasn't drifted.
|
|
58
69
|
*/
|
|
59
70
|
declare function collectCommitRangeDiffs(git: ComposeGitPort, branchName: string, from: string, to: string): Promise<BranchDiffResult>;
|
|
71
|
+
/**
|
|
72
|
+
* Shared collection path for `commit-range` and `branch` sources. Returns the combined diff
|
|
73
|
+
* `diffTree(parentOfFromSha, toSha)` parsed into hunks, with per-hunk authorship attributed
|
|
74
|
+
* via a line-range-overlap heuristic against a per-commit side-table.
|
|
75
|
+
*
|
|
76
|
+
* Merge commits walk their first-parent diff. The incoming content from the other parent is
|
|
77
|
+
* already in `parentOfFromSha`'s tree (for `branch` sources) and won't match the heuristic;
|
|
78
|
+
* conflict-resolution edits at the merge DO appear in the combined diff and correctly
|
|
79
|
+
* attribute to the merge committer.
|
|
80
|
+
*/
|
|
81
|
+
declare function collectCombinedDiffRange(git: ComposeGitPort, parentOfFromSha: string, toSha: string): Promise<BranchDiffResult>;
|
|
60
82
|
/**
|
|
61
83
|
* Collect a commit-range plus selected portions of the working directory as one combined input.
|
|
62
84
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
85
|
+
* All hunks come from a single combined diff `diffTree(parent(from), finalTree)`, where
|
|
86
|
+
* `finalTree` is HEAD's tree with the requested workdir portions applied. The safety
|
|
87
|
+
* snapshot hashes that same combined diff — the bytes the AI sees match the bytes the
|
|
88
|
+
* apply phase verifies against.
|
|
66
89
|
*
|
|
67
|
-
* Requires `to` to equal both the branch tip and HEAD
|
|
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.
|
|
68
93
|
*/
|
|
69
94
|
declare function collectCommitRangeWithWorkdir(git: ComposeGitPort, branchName: string, from: string, to: string, options: {
|
|
70
95
|
includeStaged: boolean;
|
|
@@ -72,8 +97,7 @@ declare function collectCommitRangeWithWorkdir(git: ComposeGitPort, branchName:
|
|
|
72
97
|
includeUntracked: boolean;
|
|
73
98
|
}): Promise<BranchDiffResult>;
|
|
74
99
|
/**
|
|
75
|
-
* Build a tree representing HEAD plus the requested workdir portions, and return
|
|
76
|
-
* workdir-only hunks (relative to HEAD) parsed from the same tree-to-tree diff.
|
|
100
|
+
* Build a tree representing HEAD plus the requested workdir portions, and return its SHA.
|
|
77
101
|
*
|
|
78
102
|
* Each portion is staged into a temp index in dependency order: staged first (HEAD→index),
|
|
79
103
|
* then unstaged (index→worktree-tracked), then untracked (intent-to-add + content).
|
|
@@ -83,7 +107,6 @@ declare function collectWorkdirPortionForRange(git: ComposeGitPort, headSha: str
|
|
|
83
107
|
includeUnstaged: boolean;
|
|
84
108
|
includeUntracked: boolean;
|
|
85
109
|
}): Promise<{
|
|
86
|
-
workdirHunks: ComposeHunk[];
|
|
87
110
|
finalTreeSha: string;
|
|
88
111
|
}>;
|
|
89
112
|
declare function collectCommitsForSplit(git: ComposeGitPort, branchName: string, baseBranch: string): Promise<SplitCollectionResult>;
|
|
@@ -104,4 +127,4 @@ declare function collectSourceHashes(git: ComposeGitPort, options: {
|
|
|
104
127
|
includeUntracked: boolean;
|
|
105
128
|
}): Promise<SafetySnapshot['sourceHashes']>;
|
|
106
129
|
//#endregion
|
|
107
|
-
export { BranchCommitInfo, BranchDiffResult, CollectionResult, SplitCollectionResult, WorkingDiffOptions, WorkingDiffResult, collectBranchDiffs, collectCommitRangeDiffs, collectCommitRangeWithWorkdir, collectCommitsForSplit, collectFromSource, collectSourceHashes, collectWorkdirPortionForRange, collectWorkingDiffs };
|
|
130
|
+
export { BranchCommitInfo, BranchDiffResult, CollectionResult, SplitCollectionResult, WorkingDiffOptions, WorkingDiffResult, collectBranchDiffs, collectCombinedDiffRange, collectCommitRangeDiffs, collectCommitRangeWithWorkdir, collectCommitsForSplit, collectFromSource, collectSourceHashes, collectWorkdirPortionForRange, collectWorkingDiffs };
|
|
@@ -1,8 +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,
|
|
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{getAuthorAndCoAuthorsForCombinedDiffHunk as g}from"./utils/authorship.js";import{sha256 as _}from"./utils/crypto.js";import{parseUnifiedDiff as v}from"./utils/hunk-parser.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:v(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:v(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=v(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}=g(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=v(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
|
|
4
|
-
`).filter(Boolean),
|
|
5
|
-
`).filter(Boolean);
|
|
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=
|
|
7
|
-
`))}else r.untracked=
|
|
8
|
-
`);n.length>=2&&t.push({sha:n[0],message:n[1]})}return t}export{O as collectBranchDiffs,k as collectCommitRangeDiffs,j as collectCommitRangeWithWorkdir,N as collectCommitsForSplit,S as collectFromSource,I as collectSourceHashes,M as collectWorkdirPortionForRange,C as collectWorkingDiffs};
|
|
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:_}=await R(e,m,s),y=await a(e,h,_);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=v(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}=g(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(`
|
|
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:_(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=_(await e.exec([`diff`,`--cached`,`--binary`,`HEAD`]))}catch{}if(t.includeUnstaged)try{r.unstaged=_(await e.exec([`diff`,`--binary`]))}catch{}if(t.includeUntracked)try{let t=(await e.exec([`ls-files`,`--others`,`--exclude-standard`])).trim().split(`
|
|
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=_(n.join(`
|
|
7
|
+
`))}else r.untracked=_(``)}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};
|