difit 0.0.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 +106 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +44 -0
- package/dist/cli/index.test.d.ts +1 -0
- package/dist/cli/index.test.js +676 -0
- package/dist/cli/utils.d.ts +1 -0
- package/dist/cli/utils.js +11 -0
- package/dist/cli/utils.test.d.ts +1 -0
- package/dist/cli/utils.test.js +214 -0
- package/dist/client/assets/index-BUtmfbD2.js +53 -0
- package/dist/client/assets/index-mZRIDsXW.css +1 -0
- package/dist/client/assets/prism-css-Bpx-unsJ.js +1 -0
- package/dist/client/assets/prism-json-xwnKirkR.js +1 -0
- package/dist/client/assets/prism-typescript-B2PMeEx1.js +1 -0
- package/dist/client/index.html +14 -0
- package/dist/server/comment-store.d.ts +13 -0
- package/dist/server/comment-store.js +63 -0
- package/dist/server/git-diff-tui.d.ts +2 -0
- package/dist/server/git-diff-tui.js +95 -0
- package/dist/server/git-diff.d.ts +10 -0
- package/dist/server/git-diff.js +124 -0
- package/dist/server/git-diff.test.d.ts +1 -0
- package/dist/server/git-diff.test.js +292 -0
- package/dist/server/server.d.ts +11 -0
- package/dist/server/server.js +128 -0
- package/dist/server/server.test.d.ts +1 -0
- package/dist/server/server.test.js +382 -0
- package/dist/tui/App.d.ts +8 -0
- package/dist/tui/App.js +92 -0
- package/dist/tui/App.test.d.ts +1 -0
- package/dist/tui/App.test.js +31 -0
- package/dist/tui/components/DiffViewer.d.ts +9 -0
- package/dist/tui/components/DiffViewer.js +88 -0
- package/dist/tui/components/FileList.d.ts +8 -0
- package/dist/tui/components/FileList.js +48 -0
- package/dist/tui/components/SideBySideDiffViewer.d.ts +9 -0
- package/dist/tui/components/SideBySideDiffViewer.js +237 -0
- package/dist/tui/components/StatusBar.d.ts +8 -0
- package/dist/tui/components/StatusBar.js +23 -0
- package/dist/tui/utils/parseDiff.d.ts +2 -0
- package/dist/tui/utils/parseDiff.js +68 -0
- package/dist/types/diff.d.ts +33 -0
- package/dist/types/diff.js +1 -0
- package/dist/utils/fileUtils.d.ts +12 -0
- package/dist/utils/fileUtils.js +21 -0
- package/package.json +91 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
const FileList = ({ files, selectedIndex }) => {
|
|
4
|
+
const getStatusColor = (status) => {
|
|
5
|
+
switch (status) {
|
|
6
|
+
case 'A':
|
|
7
|
+
return 'green';
|
|
8
|
+
case 'M':
|
|
9
|
+
return 'yellow';
|
|
10
|
+
case 'D':
|
|
11
|
+
return 'red';
|
|
12
|
+
default:
|
|
13
|
+
return 'white';
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
const getStatusLabel = (status) => {
|
|
17
|
+
switch (status) {
|
|
18
|
+
case 'A':
|
|
19
|
+
return '[+]';
|
|
20
|
+
case 'M':
|
|
21
|
+
return '[M]';
|
|
22
|
+
case 'D':
|
|
23
|
+
return '[-]';
|
|
24
|
+
default:
|
|
25
|
+
return '[?]';
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
29
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
30
|
+
React.createElement(Text, { bold: true },
|
|
31
|
+
"Changed Files (",
|
|
32
|
+
files.length,
|
|
33
|
+
")")),
|
|
34
|
+
files.map((file, index) => (React.createElement(Box, { key: `${file.path}-${index}` },
|
|
35
|
+
React.createElement(Text, { color: index === selectedIndex ? 'cyan' : undefined, backgroundColor: index === selectedIndex ? 'gray' : undefined },
|
|
36
|
+
index === selectedIndex ? '▶ ' : ' ',
|
|
37
|
+
React.createElement(Text, { color: getStatusColor(file.status) }, getStatusLabel(file.status)),
|
|
38
|
+
' ',
|
|
39
|
+
file.path,
|
|
40
|
+
' ',
|
|
41
|
+
React.createElement(Text, { dimColor: true },
|
|
42
|
+
"(+",
|
|
43
|
+
file.additions,
|
|
44
|
+
" -",
|
|
45
|
+
file.deletions,
|
|
46
|
+
")")))))));
|
|
47
|
+
};
|
|
48
|
+
export default FileList;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { FileDiff } from '../../types/diff.js';
|
|
3
|
+
interface SideBySideDiffViewerProps {
|
|
4
|
+
files: FileDiff[];
|
|
5
|
+
initialFileIndex: number;
|
|
6
|
+
onBack: () => void;
|
|
7
|
+
}
|
|
8
|
+
declare const SideBySideDiffViewer: React.FC<SideBySideDiffViewerProps>;
|
|
9
|
+
export default SideBySideDiffViewer;
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, Text, useInput, useApp } from 'ink';
|
|
3
|
+
import { parseDiff } from '../utils/parseDiff.js';
|
|
4
|
+
const SideBySideDiffViewer = ({ files, initialFileIndex, onBack, }) => {
|
|
5
|
+
const [currentFileIndex, setCurrentFileIndex] = useState(initialFileIndex);
|
|
6
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
7
|
+
const currentFile = files[currentFileIndex];
|
|
8
|
+
if (!currentFile || files.length === 0) {
|
|
9
|
+
return (React.createElement(Box, { flexDirection: "column", flexGrow: 1 },
|
|
10
|
+
React.createElement(Text, { color: "yellow" }, "No files to display"),
|
|
11
|
+
React.createElement(Box, { marginTop: 1 },
|
|
12
|
+
React.createElement(Text, { dimColor: true }, "Press ESC or 'b' to go back"))));
|
|
13
|
+
}
|
|
14
|
+
const parsedDiff = parseDiff(currentFile.diff);
|
|
15
|
+
// Calculate total lines for current file
|
|
16
|
+
const allLines = [];
|
|
17
|
+
parsedDiff.chunks.forEach((chunk) => {
|
|
18
|
+
// Add chunk header
|
|
19
|
+
allLines.push({
|
|
20
|
+
old: chunk.header,
|
|
21
|
+
new: chunk.header,
|
|
22
|
+
type: 'header',
|
|
23
|
+
});
|
|
24
|
+
let oldIdx = 0;
|
|
25
|
+
let newIdx = 0;
|
|
26
|
+
while (oldIdx < chunk.lines.length || newIdx < chunk.lines.length) {
|
|
27
|
+
const oldLine = chunk.lines[oldIdx];
|
|
28
|
+
const newLine = chunk.lines[newIdx];
|
|
29
|
+
if (oldLine?.type === 'remove' && newLine?.type === 'add') {
|
|
30
|
+
// Same line modified - show side by side
|
|
31
|
+
allLines.push({
|
|
32
|
+
old: oldLine.content,
|
|
33
|
+
new: newLine.content,
|
|
34
|
+
oldNum: oldLine.oldLineNumber,
|
|
35
|
+
newNum: newLine.newLineNumber,
|
|
36
|
+
type: 'modified',
|
|
37
|
+
});
|
|
38
|
+
oldIdx++;
|
|
39
|
+
newIdx++;
|
|
40
|
+
}
|
|
41
|
+
else if (oldLine?.type === 'remove') {
|
|
42
|
+
// Line removed
|
|
43
|
+
allLines.push({
|
|
44
|
+
old: oldLine.content,
|
|
45
|
+
oldNum: oldLine.oldLineNumber,
|
|
46
|
+
type: 'remove',
|
|
47
|
+
});
|
|
48
|
+
oldIdx++;
|
|
49
|
+
}
|
|
50
|
+
else if (newLine?.type === 'add') {
|
|
51
|
+
// Line added
|
|
52
|
+
allLines.push({
|
|
53
|
+
new: newLine.content,
|
|
54
|
+
newNum: newLine.newLineNumber,
|
|
55
|
+
type: 'add',
|
|
56
|
+
});
|
|
57
|
+
newIdx++;
|
|
58
|
+
}
|
|
59
|
+
else if (oldLine?.type === 'context') {
|
|
60
|
+
// Unchanged line
|
|
61
|
+
allLines.push({
|
|
62
|
+
old: oldLine.content,
|
|
63
|
+
new: oldLine.content,
|
|
64
|
+
oldNum: oldLine.oldLineNumber,
|
|
65
|
+
newNum: oldLine.newLineNumber,
|
|
66
|
+
type: 'context',
|
|
67
|
+
});
|
|
68
|
+
oldIdx++;
|
|
69
|
+
newIdx++;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
oldIdx++;
|
|
73
|
+
newIdx++;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
const viewportHeight = Math.max(10, (process.stdout.rows || 24) - 10); // StatusBar(3) + file nav(3) + footer(3) + margin(1)
|
|
78
|
+
const maxScroll = Math.max(0, allLines.length - viewportHeight);
|
|
79
|
+
const { exit } = useApp();
|
|
80
|
+
useInput((input, key) => {
|
|
81
|
+
if (input === 'q' || (key.ctrl && input === 'c')) {
|
|
82
|
+
exit();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (key.escape || input === 'b') {
|
|
86
|
+
onBack();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// Scroll within file
|
|
90
|
+
if (key.upArrow || input === 'k') {
|
|
91
|
+
setScrollOffset((prev) => Math.max(0, prev - 1));
|
|
92
|
+
}
|
|
93
|
+
if (key.downArrow || input === 'j') {
|
|
94
|
+
setScrollOffset((prev) => Math.min(maxScroll, prev + 1));
|
|
95
|
+
}
|
|
96
|
+
if (key.pageUp) {
|
|
97
|
+
setScrollOffset((prev) => Math.max(0, prev - viewportHeight));
|
|
98
|
+
}
|
|
99
|
+
if (key.pageDown) {
|
|
100
|
+
setScrollOffset((prev) => Math.min(maxScroll, prev + viewportHeight));
|
|
101
|
+
}
|
|
102
|
+
// Navigate between files
|
|
103
|
+
if (key.tab && !key.shift) {
|
|
104
|
+
// Next file (loop to first when at end)
|
|
105
|
+
setCurrentFileIndex((currentFileIndex + 1) % files.length);
|
|
106
|
+
setScrollOffset(0);
|
|
107
|
+
}
|
|
108
|
+
if (key.tab && key.shift) {
|
|
109
|
+
// Previous file (loop to last when at start)
|
|
110
|
+
setCurrentFileIndex((currentFileIndex - 1 + files.length) % files.length);
|
|
111
|
+
setScrollOffset(0);
|
|
112
|
+
}
|
|
113
|
+
}, { isActive: true });
|
|
114
|
+
const visibleLines = allLines.slice(scrollOffset, scrollOffset + viewportHeight);
|
|
115
|
+
const terminalWidth = process.stdout.columns || 80;
|
|
116
|
+
const columnWidth = Math.floor((terminalWidth - 6) / 2); // 6 for borders and separators
|
|
117
|
+
const getLineColor = (type) => {
|
|
118
|
+
switch (type) {
|
|
119
|
+
case 'add':
|
|
120
|
+
return 'green';
|
|
121
|
+
case 'remove':
|
|
122
|
+
return 'red';
|
|
123
|
+
case 'modified':
|
|
124
|
+
return undefined; // Will be handled separately for each side
|
|
125
|
+
case 'header':
|
|
126
|
+
return 'cyan';
|
|
127
|
+
default:
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
const truncateLine = (line, width) => {
|
|
132
|
+
if (line.length <= width)
|
|
133
|
+
return line.padEnd(width);
|
|
134
|
+
return line.substring(0, width - 1) + '…';
|
|
135
|
+
};
|
|
136
|
+
return (React.createElement(Box, { flexDirection: "column", flexGrow: 1 },
|
|
137
|
+
React.createElement(Box, { marginBottom: 1, flexDirection: "column" },
|
|
138
|
+
React.createElement(Box, null,
|
|
139
|
+
React.createElement(Text, { bold: true },
|
|
140
|
+
currentFile.path,
|
|
141
|
+
" (",
|
|
142
|
+
currentFileIndex + 1,
|
|
143
|
+
"/",
|
|
144
|
+
files.length,
|
|
145
|
+
")"),
|
|
146
|
+
React.createElement(Text, { dimColor: true },
|
|
147
|
+
' ',
|
|
148
|
+
"- ",
|
|
149
|
+
currentFile.additions,
|
|
150
|
+
" additions, ",
|
|
151
|
+
currentFile.deletions,
|
|
152
|
+
" deletions")),
|
|
153
|
+
React.createElement(Box, { height: 2, overflow: "hidden", flexDirection: "column" }, (() => {
|
|
154
|
+
const terminalWidth = process.stdout.columns || 80;
|
|
155
|
+
const maxWidth = terminalWidth - 4; // Leave some margin
|
|
156
|
+
// Generate file list with current file highlighted
|
|
157
|
+
const fileItems = [];
|
|
158
|
+
// Add files before current
|
|
159
|
+
for (let i = Math.max(0, currentFileIndex - 2); i < currentFileIndex; i++) {
|
|
160
|
+
fileItems.push({
|
|
161
|
+
text: files[i].path,
|
|
162
|
+
isActive: false,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
// Add current file
|
|
166
|
+
fileItems.push({
|
|
167
|
+
text: `[${files[currentFileIndex].path}]`,
|
|
168
|
+
isActive: true,
|
|
169
|
+
});
|
|
170
|
+
// Add files after current
|
|
171
|
+
for (let i = currentFileIndex + 1; i < Math.min(files.length, currentFileIndex + 3); i++) {
|
|
172
|
+
fileItems.push({
|
|
173
|
+
text: files[i].path,
|
|
174
|
+
isActive: false,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
// Build lines (max 2 lines)
|
|
178
|
+
const lines = [[]];
|
|
179
|
+
let currentLineWidth = 0;
|
|
180
|
+
for (const item of fileItems) {
|
|
181
|
+
const itemWidth = item.text.length + 3; // Include separator
|
|
182
|
+
if (currentLineWidth + itemWidth > maxWidth && lines.length < 2) {
|
|
183
|
+
lines.push([]);
|
|
184
|
+
currentLineWidth = 0;
|
|
185
|
+
}
|
|
186
|
+
if (lines.length <= 2) {
|
|
187
|
+
lines[lines.length - 1].push(item);
|
|
188
|
+
currentLineWidth += itemWidth;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return lines.map((line, lineIndex) => (React.createElement(Box, { key: lineIndex }, line.map((item, itemIndex) => (React.createElement(React.Fragment, { key: itemIndex },
|
|
192
|
+
itemIndex > 0 && React.createElement(Text, { dimColor: true }, " | "),
|
|
193
|
+
React.createElement(Text, { color: item.isActive ? 'cyan' : undefined, dimColor: !item.isActive }, item.text)))))));
|
|
194
|
+
})())),
|
|
195
|
+
React.createElement(Box, { borderStyle: "single", flexDirection: "column", flexGrow: 1 },
|
|
196
|
+
React.createElement(Box, { borderStyle: "single", borderTop: false, borderLeft: false, borderRight: false },
|
|
197
|
+
React.createElement(Box, { width: columnWidth },
|
|
198
|
+
React.createElement(Text, { dimColor: true }, " \u2502 "),
|
|
199
|
+
React.createElement(Text, { bold: true }, "Old")),
|
|
200
|
+
React.createElement(Text, { dimColor: true }, " \u2503 "),
|
|
201
|
+
React.createElement(Box, { width: columnWidth },
|
|
202
|
+
React.createElement(Text, { dimColor: true }, " \u2502 "),
|
|
203
|
+
React.createElement(Text, { bold: true }, "New"))),
|
|
204
|
+
React.createElement(Box, { flexDirection: "column", flexGrow: 1 }, visibleLines.map((line, index) => (React.createElement(Box, { key: `line-${scrollOffset + index}` },
|
|
205
|
+
React.createElement(Box, { width: columnWidth },
|
|
206
|
+
React.createElement(Text, { dimColor: true }, line.oldNum ? String(line.oldNum).padStart(4) : ' '),
|
|
207
|
+
React.createElement(Text, { dimColor: true }, " \u2502 "),
|
|
208
|
+
React.createElement(Text, { color: line.type === 'remove' || line.type === 'modified' ? 'red' : undefined, dimColor: line.type === 'header' }, line.type === 'remove' || line.type === 'modified' ? '- ' : ' '),
|
|
209
|
+
React.createElement(Text, { color: line.type === 'remove' || line.type === 'modified'
|
|
210
|
+
? 'red'
|
|
211
|
+
: getLineColor(line.type) }, line.old
|
|
212
|
+
? truncateLine(line.old, columnWidth - 10)
|
|
213
|
+
: ' '.repeat(columnWidth - 10))),
|
|
214
|
+
React.createElement(Text, { dimColor: true }, " \u2503 "),
|
|
215
|
+
React.createElement(Box, { width: columnWidth },
|
|
216
|
+
React.createElement(Text, { dimColor: true }, line.newNum ? String(line.newNum).padStart(4) : ' '),
|
|
217
|
+
React.createElement(Text, { dimColor: true }, " \u2502 "),
|
|
218
|
+
React.createElement(Text, { color: line.type === 'add' || line.type === 'modified' ? 'green' : undefined, dimColor: line.type === 'header' }, line.type === 'add' || line.type === 'modified' ? '+ ' : ' '),
|
|
219
|
+
React.createElement(Text, { color: line.type === 'add' || line.type === 'modified'
|
|
220
|
+
? 'green'
|
|
221
|
+
: getLineColor(line.type) }, line.new
|
|
222
|
+
? truncateLine(line.new, columnWidth - 10)
|
|
223
|
+
: ' '.repeat(columnWidth - 10)))))))),
|
|
224
|
+
React.createElement(Box, { marginTop: 1, justifyContent: "space-between" },
|
|
225
|
+
React.createElement(Text, { dimColor: true },
|
|
226
|
+
"Lines ",
|
|
227
|
+
scrollOffset + 1,
|
|
228
|
+
"-",
|
|
229
|
+
Math.min(scrollOffset + viewportHeight, allLines.length),
|
|
230
|
+
" of",
|
|
231
|
+
' ',
|
|
232
|
+
allLines.length,
|
|
233
|
+
scrollOffset + viewportHeight < allLines.length &&
|
|
234
|
+
` (${allLines.length - scrollOffset - viewportHeight} more)`),
|
|
235
|
+
React.createElement(Text, { dimColor: true }, "Tab: next file | Shift+Tab: prev file | \u2191\u2193/jk: scroll | ESC/b: back"))));
|
|
236
|
+
};
|
|
237
|
+
export default SideBySideDiffViewer;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
const StatusBar = ({ commitish, totalFiles, currentMode }) => {
|
|
4
|
+
return (React.createElement(Box, { borderStyle: "round", paddingX: 1, marginBottom: 1 },
|
|
5
|
+
React.createElement(Box, { flexGrow: 1 },
|
|
6
|
+
React.createElement(Text, { bold: true, color: "cyan" }, "\uD83D\uDCCB ReviewIt TUI"),
|
|
7
|
+
React.createElement(Text, null, " | "),
|
|
8
|
+
React.createElement(Text, { color: "yellow" }, commitish),
|
|
9
|
+
React.createElement(Text, null, " | "),
|
|
10
|
+
React.createElement(Text, null,
|
|
11
|
+
totalFiles,
|
|
12
|
+
" files changed")),
|
|
13
|
+
React.createElement(Box, null,
|
|
14
|
+
React.createElement(Text, { dimColor: true },
|
|
15
|
+
"[",
|
|
16
|
+
currentMode === 'list'
|
|
17
|
+
? 'File List'
|
|
18
|
+
: currentMode === 'side-by-side'
|
|
19
|
+
? 'Side-by-Side'
|
|
20
|
+
: 'Inline Diff',
|
|
21
|
+
"]"))));
|
|
22
|
+
};
|
|
23
|
+
export default StatusBar;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export function parseDiff(diffText) {
|
|
2
|
+
const lines = diffText.split('\n');
|
|
3
|
+
const chunks = [];
|
|
4
|
+
let currentChunk = null;
|
|
5
|
+
let oldLineNumber = 0;
|
|
6
|
+
let newLineNumber = 0;
|
|
7
|
+
for (let i = 0; i < lines.length; i++) {
|
|
8
|
+
const line = lines[i];
|
|
9
|
+
// Skip file headers
|
|
10
|
+
if (line.startsWith('diff --git') ||
|
|
11
|
+
line.startsWith('index ') ||
|
|
12
|
+
line.startsWith('---') ||
|
|
13
|
+
line.startsWith('+++')) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
// Chunk header
|
|
17
|
+
if (line.startsWith('@@')) {
|
|
18
|
+
const match = line.match(/@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@/);
|
|
19
|
+
if (match) {
|
|
20
|
+
const [, oldStart, oldLinesStr, newStart, newLinesStr] = match;
|
|
21
|
+
currentChunk = {
|
|
22
|
+
header: line,
|
|
23
|
+
oldStart: parseInt(oldStart),
|
|
24
|
+
oldLines: parseInt(oldLinesStr || '1'),
|
|
25
|
+
newStart: parseInt(newStart),
|
|
26
|
+
newLines: parseInt(newLinesStr || '1'),
|
|
27
|
+
lines: [],
|
|
28
|
+
};
|
|
29
|
+
chunks.push(currentChunk);
|
|
30
|
+
oldLineNumber = currentChunk.oldStart - 1;
|
|
31
|
+
newLineNumber = currentChunk.newStart - 1;
|
|
32
|
+
}
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
// Skip if no current chunk
|
|
36
|
+
if (!currentChunk)
|
|
37
|
+
continue;
|
|
38
|
+
// Parse diff lines
|
|
39
|
+
if (line.startsWith('+')) {
|
|
40
|
+
newLineNumber++;
|
|
41
|
+
currentChunk.lines.push({
|
|
42
|
+
type: 'add',
|
|
43
|
+
content: line.substring(1),
|
|
44
|
+
newLineNumber: newLineNumber,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
else if (line.startsWith('-')) {
|
|
48
|
+
oldLineNumber++;
|
|
49
|
+
currentChunk.lines.push({
|
|
50
|
+
type: 'remove',
|
|
51
|
+
content: line.substring(1),
|
|
52
|
+
oldLineNumber: oldLineNumber,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
// Context line
|
|
57
|
+
oldLineNumber++;
|
|
58
|
+
newLineNumber++;
|
|
59
|
+
currentChunk.lines.push({
|
|
60
|
+
type: 'context',
|
|
61
|
+
content: line.substring(1),
|
|
62
|
+
oldLineNumber: oldLineNumber,
|
|
63
|
+
newLineNumber: newLineNumber,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { chunks };
|
|
68
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface DiffFile {
|
|
2
|
+
path: string;
|
|
3
|
+
oldPath?: string;
|
|
4
|
+
status: 'modified' | 'added' | 'deleted' | 'renamed';
|
|
5
|
+
additions: number;
|
|
6
|
+
deletions: number;
|
|
7
|
+
chunks: DiffChunk[];
|
|
8
|
+
}
|
|
9
|
+
export interface DiffChunk {
|
|
10
|
+
header: string;
|
|
11
|
+
oldStart: number;
|
|
12
|
+
oldLines: number;
|
|
13
|
+
newStart: number;
|
|
14
|
+
newLines: number;
|
|
15
|
+
lines: DiffLine[];
|
|
16
|
+
}
|
|
17
|
+
export interface DiffLine {
|
|
18
|
+
type: 'add' | 'delete' | 'normal' | 'hunk';
|
|
19
|
+
content: string;
|
|
20
|
+
oldLineNumber?: number;
|
|
21
|
+
newLineNumber?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface DiffResponse {
|
|
24
|
+
commit: string;
|
|
25
|
+
files: DiffFile[];
|
|
26
|
+
}
|
|
27
|
+
export interface Comment {
|
|
28
|
+
id: string;
|
|
29
|
+
file: string;
|
|
30
|
+
line: number;
|
|
31
|
+
body: string;
|
|
32
|
+
timestamp: string;
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get file extension from filename or filepath
|
|
3
|
+
* @param filename - The filename or filepath
|
|
4
|
+
* @returns The file extension in lowercase, or null if no extension
|
|
5
|
+
*/
|
|
6
|
+
export declare function getFileExtension(filename: string): string | null;
|
|
7
|
+
/**
|
|
8
|
+
* Get filename from filepath
|
|
9
|
+
* @param filepath - The filepath
|
|
10
|
+
* @returns The filename
|
|
11
|
+
*/
|
|
12
|
+
export declare function getFileName(filepath: string): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get file extension from filename or filepath
|
|
3
|
+
* @param filename - The filename or filepath
|
|
4
|
+
* @returns The file extension in lowercase, or null if no extension
|
|
5
|
+
*/
|
|
6
|
+
export function getFileExtension(filename) {
|
|
7
|
+
if (!filename)
|
|
8
|
+
return null;
|
|
9
|
+
const extension = filename.split('.').pop()?.toLowerCase();
|
|
10
|
+
return extension || null;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Get filename from filepath
|
|
14
|
+
* @param filepath - The filepath
|
|
15
|
+
* @returns The filename
|
|
16
|
+
*/
|
|
17
|
+
export function getFileName(filepath) {
|
|
18
|
+
if (!filepath)
|
|
19
|
+
return '';
|
|
20
|
+
return filepath.split('/').pop() || '';
|
|
21
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "difit",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A lightweight command-line tool that spins up a local web server to display Git commit difit in a GitHub-like Files changed view",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=18.0.0"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"difit": "./dist/cli/index.js"
|
|
11
|
+
},
|
|
12
|
+
"main": "./dist/cli/index.js",
|
|
13
|
+
"homepage": "https://github.com/yoshiko-pg/difit",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/yoshiko-pg/difit.git"
|
|
17
|
+
},
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/yoshiko-pg/difit/issues"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"dev": "vite",
|
|
23
|
+
"build": "tsc && vite build",
|
|
24
|
+
"build:cli": "tsc --project tsconfig.cli.json",
|
|
25
|
+
"start": "node dist/cli/index.js",
|
|
26
|
+
"lint": "eslint . --ext .ts,.tsx",
|
|
27
|
+
"lint:fix": "eslint . --ext .ts,.tsx --fix",
|
|
28
|
+
"format": "prettier --write .",
|
|
29
|
+
"typecheck": "tsc --noEmit",
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"test:watch": "vitest",
|
|
32
|
+
"prepare": "lefthook install",
|
|
33
|
+
"prepublishOnly": "pnpm run build && pnpm run build:cli"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"commander": "^11.1.0",
|
|
37
|
+
"express": "^4.18.2",
|
|
38
|
+
"open": "^10.0.3",
|
|
39
|
+
"prism-react-renderer": "^2.4.1",
|
|
40
|
+
"prismjs": "^1.30.0",
|
|
41
|
+
"react": "^18.2.0",
|
|
42
|
+
"react-dom": "^18.2.0",
|
|
43
|
+
"shiki": "^0.14.7",
|
|
44
|
+
"simple-git": "^3.20.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@tailwindcss/forms": "^0.5.10",
|
|
48
|
+
"@tailwindcss/postcss": "^4.1.11",
|
|
49
|
+
"@tailwindcss/typography": "^0.5.16",
|
|
50
|
+
"@types/express": "^4.17.21",
|
|
51
|
+
"@types/node": "^20.10.5",
|
|
52
|
+
"@types/prismjs": "^1.26.5",
|
|
53
|
+
"@types/react": "^18.2.45",
|
|
54
|
+
"@types/react-dom": "^18.2.18",
|
|
55
|
+
"@typescript-eslint/eslint-plugin": "^6.15.0",
|
|
56
|
+
"@typescript-eslint/parser": "^6.15.0",
|
|
57
|
+
"@vitejs/plugin-react": "^4.2.1",
|
|
58
|
+
"autoprefixer": "^10.4.21",
|
|
59
|
+
"eslint": "^8.56.0",
|
|
60
|
+
"eslint-plugin-react": "^7.33.2",
|
|
61
|
+
"eslint-plugin-react-hooks": "^4.6.0",
|
|
62
|
+
"lefthook": "^1.5.5",
|
|
63
|
+
"postcss": "^8.5.6",
|
|
64
|
+
"prettier": "^3.1.1",
|
|
65
|
+
"tailwindcss": "^4.1.11",
|
|
66
|
+
"typescript": "^5.3.3",
|
|
67
|
+
"vite": "^5.0.10",
|
|
68
|
+
"vitest": "^1.1.0"
|
|
69
|
+
},
|
|
70
|
+
"files": [
|
|
71
|
+
"dist",
|
|
72
|
+
"README.md"
|
|
73
|
+
],
|
|
74
|
+
"keywords": [
|
|
75
|
+
"git",
|
|
76
|
+
"diff",
|
|
77
|
+
"cli",
|
|
78
|
+
"review",
|
|
79
|
+
"github",
|
|
80
|
+
"code-review",
|
|
81
|
+
"tailwind",
|
|
82
|
+
"react",
|
|
83
|
+
"diff-viewer"
|
|
84
|
+
],
|
|
85
|
+
"author": "yoshiko-pg",
|
|
86
|
+
"license": "MIT",
|
|
87
|
+
"publishConfig": {
|
|
88
|
+
"access": "public"
|
|
89
|
+
},
|
|
90
|
+
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
|
91
|
+
}
|