@entro314labs/ai-changelog-generator 3.2.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -10
- package/ai-changelog-mcp.sh +0 -0
- package/ai-changelog.sh +0 -0
- package/bin/ai-changelog-dxt.js +0 -0
- package/package.json +72 -80
- package/src/ai-changelog-generator.js +11 -2
- package/src/application/orchestrators/changelog.orchestrator.js +12 -202
- package/src/cli.js +4 -5
- package/src/domains/ai/ai-analysis.service.js +2 -0
- package/src/domains/analysis/analysis.engine.js +758 -5
- package/src/domains/changelog/changelog.service.js +711 -13
- package/src/domains/changelog/workspace-changelog.service.js +429 -571
- package/src/domains/git/commit-tagger.js +552 -0
- package/src/domains/git/git-manager.js +357 -0
- package/src/domains/git/git.service.js +865 -16
- package/src/infrastructure/cli/cli.controller.js +14 -9
- package/src/infrastructure/config/configuration.manager.js +24 -2
- package/src/infrastructure/interactive/interactive-workflow.service.js +8 -1
- package/src/infrastructure/mcp/mcp-server.service.js +35 -11
- package/src/infrastructure/providers/core/base-provider.js +1 -1
- package/src/infrastructure/providers/implementations/anthropic.js +16 -173
- package/src/infrastructure/providers/implementations/azure.js +16 -63
- package/src/infrastructure/providers/implementations/dummy.js +13 -16
- package/src/infrastructure/providers/implementations/mock.js +13 -26
- package/src/infrastructure/providers/implementations/ollama.js +12 -4
- package/src/infrastructure/providers/implementations/openai.js +13 -165
- package/src/infrastructure/providers/provider-management.service.js +126 -412
- package/src/infrastructure/providers/utils/base-provider-helpers.js +11 -0
- package/src/shared/utils/cli-ui.js +1 -1
- package/src/shared/utils/diff-processor.js +21 -19
- package/src/shared/utils/error-classes.js +33 -0
- package/src/shared/utils/utils.js +65 -60
- package/src/domains/git/git-repository.analyzer.js +0 -678
|
@@ -31,22 +31,56 @@ export class GitService {
|
|
|
31
31
|
const [hash, subject, author, date] = lines[0].split('|')
|
|
32
32
|
const body = lines.slice(1).join('\n').trim()
|
|
33
33
|
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
34
|
+
// Detect merge commits and handle them differently
|
|
35
|
+
let isMergeCommit = subject.toLowerCase().includes('merge')
|
|
36
|
+
if (!isMergeCommit) {
|
|
37
|
+
// Check if commit has multiple parents (more reliable for merge detection)
|
|
38
|
+
try {
|
|
39
|
+
const parents = this.gitManager
|
|
40
|
+
.execGitSafe(`git show --format='%P' --no-patch ${commitHash}`)
|
|
41
|
+
.trim()
|
|
42
|
+
.split(' ')
|
|
43
|
+
isMergeCommit = parents.length > 1
|
|
44
|
+
} catch {
|
|
45
|
+
isMergeCommit = false
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let files = []
|
|
50
|
+
|
|
51
|
+
if (isMergeCommit) {
|
|
52
|
+
// For merge commits, get the stat summary and create a special analysis
|
|
53
|
+
const statOutput = this.gitManager.execGitSafe(
|
|
54
|
+
`git show --stat --pretty=format: ${commitHash}`
|
|
55
|
+
)
|
|
56
|
+
files = this.processMergeCommitStats(commitHash, statOutput)
|
|
57
|
+
|
|
58
|
+
// Generate comprehensive summary for large merge commits
|
|
59
|
+
if (files.length > 10) {
|
|
60
|
+
const enhancedSummary = this.generateMergeCommitSummary(files, commitHash, subject)
|
|
61
|
+
// Add the enhanced summary to the first file entry for the AI to use
|
|
62
|
+
if (files.length > 0) {
|
|
63
|
+
files[0].enhancedMergeSummary = enhancedSummary
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
// For regular commits, use the existing approach
|
|
68
|
+
const filesCommand = `git show --name-status --pretty=format: ${commitHash}`
|
|
69
|
+
const filesOutput = this.gitManager.execGitSafe(filesCommand)
|
|
70
|
+
files = await Promise.all(
|
|
71
|
+
filesOutput
|
|
72
|
+
.split('\n')
|
|
73
|
+
.filter(Boolean)
|
|
74
|
+
.map(async (line) => {
|
|
75
|
+
const parts = line.split('\t')
|
|
76
|
+
if (parts.length < 2) {
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
const [status, filePath] = parts
|
|
80
|
+
return await this.analyzeFileChange(commitHash, status, filePath)
|
|
81
|
+
})
|
|
82
|
+
)
|
|
83
|
+
}
|
|
50
84
|
|
|
51
85
|
// Filter out null entries
|
|
52
86
|
const validFiles = files.filter(Boolean)
|
|
@@ -319,4 +353,819 @@ export class GitService {
|
|
|
319
353
|
return []
|
|
320
354
|
}
|
|
321
355
|
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Process merge commit statistics to create meaningful file analysis
|
|
359
|
+
*/
|
|
360
|
+
processMergeCommitStats(commitHash, statOutput) {
|
|
361
|
+
if (!statOutput || statOutput.trim() === '') {
|
|
362
|
+
return []
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Parse the git stat output to extract file information
|
|
366
|
+
const lines = statOutput.split('\n').filter(Boolean)
|
|
367
|
+
const files = []
|
|
368
|
+
|
|
369
|
+
for (const line of lines) {
|
|
370
|
+
// Look for lines with file changes: "filename | additions +++--- deletions"
|
|
371
|
+
const match = line.match(/^\s*(.+?)\s*\|\s*(\d+)\s*([+\-\s]+)/)
|
|
372
|
+
if (match) {
|
|
373
|
+
const [, filePath, changes, diffSymbols] = match
|
|
374
|
+
const additions = (diffSymbols.match(/\+/g) || []).length
|
|
375
|
+
const deletions = (diffSymbols.match(/-/g) || []).length
|
|
376
|
+
|
|
377
|
+
// Determine file status based on changes
|
|
378
|
+
let status = 'M' // Default to modified
|
|
379
|
+
if (additions > 0 && deletions === 0) {
|
|
380
|
+
status = 'A' // Likely added
|
|
381
|
+
} else if (deletions > 0 && additions === 0) {
|
|
382
|
+
status = 'D' // Likely deleted
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Create a meaningful diff summary for the AI
|
|
386
|
+
const diff = this.createMergeCommitDiffSummary(
|
|
387
|
+
filePath,
|
|
388
|
+
parseInt(changes, 10),
|
|
389
|
+
additions,
|
|
390
|
+
deletions,
|
|
391
|
+
commitHash
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
files.push({
|
|
395
|
+
status,
|
|
396
|
+
filePath: filePath.trim(),
|
|
397
|
+
diff,
|
|
398
|
+
beforeContent: '',
|
|
399
|
+
afterContent: '',
|
|
400
|
+
category: categorizeFile(filePath),
|
|
401
|
+
language: detectLanguage(filePath),
|
|
402
|
+
importance: assessFileImportance(filePath, status),
|
|
403
|
+
complexity: { level: changes > 100 ? 'high' : changes > 20 ? 'medium' : 'low' },
|
|
404
|
+
semanticChanges: { patterns: ['merge-commit'] },
|
|
405
|
+
functionalImpact: { level: changes > 50 ? 'high' : 'medium' },
|
|
406
|
+
isMergeCommit: true,
|
|
407
|
+
changeCount: parseInt(changes, 10),
|
|
408
|
+
additions,
|
|
409
|
+
deletions,
|
|
410
|
+
})
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return files
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Create a meaningful diff summary for merge commits with enhanced categorization
|
|
419
|
+
*/
|
|
420
|
+
createMergeCommitDiffSummary(filePath, totalChanges, additions, deletions, commitHash) {
|
|
421
|
+
const changeType =
|
|
422
|
+
additions > deletions ? 'expanded' : deletions > additions ? 'reduced' : 'modified'
|
|
423
|
+
|
|
424
|
+
let summary = `Merge commit changes: ${totalChanges} lines ${changeType}`
|
|
425
|
+
|
|
426
|
+
if (additions > 0 && deletions > 0) {
|
|
427
|
+
summary += ` (${additions} added, ${deletions} removed)`
|
|
428
|
+
} else if (additions > 0) {
|
|
429
|
+
summary += ` (${additions} lines added)`
|
|
430
|
+
} else if (deletions > 0) {
|
|
431
|
+
summary += ` (${deletions} lines removed)`
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Enhanced context based on file type and size of changes
|
|
435
|
+
if (filePath.includes('test')) {
|
|
436
|
+
summary += ' - Test infrastructure changes from merge'
|
|
437
|
+
} else if (filePath.includes('config') || filePath.includes('.json')) {
|
|
438
|
+
summary += ' - Configuration and dependency changes from merge'
|
|
439
|
+
} else if (filePath.includes('README') || filePath.includes('.md')) {
|
|
440
|
+
summary += ' - Documentation updates from merge'
|
|
441
|
+
} else if (filePath.includes('src/domains/')) {
|
|
442
|
+
summary += ' - Core domain logic changes from merge'
|
|
443
|
+
} else if (filePath.includes('src/infrastructure/')) {
|
|
444
|
+
summary += ' - Infrastructure and provider changes from merge'
|
|
445
|
+
} else if (filePath.includes('src/application/')) {
|
|
446
|
+
summary += ' - Application service changes from merge'
|
|
447
|
+
} else if (filePath.includes('bin/') || filePath.includes('cli')) {
|
|
448
|
+
summary += ' - CLI interface changes from merge'
|
|
449
|
+
} else if (totalChanges > 100) {
|
|
450
|
+
summary += ' - Major code changes from merge'
|
|
451
|
+
} else if (totalChanges > 20) {
|
|
452
|
+
summary += ' - Moderate code changes from merge'
|
|
453
|
+
} else {
|
|
454
|
+
summary += ' - Minor code changes from merge'
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return summary
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Generate comprehensive merge commit summary with categorized changes and technical details
|
|
462
|
+
*/
|
|
463
|
+
generateMergeCommitSummary(files, commitHash, subject) {
|
|
464
|
+
const categories = {
|
|
465
|
+
tests: { count: 0, lines: 0, files: [] },
|
|
466
|
+
docs: { count: 0, lines: 0, files: [] },
|
|
467
|
+
config: { count: 0, lines: 0, files: [] },
|
|
468
|
+
core: { count: 0, lines: 0, files: [] },
|
|
469
|
+
infrastructure: { count: 0, lines: 0, files: [] },
|
|
470
|
+
cli: { count: 0, lines: 0, files: [] },
|
|
471
|
+
other: { count: 0, lines: 0, files: [] },
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
let totalLines = 0
|
|
475
|
+
let totalFiles = files.length
|
|
476
|
+
|
|
477
|
+
// Categorize files and accumulate statistics
|
|
478
|
+
for (const file of files) {
|
|
479
|
+
const changes = file.changeCount || 0
|
|
480
|
+
totalLines += changes
|
|
481
|
+
|
|
482
|
+
if (file.filePath.includes('test')) {
|
|
483
|
+
categories.tests.count++
|
|
484
|
+
categories.tests.lines += changes
|
|
485
|
+
categories.tests.files.push(file.filePath)
|
|
486
|
+
} else if (
|
|
487
|
+
file.filePath.includes('.md') ||
|
|
488
|
+
file.filePath.includes('README') ||
|
|
489
|
+
file.filePath.includes('docs/')
|
|
490
|
+
) {
|
|
491
|
+
categories.docs.count++
|
|
492
|
+
categories.docs.lines += changes
|
|
493
|
+
categories.docs.files.push(file.filePath)
|
|
494
|
+
} else if (
|
|
495
|
+
file.filePath.includes('config') ||
|
|
496
|
+
file.filePath.includes('.json') ||
|
|
497
|
+
file.filePath.includes('package.json')
|
|
498
|
+
) {
|
|
499
|
+
categories.config.count++
|
|
500
|
+
categories.config.lines += changes
|
|
501
|
+
categories.config.files.push(file.filePath)
|
|
502
|
+
} else if (file.filePath.includes('src/domains/')) {
|
|
503
|
+
categories.core.count++
|
|
504
|
+
categories.core.lines += changes
|
|
505
|
+
categories.core.files.push(file.filePath)
|
|
506
|
+
} else if (file.filePath.includes('src/infrastructure/')) {
|
|
507
|
+
categories.infrastructure.count++
|
|
508
|
+
categories.infrastructure.lines += changes
|
|
509
|
+
categories.infrastructure.files.push(file.filePath)
|
|
510
|
+
} else if (file.filePath.includes('bin/') || file.filePath.includes('cli')) {
|
|
511
|
+
categories.cli.count++
|
|
512
|
+
categories.cli.lines += changes
|
|
513
|
+
categories.cli.files.push(file.filePath)
|
|
514
|
+
} else {
|
|
515
|
+
categories.other.count++
|
|
516
|
+
categories.other.lines += changes
|
|
517
|
+
categories.other.files.push(file.filePath)
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Extract technical details from key files for more specific descriptions
|
|
522
|
+
const technicalDetails = this.extractTechnicalDetailsFromMerge(files, commitHash)
|
|
523
|
+
|
|
524
|
+
// Generate detailed summary bullets with specifics
|
|
525
|
+
const bullets = []
|
|
526
|
+
|
|
527
|
+
if (categories.tests.count > 0) {
|
|
528
|
+
const testSamples = categories.tests.files
|
|
529
|
+
.slice(0, 3)
|
|
530
|
+
.map((f) => f.split('/').pop())
|
|
531
|
+
.join(', ')
|
|
532
|
+
bullets.push(
|
|
533
|
+
`Added comprehensive test infrastructure with ${categories.tests.count} test files (${testSamples}${categories.tests.count > 3 ? ', ...' : ''}) totaling ${categories.tests.lines.toLocaleString()} lines of test code`
|
|
534
|
+
)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (categories.core.count > 0) {
|
|
538
|
+
const coreSamples = categories.core.files
|
|
539
|
+
.slice(0, 3)
|
|
540
|
+
.map((f) => f.split('/').pop())
|
|
541
|
+
.join(', ')
|
|
542
|
+
const coreDetails = technicalDetails.filter(
|
|
543
|
+
(detail) => detail.includes('Enhanced') && detail.includes('.js')
|
|
544
|
+
)
|
|
545
|
+
const detailSuffix =
|
|
546
|
+
coreDetails.length > 0 ? ` with ${coreDetails.slice(0, 2).join(' and ')}` : ''
|
|
547
|
+
bullets.push(
|
|
548
|
+
`Enhanced core domain services (${coreSamples}${categories.core.count > 3 ? ', ...' : ''}) for changelog generation, AI analysis, and Git operations with ${categories.core.lines.toLocaleString()} lines changed${detailSuffix}`
|
|
549
|
+
)
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (categories.infrastructure.count > 0) {
|
|
553
|
+
const infraSamples = categories.infrastructure.files
|
|
554
|
+
.slice(0, 3)
|
|
555
|
+
.map((f) => f.split('/').pop())
|
|
556
|
+
.join(', ')
|
|
557
|
+
bullets.push(
|
|
558
|
+
`Updated provider integrations and infrastructure services (${infraSamples}${categories.infrastructure.count > 3 ? ', ...' : ''}) across ${categories.infrastructure.count} files`
|
|
559
|
+
)
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (categories.cli.count > 0) {
|
|
563
|
+
const cliSamples = categories.cli.files
|
|
564
|
+
.slice(0, 3)
|
|
565
|
+
.map((f) => f.split('/').pop())
|
|
566
|
+
.join(', ')
|
|
567
|
+
bullets.push(
|
|
568
|
+
`Improved CLI interface and command handling (${cliSamples}${categories.cli.count > 3 ? ', ...' : ''}) across ${categories.cli.count} files`
|
|
569
|
+
)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (categories.docs.count > 0) {
|
|
573
|
+
const docSamples = categories.docs.files
|
|
574
|
+
.slice(0, 3)
|
|
575
|
+
.map((f) => f.split('/').pop())
|
|
576
|
+
.join(', ')
|
|
577
|
+
bullets.push(
|
|
578
|
+
`Updated documentation and guides (${docSamples}${categories.docs.count > 3 ? ', ...' : ''}) across ${categories.docs.count} files`
|
|
579
|
+
)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (categories.config.count > 0) {
|
|
583
|
+
const configSamples = categories.config.files
|
|
584
|
+
.slice(0, 3)
|
|
585
|
+
.map((f) => f.split('/').pop())
|
|
586
|
+
.join(', ')
|
|
587
|
+
const configDetails = technicalDetails.filter(
|
|
588
|
+
(detail) =>
|
|
589
|
+
detail.includes('dependencies') ||
|
|
590
|
+
detail.includes('configuration') ||
|
|
591
|
+
detail.includes('.gitignore')
|
|
592
|
+
)
|
|
593
|
+
const detailSuffix =
|
|
594
|
+
configDetails.length > 0 ? ` including ${configDetails.slice(0, 2).join(' and ')}` : ''
|
|
595
|
+
bullets.push(
|
|
596
|
+
`Modified configuration files and dependencies (${configSamples}${categories.config.count > 3 ? ', ...' : ''}) across ${categories.config.count} files${detailSuffix}`
|
|
597
|
+
)
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Create enhanced merge commit description
|
|
601
|
+
let description = `${subject} brought together major updates across ${totalFiles} files with ${totalLines.toLocaleString()} total line changes:\n\n`
|
|
602
|
+
|
|
603
|
+
if (bullets.length > 0) {
|
|
604
|
+
description += bullets.map((bullet) => ` • ${bullet}`).join('\n')
|
|
605
|
+
} else {
|
|
606
|
+
description += ` • Major codebase changes across multiple modules and services`
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return description
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Extract technical details from key merge commit files for specific descriptions
|
|
614
|
+
*/
|
|
615
|
+
extractTechnicalDetailsFromMerge(files, commitHash) {
|
|
616
|
+
const details = []
|
|
617
|
+
|
|
618
|
+
// Focus on key configuration and important files for technical details
|
|
619
|
+
const keyFiles = files
|
|
620
|
+
.filter((file) => {
|
|
621
|
+
const path = file.filePath.toLowerCase()
|
|
622
|
+
return (
|
|
623
|
+
path.includes('package.json') ||
|
|
624
|
+
path.includes('.config.') ||
|
|
625
|
+
path.includes('biome.json') ||
|
|
626
|
+
path.includes('.gitignore') ||
|
|
627
|
+
path.endsWith('.md') ||
|
|
628
|
+
(path.includes('src/') && file.changeCount > 100)
|
|
629
|
+
) // Major code changes
|
|
630
|
+
})
|
|
631
|
+
.slice(0, 5) // Limit to 5 key files to avoid overwhelming
|
|
632
|
+
|
|
633
|
+
for (const file of keyFiles) {
|
|
634
|
+
try {
|
|
635
|
+
// Get a sample of the actual diff for technical details
|
|
636
|
+
const diffCommand = `git show ${commitHash} --pretty=format: -U2 -- "${file.filePath}"`
|
|
637
|
+
const diff = this.gitManager.execGitSafe(diffCommand)
|
|
638
|
+
|
|
639
|
+
if (diff && diff.length > 0) {
|
|
640
|
+
const techDetail = this.extractSpecificChanges(file.filePath, diff)
|
|
641
|
+
if (techDetail) {
|
|
642
|
+
details.push(techDetail)
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
} catch (_error) {}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
return details
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Extract specific technical changes from a diff
|
|
653
|
+
*/
|
|
654
|
+
extractSpecificChanges(filePath, diff) {
|
|
655
|
+
const fileName = filePath.split('/').pop()
|
|
656
|
+
|
|
657
|
+
// Package.json changes
|
|
658
|
+
if (fileName === 'package.json') {
|
|
659
|
+
const versionChanges = diff.match(/[-+]\s*"([^"]+)":\s*"([^"]+)"/g)
|
|
660
|
+
if (versionChanges && versionChanges.length > 0) {
|
|
661
|
+
const changes = versionChanges
|
|
662
|
+
.slice(0, 3)
|
|
663
|
+
.map((change) => {
|
|
664
|
+
const match = change.match(/[-+]\s*"([^"]+)":\s*"([^"]+)"/)
|
|
665
|
+
if (match) {
|
|
666
|
+
const [, pkg, version] = match
|
|
667
|
+
return `${pkg} to ${version}`
|
|
668
|
+
}
|
|
669
|
+
return change
|
|
670
|
+
})
|
|
671
|
+
.join(', ')
|
|
672
|
+
return `Updated dependencies: ${changes}${versionChanges.length > 3 ? ', ...' : ''}`
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Configuration file changes
|
|
677
|
+
if (fileName.includes('.json') || fileName.includes('.config')) {
|
|
678
|
+
const addedLines = diff
|
|
679
|
+
.split('\n')
|
|
680
|
+
.filter((line) => line.startsWith('+'))
|
|
681
|
+
.slice(0, 3)
|
|
682
|
+
if (addedLines.length > 0) {
|
|
683
|
+
const configChanges = addedLines
|
|
684
|
+
.map((line) => line.replace(/^\+\s*/, '').trim())
|
|
685
|
+
.filter((line) => line.length > 0)
|
|
686
|
+
if (configChanges.length > 0) {
|
|
687
|
+
return `Modified ${fileName} configuration: ${configChanges.slice(0, 2).join(', ')}${configChanges.length > 2 ? ', ...' : ''}`
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// .gitignore changes
|
|
693
|
+
if (fileName === '.gitignore') {
|
|
694
|
+
const removedPatterns = diff
|
|
695
|
+
.split('\n')
|
|
696
|
+
.filter((line) => line.startsWith('-'))
|
|
697
|
+
.map((line) => line.replace(/^-\s*/, '').trim())
|
|
698
|
+
.filter((line) => line.length > 0)
|
|
699
|
+
const addedPatterns = diff
|
|
700
|
+
.split('\n')
|
|
701
|
+
.filter((line) => line.startsWith('+'))
|
|
702
|
+
.map((line) => line.replace(/^\+\s*/, '').trim())
|
|
703
|
+
.filter((line) => line.length > 0)
|
|
704
|
+
|
|
705
|
+
if (removedPatterns.length > 0 || addedPatterns.length > 0) {
|
|
706
|
+
let change = ''
|
|
707
|
+
if (removedPatterns.length > 0) {
|
|
708
|
+
change += `removed ${removedPatterns.slice(0, 3).join(', ')} patterns`
|
|
709
|
+
}
|
|
710
|
+
if (addedPatterns.length > 0) {
|
|
711
|
+
change += `${change ? ' and ' : ''}added ${addedPatterns.slice(0, 3).join(', ')} patterns`
|
|
712
|
+
}
|
|
713
|
+
return `Updated .gitignore: ${change}`
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Major code files - look for function/method additions
|
|
718
|
+
if (filePath.includes('src/') && fileName.endsWith('.js')) {
|
|
719
|
+
const functionMatches = diff.match(/\+.*(?:function|async|const|let|var)\s+(\w+)/g)
|
|
720
|
+
if (functionMatches && functionMatches.length > 0) {
|
|
721
|
+
const functions = functionMatches
|
|
722
|
+
.slice(0, 3)
|
|
723
|
+
.map((match) => {
|
|
724
|
+
const funcMatch = match.match(/\+.*(?:function|async|const|let|var)\s+(\w+)/)
|
|
725
|
+
return funcMatch ? funcMatch[1] : null
|
|
726
|
+
})
|
|
727
|
+
.filter(Boolean)
|
|
728
|
+
|
|
729
|
+
if (functions.length > 0) {
|
|
730
|
+
return `Enhanced ${fileName}: added ${functions.join(', ')}${functionMatches.length > 3 ? ', ...' : ''} functions`
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
return null
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Repository analysis methods (formerly in GitRepositoryAnalyzer)
|
|
739
|
+
async assessRepositoryHealth(config = {}) {
|
|
740
|
+
try {
|
|
741
|
+
const health = {
|
|
742
|
+
healthy: true,
|
|
743
|
+
score: 100,
|
|
744
|
+
issues: [],
|
|
745
|
+
recommendations: [],
|
|
746
|
+
metrics: {
|
|
747
|
+
branches: 0,
|
|
748
|
+
commits: 0,
|
|
749
|
+
untrackedFiles: 0,
|
|
750
|
+
staleBranches: 0,
|
|
751
|
+
largeBinaryFiles: 0,
|
|
752
|
+
commitFrequency: 0,
|
|
753
|
+
},
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Get basic repository statistics
|
|
757
|
+
try {
|
|
758
|
+
const branchesOutput = this.gitManager.execGitSafe('git branch -a')
|
|
759
|
+
health.metrics.branches = branchesOutput.split('\n').filter((line) => line.trim()).length
|
|
760
|
+
|
|
761
|
+
const commitsOutput = this.gitManager.execGitSafe('git rev-list --all --count')
|
|
762
|
+
health.metrics.commits = parseInt(commitsOutput.trim(), 10) || 0
|
|
763
|
+
|
|
764
|
+
const untrackedOutput = this.gitManager.execGitSafe(
|
|
765
|
+
'git ls-files --others --exclude-standard'
|
|
766
|
+
)
|
|
767
|
+
health.metrics.untrackedFiles = untrackedOutput
|
|
768
|
+
.split('\n')
|
|
769
|
+
.filter((line) => line.trim()).length
|
|
770
|
+
} catch (error) {
|
|
771
|
+
health.issues.push(`Failed to collect basic metrics: ${error.message}`)
|
|
772
|
+
health.score -= 10
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Check for stale branches (no commits in last 90 days)
|
|
776
|
+
try {
|
|
777
|
+
const staleBranchesOutput = this.gitManager.execGitSafe(
|
|
778
|
+
'git for-each-ref --format="%(refname:short) %(committerdate:iso)" refs/heads'
|
|
779
|
+
)
|
|
780
|
+
const staleThreshold = new Date()
|
|
781
|
+
staleThreshold.setDate(staleThreshold.getDate() - 90)
|
|
782
|
+
|
|
783
|
+
health.metrics.staleBranches = staleBranchesOutput.split('\n').filter((line) => {
|
|
784
|
+
const parts = line.trim().split(' ')
|
|
785
|
+
if (parts.length < 2) return false
|
|
786
|
+
const commitDate = new Date(parts[1])
|
|
787
|
+
return commitDate < staleThreshold
|
|
788
|
+
}).length
|
|
789
|
+
|
|
790
|
+
if (health.metrics.staleBranches > 5) {
|
|
791
|
+
health.issues.push(
|
|
792
|
+
`${health.metrics.staleBranches} stale branches found (no commits in 90+ days)`
|
|
793
|
+
)
|
|
794
|
+
health.recommendations.push(
|
|
795
|
+
'Consider cleaning up old branches with: git branch -d <branch-name>'
|
|
796
|
+
)
|
|
797
|
+
health.score -= Math.min(20, health.metrics.staleBranches * 2)
|
|
798
|
+
}
|
|
799
|
+
} catch (error) {
|
|
800
|
+
console.warn(`Warning: Could not check for stale branches: ${error.message}`)
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Check for large binary files in repository
|
|
804
|
+
try {
|
|
805
|
+
const largeFilesOutput = this.gitManager.execGitSafe(
|
|
806
|
+
'git rev-list --objects --all | git cat-file --batch-check="%(objecttype) %(objectsize) %(rest)" | grep "^blob" | sort -nr -k2 | head -10'
|
|
807
|
+
)
|
|
808
|
+
const largeFiles = largeFilesOutput
|
|
809
|
+
.split('\n')
|
|
810
|
+
.filter((line) => line.trim())
|
|
811
|
+
.map((line) => {
|
|
812
|
+
const parts = line.split(' ')
|
|
813
|
+
return { size: parseInt(parts[1], 10), path: parts.slice(2).join(' ') }
|
|
814
|
+
})
|
|
815
|
+
.filter((file) => file.size > 10 * 1024 * 1024) // 10MB threshold
|
|
816
|
+
|
|
817
|
+
health.metrics.largeBinaryFiles = largeFiles.length
|
|
818
|
+
if (largeFiles.length > 0) {
|
|
819
|
+
health.issues.push(`${largeFiles.length} large files found (>10MB)`)
|
|
820
|
+
health.recommendations.push('Consider using Git LFS for large binary files')
|
|
821
|
+
health.score -= Math.min(15, largeFiles.length * 5)
|
|
822
|
+
}
|
|
823
|
+
} catch (error) {
|
|
824
|
+
console.warn(`Warning: Could not check for large files: ${error.message}`)
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Calculate commit frequency (commits per week over last month)
|
|
828
|
+
try {
|
|
829
|
+
const oneMonthAgo = new Date()
|
|
830
|
+
oneMonthAgo.setDate(oneMonthAgo.getDate() - 30)
|
|
831
|
+
const recentCommitsOutput = this.gitManager.execGitSafe(
|
|
832
|
+
`git rev-list --count --since="${oneMonthAgo.toISOString()}" HEAD`
|
|
833
|
+
)
|
|
834
|
+
const recentCommits = parseInt(recentCommitsOutput.trim(), 10) || 0
|
|
835
|
+
health.metrics.commitFrequency = Math.round((recentCommits / 4) * 10) / 10 // commits per week
|
|
836
|
+
|
|
837
|
+
if (health.metrics.commitFrequency < 1) {
|
|
838
|
+
health.issues.push('Low commit frequency (less than 1 commit per week)')
|
|
839
|
+
health.recommendations.push('Consider more frequent commits for better project tracking')
|
|
840
|
+
health.score -= 5
|
|
841
|
+
}
|
|
842
|
+
} catch (error) {
|
|
843
|
+
console.warn(`Warning: Could not calculate commit frequency: ${error.message}`)
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// Check if working directory is clean
|
|
847
|
+
try {
|
|
848
|
+
const statusOutput = this.gitManager.execGitSafe('git status --porcelain')
|
|
849
|
+
if (statusOutput.trim()) {
|
|
850
|
+
health.issues.push('Working directory has uncommitted changes')
|
|
851
|
+
health.recommendations.push('Commit or stash working directory changes')
|
|
852
|
+
health.score -= 5
|
|
853
|
+
}
|
|
854
|
+
} catch (error) {
|
|
855
|
+
health.issues.push(`Could not check working directory status: ${error.message}`)
|
|
856
|
+
health.score -= 5
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Check for .gitignore file
|
|
860
|
+
try {
|
|
861
|
+
const gitignoreExists = this.gitManager.execGitSafe('test -f .gitignore && echo "exists"')
|
|
862
|
+
if (!gitignoreExists.includes('exists')) {
|
|
863
|
+
health.issues.push('No .gitignore file found')
|
|
864
|
+
health.recommendations.push('Add a .gitignore file to exclude unwanted files')
|
|
865
|
+
health.score -= 10
|
|
866
|
+
}
|
|
867
|
+
} catch (error) {
|
|
868
|
+
console.warn(`Warning: Could not check .gitignore: ${error.message}`)
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Determine overall health
|
|
872
|
+
health.healthy = health.score >= 70
|
|
873
|
+
health.score = Math.max(0, health.score)
|
|
874
|
+
|
|
875
|
+
return health
|
|
876
|
+
} catch (error) {
|
|
877
|
+
console.error(`Repository health assessment failed: ${error.message}`)
|
|
878
|
+
return {
|
|
879
|
+
healthy: false,
|
|
880
|
+
score: 0,
|
|
881
|
+
issues: [`Health assessment failed: ${error.message}`],
|
|
882
|
+
recommendations: ['Ensure you are in a valid Git repository'],
|
|
883
|
+
metrics: { branches: 0, commits: 0, untrackedFiles: 0 },
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
async analyzeBranches(format = 'markdown') {
|
|
889
|
+
try {
|
|
890
|
+
const analysis = {
|
|
891
|
+
branches: [],
|
|
892
|
+
unmergedCommits: [],
|
|
893
|
+
danglingCommits: [],
|
|
894
|
+
analysis: '',
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// Get all branches with their last commit info
|
|
898
|
+
const branchesOutput = this.gitManager.execGitSafe(
|
|
899
|
+
'git for-each-ref --format="%(refname:short)|%(committerdate:iso)|%(authorname)|%(subject)" refs/heads refs/remotes'
|
|
900
|
+
)
|
|
901
|
+
|
|
902
|
+
analysis.branches = branchesOutput
|
|
903
|
+
.split('\n')
|
|
904
|
+
.filter((line) => line.trim())
|
|
905
|
+
.map((line) => {
|
|
906
|
+
const [name, date, author, subject] = line.split('|')
|
|
907
|
+
const isRemote = name.startsWith('origin/')
|
|
908
|
+
const isStale = new Date() - new Date(date) > 90 * 24 * 60 * 60 * 1000 // 90 days
|
|
909
|
+
|
|
910
|
+
return {
|
|
911
|
+
name: name.trim(),
|
|
912
|
+
lastCommitDate: date,
|
|
913
|
+
lastCommitAuthor: author,
|
|
914
|
+
lastCommitSubject: subject,
|
|
915
|
+
isRemote,
|
|
916
|
+
isStale,
|
|
917
|
+
type: isRemote ? 'remote' : 'local',
|
|
918
|
+
}
|
|
919
|
+
})
|
|
920
|
+
|
|
921
|
+
// Find unmerged commits (commits in feature branches not in main/master)
|
|
922
|
+
try {
|
|
923
|
+
const mainBranch = this.findMainBranch()
|
|
924
|
+
if (mainBranch) {
|
|
925
|
+
const localBranches = analysis.branches.filter(
|
|
926
|
+
(b) => !b.isRemote && b.name !== mainBranch
|
|
927
|
+
)
|
|
928
|
+
|
|
929
|
+
for (const branch of localBranches) {
|
|
930
|
+
try {
|
|
931
|
+
const unmergedOutput = this.gitManager.execGitSafe(
|
|
932
|
+
`git log ${mainBranch}..${branch.name} --oneline`
|
|
933
|
+
)
|
|
934
|
+
const unmergedCommits = unmergedOutput
|
|
935
|
+
.split('\n')
|
|
936
|
+
.filter((line) => line.trim())
|
|
937
|
+
.map((line) => {
|
|
938
|
+
const [hash, ...messageParts] = line.split(' ')
|
|
939
|
+
return {
|
|
940
|
+
hash: hash.trim(),
|
|
941
|
+
message: messageParts.join(' '),
|
|
942
|
+
branch: branch.name,
|
|
943
|
+
}
|
|
944
|
+
})
|
|
945
|
+
|
|
946
|
+
analysis.unmergedCommits.push(...unmergedCommits)
|
|
947
|
+
} catch (error) {
|
|
948
|
+
console.warn(`Could not check unmerged commits for ${branch.name}: ${error.message}`)
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
} catch (error) {
|
|
953
|
+
console.warn(`Could not analyze unmerged commits: ${error.message}`)
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// Find dangling commits (unreachable commits)
|
|
957
|
+
try {
|
|
958
|
+
const danglingOutput = this.gitManager.execGitSafe('git fsck --unreachable --no-reflogs')
|
|
959
|
+
analysis.danglingCommits = danglingOutput
|
|
960
|
+
.split('\n')
|
|
961
|
+
.filter((line) => line.includes('unreachable commit'))
|
|
962
|
+
.map((line) => {
|
|
963
|
+
const hash = line.split(' ').pop()
|
|
964
|
+
return { hash, type: 'dangling' }
|
|
965
|
+
})
|
|
966
|
+
} catch (error) {
|
|
967
|
+
console.warn(`Could not check for dangling commits: ${error.message}`)
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// Generate analysis summary
|
|
971
|
+
const totalBranches = analysis.branches.length
|
|
972
|
+
const localBranches = analysis.branches.filter((b) => !b.isRemote).length
|
|
973
|
+
const staleBranches = analysis.branches.filter((b) => b.isStale).length
|
|
974
|
+
const unmergedCount = analysis.unmergedCommits.length
|
|
975
|
+
const danglingCount = analysis.danglingCommits.length
|
|
976
|
+
|
|
977
|
+
if (format === 'markdown') {
|
|
978
|
+
analysis.analysis = `# Branch Analysis
|
|
979
|
+
|
|
980
|
+
## Summary
|
|
981
|
+
- **Total branches**: ${totalBranches} (${localBranches} local, ${totalBranches - localBranches} remote)
|
|
982
|
+
- **Stale branches**: ${staleBranches} (no commits in 90+ days)
|
|
983
|
+
- **Unmerged commits**: ${unmergedCount}
|
|
984
|
+
- **Dangling commits**: ${danglingCount}
|
|
985
|
+
|
|
986
|
+
## Branch Details
|
|
987
|
+
${analysis.branches
|
|
988
|
+
.map(
|
|
989
|
+
(b) =>
|
|
990
|
+
`- **${b.name}** ${b.isStale ? '(stale)' : ''}\n - Last commit: ${b.lastCommitDate} by ${b.lastCommitAuthor}\n - Subject: ${b.lastCommitSubject}`
|
|
991
|
+
)
|
|
992
|
+
.join('\n')}
|
|
993
|
+
|
|
994
|
+
${
|
|
995
|
+
unmergedCount > 0
|
|
996
|
+
? `\n## Unmerged Commits\n${analysis.unmergedCommits
|
|
997
|
+
.slice(0, 10)
|
|
998
|
+
.map((c) => `- ${c.hash}: ${c.message} (${c.branch})`)
|
|
999
|
+
.join('\n')}${unmergedCount > 10 ? `\n... and ${unmergedCount - 10} more` : ''}`
|
|
1000
|
+
: ''
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
${
|
|
1004
|
+
danglingCount > 0
|
|
1005
|
+
? `\n## Dangling Commits\n${analysis.danglingCommits
|
|
1006
|
+
.slice(0, 5)
|
|
1007
|
+
.map((c) => `- ${c.hash}`)
|
|
1008
|
+
.join('\n')}${danglingCount > 5 ? `\n... and ${danglingCount - 5} more` : ''}`
|
|
1009
|
+
: ''
|
|
1010
|
+
}
|
|
1011
|
+
`
|
|
1012
|
+
} else {
|
|
1013
|
+
analysis.analysis = `Found ${totalBranches} branches (${staleBranches} stale), ${unmergedCount} unmerged commits, ${danglingCount} dangling commits`
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
return analysis
|
|
1017
|
+
} catch (error) {
|
|
1018
|
+
console.error(`Branch analysis failed: ${error.message}`)
|
|
1019
|
+
return {
|
|
1020
|
+
branches: [],
|
|
1021
|
+
unmergedCommits: [],
|
|
1022
|
+
danglingCommits: [],
|
|
1023
|
+
analysis: `Branch analysis failed: ${error.message}`,
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
async analyzeComprehensive(includeRecommendations = true) {
|
|
1029
|
+
try {
|
|
1030
|
+
console.log('🔍 Performing comprehensive repository analysis...')
|
|
1031
|
+
|
|
1032
|
+
const analysis = {
|
|
1033
|
+
analysis: '',
|
|
1034
|
+
recommendations: includeRecommendations ? [] : undefined,
|
|
1035
|
+
metrics: {},
|
|
1036
|
+
health: {},
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// Get repository health
|
|
1040
|
+
analysis.health = await this.assessRepositoryHealth()
|
|
1041
|
+
|
|
1042
|
+
// Get branch analysis
|
|
1043
|
+
const branchAnalysis = await this.analyzeBranches('object')
|
|
1044
|
+
|
|
1045
|
+
// Get repository statistics
|
|
1046
|
+
analysis.metrics = {
|
|
1047
|
+
...analysis.health.metrics,
|
|
1048
|
+
totalCommits: analysis.health.metrics.commits,
|
|
1049
|
+
totalBranches: branchAnalysis.branches.length,
|
|
1050
|
+
unmergedCommits: branchAnalysis.unmergedCommits.length,
|
|
1051
|
+
danglingCommits: branchAnalysis.danglingCommits.length,
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// Analyze commit patterns
|
|
1055
|
+
try {
|
|
1056
|
+
const commitHistory = this.gitManager.execGitSafe('git log --oneline --since="30 days ago"')
|
|
1057
|
+
const recentCommits = commitHistory.split('\n').filter((line) => line.trim())
|
|
1058
|
+
|
|
1059
|
+
const conventionalCommits = recentCommits.filter((commit) =>
|
|
1060
|
+
/^[a-f0-9]+\s+(feat|fix|docs|style|refactor|test|chore|perf|build|ci)(\(.+\))?:/.test(
|
|
1061
|
+
commit
|
|
1062
|
+
)
|
|
1063
|
+
)
|
|
1064
|
+
|
|
1065
|
+
analysis.metrics.conventionalCommitRatio =
|
|
1066
|
+
recentCommits.length > 0
|
|
1067
|
+
? Math.round((conventionalCommits.length / recentCommits.length) * 100)
|
|
1068
|
+
: 0
|
|
1069
|
+
|
|
1070
|
+
analysis.metrics.recentCommitsCount = recentCommits.length
|
|
1071
|
+
} catch (error) {
|
|
1072
|
+
console.warn(`Could not analyze commit patterns: ${error.message}`)
|
|
1073
|
+
analysis.metrics.conventionalCommitRatio = 0
|
|
1074
|
+
analysis.metrics.recentCommitsCount = 0
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// Calculate repository age
|
|
1078
|
+
try {
|
|
1079
|
+
const firstCommitOutput = this.gitManager.execGitSafe(
|
|
1080
|
+
'git log --reverse --format="%ci" | head -1'
|
|
1081
|
+
)
|
|
1082
|
+
if (firstCommitOutput.trim()) {
|
|
1083
|
+
const firstCommitDate = new Date(firstCommitOutput.trim())
|
|
1084
|
+
const ageInDays = Math.floor((new Date() - firstCommitDate) / (1000 * 60 * 60 * 24))
|
|
1085
|
+
analysis.metrics.repositoryAgeInDays = ageInDays
|
|
1086
|
+
}
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
console.warn(`Could not determine repository age: ${error.message}`)
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// Generate comprehensive analysis
|
|
1092
|
+
const healthScore = analysis.health.score
|
|
1093
|
+
const isHealthy = analysis.health.healthy
|
|
1094
|
+
const staleBranches = branchAnalysis.branches.filter((b) => b.isStale).length
|
|
1095
|
+
const conventionalRatio = analysis.metrics.conventionalCommitRatio
|
|
1096
|
+
|
|
1097
|
+
analysis.analysis = `# Comprehensive Repository Analysis
|
|
1098
|
+
|
|
1099
|
+
## Overall Health: ${healthScore}/100 ${isHealthy ? '✅' : '⚠️'}
|
|
1100
|
+
|
|
1101
|
+
### Repository Metrics
|
|
1102
|
+
- **Age**: ${analysis.metrics.repositoryAgeInDays || 'Unknown'} days
|
|
1103
|
+
- **Total Commits**: ${analysis.metrics.totalCommits}
|
|
1104
|
+
- **Branches**: ${analysis.metrics.totalBranches} (${staleBranches} stale)
|
|
1105
|
+
- **Recent Activity**: ${analysis.metrics.recentCommitsCount} commits in last 30 days
|
|
1106
|
+
- **Commit Convention**: ${conventionalRatio}% following conventional commits
|
|
1107
|
+
|
|
1108
|
+
### Issues Found
|
|
1109
|
+
${analysis.health.issues.length > 0 ? analysis.health.issues.map((issue) => `- ${issue}`).join('\n') : '- No major issues detected'}
|
|
1110
|
+
|
|
1111
|
+
### Branch Health
|
|
1112
|
+
- **Unmerged commits**: ${analysis.metrics.unmergedCommits}
|
|
1113
|
+
- **Dangling commits**: ${analysis.metrics.danglingCommits}
|
|
1114
|
+
- **Stale branches**: ${staleBranches}
|
|
1115
|
+
`
|
|
1116
|
+
|
|
1117
|
+
// Add recommendations if requested
|
|
1118
|
+
if (includeRecommendations && analysis.health.recommendations.length > 0) {
|
|
1119
|
+
analysis.recommendations = [
|
|
1120
|
+
...analysis.health.recommendations,
|
|
1121
|
+
...(staleBranches > 3
|
|
1122
|
+
? ['Clean up stale branches to improve repository organization']
|
|
1123
|
+
: []),
|
|
1124
|
+
...(conventionalRatio < 50
|
|
1125
|
+
? ['Consider adopting conventional commit format for better changelog generation']
|
|
1126
|
+
: []),
|
|
1127
|
+
...(analysis.metrics.unmergedCommits > 10
|
|
1128
|
+
? ['Review and merge or clean up unmerged commits']
|
|
1129
|
+
: []),
|
|
1130
|
+
]
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
return analysis
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
console.error(`Comprehensive analysis failed: ${error.message}`)
|
|
1136
|
+
return {
|
|
1137
|
+
analysis: `Comprehensive analysis failed: ${error.message}`,
|
|
1138
|
+
recommendations: includeRecommendations
|
|
1139
|
+
? ['Ensure you are in a valid Git repository']
|
|
1140
|
+
: undefined,
|
|
1141
|
+
metrics: {},
|
|
1142
|
+
health: { healthy: false, score: 0 },
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// Helper method to find the main branch (master/main)
|
|
1148
|
+
findMainBranch() {
|
|
1149
|
+
try {
|
|
1150
|
+
const branches = this.gitManager.execGitSafe('git branch -l')
|
|
1151
|
+
const branchNames = branches
|
|
1152
|
+
.split('\n')
|
|
1153
|
+
.map((b) => b.replace('*', '').trim())
|
|
1154
|
+
.filter(Boolean)
|
|
1155
|
+
|
|
1156
|
+
// Check for common main branch names in order of preference
|
|
1157
|
+
const mainBranchCandidates = ['main', 'master', 'develop', 'dev']
|
|
1158
|
+
for (const candidate of mainBranchCandidates) {
|
|
1159
|
+
if (branchNames.includes(candidate)) {
|
|
1160
|
+
return candidate
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// Fallback to first branch if no standard main branch found
|
|
1165
|
+
return branchNames[0] || 'main'
|
|
1166
|
+
} catch (error) {
|
|
1167
|
+
console.warn(`Could not determine main branch: ${error.message}`)
|
|
1168
|
+
return 'main'
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
322
1171
|
}
|