@gitkraken/compose-tools 0.2.0 → 0.3.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 +40 -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 -42
- 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
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import{GitError as e}from"../errors/git.js";import{ComposeWorkflowError as t,ComposeWorkflowInputError as n}from"../errors/workflow.js";import{addUsage as r,emptyUsage as i}from"../ai/types.js";import{analyze as a}from"../tasks/analyze/analyze.js";import{composeGroup as o}from"../tasks/compose-group/compose-group.js";import{group as s}from"../tasks/group/group.js";import{order as c}from"../tasks/order/order.js";import{partition as l}from"../tasks/partition/partition.js";import{applyPatchToIndex as u,diffTree as d,forEachRef as f,readTree as p,revParse as m,writeTree as h}from"../git/port-dispatch.js";import{buildPatchForCommit as g}from"../git/utils/patch-builder.js";import{sha256 as _}from"../git/utils/crypto.js";import{collectFromSource as v}from"../git/diff-collector.js";import{verifySnapshot as y}from"../git/safety.js";import{applyToTarget as b}from"../git/apply-target.js";import{requireExec as x}from"../git/require-exec.js";import{extractConventions as S}from"../git/utils/conventions.js";import{rethrowAsWorkflowError as C}from"../utils/cancellation.js";import{randomUUID as w}from"node:crypto";import{unlinkSync as T}from"node:fs";import{tmpdir as E}from"node:os";import{join as D}from"node:path";import{UndoStore as O,capturePreOperationState as k,generateUndoId as A}from"@gitkraken/shared-tools";const j={collecting:{message:`Collecting changes…`,weight:5},composing:{message:`Composing commits…`,weight:40},"compose-grouping":{message:`Grouping changes into commits…`,weight:40},analyzing:{message:`Analyzing changes…`,weight:20},grouping:{message:`Grouping changes into commits…`,weight:25},ordering:{message:`Ordering commits…`,weight:15},partitioning:{message:`Subdividing branches…`,weight:20},verifying:{message:`Verifying…`,weight:5},applying:{message:`Applying to repository…`,weight:10}};async function M(r){if(!r.applyPlan&&!r.model)throw new n(`Either 'model' or 'applyPlan' must be provided`);if(r.applyPlan)return B(r);let{git:i,model:a,source:o={type:`workdir`},onProgress:s,onBeforeApply:c,cancellation:l}=r,u=a,d=r.depth??`balanced`,f=r.target??Y(o);Z(o,f);let p=[];s?.({phase:`collecting`,...j.collecting});let m=Date.now(),h=await v(i,o);if(p.push({name:`collecting`,durationMs:Date.now()-m,attempts:1}),h.hunks.length===0)throw new e(`NO_CHANGES`,X(o));let{hunks:g,filteredCount:_}=await U(h.hunks,r.hunkFilter);if(g.length===0)throw new e(`NO_CHANGES`,X(o));let w=_!==void 0&&_>0?await W(i,h.snapshot,g):h.snapshot,T={hunks:g,collection:h,conventions:r.conventions??await S({git:i,author:r.conventionAuthor}),existingBranchNames:await ee(i),groupMode:f.type===`auto`?`branches`:`commits`,target:f,model:u,instructions:r.instructions,context:r.context,strategy:r.strategy,maxRetries:r.maxRetries,maxTokens:r.maxTokens,onProgress:s,cancellation:l,onBeforeModelCall:r.onBeforeModelCall,onAfterModelCall:r.onAfterModelCall,onBeforePrompt:r.onBeforePrompt,session:r.session,promptOverrides:r.promptOverrides},E;try{E=await N(d,T)}catch(e){C(e,l)}let D={analysis:E.analysis,grouping:E.grouping,ordering:E.ordering,branches:E.branchPlans,allOrderedCommits:E.allOrderedCommits};if(p.push(...E.phases),c&&!await c(D))return{plan:D,snapshot:w,session:E.session,usage:E.usage,phases:p,source:{type:o.type===`branch`?`branch`:`workdir`,hunks:g,commitMessages:h.commitMessages,branchName:h.branchName,baseBranch:h.baseBranch},diffStats:V(g),feedbackTargetCallId:H(p),hunksFilteredCount:_};let O=await k(x(i,`capturing pre-operation state`));s?.({phase:`verifying`,...j.verifying});let A=Date.now(),M=await y(i,h.snapshot,G(o,h));if(!M.valid)throw new t(`SAFETY_CHECK_FAILED`,`Safety check failed: ${M.error}`);p.push({name:`verifying`,durationMs:Date.now()-A,attempts:1});let P=K(E.allOrderedCommits,r.applyUpTo,f),{leftoverHunks:F,leftoverPatch:I,leftoverPatchBranchOnly:L}=J(P,h.hunks),R=F.length>0?await W(i,w,q(P,h.hunks)):w;s?.({phase:`applying`,...j.applying});let z=Date.now(),Q=await b({git:i,target:f,branchPlans:E.branchPlans,allOrderedCommits:P,hunks:h.hunks,snapshot:R,collection:h,signing:r.signing,authorAttribution:r.authorAttribution,stashLabel:r.stashLabel,leftoverPatch:I,leftoverPatchBranchOnly:L});p.push({name:`applying`,durationMs:Date.now()-z,attempts:1});let te=await $(i,O,Q.mutations,f,o);return{plan:D,snapshot:R,partitions:Q.partitions,partitionGroups:Q.partitionGroups,session:E.session,usage:E.usage,phases:p,source:{type:o.type===`branch`?`branch`:`workdir`,hunks:g,commitMessages:h.commitMessages,branchName:h.branchName,baseBranch:h.baseBranch},diffStats:V(g),commitShas:Q.commitShas,undoId:te,feedbackTargetCallId:H(p),hunksFilteredCount:_,appliedCommitIds:P.map(e=>e.id),unappliedHunkIndices:F.length>0?F.map(e=>e.index):void 0,stashConflict:Q.stashConflict}}function N(e,t){switch(e){case`quick`:return P(t);case`balanced`:return F(t);case`deep`:return L(t)}}async function P(e){return I(e,{lightweight:!0,skipOrder:!0})}async function F(e){return I(e,{includeAnalysis:!0})}async function I(e,t){let{hunks:n,collection:a,conventions:s,existingBranchNames:u,groupMode:d,target:f,model:p,instructions:m,context:h,strategy:g,maxRetries:_,maxTokens:v,onProgress:y,cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C}=e,w=i(),T=[],E=await o({hunks:n,commitMessages:a.commitMessages,commitOrder:a.commitOrder,branchName:a.branchName,baseBranch:a.baseBranch,issueUrl:h?.issueUrl,pullRequestUrl:h?.pullRequestUrl,instructions:m,conventions:s,mode:d,strategy:g?.grouping,existingBranchNames:d===`branches`?u:void 0,includeAnalysis:t.includeAnalysis,lightweight:t.lightweight,condensed:t.condensed,model:p,session:e.session,maxRetries:_,maxTokens:v,onProgress:e=>y?.({phase:`compose-grouping`,...j[`compose-grouping`],event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.[`compose-group`]});E.usage&&r(w,E.usage),T.push({name:`compose-grouping`,durationMs:E.durationMs,attempts:E.attempts,usage:E.usage,callIds:E.callIds});let D=E.session,O,k;if(t.skipOrder)O=E.result.branches.map(e=>({branchGroup:e,orderedCommitIds:e.commits.map(e=>e.id)})),k={branches:O.map(e=>({branchId:e.branchGroup.id,orderedCommitIds:e.orderedCommitIds})),rationale:``};else{let t=await c({branches:E.result.branches,hunks:n,instructions:m,model:p,session:E.session,maxRetries:_,maxTokens:v,onProgress:e=>y?.({phase:`ordering`,...j.ordering,event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.order});t.usage&&r(w,t.usage),T.push({name:`ordering`,durationMs:t.durationMs,attempts:t.attempts,usage:t.usage,callIds:t.callIds}),D=t.session,k=t.result,O=E.result.branches.map(e=>({branchGroup:e,orderedCommitIds:k.branches.find(t=>t.branchId===e.id)?.orderedCommitIds??e.commits.map(e=>e.id)}))}if(f.type===`auto`){let t=new Set(u);for(let e of E.result.branches)e.name&&t.add(e.name);for(let n of O){let i=await l({commits:Q(n.branchGroup,n.orderedCommitIds),orderedCommitIds:n.orderedCommitIds,sourceBranchName:n.branchGroup.name,instructions:m,conventions:s,strategy:g?.partition,existingBranchNames:t,model:p,maxRetries:_,maxTokens:v,onProgress:e=>y?.({phase:`partitioning`,...j.partitioning,branch:n.branchGroup.name,event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.partition});i.usage&&r(w,i.usage),T.push({name:`partitioning:${n.branchGroup.name}`,durationMs:i.durationMs,attempts:i.attempts,usage:i.usage,callIds:i.callIds}),n.partitioning=i.result,D=i.session;for(let e of i.result.partitions)t.add(e.branchName)}}let A=O.flatMap(e=>Q(e.branchGroup,e.orderedCommitIds));return{analysis:void 0,grouping:E.result,ordering:k,branchPlans:O,allOrderedCommits:A,session:D,usage:w,phases:T}}async function L(e){let{hunks:t,collection:n,conventions:o,existingBranchNames:u,groupMode:d,target:f,model:p,instructions:m,context:h,strategy:g,maxRetries:_,maxTokens:v,onProgress:y,cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C}=e,w=i(),T=[],E=await a({hunks:t,commitMessages:n.commitMessages,commitOrder:n.commitOrder,branchName:n.branchName,baseBranch:n.baseBranch,issueUrl:h?.issueUrl,pullRequestUrl:h?.pullRequestUrl,instructions:m,model:p,session:e.session,maxRetries:_,maxTokens:v,onProgress:e=>y?.({phase:`analyzing`,...j.analyzing,event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.analyze});E.usage&&r(w,E.usage),T.push({name:`analyzing`,durationMs:E.durationMs,attempts:E.attempts,usage:E.usage,callIds:E.callIds});let D=await s({hunks:t,commitMessages:n.commitMessages,instructions:m,conventions:o,mode:d,strategy:g?.grouping,existingBranchNames:d===`branches`?u:void 0,model:p,session:E.session,maxRetries:_,maxTokens:v,onProgress:e=>y?.({phase:`grouping`,...j.grouping,event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.group});D.usage&&r(w,D.usage),T.push({name:`grouping`,durationMs:D.durationMs,attempts:D.attempts,usage:D.usage,callIds:D.callIds});let O=await c({branches:D.result.branches,hunks:t,instructions:m,model:p,session:D.session,maxRetries:_,maxTokens:v,onProgress:e=>y?.({phase:`ordering`,...j.ordering,event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.order});O.usage&&r(w,O.usage),T.push({name:`ordering`,durationMs:O.durationMs,attempts:O.attempts,usage:O.usage,callIds:O.callIds});let k=D.result.branches.map(e=>({branchGroup:e,orderedCommitIds:O.result.branches.find(t=>t.branchId===e.id)?.orderedCommitIds??e.commits.map(e=>e.id)})),A=O.session;if(f.type===`auto`){let t=new Set(u);for(let e of D.result.branches)e.name&&t.add(e.name);for(let n of k){let i=await l({commits:Q(n.branchGroup,n.orderedCommitIds),orderedCommitIds:n.orderedCommitIds,sourceBranchName:n.branchGroup.name,instructions:m,conventions:o,strategy:g?.partition,existingBranchNames:t,model:p,maxRetries:_,maxTokens:v,onProgress:e=>y?.({phase:`partitioning`,...j.partitioning,branch:n.branchGroup.name,event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.partition});i.usage&&r(w,i.usage),T.push({name:`partitioning:${n.branchGroup.name}`,durationMs:i.durationMs,attempts:i.attempts,usage:i.usage,callIds:i.callIds}),n.partitioning=i.result,A=i.session;for(let e of i.result.partitions)t.add(e.branchName)}}let M=k.flatMap(e=>Q(e.branchGroup,e.orderedCommitIds));return{analysis:E.result,grouping:D.result,ordering:O.result,branchPlans:k,allOrderedCommits:M,session:A,usage:w,phases:T}}async function R(e){let t=await M({...e,onBeforeApply:async()=>!1});return{plan:t.plan,snapshot:t.snapshot,source:t.source,session:t.session,usage:t.usage,phases:t.phases,feedbackTargetCallId:t.feedbackTargetCallId,hunksFilteredCount:t.hunksFilteredCount}}async function z(e){let t=await M({git:e.git,applyPlan:e.applyPlan,source:e.source,target:e.target,onProgress:e.onProgress,signing:e.signing,authorAttribution:e.authorAttribution,applyUpTo:e.applyUpTo,stashLabel:e.stashLabel,hunkFilter:e.hunkFilter});return{commitShas:t.commitShas,partitions:t.partitions,partitionGroups:t.partitionGroups,phases:t.phases,undoId:t.undoId,appliedCommitIds:t.appliedCommitIds,unappliedHunkIndices:t.unappliedHunkIndices,stashConflict:t.stashConflict}}async function B(n){let{git:r,applyPlan:a,onProgress:o}=n,{plan:s,snapshot:c,source:l}=a,u=n.source??l,d=n.target??a?.target??Y(u);Z(u,d);let f=await k(x(r,`capturing pre-operation state`)),p=[];o?.({phase:`collecting`,...j.collecting});let m=Date.now(),h=await v(r,u);if(p.push({name:`collecting`,durationMs:Date.now()-m,attempts:1}),h.hunks.length===0)throw new e(`NO_CHANGES`,X(u));let{hunks:g}=await U(h.hunks,n.hunkFilter);if(g.length===0)throw new e(`NO_CHANGES`,X(u));let _=g.length<h.hunks.length?await W(r,h.snapshot,g):h.snapshot,S={...h,hunks:g,snapshot:_};if(S.snapshot.diffHash!==c.diffHash)throw new t(`SAFETY_CHECK_FAILED`,`Working directory has changed since the plan was created. Re-run compose to generate a new plan.`);o?.({phase:`verifying`,...j.verifying});let C=Date.now(),w=await y(r,S.snapshot,G(u,S));if(!w.valid)throw new t(`SAFETY_CHECK_FAILED`,`Safety check failed: ${w.error}`);p.push({name:`verifying`,durationMs:Date.now()-C,attempts:1});let T=K(s.allOrderedCommits,n.applyUpTo,d),{leftoverHunks:E,leftoverPatch:D,leftoverPatchBranchOnly:O}=J(T,S.hunks),A=E.length>0?await W(r,S.snapshot,q(T,S.hunks)):S.snapshot;o?.({phase:`applying`,...j.applying});let M=Date.now(),N=await b({git:r,target:d,branchPlans:s.branches,allOrderedCommits:T,hunks:S.hunks,snapshot:A,collection:S,signing:n.signing,authorAttribution:n.authorAttribution,stashLabel:n.stashLabel,leftoverPatch:D,leftoverPatchBranchOnly:O});p.push({name:`applying`,durationMs:Date.now()-M,attempts:1});let P=await $(r,f,N.mutations,d,u);return{plan:s,snapshot:A,partitions:N.partitions,partitionGroups:N.partitionGroups,session:{messages:[],systemPrompt:``,completedSteps:{}},usage:i(),phases:p,source:{type:u.type===`branch`?`branch`:`workdir`,hunks:S.hunks,commitMessages:S.commitMessages,branchName:S.branchName,baseBranch:S.baseBranch},diffStats:V(S.hunks),commitShas:N.commitShas,undoId:P,appliedCommitIds:T.map(e=>e.id),unappliedHunkIndices:E.length>0?E.map(e=>e.index):void 0,stashConflict:N.stashConflict}}function V(e){let t=new Set,n=0,r=0;for(let i of e)t.add(i.fileName),n+=i.additions,r+=i.deletions;return{fileCount:t.size,hunkCount:e.length,addedLines:n,removedLines:r}}function H(e){for(let t=e.length-1;t>=0;t--){let n=e[t].callIds;if(n&&n.length>0)return n[n.length-1]}}async function U(e,t){if(!t)return{hunks:e,filteredCount:void 0};let n=await t(e);return{hunks:n,filteredCount:e.length-n.length}}async function W(e,t,n){let r=t.baseSha??t.headSha,i=D(E(),`compose-filtered-idx-${w()}`),a={GIT_INDEX_FILE:i};try{await p(e,r,{indexEnv:a});let i=g(n);i.length>0&&await u(e,i,{indexEnv:a});let o=await d(e,r,await h(e,{indexEnv:a}));return{...t,diffHash:_(o)}}finally{try{T(i)}catch{}}}function G(e,t){return e.type===`workdir`?{includeUnstaged:!e.stagedOnly,includeUntracked:t.untrackedIncluded}:e.type===`commit-range`&&e.includeWorkdir?{includeStaged:e.includeWorkdir.includeStaged??!0,includeUnstaged:e.includeWorkdir.includeUnstaged??!0,includeUntracked:e.includeWorkdir.includeUntracked??!0}:{includeUnstaged:!1,includeUntracked:!1}}function K(e,t,r){if(!t||t.kind===`all`)return e;if(r.type===`auto`)throw new n(`applyUpTo is not supported with target type 'auto'. Use 'head', 'branch', 'branch-replace', or 'rewrite-range'.`);if(t.kind===`count`){let r=t.count;if(!Number.isInteger(r)||r<0||r>e.length)throw new n(`applyUpTo.count ${String(r)} is out of range (0 to ${e.length})`);return e.slice(0,r)}let i=e.findIndex(e=>e.id===t.id);if(i===-1)throw new n(`applyUpTo.id '${t.id}' not found in plan.allOrderedCommits`);return e.slice(0,i+1)}function q(e,t){let n=new Set(e.flatMap(e=>e.hunkIndices));return t.filter(e=>n.has(e.index))}function J(e,t){let n=new Set(e.flatMap(e=>e.hunkIndices)),r=t.filter(e=>!n.has(e.index));if(r.length===0)return{leftoverHunks:[],leftoverPatch:void 0,leftoverPatchBranchOnly:void 0};let i=g(r),a=r.filter(e=>e.source===`branch`),o=a.length>0?g(a):``;return{leftoverHunks:r,leftoverPatch:i.length>0?i:void 0,leftoverPatchBranchOnly:o.length>0?o:void 0}}function Y(e){return e.type===`branch`?{type:`branch-replace`}:e.type===`commit-range`?{type:`rewrite-range`,branch:e.branch,rangeFrom:e.from,rangeTo:e.to}:{type:`head`}}function X(e){switch(e.type){case`workdir`:return`No changes found in working directory`;case`branch`:return`No changes found on branch '${e.name}'`;case`commit-range`:return`No changes found in range '${e.from}..${e.to}' on branch '${e.branch}'`}}function Z(e,t){if(t.type===`branch-replace`&&e.type!==`branch`)throw new n(`target 'branch-replace' requires source type 'branch'`);if(t.type===`rewrite-range`&&e.type!==`commit-range`&&e.type!==`branch`)throw new n(`target 'rewrite-range' requires source type 'commit-range' or 'branch'`)}function Q(e,n){return n.map(n=>{let r=e.commits.find(e=>e.id===n);if(!r)throw new t(`INTERNAL`,`Commit ${n} not found in branch '${e.id}'`);return r})}async function ee(e){let t=(await f(e,{format:`%(refname:short)`,prefix:`refs/heads/`})).trim().split(`
|
|
2
|
-
`)
|
|
1
|
+
import{GitError as e}from"../errors/git.js";import{ComposeWorkflowError as t,ComposeWorkflowInputError as n}from"../errors/workflow.js";import{addUsage as r,emptyUsage as i}from"../ai/types.js";import{analyze as a}from"../tasks/analyze/analyze.js";import{composeGroup as o}from"../tasks/compose-group/compose-group.js";import{group as s}from"../tasks/group/group.js";import{order as c}from"../tasks/order/order.js";import{partition as l}from"../tasks/partition/partition.js";import{applyPatchToIndex as u,currentBranch as d,diffTree as f,forEachRef as p,log as m,mergeBase as h,readTree as g,revParse as _,writeTree as v}from"../git/port-dispatch.js";import{buildPatchForCommit as y}from"../git/utils/patch-builder.js";import{sha256 as b}from"../git/utils/crypto.js";import{collectFromSource as x}from"../git/diff-collector.js";import{verifySnapshot as S}from"../git/safety.js";import{applyToTarget as C}from"../git/apply-target.js";import{requireExec as w}from"../git/require-exec.js";import{validateRewriteRangeSafety as T}from"../git/safety-range.js";import{extractConventions as E}from"../git/utils/conventions.js";import{rethrowAsWorkflowError as D}from"../utils/cancellation.js";import{randomUUID as O}from"node:crypto";import{unlinkSync as k}from"node:fs";import{tmpdir as A}from"node:os";import{join as j}from"node:path";import{UndoStore as M,capturePreOperationState as N,generateUndoId as P}from"@gitkraken/shared-tools";const F={collecting:{message:`Collecting changes…`,weight:5},composing:{message:`Composing commits…`,weight:40},"compose-grouping":{message:`Grouping changes into commits…`,weight:40},analyzing:{message:`Analyzing changes…`,weight:20},grouping:{message:`Grouping changes into commits…`,weight:25},ordering:{message:`Ordering commits…`,weight:15},partitioning:{message:`Subdividing branches…`,weight:20},verifying:{message:`Verifying…`,weight:5},applying:{message:`Applying to repository…`,weight:10}};async function I(r){if(!r.applyPlan&&!r.model)throw new n(`Either 'model' or 'applyPlan' must be provided`);if(r.applyPlan)return re(r);let{git:i,model:a,source:o={type:`workdir`},onProgress:s,onBeforeApply:c,cancellation:l}=r,u=a,d=r.depth??`balanced`,f=r.target??await X(i,o);Q(o,f),await K(i,r.applyCommitIds,r.hunkFilter,o,f),await T({git:i,source:o,target:f});let p=[];s?.({phase:`collecting`,...F.collecting});let m=Date.now(),h=await x(i,o);if(p.push({name:`collecting`,durationMs:Date.now()-m,attempts:1}),h.hunks.length===0)throw new e(`NO_CHANGES`,Z(o));let{hunks:g,filteredCount:_}=await U(h.hunks,r.hunkFilter);if(g.length===0)throw new e(`NO_CHANGES`,Z(o));let v=_!==void 0&&_>0?await W(i,h.snapshot,g):h.snapshot,y={hunks:g,collection:h,conventions:r.conventions??await E({git:i,author:r.conventionAuthor}),existingBranchNames:await ie(i),groupMode:f.type===`auto`?`branches`:`commits`,target:f,model:u,instructions:r.instructions,context:r.context,strategy:r.strategy,maxRetries:r.maxRetries,maxTokens:r.maxTokens,onProgress:s,cancellation:l,onBeforeModelCall:r.onBeforeModelCall,onAfterModelCall:r.onAfterModelCall,onBeforePrompt:r.onBeforePrompt,session:r.session,promptOverrides:r.promptOverrides,redactHunkContent:r.redactHunkContent},b;try{b=await ee(d,y)}catch(e){D(e,l)}let O={analysis:b.analysis,grouping:b.grouping,ordering:b.ordering,branches:b.branchPlans,allOrderedCommits:b.allOrderedCommits};if(p.push(...b.phases),c&&!await c(O))return{plan:O,snapshot:v,session:b.session,usage:b.usage,phases:p,source:{type:o.type===`branch`?`branch`:`workdir`,hunks:g,commitMessages:h.commitMessages,branchName:h.branchName,baseBranch:h.baseBranch},diffStats:V(g),feedbackTargetCallId:H(p),hunksFilteredCount:_};let k=await N(w(i,`capturing pre-operation state`));s?.({phase:`verifying`,...F.verifying});let A=Date.now(),j=await S(i,h.snapshot,G(o,h));if(!j.valid)throw new t(`SAFETY_CHECK_FAILED`,`Safety check failed: ${j.error}`);p.push({name:`verifying`,durationMs:Date.now()-A,attempts:1});let M=q(b.allOrderedCommits,r.applyCommitIds),{leftoverHunks:P,leftoverPatch:I}=Y(M,h.hunks),L=P.length>0?await W(i,v,J(M,h.hunks)):v;s?.({phase:`applying`,...F.applying});let R=Date.now(),z=await C({git:i,source:o,target:f,branchPlans:b.branchPlans,allOrderedCommits:M,hunks:h.hunks,snapshot:L,collection:h,signing:r.signing,authorAttribution:r.authorAttribution,stashLabel:r.stashLabel,leftoverPatch:I});p.push({name:`applying`,durationMs:Date.now()-R,attempts:1});let B=await ae(i,k,z.mutations,f,o);return{plan:O,snapshot:L,partitions:z.partitions,partitionGroups:z.partitionGroups,session:b.session,usage:b.usage,phases:p,source:{type:o.type===`branch`?`branch`:`workdir`,hunks:g,commitMessages:h.commitMessages,branchName:h.branchName,baseBranch:h.baseBranch},diffStats:V(g),commitShas:z.commitShas,undoId:B,feedbackTargetCallId:H(p),hunksFilteredCount:_,appliedCommitIds:M.map(e=>e.id),unappliedHunkIndices:P.length>0?P.map(e=>e.index):void 0,stashConflict:z.stashConflict}}function ee(e,t){switch(e){case`quick`:return L(t);case`balanced`:return R(t);case`deep`:return B(t)}}async function L(e){return z(e,{lightweight:!0,skipOrder:!0})}async function R(e){return z(e,{includeAnalysis:!0})}async function z(e,t){let{hunks:n,collection:a,conventions:s,existingBranchNames:u,groupMode:d,target:f,model:p,instructions:m,context:h,strategy:g,maxRetries:_,maxTokens:v,onProgress:y,cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C}=e,w=i(),T=[],E=await o({hunks:n,commitMessages:a.commitMessages,commitOrder:a.commitOrder,branchName:a.branchName,baseBranch:a.baseBranch,issueUrl:h?.issueUrl,pullRequestUrl:h?.pullRequestUrl,instructions:m,conventions:s,mode:d,strategy:g?.grouping,existingBranchNames:d===`branches`?u:void 0,includeAnalysis:t.includeAnalysis,lightweight:t.lightweight,condensed:t.condensed,redactHunkContent:e.redactHunkContent,model:p,session:e.session,maxRetries:_,maxTokens:v,onProgress:e=>y?.({phase:`compose-grouping`,...F[`compose-grouping`],event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.[`compose-group`]});E.usage&&r(w,E.usage),T.push({name:`compose-grouping`,durationMs:E.durationMs,attempts:E.attempts,usage:E.usage,callIds:E.callIds});let D=E.session,O,k;if(t.skipOrder)O=E.result.branches.map(e=>({branchGroup:e,orderedCommitIds:e.commits.map(e=>e.id)})),k={branches:O.map(e=>({branchId:e.branchGroup.id,orderedCommitIds:e.orderedCommitIds})),rationale:``};else{let t=await c({branches:E.result.branches,hunks:n,instructions:m,model:p,session:E.session,maxRetries:_,maxTokens:v,onProgress:e=>y?.({phase:`ordering`,...F.ordering,event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.order});t.usage&&r(w,t.usage),T.push({name:`ordering`,durationMs:t.durationMs,attempts:t.attempts,usage:t.usage,callIds:t.callIds}),D=t.session,k=t.result,O=E.result.branches.map(e=>({branchGroup:e,orderedCommitIds:k.branches.find(t=>t.branchId===e.id)?.orderedCommitIds??e.commits.map(e=>e.id)}))}if(f.type===`auto`){let t=new Set(u);for(let e of E.result.branches)e.name&&t.add(e.name);for(let n of O){let i=await l({commits:$(n.branchGroup,n.orderedCommitIds),orderedCommitIds:n.orderedCommitIds,sourceBranchName:n.branchGroup.name,instructions:m,conventions:s,strategy:g?.partition,existingBranchNames:t,model:p,maxRetries:_,maxTokens:v,onProgress:e=>y?.({phase:`partitioning`,...F.partitioning,branch:n.branchGroup.name,event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.partition});i.usage&&r(w,i.usage),T.push({name:`partitioning:${n.branchGroup.name}`,durationMs:i.durationMs,attempts:i.attempts,usage:i.usage,callIds:i.callIds}),n.partitioning=i.result,D=i.session;for(let e of i.result.partitions)t.add(e.branchName)}}let A=O.flatMap(e=>$(e.branchGroup,e.orderedCommitIds));return{analysis:void 0,grouping:E.result,ordering:k,branchPlans:O,allOrderedCommits:A,session:D,usage:w,phases:T}}async function B(e){let{hunks:t,collection:n,conventions:o,existingBranchNames:u,groupMode:d,target:f,model:p,instructions:m,context:h,strategy:g,maxRetries:_,maxTokens:v,onProgress:y,cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C}=e,w=i(),T=[],E=await a({hunks:t,commitMessages:n.commitMessages,commitOrder:n.commitOrder,branchName:n.branchName,baseBranch:n.baseBranch,issueUrl:h?.issueUrl,pullRequestUrl:h?.pullRequestUrl,instructions:m,redactHunkContent:e.redactHunkContent,model:p,session:e.session,maxRetries:_,maxTokens:v,onProgress:e=>y?.({phase:`analyzing`,...F.analyzing,event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.analyze});E.usage&&r(w,E.usage),T.push({name:`analyzing`,durationMs:E.durationMs,attempts:E.attempts,usage:E.usage,callIds:E.callIds});let D=await s({hunks:t,commitMessages:n.commitMessages,instructions:m,conventions:o,mode:d,strategy:g?.grouping,existingBranchNames:d===`branches`?u:void 0,redactHunkContent:e.redactHunkContent,model:p,session:E.session,maxRetries:_,maxTokens:v,onProgress:e=>y?.({phase:`grouping`,...F.grouping,event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.group});D.usage&&r(w,D.usage),T.push({name:`grouping`,durationMs:D.durationMs,attempts:D.attempts,usage:D.usage,callIds:D.callIds});let O=await c({branches:D.result.branches,hunks:t,instructions:m,model:p,session:D.session,maxRetries:_,maxTokens:v,onProgress:e=>y?.({phase:`ordering`,...F.ordering,event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.order});O.usage&&r(w,O.usage),T.push({name:`ordering`,durationMs:O.durationMs,attempts:O.attempts,usage:O.usage,callIds:O.callIds});let k=D.result.branches.map(e=>({branchGroup:e,orderedCommitIds:O.result.branches.find(t=>t.branchId===e.id)?.orderedCommitIds??e.commits.map(e=>e.id)})),A=O.session;if(f.type===`auto`){let t=new Set(u);for(let e of D.result.branches)e.name&&t.add(e.name);for(let n of k){let i=await l({commits:$(n.branchGroup,n.orderedCommitIds),orderedCommitIds:n.orderedCommitIds,sourceBranchName:n.branchGroup.name,instructions:m,conventions:o,strategy:g?.partition,existingBranchNames:t,model:p,maxRetries:_,maxTokens:v,onProgress:e=>y?.({phase:`partitioning`,...F.partitioning,branch:n.branchGroup.name,event:e}),cancellation:b,onBeforeModelCall:x,onAfterModelCall:S,onBeforePrompt:C,promptOverride:e.promptOverrides?.partition});i.usage&&r(w,i.usage),T.push({name:`partitioning:${n.branchGroup.name}`,durationMs:i.durationMs,attempts:i.attempts,usage:i.usage,callIds:i.callIds}),n.partitioning=i.result,A=i.session;for(let e of i.result.partitions)t.add(e.branchName)}}let j=k.flatMap(e=>$(e.branchGroup,e.orderedCommitIds));return{analysis:E.result,grouping:D.result,ordering:O.result,branchPlans:k,allOrderedCommits:j,session:A,usage:w,phases:T}}async function te(e){let t=await I({...e,onBeforeApply:async()=>!1});return{plan:t.plan,snapshot:t.snapshot,source:t.source,session:t.session,usage:t.usage,phases:t.phases,feedbackTargetCallId:t.feedbackTargetCallId,hunksFilteredCount:t.hunksFilteredCount}}async function ne(e){let t=await I({git:e.git,applyPlan:e.applyPlan,source:e.source,target:e.target,onProgress:e.onProgress,signing:e.signing,authorAttribution:e.authorAttribution,applyCommitIds:e.applyCommitIds,stashLabel:e.stashLabel,hunkFilter:e.hunkFilter});return{commitShas:t.commitShas,partitions:t.partitions,partitionGroups:t.partitionGroups,phases:t.phases,undoId:t.undoId,appliedCommitIds:t.appliedCommitIds,unappliedHunkIndices:t.unappliedHunkIndices,stashConflict:t.stashConflict}}async function re(n){let{git:r,applyPlan:a,onProgress:o}=n,{plan:s,snapshot:c,source:l}=a,u=n.source??l,d=n.target??a?.target??await X(r,u);Q(u,d),await K(r,n.applyCommitIds,n.hunkFilter,u,d),await T({git:r,source:u,target:d});let f=await N(w(r,`capturing pre-operation state`)),p=[];o?.({phase:`collecting`,...F.collecting});let m=Date.now(),h=await x(r,u);if(p.push({name:`collecting`,durationMs:Date.now()-m,attempts:1}),h.hunks.length===0)throw new e(`NO_CHANGES`,Z(u));let{hunks:g}=await U(h.hunks,n.hunkFilter);if(g.length===0)throw new e(`NO_CHANGES`,Z(u));let _=g.length<h.hunks.length?await W(r,h.snapshot,g):h.snapshot,v={...h,hunks:g,snapshot:_};if(v.snapshot.diffHash!==c.diffHash)throw new t(`SAFETY_CHECK_FAILED`,`Working directory has changed since the plan was created. Re-run compose to generate a new plan.`);o?.({phase:`verifying`,...F.verifying});let y=Date.now(),b=await S(r,v.snapshot,G(u,v));if(!b.valid)throw new t(`SAFETY_CHECK_FAILED`,`Safety check failed: ${b.error}`);p.push({name:`verifying`,durationMs:Date.now()-y,attempts:1});let E=q(s.allOrderedCommits,n.applyCommitIds),{leftoverHunks:D,leftoverPatch:O}=Y(E,v.hunks),k=D.length>0?await W(r,v.snapshot,J(E,v.hunks)):v.snapshot;o?.({phase:`applying`,...F.applying});let A=Date.now(),j=await C({git:r,source:u,target:d,branchPlans:s.branches,allOrderedCommits:E,hunks:v.hunks,snapshot:k,collection:v,signing:n.signing,authorAttribution:n.authorAttribution,stashLabel:n.stashLabel,leftoverPatch:O});p.push({name:`applying`,durationMs:Date.now()-A,attempts:1});let M=await ae(r,f,j.mutations,d,u);return{plan:s,snapshot:k,partitions:j.partitions,partitionGroups:j.partitionGroups,session:{messages:[],systemPrompt:``,completedSteps:{}},usage:i(),phases:p,source:{type:u.type===`branch`?`branch`:`workdir`,hunks:v.hunks,commitMessages:v.commitMessages,branchName:v.branchName,baseBranch:v.baseBranch},diffStats:V(v.hunks),commitShas:j.commitShas,undoId:M,appliedCommitIds:E.map(e=>e.id),unappliedHunkIndices:D.length>0?D.map(e=>e.index):void 0,stashConflict:j.stashConflict}}function V(e){let t=new Set,n=0,r=0;for(let i of e)t.add(i.fileName),n+=i.additions,r+=i.deletions;return{fileCount:t.size,hunkCount:e.length,addedLines:n,removedLines:r}}function H(e){for(let t=e.length-1;t>=0;t--){let n=e[t].callIds;if(n&&n.length>0)return n[n.length-1]}}async function U(e,t){if(!t)return{hunks:e,filteredCount:void 0};let n=await t(e);return{hunks:n,filteredCount:e.length-n.length}}async function W(e,t,n){let r=t.baseSha??t.headSha,i=j(A(),`compose-filtered-idx-${O()}`),a={GIT_INDEX_FILE:i};try{await g(e,r,{indexEnv:a});let i=y(n);i.length>0&&await u(e,i,{indexEnv:a});let o=await f(e,r,await v(e,{indexEnv:a}));return{...t,diffHash:b(o)}}finally{try{k(i)}catch{}}}function G(e,t){return e.type===`workdir`?{includeUnstaged:!e.stagedOnly,includeUntracked:t.untrackedIncluded}:e.type===`commit-range`&&e.includeWorkdir?{includeStaged:e.includeWorkdir.includeStaged??!0,includeUnstaged:e.includeWorkdir.includeUnstaged??!0,includeUntracked:e.includeWorkdir.includeUntracked??!0}:{includeUnstaged:!1,includeUntracked:!1}}async function K(e,t,r,i,a){if(r==null&&t==null||i.type===`workdir`||!(a.type===`rewrite-range`||(a.type===`branch`||a.type===`auto`)&&a.preserveOriginal!==!0))return;let o=i.type===`branch`?await _(e,i.name):await _(e,i.to),s=await _(e,`HEAD`);if(await d(e)==null)throw new n(`Filtering and partial apply require HEAD to be on a branch (non-detached) when rewriting existing commits. Check out the source branch before composing.`);if(s!==o)throw new n(`Filtering and partial apply require HEAD to be at the source's tip (${o.slice(0,8)}); HEAD is at ${s.slice(0,8)}.`)}function q(e,t){if(t==null)return e;let r=new Set(t),i=e.filter(e=>r.has(e.id)),a=new Set(i.map(e=>e.id)),o=t.filter(e=>!a.has(e));if(o.length>0)throw new n(`applyCommitIds contains commit ids not found in plan.allOrderedCommits: ${o.join(`, `)}`);return i}function J(e,t){let n=new Set(e.flatMap(e=>e.hunkIndices));return t.filter(e=>n.has(e.index))}function Y(e,t){let n=new Set(e.flatMap(e=>e.hunkIndices)),r=t.filter(e=>!n.has(e.index));if(r.length===0)return{leftoverHunks:[],leftoverPatch:void 0};let i=y(r);return{leftoverHunks:r,leftoverPatch:i.length>0?i:void 0}}async function X(e,t){if(t.type===`branch`){let r=await _(e,t.name),i=(await m(e,{format:`%H`,range:`${await h(e,await _(e,t.mergeTarget??`main`),r)}..${r}`,reverse:!0,limit:1})).trim().split(`
|
|
2
|
+
`)[0];if(!i)throw new n(`Cannot recompose branch '${t.name}': no commits between merge-base and branch tip.`);return{type:`rewrite-range`,branch:t.name,rangeFrom:i,rangeTo:r}}return t.type===`commit-range`?{type:`rewrite-range`,branch:t.branch,rangeFrom:t.from,rangeTo:t.to}:{type:`head`}}function Z(e){switch(e.type){case`workdir`:return`No changes found in working directory`;case`branch`:return`No changes found on branch '${e.name}'`;case`commit-range`:return`No changes found in range '${e.from}..${e.to}' on branch '${e.branch}'`}}function Q(e,t){if(t.type===`rewrite-range`&&e.type===`workdir`)throw new n(`target 'rewrite-range' is not valid with 'workdir' source`);if(t.type===`head`&&e.type!==`workdir`)throw new n(`target 'head' is not valid with '${e.type}' source — use 'branch', 'auto', or 'rewrite-range'`)}function $(e,n){return n.map(n=>{let r=e.commits.find(e=>e.id===n);if(!r)throw new t(`INTERNAL`,`Commit ${n} not found in branch '${e.id}'`);return r})}async function ie(e){let t=(await p(e,{format:`%(refname:short)`,prefix:`refs/heads/`})).trim().split(`
|
|
3
|
+
`).filter(Boolean);return new Set(t)}async function ae(e,t,n,r,i){let a=P(),o=n.createdRefs.filter(e=>e.kind===`branch`).length||1,s=`compose: ${o} branch${o===1?``:`es`} from ${i.type}`,c=[...n.movedRefs];if(r.type===`head`&&t.headBranch){let n=await _(e,`HEAD`);n!==t.headSha&&c.push({kind:`branch`,name:t.headBranch,previousSha:t.headSha,newSha:n})}let l={version:1,id:a,createdAt:new Date().toISOString(),description:s,previousHead:{sha:t.headSha,branch:t.headBranch},workdirClean:t.workdirClean,workdirBackupSha:t.workdirBackupSha,createdRefs:n.createdRefs,deletedRefs:n.deletedRefs,movedRefs:c};return await new M(w(e,`undo manifest persistence`)).save(l),a}export{ne as applyComposePlan,I as compose,te as composePlan,F as defaultPhaseMessages};
|
|
@@ -2,6 +2,7 @@ import { PhaseDetail, ProgressEvent, TokenUsage } from "../ai/types.js";
|
|
|
2
2
|
import { ComposeGitPort } from "../ports/git.js";
|
|
3
3
|
import { BranchPartition, CommitDetail, ComposeHunk } from "../tasks/types.js";
|
|
4
4
|
import { OnAfterModelCall, OnBeforeModelCall, OnBeforePrompt } from "../tasks/ai-loop.js";
|
|
5
|
+
import { RedactHunkContent } from "../tasks/utils/redact.js";
|
|
5
6
|
import { AnalysisResult } from "../tasks/analyze/types.js";
|
|
6
7
|
import { ComposeSession } from "../tasks/session.js";
|
|
7
8
|
import { PartitionResult } from "../tasks/partition/types.js";
|
|
@@ -33,6 +34,8 @@ interface SplitInput {
|
|
|
33
34
|
onBeforeModelCall?: OnBeforeModelCall;
|
|
34
35
|
onAfterModelCall?: OnAfterModelCall;
|
|
35
36
|
onBeforePrompt?: OnBeforePrompt;
|
|
37
|
+
/** See {@link RedactHunkContent}. */
|
|
38
|
+
redactHunkContent?: RedactHunkContent;
|
|
36
39
|
}
|
|
37
40
|
interface SplitPlan {
|
|
38
41
|
analysis: AnalysisResult;
|
package/dist/workflows/split.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import{GitError as e}from"../errors/git.js";import{addUsage as t,emptyUsage as n}from"../ai/types.js";import{analyze as r}from"../tasks/analyze/analyze.js";import{partition as i}from"../tasks/partition/partition.js";import{forEachRef as a}from"../git/port-dispatch.js";import{createStackedBranches as o}from"../git/apply-branches.js";import{parseUnifiedDiff as s}from"../git/utils/hunk-parser.js";import{collectCommitsForSplit as c}from"../git/diff-collector.js";import{requireExec as l}from"../git/require-exec.js";import{rethrowAsWorkflowError as u}from"../utils/cancellation.js";import{defaultPhaseMessages as d}from"./compose.js";import{UndoStore as f,capturePreOperationState as p,generateUndoId as m}from"@gitkraken/shared-tools";async function h(a){let{git:f,model:m,branchName:h,baseBranch:v=`main`,instructions:y,maxRetries:b,maxTokens:x,onProgress:S,onBeforeApply:C,cancellation:w,onBeforeModelCall:T,onAfterModelCall:E,onBeforePrompt:D}=a,O=n(),k=[];S?.({phase:`collecting`,...d.collecting});let A=Date.now(),{commits:j,branchSha:M}=await c(f,h,v);if(k.push({name:`collecting`,durationMs:Date.now()-A,attempts:1}),j.length===0)throw new e(`NO_CHANGES`,`No commits found between '${v}' and '${h}'`);if(j.length<2)throw new e(`NO_CHANGES`,`Too few commits to split. A branch needs at least 2 commits.`);let N=s(j.map(e=>e.diff).join(`
|
|
2
|
-
`)
|
|
3
|
-
`).filter(Boolean);return new Set(t)}async function _(e,t,n,r,i){let a=m(),o=n.map(e=>{
|
|
2
|
+
`)),P=j.map(e=>({id:e.sha,message:e.message,explanation:``,hunkIndices:[]})),F;try{F=await r({hunks:N,commitMessages:j.map(e=>e.message),commitOrder:j.map(e=>e.sha),branchName:h,baseBranch:v,instructions:y,redactHunkContent:a.redactHunkContent,model:m,maxRetries:b,maxTokens:x,onProgress:e=>S?.({phase:`analyzing`,...d.analyzing,event:e}),cancellation:w,onBeforeModelCall:T,onAfterModelCall:E,onBeforePrompt:D})}catch(e){u(e,w)}F.usage&&t(O,F.usage),k.push({name:`analyzing`,durationMs:F.durationMs,attempts:F.attempts,usage:F.usage,callIds:F.callIds});let I=j.map(e=>e.sha),L=await g(f),R;try{R=await i({commits:P,orderedCommitIds:I,sourceBranchName:h,baseBranch:v,instructions:y,existingBranchNames:L,model:m,session:F.session,maxRetries:b,maxTokens:x,onProgress:e=>S?.({phase:`partitioning`,...d.partitioning,event:e}),cancellation:w,onBeforeModelCall:T,onAfterModelCall:E,onBeforePrompt:D})}catch(e){u(e,w)}R.usage&&t(O,R.usage),k.push({name:`partitioning`,durationMs:R.durationMs,attempts:R.attempts,usage:R.usage,callIds:R.callIds});let z={type:`branch`,hunks:N,commitMessages:j.map(e=>e.message),branchName:h,baseBranch:v},B={analysis:F.result,partitioning:R.result,commits:j};if(R.result.partitions.length<=1||C&&!await C(B))return{plan:B,partitions:R.result.partitions,session:R.session,usage:O,phases:k,source:z};let V=await p(l(f,`capturing pre-operation state`));S?.({phase:`applying`,...d.applying});let H=Date.now();await o({git:f,partitions:R.result.partitions,originalBranchName:h,originalBranchSha:M}),k.push({name:`applying`,durationMs:Date.now()-H,attempts:1});let U=await _(f,V,R.result.partitions,h,M);return{plan:B,partitions:R.result.partitions,session:R.session,usage:O,phases:k,source:z,undoId:U}}async function g(e){let t=(await a(e,{format:`%(refname:short)`,prefix:`refs/heads/`})).trim().split(`
|
|
3
|
+
`).filter(Boolean);return new Set(t)}async function _(e,t,n,r,i){let a=m(),o=n.map(e=>({kind:`branch`,name:e.branchName,sha:e.commitIds[e.commitIds.length-1]})),s={version:1,id:a,createdAt:new Date().toISOString(),description:`split: ${n.length} branches from '${r}'`,previousHead:{sha:t.headSha,branch:t.headBranch},workdirClean:t.workdirClean,workdirBackupSha:t.workdirBackupSha,createdRefs:o,deletedRefs:[{kind:`branch`,name:r,sha:i}],movedRefs:[]};return await new f(l(e,`undo manifest persistence`)).save(s),a}export{h as split};
|
|
@@ -5,10 +5,13 @@
|
|
|
5
5
|
* - `workdir`: collect from the working directory. Set `stagedOnly` to ignore unstaged changes,
|
|
6
6
|
* `noUntracked` to ignore untracked files.
|
|
7
7
|
* - `branch`: collect the diff between `name` and `mergeTarget` (defaults to the upstream/base branch).
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* Internally translates to a commit range from `parent(first-commit-on-branch)` to `name`'s tip,
|
|
9
|
+
* so the same rules apply as for `commit-range`.
|
|
10
|
+
* - `commit-range`: collect from a contiguous range of commits on a branch as a single combined
|
|
11
|
+
* diff. Used by "recompose this commit" / "recompose selected commits" flows. Both `from` and
|
|
12
|
+
* `to` are **inclusive** — they name the first and last commits being rewritten. For a
|
|
13
|
+
* single-commit recompose: `from = commitSha`, `to = commitSha`. The library internally
|
|
14
|
+
* computes `parent(from)` for git operations. `from` must be an ancestor of `to`.
|
|
12
15
|
*/
|
|
13
16
|
type ComposeSource = {
|
|
14
17
|
type: 'workdir';
|
|
@@ -20,8 +23,8 @@ type ComposeSource = {
|
|
|
20
23
|
mergeTarget?: string;
|
|
21
24
|
} | {
|
|
22
25
|
type: 'commit-range';
|
|
23
|
-
branch: string;
|
|
24
|
-
from: string;
|
|
26
|
+
branch: string; /** First commit in the range to recompose (inclusive). Must be an ancestor of `to`. */
|
|
27
|
+
from: string; /** Last commit in the range to recompose (inclusive). */
|
|
25
28
|
to: string;
|
|
26
29
|
includeWorkdir?: {
|
|
27
30
|
includeStaged?: boolean;
|
|
@@ -32,13 +35,34 @@ type ComposeSource = {
|
|
|
32
35
|
/**
|
|
33
36
|
* Where compose writes the result.
|
|
34
37
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
38
|
+
* Legal source / target pairings:
|
|
39
|
+
*
|
|
40
|
+
* | source | head | branch | auto | rewrite-range |
|
|
41
|
+
* | -------------- | :--: | :----: | :--: | :-----------: |
|
|
42
|
+
* | `workdir` | ✓ | ✓ | ✓ | ✗ |
|
|
43
|
+
* | `branch` | ✗ | ✓ | ✓ | ✓ |
|
|
44
|
+
* | `commit-range` | ✗ | ✓ | ✓ | ✓ |
|
|
45
|
+
*
|
|
46
|
+
* - `head`: write the output commits onto the current branch (workdir source only).
|
|
47
|
+
* - `branch`: create a new branch from the base commit and write the chain to it. The base is
|
|
48
|
+
* HEAD for `workdir` source, or `parent(rangeFrom)` (equivalently `mergeBase` for `branch` source)
|
|
49
|
+
* for `branch`/`commit-range` sources. `preserveOriginal` behavior:
|
|
50
|
+
* - `workdir` source: `true` → WIP stays in the worktree; `false` → WIP is cleared from the
|
|
51
|
+
* worktree (except hunks excluded by `hunkFilter` or workdir flags, and any partial-apply
|
|
52
|
+
* leftover — those stay in their original location).
|
|
53
|
+
* - `branch` source: `true` → source branch is preserved; `false` → source branch ref is deleted
|
|
54
|
+
* once the full diff has been applied to the new branch.
|
|
55
|
+
* - `commit-range` source: forced to `true` (the source commits always stay reachable from the
|
|
56
|
+
* source branch). Passing `false` is a no-op.
|
|
57
|
+
* - `auto`: same as `branch` but the AI splits the work across one or more new branches or
|
|
58
|
+
* stacks; the same `preserveOriginal` rules apply per source type.
|
|
59
|
+
* - `rewrite-range`: replace the source's commits in place. The new chain stacks on
|
|
60
|
+
* `parent(rangeFrom)`. Every ref whose tip is a strict descendant of `rangeTo` gets its
|
|
61
|
+
* downstream chain re-parented onto the new tip; refs at `rangeTo` itself move with the
|
|
62
|
+
* source branch. Used with `source: 'commit-range'` for mid-branch recompose, or with
|
|
63
|
+
* `source: 'branch'` to rewrite the entire branch in place. Partial apply (`applyCommitIds`)
|
|
64
|
+
* and `hunkFilter` are supported on this target only when HEAD is on the source branch and
|
|
65
|
+
* pointing at `rangeTo`.
|
|
42
66
|
*/
|
|
43
67
|
type ComposeTarget = {
|
|
44
68
|
type: 'head';
|
|
@@ -46,18 +70,17 @@ type ComposeTarget = {
|
|
|
46
70
|
type: 'branch';
|
|
47
71
|
name: string;
|
|
48
72
|
preserveOriginal?: boolean;
|
|
49
|
-
} | {
|
|
50
|
-
type: 'branch-replace';
|
|
51
73
|
} | {
|
|
52
74
|
type: 'auto';
|
|
53
75
|
preserveOriginal?: boolean;
|
|
54
76
|
} | {
|
|
55
77
|
type: 'rewrite-range';
|
|
56
78
|
branch: string;
|
|
57
|
-
|
|
79
|
+
/** First commit in the rewritten range (inclusive). The new chain stacks on
|
|
80
|
+
* `parent(rangeFrom)`. */
|
|
81
|
+
rangeFrom: string; /** Last commit in the rewritten range (inclusive). */
|
|
58
82
|
rangeTo: string; /** Commits before `rangeFrom` always stay; this flag is reserved for future semantics. Default true. */
|
|
59
|
-
preservePrefix?: boolean;
|
|
60
|
-
preserveSuffix?: boolean;
|
|
83
|
+
preservePrefix?: boolean;
|
|
61
84
|
};
|
|
62
85
|
/** Optional context passed through to AI prompts (issue/PR URLs surface in commit messages). */
|
|
63
86
|
interface ComposeContext {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gitkraken/compose-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "AI-powered organization of git changes into clean, atomic commits and branches.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "Axosoft, LLC dba GitKraken",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"zod": "^4.3.6",
|
|
24
|
-
"@gitkraken/shared-tools": "^0.
|
|
24
|
+
"@gitkraken/shared-tools": "^0.2.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^22.0.0",
|