difit 1.1.5 → 1.1.7
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.md +30 -28
- package/dist/cli/index.js +34 -7
- package/dist/cli/utils.d.ts +21 -0
- package/dist/cli/utils.js +123 -7
- package/dist/cli/utils.test.js +85 -1
- package/dist/client/assets/{index-B2jj_GBb.js → index-BtXScDaj.js} +36 -36
- package/dist/client/assets/{prism-java-BKNDnDw7.js → prism-java-BnGaC8QK.js} +1 -1
- package/dist/client/assets/{prism-php-Dxvk8j_J.js → prism-php-BghhkkC7.js} +1 -1
- package/dist/client/assets/{prism-ruby-kawEeFmo.js → prism-ruby-CubCUQWk.js} +1 -1
- package/dist/client/assets/prism-scala-BjNo2HkN.js +1 -0
- package/dist/client/index.html +1 -1
- package/package.json +2 -1
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
|
|
@@ -103,7 +111,7 @@ ReviewIt supports syntax highlighting for multiple programming languages with dy
|
|
|
103
111
|
- **JavaScript/TypeScript**: `.js`, `.jsx`, `.ts`, `.tsx`
|
|
104
112
|
- **Web Technologies**: HTML, CSS, JSON, XML, Markdown
|
|
105
113
|
- **Shell Scripts**: `.sh`, `.bash`, `.zsh`, `.fish` files
|
|
106
|
-
- **Backend Languages**: PHP, SQL, Ruby, Java
|
|
114
|
+
- **Backend Languages**: PHP, SQL, Ruby, Java, Scala
|
|
107
115
|
- **Systems Languages**: C, C++, Rust, Go
|
|
108
116
|
- **Others**: Python, Swift, Kotlin, YAML
|
|
109
117
|
|
|
@@ -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
|
@@ -15,6 +15,8 @@ export function validateCommitish(commitish) {
|
|
|
15
15
|
}
|
|
16
16
|
const validPatterns = [
|
|
17
17
|
/^[a-f0-9]{4,40}$/i, // SHA hashes
|
|
18
|
+
/^[a-f0-9]{4,40}\^+$/i, // SHA hashes with ^ suffix (parent references)
|
|
19
|
+
/^[a-f0-9]{4,40}~\d+$/i, // SHA hashes with ~N suffix (ancestor references)
|
|
18
20
|
/^HEAD(~\d+|\^\d*)*$/, // HEAD, HEAD~1, HEAD^, HEAD^2, etc.
|
|
19
21
|
/^[a-zA-Z][a-zA-Z0-9_\-/.]*$/, // branch names, tags (must start with letter, no ^ or ~ in middle)
|
|
20
22
|
];
|
|
@@ -23,9 +25,117 @@ export function validateCommitish(commitish) {
|
|
|
23
25
|
export function shortHash(hash) {
|
|
24
26
|
return hash.substring(0, 7);
|
|
25
27
|
}
|
|
28
|
+
import { execSync } from 'child_process';
|
|
29
|
+
import { Octokit } from '@octokit/rest';
|
|
26
30
|
export function createCommitRangeString(baseHash, targetHash) {
|
|
27
31
|
return `${baseHash}...${targetHash}`;
|
|
28
32
|
}
|
|
33
|
+
export function parseGitHubPrUrl(url) {
|
|
34
|
+
try {
|
|
35
|
+
const urlObj = new URL(url);
|
|
36
|
+
if (urlObj.hostname !== 'github.com') {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const pathParts = urlObj.pathname.split('/').filter(Boolean);
|
|
40
|
+
if (pathParts.length < 4 || pathParts[2] !== 'pull') {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const owner = pathParts[0];
|
|
44
|
+
const repo = pathParts[1];
|
|
45
|
+
const pullNumber = parseInt(pathParts[3], 10);
|
|
46
|
+
if (isNaN(pullNumber)) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return { owner, repo, pullNumber };
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function getGitHubToken() {
|
|
56
|
+
// Try to get token from environment variable first
|
|
57
|
+
if (process.env.GITHUB_TOKEN) {
|
|
58
|
+
return process.env.GITHUB_TOKEN;
|
|
59
|
+
}
|
|
60
|
+
// Try to get token from GitHub CLI
|
|
61
|
+
try {
|
|
62
|
+
const result = execSync('gh auth token', { encoding: 'utf8', stdio: 'pipe' });
|
|
63
|
+
return result.trim();
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// GitHub CLI not available or not authenticated
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export async function fetchPrDetails(prInfo) {
|
|
71
|
+
const token = getGitHubToken();
|
|
72
|
+
const octokit = new Octokit({
|
|
73
|
+
auth: token,
|
|
74
|
+
});
|
|
75
|
+
try {
|
|
76
|
+
const { data: pr } = await octokit.rest.pulls.get({
|
|
77
|
+
owner: prInfo.owner,
|
|
78
|
+
repo: prInfo.repo,
|
|
79
|
+
pull_number: prInfo.pullNumber,
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
baseSha: pr.base.sha,
|
|
83
|
+
headSha: pr.head.sha,
|
|
84
|
+
baseRef: pr.base.ref,
|
|
85
|
+
headRef: pr.head.ref,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (error instanceof Error) {
|
|
90
|
+
const authHint = token
|
|
91
|
+
? ''
|
|
92
|
+
: ' (Try: gh auth login or set GITHUB_TOKEN environment variable)';
|
|
93
|
+
throw new Error(`Failed to fetch PR details: ${error.message}${authHint}`);
|
|
94
|
+
}
|
|
95
|
+
throw new Error('Failed to fetch PR details: Unknown error');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export function resolveCommitInLocalRepo(sha, context) {
|
|
99
|
+
try {
|
|
100
|
+
// Verify if the commit exists locally
|
|
101
|
+
execSync(`git cat-file -e ${sha}`, { stdio: 'ignore' });
|
|
102
|
+
return sha;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// If commit doesn't exist, try to fetch from remote
|
|
106
|
+
try {
|
|
107
|
+
execSync('git fetch origin', { stdio: 'ignore' });
|
|
108
|
+
execSync(`git cat-file -e ${sha}`, { stdio: 'ignore' });
|
|
109
|
+
return sha;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
const errorMessage = [
|
|
113
|
+
`Commit ${sha} not found in local repository.`,
|
|
114
|
+
'',
|
|
115
|
+
'Common causes:',
|
|
116
|
+
' • Are you running this command in the correct repository directory?',
|
|
117
|
+
context ? ` • Expected repository: ${context.owner}/${context.repo}` : '',
|
|
118
|
+
' • Is this PR from a fork?',
|
|
119
|
+
' • Try: git remote add upstream <original-repo-url> && git fetch upstream',
|
|
120
|
+
' • Try: git fetch --all to fetch from all remotes',
|
|
121
|
+
]
|
|
122
|
+
.filter(Boolean)
|
|
123
|
+
.join('\n');
|
|
124
|
+
throw new Error(errorMessage);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
export async function resolvePrCommits(prUrl) {
|
|
129
|
+
const prInfo = parseGitHubPrUrl(prUrl);
|
|
130
|
+
if (!prInfo) {
|
|
131
|
+
throw new Error('Invalid GitHub PR URL format. Expected: https://github.com/owner/repo/pull/123');
|
|
132
|
+
}
|
|
133
|
+
const prDetails = await fetchPrDetails(prInfo);
|
|
134
|
+
const context = { owner: prInfo.owner, repo: prInfo.repo };
|
|
135
|
+
const targetCommitish = resolveCommitInLocalRepo(prDetails.headSha, context);
|
|
136
|
+
const baseCommitish = resolveCommitInLocalRepo(prDetails.baseSha, context);
|
|
137
|
+
return { targetCommitish, baseCommitish };
|
|
138
|
+
}
|
|
29
139
|
export function validateDiffArguments(targetCommitish, baseCommitish) {
|
|
30
140
|
// Validate target commitish format
|
|
31
141
|
if (!validateCommitish(targetCommitish)) {
|
|
@@ -35,20 +145,26 @@ export function validateDiffArguments(targetCommitish, baseCommitish) {
|
|
|
35
145
|
if (baseCommitish !== undefined && !validateCommitish(baseCommitish)) {
|
|
36
146
|
return { valid: false, error: 'Invalid base commit-ish format' };
|
|
37
147
|
}
|
|
38
|
-
// Special arguments are only allowed in target, not base
|
|
148
|
+
// Special arguments are only allowed in target, not base (except staged with working)
|
|
39
149
|
const specialArgs = ['working', 'staged', '.'];
|
|
40
150
|
if (baseCommitish && specialArgs.includes(baseCommitish)) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
151
|
+
// Allow 'staged' as base only when target is 'working'
|
|
152
|
+
if (baseCommitish === 'staged' && targetCommitish === 'working') {
|
|
153
|
+
// This is valid: working vs staged
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
return {
|
|
157
|
+
valid: false,
|
|
158
|
+
error: `Special arguments (working, staged, .) are only allowed as target, not base. Got base: ${baseCommitish}`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
45
161
|
}
|
|
46
162
|
// Cannot compare same values
|
|
47
163
|
if (targetCommitish === baseCommitish) {
|
|
48
164
|
return { valid: false, error: `Cannot compare ${targetCommitish} with itself` };
|
|
49
165
|
}
|
|
50
|
-
// "working" shows unstaged changes and
|
|
51
|
-
if (targetCommitish === 'working' && baseCommitish) {
|
|
166
|
+
// "working" shows unstaged changes and can only be compared with staging area
|
|
167
|
+
if (targetCommitish === 'working' && baseCommitish && baseCommitish !== 'staged') {
|
|
52
168
|
return {
|
|
53
169
|
valid: false,
|
|
54
170
|
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,11 +1,23 @@
|
|
|
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', () => {
|
|
6
6
|
expect(validateCommitish('a1b2c3d4e5f6789012345678901234567890abcd')).toBe(true);
|
|
7
7
|
expect(validateCommitish('abc123')).toBe(true);
|
|
8
8
|
});
|
|
9
|
+
it('should validate SHA hashes with parent references', () => {
|
|
10
|
+
expect(validateCommitish('a1b2c3d4e5f6789012345678901234567890abcd^')).toBe(true);
|
|
11
|
+
expect(validateCommitish('abc123^')).toBe(true);
|
|
12
|
+
expect(validateCommitish('abc123^^')).toBe(true);
|
|
13
|
+
expect(validateCommitish('bd4b7513e075b5b245284c38fd23427b9bd0f42e^')).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
it('should validate SHA hashes with ancestor references', () => {
|
|
16
|
+
expect(validateCommitish('a1b2c3d4e5f6789012345678901234567890abcd~1')).toBe(true);
|
|
17
|
+
expect(validateCommitish('abc123~5')).toBe(true);
|
|
18
|
+
expect(validateCommitish('abc123~10')).toBe(true);
|
|
19
|
+
expect(validateCommitish('bd4b7513e075b5b245284c38fd23427b9bd0f42e~2')).toBe(true);
|
|
20
|
+
});
|
|
9
21
|
it('should validate HEAD references', () => {
|
|
10
22
|
expect(validateCommitish('HEAD')).toBe(true);
|
|
11
23
|
expect(validateCommitish('HEAD~1')).toBe(true);
|
|
@@ -73,6 +85,12 @@ describe('CLI Utils', () => {
|
|
|
73
85
|
expect(validateDiffArguments('staged', 'HEAD')).toEqual({ valid: true });
|
|
74
86
|
expect(validateDiffArguments('.', 'main')).toEqual({ valid: true });
|
|
75
87
|
});
|
|
88
|
+
it('should allow staged as base only with working target', () => {
|
|
89
|
+
expect(validateDiffArguments('working', 'staged')).toEqual({ valid: true });
|
|
90
|
+
const result = validateDiffArguments('HEAD', 'staged');
|
|
91
|
+
expect(result.valid).toBe(false);
|
|
92
|
+
expect(result.error).toBe('Special arguments (working, staged, .) are only allowed as target, not base. Got base: staged');
|
|
93
|
+
});
|
|
76
94
|
});
|
|
77
95
|
describe('same value comparison', () => {
|
|
78
96
|
it('should reject same target and base values', () => {
|
|
@@ -97,6 +115,17 @@ describe('CLI Utils', () => {
|
|
|
97
115
|
it('should allow working without compareWith', () => {
|
|
98
116
|
expect(validateDiffArguments('working')).toEqual({ valid: true });
|
|
99
117
|
});
|
|
118
|
+
it('should allow working with staged', () => {
|
|
119
|
+
expect(validateDiffArguments('working', 'staged')).toEqual({ valid: true });
|
|
120
|
+
});
|
|
121
|
+
it('should reject working with other commits', () => {
|
|
122
|
+
const result1 = validateDiffArguments('working', 'main');
|
|
123
|
+
expect(result1.valid).toBe(false);
|
|
124
|
+
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.');
|
|
125
|
+
const result2 = validateDiffArguments('working', 'abc123');
|
|
126
|
+
expect(result2.valid).toBe(false);
|
|
127
|
+
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.');
|
|
128
|
+
});
|
|
100
129
|
it('should allow other special args with compareWith', () => {
|
|
101
130
|
expect(validateDiffArguments('staged', 'HEAD')).toEqual({ valid: true });
|
|
102
131
|
expect(validateDiffArguments('.', 'main')).toEqual({ valid: true });
|
|
@@ -114,6 +143,14 @@ describe('CLI Utils', () => {
|
|
|
114
143
|
valid: true,
|
|
115
144
|
});
|
|
116
145
|
});
|
|
146
|
+
it('should handle SHA hashes with parent/ancestor references', () => {
|
|
147
|
+
expect(validateDiffArguments('bd4b7513e075b5b245284c38fd23427b9bd0f42e^', 'abc123')).toEqual({ valid: true });
|
|
148
|
+
expect(validateDiffArguments('abc123', 'def456^')).toEqual({ valid: true });
|
|
149
|
+
expect(validateDiffArguments('abc123~1', 'def456~2')).toEqual({ valid: true });
|
|
150
|
+
expect(validateDiffArguments('a1b2c3d4e5f6789012345678901234567890abcd^', 'HEAD')).toEqual({
|
|
151
|
+
valid: true,
|
|
152
|
+
});
|
|
153
|
+
});
|
|
117
154
|
});
|
|
118
155
|
});
|
|
119
156
|
describe('shortHash', () => {
|
|
@@ -127,4 +164,51 @@ describe('CLI Utils', () => {
|
|
|
127
164
|
expect(shortHash('')).toBe('');
|
|
128
165
|
});
|
|
129
166
|
});
|
|
167
|
+
describe('parseGitHubPrUrl', () => {
|
|
168
|
+
it('should parse valid GitHub PR URLs', () => {
|
|
169
|
+
const result = parseGitHubPrUrl('https://github.com/owner/repo/pull/123');
|
|
170
|
+
expect(result).toEqual({
|
|
171
|
+
owner: 'owner',
|
|
172
|
+
repo: 'repo',
|
|
173
|
+
pullNumber: 123,
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
it('should parse GitHub PR URLs with additional path segments', () => {
|
|
177
|
+
const result = parseGitHubPrUrl('https://github.com/owner/repo/pull/456/files');
|
|
178
|
+
expect(result).toEqual({
|
|
179
|
+
owner: 'owner',
|
|
180
|
+
repo: 'repo',
|
|
181
|
+
pullNumber: 456,
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
it('should parse GitHub PR URLs with query parameters', () => {
|
|
185
|
+
const result = parseGitHubPrUrl('https://github.com/owner/repo/pull/789?tab=files');
|
|
186
|
+
expect(result).toEqual({
|
|
187
|
+
owner: 'owner',
|
|
188
|
+
repo: 'repo',
|
|
189
|
+
pullNumber: 789,
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
it('should handle URLs with hyphens and underscores in owner/repo names', () => {
|
|
193
|
+
const result = parseGitHubPrUrl('https://github.com/owner-name/repo_name/pull/123');
|
|
194
|
+
expect(result).toEqual({
|
|
195
|
+
owner: 'owner-name',
|
|
196
|
+
repo: 'repo_name',
|
|
197
|
+
pullNumber: 123,
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
it('should return null for invalid URLs', () => {
|
|
201
|
+
expect(parseGitHubPrUrl('not-a-url')).toBe(null);
|
|
202
|
+
expect(parseGitHubPrUrl('https://example.com/owner/repo/pull/123')).toBe(null);
|
|
203
|
+
expect(parseGitHubPrUrl('https://github.com/owner/repo/issues/123')).toBe(null);
|
|
204
|
+
expect(parseGitHubPrUrl('https://github.com/owner/repo')).toBe(null);
|
|
205
|
+
expect(parseGitHubPrUrl('https://github.com/owner/repo/pull/abc')).toBe(null);
|
|
206
|
+
});
|
|
207
|
+
it('should handle malformed URLs gracefully', () => {
|
|
208
|
+
expect(parseGitHubPrUrl('')).toBe(null);
|
|
209
|
+
expect(parseGitHubPrUrl('https://github.com')).toBe(null);
|
|
210
|
+
expect(parseGitHubPrUrl('https://github.com/owner')).toBe(null);
|
|
211
|
+
expect(parseGitHubPrUrl('https://github.com/owner/repo/pull')).toBe(null);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
130
214
|
});
|