claude-git-hooks 2.13.0 → 2.14.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.
@@ -6,8 +6,7 @@
6
6
  import { execSync } from 'child_process';
7
7
  import fs from 'fs';
8
8
  import path from 'path';
9
- import { executeClaudeWithRetry, extractJSON } from '../utils/claude-client.js';
10
- import { loadPrompt } from '../utils/prompt-builder.js';
9
+ import { analyzeBranchForPR } from '../utils/pr-metadata-engine.js';
11
10
  import { getConfig } from '../config.js';
12
11
  import { getOrPromptTaskId, formatWithTaskId } from '../utils/task-id.js';
13
12
  import { getReviewersForFiles } from '../utils/github-client.js';
@@ -82,7 +81,7 @@ export async function runCreatePr(args) {
82
81
  const taskId = await getOrPromptTaskId({
83
82
  prompt: true, // DO prompt for PRs (unlike commit messages)
84
83
  required: false, // Allow skipping
85
- config: config // Pass config for custom pattern
84
+ config // Pass config for custom pattern
86
85
  });
87
86
  logger.debug('create-pr', 'Task ID determined', { taskId });
88
87
 
@@ -336,90 +335,26 @@ export async function runCreatePr(args) {
336
335
  }
337
336
  }
338
337
 
339
- // Step 6: Update remote and check for differences
340
- logger.debug('create-pr', 'Step 6: Fetching latest changes from remote');
341
- execSync('git fetch', { stdio: 'ignore' });
342
- const compareWith = `origin/${baseBranch}...HEAD`;
343
-
344
- try {
345
- execSync(`git rev-parse --verify origin/${baseBranch}`, { stdio: 'ignore' });
346
- } catch (e) {
347
- error(`Base branch origin/${baseBranch} does not exist`);
348
- return;
349
- }
350
-
351
- let diffFiles;
352
- try {
353
- diffFiles = execSync(`git diff ${compareWith} --name-only`, { encoding: 'utf8' }).trim();
354
- if (!diffFiles) {
355
- showWarning('No differences with remote branch. Nothing to create a PR for.');
356
- return;
357
- }
358
- } catch (e) {
359
- error('Error getting differences: ' + e.message);
360
- return;
361
- }
338
+ // Step 6: Generate PR metadata using engine
339
+ logger.debug('create-pr', 'Step 6: Generating PR metadata with engine');
340
+ showInfo('Generating PR metadata with Claude...');
362
341
 
