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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,20 @@ No changes yet.

---

## [0.8.2] - 2026-03-04

### Fixed

- `stageChangelistAll` / `unstageChangelistAll` commands now correctly use the active repo root instead of always reading from `workspaceFolders[0]`
- Stash restore round-trip now works correctly for changelist names containing spaces — names are URL-encoded in the stash message and decoded on apply/pop

### Changed

- Refactored all components to read `repoRoot` at call-time via getter functions instead of capturing it once at startup — groundwork for multi-repo switching
- `StashesTreeProvider` now exposes `setRepoRoot()` to allow switching repos without recreating the provider

---

## [0.8.1] - 2026-03-02

- Version number bumped unintentionally; no functional changes vs 0.7.0.
Expand Down
2 changes: 1 addition & 1 deletion src/adapters/git/gitCliClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export function parseStashLine(line: string): GitStashEntry | null {
message: msg,
raw: trimmed,
isGitWorklists: !!gw,
changelistId: gw?.[1],
changelistId: gw?.[1] ? decodeURIComponent(gw[1]) : undefined,
};
}

Expand Down
8 changes: 4 additions & 4 deletions src/adapters/vscode/autoRefreshController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ export class AutoRefreshController implements DisposableLike {

constructor(
private readonly vs: VscodeFacade,
private readonly repoRoot: string,
private readonly gitDir: string,
private readonly getRepoRoot: () => string,
private readonly getGitDir: () => string,
private readonly onSignal: () => void,
) {}

Expand All @@ -17,7 +17,7 @@ export class AutoRefreshController implements DisposableLike {

const isInRepo = (uri: UriLike) => {
const fsPath = uri.fsPath;
const rel = path.relative(this.repoRoot, fsPath);
const rel = path.relative(this.getRepoRoot(), fsPath);
return !!rel && !rel.startsWith("..") && !path.isAbsolute(rel);
};

Expand Down Expand Up @@ -46,7 +46,7 @@ export class AutoRefreshController implements DisposableLike {
}

private watchGitFile(relativePath: string) {
const pattern = new this.vs.RelativePattern(this.gitDir, relativePath);
const pattern = new this.vs.RelativePattern(this.getGitDir(), relativePath);
const w = this.vs.workspace.createFileSystemWatcher(pattern);

this.disposables.push(
Expand Down
6 changes: 2 additions & 4 deletions src/adapters/vscode/gitShowContentProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,20 @@ export class GitShowContentProvider

constructor(
private readonly git: GitClient,
private readonly repoRoot: string,
private readonly getRepoRoot: () => string,
) {}

async provideTextDocumentContent(uri: vscode.Uri): Promise<string> {
const parts = uri.path.split("/").filter(Boolean);
const ref = decodeURIComponent(parts[0] ?? "HEAD");
const repoRel = decodeURIComponent(parts.slice(1).join("/"));

// Special ref for “no left content” (added files / first commit)
if (ref === "EMPTY") {
return "";
}

// Use optional show to avoid crashing when file doesn not exist at ref
const txt = await this.git.showFileAtRefOptional(
this.repoRoot,
this.getRepoRoot(),
ref,
repoRel,
);
Expand Down
16 changes: 10 additions & 6 deletions src/app/deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,12 @@ export async function createDeps(
const treeProvider = new ChangelistTreeProvider(store);
treeProvider.setRepoRoot(repoRoot);

// Forward ref: deps is assigned below. All closures capture `deps` and read
// deps.repoRoot at call-time so a future switchRepo() mutation is picked up.
let deps!: Deps;

let onDndDrop: () => Promise<void> = async () => {};
const dnd = new ChangelistDragDrop(moveFiles, () => repoRoot, () => onDndDrop());
const dnd = new ChangelistDragDrop(moveFiles, () => deps.repoRoot, () => onDndDrop());

const treeView = vscode.window.createTreeView("gitWorklists.changelists", {
treeDataProvider: treeProvider,
Expand All @@ -88,12 +92,12 @@ export async function createDeps(
const reconcile = new ReconcileWithGitStatus(git, store);

const coordinator = new RefreshCoordinator(async () => {
await loadOrInit.run(repoRoot);
await reconcile.run(repoRoot);
await loadOrInit.run(deps.repoRoot);
await reconcile.run(deps.repoRoot);
treeProvider.refresh();
deco.refreshAll();

const state = await store.load(repoRoot);
const state = await store.load(deps.repoRoot);
const totalFiles =
state?.version === 1
? state.lists.reduce((sum, l) => sum + l.files.length, 0)
Expand All @@ -120,7 +124,7 @@ export async function createDeps(
const pendingStageOnSave = new PendingStageOnSave();

const newFileHandler = new HandleNewFilesCreated({
repoRoot,
getRepoRoot: () => deps.repoRoot,
git,
moveFiles,
coordinator,
Expand All @@ -130,7 +134,7 @@ export async function createDeps(
});

// commitView set in registerCommitView.ts
const deps: Deps = {
deps = {
context,
workspaceFolder,
repoRoot,
Expand Down
4 changes: 2 additions & 2 deletions src/registration/registerAutoRefresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ export function registerAutoRefresh(
) {
const auto = new AutoRefreshController(
{ workspace: vscode.workspace, RelativePattern: vscode.RelativePattern },
deps.repoRoot,
deps.gitDir,
() => deps.repoRoot,
() => deps.gitDir,
doRefresh,
);

Expand Down
20 changes: 2 additions & 18 deletions src/registration/registerCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,14 +527,6 @@ export function registerCommands(deps: Deps) {
return;
}

const repoRoot = await deps.git.tryGetRepoRoot(
vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? "",
);
if (!repoRoot) {
vscode.window.showErrorMessage("No Git repository found.");
return;
}

const ok = await vscode.window.showWarningMessage(
`Stage all files in "${group.list.name}"?`,
{ modal: true },
Expand All @@ -544,7 +536,7 @@ export function registerCommands(deps: Deps) {
return;
}

await stageChangelistAll(deps.git, repoRoot, group.list.files);
await stageChangelistAll(deps.git, deps.repoRoot, group.list.files);
await vscode.commands.executeCommand("gitWorklists.refresh");
},
),
Expand All @@ -556,14 +548,6 @@ export function registerCommands(deps: Deps) {
return;
}

const repoRoot = await deps.git.tryGetRepoRoot(
vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? "",
);
if (!repoRoot) {
vscode.window.showErrorMessage("No Git repository found.");
return;
}

const ok = await vscode.window.showWarningMessage(
`Unstage all files in "${group.list.name}"? (Working tree changes will be kept.)`,
{ modal: true },
Expand All @@ -573,7 +557,7 @@ export function registerCommands(deps: Deps) {
return;
}

await unstageChangelistAll(deps.git, repoRoot, group.list.files);
await unstageChangelistAll(deps.git, deps.repoRoot, group.list.files);
await vscode.commands.executeCommand("gitWorklists.refresh");
},
),
Expand Down
2 changes: 1 addition & 1 deletion src/registration/registerViews.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export function registerViews(context: vscode.ExtensionContext, deps: Deps) {
vscode.window.registerFileDecorationProvider(deps.deco),
);

const showProvider = new GitShowContentProvider(deps.git, deps.repoRoot);
const showProvider = new GitShowContentProvider(deps.git, () => deps.repoRoot);
context.subscriptions.push(
vscode.workspace.registerTextDocumentContentProvider(
GitShowContentProvider.scheme,
Expand Down
21 changes: 19 additions & 2 deletions src/test/unit/adapters/git/gitCliClient.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import * as path from "path";

const mocks = vi.hoisted(() => {
return {
Expand Down Expand Up @@ -109,6 +108,24 @@ describe("parseStashLine", () => {
changelistId: undefined,
});
});

it("decodes URL-encoded changelist name with spaces", () => {
const e = parseStashLine("stash@{0}: On main: GW:My%20Feature WIP");
expect(e).toMatchObject({
ref: "stash@{0}",
isGitWorklists: true,
changelistId: "My Feature",
});
});

it("decodes URL-encoded Unversioned Files name", () => {
const e = parseStashLine("stash@{1}: On main: GW:Unversioned%20Files");
expect(e).toMatchObject({
ref: "stash@{1}",
isGitWorklists: true,
changelistId: "Unversioned Files",
});
});
});

describe("GitCliClient (mocked git)", () => {
Expand Down Expand Up @@ -183,7 +200,7 @@ describe("GitCliClient (mocked git)", () => {
const git = new GitCliClient();
const gitDir = await git.getGitDir("/repo");

expect(gitDir).toBe(path.join("/repo", ".git"));
expect(gitDir).toBe("/repo/.git");
});

it("getGitDir returns absolute path unchanged", async () => {
Expand Down
8 changes: 4 additions & 4 deletions src/test/unit/adapters/vscode/autoRefreshController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ describe("AutoRefreshController", () => {
const { vs, createdPatterns, watchers } = makeVscodeStub();
const onSignal = vi.fn();

const c = new AutoRefreshController(vs, "/repo", "/repo/.git", onSignal);
const c = new AutoRefreshController(vs, () => "/repo", () => "/repo/.git", onSignal);
c.start();

expect(watchers.length).toBe(2);
Expand All @@ -117,7 +117,7 @@ describe("AutoRefreshController", () => {
const { vs, watchers } = makeVscodeStub();
const onSignal = vi.fn();

const c = new AutoRefreshController(vs, "/repo", "/repo/.git", onSignal);
const c = new AutoRefreshController(vs, () => "/repo", () => "/repo/.git", onSignal);
c.start();

// fire watcher events
Expand All @@ -132,7 +132,7 @@ describe("AutoRefreshController", () => {
const { vs, fire } = makeVscodeStub();
const onSignal = vi.fn();

const c = new AutoRefreshController(vs, "/repo", "/repo/.git", onSignal);
const c = new AutoRefreshController(vs, () => "/repo", () => "/repo/.git", onSignal);
c.start();

fire.createFiles({
Expand Down Expand Up @@ -161,7 +161,7 @@ describe("AutoRefreshController", () => {
const { vs, watchers } = makeVscodeStub();
const onSignal = vi.fn();

const c = new AutoRefreshController(vs, "/repo", "/repo/.git", onSignal);
const c = new AutoRefreshController(vs, () => "/repo", () => "/repo/.git", onSignal);
c.start();
c.dispose();

Expand Down
12 changes: 6 additions & 6 deletions src/test/unit/adapters/vscode/gitShowContentProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ describe("GitShowContentProvider", () => {
showFileAtRefOptional: vi.fn().mockResolvedValue("content"),
};

const provider = new GitShowContentProvider(git as any, "/repo");
const provider = new GitShowContentProvider(git as any, () => "/repo");

const uri = { path: "/HEAD/src/a.ts" } as any;

Expand All @@ -66,7 +66,7 @@ describe("GitShowContentProvider", () => {
showFileAtRefOptional: vi.fn().mockResolvedValue("x"),
};

const provider = new GitShowContentProvider(git as any, "/repo");
const provider = new GitShowContentProvider(git as any, () => "/repo");

const uri = { path: "/stash%40%7B2%7D/a%20b.txt" } as any;

Expand All @@ -84,7 +84,7 @@ describe("GitShowContentProvider", () => {
showFileAtRefOptional: vi.fn().mockResolvedValue("x"),
};

const provider = new GitShowContentProvider(git as any, "/repo");
const provider = new GitShowContentProvider(git as any, () => "/repo");

const uri = { path: "" } as any;

Expand All @@ -98,7 +98,7 @@ describe("GitShowContentProvider", () => {
showFileAtRefOptional: vi.fn(),
};

const provider = new GitShowContentProvider(git as any, "/repo");
const provider = new GitShowContentProvider(git as any, () => "/repo");

const uri = { path: "/EMPTY/src/a.ts" } as any;

Expand All @@ -110,7 +110,7 @@ describe("GitShowContentProvider", () => {

it("refresh fires onDidChange event with the same uri", () => {
const git: GitClientMock = { showFileAtRefOptional: vi.fn() };
const provider = new GitShowContentProvider(git as any, "/repo");
const provider = new GitShowContentProvider(git as any, () => "/repo");

const uri = { path: "/HEAD/src/a.ts" } as any;

Expand All @@ -122,7 +122,7 @@ describe("GitShowContentProvider", () => {

it("exposes onDidChange event (smoke test)", () => {
const git: GitClientMock = { showFileAtRefOptional: vi.fn() };
const provider = new GitShowContentProvider(git as any, "/repo");
const provider = new GitShowContentProvider(git as any, () => "/repo");

expect(provider.onDidChange).toBeDefined();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ describe("CreateStashForChangelist", () => {
expect(git.getStatusPorcelainZ).not.toHaveBeenCalled();
expect(git.stashPushPaths).toHaveBeenCalledWith(
"/repo",
"GW:Unversioned Files",
"GW:Unversioned%20Files",
["new-a.ts", "new-b.ts"],
{ includeUntracked: true },
);
Expand Down
9 changes: 5 additions & 4 deletions src/usecases/handleNewFilesCreated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export type MoveFilesPort = {
};

export type HandleNewFilesCreatedDeps = {
repoRoot: string;
getRepoRoot: () => string;
git: GitPort;
moveFiles: MoveFilesPort;
coordinator: RefreshPort;
Expand All @@ -46,7 +46,8 @@ export class HandleNewFilesCreated {
constructor(private readonly deps: HandleNewFilesCreatedDeps) {}

async run(createdFileUris: vscode.Uri[]): Promise<void> {
const { repoRoot, settings, prompt, coordinator } = this.deps;
const repoRoot = this.deps.getRepoRoot();
const { settings, prompt, coordinator } = this.deps;

if (!settings.getPromptOnNewFile()) {
return;
Expand Down Expand Up @@ -124,15 +125,15 @@ export class HandleNewFilesCreated {

private async moveToDefault(paths: string[]): Promise<void> {
await this.deps.moveFiles.run(
this.deps.repoRoot,
this.deps.getRepoRoot(),
paths,
SystemChangelist.Default,
);
}

private async moveToUnversioned(paths: string[]): Promise<void> {
await this.deps.moveFiles.run(
this.deps.repoRoot,
this.deps.getRepoRoot(),
paths,
SystemChangelist.Unversioned,
);
Expand Down
5 changes: 3 additions & 2 deletions src/usecases/stash/createStashForChangelist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@ export class CreateStashForChangelist {
}

const userMsg = (params.message ?? "").trim();
const encoded = encodeURIComponent(changelistName);
const msg = userMsg
? `GW:${changelistName} ${userMsg}`
: `GW:${changelistName}`;
? `GW:${encoded} ${userMsg}`
: `GW:${encoded}`;

if (changelistId === SystemChangelist.Unversioned) {
await this.git.stashPushPaths(repoRootFsPath, msg, files, {
Expand Down
2 changes: 1 addition & 1 deletion src/views/stash/stashNodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function stripWorklistTag(msg: string): { changelistId?: string; msg: string } {
const s = (msg ?? "").trim();

const m = s.match(/\b(?:GW|CL):([^\s]+)\b/);
const changelistId = m?.[1];
const changelistId = m?.[1] ? decodeURIComponent(m[1]) : undefined;

const cleaned = s.replace(/\b(?:GW|CL):[^\s]+\b\s*/g, "").trim();

Expand Down
Loading
Loading