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.
- package/README.md +198 -0
- package/bin/trident-git.mjs +153 -0
- package/eslint.config.mjs +18 -0
- package/next.config.ts +30 -0
- package/package.json +60 -0
- package/postcss.config.mjs +7 -0
- package/public/favicon.png +0 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/app/api/credentials/route.ts +113 -0
- package/src/app/api/custom-scripts/route.ts +203 -0
- package/src/app/api/fs/route.ts +75 -0
- package/src/app/api/git/action/route.ts +383 -0
- package/src/app/api/git/branches/route.ts +20 -0
- package/src/app/api/git/diff/route.ts +104 -0
- package/src/app/api/git/log/route.ts +28 -0
- package/src/app/api/git/status/route.ts +28 -0
- package/src/app/api/repos/route.ts +84 -0
- package/src/app/api/settings/route.ts +37 -0
- package/src/app/credentials/page.tsx +408 -0
- package/src/app/globals.css +109 -0
- package/src/app/icon.png +0 -0
- package/src/app/layout.tsx +38 -0
- package/src/app/page.tsx +10 -0
- package/src/app/providers.tsx +21 -0
- package/src/app/workspace/changes/page.tsx +27 -0
- package/src/app/workspace/custom-scripts/page.tsx +247 -0
- package/src/app/workspace/history/page.tsx +27 -0
- package/src/app/workspace/layout.tsx +26 -0
- package/src/app/workspace/page.tsx +27 -0
- package/src/app/workspace/settings/page.tsx +233 -0
- package/src/app/workspace/stashes/page.tsx +395 -0
- package/src/components/command-palette.tsx +178 -0
- package/src/components/context-menu.tsx +200 -0
- package/src/components/fs-browser.tsx +154 -0
- package/src/components/git/diff-view.tsx +137 -0
- package/src/components/git/git-graph.tsx +489 -0
- package/src/components/git/grouped-diff-viewer.tsx +332 -0
- package/src/components/git/history-view.tsx +4862 -0
- package/src/components/git/image-diff-view.tsx +342 -0
- package/src/components/git/status-view.tsx +597 -0
- package/src/components/home-settings-modal.tsx +192 -0
- package/src/components/layout/sidebar.tsx +256 -0
- package/src/components/repo-list.tsx +206 -0
- package/src/components/theme-toggle.tsx +37 -0
- package/src/components/toaster.tsx +36 -0
- package/src/components/workspace-repo-open-tracker.tsx +39 -0
- package/src/hooks/use-credentials.ts +123 -0
- package/src/hooks/use-escape-dismiss.ts +72 -0
- package/src/hooks/use-git.ts +448 -0
- package/src/hooks/use-toast.ts +280 -0
- package/src/hooks/use-workspace-title.ts +23 -0
- package/src/lib/api-utils.ts +24 -0
- package/src/lib/branch-colors.ts +98 -0
- package/src/lib/credentials.ts +404 -0
- package/src/lib/git.ts +1510 -0
- package/src/lib/graph-utils.ts +253 -0
- package/src/lib/store.ts +145 -0
- package/src/lib/types.ts +95 -0
- package/src/lib/utils.ts +266 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
|
|
5
|
+
const TOAST_LIMIT = 5
|
|
6
|
+
const TOAST_REMOVE_DELAY = 5000
|
|
7
|
+
|
|
8
|
+
// Helper to copy text to clipboard
|
|
9
|
+
async function copyToClipboard(text: string): Promise<boolean> {
|
|
10
|
+
try {
|
|
11
|
+
await navigator.clipboard.writeText(text);
|
|
12
|
+
return true;
|
|
13
|
+
} catch {
|
|
14
|
+
// Fallback for older browsers
|
|
15
|
+
const textArea = document.createElement('textarea');
|
|
16
|
+
textArea.value = text;
|
|
17
|
+
textArea.style.position = 'fixed';
|
|
18
|
+
textArea.style.left = '-9999px';
|
|
19
|
+
document.body.appendChild(textArea);
|
|
20
|
+
textArea.select();
|
|
21
|
+
try {
|
|
22
|
+
document.execCommand('copy');
|
|
23
|
+
return true;
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
} finally {
|
|
27
|
+
document.body.removeChild(textArea);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type ToastType = "default" | "success" | "warning" | "error" | "info"
|
|
33
|
+
|
|
34
|
+
export interface Toast {
|
|
35
|
+
id: string
|
|
36
|
+
title?: React.ReactNode
|
|
37
|
+
description?: React.ReactNode
|
|
38
|
+
type?: ToastType
|
|
39
|
+
variant?: "default" | "destructive" | "warning" // For compatibility
|
|
40
|
+
duration?: number
|
|
41
|
+
action?: React.ReactNode
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let count = 0
|
|
45
|
+
|
|
46
|
+
function genId() {
|
|
47
|
+
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
|
48
|
+
return count.toString()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type Action =
|
|
52
|
+
| {
|
|
53
|
+
type: "ADD_TOAST"
|
|
54
|
+
toast: Toast
|
|
55
|
+
}
|
|
56
|
+
| {
|
|
57
|
+
type: "UPDATE_TOAST"
|
|
58
|
+
toast: Partial<Toast>
|
|
59
|
+
}
|
|
60
|
+
| {
|
|
61
|
+
type: "DISMISS_TOAST"
|
|
62
|
+
toastId?: string
|
|
63
|
+
}
|
|
64
|
+
| {
|
|
65
|
+
type: "REMOVE_TOAST"
|
|
66
|
+
toastId?: string
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface State {
|
|
70
|
+
toasts: Toast[]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
|
74
|
+
|
|
75
|
+
const addToRemoveQueue = (toastId: string, duration = TOAST_REMOVE_DELAY) => {
|
|
76
|
+
if (toastTimeouts.has(toastId)) {
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const timeout = setTimeout(() => {
|
|
81
|
+
toastTimeouts.delete(toastId)
|
|
82
|
+
dispatch({
|
|
83
|
+
type: "REMOVE_TOAST",
|
|
84
|
+
toastId: toastId,
|
|
85
|
+
})
|
|
86
|
+
}, duration)
|
|
87
|
+
|
|
88
|
+
toastTimeouts.set(toastId, timeout)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const reducer = (state: State, action: Action): State => {
|
|
92
|
+
switch (action.type) {
|
|
93
|
+
case "ADD_TOAST":
|
|
94
|
+
return {
|
|
95
|
+
...state,
|
|
96
|
+
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
case "UPDATE_TOAST":
|
|
100
|
+
return {
|
|
101
|
+
...state,
|
|
102
|
+
toasts: state.toasts.map((t) =>
|
|
103
|
+
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
|
104
|
+
),
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
case "DISMISS_TOAST": {
|
|
108
|
+
const { toastId } = action
|
|
109
|
+
|
|
110
|
+
if (toastId) {
|
|
111
|
+
addToRemoveQueue(toastId, 0) // Remove immediately for dismiss
|
|
112
|
+
} else {
|
|
113
|
+
state.toasts.forEach((toast) => {
|
|
114
|
+
addToRemoveQueue(toast.id, 0)
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
...state,
|
|
120
|
+
toasts: state.toasts.filter((t) => t.id !== toastId && toastId !== undefined),
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
case "REMOVE_TOAST":
|
|
124
|
+
if (action.toastId === undefined) {
|
|
125
|
+
return {
|
|
126
|
+
...state,
|
|
127
|
+
toasts: [],
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
...state,
|
|
132
|
+
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const listeners: Array<(state: State) => void> = []
|
|
138
|
+
|
|
139
|
+
let memoryState: State = { toasts: [] }
|
|
140
|
+
|
|
141
|
+
function dispatch(action: Action) {
|
|
142
|
+
memoryState = reducer(memoryState, action)
|
|
143
|
+
listeners.forEach((listener) => {
|
|
144
|
+
listener(memoryState)
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function toast(props: Omit<Toast, "id">) {
|
|
149
|
+
const id = genId()
|
|
150
|
+
|
|
151
|
+
const update = (props: Toast) =>
|
|
152
|
+
dispatch({
|
|
153
|
+
type: "UPDATE_TOAST",
|
|
154
|
+
toast: { ...props, id },
|
|
155
|
+
})
|
|
156
|
+
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
|
157
|
+
|
|
158
|
+
dispatch({
|
|
159
|
+
type: "ADD_TOAST",
|
|
160
|
+
toast: {
|
|
161
|
+
...props,
|
|
162
|
+
id,
|
|
163
|
+
},
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
// Auto remove
|
|
167
|
+
addToRemoveQueue(id, props.duration || TOAST_REMOVE_DELAY)
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
id: id,
|
|
171
|
+
dismiss,
|
|
172
|
+
update,
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function useToast() {
|
|
177
|
+
const [state, setState] = React.useState<State>(memoryState)
|
|
178
|
+
|
|
179
|
+
React.useEffect(() => {
|
|
180
|
+
listeners.push(setState)
|
|
181
|
+
return () => {
|
|
182
|
+
const index = listeners.indexOf(setState)
|
|
183
|
+
if (index > -1) {
|
|
184
|
+
listeners.splice(index, 1)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}, [state])
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
...state,
|
|
191
|
+
toast,
|
|
192
|
+
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Git error toast helper - shows a destructive toast with copy button
|
|
197
|
+
interface GitErrorToastOptions {
|
|
198
|
+
title?: string;
|
|
199
|
+
operation?: string;
|
|
200
|
+
onFix?: () => void | Promise<void>;
|
|
201
|
+
fixLabel?: string;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function showGitErrorToast(error: Error | string, options: GitErrorToastOptions = {}) {
|
|
205
|
+
const errorMessage = typeof error === 'string' ? error : error.message;
|
|
206
|
+
const title = options.title || (options.operation ? `${options.operation} Failed` : 'Git Operation Failed');
|
|
207
|
+
|
|
208
|
+
const id = genId();
|
|
209
|
+
|
|
210
|
+
// Create a stateful component for the copy button
|
|
211
|
+
const CopyableErrorDescription = () => {
|
|
212
|
+
const [copied, setCopied] = React.useState(false);
|
|
213
|
+
const [fixing, setFixing] = React.useState(false);
|
|
214
|
+
|
|
215
|
+
const handleCopy = async (e: React.MouseEvent) => {
|
|
216
|
+
e.stopPropagation();
|
|
217
|
+
const success = await copyToClipboard(errorMessage);
|
|
218
|
+
if (success) {
|
|
219
|
+
setCopied(true);
|
|
220
|
+
setTimeout(() => setCopied(false), 2000);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const handleFix = async (e: React.MouseEvent) => {
|
|
225
|
+
e.stopPropagation();
|
|
226
|
+
if (!options.onFix) return;
|
|
227
|
+
|
|
228
|
+
setFixing(true);
|
|
229
|
+
try {
|
|
230
|
+
await options.onFix();
|
|
231
|
+
dispatch({ type: "DISMISS_TOAST", toastId: id });
|
|
232
|
+
} catch (err) {
|
|
233
|
+
console.error("Fix failed", err);
|
|
234
|
+
setFixing(false);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// Container
|
|
239
|
+
return React.createElement('div', {
|
|
240
|
+
className: 'mt-2 space-y-3'
|
|
241
|
+
},
|
|
242
|
+
// Error message box
|
|
243
|
+
React.createElement('div', {
|
|
244
|
+
className: 'max-h-[120px] overflow-y-auto whitespace-pre-wrap font-mono text-[11px] leading-relaxed bg-black/20 p-3 rounded-md break-all text-error-content/90 border border-error-content/20'
|
|
245
|
+
}, errorMessage),
|
|
246
|
+
// Buttons container
|
|
247
|
+
React.createElement('div', { className: 'flex gap-2' },
|
|
248
|
+
// Copy button
|
|
249
|
+
React.createElement('button', {
|
|
250
|
+
onClick: handleCopy,
|
|
251
|
+
type: 'button',
|
|
252
|
+
className: `btn btn-xs ${copied ? 'btn-success text-white' : 'bg-white/20 hover:bg-white/30 text-white border-white/20'}`,
|
|
253
|
+
},
|
|
254
|
+
copied ? 'Copied!' : 'Copy Error'
|
|
255
|
+
),
|
|
256
|
+
// Fix button
|
|
257
|
+
options.onFix ? React.createElement('button', {
|
|
258
|
+
onClick: handleFix,
|
|
259
|
+
type: 'button',
|
|
260
|
+
disabled: fixing,
|
|
261
|
+
className: 'btn btn-xs btn-warning text-white',
|
|
262
|
+
}, fixing ? 'Fixing...' : (options.fixLabel || 'Fix Issue')) : null
|
|
263
|
+
)
|
|
264
|
+
);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
toast({
|
|
268
|
+
type: "error",
|
|
269
|
+
title,
|
|
270
|
+
description: React.createElement(CopyableErrorDescription),
|
|
271
|
+
duration: 10000 // Long duration for errors
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
id,
|
|
276
|
+
dismiss: () => dispatch({ type: "DISMISS_TOAST", toastId: id }),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export { useToast, toast, showGitErrorToast }
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
import { useRepository } from './use-git';
|
|
5
|
+
import { getRepoFolderName, getRepositoryDisplayName } from '@/lib/utils';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Updates the document title with the repo name and page name.
|
|
9
|
+
* @param repoPath - The full path to the repository (e.g., "C:\\Users\\user\\projects\\repo" or "/Users/user/projects/repo")
|
|
10
|
+
* @param pageName - The name of the current page (e.g., "History", "Changes", "Stashes", "Settings")
|
|
11
|
+
*/
|
|
12
|
+
export function useWorkspaceTitle(repoPath: string | null, pageName: string) {
|
|
13
|
+
const repository = useRepository(repoPath);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const repoName = repoPath
|
|
17
|
+
? repository
|
|
18
|
+
? getRepositoryDisplayName(repository)
|
|
19
|
+
: getRepoFolderName(repoPath)
|
|
20
|
+
: 'Workspace';
|
|
21
|
+
document.title = `${repoName} | ${pageName}`;
|
|
22
|
+
}, [repoPath, repository, pageName]);
|
|
23
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Handles errors in Git API routes.
|
|
6
|
+
*
|
|
7
|
+
* @param error The error object caught in the try-catch block.
|
|
8
|
+
* @returns A NextResponse with the appropriate error message and status code.
|
|
9
|
+
*/
|
|
10
|
+
export function handleGitError(error: unknown) {
|
|
11
|
+
console.error('Git API Error:', error);
|
|
12
|
+
|
|
13
|
+
if (error instanceof z.ZodError) {
|
|
14
|
+
return NextResponse.json({ error: error.issues }, { status: 400 });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
18
|
+
|
|
19
|
+
if (message.includes('not a git repository')) {
|
|
20
|
+
return NextResponse.json({ error: 'Not a git repository' }, { status: 400 });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
24
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export interface BranchTagColors {
|
|
2
|
+
backgroundColor: string;
|
|
3
|
+
textColor: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function hashString(input: string): number {
|
|
7
|
+
let hash = 2166136261;
|
|
8
|
+
for (let i = 0; i < input.length; i++) {
|
|
9
|
+
hash ^= input.charCodeAt(i);
|
|
10
|
+
hash = Math.imul(hash, 16777619);
|
|
11
|
+
}
|
|
12
|
+
return hash >>> 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getBranchColorSeed(branchName: string): number {
|
|
16
|
+
return hashString(branchName);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function hslToRgb(h: number, s: number, l: number): { r: number; g: number; b: number } {
|
|
20
|
+
const sat = s / 100;
|
|
21
|
+
const light = l / 100;
|
|
22
|
+
const c = (1 - Math.abs(2 * light - 1)) * sat;
|
|
23
|
+
const hh = h / 60;
|
|
24
|
+
const x = c * (1 - Math.abs((hh % 2) - 1));
|
|
25
|
+
|
|
26
|
+
let r1 = 0;
|
|
27
|
+
let g1 = 0;
|
|
28
|
+
let b1 = 0;
|
|
29
|
+
|
|
30
|
+
if (hh >= 0 && hh < 1) {
|
|
31
|
+
r1 = c;
|
|
32
|
+
g1 = x;
|
|
33
|
+
} else if (hh >= 1 && hh < 2) {
|
|
34
|
+
r1 = x;
|
|
35
|
+
g1 = c;
|
|
36
|
+
} else if (hh >= 2 && hh < 3) {
|
|
37
|
+
g1 = c;
|
|
38
|
+
b1 = x;
|
|
39
|
+
} else if (hh >= 3 && hh < 4) {
|
|
40
|
+
g1 = x;
|
|
41
|
+
b1 = c;
|
|
42
|
+
} else if (hh >= 4 && hh < 5) {
|
|
43
|
+
r1 = x;
|
|
44
|
+
b1 = c;
|
|
45
|
+
} else {
|
|
46
|
+
r1 = c;
|
|
47
|
+
b1 = x;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const m = light - c / 2;
|
|
51
|
+
return {
|
|
52
|
+
r: Math.round((r1 + m) * 255),
|
|
53
|
+
g: Math.round((g1 + m) * 255),
|
|
54
|
+
b: Math.round((b1 + m) * 255),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function relativeLuminance({ r, g, b }: { r: number; g: number; b: number }): number {
|
|
59
|
+
const toLinear = (channel: number) => {
|
|
60
|
+
const value = channel / 255;
|
|
61
|
+
return value <= 0.03928 ? value / 12.92 : Math.pow((value + 0.055) / 1.055, 2.4);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const rLin = toLinear(r);
|
|
65
|
+
const gLin = toLinear(g);
|
|
66
|
+
const bLin = toLinear(b);
|
|
67
|
+
return 0.2126 * rLin + 0.7152 * gLin + 0.0722 * bLin;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getReadableTextColor(rgb: { r: number; g: number; b: number }): string {
|
|
71
|
+
const backgroundLum = relativeLuminance(rgb);
|
|
72
|
+
const whiteContrast = (1.05) / (backgroundLum + 0.05);
|
|
73
|
+
const blackContrast = (backgroundLum + 0.05) / 0.05;
|
|
74
|
+
return whiteContrast >= blackContrast ? '#ffffff' : '#111827';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getBranchTagColors(branchName: string): BranchTagColors {
|
|
78
|
+
const hash = getBranchColorSeed(branchName);
|
|
79
|
+
|
|
80
|
+
const hue = hash % 360;
|
|
81
|
+
const saturation = 72 + ((hash >>> 9) % 20); // 72-91
|
|
82
|
+
const lightness = 84 + ((hash >>> 17) % 8); // 84-91
|
|
83
|
+
|
|
84
|
+
const rgb = hslToRgb(hue, saturation, lightness);
|
|
85
|
+
const textColor = getReadableTextColor(rgb);
|
|
86
|
+
const backgroundColor = `hsl(${hue} ${saturation}% ${lightness}%)`;
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
backgroundColor,
|
|
90
|
+
textColor,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function getBranchGraphColor(branchName: string): string {
|
|
95
|
+
const hash = getBranchColorSeed(branchName);
|
|
96
|
+
const hue = hash % 360;
|
|
97
|
+
return `hsl(${hue} 82% 52%)`;
|
|
98
|
+
}
|