trident-git 0.2.0

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.
Files changed (64) hide show
  1. package/README.md +198 -0
  2. package/bin/trident-git.mjs +153 -0
  3. package/eslint.config.mjs +18 -0
  4. package/next.config.ts +30 -0
  5. package/package.json +60 -0
  6. package/postcss.config.mjs +7 -0
  7. package/public/favicon.png +0 -0
  8. package/public/file.svg +1 -0
  9. package/public/globe.svg +1 -0
  10. package/public/next.svg +1 -0
  11. package/public/vercel.svg +1 -0
  12. package/public/window.svg +1 -0
  13. package/src/app/api/credentials/route.ts +113 -0
  14. package/src/app/api/custom-scripts/route.ts +203 -0
  15. package/src/app/api/fs/route.ts +75 -0
  16. package/src/app/api/git/action/route.ts +383 -0
  17. package/src/app/api/git/branches/route.ts +20 -0
  18. package/src/app/api/git/diff/route.ts +104 -0
  19. package/src/app/api/git/log/route.ts +28 -0
  20. package/src/app/api/git/status/route.ts +28 -0
  21. package/src/app/api/repos/route.ts +84 -0
  22. package/src/app/api/settings/route.ts +37 -0
  23. package/src/app/credentials/page.tsx +408 -0
  24. package/src/app/globals.css +109 -0
  25. package/src/app/icon.png +0 -0
  26. package/src/app/layout.tsx +38 -0
  27. package/src/app/page.tsx +10 -0
  28. package/src/app/providers.tsx +21 -0
  29. package/src/app/workspace/changes/page.tsx +27 -0
  30. package/src/app/workspace/custom-scripts/page.tsx +247 -0
  31. package/src/app/workspace/history/page.tsx +27 -0
  32. package/src/app/workspace/layout.tsx +26 -0
  33. package/src/app/workspace/page.tsx +27 -0
  34. package/src/app/workspace/settings/page.tsx +233 -0
  35. package/src/app/workspace/stashes/page.tsx +395 -0
  36. package/src/components/command-palette.tsx +178 -0
  37. package/src/components/context-menu.tsx +200 -0
  38. package/src/components/fs-browser.tsx +154 -0
  39. package/src/components/git/diff-view.tsx +137 -0
  40. package/src/components/git/git-graph.tsx +489 -0
  41. package/src/components/git/grouped-diff-viewer.tsx +332 -0
  42. package/src/components/git/history-view.tsx +4862 -0
  43. package/src/components/git/image-diff-view.tsx +342 -0
  44. package/src/components/git/status-view.tsx +597 -0
  45. package/src/components/home-settings-modal.tsx +192 -0
  46. package/src/components/layout/sidebar.tsx +256 -0
  47. package/src/components/repo-list.tsx +206 -0
  48. package/src/components/theme-toggle.tsx +37 -0
  49. package/src/components/toaster.tsx +36 -0
  50. package/src/components/workspace-repo-open-tracker.tsx +39 -0
  51. package/src/hooks/use-credentials.ts +123 -0
  52. package/src/hooks/use-escape-dismiss.ts +72 -0
  53. package/src/hooks/use-git.ts +448 -0
  54. package/src/hooks/use-toast.ts +280 -0
  55. package/src/hooks/use-workspace-title.ts +23 -0
  56. package/src/lib/api-utils.ts +24 -0
  57. package/src/lib/branch-colors.ts +98 -0
  58. package/src/lib/credentials.ts +404 -0
  59. package/src/lib/git.ts +1510 -0
  60. package/src/lib/graph-utils.ts +253 -0
  61. package/src/lib/store.ts +145 -0
  62. package/src/lib/types.ts +95 -0
  63. package/src/lib/utils.ts +266 -0
  64. package/tsconfig.json +34 -0
