Skip to content
Open
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
6 changes: 6 additions & 0 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,12 @@ function App() {
onBackToBrowser={handleBackToBrowser}
onOpenGitDiff={openGitDiffPage}
onOpenFile={openFilePage}
onNavigateToDirectory={(path) => {
window.history.pushState({ page: "list" }, "", "/");
setRoute({ page: "list" });
resetFileViewer();
setCurrentDirectory(path);
}}
/>
) : isGitDiffRoute ? (
<GitDiff onBackToBrowser={handleBackToBrowser} />
Expand Down
37 changes: 28 additions & 9 deletions client/src/components/GitStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,21 @@ import type {
StageAllResponse,
} from "@shared/git";
import type { ErrorResponse } from "@shared/http";
import type { FileListItem } from "@shared/files";
import Toolbar from "@/components/Toolbar";

type GitStatusProps = {
onBackToBrowser: () => void;
onOpenGitDiff: () => void;
onOpenFile: (path: string) => void;
onNavigateToDirectory: (path: string) => void;
};

const GitStatus = ({
onBackToBrowser,
onOpenGitDiff,
onOpenFile,
onNavigateToDirectory,
}: GitStatusProps) => {
const [status, setStatus] = useState<GitStatusResponse | null>(null);
const [isLoading, setIsLoading] = useState(true);
Expand Down Expand Up @@ -157,9 +160,21 @@ const GitStatus = ({
}
};

const renderFileList = (files: string[], title: string, color: string) => {
const renderFileList = (
files: FileListItem[],
title: string,
color: string,
) => {
if (files.length === 0) return null;

const handleItemClick = (item: FileListItem) => {
if (item.kind === "directory") {
onNavigateToDirectory(item.path);
} else {
onOpenFile(item.path);
}
};

return (
<div className="rounded border border-slate-800 bg-slate-900/60 p-4">
<h3
Expand All @@ -168,16 +183,16 @@ const GitStatus = ({
{title} ({files.length})
</h3>
<ul className="space-y-1">
{files.map((file, index) => (
{files.map((item, index) => (
<li
key={`${file}-${index}`}
key={`${item.path}-${index}`}
className="font-mono text-sm text-slate-300"
>
<button
onClick={() => onOpenFile(file)}
onClick={() => handleItemClick(item)}
className="w-full cursor-pointer truncate text-left underline hover:text-sky-400 focus:text-sky-400 focus:outline-none"
>
{file}
{item.kind === "directory" ? `${item.path}/` : item.path}
</button>
</li>
))}
Expand Down Expand Up @@ -265,16 +280,20 @@ const GitStatus = ({
Unstaged ({status.unstaged.length})
</h3>
<ul className="space-y-1">
{status.unstaged.map((file, index) => (
{status.unstaged.map((item, index) => (
<li
key={`${file}-${index}`}
key={`${item.path}-${index}`}
className="font-mono text-sm text-slate-300"
>
<button
onClick={() => onOpenFile(file)}
onClick={() =>
item.kind === "directory"
? onNavigateToDirectory(item.path)
: onOpenFile(item.path)
}
className="w-full cursor-pointer truncate text-left underline hover:text-sky-400 focus:text-sky-400 focus:outline-none"
>
{file}
{item.kind === "directory" ? `${item.path}/` : item.path}
</button>
</li>
))}
Expand Down
49 changes: 43 additions & 6 deletions server/services/gitService.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,36 @@
import type { GitStatusResponse, GitDiffResponse } from "../../shared/git";
import type { FileListItem } from "../../shared/files";
import { exec } from "child_process";
import { promisify } from "util";
import { resolveFromRoot } from "../utils/paths";
import { stat } from "fs/promises";
import { join } from "path";

const execAsync = promisify(exec);

/**
* Helper function to convert a file path to a FileListItem by checking if it's a file or directory
*/
const pathToFileListItem = async (
filePath: string,
projectRoot: string,
): Promise<FileListItem> => {
try {
const fullPath = join(projectRoot, filePath);
const stats = await stat(fullPath);
return {
path: filePath,
kind: stats.isDirectory() ? "directory" : "file",
};
} catch {
// If we can't stat the file, assume it's a file
return {
path: filePath,
kind: "file",
};
}
};

/**
* Gets the current git status including branch, ahead/behind, and file statuses
*/
Expand Down Expand Up @@ -39,9 +65,9 @@ export const getGitStatus = async (): Promise<GitStatusResponse> => {
cwd: projectRoot,
});

const staged: string[] = [];
const unstaged: string[] = [];
const untracked: string[] = [];
const stagedPaths: string[] = [];
const unstagedPaths: string[] = [];
const untrackedPaths: string[] = [];

const lines = statusOutput.split("\n").filter((line) => line.length > 0);
for (const line of lines) {
Expand All @@ -53,17 +79,28 @@ export const getGitStatus = async (): Promise<GitStatusResponse> => {
const unstagedStatus = status[1];

if (stagedStatus === "?" && unstagedStatus === "?") {
untracked.push(filePath);
untrackedPaths.push(filePath);
} else {
if (stagedStatus !== " " && stagedStatus !== "?") {
staged.push(filePath);
stagedPaths.push(filePath);
}
if (unstagedStatus !== " " && unstagedStatus !== "?") {
unstaged.push(filePath);
unstagedPaths.push(filePath);
}
}
}

// Convert paths to FileListItems with kind information
const staged = await Promise.all(
stagedPaths.map((path) => pathToFileListItem(path, projectRoot)),
);
const unstaged = await Promise.all(
unstagedPaths.map((path) => pathToFileListItem(path, projectRoot)),
);
const untracked = await Promise.all(
untrackedPaths.map((path) => pathToFileListItem(path, projectRoot)),
);

return {
branch,
ahead,
Expand Down
7 changes: 4 additions & 3 deletions shared/git.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { SuccessResponse, TextRequest } from "./http";
import type { FileListItem } from "./files";

export interface GitStatusResponse {
branch: string;
ahead: number;
behind: number;
staged: string[];
unstaged: string[];
untracked: string[];
staged: FileListItem[];
unstaged: FileListItem[];
untracked: FileListItem[];
}

export interface GitDiffResponse {
Expand Down