@eldrforge/commands-git 0.1.3 ā 0.1.5
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/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import 'dotenv/config';
|
|
|
3
3
|
import shellescape from 'shell-escape';
|
|
4
4
|
import { getDryRunLogger, DEFAULT_MAX_DIFF_BYTES, DEFAULT_EXCLUDED_PATTERNS, Diff, Files, Log, DEFAULT_OUTPUT_DIRECTORY, sanitizeDirection, toAIConfig, createStorageAdapter, createLoggerAdapter, getOutputPath, getTimestampedResponseFilename, getTimestampedRequestFilename, filterContent, getTimestampedCommitFilename, improveContentWithLLM, getLogger, getTimestampedReviewNotesFilename, getTimestampedReviewFilename } from '@eldrforge/core';
|
|
5
5
|
import { ValidationError, ExternalDependencyError, CommandError, createStorage, checkForFileDependencies as checkForFileDependencies$1, logFileDependencyWarning, logFileDependencySuggestions, FileOperationError } from '@eldrforge/shared';
|
|
6
|
-
import { run, validateString, safeJsonParse, validatePackageJson, unstageAll, stageFiles, verifyStagedFiles, runSecure } from '@eldrforge/git-tools';
|
|
6
|
+
import { run, validateString, safeJsonParse, validatePackageJson, unstageAll, stageFiles, verifyStagedFiles, runSecure, getCurrentBranch, getGitStatusSummary } from '@eldrforge/git-tools';
|
|
7
7
|
import { getRecentClosedIssuesForCommit, handleIssueCreation, getReleaseNotesContent, getIssuesContent } from '@eldrforge/github-tools';
|
|
8
8
|
import { runAgenticCommit, requireTTY, generateReflectionReport, getUserChoice, STANDARD_CHOICES, getLLMFeedbackInEditor, editContentInEditor, createCompletionWithRetry, createCommitPrompt, createReviewPrompt, createCompletion } from '@eldrforge/ai-service';
|
|
9
9
|
import path from 'path';
|
|
@@ -311,6 +311,41 @@ const saveCommitMessage = async (outputDirectory, summary, storage, logger)=>{
|
|
|
311
311
|
}
|
|
312
312
|
}
|
|
313
313
|
};
|
|
314
|
+
/**
|
|
315
|
+
* Deduplicate files across splits - each file can only be in one split
|
|
316
|
+
* Later splits lose files that were already claimed by earlier splits
|
|
317
|
+
* Returns filtered splits with empty splits removed
|
|
318
|
+
*/ function deduplicateSplits(splits, logger) {
|
|
319
|
+
const claimedFiles = new Set();
|
|
320
|
+
const result = [];
|
|
321
|
+
for (const split of splits){
|
|
322
|
+
// Find files in this split that haven't been claimed yet
|
|
323
|
+
const uniqueFiles = [];
|
|
324
|
+
const duplicates = [];
|
|
325
|
+
for (const file of split.files){
|
|
326
|
+
if (claimedFiles.has(file)) {
|
|
327
|
+
duplicates.push(file);
|
|
328
|
+
} else {
|
|
329
|
+
uniqueFiles.push(file);
|
|
330
|
+
claimedFiles.add(file);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// Log if duplicates were found
|
|
334
|
+
if (duplicates.length > 0) {
|
|
335
|
+
logger.warn(`Removing duplicate files from split "${split.message.split('\n')[0]}": ${duplicates.join(', ')}`);
|
|
336
|
+
}
|
|
337
|
+
// Only include split if it has files
|
|
338
|
+
if (uniqueFiles.length > 0) {
|
|
339
|
+
result.push({
|
|
340
|
+
...split,
|
|
341
|
+
files: uniqueFiles
|
|
342
|
+
});
|
|
343
|
+
} else {
|
|
344
|
+
logger.warn(`Skipping empty split after deduplication: "${split.message.split('\n')[0]}"`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
314
349
|
/**
|
|
315
350
|
* Interactive review of a single split before committing
|
|
316
351
|
*/ async function reviewSplitInteractively(split, index, total, logger) {
|
|
@@ -516,7 +551,7 @@ const saveCommitMessage = async (outputDirectory, summary, storage, logger)=>{
|
|
|
516
551
|
lines.push('ā'.repeat(80));
|
|
517
552
|
return lines.join('\n');
|
|
518
553
|
}
|
|
519
|
-
const executeInternal$
|
|
554
|
+
const executeInternal$3 = async (runConfig)=>{
|
|
520
555
|
var _ref, _runConfig_excludedPatterns;
|
|
521
556
|
var _runConfig_commit, _runConfig_commit1, _runConfig_commit2, _runConfig_commit3, _runConfig_commit4, _runConfig_commit5, _runConfig_commit6, _aiConfig_commands_commit, _aiConfig_commands, _runConfig_commit7, _aiConfig_commands_commit1, _aiConfig_commands1, _runConfig_commit8, _runConfig_commit9, _runConfig_commit10, _runConfig_commit11, _runConfig_commit12, _runConfig_commit13, _runConfig_commit14;
|
|
522
557
|
const isDryRun = runConfig.dryRun || false;
|
|
@@ -722,8 +757,14 @@ const executeInternal$2 = async (runConfig)=>{
|
|
|
722
757
|
if (autoSplitEnabled) {
|
|
723
758
|
var _runConfig_commit19, _runConfig_commit20;
|
|
724
759
|
logger.info('\nš Auto-split enabled - creating separate commits...\n');
|
|
760
|
+
// Deduplicate files across splits to prevent staging errors
|
|
761
|
+
// (AI sometimes suggests the same file in multiple splits)
|
|
762
|
+
const deduplicatedSplits = deduplicateSplits(agenticResult.suggestedSplits, logger);
|
|
763
|
+
if (deduplicatedSplits.length === 0) {
|
|
764
|
+
throw new CommandError('All splits were empty after deduplication - no files to commit', 'SPLIT_EMPTY', false);
|
|
765
|
+
}
|
|
725
766
|
const splitResult = await executeSplitCommits({
|
|
726
|
-
splits:
|
|
767
|
+
splits: deduplicatedSplits,
|
|
727
768
|
isDryRun,
|
|
728
769
|
interactive: !!(((_runConfig_commit19 = runConfig.commit) === null || _runConfig_commit19 === void 0 ? void 0 : _runConfig_commit19.interactive) && !((_runConfig_commit20 = runConfig.commit) === null || _runConfig_commit20 === void 0 ? void 0 : _runConfig_commit20.sendit)),
|
|
729
770
|
logger});
|
|
@@ -876,9 +917,9 @@ const executeInternal$2 = async (runConfig)=>{
|
|
|
876
917
|
}
|
|
877
918
|
return summary;
|
|
878
919
|
};
|
|
879
|
-
const execute$
|
|
920
|
+
const execute$4 = async (runConfig)=>{
|
|
880
921
|
try {
|
|
881
|
-
return await executeInternal$
|
|
922
|
+
return await executeInternal$3(runConfig);
|
|
882
923
|
} catch (error) {
|
|
883
924
|
// Import getLogger for error handling
|
|
884
925
|
const { getLogger } = await import('@eldrforge/core');
|
|
@@ -1502,7 +1543,7 @@ const checkForFileDependencies = (packageJsonFiles)=>{
|
|
|
1502
1543
|
* Execute precommit checks: lint -> build -> test
|
|
1503
1544
|
* Skips clean step (clean should be run separately if needed)
|
|
1504
1545
|
* Uses optimization to skip steps when unchanged
|
|
1505
|
-
*/ const execute$
|
|
1546
|
+
*/ const execute$3 = async (runConfig)=>{
|
|
1506
1547
|
const logger = getLogger();
|
|
1507
1548
|
const isDryRun = runConfig.dryRun || false;
|
|
1508
1549
|
const packageDir = process.cwd();
|
|
@@ -1588,7 +1629,7 @@ const checkForFileDependencies = (packageJsonFiles)=>{
|
|
|
1588
1629
|
}
|
|
1589
1630
|
};
|
|
1590
1631
|
|
|
1591
|
-
const executeInternal$
|
|
1632
|
+
const executeInternal$2 = async (runConfig)=>{
|
|
1592
1633
|
const isDryRun = runConfig.dryRun || false;
|
|
1593
1634
|
const logger = getDryRunLogger(isDryRun);
|
|
1594
1635
|
const storage = createStorage();
|
|
@@ -1612,9 +1653,9 @@ const executeInternal$1 = async (runConfig)=>{
|
|
|
1612
1653
|
throw new FileOperationError('Failed to remove output directory', outputDirectory, error);
|
|
1613
1654
|
}
|
|
1614
1655
|
};
|
|
1615
|
-
const execute$
|
|
1656
|
+
const execute$2 = async (runConfig)=>{
|
|
1616
1657
|
try {
|
|
1617
|
-
await executeInternal$
|
|
1658
|
+
await executeInternal$2(runConfig);
|
|
1618
1659
|
} catch (error) {
|
|
1619
1660
|
const logger = getLogger();
|
|
1620
1661
|
if (error instanceof FileOperationError) {
|
|
@@ -2083,7 +2124,7 @@ const processSingleReview = async (reviewNote, runConfig, outputDirectory)=>{
|
|
|
2083
2124
|
}
|
|
2084
2125
|
return analysisResult;
|
|
2085
2126
|
};
|
|
2086
|
-
const executeInternal = async (runConfig)=>{
|
|
2127
|
+
const executeInternal$1 = async (runConfig)=>{
|
|
2087
2128
|
var _runConfig_review, _runConfig_review1, _runConfig_review2, _runConfig_review3, _runConfig_review4, _runConfig_review5, _runConfig_review6, _runConfig_review7, _runConfig_review8, _runConfig_review9, _runConfig_review10, _runConfig_review11, _runConfig_review12, _runConfig_review13, _runConfig_review14, _runConfig_review15, _runConfig_review16, _runConfig_review17, _runConfig_review18;
|
|
2088
2129
|
const logger = getLogger();
|
|
2089
2130
|
const isDryRun = runConfig.dryRun || false;
|
|
@@ -2313,9 +2354,9 @@ const executeInternal = async (runConfig)=>{
|
|
|
2313
2354
|
const senditMode = ((_runConfig_review18 = runConfig.review) === null || _runConfig_review18 === void 0 ? void 0 : _runConfig_review18.sendit) || false;
|
|
2314
2355
|
return await handleIssueCreation(analysisResult, senditMode);
|
|
2315
2356
|
};
|
|
2316
|
-
const execute = async (runConfig)=>{
|
|
2357
|
+
const execute$1 = async (runConfig)=>{
|
|
2317
2358
|
try {
|
|
2318
|
-
return await executeInternal(runConfig);
|
|
2359
|
+
return await executeInternal$1(runConfig);
|
|
2319
2360
|
} catch (error) {
|
|
2320
2361
|
const logger = getLogger();
|
|
2321
2362
|
if (error instanceof ValidationError) {
|
|
@@ -2342,5 +2383,585 @@ const execute = async (runConfig)=>{
|
|
|
2342
2383
|
}
|
|
2343
2384
|
};
|
|
2344
2385
|
|
|
2345
|
-
|
|
2386
|
+
// Patterns for files that can be auto-resolved
|
|
2387
|
+
const AUTO_RESOLVABLE_PATTERNS = {
|
|
2388
|
+
// Package lock files - just regenerate
|
|
2389
|
+
packageLock: /^package-lock\.json$/,
|
|
2390
|
+
yarnLock: /^yarn\.lock$/,
|
|
2391
|
+
pnpmLock: /^pnpm-lock\.yaml$/,
|
|
2392
|
+
// Generated files - take theirs and regenerate
|
|
2393
|
+
dist: /^dist\//,
|
|
2394
|
+
coverage: /^coverage\//,
|
|
2395
|
+
nodeModules: /^node_modules\//,
|
|
2396
|
+
// Build artifacts
|
|
2397
|
+
buildOutput: /\.(js\.map|d\.ts)$/
|
|
2398
|
+
};
|
|
2399
|
+
/**
|
|
2400
|
+
* Check if a file can be auto-resolved
|
|
2401
|
+
*/ function canAutoResolve(filename) {
|
|
2402
|
+
if (AUTO_RESOLVABLE_PATTERNS.packageLock.test(filename)) {
|
|
2403
|
+
return {
|
|
2404
|
+
canResolve: true,
|
|
2405
|
+
strategy: 'regenerate-lock'
|
|
2406
|
+
};
|
|
2407
|
+
}
|
|
2408
|
+
if (AUTO_RESOLVABLE_PATTERNS.yarnLock.test(filename)) {
|
|
2409
|
+
return {
|
|
2410
|
+
canResolve: true,
|
|
2411
|
+
strategy: 'regenerate-lock'
|
|
2412
|
+
};
|
|
2413
|
+
}
|
|
2414
|
+
if (AUTO_RESOLVABLE_PATTERNS.pnpmLock.test(filename)) {
|
|
2415
|
+
return {
|
|
2416
|
+
canResolve: true,
|
|
2417
|
+
strategy: 'regenerate-lock'
|
|
2418
|
+
};
|
|
2419
|
+
}
|
|
2420
|
+
if (AUTO_RESOLVABLE_PATTERNS.dist.test(filename)) {
|
|
2421
|
+
return {
|
|
2422
|
+
canResolve: true,
|
|
2423
|
+
strategy: 'take-theirs-regenerate'
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2426
|
+
if (AUTO_RESOLVABLE_PATTERNS.coverage.test(filename)) {
|
|
2427
|
+
return {
|
|
2428
|
+
canResolve: true,
|
|
2429
|
+
strategy: 'take-theirs'
|
|
2430
|
+
};
|
|
2431
|
+
}
|
|
2432
|
+
if (AUTO_RESOLVABLE_PATTERNS.nodeModules.test(filename)) {
|
|
2433
|
+
return {
|
|
2434
|
+
canResolve: true,
|
|
2435
|
+
strategy: 'take-theirs'
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2438
|
+
if (AUTO_RESOLVABLE_PATTERNS.buildOutput.test(filename)) {
|
|
2439
|
+
return {
|
|
2440
|
+
canResolve: true,
|
|
2441
|
+
strategy: 'take-theirs-regenerate'
|
|
2442
|
+
};
|
|
2443
|
+
}
|
|
2444
|
+
return {
|
|
2445
|
+
canResolve: false,
|
|
2446
|
+
strategy: 'manual'
|
|
2447
|
+
};
|
|
2448
|
+
}
|
|
2449
|
+
/**
|
|
2450
|
+
* Check if this is a package.json version conflict that can be auto-resolved
|
|
2451
|
+
*/ async function tryResolvePackageJsonConflict(filepath, logger) {
|
|
2452
|
+
const storage = createStorage();
|
|
2453
|
+
try {
|
|
2454
|
+
const content = await storage.readFile(filepath, 'utf-8');
|
|
2455
|
+
// Check if this is actually a conflict file
|
|
2456
|
+
if (!content.includes('<<<<<<<') || !content.includes('>>>>>>>')) {
|
|
2457
|
+
return {
|
|
2458
|
+
resolved: false,
|
|
2459
|
+
error: 'Not a conflict file'
|
|
2460
|
+
};
|
|
2461
|
+
}
|
|
2462
|
+
// Try to parse ours and theirs versions
|
|
2463
|
+
const oursMatch = content.match(/<<<<<<< .*?\n([\s\S]*?)=======\n/);
|
|
2464
|
+
const theirsMatch = content.match(/=======\n([\s\S]*?)>>>>>>> /);
|
|
2465
|
+
if (!oursMatch || !theirsMatch) {
|
|
2466
|
+
return {
|
|
2467
|
+
resolved: false,
|
|
2468
|
+
error: 'Cannot parse conflict markers'
|
|
2469
|
+
};
|
|
2470
|
+
}
|
|
2471
|
+
// For package.json, if only version differs, take the higher version
|
|
2472
|
+
// This is a simplified heuristic - real conflicts may need more logic
|
|
2473
|
+
const oursPart = oursMatch[1];
|
|
2474
|
+
const theirsPart = theirsMatch[1];
|
|
2475
|
+
// Check if this is just a version conflict
|
|
2476
|
+
const versionPattern = /"version":\s*"([^"]+)"/;
|
|
2477
|
+
const oursVersion = oursPart.match(versionPattern);
|
|
2478
|
+
const theirsVersion = theirsPart.match(versionPattern);
|
|
2479
|
+
if (oursVersion && theirsVersion) {
|
|
2480
|
+
// Both have versions - take higher one
|
|
2481
|
+
const semver = await import('semver');
|
|
2482
|
+
const higher = semver.gt(oursVersion[1].replace(/-.*$/, ''), theirsVersion[1].replace(/-.*$/, '')) ? oursVersion[1] : theirsVersion[1];
|
|
2483
|
+
// Replace the conflicted version with the higher one
|
|
2484
|
+
let resolvedContent = content;
|
|
2485
|
+
// Simple approach: take theirs but use higher version
|
|
2486
|
+
// Remove conflict markers and use theirs as base
|
|
2487
|
+
resolvedContent = content.replace(/<<<<<<< .*?\n[\s\S]*?=======\n([\s\S]*?)>>>>>>> .*?\n/g, '$1');
|
|
2488
|
+
// Update version to higher one
|
|
2489
|
+
resolvedContent = resolvedContent.replace(/"version":\s*"[^"]+"/, `"version": "${higher}"`);
|
|
2490
|
+
await storage.writeFile(filepath, resolvedContent, 'utf-8');
|
|
2491
|
+
logger.info(`PULL_RESOLVED_VERSION: Auto-resolved version conflict | File: ${filepath} | Version: ${higher}`);
|
|
2492
|
+
return {
|
|
2493
|
+
resolved: true
|
|
2494
|
+
};
|
|
2495
|
+
}
|
|
2496
|
+
return {
|
|
2497
|
+
resolved: false,
|
|
2498
|
+
error: 'Complex conflict - not just version'
|
|
2499
|
+
};
|
|
2500
|
+
} catch (error) {
|
|
2501
|
+
return {
|
|
2502
|
+
resolved: false,
|
|
2503
|
+
error: error.message
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
/**
|
|
2508
|
+
* Resolve a single conflict file
|
|
2509
|
+
*/ async function resolveConflict(filepath, strategy, logger, isDryRun) {
|
|
2510
|
+
if (isDryRun) {
|
|
2511
|
+
logger.info(`PULL_RESOLVE_DRY_RUN: Would resolve conflict | File: ${filepath} | Strategy: ${strategy}`);
|
|
2512
|
+
return {
|
|
2513
|
+
file: filepath,
|
|
2514
|
+
resolved: true,
|
|
2515
|
+
strategy
|
|
2516
|
+
};
|
|
2517
|
+
}
|
|
2518
|
+
try {
|
|
2519
|
+
switch(strategy){
|
|
2520
|
+
case 'regenerate-lock':
|
|
2521
|
+
{
|
|
2522
|
+
// Accept theirs and regenerate
|
|
2523
|
+
await runSecure('git', [
|
|
2524
|
+
'checkout',
|
|
2525
|
+
'--theirs',
|
|
2526
|
+
filepath
|
|
2527
|
+
]);
|
|
2528
|
+
await runSecure('git', [
|
|
2529
|
+
'add',
|
|
2530
|
+
filepath
|
|
2531
|
+
]);
|
|
2532
|
+
logger.info(`PULL_CONFLICT_RESOLVED: Accepted remote lock file | File: ${filepath} | Strategy: ${strategy} | Note: Will regenerate after pull`);
|
|
2533
|
+
return {
|
|
2534
|
+
file: filepath,
|
|
2535
|
+
resolved: true,
|
|
2536
|
+
strategy
|
|
2537
|
+
};
|
|
2538
|
+
}
|
|
2539
|
+
case 'take-theirs':
|
|
2540
|
+
case 'take-theirs-regenerate':
|
|
2541
|
+
{
|
|
2542
|
+
await runSecure('git', [
|
|
2543
|
+
'checkout',
|
|
2544
|
+
'--theirs',
|
|
2545
|
+
filepath
|
|
2546
|
+
]);
|
|
2547
|
+
await runSecure('git', [
|
|
2548
|
+
'add',
|
|
2549
|
+
filepath
|
|
2550
|
+
]);
|
|
2551
|
+
logger.info(`PULL_CONFLICT_RESOLVED: Accepted remote version | File: ${filepath} | Strategy: ${strategy}`);
|
|
2552
|
+
return {
|
|
2553
|
+
file: filepath,
|
|
2554
|
+
resolved: true,
|
|
2555
|
+
strategy
|
|
2556
|
+
};
|
|
2557
|
+
}
|
|
2558
|
+
case 'version-bump':
|
|
2559
|
+
{
|
|
2560
|
+
const result = await tryResolvePackageJsonConflict(filepath, logger);
|
|
2561
|
+
if (result.resolved) {
|
|
2562
|
+
await runSecure('git', [
|
|
2563
|
+
'add',
|
|
2564
|
+
filepath
|
|
2565
|
+
]);
|
|
2566
|
+
return {
|
|
2567
|
+
file: filepath,
|
|
2568
|
+
resolved: true,
|
|
2569
|
+
strategy
|
|
2570
|
+
};
|
|
2571
|
+
}
|
|
2572
|
+
return {
|
|
2573
|
+
file: filepath,
|
|
2574
|
+
resolved: false,
|
|
2575
|
+
strategy,
|
|
2576
|
+
error: result.error
|
|
2577
|
+
};
|
|
2578
|
+
}
|
|
2579
|
+
default:
|
|
2580
|
+
return {
|
|
2581
|
+
file: filepath,
|
|
2582
|
+
resolved: false,
|
|
2583
|
+
strategy,
|
|
2584
|
+
error: 'Unknown resolution strategy'
|
|
2585
|
+
};
|
|
2586
|
+
}
|
|
2587
|
+
} catch (error) {
|
|
2588
|
+
return {
|
|
2589
|
+
file: filepath,
|
|
2590
|
+
resolved: false,
|
|
2591
|
+
strategy,
|
|
2592
|
+
error: error.message
|
|
2593
|
+
};
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
/**
|
|
2597
|
+
* Get list of conflicted files
|
|
2598
|
+
*/ async function getConflictedFiles() {
|
|
2599
|
+
try {
|
|
2600
|
+
const { stdout } = await runSecure('git', [
|
|
2601
|
+
'diff',
|
|
2602
|
+
'--name-only',
|
|
2603
|
+
'--diff-filter=U'
|
|
2604
|
+
]);
|
|
2605
|
+
return stdout.trim().split('\n').filter((f)=>f.trim());
|
|
2606
|
+
} catch {
|
|
2607
|
+
return [];
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
/**
|
|
2611
|
+
* Try to auto-resolve all conflicts
|
|
2612
|
+
*/ async function autoResolveConflicts(logger, isDryRun) {
|
|
2613
|
+
const conflictedFiles = await getConflictedFiles();
|
|
2614
|
+
const resolved = [];
|
|
2615
|
+
const manual = [];
|
|
2616
|
+
for (const file of conflictedFiles){
|
|
2617
|
+
const { canResolve, strategy } = canAutoResolve(file);
|
|
2618
|
+
// Special handling for package.json
|
|
2619
|
+
if (file === 'package.json' || file.endsWith('/package.json')) {
|
|
2620
|
+
const result = await resolveConflict(file, 'version-bump', logger, isDryRun);
|
|
2621
|
+
if (result.resolved) {
|
|
2622
|
+
resolved.push(file);
|
|
2623
|
+
} else {
|
|
2624
|
+
manual.push(file);
|
|
2625
|
+
}
|
|
2626
|
+
continue;
|
|
2627
|
+
}
|
|
2628
|
+
if (canResolve) {
|
|
2629
|
+
const result = await resolveConflict(file, strategy, logger, isDryRun);
|
|
2630
|
+
if (result.resolved) {
|
|
2631
|
+
resolved.push(file);
|
|
2632
|
+
} else {
|
|
2633
|
+
manual.push(file);
|
|
2634
|
+
}
|
|
2635
|
+
} else {
|
|
2636
|
+
manual.push(file);
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
return {
|
|
2640
|
+
resolved,
|
|
2641
|
+
manual
|
|
2642
|
+
};
|
|
2643
|
+
}
|
|
2644
|
+
/**
|
|
2645
|
+
* Stash local changes if any
|
|
2646
|
+
*/ async function stashIfNeeded(logger, isDryRun) {
|
|
2647
|
+
const status = await getGitStatusSummary();
|
|
2648
|
+
if (status.hasUncommittedChanges || status.hasUnstagedFiles) {
|
|
2649
|
+
const changeCount = status.uncommittedCount + status.unstagedCount;
|
|
2650
|
+
logger.info(`PULL_STASHING: Stashing ${changeCount} local changes before pull | Staged: ${status.uncommittedCount} | Unstaged: ${status.unstagedCount}`);
|
|
2651
|
+
if (!isDryRun) {
|
|
2652
|
+
await runSecure('git', [
|
|
2653
|
+
'stash',
|
|
2654
|
+
'push',
|
|
2655
|
+
'-m',
|
|
2656
|
+
`kodrdriv-pull-auto-stash-${Date.now()}`
|
|
2657
|
+
]);
|
|
2658
|
+
}
|
|
2659
|
+
return true;
|
|
2660
|
+
}
|
|
2661
|
+
return false;
|
|
2662
|
+
}
|
|
2663
|
+
/**
|
|
2664
|
+
* Apply stash if we created one
|
|
2665
|
+
*/ async function applyStashIfNeeded(didStash, logger, isDryRun) {
|
|
2666
|
+
if (!didStash) return false;
|
|
2667
|
+
logger.info('PULL_STASH_POP: Restoring stashed changes');
|
|
2668
|
+
if (!isDryRun) {
|
|
2669
|
+
try {
|
|
2670
|
+
await runSecure('git', [
|
|
2671
|
+
'stash',
|
|
2672
|
+
'pop'
|
|
2673
|
+
]);
|
|
2674
|
+
return true;
|
|
2675
|
+
} catch (error) {
|
|
2676
|
+
logger.warn(`PULL_STASH_CONFLICT: Stash pop had conflicts | Error: ${error.message} | Action: Stash preserved, manual intervention needed`);
|
|
2677
|
+
// Don't fail - user can manually resolve stash conflicts
|
|
2678
|
+
return false;
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
return true;
|
|
2682
|
+
}
|
|
2683
|
+
/**
|
|
2684
|
+
* Regenerate lock files after pull
|
|
2685
|
+
*/ async function regenerateLockFiles(resolvedFiles, logger, isDryRun) {
|
|
2686
|
+
const needsRegenerate = resolvedFiles.some((f)=>f === 'package-lock.json' || f.endsWith('/package-lock.json'));
|
|
2687
|
+
if (needsRegenerate) {
|
|
2688
|
+
logger.info('PULL_REGENERATE_LOCK: Regenerating package-lock.json');
|
|
2689
|
+
if (!isDryRun) {
|
|
2690
|
+
try {
|
|
2691
|
+
await run('npm install');
|
|
2692
|
+
logger.info('PULL_REGENERATE_SUCCESS: Lock file regenerated successfully');
|
|
2693
|
+
} catch (error) {
|
|
2694
|
+
logger.warn(`PULL_REGENERATE_FAILED: Failed to regenerate lock file | Error: ${error.message}`);
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
/**
|
|
2700
|
+
* Main pull execution
|
|
2701
|
+
*/ async function executePull(remote, branch, logger, isDryRun) {
|
|
2702
|
+
const currentBranch = await getCurrentBranch();
|
|
2703
|
+
const targetBranch = branch || currentBranch;
|
|
2704
|
+
logger.info(`PULL_STARTING: Pulling changes | Remote: ${remote} | Branch: ${targetBranch} | Current: ${currentBranch}`);
|
|
2705
|
+
// Step 1: Stash any local changes
|
|
2706
|
+
const didStash = await stashIfNeeded(logger, isDryRun);
|
|
2707
|
+
// Step 2: Fetch first to see what's coming
|
|
2708
|
+
logger.info(`PULL_FETCH: Fetching from ${remote}`);
|
|
2709
|
+
if (!isDryRun) {
|
|
2710
|
+
try {
|
|
2711
|
+
await runSecure('git', [
|
|
2712
|
+
'fetch',
|
|
2713
|
+
remote,
|
|
2714
|
+
targetBranch
|
|
2715
|
+
]);
|
|
2716
|
+
} catch (error) {
|
|
2717
|
+
logger.error(`PULL_FETCH_FAILED: Failed to fetch | Error: ${error.message}`);
|
|
2718
|
+
if (didStash) await applyStashIfNeeded(true, logger, isDryRun);
|
|
2719
|
+
return {
|
|
2720
|
+
success: false,
|
|
2721
|
+
hadConflicts: false,
|
|
2722
|
+
autoResolved: [],
|
|
2723
|
+
manualRequired: [],
|
|
2724
|
+
stashApplied: didStash,
|
|
2725
|
+
strategy: 'failed',
|
|
2726
|
+
message: `Fetch failed: ${error.message}`
|
|
2727
|
+
};
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
// Step 3: Try fast-forward first
|
|
2731
|
+
logger.info('PULL_STRATEGY: Attempting fast-forward merge');
|
|
2732
|
+
if (!isDryRun) {
|
|
2733
|
+
try {
|
|
2734
|
+
await runSecure('git', [
|
|
2735
|
+
'merge',
|
|
2736
|
+
'--ff-only',
|
|
2737
|
+
`${remote}/${targetBranch}`
|
|
2738
|
+
]);
|
|
2739
|
+
await applyStashIfNeeded(didStash, logger, isDryRun);
|
|
2740
|
+
logger.info('PULL_SUCCESS: Fast-forward merge successful');
|
|
2741
|
+
return {
|
|
2742
|
+
success: true,
|
|
2743
|
+
hadConflicts: false,
|
|
2744
|
+
autoResolved: [],
|
|
2745
|
+
manualRequired: [],
|
|
2746
|
+
stashApplied: didStash,
|
|
2747
|
+
strategy: 'fast-forward',
|
|
2748
|
+
message: 'Fast-forward merge successful'
|
|
2749
|
+
};
|
|
2750
|
+
} catch {
|
|
2751
|
+
logger.info('PULL_FF_FAILED: Fast-forward not possible, trying rebase');
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
// Step 4: Try rebase
|
|
2755
|
+
logger.info('PULL_STRATEGY: Attempting rebase');
|
|
2756
|
+
if (!isDryRun) {
|
|
2757
|
+
try {
|
|
2758
|
+
await runSecure('git', [
|
|
2759
|
+
'rebase',
|
|
2760
|
+
`${remote}/${targetBranch}`
|
|
2761
|
+
]);
|
|
2762
|
+
await applyStashIfNeeded(didStash, logger, isDryRun);
|
|
2763
|
+
logger.info('PULL_SUCCESS: Rebase successful');
|
|
2764
|
+
return {
|
|
2765
|
+
success: true,
|
|
2766
|
+
hadConflicts: false,
|
|
2767
|
+
autoResolved: [],
|
|
2768
|
+
manualRequired: [],
|
|
2769
|
+
stashApplied: didStash,
|
|
2770
|
+
strategy: 'rebase',
|
|
2771
|
+
message: 'Rebase successful'
|
|
2772
|
+
};
|
|
2773
|
+
} catch {
|
|
2774
|
+
// Check if rebase is in progress with conflicts
|
|
2775
|
+
const conflictedFiles = await getConflictedFiles();
|
|
2776
|
+
if (conflictedFiles.length > 0) {
|
|
2777
|
+
logger.info(`PULL_CONFLICTS: Rebase has ${conflictedFiles.length} conflicts, attempting auto-resolution`);
|
|
2778
|
+
// Step 5: Try to auto-resolve conflicts
|
|
2779
|
+
const { resolved, manual } = await autoResolveConflicts(logger, isDryRun);
|
|
2780
|
+
if (manual.length === 0) {
|
|
2781
|
+
// All conflicts resolved, continue rebase
|
|
2782
|
+
logger.info('PULL_ALL_RESOLVED: All conflicts auto-resolved, continuing rebase');
|
|
2783
|
+
try {
|
|
2784
|
+
await runSecure('git', [
|
|
2785
|
+
'rebase',
|
|
2786
|
+
'--continue'
|
|
2787
|
+
]);
|
|
2788
|
+
await regenerateLockFiles(resolved, logger, isDryRun);
|
|
2789
|
+
await applyStashIfNeeded(didStash, logger, isDryRun);
|
|
2790
|
+
return {
|
|
2791
|
+
success: true,
|
|
2792
|
+
hadConflicts: true,
|
|
2793
|
+
autoResolved: resolved,
|
|
2794
|
+
manualRequired: [],
|
|
2795
|
+
stashApplied: didStash,
|
|
2796
|
+
strategy: 'rebase',
|
|
2797
|
+
message: `Rebase successful with ${resolved.length} auto-resolved conflicts`
|
|
2798
|
+
};
|
|
2799
|
+
} catch (continueError) {
|
|
2800
|
+
logger.warn(`PULL_CONTINUE_FAILED: Rebase continue failed | Error: ${continueError.message}`);
|
|
2801
|
+
}
|
|
2802
|
+
} else {
|
|
2803
|
+
// Some conflicts need manual resolution
|
|
2804
|
+
logger.warn(`PULL_MANUAL_REQUIRED: ${manual.length} conflicts require manual resolution`);
|
|
2805
|
+
logger.warn('PULL_MANUAL_FILES: Files needing manual resolution:');
|
|
2806
|
+
manual.forEach((f)=>logger.warn(` - ${f}`));
|
|
2807
|
+
logger.info('PULL_HINT: After resolving conflicts manually, run: git rebase --continue');
|
|
2808
|
+
// Keep rebase in progress so user can finish
|
|
2809
|
+
return {
|
|
2810
|
+
success: false,
|
|
2811
|
+
hadConflicts: true,
|
|
2812
|
+
autoResolved: resolved,
|
|
2813
|
+
manualRequired: manual,
|
|
2814
|
+
stashApplied: false,
|
|
2815
|
+
strategy: 'rebase',
|
|
2816
|
+
message: `Rebase paused: ${manual.length} files need manual conflict resolution`
|
|
2817
|
+
};
|
|
2818
|
+
}
|
|
2819
|
+
} else {
|
|
2820
|
+
// Rebase failed for other reason, abort and try merge
|
|
2821
|
+
logger.info('PULL_REBASE_ABORT: Rebase failed, aborting and trying merge');
|
|
2822
|
+
try {
|
|
2823
|
+
await runSecure('git', [
|
|
2824
|
+
'rebase',
|
|
2825
|
+
'--abort'
|
|
2826
|
+
]);
|
|
2827
|
+
} catch {
|
|
2828
|
+
// Ignore abort errors
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
// Step 6: Fall back to regular merge
|
|
2834
|
+
logger.info('PULL_STRATEGY: Attempting merge');
|
|
2835
|
+
if (!isDryRun) {
|
|
2836
|
+
try {
|
|
2837
|
+
await runSecure('git', [
|
|
2838
|
+
'merge',
|
|
2839
|
+
`${remote}/${targetBranch}`
|
|
2840
|
+
]);
|
|
2841
|
+
await applyStashIfNeeded(didStash, logger, isDryRun);
|
|
2842
|
+
logger.info('PULL_SUCCESS: Merge successful');
|
|
2843
|
+
return {
|
|
2844
|
+
success: true,
|
|
2845
|
+
hadConflicts: false,
|
|
2846
|
+
autoResolved: [],
|
|
2847
|
+
manualRequired: [],
|
|
2848
|
+
stashApplied: didStash,
|
|
2849
|
+
strategy: 'merge',
|
|
2850
|
+
message: 'Merge successful'
|
|
2851
|
+
};
|
|
2852
|
+
} catch {
|
|
2853
|
+
// Check for merge conflicts
|
|
2854
|
+
const conflictedFiles = await getConflictedFiles();
|
|
2855
|
+
if (conflictedFiles.length > 0) {
|
|
2856
|
+
logger.info(`PULL_CONFLICTS: Merge has ${conflictedFiles.length} conflicts, attempting auto-resolution`);
|
|
2857
|
+
const { resolved, manual } = await autoResolveConflicts(logger, isDryRun);
|
|
2858
|
+
if (manual.length === 0) {
|
|
2859
|
+
// All conflicts resolved, commit the merge
|
|
2860
|
+
logger.info('PULL_ALL_RESOLVED: All conflicts auto-resolved, completing merge');
|
|
2861
|
+
try {
|
|
2862
|
+
await runSecure('git', [
|
|
2863
|
+
'commit',
|
|
2864
|
+
'-m',
|
|
2865
|
+
`Merge ${remote}/${targetBranch} (auto-resolved by kodrdriv)`
|
|
2866
|
+
]);
|
|
2867
|
+
await regenerateLockFiles(resolved, logger, isDryRun);
|
|
2868
|
+
await applyStashIfNeeded(didStash, logger, isDryRun);
|
|
2869
|
+
return {
|
|
2870
|
+
success: true,
|
|
2871
|
+
hadConflicts: true,
|
|
2872
|
+
autoResolved: resolved,
|
|
2873
|
+
manualRequired: [],
|
|
2874
|
+
stashApplied: didStash,
|
|
2875
|
+
strategy: 'merge',
|
|
2876
|
+
message: `Merge successful with ${resolved.length} auto-resolved conflicts`
|
|
2877
|
+
};
|
|
2878
|
+
} catch (commitError) {
|
|
2879
|
+
logger.error(`PULL_COMMIT_FAILED: Merge commit failed | Error: ${commitError.message}`);
|
|
2880
|
+
}
|
|
2881
|
+
} else {
|
|
2882
|
+
logger.warn(`PULL_MANUAL_REQUIRED: ${manual.length} conflicts require manual resolution`);
|
|
2883
|
+
manual.forEach((f)=>logger.warn(` - ${f}`));
|
|
2884
|
+
logger.info('PULL_HINT: After resolving conflicts manually, run: git commit');
|
|
2885
|
+
return {
|
|
2886
|
+
success: false,
|
|
2887
|
+
hadConflicts: true,
|
|
2888
|
+
autoResolved: resolved,
|
|
2889
|
+
manualRequired: manual,
|
|
2890
|
+
stashApplied: false,
|
|
2891
|
+
strategy: 'merge',
|
|
2892
|
+
message: `Merge paused: ${manual.length} files need manual conflict resolution`
|
|
2893
|
+
};
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
// If we got here, something went wrong
|
|
2899
|
+
if (didStash) {
|
|
2900
|
+
logger.warn('PULL_STASH_PRESERVED: Local changes still stashed, use "git stash pop" to restore');
|
|
2901
|
+
}
|
|
2902
|
+
return {
|
|
2903
|
+
success: false,
|
|
2904
|
+
hadConflicts: false,
|
|
2905
|
+
autoResolved: [],
|
|
2906
|
+
manualRequired: [],
|
|
2907
|
+
stashApplied: false,
|
|
2908
|
+
strategy: 'failed',
|
|
2909
|
+
message: 'Pull failed - unable to merge or rebase'
|
|
2910
|
+
};
|
|
2911
|
+
}
|
|
2912
|
+
/**
|
|
2913
|
+
* Internal execution
|
|
2914
|
+
*/ const executeInternal = async (runConfig)=>{
|
|
2915
|
+
const isDryRun = runConfig.dryRun || false;
|
|
2916
|
+
const logger = getDryRunLogger(isDryRun);
|
|
2917
|
+
// Get pull configuration
|
|
2918
|
+
const pullConfig = runConfig.pull || {};
|
|
2919
|
+
const remote = pullConfig.remote || 'origin';
|
|
2920
|
+
const branch = pullConfig.branch;
|
|
2921
|
+
// Execute pull
|
|
2922
|
+
const result = await executePull(remote, branch, logger, isDryRun);
|
|
2923
|
+
// Format output
|
|
2924
|
+
const lines = [];
|
|
2925
|
+
lines.push('');
|
|
2926
|
+
lines.push('ā'.repeat(60));
|
|
2927
|
+
lines.push(result.success ? 'ā
PULL COMPLETE' : 'ā ļø PULL NEEDS ATTENTION');
|
|
2928
|
+
lines.push('ā'.repeat(60));
|
|
2929
|
+
lines.push('');
|
|
2930
|
+
lines.push(`Strategy: ${result.strategy}`);
|
|
2931
|
+
lines.push(`Message: ${result.message}`);
|
|
2932
|
+
if (result.hadConflicts) {
|
|
2933
|
+
lines.push('');
|
|
2934
|
+
lines.push(`Conflicts detected: ${result.autoResolved.length + result.manualRequired.length}`);
|
|
2935
|
+
if (result.autoResolved.length > 0) {
|
|
2936
|
+
lines.push(`ā Auto-resolved: ${result.autoResolved.length}`);
|
|
2937
|
+
result.autoResolved.forEach((f)=>lines.push(` - ${f}`));
|
|
2938
|
+
}
|
|
2939
|
+
if (result.manualRequired.length > 0) {
|
|
2940
|
+
lines.push(`ā Manual resolution needed: ${result.manualRequired.length}`);
|
|
2941
|
+
result.manualRequired.forEach((f)=>lines.push(` - ${f}`));
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
if (result.stashApplied) {
|
|
2945
|
+
lines.push('');
|
|
2946
|
+
lines.push('ā¹ļø Local changes have been restored from stash');
|
|
2947
|
+
}
|
|
2948
|
+
lines.push('');
|
|
2949
|
+
lines.push('ā'.repeat(60));
|
|
2950
|
+
const output = lines.join('\n');
|
|
2951
|
+
logger.info(output);
|
|
2952
|
+
return output;
|
|
2953
|
+
};
|
|
2954
|
+
/**
|
|
2955
|
+
* Execute pull command
|
|
2956
|
+
*/ const execute = async (runConfig)=>{
|
|
2957
|
+
try {
|
|
2958
|
+
return await executeInternal(runConfig);
|
|
2959
|
+
} catch (error) {
|
|
2960
|
+
const logger = getLogger();
|
|
2961
|
+
logger.error(`PULL_COMMAND_FAILED: Pull command failed | Error: ${error.message}`);
|
|
2962
|
+
throw error;
|
|
2963
|
+
}
|
|
2964
|
+
};
|
|
2965
|
+
|
|
2966
|
+
export { PerformanceTimer, batchReadPackageJsonFiles, checkForFileDependencies, execute$2 as clean, collectAllDependencies, execute$4 as commit, findAllPackageJsonFiles, findPackagesByScope, isCleanNeeded, isTestNeeded, optimizePrecommitCommand, execute$3 as precommit, execute as pull, recordTestRun, execute$1 as review, scanDirectoryForPackages };
|
|
2346
2967
|
//# sourceMappingURL=index.js.map
|