difit 1.1.4 → 1.1.6
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/LICENSE +21 -0
- package/README.md +29 -27
- package/dist/cli/index.js +34 -7
- package/dist/cli/utils.d.ts +21 -0
- package/dist/cli/utils.js +121 -7
- package/dist/cli/utils.test.js +65 -1
- package/dist/client/assets/{index-GkNEbjZ7.js → index-B2jj_GBb.js} +21 -21
- package/dist/client/assets/index-CUdTIauN.css +1 -0
- package/dist/client/assets/{prism-java-C-dW1Z2z.js → prism-java-BKNDnDw7.js} +1 -1
- package/dist/client/assets/{prism-php-CRAMsZfC.js → prism-php-Dxvk8j_J.js} +1 -1
- package/dist/client/assets/{prism-ruby-CfFUi7wW.js → prism-ruby-kawEeFmo.js} +1 -1
- package/dist/client/index.html +2 -2
- package/package.json +2 -1
- package/dist/client/assets/index-BKXkeB0O.css +0 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 reviewit
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -8,14 +8,9 @@ A lightweight command-line tool that spins up a local web server to display Git
|
|
|
8
8
|
|
|
9
9
|
## ✨ Features
|
|
10
10
|
|
|
11
|
-
- 🌙 **GitHub-like UI**: Familiar dark theme file list and diff interface
|
|
12
|
-
- 💬 **Inline Comments**: Add comments to specific lines and generate Claude Code prompts
|
|
13
|
-
- 🔄 **Side-by-Side & Inline Views**: Choose your preferred diff viewing mode
|
|
14
|
-
- 🖥️ **Terminal UI Mode**: View diffs directly in your terminal with `--tui` flag
|
|
15
11
|
- ⚡ **Zero Config**: Just run `npx reviewit <commit>` and it works
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
- 🎨 **Syntax Highlighting**: Dynamic language loading for Bash, PHP, SQL, Ruby, Java, and more
|
|
12
|
+
- 🌙 **Review for AI**: Add comments and generate Claude Code prompts
|
|
13
|
+
- 🖥️ **Terminal UI**: View diffs directly in terminal with `--tui`
|
|
19
14
|
|
|
20
15
|
## ⚡ Quick Start
|
|
21
16
|
|
|
@@ -30,6 +25,7 @@ npx reviewit # View HEAD commit changes in a beautiful diff viewer
|
|
|
30
25
|
```bash
|
|
31
26
|
npx reviewit <commit-ish> # View single commit diff
|
|
32
27
|
npx reviewit <commit-ish> [compare-with] # Compare two commits/branches
|
|
28
|
+
npx reviewit --pr <github-pr-url> # Review GitHub pull request
|
|
33
29
|
```
|
|
34
30
|
|
|
35
31
|
### Single commit review
|
|
@@ -65,16 +61,29 @@ npx reviewit working # Unstaged changes only (cannot use compare-with)
|
|
|
65
61
|
| `staged` | Shows staged changes ready to be committed | ✅ Yes |
|
|
66
62
|
| `working` | Shows unstaged changes in your working directory | ❌ No |
|
|
67
63
|
|
|
68
|
-
###
|
|
64
|
+
### GitHub PR
|
|
69
65
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
66
|
+
```bash
|
|
67
|
+
npx reviewit --pr https://github.com/owner/repo/pull/123
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
ReviewIt automatically handles GitHub authentication using:
|
|
71
|
+
|
|
72
|
+
1. **GitHub CLI** (recommended): If you're logged in with `gh auth login`, ReviewIt uses your existing credentials
|
|
73
|
+
2. **Environment Variable**: Set `GITHUB_TOKEN` environment variable
|
|
74
|
+
3. **No Authentication**: Public repositories work without authentication (rate-limited)
|
|
75
|
+
|
|
76
|
+
## ⚙️ CLI Options
|
|
77
|
+
|
|
78
|
+
| Flag | Default | Description |
|
|
79
|
+
| ---------------- | ------------ | ---------------------------------------------------------------------- |
|
|
80
|
+
| `<commit-ish>` | HEAD | Any Git reference: hash, tag, HEAD~n, branch, or Special Arguments |
|
|
81
|
+
| `[compare-with]` | (optional) | Optional second commit to compare with (shows diff between the two) |
|
|
82
|
+
| `--pr <url>` | - | GitHub PR URL to review (e.g., https://github.com/owner/repo/pull/123) |
|
|
83
|
+
| `--port` | auto | Preferred port; falls back if occupied |
|
|
84
|
+
| `--no-open` | false | Don't automatically open browser |
|
|
85
|
+
| `--mode` | side-by-side | Diff mode: `inline` or `side-by-side` |
|
|
86
|
+
| `--tui` | false | Use terminal UI mode instead of web interface |
|
|
78
87
|
|
|
79
88
|
## 💬 Comment System
|
|
80
89
|
|
|
@@ -88,10 +97,9 @@ ReviewIt includes an inline commenting system that integrates with Claude Code:
|
|
|
88
97
|
|
|
89
98
|
### Comment Prompt Format
|
|
90
99
|
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
Comment: This function name should probably be more specific
|
|
100
|
+
```sh
|
|
101
|
+
src/components/Button.tsx:42 # Automatically added this line
|
|
102
|
+
This name should probably be more specific.
|
|
95
103
|
```
|
|
96
104
|
|
|
97
105
|
## 🎨 Syntax Highlighting
|
|
@@ -150,19 +158,13 @@ pnpm run typecheck
|
|
|
150
158
|
|
|
151
159
|
- **CLI**: Commander.js for argument parsing with comprehensive validation
|
|
152
160
|
- **Backend**: Express server with simple-git for diff processing
|
|
161
|
+
- **GitHub Integration**: Octokit for GitHub API with automatic authentication (GitHub CLI + env vars)
|
|
153
162
|
- **Frontend**: React 18 + TypeScript + Vite
|
|
154
163
|
- **Styling**: Tailwind CSS v4 with GitHub-like dark theme
|
|
155
164
|
- **Syntax Highlighting**: Prism.js with dynamic language loading
|
|
156
165
|
- **Testing**: Vitest for unit tests with co-located test files
|
|
157
166
|
- **Quality**: ESLint, Prettier, lefthook pre-commit hooks
|
|
158
167
|
|
|
159
|
-
### Key Components
|
|
160
|
-
|
|
161
|
-
- **Validation System**: Unified validation logic for CLI arguments with comprehensive error handling
|
|
162
|
-
- **Dual Parameter System**: Internal refactoring splits commitish into targetCommitish and baseCommitish for flexibility
|
|
163
|
-
- **Special Argument Support**: Working directory, staging area, and uncommitted changes detection
|
|
164
|
-
- **Hash Utilities**: Consistent short hash generation for commit display
|
|
165
|
-
|
|
166
168
|
## 📋 Requirements
|
|
167
169
|
|
|
168
170
|
- Node.js ≥ 21.0.0
|
package/dist/cli/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { Command } from 'commander';
|
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import pkg from '../../package.json' with { type: 'json' };
|
|
5
5
|
import { startServer } from '../server/server.js';
|
|
6
|
-
import { validateDiffArguments } from './utils.js';
|
|
6
|
+
import { validateDiffArguments, resolvePrCommits } from './utils.js';
|
|
7
7
|
function isSpecialArg(arg) {
|
|
8
8
|
return arg === 'working' || arg === 'staged' || arg === '.';
|
|
9
9
|
}
|
|
@@ -18,18 +18,42 @@ program
|
|
|
18
18
|
.option('--no-open', 'do not automatically open browser')
|
|
19
19
|
.option('--mode <mode>', 'diff mode (inline only for now)', 'inline')
|
|
20
20
|
.option('--tui', 'use terminal UI instead of web interface')
|
|
21
|
+
.option('--pr <url>', 'GitHub PR URL to review (e.g., https://github.com/owner/repo/pull/123)')
|
|
21
22
|
.action(async (commitish, compareWith, options) => {
|
|
22
23
|
try {
|
|
23
24
|
// Determine target and base commitish
|
|
24
25
|
let targetCommitish = commitish;
|
|
25
26
|
let baseCommitish;
|
|
26
|
-
|
|
27
|
+
// Handle PR URL option
|
|
28
|
+
if (options.pr) {
|
|
29
|
+
if (commitish !== 'HEAD' || compareWith) {
|
|
30
|
+
console.error('Error: --pr option cannot be used with positional arguments');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const prCommits = await resolvePrCommits(options.pr);
|
|
35
|
+
targetCommitish = prCommits.targetCommitish;
|
|
36
|
+
baseCommitish = prCommits.baseCommitish;
|
|
37
|
+
console.log(`📋 Reviewing PR: ${options.pr}`);
|
|
38
|
+
console.log(`🎯 Target commit: ${targetCommitish.substring(0, 7)}`);
|
|
39
|
+
console.log(`📍 Base commit: ${baseCommitish.substring(0, 7)}`);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
console.error(`Error resolving PR: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else if (compareWith) {
|
|
27
47
|
// If compareWith is provided, use it as base
|
|
28
48
|
baseCommitish = compareWith;
|
|
29
49
|
}
|
|
30
50
|
else {
|
|
31
51
|
// Handle special arguments
|
|
32
|
-
if (
|
|
52
|
+
if (commitish === 'working') {
|
|
53
|
+
// working compares working directory with staging area
|
|
54
|
+
baseCommitish = 'staged';
|
|
55
|
+
}
|
|
56
|
+
else if (isSpecialArg(commitish)) {
|
|
33
57
|
baseCommitish = 'HEAD';
|
|
34
58
|
}
|
|
35
59
|
else {
|
|
@@ -49,10 +73,13 @@ program
|
|
|
49
73
|
render(React.createElement(TuiApp, { targetCommitish, baseCommitish }));
|
|
50
74
|
return;
|
|
51
75
|
}
|
|
52
|
-
|
|
53
|
-
if (!
|
|
54
|
-
|
|
55
|
-
|
|
76
|
+
// Skip validation for PR URLs as they're already resolved to valid commits
|
|
77
|
+
if (!options.pr) {
|
|
78
|
+
const validation = validateDiffArguments(targetCommitish, compareWith);
|
|
79
|
+
if (!validation.valid) {
|
|
80
|
+
console.error(`Error: ${validation.error}`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
56
83
|
}
|
|
57
84
|
const { url, port } = await startServer({
|
|
58
85
|
targetCommitish,
|
package/dist/cli/utils.d.ts
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
export declare function validateCommitish(commitish: string): boolean;
|
|
2
2
|
export declare function shortHash(hash: string): string;
|
|
3
3
|
export declare function createCommitRangeString(baseHash: string, targetHash: string): string;
|
|
4
|
+
export interface PullRequestInfo {
|
|
5
|
+
owner: string;
|
|
6
|
+
repo: string;
|
|
7
|
+
pullNumber: number;
|
|
8
|
+
}
|
|
9
|
+
export interface PullRequestDetails {
|
|
10
|
+
baseSha: string;
|
|
11
|
+
headSha: string;
|
|
12
|
+
baseRef: string;
|
|
13
|
+
headRef: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function parseGitHubPrUrl(url: string): PullRequestInfo | null;
|
|
16
|
+
export declare function fetchPrDetails(prInfo: PullRequestInfo): Promise<PullRequestDetails>;
|
|
17
|
+
export declare function resolveCommitInLocalRepo(sha: string, context?: {
|
|
18
|
+
owner: string;
|
|
19
|
+
repo: string;
|
|
20
|
+
}): string;
|
|
21
|
+
export declare function resolvePrCommits(prUrl: string): Promise<{
|
|
22
|
+
targetCommitish: string;
|
|
23
|
+
baseCommitish: string;
|
|
24
|
+
}>;
|
|
4
25
|
export declare function validateDiffArguments(targetCommitish: string, baseCommitish?: string): {
|
|
5
26
|
valid: boolean;
|
|
6
27
|
error?: string;
|
package/dist/cli/utils.js
CHANGED
|
@@ -23,9 +23,117 @@ export function validateCommitish(commitish) {
|
|
|
23
23
|
export function shortHash(hash) {
|
|
24
24
|
return hash.substring(0, 7);
|
|
25
25
|
}
|
|
26
|
+
import { execSync } from 'child_process';
|
|
27
|
+
import { Octokit } from '@octokit/rest';
|
|
26
28
|
export function createCommitRangeString(baseHash, targetHash) {
|
|
27
29
|
return `${baseHash}...${targetHash}`;
|
|
28
30
|
}
|
|
31
|
+
export function parseGitHubPrUrl(url) {
|
|
32
|
+
try {
|
|
33
|
+
const urlObj = new URL(url);
|
|
34
|
+
if (urlObj.hostname !== 'github.com') {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const pathParts = urlObj.pathname.split('/').filter(Boolean);
|
|
38
|
+
if (pathParts.length < 4 || pathParts[2] !== 'pull') {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const owner = pathParts[0];
|
|
42
|
+
const repo = pathParts[1];
|
|
43
|
+
const pullNumber = parseInt(pathParts[3], 10);
|
|
44
|
+
if (isNaN(pullNumber)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return { owner, repo, pullNumber };
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function getGitHubToken() {
|
|
54
|
+
// Try to get token from environment variable first
|
|
55
|
+
if (process.env.GITHUB_TOKEN) {
|
|
56
|
+
return process.env.GITHUB_TOKEN;
|
|
57
|
+
}
|
|
58
|
+
// Try to get token from GitHub CLI
|
|
59
|
+
try {
|
|
60
|
+
const result = execSync('gh auth token', { encoding: 'utf8', stdio: 'pipe' });
|
|
61
|
+
return result.trim();
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// GitHub CLI not available or not authenticated
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export async function fetchPrDetails(prInfo) {
|
|
69
|
+
const token = getGitHubToken();
|
|
70
|
+
const octokit = new Octokit({
|
|
71
|
+
auth: token,
|
|
72
|
+
});
|
|
73
|
+
try {
|
|
74
|
+
const { data: pr } = await octokit.rest.pulls.get({
|
|
75
|
+
owner: prInfo.owner,
|
|
76
|
+
repo: prInfo.repo,
|
|
77
|
+
pull_number: prInfo.pullNumber,
|
|
78
|
+
});
|
|
79
|
+
return {
|
|
80
|
+
baseSha: pr.base.sha,
|
|
81
|
+
headSha: pr.head.sha,
|
|
82
|
+
baseRef: pr.base.ref,
|
|
83
|
+
headRef: pr.head.ref,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (error instanceof Error) {
|
|
88
|
+
const authHint = token
|
|
89
|
+
? ''
|
|
90
|
+
: ' (Try: gh auth login or set GITHUB_TOKEN environment variable)';
|
|
91
|
+
throw new Error(`Failed to fetch PR details: ${error.message}${authHint}`);
|
|
92
|
+
}
|
|
93
|
+
throw new Error('Failed to fetch PR details: Unknown error');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export function resolveCommitInLocalRepo(sha, context) {
|
|
97
|
+
try {
|
|
98
|
+
// Verify if the commit exists locally
|
|
99
|
+
execSync(`git cat-file -e ${sha}`, { stdio: 'ignore' });
|
|
100
|
+
return sha;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// If commit doesn't exist, try to fetch from remote
|
|
104
|
+
try {
|
|
105
|
+
execSync('git fetch origin', { stdio: 'ignore' });
|
|
106
|
+
execSync(`git cat-file -e ${sha}`, { stdio: 'ignore' });
|
|
107
|
+
return sha;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
const errorMessage = [
|
|
111
|
+
`Commit ${sha} not found in local repository.`,
|
|
112
|
+
'',
|
|
113
|
+
'Common causes:',
|
|
114
|
+
' • Are you running this command in the correct repository directory?',
|
|
115
|
+
context ? ` • Expected repository: ${context.owner}/${context.repo}` : '',
|
|
116
|
+
' • Is this PR from a fork?',
|
|
117
|
+
' • Try: git remote add upstream <original-repo-url> && git fetch upstream',
|
|
118
|
+
' • Try: git fetch --all to fetch from all remotes',
|
|
119
|
+
]
|
|
120
|
+
.filter(Boolean)
|
|
121
|
+
.join('\n');
|
|
122
|
+
throw new Error(errorMessage);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
export async function resolvePrCommits(prUrl) {
|
|
127
|
+
const prInfo = parseGitHubPrUrl(prUrl);
|
|
128
|
+
if (!prInfo) {
|
|
129
|
+
throw new Error('Invalid GitHub PR URL format. Expected: https://github.com/owner/repo/pull/123');
|
|
130
|
+
}
|
|
131
|
+
const prDetails = await fetchPrDetails(prInfo);
|
|
132
|
+
const context = { owner: prInfo.owner, repo: prInfo.repo };
|
|
133
|
+
const targetCommitish = resolveCommitInLocalRepo(prDetails.headSha, context);
|
|
134
|
+
const baseCommitish = resolveCommitInLocalRepo(prDetails.baseSha, context);
|
|
135
|
+
return { targetCommitish, baseCommitish };
|
|
136
|
+
}
|
|
29
137
|
export function validateDiffArguments(targetCommitish, baseCommitish) {
|
|
30
138
|
// Validate target commitish format
|
|
31
139
|
if (!validateCommitish(targetCommitish)) {
|
|
@@ -35,20 +143,26 @@ export function validateDiffArguments(targetCommitish, baseCommitish) {
|
|
|
35
143
|
if (baseCommitish !== undefined && !validateCommitish(baseCommitish)) {
|
|
36
144
|
return { valid: false, error: 'Invalid base commit-ish format' };
|
|
37
145
|
}
|
|
38
|
-
// Special arguments are only allowed in target, not base
|
|
146
|
+
// Special arguments are only allowed in target, not base (except staged with working)
|
|
39
147
|
const specialArgs = ['working', 'staged', '.'];
|
|
40
148
|
if (baseCommitish && specialArgs.includes(baseCommitish)) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
149
|
+
// Allow 'staged' as base only when target is 'working'
|
|
150
|
+
if (baseCommitish === 'staged' && targetCommitish === 'working') {
|
|
151
|
+
// This is valid: working vs staged
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
return {
|
|
155
|
+
valid: false,
|
|
156
|
+
error: `Special arguments (working, staged, .) are only allowed as target, not base. Got base: ${baseCommitish}`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
45
159
|
}
|
|
46
160
|
// Cannot compare same values
|
|
47
161
|
if (targetCommitish === baseCommitish) {
|
|
48
162
|
return { valid: false, error: `Cannot compare ${targetCommitish} with itself` };
|
|
49
163
|
}
|
|
50
|
-
// "working" shows unstaged changes and
|
|
51
|
-
if (targetCommitish === 'working' && baseCommitish) {
|
|
164
|
+
// "working" shows unstaged changes and can only be compared with staging area
|
|
165
|
+
if (targetCommitish === 'working' && baseCommitish && baseCommitish !== 'staged') {
|
|
52
166
|
return {
|
|
53
167
|
valid: false,
|
|
54
168
|
error: '"working" shows unstaged changes and cannot be compared with another commit. Use "." instead to compare all uncommitted changes with a specific commit.',
|
package/dist/cli/utils.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { validateCommitish, validateDiffArguments, shortHash } from './utils';
|
|
2
|
+
import { validateCommitish, validateDiffArguments, shortHash, parseGitHubPrUrl } from './utils';
|
|
3
3
|
describe('CLI Utils', () => {
|
|
4
4
|
describe('validateCommitish', () => {
|
|
5
5
|
it('should validate full SHA hashes', () => {
|
|
@@ -73,6 +73,12 @@ describe('CLI Utils', () => {
|
|
|
73
73
|
expect(validateDiffArguments('staged', 'HEAD')).toEqual({ valid: true });
|
|
74
74
|
expect(validateDiffArguments('.', 'main')).toEqual({ valid: true });
|
|
75
75
|
});
|
|
76
|
+
it('should allow staged as base only with working target', () => {
|
|
77
|
+
expect(validateDiffArguments('working', 'staged')).toEqual({ valid: true });
|
|
78
|
+
const result = validateDiffArguments('HEAD', 'staged');
|
|
79
|
+
expect(result.valid).toBe(false);
|
|
80
|
+
expect(result.error).toBe('Special arguments (working, staged, .) are only allowed as target, not base. Got base: staged');
|
|
81
|
+
});
|
|
76
82
|
});
|
|
77
83
|
describe('same value comparison', () => {
|
|
78
84
|
it('should reject same target and base values', () => {
|
|
@@ -97,6 +103,17 @@ describe('CLI Utils', () => {
|
|
|
97
103
|
it('should allow working without compareWith', () => {
|
|
98
104
|
expect(validateDiffArguments('working')).toEqual({ valid: true });
|
|
99
105
|
});
|
|
106
|
+
it('should allow working with staged', () => {
|
|
107
|
+
expect(validateDiffArguments('working', 'staged')).toEqual({ valid: true });
|
|
108
|
+
});
|
|
109
|
+
it('should reject working with other commits', () => {
|
|
110
|
+
const result1 = validateDiffArguments('working', 'main');
|
|
111
|
+
expect(result1.valid).toBe(false);
|
|
112
|
+
expect(result1.error).toBe('"working" shows unstaged changes and cannot be compared with another commit. Use "." instead to compare all uncommitted changes with a specific commit.');
|
|
113
|
+
const result2 = validateDiffArguments('working', 'abc123');
|
|
114
|
+
expect(result2.valid).toBe(false);
|
|
115
|
+
expect(result2.error).toBe('"working" shows unstaged changes and cannot be compared with another commit. Use "." instead to compare all uncommitted changes with a specific commit.');
|
|
116
|
+
});
|
|
100
117
|
it('should allow other special args with compareWith', () => {
|
|
101
118
|
expect(validateDiffArguments('staged', 'HEAD')).toEqual({ valid: true });
|
|
102
119
|
expect(validateDiffArguments('.', 'main')).toEqual({ valid: true });
|
|
@@ -127,4 +144,51 @@ describe('CLI Utils', () => {
|
|
|
127
144
|
expect(shortHash('')).toBe('');
|
|
128
145
|
});
|
|
129
146
|
});
|
|
147
|
+
describe('parseGitHubPrUrl', () => {
|
|
148
|
+
it('should parse valid GitHub PR URLs', () => {
|
|
149
|
+
const result = parseGitHubPrUrl('https://github.com/owner/repo/pull/123');
|
|
150
|
+
expect(result).toEqual({
|
|
151
|
+
owner: 'owner',
|
|
152
|
+
repo: 'repo',
|
|
153
|
+
pullNumber: 123,
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
it('should parse GitHub PR URLs with additional path segments', () => {
|
|
157
|
+
const result = parseGitHubPrUrl('https://github.com/owner/repo/pull/456/files');
|
|
158
|
+
expect(result).toEqual({
|
|
159
|
+
owner: 'owner',
|
|
160
|
+
repo: 'repo',
|
|
161
|
+
pullNumber: 456,
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
it('should parse GitHub PR URLs with query parameters', () => {
|
|
165
|
+
const result = parseGitHubPrUrl('https://github.com/owner/repo/pull/789?tab=files');
|
|
166
|
+
expect(result).toEqual({
|
|
167
|
+
owner: 'owner',
|
|
168
|
+
repo: 'repo',
|
|
169
|
+
pullNumber: 789,
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
it('should handle URLs with hyphens and underscores in owner/repo names', () => {
|
|
173
|
+
const result = parseGitHubPrUrl('https://github.com/owner-name/repo_name/pull/123');
|
|
174
|
+
expect(result).toEqual({
|
|
175
|
+
owner: 'owner-name',
|
|
176
|
+
repo: 'repo_name',
|
|
177
|
+
pullNumber: 123,
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
it('should return null for invalid URLs', () => {
|
|
181
|
+
expect(parseGitHubPrUrl('not-a-url')).toBe(null);
|
|
182
|
+
expect(parseGitHubPrUrl('https://example.com/owner/repo/pull/123')).toBe(null);
|
|
183
|
+
expect(parseGitHubPrUrl('https://github.com/owner/repo/issues/123')).toBe(null);
|
|
184
|
+
expect(parseGitHubPrUrl('https://github.com/owner/repo')).toBe(null);
|
|
185
|
+
expect(parseGitHubPrUrl('https://github.com/owner/repo/pull/abc')).toBe(null);
|
|
186
|
+
});
|
|
187
|
+
it('should handle malformed URLs gracefully', () => {
|
|
188
|
+
expect(parseGitHubPrUrl('')).toBe(null);
|
|
189
|
+
expect(parseGitHubPrUrl('https://github.com')).toBe(null);
|
|
190
|
+
expect(parseGitHubPrUrl('https://github.com/owner')).toBe(null);
|
|
191
|
+
expect(parseGitHubPrUrl('https://github.com/owner/repo/pull')).toBe(null);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
130
194
|
});
|