@lumpcode/core 0.0.16 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +3 -3
  2. package/dist/helpers/executeStepsForContextList/main.d.ts.map +1 -1
  3. package/dist/helpers/getContextStatus/main.d.ts +5 -0
  4. package/dist/helpers/getContextStatus/main.d.ts.map +1 -1
  5. package/dist/helpers/getToDoContextList/main.d.ts +6 -0
  6. package/dist/helpers/getToDoContextList/main.d.ts.map +1 -1
  7. package/dist/helpers/index.d.ts +1 -0
  8. package/dist/helpers/index.d.ts.map +1 -1
  9. package/dist/helpers/refreshRemoteTrackingRefs/index.d.ts +2 -0
  10. package/dist/helpers/refreshRemoteTrackingRefs/index.d.ts.map +1 -0
  11. package/dist/helpers/refreshRemoteTrackingRefs/main.d.ts +11 -0
  12. package/dist/helpers/refreshRemoteTrackingRefs/main.d.ts.map +1 -0
  13. package/dist/index.cjs +289 -144
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.js +286 -145
  16. package/dist/index.js.map +1 -1
  17. package/dist/types/ExecShellFn.d.ts +16 -0
  18. package/dist/types/ExecShellFn.d.ts.map +1 -0
  19. package/dist/types/GitAddCommandFn.d.ts +7 -1
  20. package/dist/types/GitAddCommandFn.d.ts.map +1 -1
  21. package/dist/types/GitCommitCommandFn.d.ts +7 -1
  22. package/dist/types/GitCommitCommandFn.d.ts.map +1 -1
  23. package/dist/types/GitPushCommandFn.d.ts +7 -1
  24. package/dist/types/GitPushCommandFn.d.ts.map +1 -1
  25. package/dist/types/PostCommandExecFn.d.ts +2 -0
  26. package/dist/types/PostCommandExecFn.d.ts.map +1 -1
  27. package/dist/usages/runLump/main.d.ts +6 -1
  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) {
@@ -3766,6 +3800,75 @@ async function collectStepsForContext(params) {
3766
3800
  return collectedSteps;
3767
3801
  }
3768
3802
 
3803
+ const ABORT_STEP_WALK_MESSAGE = 'Process aborted';
3804
+ function stepWalkAbortedFailure() {
3805
+ return {
3806
+ success: false,
3807
+ data: {
3808
+ message: ABORT_STEP_WALK_MESSAGE,
3809
+ reason: 'stepWalkFailed',
3810
+ },
3811
+ };
3812
+ }
3813
+ function createStepWalkAbortError() {
3814
+ const error = new Error(ABORT_STEP_WALK_MESSAGE);
3815
+ error.name = 'AbortError';
3816
+ return error;
3817
+ }
3818
+ function isStepWalkAbortError(error) {
3819
+ return (typeof error === 'object'
3820
+ && error !== null
3821
+ && 'name' in error
3822
+ && error.name === 'AbortError');
3823
+ }
3824
+ /**
3825
+ * Await user-hook work, but stop waiting once `signal` aborts so the walk can
3826
+ * unwind (locks/teardown). Does not cancel sync busy-loops on the event loop.
3827
+ */
3828
+ async function awaitUnlessAborted(work, signal) {
3829
+ if (!signal) {
3830
+ return await work;
3831
+ }
3832
+ signal.throwIfAborted();
3833
+ return new Promise((resolve, reject) => {
3834
+ const onAbort = () => {
3835
+ cleanup();
3836
+ reject(createStepWalkAbortError());
3837
+ };
3838
+ const cleanup = () => {
3839
+ signal.removeEventListener('abort', onAbort);
3840
+ };
3841
+ signal.addEventListener('abort', onAbort, { once: true });
3842
+ Promise.resolve(work).then((value) => {
3843
+ cleanup();
3844
+ resolve(value);
3845
+ }, (error) => {
3846
+ cleanup();
3847
+ reject(error);
3848
+ });
3849
+ });
3850
+ }
3851
+ async function runOptionalGitCommand(input) {
3852
+ const { label, getCommand, cwd, logger } = input;
3853
+ try {
3854
+ const command = await getCommand();
3855
+ if (command == null || command === '') {
3856
+ return 'ok';
3857
+ }
3858
+ const result = await execAsync(command, { cwd });
3859
+ logger.verbose(`${label} ${JSON.stringify(result)}`);
3860
+ if (!result.success) {
3861
+ logger.error(formatExecFailureMessage({ label, failure: result }));
3862
+ return 'failed';
3863
+ }
3864
+ return 'ok';
3865
+ }
3866
+ catch (error) {
3867
+ const message = error instanceof Error ? error.message : String(error);
3868
+ logger.error(`Failed to run ${label}: ${message}`);
3869
+ return 'failed';
3870
+ }
3871
+ }
3769
3872
  async function executeStepsForContextList({ baseBranch, branchFn, lumpVariables, contextList, gitAddCommandFn, gitCommitCommandFn, gitPushCommandFn, gitCommitMessageFn, projectRoot, steps, setupFn, setupWorkspaceFn, teardownFn, teardownWorkspaceFn, getKeepHistoryFilePathFn, logger: loggerInput, signal, }) {
3770
3873
  const logger = loggerInput ?? createConsoleLogger({});
3771
3874
  const contextNames = contextList.map(context => context.name);
@@ -3804,6 +3907,10 @@ async function executeStepsForContextList({ baseBranch, branchFn, lumpVariables,
3804
3907
  let runFailure;
3805
3908
  try {
3806
3909
  for (let i = 0; i < contextList.length; i++) {
3910
+ if (signal?.aborted) {
3911
+ runFailure = stepWalkAbortedFailure();
3912
+ break;
3913
+ }
3807
3914
  const context = contextList[i];
3808
3915
  logger.info(contextList.length > 1
3809
3916
  ? `Running context "${context.name}" (${i + 1}/${contextList.length})`
@@ -3816,127 +3923,137 @@ async function executeStepsForContextList({ baseBranch, branchFn, lumpVariables,
3816
3923
  const contextRunState = setupResult?.contextRunState || {};
3817
3924
  let stepWalkFailure;
3818
3925
  async function walkAndExecuteSteps(stepsToExec, currStepIndex) {
3819
- for (let stepIndex = 0; stepIndex < stepsToExec.length; stepIndex++) {
3820
- if (stepWalkFailure) {
3821
- return;
3822
- }
3823
- const step = stepsToExec[stepIndex];
3824
- const nextCallHeadIndex = [...currStepIndex, stepIndex];
3825
- const compositeStepIndex = nextCallHeadIndex.length === 1 ? nextCallHeadIndex[0] : nextCallHeadIndex;
3826
- if (typeof step === 'function' || Array.isArray(step)) {
3827
- let subSteps = [];
3828
- if (typeof step === 'function') {
3829
- subSteps = await step({
3926
+ try {
3927
+ for (let stepIndex = 0; stepIndex < stepsToExec.length; stepIndex++) {
3928
+ if (stepWalkFailure) {
3929
+ return;
3930
+ }
3931
+ signal?.throwIfAborted();
3932
+ const step = stepsToExec[stepIndex];
3933
+ const nextCallHeadIndex = [...currStepIndex, stepIndex];
3934
+ const compositeStepIndex = nextCallHeadIndex.length === 1 ? nextCallHeadIndex[0] : nextCallHeadIndex;
3935
+ if (typeof step === 'function' || Array.isArray(step)) {
3936
+ const subSteps = typeof step === 'function'
3937
+ ? await awaitUnlessAborted(step({
3938
+ context,
3939
+ stepIndex: compositeStepIndex,
3940
+ contextRunState,
3941
+ lumpVariables,
3942
+ }), signal)
3943
+ : step;
3944
+ await walkAndExecuteSteps(subSteps, nextCallHeadIndex);
3945
+ continue;
3946
+ }
3947
+ logger.verbose(`step ${JSON.stringify(step)}`);
3948
+ const { commandFn = () => null, stepVariables, promptFn, postCommandExecFn, continueOnError, timeoutMillis = 1000 * 60 * 30, } = step;
3949
+ const prompt = promptFn
3950
+ ? await awaitUnlessAborted(promptFn({
3830
3951
  context,
3831
3952
  stepIndex: compositeStepIndex,
3832
3953
  contextRunState,
3833
3954
  lumpVariables,
3955
+ stepVariables,
3956
+ }), signal)
3957
+ : '';
3958
+ const command = await awaitUnlessAborted(commandFn({
3959
+ context,
3960
+ prompt,
3961
+ stepIndex: compositeStepIndex,
3962
+ contextRunState,
3963
+ lumpVariables,
3964
+ stepVariables,
3965
+ projectRoot,
3966
+ workspacePath,
3967
+ }), signal);
3968
+ let commandResult = '';
3969
+ let commandSucceeded = true;
3970
+ if (command != null) {
3971
+ const { executable, args, env } = command;
3972
+ logger.verbose(`command for prompt ${executable} ${args.join(' ')}`);
3973
+ if (env != null) {
3974
+ logger.verbose(`command env overrides ${JSON.stringify(env)}`);
3975
+ }
3976
+ logger.verbose(`workspacePath ${workspacePath}`);
3977
+ const commandExec = await execBinary({
3978
+ binaryPath: executable,
3979
+ args,
3980
+ timeoutMillis,
3981
+ stdio: ['inherit', 'pipe', 'pipe'],
3982
+ cwd: workspacePath,
3983
+ signal,
3984
+ ...(env != null ? { env: { ...process.env, ...env } } : {}),
3834
3985
  });
3986
+ logger.verbose(`commandExec ${JSON.stringify(commandExec)}`);
3987
+ if (!commandExec.success) {
3988
+ const aborted = commandExec.data.reason === 'aborted';
3989
+ if (aborted || !continueOnError) {
3990
+ stepWalkFailure = {
3991
+ success: false,
3992
+ data: {
3993
+ message: `Failed to run the command: ${commandExec.data.message}. Command: ${executable} ${args.join(' ')}`,
3994
+ reason: 'stepWalkFailed',
3995
+ },
3996
+ };
3997
+ return;
3998
+ }
3999
+ commandSucceeded = false;
4000
+ commandResult = (commandExec.data.stdout
4001
+ || commandExec.data.stderr
4002
+ || commandExec.data.message
4003
+ || '').toString();
4004
+ logger.verbose(`commandResult ${commandResult}`);
4005
+ }
4006
+ else {
4007
+ commandResult = (commandExec.data.stdout || commandExec.data.stderr || '').toString();
4008
+ logger.verbose(`commandResult ${commandResult}`);
4009
+ }
4010
+ if (commandSucceeded) {
4011
+ const gitStatusAfterCommand = await execAsync(`git status`, { cwd: workspacePath });
4012
+ logger.verbose(`gitStatusCommand ${JSON.stringify(gitStatusAfterCommand.data)}`);
4013
+ }
3835
4014
  }
3836
- else {
3837
- subSteps = step;
3838
- }
3839
- await walkAndExecuteSteps(subSteps, nextCallHeadIndex);
3840
- continue;
3841
- }
3842
- logger.verbose(`step ${JSON.stringify(step)}`);
3843
- const { commandFn = () => null, stepVariables, promptFn, postCommandExecFn, continueOnError, timeoutMillis = 1000 * 60 * 30, } = step;
3844
- const prompt = promptFn
3845
- ? await promptFn({
4015
+ const historyEntry = {
4016
+ commandResult,
4017
+ commandSucceeded,
3846
4018
  context,
4019
+ prompt,
3847
4020
  stepIndex: compositeStepIndex,
3848
4021
  contextRunState,
3849
4022
  lumpVariables,
3850
4023
  stepVariables,
3851
- })
3852
- : '';
3853
- const command = await commandFn({
3854
- context,
3855
- prompt,
3856
- stepIndex: compositeStepIndex,
3857
- contextRunState,
3858
- lumpVariables,
3859
- stepVariables,
3860
- projectRoot,
3861
- workspacePath,
3862
- });
3863
- let commandResult = '';
3864
- let commandSucceeded = true;
3865
- if (command != null) {
3866
- const { executable, args, env } = command;
3867
- logger.verbose(`command for prompt ${executable} ${args.join(' ')}`);
3868
- if (env != null) {
3869
- logger.verbose(`command env overrides ${JSON.stringify(env)}`);
3870
- }
3871
- logger.verbose(`workspacePath ${workspacePath}`);
3872
- const commandExec = await execBinary({
3873
- binaryPath: executable,
3874
- args,
3875
- timeoutMillis,
3876
- stdio: ['inherit', 'pipe', 'pipe'],
3877
- cwd: workspacePath,
4024
+ projectRoot,
4025
+ };
4026
+ const postCommandExecFnInput = {
4027
+ ...historyEntry,
3878
4028
  signal,
3879
- ...(env != null ? { env: { ...process.env, ...env } } : {}),
3880
- });
3881
- logger.verbose(`commandExec ${JSON.stringify(commandExec)}`);
3882
- if (!commandExec.success) {
3883
- const aborted = commandExec.data.reason === 'aborted';
3884
- if (aborted || !continueOnError) {
3885
- stepWalkFailure = {
3886
- success: false,
3887
- data: {
3888
- message: `Failed to run the command: ${commandExec.data.message}. Command: ${executable} ${args.join(' ')}`,
3889
- reason: 'stepWalkFailed',
3890
- },
3891
- };
3892
- return;
4029
+ };
4030
+ logger.verbose(`context is ${JSON.stringify(context)}`);
4031
+ const keepHistoryFilePath = getKeepHistoryFilePathFn(context) || '';
4032
+ logger.verbose(`keepHistoryFilePath ${keepHistoryFilePath}`);
4033
+ if (!!command && keepHistoryFilePath.length > 0) {
4034
+ const appendResult = await appendHistoryEntry({
4035
+ filePath: keepHistoryFilePath,
4036
+ entry: historyEntry,
4037
+ });
4038
+ if (!appendResult.success) {
4039
+ // TODO: sanitize history appending to avoid this warning for certain commands outputs
4040
+ logger.warn(`Failed to append history entry to ${keepHistoryFilePath}: ${appendResult.data}`);
3893
4041
  }
3894
- commandSucceeded = false;
3895
- commandResult = (commandExec.data.stdout
3896
- || commandExec.data.stderr
3897
- || commandExec.data.message
3898
- || '').toString();
3899
- logger.verbose(`commandResult ${commandResult}`);
3900
4042
  }
3901
- else {
3902
- commandResult = (commandExec.data.stdout || commandExec.data.stderr || '').toString();
3903
- logger.verbose(`commandResult ${commandResult}`);
3904
- }
3905
- if (commandSucceeded) {
3906
- const gitStatusAfterCommand = await execAsync(`git status`, { cwd: workspacePath });
3907
- logger.verbose(`gitStatusCommand ${JSON.stringify(gitStatusAfterCommand.data)}`);
3908
- }
3909
- }
3910
- const postCommandExecFnInput = {
3911
- commandResult,
3912
- commandSucceeded,
3913
- context,
3914
- prompt,
3915
- stepIndex: compositeStepIndex,
3916
- contextRunState,
3917
- lumpVariables,
3918
- stepVariables,
3919
- projectRoot,
3920
- };
3921
- logger.verbose(`context is ${JSON.stringify(context)}`);
3922
- const keepHistoryFilePath = getKeepHistoryFilePathFn(context) || '';
3923
- logger.verbose(`keepHistoryFilePath ${keepHistoryFilePath}`);
3924
- if (!!command && keepHistoryFilePath.length > 0) {
3925
- const appendResult = await appendHistoryEntry({
3926
- filePath: keepHistoryFilePath,
3927
- entry: postCommandExecFnInput,
3928
- });
3929
- if (!appendResult.success) {
3930
- // TODO: sanitize history appending to avoid this warning for certain commands outputs
3931
- logger.warn(`Failed to append history entry to ${keepHistoryFilePath}: ${appendResult.data}`);
4043
+ if (postCommandExecFn) {
4044
+ const returnedSteps = await awaitUnlessAborted(postCommandExecFn(postCommandExecFnInput), signal);
4045
+ if (returnedSteps != null && returnedSteps.length > 0) {
4046
+ await walkAndExecuteSteps(returnedSteps, nextCallHeadIndex);
4047
+ }
3932
4048
  }
3933
4049
  }
3934
- if (postCommandExecFn) {
3935
- const returnedSteps = await postCommandExecFn(postCommandExecFnInput);
3936
- if (returnedSteps != null && returnedSteps.length > 0) {
3937
- await walkAndExecuteSteps(returnedSteps, nextCallHeadIndex);
3938
- }
4050
+ }
4051
+ catch (error) {
4052
+ if (isStepWalkAbortError(error)) {
4053
+ stepWalkFailure = stepWalkAbortedFailure();
4054
+ return;
3939
4055
  }
4056
+ throw error;
3940
4057
  }
3941
4058
  }
3942
4059
  try {
@@ -3964,39 +4081,39 @@ async function executeStepsForContextList({ baseBranch, branchFn, lumpVariables,
3964
4081
  ...injectedGitAndWorkspaceFnsInput,
3965
4082
  context,
3966
4083
  };
3967
- const gitAddCommand = await execAsync(gitAddCommandFn(perContextInput), { cwd: workspacePath });
3968
- logger.verbose(`gitAddCommand ${JSON.stringify(gitAddCommand)}`);
3969
- if (!gitAddCommand.success) {
4084
+ const addOutcome = await runOptionalGitCommand({
4085
+ label: `git add for context ${context.name}`,
4086
+ getCommand: () => gitAddCommandFn(perContextInput),
4087
+ cwd: workspacePath,
4088
+ logger,
4089
+ });
4090
+ if (addOutcome === 'failed') {
3970
4091
  runFailure = {
3971
4092
  success: false,
3972
4093
  data: {
3973
- message: `Failed to add the changes for context ${context.name}: ${gitAddCommand.data.message}`,
4094
+ message: `Failed to add the changes for context ${context.name}`,
3974
4095
  },
3975
4096
  };
3976
4097
  break;
3977
4098
  }
3978
4099
  const commitMessage = gitCommitMessageFn({ context, lumpVariables, baseBranch });
3979
- const commitCommand = await execAsync(gitCommitCommandFn({
3980
- ...perContextInput,
3981
- commitMessage,
3982
- }), { cwd: workspacePath });
3983
- logger.verbose(`commitCommand ${JSON.stringify(commitCommand)}`);
3984
- if (!commitCommand.success) {
3985
- logger.error(formatExecFailureMessage({
3986
- label: `git commit for context ${context.name}`,
3987
- failure: commitCommand,
3988
- }));
3989
- }
4100
+ await runOptionalGitCommand({
4101
+ label: `git commit for context ${context.name}`,
4102
+ getCommand: () => gitCommitCommandFn({
4103
+ ...perContextInput,
4104
+ commitMessage,
4105
+ }),
4106
+ cwd: workspacePath,
4107
+ logger,
4108
+ });
3990
4109
  }
3991
4110
  if (!runFailure) {
3992
- const pushCommand = await execAsync(gitPushCommandFn(injectedGitAndWorkspaceFnsInput), { cwd: workspacePath });
3993
- logger.verbose(`pushCommand ${JSON.stringify(pushCommand)}`);
3994
- if (!pushCommand.success) {
3995
- logger.error(formatExecFailureMessage({
3996
- label: `git push on branch ${branchName}`,
3997
- failure: pushCommand,
3998
- }));
3999
- }
4111
+ await runOptionalGitCommand({
4112
+ label: `git push on branch ${branchName}`,
4113
+ getCommand: () => gitPushCommandFn(injectedGitAndWorkspaceFnsInput),
4114
+ cwd: workspacePath,
4115
+ logger,
4116
+ });
4000
4117
  }
4001
4118
  }
4002
4119
  finally {
@@ -4112,25 +4229,41 @@ async function scanDirectory({ dirPath, allPaths, gitignoresMap, projectRoot, lo
4112
4229
  }
4113
4230
  }
4114
4231
 
4232
+ /**
4233
+ * Refreshes remote-tracking refs for status / planning (one network fetch).
4234
+ * Uses `--no-write-fetch-head` so concurrent preflight/pull paths are safer.
4235
+ */
4236
+ const refreshRemoteTrackingRefs = async (input) => {
4237
+ const { projectRoot, remoteName = 'origin' } = input;
4238
+ const result = await execAsync(`git fetch --prune --no-write-fetch-head ${remoteName}`, { cwd: projectRoot });
4239
+ if (!result.success) {
4240
+ return failure(result.data.message);
4241
+ }
4242
+ return success(undefined);
4243
+ };
4244
+
4115
4245
  async function getContextStatus(params) {
4116
- const { contextName, gitCommitMessageFn, projectRoot, baseBranch, lumpVariables: lumpVariablesInput, contextVariables = {}, remoteName = "origin", logger, } = params;
4246
+ const { contextName, gitCommitMessageFn, projectRoot, baseBranch, lumpVariables: lumpVariablesInput, contextVariables = {}, remoteName = "origin", logger, skipFetch = false, } = params;
4117
4247
  const lumpVariables = (lumpVariablesInput ?? {});
4118
4248
  const commitMessage = gitCommitMessageFn({
4119
4249
  context: { name: contextName, variables: contextVariables },
4120
4250
  lumpVariables,
4121
4251
  baseBranch,
4122
4252
  });
4123
- const fetchAll = await execAsync(`git fetch --all --prune`, { cwd: projectRoot });
4124
- if (!fetchAll.success)
4125
- return 'toDo';
4126
- const logResult = await execAsync(`git log --remotes=${remoteName} -F --grep=${shellSingleQuote(commitMessage)} --format=${shellSingleQuote('%H %s')}`, { cwd: projectRoot });
4253
+ if (!skipFetch) {
4254
+ const fetchResult = await refreshRemoteTrackingRefs({ projectRoot, remoteName });
4255
+ if (!fetchResult.success)
4256
+ return 'toDo';
4257
+ }
4258
+ const logResult = await execAsync(`git log --remotes=${remoteName} -F --grep=${shellSingleQuote(commitMessage)} --format=${shellSingleQuote(GIT_LOG_HASH_BODY_FORMAT)}`, { cwd: projectRoot });
4127
4259
  if (!logResult.success)
4128
4260
  return 'toDo';
4129
4261
  logger?.verbose(`logResult ${JSON.stringify(logResult.data)}`);
4130
4262
  const logResultOutput = logResult.data.stdout || logResult.data.stderr || '';
4131
- const matchingHashes = parseGitLogHashSubjectLines(logResultOutput)
4132
- .filter((entry) => entry.subject === commitMessage)
4263
+ const matchingHashes = parseGitLogHashBodyRecords(logResultOutput)
4264
+ .filter((entry) => commitMessageIncludesMarker(entry.message, commitMessage))
4133
4265
  .map((entry) => entry.hash);
4266
+ logger?.verbose(`contextName ${contextName}`);
4134
4267
  logger?.verbose(`remoteName ${remoteName}`);
4135
4268
  logger?.verbose(`baseBranch ${baseBranch}`);
4136
4269
  logger?.verbose(`commitMessage ${commitMessage}`);
@@ -4185,7 +4318,7 @@ function validateContextListNames(contextList) {
4185
4318
  }
4186
4319
 
4187
4320
  async function getToDoContextList(params) {
4188
- const { getContextListFn, lumpVariables, gitCommitMessageFn, projectRoot, baseBranch, logger } = params;
4321
+ const { getContextListFn, lumpVariables, gitCommitMessageFn, projectRoot, baseBranch, logger, refreshRemoteTrackingRefsFn = refreshRemoteTrackingRefs, } = params;
4189
4322
  const codeBasePathsResult = await getCodeBasePaths({ cwd: projectRoot, logger });
4190
4323
  if (!codeBasePathsResult.success) {
4191
4324
  return failure({
@@ -4203,21 +4336,28 @@ async function getToDoContextList(params) {
4203
4336
  const allCtxNames = contextList.flatMap(context => [context.name, ...(context.options?.dependsOnContexts ?? [])]);
4204
4337
  const allCtxNamesSet = new Set(allCtxNames);
4205
4338
  const allCtxNamesList = Array.from(allCtxNamesSet);
4206
- const contextStatusList = await Promise.all(allCtxNamesList.map((contextName) => {
4207
- return getContextStatus({
4208
- contextName: contextName,
4339
+ const refreshResult = await refreshRemoteTrackingRefsFn({ projectRoot });
4340
+ let contextStatusMap;
4341
+ if (!refreshResult.success) {
4342
+ logger?.warn(`Failed to refresh remote-tracking refs for context status; treating contexts as toDo: ${refreshResult.data}`);
4343
+ contextStatusMap = new Map(allCtxNamesList.map((name) => [name, 'toDo']));
4344
+ }
4345
+ else {
4346
+ const contextStatusList = await Promise.all(allCtxNamesList.map((contextName) => getContextStatus({
4347
+ contextName,
4209
4348
  contextVariables: {}, // TODO: Remove contextVariables from getContextStatus, really not needed
4210
4349
  gitCommitMessageFn,
4211
4350
  lumpVariables,
4212
4351
  projectRoot,
4213
4352
  baseBranch,
4214
4353
  logger,
4215
- });
4216
- }));
4217
- const contextStatusMap = new Map(allCtxNamesList.map((contextName, i) => [contextName, contextStatusList[i]]));
4354
+ skipFetch: true,
4355
+ })));
4356
+ contextStatusMap = new Map(allCtxNamesList.map((contextName, i) => [contextName, contextStatusList[i]]));
4357
+ }
4218
4358
  const contextListToDo = contextList
4219
- .filter((context, contextIndex) => {
4220
- const contextStatus = contextStatusList[contextIndex];
4359
+ .filter((context) => {
4360
+ const contextStatus = contextStatusMap.get(context.name);
4221
4361
  if (contextStatus && contextStatus !== 'toDo')
4222
4362
  return false;
4223
4363
  const deps = context.options?.dependsOnContexts;
@@ -4283,7 +4423,7 @@ const defaultSetupWorkspaceFnWithWorktree = async (input) => {
4283
4423
  };
4284
4424
 
4285
4425
  async function runLump(input) {
4286
- 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, } = input;
4426
+ 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;
4287
4427
  const lumpVariables = (lumpVariablesInput ?? {});
4288
4428
  const logger = loggerInput ?? createConsoleLogger({});
4289
4429
  const contextListToDoResult = await getToDoContextList({
@@ -4293,6 +4433,7 @@ async function runLump(input) {
4293
4433
  baseBranch,
4294
4434
  gitCommitMessageFn,
4295
4435
  logger,
4436
+ refreshRemoteTrackingRefsFn,
4296
4437
  });
4297
4438
  if (!contextListToDoResult.success) {
4298
4439
  return set(contextListToDoResult, ['data', 'message'], "Error in runLump: Failed to get to do context list. Original Error: " + contextListToDoResult.data.message);
@@ -4339,8 +4480,10 @@ async function runLump(input) {
4339
4480
  });
4340
4481
  }
4341
4482
 
4483
+ exports.GIT_LOG_HASH_BODY_FORMAT = GIT_LOG_HASH_BODY_FORMAT;
4342
4484
  exports.appendHistoryEntry = appendHistoryEntry;
4343
4485
  exports.collectStepsForContext = collectStepsForContext;
4486
+ exports.commitMessageIncludesMarker = commitMessageIncludesMarker;
4344
4487
  exports.contextStatus = contextStatus;
4345
4488
  exports.createConsoleLogger = createConsoleLogger;
4346
4489
  exports.defaultGitAddCommandFn = defaultGitAddCommandFn;
@@ -4363,9 +4506,11 @@ exports.historyFormatFromPath = historyFormatFromPath;
4363
4506
  exports.isProcessAlive = isProcessAlive;
4364
4507
  exports.killProcessTree = killProcessTree;
4365
4508
  exports.nodeErrnoCode = nodeErrnoCode;
4509
+ exports.parseGitLogHashBodyRecords = parseGitLogHashBodyRecords;
4366
4510
  exports.parseGitLogHashSubjectLines = parseGitLogHashSubjectLines;
4367
4511
  exports.pathExists = pathExists;
4368
4512
  exports.readHistoryFile = readHistoryFile;
4513
+ exports.refreshRemoteTrackingRefs = refreshRemoteTrackingRefs;
4369
4514
  exports.resolveSpawnExecutable = resolveSpawnExecutable;
4370
4515
  exports.runLump = runLump;
4371
4516
  exports.set = set;