@@ -0,0 +1,332 @@
1
+ 'use client';
2
+
3
+ import { cn } from '@/lib/utils';
4
+ import ReactDiffViewer from '@alexbruf/react-diff-viewer';
5
+ import { computeLineInformation, DiffType, LineInformation } from '@alexbruf/react-diff-viewer/compute-lines';
6
+ import '@alexbruf/react-diff-viewer/index.css';
7
+ import type { ReactNode } from 'react';
8
+ import { useMemo, useState } from 'react';
9
+
10
+ interface GroupedDiffViewerProps {
11
+ oldValue: string;
12
+ newValue: string;
13
+ splitView: boolean;
14
+ useDarkTheme?: boolean;
15
+ linesOffset?: number;
16
+ showDiffOnly?: boolean;
17
+ extraLinesSurroundingDiff?: number;
18
+ }
19
+
20
+ type InlineEntry =
21
+ | { type: 'line'; line: LineInformation; sourceIndex: number }
22
+ | { type: 'fold'; foldStartIndex: number; totalLines: number };
23
+
24
+ interface InlineRow {
25
+ key: string;
26
+ type: DiffType;
27
+ leftLineNumber: number | null;
28
+ rightLineNumber: number | null;
29
+ value: string;
30
+ }
31
+
32
+ const NO_EXPANDED_BLOCKS: number[] = [];
33
+
34
+ function isPairedReplacement(line: LineInformation): boolean {
35
+ return line.left?.type === DiffType.REMOVED && line.right?.type === DiffType.ADDED;
36
+ }
37
+
38
+ function getLineText(value: unknown): string {
39
+ if (typeof value === 'string') {
40
+ return value.length > 0 ? value : ' ';
41
+ }
42
+ return ' ';
43
+ }
44
+
45
+ function toInlineRow(line: LineInformation, sourceIndex: number): InlineRow {
46
+ const left = line.left ?? {};
47
+ const right = line.right ?? {};
48
+
49
+ if (left.type === DiffType.REMOVED) {
50
+ return {
51
+ key: `line-${sourceIndex}-removed`,
52
+ type: DiffType.REMOVED,
53
+ leftLineNumber: left.lineNumber ?? null,
54
+ rightLineNumber: null,
55
+ value: getLineText(left.value),
56
+ };
57
+ }
58
+
59
+ if (right.type === DiffType.ADDED) {
60
+ return {
61
+ key: `line-${sourceIndex}-added`,
62
+ type: DiffType.ADDED,
63
+ leftLineNumber: null,
64
+ rightLineNumber: right.lineNumber ?? null,
65
+ value: getLineText(right.value),
66
+ };
67
+ }
68
+
69
+ return {
70
+ key: `line-${sourceIndex}-default`,
71
+ type: DiffType.DEFAULT,
72
+ leftLineNumber: left.lineNumber ?? null,
73
+ rightLineNumber: right.lineNumber ?? null,
74
+ value: getLineText(left.value ?? right.value),
75
+ };
76
+ }
77
+
78
+ function renderInlineRow(row: InlineRow) {
79
+ const isAdded = row.type === DiffType.ADDED;
80
+ const isRemoved = row.type === DiffType.REMOVED;
81
+
82
+ return (
83
+ <tr key={row.key} className="line">
84
+ <td
85
+ className={cn('gutter', {
86
+ 'empty-gutter': row.leftLineNumber === null,
87
+ 'diff-added': isAdded,
88
+ 'diff-removed': isRemoved,
89
+ })}
90
+ >
91
+ <pre className="line-number">{row.leftLineNumber ?? ''}</pre>
92
+ </td>
93
+ <td
94
+ className={cn('gutter', {
95
+ 'empty-gutter': row.rightLineNumber === null,
96
+ 'diff-added': isAdded,
97
+ 'diff-removed': isRemoved,
98
+ })}
99
+ >
100
+ <pre className="line-number">{row.rightLineNumber ?? ''}</pre>
101
+ </td>
102
+ <td
103
+ className={cn('marker', {
104
+ 'diff-added': isAdded,
105
+ 'diff-removed': isRemoved,
106
+ })}
107
+ >
108
+ <pre>{isAdded ? '+' : isRemoved ? '-' : ''}</pre>
109
+ </td>
110
+ <td
111
+ className={cn('content', {
112
+ 'diff-added': isAdded,
113
+ 'diff-removed': isRemoved,
114
+ })}
115
+ >
116
+ <pre className="content-text">{row.value}</pre>
117
+ </td>
118
+ </tr>
119
+ );
120
+ }
121
+
122
+ function GroupedInlineDiff({
123
+ oldValue,
124
+ newValue,
125
+ useDarkTheme = false,
126
+ linesOffset = 0,
127
+ showDiffOnly = true,
128
+ extraLinesSurroundingDiff = 3,
129
+ }: Omit<GroupedDiffViewerProps, 'splitView'>) {
130
+ const [expandedByDiff, setExpandedByDiff] = useState<{ signature: string; blocks: number[] }>({
131
+ signature: '',
132
+ blocks: [],
133
+ });
134
+
135
+ const diffSignature = useMemo(
136
+ () => `${linesOffset}:${showDiffOnly ? 1 : 0}:${extraLinesSurroundingDiff}:${oldValue}\0${newValue}`,
137
+ [oldValue, newValue, linesOffset, showDiffOnly, extraLinesSurroundingDiff]
138
+ );
139
+
140
+ const expandedBlocks = useMemo(
141
+ () =>
142
+ expandedByDiff.signature === diffSignature
143
+ ? expandedByDiff.blocks
144
+ : NO_EXPANDED_BLOCKS,
145
+ [expandedByDiff, diffSignature]
146
+ );
147
+
148
+ const { lineInformation, diffLines } = useMemo(
149
+ () => computeLineInformation(oldValue, newValue, true, undefined, linesOffset),
150
+ [oldValue, newValue, linesOffset]
151
+ );
152
+
153
+ const entries = useMemo<InlineEntry[]>(() => {
154
+ if (lineInformation.length === 0) return [];
155
+
156
+ if (!showDiffOnly || diffLines.length === 0) {
157
+ return lineInformation.map((line, sourceIndex) => ({ type: 'line', line, sourceIndex }));
158
+ }
159
+
160
+ const contextSize = Math.max(0, extraLinesSurroundingDiff);
161
+ const visible = Array.from({ length: lineInformation.length }, () => false);
162
+
163
+ diffLines.forEach((diffIndex) => {
164
+ const start = Math.max(0, diffIndex - contextSize);
165
+ const end = Math.min(lineInformation.length - 1, diffIndex + contextSize);
166
+ for (let index = start; index <= end; index += 1) {
167
+ visible[index] = true;
168
+ }
169
+ });
170
+
171
+ for (let index = 0; index < visible.length; index += 1) {
172
+ if (visible[index]) continue;
173
+ const foldStartIndex = index;
174
+ while (index < visible.length && !visible[index]) {
175
+ index += 1;
176
+ }
177
+ if (expandedBlocks.includes(foldStartIndex)) {
178
+ for (let expandedIndex = foldStartIndex; expandedIndex < index; expandedIndex += 1) {
179
+ visible[expandedIndex] = true;
180
+ }
181
+ }
182
+ index -= 1;
183
+ }
184
+
185
+ const nextEntries: InlineEntry[] = [];
186
+ for (let index = 0; index < visible.length; index += 1) {
187
+ if (visible[index]) {
188
+ nextEntries.push({
189
+ type: 'line',
190
+ line: lineInformation[index],
191
+ sourceIndex: index,
192
+ });
193
+ continue;
194
+ }
195
+
196
+ const foldStartIndex = index;
197
+ while (index < visible.length && !visible[index]) {
198
+ index += 1;
199
+ }
200
+ nextEntries.push({
201
+ type: 'fold',
202
+ foldStartIndex,
203
+ totalLines: index - foldStartIndex,
204
+ });
205
+ index -= 1;
206
+ }
207
+
208
+ return nextEntries;
209
+ }, [lineInformation, diffLines, showDiffOnly, extraLinesSurroundingDiff, expandedBlocks]);
210
+
211
+ const rows = useMemo(() => {
212
+ const renderedRows: ReactNode[] = [];
213
+
214
+ for (let index = 0; index < entries.length; index += 1) {
215
+ const entry = entries[index];
216
+ if (entry.type === 'fold') {
217
+ renderedRows.push(
218
+ <tr key={`fold-${entry.foldStartIndex}`} className="code-fold">
219
+ <td className="code-fold-gutter" />
220
+ <td className="code-fold-gutter" />
221
+ <td />
222
+ <td>
223
+ <button
224
+ type="button"
225
+ className="cursor-pointer underline"
226
+ onClick={() => {
227
+ setExpandedByDiff((previous) => {
228
+ const currentBlocks =
229
+ previous.signature === diffSignature ? previous.blocks : [];
230
+ if (currentBlocks.includes(entry.foldStartIndex)) {
231
+ return previous.signature === diffSignature
232
+ ? previous
233
+ : { signature: diffSignature, blocks: currentBlocks };
234
+ }
235
+ return {
236
+ signature: diffSignature,
237
+ blocks: [...currentBlocks, entry.foldStartIndex],
238
+ };
239
+ });
240
+ }}
241
+ >
242
+ <pre className="code-fold-content">Expand {entry.totalLines} lines ...</pre>
243
+ </button>
244
+ </td>
245
+ </tr>
246
+ );
247
+ continue;
248
+ }
249
+
250
+ if (isPairedReplacement(entry.line)) {
251
+ const removedRows: InlineRow[] = [];
252
+ const addedRows: InlineRow[] = [];
253
+ let pairedIndex = index;
254
+
255
+ while (pairedIndex < entries.length) {
256
+ const candidate = entries[pairedIndex];
257
+ if (candidate.type !== 'line' || !isPairedReplacement(candidate.line)) {
258
+ break;
259
+ }
260
+ removedRows.push({
261
+ key: `line-${candidate.sourceIndex}-paired-removed`,
262
+ type: DiffType.REMOVED,
263
+ leftLineNumber: candidate.line.left?.lineNumber ?? null,
264
+ rightLineNumber: null,
265
+ value: getLineText(candidate.line.left?.value),
266
+ });
267
+ addedRows.push({
268
+ key: `line-${candidate.sourceIndex}-paired-added`,
269
+ type: DiffType.ADDED,
270
+ leftLineNumber: null,
271
+ rightLineNumber: candidate.line.right?.lineNumber ?? null,
272
+ value: getLineText(candidate.line.right?.value),
273
+ });
274
+ pairedIndex += 1;
275
+ }
276
+
277
+ removedRows.forEach((row) => {
278
+ renderedRows.push(renderInlineRow(row));
279
+ });
280
+ addedRows.forEach((row) => {
281
+ renderedRows.push(renderInlineRow(row));
282
+ });
283
+
284
+ index = pairedIndex - 1;
285
+ continue;
286
+ }
287
+
288
+ renderedRows.push(renderInlineRow(toInlineRow(entry.line, entry.sourceIndex)));
289
+ }
290
+
291
+ return renderedRows;
292
+ }, [entries, diffSignature]);
293
+
294
+ return (
295
+ <table className={cn('diff-container', useDarkTheme ? 'dark-theme' : 'light-theme')}>
296
+ <tbody>{rows}</tbody>
297
+ </table>
298
+ );
299
+ }
300
+
301
+ export function GroupedDiffViewer({
302
+ oldValue,
303
+ newValue,
304
+ splitView,
305
+ useDarkTheme = false,
306
+ linesOffset = 0,
307
+ showDiffOnly = true,
308
+ extraLinesSurroundingDiff = 3,
309
+ }: GroupedDiffViewerProps) {
310
+ if (splitView) {
311
+ return (
312
+ <ReactDiffViewer
313
+ oldValue={oldValue}
314
+ newValue={newValue}
315
+ splitView={true}
316
+ useDarkTheme={useDarkTheme}
317
+ disableWordDiff={true}
318
+ />
319
+ );
320
+ }
321
+
322
+ return (
323
+ <GroupedInlineDiff
324
+ oldValue={oldValue}
325
+ newValue={newValue}
326
+ useDarkTheme={useDarkTheme}
327
+ linesOffset={linesOffset}
328
+ showDiffOnly={showDiffOnly}
329
+ extraLinesSurroundingDiff={extraLinesSurroundingDiff}
330
+ />
331
+ );
332
+ }