@lumpcode/core 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +18 -16
  2. package/dist/helpers/execAsync/main.d.ts +14 -9
  3. package/dist/helpers/execAsync/main.d.ts.map +1 -1
  4. package/dist/helpers/executeStepsForContextList/main.d.ts +4 -9
  5. package/dist/helpers/executeStepsForContextList/main.d.ts.map +1 -1
  6. package/dist/helpers/getContextStatus/main.d.ts.map +1 -1
  7. package/dist/index.cjs +303 -177
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.js +299 -175
  10. package/dist/index.js.map +1 -1
  11. package/dist/testing/processTreeTestHelpers.d.ts +3 -0
  12. package/dist/testing/processTreeTestHelpers.d.ts.map +1 -1
  13. package/dist/types/ExecuteStepsFailureReason.d.ts +1 -1
  14. package/dist/types/ExecuteStepsFailureReason.d.ts.map +1 -1
  15. package/dist/types/GitAddCommitFn.d.ts +16 -0
  16. package/dist/types/GitAddCommitFn.d.ts.map +1 -0
  17. package/dist/types/GitPushFn.d.ts +12 -0
  18. package/dist/types/GitPushFn.d.ts.map +1 -0
  19. package/dist/types/PostCommandExecFn.d.ts +2 -0
  20. package/dist/types/PostCommandExecFn.d.ts.map +1 -1
  21. package/dist/types/index.d.ts +2 -3
  22. package/dist/types/index.d.ts.map +1 -1
  23. package/dist/usages/runLump/defaultInjectedFns.d.ts +3 -4
  24. package/dist/usages/runLump/defaultInjectedFns.d.ts.map +1 -1
  25. package/dist/usages/runLump/index.d.ts +1 -1
  26. package/dist/usages/runLump/index.d.ts.map +1 -1
  27. package/dist/usages/runLump/main.d.ts +3 -4
  28. package/dist/usages/runLump/main.d.ts.map +1 -1
  29. package/dist/utils/commitMessageIncludesMarker/index.d.ts +2 -0
  30. package/dist/utils/commitMessageIncludesMarker/index.d.ts.map +1 -0
  31. package/dist/utils/commitMessageIncludesMarker/main.d.ts +3 -0
  32. package/dist/utils/commitMessageIncludesMarker/main.d.ts.map +1 -0
  33. package/dist/utils/index.d.ts +2 -0
  34. package/dist/utils/index.d.ts.map +1 -1
  35. package/dist/utils/parseGitLogHashBodyRecords/index.d.ts +2 -0
  36. package/dist/utils/parseGitLogHashBodyRecords/index.d.ts.map +1 -0
  37. package/dist/utils/parseGitLogHashBodyRecords/main.d.ts +8 -0
  38. package/dist/utils/parseGitLogHashBodyRecords/main.d.ts.map +1 -0
  39. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -3424,6 +3424,40 @@ function parseGitLogHashSubjectLines(stdout) {
3424
3424
  });
3425
3425
  }
3426
3426
 
3427
+ const GIT_LOG_HASH_BODY_FORMAT = '%x1e%H%x00%B';
3428
+ const RECORD_SEPARATOR = '\x1e';
3429
+ const HASH_MESSAGE_SEPARATOR = '\x00';
3430
+ /** Parses `git log --format='%x1e%H%x00%B'` stdout into commit hash and full-message pairs. */
3431
+ function parseGitLogHashBodyRecords(stdout) {
3432
+ const records = [];
3433
+ for (const record of stdout.split(RECORD_SEPARATOR)) {
3434
+ if (!record)
3435
+ continue;
3436
+ const nul = record.indexOf(HASH_MESSAGE_SEPARATOR);
3437
+ if (nul === -1)
3438
+ continue;
3439
+ const hash = record.slice(0, nul).trim();
3440
+ if (!hash)
3441
+ continue;
3442
+ records.push({
3443
+ hash,
3444
+ message: record.slice(nul + 1),
3445
+ });
3446
+ }
3447
+ return records;
3448
+ }
3449
+
3450
+ const MARKER_CONTINUE = '[A-Za-z0-9_-]';
3451
+ function escapeRegExp(value) {
3452
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3453
+ }
3454
+ /** True when `marker` occurs in `message` and is not a prefix of a longer `[A-Za-z0-9_-]*` token. */
3455
+ function commitMessageIncludesMarker(message, marker) {
3456
+ if (marker.length === 0)
3457
+ return false;
3458
+ return new RegExp(`${escapeRegExp(marker)}(?!${MARKER_CONTINUE})`).test(message);
3459
+ }
3460
+
3427
3461
  /** Node-style `code` from a caught unknown error (e.g. `ENOENT`), or undefined. */
