difit 3.1.8 → 3.1.10
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/README.ja.md +31 -14
- package/README.ko.md +31 -14
- package/README.md +31 -14
- package/README.zh.md +31 -14
- package/dist/cli/index.js +55 -40
- package/dist/cli/index.test.js +114 -56
- package/dist/cli/utils.d.ts +14 -4
- package/dist/cli/utils.js +40 -98
- package/dist/cli/utils.test.js +99 -1
- package/dist/client/assets/{index-C3dUtNTw.js → index-CH5h4Jfu.js} +40 -40
- package/dist/client/assets/{prism-csharp-CBHytnk2.js → prism-csharp-omuTcU4j.js} +1 -1
- package/dist/client/assets/{prism-hcl-Dg0qCBz9.js → prism-hcl-BVGrHlPv.js} +1 -1
- package/dist/client/assets/{prism-java-qbAyqoLL.js → prism-java-BjHLe7qM.js} +1 -1
- package/dist/client/assets/{prism-perl-C5wGhuJd.js → prism-perl-Cl4F-f2r.js} +1 -1
- package/dist/client/assets/{prism-php-BAMztFi0.js → prism-php-BbsdsICU.js} +1 -1
- package/dist/client/assets/{prism-ruby-BVXH3cUk.js → prism-ruby-CSLiFfIy.js} +1 -1
- package/dist/client/assets/{prism-solidity-DLMGfXyj.js → prism-solidity-C3yStBME.js} +1 -1
- package/dist/client/index.html +1 -1
- package/dist/server/git-diff.js +38 -22
- package/dist/server/server.d.ts +1 -0
- package/dist/server/server.js +16 -10
- package/dist/server/server.test.js +76 -0
- package/dist/tui/App.js +7 -10
- package/dist/tui/components/SideBySideDiffViewer.js +8 -8
- package/dist/tui/components/StatusBar.js +1 -5
- package/dist/utils/commentFormatting.js +1 -3
- package/package.json +9 -19
package/dist/cli/index.test.js
CHANGED
|
@@ -13,19 +13,19 @@ vi.mock('./utils.js', async () => {
|
|
|
13
13
|
promptUser: vi.fn(),
|
|
14
14
|
findUntrackedFiles: vi.fn(),
|
|
15
15
|
markFilesIntentToAdd: vi.fn(),
|
|
16
|
-
|
|
16
|
+
getPrPatch: vi.fn(),
|
|
17
17
|
};
|
|
18
18
|
});
|
|
19
19
|
const { simpleGit } = await import('simple-git');
|
|
20
20
|
const { startServer } = await import('../server/server.js');
|
|
21
|
-
const { promptUser, findUntrackedFiles, markFilesIntentToAdd,
|
|
21
|
+
const { promptUser, findUntrackedFiles, markFilesIntentToAdd, getPrPatch } = await import('./utils.js');
|
|
22
22
|
describe('CLI index.ts', () => {
|
|
23
23
|
let mockGit;
|
|
24
24
|
let mockStartServer;
|
|
25
25
|
let mockPromptUser;
|
|
26
26
|
let mockFindUntrackedFiles;
|
|
27
27
|
let mockMarkFilesIntentToAdd;
|
|
28
|
-
let
|
|
28
|
+
let mockGetPrPatch;
|
|
29
29
|
// Store original console methods
|
|
30
30
|
let originalConsoleLog;
|
|
31
31
|
let originalConsoleError;
|
|
@@ -46,7 +46,7 @@ describe('CLI index.ts', () => {
|
|
|
46
46
|
mockPromptUser = vi.mocked(promptUser);
|
|
47
47
|
mockFindUntrackedFiles = vi.mocked(findUntrackedFiles);
|
|
48
48
|
mockMarkFilesIntentToAdd = vi.mocked(markFilesIntentToAdd);
|
|
49
|
-
|
|
49
|
+
mockGetPrPatch = vi.mocked(getPrPatch);
|
|
50
50
|
// Mock console and process.exit
|
|
51
51
|
originalConsoleLog = console.log;
|
|
52
52
|
originalConsoleError = console.error;
|
|
@@ -194,6 +194,11 @@ describe('CLI index.ts', () => {
|
|
|
194
194
|
args: ['--clean'],
|
|
195
195
|
expectedOptions: { clean: true },
|
|
196
196
|
},
|
|
197
|
+
{
|
|
198
|
+
name: '--keep-alive option',
|
|
199
|
+
args: ['--keep-alive'],
|
|
200
|
+
expectedOptions: { keepAlive: true },
|
|
201
|
+
},
|
|
197
202
|
])('$name', async ({ args, expectedOptions }) => {
|
|
198
203
|
mockFindUntrackedFiles.mockResolvedValue([]);
|
|
199
204
|
const program = new Command();
|
|
@@ -207,6 +212,7 @@ describe('CLI index.ts', () => {
|
|
|
207
212
|
.option('--tui', 'tui')
|
|
208
213
|
.option('--pr <url>', 'pr')
|
|
209
214
|
.option('--clean', 'start with a clean slate by clearing all existing comments')
|
|
215
|
+
.option('--keep-alive', 'keep server running even after browser disconnects')
|
|
210
216
|
.action(async (commitish, _compareWith, options) => {
|
|
211
217
|
let targetCommitish = commitish;
|
|
212
218
|
let baseCommitish = commitish + '^';
|
|
@@ -218,6 +224,7 @@ describe('CLI index.ts', () => {
|
|
|
218
224
|
openBrowser: options.open,
|
|
219
225
|
mode: options.mode,
|
|
220
226
|
clearComments: options.clean,
|
|
227
|
+
keepAlive: options.keepAlive,
|
|
221
228
|
});
|
|
222
229
|
});
|
|
223
230
|
await program.parseAsync([...args], { from: 'user' });
|
|
@@ -229,6 +236,7 @@ describe('CLI index.ts', () => {
|
|
|
229
236
|
openBrowser: expectedOptions.open !== false,
|
|
230
237
|
mode: expectedOptions.mode || 'split',
|
|
231
238
|
clearComments: expectedOptions.clean,
|
|
239
|
+
keepAlive: expectedOptions.keepAlive,
|
|
232
240
|
};
|
|
233
241
|
expect(mockStartServer).toHaveBeenCalledWith(expectedCall);
|
|
234
242
|
});
|
|
@@ -372,13 +380,10 @@ describe('CLI index.ts', () => {
|
|
|
372
380
|
});
|
|
373
381
|
});
|
|
374
382
|
describe('GitHub PR integration', () => {
|
|
375
|
-
it('
|
|
383
|
+
it('loads PR patch with gh and starts server with stdin diff', async () => {
|
|
376
384
|
const prUrl = 'https://github.com/owner/repo/pull/123';
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
baseCommitish: 'def456',
|
|
380
|
-
};
|
|
381
|
-
mockResolvePrCommits.mockResolvedValue(prCommits);
|
|
385
|
+
const prPatch = 'diff --git a/file.ts b/file.ts\nindex 1111111..2222222 100644\n';
|
|
386
|
+
mockGetPrPatch.mockReturnValue(prPatch);
|
|
382
387
|
const program = new Command();
|
|
383
388
|
program
|
|
384
389
|
.argument('[commit-ish]', 'commit-ish', 'HEAD')
|
|
@@ -390,23 +395,14 @@ describe('CLI index.ts', () => {
|
|
|
390
395
|
.option('--tui', 'tui')
|
|
391
396
|
.option('--pr <url>', 'pr')
|
|
392
397
|
.action(async (commitish, _compareWith, options) => {
|
|
393
|
-
let targetCommitish = commitish;
|
|
394
|
-
let baseCommitish;
|
|
395
398
|
if (options.pr) {
|
|
396
399
|
if (commitish !== 'HEAD' || _compareWith) {
|
|
397
400
|
console.error('Error: --pr option cannot be used with positional arguments');
|
|
398
401
|
process.exit(1);
|
|
399
402
|
}
|
|
400
|
-
const prCommits = await resolvePrCommits(options.pr);
|
|
401
|
-
targetCommitish = prCommits.targetCommitish;
|
|
402
|
-
baseCommitish = prCommits.baseCommitish;
|
|
403
|
-
}
|
|
404
|
-
else {
|
|
405
|
-
baseCommitish = commitish + '^';
|
|
406
403
|
}
|
|
407
404
|
await startServer({
|
|
408
|
-
|
|
409
|
-
baseCommitish,
|
|
405
|
+
stdinDiff: getPrPatch(options.pr),
|
|
410
406
|
preferredPort: options.port,
|
|
411
407
|
host: options.host,
|
|
412
408
|
openBrowser: options.open,
|
|
@@ -414,10 +410,9 @@ describe('CLI index.ts', () => {
|
|
|
414
410
|
});
|
|
415
411
|
});
|
|
416
412
|
await program.parseAsync(['--pr', prUrl], { from: 'user' });
|
|
417
|
-
expect(
|
|
413
|
+
expect(mockGetPrPatch).toHaveBeenCalledWith(prUrl);
|
|
418
414
|
expect(mockStartServer).toHaveBeenCalledWith({
|
|
419
|
-
|
|
420
|
-
baseCommitish: 'def456',
|
|
415
|
+
stdinDiff: prPatch,
|
|
421
416
|
preferredPort: undefined,
|
|
422
417
|
host: '',
|
|
423
418
|
openBrowser: true,
|
|
@@ -449,13 +444,8 @@ describe('CLI index.ts', () => {
|
|
|
449
444
|
expect(console.error).toHaveBeenCalledWith('Error: --pr option cannot be used with positional arguments');
|
|
450
445
|
expect(process.exit).toHaveBeenCalledWith(1);
|
|
451
446
|
});
|
|
452
|
-
it('
|
|
453
|
-
const prUrl = 'https://github.
|
|
454
|
-
const prCommits = {
|
|
455
|
-
targetCommitish: 'xyz789',
|
|
456
|
-
baseCommitish: 'uvw012',
|
|
457
|
-
};
|
|
458
|
-
mockResolvePrCommits.mockResolvedValue(prCommits);
|
|
447
|
+
it('rejects PR option with --tui', async () => {
|
|
448
|
+
const prUrl = 'https://github.com/owner/repo/pull/123';
|
|
459
449
|
const program = new Command();
|
|
460
450
|
program
|
|
461
451
|
.argument('[commit-ish]', 'commit-ish', 'HEAD')
|
|
@@ -467,39 +457,21 @@ describe('CLI index.ts', () => {
|
|
|
467
457
|
.option('--tui', 'tui')
|
|
468
458
|
.option('--pr <url>', 'pr')
|
|
469
459
|
.action(async (commitish, _compareWith, options) => {
|
|
470
|
-
let targetCommitish = commitish;
|
|
471
|
-
let baseCommitish;
|
|
472
460
|
if (options.pr) {
|
|
473
461
|
if (commitish !== 'HEAD' || _compareWith) {
|
|
474
462
|
console.error('Error: --pr option cannot be used with positional arguments');
|
|
475
463
|
process.exit(1);
|
|
476
464
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
else {
|
|
482
|
-
baseCommitish = commitish + '^';
|
|
465
|
+
if (options.tui) {
|
|
466
|
+
console.error('Error: --pr option cannot be used with --tui');
|
|
467
|
+
process.exit(1);
|
|
468
|
+
}
|
|
483
469
|
}
|
|
484
|
-
await startServer({
|
|
485
|
-
targetCommitish,
|
|
486
|
-
baseCommitish,
|
|
487
|
-
preferredPort: options.port,
|
|
488
|
-
host: options.host,
|
|
489
|
-
openBrowser: options.open,
|
|
490
|
-
mode: options.mode,
|
|
491
|
-
});
|
|
492
|
-
});
|
|
493
|
-
await program.parseAsync(['--pr', prUrl], { from: 'user' });
|
|
494
|
-
expect(mockResolvePrCommits).toHaveBeenCalledWith(prUrl);
|
|
495
|
-
expect(mockStartServer).toHaveBeenCalledWith({
|
|
496
|
-
targetCommitish: 'xyz789',
|
|
497
|
-
baseCommitish: 'uvw012',
|
|
498
|
-
preferredPort: undefined,
|
|
499
|
-
host: '',
|
|
500
|
-
openBrowser: true,
|
|
501
|
-
mode: 'split',
|
|
502
470
|
});
|
|
471
|
+
await program.parseAsync(['--pr', prUrl, '--tui'], { from: 'user' });
|
|
472
|
+
expect(console.error).toHaveBeenCalledWith('Error: --pr option cannot be used with --tui');
|
|
473
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
474
|
+
expect(mockStartServer).not.toHaveBeenCalled();
|
|
503
475
|
});
|
|
504
476
|
});
|
|
505
477
|
describe('Clean flag functionality', () => {
|
|
@@ -584,6 +556,92 @@ describe('CLI index.ts', () => {
|
|
|
584
556
|
expect(console.log).not.toHaveBeenCalledWith('🧹 Starting with a clean slate - all existing comments will be cleared');
|
|
585
557
|
});
|
|
586
558
|
});
|
|
559
|
+
describe('Keep-alive flag functionality', () => {
|
|
560
|
+
it('displays keep-alive message when flag is used', async () => {
|
|
561
|
+
mockFindUntrackedFiles.mockResolvedValue([]);
|
|
562
|
+
mockStartServer.mockResolvedValue({
|
|
563
|
+
port: 4966,
|
|
564
|
+
url: 'http://localhost:4966',
|
|
565
|
+
isEmpty: false,
|
|
566
|
+
});
|
|
567
|
+
const program = new Command();
|
|
568
|
+
program
|
|
569
|
+
.argument('[commit-ish]', 'commit-ish', 'HEAD')
|
|
570
|
+
.argument('[compare-with]', 'compare-with')
|
|
571
|
+
.option('--port <port>', 'port', parseInt)
|
|
572
|
+
.option('--host <host>', 'host', '')
|
|
573
|
+
.option('--no-open', 'no-open')
|
|
574
|
+
.option('--mode <mode>', 'mode', normalizeDiffViewMode, DEFAULT_DIFF_VIEW_MODE)
|
|
575
|
+
.option('--tui', 'tui')
|
|
576
|
+
.option('--pr <url>', 'pr')
|
|
577
|
+
.option('--clean', 'start with a clean slate by clearing all existing comments')
|
|
578
|
+
.option('--keep-alive', 'keep server running even after browser disconnects')
|
|
579
|
+
.action(async (commitish, _compareWith, options) => {
|
|
580
|
+
const { url } = await startServer({
|
|
581
|
+
targetCommitish: commitish,
|
|
582
|
+
baseCommitish: commitish + '^',
|
|
583
|
+
preferredPort: options.port,
|
|
584
|
+
host: options.host,
|
|
585
|
+
openBrowser: options.open,
|
|
586
|
+
mode: options.mode,
|
|
587
|
+
clearComments: options.clean,
|
|
588
|
+
keepAlive: options.keepAlive,
|
|
589
|
+
});
|
|
590
|
+
console.log(`\n🚀 difit server started on ${url}`);
|
|
591
|
+
console.log(`📋 Reviewing: ${commitish}`);
|
|
592
|
+
if (options.keepAlive) {
|
|
593
|
+
console.log('🔒 Keep-alive mode: server will stay running after browser disconnects');
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
await program.parseAsync(['--keep-alive'], { from: 'user' });
|
|
597
|
+
expect(mockStartServer).toHaveBeenCalledWith(expect.objectContaining({
|
|
598
|
+
keepAlive: true,
|
|
599
|
+
}));
|
|
600
|
+
expect(console.log).toHaveBeenCalledWith('🔒 Keep-alive mode: server will stay running after browser disconnects');
|
|
601
|
+
});
|
|
602
|
+
it('does not display keep-alive message when flag is not used', async () => {
|
|
603
|
+
mockFindUntrackedFiles.mockResolvedValue([]);
|
|
604
|
+
mockStartServer.mockResolvedValue({
|
|
605
|
+
port: 4966,
|
|
606
|
+
url: 'http://localhost:4966',
|
|
607
|
+
isEmpty: false,
|
|
608
|
+
});
|
|
609
|
+
const program = new Command();
|
|
610
|
+
program
|
|
611
|
+
.argument('[commit-ish]', 'commit-ish', 'HEAD')
|
|
612
|
+
.argument('[compare-with]', 'compare-with')
|
|
613
|
+
.option('--port <port>', 'port', parseInt)
|
|
614
|
+
.option('--host <host>', 'host', '')
|
|
615
|
+
.option('--no-open', 'no-open')
|
|
616
|
+
.option('--mode <mode>', 'mode', normalizeDiffViewMode, DEFAULT_DIFF_VIEW_MODE)
|
|
617
|
+
.option('--tui', 'tui')
|
|
618
|
+
.option('--pr <url>', 'pr')
|
|
619
|
+
.option('--clean', 'start with a clean slate by clearing all existing comments')
|
|
620
|
+
.option('--keep-alive', 'keep server running even after browser disconnects')
|
|
621
|
+
.action(async (commitish, _compareWith, options) => {
|
|
622
|
+
const { url } = await startServer({
|
|
623
|
+
targetCommitish: commitish,
|
|
624
|
+
baseCommitish: commitish + '^',
|
|
625
|
+
preferredPort: options.port,
|
|
626
|
+
host: options.host,
|
|
627
|
+
openBrowser: options.open,
|
|
628
|
+
mode: options.mode,
|
|
629
|
+
clearComments: options.clean,
|
|
630
|
+
keepAlive: options.keepAlive,
|
|
631
|
+
});
|
|
632
|
+
console.log(`\n🚀 difit server started on ${url}`);
|
|
633
|
+
console.log(`📋 Reviewing: ${commitish}`);
|
|
634
|
+
if (options.keepAlive) {
|
|
635
|
+
console.log('🔒 Keep-alive mode: server will stay running after browser disconnects');
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
await program.parseAsync([], { from: 'user' });
|
|
639
|
+
expect(mockStartServer).toHaveBeenCalledWith(expect.objectContaining({
|
|
640
|
+
keepAlive: undefined,
|
|
641
|
+
}));
|
|
642
|
+
expect(console.log).not.toHaveBeenCalledWith('🔒 Keep-alive mode: server will stay running after browser disconnects');
|
|
643
|
+
});
|
|
644
|
+
});
|
|
587
645
|
describe('Console output', () => {
|
|
588
646
|
it('displays server startup message with correct URL', async () => {
|
|
589
647
|
mockFindUntrackedFiles.mockResolvedValue([]);
|
package/dist/cli/utils.d.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
|
+
import { type Stats } from 'node:fs';
|
|
1
2
|
import type { SimpleGit } from 'simple-git';
|
|
3
|
+
type StdinStat = Pick<Stats, 'isFIFO' | 'isFile' | 'isSocket'>;
|
|
4
|
+
export type StdinSource = 'pipe' | 'file' | 'socket' | 'tty';
|
|
5
|
+
export declare function detectStdinSource(stdinStat?: StdinStat): StdinSource;
|
|
6
|
+
export interface ShouldReadStdinOptions {
|
|
7
|
+
commitish: string;
|
|
8
|
+
hasPositionalArgs: boolean;
|
|
9
|
+
hasPrOption: boolean;
|
|
10
|
+
hasTuiOption: boolean;
|
|
11
|
+
stdinSource?: StdinSource;
|
|
12
|
+
}
|
|
13
|
+
export declare function shouldReadStdin(options: ShouldReadStdinOptions): boolean;
|
|
2
14
|
export declare function getGitRoot(): string;
|
|
3
15
|
export declare function validateCommitish(commitish: string): boolean;
|
|
4
16
|
export declare function shortHash(hash: string): string;
|
|
@@ -10,10 +22,7 @@ export interface PullRequestInfo {
|
|
|
10
22
|
hostname: string;
|
|
11
23
|
}
|
|
12
24
|
export declare function parseGitHubPrUrl(url: string): PullRequestInfo | null;
|
|
13
|
-
export declare function
|
|
14
|
-
targetCommitish: string;
|
|
15
|
-
baseCommitish: string;
|
|
16
|
-
}>;
|
|
25
|
+
export declare function getPrPatch(prArg: string): string;
|
|
17
26
|
export declare function validateDiffArguments(targetCommitish: string, baseCommitish?: string): {
|
|
18
27
|
valid: boolean;
|
|
19
28
|
error?: string;
|
|
@@ -21,3 +30,4 @@ export declare function validateDiffArguments(targetCommitish: string, baseCommi
|
|
|
21
30
|
export declare function findUntrackedFiles(git: SimpleGit): Promise<string[]>;
|
|
22
31
|
export declare function markFilesIntentToAdd(git: SimpleGit, files: string[]): Promise<void>;
|
|
23
32
|
export declare function promptUser(message: string): Promise<boolean>;
|
|
33
|
+
export {};
|
package/dist/cli/utils.js
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
|
-
import { execSync } from 'child_process';
|
|
1
|
+
import { execFileSync, execSync } from 'child_process';
|
|
2
|
+
import { fstatSync } from 'node:fs';
|
|
2
3
|
import { createInterface } from 'readline/promises';
|
|
3
|
-
|
|
4
|
+
export function detectStdinSource(stdinStat = fstatSync(0)) {
|
|
5
|
+
if (stdinStat.isFIFO()) {
|
|
6
|
+
return 'pipe';
|
|
7
|
+
}
|
|
8
|
+
if (stdinStat.isFile()) {
|
|
9
|
+
return 'file';
|
|
10
|
+
}
|
|
11
|
+
if (stdinStat.isSocket()) {
|
|
12
|
+
return 'socket';
|
|
13
|
+
}
|
|
14
|
+
return 'tty';
|
|
15
|
+
}
|
|
16
|
+
export function shouldReadStdin(options) {
|
|
17
|
+
if (options.commitish === '-') {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
if (options.hasPositionalArgs || options.hasPrOption || options.hasTuiOption) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const stdinSource = options.stdinSource ?? detectStdinSource();
|
|
24
|
+
return stdinSource === 'pipe' || stdinSource === 'file' || stdinSource === 'socket';
|
|
25
|
+
}
|
|
4
26
|
export function getGitRoot() {
|
|
5
27
|
try {
|
|
6
28
|
const result = execSync('git rev-parse --show-toplevel', { encoding: 'utf8', stdio: 'pipe' });
|
|
@@ -99,107 +121,27 @@ export function parseGitHubPrUrl(url) {
|
|
|
99
121
|
return null;
|
|
100
122
|
}
|
|
101
123
|
}
|
|
102
|
-
function
|
|
103
|
-
// Try to get token from environment variable first
|
|
104
|
-
if (process.env.GITHUB_TOKEN) {
|
|
105
|
-
return process.env.GITHUB_TOKEN;
|
|
106
|
-
}
|
|
107
|
-
// Try to get token from GitHub CLI
|
|
108
|
-
try {
|
|
109
|
-
const result = execSync('gh auth token', { encoding: 'utf8', stdio: 'pipe' });
|
|
110
|
-
return result.trim();
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
// GitHub CLI not available or not authenticated
|
|
114
|
-
return undefined;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
async function fetchPrDetails(prInfo) {
|
|
118
|
-
const token = getGitHubToken();
|
|
119
|
-
const octokitOptions = {
|
|
120
|
-
auth: token,
|
|
121
|
-
};
|
|
122
|
-
// For GitHub Enterprise, set the base URL
|
|
123
|
-
if (prInfo.hostname !== 'github.com') {
|
|
124
|
-
octokitOptions.baseUrl = `https://${prInfo.hostname}/api/v3`;
|
|
125
|
-
}
|
|
126
|
-
const octokit = new Octokit(octokitOptions);
|
|
124
|
+
export function getPrPatch(prArg) {
|
|
127
125
|
try {
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
pull_number: prInfo.pullNumber,
|
|
126
|
+
const patch = execFileSync('gh', ['pr', 'diff', prArg, '--patch'], {
|
|
127
|
+
encoding: 'utf8',
|
|
128
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
132
129
|
});
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
headSha: pr.head.sha,
|
|
136
|
-
baseRef: pr.base.ref,
|
|
137
|
-
headRef: pr.head.ref,
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
catch (error) {
|
|
141
|
-
if (error instanceof Error) {
|
|
142
|
-
let authHint = '';
|
|
143
|
-
// Provide more specific error messages for authentication issues
|
|
144
|
-
if (error.message.includes('Bad credentials')) {
|
|
145
|
-
if (prInfo.hostname !== 'github.com') {
|
|
146
|
-
authHint = `\n\nFor GitHub Enterprise Server (${prInfo.hostname}):
|
|
147
|
-
1. Generate a token on YOUR Enterprise Server: https://${prInfo.hostname}/settings/tokens
|
|
148
|
-
2. Set it as GITHUB_TOKEN environment variable
|
|
149
|
-
3. Tokens from github.com will NOT work on Enterprise servers`;
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
authHint = '\n\nTry: gh auth login or set GITHUB_TOKEN environment variable';
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
else if (!token) {
|
|
156
|
-
authHint = ' (Try: gh auth login or set GITHUB_TOKEN environment variable)';
|
|
157
|
-
}
|
|
158
|
-
throw new Error(`Failed to fetch PR details: ${error.message}${authHint}`);
|
|
130
|
+
if (!patch.trim()) {
|
|
131
|
+
throw new Error('No patch content returned from gh pr diff --patch');
|
|
159
132
|
}
|
|
160
|
-
|
|
133
|
+
return patch;
|
|
161
134
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
try {
|
|
172
|
-
execSync('git fetch origin', { stdio: 'ignore' });
|
|
173
|
-
execSync(`git cat-file -e ${sha}`, { stdio: 'ignore' });
|
|
174
|
-
return sha;
|
|
175
|
-
}
|
|
176
|
-
catch {
|
|
177
|
-
const errorMessage = [
|
|
178
|
-
`Commit ${sha} not found in local repository.`,
|
|
179
|
-
'',
|
|
180
|
-
'Common causes:',
|
|
181
|
-
' • Are you running this command in the correct repository directory?',
|
|
182
|
-
context ? ` • Expected repository: ${context.owner}/${context.repo}` : '',
|
|
183
|
-
' • Is this PR from a fork?',
|
|
184
|
-
' • Try: git remote add upstream <original-repo-url> && git fetch upstream',
|
|
185
|
-
' • Try: git fetch --all to fetch from all remotes',
|
|
186
|
-
]
|
|
187
|
-
.filter(Boolean)
|
|
188
|
-
.join('\n');
|
|
189
|
-
throw new Error(errorMessage);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
export async function resolvePrCommits(prUrl) {
|
|
194
|
-
const prInfo = parseGitHubPrUrl(prUrl);
|
|
195
|
-
if (!prInfo) {
|
|
196
|
-
throw new Error('Invalid GitHub PR URL format. Expected: https://github.com/owner/repo/pull/123 or https://github.enterprise.com/owner/repo/pull/123');
|
|
135
|
+
catch (error) {
|
|
136
|
+
const stderr = error.stderr;
|
|
137
|
+
const stderrText = typeof stderr === 'string'
|
|
138
|
+
? stderr.trim()
|
|
139
|
+
: Buffer.isBuffer(stderr)
|
|
140
|
+
? stderr.toString('utf8').trim()
|
|
141
|
+
: '';
|
|
142
|
+
const message = stderrText || (error instanceof Error ? error.message : 'Unknown error while running gh');
|
|
143
|
+
throw new Error(`${message}\nTry: gh auth login`);
|
|
197
144
|
}
|
|
198
|
-
const prDetails = await fetchPrDetails(prInfo);
|
|
199
|
-
const context = { owner: prInfo.owner, repo: prInfo.repo };
|
|
200
|
-
const targetCommitish = resolveCommitInLocalRepo(prDetails.headSha, context);
|
|
201
|
-
const baseCommitish = resolveCommitInLocalRepo(prDetails.baseSha, context);
|
|
202
|
-
return { targetCommitish, baseCommitish };
|
|
203
145
|
}
|
|
204
146
|
export function validateDiffArguments(targetCommitish, baseCommitish) {
|
|
205
147
|
// Validate target commitish format
|
package/dist/cli/utils.test.js
CHANGED
|
@@ -1,6 +1,104 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import { detectStdinSource, parseGitHubPrUrl, shortHash, shouldReadStdin, validateCommitish, validateDiffArguments, } from './utils';
|
|
3
3
|
describe('CLI Utils', () => {
|
|
4
|
+
describe('stdin detection', () => {
|
|
5
|
+
it('detects pipe from stdin stat', () => {
|
|
6
|
+
expect(detectStdinSource({
|
|
7
|
+
isFIFO: () => true,
|
|
8
|
+
isFile: () => false,
|
|
9
|
+
isSocket: () => false,
|
|
10
|
+
})).toBe('pipe');
|
|
11
|
+
});
|
|
12
|
+
it('detects file from stdin stat', () => {
|
|
13
|
+
expect(detectStdinSource({
|
|
14
|
+
isFIFO: () => false,
|
|
15
|
+
isFile: () => true,
|
|
16
|
+
isSocket: () => false,
|
|
17
|
+
})).toBe('file');
|
|
18
|
+
});
|
|
19
|
+
it('detects socket from stdin stat', () => {
|
|
20
|
+
expect(detectStdinSource({
|
|
21
|
+
isFIFO: () => false,
|
|
22
|
+
isFile: () => false,
|
|
23
|
+
isSocket: () => true,
|
|
24
|
+
})).toBe('socket');
|
|
25
|
+
});
|
|
26
|
+
it('detects tty for character device stdin stat', () => {
|
|
27
|
+
expect(detectStdinSource({
|
|
28
|
+
isFIFO: () => false,
|
|
29
|
+
isFile: () => false,
|
|
30
|
+
isSocket: () => false,
|
|
31
|
+
})).toBe('tty');
|
|
32
|
+
});
|
|
33
|
+
it('reads stdin when commitish is explicit stdin marker', () => {
|
|
34
|
+
expect(shouldReadStdin({
|
|
35
|
+
commitish: '-',
|
|
36
|
+
hasPositionalArgs: true,
|
|
37
|
+
hasPrOption: true,
|
|
38
|
+
hasTuiOption: true,
|
|
39
|
+
stdinSource: 'tty',
|
|
40
|
+
})).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
it('does not read stdin when positional args are explicitly passed', () => {
|
|
43
|
+
expect(shouldReadStdin({
|
|
44
|
+
commitish: '.',
|
|
45
|
+
hasPositionalArgs: true,
|
|
46
|
+
hasPrOption: false,
|
|
47
|
+
hasTuiOption: false,
|
|
48
|
+
stdinSource: 'pipe',
|
|
49
|
+
})).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
it('does not read stdin when --pr is specified', () => {
|
|
52
|
+
expect(shouldReadStdin({
|
|
53
|
+
commitish: 'HEAD',
|
|
54
|
+
hasPositionalArgs: false,
|
|
55
|
+
hasPrOption: true,
|
|
56
|
+
hasTuiOption: false,
|
|
57
|
+
stdinSource: 'pipe',
|
|
58
|
+
})).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
it('does not read stdin when --tui is specified', () => {
|
|
61
|
+
expect(shouldReadStdin({
|
|
62
|
+
commitish: 'HEAD',
|
|
63
|
+
hasPositionalArgs: false,
|
|
64
|
+
hasPrOption: false,
|
|
65
|
+
hasTuiOption: true,
|
|
66
|
+
stdinSource: 'pipe',
|
|
67
|
+
})).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
it('auto-detects stdin for pipe/file/socket only when no explicit git mode is selected', () => {
|
|
70
|
+
expect(shouldReadStdin({
|
|
71
|
+
commitish: 'HEAD',
|
|
72
|
+
hasPositionalArgs: false,
|
|
73
|
+
hasPrOption: false,
|
|
74
|
+
hasTuiOption: false,
|
|
75
|
+
stdinSource: 'pipe',
|
|
76
|
+
})).toBe(true);
|
|
77
|
+
expect(shouldReadStdin({
|
|
78
|
+
commitish: 'HEAD',
|
|
79
|
+
hasPositionalArgs: false,
|
|
80
|
+
hasPrOption: false,
|
|
81
|
+
hasTuiOption: false,
|
|
82
|
+
stdinSource: 'file',
|
|
83
|
+
})).toBe(true);
|
|
84
|
+
expect(shouldReadStdin({
|
|
85
|
+
commitish: 'HEAD',
|
|
86
|
+
hasPositionalArgs: false,
|
|
87
|
+
hasPrOption: false,
|
|
88
|
+
hasTuiOption: false,
|
|
89
|
+
stdinSource: 'socket',
|
|
90
|
+
})).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
it('does not auto-read stdin for tty source', () => {
|
|
93
|
+
expect(shouldReadStdin({
|
|
94
|
+
commitish: 'HEAD',
|
|
95
|
+
hasPositionalArgs: false,
|
|
96
|
+
hasPrOption: false,
|
|
97
|
+
hasTuiOption: false,
|
|
98
|
+
stdinSource: 'tty',
|
|
99
|
+
})).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
4
102
|
describe('validateCommitish', () => {
|
|
5
103
|
it('should validate full SHA hashes', () => {
|
|
6
104
|
expect(validateCommitish('a1b2c3d4e5f6789012345678901234567890abcd')).toBe(true);
|