Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions bridge/src/handlers/gitHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,19 @@ export class GitHandlers {
}
}

async handleLogGraph(params: any, id: number | string) {
const dir = this.requireDir(params, id);
if (!dir) return;
try {
const count = params?.count ?? 100;
const entries = await this.gitService.logGraph(dir, count);
this.rpc.sendResponse(id, { ok: true, data: entries });
} catch (e: any) {
this.logger?.error({ e }, "git.logGraph failed");
this.rpc.sendError(id, { code: "GIT_ERROR", message: String(e.message || e) });
}
}

async handleBranches(params: any, id: number | string) {
const dir = this.requireDir(params, id);
if (!dir) return;
Expand Down
1 change: 1 addition & 0 deletions bridge/src/jsonRpcHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ export function registerDbHandlers(
rpcRegister(rpc, "git.stashPop", (p, id) => gitHandlers.handleStashPop(p, id));
rpcRegister(rpc, "git.diff", (p, id) => gitHandlers.handleDiff(p, id));
rpcRegister(rpc, "git.ensureIgnore", (p, id) => gitHandlers.handleEnsureIgnore(p, id));
rpcRegister(rpc, "git.logGraph", (p, id) => gitHandlers.handleLogGraph(p, id));

// ==========================================
// GIT ADVANCED HANDLERS
Expand Down
77 changes: 66 additions & 11 deletions bridge/src/services/gitService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ export interface GitLogEntry {
/** Full commit hash */
fullHash: string;

/** Parent hashes (space-separated) */
parents: string[];

/** Author name */
author: string;

Expand All @@ -70,15 +73,16 @@ export interface GitLogEntry {

/** First line of commit message */
subject: string;

/** Ref names (branches, tags) */
refs: string;
}

export interface GitBranchInfo {
/** Branch name */
name: string;

/** Is this the current branch? */
current: boolean;

/** Remote tracking branch (null for local-only branches) */
upstream: string | null;
}
Expand Down Expand Up @@ -233,7 +237,7 @@ export class GitService {
for (const line of statusOutput.split("\n")) {
if (!line) continue;
const x = line[0]; // staged status
const y = line[1]; // unstaged status
const y = line[1]; // working tree

if (x === "?" && y === "?") {
untrackedCount++;
Expand Down Expand Up @@ -332,7 +336,7 @@ export class GitService {
async log(dir: string, count = 20): Promise<GitLogEntry[]> {
try {
const SEP = "<<SEP>>";
const format = ["%h", "%H", "%an", "%aI", "%s"].join(SEP);
const format = ["%h", "%H", "%p", "%an", "%aI", "%s", "%D"].join(SEP);
const output = await this.git(
dir,
"log",
Expand All @@ -342,15 +346,58 @@ export class GitService {

if (!output) return [];

return output.split("\n").map((line) => {
const [hash, fullHash, author, date, subject] = line.split(SEP);
return { hash, fullHash, author, date, subject };
return output.split("\n").filter(Boolean).map((line) => {
const [hash, fullHash, parents, author, date, subject, refs] = line.split(SEP);
return {
hash,
fullHash,
parents: parents ? parents.split(" ") : [],
author,
date,
subject,
refs: refs || ""
};
});
} catch {
return []; // No commits yet
}
}

/**
* Get detailed commit log for graph visualization
*/
async logGraph(dir: string, count = 100): Promise<GitLogEntry[]> {
try {
// %h: short hash, %H: full hash, %p: abbreviated parent hashes, %an: author name, %aI: author date ISO, %s: subject, %D: ref names
const SEP = "<<SEP>>";
const format = ["%h", "%H", "%p", "%an", "%aI", "%s", "%D"].join(SEP);
const output = await this.git(
dir,
"log",
`--max-count=${count}`,
`--format=${format}`,
"--all" // Include all branches
);

if (!output) return [];

return output.split("\n").filter(Boolean).map((line) => {
const [hash, fullHash, parents, author, date, subject, refs] = line.split(SEP);
return {
hash,
fullHash,
parents: parents ? parents.split(" ") : [],
author,
date,
subject,
refs: refs || ""
};
});
} catch (err: any) {
return [];
}
}

/**
* List branches
*/
Expand Down Expand Up @@ -444,7 +491,7 @@ export class GitService {
async fileLog(dir: string, filePath: string, count = 20): Promise<GitLogEntry[]> {
try {
const SEP = "<<SEP>>";
const format = ["%h", "%H", "%an", "%aI", "%s"].join(SEP);
const format = ["%h", "%H", "%p", "%an", "%aI", "%s", "%D"].join(SEP);
const output = await this.git(
dir,
"log",
Expand All @@ -455,9 +502,17 @@ export class GitService {
filePath
);
if (!output) return [];
return output.split("\n").map((line) => {
const [hash, fullHash, author, date, subject] = line.split(SEP);
return { hash, fullHash, author, date, subject };
return output.split("\n").filter(Boolean).map((line) => {
const [hash, fullHash, parents, author, date, subject, refs] = line.split(SEP);
return {
hash,
fullHash,
parents: parents ? parents.split(" ") : [],
author,
date,
subject,
refs: refs || ""
};
});
} catch {
return [];
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
"@codemirror/lang-sql": "^6.10.0",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.39.8",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.12",
Expand Down Expand Up @@ -43,6 +46,7 @@
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"dagre": "^0.8.5",
"date-fns": "^4.2.1",
"html-to-image": "^1.11.13",
"lucide-react": "^0.554.0",
"next-themes": "^0.4.6",
Expand Down
64 changes: 64 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

81 changes: 81 additions & 0 deletions src/components/shared/InputDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import * as React from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
DialogDescription,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";

interface InputDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
title: string;
description?: string;
placeholder?: string;
defaultValue?: string;
confirmLabel?: string;
onConfirm: (value: string) => void;
}

export function InputDialog({
open,
onOpenChange,
title,
description,
placeholder,
defaultValue = "",
confirmLabel = "Confirm",
onConfirm,
}: InputDialogProps) {
const [value, setValue] = React.useState(defaultValue);

React.useEffect(() => {
if (open) {
setValue(defaultValue);
}
}, [open, defaultValue]);

const handleSubmit = (e?: React.FormEvent) => {
e?.preventDefault();
if (value.trim()) {
onConfirm(value.trim());
onOpenChange(false);
}
};

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md bg-sidebar/95 backdrop-blur-xl border-sidebar-border">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
{description && <DialogDescription>{description}</DialogDescription>}
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4 py-2">
<Input
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder={placeholder}
autoFocus
className="bg-background/50 border-sidebar-border"
/>
<DialogFooter>
<Button
type="button"
variant="ghost"
onClick={() => onOpenChange(false)}
>
Cancel
</Button>
<Button type="submit" disabled={!value.trim()}>
{confirmLabel}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
Loading
Loading