difit 3.1.9 → 3.1.11
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 +30 -13
- package/README.ko.md +30 -13
- package/README.md +30 -13
- package/README.zh.md +30 -13
- package/dist/cli/index.js +56 -46
- package/dist/cli/index.test.js +143 -56
- package/dist/cli/utils.d.ts +4 -7
- package/dist/cli/utils.js +17 -98
- package/dist/client/assets/{index-B1Ye1njT.js → index-0eidGb4G.js} +30 -30
- package/dist/client/assets/{prism-csharp-Dv4F6oYg.js → prism-csharp-Bz_zphOC.js} +1 -1
- package/dist/client/assets/{prism-hcl-BKiQPDt1.js → prism-hcl-HO7lwwhc.js} +1 -1
- package/dist/client/assets/{prism-java-CsLA9BYF.js → prism-java-Dh0Q2eOX.js} +1 -1
- package/dist/client/assets/{prism-perl-CjbksjRr.js → prism-perl-D_qbRz8N.js} +1 -1
- package/dist/client/assets/{prism-php-Blls7Q4g.js → prism-php-Dgr7EUBn.js} +1 -1
- package/dist/client/assets/{prism-ruby-DVHs5uYA.js → prism-ruby-CaFchRnn.js} +1 -1
- package/dist/client/assets/{prism-solidity-BxgmEX2j.js → prism-solidity-CDfI790k.js} +1 -1
- package/dist/client/index.html +1 -1
- package/dist/server/generated-file-check.d.ts +2 -1
- package/dist/server/git-diff.js +38 -22
- package/dist/server/server.d.ts +1 -0
- package/dist/server/server.js +17 -10
- package/dist/server/server.test.js +104 -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/types/diff.d.ts +1 -0
- package/dist/utils/commentFormatting.js +1 -3
- package/dist/utils/suggestionUtils.d.ts +2 -1
- package/package.json +39 -48
package/dist/cli/index.test.js
CHANGED
|
@@ -3,6 +3,7 @@ import React from 'react';
|
|
|
3
3
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
4
4
|
import { DiffMode } from '../types/watch.js';
|
|
5
5
|
import { DEFAULT_DIFF_VIEW_MODE, normalizeDiffViewMode } from '../utils/diffMode.js';
|
|
6
|
+
import pkg from '../../package.json' with { type: 'json' };
|
|
6
7
|
// Mock all external dependencies
|
|
7
8
|
vi.mock('simple-git');
|
|
8
9
|
vi.mock('../server/server.js');
|
|
@@ -13,19 +14,19 @@ vi.mock('./utils.js', async () => {
|
|
|
13
14
|
promptUser: vi.fn(),
|
|
14
15
|
findUntrackedFiles: vi.fn(),
|
|
15
16
|
markFilesIntentToAdd: vi.fn(),
|
|
16
|
-
|
|
17
|
+
getPrPatch: vi.fn(),
|
|
17
18
|
};
|
|
18
19
|
});
|
|
19
20
|
const { simpleGit } = await import('simple-git');
|
|
20
21
|
const { startServer } = await import('../server/server.js');
|
|
21
|
-
const { promptUser, findUntrackedFiles, markFilesIntentToAdd,
|
|
22
|
+
const { promptUser, findUntrackedFiles, markFilesIntentToAdd, getPrPatch } = await import('./utils.js');
|
|
22
23
|
describe('CLI index.ts', () => {
|
|
23
24
|
let mockGit;
|
|
24
25
|
let mockStartServer;
|
|
25
26
|
let mockPromptUser;
|
|
26
27
|
let mockFindUntrackedFiles;
|
|
27
28
|
let mockMarkFilesIntentToAdd;
|
|
28
|
-
let
|
|
29
|
+
let mockGetPrPatch;
|
|
29
30
|
// Store original console methods
|
|
30
31
|
let originalConsoleLog;
|
|
31
32
|
let originalConsoleError;
|
|
@@ -46,7 +47,7 @@ describe('CLI index.ts', () => {
|
|
|
46
47
|
mockPromptUser = vi.mocked(promptUser);
|
|
47
48
|
mockFindUntrackedFiles = vi.mocked(findUntrackedFiles);
|
|
48
49
|
mockMarkFilesIntentToAdd = vi.mocked(markFilesIntentToAdd);
|
|
49
|
-
|
|
50
|
+
mockGetPrPatch = vi.mocked(getPrPatch);
|
|
50
51
|
// Mock console and process.exit
|
|
51
52
|
originalConsoleLog = console.log;
|
|
52
53
|
originalConsoleError = console.error;
|
|
@@ -194,6 +195,11 @@ describe('CLI index.ts', () => {
|
|
|
194
195
|
args: ['--clean'],
|
|
195
196
|
expectedOptions: { clean: true },
|
|
196
197
|
},
|
|
198
|
+
{
|
|
199
|
+
name: '--keep-alive option',
|
|
200
|
+
args: ['--keep-alive'],
|
|
201
|
+
expectedOptions: { keepAlive: true },
|
|
202
|
+
},
|
|
197
203
|
])('$name', async ({ args, expectedOptions }) => {
|
|
198
204
|
mockFindUntrackedFiles.mockResolvedValue([]);
|
|
199
205
|
const program = new Command();
|
|
@@ -207,6 +213,7 @@ describe('CLI index.ts', () => {
|
|
|
207
213
|
.option('--tui', 'tui')
|
|
208
214
|
.option('--pr <url>', 'pr')
|
|
209
215
|
.option('--clean', 'start with a clean slate by clearing all existing comments')
|
|
216
|
+
.option('--keep-alive', 'keep server running even after browser disconnects')
|
|
210
217
|
.action(async (commitish, _compareWith, options) => {
|
|
211
218
|
let targetCommitish = commitish;
|
|
212
219
|
let baseCommitish = commitish + '^';
|
|
@@ -218,6 +225,7 @@ describe('CLI index.ts', () => {
|
|
|
218
225
|
openBrowser: options.open,
|
|
219
226
|
mode: options.mode,
|
|
220
227
|
clearComments: options.clean,
|
|
228
|
+
keepAlive: options.keepAlive,
|
|
221
229
|
});
|
|
222
230
|
});
|
|
223
231
|
await program.parseAsync([...args], { from: 'user' });
|
|
@@ -229,10 +237,39 @@ describe('CLI index.ts', () => {
|
|
|
229
237
|
openBrowser: expectedOptions.open !== false,
|
|
230
238
|
mode: expectedOptions.mode || 'split',
|
|
231
239
|
clearComments: expectedOptions.clean,
|
|
240
|
+
keepAlive: expectedOptions.keepAlive,
|
|
232
241
|
};
|
|
233
242
|
expect(mockStartServer).toHaveBeenCalledWith(expectedCall);
|
|
234
243
|
});
|
|
235
244
|
});
|
|
245
|
+
describe('Version option', () => {
|
|
246
|
+
it('supports --version flag', async () => {
|
|
247
|
+
const program = new Command();
|
|
248
|
+
const stdoutWrite = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
249
|
+
program.version(pkg.version, '-v, --version', 'output the version number').exitOverride();
|
|
250
|
+
try {
|
|
251
|
+
await program.parseAsync(['--version'], { from: 'user' });
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
// commander exits after printing version
|
|
255
|
+
}
|
|
256
|
+
expect(stdoutWrite).toHaveBeenCalledWith(`${pkg.version}\n`);
|
|
257
|
+
stdoutWrite.mockRestore();
|
|
258
|
+
});
|
|
259
|
+
it('supports -v flag', async () => {
|
|
260
|
+
const program = new Command();
|
|
261
|
+
const stdoutWrite = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
262
|
+
program.version(pkg.version, '-v, --version', 'output the version number').exitOverride();
|
|
263
|
+
try {
|
|
264
|
+
await program.parseAsync(['-v'], { from: 'user' });
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// commander exits after printing version
|
|
268
|
+
}
|
|
269
|
+
expect(stdoutWrite).toHaveBeenCalledWith(`${pkg.version}\n`);
|
|
270
|
+
stdoutWrite.mockRestore();
|
|
271
|
+
});
|
|
272
|
+
});
|
|
236
273
|
describe('Git operations', () => {
|
|
237
274
|
it('handles untracked files for working directory', async () => {
|
|
238
275
|
const untrackedFiles = ['file1.js', 'file2.js'];
|
|
@@ -372,13 +409,10 @@ describe('CLI index.ts', () => {
|
|
|
372
409
|
});
|
|
373
410
|
});
|
|
374
411
|
describe('GitHub PR integration', () => {
|
|
375
|
-
it('
|
|
412
|
+
it('loads PR patch with gh and starts server with stdin diff', async () => {
|
|
376
413
|
const prUrl = 'https://github.com/owner/repo/pull/123';
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
baseCommitish: 'def456',
|
|
380
|
-
};
|
|
381
|
-
mockResolvePrCommits.mockResolvedValue(prCommits);
|
|
414
|
+
const prPatch = 'diff --git a/file.ts b/file.ts\nindex 1111111..2222222 100644\n';
|
|
415
|
+
mockGetPrPatch.mockReturnValue(prPatch);
|
|
382
416
|
const program = new Command();
|
|
383
417
|
program
|
|
384
418
|
.argument('[commit-ish]', 'commit-ish', 'HEAD')
|
|
@@ -390,23 +424,14 @@ describe('CLI index.ts', () => {
|
|
|
390
424
|
.option('--tui', 'tui')
|
|
391
425
|
.option('--pr <url>', 'pr')
|
|
392
426
|
.action(async (commitish, _compareWith, options) => {
|
|
393
|
-
let targetCommitish = commitish;
|
|
394
|
-
let baseCommitish;
|
|
395
427
|
if (options.pr) {
|
|
396
428
|
if (commitish !== 'HEAD' || _compareWith) {
|
|
397
429
|
console.error('Error: --pr option cannot be used with positional arguments');
|
|
398
430
|
process.exit(1);
|
|
399
431
|
}
|
|
400
|
-
const prCommits = await resolvePrCommits(options.pr);
|
|
401
|
-
targetCommitish = prCommits.targetCommitish;
|
|
402
|
-
baseCommitish = prCommits.baseCommitish;
|
|
403
|
-
}
|
|
404
|
-
else {
|
|
405
|
-
baseCommitish = commitish + '^';
|
|
406
432
|
}
|
|
407
433
|
await startServer({
|
|
408
|
-
|
|
409
|
-
baseCommitish,
|
|
434
|
+
stdinDiff: getPrPatch(options.pr),
|
|
410
435
|
preferredPort: options.port,
|
|
411
436
|
host: options.host,
|
|
412
437
|
openBrowser: options.open,
|
|
@@ -414,10 +439,9 @@ describe('CLI index.ts', () => {
|
|
|
414
439
|
});
|
|
415
440
|
});
|
|
416
441
|
await program.parseAsync(['--pr', prUrl], { from: 'user' });
|
|
417
|
-
expect(
|
|
442
|
+
expect(mockGetPrPatch).toHaveBeenCalledWith(prUrl);
|
|
418
443
|
expect(mockStartServer).toHaveBeenCalledWith({
|
|
419
|
-
|
|
420
|
-
baseCommitish: 'def456',
|
|
444
|
+
stdinDiff: prPatch,
|
|
421
445
|
preferredPort: undefined,
|
|
422
446
|
host: '',
|
|
423
447
|
openBrowser: true,
|
|
@@ -449,13 +473,8 @@ describe('CLI index.ts', () => {
|
|
|
449
473
|
expect(console.error).toHaveBeenCalledWith('Error: --pr option cannot be used with positional arguments');
|
|
450
474
|
expect(process.exit).toHaveBeenCalledWith(1);
|
|
451
475
|
});
|
|
452
|
-
it('
|
|
453
|
-
const prUrl = 'https://github.
|
|
454
|
-
const prCommits = {
|
|
455
|
-
targetCommitish: 'xyz789',
|
|
456
|
-
baseCommitish: 'uvw012',
|
|
457
|
-
};
|
|
458
|
-
mockResolvePrCommits.mockResolvedValue(prCommits);
|
|
476
|
+
it('rejects PR option with --tui', async () => {
|
|
477
|
+
const prUrl = 'https://github.com/owner/repo/pull/123';
|
|
459
478
|
const program = new Command();
|
|
460
479
|
program
|
|
461
480
|
.argument('[commit-ish]', 'commit-ish', 'HEAD')
|
|
@@ -467,39 +486,21 @@ describe('CLI index.ts', () => {
|
|
|
467
486
|
.option('--tui', 'tui')
|
|
468
487
|
.option('--pr <url>', 'pr')
|
|
469
488
|
.action(async (commitish, _compareWith, options) => {
|
|
470
|
-
let targetCommitish = commitish;
|
|
471
|
-
let baseCommitish;
|
|
472
489
|
if (options.pr) {
|
|
473
490
|
if (commitish !== 'HEAD' || _compareWith) {
|
|
474
491
|
console.error('Error: --pr option cannot be used with positional arguments');
|
|
475
492
|
process.exit(1);
|
|
476
493
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
else {
|
|
482
|
-
baseCommitish = commitish + '^';
|
|
494
|
+
if (options.tui) {
|
|
495
|
+
console.error('Error: --pr option cannot be used with --tui');
|
|
496
|
+
process.exit(1);
|
|
497
|
+
}
|
|
483
498
|
}
|
|
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
499
|
});
|
|
500
|
+
await program.parseAsync(['--pr', prUrl, '--tui'], { from: 'user' });
|
|
501
|
+
expect(console.error).toHaveBeenCalledWith('Error: --pr option cannot be used with --tui');
|
|
502
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
503
|
+
expect(mockStartServer).not.toHaveBeenCalled();
|
|
503
504
|
});
|
|
504
505
|
});
|
|
505
506
|
describe('Clean flag functionality', () => {
|
|
@@ -584,6 +585,92 @@ describe('CLI index.ts', () => {
|
|
|
584
585
|
expect(console.log).not.toHaveBeenCalledWith('🧹 Starting with a clean slate - all existing comments will be cleared');
|
|
585
586
|
});
|
|
586
587
|
});
|
|
588
|
+
describe('Keep-alive flag functionality', () => {
|
|
589
|
+
it('displays keep-alive message when flag is used', async () => {
|
|
590
|
+
mockFindUntrackedFiles.mockResolvedValue([]);
|
|
591
|
+
mockStartServer.mockResolvedValue({
|
|
592
|
+
port: 4966,
|
|
593
|
+
url: 'http://localhost:4966',
|
|
594
|
+
isEmpty: false,
|
|
595
|
+
});
|
|
596
|
+
const program = new Command();
|
|
597
|
+
program
|
|
598
|
+
.argument('[commit-ish]', 'commit-ish', 'HEAD')
|
|
599
|
+
.argument('[compare-with]', 'compare-with')
|
|
600
|
+
.option('--port <port>', 'port', parseInt)
|
|
601
|
+
.option('--host <host>', 'host', '')
|
|
602
|
+
.option('--no-open', 'no-open')
|
|
603
|
+
.option('--mode <mode>', 'mode', normalizeDiffViewMode, DEFAULT_DIFF_VIEW_MODE)
|
|
604
|
+
.option('--tui', 'tui')
|
|
605
|
+
.option('--pr <url>', 'pr')
|
|
606
|
+
.option('--clean', 'start with a clean slate by clearing all existing comments')
|
|
607
|
+
.option('--keep-alive', 'keep server running even after browser disconnects')
|
|
608
|
+
.action(async (commitish, _compareWith, options) => {
|
|
609
|
+
const { url } = await startServer({
|
|
610
|
+
targetCommitish: commitish,
|
|
611
|
+
baseCommitish: commitish + '^',
|
|
612
|
+
preferredPort: options.port,
|
|
613
|
+
host: options.host,
|
|
614
|
+
openBrowser: options.open,
|
|
615
|
+
mode: options.mode,
|
|
616
|
+
clearComments: options.clean,
|
|
617
|
+
keepAlive: options.keepAlive,
|
|
618
|
+
});
|
|
619
|
+
console.log(`\n🚀 difit server started on ${url}`);
|
|
620
|
+
console.log(`📋 Reviewing: ${commitish}`);
|
|
621
|
+
if (options.keepAlive) {
|
|
622
|
+
console.log('🔒 Keep-alive mode: server will stay running after browser disconnects');
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
await program.parseAsync(['--keep-alive'], { from: 'user' });
|
|
626
|
+
expect(mockStartServer).toHaveBeenCalledWith(expect.objectContaining({
|
|
627
|
+
keepAlive: true,
|
|
628
|
+
}));
|
|
629
|
+
expect(console.log).toHaveBeenCalledWith('🔒 Keep-alive mode: server will stay running after browser disconnects');
|
|
630
|
+
});
|
|
631
|
+
it('does not display keep-alive message when flag is not used', async () => {
|
|
632
|
+
mockFindUntrackedFiles.mockResolvedValue([]);
|
|
633
|
+
mockStartServer.mockResolvedValue({
|
|
634
|
+
port: 4966,
|
|
635
|
+
url: 'http://localhost:4966',
|
|
636
|
+
isEmpty: false,
|
|
637
|
+
});
|
|
638
|
+
const program = new Command();
|
|
639
|
+
program
|
|
640
|
+
.argument('[commit-ish]', 'commit-ish', 'HEAD')
|
|
641
|
+
.argument('[compare-with]', 'compare-with')
|
|
642
|
+
.option('--port <port>', 'port', parseInt)
|
|
643
|
+
.option('--host <host>', 'host', '')
|
|
644
|
+
.option('--no-open', 'no-open')
|
|
645
|
+
.option('--mode <mode>', 'mode', normalizeDiffViewMode, DEFAULT_DIFF_VIEW_MODE)
|
|
646
|
+
.option('--tui', 'tui')
|
|
647
|
+
.option('--pr <url>', 'pr')
|
|
648
|
+
.option('--clean', 'start with a clean slate by clearing all existing comments')
|
|
649
|
+
.option('--keep-alive', 'keep server running even after browser disconnects')
|
|
650
|
+
.action(async (commitish, _compareWith, options) => {
|
|
651
|
+
const { url } = await startServer({
|
|
652
|
+
targetCommitish: commitish,
|
|
653
|
+
baseCommitish: commitish + '^',
|
|
654
|
+
preferredPort: options.port,
|
|
655
|
+
host: options.host,
|
|
656
|
+
openBrowser: options.open,
|
|
657
|
+
mode: options.mode,
|
|
658
|
+
clearComments: options.clean,
|
|
659
|
+
keepAlive: options.keepAlive,
|
|
660
|
+
});
|
|
661
|
+
console.log(`\n🚀 difit server started on ${url}`);
|
|
662
|
+
console.log(`📋 Reviewing: ${commitish}`);
|
|
663
|
+
if (options.keepAlive) {
|
|
664
|
+
console.log('🔒 Keep-alive mode: server will stay running after browser disconnects');
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
await program.parseAsync([], { from: 'user' });
|
|
668
|
+
expect(mockStartServer).toHaveBeenCalledWith(expect.objectContaining({
|
|
669
|
+
keepAlive: undefined,
|
|
670
|
+
}));
|
|
671
|
+
expect(console.log).not.toHaveBeenCalledWith('🔒 Keep-alive mode: server will stay running after browser disconnects');
|
|
672
|
+
});
|
|
673
|
+
});
|
|
587
674
|
describe('Console output', () => {
|
|
588
675
|
it('displays server startup message with correct URL', async () => {
|
|
589
676
|
mockFindUntrackedFiles.mockResolvedValue([]);
|
package/dist/cli/utils.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { type Stats } from 'node:fs';
|
|
2
2
|
import type { SimpleGit } from 'simple-git';
|
|
3
3
|
type StdinStat = Pick<Stats, 'isFIFO' | 'isFile' | 'isSocket'>;
|
|
4
|
-
|
|
4
|
+
type StdinSource = 'pipe' | 'file' | 'socket' | 'tty';
|
|
5
5
|
export declare function detectStdinSource(stdinStat?: StdinStat): StdinSource;
|
|
6
|
-
|
|
6
|
+
interface ShouldReadStdinOptions {
|
|
7
7
|
commitish: string;
|
|
8
8
|
hasPositionalArgs: boolean;
|
|
9
9
|
hasPrOption: boolean;
|
|
@@ -15,17 +15,14 @@ export declare function getGitRoot(): string;
|
|
|
15
15
|
export declare function validateCommitish(commitish: string): boolean;
|
|
16
16
|
export declare function shortHash(hash: string): string;
|
|
17
17
|
export declare function createCommitRangeString(baseHash: string, targetHash: string): string;
|
|
18
|
-
|
|
18
|
+
interface PullRequestInfo {
|
|
19
19
|
owner: string;
|
|
20
20
|
repo: string;
|
|
21
21
|
pullNumber: number;
|
|
22
22
|
hostname: string;
|
|
23
23
|
}
|
|
24
24
|
export declare function parseGitHubPrUrl(url: string): PullRequestInfo | null;
|
|
25
|
-
export declare function
|
|
26
|
-
targetCommitish: string;
|
|
27
|
-
baseCommitish: string;
|
|
28
|
-
}>;
|
|
25
|
+
export declare function getPrPatch(prArg: string): string;
|
|
29
26
|
export declare function validateDiffArguments(targetCommitish: string, baseCommitish?: string): {
|
|
30
27
|
valid: boolean;
|
|
31
28
|
error?: string;
|
package/dist/cli/utils.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { execSync } from 'child_process';
|
|
1
|
+
import { execFileSync, execSync } from 'child_process';
|
|
2
2
|
import { fstatSync } from 'node:fs';
|
|
3
3
|
import { createInterface } from 'readline/promises';
|
|
4
|
-
import { Octokit } from '@octokit/rest';
|
|
5
4
|
export function detectStdinSource(stdinStat = fstatSync(0)) {
|
|
6
5
|
if (stdinStat.isFIFO()) {
|
|
7
6
|
return 'pipe';
|
|
@@ -122,107 +121,27 @@ export function parseGitHubPrUrl(url) {
|
|
|
122
121
|
return null;
|
|
123
122
|
}
|
|
124
123
|
}
|
|
125
|
-
function
|
|
126
|
-
// Try to get token from environment variable first
|
|
127
|
-
if (process.env.GITHUB_TOKEN) {
|
|
128
|
-
return process.env.GITHUB_TOKEN;
|
|
129
|
-
}
|
|
130
|
-
// Try to get token from GitHub CLI
|
|
124
|
+
export function getPrPatch(prArg) {
|
|
131
125
|
try {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
catch {
|
|
136
|
-
// GitHub CLI not available or not authenticated
|
|
137
|
-
return undefined;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
async function fetchPrDetails(prInfo) {
|
|
141
|
-
const token = getGitHubToken();
|
|
142
|
-
const octokitOptions = {
|
|
143
|
-
auth: token,
|
|
144
|
-
};
|
|
145
|
-
// For GitHub Enterprise, set the base URL
|
|
146
|
-
if (prInfo.hostname !== 'github.com') {
|
|
147
|
-
octokitOptions.baseUrl = `https://${prInfo.hostname}/api/v3`;
|
|
148
|
-
}
|
|
149
|
-
const octokit = new Octokit(octokitOptions);
|
|
150
|
-
try {
|
|
151
|
-
const { data: pr } = await octokit.rest.pulls.get({
|
|
152
|
-
owner: prInfo.owner,
|
|
153
|
-
repo: prInfo.repo,
|
|
154
|
-
pull_number: prInfo.pullNumber,
|
|
126
|
+
const patch = execFileSync('gh', ['pr', 'diff', prArg, '--patch'], {
|
|
127
|
+
encoding: 'utf8',
|
|
128
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
155
129
|
});
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
headSha: pr.head.sha,
|
|
159
|
-
baseRef: pr.base.ref,
|
|
160
|
-
headRef: pr.head.ref,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
catch (error) {
|
|
164
|
-
if (error instanceof Error) {
|
|
165
|
-
let authHint = '';
|
|
166
|
-
// Provide more specific error messages for authentication issues
|
|
167
|
-
if (error.message.includes('Bad credentials')) {
|
|
168
|
-
if (prInfo.hostname !== 'github.com') {
|
|
169
|
-
authHint = `\n\nFor GitHub Enterprise Server (${prInfo.hostname}):
|
|
170
|
-
1. Generate a token on YOUR Enterprise Server: https://${prInfo.hostname}/settings/tokens
|
|
171
|
-
2. Set it as GITHUB_TOKEN environment variable
|
|
172
|
-
3. Tokens from github.com will NOT work on Enterprise servers`;
|
|
173
|
-
}
|
|
174
|
-
else {
|
|
175
|
-
authHint = '\n\nTry: gh auth login or set GITHUB_TOKEN environment variable';
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
else if (!token) {
|
|
179
|
-
authHint = ' (Try: gh auth login or set GITHUB_TOKEN environment variable)';
|
|
180
|
-
}
|
|
181
|
-
throw new Error(`Failed to fetch PR details: ${error.message}${authHint}`);
|
|
182
|
-
}
|
|
183
|
-
throw new Error('Failed to fetch PR details: Unknown error');
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
function resolveCommitInLocalRepo(sha, context) {
|
|
187
|
-
try {
|
|
188
|
-
// Verify if the commit exists locally
|
|
189
|
-
execSync(`git cat-file -e ${sha}`, { stdio: 'ignore' });
|
|
190
|
-
return sha;
|
|
191
|
-
}
|
|
192
|
-
catch {
|
|
193
|
-
// If commit doesn't exist, try to fetch from remote
|
|
194
|
-
try {
|
|
195
|
-
execSync('git fetch origin', { stdio: 'ignore' });
|
|
196
|
-
execSync(`git cat-file -e ${sha}`, { stdio: 'ignore' });
|
|
197
|
-
return sha;
|
|
198
|
-
}
|
|
199
|
-
catch {
|
|
200
|
-
const errorMessage = [
|
|
201
|
-
`Commit ${sha} not found in local repository.`,
|
|
202
|
-
'',
|
|
203
|
-
'Common causes:',
|
|
204
|
-
' • Are you running this command in the correct repository directory?',
|
|
205
|
-
context ? ` • Expected repository: ${context.owner}/${context.repo}` : '',
|
|
206
|
-
' • Is this PR from a fork?',
|
|
207
|
-
' • Try: git remote add upstream <original-repo-url> && git fetch upstream',
|
|
208
|
-
' • Try: git fetch --all to fetch from all remotes',
|
|
209
|
-
]
|
|
210
|
-
.filter(Boolean)
|
|
211
|
-
.join('\n');
|
|
212
|
-
throw new Error(errorMessage);
|
|
130
|
+
if (!patch.trim()) {
|
|
131
|
+
throw new Error('No patch content returned from gh pr diff --patch');
|
|
213
132
|
}
|
|
133
|
+
return patch;
|
|
214
134
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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`);
|
|
220
144
|
}
|
|
221
|
-
const prDetails = await fetchPrDetails(prInfo);
|
|
222
|
-
const context = { owner: prInfo.owner, repo: prInfo.repo };
|
|
223
|
-
const targetCommitish = resolveCommitInLocalRepo(prDetails.headSha, context);
|
|
224
|
-
const baseCommitish = resolveCommitInLocalRepo(prDetails.baseSha, context);
|
|
225
|
-
return { targetCommitish, baseCommitish };
|
|
226
145
|
}
|
|
227
146
|
export function validateDiffArguments(targetCommitish, baseCommitish) {
|
|
228
147
|
// Validate target commitish format
|