363
- const filesArray = diffFiles.split('\n').filter(f => f.trim());
364
- logger.debug('create-pr', 'Modified files detected', {
365
- fileCount: filesArray.length,
366
- files: filesArray
342
+ const { success: engineSuccess, result: analysisResult, error: engineError } = await analyzeBranchForPR(baseBranch, {
343
+ hook: 'create-pr'
367
344
  });
368
- showInfo(`Found ${filesArray.length} modified file(s)`);
369
345
 
370
- // Step 7: Generate PR metadata with Claude (reuse analyze-diff logic)
371
- logger.debug('create-pr', 'Step 7: Generating PR metadata with Claude');
372
- let fullDiff, commits;
373
- try {
374
- fullDiff = execSync(`git diff ${compareWith}`, { encoding: 'utf8' });
375
- commits = execSync(`git log origin/${baseBranch}..HEAD --oneline`, { encoding: 'utf8' }).trim();
376
- } catch (e) {
377
- error('Error getting diff or commits: ' + e.message);
346
+ if (!engineSuccess) {
347
+ logger.error('create-pr', 'Failed to generate PR metadata', { error: engineError });
348
+ error(engineError || 'Failed to generate PR metadata');
378
349
  return;
379
350
  }
380
351
 
381
- const truncatedDiff = fullDiff.length > 50000
382
- ? fullDiff.substring(0, 50000) + '\n... (truncated)'
383
- : fullDiff;
384
-
385
- const contextDescription = `${currentBranch} vs origin/${baseBranch}`;
386
- const prompt = await loadPrompt('ANALYZE_DIFF.md', {
387
- CONTEXT_DESCRIPTION: contextDescription,
388
- SUBAGENT_INSTRUCTION: '',
389
- COMMITS: commits,
390
- DIFF_FILES: diffFiles,
391
- FULL_DIFF: truncatedDiff
392
- });
393
-
394
- showInfo('Generating PR metadata with Claude...');
395
- logger.debug('create-pr', 'Calling Claude with prompt', { promptLength: prompt.length });
396
-
397
- // Prepare telemetry context for create-pr
398
- const telemetryContext = {
352
+ const filesArray = analysisResult.context.files;
353
+ logger.debug('create-pr', 'PR metadata generated', {
399
354
  fileCount: filesArray.length,
400
- batchSize: filesArray.length,
401
- totalBatches: 1,
402
- model: 'sonnet', // create-pr always uses main model
403
- hook: 'create-pr'
404
- };
405
-
406
- const response = await executeClaudeWithRetry(prompt, {
407
- timeout: 180000,
408
- telemetryContext
355
+ hasPrTitle: !!analysisResult.prTitle
409
356
  });
410
- logger.debug('create-pr', 'Claude response received', { responseLength: response.length });
411
-
412
- const analysisResult = extractJSON(response);
413
- logger.debug('create-pr', 'Analysis result extracted', {
414
- hasResult: !!analysisResult,
415
- hasPrTitle: !!analysisResult?.prTitle
416
- });
417
-
418
- if (!analysisResult || !analysisResult.prTitle) {
419
- logger.error('create-pr', 'Failed to generate PR metadata from analysis', { analysisResult });
420
- error('Failed to generate PR metadata from analysis');
421
- return;
422
- }
357
+ showInfo(`Found ${filesArray.length} modified file(s)`);
423
358
 
424
359
  // Step 8: Prepare PR data
425
360
  logger.debug('create-pr', 'Step 8: Preparing PR data');
@@ -562,7 +497,7 @@ export async function runCreatePr(args) {
562
497
 
563
498
  } catch (err) {
564
499
  logger.error('create-pr', 'Error creating PR', err);
565
- showError('Error creating PR: ' + err.message);
500
+ showError(`Error creating PR: ${err.message}`);
566
501
 
567
502
  if (err.context) {
568
503
  logger.debug('create-pr', 'Error context', err.context);
@@ -193,8 +193,15 @@ export async function updateConfig(propertyPath, value, options = {}) {
193
193
  }
194
194
 
195
195
  // Set value at propertyPath (support dot notation like 'system.debug')
196
+ // For v2.8.0 format, write inside 'overrides' so loadUserConfig picks it up
196
197
  const pathParts = propertyPath.split('.');
197
198
  let current = config;
199
+ if (config.version === '2.8.0') {
200
+ if (!config.overrides || typeof config.overrides !== 'object') {
201
+ config.overrides = {};
202
+ }
203
+ current = config.overrides;
204
+ }
198
205
  for (let i = 0; i < pathParts.length - 1; i++) {
199
206
  const part = pathParts[i];
200
207
  if (!current[part] || typeof current[part] !== 'object') {
@@ -380,7 +380,12 @@ const executeClaude = (prompt, { timeout = 120000, allowedTools = [] } = {}) =>
380
380
  reject(new ClaudeClientError('Claude CLI execution timed out', {
381
381
  context: {
382
382
  elapsedTime,
383
- timeoutValue: timeout
383
+ timeoutValue: timeout,
384
+ errorInfo: {
385
+ type: ClaudeErrorType.TIMEOUT,
386
+ elapsedTime,
387
+ timeoutValue: timeout
388
+ }
384
389
  }
385
390
  }));
386
391
  }, timeout);
@@ -353,5 +353,6 @@ const formatGenericError = (errorInfo) => {
353
353
  export const isRecoverableError = (errorInfo) => {
354
354
  return errorInfo.type === ClaudeErrorType.EXECUTION_ERROR ||
355
355
  errorInfo.type === ClaudeErrorType.RATE_LIMIT ||
356
- errorInfo.type === ClaudeErrorType.NETWORK;
356
+ errorInfo.type === ClaudeErrorType.NETWORK ||
357
+ errorInfo.type === ClaudeErrorType.TIMEOUT;
357
358
  };
@@ -13,8 +13,10 @@
13
13
  */
14
14
 
15
15
  import { execSync } from 'child_process';
16
+ import fs from 'fs';
16
17
  import path from 'path';
17
18
  import logger from './logger.js';
19
+ import { sanitizeBranchName } from './sanitize.js';
18
20
 
19
21
  /**
20
22
  * Custom error for git operation failures
@@ -616,6 +618,82 @@ const createCommit = (message, { noVerify = false } = {}) => {
616
618
  }
617
619
  };
618
620
 
621
+ /**
622
+ * Stages files for commit
623
+ * Why: Prepares specific files for git commit (used by version bumping, changelog generation)
624
+ *
625
+ * @param {Array<string>} filePaths - Array of absolute file paths to stage
626
+ * @returns {Object} Result: { success: boolean, stagedFiles: Array<string>, error: string }
627
+ */
628
+ const stageFiles = (filePaths) => {
629
+ logger.debug('git-operations - stageFiles', 'Staging files', { count: filePaths.length });
630
+
631
+ if (!Array.isArray(filePaths) || filePaths.length === 0) {
632
+ logger.warning('git-operations - stageFiles', 'No files provided');
633
+ return {
634
+ success: false,
635
+ stagedFiles: [],
636
+ error: 'No files provided to stage'
637
+ };
638
+ }
639
+
640
+ try {
641
+ const repoRoot = getRepoRoot();
642
+ const stagedFiles = [];
643
+
644
+ // Stage each file individually for granular error handling
645
+ for (const filePath of filePaths) {
646
+ // Convert to repo-relative path
647
+ const relativePath = path.relative(repoRoot, filePath);
648
+
649
+ // Verify file exists
650
+ if (!fs.existsSync(filePath)) {
651
+ logger.warning('git-operations - stageFiles', 'File not found, skipping', { filePath });
652
+ continue;
653
+ }
654
+
655
+ try {
656
+ execGitCommand(`add "${relativePath}"`);
657
+ stagedFiles.push(filePath);
658
+ logger.debug('git-operations - stageFiles', 'File staged', { relativePath });
659
+ } catch (error) {
660
+ logger.warning('git-operations - stageFiles', 'Failed to stage file', {
661
+ filePath,
662
+ error: error.message
663
+ });
664
+ // Continue staging other files
665
+ }
666
+ }
667
+
668
+ if (stagedFiles.length === 0) {
669
+ return {
670
+ success: false,
671
+ stagedFiles: [],
672
+ error: 'No files were successfully staged'
673
+ };
674
+ }
675
+
676
+ logger.debug('git-operations - stageFiles', 'Files staged successfully', {
677
+ count: stagedFiles.length
678
+ });
679
+
680
+ return {
681
+ success: true,
682
+ stagedFiles,
683
+ error: ''
684
+ };
685
+
686
+ } catch (error) {
687
+ logger.error('git-operations - stageFiles', 'Staging failed', error);
688
+
689
+ return {
690
+ success: false,
691
+ stagedFiles: [],
692
+ error: error.message || 'Unknown staging error'
693
+ };
694
+ }
695
+ };
696
+
619
697
  /**
620
698
  * Pushes branch to remote
621
699
  * Why: Publishes local branch to remote before creating PR
@@ -663,6 +741,327 @@ const pushBranch = (branchName, { setUpstream = false } = {}) => {
663
741
  }
664
742
  };
665
743
 
744
+ /**
745
+ * Fetches latest from remote
746
+ * Why: Ensures local git has up-to-date remote refs before comparing branches
747
+ *
748
+ * @param {string} [remoteName='origin'] - Remote name
749
+ */
750
+ const fetchRemote = (remoteName = 'origin') => {
751
+ logger.debug('git-operations - fetchRemote', 'Fetching from remote', { remoteName });
752
+
753
+ // Sanitize remote name
754
+ const sanitizedRemote = sanitizeBranchName(remoteName);
755
+ if (!sanitizedRemote) {
756
+ throw new GitError('Invalid remote name', { command: 'fetch', output: remoteName });
757
+ }
758
+
759
+ try {
760
+ execGitCommand(`fetch ${sanitizedRemote}`);
761
+ logger.debug('git-operations - fetchRemote', 'Fetch successful', { remoteName: sanitizedRemote });
762
+ } catch (error) {
763
+ logger.error('git-operations - fetchRemote', 'Fetch failed', error);
764
+ throw new GitError('Failed to fetch from remote', {
765
+ command: `git fetch ${sanitizedRemote}`,
766
+ cause: error
767
+ });
768
+ }
769
+ };
770
+
771
+ /**
772
+ * Checks if a branch reference exists
773
+ * Why: Validates branch refs before attempting operations to avoid failures
774
+ *
775
+ * @param {string} branchRef - Branch reference to verify (e.g., 'origin/main')
776
+ * @returns {boolean}
777
+ */
778
+ const branchExists = (branchRef) => {
779
+ logger.debug('git-operations - branchExists', 'Checking if branch exists', { branchRef });
780
+
781
+ // Sanitize branch reference
782
+ const sanitizedRef = sanitizeBranchName(branchRef);
783
+ if (!sanitizedRef) {
784
+ logger.debug('git-operations - branchExists', 'Invalid branch reference', { branchRef });
785
+ return false;
786
+ }
787
+
788
+ try {
789
+ // Why: rev-parse --verify exits with 0 if ref exists, non-zero otherwise
790
+ // Why execSync instead of execGitCommand: Expected failures should not log at error level.
791
+ // branchExists() is a probe — failure is a valid outcome, not an error.
792
+ execSync(`git rev-parse --verify ${sanitizedRef}`, {
793
+ encoding: 'utf8',
794
+ stdio: ['pipe', 'pipe', 'pipe']
795
+ });
796
+ logger.debug('git-operations - branchExists', 'Branch exists', { branchRef: sanitizedRef });
797
+ return true;
798
+ } catch (error) {
799
+ logger.debug('git-operations - branchExists', 'Branch does not exist', { branchRef: sanitizedRef });
800
+ return false;
801
+ }
802
+ };
803
+
804
+ /**
805
+ * Gets remote branch names (stripped of remote prefix)
806
+ * Why: Used by resolveBaseBranch to suggest similar branches
807
+ *
808
+ * @param {string} [remoteName='origin'] - Remote name
809
+ * @returns {string[]} Branch names without remote prefix
810
+ */
811
+ const getRemoteBranches = (remoteName = 'origin') => {
812
+ try {
813
+ const output = execSync(`git branch -r --list "${remoteName}/*"`, {
814
+ encoding: 'utf8',
815
+ stdio: ['pipe', 'pipe', 'pipe']
816
+ }).trim();
817
+
818
+ if (!output) return [];
819
+
820
+ return output
821
+ .split(/\r?\n/)
822
+ .map(line => line.trim())
823
+ .filter(line => !line.includes('->')) // Skip HEAD -> origin/main
824
+ .map(line => line.replace(`${remoteName}/`, ''));
825
+ } catch {
826
+ return [];
827
+ }
828
+ };
829
+
830
+ /**
831
+ * Finds remote branches similar to the target by shared keywords
832
+ *
833
+ * @param {string} target - Branch name to match against
834
+ * @param {string[]} branches - Available branch names
835
+ * @returns {string[]} Similar branch names, sorted by relevance
836
+ */
837
+ const findSimilarBranches = (target, branches) => {
838
+ const targetWords = target.toLowerCase().split(/[/\-_.]/).filter(w => w.length > 1);
839
+
840
+ const scored = branches
841
+ .map(branch => {
842
+ const branchWords = branch.toLowerCase().split(/[/\-_.]/).filter(w => w.length > 1);
843
+ const matches = targetWords.filter(tw => branchWords.some(bw => bw.includes(tw) || tw.includes(bw)));
844
+ return { branch, score: matches.length };
845
+ })
846
+ .filter(({ score }) => score > 0)
847
+ .sort((a, b) => b.score - a.score);
848
+
849
+ return scored.map(({ branch }) => branch);
850
+ };
851
+
852
+ /**
853
+ * Resolves base branch on remote.
854
+ * Verifies origin/{target} exists. If not, throws with suggestions
855
+ * of similar remote branches to help the user correct the name.
856
+ *
857
+ * @param {string} targetBranch - Requested base branch
858
+ * @returns {string} Resolved remote ref (e.g., 'origin/main')
859
+ * @throws {GitError} If branch not found (message includes suggestions)
860
+ */
861
+ const resolveBaseBranch = (targetBranch) => {
862
+ logger.debug('git-operations - resolveBaseBranch', 'Resolving base branch', { targetBranch });
863
+
864
+ // Sanitize target branch
865
+ const sanitizedTarget = sanitizeBranchName(targetBranch);
866
+ if (!sanitizedTarget) {
867
+ throw new GitError('Invalid target branch name', { command: 'resolveBaseBranch', output: targetBranch });
868
+ }
869
+
870
+ // Try requested branch with origin/ prefix
871
+ const targetRef = `origin/${sanitizedTarget}`;
872
+ if (branchExists(targetRef)) {
873
+ logger.debug('git-operations - resolveBaseBranch', 'Target branch found', { ref: targetRef });
874
+ return targetRef;
875
+ }
876
+
877
+ // Branch not found — build helpful error with suggestions
878
+ const remoteBranches = getRemoteBranches();
879
+ const similar = findSimilarBranches(sanitizedTarget, remoteBranches);
880
+
881
+ let suggestion = '';
882
+ if (similar.length > 0) {
883
+ const shown = similar.slice(0, 5);
884
+ suggestion = `\n\nSimilar branches on remote:\n${shown.map(b => ` - ${b}`).join('\n')}`;
885
+ } else if (remoteBranches.length > 0) {
886
+ const shown = remoteBranches.slice(0, 10);
887
+ suggestion = `\n\nAvailable branches on remote:\n${shown.map(b => ` - ${b}`).join('\n')}`;
888
+ if (remoteBranches.length > 10) {
889
+ suggestion += `\n ... and ${remoteBranches.length - 10} more`;
890
+ }
891
+ }
892
+
893
+ throw new GitError(`Branch '${sanitizedTarget}' not found on remote${suggestion}`, {
894
+ command: 'resolveBaseBranch',
895
+ output: `target: ${targetBranch}`
896
+ });
897
+ };
898
+
899
+ /**
900
+ * Gets list of files changed between two refs
901
+ * Why: Shows which files differ between branches for PR context
902
+ *
903
+ * @param {string} baseRef - Base reference (e.g., 'origin/main')
904
+ * @param {string} [headRef='HEAD'] - Head reference
905
+ * @returns {string[]} Array of changed file paths
906
+ */
907
+ const getChangedFilesBetweenRefs = (baseRef, headRef = 'HEAD') => {
908
+ logger.debug('git-operations - getChangedFilesBetweenRefs', 'Getting changed files', { baseRef, headRef });
909
+
910
+ // Sanitize refs
911
+ const sanitizedBase = sanitizeBranchName(baseRef);
912
+ const sanitizedHead = sanitizeBranchName(headRef);
913
+
914
+ if (!sanitizedBase || !sanitizedHead) {
915
+ throw new GitError('Invalid branch reference', {
916
+ command: 'getChangedFilesBetweenRefs',
917
+ output: `base: ${baseRef}, head: ${headRef}`
918
+ });
919
+ }
920
+
921
+ try {
922
+ // Why: --name-only shows just file paths, ... compares merge-base to head
923
+ const output = execGitCommand(`diff --name-only ${sanitizedBase}...${sanitizedHead}`);
924
+
925
+ if (!output) {
926
+ logger.debug('git-operations - getChangedFilesBetweenRefs', 'No changed files', { baseRef, headRef });
927
+ return [];
928
+ }
929
+
930
+ const files = output.split(/\r?\n/).filter(f => f.length > 0);
931
+
932
+ logger.debug('git-operations - getChangedFilesBetweenRefs', 'Changed files retrieved', {
933
+ baseRef,
934
+ headRef,
935
+ fileCount: files.length
936
+ });
937
+
938
+ return files;
939
+
940
+ } catch (error) {
941
+ logger.error('git-operations - getChangedFilesBetweenRefs', 'Failed to get changed files', error);
942
+ throw new GitError('Failed to get changed files between refs', {
943
+ command: `git diff --name-only ${sanitizedBase}...${sanitizedHead}`,
944
+ cause: error
945
+ });
946
+ }
947
+ };
948
+
949
+ /**
950
+ * Gets full diff between two refs.
951
+ *
952
+ * Complements the existing getFileDiff() which operates on a single file's
953
+ * staged/unstaged diff (git diff [--cached] -- <file>). This function operates
954
+ * on the comparison between two branch refs (git diff origin/main...HEAD) —
955
+ * the branch-level counterpart that is currently missing, which is why both
956
+ * analyze-diff.js and create-pr.js resort to raw execSync.
957
+ *
958
+ * Returns the raw diff string. Truncation/reduction is handled by
959
+ * buildDiffPayload() in pr-metadata-engine.js, not here.
960
+ *
961
+ * @param {string} baseRef - Base reference
962
+ * @param {string} [headRef='HEAD'] - Head reference
963
+ * @param {Object} [options]
964
+ * @param {number} [options.contextLines=3] - Lines of context around changes (-U flag)
965
+ * @returns {string} Raw unified diff
966
+ */
967
+ const getDiffBetweenRefs = (baseRef, headRef = 'HEAD', { contextLines = 3 } = {}) => {
968
+ logger.debug('git-operations - getDiffBetweenRefs', 'Getting diff between refs', {
969
+ baseRef,
970
+ headRef,
971
+ contextLines
972
+ });
973
+
974
+ // Sanitize refs
975
+ const sanitizedBase = sanitizeBranchName(baseRef);
976
+ const sanitizedHead = sanitizeBranchName(headRef);
977
+
978
+ if (!sanitizedBase || !sanitizedHead) {
979
+ throw new GitError('Invalid branch reference', {
980
+ command: 'getDiffBetweenRefs',
981
+ output: `base: ${baseRef}, head: ${headRef}`
982
+ });
983
+ }
984
+
985
+ try {
986
+ // Why: -U flag sets context lines, ... compares merge-base to head
987
+ const contextFlag = `-U${contextLines}`;
988
+ const output = execGitCommand(`diff ${contextFlag} ${sanitizedBase}...${sanitizedHead}`);
989
+
990
+ logger.debug('git-operations - getDiffBetweenRefs', 'Diff retrieved', {
991
+ baseRef,
992
+ headRef,
993
+ contextLines,
994
+ diffSize: output.length
995
+ });
996
+
997
+ return output;
998
+
999
+ } catch (error) {
1000
+ logger.error('git-operations - getDiffBetweenRefs', 'Failed to get diff', error);
1001
+ throw new GitError('Failed to get diff between refs', {
1002
+ command: `git diff -U${contextLines} ${sanitizedBase}...${sanitizedHead}`,
1003
+ cause: error
1004
+ });
1005
+ }
1006
+ };
1007
+
1008
+ /**
1009
+ * Gets commit log between two refs
1010
+ * Why: Shows commit history for PR description and context
1011
+ *
1012
+ * @param {string} baseRef - Base reference
1013
+ * @param {string} [headRef='HEAD'] - Head reference
1014
+ * @param {Object} [options]
1015
+ * @param {string} [options.format='oneline'] - Git log format
1016
+ * @returns {string} Commit log output
1017
+ */
1018
+ const getCommitsBetweenRefs = (baseRef, headRef = 'HEAD', { format = 'oneline' } = {}) => {
1019
+ logger.debug('git-operations - getCommitsBetweenRefs', 'Getting commits between refs', {
1020
+ baseRef,
1021
+ headRef,
1022
+ format
1023
+ });
1024
+
1025
+ // Sanitize refs
1026
+ const sanitizedBase = sanitizeBranchName(baseRef);
1027
+ const sanitizedHead = sanitizeBranchName(headRef);
1028
+
1029
+ if (!sanitizedBase || !sanitizedHead) {
1030
+ throw new GitError('Invalid branch reference', {
1031
+ command: 'getCommitsBetweenRefs',
1032
+ output: `base: ${baseRef}, head: ${headRef}`
1033
+ });
1034
+ }
1035
+
1036
+ try {
1037
+ // Why: --format specifies output format, ... shows commits on head not on base
1038
+ const output = execGitCommand(`log --format=${format} ${sanitizedBase}...${sanitizedHead}`);
1039
+
1040
+ if (!output) {
1041
+ logger.debug('git-operations - getCommitsBetweenRefs', 'No commits found', { baseRef, headRef });
1042
+ return '';
1043
+ }
1044
+
1045
+ const commitCount = output.split(/\r?\n/).filter(line => line.length > 0).length;
1046
+
1047
+ logger.debug('git-operations - getCommitsBetweenRefs', 'Commits retrieved', {
1048
+ baseRef,
1049
+ headRef,
1050
+ format,
1051
+ commitCount
1052
+ });
1053
+
1054
+ return output;
1055
+
1056
+ } catch (error) {
1057
+ logger.error('git-operations - getCommitsBetweenRefs', 'Failed to get commits', error);
1058
+ throw new GitError('Failed to get commits between refs', {
1059
+ command: `git log --format=${format} ${sanitizedBase}...${sanitizedHead}`,
1060
+ cause: error
1061
+ });
1062
+ }
1063
+ };
1064
+
666
1065
  export {
667
1066
  GitError,
668
1067
  getStagedFiles,
@@ -679,5 +1078,13 @@ export {
679
1078
  verifyRemoteExists,
680
1079
  getBranchPushStatus,
681
1080
  pushBranch,
682
- createCommit
1081
+ stageFiles,
1082
+ createCommit,
1083
+ fetchRemote,
1084
+ branchExists,
1085
+ getRemoteBranches,
1086
+ resolveBaseBranch,
1087
+ getChangedFilesBetweenRefs,
1088
+ getDiffBetweenRefs,
1089
+ getCommitsBetweenRefs
683
1090
  };