claude-git-hooks 2.13.0 → 2.14.4
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 +64 -0
- package/README.md +20 -10
- package/lib/commands/analyze-diff.js +32 -153
- package/lib/commands/bump-version.js +164 -56
- package/lib/commands/create-pr.js +171 -94
- package/lib/commands/helpers.js +7 -0
- package/lib/utils/claude-client.js +6 -1
- package/lib/utils/claude-diagnostics.js +2 -1
- package/lib/utils/git-operations.js +408 -1
- package/lib/utils/pr-metadata-engine.js +474 -0
- package/package.json +60 -60
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: pr-metadata-engine.js
|
|
3
|
+
* Purpose: PR metadata generation engine - single source of truth for PR analysis
|
|
4
|
+
*
|
|
5
|
+
* Why this exists: Both analyze-diff and create-pr need to:
|
|
6
|
+
* - Extract branch context (diff, commits, files)
|
|
7
|
+
* - Generate PR metadata (title, description, test plan)
|
|
8
|
+
* - Handle large diffs through tiered reduction
|
|
9
|
+
*
|
|
10
|
+
* By extracting this logic, we:
|
|
11
|
+
* - Eliminate code duplication between commands
|
|
12
|
+
* - Provide output-format agnostic data
|
|
13
|
+
* - Enable timeout resilience through intelligent diff reduction
|
|
14
|
+
* - Ensure consistent behavior across commands
|
|
15
|
+
*
|
|
16
|
+
* Dependencies:
|
|
17
|
+
* - git-operations: Branch comparison, diff extraction
|
|
18
|
+
* - claude-client: Claude CLI execution with retry
|
|
19
|
+
* - prompt-builder: Template loading and variable replacement
|
|
20
|
+
* - logger: Debug and error logging
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
getCurrentBranch,
|
|
25
|
+
fetchRemote,
|
|
26
|
+
resolveBaseBranch,
|
|
27
|
+
getChangedFilesBetweenRefs,
|
|
28
|
+
getDiffBetweenRefs,
|
|
29
|
+
getCommitsBetweenRefs
|
|
30
|
+
} from './git-operations.js';
|
|
31
|
+
import { executeClaudeWithRetry, extractJSON } from './claude-client.js';
|
|
32
|
+
import { loadPrompt } from './prompt-builder.js';
|
|
33
|
+
import { getConfig } from '../config.js';
|
|
34
|
+
import logger from './logger.js';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {Object} BranchContext
|
|
38
|
+
* @property {string} baseBranch - e.g., 'origin/main'
|
|
39
|
+
* @property {string} headRef - e.g., 'HEAD'
|
|
40
|
+
* @property {string} currentBranch - e.g., 'feature/add-auth'
|
|
41
|
+
* @property {string[]} files - Changed file paths
|
|
42
|
+
* @property {string} diff - Full diff (possibly truncated)
|
|
43
|
+
* @property {string} commits - Commit log
|
|
44
|
+
* @property {number} filesCount
|
|
45
|
+
* @property {boolean} isTruncated - True if any tier of reduction was applied
|
|
46
|
+
* @property {{ fullDiff: number, statOnly: number, contextLines: number }} [truncationDetails] - Present when isTruncated=true
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {Object} PRMetadata
|
|
51
|
+
* @property {string} prTitle
|
|
52
|
+
* @property {string} prDescription
|
|
53
|
+
* @property {string} suggestedBranchName
|
|
54
|
+
* @property {string} changeType - feat, fix, refactor, etc.
|
|
55
|
+
* @property {boolean} breakingChanges
|
|
56
|
+
* @property {string} testingNotes
|
|
57
|
+
* @property {BranchContext} context
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Internal defaults (not documented in config.example.json)
|
|
62
|
+
* Why: Convention over configuration - sensible defaults for 95% of cases
|
|
63
|
+
*/
|
|
64
|
+
const DEFAULTS = {
|
|
65
|
+
timeout: 180000, // 3 minutes
|
|
66
|
+
maxDiffSize: 50000, // 50KB
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Builds diff payload with tiered reduction strategy
|
|
71
|
+
* Why: Large diffs cause timeouts - reduce intelligently while preserving essential info
|
|
72
|
+
*
|
|
73
|
+
* Tiered Reduction Strategy:
|
|
74
|
+
* - Always included: file list + per-file stats (never truncated)
|
|
75
|
+
* - Tier 1: Reduce context lines from -U3 to -U1
|
|
76
|
+
* - Tier 2: Allocate proportional budget per file
|
|
77
|
+
* - Tier 3: Replace oversized files with stat-only summary
|
|
78
|
+
*
|
|
79
|
+
* @param {string} baseRef - Base reference (e.g., 'origin/main')
|
|
80
|
+
* @param {string} headRef - Head reference (e.g., 'HEAD')
|
|
81
|
+
* @param {string[]} files - Changed file paths
|
|
82
|
+
* @param {number} maxSize - Maximum diff size in bytes (default: 50KB)
|
|
83
|
+
* @returns {{ payload: string, isTruncated: boolean, truncationDetails: Object }}
|
|
84
|
+
*/
|
|
85
|
+
const buildDiffPayload = (baseRef, headRef, files, maxSize = DEFAULTS.maxDiffSize) => {
|
|
86
|
+
logger.debug('pr-metadata-engine - buildDiffPayload', 'Building diff payload', {
|
|
87
|
+
baseRef,
|
|
88
|
+
headRef,
|
|
89
|
+
fileCount: files.length,
|
|
90
|
+
maxSize
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Always include file stats (never truncated)
|
|
94
|
+
const statsOutput = getDiffBetweenRefs(baseRef, headRef, { contextLines: 0 })
|
|
95
|
+
.split('\n')
|
|
96
|
+
.filter(line => line.match(/^\s*[\w/.-]+\s*\|\s*\d+\s*[+-]*$/))
|
|
97
|
+
.join('\n');
|
|
98
|
+
|
|
99
|
+
const statsSummary = `Files changed: ${files.length}\n\n${statsOutput}\n\n`;
|
|
100
|
+
const summarySize = statsSummary.length;
|
|
101
|
+
|
|
102
|
+
logger.debug('pr-metadata-engine - buildDiffPayload', 'File stats summary created', {
|
|
103
|
+
summarySize,
|
|
104
|
+
filesCount: files.length
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Tier 1: Try with reduced context (U1)
|
|
108
|
+
logger.debug('pr-metadata-engine - buildDiffPayload', 'Tier 1: Fetching diff with -U1 context');
|
|
109
|
+
const fullDiff = getDiffBetweenRefs(baseRef, headRef, { contextLines: 1 });
|
|
110
|
+
|
|
111
|
+
if (fullDiff.length + summarySize <= maxSize) {
|
|
112
|
+
logger.debug('pr-metadata-engine - buildDiffPayload', 'Tier 1: Diff fits within budget', {
|
|
113
|
+
diffSize: fullDiff.length,
|
|
114
|
+
totalSize: fullDiff.length + summarySize
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
payload: statsSummary + fullDiff,
|
|
119
|
+
isTruncated: false,
|
|
120
|
+
truncationDetails: {
|
|
121
|
+
fullDiff: files.length,
|
|
122
|
+
statOnly: 0,
|
|
123
|
+
contextLines: 1
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
logger.debug('pr-metadata-engine - buildDiffPayload', 'Tier 1: Exceeded budget, applying Tier 2', {
|
|
129
|
+
diffSize: fullDiff.length,
|
|
130
|
+
maxSize
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Tier 2: Split diff by file and allocate proportional budget
|
|
134
|
+
const diffPattern = /^diff --git a\/.+ b\/.+$/gm;
|
|
135
|
+
const fileDiffs = [];
|
|
136
|
+
let lastIndex = 0;
|
|
137
|
+
let match;
|
|
138
|
+
|
|
139
|
+
while ((match = diffPattern.exec(fullDiff)) !== null) {
|
|
140
|
+
if (lastIndex > 0) {
|
|
141
|
+
fileDiffs.push(fullDiff.substring(lastIndex, match.index));
|
|
142
|
+
}
|
|
143
|
+
lastIndex = match.index;
|
|
144
|
+
}
|
|
145
|
+
if (lastIndex > 0) {
|
|
146
|
+
fileDiffs.push(fullDiff.substring(lastIndex));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
logger.debug('pr-metadata-engine - buildDiffPayload', 'Tier 2: Split diff into per-file chunks', {
|
|
150
|
+
chunkCount: fileDiffs.length
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const availableBudget = maxSize - summarySize;
|
|
154
|
+
const budgetPerFile = Math.floor(availableBudget / files.length);
|
|
155
|
+
|
|
156
|
+
logger.debug('pr-metadata-engine - buildDiffPayload', 'Tier 2: Allocated per-file budget', {
|
|
157
|
+
availableBudget,
|
|
158
|
+
budgetPerFile,
|
|
159
|
+
fileCount: files.length
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
let finalDiff = '';
|
|
163
|
+
let fullDiffCount = 0;
|
|
164
|
+
let statOnlyCount = 0;
|
|
165
|
+
const oversizedFiles = [];
|
|
166
|
+
|
|
167
|
+
// Apply per-file budget
|
|
168
|
+
for (let i = 0; i < fileDiffs.length; i++) {
|
|
169
|
+
const fileDiff = fileDiffs[i];
|
|
170
|
+
|
|
171
|
+
if (fileDiff.length <= budgetPerFile) {
|
|
172
|
+
// File fits within budget - include full diff
|
|
173
|
+
finalDiff += fileDiff;
|
|
174
|
+
fullDiffCount++;
|
|
175
|
+
} else {
|
|
176
|
+
// File exceeds budget - mark for Tier 3
|
|
177
|
+
oversizedFiles.push({ index: i, diff: fileDiff, size: fileDiff.length });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
logger.debug('pr-metadata-engine - buildDiffPayload', 'Tier 2: Applied budget allocation', {
|
|
182
|
+
fullDiffCount,
|
|
183
|
+
oversizedCount: oversizedFiles.length
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Tier 3: Replace oversized files with stat-only summary
|
|
187
|
+
if (oversizedFiles.length > 0) {
|
|
188
|
+
logger.debug('pr-metadata-engine - buildDiffPayload', 'Tier 3: Replacing oversized files with stat-only');
|
|
189
|
+
|
|
190
|
+
for (const { diff } of oversizedFiles) {
|
|
191
|
+
// Extract file path and stats from diff header
|
|
192
|
+
const filePathMatch = diff.match(/^diff --git a\/(.+?) b\/(.+?)$/m);
|
|
193
|
+
if (filePathMatch) {
|
|
194
|
+
const filePath = filePathMatch[2];
|
|
195
|
+
// Extract stat line for this file
|
|
196
|
+
const statLine = statsOutput.split('\n').find(line => line.includes(filePath));
|
|
197
|
+
if (statLine) {
|
|
198
|
+
finalDiff += `\n--- ${filePath} (truncated - stat only) ---\n${statLine}\n`;
|
|
199
|
+
statOnlyCount++;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
logger.debug('pr-metadata-engine - buildDiffPayload', 'Tier 3: Stat-only demotion complete', {
|
|
205
|
+
statOnlyCount
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const payload = statsSummary + finalDiff;
|
|
210
|
+
const isTruncated = statOnlyCount > 0 || fullDiff.length > maxSize;
|
|
211
|
+
|
|
212
|
+
logger.debug('pr-metadata-engine - buildDiffPayload', 'Diff payload built', {
|
|
213
|
+
totalSize: payload.length,
|
|
214
|
+
isTruncated,
|
|
215
|
+
fullDiffCount,
|
|
216
|
+
statOnlyCount
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
payload,
|
|
221
|
+
isTruncated,
|
|
222
|
+
truncationDetails: {
|
|
223
|
+
fullDiff: fullDiffCount,
|
|
224
|
+
statOnly: statOnlyCount,
|
|
225
|
+
contextLines: 1
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Gets branch context for PR creation
|
|
232
|
+
* Why: Extracts all git information needed for PR metadata generation
|
|
233
|
+
*
|
|
234
|
+
* @param {string} [targetBranch] - Target base branch (optional)
|
|
235
|
+
* @returns {Promise<{ success: boolean, context?: BranchContext, error?: string }>}
|
|
236
|
+
*/
|
|
237
|
+
export const getBranchContext = async (targetBranch) => {
|
|
238
|
+
logger.debug('pr-metadata-engine - getBranchContext', 'Getting branch context', { targetBranch });
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
// Get current branch
|
|
242
|
+
const currentBranch = getCurrentBranch();
|
|
243
|
+
|
|
244
|
+
if (!currentBranch || currentBranch === 'unknown') {
|
|
245
|
+
return {
|
|
246
|
+
success: false,
|
|
247
|
+
error: 'Not on a valid branch'
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
logger.debug('pr-metadata-engine - getBranchContext', 'Current branch identified', { currentBranch });
|
|
252
|
+
|
|
253
|
+
// Fetch remote references
|
|
254
|
+
try {
|
|
255
|
+
fetchRemote();
|
|
256
|
+
logger.debug('pr-metadata-engine - getBranchContext', 'Remote fetched successfully');
|
|
257
|
+
} catch (fetchError) {
|
|
258
|
+
logger.warning('pr-metadata-engine - getBranchContext', 'Failed to fetch remote', fetchError);
|
|
259
|
+
// Continue anyway - may work with cached remote refs
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Resolve base branch (throws with suggestions if not found)
|
|
263
|
+
const target = targetBranch || currentBranch;
|
|
264
|
+
const baseBranch = resolveBaseBranch(target);
|
|
265
|
+
|
|
266
|
+
logger.debug('pr-metadata-engine - getBranchContext', 'Base branch resolved', {
|
|
267
|
+
requestedBranch: target,
|
|
268
|
+
baseBranch
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Get changed files
|
|
272
|
+
const files = getChangedFilesBetweenRefs(baseBranch, 'HEAD');
|
|
273
|
+
|
|
274
|
+
if (files.length === 0) {
|
|
275
|
+
return {
|
|
276
|
+
success: false,
|
|
277
|
+
error: `No differences found between '${baseBranch}' and HEAD`
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
logger.debug('pr-metadata-engine - getBranchContext', 'Changed files retrieved', {
|
|
282
|
+
fileCount: files.length
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Get commits
|
|
286
|
+
const commits = getCommitsBetweenRefs(baseBranch, 'HEAD', { format: 'oneline' });
|
|
287
|
+
|
|
288
|
+
logger.debug('pr-metadata-engine - getBranchContext', 'Commits retrieved');
|
|
289
|
+
|
|
290
|
+
// Build diff payload with tiered reduction
|
|
291
|
+
const { payload: diff, isTruncated, truncationDetails } = buildDiffPayload(
|
|
292
|
+
baseBranch,
|
|
293
|
+
'HEAD',
|
|
294
|
+
files,
|
|
295
|
+
DEFAULTS.maxDiffSize
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
logger.debug('pr-metadata-engine - getBranchContext', 'Diff payload built', {
|
|
299
|
+
diffSize: diff.length,
|
|
300
|
+
isTruncated
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const context = {
|
|
304
|
+
baseBranch,
|
|
305
|
+
headRef: 'HEAD',
|
|
306
|
+
currentBranch,
|
|
307
|
+
files,
|
|
308
|
+
diff,
|
|
309
|
+
commits,
|
|
310
|
+
filesCount: files.length,
|
|
311
|
+
isTruncated,
|
|
312
|
+
...(isTruncated && { truncationDetails })
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
logger.debug('pr-metadata-engine - getBranchContext', 'Branch context complete', {
|
|
316
|
+
filesCount: context.filesCount,
|
|
317
|
+
isTruncated: context.isTruncated
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
success: true,
|
|
322
|
+
context
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
} catch (error) {
|
|
326
|
+
logger.error('pr-metadata-engine - getBranchContext', 'Failed to get branch context', error);
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
success: false,
|
|
330
|
+
error: error.message || 'Failed to get branch context'
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Generates PR metadata from branch context
|
|
337
|
+
* Why: Sends context to Claude and parses structured PR information
|
|
338
|
+
*
|
|
339
|
+
* @param {BranchContext} context - Branch context from getBranchContext
|
|
340
|
+
* @param {Object} [options] - Generation options
|
|
341
|
+
* @param {number} [options.timeout] - Claude timeout in ms (default: 180000)
|
|
342
|
+
* @param {string} [options.hook] - Hook name for telemetry (default: 'pr-metadata')
|
|
343
|
+
* @returns {Promise<PRMetadata>}
|
|
344
|
+
* @throws {Error} If metadata generation fails
|
|
345
|
+
*/
|
|
346
|
+
export const generatePRMetadata = async (context, options = {}) => {
|
|
347
|
+
const config = await getConfig();
|
|
348
|
+
const configTimeout = config.analysis?.timeout;
|
|
349
|
+
const { timeout = configTimeout || DEFAULTS.timeout, hook = 'pr-metadata' } = options;
|
|
350
|
+
|
|
351
|
+
logger.debug('pr-metadata-engine - generatePRMetadata', 'Generating PR metadata', {
|
|
352
|
+
filesCount: context.filesCount,
|
|
353
|
+
timeout,
|
|
354
|
+
hook
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
// Build context description for prompt
|
|
359
|
+
const contextDescription = `${context.currentBranch} vs ${context.baseBranch}`;
|
|
360
|
+
|
|
361
|
+
// Build prompt from template using existing ANALYZE_DIFF.md template
|
|
362
|
+
// Template variables match analyze-diff.js:148-154 for compatibility
|
|
363
|
+
const prompt = await loadPrompt('ANALYZE_DIFF.md', {
|
|
364
|
+
CONTEXT_DESCRIPTION: contextDescription,
|
|
365
|
+
SUBAGENT_INSTRUCTION: '', // Engine uses single Claude spawn, no subagents
|
|
366
|
+
COMMITS: context.commits,
|
|
367
|
+
DIFF_FILES: context.files.join('\n'),
|
|
368
|
+
FULL_DIFF: context.diff
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
logger.debug('pr-metadata-engine - generatePRMetadata', 'Prompt built', {
|
|
372
|
+
promptLength: prompt.length
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// Prepare telemetry context
|
|
376
|
+
const telemetryContext = {
|
|
377
|
+
fileCount: context.filesCount,
|
|
378
|
+
batchSize: context.filesCount,
|
|
379
|
+
totalBatches: 1,
|
|
380
|
+
model: 'sonnet',
|
|
381
|
+
hook
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
logger.debug('pr-metadata-engine - generatePRMetadata', 'Calling Claude CLI');
|
|
385
|
+
|
|
386
|
+
// Call Claude with retry logic
|
|
387
|
+
const response = await executeClaudeWithRetry(prompt, {
|
|
388
|
+
timeout,
|
|
389
|
+
telemetryContext
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
logger.debug('pr-metadata-engine - generatePRMetadata', 'Claude response received', {
|
|
393
|
+
responseLength: response.length
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// Parse JSON response
|
|
397
|
+
const result = extractJSON(response, telemetryContext);
|
|
398
|
+
|
|
399
|
+
logger.debug('pr-metadata-engine - generatePRMetadata', 'JSON parsed successfully', {
|
|
400
|
+
hasPrTitle: !!result.prTitle,
|
|
401
|
+
hasPrDescription: !!result.prDescription
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// Return PRMetadata with context attached
|
|
405
|
+
return {
|
|
406
|
+
prTitle: result.prTitle,
|
|
407
|
+
prDescription: result.prDescription,
|
|
408
|
+
suggestedBranchName: result.suggestedBranchName,
|
|
409
|
+
changeType: result.changeType,
|
|
410
|
+
breakingChanges: result.breakingChanges || false,
|
|
411
|
+
testingNotes: result.testingNotes || '',
|
|
412
|
+
context
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
} catch (error) {
|
|
416
|
+
logger.error('pr-metadata-engine - generatePRMetadata', 'Failed to generate PR metadata', error);
|
|
417
|
+
throw error;
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Analyzes branch for PR (convenience wrapper)
|
|
423
|
+
* Why: Single function call for complete PR analysis workflow
|
|
424
|
+
*
|
|
425
|
+
* @param {string} [targetBranch] - Target base branch (optional)
|
|
426
|
+
* @param {Object} [options] - Analysis options
|
|
427
|
+
* @param {number} [options.timeout] - Claude timeout in ms
|
|
428
|
+
* @param {string} [options.hook] - Hook name for telemetry
|
|
429
|
+
* @returns {Promise<{ success: boolean, result?: PRMetadata, error?: string }>}
|
|
430
|
+
*/
|
|
431
|
+
export const analyzeBranchForPR = async (targetBranch, options = {}) => {
|
|
432
|
+
logger.debug('pr-metadata-engine - analyzeBranchForPR', 'Starting PR analysis', {
|
|
433
|
+
targetBranch,
|
|
434
|
+
options
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
try {
|
|
438
|
+
// Step 1: Get branch context
|
|
439
|
+
const contextResult = await getBranchContext(targetBranch);
|
|
440
|
+
|
|
441
|
+
if (!contextResult.success) {
|
|
442
|
+
logger.error('pr-metadata-engine - analyzeBranchForPR', 'Failed to get branch context', {
|
|
443
|
+
error: contextResult.error
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
success: false,
|
|
448
|
+
error: contextResult.error
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
logger.debug('pr-metadata-engine - analyzeBranchForPR', 'Branch context retrieved', {
|
|
453
|
+
filesCount: contextResult.context.filesCount
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// Step 2: Generate PR metadata
|
|
457
|
+
const metadata = await generatePRMetadata(contextResult.context, options);
|
|
458
|
+
|
|
459
|
+
logger.debug('pr-metadata-engine - analyzeBranchForPR', 'PR metadata generated successfully');
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
success: true,
|
|
463
|
+
result: metadata
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
} catch (error) {
|
|
467
|
+
logger.error('pr-metadata-engine - analyzeBranchForPR', 'PR analysis failed', error);
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
success: false,
|
|
471
|
+
error: error.message || 'Failed to analyze branch for PR'
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
};
|
package/package.json
CHANGED
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "claude-git-hooks",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Git hooks with Claude CLI for code analysis and automatic commit messages",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"claude-hooks": "./bin/claude-hooks"
|
|
8
|
-
},
|
|
9
|
-
"scripts": {
|
|
10
|
-
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
11
|
-
"test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
|
|
12
|
-
"test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
|
|
13
|
-
"lint": "eslint lib/ bin/",
|
|
14
|
-
"lint:fix": "eslint lib/ bin/ --fix",
|
|
15
|
-
"format": "prettier --write \"lib/**/*.js\" \"bin/**\""
|
|
16
|
-
},
|
|
17
|
-
"keywords": [
|
|
18
|
-
"git",
|
|
19
|
-
"hooks",
|
|
20
|
-
"claude",
|
|
21
|
-
"ai",
|
|
22
|
-
"code-review",
|
|
23
|
-
"commit-messages",
|
|
24
|
-
"pre-commit",
|
|
25
|
-
"automation"
|
|
26
|
-
],
|
|
27
|
-
"author": "Pablo Rovito",
|
|
28
|
-
"license": "MIT",
|
|
29
|
-
"repository": {
|
|
30
|
-
"type": "git",
|
|
31
|
-
"url": "https://github.com/pablorovito/claude-git-hooks.git"
|
|
32
|
-
},
|
|
33
|
-
"engines": {
|
|
34
|
-
"node": ">=16.9.0"
|
|
35
|
-
},
|
|
36
|
-
"engineStrict": false,
|
|
37
|
-
"os": [
|
|
38
|
-
"darwin",
|
|
39
|
-
"linux",
|
|
40
|
-
"win32"
|
|
41
|
-
],
|
|
42
|
-
"preferGlobal": true,
|
|
43
|
-
"files": [
|
|
44
|
-
"bin/",
|
|
45
|
-
"lib/",
|
|
46
|
-
"templates/",
|
|
47
|
-
"README.md",
|
|
48
|
-
"CHANGELOG.md",
|
|
49
|
-
"LICENSE"
|
|
50
|
-
],
|
|
51
|
-
"dependencies": {
|
|
52
|
-
"@octokit/rest": "^21.0.0"
|
|
53
|
-
},
|
|
54
|
-
"devDependencies": {
|
|
55
|
-
"@types/jest": "^29.5.0",
|
|
56
|
-
"eslint": "^8.57.0",
|
|
57
|
-
"jest": "^29.7.0",
|
|
58
|
-
"prettier": "^3.2.0"
|
|
59
|
-
}
|
|
60
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-git-hooks",
|
|
3
|
+
"version": "2.14.4",
|
|
4
|
+
"description": "Git hooks with Claude CLI for code analysis and automatic commit messages",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"claude-hooks": "./bin/claude-hooks"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
11
|
+
"test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
|
|
12
|
+
"test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
|
|
13
|
+
"lint": "eslint lib/ bin/",
|
|
14
|
+
"lint:fix": "eslint lib/ bin/ --fix",
|
|
15
|
+
"format": "prettier --write \"lib/**/*.js\" \"bin/**\""
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"git",
|
|
19
|
+
"hooks",
|
|
20
|
+
"claude",
|
|
21
|
+
"ai",
|
|
22
|
+
"code-review",
|
|
23
|
+
"commit-messages",
|
|
24
|
+
"pre-commit",
|
|
25
|
+
"automation"
|
|
26
|
+
],
|
|
27
|
+
"author": "Pablo Rovito",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/pablorovito/claude-git-hooks.git"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=16.9.0"
|
|
35
|
+
},
|
|
36
|
+
"engineStrict": false,
|
|
37
|
+
"os": [
|
|
38
|
+
"darwin",
|
|
39
|
+
"linux",
|
|
40
|
+
"win32"
|
|
41
|
+
],
|
|
42
|
+
"preferGlobal": true,
|
|
43
|
+
"files": [
|
|
44
|
+
"bin/",
|
|
45
|
+
"lib/",
|
|
46
|
+
"templates/",
|
|
47
|
+
"README.md",
|
|
48
|
+
"CHANGELOG.md",
|
|
49
|
+
"LICENSE"
|
|
50
|
+
],
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@octokit/rest": "^21.0.0"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/jest": "^29.5.0",
|
|
56
|
+
"eslint": "^8.57.0",
|
|
57
|
+
"jest": "^29.7.0",
|
|
58
|
+
"prettier": "^3.2.0"
|
|
59
|
+
}
|
|
60
|
+
}
|