Skip to content

Commit 100fca1

Browse files
committed
move to electron webapp, integrate vibecommit
1 parent 8d7f1af commit 100fca1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

94 files changed

+11365
-4482
lines changed

.prettierignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Ignore artifacts:
2+
build
3+
coverage

.prettierrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

app/api/git/branches/route.ts

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,66 @@
1-
import { execSync } from "child_process"
2-
import { type NextRequest, NextResponse } from "next/server"
1+
import { execSync } from "child_process";
2+
import { type NextRequest, NextResponse } from "next/server";
33

44
export async function POST(request: NextRequest) {
55
try {
6-
const { repoPath } = await request.json()
6+
const { repoPath } = await request.json();
77

88
if (!repoPath) {
9-
return NextResponse.json({ error: "Repository path is required" }, { status: 400 })
9+
return NextResponse.json(
10+
{ error: "Repository path is required" },
11+
{ status: 400 },
12+
);
1013
}
1114

12-
const branches = execSync(`cd "${repoPath}" && git branch -a --format="%(refname:short)"`, { encoding: "utf-8" })
15+
let branchOutput = "";
16+
try {
17+
branchOutput = execSync(`git branch --format="%(refname:short)"`, {
18+
encoding: "utf-8",
19+
cwd: repoPath,
20+
});
21+
} catch {
22+
// If no branches exist or other git error, return empty array
23+
return NextResponse.json({ branches: [] });
24+
}
25+
26+
const allBranches = branchOutput
1327
.split("\n")
14-
.filter((b) => b.trim())
15-
.map((b) => b.replace("remotes/origin/", ""))
16-
.filter((b, idx, arr) => arr.indexOf(b) === idx)
28+
.map((b) => b.trim())
29+
.filter(
30+
(b) =>
31+
b &&
32+
!b.startsWith("origin/") &&
33+
!b.startsWith("remotes/") &&
34+
!b.startsWith("backup- "),
35+
)
36+
.filter((b, idx, arr) => arr.indexOf(b) === idx); // deduplicate
37+
38+
if (allBranches.length === 0) {
39+
return NextResponse.json({ branches: [] });
40+
}
41+
42+
// Sort branches to prioritize main-like branches
43+
const priorityBranches = ["main", "master", "develop", "dev"];
44+
const sortedBranches = allBranches.sort((a, b) => {
45+
const aPriority = priorityBranches.indexOf(a.toLowerCase());
46+
const bPriority = priorityBranches.indexOf(b.toLowerCase());
47+
48+
// If both are priority branches, sort by priority order
49+
if (aPriority !== -1 && bPriority !== -1) {
50+
return aPriority - bPriority;
51+
}
52+
// If only one is priority, put it first
53+
if (aPriority !== -1) return -1;
54+
if (bPriority !== -1) return 1;
55+
// Otherwise, alphabetical
56+
return a.localeCompare(b);
57+
});
1758

18-
return NextResponse.json({ branches })
59+
return NextResponse.json({ branches: sortedBranches });
1960
} catch (error: any) {
20-
return NextResponse.json({ error: error.message || "Failed to fetch branches" }, { status: 500 })
61+
return NextResponse.json(
62+
{ error: error.message || "Failed to fetch branches" },
63+
{ status: 500 },
64+
);
2165
}
2266
}

app/api/git/commits/route.ts

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,55 @@
1-
import { execSync } from "child_process"
2-
import { type NextRequest, NextResponse } from "next/server"
1+
import { execSync } from "child_process";
2+
import { type NextRequest, NextResponse } from "next/server";
33

44
export async function POST(request: NextRequest) {
55
try {
6-
const { repoPath, branch } = await request.json()
6+
const { repoPath, branch } = await request.json();
77

88
if (!repoPath || !branch) {
9-
return NextResponse.json({ error: "Repository path and branch are required" }, { status: 400 })
9+
return NextResponse.json(
10+
{ error: "Repository path and branch are required" },
11+
{ status: 400 },
12+
);
1013
}
1114

1215
// Include parent hashes (%P) to build a proper DAG client-side
13-
const logOutput = execSync(
14-
`cd "${repoPath}" && git log ${branch} --pretty=format:"%H|%h|%s|%an|%ai|%P" --max-count=100`,
15-
{ encoding: "utf-8" },
16-
)
16+
let logOutput = "";
17+
try {
18+
logOutput = execSync(
19+
`git log ${branch} --pretty=format:"%H|%h|%s|%an|%ai|%P" --max-count=100`,
20+
{
21+
encoding: "utf-8",
22+
cwd: repoPath,
23+
},
24+
);
25+
} catch (error: any) {
26+
// If branch doesn't exist or no commits, return empty array
27+
if (
28+
error.message.includes("unknown revision") ||
29+
error.message.includes("ambiguous argument")
30+
) {
31+
return NextResponse.json({
32+
commits: [],
33+
error: `Branch '${branch}' not found in repository`,
34+
});
35+
}
36+
throw error; // Re-throw other errors
37+
}
38+
39+
if (!logOutput.trim()) {
40+
return NextResponse.json({ commits: [] });
41+
}
1742

1843
const commits = logOutput
1944
.split("\n")
2045
.filter((line) => line.trim())
2146
.map((line) => {
22-
const [hash, shortHash, message, author, date, parentsStr] = line.split("|")
47+
const [hash, shortHash, message, author, date, parentsStr] =
48+
line.split("|");
2349
const parents = (parentsStr || "")
2450
.split(" ")
2551
.map((p) => p.trim())
26-
.filter((p) => p.length > 0)
52+
.filter((p) => p.length > 0);
2753

2854
return {
2955
id: hash,
@@ -33,15 +59,18 @@ export async function POST(request: NextRequest) {
3359
author,
3460
date,
3561
parents,
36-
}
37-
})
62+
};
63+
});
3864

