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
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,31 @@ and adheres to Semantic Versioning.

---

## [0.6.0] - 2026-02-28

### Added

- **Accordion-style Stashes view**
- Stashes are now expandable nodes instead of a flat list.
- Expanding a stash reveals the files contained within it.
- **Per-file diff preview in Stashes view**
- Clicking a stashed file opens a diff against the stash base commit.
- Newly added files in a stash open as single-file preview.
- Status-aware stash file nodes (A/M/D/R/C).

### Changed

- Stash nodes are now collapsible instead of non-expandable items.
- Refactored Stashes tree provider to support hierarchical rendering.
- Extended Git adapter with `git diff --name-status` integration for stash file enumeration.

### Improved

- Safer handling of very large stashed files to prevent preview crashes.
- Increased unit test coverage for stash tree rendering and Git stash parsing logic.

---

## [0.5.0] - 2026-02-27

### Added
Expand Down
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,11 +187,24 @@ Integrated Git stash support directly inside Git Worklists.
### Stash List View

- Dedicated **Stashes** view
- Stashes are displayed as expandable (accordion-style) nodes
- Expanding a stash reveals the list of files contained within it
- Status-aware file nodes (A / M / D / R / C)
- Clean, readable labels (no raw `stash@{0}` noise)
- Displays originating changelist (e.g. `[CL:changes]`)
- Shows branch context
- Hover tooltip includes full Git reference

### Stash File Preview

- Click a stashed file to open a **diff preview**
- Compares stash base commit (`stash^1`) with stash contents
- Newly added files in a stash open as single-file preview
- Large files are handled safely to prevent editor crashes
- Preview behavior is consistent with the main diff integration

![Stash diff preview demo](media/demo-stash-diff.gif)

### Stash Actions

Per-stash context actions:
Expand Down Expand Up @@ -221,6 +234,7 @@ Supported operations:
- `git stash apply`
- `git stash pop`
- `git stash drop`
- `git diff --name-status`
- `git ls-files --others --exclude-standard`

All operations are executed per repository using repo-relative paths.
Expand Down Expand Up @@ -259,7 +273,9 @@ All operations are executed per repository using repo-relative paths.
1. Right-click a changelist -> **Stash changes…**
2. Enter an optional stash message
3. Open the **Stashes** view
4. Apply, Pop, or Delete stashes as needed
4. Expand a stash to inspect its files
5. Click a file to preview its diff
6. Apply, Pop, or Delete stashes as needed

---

Expand Down
Binary file added media/demo-stash-diff.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
57 changes: 54 additions & 3 deletions src/adapters/git/gitCliClient.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import * as cp from "child_process";
import * as path from "path";
import {
CommitFileChange,
GitClient,
GitStatusEntry,
GitStashEntry,
GitStatusEntry,
OutgoingCommit,
CommitFileChange,
StashFileEntry,
} from "./gitClient";

function execGit(args: string[], cwd: string): Promise<string> {
return new Promise((resolve, reject) => {
cp.execFile(
"git",
args,
{ cwd, encoding: "utf8" },
{
cwd,
encoding: "utf8",
maxBuffer: 50 * 1024 * 1024, // 50MB
},
(err, stdout, stderr) => {
if (err) {
reject(
Expand Down Expand Up @@ -395,4 +400,50 @@ export class GitCliClient implements GitClient {
return undefined;
}
}

async stashListFiles(
repoRootFsPath: string,
stashRef: string,
): Promise<StashFileEntry[]> {
// Compare stash base commit to stash commit:
// stashRef^1 is "base" (commit the stash was made from)
// stashRef is the stash commit object (worktree changes)
const out = await execGit(
["diff", "--name-status", `${stashRef}^1`, stashRef],
repoRootFsPath,
);

const lines = out
.split("\n")
.map((l) => l.trim())
.filter(Boolean);

const files: StashFileEntry[] = [];

for (const line of lines) {
const parts = line.split("\t");
if (parts.length < 2) {
continue;
}

const statusRaw = parts[0] ?? "?";
const code = (statusRaw[0] ?? "?") as StashFileEntry["status"];

if (code === "R" || code === "C") {
const oldPath = parts[1] ?? "";
const newPath = parts[2] ?? "";
if (newPath) {
files.push({ status: code, oldPath, path: newPath });
}
continue;
}

const p = parts[1] ?? "";
if (p) {
files.push({ status: code, path: p });
}
}

return files;
}
}
18 changes: 18 additions & 0 deletions src/adapters/git/gitClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ export type CommitFileChange = {
oldPath?: string;
};

export type StashFileEntry = {
/** repo-relative path (new path if renamed) */
path: string;
/** best effort status */
status: "A" | "M" | "D" | "R" | "C" | "T" | "U" | "?";
/** old path for rename/copy */
oldPath?: string;
};

export interface GitClient {
/** returns repo root absolute path */
getRepoRoot(workspaceFsPath: string): Promise<string>;
Expand Down Expand Up @@ -117,4 +126,13 @@ export interface GitClient {
ref: string,
repoRelativePath: string,
): Promise<string | undefined>;

/**
* Files affected by a stash.
* Uses name-status so we can show A/M/D and handle renames.
*/
stashListFiles(
repoRootFsPath: string,
stashRef: string,
): Promise<StashFileEntry[]>;
}
47 changes: 47 additions & 0 deletions src/registration/registerStash.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import * as vscode from "vscode";
import { GitShowContentProvider } from "../adapters/vscode/gitShowContentProvider"; // adjust path
import { Deps } from "../app/types";
import { CreateStashForChangelist } from "../usecases/stash/createStashForChangelist";

function showUri(ref: string, repoRelPath: string): vscode.Uri {
const p = repoRelPath
.split("/")
.map((s) => encodeURIComponent(s))
.join("/");

return vscode.Uri.parse(
`${GitShowContentProvider.scheme}:/${encodeURIComponent(ref)}/${p}`,
);
}

export function registerStash(deps: Deps) {
const { context } = deps;

Expand Down Expand Up @@ -141,5 +153,40 @@ export function registerStash(deps: Deps) {
}
},
),

vscode.commands.registerCommand(
"gitWorklists.stash.openFileDiff",
async (node: any) => {
const stashRef =
typeof node?.stash?.ref === "string" ? node.stash.ref : "";
const repoRelPath = typeof node?.path === "string" ? node.path : "";
const status =
typeof node?.status === "string" ? node.status : undefined;

if (!stashRef || !repoRelPath) {
return;
}

const right = showUri(stashRef, repoRelPath);

if (status === "A") {
const doc = await vscode.workspace.openTextDocument(right);
await vscode.window.showTextDocument(doc, { preview: true });
return;
}

// Default left = base commit of stash
let leftRef = `${stashRef}^1`;

if (status === "A") {
leftRef = "EMPTY";
}

const left = showUri(leftRef, repoRelPath);
const title = `${repoRelPath} (${stashRef})`;

await vscode.commands.executeCommand("vscode.diff", left, right, title);
},
),
);
}
Loading
Loading