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,104 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { GitService } from '@/lib/git';
|
|
3
|
+
import { getImageMimeType, isImageFile } from '@/lib/utils';
|
|
4
|
+
import { handleGitError } from '@/lib/api-utils';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import pathLib from 'path';
|
|
7
|
+
|
|
8
|
+
function toImageSide(buffer: Buffer | null, mimeType: string) {
|
|
9
|
+
if (!buffer) return null;
|
|
10
|
+
return {
|
|
11
|
+
mimeType,
|
|
12
|
+
base64: buffer.toString('base64'),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function GET(request: Request) {
|
|
17
|
+
const { searchParams } = new URL(request.url);
|
|
18
|
+
const repoPath = searchParams.get('path');
|
|
19
|
+
const filePath = searchParams.get('file');
|
|
20
|
+
const commitHash = searchParams.get('commit');
|
|
21
|
+
|
|
22
|
+
if (!repoPath) {
|
|
23
|
+
return NextResponse.json({ error: 'Repo path is required' }, { status: 400 });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check if path exists
|
|
27
|
+
if (!fs.existsSync(repoPath)) {
|
|
28
|
+
return NextResponse.json({ error: `Path not found: ${repoPath}` }, { status: 404 });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const git = new GitService(repoPath);
|
|
33
|
+
|
|
34
|
+
// If commit hash is provided, get commit diff
|
|
35
|
+
if (commitHash) {
|
|
36
|
+
// If file path is also provided, get diff for that specific file in the commit
|
|
37
|
+
if (filePath) {
|
|
38
|
+
if (isImageFile(filePath)) {
|
|
39
|
+
const mimeType = getImageMimeType(filePath);
|
|
40
|
+
const [beforeBuffer, afterBuffer, diff] = await Promise.all([
|
|
41
|
+
git.getFileContentBuffer(filePath, `${commitHash}^`),
|
|
42
|
+
git.getFileContentBuffer(filePath, commitHash),
|
|
43
|
+
git.getCommitFilePatch(commitHash, filePath),
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
return NextResponse.json({
|
|
47
|
+
left: '',
|
|
48
|
+
right: '',
|
|
49
|
+
diff,
|
|
50
|
+
imageDiff: {
|
|
51
|
+
left: toImageSide(beforeBuffer, mimeType),
|
|
52
|
+
right: toImageSide(afterBuffer, mimeType),
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const { before, after, diff } = await git.getCommitFileDiff(commitHash, filePath);
|
|
58
|
+
return NextResponse.json({ left: before, right: after, diff });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Otherwise, get the list of files changed in the commit
|
|
62
|
+
const { files, diff } = await git.getCommitDiff(commitHash);
|
|
63
|
+
return NextResponse.json({ files, diff });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Original behavior: diff against working directory
|
|
67
|
+
if (!filePath) {
|
|
68
|
+
return NextResponse.json({ error: 'File path is required' }, { status: 400 });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const diff = await git.getDiff(filePath);
|
|
72
|
+
|
|
73
|
+
if (isImageFile(filePath)) {
|
|
74
|
+
const mimeType = getImageMimeType(filePath);
|
|
75
|
+
const fullPath = pathLib.join(repoPath, filePath);
|
|
76
|
+
const [leftBuffer, rightBuffer] = await Promise.all([
|
|
77
|
+
git.getFileContentBuffer(filePath, 'HEAD'),
|
|
78
|
+
fs.existsSync(fullPath) ? fs.promises.readFile(fullPath) : Promise.resolve(null),
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
return NextResponse.json({
|
|
82
|
+
diff,
|
|
83
|
+
left: '',
|
|
84
|
+
right: '',
|
|
85
|
+
imageDiff: {
|
|
86
|
+
left: toImageSide(leftBuffer, mimeType),
|
|
87
|
+
right: toImageSide(rightBuffer, mimeType),
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Get content for Diff Viewer
|
|
93
|
+
const left = await git.getFileContent(filePath, 'HEAD');
|
|
94
|
+
let right = '';
|
|
95
|
+
const fullPath = pathLib.join(repoPath, filePath);
|
|
96
|
+
if (fs.existsSync(fullPath)) {
|
|
97
|
+
right = await fs.promises.readFile(fullPath, 'utf-8');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return NextResponse.json({ diff, left, right });
|
|
101
|
+
} catch (error) {
|
|
102
|
+
return handleGitError(error);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import { GitService } from '@/lib/git';
|
|
4
|
+
import { handleGitError } from '@/lib/api-utils';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
|
|
7
|
+
export async function GET(request: Request) {
|
|
8
|
+
const { searchParams } = new URL(request.url);
|
|
9
|
+
const path = searchParams.get('path');
|
|
10
|
+
const limit = searchParams.get('limit');
|
|
11
|
+
|
|
12
|
+
if (!path) {
|
|
13
|
+
return NextResponse.json({ error: 'Repo path is required' }, { status: 400 });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Check if path exists
|
|
17
|
+
if (!fs.existsSync(path)) {
|
|
18
|
+
return NextResponse.json({ error: `Path not found: ${path}` }, { status: 404 });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const git = new GitService(path);
|
|
23
|
+
const log = await git.getLog(limit ? parseInt(limit) : 50);
|
|
24
|
+
return NextResponse.json(log);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
return handleGitError(error);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { GitService } from '@/lib/git';
|
|
3
|
+
import { handleGitError } from '@/lib/api-utils';
|
|
4
|
+
import fs from 'node:fs/promises';
|
|
5
|
+
|
|
6
|
+
export async function GET(request: Request) {
|
|
7
|
+
const { searchParams } = new URL(request.url);
|
|
8
|
+
const path = searchParams.get('path');
|
|
9
|
+
|
|
10
|
+
if (!path) {
|
|
11
|
+
return NextResponse.json({ error: 'Repo path is required' }, { status: 400 });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Check if path exists
|
|
15
|
+
try {
|
|
16
|
+
await fs.access(path);
|
|
17
|
+
} catch {
|
|
18
|
+
return NextResponse.json({ error: `Path not found: ${path}` }, { status: 404 });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const git = new GitService(path);
|
|
23
|
+
const status = await git.getStatus();
|
|
24
|
+
return NextResponse.json(status);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
return handleGitError(error);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import { getRepositories, addRepository, updateRepository, removeRepository } from '@/lib/store';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
const customScriptSchema = z.object({
|
|
7
|
+
id: z.string().min(1),
|
|
8
|
+
name: z.string().min(1),
|
|
9
|
+
target: z.literal('branch'),
|
|
10
|
+
action: z.literal('run-bash-script'),
|
|
11
|
+
content: z.string(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export async function GET() {
|
|
15
|
+
const repos = getRepositories();
|
|
16
|
+
return NextResponse.json(repos);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const addRepoSchema = z.object({
|
|
20
|
+
path: z.string().min(1),
|
|
21
|
+
name: z.string().optional(),
|
|
22
|
+
displayName: z.string().nullable().optional(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const updateRepoSchema = z.object({
|
|
26
|
+
path: z.string().min(1),
|
|
27
|
+
updates: z.object({
|
|
28
|
+
name: z.string().optional(),
|
|
29
|
+
displayName: z.string().nullable().optional(),
|
|
30
|
+
lastOpenedAt: z.string().optional(),
|
|
31
|
+
credentialId: z.string().optional().nullable(),
|
|
32
|
+
customScripts: z.array(customScriptSchema).optional(),
|
|
33
|
+
expandedFolders: z.array(z.string()).optional(),
|
|
34
|
+
visibilityMap: z.record(z.string(), z.enum(['visible', 'hidden'])).optional(),
|
|
35
|
+
localGroupExpanded: z.boolean().optional(),
|
|
36
|
+
remotesGroupExpanded: z.boolean().optional(),
|
|
37
|
+
}),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export async function POST(request: Request) {
|
|
41
|
+
try {
|
|
42
|
+
const body = await request.json();
|
|
43
|
+
const { path, name, displayName } = addRepoSchema.parse(body);
|
|
44
|
+
const repo = addRepository(path, name, displayName);
|
|
45
|
+
return NextResponse.json(repo);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (error instanceof z.ZodError) {
|
|
48
|
+
return NextResponse.json({ error: error.issues }, { status: 400 });
|
|
49
|
+
}
|
|
50
|
+
return NextResponse.json({ error: (error as Error).message }, { status: 500 });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function PUT(request: Request) {
|
|
55
|
+
try {
|
|
56
|
+
const body = await request.json();
|
|
57
|
+
const { path, updates } = updateRepoSchema.parse(body);
|
|
58
|
+
const repo = updateRepository(path, updates);
|
|
59
|
+
return NextResponse.json(repo);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (error instanceof z.ZodError) {
|
|
62
|
+
return NextResponse.json({ error: error.issues }, { status: 400 });
|
|
63
|
+
}
|
|
64
|
+
return NextResponse.json({ error: (error as Error).message }, { status: 500 });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const deleteRepoSchema = z.object({
|
|
69
|
+
path: z.string().min(1),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export async function DELETE(request: Request) {
|
|
73
|
+
try {
|
|
74
|
+
const body = await request.json();
|
|
75
|
+
const { path } = deleteRepoSchema.parse(body);
|
|
76
|
+
removeRepository(path);
|
|
77
|
+
return NextResponse.json({ success: true });
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (error instanceof z.ZodError) {
|
|
80
|
+
return NextResponse.json({ error: error.issues }, { status: 400 });
|
|
81
|
+
}
|
|
82
|
+
return NextResponse.json({ error: (error as Error).message }, { status: 500 });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import { getSettings, updateSettings, getDefaultRootFolder } from '@/lib/store';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
export async function GET() {
|
|
7
|
+
const settings = getSettings();
|
|
8
|
+
const resolvedDefaultFolder = getDefaultRootFolder();
|
|
9
|
+
return NextResponse.json({
|
|
10
|
+
...settings,
|
|
11
|
+
resolvedDefaultFolder, // The actual folder that will be used (after fallback logic)
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const updateSettingsSchema = z.object({
|
|
16
|
+
defaultRootFolder: z.string().nullable().optional(),
|
|
17
|
+
sidebarCollapsed: z.boolean().optional(),
|
|
18
|
+
historyPanelHeight: z.number().optional(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export async function PUT(request: Request) {
|
|
22
|
+
try {
|
|
23
|
+
const body = await request.json();
|
|
24
|
+
const updates = updateSettingsSchema.parse(body);
|
|
25
|
+
const settings = updateSettings(updates);
|
|
26
|
+
const resolvedDefaultFolder = getDefaultRootFolder();
|
|
27
|
+
return NextResponse.json({
|
|
28
|
+
...settings,
|
|
29
|
+
resolvedDefaultFolder,
|
|
30
|
+
});
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error instanceof z.ZodError) {
|
|
33
|
+
return NextResponse.json({ error: error.issues }, { status: 400 });
|
|
34
|
+
}
|
|
35
|
+
return NextResponse.json({ error: (error as Error).message }, { status: 500 });
|
|
36
|
+
}
|
|
37
|
+
}
|