Archived
Show staged and unstaged git diffs
This commit is contained in:
@@ -28,6 +28,7 @@ export interface AppState {
|
||||
gitStatus: GitStatusResponse | undefined;
|
||||
selectedDiffPath: string | undefined;
|
||||
selectedDiff: GitDiffResponse | undefined;
|
||||
selectedStagedDiff: GitDiffResponse | undefined;
|
||||
gitStale: boolean;
|
||||
error: string;
|
||||
}
|
||||
@@ -60,6 +61,7 @@ export function initialAppState(): AppState {
|
||||
gitStatus: undefined,
|
||||
selectedDiffPath: undefined,
|
||||
selectedDiff: undefined,
|
||||
selectedStagedDiff: undefined,
|
||||
gitStale: false,
|
||||
error: "",
|
||||
};
|
||||
|
||||
@@ -173,7 +173,7 @@ export class PiWebApp extends LitElement {
|
||||
}
|
||||
|
||||
private renderWorkspacePanel() {
|
||||
return html`<workspace-panel .workspace=${this.state.selectedWorkspace} .tool=${this.state.workspaceTool} .fileTree=${this.state.fileTree} .expandedDirs=${this.state.expandedDirs} .selectedFilePath=${this.state.selectedFilePath} .selectedFileContent=${this.state.selectedFileContent} .fileTreeStale=${this.state.fileTreeStale} .gitStatus=${this.state.gitStatus} .selectedDiffPath=${this.state.selectedDiffPath} .selectedDiff=${this.state.selectedDiff} .gitStale=${this.state.gitStale} .onSelectTool=${(tool: "files" | "git") => { this.selectWorkspaceTool(tool); }} .onRefreshFiles=${() => this.files.refreshFiles()} .onExpandDir=${(path: string) => this.files.expandDir(path)} .onSelectFile=${(path: string) => this.files.selectFile(path)} .onRefreshGit=${() => this.git.refreshGit()} .onSelectDiff=${(path: string) => this.git.selectDiff(path)}></workspace-panel>`;
|
||||
return html`<workspace-panel .workspace=${this.state.selectedWorkspace} .tool=${this.state.workspaceTool} .fileTree=${this.state.fileTree} .expandedDirs=${this.state.expandedDirs} .selectedFilePath=${this.state.selectedFilePath} .selectedFileContent=${this.state.selectedFileContent} .fileTreeStale=${this.state.fileTreeStale} .gitStatus=${this.state.gitStatus} .selectedDiffPath=${this.state.selectedDiffPath} .selectedDiff=${this.state.selectedDiff} .selectedStagedDiff=${this.state.selectedStagedDiff} .gitStale=${this.state.gitStale} .onSelectTool=${(tool: "files" | "git") => { this.selectWorkspaceTool(tool); }} .onRefreshFiles=${() => this.files.refreshFiles()} .onExpandDir=${(path: string) => this.files.expandDir(path)} .onSelectFile=${(path: string) => this.files.selectFile(path)} .onRefreshGit=${() => this.git.refreshGit()} .onSelectDiff=${(path: string) => this.git.selectDiff(path)}></workspace-panel>`;
|
||||
}
|
||||
|
||||
private getActions(): AppAction[] {
|
||||
|
||||
@@ -16,6 +16,7 @@ export class WorkspacePanel extends LitElement {
|
||||
@property({ attribute: false }) gitStatus: GitStatusResponse | undefined;
|
||||
@property({ attribute: false }) selectedDiffPath: string | undefined;
|
||||
@property({ attribute: false }) selectedDiff: GitDiffResponse | undefined;
|
||||
@property({ attribute: false }) selectedStagedDiff: GitDiffResponse | undefined;
|
||||
@property({ type: Boolean }) gitStale = false;
|
||||
@property({ attribute: false }) onSelectTool: (tool: "files" | "git") => void = () => undefined;
|
||||
@property({ attribute: false }) onRefreshFiles: () => void = () => undefined;
|
||||
@@ -113,11 +114,24 @@ export class WorkspacePanel extends LitElement {
|
||||
|
||||
private renderDiffViewer() {
|
||||
if (this.selectedDiffPath === undefined || this.selectedDiffPath === "") return html`<p class="muted">Select a changed file.</p>`;
|
||||
const diff = this.selectedDiff;
|
||||
if (diff === undefined) return html`<p class="muted">Loading diff…</p>`;
|
||||
const unstaged = this.selectedDiff;
|
||||
const staged = this.selectedStagedDiff;
|
||||
if (unstaged === undefined || staged === undefined) return html`<p class="muted">Loading diff…</p>`;
|
||||
const diffs = [staged, unstaged].filter((diff) => diff.diff !== "");
|
||||
if (diffs.length === 0) return html`<p class="muted">No staged or unstaged diff.</p>`;
|
||||
return html`
|
||||
<div class=${diffs.length === 1 ? "diffs single" : "diffs"}>
|
||||
${diffs.map((diff) => this.renderDiffSection(diff))}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderDiffSection(diff: GitDiffResponse) {
|
||||
return html`
|
||||
<section class="diff-section">
|
||||
<div class="viewer-header"><strong>${diff.path ?? "diff"}</strong><small>${diff.staged ? "staged" : "unstaged"}${diff.truncated ? " · truncated" : ""}</small></div>
|
||||
<code-viewer .content=${diff.diff !== "" ? diff.diff : "No unstaged diff."} .language=${"diff"}></code-viewer>
|
||||
<code-viewer .content=${diff.diff} .language=${"diff"}></code-viewer>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -73,6 +73,10 @@ export const workspacePanelStyles = css`
|
||||
.row span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.summary { margin: 4px 6px 8px; color: #8b949e; }
|
||||
.viewer { min-height: 0; overflow: hidden; display: flex; flex-direction: column; }
|
||||
.diffs { flex: 1 1 auto; min-height: 0; overflow: auto; display: grid; grid-template-rows: minmax(120px, 1fr) minmax(120px, 1fr); }
|
||||
.diffs.single { grid-template-rows: minmax(0, 1fr); }
|
||||
.diff-section { min-height: 0; display: flex; flex-direction: column; border-bottom: 1px solid #30363d; }
|
||||
.diff-section:last-child { border-bottom: 0; }
|
||||
.viewer-header { position: sticky; top: 0; display: flex; justify-content: space-between; gap: 8px; padding: 8px; border-bottom: 1px solid #21262d; background: #0d1117; }
|
||||
.viewer-header strong { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
code-viewer { flex: 1 1 auto; min-height: 0; }
|
||||
|
||||
@@ -22,7 +22,7 @@ export class GitController {
|
||||
if (selectedDiffPath !== undefined) {
|
||||
if (status.files.some((file) => file.path === selectedDiffPath)) await this.refreshDiff(selectedDiffPath);
|
||||
else {
|
||||
this.setState({ selectedDiffPath: undefined, selectedDiff: undefined });
|
||||
this.setState({ selectedDiffPath: undefined, selectedDiff: undefined, selectedStagedDiff: undefined });
|
||||
this.updateUrl();
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ export class GitController {
|
||||
}
|
||||
|
||||
async selectDiff(path: string): Promise<void> {
|
||||
this.setState({ selectedDiffPath: path, selectedDiff: undefined, workspaceTool: "git", mainView: this.getState().mainView === "chat" ? "chat" : "git" });
|
||||
this.setState({ selectedDiffPath: path, selectedDiff: undefined, selectedStagedDiff: undefined, workspaceTool: "git", mainView: this.getState().mainView === "chat" ? "chat" : "git" });
|
||||
this.updateUrl();
|
||||
await this.refreshDiff(path);
|
||||
}
|
||||
@@ -42,7 +42,11 @@ export class GitController {
|
||||
const workspace = this.getState().selectedWorkspace;
|
||||
if (project === undefined || workspace === undefined) return;
|
||||
try {
|
||||
this.setState({ selectedDiff: await api.gitDiff(project.id, workspace.id, { path }), error: "" });
|
||||
const [selectedDiff, selectedStagedDiff] = await Promise.all([
|
||||
api.gitDiff(project.id, workspace.id, { path }),
|
||||
api.gitDiff(project.id, workspace.id, { path, staged: true }),
|
||||
]);
|
||||
this.setState({ selectedDiff, selectedStagedDiff, error: "" });
|
||||
} catch (error) {
|
||||
this.setState({ error: String(error) });
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export class WorkspaceController {
|
||||
|
||||
async selectProject(project: Project, target?: RouteTarget) {
|
||||
this.sessions.clearActiveSession();
|
||||
this.setState({ selectedProject: project, selectedWorkspace: undefined, sessions: [], workspaces: [], fileTree: [], expandedDirs: {}, selectedFilePath: undefined, selectedFileContent: undefined, fileTreeStale: false, gitStatus: undefined, selectedDiffPath: undefined, selectedDiff: undefined, gitStale: false, error: "" });
|
||||
this.setState({ selectedProject: project, selectedWorkspace: undefined, sessions: [], workspaces: [], fileTree: [], expandedDirs: {}, selectedFilePath: undefined, selectedFileContent: undefined, fileTreeStale: false, gitStatus: undefined, selectedDiffPath: undefined, selectedDiff: undefined, selectedStagedDiff: undefined, gitStale: false, error: "" });
|
||||
try {
|
||||
const workspaces = await api.workspaces(project.id);
|
||||
this.setState({ workspaces });
|
||||
@@ -26,7 +26,7 @@ export class WorkspaceController {
|
||||
|
||||
async selectWorkspace(workspace: Workspace, target?: { sessionId?: string | undefined; updateUrl?: boolean | undefined }) {
|
||||
this.sessions.clearActiveSession();
|
||||
this.setState({ selectedWorkspace: workspace, sessions: [], fileTree: [], expandedDirs: {}, selectedFilePath: undefined, selectedFileContent: undefined, fileTreeStale: false, gitStatus: undefined, selectedDiffPath: undefined, selectedDiff: undefined, gitStale: false, error: "" });
|
||||
this.setState({ selectedWorkspace: workspace, sessions: [], fileTree: [], expandedDirs: {}, selectedFilePath: undefined, selectedFileContent: undefined, fileTreeStale: false, gitStatus: undefined, selectedDiffPath: undefined, selectedDiff: undefined, selectedStagedDiff: undefined, gitStale: false, error: "" });
|
||||
try {
|
||||
const sessions = await api.sessions(workspace.path);
|
||||
this.setState({ sessions });
|
||||
|
||||
@@ -13,18 +13,28 @@ export async function gitStatus(cwd: string): Promise<GitStatusResponse> {
|
||||
|
||||
export async function gitDiff(cwd: string, options: { path?: string; staged?: boolean }): Promise<GitDiffResponse> {
|
||||
const staged = options.staged === true;
|
||||
let path: string | undefined;
|
||||
if (options.path !== undefined && options.path !== "") path = normalizeRelativePath(options.path);
|
||||
|
||||
const args = ["diff", "--no-ext-diff", "--color=never"];
|
||||
if (staged) args.push("--cached");
|
||||
let path: string | undefined;
|
||||
if (options.path !== undefined && options.path !== "") {
|
||||
path = normalizeRelativePath(options.path);
|
||||
args.push("--", path);
|
||||
}
|
||||
if (path !== undefined) args.push("--", path);
|
||||
|
||||
const result = await runGit(cwd, args);
|
||||
if (result.code !== 0) throw new Error(result.stderr.trim() || "git diff failed");
|
||||
if (!staged && path !== undefined && result.stdout === "" && await isUntracked(cwd, path)) {
|
||||
const untracked = await runGit(cwd, ["diff", "--no-ext-diff", "--color=never", "--no-index", "/dev/null", "--", path]);
|
||||
if (untracked.code !== 0 && untracked.code !== 1) throw new Error(untracked.stderr.trim() || "git diff failed");
|
||||
return { path, staged, hash: hash(untracked.stdout), diff: untracked.stdout, truncated: untracked.truncated };
|
||||
}
|
||||
return { ...(path === undefined ? {} : { path }), staged, hash: hash(result.stdout), diff: result.stdout, truncated: result.truncated };
|
||||
}
|
||||
|
||||
async function isUntracked(cwd: string, path: string): Promise<boolean> {
|
||||
const result = await runGit(cwd, ["ls-files", "--others", "--exclude-standard", "-z", "--", path]);
|
||||
return result.code === 0 && result.stdout.split("\0").includes(path);
|
||||
}
|
||||
|
||||
function parseStatus(raw: string): GitStatusResponse {
|
||||
const records = raw.split("\0").filter((record) => record !== "");
|
||||
const files: GitStatusFile[] = [];
|
||||
|
||||
Reference in New Issue
Block a user