korekt-cli 0.5.0 ā 0.6.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/package.json +1 -1
- package/src/git-logic.js +59 -1
- package/src/git-logic.test.js +136 -0
- package/src/index.js +2 -2
package/package.json
CHANGED
package/src/git-logic.js
CHANGED
|
@@ -280,6 +280,55 @@ export async function runUncommittedReview(
|
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Extract contributors from git commits in a range
|
|
285
|
+
* Returns the author (most commits) and full list of contributors
|
|
286
|
+
* @param {string} diffRange - The git range to analyze (e.g., "abc123..HEAD")
|
|
287
|
+
* @param {string} repoRootPath - The repository root directory
|
|
288
|
+
* @returns {Object} - { author_email, author_name, contributors[] }
|
|
289
|
+
*/
|
|
290
|
+
export async function getContributors(diffRange, repoRootPath) {
|
|
291
|
+
try {
|
|
292
|
+
// Get all commit authors with email and name
|
|
293
|
+
const { stdout: authorOutput } = await execa('git', ['log', '--format=%ae|%an', diffRange], {
|
|
294
|
+
cwd: repoRootPath,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
if (!authorOutput.trim()) {
|
|
298
|
+
return { author_email: null, author_name: null, contributors: [] };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const lines = authorOutput.trim().split('\n').filter(Boolean);
|
|
302
|
+
|
|
303
|
+
// Count commits per email and track name
|
|
304
|
+
const contributorMap = new Map();
|
|
305
|
+
for (const line of lines) {
|
|
306
|
+
const [email, name] = line.split('|');
|
|
307
|
+
if (!email) continue;
|
|
308
|
+
|
|
309
|
+
if (!contributorMap.has(email)) {
|
|
310
|
+
contributorMap.set(email, { email, name: name || email, commits: 0 });
|
|
311
|
+
}
|
|
312
|
+
contributorMap.get(email).commits++;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Convert to array and sort by commits (descending)
|
|
316
|
+
const contributors = Array.from(contributorMap.values()).sort((a, b) => b.commits - a.commits);
|
|
317
|
+
|
|
318
|
+
// Author = most commits
|
|
319
|
+
const author = contributors[0] || null;
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
author_email: author?.email || null,
|
|
323
|
+
author_name: author?.name || null,
|
|
324
|
+
contributors,
|
|
325
|
+
};
|
|
326
|
+
} catch (error) {
|
|
327
|
+
console.warn(chalk.yellow('Could not extract contributors:'), error.message);
|
|
328
|
+
return { author_email: null, author_name: null, contributors: [] };
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
283
332
|
/**
|
|
284
333
|
* Main function to analyze local git changes and prepare review payload
|
|
285
334
|
* @param {string|null} targetBranch - The branch to compare against. If null, uses git reflog to find fork point.
|
|
@@ -486,12 +535,21 @@ export async function runLocalReview(
|
|
|
486
535
|
});
|
|
487
536
|
}
|
|
488
537
|
|
|
489
|
-
// 5.
|
|
538
|
+
// 5. Get contributors from commits
|
|
539
|
+
const { author_email, author_name, contributors } = await getContributors(
|
|
540
|
+
diffRange,
|
|
541
|
+
repoRootPath
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
// 6. Assemble the final payload
|
|
490
545
|
return {
|
|
491
546
|
repo_url: normalizeRepoUrl(repoUrl.trim()),
|
|
492
547
|
commit_messages: commitMessages,
|
|
493
548
|
changed_files: changedFiles,
|
|
494
549
|
source_branch: branchName,
|
|
550
|
+
author_email,
|
|
551
|
+
author_name,
|
|
552
|
+
contributors,
|
|
495
553
|
};
|
|
496
554
|
} catch (error) {
|
|
497
555
|
console.error(chalk.red('Failed to run local review analysis:'), error.message);
|
package/src/git-logic.test.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
truncateContent,
|
|
7
7
|
normalizeRepoUrl,
|
|
8
8
|
shouldIgnoreFile,
|
|
9
|
+
getContributors,
|
|
9
10
|
} from './git-logic.js';
|
|
10
11
|
import { execa } from 'execa';
|
|
11
12
|
|
|
@@ -571,3 +572,138 @@ describe('shouldIgnoreFile', () => {
|
|
|
571
572
|
expect(shouldIgnoreFile('file.js', pattern)).toBe(false);
|
|
572
573
|
});
|
|
573
574
|
});
|
|
575
|
+
|
|
576
|
+
describe('getContributors', () => {
|
|
577
|
+
beforeEach(() => {
|
|
578
|
+
vi.mock('execa');
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
afterEach(() => {
|
|
582
|
+
vi.restoreAllMocks();
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it('should extract single contributor with commit count', async () => {
|
|
586
|
+
vi.mocked(execa).mockImplementation(async (cmd, args) => {
|
|
587
|
+
const command = [cmd, ...args].join(' ');
|
|
588
|
+
if (command.includes('log --format=%ae|%an')) {
|
|
589
|
+
return {
|
|
590
|
+
stdout: 'john@example.com|John Doe\njohn@example.com|John Doe\njohn@example.com|John Doe',
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
throw new Error(`Unmocked command: ${command}`);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
const result = await getContributors('abc123..HEAD', '/path/to/repo');
|
|
597
|
+
|
|
598
|
+
expect(result.author_email).toBe('john@example.com');
|
|
599
|
+
expect(result.author_name).toBe('John Doe');
|
|
600
|
+
expect(result.contributors).toHaveLength(1);
|
|
601
|
+
expect(result.contributors[0]).toEqual({
|
|
602
|
+
email: 'john@example.com',
|
|
603
|
+
name: 'John Doe',
|
|
604
|
+
commits: 3,
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it('should identify author as contributor with most commits', async () => {
|
|
609
|
+
vi.mocked(execa).mockImplementation(async (cmd, args) => {
|
|
610
|
+
const command = [cmd, ...args].join(' ');
|
|
611
|
+
if (command.includes('log --format=%ae|%an')) {
|
|
612
|
+
return {
|
|
613
|
+
stdout: [
|
|
614
|
+
'alice@example.com|Alice Smith',
|
|
615
|
+
'alice@example.com|Alice Smith',
|
|
616
|
+
'alice@example.com|Alice Smith',
|
|
617
|
+
'alice@example.com|Alice Smith',
|
|
618
|
+
'alice@example.com|Alice Smith',
|
|
619
|
+
'bob@example.com|Bob Jones',
|
|
620
|
+
'bob@example.com|Bob Jones',
|
|
621
|
+
'charlie@example.com|Charlie Brown',
|
|
622
|
+
].join('\n'),
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
throw new Error(`Unmocked command: ${command}`);
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
const result = await getContributors('abc123..HEAD', '/path/to/repo');
|
|
629
|
+
|
|
630
|
+
expect(result.author_email).toBe('alice@example.com');
|
|
631
|
+
expect(result.author_name).toBe('Alice Smith');
|
|
632
|
+
expect(result.contributors).toHaveLength(3);
|
|
633
|
+
expect(result.contributors[0].commits).toBe(5); // Alice - most commits
|
|
634
|
+
expect(result.contributors[1].commits).toBe(2); // Bob
|
|
635
|
+
expect(result.contributors[2].commits).toBe(1); // Charlie
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it('should handle empty commit range', async () => {
|
|
639
|
+
vi.mocked(execa).mockImplementation(async (cmd, args) => {
|
|
640
|
+
const command = [cmd, ...args].join(' ');
|
|
641
|
+
if (command.includes('log --format=%ae|%an')) {
|
|
642
|
+
return { stdout: '' };
|
|
643
|
+
}
|
|
644
|
+
throw new Error(`Unmocked command: ${command}`);
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
const result = await getContributors('abc123..HEAD', '/path/to/repo');
|
|
648
|
+
|
|
649
|
+
expect(result.author_email).toBeNull();
|
|
650
|
+
expect(result.author_name).toBeNull();
|
|
651
|
+
expect(result.contributors).toEqual([]);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
it('should handle git command errors gracefully', async () => {
|
|
655
|
+
vi.mocked(execa).mockImplementation(async () => {
|
|
656
|
+
throw new Error('Git error');
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
const result = await getContributors('abc123..HEAD', '/path/to/repo');
|
|
660
|
+
|
|
661
|
+
expect(result.author_email).toBeNull();
|
|
662
|
+
expect(result.author_name).toBeNull();
|
|
663
|
+
expect(result.contributors).toEqual([]);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it('should handle missing name in git log', async () => {
|
|
667
|
+
vi.mocked(execa).mockImplementation(async (cmd, args) => {
|
|
668
|
+
const command = [cmd, ...args].join(' ');
|
|
669
|
+
if (command.includes('log --format=%ae|%an')) {
|
|
670
|
+
return { stdout: 'user@example.com|' };
|
|
671
|
+
}
|
|
672
|
+
throw new Error(`Unmocked command: ${command}`);
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
const result = await getContributors('abc123..HEAD', '/path/to/repo');
|
|
676
|
+
|
|
677
|
+
expect(result.author_email).toBe('user@example.com');
|
|
678
|
+
expect(result.author_name).toBe('user@example.com'); // Falls back to email
|
|
679
|
+
expect(result.contributors[0].name).toBe('user@example.com');
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
it('should sort contributors by commit count descending', async () => {
|
|
683
|
+
vi.mocked(execa).mockImplementation(async (cmd, args) => {
|
|
684
|
+
const command = [cmd, ...args].join(' ');
|
|
685
|
+
if (command.includes('log --format=%ae|%an')) {
|
|
686
|
+
return {
|
|
687
|
+
stdout: [
|
|
688
|
+
'c@example.com|C User',
|
|
689
|
+
'a@example.com|A User',
|
|
690
|
+
'a@example.com|A User',
|
|
691
|
+
'a@example.com|A User',
|
|
692
|
+
'b@example.com|B User',
|
|
693
|
+
'b@example.com|B User',
|
|
694
|
+
].join('\n'),
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
throw new Error(`Unmocked command: ${command}`);
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
const result = await getContributors('abc123..HEAD', '/path/to/repo');
|
|
701
|
+
|
|
702
|
+
expect(result.contributors[0].email).toBe('a@example.com');
|
|
703
|
+
expect(result.contributors[0].commits).toBe(3);
|
|
704
|
+
expect(result.contributors[1].email).toBe('b@example.com');
|
|
705
|
+
expect(result.contributors[1].commits).toBe(2);
|
|
706
|
+
expect(result.contributors[2].email).toBe('c@example.com');
|
|
707
|
+
expect(result.contributors[2].commits).toBe(1);
|
|
708
|
+
});
|
|
709
|
+
});
|
package/src/index.js
CHANGED
|
@@ -200,7 +200,7 @@ program
|
|
|
200
200
|
log(` Commits: ${chalk.cyan(payload.commit_messages.length)}`);
|
|
201
201
|
log(` Files: ${chalk.cyan(payload.changed_files.length)}\n`);
|
|
202
202
|
|
|
203
|
-
log(chalk.bold(
|
|
203
|
+
log(chalk.bold(` ${payload.changed_files.length} files to review:`));
|
|
204
204
|
payload.changed_files.forEach((file) => {
|
|
205
205
|
const statusColor =
|
|
206
206
|
{
|
|
@@ -364,7 +364,7 @@ async function reviewUncommitted(mode, options) {
|
|
|
364
364
|
if (!options.json) {
|
|
365
365
|
log(chalk.yellow('\nš Ready to submit uncommitted changes for review:\n'));
|
|
366
366
|
log(chalk.gray(' Comparing against HEAD (last commit)\n'));
|
|
367
|
-
log(chalk.bold(
|
|
367
|
+
log(chalk.bold(` ${payload.changed_files.length} files to review:`));
|
|
368
368
|
payload.changed_files.forEach((file) => {
|
|
369
369
|
const statusColor =
|
|
370
370
|
{
|