3965
// Detect uncommitted working directory changes and append a pseudo-commit at the end
4066
try {
41-
const status = execSync(`cd "${repoPath}" && git status --porcelain`, { encoding: "utf-8" })
42-
const hasChanges = status.trim().length > 0
67+
const status = execSync(`git status --porcelain`, {
68+
encoding: "utf-8",
69+
cwd: repoPath,
70+
});
71+
const hasChanges = status.trim().length > 0;
4372
if (hasChanges && commits.length > 0) {
44-
const head = commits[0] // git log lists HEAD first
73+
const head = commits[0]; // git log lists HEAD first
4574
commits.push({
4675
id: "WORKING_DIR",
4776
label: "WORK",
@@ -53,14 +82,17 @@ export async function POST(request: NextRequest) {
5382
// extra metadata for the client to style differently
5483
kind: "working",
5584
isWorkingDir: true,
56-
} as any)
85+
} as any);
5786
}
5887
} catch {
5988
// If status fails (e.g., not a git repo), ignore silently
6089
}
6190

62-
return NextResponse.json({ commits })
91+
return NextResponse.json({ commits });
6392
} catch (error: any) {
64-
return NextResponse.json({ error: error.message || "Failed to fetch commits" }, { status: 500 })
93+
return NextResponse.json(
94+
{ error: error.message || "Failed to fetch commits" },
95+
{ status: 500 },
96+
);
6597
}
6698
}

app/api/git/diff/route.ts

Lines changed: 82 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,101 @@
1-
import { execSync } from "child_process"
2-
import fs from "fs"
3-
import os from "os"
4-
import path from "path"
5-
import { type NextRequest, NextResponse } from "next/server"
1+
import { execSync } from "child_process";
2+
import fs from "fs";
3+
import os from "os";
4+
import path from "path";
5+
import { type NextRequest, NextResponse } from "next/server";
66

