difit 1.1.0 → 1.1.1
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 +53 -46
- package/dist/cli/index.js +25 -15
- package/dist/cli/utils.d.ts +5 -0
- package/dist/cli/utils.js +33 -0
- package/dist/cli/utils.test.js +1 -85
- package/dist/server/git-diff-tui.d.ts +1 -1
- package/dist/server/git-diff-tui.js +31 -23
- package/dist/server/git-diff.d.ts +1 -1
- package/dist/server/git-diff.js +27 -20
- package/dist/server/server.d.ts +2 -1
- package/dist/server/server.js +4 -4
- package/dist/tui/App.d.ts +2 -1
- package/dist/tui/App.js +7 -7
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -18,59 +18,64 @@ A lightweight command-line tool that spins up a local web server to display Git
|
|
|
18
18
|
- 🎨 **Syntax Highlighting**: Dynamic language loading for Bash, PHP, SQL, Ruby, Java, and more
|
|
19
19
|
- ✨ **100% vibe coding**: Built with pure coding energy and good vibes
|
|
20
20
|
|
|
21
|
-
##
|
|
21
|
+
## ⚡ Quick Start
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
#
|
|
25
|
-
npx reviewit <commit-ish>
|
|
26
|
-
|
|
27
|
-
# or Global install
|
|
28
|
-
npm install -g reviewit
|
|
24
|
+
npx reviewit # View HEAD commit changes in a beautiful diff viewer
|
|
29
25
|
```
|
|
30
26
|
|
|
31
27
|
## 🚀 Usage
|
|
32
28
|
|
|
29
|
+
### Basic Usage
|
|
30
|
+
|
|
33
31
|
```bash
|
|
34
|
-
#
|
|
35
|
-
npx reviewit
|
|
32
|
+
npx reviewit <commit-ish> # View single commit diff
|
|
33
|
+
npx reviewit <commit-ish> [compare-with] # Compare two commits/branches
|
|
34
|
+
```
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
npx reviewit 6f4a9b7
|
|
39
|
-
npx reviewit HEAD^
|
|
40
|
-
npx reviewit HEAD~3
|
|
36
|
+
### Single commit review
|
|
41
37
|
|
|
42
|
-
|
|
43
|
-
npx reviewit
|
|
44
|
-
npx reviewit
|
|
45
|
-
npx reviewit
|
|
38
|
+
```bash
|
|
39
|
+
npx reviewit 6f4a9b7 # Specific commit
|
|
40
|
+
npx reviewit HEAD^ # Previous commit
|
|
41
|
+
npx reviewit feature # Latest commit on branch
|
|
42
|
+
```
|
|
46
43
|
|
|
47
|
-
|
|
48
|
-
npx reviewit 6f4a9b7 --port 4300 --no-open
|
|
44
|
+
### Compare two commits
|
|
49
45
|
|
|
50
|
-
|
|
51
|
-
npx reviewit
|
|
52
|
-
npx reviewit
|
|
46
|
+
```bash
|
|
47
|
+
npx reviewit HEAD main # Compare HEAD with main branch
|
|
48
|
+
npx reviewit feature main # Compare branches
|
|
49
|
+
npx reviewit . origin/main # Compare working directory with remote main
|
|
53
50
|
```
|
|
54
51
|
|
|
55
|
-
###
|
|
52
|
+
### Special Arguments
|
|
56
53
|
|
|
57
|
-
|
|
58
|
-
| -------------- | ------------ | ------------------------------------------------------------------ |
|
|
59
|
-
| `<commit-ish>` | (required) | Any Git reference: hash, tag, HEAD~n, branch, or Special Arguments |
|
|
60
|
-
| `--port` | auto | Preferred port; falls back if occupied |
|
|
61
|
-
| `--no-open` | false | Don't automatically open browser |
|
|
62
|
-
| `--mode` | side-by-side | Diff mode: `inline` or `side-by-side` |
|
|
63
|
-
| `--tui` | false | Use terminal UI mode instead of web interface |
|
|
54
|
+
ReviewIt supports special keywords for common diff scenarios:
|
|
64
55
|
|
|
65
|
-
|
|
56
|
+
```bash
|
|
57
|
+
npx reviewit # HEAD commit changes
|
|
58
|
+
npx reviewit . # All uncommitted changes (staged + unstaged)
|
|
59
|
+
npx reviewit staged # Staged changes ready for commit
|
|
60
|
+
npx reviewit working # Unstaged changes only (cannot use compare-with)
|
|
61
|
+
```
|
|
66
62
|
|
|
67
|
-
|
|
63
|
+
| Keyword | Description | Compare-with Support |
|
|
64
|
+
| --------- | ------------------------------------------------------ | -------------------- |
|
|
65
|
+
| `.` | Shows all uncommitted changes (both staged & unstaged) | ✅ Yes |
|
|
66
|
+
| `staged` | Shows staged changes ready to be committed | ✅ Yes |
|
|
67
|
+
| `working` | Shows unstaged changes in your working directory | ❌ No |
|
|
68
|
+
|
|
69
|
+
### ⚙️ CLI Options
|
|
68
70
|
|
|
69
|
-
|
|
|
70
|
-
|
|
|
71
|
-
|
|
|
72
|
-
| `
|
|
73
|
-
|
|
|
71
|
+
| Flag | Default | Description |
|
|
72
|
+
| ---------------- | ------------ | ------------------------------------------------------------------- |
|
|
73
|
+
| `<commit-ish>` | HEAD | Any Git reference: hash, tag, HEAD~n, branch, or Special Arguments |
|
|
74
|
+
| `[compare-with]` | (optional) | Optional second commit to compare with (shows diff between the two) |
|
|
75
|
+
| `--port` | auto | Preferred port; falls back if occupied |
|
|
76
|
+
| `--no-open` | false | Don't automatically open browser |
|
|
77
|
+
| `--mode` | side-by-side | Diff mode: `inline` or `side-by-side` |
|
|
78
|
+
| `--tui` | false | Use terminal UI mode instead of web interface |
|
|
74
79
|
|
|
75
80
|
## 💬 Comment System
|
|
76
81
|
|
|
@@ -118,15 +123,10 @@ pnpm install
|
|
|
118
123
|
|
|
119
124
|
# Start development server (with hot reload)
|
|
120
125
|
# This runs both Vite dev server and CLI with NODE_ENV=development
|
|
121
|
-
pnpm run dev
|
|
122
|
-
pnpm run dev HEAD~3 # review HEAD~3
|
|
123
|
-
pnpm run dev main # review main branch
|
|
124
|
-
|
|
125
|
-
# For development CLI only (connects to separate Vite server)
|
|
126
|
-
pnpm run dev:cli <commit-ish>
|
|
126
|
+
pnpm run dev
|
|
127
127
|
|
|
128
128
|
# Build and start production server
|
|
129
|
-
pnpm run start
|
|
129
|
+
pnpm run start <commit-ish>
|
|
130
130
|
|
|
131
131
|
# Build for production
|
|
132
132
|
pnpm run build
|
|
@@ -143,20 +143,27 @@ pnpm run typecheck
|
|
|
143
143
|
### Development Workflow
|
|
144
144
|
|
|
145
145
|
- **`pnpm run dev`**: Starts both Vite dev server (with hot reload) and CLI server simultaneously
|
|
146
|
-
- **`pnpm run start <commit>`**: Builds everything and starts production server (for testing final build)
|
|
146
|
+
- **`pnpm run start <commit-ish>`**: Builds everything and starts production server (for testing final build)
|
|
147
147
|
- **Development mode**: Uses Vite's dev server for hot reload and fast development
|
|
148
148
|
- **Production mode**: Serves built static files (used by npx and production builds)
|
|
149
149
|
|
|
150
150
|
## 🏗️ Architecture
|
|
151
151
|
|
|
152
|
-
- **CLI**: Commander.js for argument parsing
|
|
152
|
+
- **CLI**: Commander.js for argument parsing with comprehensive validation
|
|
153
153
|
- **Backend**: Express server with simple-git for diff processing
|
|
154
154
|
- **Frontend**: React 18 + TypeScript + Vite
|
|
155
155
|
- **Styling**: Tailwind CSS v4 with GitHub-like dark theme
|
|
156
156
|
- **Syntax Highlighting**: Prism.js with dynamic language loading
|
|
157
|
-
- **Testing**: Vitest for unit tests
|
|
157
|
+
- **Testing**: Vitest for unit tests with co-located test files
|
|
158
158
|
- **Quality**: ESLint, Prettier, lefthook pre-commit hooks
|
|
159
159
|
|
|
160
|
+
### Key Components
|
|
161
|
+
|
|
162
|
+
- **Validation System**: Unified validation logic for CLI arguments with comprehensive error handling
|
|
163
|
+
- **Dual Parameter System**: Internal refactoring splits commitish into targetCommitish and baseCommitish for flexibility
|
|
164
|
+
- **Special Argument Support**: Working directory, staging area, and uncommitted changes detection
|
|
165
|
+
- **Hash Utilities**: Consistent short hash generation for commit display
|
|
166
|
+
|
|
160
167
|
## 📋 Requirements
|
|
161
168
|
|
|
162
169
|
- Node.js ≥ 18.0.0
|
package/dist/cli/index.js
CHANGED
|
@@ -3,30 +3,38 @@ 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 {
|
|
6
|
+
import { validateDiffArguments } from './utils.js';
|
|
7
|
+
function isSpecialArg(arg) {
|
|
8
|
+
return arg === 'working' || arg === 'staged' || arg === '.';
|
|
9
|
+
}
|
|
7
10
|
const program = new Command();
|
|
8
11
|
program
|
|
9
12
|
.name('reviewit')
|
|
10
13
|
.description('A lightweight Git diff viewer with GitHub-like interface')
|
|
11
14
|
.version(pkg.version)
|
|
12
15
|
.argument('[commit-ish]', 'Git commit, tag, branch, HEAD~n reference, or "working"/"staged"/"." (default: HEAD)', 'HEAD')
|
|
16
|
+
.argument('[compare-with]', 'Optional: Compare with this commit/branch (shows diff between commit-ish and compare-with)')
|
|
13
17
|
.option('--port <port>', 'preferred port (auto-assigned if occupied)', parseInt)
|
|
14
18
|
.option('--no-open', 'do not automatically open browser')
|
|
15
19
|
.option('--mode <mode>', 'diff mode (inline only for now)', 'inline')
|
|
16
20
|
.option('--tui', 'use terminal UI instead of web interface')
|
|
17
|
-
.action(async (commitish, options) => {
|
|
21
|
+
.action(async (commitish, compareWith, options) => {
|
|
18
22
|
try {
|
|
19
|
-
// Determine
|
|
23
|
+
// Determine target and base commitish
|
|
20
24
|
let targetCommitish = commitish;
|
|
21
|
-
|
|
22
|
-
if (
|
|
23
|
-
|
|
25
|
+
let baseCommitish;
|
|
26
|
+
if (compareWith) {
|
|
27
|
+
// If compareWith is provided, use it as base
|
|
28
|
+
baseCommitish = compareWith;
|
|
24
29
|
}
|
|
25
|
-
else
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
else {
|
|
31
|
+
// Handle special arguments
|
|
32
|
+
if (isSpecialArg(commitish)) {
|
|
33
|
+
baseCommitish = 'HEAD';
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
baseCommitish = commitish + '^';
|
|
37
|
+
}
|
|
30
38
|
}
|
|
31
39
|
if (options.tui) {
|
|
32
40
|
// Check if we're in a TTY environment
|
|
@@ -38,15 +46,17 @@ program
|
|
|
38
46
|
// Dynamic import for TUI mode
|
|
39
47
|
const { render } = await import('ink');
|
|
40
48
|
const { default: TuiApp } = await import('../tui/App.js');
|
|
41
|
-
render(React.createElement(TuiApp, {
|
|
49
|
+
render(React.createElement(TuiApp, { targetCommitish, baseCommitish }));
|
|
42
50
|
return;
|
|
43
51
|
}
|
|
44
|
-
|
|
45
|
-
|
|
52
|
+
const validation = validateDiffArguments(targetCommitish, compareWith);
|
|
53
|
+
if (!validation.valid) {
|
|
54
|
+
console.error(`Error: ${validation.error}`);
|
|
46
55
|
process.exit(1);
|
|
47
56
|
}
|
|
48
57
|
const { url } = await startServer({
|
|
49
|
-
|
|
58
|
+
targetCommitish,
|
|
59
|
+
baseCommitish,
|
|
50
60
|
preferredPort: options.port,
|
|
51
61
|
openBrowser: options.open,
|
|
52
62
|
mode: options.mode,
|
package/dist/cli/utils.d.ts
CHANGED
package/dist/cli/utils.js
CHANGED
|
@@ -20,3 +20,36 @@ export function validateCommitish(commitish) {
|
|
|
20
20
|
];
|
|
21
21
|
return validPatterns.some((pattern) => pattern.test(trimmed));
|
|
22
22
|
}
|
|
23
|
+
export function shortHash(hash) {
|
|
24
|
+
return hash.substring(0, 7);
|
|
25
|
+
}
|
|
26
|
+
export function validateDiffArguments(targetCommitish, baseCommitish) {
|
|
27
|
+
// Validate target commitish format
|
|
28
|
+
if (!validateCommitish(targetCommitish)) {
|
|
29
|
+
return { valid: false, error: 'Invalid target commit-ish format' };
|
|
30
|
+
}
|
|
31
|
+
// Validate base commitish format if provided
|
|
32
|
+
if (baseCommitish !== undefined && !validateCommitish(baseCommitish)) {
|
|
33
|
+
return { valid: false, error: 'Invalid base commit-ish format' };
|
|
34
|
+
}
|
|
35
|
+
// Special arguments are only allowed in target, not base
|
|
36
|
+
const specialArgs = ['working', 'staged', '.'];
|
|
37
|
+
if (baseCommitish && specialArgs.includes(baseCommitish)) {
|
|
38
|
+
return {
|
|
39
|
+
valid: false,
|
|
40
|
+
error: `Special arguments (working, staged, .) are only allowed as target, not base. Got base: ${baseCommitish}`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// Cannot compare same values
|
|
44
|
+
if (targetCommitish === baseCommitish) {
|
|
45
|
+
return { valid: false, error: `Cannot compare ${targetCommitish} with itself` };
|
|
46
|
+
}
|
|
47
|
+
// "working" shows unstaged changes and cannot be compared with another commit
|
|
48
|
+
if (targetCommitish === 'working' && baseCommitish) {
|
|
49
|
+
return {
|
|
50
|
+
valid: false,
|
|
51
|
+
error: '"working" shows unstaged changes and cannot be compared with another commit. Use "." instead to compare all uncommitted changes with a specific commit.',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return { valid: true };
|
|
55
|
+
}
|
package/dist/cli/utils.test.js
CHANGED
|
@@ -1,23 +1,11 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { validateCommitish, validateDiffArguments, shortHash
|
|
2
|
+
import { validateCommitish, validateDiffArguments, shortHash } 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
|
-
});
|
|
21
9
|
it('should validate HEAD references', () => {
|
|
22
10
|
expect(validateCommitish('HEAD')).toBe(true);
|
|
23
11
|
expect(validateCommitish('HEAD~1')).toBe(true);
|
|
@@ -85,12 +73,6 @@ describe('CLI Utils', () => {
|
|
|
85
73
|
expect(validateDiffArguments('staged', 'HEAD')).toEqual({ valid: true });
|
|
86
74
|
expect(validateDiffArguments('.', 'main')).toEqual({ valid: true });
|
|
87
75
|
});
|
|
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
|
-
});
|
|
94
76
|
});
|
|
95
77
|
describe('same value comparison', () => {
|
|
96
78
|
it('should reject same target and base values', () => {
|
|
@@ -115,17 +97,6 @@ describe('CLI Utils', () => {
|
|
|
115
97
|
it('should allow working without compareWith', () => {
|
|
116
98
|
expect(validateDiffArguments('working')).toEqual({ valid: true });
|
|
117
99
|
});
|
|
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
|
-
});
|
|
129
100
|
it('should allow other special args with compareWith', () => {
|
|
130
101
|
expect(validateDiffArguments('staged', 'HEAD')).toEqual({ valid: true });
|
|
131
102
|
expect(validateDiffArguments('.', 'main')).toEqual({ valid: true });
|
|
@@ -143,14 +114,6 @@ describe('CLI Utils', () => {
|
|
|
143
114
|
valid: true,
|
|
144
115
|
});
|
|
145
116
|
});
|
|
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
|
-
});
|
|
154
117
|
});
|
|
155
118
|
});
|
|
156
119
|
describe('shortHash', () => {
|
|
@@ -164,51 +127,4 @@ describe('CLI Utils', () => {
|
|
|
164
127
|
expect(shortHash('')).toBe('');
|
|
165
128
|
});
|
|
166
129
|
});
|
|
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
|
-
});
|
|
214
130
|
});
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { FileDiff } from '../types/diff.js';
|
|
2
|
-
export declare function loadGitDiff(
|
|
2
|
+
export declare function loadGitDiff(targetCommitish: string, baseCommitish: string): Promise<FileDiff[]>;
|
|
@@ -1,25 +1,32 @@
|
|
|
1
1
|
import simpleGit from 'simple-git';
|
|
2
|
-
|
|
2
|
+
import { validateDiffArguments } from '../cli/utils.js';
|
|
3
|
+
export async function loadGitDiff(targetCommitish, baseCommitish) {
|
|
4
|
+
// Validate arguments
|
|
5
|
+
const validation = validateDiffArguments(targetCommitish, baseCommitish);
|
|
6
|
+
if (!validation.valid) {
|
|
7
|
+
throw new Error(validation.error);
|
|
8
|
+
}
|
|
3
9
|
const git = simpleGit();
|
|
4
10
|
let diff;
|
|
5
|
-
|
|
6
|
-
|
|
11
|
+
// Handle target special chars (base is always a regular commit)
|
|
12
|
+
if (targetCommitish === 'working') {
|
|
13
|
+
// Show unstaged changes (working vs staged)
|
|
7
14
|
diff = await git.diff(['--name-status']);
|
|
8
15
|
}
|
|
9
|
-
else if (
|
|
10
|
-
// Show staged changes
|
|
11
|
-
diff = await git.diff(['--cached', '--name-status']);
|
|
16
|
+
else if (targetCommitish === 'staged') {
|
|
17
|
+
// Show staged changes against base commit
|
|
18
|
+
diff = await git.diff(['--cached', baseCommitish, '--name-status']);
|
|
12
19
|
}
|
|
13
|
-
else if (
|
|
14
|
-
// Show all changes
|
|
15
|
-
diff = await git.diff([
|
|
20
|
+
else if (targetCommitish === '.') {
|
|
21
|
+
// Show all uncommitted changes against base commit
|
|
22
|
+
diff = await git.diff([baseCommitish, '--name-status']);
|
|
16
23
|
}
|
|
17
24
|
else {
|
|
18
|
-
//
|
|
19
|
-
diff = await git.diff([`${
|
|
25
|
+
// Both are regular commits: standard commit-to-commit comparison
|
|
26
|
+
diff = await git.diff([`${baseCommitish}..${targetCommitish}`, '--name-status']);
|
|
20
27
|
if (!diff.trim()) {
|
|
21
28
|
// Try without parent (for initial commit)
|
|
22
|
-
const diffInitial = await git.diff([
|
|
29
|
+
const diffInitial = await git.diff([targetCommitish, '--name-status']);
|
|
23
30
|
if (!diffInitial.trim()) {
|
|
24
31
|
throw new Error('No changes found in this commit');
|
|
25
32
|
}
|
|
@@ -37,26 +44,27 @@ export async function loadGitDiff(commitish) {
|
|
|
37
44
|
// Get diff for each file individually
|
|
38
45
|
const fileDiffs = await Promise.all(fileChanges.map(async ({ status, path }) => {
|
|
39
46
|
let fileDiff = '';
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
// Handle individual file diffs (base is always a regular commit)
|
|
48
|
+
if (targetCommitish === 'working') {
|
|
49
|
+
// Show unstaged changes (working vs staged)
|
|
42
50
|
fileDiff = await git.diff(['--', path]);
|
|
43
51
|
}
|
|
44
|
-
else if (
|
|
45
|
-
//
|
|
46
|
-
fileDiff = await git.diff(['--cached', '--', path]);
|
|
52
|
+
else if (targetCommitish === 'staged') {
|
|
53
|
+
// Show staged changes against base commit
|
|
54
|
+
fileDiff = await git.diff(['--cached', baseCommitish, '--', path]);
|
|
47
55
|
}
|
|
48
|
-
else if (
|
|
49
|
-
//
|
|
50
|
-
fileDiff = await git.diff([
|
|
56
|
+
else if (targetCommitish === '.') {
|
|
57
|
+
// Show all uncommitted changes against base commit
|
|
58
|
+
fileDiff = await git.diff([baseCommitish, '--', path]);
|
|
51
59
|
}
|
|
52
60
|
else {
|
|
53
61
|
try {
|
|
54
|
-
//
|
|
55
|
-
fileDiff = await git.diff([`${
|
|
62
|
+
// Both are regular commits: standard commit-to-commit comparison
|
|
63
|
+
fileDiff = await git.diff([`${baseCommitish}..${targetCommitish}`, '--', path]);
|
|
56
64
|
}
|
|
57
65
|
catch {
|
|
58
66
|
// For new files or if parent doesn't exist
|
|
59
|
-
fileDiff = await git.diff([
|
|
67
|
+
fileDiff = await git.diff([targetCommitish, '--', path]);
|
|
60
68
|
}
|
|
61
69
|
}
|
|
62
70
|
const lines = fileDiff.split('\n');
|
|
@@ -2,7 +2,7 @@ import { type DiffResponse } from '../types/diff.js';
|
|
|
2
2
|
export declare class GitDiffParser {
|
|
3
3
|
private git;
|
|
4
4
|
constructor(repoPath?: string);
|
|
5
|
-
parseDiff(
|
|
5
|
+
parseDiff(targetCommitish: string, baseCommitish: string, ignoreWhitespace?: boolean): Promise<DiffResponse>;
|
|
6
6
|
private parseUnifiedDiff;
|
|
7
7
|
private parseFileBlock;
|
|
8
8
|
private parseChunks;
|
package/dist/server/git-diff.js
CHANGED
|
@@ -1,35 +1,42 @@
|
|
|
1
1
|
import { simpleGit } from 'simple-git';
|
|
2
|
+
import { validateDiffArguments, shortHash } from '../cli/utils.js';
|
|
2
3
|
export class GitDiffParser {
|
|
3
4
|
constructor(repoPath = process.cwd()) {
|
|
4
5
|
this.git = simpleGit(repoPath);
|
|
5
6
|
}
|
|
6
|
-
async parseDiff(
|
|
7
|
+
async parseDiff(targetCommitish, baseCommitish, ignoreWhitespace = false) {
|
|
7
8
|
try {
|
|
9
|
+
// Validate arguments
|
|
10
|
+
const validation = validateDiffArguments(targetCommitish, baseCommitish);
|
|
11
|
+
if (!validation.valid) {
|
|
12
|
+
throw new Error(validation.error);
|
|
13
|
+
}
|
|
8
14
|
let resolvedCommit;
|
|
9
15
|
let diffArgs;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
diffArgs = ['HEAD'];
|
|
14
|
-
}
|
|
15
|
-
else if (commitish === 'working') {
|
|
16
|
-
// Show only unstaged changes
|
|
16
|
+
// Handle target special chars (base is always a regular commit)
|
|
17
|
+
if (targetCommitish === 'working') {
|
|
18
|
+
// Show unstaged changes (working vs staged)
|
|
17
19
|
resolvedCommit = 'Working Directory (unstaged changes)';
|
|
18
20
|
diffArgs = [];
|
|
19
21
|
}
|
|
20
|
-
else if (
|
|
21
|
-
// Show
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
else if (targetCommitish === 'staged') {
|
|
23
|
+
// Show staged changes against base commit
|
|
24
|
+
const baseHash = await this.git.revparse([baseCommitish]);
|
|
25
|
+
resolvedCommit = `${shortHash(baseHash)} vs Staging Area (staged changes)`;
|
|
26
|
+
diffArgs = ['--cached', baseCommitish];
|
|
27
|
+
}
|
|
28
|
+
else if (targetCommitish === '.') {
|
|
29
|
+
// Show all uncommitted changes against base commit
|
|
30
|
+
const baseHash = await this.git.revparse([baseCommitish]);
|
|
31
|
+
resolvedCommit = `${shortHash(baseHash)} vs Working Directory (all uncommitted changes)`;
|
|
32
|
+
diffArgs = [baseCommitish];
|
|
24
33
|
}
|
|
25
34
|
else {
|
|
26
|
-
//
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
resolvedCommit = `${shortParentHash}..${shortHash}`;
|
|
32
|
-
diffArgs = [`${commitish}^`, commitish];
|
|
35
|
+
// Both are regular commits: standard commit-to-commit comparison
|
|
36
|
+
const targetHash = await this.git.revparse([targetCommitish]);
|
|
37
|
+
const baseHash = await this.git.revparse([baseCommitish]);
|
|
38
|
+
resolvedCommit = `${shortHash(baseHash)}..${shortHash(targetHash)}`;
|
|
39
|
+
diffArgs = [baseCommitish, targetCommitish];
|
|
33
40
|
}
|
|
34
41
|
if (ignoreWhitespace) {
|
|
35
42
|
diffArgs.push('-w');
|
|
@@ -44,7 +51,7 @@ export class GitDiffParser {
|
|
|
44
51
|
};
|
|
45
52
|
}
|
|
46
53
|
catch (error) {
|
|
47
|
-
throw new Error(`Failed to parse diff for ${
|
|
54
|
+
throw new Error(`Failed to parse diff for ${targetCommitish} vs ${baseCommitish}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
48
55
|
}
|
|
49
56
|
}
|
|
50
57
|
parseUnifiedDiff(diffText, summary) {
|
package/dist/server/server.d.ts
CHANGED
package/dist/server/server.js
CHANGED
|
@@ -17,16 +17,16 @@ export async function startServer(options) {
|
|
|
17
17
|
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
|
|
18
18
|
next();
|
|
19
19
|
});
|
|
20
|
-
const isValidCommit = await parser.validateCommit(options.
|
|
20
|
+
const isValidCommit = await parser.validateCommit(options.targetCommitish);
|
|
21
21
|
if (!isValidCommit) {
|
|
22
|
-
throw new Error(`Invalid or non-existent commit: ${options.
|
|
22
|
+
throw new Error(`Invalid or non-existent commit: ${options.targetCommitish}`);
|
|
23
23
|
}
|
|
24
|
-
diffData = await parser.parseDiff(options.
|
|
24
|
+
diffData = await parser.parseDiff(options.targetCommitish, options.baseCommitish, currentIgnoreWhitespace);
|
|
25
25
|
app.get('/api/diff', async (req, res) => {
|
|
26
26
|
const ignoreWhitespace = req.query.ignoreWhitespace === 'true';
|
|
27
27
|
if (ignoreWhitespace !== currentIgnoreWhitespace) {
|
|
28
28
|
currentIgnoreWhitespace = ignoreWhitespace;
|
|
29
|
-
diffData = await parser.parseDiff(options.
|
|
29
|
+
diffData = await parser.parseDiff(options.targetCommitish, options.baseCommitish, ignoreWhitespace);
|
|
30
30
|
}
|
|
31
31
|
res.json({ ...diffData, ignoreWhitespace });
|
|
32
32
|
});
|
package/dist/tui/App.d.ts
CHANGED
package/dist/tui/App.js
CHANGED
|
@@ -5,7 +5,7 @@ import FileList from './components/FileList.js';
|
|
|
5
5
|
import DiffViewer from './components/DiffViewer.js';
|
|
6
6
|
import SideBySideDiffViewer from './components/SideBySideDiffViewer.js';
|
|
7
7
|
import StatusBar from './components/StatusBar.js';
|
|
8
|
-
const App = ({
|
|
8
|
+
const App = ({ targetCommitish, baseCommitish }) => {
|
|
9
9
|
const [files, setFiles] = useState([]);
|
|
10
10
|
const [selectedFileIndex, setSelectedFileIndex] = useState(0);
|
|
11
11
|
const [loading, setLoading] = useState(true);
|
|
@@ -16,7 +16,7 @@ const App = ({ commitish }) => {
|
|
|
16
16
|
setLoading(true);
|
|
17
17
|
setError(null);
|
|
18
18
|
try {
|
|
19
|
-
const fileDiffs = await loadGitDiff(
|
|
19
|
+
const fileDiffs = await loadGitDiff(targetCommitish, baseCommitish);
|
|
20
20
|
setFiles(fileDiffs);
|
|
21
21
|
setLoading(false);
|
|
22
22
|
}
|
|
@@ -28,7 +28,7 @@ const App = ({ commitish }) => {
|
|
|
28
28
|
useEffect(() => {
|
|
29
29
|
loadDiff();
|
|
30
30
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
31
|
-
}, [
|
|
31
|
+
}, [targetCommitish, baseCommitish]);
|
|
32
32
|
useInput((input, key) => {
|
|
33
33
|
if (input === 'q' || (key.ctrl && input === 'c')) {
|
|
34
34
|
exit();
|
|
@@ -61,7 +61,7 @@ const App = ({ commitish }) => {
|
|
|
61
61
|
if (loading) {
|
|
62
62
|
return React.createElement(Text, null,
|
|
63
63
|
"Loading diff for ",
|
|
64
|
-
|
|
64
|
+
targetCommitish,
|
|
65
65
|
"...");
|
|
66
66
|
}
|
|
67
67
|
if (error) {
|
|
@@ -71,16 +71,16 @@ const App = ({ commitish }) => {
|
|
|
71
71
|
}
|
|
72
72
|
if (files.length === 0) {
|
|
73
73
|
return (React.createElement(Box, { flexDirection: "column" },
|
|
74
|
-
React.createElement(StatusBar, { commitish:
|
|
74
|
+
React.createElement(StatusBar, { commitish: targetCommitish, totalFiles: 0, currentMode: "list" }),
|
|
75
75
|
React.createElement(Box, { marginTop: 1 },
|
|
76
76
|
React.createElement(Text, { color: "yellow" },
|
|
77
77
|
"No changes found for ",
|
|
78
|
-
|
|
78
|
+
targetCommitish)),
|
|
79
79
|
React.createElement(Box, { marginTop: 1 },
|
|
80
80
|
React.createElement(Text, { dimColor: true }, "Press 'q' to quit"))));
|
|
81
81
|
}
|
|
82
82
|
return (React.createElement(Box, { flexDirection: "column", height: process.stdout.rows },
|
|
83
|
-
React.createElement(StatusBar, { commitish:
|
|
83
|
+
React.createElement(StatusBar, { commitish: targetCommitish, totalFiles: files.length, currentMode: viewMode }),
|
|
84
84
|
React.createElement(Box, { flexGrow: 1, flexDirection: "column" }, viewMode === 'list' ? (React.createElement(FileList, { files: files, selectedIndex: selectedFileIndex })) : viewMode === 'side-by-side' ? (React.createElement(SideBySideDiffViewer, { files: files, initialFileIndex: selectedFileIndex, onBack: () => setViewMode('list') })) : (React.createElement(DiffViewer, { files: files, initialFileIndex: selectedFileIndex, onBack: () => setViewMode('list') }))),
|
|
85
85
|
React.createElement(Box, { borderStyle: "single", paddingX: 1 },
|
|
86
86
|
React.createElement(Text, { dimColor: true }, viewMode === 'list'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "difit",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "A lightweight command-line tool that spins up a local web server to display Git commit diffs in a GitHub-like Files changed view",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -21,17 +21,17 @@
|
|
|
21
21
|
"scripts": {
|
|
22
22
|
"dev": "node scripts/dev.js",
|
|
23
23
|
"dev:cli": "tsc --project tsconfig.cli.json && NODE_ENV=development node dist/cli/index.js",
|
|
24
|
-
"build": "
|
|
24
|
+
"build": "tsc -b && vite build",
|
|
25
25
|
"build:cli": "tsc --project tsconfig.cli.json",
|
|
26
|
-
"start": "pnpm run build &&
|
|
26
|
+
"start": "pnpm run build && node dist/cli/index.js",
|
|
27
27
|
"lint": "eslint .",
|
|
28
28
|
"lint:fix": "eslint . --fix",
|
|
29
29
|
"format": "prettier --write .",
|
|
30
|
-
"typecheck": "tsc
|
|
30
|
+
"typecheck": "tsc -b",
|
|
31
31
|
"test": "vitest run",
|
|
32
32
|
"test:watch": "vitest",
|
|
33
33
|
"prepare": "lefthook install",
|
|
34
|
-
"prepublishOnly": "NODE_ENV=production pnpm run build
|
|
34
|
+
"prepublishOnly": "NODE_ENV=production pnpm run build"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"commander": "^11.1.0",
|