3428
3462
  function nodeErrnoCode(error) {
3429
3463
  if (error && typeof error === 'object' && 'code' in error) {
@@ -3571,32 +3605,54 @@ async function killProcessTree(input) {
3571
3605
  }
3572
3606
 
3573
3607
  const execAsyncBase = node_util.promisify(node_child_process.exec);
3608
+ /** Same signal Node's `exec` `timeout` option uses by default. */
3609
+ const EXEC_TIMEOUT_KILL_SIGNAL = 'SIGTERM';
3610
+ function isExecTimeout(error, timeoutMillis) {
3611
+ if (timeoutMillis === undefined) {
3612
+ return false;
3613
+ }
3614
+ if (typeof error !== 'object' || error === null) {
3615
+ return false;
3616
+ }
3617
+ const execError = error;
3618
+ if (execError.killed !== true) {
3619
+ return false;
3620
+ }
3621
+ // Windows has no POSIX signals; Node force-kills and `signal` is often null.
3622
+ if (process.platform === 'win32') {
3623
+ return true;
3624
+ }
3625
+ return execError.signal === EXEC_TIMEOUT_KILL_SIGNAL;
3626
+ }
3574
3627
  async function execAsync(command, options) {
3575
- const { stdout, stderr, hasErrored } = await execAsyncBase(command, { cwd: options?.cwd })
3576
- .then(result => ({
3577
- stdout: result.stdout,
3578
- stderr: result.stderr,
3579
- hasErrored: false,
3580
- }))
3581
- .catch(e => ({
3582
- stderr: e,
3583
- stdout: e,
3584
- hasErrored: true,
3585
- }));
3586
- if (hasErrored) {
3628
+ const timeoutMillis = options?.timeoutMillis;
3629
+ try {
3630
+ const result = await execAsyncBase(command, {
3631
+ cwd: options?.cwd,
3632
+ ...(timeoutMillis !== undefined
3633
+ ? { timeout: timeoutMillis, killSignal: EXEC_TIMEOUT_KILL_SIGNAL }
3634
+ : {}),
3635
+ });
3636
+ return success({
3637
+ stdout: String(result.stdout),
3638
+ stderr: String(result.stderr),
3639
+ });
3640
+ }
3641
+ catch (e) {
3642
+ const timedOut = isExecTimeout(e, timeoutMillis);
3643
+ const reason = timedOut ? 'timeout' : 'exit';
3587
3644
  return failure({
3588
- message: `Command ${command} failed with error: ${stderr}`,
3645
+ message: timedOut
3646
+ ? `Command ${command} timed out after ${timeoutMillis}ms`
3647
+ : `Command ${command} failed with error: ${e}`,
3648
+ reason,
3589
3649
  info: {
3590
3650
  command,
3591
- stdout,
3592
- stderr,
3651
+ stdout: e,
3652
+ stderr: e,
3593
3653
  },
3594
3654
  });
3595
3655
  }
3596
- return success({
3597
- stdout,
3598
- stderr,
3599
- });
3600
3656
  }
3601
3657
 
3602
3658
  function execBinary(input) {
@@ -3766,28 +3822,85 @@ async function collectStepsForContext(params) {
3766
3822
  return collectedSteps;
3767
3823
  }
3768
3824
 
3769
- async function runOptionalGitCommand(input) {
3770
- const { label, getCommand, cwd, logger } = input;
3825
+ const ABORT_STEP_WALK_MESSAGE = 'Process aborted';
3826
+ function stepWalkAbortedFailure() {
3827
+ return {
3828
+ success: false,
3829
+ data: {
3830
+ message: ABORT_STEP_WALK_MESSAGE,
3831
+ reason: 'stepWalkFailed',
3832
+ },
3833
+ };
3834
+ }
3835
+ function createStepWalkAbortError() {
3836
+ const error = new Error(ABORT_STEP_WALK_MESSAGE);
3837
+ error.name = 'AbortError';
3838
+ return error;
3839
+ }
3840
+ function isStepWalkAbortError(error) {
3841
+ return (typeof error === 'object'
3842
+ && error !== null
3843
+ && 'name' in error
3844
+ && error.name === 'AbortError');
3845
+ }
3846
+ /**
3847
+ * Await user-hook work, but stop waiting once `signal` aborts so the walk can
3848
+ * unwind (locks/teardown). Does not cancel sync busy-loops on the event loop.
3849
+ */
3850
+ async function awaitUnlessAborted(work, signal) {
3851
+ if (!signal) {
3852
+ return await work;
3853
+ }
3854
+ signal.throwIfAborted();
3855
+ return new Promise((resolve, reject) => {
3856
+ const onAbort = () => {
3857
+ cleanup();
3858
+ reject(createStepWalkAbortError());
3859
+ };
3860
+ const cleanup = () => {
3861
+ signal.removeEventListener('abort', onAbort);
3862
+ };
3863
+ signal.addEventListener('abort', onAbort, { once: true });
3864
+ Promise.resolve(work).then((value) => {
3865
+ cleanup();
3866
+ resolve(value);
3867
+ }, (error) => {
3868
+ cleanup();
3869
+ reject(error);
3870
+ });
3871
+ });
3872
+ }
3873
+ /**
3874
+ * Result-aware runner for `gitAddCommitFn` / `gitPushFn`.
3875
+ * nullish Success → no-op; empty string / Failure / throw / exec fail → failed.
3876
+ */
3877
+ async function runGitResultHook(input) {
3878
+ const { getResult, cwd, logger, execLabel } = input;
3771
3879
  try {
3772
- const command = await getCommand();
3773
- if (command == null || command === '') {
3774
- return 'ok';
3775
- }
3776
- const result = await execAsync(command, { cwd });
3777
- logger.verbose(`${label} ${JSON.stringify(result)}`);
3880
+ const result = await getResult();
3778
3881
  if (!result.success) {
3779
- logger.error(formatExecFailureMessage({ label, failure: result }));
3780
- return 'failed';
3882
+ return { status: 'failed', detail: result.data };
3883
+ }
3884
+ const command = result.data;
3885
+ if (command == null) {
3886
+ return { status: 'ok' };
3887
+ }
3888
+ if (command === '') {
3889
+ return { status: 'failed', detail: 'empty command string' };
3781
3890
  }
3782
- return 'ok';
3891
+ const execResult = await execAsync(command, { cwd });
3892
+ logger.verbose(`${execLabel} ${JSON.stringify(execResult)}`);
3893
+ if (!execResult.success) {
3894
+ return { status: 'failed', detail: execResult.data.message };
3895
+ }
3896
+ return { status: 'ok' };
3783
3897
  }
3784
3898
  catch (error) {
3785
3899
  const message = error instanceof Error ? error.message : String(error);
3786
- logger.error(`Failed to run ${label}: ${message}`);
3787
- return 'failed';
3900
+ return { status: 'failed', detail: message };
3788
3901
  }
3789
3902
  }
3790
- async function executeStepsForContextList({ baseBranch, branchFn, lumpVariables, contextList, gitAddCommandFn, gitCommitCommandFn, gitPushCommandFn, gitCommitMessageFn, projectRoot, steps, setupFn, setupWorkspaceFn, teardownFn, teardownWorkspaceFn, getKeepHistoryFilePathFn, logger: loggerInput, signal, }) {
3903
+ async function executeStepsForContextList({ baseBranch, branchFn, lumpVariables, contextList, gitAddCommitFn, gitPushFn, gitCommitMessageFn, projectRoot, steps, setupFn, setupWorkspaceFn, teardownFn, teardownWorkspaceFn, getKeepHistoryFilePathFn, logger: loggerInput, signal, }) {
3791
3904
  const logger = loggerInput ?? createConsoleLogger({});
3792
3905
  const contextNames = contextList.map(context => context.name);
3793
3906
  logger.verbose(`contextNames ${JSON.stringify(contextNames)}`);
@@ -3813,7 +3926,9 @@ async function executeStepsForContextList({ baseBranch, branchFn, lumpVariables,
3813
3926
  });
3814
3927
  logger.verbose(`setupWorkspaceCommandExec ${JSON.stringify(setupWorkspaceCommandExec)}`);
3815
3928
  if (!setupWorkspaceCommandExec.success) {
3816
- return set(setupWorkspaceCommandExec, ['data', 'message'], `Failed to setup the workspace: ${setupWorkspaceCommandExec.data.message}`);
3929
+ return failure({
3930
+ message: `Failed to setup the workspace: ${setupWorkspaceCommandExec.data.message}`,
3931
+ });
3817
3932
  }
3818
3933
  if (afterExec) {
3819
3934
  await afterExec({ workspacePath });
@@ -3825,6 +3940,10 @@ async function executeStepsForContextList({ baseBranch, branchFn, lumpVariables,
3825
3940
  let runFailure;
3826
3941
  try {
3827
3942
  for (let i = 0; i < contextList.length; i++) {
3943
+ if (signal?.aborted) {
3944
+ runFailure = stepWalkAbortedFailure();
3945
+ break;
3946
+ }
3828
3947
  const context = contextList[i];
3829
3948
  logger.info(contextList.length > 1
3830
3949
  ? `Running context "${context.name}" (${i + 1}/${contextList.length})`
@@ -3837,127 +3956,137 @@ async function executeStepsForContextList({ baseBranch, branchFn, lumpVariables,
3837
3956
  const contextRunState = setupResult?.contextRunState || {};
3838
3957
  let stepWalkFailure;
3839
3958
  async function walkAndExecuteSteps(stepsToExec, currStepIndex) {
3840
- for (let stepIndex = 0; stepIndex < stepsToExec.length; stepIndex++) {
3841
- if (stepWalkFailure) {
3842
- return;
3843
- }
3844
- const step = stepsToExec[stepIndex];
3845
- const nextCallHeadIndex = [...currStepIndex, stepIndex];
3846
- const compositeStepIndex = nextCallHeadIndex.length === 1 ? nextCallHeadIndex[0] : nextCallHeadIndex;
3847
- if (typeof step === 'function' || Array.isArray(step)) {
3848
- let subSteps = [];
3849
- if (typeof step === 'function') {
3850
- subSteps = await step({
3959
+ try {
3960
+ for (let stepIndex = 0; stepIndex < stepsToExec.length; stepIndex++) {
3961
+ if (stepWalkFailure) {
3962
+ return;
3963
+ }
3964
+ signal?.throwIfAborted();
3965
+ const step = stepsToExec[stepIndex];
3966
+ const nextCallHeadIndex = [...currStepIndex, stepIndex];
3967
+ const compositeStepIndex = nextCallHeadIndex.length === 1 ? nextCallHeadIndex[0] : nextCallHeadIndex;
3968
+ if (typeof step === 'function' || Array.isArray(step)) {
3969
+ const subSteps = typeof step === 'function'
3970
+ ? await awaitUnlessAborted(step({
3971
+ context,
3972
+ stepIndex: compositeStepIndex,
3973
+ contextRunState,
3974
+ lumpVariables,
3975
+ }), signal)
3976
+ : step;
3977
+ await walkAndExecuteSteps(subSteps, nextCallHeadIndex);
3978
+ continue;
3979
+ }
3980
+ logger.verbose(`step ${JSON.stringify(step)}`);
3981
+ const { commandFn = () => null, stepVariables, promptFn, postCommandExecFn, continueOnError, timeoutMillis = 1000 * 60 * 30, } = step;
3982
+ const prompt = promptFn
3983
+ ? await awaitUnlessAborted(promptFn({
3851
3984
  context,
3852
3985
  stepIndex: compositeStepIndex,
3853
3986
  contextRunState,
3854
3987
  lumpVariables,
3988
+ stepVariables,
3989
+ }), signal)
3990
+ : '';
3991
+ const command = await awaitUnlessAborted(commandFn({
3992
+ context,
3993
+ prompt,
3994
+ stepIndex: compositeStepIndex,
3995
+ contextRunState,
3996
+ lumpVariables,
3997
+ stepVariables,
3998
+ projectRoot,
3999
+ workspacePath,
4000
+ }), signal);
4001
+ let commandResult = '';
4002
+ let commandSucceeded = true;
4003
+ if (command != null) {
4004
+ const { executable, args, env } = command;
4005
+ logger.verbose(`command for prompt ${executable} ${args.join(' ')}`);
4006
+ if (env != null) {
4007
+ logger.verbose(`command env overrides ${JSON.stringify(env)}`);
4008
+ }
4009
+ logger.verbose(`workspacePath ${workspacePath}`);
4010
+ const commandExec = await execBinary({
4011
+ binaryPath: executable,
4012
+ args,
4013
+ timeoutMillis,
4014
+ stdio: ['inherit', 'pipe', 'pipe'],
4015
+ cwd: workspacePath,
4016
+ signal,
4017
+ ...(env != null ? { env: { ...process.env, ...env } } : {}),
3855
4018
  });
4019
+ logger.verbose(`commandExec ${JSON.stringify(commandExec)}`);
4020
+ if (!commandExec.success) {
4021
+ const aborted = commandExec.data.reason === 'aborted';
4022
+ if (aborted || !continueOnError) {
4023
+ stepWalkFailure = {
4024
+ success: false,
4025
+ data: {
4026
+ message: `Failed to run the command: ${commandExec.data.message}. Command: ${executable} ${args.join(' ')}`,
4027
+ reason: 'stepWalkFailed',
4028
+ },
4029
+ };
4030
+ return;
4031
+ }
4032
+ commandSucceeded = false;
4033
+ commandResult = (commandExec.data.stdout
4034
+ || commandExec.data.stderr
4035
+ || commandExec.data.message
4036
+ || '').toString();
4037
+ logger.verbose(`commandResult ${commandResult}`);
4038
+ }
4039
+ else {
4040
+ commandResult = (commandExec.data.stdout || commandExec.data.stderr || '').toString();
4041
+ logger.verbose(`commandResult ${commandResult}`);
4042
+ }
4043
+ if (commandSucceeded) {
4044
+ const gitStatusAfterCommand = await execAsync(`git status`, { cwd: workspacePath });
4045
+ logger.verbose(`gitStatusCommand ${JSON.stringify(gitStatusAfterCommand.data)}`);
4046
+ }
3856
4047
  }
3857
- else {
3858
- subSteps = step;
3859
- }
3860
- await walkAndExecuteSteps(subSteps, nextCallHeadIndex);
3861
- continue;
3862
- }
3863
- logger.verbose(`step ${JSON.stringify(step)}`);
3864
- const { commandFn = () => null, stepVariables, promptFn, postCommandExecFn, continueOnError, timeoutMillis = 1000 * 60 * 30, } = step;
3865
- const prompt = promptFn
3866
- ? await promptFn({
4048
+ const historyEntry = {
4049
+ commandResult,
4050
+ commandSucceeded,
3867
4051
  context,
4052
+ prompt,
3868
4053
  stepIndex: compositeStepIndex,
3869
4054
  contextRunState,
3870
4055
  lumpVariables,
3871
4056
  stepVariables,
3872
- })
3873
- : '';
3874
- const command = await commandFn({
3875
- context,
3876
- prompt,
3877
- stepIndex: compositeStepIndex,
3878
- contextRunState,
3879
- lumpVariables,
3880
- stepVariables,
3881
- projectRoot,
3882
- workspacePath,
3883
- });
3884
- let commandResult = '';
3885
- let commandSucceeded = true;
3886
- if (command != null) {
3887
- const { executable, args, env } = command;
3888
- logger.verbose(`command for prompt ${executable} ${args.join(' ')}`);
3889
- if (env != null) {
3890
- logger.verbose(`command env overrides ${JSON.stringify(env)}`);
3891
- }
3892
- logger.verbose(`workspacePath ${workspacePath}`);
3893
- const commandExec = await execBinary({
3894
- binaryPath: executable,
3895
- args,
3896
- timeoutMillis,
3897
- stdio: ['inherit', 'pipe', 'pipe'],
3898
- cwd: workspacePath,
4057
+ projectRoot,
4058
+ };
4059
+ const postCommandExecFnInput = {
4060
+ ...historyEntry,
3899
4061
  signal,
3900
- ...(env != null ? { env: { ...process.env, ...env } } : {}),
3901
- });
3902
- logger.verbose(`commandExec ${JSON.stringify(commandExec)}`);
3903
- if (!commandExec.success) {
3904
- const aborted = commandExec.data.reason === 'aborted';
3905
- if (aborted || !continueOnError) {
3906
- stepWalkFailure = {
3907
- success: false,
3908
- data: {
3909
- message: `Failed to run the command: ${commandExec.data.message}. Command: ${executable} ${args.join(' ')}`,
3910
- reason: 'stepWalkFailed',
3911
- },
3912
- };
3913
- return;
4062
+ };
4063
+ logger.verbose(`context is ${JSON.stringify(context)}`);
4064
+ const keepHistoryFilePath = getKeepHistoryFilePathFn(context) || '';
4065
+ logger.verbose(`keepHistoryFilePath ${keepHistoryFilePath}`);
4066
+ if (!!command && keepHistoryFilePath.length > 0) {
4067
+ const appendResult = await appendHistoryEntry({
4068
+ filePath: keepHistoryFilePath,
4069
+ entry: historyEntry,
4070
+ });
4071
+ if (!appendResult.success) {
4072
+ // TODO: sanitize history appending to avoid this warning for certain commands outputs
4073
+ logger.warn(`Failed to append history entry to ${keepHistoryFilePath}: ${appendResult.data}`);
3914
4074
  }
3915
- commandSucceeded = false;
3916
- commandResult = (commandExec.data.stdout
3917
- || commandExec.data.stderr
3918
- || commandExec.data.message
3919
- || '').toString();
3920
- logger.verbose(`commandResult ${commandResult}`);
3921
- }
3922
- else {
3923
- commandResult = (commandExec.data.stdout || commandExec.data.stderr || '').toString();
3924
- logger.verbose(`commandResult ${commandResult}`);
3925
- }
3926
- if (commandSucceeded) {
3927
- const gitStatusAfterCommand = await execAsync(`git status`, { cwd: workspacePath });
3928
- logger.verbose(`gitStatusCommand ${JSON.stringify(gitStatusAfterCommand.data)}`);
3929
4075
  }
3930
- }
3931
- const postCommandExecFnInput = {
3932
- commandResult,
3933
- commandSucceeded,
3934
- context,
3935
- prompt,
3936
- stepIndex: compositeStepIndex,
3937
- contextRunState,
3938
- lumpVariables,
3939
- stepVariables,
3940
- projectRoot,
3941
- };
3942
- logger.verbose(`context is ${JSON.stringify(context)}`);
3943
- const keepHistoryFilePath = getKeepHistoryFilePathFn(context) || '';
3944
- logger.verbose(`keepHistoryFilePath ${keepHistoryFilePath}`);
3945
- if (!!command && keepHistoryFilePath.length > 0) {
3946
- const appendResult = await appendHistoryEntry({
3947
- filePath: keepHistoryFilePath,
3948
- entry: postCommandExecFnInput,
3949
- });
3950
- if (!appendResult.success) {
3951
- // TODO: sanitize history appending to avoid this warning for certain commands outputs
3952
- logger.warn(`Failed to append history entry to ${keepHistoryFilePath}: ${appendResult.data}`);
4076
+ if (postCommandExecFn) {
4077
+ const returnedSteps = await awaitUnlessAborted(postCommandExecFn(postCommandExecFnInput), signal);
4078
+ if (returnedSteps != null && returnedSteps.length > 0) {
4079
+ await walkAndExecuteSteps(returnedSteps, nextCallHeadIndex);
4080
+ }
3953
4081
  }
3954
4082
  }
3955
- if (postCommandExecFn) {
3956
- const returnedSteps = await postCommandExecFn(postCommandExecFnInput);
3957
- if (returnedSteps != null && returnedSteps.length > 0) {
3958
- await walkAndExecuteSteps(returnedSteps, nextCallHeadIndex);
3959
- }
4083
+ }
4084
+ catch (error) {
4085
+ if (isStepWalkAbortError(error)) {
4086
+ stepWalkFailure = stepWalkAbortedFailure();
4087
+ return;
3960
4088
  }
4089
+ throw error;
3961
4090
  }
3962
4091
  }
3963
4092
  try {
@@ -3981,43 +4110,42 @@ async function executeStepsForContextList({ baseBranch, branchFn, lumpVariables,
3981
4110
  runFailure = stepWalkFailure;
3982
4111
  break;
3983
4112
  }
3984
- const perContextInput = {
3985
- ...injectedGitAndWorkspaceFnsInput,
3986
- context,
3987
- };
3988
- const addOutcome = await runOptionalGitCommand({
3989
- label: `git add for context ${context.name}`,
3990
- getCommand: () => gitAddCommandFn(perContextInput),
4113
+ const commitMessage = gitCommitMessageFn({ context, lumpVariables, baseBranch });
4114
+ const addCommitExecLabel = `git add+commit for context ${context.name}`;
4115
+ const addCommitOutcome = await runGitResultHook({
4116
+ getResult: () => gitAddCommitFn({
4117
+ ...injectedGitAndWorkspaceFnsInput,
4118
+ context,
4119
+ commitMessage,
4120
+ }),
3991
4121
  cwd: workspacePath,
3992
4122
  logger,
4123
+ execLabel: addCommitExecLabel,
3993
4124
  });
3994
- if (addOutcome === 'failed') {
4125
+ if (addCommitOutcome.status === 'failed') {
4126
+ const message = `Failed to add and commit for context ${context.name}: ${addCommitOutcome.detail}`;
4127
+ logger.error(message);
3995
4128
  runFailure = {
3996
4129
  success: false,
3997
4130
  data: {
3998
- message: `Failed to add the changes for context ${context.name}`,
4131
+ message,
4132
+ reason: 'gitAddCommitFailed',
3999
4133
  },
4000
4134
  };
4001
4135
  break;
4002
4136
  }
4003
- const commitMessage = gitCommitMessageFn({ context, lumpVariables, baseBranch });
4004
- await runOptionalGitCommand({
4005
- label: `git commit for context ${context.name}`,
4006
- getCommand: () => gitCommitCommandFn({
4007
- ...perContextInput,
4008
- commitMessage,
4009
- }),
4010
- cwd: workspacePath,
4011
- logger,
4012
- });
4013
4137
  }
4014
4138
  if (!runFailure) {
4015
- await runOptionalGitCommand({
4016
- label: `git push on branch ${branchName}`,
4017
- getCommand: () => gitPushCommandFn(injectedGitAndWorkspaceFnsInput),
4139
+ const pushExecLabel = `git push on branch ${branchName}`;
4140
+ const pushOutcome = await runGitResultHook({
4141
+ getResult: () => gitPushFn(injectedGitAndWorkspaceFnsInput),
4018
4142
  cwd: workspacePath,
4019
4143
  logger,
4144
+ execLabel: pushExecLabel,
4020
4145
  });
4146
+ if (pushOutcome.status === 'failed') {
4147
+ logger.error(`Failed to ${pushExecLabel}: ${pushOutcome.detail}`);
4148
+ }
4021
4149
  }
4022
4150
  }
4023
4151
  finally {
@@ -4159,13 +4287,13 @@ async function getContextStatus(params) {
4159
4287
  if (!fetchResult.success)
4160
4288
  return 'toDo';
4161
4289
  }
4162
- const logResult = await execAsync(`git log --remotes=${remoteName} -F --grep=${shellSingleQuote(commitMessage)} --format=${shellSingleQuote('%H %s')}`, { cwd: projectRoot });
4290
+ const logResult = await execAsync(`git log --remotes=${remoteName} -F --grep=${shellSingleQuote(commitMessage)} --format=${shellSingleQuote(GIT_LOG_HASH_BODY_FORMAT)}`, { cwd: projectRoot });
4163
4291
  if (!logResult.success)
4164
4292
  return 'toDo';
4165
4293
  logger?.verbose(`logResult ${JSON.stringify(logResult.data)}`);
4166
4294
  const logResultOutput = logResult.data.stdout || logResult.data.stderr || '';
4167
- const matchingHashes = parseGitLogHashSubjectLines(logResultOutput)
4168
- .filter((entry) => entry.subject === commitMessage)
4295
+ const matchingHashes = parseGitLogHashBodyRecords(logResultOutput)
4296
+ .filter((entry) => commitMessageIncludesMarker(entry.message, commitMessage))
4169
4297
  .map((entry) => entry.hash);
4170
4298
  logger?.verbose(`contextName ${contextName}`);
4171
4299
  logger?.verbose(`remoteName ${remoteName}`);
@@ -4282,14 +4410,11 @@ const contextStatus = ['toDo', 'branchPushed', 'finished'];
4282
4410
  const defaultGitCommitMessageFn = ({ context }) => {
4283
4411
  return `LUMP:${context.name}`;
4284
4412
  };
4285
- const defaultGitPushCommandFn = (input) => {
4286
- return `git push origin ${shellSingleQuote(input.branchName)}`;
4287
- };
4288
- const defaultGitAddCommandFn = () => {
4289
- return `git add .`;
4413
+ const defaultGitAddCommitFn = (input) => {
4414
+ return success(`git add . && git commit --allow-empty -m ${shellSingleQuote(input.commitMessage)}`);
4290
4415
  };
4291
- const defaultGitCommitCommandFn = (input) => {
4292
- return `git commit --allow-empty -m ${shellSingleQuote(input.commitMessage)}`;
4416
+ const defaultGitPushFn = (input) => {
4417
+ return success(`git push origin ${shellSingleQuote(input.branchName)}`);
4293
4418
  };
4294
4419
  const defaultSetupWorkspaceFn = async (input) => {
4295
4420
  const { baseBranch, branchName } = input;
@@ -4327,7 +4452,7 @@ const defaultSetupWorkspaceFnWithWorktree = async (input) => {
4327
4452
  };
4328
4453
 
4329
4454
  async function runLump(input) {
4330
- const { baseBranch, branchFn, lumpVariables: lumpVariablesInput, getContextListFn, gitAddCommandFn = defaultGitAddCommandFn, gitCommitCommandFn = defaultGitCommitCommandFn, gitCommitMessageFn = defaultGitCommitMessageFn, gitPushCommandFn = defaultGitPushCommandFn, numberOfContextsPerBranch = 1, projectRoot, steps, setupFn = () => ({ contextRunState: {} }), setupWorkspaceFn = defaultSetupWorkspaceFn, teardownFn = () => undefined, teardownWorkspaceFn = defaultTeardownWorkspaceFn, getKeepHistoryFilePathFn = () => undefined, logger: loggerInput, signal, refreshRemoteTrackingRefsFn, } = input;
4455
+ const { baseBranch, branchFn, lumpVariables: lumpVariablesInput, getContextListFn, gitAddCommitFn = defaultGitAddCommitFn, gitCommitMessageFn = defaultGitCommitMessageFn, gitPushFn = defaultGitPushFn, numberOfContextsPerBranch = 1, projectRoot, steps, setupFn = () => ({ contextRunState: {} }), setupWorkspaceFn = defaultSetupWorkspaceFn, teardownFn = () => undefined, teardownWorkspaceFn = defaultTeardownWorkspaceFn, getKeepHistoryFilePathFn = () => undefined, logger: loggerInput, signal, refreshRemoteTrackingRefsFn, } = input;
4331
4456
  const lumpVariables = (lumpVariablesInput ?? {});
4332
4457
  const logger = loggerInput ?? createConsoleLogger({});
4333
4458
  const contextListToDoResult = await getToDoContextList({
@@ -4362,10 +4487,9 @@ async function runLump(input) {
4362
4487
  branchFn: branchFn,
4363
4488
  lumpVariables: lumpVariables,
4364
4489
  contextList: nextContextsForBranchList,
4365
- gitAddCommandFn,
4366
- gitCommitCommandFn,
4490
+ gitAddCommitFn,
4367
4491
  gitCommitMessageFn,
4368
- gitPushCommandFn,
4492
+ gitPushFn,
4369
4493
  projectRoot,
4370
4494
  steps,
4371
4495
  setupFn,
@@ -4384,14 +4508,15 @@ async function runLump(input) {
4384
4508
  });
4385
4509
  }
4386
4510
 
4511
+ exports.GIT_LOG_HASH_BODY_FORMAT = GIT_LOG_HASH_BODY_FORMAT;
4387
4512
  exports.appendHistoryEntry = appendHistoryEntry;
4388
4513
  exports.collectStepsForContext = collectStepsForContext;
4514
+ exports.commitMessageIncludesMarker = commitMessageIncludesMarker;
4389
4515
  exports.contextStatus = contextStatus;
4390
4516
  exports.createConsoleLogger = createConsoleLogger;
4391
- exports.defaultGitAddCommandFn = defaultGitAddCommandFn;
4392
- exports.defaultGitCommitCommandFn = defaultGitCommitCommandFn;
4517
+ exports.defaultGitAddCommitFn = defaultGitAddCommitFn;
4393
4518
  exports.defaultGitCommitMessageFn = defaultGitCommitMessageFn;
4394
- exports.defaultGitPushCommandFn = defaultGitPushCommandFn;
4519
+ exports.defaultGitPushFn = defaultGitPushFn;
4395
4520
  exports.defaultSetupWorkspaceFn = defaultSetupWorkspaceFn;
4396
4521
  exports.defaultSetupWorkspaceFnWithWorktree = defaultSetupWorkspaceFnWithWorktree;
4397
4522
  exports.defaultTeardownWorkspaceFn = defaultTeardownWorkspaceFn;
@@ -4408,6 +4533,7 @@ exports.historyFormatFromPath = historyFormatFromPath;
4408
4533
  exports.isProcessAlive = isProcessAlive;
4409
4534
  exports.killProcessTree = killProcessTree;
4410
4535
  exports.nodeErrnoCode = nodeErrnoCode;
4536
+ exports.parseGitLogHashBodyRecords = parseGitLogHashBodyRecords;
4411
4537
  exports.parseGitLogHashSubjectLines = parseGitLogHashSubjectLines;
4412
4538
  exports.pathExists = pathExists;
4413
4539
  exports.readHistoryFile = readHistoryFile;