77
export async function POST(request: NextRequest) {
88
try {
9-
const { repoPath, commitHash } = await request.json()
9+
const { repoPath, commitHash } = await request.json();
1010

1111
if (!repoPath || !commitHash) {
12-
return NextResponse.json({ error: "Repository path and commit hash are required" }, { status: 400 })
12+
return NextResponse.json(
13+
{ error: "Repository path and commit hash are required" },
14+
{ status: 400 },
15+
);
1316
}
1417

15-
// Helper to run git commands that may exit with non-zero when differences exist (git diff returns 1)
16-
const runGit = (cmd: string): string => {
17-
try {
18-
return execSync(cmd, { encoding: "utf-8", env: { ...process.env, GIT_PAGER: "" } })
19-
} catch (e: any) {
20-
const out: Buffer | string | undefined = e?.stdout
21-
if (out) {
22-
return Buffer.isBuffer(out) ? out.toString("utf-8") : String(out)
18+
// Helper to run git commands in a specific repository directory
19+
const runGitInRepo = (repoPath: string, cmd: string): string => {
20+
try {
21+
return execSync(cmd, {
22+
encoding: "utf-8",
23+
env: { ...process.env, GIT_PAGER: "" },
24+
cwd: repoPath,
25+
});
26+
} catch (e: any) {
27+
const out: Buffer | string | undefined = e?.stdout;
28+
if (out) {
29+
return Buffer.isBuffer(out) ? out.toString("utf-8") : String(out);
30+
}
31+
throw e;
2332
}
24-
throw e
25-
}
26-
}
33+
};
2734

28-
// If the special working directory node is requested, return the working tree diff vs HEAD
29-
let diffText = ""
30-
if (commitHash === "WORKING_DIR") {
31-
// Base diff: tracked changes vs HEAD (staged + unstaged)
32-
const baseCmd = `cd "${repoPath}" && git diff HEAD --no-color --no-ext-diff`
33-
diffText = runGit(baseCmd)
35+
// If the special working directory node is requested, return the working tree diff vs HEAD
36+
let diffText = "";
37+
if (commitHash === "WORKING_DIR") {
38+
// Base diff: tracked changes vs HEAD (staged + unstaged)
39+
diffText = runGitInRepo(
40+
repoPath,
41+
`git diff HEAD --no-color --no-ext-diff`,
42+
);
3443

35-
// Append diffs for untracked files using no-index against an empty temp file
36-
const listCmd = `cd "${repoPath}" && git ls-files --others --exclude-standard -z`
37-
let untrackedRaw = ""
38-
try {
39-
// Capture as Buffer to safely handle NULs
40-
const buf: Buffer = execSync(listCmd, { env: { ...process.env, GIT_PAGER: "" } }) as unknown as Buffer
41-
untrackedRaw = buf.toString("utf-8")
42-
} catch {
43-
untrackedRaw = ""
44-
}
45-
const files = untrackedRaw
46-
.split("\u0000")
47-
.map((s) => s.trim())
48-
.filter((s) => s.length > 0)
49-
50-
if (files.length > 0) {
51-
// Create a temporary empty file
52-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "vcv-"))
53-
const emptyPath = path.join(tmpDir, "empty")
54-
fs.writeFileSync(emptyPath, "")
44+
// Append diffs for untracked files using no-index against an empty temp file
45+
const listCmd = `git ls-files --others --exclude-standard -z`;
46+
let untrackedRaw = "";
5547
try {
56-
for (const f of files) {
57-
const perFileCmd = `cd "${repoPath}" && git diff --no-index --no-color --no-ext-diff -- "${emptyPath}" "${f}"`
58-
const perFile = runGit(perFileCmd)
59-
if (perFile && perFile.trim().length > 0) {
60-
if (diffText && !diffText.endsWith("\n")) diffText += "\n"
61-
diffText += perFile
48+
// Capture as Buffer to safely handle NULs
49+
const buf: Buffer = execSync(listCmd, {
50+
env: { ...process.env, GIT_PAGER: "" },
51+
cwd: repoPath,
52+
}) as unknown as Buffer;
53+
untrackedRaw = buf.toString("utf-8");
54+
} catch {
55+
untrackedRaw = "";
56+
}
57+
const files = untrackedRaw
58+
.split("\u0000")
59+
.map((s) => s.trim())
60+
.filter((s) => s.length > 0);
61+
62+
if (files.length > 0) {
63+
// Create a temporary empty file
64+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "vcv-"));
65+
const emptyPath = path.join(tmpDir, "empty");
66+
fs.writeFileSync(emptyPath, "");
67+
try {
68+
for (const f of files) {
69+
const perFileCmd = `git diff --no-index --no-color --no-ext-diff -- "${emptyPath}" "${f}"`;
70+
const perFile = runGitInRepo(repoPath, perFileCmd);
71+
if (perFile && perFile.trim().length > 0) {
72+
if (diffText && !diffText.endsWith("\n")) diffText += "\n";
73+
diffText += perFile;
74+
}
6275
}
76+
} finally {
77+
// Cleanup temp file and dir
78+
try {
79+
fs.unlinkSync(emptyPath);
80+
} catch {}
81+
try {
82+
fs.rmdirSync(tmpDir);
83+
} catch {}
6384
}
64-
} finally {
65-
// Cleanup temp file and dir
66-
try { fs.unlinkSync(emptyPath) } catch {}
67-
try { fs.rmdirSync(tmpDir) } catch {}
6885
}
86+
} else {
87+
// Use git show to get the patch for a single commit. Disable color and pager for clean parsing.
88+
diffText = runGitInRepo(
89+
repoPath,
90+
`git show ${commitHash} --no-color --no-ext-diff`,
91+
);
6992
}
70-
} else {
71-
// Use git show to get the patch for a single commit. Disable color and pager for clean parsing.
72-
const cmd = `cd "${repoPath}" && git show ${commitHash} --no-color --no-ext-diff`
73-
diffText = runGit(cmd)
74-
}
7593

76-
return NextResponse.json({ diff: diffText })
94+
return NextResponse.json({ diff: diffText });
7795
} catch (error: any) {
78-
return NextResponse.json({ error: error.message || "Failed to get diff" }, { status: 500 })
96+
return NextResponse.json(
97+
{ error: error.message || "Failed to get diff" },
98+
{ status: 500 },
99+
);
79100
}
80101
}

0 commit comments

Comments
 (0)