feat(FN-3623): add stash diff viewing UI and backend contract
Merges FN-3623 stash diff inspection system (backend contract, GitManagerModal viewing UI with new API route and tests) and FN-3088 dependency graph hover/selection highlighting. The stash diff work dominates the commit count and touches the core GitManagerModal, while the graph plugin gains new hig Fusion-Task-Id: FN-3623
This commit is contained in:
@@ -2398,6 +2398,11 @@ export function dropStash(index: number, projectId?: string): Promise<{ message:
|
||||
});
|
||||
}
|
||||
|
||||
/** Fetch stash diff (stat + patch) */
|
||||
export function fetchStashDiff(index: number, projectId?: string): Promise<{ stat: string; patch: string }> {
|
||||
return api<{ stat: string; patch: string }>(withProjectId(`/git/stashes/${index}/diff`, projectId));
|
||||
}
|
||||
|
||||
/** Fetch unstaged diff (working directory changes) */
|
||||
export function fetchUnstagedDiff(projectId?: string): Promise<{ stat: string; patch: string }> {
|
||||
return api<{ stat: string; patch: string }>(withProjectId("/git/diff", projectId));
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
createStash,
|
||||
applyStash,
|
||||
dropStash,
|
||||
fetchStashDiff,
|
||||
fetchFileChanges,
|
||||
stageFiles,
|
||||
unstageFiles,
|
||||
@@ -253,6 +254,11 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj
|
||||
const [stashes, setStashes] = useState<GitStash[]>([]);
|
||||
const [stashMessage, setStashMessage] = useState("");
|
||||
const [stashLoading, setStashLoading] = useState<string | null>(null);
|
||||
const [expandedStashIndex, setExpandedStashIndex] = useState<number | null>(null);
|
||||
const [stashDiff, setStashDiff] = useState<{ stat: string; patch: string } | null>(null);
|
||||
const [loadingStashDiff, setLoadingStashDiff] = useState(false);
|
||||
const [stashDiffError, setStashDiffError] = useState<string | null>(null);
|
||||
const stashDiffRequestIdRef = useRef(0);
|
||||
|
||||
// ── Remotes state
|
||||
const [remoteLoading, setRemoteLoading] = useState<string | null>(null);
|
||||
@@ -300,6 +306,10 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj
|
||||
case "stashes": {
|
||||
const stashesData = await fetchGitStashList(projectId);
|
||||
setStashes(stashesData);
|
||||
setExpandedStashIndex(null);
|
||||
setStashDiff(null);
|
||||
setStashDiffError(null);
|
||||
stashDiffRequestIdRef.current += 1;
|
||||
break;
|
||||
}
|
||||
case "remotes": {
|
||||
@@ -658,9 +668,18 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj
|
||||
|
||||
// ── Stash Handlers ──────────────────────────────────────────────
|
||||
|
||||
const resetStashDiffState = useCallback(() => {
|
||||
stashDiffRequestIdRef.current += 1;
|
||||
setExpandedStashIndex(null);
|
||||
setStashDiff(null);
|
||||
setStashDiffError(null);
|
||||
setLoadingStashDiff(false);
|
||||
}, []);
|
||||
|
||||
const handleCreateStash = useCallback(async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setStashLoading("create");
|
||||
resetStashDiffState();
|
||||
try {
|
||||
await createStash(stashMessage.trim() || undefined, projectId);
|
||||
addToast("Changes stashed", "success");
|
||||
@@ -672,10 +691,11 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj
|
||||
} finally {
|
||||
setStashLoading(null);
|
||||
}
|
||||
}, [stashMessage, addToast, projectId]);
|
||||
}, [stashMessage, addToast, projectId, resetStashDiffState]);
|
||||
|
||||
const handleApplyStash = useCallback(async (index: number, drop: boolean = false) => {
|
||||
setStashLoading(`apply-${index}`);
|
||||
resetStashDiffState();
|
||||
try {
|
||||
await applyStash(index, drop, projectId);
|
||||
addToast(drop ? "Stash popped" : "Stash applied", "success");
|
||||
@@ -686,7 +706,7 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj
|
||||
} finally {
|
||||
setStashLoading(null);
|
||||
}
|
||||
}, [addToast, projectId]);
|
||||
}, [addToast, projectId, resetStashDiffState]);
|
||||
|
||||
const handleDropStash = useCallback(async (index: number) => {
|
||||
const shouldDrop = await confirmContext.confirm({
|
||||
@@ -696,6 +716,7 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj
|
||||
});
|
||||
if (!shouldDrop) return;
|
||||
setStashLoading(`drop-${index}`);
|
||||
resetStashDiffState();
|
||||
try {
|
||||
await dropStash(index, projectId);
|
||||
addToast("Stash dropped", "success");
|
||||
@@ -706,7 +727,38 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj
|
||||
} finally {
|
||||
setStashLoading(null);
|
||||
}
|
||||
}, [addToast, projectId, confirmContext]);
|
||||
}, [addToast, projectId, confirmContext, resetStashDiffState]);
|
||||
|
||||
const handleToggleStashDiff = useCallback(async (index: number) => {
|
||||
if (expandedStashIndex === index) {
|
||||
resetStashDiffState();
|
||||
return;
|
||||
}
|
||||
|
||||
const requestId = stashDiffRequestIdRef.current + 1;
|
||||
stashDiffRequestIdRef.current = requestId;
|
||||
setExpandedStashIndex(index);
|
||||
setStashDiff(null);
|
||||
setStashDiffError(null);
|
||||
setLoadingStashDiff(true);
|
||||
try {
|
||||
const diff = await fetchStashDiff(index, projectId);
|
||||
if (stashDiffRequestIdRef.current !== requestId) {
|
||||
return;
|
||||
}
|
||||
setStashDiff(diff);
|
||||
} catch (err) {
|
||||
if (stashDiffRequestIdRef.current !== requestId) {
|
||||
return;
|
||||
}
|
||||
setStashDiff(null);
|
||||
setStashDiffError(getErrorMessage(err) || "Failed to load stash diff");
|
||||
} finally {
|
||||
if (stashDiffRequestIdRef.current === requestId) {
|
||||
setLoadingStashDiff(false);
|
||||
}
|
||||
}
|
||||
}, [expandedStashIndex, projectId, resetStashDiffState]);
|
||||
|
||||
// ── Remote Handlers ─────────────────────────────────────────────
|
||||
|
||||
@@ -918,7 +970,12 @@ export function GitManagerModal({ isOpen, onClose, tasks: _tasks, addToast, proj
|
||||
onCreateStash={handleCreateStash}
|
||||
onApplyStash={handleApplyStash}
|
||||
onDropStash={handleDropStash}
|
||||
onToggleStashDiff={handleToggleStashDiff}
|
||||
stashLoading={stashLoading}
|
||||
expandedStashIndex={expandedStashIndex}
|
||||
stashDiff={stashDiff}
|
||||
loadingStashDiff={loadingStashDiff}
|
||||
stashDiffError={stashDiffError}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1698,7 +1755,12 @@ function StashesPanel({
|
||||
onCreateStash,
|
||||
onApplyStash,
|
||||
onDropStash,
|
||||
onToggleStashDiff,
|
||||
stashLoading,
|
||||
expandedStashIndex,
|
||||
stashDiff,
|
||||
loadingStashDiff,
|
||||
stashDiffError,
|
||||
}: {
|
||||
stashes: GitStash[];
|
||||
stashMessage: string;
|
||||
@@ -1706,7 +1768,12 @@ function StashesPanel({
|
||||
onCreateStash: (e: React.FormEvent) => void;
|
||||
onApplyStash: (index: number, drop?: boolean) => void;
|
||||
onDropStash: (index: number) => void;
|
||||
onToggleStashDiff: (index: number) => void;
|
||||
stashLoading: string | null;
|
||||
expandedStashIndex: number | null;
|
||||
stashDiff: { stat: string; patch: string } | null;
|
||||
loadingStashDiff: boolean;
|
||||
stashDiffError: string | null;
|
||||
}) {
|
||||
return (
|
||||
<div className="gm-panel" data-testid="stashes-panel">
|
||||
@@ -1744,53 +1811,80 @@ function StashesPanel({
|
||||
) : (
|
||||
stashes.map((stash) => (
|
||||
<div key={stash.index} className="gm-stash-item">
|
||||
<div className="gm-stash-info">
|
||||
<span className="gm-stash-ref">stash@{`{${stash.index}}`}</span>
|
||||
<span className="gm-stash-message">{stash.message}</span>
|
||||
<div className="gm-stash-meta">
|
||||
{stash.branch && (
|
||||
<span className="gm-stash-branch">
|
||||
<GitBranchIcon size={12} />
|
||||
{stash.branch}
|
||||
</span>
|
||||
)}
|
||||
<span>{relativeDate(stash.date)}</span>
|
||||
<div className="gm-stash-header">
|
||||
<div className="gm-stash-info">
|
||||
<span className="gm-stash-ref">stash@{`{${stash.index}}`}</span>
|
||||
<span className="gm-stash-message">{stash.message}</span>
|
||||
<div className="gm-stash-meta">
|
||||
{stash.branch && (
|
||||
<span className="gm-stash-branch">
|
||||
<GitBranchIcon size={12} />
|
||||
{stash.branch}
|
||||
</span>
|
||||
)}
|
||||
<span>{relativeDate(stash.date)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="gm-stash-actions">
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() => onToggleStashDiff(stash.index)}
|
||||
disabled={stashLoading !== null}
|
||||
>
|
||||
{expandedStashIndex === stash.index ? "Hide" : "View"}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
onClick={() => onApplyStash(stash.index, false)}
|
||||
disabled={stashLoading !== null}
|
||||
title="Apply stash (keep)"
|
||||
>
|
||||
{stashLoading === `apply-${stash.index}` ? (
|
||||
<Loader2 size={14} className="spin" />
|
||||
) : (
|
||||
"Apply"
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() => onApplyStash(stash.index, true)}
|
||||
disabled={stashLoading !== null}
|
||||
title="Pop stash (apply and drop)"
|
||||
>
|
||||
Pop
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-danger"
|
||||
onClick={() => onDropStash(stash.index)}
|
||||
disabled={stashLoading !== null}
|
||||
title="Drop stash"
|
||||
>
|
||||
{stashLoading === `drop-${stash.index}` ? (
|
||||
<Loader2 size={14} className="spin" />
|
||||
) : (
|
||||
<Trash2 size={14} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="gm-stash-actions">
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
onClick={() => onApplyStash(stash.index, false)}
|
||||
disabled={stashLoading !== null}
|
||||
title="Apply stash (keep)"
|
||||
>
|
||||
{stashLoading === `apply-${stash.index}` ? (
|
||||
<Loader2 size={14} className="spin" />
|
||||
) : (
|
||||
"Apply"
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
onClick={() => onApplyStash(stash.index, true)}
|
||||
disabled={stashLoading !== null}
|
||||
title="Pop stash (apply and drop)"
|
||||
>
|
||||
Pop
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-danger"
|
||||
onClick={() => onDropStash(stash.index)}
|
||||
disabled={stashLoading !== null}
|
||||
title="Drop stash"
|
||||
>
|
||||
{stashLoading === `drop-${stash.index}` ? (
|
||||
<Loader2 size={14} className="spin" />
|
||||
) : (
|
||||
<Trash2 size={14} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{expandedStashIndex === stash.index && (
|
||||
<div className="gm-stash-diff">
|
||||
{loadingStashDiff ? (
|
||||
<div className="gm-diff-loading">
|
||||
<Loader2 size={14} className="spin" />
|
||||
Loading stash diff…
|
||||
</div>
|
||||
) : stashDiffError ? (
|
||||
<div className="gm-diff-error">{stashDiffError}</div>
|
||||
) : stashDiff ? (
|
||||
<div className="gm-diff-viewer">
|
||||
{stashDiff.stat && <pre className="gm-diff-stat">{stashDiff.stat}</pre>}
|
||||
<pre className="gm-diff-patch">{stashDiff.patch}</pre>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -2876,15 +2876,21 @@
|
||||
|
||||
.gm-stash-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-md);
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.gm-stash-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.gm-stash-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -2924,10 +2930,16 @@
|
||||
|
||||
.gm-stash-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-xs);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.gm-stash-diff {
|
||||
border-top: 1px solid var(--border);
|
||||
padding-top: var(--space-sm);
|
||||
}
|
||||
|
||||
/* ── Remote Panel ── */
|
||||
|
||||
.gm-remote-status {
|
||||
@@ -3559,8 +3571,13 @@
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.gm-stash-item {
|
||||
.gm-stash-header {
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.gm-stash-actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gm-remote-actions {
|
||||
|
||||
@@ -43,6 +43,7 @@ vi.mock("../../api", async () => {
|
||||
createStash: vi.fn(),
|
||||
applyStash: vi.fn(),
|
||||
dropStash: vi.fn(),
|
||||
fetchStashDiff: vi.fn(),
|
||||
fetchFileChanges: vi.fn(),
|
||||
fetchGitFileDiff: vi.fn(),
|
||||
stageFiles: vi.fn(),
|
||||
@@ -82,6 +83,7 @@ import {
|
||||
createStash,
|
||||
applyStash,
|
||||
dropStash,
|
||||
fetchStashDiff,
|
||||
fetchFileChanges,
|
||||
fetchGitFileDiff,
|
||||
stageFiles,
|
||||
@@ -203,6 +205,10 @@ describe("GitManagerModal", () => {
|
||||
(createStash as any).mockResolvedValue({ message: "Stash created" });
|
||||
(applyStash as any).mockResolvedValue({ message: "Stash applied" });
|
||||
(dropStash as any).mockResolvedValue({ message: "Stash dropped" });
|
||||
(fetchStashDiff as any).mockResolvedValue({
|
||||
stat: " README.md | 2 ++",
|
||||
patch: "diff --git a/README.md b/README.md\n+stash diff",
|
||||
});
|
||||
(discardChanges as any).mockResolvedValue({ discarded: ["src/app.ts"] });
|
||||
(createBranch as any).mockResolvedValue(undefined);
|
||||
(checkoutBranch as any).mockResolvedValue(undefined);
|
||||
@@ -1160,6 +1166,77 @@ describe("GitManagerModal", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("views stash contents", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<GitManagerModal isOpen={true} onClose={vi.fn()} tasks={mockTasks} addToast={mockAddToast} />
|
||||
);
|
||||
fireEvent.click(screen.getByRole("tab", { name: /stashes/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "View" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "View" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expectLatestCallStartsWith(fetchStashDiff as any, 0);
|
||||
expect(screen.getByText("Hide")).toBeInTheDocument();
|
||||
expect(screen.getByText("README.md | 2 ++")).toBeInTheDocument();
|
||||
expect(screen.getByText(/\+stash diff/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows stash diff loading and error states", async () => {
|
||||
const user = userEvent.setup();
|
||||
let resolveDiff: ((value: { stat: string; patch: string }) => void) | null = null;
|
||||
(fetchStashDiff as any).mockImplementationOnce(
|
||||
() =>
|
||||
new Promise<{ stat: string; patch: string }>((resolve) => {
|
||||
resolveDiff = resolve;
|
||||
})
|
||||
);
|
||||
|
||||
render(
|
||||
<GitManagerModal isOpen={true} onClose={vi.fn()} tasks={mockTasks} addToast={mockAddToast} />
|
||||
);
|
||||
fireEvent.click(screen.getByRole("tab", { name: /stashes/i }));
|
||||
|
||||
await user.click(await screen.findByRole("button", { name: "View" }));
|
||||
expect(screen.getByText("Loading stash diff…")).toBeInTheDocument();
|
||||
|
||||
resolveDiff?.({ stat: " README.md | 1 +", patch: "diff --git a/README.md b/README.md\n+ok" });
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("README.md | 1 +")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
(fetchStashDiff as any).mockRejectedValueOnce(new Error("stash diff failed"));
|
||||
await user.click(screen.getByRole("button", { name: "Hide" }));
|
||||
await user.click(screen.getByRole("button", { name: "View" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("stash diff failed")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps stash actions available when viewing stash contents", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<GitManagerModal isOpen={true} onClose={vi.fn()} tasks={mockTasks} addToast={mockAddToast} />
|
||||
);
|
||||
fireEvent.click(screen.getByRole("tab", { name: /stashes/i }));
|
||||
|
||||
await user.click(await screen.findByRole("button", { name: "View" }));
|
||||
await user.click(screen.getByText("Apply"));
|
||||
await user.click(screen.getByText("Pop"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect((applyStash as any).mock.calls).toContainEqual([0, false, undefined]);
|
||||
expect((applyStash as any).mock.calls).toContainEqual([0, true, undefined]);
|
||||
expect(screen.getByTitle("Drop stash")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("creates a stash", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
|
||||
@@ -440,6 +440,53 @@ describe("Git Management endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /git/stashes/:index/diff", () => {
|
||||
const resetGitRepo = () => {
|
||||
const { headSha } = getSharedGitTestRepo();
|
||||
execFileSync("git", ["-C", gitRepoDir, "reset", "--hard", headSha], { stdio: "pipe" });
|
||||
execFileSync("git", ["-C", gitRepoDir, "clean", "-fd"], { stdio: "pipe" });
|
||||
execFileSync("git", ["-C", gitRepoDir, "stash", "clear"], { stdio: "pipe" });
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
resetGitRepo();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetGitRepo();
|
||||
});
|
||||
|
||||
it("returns 400 for invalid stash index", async () => {
|
||||
const res = await GET(buildApp(), "/api/git/stashes/not-a-number/diff");
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toContain("Invalid stash index");
|
||||
});
|
||||
|
||||
it("returns 404 for missing stash entry", async () => {
|
||||
const res = await GET(buildApp(), "/api/git/stashes/0/diff");
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
expect(res.body.error).toContain("Stash not found");
|
||||
});
|
||||
|
||||
it("returns stash diff for an existing stash", async () => {
|
||||
const readmePath = join(gitRepoDir, "README.md");
|
||||
const original = readFileSync(readmePath, "utf-8");
|
||||
const marker = `\nstash-diff-${Date.now()}\n`;
|
||||
writeFileSync(readmePath, `${original}${marker}`);
|
||||
execFileSync("git", ["-C", gitRepoDir, "stash", "push", "-m", "test stash diff"], { stdio: "pipe" });
|
||||
|
||||
const res = await GET(buildApp(), "/api/git/stashes/0/diff");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toHaveProperty("stat");
|
||||
expect(res.body).toHaveProperty("patch");
|
||||
expect(res.body.patch).toContain("diff --git a/README.md b/README.md");
|
||||
expect(res.body.patch).toContain(marker.trim());
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /git/changes", () => {
|
||||
const resetGitRepo = () => {
|
||||
const { headSha } = getSharedGitTestRepo();
|
||||
|
||||
@@ -705,6 +705,23 @@ export async function dropGitStash(index: number, cwd?: string): Promise<string>
|
||||
return output || "Stash dropped";
|
||||
}
|
||||
|
||||
export async function getGitStashDiff(index: number, cwd?: string): Promise<{ stat: string; patch: string } | null> {
|
||||
if (index < 0 || !Number.isInteger(index)) {
|
||||
throw new Error("Invalid stash index");
|
||||
}
|
||||
|
||||
const stashRef = `stash@{${index}}`;
|
||||
try {
|
||||
await runGitCommand(["rev-parse", "--verify", stashRef], cwd, 5000);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stat = (await runGitCommand(["stash", "show", "--stat", stashRef], cwd, 10000)).trim();
|
||||
const patch = await runGitCommand(["stash", "show", "-p", stashRef], cwd, 10000);
|
||||
return { stat, patch };
|
||||
}
|
||||
|
||||
export async function getGitFileChanges(cwd?: string): Promise<GitFileChange[]> {
|
||||
try {
|
||||
const output = await runGitCommand(["status", "--porcelain=v1"], cwd, 5000);
|
||||
@@ -1755,6 +1772,37 @@ export function registerGitGitHubRoutes(ctx: ApiRoutesContext): void {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/git/stashes/:index/diff
|
||||
* Returns stash diff (stat + patch) for a stash entry.
|
||||
*/
|
||||
router.get("/git/stashes/:index/diff", async (req, res) => {
|
||||
try {
|
||||
const { store: scopedStore } = await getProjectContext(req);
|
||||
const rootDir = scopedStore.getRootDir();
|
||||
if (!(await isGitRepo(rootDir))) {
|
||||
throw badRequest("Not a git repository");
|
||||
}
|
||||
|
||||
const index = parseInt(req.params.index, 10);
|
||||
if (isNaN(index) || index < 0) {
|
||||
throw badRequest("Invalid stash index");
|
||||
}
|
||||
|
||||
const diff = await getGitStashDiff(index, rootDir);
|
||||
if (!diff) {
|
||||
throw notFound("Stash not found");
|
||||
}
|
||||
|
||||
res.json(diff);
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof ApiError) {
|
||||
throw err;
|
||||
}
|
||||
rethrowAsApiError(err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/git/stashes/:index
|
||||
* Drop a stash entry.
|
||||
|
||||
Reference in New Issue
Block a user